mirror of
https://github.com/home-assistant/core.git
synced 2025-07-27 15:17:35 +00:00
commit
2d68b37dd5
25
.coveragerc
25
.coveragerc
@ -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
12
.github/stale.yml
vendored
@ -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.
|
||||
|
@ -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$
|
@ -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$
|
||||
|
15
CODEOWNERS
15
CODEOWNERS
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -13,4 +13,7 @@ coverage:
|
||||
url: "secret:TgWDUM4Jw0w7wMJxuxNF/yhSOHglIo1fGwInJnRLEVPy2P2aLimkoK1mtKCowH5TFw+baUXVXT3eAqefbdvIuM8BjRR4aRji95C6CYyD0QHy4N8i7nn1SQkWDPpS8IthYTg07rUDF7s5guurkKv2RrgoCdnnqjAMSzHoExMOF7xUmblMdhBTWJgBpWEhASJy85w/xxjlsE1xoTkzeJu9Q67pTXtRcn+5kb5/vIzPSYg="
|
||||
comment:
|
||||
require_changes: yes
|
||||
branches: master
|
||||
layout: reach
|
||||
branches:
|
||||
- master
|
||||
- !dev
|
@ -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:
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Plugable auth modules for Home Assistant."""
|
||||
"""Pluggable auth modules for Home Assistant."""
|
||||
import importlib
|
||||
import logging
|
||||
import types
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
22
homeassistant/components/abode/.translations/es-419.json
Normal file
22
homeassistant/components/abode/.translations/es-419.json
Normal 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"
|
||||
}
|
||||
}
|
22
homeassistant/components/abode/.translations/hu.json
Normal file
22
homeassistant/components/abode/.translations/hu.json
Normal 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"
|
||||
}
|
||||
}
|
@ -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": {
|
||||
|
22
homeassistant/components/abode/.translations/sv.json
Normal file
22
homeassistant/components/abode/.translations/sv.json
Normal 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"
|
||||
}
|
||||
}
|
@ -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):
|
||||
|
@ -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."
|
||||
},
|
||||
|
@ -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."
|
||||
},
|
||||
|
22
homeassistant/components/airly/.translations/es-419.json
Normal file
22
homeassistant/components/airly/.translations/es-419.json
Normal 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"
|
||||
}
|
||||
}
|
25
homeassistant/components/airly/.translations/hu.json
Normal file
25
homeassistant/components/airly/.translations/hu.json
Normal 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"
|
||||
}
|
||||
}
|
@ -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.",
|
||||
|
@ -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",
|
||||
|
25
homeassistant/components/airly/.translations/sv.json
Normal file
25
homeassistant/components/airly/.translations/sv.json
Normal 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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
||||
|
@ -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 = (
|
||||
|
@ -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:
|
||||
|
@ -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("#", ".")
|
||||
|
19
homeassistant/components/almond/.translations/hu.json
Normal file
19
homeassistant/components/almond/.translations/hu.json
Normal 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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"]
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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",
|
||||
|
@ -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):
|
||||
|
@ -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"
|
||||
|
@ -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]:
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Suppoort for Amcrest IP camera binary sensors."""
|
||||
"""Support for Amcrest IP camera binary sensors."""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
|
@ -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."""
|
||||
|
@ -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"
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Suppoort for Amcrest IP camera sensors."""
|
||||
"""Support for Amcrest IP camera sensors."""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"domain": "apcupsd",
|
||||
"name": "APCUPSd",
|
||||
"name": "apcupsd",
|
||||
"documentation": "https://www.home-assistant.io/integrations/apcupsd",
|
||||
"requirements": ["apcaccess==0.0.13"],
|
||||
"dependencies": [],
|
||||
|
@ -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.
|
||||
"""
|
||||
|
@ -4,5 +4,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
|
||||
"requirements": ["pyatv==0.3.13"],
|
||||
"dependencies": ["configurator"],
|
||||
"after_dependencies": ["discovery"],
|
||||
"codeowners": []
|
||||
}
|
||||
|
@ -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"]
|
||||
}
|
||||
|
5
homeassistant/components/arcam_fmj/.translations/sv.json
Normal file
5
homeassistant/components/arcam_fmj/.translations/sv.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"config": {
|
||||
"title": "Arcam FMJ"
|
||||
}
|
||||
}
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"domain": "asuswrt",
|
||||
"name": "Asuswrt",
|
||||
"name": "ASUSWRT",
|
||||
"documentation": "https://www.home-assistant.io/integrations/asuswrt",
|
||||
"requirements": ["aioasuswrt==1.1.22"],
|
||||
"dependencies": [],
|
||||
|
@ -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):
|
||||
|
31
homeassistant/components/august/.translations/de.json
Normal file
31
homeassistant/components/august/.translations/de.json
Normal 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"
|
||||
}
|
||||
}
|
32
homeassistant/components/august/.translations/en.json
Normal file
32
homeassistant/components/august/.translations/en.json
Normal 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"
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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(),
|
||||
)
|
||||
|
@ -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."""
|
||||
|
@ -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):
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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"
|
||||
|
@ -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": {
|
||||
|
@ -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"
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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": {
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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 = {
|
||||
|
@ -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:
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"],
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
94
homeassistant/components/binary_sensor/.translations/sv.json
Normal file
94
homeassistant/components/binary_sensor/.translations/sv.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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": [],
|
||||
|
@ -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."""
|
||||
|
@ -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)
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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:
|
||||
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"config": {
|
||||
"title": "Impresora Brother"
|
||||
}
|
||||
}
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user