mirror of
https://github.com/home-assistant/core.git
synced 2025-07-27 15:17:35 +00:00
commit
2d68b37dd5
25
.coveragerc
25
.coveragerc
@ -166,7 +166,6 @@ omit =
|
|||||||
homeassistant/components/dsmr_reader/*
|
homeassistant/components/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
12
.github/stale.yml
vendored
@ -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.
|
||||||
|
@ -1,59 +0,0 @@
|
|||||||
# This configuration includes the full set of hooks we use. In
|
|
||||||
# addition to the defaults (see .pre-commit-config.yaml), this
|
|
||||||
# includes hooks that require our development and test dependencies
|
|
||||||
# installed and the virtualenv containing them active by the time
|
|
||||||
# pre-commit runs to produce correct results.
|
|
||||||
#
|
|
||||||
# If this is not a problem for your workflow, using this config is
|
|
||||||
# recommended, install it with
|
|
||||||
# pre-commit install --config .pre-commit-config-all.yaml
|
|
||||||
# Otherwise, see the default .pre-commit-config.yaml for a lighter one.
|
|
||||||
|
|
||||||
repos:
|
|
||||||
- repo: https://github.com/psf/black
|
|
||||||
rev: 19.10b0
|
|
||||||
hooks:
|
|
||||||
- id: black
|
|
||||||
args:
|
|
||||||
- --safe
|
|
||||||
- --quiet
|
|
||||||
files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$
|
|
||||||
- repo: https://github.com/PyCQA/flake8
|
|
||||||
rev: 3.7.9
|
|
||||||
hooks:
|
|
||||||
- id: flake8
|
|
||||||
additional_dependencies:
|
|
||||||
- flake8-docstrings==1.5.0
|
|
||||||
- pydocstyle==5.0.2
|
|
||||||
files: ^(homeassistant|script|tests)/.+\.py$
|
|
||||||
- repo: https://github.com/PyCQA/bandit
|
|
||||||
rev: 1.6.2
|
|
||||||
hooks:
|
|
||||||
- id: bandit
|
|
||||||
args:
|
|
||||||
- --quiet
|
|
||||||
- --format=custom
|
|
||||||
- --configfile=tests/bandit.yaml
|
|
||||||
files: ^(homeassistant|script|tests)/.+\.py$
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-isort
|
|
||||||
rev: v4.3.21
|
|
||||||
hooks:
|
|
||||||
- id: isort
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
||||||
rev: v2.4.0
|
|
||||||
hooks:
|
|
||||||
- id: check-json
|
|
||||||
# Using a local "system" mypy instead of the mypy hook, because its
|
|
||||||
# results depend on what is installed. And the mypy hook runs in a
|
|
||||||
# virtualenv of its own, meaning we'd need to install and maintain
|
|
||||||
# another set of our dependencies there... no. Use the "system" one
|
|
||||||
# and reuse the environment that is set up anyway already instead.
|
|
||||||
- repo: local
|
|
||||||
hooks:
|
|
||||||
- id: mypy
|
|
||||||
name: mypy
|
|
||||||
entry: mypy
|
|
||||||
language: system
|
|
||||||
types: [python]
|
|
||||||
require_serial: true
|
|
||||||
files: ^homeassistant/.+\.py$
|
|
@ -1,10 +1,3 @@
|
|||||||
# This configuration includes the default, minimal set of hooks to be
|
|
||||||
# run on all commits. It requires no specific setup and one can just
|
|
||||||
# start using pre-commit with it.
|
|
||||||
#
|
|
||||||
# See .pre-commit-config-all.yaml for a more complete one that comes
|
|
||||||
# with a better coverage at the cost of some specific setup needed.
|
|
||||||
|
|
||||||
repos:
|
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$
|
||||||
|
15
CODEOWNERS
15
CODEOWNERS
@ -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
|
||||||
|
@ -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'
|
||||||
|
@ -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'
|
||||||
|
@ -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
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
22
homeassistant/components/abode/.translations/es-419.json
Normal file
22
homeassistant/components/abode/.translations/es-419.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"single_instance_allowed": "Solo se permite una \u00fanica configuraci\u00f3n de Abode."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"connection_error": "No se puede conectar a Abode.",
|
||||||
|
"identifier_exists": "Cuenta ya registrada.",
|
||||||
|
"invalid_credentials": "Credenciales inv\u00e1lidas."
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"password": "Contrase\u00f1a",
|
||||||
|
"username": "Direcci\u00f3n de correo electr\u00f3nico"
|
||||||
|
},
|
||||||
|
"title": "Complete su informaci\u00f3n de inicio de sesi\u00f3n de Abode"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Abode"
|
||||||
|
}
|
||||||
|
}
|
22
homeassistant/components/abode/.translations/hu.json
Normal file
22
homeassistant/components/abode/.translations/hu.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"single_instance_allowed": "Csak egyetlen Abode konfigur\u00e1ci\u00f3 enged\u00e9lyezett."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"connection_error": "Nem lehet csatlakozni az Abode-hez.",
|
||||||
|
"identifier_exists": "Fi\u00f3k m\u00e1r regisztr\u00e1lva van",
|
||||||
|
"invalid_credentials": "\u00c9rv\u00e9nytelen hiteles\u00edt\u0151 adatok"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"password": "Jelsz\u00f3",
|
||||||
|
"username": "Email c\u00edm"
|
||||||
|
},
|
||||||
|
"title": "T\u00f6ltse ki az Abode bejelentkez\u00e9si adatait"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Abode"
|
||||||
|
}
|
||||||
|
}
|
@ -5,7 +5,7 @@
|
|||||||
},
|
},
|
||||||
"error": {
|
"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": {
|
||||||
|
22
homeassistant/components/abode/.translations/sv.json
Normal file
22
homeassistant/components/abode/.translations/sv.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"single_instance_allowed": "Endast en enda konfiguration av Abode \u00e4r till\u00e5ten."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"connection_error": "Det gick inte att ansluta till Abode.",
|
||||||
|
"identifier_exists": "Kontot \u00e4r redan registrerat.",
|
||||||
|
"invalid_credentials": "Ogiltiga autentiseringsuppgifter."
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"password": "L\u00f6senord",
|
||||||
|
"username": "E-postadress"
|
||||||
|
},
|
||||||
|
"title": "Fyll i din inloggningsinformation f\u00f6r Abode"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Abode"
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,8 @@ from .const import DOMAIN
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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(
|
||||||
|
@ -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."
|
||||||
},
|
},
|
||||||
|
@ -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."
|
||||||
},
|
},
|
||||||
|
22
homeassistant/components/airly/.translations/es-419.json
Normal file
22
homeassistant/components/airly/.translations/es-419.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"error": {
|
||||||
|
"auth": "La clave API no es correcta.",
|
||||||
|
"name_exists": "El nombre ya existe.",
|
||||||
|
"wrong_location": "No hay estaciones de medici\u00f3n Airly en esta \u00e1rea."
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"api_key": "Clave API de Airly",
|
||||||
|
"latitude": "Latitud",
|
||||||
|
"longitude": "Longitud",
|
||||||
|
"name": "Nombre de la integraci\u00f3n"
|
||||||
|
},
|
||||||
|
"description": "Configure la integraci\u00f3n de la calidad del aire de Airly. Para generar la clave API, vaya a https://developer.airly.eu/register",
|
||||||
|
"title": "Airly"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Airly"
|
||||||
|
}
|
||||||
|
}
|
25
homeassistant/components/airly/.translations/hu.json
Normal file
25
homeassistant/components/airly/.translations/hu.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Ezen koordin\u00e1t\u00e1k Airly integr\u00e1ci\u00f3ja m\u00e1r konfigur\u00e1lva van."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"auth": "Az API kulcs nem megfelel\u0151.",
|
||||||
|
"name_exists": "A n\u00e9v m\u00e1r l\u00e9tezik",
|
||||||
|
"wrong_location": "Ezen a ter\u00fcleten nincs Airly m\u00e9r\u0151\u00e1llom\u00e1s."
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"api_key": "Airly API kulcs",
|
||||||
|
"latitude": "Sz\u00e9less\u00e9g",
|
||||||
|
"longitude": "Hossz\u00fas\u00e1g",
|
||||||
|
"name": "Az integr\u00e1ci\u00f3 neve"
|
||||||
|
},
|
||||||
|
"description": "Az Airly leveg\u0151min\u0151s\u00e9gi integr\u00e1ci\u00f3j\u00e1nak be\u00e1ll\u00edt\u00e1sa. Api-kulcs l\u00e9trehoz\u00e1s\u00e1hoz nyissa meg a k\u00f6vetkez\u0151 weboldalt: https://developer.airly.eu/register",
|
||||||
|
"title": "Airly"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Airly"
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"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.",
|
||||||
|
@ -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",
|
||||||
|
25
homeassistant/components/airly/.translations/sv.json
Normal file
25
homeassistant/components/airly/.translations/sv.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Airly-integrationen f\u00f6r dessa koordinater \u00e4r redan konfigurerad."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"auth": "API-nyckeln \u00e4r inte korrekt.",
|
||||||
|
"name_exists": "Namnet finns redan.",
|
||||||
|
"wrong_location": "Inga Airly m\u00e4tstationer i detta omr\u00e5de."
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"api_key": "Airly API-nyckel",
|
||||||
|
"latitude": "Latitud",
|
||||||
|
"longitude": "Longitud",
|
||||||
|
"name": "Integrationens namn"
|
||||||
|
},
|
||||||
|
"description": "Konfigurera integration av luftkvalitet. F\u00f6r att skapa API-nyckel, g\u00e5 till https://developer.airly.eu/register",
|
||||||
|
"title": "Airly"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Airly"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"device_automation": {
|
||||||
|
"action_type": {
|
||||||
|
"arm_away": "Larma {entity_name} borta",
|
||||||
|
"arm_home": "Larma {entity_name} hemma",
|
||||||
|
"arm_night": "Larma {entity_name} natt",
|
||||||
|
"disarm": "Avlarma {entity_name}",
|
||||||
|
"trigger": "Utl\u00f6sare {entity_name}"
|
||||||
|
},
|
||||||
|
"trigger_type": {
|
||||||
|
"armed_away": "{entity_name} larmad borta",
|
||||||
|
"armed_home": "{entity_name} larmad hemma",
|
||||||
|
"armed_night": "{entity_name} larmad natt",
|
||||||
|
"disarmed": "{entity_name} bortkopplad",
|
||||||
|
"triggered": "{entity_name} utl\u00f6st"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -138,7 +138,7 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
|
|||||||
|
|
||||||
def _restore_callback(self, zone):
|
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()
|
||||||
|
|
||||||
|
@ -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 = (
|
||||||
|
@ -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)
|
||||||
|
@ -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("#", ".")
|
||||||
|
19
homeassistant/components/almond/.translations/hu.json
Normal file
19
homeassistant/components/almond/.translations/hu.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_setup": "Csak egy Almond fi\u00f3kot konfigur\u00e1lhat.",
|
||||||
|
"cannot_connect": "Nem lehet csatlakozni az Almond szerverhez.",
|
||||||
|
"missing_configuration": "K\u00e9rj\u00fck, ellen\u0151rizze az Almond be\u00e1ll\u00edt\u00e1s\u00e1nak dokument\u00e1ci\u00f3j\u00e1t."
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"hassio_confirm": {
|
||||||
|
"description": "Be szeretn\u00e9 \u00e1ll\u00edtani a Home Assistant alkalmaz\u00e1st az Almondhoz val\u00f3 csatlakoz\u00e1shoz, amelyet a Hass.io kieg\u00e9sz\u00edt\u0151 biztos\u00edt: {addon} ?",
|
||||||
|
"title": "Almond a Hass.io kieg\u00e9sz\u00edt\u0151n kereszt\u00fcl"
|
||||||
|
},
|
||||||
|
"pick_implementation": {
|
||||||
|
"title": "V\u00e1lassza ki a hiteles\u00edt\u00e9si m\u00f3dszert"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Almond"
|
||||||
|
}
|
||||||
|
}
|
@ -6,6 +6,10 @@
|
|||||||
"missing_configuration": "Raadpleeg de documentatie over het instellen van Almond."
|
"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"
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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"
|
||||||
},
|
},
|
||||||
|
@ -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",
|
||||||
|
@ -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):
|
||||||
|
@ -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"
|
||||||
|
@ -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]:
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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."""
|
||||||
|
@ -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"
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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": [],
|
||||||
|
@ -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.
|
||||||
"""
|
"""
|
||||||
|
@ -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": []
|
||||||
}
|
}
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
5
homeassistant/components/arcam_fmj/.translations/sv.json
Normal file
5
homeassistant/components/arcam_fmj/.translations/sv.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"title": "Arcam FMJ"
|
||||||
|
}
|
||||||
|
}
|
@ -110,19 +110,19 @@ class ArloBaseStation(AlarmControlPanel):
|
|||||||
else:
|
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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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": [],
|
||||||
|
@ -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):
|
||||||
|
31
homeassistant/components/august/.translations/de.json
Normal file
31
homeassistant/components/august/.translations/de.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Konto ist bereits konfiguriert"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut",
|
||||||
|
"invalid_auth": "Ung\u00fcltige Authentifizierung",
|
||||||
|
"unknown": "Unerwarteter Fehler"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"login_method": "Anmeldemethode",
|
||||||
|
"password": "Passwort",
|
||||||
|
"timeout": "Zeit\u00fcberschreitung (Sekunden)",
|
||||||
|
"username": "Benutzername"
|
||||||
|
},
|
||||||
|
"title": "Richten Sie ein August-Konto ein"
|
||||||
|
},
|
||||||
|
"validation": {
|
||||||
|
"data": {
|
||||||
|
"code": "Verifizierungs-Code"
|
||||||
|
},
|
||||||
|
"description": "Bitte \u00fcberpr\u00fcfen Sie Ihre {login_method} ({username}) und geben Sie den Best\u00e4tigungscode ein",
|
||||||
|
"title": "Zwei-Faktor-Authentifizierung"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "August"
|
||||||
|
}
|
||||||
|
}
|
32
homeassistant/components/august/.translations/en.json
Normal file
32
homeassistant/components/august/.translations/en.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Account is already configured"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Failed to connect, please try again",
|
||||||
|
"invalid_auth": "Invalid authentication",
|
||||||
|
"unknown": "Unexpected error"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"login_method": "Login Method",
|
||||||
|
"password": "Password",
|
||||||
|
"timeout": "Timeout (seconds)",
|
||||||
|
"username": "Username"
|
||||||
|
},
|
||||||
|
"description": "If the Login Method is 'email', Username is the email address. If the Login Method is 'phone', Username is the phone number in the format '+NNNNNNNNN'.",
|
||||||
|
"title": "Setup an August account"
|
||||||
|
},
|
||||||
|
"validation": {
|
||||||
|
"data": {
|
||||||
|
"code": "Verification code"
|
||||||
|
},
|
||||||
|
"description": "Please check your {login_method} ({username}) and enter the verification code below",
|
||||||
|
"title": "Two factor authentication"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "August"
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,10 @@
|
|||||||
"""Support for August devices."""
|
"""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
|
||||||
|
@ -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(),
|
||||||
)
|
)
|
||||||
|
@ -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."""
|
||||||
|
@ -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):
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
},
|
},
|
||||||
|
@ -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"
|
||||||
|
@ -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": {
|
||||||
|
@ -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"
|
||||||
|
@ -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",
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
},
|
},
|
||||||
|
@ -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": {
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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 = {
|
||||||
|
@ -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:
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"],
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
94
homeassistant/components/binary_sensor/.translations/sv.json
Normal file
94
homeassistant/components/binary_sensor/.translations/sv.json
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
{
|
||||||
|
"device_automation": {
|
||||||
|
"condition_type": {
|
||||||
|
"is_bat_low": "{entity_name}-batteriet \u00e4r l\u00e5gt",
|
||||||
|
"is_cold": "{entity_name} \u00e4r kall",
|
||||||
|
"is_connected": "{entity_name} \u00e4r ansluten",
|
||||||
|
"is_gas": "{entity_name} detekterar gas",
|
||||||
|
"is_hot": "{entity_name} \u00e4r varm",
|
||||||
|
"is_light": "{entity_name} uppt\u00e4cker ljus",
|
||||||
|
"is_locked": "{entity_name} \u00e4r l\u00e5st",
|
||||||
|
"is_moist": "{entity_name} \u00e4r fuktig",
|
||||||
|
"is_motion": "{entity_name} detekterar r\u00f6relse",
|
||||||
|
"is_moving": "{entity_name} r\u00f6r sig",
|
||||||
|
"is_no_gas": "{entity_name} uppt\u00e4cker inte gas",
|
||||||
|
"is_no_light": "{entity_name} uppt\u00e4cker inte ljus",
|
||||||
|
"is_no_motion": "{entity_name} detekterar inte r\u00f6relse",
|
||||||
|
"is_no_problem": "{entity_name} uppt\u00e4cker inte problem",
|
||||||
|
"is_no_smoke": "{entity_name} detekterar inte r\u00f6k",
|
||||||
|
"is_no_sound": "{entity_name} uppt\u00e4cker inte ljud",
|
||||||
|
"is_no_vibration": "{entity_name} uppt\u00e4cker inte vibrationer",
|
||||||
|
"is_not_bat_low": "{entity_name} batteri \u00e4r normalt",
|
||||||
|
"is_not_cold": "{entity_name} \u00e4r inte kall",
|
||||||
|
"is_not_connected": "{entity_name} \u00e4r fr\u00e5nkopplad",
|
||||||
|
"is_not_hot": "{entity_name} \u00e4r inte varm",
|
||||||
|
"is_not_locked": "{entity_name} \u00e4r ol\u00e5st",
|
||||||
|
"is_not_moist": "{entity_name} \u00e4r torr",
|
||||||
|
"is_not_moving": "{entity_name} r\u00f6r sig inte",
|
||||||
|
"is_not_occupied": "{entity_name} \u00e4r inte upptagen",
|
||||||
|
"is_not_open": "{entity_name} \u00e4r st\u00e4ngd",
|
||||||
|
"is_not_plugged_in": "{entity_name} \u00e4r urkopplad",
|
||||||
|
"is_not_powered": "{entity_name} \u00e4r inte str\u00f6mf\u00f6rd",
|
||||||
|
"is_not_present": "{entity_name} finns inte",
|
||||||
|
"is_not_unsafe": "{entity_name} \u00e4r s\u00e4ker",
|
||||||
|
"is_occupied": "{entity_name} \u00e4r upptagen",
|
||||||
|
"is_off": "{entity_name} \u00e4r avst\u00e4ngd",
|
||||||
|
"is_on": "{entity_name} \u00e4r p\u00e5",
|
||||||
|
"is_open": "{entity_name} \u00e4r \u00f6ppen",
|
||||||
|
"is_plugged_in": "{entity_name} \u00e4r ansluten",
|
||||||
|
"is_powered": "{entity_name} \u00e4r str\u00f6mf\u00f6rd",
|
||||||
|
"is_present": "{entity_name} \u00e4r n\u00e4rvarande",
|
||||||
|
"is_problem": "{entity_name} uppt\u00e4cker problem",
|
||||||
|
"is_smoke": "{entity_name} detekterar r\u00f6k",
|
||||||
|
"is_sound": "{entity_name} uppt\u00e4cker ljud",
|
||||||
|
"is_unsafe": "{entity_name} \u00e4r os\u00e4ker",
|
||||||
|
"is_vibration": "{entity_name} uppt\u00e4cker vibrationer"
|
||||||
|
},
|
||||||
|
"trigger_type": {
|
||||||
|
"bat_low": "{entity_name} batteri l\u00e5gt",
|
||||||
|
"closed": "{entity_name} st\u00e4ngd",
|
||||||
|
"cold": "{entity_name} blev kall",
|
||||||
|
"connected": "{entity_name} ansluten",
|
||||||
|
"gas": "{entity_name} b\u00f6rjade detektera gas",
|
||||||
|
"hot": "{entity_name} blev varm",
|
||||||
|
"light": "{entity_name} b\u00f6rjade uppt\u00e4cka ljus",
|
||||||
|
"locked": "{entity_name} l\u00e5st",
|
||||||
|
"moist": "{entity_name} blev fuktig",
|
||||||
|
"moist\u00a7": "{entity_name} blev fuktig",
|
||||||
|
"motion": "{entity_name} b\u00f6rjade detektera r\u00f6relse",
|
||||||
|
"moving": "{entity_name} b\u00f6rjade r\u00f6ra sig",
|
||||||
|
"no_gas": "{entity_name} slutade uppt\u00e4cka gas",
|
||||||
|
"no_light": "{entity_name} slutade uppt\u00e4cka ljus",
|
||||||
|
"no_motion": "{entity_name} slutade uppt\u00e4cka r\u00f6relse",
|
||||||
|
"no_problem": "{entity_name} slutade uppt\u00e4cka problem",
|
||||||
|
"no_smoke": "{entity_name} slutade detektera r\u00f6k",
|
||||||
|
"no_sound": "{entity_name} slutade uppt\u00e4cka ljud",
|
||||||
|
"no_vibration": "{entity_name} slutade uppt\u00e4cka vibrationer",
|
||||||
|
"not_bat_low": "{entity_name} batteri normalt",
|
||||||
|
"not_cold": "{entity_name} blev inte kall",
|
||||||
|
"not_connected": "{entity_name} fr\u00e5nkopplad",
|
||||||
|
"not_hot": "{entity_name} blev inte varm",
|
||||||
|
"not_locked": "{entity_name} ol\u00e5st",
|
||||||
|
"not_moist": "{entity_name} blev torr",
|
||||||
|
"not_moving": "{entity_name} slutade r\u00f6ra sig",
|
||||||
|
"not_occupied": "{entity_name} blev inte upptagen",
|
||||||
|
"not_opened": "{entity_name} st\u00e4ngd",
|
||||||
|
"not_plugged_in": "{entity_name} urkopplad",
|
||||||
|
"not_powered": "{entity_name} inte str\u00f6mf\u00f6rd",
|
||||||
|
"not_present": "{entity_name} inte n\u00e4rvarande",
|
||||||
|
"not_unsafe": "{entity_name} blev s\u00e4ker",
|
||||||
|
"occupied": "{entity_name} blev upptagen",
|
||||||
|
"opened": "{entity_name} \u00f6ppnades",
|
||||||
|
"plugged_in": "{entity_name} ansluten",
|
||||||
|
"powered": "{entity_name} str\u00f6mf\u00f6rd",
|
||||||
|
"present": "{entity_name} n\u00e4rvarande",
|
||||||
|
"problem": "{entity_name} b\u00f6rjade uppt\u00e4cka problem",
|
||||||
|
"smoke": "{entity_name} b\u00f6rjade detektera r\u00f6k",
|
||||||
|
"sound": "{entity_name} b\u00f6rjade uppt\u00e4cka ljud",
|
||||||
|
"turned_off": "{entity_name} st\u00e4ngdes av",
|
||||||
|
"turned_on": "{entity_name} slogs p\u00e5",
|
||||||
|
"unsafe": "{entity_name} blev os\u00e4ker",
|
||||||
|
"vibration": "{entity_name} b\u00f6rjade detektera vibrationer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
"""Implemenet device conditions for binary sensor."""
|
"""Implement device conditions for binary sensor."""
|
||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
"""Bitcoin information service that uses blockchain.info."""
|
"""Bitcoin information service that uses blockchain.com."""
|
||||||
from datetime import timedelta
|
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()
|
||||||
|
@ -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": [],
|
||||||
|
@ -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."""
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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:
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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:
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"title": "Impresora Brother"
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,7 @@
|
|||||||
"snmp_error": "Serveur SNMP d\u00e9sactiv\u00e9 ou imprimante non prise en charge.",
|
"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"
|
||||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user