Merge pull request #32211 from home-assistant/rc

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

View File

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

12
.github/stale.yml vendored
View File

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

View File

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

View File

@ -1,10 +1,3 @@
# This configuration includes the default, minimal set of hooks to be
# run on all commits. It requires no specific setup and one can just
# start using pre-commit with it.
#
# See .pre-commit-config-all.yaml for a more complete one that comes
# with a better coverage at the cost of some specific setup needed.
repos: repos:
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 19.10b0 rev: 19.10b0
@ -14,6 +7,15 @@ repos:
- --safe - --safe
- --quiet - --quiet
files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$ 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 - repo: https://gitlab.com/pycqa/flake8
rev: 3.7.9 rev: 3.7.9
hooks: hooks:
@ -39,3 +41,16 @@ repos:
rev: v2.4.0 rev: v2.4.0
hooks: hooks:
- id: check-json - id: check-json
- repo: local
hooks:
# Run mypy through our wrapper script in order to get the possible
# pyenv and/or virtualenv activated; it may not have been e.g. if
# committing from a GUI tool that was not launched from an activated
# shell.
- id: mypy
name: mypy
entry: script/run-in-env.sh mypy
language: script
types: [python]
require_serial: true
files: ^homeassistant/.+\.py$

View File

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

View File

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

View File

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

View File

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

View File

@ -301,7 +301,7 @@ class AuthManager:
async def async_deactivate_user(self, user: models.User) -> None: async def async_deactivate_user(self, user: models.User) -> None:
"""Deactivate a user.""" """Deactivate a user."""
if user.is_owner: 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) await self._store.async_deactivate_user(user)
async def async_remove_credentials(self, credentials: models.Credentials) -> None: async def async_remove_credentials(self, credentials: models.Credentials) -> None:

View File

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

View File

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

View File

@ -31,22 +31,28 @@ class User:
"""A user.""" """A user."""
name = attr.ib(type=Optional[str]) 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) id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex)
is_owner = attr.ib(type=bool, default=False) is_owner = attr.ib(type=bool, default=False)
is_active = attr.ib(type=bool, default=False) is_active = attr.ib(type=bool, default=False)
system_generated = 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. # 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. # 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( _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 @property

View File

