Merge pull request #32211 from home-assistant/rc

0.106.0
This commit is contained in:
Franck Nijhof 2020-02-26 14:30:57 +01:00 committed by GitHub
commit 2d68b37dd5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1197 changed files with 32103 additions and 8449 deletions

View File

@ -166,7 +166,6 @@ omit =
homeassistant/components/dsmr_reader/*
homeassistant/components/dte_energy_bridge/sensor.py
homeassistant/components/dublin_bus_transport/sensor.py
homeassistant/components/duke_energy/sensor.py
homeassistant/components/dunehd/media_player.py
homeassistant/components/dwd_weather_warnings/sensor.py
homeassistant/components/dweet/*
@ -248,7 +247,6 @@ omit =
homeassistant/components/fritzbox/*
homeassistant/components/fritzbox_callmonitor/sensor.py
homeassistant/components/fritzbox_netmonitor/sensor.py
homeassistant/components/fritzdect/switch.py
homeassistant/components/fronius/sensor.py
homeassistant/components/frontier_silicon/media_player.py
homeassistant/components/futurenow/light.py
@ -387,7 +385,6 @@ omit =
homeassistant/components/linode/*
homeassistant/components/linux_battery/sensor.py
homeassistant/components/lirc/*
homeassistant/components/liveboxplaytv/media_player.py
homeassistant/components/llamalab_automate/notify.py
homeassistant/components/lockitron/lock.py
homeassistant/components/logi_circle/__init__.py
@ -412,9 +409,15 @@ omit =
homeassistant/components/mcp23017/*
homeassistant/components/media_extractor/*
homeassistant/components/mediaroom/media_player.py
homeassistant/components/melcloud/__init__.py
homeassistant/components/melcloud/climate.py
homeassistant/components/melcloud/sensor.py
homeassistant/components/message_bird/notify.py
homeassistant/components/met/weather.py
homeassistant/components/meteo_france/*
homeassistant/components/meteo_france/__init__.py
homeassistant/components/meteo_france/const.py
homeassistant/components/meteo_france/sensor.py
homeassistant/components/meteo_france/weather.py
homeassistant/components/meteoalarm/*
homeassistant/components/metoffice/sensor.py
homeassistant/components/metoffice/weather.py
@ -424,6 +427,10 @@ omit =
homeassistant/components/mikrotik/device_tracker.py
homeassistant/components/mill/climate.py
homeassistant/components/mill/const.py
homeassistant/components/minecraft_server/__init__.py
homeassistant/components/minecraft_server/binary_sensor.py
homeassistant/components/minecraft_server/const.py
homeassistant/components/minecraft_server/sensor.py
homeassistant/components/minio/*
homeassistant/components/mitemp_bt/sensor.py
homeassistant/components/mjpeg/camera.py
@ -603,6 +610,7 @@ omit =
homeassistant/components/russound_rnet/media_player.py
homeassistant/components/sabnzbd/*
homeassistant/components/saj/sensor.py
homeassistant/components/salt/device_tracker.py
homeassistant/components/satel_integra/*
homeassistant/components/scrape/sensor.py
homeassistant/components/scsgate/*
@ -622,8 +630,6 @@ omit =
homeassistant/components/shodan/sensor.py
homeassistant/components/sht31/sensor.py
homeassistant/components/sigfox/sensor.py
homeassistant/components/signal_messenger/__init__.py
homeassistant/components/signal_messenger/notify.py
homeassistant/components/simplepush/notify.py
homeassistant/components/simplisafe/__init__.py
homeassistant/components/simplisafe/alarm_control_panel.py
@ -750,7 +756,6 @@ omit =
homeassistant/components/twentemilieu/sensor.py
homeassistant/components/twilio_call/notify.py
homeassistant/components/twilio_sms/notify.py
homeassistant/components/twitch/sensor.py
homeassistant/components/twitter/notify.py
homeassistant/components/ubee/device_tracker.py
homeassistant/components/ubus/device_tracker.py
@ -781,10 +786,10 @@ omit =
homeassistant/components/vesync/switch.py
homeassistant/components/viaggiatreno/sensor.py
homeassistant/components/vicare/*
homeassistant/components/vilfo/__init__.py
homeassistant/components/vilfo/sensor.py
homeassistant/components/vilfo/const.py
homeassistant/components/vivotek/camera.py
homeassistant/components/vizio/__init__.py
homeassistant/components/vizio/const.py
homeassistant/components/vizio/media_player.py
homeassistant/components/vlc/media_player.py
homeassistant/components/vlc_telnet/media_player.py
homeassistant/components/volkszaehler/sensor.py

12
.github/stale.yml vendored
View File

@ -52,4 +52,14 @@ markComment: >
limitPerRun: 30
# Limit to only `issues` or `pulls`
only: issues
# only: issues
# Handle pull requests a little bit faster and with an adjusted comment.
pulls:
daysUntilStale: 30
markComment: >
There hasn't been any activity on this pull request recently. This pull
request has been automatically marked as stale because of that and will
be closed if no further activity occurs within 7 days.
Thank you for your contributions.

View File

@ -1,59 +0,0 @@
# 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://github.com/PyCQA/flake8
rev: 3.7.9
hooks:
- id: flake8
additional_dependencies:
- flake8-docstrings==1.5.0
- pydocstyle==5.0.2
files: ^(homeassistant|script|tests)/.+\.py$
- repo: https://github.com/PyCQA/bandit
rev: 1.6.2
hooks:
- id: bandit
args:
- --quiet
- --format=custom
- --configfile=tests/bandit.yaml
files: ^(homeassistant|script|tests)/.+\.py$
- repo: https://github.com/pre-commit/mirrors-isort
rev: v4.3.21
hooks:
- id: isort
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.4.0
hooks:
- id: check-json
# 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,10 +1,3 @@
# 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.10b0
@ -14,6 +7,15 @@ repos:
- --safe
- --quiet
files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$
- repo: https://github.com/codespell-project/codespell
rev: v1.16.0
hooks:
- id: codespell
args:
- --ignore-words-list=hass,alot,datas,dof,dur,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing
- --skip="./.*,*.json"
- --quiet-level=2
exclude_types: [json]
- repo: https://gitlab.com/pycqa/flake8
rev: 3.7.9
hooks:
@ -39,3 +41,16 @@ repos:
rev: v2.4.0
hooks:
- id: check-json
- repo: local
hooks:
# Run mypy through our wrapper script in order to get the possible
# pyenv and/or virtualenv activated; it may not have been e.g. if
# committing from a GUI tool that was not launched from an activated
# shell.
- id: mypy
name: mypy
entry: script/run-in-env.sh mypy
language: script
types: [python]
require_serial: true
files: ^homeassistant/.+\.py$

View File

@ -35,6 +35,7 @@ homeassistant/components/arest/* @fabaff
homeassistant/components/asuswrt/* @kennedyshead
homeassistant/components/aten_pe/* @mtdcr
homeassistant/components/atome/* @baqs
homeassistant/components/august/* @bdraco
homeassistant/components/aurora_abb_powerone/* @davet2001
homeassistant/components/auth/* @home-assistant/core
homeassistant/components/automatic/* @armills
@ -83,6 +84,7 @@ homeassistant/components/discogs/* @thibmaek
homeassistant/components/doorbird/* @oblogic7
homeassistant/components/dsmr_reader/* @depl0y
homeassistant/components/dweet/* @fabaff
homeassistant/components/dynalite/* @ziv1234
homeassistant/components/dyson/* @etheralm
homeassistant/components/ecobee/* @marthoc
homeassistant/components/ecovacs/* @OverloadUT
@ -117,6 +119,7 @@ homeassistant/components/freebox/* @snoof85
homeassistant/components/fronius/* @nielstron
homeassistant/components/frontend/* @home-assistant/frontend
homeassistant/components/garmin_connect/* @cyberjunky
homeassistant/components/gdacs/* @exxamalte
homeassistant/components/gearbest/* @HerrHofrat
homeassistant/components/geniushub/* @zxdavb
homeassistant/components/geo_rss_events/* @exxamalte
@ -184,14 +187,13 @@ homeassistant/components/kef/* @basnijholt
homeassistant/components/keyboard_remote/* @bendavid
homeassistant/components/knx/* @Julius2342
homeassistant/components/kodi/* @armills
homeassistant/components/konnected/* @heythisisnate
homeassistant/components/konnected/* @heythisisnate @kit-klein
homeassistant/components/lametric/* @robbiet480
homeassistant/components/launch_library/* @ludeeus
homeassistant/components/lcn/* @alengwenus
homeassistant/components/life360/* @pnbruckner
homeassistant/components/linky/* @Quentame
homeassistant/components/linux_battery/* @fabaff
homeassistant/components/liveboxplaytv/* @pschmitt
homeassistant/components/local_ip/* @issacg
homeassistant/components/logger/* @home-assistant/core
homeassistant/components/logi_circle/* @evanjd
@ -204,14 +206,16 @@ homeassistant/components/mastodon/* @fabaff
homeassistant/components/matrix/* @tinloaf
homeassistant/components/mcp23017/* @jardiamj
homeassistant/components/mediaroom/* @dgomes
homeassistant/components/melcloud/* @vilppuvuorinen
homeassistant/components/melissa/* @kennedyshead
homeassistant/components/met/* @danielhiversen
homeassistant/components/meteo_france/* @victorcerutti @oncleben31
homeassistant/components/meteo_france/* @victorcerutti @oncleben31 @Quentame
homeassistant/components/meteoalarm/* @rolfberkenbosch
homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel
homeassistant/components/mikrotik/* @engrbm87
homeassistant/components/mill/* @danielhiversen
homeassistant/components/min_max/* @fabaff
homeassistant/components/minecraft_server/* @elmurato
homeassistant/components/minio/* @tkislan
homeassistant/components/mobile_app/* @robbiet480
homeassistant/components/modbus/* @adamchengtkc
@ -275,7 +279,7 @@ homeassistant/components/quantum_gateway/* @cisasteelersfan
homeassistant/components/qwikswitch/* @kellerza
homeassistant/components/rainbird/* @konikvranik
homeassistant/components/raincloud/* @vanstinator
homeassistant/components/rainforest_eagle/* @gtdiehl
homeassistant/components/rainforest_eagle/* @gtdiehl @jcalbert
homeassistant/components/rainmachine/* @bachya
homeassistant/components/random/* @fabaff
homeassistant/components/repetier/* @MTrab
@ -285,6 +289,7 @@ homeassistant/components/rmvtransport/* @cgtobi
homeassistant/components/roomba/* @pschmitt
homeassistant/components/safe_mode/* @home-assistant/core
homeassistant/components/saj/* @fredericvl
homeassistant/components/salt/* @bjornorri
homeassistant/components/samsungtv/* @escoand
homeassistant/components/scene/* @home-assistant/core
homeassistant/components/scrape/* @fabaff
@ -354,6 +359,7 @@ homeassistant/components/time_date/* @fabaff
homeassistant/components/tmb/* @alemuro
homeassistant/components/todoist/* @boralyl
homeassistant/components/toon/* @frenck
homeassistant/components/totalconnect/* @austinmroczek
homeassistant/components/tplink/* @rytilahti
homeassistant/components/traccar/* @ludeeus
homeassistant/components/tradfri/* @ggravlingen
@ -379,6 +385,7 @@ homeassistant/components/versasense/* @flamm3blemuff1n
homeassistant/components/version/* @fabaff
homeassistant/components/vesync/* @markperdue @webdjoe
homeassistant/components/vicare/* @oischinger
homeassistant/components/vilfo/* @ManneW
homeassistant/components/vivotek/* @HarlemSquirrel
homeassistant/components/vizio/* @raman325
homeassistant/components/vlc_telnet/* @rodripf

View File

@ -43,7 +43,11 @@ stages:
. venv/bin/activate
pip install -r requirements_test.txt -c homeassistant/package_constraints.txt
pre-commit install-hooks --config .pre-commit-config-all.yaml
pre-commit install-hooks
- script: |
. venv/bin/activate
pre-commit run codespell --all-files
displayName: 'Run codespell'
- script: |
. venv/bin/activate
pre-commit run flake8 --all-files
@ -94,7 +98,7 @@ stages:
. venv/bin/activate
pip install -r requirements_test.txt -c homeassistant/package_constraints.txt
pre-commit install-hooks --config .pre-commit-config-all.yaml
pre-commit install-hooks
- script: |
. venv/bin/activate
pre-commit run black --all-files --show-diff-on-failure
@ -190,8 +194,8 @@ stages:
. venv/bin/activate
pip install -e . -r requirements_test.txt -c homeassistant/package_constraints.txt
pre-commit install-hooks --config .pre-commit-config-all.yaml
pre-commit install-hooks
- script: |
. venv/bin/activate
pre-commit run --config .pre-commit-config-all.yaml mypy --all-files
pre-commit run mypy --all-files
displayName: 'Run mypy'

View File

@ -163,7 +163,7 @@ stages:
git commit -am "Bump Home Assistant $version"
git push
displayName: 'Update version files'
displayName: "Update version files"
- job: 'ReleaseDocker'
pool:
vmImage: 'ubuntu-latest'

View File

@ -13,4 +13,7 @@ coverage:
url: "secret:TgWDUM4Jw0w7wMJxuxNF/yhSOHglIo1fGwInJnRLEVPy2P2aLimkoK1mtKCowH5TFw+baUXVXT3eAqefbdvIuM8BjRR4aRji95C6CYyD0QHy4N8i7nn1SQkWDPpS8IthYTg07rUDF7s5guurkKv2RrgoCdnnqjAMSzHoExMOF7xUmblMdhBTWJgBpWEhASJy85w/xxjlsE1xoTkzeJu9Q67pTXtRcn+5kb5/vIzPSYg="
comment:
require_changes: yes
branches: master
layout: reach
branches:
- master
- !dev

View File

@ -301,7 +301,7 @@ class AuthManager:
async def async_deactivate_user(self, user: models.User) -> None:
"""Deactivate a user."""
if user.is_owner:
raise ValueError("Unable to deactive the owner")
raise ValueError("Unable to deactivate the owner")
await self._store.async_deactivate_user(user)
async def async_remove_credentials(self, credentials: models.Credentials) -> None:

View File

@ -1,4 +1,4 @@
"""Plugable auth modules for Home Assistant."""
"""Pluggable auth modules for Home Assistant."""
import importlib
import logging
import types

View File

@ -317,7 +317,7 @@ class NotifySetupFlow(SetupFlow):
async def async_step_setup(
self, user_input: Optional[Dict[str, str]] = None
) -> Dict[str, Any]:
"""Verify user can recevie one-time password."""
"""Verify user can receive one-time password."""
errors: Dict[str, str] = {}
hass = self._auth_module.hass

View File

@ -31,22 +31,28 @@ class User:
"""A user."""
name = attr.ib(type=Optional[str])
perm_lookup = attr.ib(type=perm_mdl.PermissionLookup, cmp=False)
perm_lookup = attr.ib(type=perm_mdl.PermissionLookup, eq=False, order=False)
id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex)
is_owner = attr.ib(type=bool, default=False)
is_active = attr.ib(type=bool, default=False)
system_generated = attr.ib(type=bool, default=False)
groups = attr.ib(type=List[Group], factory=list, cmp=False)
groups = attr.ib(type=List[Group], factory=list, eq=False, order=False)
# List of credentials of a user.
credentials = attr.ib(type=List["Credentials"], factory=list, cmp=False)
credentials = attr.ib(type=List["Credentials"], factory=list, eq=False, order=False)
# Tokens associated with a user.
refresh_tokens = attr.ib(type=Dict[str, "RefreshToken"], factory=dict, cmp=False)
refresh_tokens = attr.ib(
type=Dict[str, "RefreshToken"], factory=dict, eq=False, order=False
)
_permissions = attr.ib(
type=Optional[perm_mdl.PolicyPermissions], init=False, cmp=False, default=None
type=Optional[perm_mdl.PolicyPermissions],
init=False,
eq=False,
order=False,
default=None,
)
@property

View File

@ -1,23 +1,26 @@
"""Provide methods to bootstrap a Home Assistant instance."""
import asyncio
import contextlib
import logging
import logging.handlers
import os
import sys
from time import time
from time import monotonic
from typing import Any, Dict, Optional, Set
from async_timeout import timeout
import voluptuous as vol
from homeassistant import config as conf_util, config_entries, core, loader
from homeassistant.components import http
from homeassistant.const import (
EVENT_HOMEASSISTANT_CLOSE,
EVENT_HOMEASSISTANT_STOP,
REQUIRED_NEXT_PYTHON_DATE,
REQUIRED_NEXT_PYTHON_VER,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.setup import async_setup_component
from homeassistant.setup import DATA_SETUP, async_setup_component
from homeassistant.util.logging import AsyncHandler
from homeassistant.util.package import async_get_user_site, is_virtual_env
from homeassistant.util.yaml import clear_secret_cache
@ -71,6 +74,7 @@ async def async_setup_hass(
_LOGGER.info("Config directory: %s", config_dir)
config_dict = None
basic_setup_success = False
if not safe_mode:
await hass.async_add_executor_job(conf_util.process_ha_config_upgrade, hass)
@ -79,19 +83,45 @@ async def async_setup_hass(
config_dict = await conf_util.async_hass_config_yaml(hass)
except HomeAssistantError as err:
_LOGGER.error(
"Failed to parse configuration.yaml: %s. Falling back to safe mode",
err,
"Failed to parse configuration.yaml: %s. Activating safe mode", err,
)
else:
if not is_virtual_env():
await async_mount_local_lib_path(config_dir)
await async_from_config_dict(config_dict, hass)
basic_setup_success = (
await async_from_config_dict(config_dict, hass) is not None
)
finally:
clear_secret_cache()
if safe_mode or config_dict is None:
if config_dict is None:
safe_mode = True
elif not basic_setup_success:
_LOGGER.warning("Unable to set up core integrations. Activating safe mode")
safe_mode = True
elif (
"frontend" in hass.data.get(DATA_SETUP, {})
and "frontend" not in hass.config.components
):
_LOGGER.warning("Detected that frontend did not load. Activating safe mode")
# Ask integrations to shut down. It's messy but we can't
# do a clean stop without knowing what is broken
hass.async_track_tasks()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP, {})
with contextlib.suppress(asyncio.TimeoutError):
async with timeout(10):
await hass.async_block_till_done()
safe_mode = True
hass = core.HomeAssistant()
hass.config.config_dir = config_dir
if safe_mode:
_LOGGER.info("Starting in safe mode")
hass.config.safe_mode = True
http_conf = (await http.async_get_last_config(hass)) or {}
@ -110,7 +140,26 @@ async def async_from_config_dict(
Dynamically loads required components and its dependencies.
This method is a coroutine.
"""
start = time()
start = monotonic()
hass.config_entries = config_entries.ConfigEntries(hass, config)
await hass.config_entries.async_initialize()
# Set up core.
_LOGGER.debug("Setting up %s", CORE_INTEGRATIONS)
if not all(
await asyncio.gather(
*(
async_setup_component(hass, domain, config)
for domain in CORE_INTEGRATIONS
)
)
):
_LOGGER.error("Home Assistant core failed to initialize. ")
return None
_LOGGER.debug("Home Assistant core initialized")
core_config = config.get(core.DOMAIN, {})
@ -126,12 +175,9 @@ async def async_from_config_dict(
)
return None
hass.config_entries = config_entries.ConfigEntries(hass, config)
await hass.config_entries.async_initialize()
await _async_set_up_integrations(hass, config)
stop = time()
stop = monotonic()
_LOGGER.info("Home Assistant initialized in %.2fs", stop - start)
if REQUIRED_NEXT_PYTHON_DATE and sys.version_info[:3] < REQUIRED_NEXT_PYTHON_VER:
@ -193,7 +239,7 @@ def async_enable_logging(
pass
# If the above initialization failed for any reason, setup the default
# formatting. If the above succeeds, this wil result in a no-op.
# formatting. If the above succeeds, this will result in a no-op.
logging.basicConfig(format=fmt, datefmt=datefmt, level=logging.INFO)
# Suppress overly verbose logs from libraries that aren't helpful
@ -264,7 +310,7 @@ def _get_domains(hass: core.HomeAssistant, config: Dict[str, Any]) -> Set[str]:
domains = set(key.split(" ")[0] for key in config.keys() if key != core.DOMAIN)
# Add config entry domains
if "safe_mode" not in config:
if not hass.config.safe_mode:
domains.update(hass.config_entries.async_domains())
# Make sure the Hass.io component is loaded
@ -296,25 +342,6 @@ async def _async_set_up_integrations(
return_exceptions=True,
)
# Set up core.
_LOGGER.debug("Setting up %s", CORE_INTEGRATIONS)
if not all(
await asyncio.gather(
*(
async_setup_component(hass, domain, config)
for domain in CORE_INTEGRATIONS
)
)
):
_LOGGER.error(
"Home Assistant core failed to initialize. "
"Further initialization aborted"
)
return
_LOGGER.debug("Home Assistant core initialized")
# Finish resolving domains
for dep_domains in await resolved_domains_task:
# Result is either a set or an exception. We ignore exceptions

View File

@ -0,0 +1,22 @@
{
"config": {
"abort": {
"single_instance_allowed": "Solo se permite una \u00fanica configuraci\u00f3n de Abode."
},
"error": {
"connection_error": "No se puede conectar a Abode.",
"identifier_exists": "Cuenta ya registrada.",
"invalid_credentials": "Credenciales inv\u00e1lidas."
},
"step": {
"user": {
"data": {
"password": "Contrase\u00f1a",
"username": "Direcci\u00f3n de correo electr\u00f3nico"
},
"title": "Complete su informaci\u00f3n de inicio de sesi\u00f3n de Abode"
}
},
"title": "Abode"
}
}

View File

@ -0,0 +1,22 @@
{
"config": {
"abort": {
"single_instance_allowed": "Csak egyetlen Abode konfigur\u00e1ci\u00f3 enged\u00e9lyezett."
},
"error": {
"connection_error": "Nem lehet csatlakozni az Abode-hez.",
"identifier_exists": "Fi\u00f3k m\u00e1r regisztr\u00e1lva van",
"invalid_credentials": "\u00c9rv\u00e9nytelen hiteles\u00edt\u0151 adatok"
},
"step": {
"user": {
"data": {
"password": "Jelsz\u00f3",
"username": "Email c\u00edm"
},
"title": "T\u00f6ltse ki az Abode bejelentkez\u00e9si adatait"
}
},
"title": "Abode"
}
}

View File

@ -5,7 +5,7 @@
},
"error": {
"connection_error": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z Abode.",
"identifier_exists": "Konto zosta\u0142o ju\u017c zarejestrowane",
"identifier_exists": "Konto jest ju\u017c zarejestrowane.",
"invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce"
},
"step": {

View File

@ -0,0 +1,22 @@
{
"config": {
"abort": {
"single_instance_allowed": "Endast en enda konfiguration av Abode \u00e4r till\u00e5ten."
},
"error": {
"connection_error": "Det gick inte att ansluta till Abode.",
"identifier_exists": "Kontot \u00e4r redan registrerat.",
"invalid_credentials": "Ogiltiga autentiseringsuppgifter."
},
"step": {
"user": {
"data": {
"password": "L\u00f6senord",
"username": "E-postadress"
},
"title": "Fyll i din inloggningsinformation f\u00f6r Abode"
}
},
"title": "Abode"
}
}

View File

@ -11,6 +11,8 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
DEVICE_TYPES = [CONST.TYPE_SWITCH, CONST.TYPE_VALVE]
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Abode switch devices."""
@ -18,7 +20,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
entities = []
for device in data.abode.get_devices(generic_type=CONST.TYPE_SWITCH):
for device_type in DEVICE_TYPES:
for device in data.abode.get_devices(generic_type=device_type):
entities.append(AbodeSwitch(data, device))
for automation in data.abode.get_automations(generic_type=CONST.TYPE_AUTOMATION):

View File

@ -1,6 +1,8 @@
{
"config": {
"abort": {
"adguard_home_addon_outdated": "Esta integraci\u00f3n requiere AdGuard Home {minimal_version} o superior, tiene {current_version}. Actualice su complemento Hass.io AdGuard Home.",
"adguard_home_outdated": "Esta integraci\u00f3n requiere AdGuard Home {minimal_version} o superior, tiene {current_version}.",
"existing_instance_updated": "Se actualiz\u00f3 la configuraci\u00f3n existente.",
"single_instance_allowed": "Solo se permite una \u00fanica configuraci\u00f3n de AdGuard Home."
},

View File

@ -1,6 +1,8 @@
{
"config": {
"abort": {
"adguard_home_addon_outdated": "Den h\u00e4r integrationen kr\u00e4ver AdGuard Home {minimal_version} eller senare, du har {current_version}. Uppdatera ditt Hass.io AdGuard Home-till\u00e4gg.",
"adguard_home_outdated": "Den h\u00e4r integrationen kr\u00e4ver AdGuard Home {minimal_version} eller senare, du har {current_version}.",
"existing_instance_updated": "Uppdaterade existerande konfiguration.",
"single_instance_allowed": "Endast en enda konfiguration av AdGuard Home \u00e4r till\u00e5ten."
},

View File

@ -0,0 +1,22 @@
{
"config": {
"error": {
"auth": "La clave API no es correcta.",
"name_exists": "El nombre ya existe.",
"wrong_location": "No hay estaciones de medici\u00f3n Airly en esta \u00e1rea."
},
"step": {
"user": {
"data": {
"api_key": "Clave API de Airly",
"latitude": "Latitud",
"longitude": "Longitud",
"name": "Nombre de la integraci\u00f3n"
},
"description": "Configure la integraci\u00f3n de la calidad del aire de Airly. Para generar la clave API, vaya a https://developer.airly.eu/register",
"title": "Airly"
}
},
"title": "Airly"
}
}

View File

@ -0,0 +1,25 @@
{
"config": {
"abort": {
"already_configured": "Ezen koordin\u00e1t\u00e1k Airly integr\u00e1ci\u00f3ja m\u00e1r konfigur\u00e1lva van."
},
"error": {
"auth": "Az API kulcs nem megfelel\u0151.",
"name_exists": "A n\u00e9v m\u00e1r l\u00e9tezik",
"wrong_location": "Ezen a ter\u00fcleten nincs Airly m\u00e9r\u0151\u00e1llom\u00e1s."
},
"step": {
"user": {
"data": {
"api_key": "Airly API kulcs",
"latitude": "Sz\u00e9less\u00e9g",
"longitude": "Hossz\u00fas\u00e1g",
"name": "Az integr\u00e1ci\u00f3 neve"
},
"description": "Az Airly leveg\u0151min\u0151s\u00e9gi integr\u00e1ci\u00f3j\u00e1nak be\u00e1ll\u00edt\u00e1sa. Api-kulcs l\u00e9trehoz\u00e1s\u00e1hoz nyissa meg a k\u00f6vetkez\u0151 weboldalt: https://developer.airly.eu/register",
"title": "Airly"
}
},
"title": "Airly"
}
}

View File

@ -1,5 +1,8 @@
{
"config": {
"abort": {
"already_configured": "Airly-integratie voor deze co\u00f6rdinaten is al geconfigureerd."
},
"error": {
"auth": "API-sleutel is niet correct.",
"name_exists": "Naam bestaat al.",

View File

@ -1,5 +1,8 @@
{
"config": {
"abort": {
"already_configured": "Airly integracija za te koordinate je \u017ee nastavljen."
},
"error": {
"auth": "Klju\u010d API ni pravilen.",
"name_exists": "Ime \u017ee obstaja",

View File

@ -0,0 +1,25 @@
{
"config": {
"abort": {
"already_configured": "Airly-integrationen f\u00f6r dessa koordinater \u00e4r redan konfigurerad."
},
"error": {
"auth": "API-nyckeln \u00e4r inte korrekt.",
"name_exists": "Namnet finns redan.",
"wrong_location": "Inga Airly m\u00e4tstationer i detta omr\u00e5de."
},
"step": {
"user": {
"data": {
"api_key": "Airly API-nyckel",
"latitude": "Latitud",
"longitude": "Longitud",
"name": "Integrationens namn"
},
"description": "Konfigurera integration av luftkvalitet. F\u00f6r att skapa API-nyckel, g\u00e5 till https://developer.airly.eu/register",
"title": "Airly"
}
},
"title": "Airly"
}
}

View File

@ -0,0 +1,18 @@
{
"device_automation": {
"action_type": {
"arm_away": "Larma {entity_name} borta",
"arm_home": "Larma {entity_name} hemma",
"arm_night": "Larma {entity_name} natt",
"disarm": "Avlarma {entity_name}",
"trigger": "Utl\u00f6sare {entity_name}"
},
"trigger_type": {
"armed_away": "{entity_name} larmad borta",
"armed_home": "{entity_name} larmad hemma",
"armed_night": "{entity_name} larmad natt",
"disarmed": "{entity_name} bortkopplad",
"triggered": "{entity_name} utl\u00f6st"
}
}
}

View File

@ -138,7 +138,7 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
def _restore_callback(self, zone):
"""Update the zone's state, if needed."""
if zone is None or int(zone) == self._zone_number:
if zone is None or (int(zone) == self._zone_number and not self._loop):
self._state = 0
self.schedule_update_ha_state()

View File

@ -1,5 +1,6 @@
"""Alexa capabilities."""
import logging
import math
from homeassistant.components import (
cover,
@ -645,6 +646,43 @@ class AlexaSpeaker(AlexaCapability):
"""Return the Alexa API name of this interface."""
return "Alexa.Speaker"
def properties_supported(self):
"""Return what properties this entity supports."""
properties = [{"name": "volume"}]
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & media_player.SUPPORT_VOLUME_MUTE:
properties.append({"name": "muted"})
return properties
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 == "volume":
current_level = self.entity.attributes.get(
media_player.ATTR_MEDIA_VOLUME_LEVEL
)
try:
current = math.floor(int(current_level * 100))
except ZeroDivisionError:
current = 0
return current
if name == "muted":
return bool(
self.entity.attributes.get(media_player.ATTR_MEDIA_VOLUME_MUTED)
)
return None
class AlexaStepSpeaker(AlexaCapability):
"""Implements Alexa.StepSpeaker.
@ -711,6 +749,13 @@ class AlexaInputController(AlexaCapability):
source_list = self.entity.attributes.get(
media_player.ATTR_INPUT_SOURCE_LIST, []
)
input_list = AlexaInputController.get_valid_inputs(source_list)
return input_list
@staticmethod
def get_valid_inputs(source_list):
"""Return list of supported inputs."""
input_list = []
for source in source_list:
formatted_source = (

View File

@ -508,12 +508,7 @@ class MediaPlayerCapabilities(AlexaEntity):
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & media_player.const.SUPPORT_VOLUME_SET:
yield AlexaSpeaker(self.entity)
step_volume_features = (
media_player.const.SUPPORT_VOLUME_MUTE
| media_player.const.SUPPORT_VOLUME_STEP
)
if supported & step_volume_features:
elif supported & media_player.const.SUPPORT_VOLUME_STEP:
yield AlexaStepSpeaker(self.entity)
playback_features = (
@ -531,6 +526,12 @@ class MediaPlayerCapabilities(AlexaEntity):
yield AlexaSeekController(self.entity)
if supported & media_player.SUPPORT_SELECT_SOURCE:
inputs = AlexaInputController.get_valid_inputs(
self.entity.attributes.get(
media_player.const.ATTR_INPUT_SOURCE_LIST, []
)
)
if len(inputs) > 0:
yield AlexaInputController(self.entity)
if supported & media_player.const.SUPPORT_PLAY_MEDIA:

View File

@ -43,7 +43,7 @@ class AlexaDirective:
Behavior when self.has_endpoint is False is undefined.
Will raise AlexaInvalidEndpointError if the endpoint in the request is
malformed or nonexistant.
malformed or nonexistent.
"""
_endpoint_id = self._directive[API_ENDPOINT]["endpointId"]
self.entity_id = _endpoint_id.replace("#", ".")

View File

@ -0,0 +1,19 @@
{
"config": {
"abort": {
"already_setup": "Csak egy Almond fi\u00f3kot konfigur\u00e1lhat.",
"cannot_connect": "Nem lehet csatlakozni az Almond szerverhez.",
"missing_configuration": "K\u00e9rj\u00fck, ellen\u0151rizze az Almond be\u00e1ll\u00edt\u00e1s\u00e1nak dokument\u00e1ci\u00f3j\u00e1t."
},
"step": {
"hassio_confirm": {
"description": "Be szeretn\u00e9 \u00e1ll\u00edtani a Home Assistant alkalmaz\u00e1st az Almondhoz val\u00f3 csatlakoz\u00e1shoz, amelyet a Hass.io kieg\u00e9sz\u00edt\u0151 biztos\u00edt: {addon} ?",
"title": "Almond a Hass.io kieg\u00e9sz\u00edt\u0151n kereszt\u00fcl"
},
"pick_implementation": {
"title": "V\u00e1lassza ki a hiteles\u00edt\u00e9si m\u00f3dszert"
}
},
"title": "Almond"
}
}

View File

@ -6,6 +6,10 @@
"missing_configuration": "Raadpleeg de documentatie over het instellen van Almond."
},
"step": {
"hassio_confirm": {
"description": "Wilt u Home Assistant configureren om verbinding te maken met Almond die wordt aangeboden door de hass.io add-on {addon} ?",
"title": "Almond via Hass.io add-on"
},
"pick_implementation": {
"title": "Kies de authenticatie methode"
}

View File

@ -6,6 +6,10 @@
"missing_configuration": "Prosimo, preverite dokumentacijo o tem, kako nastaviti Almond."
},
"step": {
"hassio_confirm": {
"description": "Ali \u017eelite konfigurirati Home Assistant za povezavo z Almondom, ki ga ponuja dodatek Hass.io: {addon} ?",
"title": "Almond prek dodatka Hass.io"
},
"pick_implementation": {
"title": "Izberite na\u010din preverjanja pristnosti"
}

View File

@ -1,9 +1,19 @@
{
"config": {
"abort": {
"already_setup": "Du kan bara konfigurera ett Almond-konto.",
"cannot_connect": "Det g\u00e5r inte att ansluta till Almond-servern.",
"missing_configuration": "Kontrollera dokumentationen f\u00f6r hur du st\u00e4ller in Almond."
},
"step": {
"hassio_confirm": {
"description": "Vill du konfigurera Home Assistant f\u00f6r att ansluta till Almond som tillhandah\u00e5lls av Hass.io-till\u00e4gget: {addon} ?",
"title": "Almond via Hass.io-till\u00e4gget"
}
}
},
"pick_implementation": {
"title": "V\u00e4lj autentiseringsmetod"
}
},
"title": "Almond"
}
}

View File

@ -2,7 +2,7 @@
"domain": "alpha_vantage",
"name": "Alpha Vantage",
"documentation": "https://www.home-assistant.io/integrations/alpha_vantage",
"requirements": ["alpha_vantage==2.1.2"],
"requirements": ["alpha_vantage==2.1.3"],
"dependencies": [],
"codeowners": ["@fabaff"]
}

View File

@ -1,5 +1,8 @@
{
"config": {
"abort": {
"already_configured": "Aquesta clau d'aplicaci\u00f3 ja est\u00e0 en \u00fas."
},
"error": {
"identifier_exists": "Clau d'aplicaci\u00f3 i/o clau API ja registrada",
"invalid_key": "Clau API i/o clau d'aplicaci\u00f3 inv\u00e0lida/es",

View File

@ -1,5 +1,8 @@
{
"config": {
"abort": {
"already_configured": "Dieser App-Schl\u00fcssel wird bereits verwendet."
},
"error": {
"identifier_exists": "Anwendungsschl\u00fcssel und / oder API-Schl\u00fcssel bereits registriert",
"invalid_key": "Ung\u00fcltiger API Key und / oder Anwendungsschl\u00fcssel",

View File

@ -1,5 +1,8 @@
{
"config": {
"abort": {
"already_configured": "This app key is already in use."
},
"error": {
"identifier_exists": "Application Key and/or API Key already registered",
"invalid_key": "Invalid API Key and/or Application Key",

View File

@ -1,5 +1,8 @@
{
"config": {
"abort": {
"already_configured": "\uc774 \uc571 \ud0a4\ub294 \uc774\ubbf8 \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4."
},
"error": {
"identifier_exists": "\uc560\ud50c\ub9ac\ucf00\uc774\uc158 \ud0a4 \ud639\uc740 API \ud0a4\uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"invalid_key": "\uc560\ud50c\ub9ac\ucf00\uc774\uc158 \ud0a4 \ud639\uc740 API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",

View File

@ -1,5 +1,8 @@
{
"config": {
"abort": {
"already_configured": "Denne app n\u00f8kkelen er allerede i bruk."
},
"error": {
"identifier_exists": "Programn\u00f8kkel og/eller API-n\u00f8kkel er allerede registrert",
"invalid_key": "Ugyldig API-n\u00f8kkel og/eller programn\u00f8kkel",

View File

@ -1,7 +1,10 @@
{
"config": {
"abort": {
"already_configured": "Ten klucz aplikacji jest ju\u017c w u\u017cyciu."
},
"error": {
"identifier_exists": "Klucz aplikacji i/lub klucz API ju\u017c jest zarejestrowany",
"identifier_exists": "Klucz aplikacji i/lub klucz API ju\u017c jest zarejestrowany.",
"invalid_key": "Nieprawid\u0142owy klucz API i/lub klucz aplikacji",
"no_devices": "Nie znaleziono urz\u0105dze\u0144 na koncie"
},

View File

@ -1,5 +1,8 @@
{
"config": {
"abort": {
"already_configured": "\u6b64\u61c9\u7528\u7a0b\u5f0f\u5bc6\u9470\u5df2\u88ab\u4f7f\u7528\u3002"
},
"error": {
"identifier_exists": "API \u5bc6\u9470\u53ca/\u6216\u61c9\u7528\u5bc6\u9470\u5df2\u8a3b\u518a",
"invalid_key": "API \u5bc6\u9470\u53ca/\u6216\u61c9\u7528\u5bc6\u9470\u7121\u6548",

View File

@ -378,7 +378,7 @@ class AmbientStation:
if data != self.stations[mac_address][ATTR_LAST_DATA]:
_LOGGER.debug("New data received: %s", data)
self.stations[mac_address][ATTR_LAST_DATA] = data
async_dispatcher_send(self._hass, TOPIC_UPDATE)
async_dispatcher_send(self._hass, TOPIC_UPDATE.format(mac_address))
_LOGGER.debug("Resetting watchdog")
self._watchdog_listener()
@ -518,7 +518,7 @@ class AmbientWeatherEntity(Entity):
self.async_schedule_update_ha_state(True)
self._async_unsub_dispatcher_connect = async_dispatcher_connect(
self.hass, TOPIC_UPDATE, update
self.hass, TOPIC_UPDATE.format(self._mac_address), update
)
async def async_will_remove_from_hass(self):

View File

@ -8,7 +8,7 @@ CONF_APP_KEY = "app_key"
DATA_CLIENT = "data_client"
TOPIC_UPDATE = "update"
TOPIC_UPDATE = "ambient_station_data_update_{0}"
TYPE_BINARY_SENSOR = "binary_sensor"
TYPE_SENSOR = "sensor"

View File

@ -23,6 +23,7 @@ from homeassistant.const import (
CONF_SENSORS,
CONF_USERNAME,
ENTITY_MATCH_ALL,
ENTITY_MATCH_NONE,
HTTP_BASIC_AUTHENTICATION,
)
from homeassistant.exceptions import Unauthorized, UnknownUser
@ -34,7 +35,15 @@ from homeassistant.helpers.service import async_extract_entity_ids
from .binary_sensor import BINARY_SENSORS
from .camera import CAMERA_SERVICES, STREAM_SOURCE_LIST
from .const import CAMERAS, DATA_AMCREST, DEVICES, DOMAIN, SERVICE_UPDATE
from .const import (
CAMERAS,
COMM_RETRIES,
COMM_TIMEOUT,
DATA_AMCREST,
DEVICES,
DOMAIN,
SERVICE_UPDATE,
)
from .helpers import service_signal
from .sensor import SENSORS
@ -110,38 +119,56 @@ class AmcrestChecker(Http):
self._wrap_name = name
self._wrap_errors = 0
self._wrap_lock = threading.Lock()
self._wrap_login_err = False
self._unsub_recheck = None
super().__init__(
host, port, user, password, retries_connection=1, timeout_protocol=3.05
host,
port,
user,
password,
retries_connection=COMM_RETRIES,
timeout_protocol=COMM_TIMEOUT,
)
@property
def available(self):
"""Return if camera's API is responding."""
return self._wrap_errors <= MAX_ERRORS
return self._wrap_errors <= MAX_ERRORS and not self._wrap_login_err
def _start_recovery(self):
dispatcher_send(self._hass, service_signal(SERVICE_UPDATE, self._wrap_name))
self._unsub_recheck = track_time_interval(
self._hass, self._wrap_test_online, RECHECK_INTERVAL
)
def command(self, cmd, retries=None, timeout_cmd=None, stream=False):
"""amcrest.Http.command wrapper to catch errors."""
try:
ret = super().command(cmd, retries, timeout_cmd, stream)
except LoginError as ex:
with self._wrap_lock:
was_online = self.available
was_login_err = self._wrap_login_err
self._wrap_login_err = True
if not was_login_err:
_LOGGER.error("%s camera offline: Login error: %s", self._wrap_name, ex)
if was_online:
self._start_recovery()
raise
except AmcrestError:
with self._wrap_lock:
was_online = self.available
self._wrap_errors += 1
_LOGGER.debug("%s camera errs: %i", self._wrap_name, self._wrap_errors)
errs = self._wrap_errors = self._wrap_errors + 1
offline = not self.available
if offline and was_online:
_LOGGER.debug("%s camera errs: %i", self._wrap_name, errs)
if was_online and offline:
_LOGGER.error("%s camera offline: Too many errors", self._wrap_name)
dispatcher_send(
self._hass, service_signal(SERVICE_UPDATE, self._wrap_name)
)
self._unsub_recheck = track_time_interval(
self._hass, self._wrap_test_online, RECHECK_INTERVAL
)
self._start_recovery()
raise
with self._wrap_lock:
was_offline = not self.available
self._wrap_errors = 0
self._wrap_login_err = False
if was_offline:
self._unsub_recheck()
self._unsub_recheck = None
@ -151,6 +178,7 @@ class AmcrestChecker(Http):
def _wrap_test_online(self, now):
"""Test if camera is back online."""
_LOGGER.debug("Testing if %s back online", self._wrap_name)
try:
self.current_time
except AmcrestError:
@ -166,15 +194,10 @@ def setup(hass, config):
username = device[CONF_USERNAME]
password = device[CONF_PASSWORD]
try:
api = AmcrestChecker(
hass, name, device[CONF_HOST], device[CONF_PORT], username, password
)
except LoginError as ex:
_LOGGER.error("Login error for %s camera: %s", name, ex)
continue
ffmpeg_arguments = device[CONF_FFMPEG_ARGUMENTS]
resolution = RESOLUTION_LIST[device[CONF_RESOLUTION]]
binary_sensors = device.get(CONF_BINARY_SENSORS)
@ -236,6 +259,9 @@ def setup(hass, config):
if have_permission(user, entity_id)
]
if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_NONE:
return []
call_ids = await async_extract_entity_ids(hass, call)
entity_ids = []
for entity_id in hass.data[DATA_AMCREST][CAMERAS]:

View File

@ -1,4 +1,4 @@
"""Suppoort for Amcrest IP camera binary sensors."""
"""Support for Amcrest IP camera binary sensors."""
from datetime import timedelta
import logging

View File

@ -1,11 +1,11 @@
"""Support for Amcrest IP cameras."""
import asyncio
from datetime import timedelta
from functools import partial
import logging
from amcrest import AmcrestError
from haffmpeg.camera import CameraMjpeg
from urllib3.exceptions import HTTPError
import voluptuous as vol
from homeassistant.components.camera import (
@ -26,9 +26,11 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import (
CAMERA_WEB_SESSION_TIMEOUT,
CAMERAS,
COMM_TIMEOUT,
DATA_AMCREST,
DEVICES,
SERVICE_UPDATE,
SNAPSHOT_TIMEOUT,
)
from .helpers import log_update_error, service_signal
@ -90,6 +92,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities([AmcrestCam(name, device, hass.data[DATA_FFMPEG])], True)
class CannotSnapshot(Exception):
"""Conditions are not valid for taking a snapshot."""
class AmcrestCam(Camera):
"""An implementation of an Amcrest IP camera."""
@ -112,28 +118,58 @@ class AmcrestCam(Camera):
self._motion_recording_enabled = None
self._color_bw = None
self._rtsp_url = None
self._snapshot_lock = asyncio.Lock()
self._snapshot_task = None
self._unsub_dispatcher = []
self._update_succeeded = False
async def async_camera_image(self):
"""Return a still image response from the camera."""
def _check_snapshot_ok(self):
available = self.available
if not available or not self.is_on:
_LOGGER.warning(
"Attempt to take snaphot when %s camera is %s",
"Attempt to take snapshot when %s camera is %s",
self.name,
"offline" if not available else "off",
)
return None
async with self._snapshot_lock:
raise CannotSnapshot
async def _async_get_image(self):
try:
# Send the request to snap a picture and return raw jpg data
response = await self.hass.async_add_executor_job(self._api.snapshot)
return response.data
except (AmcrestError, HTTPError) as error:
# Snapshot command needs a much longer read timeout than other commands.
return await self.hass.async_add_executor_job(
partial(
self._api.snapshot,
timeout=(COMM_TIMEOUT, SNAPSHOT_TIMEOUT),
stream=False,
)
)
except AmcrestError as error:
log_update_error(_LOGGER, "get image from", self.name, "camera", error)
return None
finally:
self._snapshot_task = None
async def async_camera_image(self):
"""Return a still image response from the camera."""
_LOGGER.debug("Take snapshot from %s", self._name)
try:
# Amcrest cameras only support one snapshot command at a time.
# Hence need to wait if a previous snapshot has not yet finished.
# Also need to check that camera is online and turned on before each wait
# and before initiating shapshot.
while self._snapshot_task:
self._check_snapshot_ok()
_LOGGER.debug("Waiting for previous snapshot from %s ...", self._name)
await self._snapshot_task
self._check_snapshot_ok()
# Run snapshot command in separate Task that can't be cancelled so
# 1) it's not possible to send another snapshot command while camera is
# still working on a previous one, and
# 2) someone will be around to catch any exceptions.
self._snapshot_task = self.hass.async_create_task(self._async_get_image())
return await asyncio.shield(self._snapshot_task)
except CannotSnapshot:
return None
async def handle_async_mjpeg_stream(self, request):
"""Return an MJPEG stream."""

View File

@ -6,6 +6,9 @@ DEVICES = "devices"
BINARY_SENSOR_SCAN_INTERVAL_SECS = 5
CAMERA_WEB_SESSION_TIMEOUT = 10
COMM_RETRIES = 1
COMM_TIMEOUT = 6.05
SENSOR_SCAN_INTERVAL_SECS = 10
SNAPSHOT_TIMEOUT = 20
SERVICE_UPDATE = "update"

View File

@ -2,7 +2,7 @@
"domain": "amcrest",
"name": "Amcrest",
"documentation": "https://www.home-assistant.io/integrations/amcrest",
"requirements": ["amcrest==1.5.3"],
"requirements": ["amcrest==1.5.6"],
"dependencies": ["ffmpeg"],
"codeowners": ["@pnbruckner"]
}

View File

@ -1,4 +1,4 @@
"""Suppoort for Amcrest IP camera sensors."""
"""Support for Amcrest IP camera sensors."""
from datetime import timedelta
import logging

View File

@ -1,6 +1,6 @@
{
"domain": "apcupsd",
"name": "APCUPSd",
"name": "apcupsd",
"documentation": "https://www.home-assistant.io/integrations/apcupsd",
"requirements": ["apcaccess==0.0.13"],
"dependencies": [],

View File

@ -177,7 +177,7 @@ class ApnsNotificationService(BaseNotificationService):
def device_state_changed_listener(self, entity_id, from_s, to_s):
"""
Listen for sate change.
Listen for state change.
Track device state change if a device has a tracking id specified.
"""

View File

@ -4,5 +4,6 @@
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
"requirements": ["pyatv==0.3.13"],
"dependencies": ["configurator"],
"after_dependencies": ["discovery"],
"codeowners": []
}

View File

@ -2,7 +2,7 @@
"domain": "apprise",
"name": "Apprise",
"documentation": "https://www.home-assistant.io/integrations/apprise",
"requirements": ["apprise==0.8.3"],
"requirements": ["apprise==0.8.4"],
"dependencies": [],
"codeowners": ["@caronc"]
}

View File

@ -0,0 +1,5 @@
{
"config": {
"title": "Arcam FMJ"
}
}

View File

@ -110,19 +110,19 @@ class ArloBaseStation(AlarmControlPanel):
else:
self._state = None
async def async_alarm_disarm(self, code=None):
def alarm_disarm(self, code=None):
"""Send disarm command."""
self._base_station.mode = DISARMED
async def async_alarm_arm_away(self, code=None):
def alarm_arm_away(self, code=None):
"""Send arm away command. Uses custom mode."""
self._base_station.mode = self._away_mode_name
async def async_alarm_arm_home(self, code=None):
def alarm_arm_home(self, code=None):
"""Send arm home command. Uses custom mode."""
self._base_station.mode = self._home_mode_name
async def async_alarm_arm_night(self, code=None):
def alarm_arm_night(self, code=None):
"""Send arm night command. Uses custom mode."""
self._base_station.mode = self._night_mode_name

View File

@ -78,8 +78,10 @@ class ArloCam(Camera):
async def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from the camera."""
video = await self.hass.async_add_executor_job(
getattr, self._camera, "last_video"
)
video = self._camera.last_video
if not video:
error_msg = "Video not found for {0}. Is it older than {1} days?".format(
self.name, self._camera.min_days_vdo_cache

View File

@ -70,7 +70,7 @@ async def async_setup(hass, config):
await api.connection.async_connect()
if not api.is_connected:
_LOGGER.error("Unable to setup asuswrt component")
_LOGGER.error("Unable to setup component")
return False
hass.data[DATA_ASUSWRT] = api

View File

@ -1,6 +1,6 @@
{
"domain": "asuswrt",
"name": "Asuswrt",
"name": "ASUSWRT",
"documentation": "https://www.home-assistant.io/integrations/asuswrt",
"requirements": ["aioasuswrt==1.1.22"],
"dependencies": [],

View File

@ -1,6 +1,7 @@
"""Asuswrt status sensors."""
import logging
from homeassistant.const import DATA_GIGABYTES, DATA_RATE_MEGABITS_PER_SECOND
from homeassistant.helpers.entity import Entity
from . import DATA_ASUSWRT
@ -61,7 +62,7 @@ class AsuswrtRXSensor(AsuswrtSensor):
"""Representation of a asuswrt download speed sensor."""
_name = "Asuswrt Download Speed"
_unit = "Mbit/s"
_unit = DATA_RATE_MEGABITS_PER_SECOND
@property
def unit_of_measurement(self):
@ -79,7 +80,7 @@ class AsuswrtTXSensor(AsuswrtSensor):
"""Representation of a asuswrt upload speed sensor."""
_name = "Asuswrt Upload Speed"
_unit = "Mbit/s"
_unit = DATA_RATE_MEGABITS_PER_SECOND
@property
def unit_of_measurement(self):
@ -97,7 +98,7 @@ class AsuswrtTotalRXSensor(AsuswrtSensor):
"""Representation of a asuswrt total download sensor."""
_name = "Asuswrt Download"
_unit = "Gigabyte"
_unit = DATA_GIGABYTES
@property
def unit_of_measurement(self):
@ -115,7 +116,7 @@ class AsuswrtTotalTXSensor(AsuswrtSensor):
"""Representation of a asuswrt total upload sensor."""
_name = "Asuswrt Upload"
_unit = "Gigabyte"
_unit = DATA_GIGABYTES
@property
def unit_of_measurement(self):

View File

@ -0,0 +1,31 @@
{
"config": {
"abort": {
"already_configured": "Konto ist bereits konfiguriert"
},
"error": {
"cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut",
"invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler"
},
"step": {
"user": {
"data": {
"login_method": "Anmeldemethode",
"password": "Passwort",
"timeout": "Zeit\u00fcberschreitung (Sekunden)",
"username": "Benutzername"
},
"title": "Richten Sie ein August-Konto ein"
},
"validation": {
"data": {
"code": "Verifizierungs-Code"
},
"description": "Bitte \u00fcberpr\u00fcfen Sie Ihre {login_method} ({username}) und geben Sie den Best\u00e4tigungscode ein",
"title": "Zwei-Faktor-Authentifizierung"
}
},
"title": "August"
}
}

View File

@ -0,0 +1,32 @@
{
"config": {
"abort": {
"already_configured": "Account is already configured"
},
"error": {
"cannot_connect": "Failed to connect, please try again",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
"step": {
"user": {
"data": {
"login_method": "Login Method",
"password": "Password",
"timeout": "Timeout (seconds)",
"username": "Username"
},
"description": "If the Login Method is 'email', Username is the email address. If the Login Method is 'phone', Username is the phone number in the format '+NNNNNNNNN'.",
"title": "Setup an August account"
},
"validation": {
"data": {
"code": "Verification code"
},
"description": "Please check your {login_method} ({username}) and enter the verification code below",
"title": "Two factor authentication"
}
},
"title": "August"
}
}

View File

@ -1,8 +1,10 @@
"""Support for August devices."""
import asyncio
from datetime import timedelta
from functools import partial
import logging
from august.api import Api
from august.api import Api, AugustApiHTTPError
from august.authenticator import AuthenticationState, Authenticator, ValidationResult
from requests import RequestException, Session
import voluptuous as vol
@ -13,9 +15,10 @@ from homeassistant.const import (
CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
from homeassistant.util import Throttle, dt
_LOGGER = logging.getLogger(__name__)
@ -42,9 +45,22 @@ DEFAULT_ENTITY_NAMESPACE = "august"
# avoid hitting rate limits
MIN_TIME_BETWEEN_LOCK_DETAIL_UPDATES = timedelta(seconds=1800)
DEFAULT_SCAN_INTERVAL = timedelta(seconds=10)
# Limit locks status check to 900 seconds now that
# we get the state from the lock and unlock api calls
# and the lock and unlock activities are now captured
MIN_TIME_BETWEEN_LOCK_STATUS_UPDATES = timedelta(seconds=900)
# Doorbells need to update more frequently than locks
# since we get an image from the doorbell api
MIN_TIME_BETWEEN_DOORBELL_STATUS_UPDATES = timedelta(seconds=20)
# Activity needs to be checked more frequently as the
# doorbell motion and rings are included here
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10)
DEFAULT_SCAN_INTERVAL = timedelta(seconds=10)
LOGIN_METHODS = ["phone", "email"]
CONFIG_SCHEMA = vol.Schema(
@ -65,7 +81,7 @@ CONFIG_SCHEMA = vol.Schema(
AUGUST_COMPONENTS = ["camera", "binary_sensor", "lock"]
def request_configuration(hass, config, api, authenticator):
def request_configuration(hass, config, api, authenticator, token_refresh_lock):
"""Request configuration steps from the user."""
configurator = hass.components.configurator
@ -79,7 +95,7 @@ def request_configuration(hass, config, api, authenticator):
_CONFIGURING[DOMAIN], "Invalid verification code"
)
elif result == ValidationResult.VALIDATED:
setup_august(hass, config, api, authenticator)
setup_august(hass, config, api, authenticator, token_refresh_lock)
if DOMAIN not in _CONFIGURING:
authenticator.send_verification_code()
@ -100,7 +116,7 @@ def request_configuration(hass, config, api, authenticator):
)
def setup_august(hass, config, api, authenticator):
def setup_august(hass, config, api, authenticator, token_refresh_lock):
"""Set up the August component."""
authentication = None
@ -123,7 +139,9 @@ def setup_august(hass, config, api, authenticator):
if DOMAIN in _CONFIGURING:
hass.components.configurator.request_done(_CONFIGURING.pop(DOMAIN))
hass.data[DATA_AUGUST] = AugustData(hass, api, authentication.access_token)
hass.data[DATA_AUGUST] = AugustData(
hass, api, authentication, authenticator, token_refresh_lock
)
for component in AUGUST_COMPONENTS:
discovery.load_platform(hass, component, DOMAIN, {}, config)
@ -133,13 +151,13 @@ def setup_august(hass, config, api, authenticator):
_LOGGER.error("Invalid password provided")
return False
if state == AuthenticationState.REQUIRES_VALIDATION:
request_configuration(hass, config, api, authenticator)
request_configuration(hass, config, api, authenticator, token_refresh_lock)
return True
return False
def setup(hass, config):
async def async_setup(hass, config):
"""Set up the August component."""
conf = config[DOMAIN]
@ -171,20 +189,28 @@ def setup(hass, config):
_LOGGER.debug("August HTTP session closed.")
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, close_http_session)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, close_http_session)
_LOGGER.debug("Registered for Home Assistant stop event")
return setup_august(hass, config, api, authenticator)
token_refresh_lock = asyncio.Lock()
return await hass.async_add_executor_job(
setup_august, hass, config, api, authenticator, token_refresh_lock
)
class AugustData:
"""August data object."""
def __init__(self, hass, api, access_token):
def __init__(self, hass, api, authentication, authenticator, token_refresh_lock):
"""Init August data object."""
self._hass = hass
self._api = api
self._access_token = access_token
self._authenticator = authenticator
self._access_token = authentication.access_token
self._access_token_expires = authentication.access_token_expires
self._token_refresh_lock = token_refresh_lock
self._doorbells = self._api.get_doorbells(self._access_token) or []
self._locks = self._api.get_operable_locks(self._access_token) or []
self._house_ids = set()
@ -192,11 +218,20 @@ class AugustData:
self._house_ids.add(device.house_id)
self._doorbell_detail_by_id = {}
self._door_last_state_update_time_utc_by_id = {}
self._lock_last_status_update_time_utc_by_id = {}
self._lock_status_by_id = {}
self._lock_detail_by_id = {}
self._door_state_by_id = {}
self._activities_by_id = {}
# We check the locks right away so we can
# remove inoperative ones
self._update_locks_status()
self._update_locks_detail()
self._filter_inoperative_locks()
@property
def house_ids(self):
"""Return a list of house_ids."""
@ -212,24 +247,48 @@ class AugustData:
"""Return a list of locks."""
return self._locks
def get_device_activities(self, device_id, *activity_types):
async def _async_refresh_access_token_if_needed(self):
"""Refresh the august access token if needed."""
if self._authenticator.should_refresh():
async with self._token_refresh_lock:
await self._hass.async_add_executor_job(self._refresh_access_token)
def _refresh_access_token(self):
refreshed_authentication = self._authenticator.refresh_access_token(force=False)
_LOGGER.info(
"Refreshed august access token. The old token expired at %s, and the new token expires at %s",
self._access_token_expires,
refreshed_authentication.access_token_expires,
)
self._access_token = refreshed_authentication.access_token
self._access_token_expires = refreshed_authentication.access_token_expires
async def async_get_device_activities(self, device_id, *activity_types):
"""Return a list of activities."""
_LOGGER.debug("Getting device activities")
self._update_device_activities()
_LOGGER.debug("Getting device activities for %s", device_id)
await self._async_update_device_activities()
activities = self._activities_by_id.get(device_id, [])
if activity_types:
return [a for a in activities if a.activity_type in activity_types]
return activities
def get_latest_device_activity(self, device_id, *activity_types):
async def async_get_latest_device_activity(self, device_id, *activity_types):
"""Return latest activity."""
activities = self.get_device_activities(device_id, *activity_types)
activities = await self.async_get_device_activities(device_id, *activity_types)
return next(iter(activities or []), None)
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def _update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT):
async def _async_update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT):
"""Update data object with latest from August API."""
# This is the only place we refresh the api token
await self._async_refresh_access_token_if_needed()
return await self._hass.async_add_executor_job(
partial(self._update_device_activities, limit=ACTIVITY_FETCH_LIMIT)
)
def _update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT):
_LOGGER.debug("Start retrieving device activities")
for house_id in self.house_ids:
_LOGGER.debug("Updating device activity for house id %s", house_id)
@ -243,14 +302,18 @@ class AugustData:
self._activities_by_id[device_id] = [
a for a in activities if a.device_id == device_id
]
_LOGGER.debug("Completed retrieving device activities")
def get_doorbell_detail(self, doorbell_id):
async def async_get_doorbell_detail(self, doorbell_id):
"""Return doorbell detail."""
self._update_doorbells()
await self._async_update_doorbells()
return self._doorbell_detail_by_id.get(doorbell_id)
@Throttle(MIN_TIME_BETWEEN_UPDATES)
@Throttle(MIN_TIME_BETWEEN_DOORBELL_STATUS_UPDATES)
async def _async_update_doorbells(self):
await self._hass.async_add_executor_job(self._update_doorbells)
def _update_doorbells(self):
detail_by_id = {}
@ -275,38 +338,79 @@ class AugustData:
_LOGGER.debug("Completed retrieving doorbell details")
self._doorbell_detail_by_id = detail_by_id
def get_lock_status(self, lock_id):
def update_door_state(self, lock_id, door_state, update_start_time_utc):
"""Set the door status and last status update time.
This is called when newer activity is detected on the activity feed
in order to keep the internal data in sync
"""
self._door_state_by_id[lock_id] = door_state
self._door_last_state_update_time_utc_by_id[lock_id] = update_start_time_utc
return True
def update_lock_status(self, lock_id, lock_status, update_start_time_utc):
"""Set the lock status and last status update time.
This is used when the lock, unlock apis are called
or newer activity is detected on the activity feed
in order to keep the internal data in sync
"""
self._lock_status_by_id[lock_id] = lock_status
self._lock_last_status_update_time_utc_by_id[lock_id] = update_start_time_utc
return True
def lock_has_doorsense(self, lock_id):
"""Determine if a lock has doorsense installed and can tell when the door is open or closed."""
# We do not update here since this is not expected
# to change until restart
if self._lock_detail_by_id[lock_id] is None:
return False
return self._lock_detail_by_id[lock_id].doorsense
async def async_get_lock_status(self, lock_id):
"""Return status if the door is locked or unlocked.
This is status for the lock itself.
"""
self._update_locks()
await self._async_update_locks()
return self._lock_status_by_id.get(lock_id)
def get_lock_detail(self, lock_id):
async def async_get_lock_detail(self, lock_id):
"""Return lock detail."""
self._update_locks()
await self._async_update_locks()
return self._lock_detail_by_id.get(lock_id)
def get_door_state(self, lock_id):
def get_lock_name(self, device_id):
"""Return lock name as August has it stored."""
for lock in self._locks:
if lock.device_id == device_id:
return lock.device_name
async def async_get_door_state(self, lock_id):
"""Return status if the door is open or closed.
This is the status from the door sensor.
"""
self._update_locks_status()
await self._async_update_locks_status()
return self._door_state_by_id.get(lock_id)
def _update_locks(self):
self._update_locks_status()
self._update_locks_detail()
async def _async_update_locks(self):
await self._async_update_locks_status()
await self._async_update_locks_detail()
@Throttle(MIN_TIME_BETWEEN_LOCK_STATUS_UPDATES)
async def _async_update_locks_status(self):
await self._hass.async_add_executor_job(self._update_locks_status)
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def _update_locks_status(self):
status_by_id = {}
state_by_id = {}
lock_last_status_update_by_id = {}
door_last_state_update_by_id = {}
_LOGGER.debug("Start retrieving lock and door status")
for lock in self._locks:
update_start_time_utc = dt.utcnow()
_LOGGER.debug("Updating lock and door status for %s", lock.device_name)
try:
(
@ -315,6 +419,13 @@ class AugustData:
) = self._api.get_lock_status(
self._access_token, lock.device_id, door_status=True
)
# Since there is a a race condition between calling the
# lock and activity apis, we set the last update time
# BEFORE making the api call since we will compare this
# to activity later we want activity to win over stale lock/door
# state.
lock_last_status_update_by_id[lock.device_id] = update_start_time_utc
door_last_state_update_by_id[lock.device_id] = update_start_time_utc
except RequestException as ex:
_LOGGER.error(
"Request error trying to retrieve lock and door status for %s. %s",
@ -331,8 +442,33 @@ class AugustData:
_LOGGER.debug("Completed retrieving lock and door status")
self._lock_status_by_id = status_by_id
self._door_state_by_id = state_by_id
self._door_last_state_update_time_utc_by_id = door_last_state_update_by_id
self._lock_last_status_update_time_utc_by_id = lock_last_status_update_by_id
def get_last_lock_status_update_time_utc(self, lock_id):
"""Return the last time that a lock status update was seen from the august API."""
# Since the activity api is called more frequently than
# the lock api it is possible that the lock has not
# been updated yet
if lock_id not in self._lock_last_status_update_time_utc_by_id:
return dt.utc_from_timestamp(0)
return self._lock_last_status_update_time_utc_by_id[lock_id]
def get_last_door_state_update_time_utc(self, lock_id):
"""Return the last time that a door status update was seen from the august API."""
# Since the activity api is called more frequently than
# the lock api it is possible that the door has not
# been updated yet
if lock_id not in self._door_last_state_update_time_utc_by_id:
return dt.utc_from_timestamp(0)
return self._door_last_state_update_time_utc_by_id[lock_id]
@Throttle(MIN_TIME_BETWEEN_LOCK_DETAIL_UPDATES)
async def _async_update_locks_detail(self):
await self._hass.async_add_executor_job(self._update_locks_detail)
def _update_locks_detail(self):
detail_by_id = {}
@ -358,8 +494,60 @@ class AugustData:
def lock(self, device_id):
"""Lock the device."""
return self._api.lock(self._access_token, device_id)
return _call_api_operation_that_requires_bridge(
self.get_lock_name(device_id),
"lock",
self._api.lock,
self._access_token,
device_id,
)
def unlock(self, device_id):
"""Unlock the device."""
return self._api.unlock(self._access_token, device_id)
return _call_api_operation_that_requires_bridge(
self.get_lock_name(device_id),
"unlock",
self._api.unlock,
self._access_token,
device_id,
)
def _filter_inoperative_locks(self):
# Remove non-operative locks as there must
# be a bridge (August Connect) for them to
# be usable
operative_locks = []
for lock in self._locks:
lock_detail = self._lock_detail_by_id.get(lock.device_id)
if lock_detail is None:
_LOGGER.info(
"The lock %s could not be setup because the system could not fetch details about the lock.",
lock.device_name,
)
elif lock_detail.bridge is None:
_LOGGER.info(
"The lock %s could not be setup because it does not have a bridge (Connect).",
lock.device_name,
)
elif not lock_detail.bridge.operative:
_LOGGER.info(
"The lock %s could not be setup because the bridge (Connect) is not operative.",
lock.device_name,
)
else:
operative_locks.append(lock)
self._locks = operative_locks
def _call_api_operation_that_requires_bridge(
device_name, operation_name, func, *args, **kwargs
):
"""Call an API that requires the bridge to be online."""
ret = None
try:
ret = func(*args, **kwargs)
except AugustApiHTTPError as err:
raise HomeAssistantError(device_name + ": " + str(err))
return ret

View File

@ -2,84 +2,92 @@
from datetime import datetime, timedelta
import logging
from august.activity import ActivityType
from august.activity import ACTIVITY_ACTION_STATES, ActivityType
from august.lock import LockDoorStatus
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.util import dt
from . import DATA_AUGUST
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=10)
SCAN_INTERVAL = timedelta(seconds=5)
def _retrieve_door_state(data, lock):
async def _async_retrieve_door_state(data, lock):
"""Get the latest state of the DoorSense sensor."""
return data.get_door_state(lock.device_id)
return await data.async_get_door_state(lock.device_id)
def _retrieve_online_state(data, doorbell):
async def _async_retrieve_online_state(data, doorbell):
"""Get the latest state of the sensor."""
detail = data.get_doorbell_detail(doorbell.device_id)
detail = await data.async_get_doorbell_detail(doorbell.device_id)
if detail is None:
return None
return detail.is_online
def _retrieve_motion_state(data, doorbell):
async def _async_retrieve_motion_state(data, doorbell):
return _activity_time_based_state(
return await _async_activity_time_based_state(
data, doorbell, [ActivityType.DOORBELL_MOTION, ActivityType.DOORBELL_DING]
)
def _retrieve_ding_state(data, doorbell):
async def _async_retrieve_ding_state(data, doorbell):
return _activity_time_based_state(data, doorbell, [ActivityType.DOORBELL_DING])
return await _async_activity_time_based_state(
data, doorbell, [ActivityType.DOORBELL_DING]
)
def _activity_time_based_state(data, doorbell, activity_types):
async def _async_activity_time_based_state(data, doorbell, activity_types):
"""Get the latest state of the sensor."""
latest = data.get_latest_device_activity(doorbell.device_id, *activity_types)
latest = await data.async_get_latest_device_activity(
doorbell.device_id, *activity_types
)
if latest is not None:
start = latest.activity_start_time
end = latest.activity_end_time + timedelta(seconds=30)
end = latest.activity_end_time + timedelta(seconds=45)
return start <= datetime.now() <= end
return None
# Sensor types: Name, device_class, state_provider
SENSOR_TYPES_DOOR = {"door_open": ["Open", "door", _retrieve_door_state]}
SENSOR_NAME = 0
SENSOR_DEVICE_CLASS = 1
SENSOR_STATE_PROVIDER = 2
# sensor_type: [name, device_class, async_state_provider]
SENSOR_TYPES_DOOR = {"door_open": ["Open", "door", _async_retrieve_door_state]}
SENSOR_TYPES_DOORBELL = {
"doorbell_ding": ["Ding", "occupancy", _retrieve_ding_state],
"doorbell_motion": ["Motion", "motion", _retrieve_motion_state],
"doorbell_online": ["Online", "connectivity", _retrieve_online_state],
"doorbell_ding": ["Ding", "occupancy", _async_retrieve_ding_state],
"doorbell_motion": ["Motion", "motion", _async_retrieve_motion_state],
"doorbell_online": ["Online", "connectivity", _async_retrieve_online_state],
}
def setup_platform(hass, config, add_entities, discovery_info=None):
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the August binary sensors."""
data = hass.data[DATA_AUGUST]
devices = []
for door in data.locks:
for sensor_type in SENSOR_TYPES_DOOR:
state_provider = SENSOR_TYPES_DOOR[sensor_type][2]
if state_provider(data, door) is LockDoorStatus.UNKNOWN:
if not data.lock_has_doorsense(door.device_id):
_LOGGER.debug(
"Not adding sensor class %s for lock %s ",
SENSOR_TYPES_DOOR[sensor_type][1],
SENSOR_TYPES_DOOR[sensor_type][SENSOR_DEVICE_CLASS],
door.device_name,
)
continue
_LOGGER.debug(
"Adding sensor class %s for %s",
SENSOR_TYPES_DOOR[sensor_type][1],
SENSOR_TYPES_DOOR[sensor_type][SENSOR_DEVICE_CLASS],
door.device_name,
)
devices.append(AugustDoorBinarySensor(data, sensor_type, door))
@ -88,12 +96,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
for sensor_type in SENSOR_TYPES_DOORBELL:
_LOGGER.debug(
"Adding doorbell sensor class %s for %s",
SENSOR_TYPES_DOORBELL[sensor_type][1],
SENSOR_TYPES_DOORBELL[sensor_type][SENSOR_DEVICE_CLASS],
doorbell.device_name,
)
devices.append(AugustDoorbellBinarySensor(data, sensor_type, doorbell))
add_entities(devices, True)
async_add_entities(devices, True)
class AugustDoorBinarySensor(BinarySensorDevice):
@ -120,28 +128,79 @@ class AugustDoorBinarySensor(BinarySensorDevice):
@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
return SENSOR_TYPES_DOOR[self._sensor_type][1]
return SENSOR_TYPES_DOOR[self._sensor_type][SENSOR_DEVICE_CLASS]
@property
def name(self):
"""Return the name of the binary sensor."""
return "{} {}".format(
self._door.device_name, SENSOR_TYPES_DOOR[self._sensor_type][0]
self._door.device_name, SENSOR_TYPES_DOOR[self._sensor_type][SENSOR_NAME]
)
def update(self):
"""Get the latest state of the sensor."""
state_provider = SENSOR_TYPES_DOOR[self._sensor_type][2]
self._state = state_provider(self._data, self._door)
self._available = self._state is not None
async def async_update(self):
"""Get the latest state of the sensor and update activity."""
async_state_provider = SENSOR_TYPES_DOOR[self._sensor_type][
SENSOR_STATE_PROVIDER
]
lock_door_state = await async_state_provider(self._data, self._door)
self._available = (
lock_door_state is not None and lock_door_state != LockDoorStatus.UNKNOWN
)
self._state = lock_door_state == LockDoorStatus.OPEN
self._state = self._state == LockDoorStatus.OPEN
door_activity = await self._data.async_get_latest_device_activity(
self._door.device_id, ActivityType.DOOR_OPERATION
)
if door_activity is not None:
self._sync_door_activity(door_activity)
def _update_door_state(self, door_state, update_start_time):
new_state = door_state == LockDoorStatus.OPEN
if self._state != new_state:
self._state = new_state
self._data.update_door_state(
self._door.device_id, door_state, update_start_time
)
def _sync_door_activity(self, door_activity):
"""Check the activity for the latest door open/close activity (events).
We use this to determine the door state in between calls to the lock
api as we update it more frequently
"""
last_door_state_update_time_utc = self._data.get_last_door_state_update_time_utc(
self._door.device_id
)
activity_end_time_utc = dt.as_utc(door_activity.activity_end_time)
if activity_end_time_utc > last_door_state_update_time_utc:
_LOGGER.debug(
"The activity log has new events for %s: [action=%s] [activity_end_time_utc=%s] > [last_door_state_update_time_utc=%s]",
self.name,
door_activity.action,
activity_end_time_utc,
last_door_state_update_time_utc,
)
activity_start_time_utc = dt.as_utc(door_activity.activity_start_time)
if door_activity.action in ACTIVITY_ACTION_STATES:
self._update_door_state(
ACTIVITY_ACTION_STATES[door_activity.action],
activity_start_time_utc,
)
else:
_LOGGER.info(
"Unhandled door activity action %s for %s",
door_activity.action,
self.name,
)
@property
def unique_id(self) -> str:
"""Get the unique of the door open binary sensor."""
return "{:s}_{:s}".format(
self._door.device_id, SENSOR_TYPES_DOOR[self._sensor_type][0].lower()
self._door.device_id,
SENSOR_TYPES_DOOR[self._sensor_type][SENSOR_NAME].lower(),
)
@ -169,25 +228,31 @@ class AugustDoorbellBinarySensor(BinarySensorDevice):
@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
return SENSOR_TYPES_DOORBELL[self._sensor_type][1]
return SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_DEVICE_CLASS]
@property
def name(self):
"""Return the name of the binary sensor."""
return "{} {}".format(
self._doorbell.device_name, SENSOR_TYPES_DOORBELL[self._sensor_type][0]
self._doorbell.device_name,
SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_NAME],
)
def update(self):
async def async_update(self):
"""Get the latest state of the sensor."""
state_provider = SENSOR_TYPES_DOORBELL[self._sensor_type][2]
self._state = state_provider(self._data, self._doorbell)
self._available = self._doorbell.is_online
async_state_provider = SENSOR_TYPES_DOORBELL[self._sensor_type][
SENSOR_STATE_PROVIDER
]
self._state = await async_state_provider(self._data, self._doorbell)
# The doorbell will go into standby mode when there is no motion
# for a short while. It will wake by itself when needed so we need
# to consider is available or we will not report motion or dings
self._available = self._doorbell.is_online or self._doorbell.status == "standby"
@property
def unique_id(self) -> str:
"""Get the unique id of the doorbell sensor."""
return "{:s}_{:s}".format(
self._doorbell.device_id,
SENSOR_TYPES_DOORBELL[self._sensor_type][0].lower(),
SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_NAME].lower(),
)

View File

@ -10,7 +10,7 @@ from . import DATA_AUGUST, DEFAULT_TIMEOUT
SCAN_INTERVAL = timedelta(seconds=10)
def setup_platform(hass, config, add_entities, discovery_info=None):
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up August cameras."""
data = hass.data[DATA_AUGUST]
devices = []
@ -18,14 +18,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
for doorbell in data.doorbells:
devices.append(AugustCamera(data, doorbell, DEFAULT_TIMEOUT))
add_entities(devices, True)
async_add_entities(devices, True)
class AugustCamera(Camera):
"""An implementation of a Canary security camera."""
"""An implementation of a August security camera."""
def __init__(self, data, doorbell, timeout):
"""Initialize a Canary security camera."""
"""Initialize a August security camera."""
super().__init__()
self._data = data
self._doorbell = doorbell
@ -58,18 +58,23 @@ class AugustCamera(Camera):
"""Return the camera model."""
return "Doorbell"
def camera_image(self):
async def async_camera_image(self):
"""Return bytes of camera image."""
latest = self._data.get_doorbell_detail(self._doorbell.device_id)
latest = await self._data.async_get_doorbell_detail(self._doorbell.device_id)
if self._image_url is not latest.image_url:
self._image_url = latest.image_url
self._image_content = requests.get(
self._image_url, timeout=self._timeout
).content
self._image_content = await self.hass.async_add_executor_job(
self._camera_image
)
return self._image_content
def _camera_image(self):
"""Return bytes of camera image via http get."""
# Move this to py-august: see issue#32048
return requests.get(self._image_url, timeout=self._timeout).content
@property
def unique_id(self) -> str:
"""Get the unique id of the camera."""

View File

@ -2,11 +2,12 @@
from datetime import timedelta
import logging
from august.activity import ActivityType
from august.activity import ACTIVITY_ACTION_STATES, ActivityType
from august.lock import LockStatus
from homeassistant.components.lock import LockDevice
from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.util import dt
from . import DATA_AUGUST
@ -15,7 +16,7 @@ _LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=10)
def setup_platform(hass, config, add_entities, discovery_info=None):
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up August locks."""
data = hass.data[DATA_AUGUST]
devices = []
@ -24,7 +25,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
_LOGGER.debug("Adding lock for %s", lock.device_name)
devices.append(AugustLock(data, lock))
add_entities(devices, True)
async_add_entities(devices, True)
class AugustLock(LockDevice):
@ -39,27 +40,77 @@ class AugustLock(LockDevice):
self._changed_by = None
self._available = False
def lock(self, **kwargs):
async def async_lock(self, **kwargs):
"""Lock the device."""
self._data.lock(self._lock.device_id)
update_start_time_utc = dt.utcnow()
lock_status = await self.hass.async_add_executor_job(
self._data.lock, self._lock.device_id
)
self._update_lock_status(lock_status, update_start_time_utc)
def unlock(self, **kwargs):
async def async_unlock(self, **kwargs):
"""Unlock the device."""
self._data.unlock(self._lock.device_id)
update_start_time_utc = dt.utcnow()
lock_status = await self.hass.async_add_executor_job(
self._data.unlock, self._lock.device_id
)
self._update_lock_status(lock_status, update_start_time_utc)
def update(self):
"""Get the latest state of the sensor."""
self._lock_status = self._data.get_lock_status(self._lock.device_id)
self._available = self._lock_status is not None
def _update_lock_status(self, lock_status, update_start_time_utc):
if self._lock_status != lock_status:
self._lock_status = lock_status
self._data.update_lock_status(
self._lock.device_id, lock_status, update_start_time_utc
)
self.schedule_update_ha_state()
self._lock_detail = self._data.get_lock_detail(self._lock.device_id)
async def async_update(self):
"""Get the latest state of the sensor and update activity."""
self._lock_status = await self._data.async_get_lock_status(self._lock.device_id)
self._available = (
self._lock_status is not None and self._lock_status != LockStatus.UNKNOWN
)
self._lock_detail = await self._data.async_get_lock_detail(self._lock.device_id)
activity = self._data.get_latest_device_activity(
lock_activity = await self._data.async_get_latest_device_activity(
self._lock.device_id, ActivityType.LOCK_OPERATION
)
if activity is not None:
self._changed_by = activity.operated_by
if lock_activity is not None:
self._changed_by = lock_activity.operated_by
self._sync_lock_activity(lock_activity)
def _sync_lock_activity(self, lock_activity):
"""Check the activity for the latest lock/unlock activity (events).
We use this to determine the lock state in between calls to the lock
api as we update it more frequently
"""
last_lock_status_update_time_utc = self._data.get_last_lock_status_update_time_utc(
self._lock.device_id
)
activity_end_time_utc = dt.as_utc(lock_activity.activity_end_time)
if activity_end_time_utc > last_lock_status_update_time_utc:
_LOGGER.debug(
"The activity log has new events for %s: [action=%s] [activity_end_time_utc=%s] > [last_lock_status_update_time_utc=%s]",
self.name,
lock_activity.action,
activity_end_time_utc,
last_lock_status_update_time_utc,
)
activity_start_time_utc = dt.as_utc(lock_activity.activity_start_time)
if lock_activity.action in ACTIVITY_ACTION_STATES:
self._update_lock_status(
ACTIVITY_ACTION_STATES[lock_activity.action],
activity_start_time_utc,
)
else:
_LOGGER.info(
"Unhandled lock activity action %s for %s",
lock_activity.action,
self.name,
)
@property
def name(self):

View File

@ -2,7 +2,7 @@
"domain": "august",
"name": "August",
"documentation": "https://www.home-assistant.io/integrations/august",
"requirements": ["py-august==0.8.1"],
"requirements": ["py-august==0.14.0"],
"dependencies": ["configurator"],
"codeowners": []
"codeowners": ["@bdraco"]
}

View File

@ -11,8 +11,10 @@ import homeassistant.helpers.config_validation as cv
# mypy: allow-untyped-defs
CONF_ENCODING = "encoding"
CONF_QOS = "qos"
CONF_TOPIC = "topic"
DEFAULT_ENCODING = "utf-8"
DEFAULT_QOS = 0
TRIGGER_SCHEMA = vol.Schema(
{
@ -20,6 +22,9 @@ TRIGGER_SCHEMA = vol.Schema(
vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_PAYLOAD): cv.string,
vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string,
vol.Optional(CONF_QOS, default=DEFAULT_QOS): vol.All(
vol.Coerce(int), vol.In([0, 1, 2])
),
}
)
@ -29,6 +34,7 @@ async def async_attach_trigger(hass, config, action, automation_info):
topic = config[CONF_TOPIC]
payload = config.get(CONF_PAYLOAD)
encoding = config[CONF_ENCODING] or None
qos = config[CONF_QOS]
@callback
def mqtt_automation_listener(mqttmsg):
@ -49,6 +55,6 @@ async def async_attach_trigger(hass, config, action, automation_info):
hass.async_run_job(action, {"trigger": data})
remove = await mqtt.async_subscribe(
hass, topic, mqtt_automation_listener, encoding=encoding
hass, topic, mqtt_automation_listener, encoding=encoding, qos=qos
)
return remove

View File

@ -9,7 +9,7 @@
},
"error": {
"already_configured": "El dispositiu ja est\u00e0 configurat",
"already_in_progress": "El flux de dades pel dispositiu ja est\u00e0 en curs.",
"already_in_progress": "El flux de dades de configuraci\u00f3 pel dispositiu ja est\u00e0 en curs.",
"device_unavailable": "El dispositiu no est\u00e0 disponible",
"faulty_credentials": "Credencials d'usuari incorrectes"
},

View File

@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "Device is already configured",
"bad_config_file": "Bad data from config file",
"bad_config_file": "Bad data from configuration file",
"link_local_address": "Link local addresses are not supported",
"not_axis_device": "Discovered device not an Axis device",
"updated_configuration": "Updated device configuration with new host address"

View File

@ -1,10 +1,14 @@
{
"config": {
"abort": {
"updated_configuration": "Friss\u00edtett eszk\u00f6zkonfigur\u00e1ci\u00f3 \u00faj \u00e1llom\u00e1sc\u00edmmel"
},
"error": {
"already_configured": "Az eszk\u00f6zt m\u00e1r konfigur\u00e1ltuk",
"device_unavailable": "Az eszk\u00f6z nem \u00e9rhet\u0151 el",
"faulty_credentials": "Rossz felhaszn\u00e1l\u00f3i hiteles\u00edt\u0151 adatok"
},
"flow_title": "Axis eszk\u00f6z: {name} ({host})",
"step": {
"user": {
"data": {

View File

@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"bad_config_file": "\uad6c\uc131 \ud30c\uc77c\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"bad_config_file": "\uad6c\uc131 \ud30c\uc77c\uc5d0 \uc798\ubabb\ub41c \ub370\uc774\ud130\uac00 \uc788\uc2b5\ub2c8\ub2e4",
"link_local_address": "\ub85c\uceec \uc8fc\uc18c \uc5f0\uacb0\uc740 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4",
"not_axis_device": "\ubc1c\uacac\ub41c \uae30\uae30\ub294 Axis \uae30\uae30\uac00 \uc544\ub2d9\ub2c8\ub2e4",
"updated_configuration": "\uc0c8\ub85c\uc6b4 \ud638\uc2a4\ud2b8 \uc8fc\uc18c\ub85c \uc5c5\ub370\uc774\ud2b8\ub41c \uae30\uae30 \uad6c\uc131"

View File

@ -4,7 +4,8 @@
"already_configured": "Apparaat is al geconfigureerd",
"bad_config_file": "Slechte gegevens van het configuratiebestand",
"link_local_address": "Link-lokale adressen worden niet ondersteund",
"not_axis_device": "Ontdekte apparaat, is geen Axis-apparaat"
"not_axis_device": "Ontdekte apparaat, is geen Axis-apparaat",
"updated_configuration": "Bijgewerkte apparaatconfiguratie met nieuw hostadres"
},
"error": {
"already_configured": "Apparaat is al geconfigureerd",

View File

@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "Enheten er allerede konfigurert",
"bad_config_file": "D\u00e5rlig data fra konfigurasjonsfilen",
"bad_config_file": "D\u00e5rlige data fra konfigurasjonsfilen",
"link_local_address": "Linking av lokale adresser st\u00f8ttes ikke",
"not_axis_device": "Oppdaget enhet ikke en Axis enhet",
"updated_configuration": "Oppdatert enhetskonfigurasjonen med ny vertsadresse"

View File

@ -1,15 +1,15 @@
{
"config": {
"abort": {
"already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
"already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.",
"bad_config_file": "B\u0142\u0119dne dane z pliku konfiguracyjnego",
"link_local_address": "Po\u0142\u0105czenie lokalnego adresu nie jest obs\u0142ugiwane",
"not_axis_device": "Wykryte urz\u0105dzenie nie jest urz\u0105dzeniem Axis",
"updated_configuration": "Zaktualizowano konfiguracj\u0119 urz\u0105dzenia o nowy adres hosta"
},
"error": {
"already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
"already_in_progress": "Konfigurowanie urz\u0105dzenia jest ju\u017c w toku.",
"already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.",
"already_in_progress": "Konfiguracja urz\u0105dzenia jest ju\u017c w toku.",
"device_unavailable": "Urz\u0105dzenie jest niedost\u0119pne",
"faulty_credentials": "B\u0142\u0119dne dane uwierzytelniaj\u0105ce"
},

View File

@ -2,9 +2,10 @@
"config": {
"abort": {
"already_configured": "Enheten \u00e4r redan konfigurerad",
"bad_config_file": "Felaktig data fr\u00e5n config fil",
"bad_config_file": "Felaktig data fr\u00e5n konfigurationsfilen",
"link_local_address": "Link local addresses are not supported",
"not_axis_device": "Uppt\u00e4ckte enhet som inte \u00e4r en Axis enhet"
"not_axis_device": "Uppt\u00e4ckte enhet som inte \u00e4r en Axis enhet",
"updated_configuration": "Uppdaterad enhetskonfiguration med ny v\u00e4rdadress"
},
"error": {
"already_configured": "Enheten \u00e4r redan konfigurerad",
@ -12,6 +13,7 @@
"device_unavailable": "Enheten \u00e4r inte tillg\u00e4nglig",
"faulty_credentials": "Felaktiga anv\u00e4ndaruppgifter"
},
"flow_title": "Axisenhet: {name} ({host})",
"step": {
"user": {
"data": {

View File

@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
"bad_config_file": "\u8a2d\u5b9a\u6a94\u6848\u8cc7\u6599\u7121\u6548",
"bad_config_file": "\u8a2d\u5b9a\u6a94\u6848\u8cc7\u6599\u7121\u6548\u932f\u8aa4",
"link_local_address": "\u4e0d\u652f\u63f4\u9023\u7d50\u672c\u5730\u7aef\u4f4d\u5740",
"not_axis_device": "\u6240\u767c\u73fe\u7684\u8a2d\u5099\u4e26\u975e Axis \u8a2d\u5099",
"updated_configuration": "\u4f7f\u7528\u65b0\u4e3b\u6a5f\u7aef\u4f4d\u5740\u66f4\u65b0\u88dd\u7f6e\u8a2d\u5b9a"

View File

@ -1,15 +1,23 @@
"""Support for Axis devices."""
import logging
from homeassistant.const import (
CONF_DEVICE,
CONF_HOST,
CONF_MAC,
CONF_PASSWORD,
CONF_PORT,
CONF_TRIGGER_TIME,
CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP,
)
from .const import CONF_CAMERA, CONF_EVENTS, DEFAULT_TRIGGER_TIME, DOMAIN
from .device import AxisNetworkDevice, get_device
LOGGER = logging.getLogger(__name__)
async def async_setup(hass, config):
"""Old way to set up Axis devices."""
@ -35,7 +43,7 @@ async def async_setup_entry(hass, config_entry):
config_entry, unique_id=device.api.vapix.params.system_serialnumber
)
hass.data[DOMAIN][device.serial] = device
hass.data[DOMAIN][config_entry.unique_id] = device
await device.async_update_device_registry()
@ -52,7 +60,13 @@ async def async_unload_entry(hass, config_entry):
async def async_populate_options(hass, config_entry):
"""Populate default options for device."""
device = await get_device(hass, config_entry.data[CONF_DEVICE])
device = await get_device(
hass,
host=config_entry.data[CONF_HOST],
port=config_entry.data[CONF_PORT],
username=config_entry.data[CONF_USERNAME],
password=config_entry.data[CONF_PASSWORD],
)
supported_formats = device.vapix.params.image_format
camera = bool(supported_formats)
@ -64,3 +78,18 @@ async def async_populate_options(hass, config_entry):
}
hass.config_entries.async_update_entry(config_entry, options=options)
async def async_migrate_entry(hass, config_entry):
"""Migrate old entry."""
LOGGER.debug("Migrating from version %s", config_entry.version)
# Flatten configuration but keep old data if user rollbacks HASS
if config_entry.version == 1:
config_entry.data = {**config_entry.data, **config_entry.data[CONF_DEVICE]}
config_entry.version = 2
LOGGER.info("Migration to version %s successful", config_entry.version)
return True

View File

@ -9,7 +9,6 @@ from homeassistant.components.mjpeg.camera import (
)
from homeassistant.const import (
CONF_AUTHENTICATION,
CONF_DEVICE,
CONF_HOST,
CONF_NAME,
CONF_PASSWORD,
@ -35,15 +34,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
config = {
CONF_NAME: config_entry.data[CONF_NAME],
CONF_USERNAME: config_entry.data[CONF_DEVICE][CONF_USERNAME],
CONF_PASSWORD: config_entry.data[CONF_DEVICE][CONF_PASSWORD],
CONF_USERNAME: config_entry.data[CONF_USERNAME],
CONF_PASSWORD: config_entry.data[CONF_PASSWORD],
CONF_MJPEG_URL: AXIS_VIDEO.format(
config_entry.data[CONF_DEVICE][CONF_HOST],
config_entry.data[CONF_DEVICE][CONF_PORT],
config_entry.data[CONF_HOST], config_entry.data[CONF_PORT],
),
CONF_STILL_IMAGE_URL: AXIS_IMAGE.format(
config_entry.data[CONF_DEVICE][CONF_HOST],
config_entry.data[CONF_DEVICE][CONF_PORT],
config_entry.data[CONF_HOST], config_entry.data[CONF_PORT],
),
CONF_AUTHENTICATION: HTTP_DIGEST_AUTHENTICATION,
}
@ -76,14 +73,14 @@ class AxisCamera(AxisEntityBase, MjpegCamera):
async def stream_source(self):
"""Return the stream source."""
return AXIS_STREAM.format(
self.device.config_entry.data[CONF_DEVICE][CONF_USERNAME],
self.device.config_entry.data[CONF_DEVICE][CONF_PASSWORD],
self.device.config_entry.data[CONF_USERNAME],
self.device.config_entry.data[CONF_PASSWORD],
self.device.host,
)
def _new_address(self):
"""Set new device address for video stream."""
port = self.device.config_entry.data[CONF_DEVICE][CONF_PORT]
port = self.device.config_entry.data[CONF_PORT]
self._mjpeg_url = AXIS_VIDEO.format(self.device.host, port)
self._still_image_url = AXIS_IMAGE.format(self.device.host, port)

View File

@ -4,7 +4,6 @@ import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import (
CONF_DEVICE,
CONF_HOST,
CONF_MAC,
CONF_NAME,
@ -33,16 +32,12 @@ DEFAULT_PORT = 80
class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a Axis config flow."""
VERSION = 1
VERSION = 2
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
def __init__(self):
"""Initialize the Axis config flow."""
self.device_config = {}
self.model = None
self.name = None
self.serial_number = None
self.discovery_schema = {}
self.import_schema = {}
@ -55,24 +50,32 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if user_input is not None:
try:
device = await get_device(
self.hass,
host=user_input[CONF_HOST],
port=user_input[CONF_PORT],
username=user_input[CONF_USERNAME],
password=user_input[CONF_PASSWORD],
)
serial_number = device.vapix.params.system_serialnumber
await self.async_set_unique_id(serial_number)
self._abort_if_unique_id_configured(
updates={
CONF_HOST: user_input[CONF_HOST],
CONF_PORT: user_input[CONF_PORT],
}
)
self.device_config = {
CONF_HOST: user_input[CONF_HOST],
CONF_PORT: user_input[CONF_PORT],
CONF_USERNAME: user_input[CONF_USERNAME],
CONF_PASSWORD: user_input[CONF_PASSWORD],
CONF_MAC: serial_number,
CONF_MODEL: device.vapix.params.prodnbr,
}
device = await get_device(self.hass, self.device_config)
self.serial_number = device.vapix.params.system_serialnumber
config_entry = await self.async_set_unique_id(self.serial_number)
if config_entry:
return self._update_entry(
config_entry,
host=user_input[CONF_HOST],
port=user_input[CONF_PORT],
)
self.model = device.vapix.params.prodnbr
return await self._create_entry()
@ -101,41 +104,23 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
Generate a name to be used as a prefix for device entities.
"""
model = self.device_config[CONF_MODEL]
same_model = [
entry.data[CONF_NAME]
for entry in self.hass.config_entries.async_entries(DOMAIN)
if entry.data[CONF_MODEL] == self.model
if entry.data[CONF_MODEL] == model
]
self.name = f"{self.model}"
name = model
for idx in range(len(same_model) + 1):
self.name = f"{self.model} {idx}"
if self.name not in same_model:
name = f"{model} {idx}"
if name not in same_model:
break
data = {
CONF_DEVICE: self.device_config,
CONF_NAME: self.name,
CONF_MAC: self.serial_number,
CONF_MODEL: self.model,
}
self.device_config[CONF_NAME] = name
title = f"{self.model} - {self.serial_number}"
return self.async_create_entry(title=title, data=data)
def _update_entry(self, entry, host, port):
"""Update existing entry."""
if (
entry.data[CONF_DEVICE][CONF_HOST] == host
and entry.data[CONF_DEVICE][CONF_PORT] == port
):
return self.async_abort(reason="already_configured")
entry.data[CONF_DEVICE][CONF_HOST] = host
entry.data[CONF_DEVICE][CONF_PORT] = port
self.hass.config_entries.async_update_entry(entry)
return self.async_abort(reason="updated_configuration")
title = f"{model} - {self.device_config[CONF_MAC]}"
return self.async_create_entry(title=title, data=self.device_config)
async def async_step_zeroconf(self, discovery_info):
"""Prepare configuration for a discovered Axis device."""
@ -147,18 +132,19 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if discovery_info[CONF_HOST].startswith("169.254"):
return self.async_abort(reason="link_local_address")
config_entry = await self.async_set_unique_id(serial_number)
if config_entry:
return self._update_entry(
config_entry,
host=discovery_info[CONF_HOST],
port=discovery_info[CONF_PORT],
await self.async_set_unique_id(serial_number)
self._abort_if_unique_id_configured(
updates={
CONF_HOST: discovery_info[CONF_HOST],
CONF_PORT: discovery_info[CONF_PORT],
}
)
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context["title_placeholders"] = {
"name": discovery_info["hostname"][:-7],
"host": discovery_info[CONF_HOST],
CONF_NAME: discovery_info["hostname"][:-7],
CONF_HOST: discovery_info[CONF_HOST],
}
self.discovery_schema = {

View File

@ -7,9 +7,7 @@ import axis
from axis.streammanager import SIGNAL_PLAYING
from homeassistant.const import (
CONF_DEVICE,
CONF_HOST,
CONF_MAC,
CONF_NAME,
CONF_PASSWORD,
CONF_PORT,
@ -42,7 +40,7 @@ class AxisNetworkDevice:
@property
def host(self):
"""Return the host of this device."""
return self.config_entry.data[CONF_DEVICE][CONF_HOST]
return self.config_entry.data[CONF_HOST]
@property
def model(self):
@ -75,7 +73,13 @@ class AxisNetworkDevice:
async def async_setup(self):
"""Set up the device."""
try:
self.api = await get_device(self.hass, self.config_entry.data[CONF_DEVICE])
self.api = await get_device(
self.hass,
host=self.config_entry.data[CONF_HOST],
port=self.config_entry.data[CONF_PORT],
username=self.config_entry.data[CONF_USERNAME],
password=self.config_entry.data[CONF_PASSWORD],
)
except CannotConnect:
raise ConfigEntryNotReady
@ -126,7 +130,7 @@ class AxisNetworkDevice:
This is a static method because a class method (bound method),
can not be used with weak references.
"""
device = hass.data[DOMAIN][entry.data[CONF_MAC]]
device = hass.data[DOMAIN][entry.unique_id]
device.api.config.host = device.host
async_dispatcher_send(hass, device.event_new_address)
@ -197,15 +201,15 @@ class AxisNetworkDevice:
return True
async def get_device(hass, config):
async def get_device(hass, host, port, username, password):
"""Create a Axis device."""
device = axis.AxisDevice(
loop=hass.loop,
host=config[CONF_HOST],
username=config[CONF_USERNAME],
password=config[CONF_PASSWORD],
port=config[CONF_PORT],
host=host,
port=port,
username=username,
password=password,
web_proto="http",
)
@ -224,13 +228,11 @@ async def get_device(hass, config):
return device
except axis.Unauthorized:
LOGGER.warning(
"Connected to device at %s but not registered.", config[CONF_HOST]
)
LOGGER.warning("Connected to device at %s but not registered.", host)
raise AuthenticationRequired
except (asyncio.TimeoutError, axis.RequestError):
LOGGER.error("Error connecting to the Axis device at %s", config[CONF_HOST])
LOGGER.error("Error connecting to the Axis device at %s", host)
raise CannotConnect
except axis.AxisException:

View File

@ -21,10 +21,9 @@
},
"abort": {
"already_configured": "Device is already configured",
"bad_config_file": "Bad data from config file",
"bad_config_file": "Bad data from configuration file",
"link_local_address": "Link local addresses are not supported",
"not_axis_device": "Discovered device not an Axis device",
"updated_configuration": "Updated device configuration with new host address"
"not_axis_device": "Discovered device not an Axis device"
}
}
}

View File

@ -11,6 +11,7 @@ from homeassistant.const import (
ATTR_ATTRIBUTION,
CONF_MONITORED_VARIABLES,
CONF_NAME,
DATA_RATE_MEGABITS_PER_SECOND,
DEVICE_CLASS_TIMESTAMP,
)
import homeassistant.helpers.config_validation as cv
@ -20,8 +21,6 @@ from homeassistant.util.dt import utcnow
_LOGGER = logging.getLogger(__name__)
BANDWIDTH_MEGABITS_SECONDS = "Mb/s"
ATTRIBUTION = "Powered by Bouygues Telecom"
DEFAULT_NAME = "Bbox"
@ -32,22 +31,22 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
SENSOR_TYPES = {
"down_max_bandwidth": [
"Maximum Download Bandwidth",
BANDWIDTH_MEGABITS_SECONDS,
DATA_RATE_MEGABITS_PER_SECOND,
"mdi:download",
],
"up_max_bandwidth": [
"Maximum Upload Bandwidth",
BANDWIDTH_MEGABITS_SECONDS,
DATA_RATE_MEGABITS_PER_SECOND,
"mdi:upload",
],
"current_down_bandwidth": [
"Currently Used Download Bandwidth",
BANDWIDTH_MEGABITS_SECONDS,
DATA_RATE_MEGABITS_PER_SECOND,
"mdi:download",
],
"current_up_bandwidth": [
"Currently Used Upload Bandwidth",
BANDWIDTH_MEGABITS_SECONDS,
DATA_RATE_MEGABITS_PER_SECOND,
"mdi:upload",
],
"uptime": ["Uptime", None, "mdi:clock"],

View File

@ -28,6 +28,12 @@
"is_not_occupied": "{entity_name} no est\u00e1 ocupado",
"is_not_open": "{entity_name} est\u00e1 cerrado",
"is_not_plugged_in": "{entity_name} est\u00e1 desconectado",
"is_not_unsafe": "{entity_name} es seguro",
"is_occupied": "{entity_name} est\u00e1 ocupado",
"is_off": "{entity_name} est\u00e1 apagado",
"is_on": "{entity_name} est\u00e1 encendido",
"is_open": "{entity_name} est\u00e1 abierto",
"is_plugged_in": "{entity_name} est\u00e1 enchufado",
"is_powered": "{entity_name} est\u00e1 encendido",
"is_present": "{entity_name} est\u00e1 presente",
"is_problem": "{entity_name} est\u00e1 detectando un problema",
@ -45,6 +51,7 @@
"hot": "{entity_name} se calent\u00f3",
"light": "{entity_name} comenz\u00f3 a detectar luz",
"locked": "{entity_name} bloqueado",
"moist": "{entity_name} se humedeci\u00f3",
"moist\u00a7": "{entity_name} se humedeci\u00f3",
"motion": "{entity_name} comenz\u00f3 a detectar movimiento",
"moving": "{entity_name} comenz\u00f3 a moverse",
@ -59,7 +66,22 @@
"not_cold": "{entity_name} no se enfri\u00f3",
"not_connected": "{entity_name} desconectado",
"not_hot": "{entity_name} no se calent\u00f3",
"not_locked": "{entity_name} desbloqueado"
"not_locked": "{entity_name} desbloqueado",
"not_moist": "{entity_name} se sec\u00f3",
"not_moving": "{entity_name} dej\u00f3 de moverse",
"not_opened": "{entity_name} cerrado",
"not_plugged_in": "{entity_name} desconectado",
"not_present": "{entity_name} no presente",
"not_unsafe": "{entity_name} se volvi\u00f3 seguro",
"occupied": "{entity_name} se ocup\u00f3",
"opened": "{entity_name} abierto",
"plugged_in": "{entity_name} enchufado",
"present": "{entity_name} presente",
"problem": "{entity_name} comenz\u00f3 a detectar problemas",
"smoke": "{entity_name} comenz\u00f3 a detectar humo",
"sound": "{entity_name} comenz\u00f3 a detectar sonido",
"turned_off": "{entity_name} apagado",
"turned_on": "{entity_name} encendido"
}
}
}

View File

@ -0,0 +1,94 @@
{
"device_automation": {
"condition_type": {
"is_bat_low": "{entity_name}-batteriet \u00e4r l\u00e5gt",
"is_cold": "{entity_name} \u00e4r kall",
"is_connected": "{entity_name} \u00e4r ansluten",
"is_gas": "{entity_name} detekterar gas",
"is_hot": "{entity_name} \u00e4r varm",
"is_light": "{entity_name} uppt\u00e4cker ljus",
"is_locked": "{entity_name} \u00e4r l\u00e5st",
"is_moist": "{entity_name} \u00e4r fuktig",
"is_motion": "{entity_name} detekterar r\u00f6relse",
"is_moving": "{entity_name} r\u00f6r sig",
"is_no_gas": "{entity_name} uppt\u00e4cker inte gas",
"is_no_light": "{entity_name} uppt\u00e4cker inte ljus",
"is_no_motion": "{entity_name} detekterar inte r\u00f6relse",
"is_no_problem": "{entity_name} uppt\u00e4cker inte problem",
"is_no_smoke": "{entity_name} detekterar inte r\u00f6k",
"is_no_sound": "{entity_name} uppt\u00e4cker inte ljud",
"is_no_vibration": "{entity_name} uppt\u00e4cker inte vibrationer",
"is_not_bat_low": "{entity_name} batteri \u00e4r normalt",
"is_not_cold": "{entity_name} \u00e4r inte kall",
"is_not_connected": "{entity_name} \u00e4r fr\u00e5nkopplad",
"is_not_hot": "{entity_name} \u00e4r inte varm",
"is_not_locked": "{entity_name} \u00e4r ol\u00e5st",
"is_not_moist": "{entity_name} \u00e4r torr",
"is_not_moving": "{entity_name} r\u00f6r sig inte",
"is_not_occupied": "{entity_name} \u00e4r inte upptagen",
"is_not_open": "{entity_name} \u00e4r st\u00e4ngd",
"is_not_plugged_in": "{entity_name} \u00e4r urkopplad",
"is_not_powered": "{entity_name} \u00e4r inte str\u00f6mf\u00f6rd",
"is_not_present": "{entity_name} finns inte",
"is_not_unsafe": "{entity_name} \u00e4r s\u00e4ker",
"is_occupied": "{entity_name} \u00e4r upptagen",
"is_off": "{entity_name} \u00e4r avst\u00e4ngd",
"is_on": "{entity_name} \u00e4r p\u00e5",
"is_open": "{entity_name} \u00e4r \u00f6ppen",
"is_plugged_in": "{entity_name} \u00e4r ansluten",
"is_powered": "{entity_name} \u00e4r str\u00f6mf\u00f6rd",
"is_present": "{entity_name} \u00e4r n\u00e4rvarande",
"is_problem": "{entity_name} uppt\u00e4cker problem",
"is_smoke": "{entity_name} detekterar r\u00f6k",
"is_sound": "{entity_name} uppt\u00e4cker ljud",
"is_unsafe": "{entity_name} \u00e4r os\u00e4ker",
"is_vibration": "{entity_name} uppt\u00e4cker vibrationer"
},
"trigger_type": {
"bat_low": "{entity_name} batteri l\u00e5gt",
"closed": "{entity_name} st\u00e4ngd",
"cold": "{entity_name} blev kall",
"connected": "{entity_name} ansluten",
"gas": "{entity_name} b\u00f6rjade detektera gas",
"hot": "{entity_name} blev varm",
"light": "{entity_name} b\u00f6rjade uppt\u00e4cka ljus",
"locked": "{entity_name} l\u00e5st",
"moist": "{entity_name} blev fuktig",
"moist\u00a7": "{entity_name} blev fuktig",
"motion": "{entity_name} b\u00f6rjade detektera r\u00f6relse",
"moving": "{entity_name} b\u00f6rjade r\u00f6ra sig",
"no_gas": "{entity_name} slutade uppt\u00e4cka gas",
"no_light": "{entity_name} slutade uppt\u00e4cka ljus",
"no_motion": "{entity_name} slutade uppt\u00e4cka r\u00f6relse",
"no_problem": "{entity_name} slutade uppt\u00e4cka problem",
"no_smoke": "{entity_name} slutade detektera r\u00f6k",
"no_sound": "{entity_name} slutade uppt\u00e4cka ljud",
"no_vibration": "{entity_name} slutade uppt\u00e4cka vibrationer",
"not_bat_low": "{entity_name} batteri normalt",
"not_cold": "{entity_name} blev inte kall",
"not_connected": "{entity_name} fr\u00e5nkopplad",
"not_hot": "{entity_name} blev inte varm",
"not_locked": "{entity_name} ol\u00e5st",
"not_moist": "{entity_name} blev torr",
"not_moving": "{entity_name} slutade r\u00f6ra sig",
"not_occupied": "{entity_name} blev inte upptagen",
"not_opened": "{entity_name} st\u00e4ngd",
"not_plugged_in": "{entity_name} urkopplad",
"not_powered": "{entity_name} inte str\u00f6mf\u00f6rd",
"not_present": "{entity_name} inte n\u00e4rvarande",
"not_unsafe": "{entity_name} blev s\u00e4ker",
"occupied": "{entity_name} blev upptagen",
"opened": "{entity_name} \u00f6ppnades",
"plugged_in": "{entity_name} ansluten",
"powered": "{entity_name} str\u00f6mf\u00f6rd",
"present": "{entity_name} n\u00e4rvarande",
"problem": "{entity_name} b\u00f6rjade uppt\u00e4cka problem",
"smoke": "{entity_name} b\u00f6rjade detektera r\u00f6k",
"sound": "{entity_name} b\u00f6rjade uppt\u00e4cka ljud",
"turned_off": "{entity_name} st\u00e4ngdes av",
"turned_on": "{entity_name} slogs p\u00e5",
"unsafe": "{entity_name} blev os\u00e4ker",
"vibration": "{entity_name} b\u00f6rjade detektera vibrationer"
}
}
}

View File

@ -1,4 +1,4 @@
"""Implemenet device conditions for binary sensor."""
"""Implement device conditions for binary sensor."""
from typing import Dict, List
import voluptuous as vol

View File

@ -1,4 +1,4 @@
"""Bitcoin information service that uses blockchain.info."""
"""Bitcoin information service that uses blockchain.com."""
from datetime import timedelta
import logging
@ -12,7 +12,7 @@ from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
ATTRIBUTION = "Data provided by blockchain.info"
ATTRIBUTION = "Data provided by blockchain.com"
DEFAULT_CURRENCY = "USD"
@ -168,7 +168,7 @@ class BitcoinData:
self.ticker = None
def update(self):
"""Get the latest data from blockchain.info."""
"""Get the latest data from blockchain.com."""
self.stats = statistics.get()
self.ticker = exchangerates.get_ticker()

View File

@ -1,6 +1,6 @@
{
"domain": "blockchain",
"name": "Blockchain.info",
"name": "Blockchain.com",
"documentation": "https://www.home-assistant.io/integrations/blockchain",
"requirements": ["python-blockchain-api==0.0.2"],
"dependencies": [],

View File

@ -1,4 +1,4 @@
"""Support for Blockchain.info sensors."""
"""Support for Blockchain.com sensors."""
from datetime import timedelta
import logging
@ -12,7 +12,7 @@ from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
ATTRIBUTION = "Data provided by blockchain.info"
ATTRIBUTION = "Data provided by blockchain.com"
CONF_ADDRESSES = "addresses"
@ -31,7 +31,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Blockchain.info sensors."""
"""Set up the Blockchain.com sensors."""
addresses = config.get(CONF_ADDRESSES)
name = config.get(CONF_NAME)
@ -45,7 +45,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class BlockchainSensor(Entity):
"""Representation of a Blockchain.info sensor."""
"""Representation of a Blockchain.com sensor."""
def __init__(self, name, addresses):
"""Initialize the sensor."""

View File

@ -1,7 +1,7 @@
"""Support for BME680 Sensor over SMBus."""
import logging
import threading
from time import sleep, time
from time import monotonic, sleep
import bme680 # pylint: disable=import-error
from smbus import SMBus # pylint: disable=import-error
@ -240,15 +240,15 @@ class BME680Handler:
# Pause to allow initial data read for device validation.
sleep(1)
start_time = time()
curr_time = time()
start_time = monotonic()
curr_time = monotonic()
burn_in_data = []
_LOGGER.info(
"Beginning %d second gas sensor burn in for Air Quality", burn_in_time
)
while curr_time - start_time < burn_in_time:
curr_time = time()
curr_time = monotonic()
if self._sensor.get_sensor_data() and self._sensor.data.heat_stable:
gas_resistance = self._sensor.data.gas_resistance
burn_in_data.append(gas_resistance)

View File

@ -2,7 +2,7 @@
"domain": "bmw_connected_drive",
"name": "BMW Connected Drive",
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
"requirements": ["bimmer_connected==0.7.0"],
"requirements": ["bimmer_connected==0.7.1"],
"dependencies": [],
"codeowners": ["@gerard33"]
}

View File

@ -112,7 +112,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
if station is not None:
if zone_id and wmo_id:
_LOGGER.warning(
"Using config %s, not %s and %s for BOM sensor",
"Using configuration %s, not %s and %s for BOM sensor",
CONF_STATION,
CONF_ZONE_ID,
CONF_WMO_ID,
@ -281,7 +281,7 @@ def _get_bom_stations():
"""Return {CONF_STATION: (lat, lon)} for all stations, for auto-config.
This function does several MB of internet requests, so please use the
caching version to minimise latency and hit-count.
caching version to minimize latency and hit-count.
"""
latlon = {}
with io.BytesIO() as file_obj:

View File

@ -2,7 +2,7 @@
"domain": "braviatv",
"name": "Sony Bravia TV",
"documentation": "https://www.home-assistant.io/integrations/braviatv",
"requirements": ["braviarc-homeassistant==0.3.7.dev0", "getmac==0.8.1"],
"requirements": ["bravia-tv==1.0", "getmac==0.8.1"],
"dependencies": ["configurator"],
"codeowners": ["@robbiet480"]
}

View File

@ -2,7 +2,7 @@
import ipaddress
import logging
from braviarc.braviarc import BraviaRC
from bravia_tv import BraviaRC
from getmac import get_mac_address
import voluptuous as vol

View File

@ -75,18 +75,20 @@ def async_setup_service(hass, host, device):
async def _learn_command(call):
"""Learn a packet from remote."""
device = hass.data[DOMAIN][call.data[CONF_HOST]]
for retry in range(DEFAULT_RETRY):
try:
auth = await hass.async_add_executor_job(device.auth)
except socket.timeout:
_LOGGER.error("Failed to connect to device, timeout")
return
if not auth:
_LOGGER.error("Failed to connect to device")
return
await hass.async_add_executor_job(device.enter_learning)
break
except (socket.timeout, ValueError):
try:
await hass.async_add_executor_job(device.auth)
except socket.timeout:
if retry == DEFAULT_RETRY - 1:
_LOGGER.error("Failed to enter learning mode")
return
_LOGGER.info("Press the key you want Home Assistant to learn")
start_time = utcnow()

View File

@ -270,7 +270,7 @@ class BroadlinkRemote(RemoteDevice):
async def _async_learn_code(self, command, device, toggle, timeout):
"""Learn a code from a remote.
Capture an aditional code for toggle commands.
Capture an additional code for toggle commands.
"""
try:
if not toggle:

View File

@ -0,0 +1,5 @@
{
"config": {
"title": "Impresora Brother"
}
}

View File

@ -9,6 +9,7 @@
"snmp_error": "Serveur SNMP d\u00e9sactiv\u00e9 ou imprimante non prise en charge.",
"wrong_host": "Nom d'h\u00f4te ou adresse IP invalide."
},
"flow_title": "Imprimante Brother: {model} {serial_number}",
"step": {
"user": {
"data": {
@ -21,7 +22,9 @@
"zeroconf_confirm": {
"data": {
"type": "Type d'imprimante"
}
},
"description": "Voulez-vous ajouter l'imprimante Brother {model} avec le num\u00e9ro de s\u00e9rie `{serial_number}` \u00e0 Home Assistant ?",
"title": "Imprimante Brother d\u00e9couverte"
}
},
"title": "Imprimante Brother"

View File

@ -1,7 +1,24 @@
{
"config": {
"abort": {
"already_configured": "Ez a nyomtat\u00f3 m\u00e1r konfigur\u00e1lva van.",
"unsupported_model": "Ez a nyomtat\u00f3modell nem t\u00e1mogatott."
},
"error": {
"connection_error": "Csatlakoz\u00e1si hiba.",
"snmp_error": "Az SNMP szerver ki van kapcsolva, vagy a nyomtat\u00f3 nem t\u00e1mogatott.",
"wrong_host": "\u00c9rv\u00e9nytelen \u00e1llom\u00e1sn\u00e9v vagy IP-c\u00edm."
},
"flow_title": "Brother nyomtat\u00f3: {model} {serial_number}",
"step": {
"user": {
"data": {
"host": "Nyomtat\u00f3 \u00e1llom\u00e1sneve vagy IP-c\u00edme",
"type": "A nyomtat\u00f3 t\u00edpusa"
},
"description": "A Brother nyomtat\u00f3 integr\u00e1ci\u00f3j\u00e1nak be\u00e1ll\u00edt\u00e1sa. Ha probl\u00e9m\u00e1id vannak a konfigur\u00e1ci\u00f3val, l\u00e1togass el a k\u00f6vetkez\u0151 oldalra: https://www.home-assistant.io/integrations/brother",
"title": "Brother nyomtat\u00f3"
},
"zeroconf_confirm": {
"data": {
"type": "A nyomtat\u00f3 t\u00edpusa"

View File

@ -1,18 +1,32 @@
{
"config": {
"abort": {
"already_configured": "Deze printer is al geconfigureerd.",
"unsupported_model": "Dit printermodel wordt niet ondersteund."
},
"error": {
"connection_error": "Verbindingsfout.",
"snmp_error": "SNMP-server uitgeschakeld of printer wordt niet ondersteund.",
"wrong_host": "Ongeldige hostnaam of IP-adres."
},
"flow_title": "Brother Printer: {model} {serial_number}",
"step": {
"user": {
"data": {
"host": "Printerhostnaam of IP-adres"
}
}
}
"host": "Printerhostnaam of IP-adres",
"type": "Type printer"
},
"description": "Zet Brother printerintegratie op. Als u problemen heeft met de configuratie ga dan naar: https://www.home-assistant.io/integrations/brother",
"title": "Brother Printer"
},
"zeroconf_confirm": {
"data": {
"type": "Type printer"
},
"description": "Wilt u het Brother Printer {model} met serienummer {serial_number}' toevoegen aan Home Assistant?",
"title": "Ontdekte Brother Printer"
}
},
"title": "Brother Printer"
}
}

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