@ -1,23 +1,26 @@
"""Provide methods to bootstrap a Home Assistant instance.""" """Provide methods to bootstrap a Home Assistant instance."""
import asyncio import asyncio
import contextlib
import logging import logging
import logging.handlers import logging.handlers
import os import os
import sys import sys
from time import time from time import monotonic
from typing import Any, Dict, Optional, Set from typing import Any, Dict, Optional, Set
from async_timeout import timeout
import voluptuous as vol import voluptuous as vol
from homeassistant import config as conf_util, config_entries, core, loader from homeassistant import config as conf_util, config_entries, core, loader
from homeassistant.components import http from homeassistant.components import http
from homeassistant.const import ( from homeassistant.const import (
EVENT_HOMEASSISTANT_CLOSE, EVENT_HOMEASSISTANT_CLOSE,
EVENT_HOMEASSISTANT_STOP,
REQUIRED_NEXT_PYTHON_DATE, REQUIRED_NEXT_PYTHON_DATE,
REQUIRED_NEXT_PYTHON_VER, REQUIRED_NEXT_PYTHON_VER,
) )
from homeassistant.exceptions import HomeAssistantError 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.logging import AsyncHandler
from homeassistant.util.package import async_get_user_site, is_virtual_env from homeassistant.util.package import async_get_user_site, is_virtual_env
from homeassistant.util.yaml import clear_secret_cache from homeassistant.util.yaml import clear_secret_cache
@ -71,6 +74,7 @@ async def async_setup_hass(
_LOGGER.info("Config directory: %s", config_dir) _LOGGER.info("Config directory: %s", config_dir)
config_dict = None config_dict = None
basic_setup_success = False
if not safe_mode: if not safe_mode:
await hass.async_add_executor_job(conf_util.process_ha_config_upgrade, hass) 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) config_dict = await conf_util.async_hass_config_yaml(hass)
except HomeAssistantError as err: except HomeAssistantError as err:
_LOGGER.error( _LOGGER.error(
"Failed to parse configuration.yaml: %s. Falling back to safe mode", "Failed to parse configuration.yaml: %s. Activating safe mode", err,
err,
) )
else: else:
if not is_virtual_env(): if not is_virtual_env():
await async_mount_local_lib_path(config_dir) 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: finally:
clear_secret_cache() 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") _LOGGER.info("Starting in safe mode")
hass.config.safe_mode = True
http_conf = (await http.async_get_last_config(hass)) or {} 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. Dynamically loads required components and its dependencies.
This method is a coroutine. 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, {}) core_config = config.get(core.DOMAIN, {})
@ -126,12 +175,9 @@ async def async_from_config_dict(
) )
return None return None
hass.config_entries = config_entries.ConfigEntries(hass, config)
await hass.config_entries.async_initialize()
await _async_set_up_integrations(hass, config) await _async_set_up_integrations(hass, config)
stop = time() stop = monotonic()
_LOGGER.info("Home Assistant initialized in %.2fs", stop - start) _LOGGER.info("Home Assistant initialized in %.2fs", stop - start)
if REQUIRED_NEXT_PYTHON_DATE and sys.version_info[:3] < REQUIRED_NEXT_PYTHON_VER: if REQUIRED_NEXT_PYTHON_DATE and sys.version_info[:3] < REQUIRED_NEXT_PYTHON_VER:
@ -193,7 +239,7 @@ def async_enable_logging(
pass pass
# If the above initialization failed for any reason, setup the default # 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) logging.basicConfig(format=fmt, datefmt=datefmt, level=logging.INFO)
# Suppress overly verbose logs from libraries that aren't helpful # 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) domains = set(key.split(" ")[0] for key in config.keys() if key != core.DOMAIN)
# Add config entry domains # Add config entry domains
if "safe_mode" not in config: if not hass.config.safe_mode:
domains.update(hass.config_entries.async_domains()) domains.update(hass.config_entries.async_domains())
# Make sure the Hass.io component is loaded # Make sure the Hass.io component is loaded
@ -296,25 +342,6 @@ async def _async_set_up_integrations(
return_exceptions=True, 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 # Finish resolving domains
for dep_domains in await resolved_domains_task: for dep_domains in await resolved_domains_task:
# Result is either a set or an exception. We ignore exceptions # Result is either a set or an exception. We ignore exceptions

View File

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

View File

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

View File

@ -5,7 +5,7 @@
}, },
"error": { "error": {
"connection_error": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z Abode.", "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" "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce"
}, },
"step": { "step": {

View File

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

View File

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

View File

@ -1,6 +1,8 @@
{ {
"config": { "config": {
"abort": { "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.", "existing_instance_updated": "Se actualiz\u00f3 la configuraci\u00f3n existente.",
"single_instance_allowed": "Solo se permite una \u00fanica configuraci\u00f3n de AdGuard Home." "single_instance_allowed": "Solo se permite una \u00fanica configuraci\u00f3n de AdGuard Home."
}, },

View File

@ -1,6 +1,8 @@
{ {
"config": { "config": {
"abort": { "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.", "existing_instance_updated": "Uppdaterade existerande konfiguration.",
"single_instance_allowed": "Endast en enda konfiguration av AdGuard Home \u00e4r till\u00e5ten." "single_instance_allowed": "Endast en enda konfiguration av AdGuard Home \u00e4r till\u00e5ten."
}, },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -138,7 +138,7 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
def _restore_callback(self, zone): def _restore_callback(self, zone):
"""Update the zone's state, if needed.""" """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._state = 0
self.schedule_update_ha_state() self.schedule_update_ha_state()

View File

@ -1,5 +1,6 @@
"""Alexa capabilities.""" """Alexa capabilities."""
import logging import logging
import math
from homeassistant.components import ( from homeassistant.components import (
cover, cover,
@ -645,6 +646,43 @@ class AlexaSpeaker(AlexaCapability):
"""Return the Alexa API name of this interface.""" """Return the Alexa API name of this interface."""
return "Alexa.Speaker" 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): class AlexaStepSpeaker(AlexaCapability):
"""Implements Alexa.StepSpeaker. """Implements Alexa.StepSpeaker.
@ -711,6 +749,13 @@ class AlexaInputController(AlexaCapability):
source_list = self.entity.attributes.get( source_list = self.entity.attributes.get(
media_player.ATTR_INPUT_SOURCE_LIST, [] 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 = [] input_list = []
for source in source_list: for source in source_list:
formatted_source = ( formatted_source = (

View File

@ -508,12 +508,7 @@ class MediaPlayerCapabilities(AlexaEntity):
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & media_player.const.SUPPORT_VOLUME_SET: if supported & media_player.const.SUPPORT_VOLUME_SET:
yield AlexaSpeaker(self.entity) yield AlexaSpeaker(self.entity)
elif supported & media_player.const.SUPPORT_VOLUME_STEP:
step_volume_features = (
media_player.const.SUPPORT_VOLUME_MUTE
| media_player.const.SUPPORT_VOLUME_STEP
)
if supported & step_volume_features:
yield AlexaStepSpeaker(self.entity) yield AlexaStepSpeaker(self.entity)
playback_features = ( playback_features = (
@ -531,7 +526,13 @@ class MediaPlayerCapabilities(AlexaEntity):
yield AlexaSeekController(self.entity) yield AlexaSeekController(self.entity)
if supported & media_player.SUPPORT_SELECT_SOURCE: if supported & media_player.SUPPORT_SELECT_SOURCE:
yield AlexaInputController(self.entity) 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: if supported & media_player.const.SUPPORT_PLAY_MEDIA:
yield AlexaChannelController(self.entity) yield AlexaChannelController(self.entity)

View File

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

View File

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

View File

@ -6,6 +6,10 @@
"missing_configuration": "Raadpleeg de documentatie over het instellen van Almond." "missing_configuration": "Raadpleeg de documentatie over het instellen van Almond."
}, },
"step": { "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": { "pick_implementation": {
"title": "Kies de authenticatie methode" "title": "Kies de authenticatie methode"
} }

View File

@ -6,6 +6,10 @@
"missing_configuration": "Prosimo, preverite dokumentacijo o tem, kako nastaviti Almond." "missing_configuration": "Prosimo, preverite dokumentacijo o tem, kako nastaviti Almond."
}, },
"step": { "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": { "pick_implementation": {
"title": "Izberite na\u010din preverjanja pristnosti" "title": "Izberite na\u010din preverjanja pristnosti"
} }

View File

@ -1,9 +1,19 @@
{ {
"config": { "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": { "step": {
"hassio_confirm": { "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" "title": "Almond via Hass.io-till\u00e4gget"
},
"pick_implementation": {
"title": "V\u00e4lj autentiseringsmetod"
} }
} },
"title": "Almond"
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -378,7 +378,7 @@ class AmbientStation:
if data != self.stations[mac_address][ATTR_LAST_DATA]: if data != self.stations[mac_address][ATTR_LAST_DATA]:
_LOGGER.debug("New data received: %s", data) _LOGGER.debug("New data received: %s", data)
self.stations[mac_address][ATTR_LAST_DATA] = 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") _LOGGER.debug("Resetting watchdog")
self._watchdog_listener() self._watchdog_listener()
@ -518,7 +518,7 @@ class AmbientWeatherEntity(Entity):
self.async_schedule_update_ha_state(True) self.async_schedule_update_ha_state(True)
self._async_unsub_dispatcher_connect = async_dispatcher_connect( 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): async def async_will_remove_from_hass(self):

View File

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

View File

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

View File

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

View File

@ -1,11 +1,11 @@
"""Support for Amcrest IP cameras.""" """Support for Amcrest IP cameras."""
import asyncio import asyncio
from datetime import timedelta from datetime import timedelta
from functools import partial
import logging import logging
from amcrest import AmcrestError from amcrest import AmcrestError
from haffmpeg.camera import CameraMjpeg from haffmpeg.camera import CameraMjpeg
from urllib3.exceptions import HTTPError
import voluptuous as vol import voluptuous as vol
from homeassistant.components.camera import ( from homeassistant.components.camera import (
@ -26,9 +26,11 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import ( from .const import (
CAMERA_WEB_SESSION_TIMEOUT, CAMERA_WEB_SESSION_TIMEOUT,
CAMERAS, CAMERAS,
COMM_TIMEOUT,
DATA_AMCREST, DATA_AMCREST,
DEVICES, DEVICES,
SERVICE_UPDATE, SERVICE_UPDATE,
SNAPSHOT_TIMEOUT,
) )
from .helpers import log_update_error, service_signal 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) 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): class AmcrestCam(Camera):
"""An implementation of an Amcrest IP camera.""" """An implementation of an Amcrest IP camera."""
@ -112,28 +118,58 @@ class AmcrestCam(Camera):
self._motion_recording_enabled = None self._motion_recording_enabled = None
self._color_bw = None self._color_bw = None
self._rtsp_url = None self._rtsp_url = None
self._snapshot_lock = asyncio.Lock() self._snapshot_task = None
self._unsub_dispatcher = [] self._unsub_dispatcher = []
self._update_succeeded = False self._update_succeeded = False
async def async_camera_image(self): def _check_snapshot_ok(self):
"""Return a still image response from the camera."""
available = self.available available = self.available
if not available or not self.is_on: if not available or not self.is_on:
_LOGGER.warning( _LOGGER.warning(
"Attempt to take snaphot when %s camera is %s", "Attempt to take snapshot when %s camera is %s",
self.name, self.name,
"offline" if not available else "off", "offline" if not available else "off",
) )
raise CannotSnapshot
async def _async_get_image(self):
try:
# Send the request to snap a picture and return raw jpg data
# 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 return None
async with self._snapshot_lock:
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:
log_update_error(_LOGGER, "get image from", self.name, "camera", error)
return None
async def handle_async_mjpeg_stream(self, request): async def handle_async_mjpeg_stream(self, request):
"""Return an MJPEG stream.""" """Return an MJPEG stream."""

View File

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

View File

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

View File

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

View File

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

View File

@ -177,7 +177,7 @@ class ApnsNotificationService(BaseNotificationService):
def device_state_changed_listener(self, entity_id, from_s, to_s): 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. Track device state change if a device has a tracking id specified.
""" """

View File

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

View File

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

View File

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

View File

@ -110,19 +110,19 @@ class ArloBaseStation(AlarmControlPanel):
else: else:
self._state = None self._state = None
async def async_alarm_disarm(self, code=None): def alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""
self._base_station.mode = DISARMED 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.""" """Send arm away command. Uses custom mode."""
self._base_station.mode = self._away_mode_name 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.""" """Send arm home command. Uses custom mode."""
self._base_station.mode = self._home_mode_name 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.""" """Send arm night command. Uses custom mode."""
self._base_station.mode = self._night_mode_name self._base_station.mode = self._night_mode_name

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,10 @@
"""Support for August devices.""" """Support for August devices."""
import asyncio
from datetime import timedelta from datetime import timedelta
from functools import partial
import logging import logging
from august.api import Api from august.api import Api, AugustApiHTTPError
from august.authenticator import AuthenticationState, Authenticator, ValidationResult from august.authenticator import AuthenticationState, Authenticator, ValidationResult
from requests import RequestException, Session from requests import RequestException, Session
import voluptuous as vol import voluptuous as vol
@ -13,9 +15,10 @@ from homeassistant.const import (
CONF_USERNAME, CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_STOP,
) )
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle from homeassistant.util import Throttle, dt
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -42,9 +45,22 @@ DEFAULT_ENTITY_NAMESPACE = "august"
# avoid hitting rate limits # avoid hitting rate limits
MIN_TIME_BETWEEN_LOCK_DETAIL_UPDATES = timedelta(seconds=1800) 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) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10)
DEFAULT_SCAN_INTERVAL = timedelta(seconds=10)
LOGIN_METHODS = ["phone", "email"] LOGIN_METHODS = ["phone", "email"]
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
@ -65,7 +81,7 @@ CONFIG_SCHEMA = vol.Schema(
AUGUST_COMPONENTS = ["camera", "binary_sensor", "lock"] 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.""" """Request configuration steps from the user."""
configurator = hass.components.configurator configurator = hass.components.configurator
@ -79,7 +95,7 @@ def request_configuration(hass, config, api, authenticator):
_CONFIGURING[DOMAIN], "Invalid verification code" _CONFIGURING[DOMAIN], "Invalid verification code"
) )
elif result == ValidationResult.VALIDATED: elif result == ValidationResult.VALIDATED:
setup_august(hass, config, api, authenticator) setup_august(hass, config, api, authenticator, token_refresh_lock)
if DOMAIN not in _CONFIGURING: if DOMAIN not in _CONFIGURING:
authenticator.send_verification_code() 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.""" """Set up the August component."""
authentication = None authentication = None
@ -123,7 +139,9 @@ def setup_august(hass, config, api, authenticator):
if DOMAIN in _CONFIGURING: if DOMAIN in _CONFIGURING:
hass.components.configurator.request_done(_CONFIGURING.pop(DOMAIN)) 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: for component in AUGUST_COMPONENTS:
discovery.load_platform(hass, component, DOMAIN, {}, config) discovery.load_platform(hass, component, DOMAIN, {}, config)
@ -133,13 +151,13 @@ def setup_august(hass, config, api, authenticator):
_LOGGER.error("Invalid password provided") _LOGGER.error("Invalid password provided")
return False return False
if state == AuthenticationState.REQUIRES_VALIDATION: if state == AuthenticationState.REQUIRES_VALIDATION:
request_configuration(hass, config, api, authenticator) request_configuration(hass, config, api, authenticator, token_refresh_lock)
return True return True
return False return False
def setup(hass, config): async def async_setup(hass, config):
"""Set up the August component.""" """Set up the August component."""
conf = config[DOMAIN] conf = config[DOMAIN]
@ -171,20 +189,28 @@ def setup(hass, config):
_LOGGER.debug("August HTTP session closed.") _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") _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: class AugustData:
"""August data object.""" """August data object."""
def __init__(self, hass, api, access_token): def __init__(self, hass, api, authentication, authenticator, token_refresh_lock):
"""Init August data object.""" """Init August data object."""
self._hass = hass self._hass = hass
self._api = api 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._doorbells = self._api.get_doorbells(self._access_token) or []
self._locks = self._api.get_operable_locks(self._access_token) or [] self._locks = self._api.get_operable_locks(self._access_token) or []
self._house_ids = set() self._house_ids = set()
@ -192,11 +218,20 @@ class AugustData:
self._house_ids.add(device.house_id) self._house_ids.add(device.house_id)
self._doorbell_detail_by_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_status_by_id = {}
self._lock_detail_by_id = {} self._lock_detail_by_id = {}
self._door_state_by_id = {} self._door_state_by_id = {}
self._activities_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 @property
def house_ids(self): def house_ids(self):
"""Return a list of house_ids.""" """Return a list of house_ids."""
@ -212,24 +247,48 @@ class AugustData:
"""Return a list of locks.""" """Return a list of locks."""
return self._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.""" """Return a list of activities."""
_LOGGER.debug("Getting device activities") _LOGGER.debug("Getting device activities for %s", device_id)
self._update_device_activities() await self._async_update_device_activities()
activities = self._activities_by_id.get(device_id, []) activities = self._activities_by_id.get(device_id, [])
if activity_types: if activity_types:
return [a for a in activities if a.activity_type in activity_types] return [a for a in activities if a.activity_type in activity_types]
return activities 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.""" """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) return next(iter(activities or []), None)
@Throttle(MIN_TIME_BETWEEN_UPDATES) @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.""" """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") _LOGGER.debug("Start retrieving device activities")
for house_id in self.house_ids: for house_id in self.house_ids:
_LOGGER.debug("Updating device activity for house id %s", house_id) _LOGGER.debug("Updating device activity for house id %s", house_id)
@ -243,14 +302,18 @@ class AugustData:
self._activities_by_id[device_id] = [ self._activities_by_id[device_id] = [
a for a in activities if a.device_id == device_id a for a in activities if a.device_id == device_id
] ]
_LOGGER.debug("Completed retrieving device activities") _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.""" """Return doorbell detail."""
self._update_doorbells() await self._async_update_doorbells()
return self._doorbell_detail_by_id.get(doorbell_id) 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): def _update_doorbells(self):
detail_by_id = {} detail_by_id = {}
@ -275,38 +338,79 @@ class AugustData:
_LOGGER.debug("Completed retrieving doorbell details") _LOGGER.debug("Completed retrieving doorbell details")
self._doorbell_detail_by_id = detail_by_id 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. """Return status if the door is locked or unlocked.
This is status for the lock itself. This is status for the lock itself.
""" """
self._update_locks() await self._async_update_locks()
return self._lock_status_by_id.get(lock_id) 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.""" """Return lock detail."""
self._update_locks() await self._async_update_locks()
return self._lock_detail_by_id.get(lock_id) 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. """Return status if the door is open or closed.
This is the status from the door sensor. 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) return self._door_state_by_id.get(lock_id)
def _update_locks(self): async def _async_update_locks(self):
self._update_locks_status() await self._async_update_locks_status()
self._update_locks_detail() 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): def _update_locks_status(self):
status_by_id = {} status_by_id = {}
state_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") _LOGGER.debug("Start retrieving lock and door status")
for lock in self._locks: for lock in self._locks:
update_start_time_utc = dt.utcnow()
_LOGGER.debug("Updating lock and door status for %s", lock.device_name) _LOGGER.debug("Updating lock and door status for %s", lock.device_name)
try: try:
( (
@ -315,6 +419,13 @@ class AugustData:
) = self._api.get_lock_status( ) = self._api.get_lock_status(
self._access_token, lock.device_id, door_status=True 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: except RequestException as ex:
_LOGGER.error( _LOGGER.error(
"Request error trying to retrieve lock and door status for %s. %s", "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") _LOGGER.debug("Completed retrieving lock and door status")
self._lock_status_by_id = status_by_id self._lock_status_by_id = status_by_id
self._door_state_by_id = state_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) @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): def _update_locks_detail(self):
detail_by_id = {} detail_by_id = {}
@ -358,8 +494,60 @@ class AugustData:
def lock(self, device_id): def lock(self, device_id):
"""Lock the device.""" """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): def unlock(self, device_id):
"""Unlock the device.""" """Unlock the device."""
return self._api.unlock(self._access_token, device_id) return _call_api_operation_that_requires_bridge(
self.get_lock_name(device_id),
"unlock",
self._api.unlock,
self._access_token,
device_id,
)
def _filter_inoperative_locks(self):
# Remove non-operative locks as there must
# be a bridge (August Connect) for them to
# be usable
operative_locks = []
for lock in self._locks:
lock_detail = self._lock_detail_by_id.get(lock.device_id)
if lock_detail is None:
_LOGGER.info(
"The lock %s could not be setup because the system could not fetch details about the lock.",
lock.device_name,
)
elif lock_detail.bridge is None:
_LOGGER.info(
"The lock %s could not be setup because it does not have a bridge (Connect).",
lock.device_name,
)
elif not lock_detail.bridge.operative:
_LOGGER.info(
"The lock %s could not be setup because the bridge (Connect) is not operative.",
lock.device_name,
)
else:
operative_locks.append(lock)
self._locks = operative_locks
def _call_api_operation_that_requires_bridge(
device_name, operation_name, func, *args, **kwargs
):
"""Call an API that requires the bridge to be online."""
ret = None
try:
ret = func(*args, **kwargs)
except AugustApiHTTPError as err:
raise HomeAssistantError(device_name + ": " + str(err))
return ret

View File

@ -2,84 +2,92 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
import logging import logging
from august.activity import ActivityType from august.activity import ACTIVITY_ACTION_STATES, ActivityType
from august.lock import LockDoorStatus from august.lock import LockDoorStatus
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.util import dt
from . import DATA_AUGUST from . import DATA_AUGUST
_LOGGER = logging.getLogger(__name__) _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.""" """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.""" """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: if detail is None:
return None return None
return detail.is_online 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] 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.""" """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: if latest is not None:
start = latest.activity_start_time 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 start <= datetime.now() <= end
return None return None
# Sensor types: Name, device_class, state_provider SENSOR_NAME = 0
SENSOR_TYPES_DOOR = {"door_open": ["Open", "door", _retrieve_door_state]} 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 = { SENSOR_TYPES_DOORBELL = {
"doorbell_ding": ["Ding", "occupancy", _retrieve_ding_state], "doorbell_ding": ["Ding", "occupancy", _async_retrieve_ding_state],
"doorbell_motion": ["Motion", "motion", _retrieve_motion_state], "doorbell_motion": ["Motion", "motion", _async_retrieve_motion_state],
"doorbell_online": ["Online", "connectivity", _retrieve_online_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.""" """Set up the August binary sensors."""
data = hass.data[DATA_AUGUST] data = hass.data[DATA_AUGUST]
devices = [] devices = []
for door in data.locks: for door in data.locks:
for sensor_type in SENSOR_TYPES_DOOR: for sensor_type in SENSOR_TYPES_DOOR:
state_provider = SENSOR_TYPES_DOOR[sensor_type][2] if not data.lock_has_doorsense(door.device_id):
if state_provider(data, door) is LockDoorStatus.UNKNOWN:
_LOGGER.debug( _LOGGER.debug(
"Not adding sensor class %s for lock %s ", "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, door.device_name,
) )
continue continue
_LOGGER.debug( _LOGGER.debug(
"Adding sensor class %s for %s", "Adding sensor class %s for %s",
SENSOR_TYPES_DOOR[sensor_type][1], SENSOR_TYPES_DOOR[sensor_type][SENSOR_DEVICE_CLASS],
door.device_name, door.device_name,
) )
devices.append(AugustDoorBinarySensor(data, sensor_type, door)) 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: for sensor_type in SENSOR_TYPES_DOORBELL:
_LOGGER.debug( _LOGGER.debug(
"Adding doorbell sensor class %s for %s", "Adding doorbell sensor class %s for %s",
SENSOR_TYPES_DOORBELL[sensor_type][1], SENSOR_TYPES_DOORBELL[sensor_type][SENSOR_DEVICE_CLASS],
doorbell.device_name, doorbell.device_name,
) )
devices.append(AugustDoorbellBinarySensor(data, sensor_type, doorbell)) devices.append(AugustDoorbellBinarySensor(data, sensor_type, doorbell))
add_entities(devices, True) async_add_entities(devices, True)
class AugustDoorBinarySensor(BinarySensorDevice): class AugustDoorBinarySensor(BinarySensorDevice):
@ -120,28 +128,79 @@ class AugustDoorBinarySensor(BinarySensorDevice):
@property @property
def device_class(self): def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES.""" """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 @property
def name(self): def name(self):
"""Return the name of the binary sensor.""" """Return the name of the binary sensor."""
return "{} {}".format( 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): async def async_update(self):
"""Get the latest state of the sensor.""" """Get the latest state of the sensor and update activity."""
state_provider = SENSOR_TYPES_DOOR[self._sensor_type][2] async_state_provider = SENSOR_TYPES_DOOR[self._sensor_type][
self._state = state_provider(self._data, self._door) SENSOR_STATE_PROVIDER
self._available = self._state is not None ]
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 @property
def unique_id(self) -> str: def unique_id(self) -> str:
"""Get the unique of the door open binary sensor.""" """Get the unique of the door open binary sensor."""
return "{:s}_{:s}".format( 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 @property
def device_class(self): def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES.""" """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 @property
def name(self): def name(self):
"""Return the name of the binary sensor.""" """Return the name of the binary sensor."""
return "{} {}".format( 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.""" """Get the latest state of the sensor."""
state_provider = SENSOR_TYPES_DOORBELL[self._sensor_type][2] async_state_provider = SENSOR_TYPES_DOORBELL[self._sensor_type][
self._state = state_provider(self._data, self._doorbell) SENSOR_STATE_PROVIDER
self._available = self._doorbell.is_online ]
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 @property
def unique_id(self) -> str: def unique_id(self) -> str:
"""Get the unique id of the doorbell sensor.""" """Get the unique id of the doorbell sensor."""
return "{:s}_{:s}".format( return "{:s}_{:s}".format(
self._doorbell.device_id, self._doorbell.device_id,
SENSOR_TYPES_DOORBELL[self._sensor_type][0].lower(), SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_NAME].lower(),
) )

View File

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

View File

@ -2,11 +2,12 @@
from datetime import timedelta from datetime import timedelta
import logging import logging
from august.activity import ActivityType from august.activity import ACTIVITY_ACTION_STATES, ActivityType
from august.lock import LockStatus from august.lock import LockStatus
from homeassistant.components.lock import LockDevice from homeassistant.components.lock import LockDevice
from homeassistant.const import ATTR_BATTERY_LEVEL from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.util import dt
from . import DATA_AUGUST from . import DATA_AUGUST
@ -15,7 +16,7 @@ _LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=10) 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.""" """Set up August locks."""
data = hass.data[DATA_AUGUST] data = hass.data[DATA_AUGUST]
devices = [] devices = []
@ -24,7 +25,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
_LOGGER.debug("Adding lock for %s", lock.device_name) _LOGGER.debug("Adding lock for %s", lock.device_name)
devices.append(AugustLock(data, lock)) devices.append(AugustLock(data, lock))
add_entities(devices, True) async_add_entities(devices, True)
class AugustLock(LockDevice): class AugustLock(LockDevice):
@ -39,27 +40,77 @@ class AugustLock(LockDevice):
self._changed_by = None self._changed_by = None
self._available = False self._available = False
def lock(self, **kwargs): async def async_lock(self, **kwargs):
"""Lock the device.""" """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.""" """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): def _update_lock_status(self, lock_status, update_start_time_utc):
"""Get the latest state of the sensor.""" if self._lock_status != lock_status:
self._lock_status = self._data.get_lock_status(self._lock.device_id) self._lock_status = lock_status
self._available = self._lock_status is not None 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 self._lock.device_id, ActivityType.LOCK_OPERATION
) )
if activity is not None: if lock_activity is not None:
self._changed_by = activity.operated_by 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 @property
def name(self): def name(self):

View File

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

View File

@ -11,8 +11,10 @@ import homeassistant.helpers.config_validation as cv
# mypy: allow-untyped-defs # mypy: allow-untyped-defs
CONF_ENCODING = "encoding" CONF_ENCODING = "encoding"
CONF_QOS = "qos"
CONF_TOPIC = "topic" CONF_TOPIC = "topic"
DEFAULT_ENCODING = "utf-8" DEFAULT_ENCODING = "utf-8"
DEFAULT_QOS = 0
TRIGGER_SCHEMA = vol.Schema( TRIGGER_SCHEMA = vol.Schema(
{ {
@ -20,6 +22,9 @@ TRIGGER_SCHEMA = vol.Schema(
vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic, vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_PAYLOAD): cv.string, vol.Optional(CONF_PAYLOAD): cv.string,
vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): 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] topic = config[CONF_TOPIC]
payload = config.get(CONF_PAYLOAD) payload = config.get(CONF_PAYLOAD)
encoding = config[CONF_ENCODING] or None encoding = config[CONF_ENCODING] or None
qos = config[CONF_QOS]
@callback @callback
def mqtt_automation_listener(mqttmsg): 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}) hass.async_run_job(action, {"trigger": data})
remove = await mqtt.async_subscribe( remove = await mqtt.async_subscribe(
hass, topic, mqtt_automation_listener, encoding=encoding hass, topic, mqtt_automation_listener, encoding=encoding, qos=qos
) )
return remove return remove

View File

@ -9,7 +9,7 @@
}, },
"error": { "error": {
"already_configured": "El dispositiu ja est\u00e0 configurat", "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", "device_unavailable": "El dispositiu no est\u00e0 disponible",
"faulty_credentials": "Credencials d'usuari incorrectes" "faulty_credentials": "Credencials d'usuari incorrectes"
}, },

View File

@ -2,7 +2,7 @@
"config": { "config": {
"abort": { "abort": {
"already_configured": "Device is already configured", "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", "link_local_address": "Link local addresses are not supported",
"not_axis_device": "Discovered device not an Axis device", "not_axis_device": "Discovered device not an Axis device",
"updated_configuration": "Updated device configuration with new host address" "updated_configuration": "Updated device configuration with new host address"

View File

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

View File

@ -2,7 +2,7 @@
"config": { "config": {
"abort": { "abort": {
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "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", "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", "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" "updated_configuration": "\uc0c8\ub85c\uc6b4 \ud638\uc2a4\ud2b8 \uc8fc\uc18c\ub85c \uc5c5\ub370\uc774\ud2b8\ub41c \uae30\uae30 \uad6c\uc131"

View File

@ -4,7 +4,8 @@
"already_configured": "Apparaat is al geconfigureerd", "already_configured": "Apparaat is al geconfigureerd",
"bad_config_file": "Slechte gegevens van het configuratiebestand", "bad_config_file": "Slechte gegevens van het configuratiebestand",
"link_local_address": "Link-lokale adressen worden niet ondersteund", "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": { "error": {
"already_configured": "Apparaat is al geconfigureerd", "already_configured": "Apparaat is al geconfigureerd",

View File

@ -2,7 +2,7 @@
"config": { "config": {
"abort": { "abort": {
"already_configured": "Enheten er allerede konfigurert", "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", "link_local_address": "Linking av lokale adresser st\u00f8ttes ikke",
"not_axis_device": "Oppdaget enhet ikke en Axis enhet", "not_axis_device": "Oppdaget enhet ikke en Axis enhet",
"updated_configuration": "Oppdatert enhetskonfigurasjonen med ny vertsadresse" "updated_configuration": "Oppdatert enhetskonfigurasjonen med ny vertsadresse"

View File

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

View File

@ -2,9 +2,10 @@
"config": { "config": {
"abort": { "abort": {
"already_configured": "Enheten \u00e4r redan konfigurerad", "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", "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": { "error": {
"already_configured": "Enheten \u00e4r redan konfigurerad", "already_configured": "Enheten \u00e4r redan konfigurerad",
@ -12,6 +13,7 @@
"device_unavailable": "Enheten \u00e4r inte tillg\u00e4nglig", "device_unavailable": "Enheten \u00e4r inte tillg\u00e4nglig",
"faulty_credentials": "Felaktiga anv\u00e4ndaruppgifter" "faulty_credentials": "Felaktiga anv\u00e4ndaruppgifter"
}, },
"flow_title": "Axisenhet: {name} ({host})",
"step": { "step": {
"user": { "user": {
"data": { "data": {

View File

@ -2,7 +2,7 @@
"config": { "config": {
"abort": { "abort": {
"already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "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", "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", "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" "updated_configuration": "\u4f7f\u7528\u65b0\u4e3b\u6a5f\u7aef\u4f4d\u5740\u66f4\u65b0\u88dd\u7f6e\u8a2d\u5b9a"

View File

@ -1,15 +1,23 @@
"""Support for Axis devices.""" """Support for Axis devices."""
import logging
from homeassistant.const import ( from homeassistant.const import (
CONF_DEVICE, CONF_DEVICE,
CONF_HOST,
CONF_MAC, CONF_MAC,
CONF_PASSWORD,
CONF_PORT,
CONF_TRIGGER_TIME, CONF_TRIGGER_TIME,
CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_STOP,
) )
from .const import CONF_CAMERA, CONF_EVENTS, DEFAULT_TRIGGER_TIME, DOMAIN from .const import CONF_CAMERA, CONF_EVENTS, DEFAULT_TRIGGER_TIME, DOMAIN
from .device import AxisNetworkDevice, get_device from .device import AxisNetworkDevice, get_device
LOGGER = logging.getLogger(__name__)
async def async_setup(hass, config): async def async_setup(hass, config):
"""Old way to set up Axis devices.""" """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 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() 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): async def async_populate_options(hass, config_entry):
"""Populate default options for device.""" """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 supported_formats = device.vapix.params.image_format
camera = bool(supported_formats) 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) hass.config_entries.async_update_entry(config_entry, options=options)
async def async_migrate_entry(hass, config_entry):
"""Migrate old entry."""
LOGGER.debug("Migrating from version %s", config_entry.version)
# Flatten configuration but keep old data if user rollbacks HASS
if config_entry.version == 1:
config_entry.data = {**config_entry.data, **config_entry.data[CONF_DEVICE]}
config_entry.version = 2
LOGGER.info("Migration to version %s successful", config_entry.version)
return True

View File

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

View File

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

View File

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

View File

@ -1,30 +1,29 @@
{ {
"config": { "config": {
"title": "Axis device", "title": "Axis device",
"flow_title": "Axis device: {name} ({host})", "flow_title": "Axis device: {name} ({host})",
"step": { "step": {
"user": { "user": {
"title": "Set up Axis device", "title": "Set up Axis device",
"data": { "data": {
"host": "Host", "host": "Host",
"username": "Username", "username": "Username",
"password": "Password", "password": "Password",
"port": "Port" "port": "Port"
}
}
},
"error": {
"already_configured": "Device is already configured",
"already_in_progress": "Config flow for device is already in progress.",
"device_unavailable": "Device is not available",
"faulty_credentials": "Bad user credentials"
},
"abort": {
"already_configured": "Device is already configured",
"bad_config_file": "Bad data from config 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"
} }
}
},
"error": {
"already_configured": "Device is already configured",
"already_in_progress": "Config flow for device is already in progress.",
"device_unavailable": "Device is not available",
"faulty_credentials": "Bad user credentials"
},
"abort": {
"already_configured": "Device is already configured",
"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"
} }
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -421,7 +421,7 @@ class BluesoundPlayer(MediaPlayerDevice):
# sync_status. We will force an update if the player is # sync_status. We will force an update if the player is
# grouped this isn't a foolproof solution. A better # grouped this isn't a foolproof solution. A better
# solution would be to fetch sync_status more often when # solution would be to fetch sync_status more often when
# the device is playing. This would solve alot of # the device is playing. This would solve a lot of
# problems. This change will be done when the # problems. This change will be done when the
# communication is moved to a separate library # communication is moved to a separate library
await self.force_update_sync_status() await self.force_update_sync_status()

View File

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

View File

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

View File

@ -112,7 +112,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
if station is not None: if station is not None:
if zone_id and wmo_id: if zone_id and wmo_id:
_LOGGER.warning( _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_STATION,
CONF_ZONE_ID, CONF_ZONE_ID,
CONF_WMO_ID, CONF_WMO_ID,
@ -281,7 +281,7 @@ def _get_bom_stations():
"""Return {CONF_STATION: (lat, lon)} for all stations, for auto-config. """Return {CONF_STATION: (lat, lon)} for all stations, for auto-config.
This function does several MB of internet requests, so please use the 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 = {} latlon = {}
with io.BytesIO() as file_obj: with io.BytesIO() as file_obj:

View File

@ -2,7 +2,7 @@
"domain": "braviatv", "domain": "braviatv",
"name": "Sony Bravia TV", "name": "Sony Bravia TV",
"documentation": "https://www.home-assistant.io/integrations/braviatv", "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"], "dependencies": ["configurator"],
"codeowners": ["@robbiet480"] "codeowners": ["@robbiet480"]
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -9,6 +9,7 @@
"snmp_error": "Serveur SNMP d\u00e9sactiv\u00e9 ou imprimante non prise en charge.", "snmp_error": "Serveur SNMP d\u00e9sactiv\u00e9 ou imprimante non prise en charge.",
"wrong_host": "Nom d'h\u00f4te ou adresse IP invalide." "wrong_host": "Nom d'h\u00f4te ou adresse IP invalide."
}, },
"flow_title": "Imprimante Brother: {model} {serial_number}",
"step": { "step": {
"user": { "user": {
"data": { "data": {
@ -21,7 +22,9 @@
"zeroconf_confirm": { "zeroconf_confirm": {
"data": { "data": {
"type": "Type d'imprimante" "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" "title": "Imprimante Brother"

View File

@ -1,7 +1,24 @@
{ {
"config": { "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}", "flow_title": "Brother nyomtat\u00f3: {model} {serial_number}",
"step": { "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": { "zeroconf_confirm": {
"data": { "data": {
"type": "A nyomtat\u00f3 t\u00edpusa" "type": "A nyomtat\u00f3 t\u00edpusa"

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