mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 07:37:34 +00:00
commit
d520a02b8c
18
.coveragerc
18
.coveragerc
@ -29,6 +29,7 @@ omit =
|
|||||||
homeassistant/components/airly/air_quality.py
|
homeassistant/components/airly/air_quality.py
|
||||||
homeassistant/components/airly/sensor.py
|
homeassistant/components/airly/sensor.py
|
||||||
homeassistant/components/airly/const.py
|
homeassistant/components/airly/const.py
|
||||||
|
homeassistant/components/airvisual/__init__.py
|
||||||
homeassistant/components/airvisual/sensor.py
|
homeassistant/components/airvisual/sensor.py
|
||||||
homeassistant/components/aladdin_connect/cover.py
|
homeassistant/components/aladdin_connect/cover.py
|
||||||
homeassistant/components/alarmdecoder/*
|
homeassistant/components/alarmdecoder/*
|
||||||
@ -58,14 +59,13 @@ omit =
|
|||||||
homeassistant/components/arwn/sensor.py
|
homeassistant/components/arwn/sensor.py
|
||||||
homeassistant/components/asterisk_cdr/mailbox.py
|
homeassistant/components/asterisk_cdr/mailbox.py
|
||||||
homeassistant/components/asterisk_mbox/*
|
homeassistant/components/asterisk_mbox/*
|
||||||
homeassistant/components/asuswrt/device_tracker.py
|
|
||||||
homeassistant/components/aten_pe/*
|
homeassistant/components/aten_pe/*
|
||||||
homeassistant/components/atome/*
|
homeassistant/components/atome/*
|
||||||
homeassistant/components/august/*
|
|
||||||
homeassistant/components/aurora_abb_powerone/sensor.py
|
homeassistant/components/aurora_abb_powerone/sensor.py
|
||||||
homeassistant/components/automatic/device_tracker.py
|
homeassistant/components/automatic/device_tracker.py
|
||||||
homeassistant/components/avea/light.py
|
homeassistant/components/avea/light.py
|
||||||
homeassistant/components/avion/light.py
|
homeassistant/components/avion/light.py
|
||||||
|
homeassistant/components/avri/sensor.py
|
||||||
homeassistant/components/azure_event_hub/*
|
homeassistant/components/azure_event_hub/*
|
||||||
homeassistant/components/azure_service_bus/*
|
homeassistant/components/azure_service_bus/*
|
||||||
homeassistant/components/baidu/tts.py
|
homeassistant/components/baidu/tts.py
|
||||||
@ -108,7 +108,6 @@ omit =
|
|||||||
homeassistant/components/canary/alarm_control_panel.py
|
homeassistant/components/canary/alarm_control_panel.py
|
||||||
homeassistant/components/canary/camera.py
|
homeassistant/components/canary/camera.py
|
||||||
homeassistant/components/cast/*
|
homeassistant/components/cast/*
|
||||||
homeassistant/components/cert_expiry/sensor.py
|
|
||||||
homeassistant/components/cert_expiry/helper.py
|
homeassistant/components/cert_expiry/helper.py
|
||||||
homeassistant/components/channels/*
|
homeassistant/components/channels/*
|
||||||
homeassistant/components/cisco_ios/device_tracker.py
|
homeassistant/components/cisco_ios/device_tracker.py
|
||||||
@ -150,7 +149,6 @@ omit =
|
|||||||
homeassistant/components/dht/sensor.py
|
homeassistant/components/dht/sensor.py
|
||||||
homeassistant/components/digital_ocean/*
|
homeassistant/components/digital_ocean/*
|
||||||
homeassistant/components/digitalloggers/switch.py
|
homeassistant/components/digitalloggers/switch.py
|
||||||
homeassistant/components/directv/media_player.py
|
|
||||||
homeassistant/components/discogs/sensor.py
|
homeassistant/components/discogs/sensor.py
|
||||||
homeassistant/components/discord/notify.py
|
homeassistant/components/discord/notify.py
|
||||||
homeassistant/components/dlib_face_detect/image_processing.py
|
homeassistant/components/dlib_face_detect/image_processing.py
|
||||||
@ -180,6 +178,7 @@ omit =
|
|||||||
homeassistant/components/ecobee/weather.py
|
homeassistant/components/ecobee/weather.py
|
||||||
homeassistant/components/econet/*
|
homeassistant/components/econet/*
|
||||||
homeassistant/components/ecovacs/*
|
homeassistant/components/ecovacs/*
|
||||||
|
homeassistant/components/edl21/*
|
||||||
homeassistant/components/eddystone_temperature/sensor.py
|
homeassistant/components/eddystone_temperature/sensor.py
|
||||||
homeassistant/components/edimax/switch.py
|
homeassistant/components/edimax/switch.py
|
||||||
homeassistant/components/egardia/*
|
homeassistant/components/egardia/*
|
||||||
@ -218,6 +217,7 @@ omit =
|
|||||||
homeassistant/components/eufy/*
|
homeassistant/components/eufy/*
|
||||||
homeassistant/components/everlights/light.py
|
homeassistant/components/everlights/light.py
|
||||||
homeassistant/components/evohome/*
|
homeassistant/components/evohome/*
|
||||||
|
homeassistant/components/ezviz/*
|
||||||
homeassistant/components/familyhub/camera.py
|
homeassistant/components/familyhub/camera.py
|
||||||
homeassistant/components/fastdotcom/*
|
homeassistant/components/fastdotcom/*
|
||||||
homeassistant/components/ffmpeg/camera.py
|
homeassistant/components/ffmpeg/camera.py
|
||||||
@ -314,6 +314,7 @@ omit =
|
|||||||
homeassistant/components/hydrawise/*
|
homeassistant/components/hydrawise/*
|
||||||
homeassistant/components/hyperion/light.py
|
homeassistant/components/hyperion/light.py
|
||||||
homeassistant/components/ialarm/alarm_control_panel.py
|
homeassistant/components/ialarm/alarm_control_panel.py
|
||||||
|
homeassistant/components/iammeter/sensor.py
|
||||||
homeassistant/components/iaqualink/binary_sensor.py
|
homeassistant/components/iaqualink/binary_sensor.py
|
||||||
homeassistant/components/iaqualink/climate.py
|
homeassistant/components/iaqualink/climate.py
|
||||||
homeassistant/components/iaqualink/light.py
|
homeassistant/components/iaqualink/light.py
|
||||||
@ -411,7 +412,9 @@ omit =
|
|||||||
homeassistant/components/mediaroom/media_player.py
|
homeassistant/components/mediaroom/media_player.py
|
||||||
homeassistant/components/melcloud/__init__.py
|
homeassistant/components/melcloud/__init__.py
|
||||||
homeassistant/components/melcloud/climate.py
|
homeassistant/components/melcloud/climate.py
|
||||||
|
homeassistant/components/melcloud/const.py
|
||||||
homeassistant/components/melcloud/sensor.py
|
homeassistant/components/melcloud/sensor.py
|
||||||
|
homeassistant/components/melcloud/water_heater.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/__init__.py
|
homeassistant/components/meteo_france/__init__.py
|
||||||
@ -463,7 +466,6 @@ omit =
|
|||||||
homeassistant/components/nello/lock.py
|
homeassistant/components/nello/lock.py
|
||||||
homeassistant/components/nest/*
|
homeassistant/components/nest/*
|
||||||
homeassistant/components/netatmo/__init__.py
|
homeassistant/components/netatmo/__init__.py
|
||||||
homeassistant/components/netatmo/binary_sensor.py
|
|
||||||
homeassistant/components/netatmo/api.py
|
homeassistant/components/netatmo/api.py
|
||||||
homeassistant/components/netatmo/camera.py
|
homeassistant/components/netatmo/camera.py
|
||||||
homeassistant/components/netatmo/climate.py
|
homeassistant/components/netatmo/climate.py
|
||||||
@ -480,6 +482,7 @@ omit =
|
|||||||
homeassistant/components/nissan_leaf/*
|
homeassistant/components/nissan_leaf/*
|
||||||
homeassistant/components/nmap_tracker/device_tracker.py
|
homeassistant/components/nmap_tracker/device_tracker.py
|
||||||
homeassistant/components/nmbs/sensor.py
|
homeassistant/components/nmbs/sensor.py
|
||||||
|
homeassistant/components/notion/__init__.py
|
||||||
homeassistant/components/notion/binary_sensor.py
|
homeassistant/components/notion/binary_sensor.py
|
||||||
homeassistant/components/notion/sensor.py
|
homeassistant/components/notion/sensor.py
|
||||||
homeassistant/components/noaa_tides/sensor.py
|
homeassistant/components/noaa_tides/sensor.py
|
||||||
@ -538,10 +541,8 @@ omit =
|
|||||||
homeassistant/components/pioneer/media_player.py
|
homeassistant/components/pioneer/media_player.py
|
||||||
homeassistant/components/pjlink/media_player.py
|
homeassistant/components/pjlink/media_player.py
|
||||||
homeassistant/components/plaato/*
|
homeassistant/components/plaato/*
|
||||||
homeassistant/components/plex/__init__.py
|
|
||||||
homeassistant/components/plex/media_player.py
|
homeassistant/components/plex/media_player.py
|
||||||
homeassistant/components/plex/sensor.py
|
homeassistant/components/plex/sensor.py
|
||||||
homeassistant/components/plex/server.py
|
|
||||||
homeassistant/components/plugwise/*
|
homeassistant/components/plugwise/*
|
||||||
homeassistant/components/plum_lightpad/*
|
homeassistant/components/plum_lightpad/*
|
||||||
homeassistant/components/pocketcasts/sensor.py
|
homeassistant/components/pocketcasts/sensor.py
|
||||||
@ -565,6 +566,7 @@ omit =
|
|||||||
homeassistant/components/qnap/sensor.py
|
homeassistant/components/qnap/sensor.py
|
||||||
homeassistant/components/qrcode/image_processing.py
|
homeassistant/components/qrcode/image_processing.py
|
||||||
homeassistant/components/quantum_gateway/device_tracker.py
|
homeassistant/components/quantum_gateway/device_tracker.py
|
||||||
|
homeassistant/components/qvr_pro/*
|
||||||
homeassistant/components/qwikswitch/*
|
homeassistant/components/qwikswitch/*
|
||||||
homeassistant/components/rachio/*
|
homeassistant/components/rachio/*
|
||||||
homeassistant/components/radarr/sensor.py
|
homeassistant/components/radarr/sensor.py
|
||||||
@ -698,6 +700,7 @@ omit =
|
|||||||
homeassistant/components/tado/device_tracker.py
|
homeassistant/components/tado/device_tracker.py
|
||||||
homeassistant/components/tahoma/*
|
homeassistant/components/tahoma/*
|
||||||
homeassistant/components/tank_utility/sensor.py
|
homeassistant/components/tank_utility/sensor.py
|
||||||
|
homeassistant/components/tankerkoenig/*
|
||||||
homeassistant/components/tapsaff/binary_sensor.py
|
homeassistant/components/tapsaff/binary_sensor.py
|
||||||
homeassistant/components/tautulli/sensor.py
|
homeassistant/components/tautulli/sensor.py
|
||||||
homeassistant/components/ted5000/sensor.py
|
homeassistant/components/ted5000/sensor.py
|
||||||
@ -842,6 +845,7 @@ omit =
|
|||||||
homeassistant/components/zha/core/helpers.py
|
homeassistant/components/zha/core/helpers.py
|
||||||
homeassistant/components/zha/core/patches.py
|
homeassistant/components/zha/core/patches.py
|
||||||
homeassistant/components/zha/core/registries.py
|
homeassistant/components/zha/core/registries.py
|
||||||
|
homeassistant/components/zha/core/typing.py
|
||||||
homeassistant/components/zha/entity.py
|
homeassistant/components/zha/entity.py
|
||||||
homeassistant/components/zha/light.py
|
homeassistant/components/zha/light.py
|
||||||
homeassistant/components/zha/sensor.py
|
homeassistant/components/zha/sensor.py
|
||||||
|
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,7 +1,7 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Report a bug with the UI, Frontend or Lovelace
|
- name: Report a bug with the UI, Frontend or Lovelace
|
||||||
url: https://github.com/home-assistant/home-assistant-polymer/issues
|
url: https://github.com/home-assistant/frontend/issues
|
||||||
about: This is the issue tracker for our backend. Please report issues with the UI in the frontend repository.
|
about: This is the issue tracker for our backend. Please report issues with the UI in the frontend repository.
|
||||||
- name: Report incorrect or missing information on our website
|
- name: Report incorrect or missing information on our website
|
||||||
url: https://github.com/home-assistant/home-assistant.io/issues
|
url: https://github.com/home-assistant/home-assistant.io/issues
|
||||||
|
@ -1,53 +1,58 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 19.10b0
|
rev: 19.10b0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
args:
|
args:
|
||||||
- --safe
|
- --safe
|
||||||
- --quiet
|
- --quiet
|
||||||
files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$
|
files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$
|
||||||
- repo: https://github.com/codespell-project/codespell
|
- repo: https://github.com/codespell-project/codespell
|
||||||
rev: v1.16.0
|
rev: v1.16.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: codespell
|
- id: codespell
|
||||||
args:
|
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
|
- --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"
|
- --skip="./.*,*.json"
|
||||||
- --quiet-level=2
|
- --quiet-level=2
|
||||||
exclude_types: [json]
|
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:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- flake8-docstrings==1.5.0
|
- flake8-docstrings==1.5.0
|
||||||
- pydocstyle==5.0.2
|
- pydocstyle==5.0.2
|
||||||
files: ^(homeassistant|script|tests)/.+\.py$
|
files: ^(homeassistant|script|tests)/.+\.py$
|
||||||
- repo: https://github.com/PyCQA/bandit
|
- repo: https://github.com/PyCQA/bandit
|
||||||
rev: 1.6.2
|
rev: 1.6.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: bandit
|
- id: bandit
|
||||||
args:
|
args:
|
||||||
- --quiet
|
- --quiet
|
||||||
- --format=custom
|
- --format=custom
|
||||||
- --configfile=tests/bandit.yaml
|
- --configfile=tests/bandit.yaml
|
||||||
files: ^(homeassistant|script|tests)/.+\.py$
|
files: ^(homeassistant|script|tests)/.+\.py$
|
||||||
- repo: https://github.com/pre-commit/mirrors-isort
|
- repo: https://github.com/pre-commit/mirrors-isort
|
||||||
rev: v4.3.21
|
rev: v4.3.21
|
||||||
hooks:
|
hooks:
|
||||||
- id: isort
|
- id: isort
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v2.4.0
|
rev: v2.4.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-json
|
- id: check-json
|
||||||
- repo: local
|
- id: no-commit-to-branch
|
||||||
|
args:
|
||||||
|
- --branch=dev
|
||||||
|
- --branch=master
|
||||||
|
- --branch=rc
|
||||||
|
- repo: local
|
||||||
hooks:
|
hooks:
|
||||||
# Run mypy through our wrapper script in order to get the possible
|
# 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
|
# 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
|
# committing from a GUI tool that was not launched from an activated
|
||||||
# shell.
|
# shell.
|
||||||
- id: mypy
|
- id: mypy
|
||||||
name: mypy
|
name: mypy
|
||||||
entry: script/run-in-env.sh mypy
|
entry: script/run-in-env.sh mypy
|
||||||
language: script
|
language: script
|
||||||
|
17
CODEOWNERS
17
CODEOWNERS
@ -41,6 +41,7 @@ homeassistant/components/auth/* @home-assistant/core
|
|||||||
homeassistant/components/automatic/* @armills
|
homeassistant/components/automatic/* @armills
|
||||||
homeassistant/components/automation/* @home-assistant/core
|
homeassistant/components/automation/* @home-assistant/core
|
||||||
homeassistant/components/avea/* @pattyland
|
homeassistant/components/avea/* @pattyland
|
||||||
|
homeassistant/components/avri/* @timvancann
|
||||||
homeassistant/components/awair/* @danielsjf
|
homeassistant/components/awair/* @danielsjf
|
||||||
homeassistant/components/aws/* @awarecan @robbiet480
|
homeassistant/components/aws/* @awarecan @robbiet480
|
||||||
homeassistant/components/axis/* @kane610
|
homeassistant/components/axis/* @kane610
|
||||||
@ -51,6 +52,7 @@ homeassistant/components/bitcoin/* @fabaff
|
|||||||
homeassistant/components/bizkaibus/* @UgaitzEtxebarria
|
homeassistant/components/bizkaibus/* @UgaitzEtxebarria
|
||||||
homeassistant/components/blink/* @fronzbot
|
homeassistant/components/blink/* @fronzbot
|
||||||
homeassistant/components/bmw_connected_drive/* @gerard33
|
homeassistant/components/bmw_connected_drive/* @gerard33
|
||||||
|
homeassistant/components/bom/* @maddenp
|
||||||
homeassistant/components/braviatv/* @robbiet480
|
homeassistant/components/braviatv/* @robbiet480
|
||||||
homeassistant/components/broadlink/* @danielhiversen @felipediel
|
homeassistant/components/broadlink/* @danielhiversen @felipediel
|
||||||
homeassistant/components/brother/* @bieniu
|
homeassistant/components/brother/* @bieniu
|
||||||
@ -81,6 +83,7 @@ homeassistant/components/demo/* @home-assistant/core
|
|||||||
homeassistant/components/derivative/* @afaucogney
|
homeassistant/components/derivative/* @afaucogney
|
||||||
homeassistant/components/device_automation/* @home-assistant/core
|
homeassistant/components/device_automation/* @home-assistant/core
|
||||||
homeassistant/components/digital_ocean/* @fabaff
|
homeassistant/components/digital_ocean/* @fabaff
|
||||||
|
homeassistant/components/directv/* @ctalkington
|
||||||
homeassistant/components/discogs/* @thibmaek
|
homeassistant/components/discogs/* @thibmaek
|
||||||
homeassistant/components/doorbird/* @oblogic7
|
homeassistant/components/doorbird/* @oblogic7
|
||||||
homeassistant/components/dsmr_reader/* @depl0y
|
homeassistant/components/dsmr_reader/* @depl0y
|
||||||
@ -89,11 +92,13 @@ 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
|
||||||
|
homeassistant/components/edl21/* @mtdcr
|
||||||
homeassistant/components/egardia/* @jeroenterheerdt
|
homeassistant/components/egardia/* @jeroenterheerdt
|
||||||
homeassistant/components/eight_sleep/* @mezz64
|
homeassistant/components/eight_sleep/* @mezz64
|
||||||
homeassistant/components/elgato/* @frenck
|
homeassistant/components/elgato/* @frenck
|
||||||
homeassistant/components/elv/* @majuss
|
homeassistant/components/elv/* @majuss
|
||||||
homeassistant/components/emby/* @mezz64
|
homeassistant/components/emby/* @mezz64
|
||||||
|
homeassistant/components/emoncms/* @borpin
|
||||||
homeassistant/components/enigma2/* @fbradyirl
|
homeassistant/components/enigma2/* @fbradyirl
|
||||||
homeassistant/components/enocean/* @bdurrer
|
homeassistant/components/enocean/* @bdurrer
|
||||||
homeassistant/components/entur_public_transport/* @hfurubotten
|
homeassistant/components/entur_public_transport/* @hfurubotten
|
||||||
@ -104,6 +109,7 @@ homeassistant/components/eq3btsmart/* @rytilahti
|
|||||||
homeassistant/components/esphome/* @OttoWinter
|
homeassistant/components/esphome/* @OttoWinter
|
||||||
homeassistant/components/essent/* @TheLastProject
|
homeassistant/components/essent/* @TheLastProject
|
||||||
homeassistant/components/evohome/* @zxdavb
|
homeassistant/components/evohome/* @zxdavb
|
||||||
|
homeassistant/components/ezviz/* @baqs
|
||||||
homeassistant/components/fastdotcom/* @rohankapoorcom
|
homeassistant/components/fastdotcom/* @rohankapoorcom
|
||||||
homeassistant/components/file/* @fabaff
|
homeassistant/components/file/* @fabaff
|
||||||
homeassistant/components/filter/* @dgomes
|
homeassistant/components/filter/* @dgomes
|
||||||
@ -136,6 +142,7 @@ homeassistant/components/google_translate/* @awarecan
|
|||||||
homeassistant/components/google_travel_time/* @robbiet480
|
homeassistant/components/google_travel_time/* @robbiet480
|
||||||
homeassistant/components/gpsd/* @fabaff
|
homeassistant/components/gpsd/* @fabaff
|
||||||
homeassistant/components/greeneye_monitor/* @jkeljo
|
homeassistant/components/greeneye_monitor/* @jkeljo
|
||||||
|
homeassistant/components/griddy/* @bdraco
|
||||||
homeassistant/components/group/* @home-assistant/core
|
homeassistant/components/group/* @home-assistant/core
|
||||||
homeassistant/components/growatt_server/* @indykoning
|
homeassistant/components/growatt_server/* @indykoning
|
||||||
homeassistant/components/gtfs/* @robbiet480
|
homeassistant/components/gtfs/* @robbiet480
|
||||||
@ -148,7 +155,6 @@ homeassistant/components/hikvision/* @mezz64
|
|||||||
homeassistant/components/hikvisioncam/* @fbradyirl
|
homeassistant/components/hikvisioncam/* @fbradyirl
|
||||||
homeassistant/components/hisense_aehw4a1/* @bannhead
|
homeassistant/components/hisense_aehw4a1/* @bannhead
|
||||||
homeassistant/components/history/* @home-assistant/core
|
homeassistant/components/history/* @home-assistant/core
|
||||||
homeassistant/components/history_graph/* @andrey-git
|
|
||||||
homeassistant/components/hive/* @Rendili @KJonline
|
homeassistant/components/hive/* @Rendili @KJonline
|
||||||
homeassistant/components/homeassistant/* @home-assistant/core
|
homeassistant/components/homeassistant/* @home-assistant/core
|
||||||
homeassistant/components/homekit_controller/* @Jc2k
|
homeassistant/components/homekit_controller/* @Jc2k
|
||||||
@ -160,6 +166,7 @@ homeassistant/components/http/* @home-assistant/core
|
|||||||
homeassistant/components/huawei_lte/* @scop
|
homeassistant/components/huawei_lte/* @scop
|
||||||
homeassistant/components/huawei_router/* @abmantis
|
homeassistant/components/huawei_router/* @abmantis
|
||||||
homeassistant/components/hue/* @balloob
|
homeassistant/components/hue/* @balloob
|
||||||
|
homeassistant/components/iammeter/* @lewei50
|
||||||
homeassistant/components/iaqualink/* @flz
|
homeassistant/components/iaqualink/* @flz
|
||||||
homeassistant/components/icloud/* @Quentame
|
homeassistant/components/icloud/* @Quentame
|
||||||
homeassistant/components/ign_sismologia/* @exxamalte
|
homeassistant/components/ign_sismologia/* @exxamalte
|
||||||
@ -277,6 +284,7 @@ homeassistant/components/pvoutput/* @fabaff
|
|||||||
homeassistant/components/qld_bushfire/* @exxamalte
|
homeassistant/components/qld_bushfire/* @exxamalte
|
||||||
homeassistant/components/qnap/* @colinodell
|
homeassistant/components/qnap/* @colinodell
|
||||||
homeassistant/components/quantum_gateway/* @cisasteelersfan
|
homeassistant/components/quantum_gateway/* @cisasteelersfan
|
||||||
|
homeassistant/components/qvr_pro/* @oblogic7
|
||||||
homeassistant/components/qwikswitch/* @kellerza
|
homeassistant/components/qwikswitch/* @kellerza
|
||||||
homeassistant/components/rainbird/* @konikvranik
|
homeassistant/components/rainbird/* @konikvranik
|
||||||
homeassistant/components/raincloud/* @vanstinator
|
homeassistant/components/raincloud/* @vanstinator
|
||||||
@ -287,6 +295,7 @@ homeassistant/components/repetier/* @MTrab
|
|||||||
homeassistant/components/rfxtrx/* @danielhiversen
|
homeassistant/components/rfxtrx/* @danielhiversen
|
||||||
homeassistant/components/ring/* @balloob
|
homeassistant/components/ring/* @balloob
|
||||||
homeassistant/components/rmvtransport/* @cgtobi
|
homeassistant/components/rmvtransport/* @cgtobi
|
||||||
|
homeassistant/components/roku/* @ctalkington
|
||||||
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
|
||||||
@ -347,6 +356,7 @@ homeassistant/components/synology_srm/* @aerialls
|
|||||||
homeassistant/components/syslog/* @fabaff
|
homeassistant/components/syslog/* @fabaff
|
||||||
homeassistant/components/tado/* @michaelarnauts
|
homeassistant/components/tado/* @michaelarnauts
|
||||||
homeassistant/components/tahoma/* @philklei
|
homeassistant/components/tahoma/* @philklei
|
||||||
|
homeassistant/components/tankerkoenig/* @guillempages
|
||||||
homeassistant/components/tautulli/* @ludeeus
|
homeassistant/components/tautulli/* @ludeeus
|
||||||
homeassistant/components/tellduslive/* @fredrike
|
homeassistant/components/tellduslive/* @fredrike
|
||||||
homeassistant/components/template/* @PhracturedBlue @tetienne
|
homeassistant/components/template/* @PhracturedBlue @tetienne
|
||||||
@ -366,7 +376,7 @@ homeassistant/components/traccar/* @ludeeus
|
|||||||
homeassistant/components/tradfri/* @ggravlingen
|
homeassistant/components/tradfri/* @ggravlingen
|
||||||
homeassistant/components/trafikverket_train/* @endor-force
|
homeassistant/components/trafikverket_train/* @endor-force
|
||||||
homeassistant/components/transmission/* @engrbm87 @JPHutchins
|
homeassistant/components/transmission/* @engrbm87 @JPHutchins
|
||||||
homeassistant/components/tts/* @robbiet480
|
homeassistant/components/tts/* @pvizeli
|
||||||
homeassistant/components/twentemilieu/* @frenck
|
homeassistant/components/twentemilieu/* @frenck
|
||||||
homeassistant/components/twilio_call/* @robbiet480
|
homeassistant/components/twilio_call/* @robbiet480
|
||||||
homeassistant/components/twilio_sms/* @robbiet480
|
homeassistant/components/twilio_sms/* @robbiet480
|
||||||
@ -376,7 +386,7 @@ homeassistant/components/unifiled/* @florisvdk
|
|||||||
homeassistant/components/upc_connect/* @pvizeli
|
homeassistant/components/upc_connect/* @pvizeli
|
||||||
homeassistant/components/upcloud/* @scop
|
homeassistant/components/upcloud/* @scop
|
||||||
homeassistant/components/updater/* @home-assistant/core
|
homeassistant/components/updater/* @home-assistant/core
|
||||||
homeassistant/components/upnp/* @robbiet480
|
homeassistant/components/upnp/* @StevenLooman
|
||||||
homeassistant/components/uptimerobot/* @ludeeus
|
homeassistant/components/uptimerobot/* @ludeeus
|
||||||
homeassistant/components/usgs_earthquakes_feed/* @exxamalte
|
homeassistant/components/usgs_earthquakes_feed/* @exxamalte
|
||||||
homeassistant/components/utility_meter/* @dgomes
|
homeassistant/components/utility_meter/* @dgomes
|
||||||
@ -393,7 +403,6 @@ homeassistant/components/vlc_telnet/* @rodripf
|
|||||||
homeassistant/components/waqi/* @andrey-git
|
homeassistant/components/waqi/* @andrey-git
|
||||||
homeassistant/components/watson_tts/* @rutkai
|
homeassistant/components/watson_tts/* @rutkai
|
||||||
homeassistant/components/weather/* @fabaff
|
homeassistant/components/weather/* @fabaff
|
||||||
homeassistant/components/weblink/* @home-assistant/core
|
|
||||||
homeassistant/components/webostv/* @bendavid
|
homeassistant/components/webostv/* @bendavid
|
||||||
homeassistant/components/websocket_api/* @home-assistant/core
|
homeassistant/components/websocket_api/* @home-assistant/core
|
||||||
homeassistant/components/wemo/* @sqldiablo
|
homeassistant/components/wemo/* @sqldiablo
|
||||||
|
@ -148,7 +148,7 @@ stages:
|
|||||||
|
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
pytest --timeout=9 --durations=10 -n auto --dist=loadfile --cov homeassistant --cov-report html -qq -o console_output_style=count -p no:sugar tests
|
pytest --timeout=9 --durations=10 -n auto --dist=loadfile --cov homeassistant --cov-report html -qq -o console_output_style=count -p no:sugar tests
|
||||||
codecov --token $(codecovToken)
|
#codecov --token $(codecovToken)
|
||||||
script/check_dirty
|
script/check_dirty
|
||||||
displayName: 'Run pytest for python $(python.container) / coverage'
|
displayName: 'Run pytest for python $(python.container) / coverage'
|
||||||
condition: and(succeeded(), eq(variables['python.container'], variables['PythonMain']))
|
condition: and(succeeded(), eq(variables['python.container'], variables['PythonMain']))
|
||||||
|
@ -40,7 +40,7 @@ jobs:
|
|||||||
if [[ "$(Build.Reason)" =~ (Schedule|Manual) ]]; then
|
if [[ "$(Build.Reason)" =~ (Schedule|Manual) ]]; then
|
||||||
touch requirements_diff.txt
|
touch requirements_diff.txt
|
||||||
else
|
else
|
||||||
curl -s -o requirements_diff.txt https://raw.githubusercontent.com/home-assistant/home-assistant/master/requirements_all.txt
|
curl -s -o requirements_diff.txt https://raw.githubusercontent.com/home-assistant/core/master/requirements_all.txt
|
||||||
fi
|
fi
|
||||||
|
|
||||||
requirement_files="requirements_wheels.txt requirements_diff.txt"
|
requirement_files="requirements_wheels.txt requirements_diff.txt"
|
||||||
|
12
homeassistant/components/abode/.translations/lv.json
Normal file
12
homeassistant/components/abode/.translations/lv.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"password": "Parole",
|
||||||
|
"username": "E-pasta adrese"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,6 @@
|
|||||||
from asyncio import gather
|
from asyncio import gather
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import logging
|
|
||||||
|
|
||||||
from abodepy import Abode
|
from abodepy import Abode
|
||||||
from abodepy.exceptions import AbodeException
|
from abodepy.exceptions import AbodeException
|
||||||
@ -24,21 +23,13 @@ from homeassistant.helpers import config_validation as cv
|
|||||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
from .const import (
|
from .const import ATTRIBUTION, DEFAULT_CACHEDB, DOMAIN, LOGGER
|
||||||
ATTRIBUTION,
|
|
||||||
DEFAULT_CACHEDB,
|
|
||||||
DOMAIN,
|
|
||||||
SIGNAL_CAPTURE_IMAGE,
|
|
||||||
SIGNAL_TRIGGER_QUICK_ACTION,
|
|
||||||
)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
CONF_POLLING = "polling"
|
CONF_POLLING = "polling"
|
||||||
|
|
||||||
SERVICE_SETTINGS = "change_setting"
|
SERVICE_SETTINGS = "change_setting"
|
||||||
SERVICE_CAPTURE_IMAGE = "capture_image"
|
SERVICE_CAPTURE_IMAGE = "capture_image"
|
||||||
SERVICE_TRIGGER = "trigger_quick_action"
|
SERVICE_TRIGGER_AUTOMATION = "trigger_automation"
|
||||||
|
|
||||||
ATTR_DEVICE_ID = "device_id"
|
ATTR_DEVICE_ID = "device_id"
|
||||||
ATTR_DEVICE_NAME = "device_name"
|
ATTR_DEVICE_NAME = "device_name"
|
||||||
@ -53,8 +44,6 @@ ATTR_APP_TYPE = "app_type"
|
|||||||
ATTR_EVENT_BY = "event_by"
|
ATTR_EVENT_BY = "event_by"
|
||||||
ATTR_VALUE = "value"
|
ATTR_VALUE = "value"
|
||||||
|
|
||||||
ABODE_DEVICE_ID_LIST_SCHEMA = vol.Schema([str])
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema(
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
DOMAIN: vol.Schema(
|
DOMAIN: vol.Schema(
|
||||||
@ -74,7 +63,7 @@ CHANGE_SETTING_SCHEMA = vol.Schema(
|
|||||||
|
|
||||||
CAPTURE_IMAGE_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
|
CAPTURE_IMAGE_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
|
||||||
|
|
||||||
TRIGGER_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
|
AUTOMATION_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
|
||||||
|
|
||||||
ABODE_PLATFORMS = [
|
ABODE_PLATFORMS = [
|
||||||
"alarm_control_panel",
|
"alarm_control_panel",
|
||||||
@ -93,7 +82,6 @@ class AbodeSystem:
|
|||||||
|
|
||||||
def __init__(self, abode, polling):
|
def __init__(self, abode, polling):
|
||||||
"""Initialize the system."""
|
"""Initialize the system."""
|
||||||
|
|
||||||
self.abode = abode
|
self.abode = abode
|
||||||
self.polling = polling
|
self.polling = polling
|
||||||
self.entity_ids = set()
|
self.entity_ids = set()
|
||||||
@ -130,7 +118,7 @@ async def async_setup_entry(hass, config_entry):
|
|||||||
hass.data[DOMAIN] = AbodeSystem(abode, polling)
|
hass.data[DOMAIN] = AbodeSystem(abode, polling)
|
||||||
|
|
||||||
except (AbodeException, ConnectTimeout, HTTPError) as ex:
|
except (AbodeException, ConnectTimeout, HTTPError) as ex:
|
||||||
_LOGGER.error("Unable to connect to Abode: %s", str(ex))
|
LOGGER.error("Unable to connect to Abode: %s", str(ex))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
for platform in ABODE_PLATFORMS:
|
for platform in ABODE_PLATFORMS:
|
||||||
@ -149,7 +137,7 @@ async def async_unload_entry(hass, config_entry):
|
|||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
hass.services.async_remove(DOMAIN, SERVICE_SETTINGS)
|
hass.services.async_remove(DOMAIN, SERVICE_SETTINGS)
|
||||||
hass.services.async_remove(DOMAIN, SERVICE_CAPTURE_IMAGE)
|
hass.services.async_remove(DOMAIN, SERVICE_CAPTURE_IMAGE)
|
||||||
hass.services.async_remove(DOMAIN, SERVICE_TRIGGER)
|
hass.services.async_remove(DOMAIN, SERVICE_TRIGGER_AUTOMATION)
|
||||||
|
|
||||||
tasks = []
|
tasks = []
|
||||||
|
|
||||||
@ -180,7 +168,7 @@ def setup_hass_services(hass):
|
|||||||
try:
|
try:
|
||||||
hass.data[DOMAIN].abode.set_setting(setting, value)
|
hass.data[DOMAIN].abode.set_setting(setting, value)
|
||||||
except AbodeException as ex:
|
except AbodeException as ex:
|
||||||
_LOGGER.warning(ex)
|
LOGGER.warning(ex)
|
||||||
|
|
||||||
def capture_image(call):
|
def capture_image(call):
|
||||||
"""Capture a new image."""
|
"""Capture a new image."""
|
||||||
@ -193,11 +181,11 @@ def setup_hass_services(hass):
|
|||||||
]
|
]
|
||||||
|
|
||||||
for entity_id in target_entities:
|
for entity_id in target_entities:
|
||||||
signal = SIGNAL_CAPTURE_IMAGE.format(entity_id)
|
signal = f"abode_camera_capture_{entity_id}"
|
||||||
dispatcher_send(hass, signal)
|
dispatcher_send(hass, signal)
|
||||||
|
|
||||||
def trigger_quick_action(call):
|
def trigger_automation(call):
|
||||||
"""Trigger a quick action."""
|
"""Trigger an Abode automation."""
|
||||||
entity_ids = call.data.get(ATTR_ENTITY_ID, None)
|
entity_ids = call.data.get(ATTR_ENTITY_ID, None)
|
||||||
|
|
||||||
target_entities = [
|
target_entities = [
|
||||||
@ -207,7 +195,7 @@ def setup_hass_services(hass):
|
|||||||
]
|
]
|
||||||
|
|
||||||
for entity_id in target_entities:
|
for entity_id in target_entities:
|
||||||
signal = SIGNAL_TRIGGER_QUICK_ACTION.format(entity_id)
|
signal = f"abode_trigger_automation_{entity_id}"
|
||||||
dispatcher_send(hass, signal)
|
dispatcher_send(hass, signal)
|
||||||
|
|
||||||
hass.services.register(
|
hass.services.register(
|
||||||
@ -219,7 +207,7 @@ def setup_hass_services(hass):
|
|||||||
)
|
)
|
||||||
|
|
||||||
hass.services.register(
|
hass.services.register(
|
||||||
DOMAIN, SERVICE_TRIGGER, trigger_quick_action, schema=TRIGGER_SCHEMA
|
DOMAIN, SERVICE_TRIGGER_AUTOMATION, trigger_automation, schema=AUTOMATION_SCHEMA
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -232,7 +220,7 @@ async def setup_hass_events(hass):
|
|||||||
hass.data[DOMAIN].abode.events.stop()
|
hass.data[DOMAIN].abode.events.stop()
|
||||||
|
|
||||||
hass.data[DOMAIN].abode.logout()
|
hass.data[DOMAIN].abode.logout()
|
||||||
_LOGGER.info("Logged out of Abode")
|
LOGGER.info("Logged out of Abode")
|
||||||
|
|
||||||
if not hass.data[DOMAIN].polling:
|
if not hass.data[DOMAIN].polling:
|
||||||
await hass.async_add_executor_job(hass.data[DOMAIN].abode.events.start)
|
await hass.async_add_executor_job(hass.data[DOMAIN].abode.events.start)
|
||||||
@ -390,11 +378,14 @@ class AbodeAutomation(Entity):
|
|||||||
"""Return the state attributes."""
|
"""Return the state attributes."""
|
||||||
return {
|
return {
|
||||||
ATTR_ATTRIBUTION: ATTRIBUTION,
|
ATTR_ATTRIBUTION: ATTRIBUTION,
|
||||||
"automation_id": self._automation.automation_id,
|
"type": "CUE automation",
|
||||||
"type": self._automation.type,
|
|
||||||
"sub_type": self._automation.sub_type,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self):
|
||||||
|
"""Return a unique ID to use for this automation."""
|
||||||
|
return self._automation.automation_id
|
||||||
|
|
||||||
def _update_callback(self, device):
|
def _update_callback(self, device):
|
||||||
"""Update the automation state."""
|
"""Update the automation state."""
|
||||||
self._automation.refresh()
|
self._automation.refresh()
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
"""Support for Abode Security System alarm control panels."""
|
"""Support for Abode Security System alarm control panels."""
|
||||||
import logging
|
|
||||||
|
|
||||||
import homeassistant.components.alarm_control_panel as alarm
|
import homeassistant.components.alarm_control_panel as alarm
|
||||||
from homeassistant.components.alarm_control_panel.const import (
|
from homeassistant.components.alarm_control_panel.const import (
|
||||||
SUPPORT_ALARM_ARM_AWAY,
|
SUPPORT_ALARM_ARM_AWAY,
|
||||||
@ -16,8 +14,6 @@ from homeassistant.const import (
|
|||||||
from . import AbodeDevice
|
from . import AbodeDevice
|
||||||
from .const import ATTRIBUTION, DOMAIN
|
from .const import ATTRIBUTION, DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
ICON = "mdi:security"
|
ICON = "mdi:security"
|
||||||
|
|
||||||
|
|
||||||
@ -50,6 +46,11 @@ class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanel):
|
|||||||
state = None
|
state = None
|
||||||
return state
|
return state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def code_arm_required(self):
|
||||||
|
"""Whether the code is required for arm actions."""
|
||||||
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_features(self) -> int:
|
def supported_features(self) -> int:
|
||||||
"""Return the list of supported features."""
|
"""Return the list of supported features."""
|
||||||
|
@ -1,16 +1,10 @@
|
|||||||
"""Support for Abode Security System binary sensors."""
|
"""Support for Abode Security System binary sensors."""
|
||||||
import logging
|
|
||||||
|
|
||||||
import abodepy.helpers.constants as CONST
|
import abodepy.helpers.constants as CONST
|
||||||
import abodepy.helpers.timeline as TIMELINE
|
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|
||||||
|
|
||||||
from . import AbodeAutomation, AbodeDevice
|
from . import AbodeDevice
|
||||||
from .const import DOMAIN, SIGNAL_TRIGGER_QUICK_ACTION
|
from .const import DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
@ -30,13 +24,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
|||||||
for device in data.abode.get_devices(generic_type=device_types):
|
for device in data.abode.get_devices(generic_type=device_types):
|
||||||
entities.append(AbodeBinarySensor(data, device))
|
entities.append(AbodeBinarySensor(data, device))
|
||||||
|
|
||||||
for automation in data.abode.get_automations(generic_type=CONST.TYPE_QUICK_ACTION):
|
|
||||||
entities.append(
|
|
||||||
AbodeQuickActionBinarySensor(
|
|
||||||
data, automation, TIMELINE.AUTOMATION_EDIT_GROUP
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
@ -52,22 +39,3 @@ class AbodeBinarySensor(AbodeDevice, BinarySensorDevice):
|
|||||||
def device_class(self):
|
def device_class(self):
|
||||||
"""Return the class of the binary sensor."""
|
"""Return the class of the binary sensor."""
|
||||||
return self._device.generic_type
|
return self._device.generic_type
|
||||||
|
|
||||||
|
|
||||||
class AbodeQuickActionBinarySensor(AbodeAutomation, BinarySensorDevice):
|
|
||||||
"""A binary sensor implementation for Abode quick action automations."""
|
|
||||||
|
|
||||||
async def async_added_to_hass(self):
|
|
||||||
"""Subscribe Abode events."""
|
|
||||||
await super().async_added_to_hass()
|
|
||||||
signal = SIGNAL_TRIGGER_QUICK_ACTION.format(self.entity_id)
|
|
||||||
async_dispatcher_connect(self.hass, signal, self.trigger)
|
|
||||||
|
|
||||||
def trigger(self):
|
|
||||||
"""Trigger a quick automation."""
|
|
||||||
self._automation.trigger()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_on(self):
|
|
||||||
"""Return True if the binary sensor is on."""
|
|
||||||
return self._automation.is_active
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
"""Support for Abode Security System cameras."""
|
"""Support for Abode Security System cameras."""
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
|
||||||
|
|
||||||
import abodepy.helpers.constants as CONST
|
import abodepy.helpers.constants as CONST
|
||||||
import abodepy.helpers.timeline as TIMELINE
|
import abodepy.helpers.timeline as TIMELINE
|
||||||
@ -11,12 +10,10 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
from . import AbodeDevice
|
from . import AbodeDevice
|
||||||
from .const import DOMAIN, SIGNAL_CAPTURE_IMAGE
|
from .const import DOMAIN, LOGGER
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90)
|
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
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 camera devices."""
|
"""Set up Abode camera devices."""
|
||||||
@ -50,7 +47,7 @@ class AbodeCamera(AbodeDevice, Camera):
|
|||||||
self._capture_callback,
|
self._capture_callback,
|
||||||
)
|
)
|
||||||
|
|
||||||
signal = SIGNAL_CAPTURE_IMAGE.format(self.entity_id)
|
signal = f"abode_camera_capture_{self.entity_id}"
|
||||||
async_dispatcher_connect(self.hass, signal, self.capture)
|
async_dispatcher_connect(self.hass, signal, self.capture)
|
||||||
|
|
||||||
def capture(self):
|
def capture(self):
|
||||||
@ -71,7 +68,7 @@ class AbodeCamera(AbodeDevice, Camera):
|
|||||||
|
|
||||||
self._response.raise_for_status()
|
self._response.raise_for_status()
|
||||||
except requests.HTTPError as err:
|
except requests.HTTPError as err:
|
||||||
_LOGGER.warning("Failed to get camera image: %s", err)
|
LOGGER.warning("Failed to get camera image: %s", err)
|
||||||
self._response = None
|
self._response = None
|
||||||
else:
|
else:
|
||||||
self._response = None
|
self._response = None
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
"""Config flow for the Abode Security System component."""
|
"""Config flow for the Abode Security System component."""
|
||||||
import logging
|
|
||||||
|
|
||||||
from abodepy import Abode
|
from abodepy import Abode
|
||||||
from abodepy.exceptions import AbodeException
|
from abodepy.exceptions import AbodeException
|
||||||
from requests.exceptions import ConnectTimeout, HTTPError
|
from requests.exceptions import ConnectTimeout, HTTPError
|
||||||
@ -10,12 +8,10 @@ from homeassistant import config_entries
|
|||||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
|
|
||||||
from .const import DEFAULT_CACHEDB, DOMAIN # pylint: disable=unused-import
|
from .const import DEFAULT_CACHEDB, DOMAIN, LOGGER # pylint: disable=unused-import
|
||||||
|
|
||||||
CONF_POLLING = "polling"
|
CONF_POLLING = "polling"
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
"""Config flow for Abode."""
|
"""Config flow for Abode."""
|
||||||
@ -32,7 +28,6 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
async def async_step_user(self, user_input=None):
|
async def async_step_user(self, user_input=None):
|
||||||
"""Handle a flow initialized by the user."""
|
"""Handle a flow initialized by the user."""
|
||||||
|
|
||||||
if self._async_current_entries():
|
if self._async_current_entries():
|
||||||
return self.async_abort(reason="single_instance_allowed")
|
return self.async_abort(reason="single_instance_allowed")
|
||||||
|
|
||||||
@ -50,7 +45,7 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
)
|
)
|
||||||
|
|
||||||
except (AbodeException, ConnectTimeout, HTTPError) as ex:
|
except (AbodeException, ConnectTimeout, HTTPError) as ex:
|
||||||
_LOGGER.error("Unable to connect to Abode: %s", str(ex))
|
LOGGER.error("Unable to connect to Abode: %s", str(ex))
|
||||||
if ex.errcode == 400:
|
if ex.errcode == 400:
|
||||||
return self._show_form({"base": "invalid_credentials"})
|
return self._show_form({"base": "invalid_credentials"})
|
||||||
return self._show_form({"base": "connection_error"})
|
return self._show_form({"base": "connection_error"})
|
||||||
@ -76,7 +71,7 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
async def async_step_import(self, import_config):
|
async def async_step_import(self, import_config):
|
||||||
"""Import a config entry from configuration.yaml."""
|
"""Import a config entry from configuration.yaml."""
|
||||||
if self._async_current_entries():
|
if self._async_current_entries():
|
||||||
_LOGGER.warning("Only one configuration of abode is allowed.")
|
LOGGER.warning("Only one configuration of abode is allowed.")
|
||||||
return self.async_abort(reason="single_instance_allowed")
|
return self.async_abort(reason="single_instance_allowed")
|
||||||
|
|
||||||
return await self.async_step_user(import_config)
|
return await self.async_step_user(import_config)
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
"""Constants for the Abode Security System component."""
|
"""Constants for the Abode Security System component."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
LOGGER = logging.getLogger(__package__)
|
||||||
|
|
||||||
DOMAIN = "abode"
|
DOMAIN = "abode"
|
||||||
ATTRIBUTION = "Data provided by goabode.com"
|
ATTRIBUTION = "Data provided by goabode.com"
|
||||||
|
|
||||||
DEFAULT_CACHEDB = "abodepy_cache.pickle"
|
DEFAULT_CACHEDB = "abodepy_cache.pickle"
|
||||||
|
|
||||||
SIGNAL_CAPTURE_IMAGE = "abode_camera_capture_{}"
|
|
||||||
SIGNAL_TRIGGER_QUICK_ACTION = "abode_trigger_quick_action_{}"
|
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
"""Support for Abode Security System covers."""
|
"""Support for Abode Security System covers."""
|
||||||
import logging
|
|
||||||
|
|
||||||
import abodepy.helpers.constants as CONST
|
import abodepy.helpers.constants as CONST
|
||||||
|
|
||||||
from homeassistant.components.cover import CoverDevice
|
from homeassistant.components.cover import CoverDevice
|
||||||
@ -8,8 +6,6 @@ from homeassistant.components.cover import CoverDevice
|
|||||||
from . import AbodeDevice
|
from . import AbodeDevice
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
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 cover devices."""
|
"""Set up Abode cover devices."""
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
"""Support for Abode Security System lights."""
|
"""Support for Abode Security System lights."""
|
||||||
import logging
|
|
||||||
from math import ceil
|
from math import ceil
|
||||||
|
|
||||||
import abodepy.helpers.constants as CONST
|
import abodepy.helpers.constants as CONST
|
||||||
@ -21,8 +20,6 @@ from homeassistant.util.color import (
|
|||||||
from . import AbodeDevice
|
from . import AbodeDevice
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
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 light devices."""
|
"""Set up Abode light devices."""
|
||||||
@ -45,16 +42,19 @@ class AbodeLight(AbodeDevice, Light):
|
|||||||
self._device.set_color_temp(
|
self._device.set_color_temp(
|
||||||
int(color_temperature_mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]))
|
int(color_temperature_mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]))
|
||||||
)
|
)
|
||||||
|
return
|
||||||
|
|
||||||
if ATTR_HS_COLOR in kwargs and self._device.is_color_capable:
|
if ATTR_HS_COLOR in kwargs and self._device.is_color_capable:
|
||||||
self._device.set_color(kwargs[ATTR_HS_COLOR])
|
self._device.set_color(kwargs[ATTR_HS_COLOR])
|
||||||
|
return
|
||||||
|
|
||||||
if ATTR_BRIGHTNESS in kwargs and self._device.is_dimmable:
|
if ATTR_BRIGHTNESS in kwargs and self._device.is_dimmable:
|
||||||
# Convert Home Assistant brightness (0-255) to Abode brightness (0-99)
|
# Convert Home Assistant brightness (0-255) to Abode brightness (0-99)
|
||||||
# If 100 is sent to Abode, response is 99 causing an error
|
# If 100 is sent to Abode, response is 99 causing an error
|
||||||
self._device.set_level(ceil(kwargs[ATTR_BRIGHTNESS] * 99 / 255.0))
|
self._device.set_level(ceil(kwargs[ATTR_BRIGHTNESS] * 99 / 255.0))
|
||||||
else:
|
return
|
||||||
self._device.switch_on()
|
|
||||||
|
self._device.switch_on()
|
||||||
|
|
||||||
def turn_off(self, **kwargs):
|
def turn_off(self, **kwargs):
|
||||||
"""Turn off the light."""
|
"""Turn off the light."""
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
"""Support for the Abode Security System locks."""
|
"""Support for the Abode Security System locks."""
|
||||||
import logging
|
|
||||||
|
|
||||||
import abodepy.helpers.constants as CONST
|
import abodepy.helpers.constants as CONST
|
||||||
|
|
||||||
from homeassistant.components.lock import LockDevice
|
from homeassistant.components.lock import LockDevice
|
||||||
@ -8,8 +6,6 @@ from homeassistant.components.lock import LockDevice
|
|||||||
from . import AbodeDevice
|
from . import AbodeDevice
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
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 lock devices."""
|
"""Set up Abode lock devices."""
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "Abode",
|
"name": "Abode",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/abode",
|
"documentation": "https://www.home-assistant.io/integrations/abode",
|
||||||
"requirements": ["abodepy==0.17.0"],
|
"requirements": ["abodepy==0.18.1"],
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"codeowners": ["@shred86"]
|
"codeowners": ["@shred86"]
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
"""Support for Abode Security System sensors."""
|
"""Support for Abode Security System sensors."""
|
||||||
import logging
|
|
||||||
|
|
||||||
import abodepy.helpers.constants as CONST
|
import abodepy.helpers.constants as CONST
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -12,8 +10,6 @@ from homeassistant.const import (
|
|||||||
from . import AbodeDevice
|
from . import AbodeDevice
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# Sensor types: Name, icon
|
# Sensor types: Name, icon
|
||||||
SENSOR_TYPES = {
|
SENSOR_TYPES = {
|
||||||
CONST.TEMP_STATUS_KEY: ["Temperature", DEVICE_CLASS_TEMPERATURE],
|
CONST.TEMP_STATUS_KEY: ["Temperature", DEVICE_CLASS_TEMPERATURE],
|
||||||
@ -44,9 +40,7 @@ class AbodeSensor(AbodeDevice):
|
|||||||
"""Initialize a sensor for an Abode device."""
|
"""Initialize a sensor for an Abode device."""
|
||||||
super().__init__(data, device)
|
super().__init__(data, device)
|
||||||
self._sensor_type = sensor_type
|
self._sensor_type = sensor_type
|
||||||
self._name = "{0} {1}".format(
|
self._name = f"{self._device.name} {SENSOR_TYPES[self._sensor_type][0]}"
|
||||||
self._device.name, SENSOR_TYPES[self._sensor_type][0]
|
|
||||||
)
|
|
||||||
self._device_class = SENSOR_TYPES[self._sensor_type][1]
|
self._device_class = SENSOR_TYPES[self._sensor_type][1]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -7,7 +7,7 @@ change_setting:
|
|||||||
fields:
|
fields:
|
||||||
setting: {description: Setting to change., example: beeper_mute}
|
setting: {description: Setting to change., example: beeper_mute}
|
||||||
value: {description: Value of the setting., example: '1'}
|
value: {description: Value of the setting., example: '1'}
|
||||||
trigger_quick_action:
|
trigger_automation:
|
||||||
description: Trigger an Abode quick action.
|
description: Trigger an Abode automation.
|
||||||
fields:
|
fields:
|
||||||
entity_id: {description: Entity id of the quick action to trigger., example: binary_sensor.home_quick_action}
|
entity_id: {description: Entity id of the automation to trigger., example: switch.my_automation}
|
@ -1,18 +1,17 @@
|
|||||||
"""Support for Abode Security System switches."""
|
"""Support for Abode Security System switches."""
|
||||||
import logging
|
|
||||||
|
|
||||||
import abodepy.helpers.constants as CONST
|
import abodepy.helpers.constants as CONST
|
||||||
import abodepy.helpers.timeline as TIMELINE
|
import abodepy.helpers.timeline as TIMELINE
|
||||||
|
|
||||||
from homeassistant.components.switch import SwitchDevice
|
from homeassistant.components.switch import SwitchDevice
|
||||||
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
|
|
||||||
from . import AbodeAutomation, AbodeDevice
|
from . import AbodeAutomation, AbodeDevice
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
DEVICE_TYPES = [CONST.TYPE_SWITCH, CONST.TYPE_VALVE]
|
DEVICE_TYPES = [CONST.TYPE_SWITCH, CONST.TYPE_VALVE]
|
||||||
|
|
||||||
|
ICON = "mdi:robot"
|
||||||
|
|
||||||
|
|
||||||
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."""
|
||||||
@ -24,7 +23,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
|||||||
for device in data.abode.get_devices(generic_type=device_type):
|
for device in data.abode.get_devices(generic_type=device_type):
|
||||||
entities.append(AbodeSwitch(data, device))
|
entities.append(AbodeSwitch(data, device))
|
||||||
|
|
||||||
for automation in data.abode.get_automations(generic_type=CONST.TYPE_AUTOMATION):
|
for automation in data.abode.get_automations():
|
||||||
entities.append(
|
entities.append(
|
||||||
AbodeAutomationSwitch(data, automation, TIMELINE.AUTOMATION_EDIT_GROUP)
|
AbodeAutomationSwitch(data, automation, TIMELINE.AUTOMATION_EDIT_GROUP)
|
||||||
)
|
)
|
||||||
@ -52,15 +51,33 @@ class AbodeSwitch(AbodeDevice, SwitchDevice):
|
|||||||
class AbodeAutomationSwitch(AbodeAutomation, SwitchDevice):
|
class AbodeAutomationSwitch(AbodeAutomation, SwitchDevice):
|
||||||
"""A switch implementation for Abode automations."""
|
"""A switch implementation for Abode automations."""
|
||||||
|
|
||||||
|
async def async_added_to_hass(self):
|
||||||
|
"""Subscribe Abode events."""
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
|
||||||
|
signal = f"abode_trigger_automation_{self.entity_id}"
|
||||||
|
async_dispatcher_connect(self.hass, signal, self.trigger)
|
||||||
|
|
||||||
def turn_on(self, **kwargs):
|
def turn_on(self, **kwargs):
|
||||||
"""Turn on the device."""
|
"""Enable the automation."""
|
||||||
self._automation.set_active(True)
|
if self._automation.enable(True):
|
||||||
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def turn_off(self, **kwargs):
|
def turn_off(self, **kwargs):
|
||||||
"""Turn off the device."""
|
"""Disable the automation."""
|
||||||
self._automation.set_active(False)
|
if self._automation.enable(False):
|
||||||
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
|
def trigger(self):
|
||||||
|
"""Trigger the automation."""
|
||||||
|
self._automation.trigger()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
"""Return True if the binary sensor is on."""
|
"""Return True if the automation is enabled."""
|
||||||
return self._automation.is_active
|
return self._automation.is_enabled
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self):
|
||||||
|
"""Return the robot icon to match Home Assistant automations."""
|
||||||
|
return ICON
|
||||||
|
@ -11,6 +11,7 @@ from homeassistant.components.adguard.const import (
|
|||||||
DOMAIN,
|
DOMAIN,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import TIME_MILLISECONDS, UNIT_PERCENTAGE
|
||||||
from homeassistant.exceptions import PlatformNotReady
|
from homeassistant.exceptions import PlatformNotReady
|
||||||
from homeassistant.helpers.typing import HomeAssistantType
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
|
|
||||||
@ -133,7 +134,7 @@ class AdGuardHomePercentageBlockedSensor(AdGuardHomeSensor):
|
|||||||
"AdGuard DNS Queries Blocked Ratio",
|
"AdGuard DNS Queries Blocked Ratio",
|
||||||
"mdi:magnify-close",
|
"mdi:magnify-close",
|
||||||
"blocked_percentage",
|
"blocked_percentage",
|
||||||
"%",
|
UNIT_PERCENTAGE,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _adguard_update(self) -> None:
|
async def _adguard_update(self) -> None:
|
||||||
@ -206,7 +207,7 @@ class AdGuardHomeAverageProcessingTimeSensor(AdGuardHomeSensor):
|
|||||||
"AdGuard Average Processing Speed",
|
"AdGuard Average Processing Speed",
|
||||||
"mdi:speedometer",
|
"mdi:speedometer",
|
||||||
"average_speed",
|
"average_speed",
|
||||||
"ms",
|
TIME_MILLISECONDS,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _adguard_update(self) -> None:
|
async def _adguard_update(self) -> None:
|
||||||
|
@ -2,12 +2,14 @@
|
|||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ATTRIBUTION,
|
ATTR_ATTRIBUTION,
|
||||||
ATTR_DEVICE_CLASS,
|
ATTR_DEVICE_CLASS,
|
||||||
|
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
DEVICE_CLASS_HUMIDITY,
|
DEVICE_CLASS_HUMIDITY,
|
||||||
DEVICE_CLASS_PRESSURE,
|
DEVICE_CLASS_PRESSURE,
|
||||||
DEVICE_CLASS_TEMPERATURE,
|
DEVICE_CLASS_TEMPERATURE,
|
||||||
PRESSURE_HPA,
|
PRESSURE_HPA,
|
||||||
TEMP_CELSIUS,
|
TEMP_CELSIUS,
|
||||||
|
UNIT_PERCENTAGE,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
@ -26,21 +28,18 @@ ATTR_ICON = "icon"
|
|||||||
ATTR_LABEL = "label"
|
ATTR_LABEL = "label"
|
||||||
ATTR_UNIT = "unit"
|
ATTR_UNIT = "unit"
|
||||||
|
|
||||||
HUMI_PERCENT = "%"
|
|
||||||
VOLUME_MICROGRAMS_PER_CUBIC_METER = "µg/m³"
|
|
||||||
|
|
||||||
SENSOR_TYPES = {
|
SENSOR_TYPES = {
|
||||||
ATTR_API_PM1: {
|
ATTR_API_PM1: {
|
||||||
ATTR_DEVICE_CLASS: None,
|
ATTR_DEVICE_CLASS: None,
|
||||||
ATTR_ICON: "mdi:blur",
|
ATTR_ICON: "mdi:blur",
|
||||||
ATTR_LABEL: ATTR_API_PM1,
|
ATTR_LABEL: ATTR_API_PM1,
|
||||||
ATTR_UNIT: VOLUME_MICROGRAMS_PER_CUBIC_METER,
|
ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
},
|
},
|
||||||
ATTR_API_HUMIDITY: {
|
ATTR_API_HUMIDITY: {
|
||||||
ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY,
|
ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY,
|
||||||
ATTR_ICON: None,
|
ATTR_ICON: None,
|
||||||
ATTR_LABEL: ATTR_API_HUMIDITY.capitalize(),
|
ATTR_LABEL: ATTR_API_HUMIDITY.capitalize(),
|
||||||
ATTR_UNIT: HUMI_PERCENT,
|
ATTR_UNIT: UNIT_PERCENTAGE,
|
||||||
},
|
},
|
||||||
ATTR_API_PRESSURE: {
|
ATTR_API_PRESSURE: {
|
||||||
ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE,
|
ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE,
|
||||||
|
23
homeassistant/components/airvisual/.translations/ca.json
Normal file
23
homeassistant/components/airvisual/.translations/ca.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Aquesta clau API ja est\u00e0 sent utilitzada."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"invalid_api_key": "Clau API inv\u00e0lida"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"api_key": "Clau API",
|
||||||
|
"latitude": "Latitud",
|
||||||
|
"longitude": "Longitud",
|
||||||
|
"show_on_map": "Mostra al mapa l'\u00e0rea geogr\u00e0fica monitoritzada"
|
||||||
|
},
|
||||||
|
"description": "Monitoritzaci\u00f3 de la qualitat de l'aire per ubicaci\u00f3 geogr\u00e0fica.",
|
||||||
|
"title": "Configura AirVisual"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "AirVisual"
|
||||||
|
}
|
||||||
|
}
|
21
homeassistant/components/airvisual/.translations/de.json
Normal file
21
homeassistant/components/airvisual/.translations/de.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Dieser API-Schl\u00fcssel wird bereits verwendet."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"api_key": "API-Schl\u00fcssel",
|
||||||
|
"latitude": "Breitengrad",
|
||||||
|
"longitude": "L\u00e4ngengrad"
|
||||||
|
},
|
||||||
|
"title": "Konfigurieren Sie AirVisual"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "AirVisual"
|
||||||
|
}
|
||||||
|
}
|
33
homeassistant/components/airvisual/.translations/en.json
Normal file
33
homeassistant/components/airvisual/.translations/en.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "This API key is already in use."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"invalid_api_key": "Invalid API key"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"api_key": "API Key",
|
||||||
|
"latitude": "Latitude",
|
||||||
|
"longitude": "Longitude"
|
||||||
|
},
|
||||||
|
"description": "Monitor air quality in a geographical location.",
|
||||||
|
"title": "Configure AirVisual"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "AirVisual"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"show_on_map": "Show monitored geography on the map"
|
||||||
|
},
|
||||||
|
"description": "Set various options for the AirVisual integration.",
|
||||||
|
"title": "Configure AirVisual"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
homeassistant/components/airvisual/.translations/es.json
Normal file
23
homeassistant/components/airvisual/.translations/es.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Esta clave API ya est\u00e1 en uso."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"invalid_api_key": "Clave API inv\u00e1lida"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"api_key": "Clave API",
|
||||||
|
"latitude": "Latitud",
|
||||||
|
"longitude": "Longitud",
|
||||||
|
"show_on_map": "Mostrar geograf\u00eda monitorizada en el mapa"
|
||||||
|
},
|
||||||
|
"description": "Monitorizar la calidad del aire en una ubicaci\u00f3n geogr\u00e1fica.",
|
||||||
|
"title": "Configurar AirVisual"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "AirVisual"
|
||||||
|
}
|
||||||
|
}
|
23
homeassistant/components/airvisual/.translations/it.json
Normal file
23
homeassistant/components/airvisual/.translations/it.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Questa chiave API \u00e8 gi\u00e0 in uso."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"invalid_api_key": "Chiave API non valida"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"api_key": "Chiave API",
|
||||||
|
"latitude": "Latitudine",
|
||||||
|
"longitude": "Logitudine",
|
||||||
|
"show_on_map": "Mostra l'area geografica monitorata sulla mappa"
|
||||||
|
},
|
||||||
|
"description": "Monitorare la qualit\u00e0 dell'aria in una posizione geografica.",
|
||||||
|
"title": "Configura AirVisual"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "AirVisual"
|
||||||
|
}
|
||||||
|
}
|
21
homeassistant/components/airvisual/.translations/lb.json
Normal file
21
homeassistant/components/airvisual/.translations/lb.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "D\u00ebsen App Schl\u00ebssel g\u00ebtt scho benotzt"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"invalid_api_key": "Ong\u00ebltegen API Schl\u00ebssel"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"api_key": "API Schl\u00ebssel",
|
||||||
|
"latitude": "Breedegrad",
|
||||||
|
"longitude": "L\u00e4ngegrad"
|
||||||
|
},
|
||||||
|
"title": "AirVisual konfigur\u00e9ieren"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "AirVisual"
|
||||||
|
}
|
||||||
|
}
|
23
homeassistant/components/airvisual/.translations/no.json
Normal file
23
homeassistant/components/airvisual/.translations/no.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Denne API-n\u00f8kkelen er allerede i bruk."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"invalid_api_key": "Ugyldig API-n\u00f8kkel"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"api_key": "API-n\u00f8kkel",
|
||||||
|
"latitude": "Breddegrad",
|
||||||
|
"longitude": "Lengdegrad",
|
||||||
|
"show_on_map": "Vis overv\u00e5ket geografi p\u00e5 kartet"
|
||||||
|
},
|
||||||
|
"description": "Overv\u00e5k luftkvaliteten p\u00e5 et geografisk sted.",
|
||||||
|
"title": "Konfigurer AirVisual"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "AirVisual"
|
||||||
|
}
|
||||||
|
}
|
23
homeassistant/components/airvisual/.translations/ru.json
Normal file
23
homeassistant/components/airvisual/.translations/ru.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "\u042d\u0442\u043e\u0442 \u043a\u043b\u044e\u0447 API \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API."
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"api_key": "\u041a\u043b\u044e\u0447 API",
|
||||||
|
"latitude": "\u0428\u0438\u0440\u043e\u0442\u0430",
|
||||||
|
"longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430",
|
||||||
|
"show_on_map": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0435\u043c\u0443\u044e \u043e\u0431\u043b\u0430\u0441\u0442\u044c \u043d\u0430 \u043a\u0430\u0440\u0442\u0435"
|
||||||
|
},
|
||||||
|
"description": "\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u0438\u0440\u0443\u0439\u0442\u0435 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u043e \u0432\u043e\u0437\u0434\u0443\u0445\u0430 \u0432 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u043c \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0438.",
|
||||||
|
"title": "AirVisual"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "AirVisual"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "\u6b64 API \u5bc6\u9470\u5df2\u88ab\u4f7f\u7528\u3002"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"invalid_api_key": "API \u5bc6\u78bc\u7121\u6548"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"api_key": "API \u5bc6\u9470",
|
||||||
|
"latitude": "\u7def\u5ea6",
|
||||||
|
"longitude": "\u7d93\u5ea6",
|
||||||
|
"show_on_map": "\u65bc\u5730\u5716\u4e0a\u986f\u793a\u76e3\u63a7\u4f4d\u7f6e\u3002"
|
||||||
|
},
|
||||||
|
"description": "\u4f9d\u5730\u7406\u4f4d\u7f6e\u76e3\u63a7\u7a7a\u6c23\u54c1\u8cea\u3002",
|
||||||
|
"title": "\u8a2d\u5b9a AirVisual"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "AirVisual"
|
||||||
|
}
|
||||||
|
}
|
@ -1 +1,215 @@
|
|||||||
"""The airvisual component."""
|
"""The airvisual component."""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from pyairvisual import Client
|
||||||
|
from pyairvisual.errors import AirVisualError, InvalidKeyError
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config_entries import SOURCE_IMPORT
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_API_KEY,
|
||||||
|
CONF_LATITUDE,
|
||||||
|
CONF_LONGITUDE,
|
||||||
|
CONF_SHOW_ON_MAP,
|
||||||
|
CONF_STATE,
|
||||||
|
)
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||||
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
|
from homeassistant.helpers.event import async_track_time_interval
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
CONF_CITY,
|
||||||
|
CONF_COUNTRY,
|
||||||
|
CONF_GEOGRAPHIES,
|
||||||
|
DATA_CLIENT,
|
||||||
|
DEFAULT_SCAN_INTERVAL,
|
||||||
|
DOMAIN,
|
||||||
|
TOPIC_UPDATE,
|
||||||
|
)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DATA_LISTENER = "listener"
|
||||||
|
|
||||||
|
DEFAULT_OPTIONS = {CONF_SHOW_ON_MAP: True}
|
||||||
|
|
||||||
|
CONF_NODE_ID = "node_id"
|
||||||
|
|
||||||
|
GEOGRAPHY_COORDINATES_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_LATITUDE): cv.latitude,
|
||||||
|
vol.Required(CONF_LONGITUDE): cv.longitude,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
GEOGRAPHY_PLACE_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_CITY): cv.string,
|
||||||
|
vol.Required(CONF_STATE): cv.string,
|
||||||
|
vol.Required(CONF_COUNTRY): cv.string,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
CLOUD_API_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_API_KEY): cv.string,
|
||||||
|
vol.Optional(CONF_GEOGRAPHIES, default=[]): vol.All(
|
||||||
|
cv.ensure_list,
|
||||||
|
[vol.Any(GEOGRAPHY_COORDINATES_SCHEMA, GEOGRAPHY_PLACE_SCHEMA)],
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = vol.Schema({DOMAIN: CLOUD_API_SCHEMA}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_get_geography_id(geography_dict):
|
||||||
|
"""Generate a unique ID from a geography dict."""
|
||||||
|
if CONF_CITY in geography_dict:
|
||||||
|
return ",".join(
|
||||||
|
(
|
||||||
|
geography_dict[CONF_CITY],
|
||||||
|
geography_dict[CONF_STATE],
|
||||||
|
geography_dict[CONF_COUNTRY],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return ",".join(
|
||||||
|
(str(geography_dict[CONF_LATITUDE]), str(geography_dict[CONF_LONGITUDE]))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass, config):
|
||||||
|
"""Set up the AirVisual component."""
|
||||||
|
hass.data[DOMAIN] = {}
|
||||||
|
hass.data[DOMAIN][DATA_CLIENT] = {}
|
||||||
|
hass.data[DOMAIN][DATA_LISTENER] = {}
|
||||||
|
|
||||||
|
if DOMAIN not in config:
|
||||||
|
return True
|
||||||
|
|
||||||
|
conf = config[DOMAIN]
|
||||||
|
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_IMPORT}, data=conf
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, config_entry):
|
||||||
|
"""Set up AirVisual as config entry."""
|
||||||
|
entry_updates = {}
|
||||||
|
if not config_entry.unique_id:
|
||||||
|
# If the config entry doesn't already have a unique ID, set one:
|
||||||
|
entry_updates["unique_id"] = config_entry.data[CONF_API_KEY]
|
||||||
|
if not config_entry.options:
|
||||||
|
# If the config entry doesn't already have any options set, set defaults:
|
||||||
|
entry_updates["options"] = DEFAULT_OPTIONS
|
||||||
|
|
||||||
|
if entry_updates:
|
||||||
|
hass.config_entries.async_update_entry(config_entry, **entry_updates)
|
||||||
|
|
||||||
|
websession = aiohttp_client.async_get_clientsession(hass)
|
||||||
|
|
||||||
|
hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = AirVisualData(
|
||||||
|
hass, Client(websession, api_key=config_entry.data[CONF_API_KEY]), config_entry
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id].async_update()
|
||||||
|
except InvalidKeyError:
|
||||||
|
_LOGGER.error("Invalid API key provided")
|
||||||
|
raise ConfigEntryNotReady
|
||||||
|
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.async_forward_entry_setup(config_entry, "sensor")
|
||||||
|
)
|
||||||
|
|
||||||
|
async def refresh(event_time):
|
||||||
|
"""Refresh data from AirVisual."""
|
||||||
|
await hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id].async_update()
|
||||||
|
|
||||||
|
hass.data[DOMAIN][DATA_LISTENER][config_entry.entry_id] = async_track_time_interval(
|
||||||
|
hass, refresh, DEFAULT_SCAN_INTERVAL
|
||||||
|
)
|
||||||
|
|
||||||
|
config_entry.add_update_listener(async_update_options)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass, config_entry):
|
||||||
|
"""Unload an AirVisual config entry."""
|
||||||
|
hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id)
|
||||||
|
|
||||||
|
remove_listener = hass.data[DOMAIN][DATA_LISTENER].pop(config_entry.entry_id)
|
||||||
|
remove_listener()
|
||||||
|
|
||||||
|
await hass.config_entries.async_forward_entry_unload(config_entry, "sensor")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_update_options(hass, config_entry):
|
||||||
|
"""Handle an options update."""
|
||||||
|
airvisual = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id]
|
||||||
|
airvisual.async_update_options(config_entry.options)
|
||||||
|
|
||||||
|
|
||||||
|
class AirVisualData:
|
||||||
|
"""Define a class to manage data from the AirVisual cloud API."""
|
||||||
|
|
||||||
|
def __init__(self, hass, client, config_entry):
|
||||||
|
"""Initialize."""
|
||||||
|
self._client = client
|
||||||
|
self._hass = hass
|
||||||
|
self.data = {}
|
||||||
|
self.options = config_entry.options
|
||||||
|
|
||||||
|
self.geographies = {
|
||||||
|
async_get_geography_id(geography): geography
|
||||||
|
for geography in config_entry.data[CONF_GEOGRAPHIES]
|
||||||
|
}
|
||||||
|
|
||||||
|
async def async_update(self):
|
||||||
|
"""Get new data for all locations from the AirVisual cloud API."""
|
||||||
|
tasks = []
|
||||||
|
|
||||||
|
for geography in self.geographies.values():
|
||||||
|
if CONF_CITY in geography:
|
||||||
|
tasks.append(
|
||||||
|
self._client.api.city(
|
||||||
|
geography[CONF_CITY],
|
||||||
|
geography[CONF_STATE],
|
||||||
|
geography[CONF_COUNTRY],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
tasks.append(
|
||||||
|
self._client.api.nearest_city(
|
||||||
|
geography[CONF_LATITUDE], geography[CONF_LONGITUDE],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
|
for geography_id, result in zip(self.geographies, results):
|
||||||
|
if isinstance(result, AirVisualError):
|
||||||
|
_LOGGER.error("Error while retrieving data: %s", result)
|
||||||
|
self.data[geography_id] = {}
|
||||||
|
continue
|
||||||
|
self.data[geography_id] = result
|
||||||
|
|
||||||
|
_LOGGER.debug("Received new data")
|
||||||
|
async_dispatcher_send(self._hass, TOPIC_UPDATE)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_update_options(self, options):
|
||||||
|
"""Update the data manager's options."""
|
||||||
|
self.options = options
|
||||||
|
async_dispatcher_send(self._hass, TOPIC_UPDATE)
|
||||||
|
123
homeassistant/components/airvisual/config_flow.py
Normal file
123
homeassistant/components/airvisual/config_flow.py
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
"""Define a config flow manager for AirVisual."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from pyairvisual import Client
|
||||||
|
from pyairvisual.errors import InvalidKeyError
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_API_KEY,
|
||||||
|
CONF_LATITUDE,
|
||||||
|
CONF_LONGITUDE,
|
||||||
|
CONF_SHOW_ON_MAP,
|
||||||
|
)
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||||
|
|
||||||
|
from .const import CONF_GEOGRAPHIES, DOMAIN # pylint: disable=unused-import
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger("homeassistant.components.airvisual")
|
||||||
|
|
||||||
|
|
||||||
|
class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle an AirVisual config flow."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cloud_api_schema(self):
|
||||||
|
"""Return the data schema for the cloud API."""
|
||||||
|
return vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_API_KEY): str,
|
||||||
|
vol.Required(
|
||||||
|
CONF_LATITUDE, default=self.hass.config.latitude
|
||||||
|
): cv.latitude,
|
||||||
|
vol.Required(
|
||||||
|
CONF_LONGITUDE, default=self.hass.config.longitude
|
||||||
|
): cv.longitude,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _async_set_unique_id(self, unique_id):
|
||||||
|
"""Set the unique ID of the config flow and abort if it already exists."""
|
||||||
|
await self.async_set_unique_id(unique_id)
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
async def _show_form(self, errors=None):
|
||||||
|
"""Show the form to the user."""
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user", data_schema=self.cloud_api_schema, errors=errors or {},
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@callback
|
||||||
|
def async_get_options_flow(config_entry):
|
||||||
|
"""Define the config flow to handle options."""
|
||||||
|
return AirVisualOptionsFlowHandler(config_entry)
|
||||||
|
|
||||||
|
async def async_step_import(self, import_config):
|
||||||
|
"""Import a config entry from configuration.yaml."""
|
||||||
|
return await self.async_step_user(import_config)
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input=None):
|
||||||
|
"""Handle the start of the config flow."""
|
||||||
|
if not user_input:
|
||||||
|
return await self._show_form()
|
||||||
|
|
||||||
|
await self._async_set_unique_id(user_input[CONF_API_KEY])
|
||||||
|
|
||||||
|
websession = aiohttp_client.async_get_clientsession(self.hass)
|
||||||
|
client = Client(websession, api_key=user_input[CONF_API_KEY])
|
||||||
|
|
||||||
|
try:
|
||||||
|
await client.api.nearest_city()
|
||||||
|
except InvalidKeyError:
|
||||||
|
return await self._show_form(errors={CONF_API_KEY: "invalid_api_key"})
|
||||||
|
|
||||||
|
data = {CONF_API_KEY: user_input[CONF_API_KEY]}
|
||||||
|
if user_input.get(CONF_GEOGRAPHIES):
|
||||||
|
data[CONF_GEOGRAPHIES] = user_input[CONF_GEOGRAPHIES]
|
||||||
|
else:
|
||||||
|
data[CONF_GEOGRAPHIES] = [
|
||||||
|
{
|
||||||
|
CONF_LATITUDE: user_input.get(
|
||||||
|
CONF_LATITUDE, self.hass.config.latitude
|
||||||
|
),
|
||||||
|
CONF_LONGITUDE: user_input.get(
|
||||||
|
CONF_LONGITUDE, self.hass.config.longitude
|
||||||
|
),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=f"Cloud API (API key: {user_input[CONF_API_KEY][:4]}...)", data=data
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AirVisualOptionsFlowHandler(config_entries.OptionsFlow):
|
||||||
|
"""Handle an AirVisual options flow."""
|
||||||
|
|
||||||
|
def __init__(self, config_entry):
|
||||||
|
"""Initialize."""
|
||||||
|
self.config_entry = config_entry
|
||||||
|
|
||||||
|
async def async_step_init(self, user_input=None):
|
||||||
|
"""Manage the options."""
|
||||||
|
if user_input is not None:
|
||||||
|
return self.async_create_entry(title="", data=user_input)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="init",
|
||||||
|
data_schema=vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(
|
||||||
|
CONF_SHOW_ON_MAP,
|
||||||
|
default=self.config_entry.options.get(CONF_SHOW_ON_MAP),
|
||||||
|
): bool
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
14
homeassistant/components/airvisual/const.py
Normal file
14
homeassistant/components/airvisual/const.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
"""Define AirVisual constants."""
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
DOMAIN = "airvisual"
|
||||||
|
|
||||||
|
CONF_CITY = "city"
|
||||||
|
CONF_COUNTRY = "country"
|
||||||
|
CONF_GEOGRAPHIES = "geographies"
|
||||||
|
|
||||||
|
DATA_CLIENT = "client"
|
||||||
|
|
||||||
|
DEFAULT_SCAN_INTERVAL = timedelta(minutes=10)
|
||||||
|
|
||||||
|
TOPIC_UPDATE = f"{DOMAIN}_update"
|
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"domain": "airvisual",
|
"domain": "airvisual",
|
||||||
"name": "AirVisual",
|
"name": "AirVisual",
|
||||||
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/airvisual",
|
"documentation": "https://www.home-assistant.io/integrations/airvisual",
|
||||||
"requirements": ["pyairvisual==3.0.1"],
|
"requirements": ["pyairvisual==3.0.1"],
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
|
@ -1,27 +1,24 @@
|
|||||||
"""Support for AirVisual air quality sensors."""
|
"""Support for AirVisual air quality sensors."""
|
||||||
from datetime import timedelta
|
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
from pyairvisual import Client
|
|
||||||
from pyairvisual.errors import AirVisualError
|
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ATTRIBUTION,
|
ATTR_ATTRIBUTION,
|
||||||
ATTR_LATITUDE,
|
ATTR_LATITUDE,
|
||||||
ATTR_LONGITUDE,
|
ATTR_LONGITUDE,
|
||||||
CONF_API_KEY,
|
ATTR_STATE,
|
||||||
|
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
CONCENTRATION_PARTS_PER_BILLION,
|
||||||
|
CONCENTRATION_PARTS_PER_MILLION,
|
||||||
CONF_LATITUDE,
|
CONF_LATITUDE,
|
||||||
CONF_LONGITUDE,
|
CONF_LONGITUDE,
|
||||||
CONF_MONITORED_CONDITIONS,
|
|
||||||
CONF_SCAN_INTERVAL,
|
|
||||||
CONF_SHOW_ON_MAP,
|
CONF_SHOW_ON_MAP,
|
||||||
CONF_STATE,
|
CONF_STATE,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.util import Throttle
|
|
||||||
|
from .const import CONF_CITY, CONF_COUNTRY, DATA_CLIENT, DOMAIN, TOPIC_UPDATE
|
||||||
|
|
||||||
_LOGGER = getLogger(__name__)
|
_LOGGER = getLogger(__name__)
|
||||||
|
|
||||||
@ -31,23 +28,19 @@ ATTR_POLLUTANT_SYMBOL = "pollutant_symbol"
|
|||||||
ATTR_POLLUTANT_UNIT = "pollutant_unit"
|
ATTR_POLLUTANT_UNIT = "pollutant_unit"
|
||||||
ATTR_REGION = "region"
|
ATTR_REGION = "region"
|
||||||
|
|
||||||
CONF_CITY = "city"
|
|
||||||
CONF_COUNTRY = "country"
|
|
||||||
|
|
||||||
DEFAULT_ATTRIBUTION = "Data provided by AirVisual"
|
DEFAULT_ATTRIBUTION = "Data provided by AirVisual"
|
||||||
DEFAULT_SCAN_INTERVAL = timedelta(minutes=10)
|
|
||||||
|
|
||||||
MASS_PARTS_PER_MILLION = "ppm"
|
MASS_PARTS_PER_MILLION = "ppm"
|
||||||
MASS_PARTS_PER_BILLION = "ppb"
|
MASS_PARTS_PER_BILLION = "ppb"
|
||||||
VOLUME_MICROGRAMS_PER_CUBIC_METER = "µg/m3"
|
VOLUME_MICROGRAMS_PER_CUBIC_METER = "µg/m3"
|
||||||
|
|
||||||
SENSOR_TYPE_LEVEL = "air_pollution_level"
|
SENSOR_KIND_LEVEL = "air_pollution_level"
|
||||||
SENSOR_TYPE_AQI = "air_quality_index"
|
SENSOR_KIND_AQI = "air_quality_index"
|
||||||
SENSOR_TYPE_POLLUTANT = "main_pollutant"
|
SENSOR_KIND_POLLUTANT = "main_pollutant"
|
||||||
SENSORS = [
|
SENSORS = [
|
||||||
(SENSOR_TYPE_LEVEL, "Air Pollution Level", "mdi:gauge", None),
|
(SENSOR_KIND_LEVEL, "Air Pollution Level", "mdi:gauge", None),
|
||||||
(SENSOR_TYPE_AQI, "Air Quality Index", "mdi:chart-line", "AQI"),
|
(SENSOR_KIND_AQI, "Air Quality Index", "mdi:chart-line", "AQI"),
|
||||||
(SENSOR_TYPE_POLLUTANT, "Main Pollutant", "mdi:chemical-weapon", None),
|
(SENSOR_KIND_POLLUTANT, "Main Pollutant", "mdi:chemical-weapon", None),
|
||||||
]
|
]
|
||||||
|
|
||||||
POLLUTANT_LEVEL_MAPPING = [
|
POLLUTANT_LEVEL_MAPPING = [
|
||||||
@ -70,112 +63,68 @@ POLLUTANT_LEVEL_MAPPING = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
POLLUTANT_MAPPING = {
|
POLLUTANT_MAPPING = {
|
||||||
"co": {"label": "Carbon Monoxide", "unit": MASS_PARTS_PER_MILLION},
|
"co": {"label": "Carbon Monoxide", "unit": CONCENTRATION_PARTS_PER_MILLION},
|
||||||
"n2": {"label": "Nitrogen Dioxide", "unit": MASS_PARTS_PER_BILLION},
|
"n2": {"label": "Nitrogen Dioxide", "unit": CONCENTRATION_PARTS_PER_BILLION},
|
||||||
"o3": {"label": "Ozone", "unit": MASS_PARTS_PER_BILLION},
|
"o3": {"label": "Ozone", "unit": CONCENTRATION_PARTS_PER_BILLION},
|
||||||
"p1": {"label": "PM10", "unit": VOLUME_MICROGRAMS_PER_CUBIC_METER},
|
"p1": {"label": "PM10", "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
|
||||||
"p2": {"label": "PM2.5", "unit": VOLUME_MICROGRAMS_PER_CUBIC_METER},
|
"p2": {"label": "PM2.5", "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
|
||||||
"s2": {"label": "Sulfur Dioxide", "unit": MASS_PARTS_PER_BILLION},
|
"s2": {"label": "Sulfur Dioxide", "unit": CONCENTRATION_PARTS_PER_BILLION},
|
||||||
}
|
}
|
||||||
|
|
||||||
SENSOR_LOCALES = {"cn": "Chinese", "us": "U.S."}
|
SENSOR_LOCALES = {"cn": "Chinese", "us": "U.S."}
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|
||||||
{
|
|
||||||
vol.Required(CONF_API_KEY): cv.string,
|
|
||||||
vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSOR_LOCALES)): vol.All(
|
|
||||||
cv.ensure_list, [vol.In(SENSOR_LOCALES)]
|
|
||||||
),
|
|
||||||
vol.Inclusive(CONF_CITY, "city"): cv.string,
|
|
||||||
vol.Inclusive(CONF_COUNTRY, "city"): cv.string,
|
|
||||||
vol.Inclusive(CONF_LATITUDE, "coords"): cv.latitude,
|
|
||||||
vol.Inclusive(CONF_LONGITUDE, "coords"): cv.longitude,
|
|
||||||
vol.Optional(CONF_SHOW_ON_MAP, default=True): cv.boolean,
|
|
||||||
vol.Inclusive(CONF_STATE, "city"): cv.string,
|
|
||||||
vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): cv.time_period,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, entry, async_add_entities):
|
||||||
|
"""Set up AirVisual sensors based on a config entry."""
|
||||||
|
airvisual = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id]
|
||||||
|
|
||||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
async_add_entities(
|
||||||
"""Configure the platform and add the sensors."""
|
[
|
||||||
|
AirVisualSensor(airvisual, kind, name, icon, unit, locale, geography_id)
|
||||||
city = config.get(CONF_CITY)
|
for geography_id in airvisual.data
|
||||||
state = config.get(CONF_STATE)
|
for locale in SENSOR_LOCALES
|
||||||
country = config.get(CONF_COUNTRY)
|
for kind, name, icon, unit in SENSORS
|
||||||
|
],
|
||||||
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
|
True,
|
||||||
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
|
)
|
||||||
|
|
||||||
websession = aiohttp_client.async_get_clientsession(hass)
|
|
||||||
|
|
||||||
if city and state and country:
|
|
||||||
_LOGGER.debug(
|
|
||||||
"Using city, state, and country: %s, %s, %s", city, state, country
|
|
||||||
)
|
|
||||||
location_id = ",".join((city, state, country))
|
|
||||||
data = AirVisualData(
|
|
||||||
Client(websession, api_key=config[CONF_API_KEY]),
|
|
||||||
city=city,
|
|
||||||
state=state,
|
|
||||||
country=country,
|
|
||||||
show_on_map=config[CONF_SHOW_ON_MAP],
|
|
||||||
scan_interval=config[CONF_SCAN_INTERVAL],
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
_LOGGER.debug("Using latitude and longitude: %s, %s", latitude, longitude)
|
|
||||||
location_id = ",".join((str(latitude), str(longitude)))
|
|
||||||
data = AirVisualData(
|
|
||||||
Client(websession, api_key=config[CONF_API_KEY]),
|
|
||||||
latitude=latitude,
|
|
||||||
longitude=longitude,
|
|
||||||
show_on_map=config[CONF_SHOW_ON_MAP],
|
|
||||||
scan_interval=config[CONF_SCAN_INTERVAL],
|
|
||||||
)
|
|
||||||
|
|
||||||
await data.async_update()
|
|
||||||
|
|
||||||
sensors = []
|
|
||||||
for locale in config[CONF_MONITORED_CONDITIONS]:
|
|
||||||
for kind, name, icon, unit in SENSORS:
|
|
||||||
sensors.append(
|
|
||||||
AirVisualSensor(data, kind, name, icon, unit, locale, location_id)
|
|
||||||
)
|
|
||||||
|
|
||||||
async_add_entities(sensors, True)
|
|
||||||
|
|
||||||
|
|
||||||
class AirVisualSensor(Entity):
|
class AirVisualSensor(Entity):
|
||||||
"""Define an AirVisual sensor."""
|
"""Define an AirVisual sensor."""
|
||||||
|
|
||||||
def __init__(self, airvisual, kind, name, icon, unit, locale, location_id):
|
def __init__(self, airvisual, kind, name, icon, unit, locale, geography_id):
|
||||||
"""Initialize."""
|
"""Initialize."""
|
||||||
self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
|
self._airvisual = airvisual
|
||||||
|
self._async_unsub_dispatcher_connects = []
|
||||||
|
self._geography_id = geography_id
|
||||||
self._icon = icon
|
self._icon = icon
|
||||||
|
self._kind = kind
|
||||||
self._locale = locale
|
self._locale = locale
|
||||||
self._location_id = location_id
|
|
||||||
self._name = name
|
self._name = name
|
||||||
self._state = None
|
self._state = None
|
||||||
self._type = kind
|
|
||||||
self._unit = unit
|
self._unit = unit
|
||||||
self.airvisual = airvisual
|
|
||||||
|
|
||||||
@property
|
self._attrs = {
|
||||||
def device_state_attributes(self):
|
ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION,
|
||||||
"""Return the device state attributes."""
|
ATTR_CITY: airvisual.data[geography_id].get(CONF_CITY),
|
||||||
if self.airvisual.show_on_map:
|
ATTR_STATE: airvisual.data[geography_id].get(CONF_STATE),
|
||||||
self._attrs[ATTR_LATITUDE] = self.airvisual.latitude
|
ATTR_COUNTRY: airvisual.data[geography_id].get(CONF_COUNTRY),
|
||||||
self._attrs[ATTR_LONGITUDE] = self.airvisual.longitude
|
}
|
||||||
else:
|
|
||||||
self._attrs["lati"] = self.airvisual.latitude
|
|
||||||
self._attrs["long"] = self.airvisual.longitude
|
|
||||||
|
|
||||||
return self._attrs
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available(self):
|
def available(self):
|
||||||
"""Return True if entity is available."""
|
"""Return True if entity is available."""
|
||||||
return bool(self.airvisual.pollution_info)
|
try:
|
||||||
|
return bool(
|
||||||
|
self._airvisual.data[self._geography_id]["current"]["pollution"]
|
||||||
|
)
|
||||||
|
except KeyError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_state_attributes(self):
|
||||||
|
"""Return the device state attributes."""
|
||||||
|
return self._attrs
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def icon(self):
|
def icon(self):
|
||||||
@ -185,7 +134,7 @@ class AirVisualSensor(Entity):
|
|||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name."""
|
"""Return the name."""
|
||||||
return "{0} {1}".format(SENSOR_LOCALES[self._locale], self._name)
|
return f"{SENSOR_LOCALES[self._locale]} {self._name}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
@ -195,22 +144,33 @@ class AirVisualSensor(Entity):
|
|||||||
@property
|
@property
|
||||||
def unique_id(self):
|
def unique_id(self):
|
||||||
"""Return a unique, Home Assistant friendly identifier for this entity."""
|
"""Return a unique, Home Assistant friendly identifier for this entity."""
|
||||||
return f"{self._location_id}_{self._locale}_{self._type}"
|
return f"{self._geography_id}_{self._locale}_{self._kind}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unit_of_measurement(self):
|
def unit_of_measurement(self):
|
||||||
"""Return the unit the value is expressed in."""
|
"""Return the unit the value is expressed in."""
|
||||||
return self._unit
|
return self._unit
|
||||||
|
|
||||||
|
async def async_added_to_hass(self):
|
||||||
|
"""Register callbacks."""
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def update():
|
||||||
|
"""Update the state."""
|
||||||
|
self.async_schedule_update_ha_state(True)
|
||||||
|
|
||||||
|
self._async_unsub_dispatcher_connects.append(
|
||||||
|
async_dispatcher_connect(self.hass, TOPIC_UPDATE, update)
|
||||||
|
)
|
||||||
|
|
||||||
async def async_update(self):
|
async def async_update(self):
|
||||||
"""Update the sensor."""
|
"""Update the sensor."""
|
||||||
await self.airvisual.async_update()
|
try:
|
||||||
data = self.airvisual.pollution_info
|
data = self._airvisual.data[self._geography_id]["current"]["pollution"]
|
||||||
|
except KeyError:
|
||||||
if not data:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if self._type == SENSOR_TYPE_LEVEL:
|
if self._kind == SENSOR_KIND_LEVEL:
|
||||||
aqi = data[f"aqi{self._locale}"]
|
aqi = data[f"aqi{self._locale}"]
|
||||||
[level] = [
|
[level] = [
|
||||||
i
|
i
|
||||||
@ -219,9 +179,9 @@ class AirVisualSensor(Entity):
|
|||||||
]
|
]
|
||||||
self._state = level["label"]
|
self._state = level["label"]
|
||||||
self._icon = level["icon"]
|
self._icon = level["icon"]
|
||||||
elif self._type == SENSOR_TYPE_AQI:
|
elif self._kind == SENSOR_KIND_AQI:
|
||||||
self._state = data[f"aqi{self._locale}"]
|
self._state = data[f"aqi{self._locale}"]
|
||||||
elif self._type == SENSOR_TYPE_POLLUTANT:
|
elif self._kind == SENSOR_KIND_POLLUTANT:
|
||||||
symbol = data[f"main{self._locale}"]
|
symbol = data[f"main{self._locale}"]
|
||||||
self._state = POLLUTANT_MAPPING[symbol]["label"]
|
self._state = POLLUTANT_MAPPING[symbol]["label"]
|
||||||
self._attrs.update(
|
self._attrs.update(
|
||||||
@ -231,43 +191,21 @@ class AirVisualSensor(Entity):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
geography = self._airvisual.geographies[self._geography_id]
|
||||||
class AirVisualData:
|
if CONF_LATITUDE in geography:
|
||||||
"""Define an object to hold sensor data."""
|
if self._airvisual.options[CONF_SHOW_ON_MAP]:
|
||||||
|
self._attrs[ATTR_LATITUDE] = geography[CONF_LATITUDE]
|
||||||
def __init__(self, client, **kwargs):
|
self._attrs[ATTR_LONGITUDE] = geography[CONF_LONGITUDE]
|
||||||
"""Initialize."""
|
self._attrs.pop("lati", None)
|
||||||
self._client = client
|
self._attrs.pop("long", None)
|
||||||
self.city = kwargs.get(CONF_CITY)
|
|
||||||
self.country = kwargs.get(CONF_COUNTRY)
|
|
||||||
self.latitude = kwargs.get(CONF_LATITUDE)
|
|
||||||
self.longitude = kwargs.get(CONF_LONGITUDE)
|
|
||||||
self.pollution_info = {}
|
|
||||||
self.show_on_map = kwargs.get(CONF_SHOW_ON_MAP)
|
|
||||||
self.state = kwargs.get(CONF_STATE)
|
|
||||||
|
|
||||||
self.async_update = Throttle(kwargs[CONF_SCAN_INTERVAL])(self._async_update)
|
|
||||||
|
|
||||||
async def _async_update(self):
|
|
||||||
"""Update AirVisual data."""
|
|
||||||
|
|
||||||
try:
|
|
||||||
if self.city and self.state and self.country:
|
|
||||||
resp = await self._client.api.city(self.city, self.state, self.country)
|
|
||||||
self.longitude, self.latitude = resp["location"]["coordinates"]
|
|
||||||
else:
|
else:
|
||||||
resp = await self._client.api.nearest_city(
|
self._attrs["lati"] = geography[CONF_LATITUDE]
|
||||||
self.latitude, self.longitude
|
self._attrs["long"] = geography[CONF_LONGITUDE]
|
||||||
)
|
self._attrs.pop(ATTR_LATITUDE, None)
|
||||||
|
self._attrs.pop(ATTR_LONGITUDE, None)
|
||||||
|
|
||||||
_LOGGER.debug("New data retrieved: %s", resp)
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
|
"""Disconnect dispatcher listener when removed."""
|
||||||
self.pollution_info = resp["current"]["pollution"]
|
for cancel in self._async_unsub_dispatcher_connects:
|
||||||
except (KeyError, AirVisualError) as err:
|
cancel()
|
||||||
if self.city and self.state and self.country:
|
self._async_unsub_dispatcher_connects = []
|
||||||
location = (self.city, self.state, self.country)
|
|
||||||
else:
|
|
||||||
location = (self.latitude, self.longitude)
|
|
||||||
|
|
||||||
_LOGGER.error("Can't retrieve data for location: %s (%s)", location, err)
|
|
||||||
self.pollution_info = {}
|
|
||||||
|
33
homeassistant/components/airvisual/strings.json
Normal file
33
homeassistant/components/airvisual/strings.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"title": "AirVisual",
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Configure AirVisual",
|
||||||
|
"description": "Monitor air quality in a geographical location.",
|
||||||
|
"data": {
|
||||||
|
"api_key": "API Key",
|
||||||
|
"latitude": "Latitude",
|
||||||
|
"longitude": "Longitude"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"invalid_api_key": "Invalid API key"
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "This API key is already in use."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"title": "Configure AirVisual",
|
||||||
|
"description": "Set various options for the AirVisual integration.",
|
||||||
|
"data": {
|
||||||
|
"show_on_map": "Show monitored geography on the map"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -53,9 +53,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||||||
except (TypeError, KeyError, NameError, ValueError) as ex:
|
except (TypeError, KeyError, NameError, ValueError) as ex:
|
||||||
_LOGGER.error("%s", ex)
|
_LOGGER.error("%s", ex)
|
||||||
hass.components.persistent_notification.create(
|
hass.components.persistent_notification.create(
|
||||||
"Error: {}<br />"
|
"Error: {ex}<br />You will need to restart hass after fixing.",
|
||||||
"You will need to restart hass after fixing."
|
|
||||||
"".format(ex),
|
|
||||||
title=NOTIFICATION_TITLE,
|
title=NOTIFICATION_TITLE,
|
||||||
notification_id=NOTIFICATION_ID,
|
notification_id=NOTIFICATION_ID,
|
||||||
)
|
)
|
||||||
|
@ -177,7 +177,7 @@ class AlarmDecoderAlarmPanel(AlarmControlPanel):
|
|||||||
def alarm_arm_night(self, code=None):
|
def alarm_arm_night(self, code=None):
|
||||||
"""Send arm night command."""
|
"""Send arm night command."""
|
||||||
if code:
|
if code:
|
||||||
self.hass.data[DATA_AD].send(f"{code!s}33")
|
self.hass.data[DATA_AD].send(f"{code!s}7")
|
||||||
|
|
||||||
def alarm_toggle_chime(self, code=None):
|
def alarm_toggle_chime(self, code=None):
|
||||||
"""Send toggle chime command."""
|
"""Send toggle chime command."""
|
||||||
|
@ -31,7 +31,6 @@ from homeassistant.util.dt import now
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN = "alert"
|
DOMAIN = "alert"
|
||||||
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
|
||||||
|
|
||||||
CONF_CAN_ACK = "can_acknowledge"
|
CONF_CAN_ACK = "can_acknowledge"
|
||||||
CONF_NOTIFIERS = "notifiers"
|
CONF_NOTIFIERS = "notifiers"
|
||||||
@ -200,7 +199,7 @@ class Alert(ToggleEntity):
|
|||||||
self._ack = False
|
self._ack = False
|
||||||
self._cancel = None
|
self._cancel = None
|
||||||
self._send_done_message = False
|
self._send_done_message = False
|
||||||
self.entity_id = ENTITY_ID_FORMAT.format(entity_id)
|
self.entity_id = f"{DOMAIN}.{entity_id}"
|
||||||
|
|
||||||
event.async_track_state_change(
|
event.async_track_state_change(
|
||||||
hass, watched_entity_id, self.watched_entity_change
|
hass, watched_entity_id, self.watched_entity_change
|
||||||
|
@ -4,6 +4,7 @@ import logging
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import CONF_NAME
|
from homeassistant.const import CONF_NAME
|
||||||
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers import config_validation as cv, entityfilter
|
from homeassistant.helpers import config_validation as cv, entityfilter
|
||||||
|
|
||||||
from . import flash_briefings, intent, smart_home_http
|
from . import flash_briefings, intent, smart_home_http
|
||||||
@ -23,6 +24,7 @@ from .const import (
|
|||||||
CONF_TITLE,
|
CONF_TITLE,
|
||||||
CONF_UID,
|
CONF_UID,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
EVENT_ALEXA_SMART_HOME,
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -80,7 +82,37 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
|
|
||||||
async def async_setup(hass, config):
|
async def async_setup(hass, config):
|
||||||
"""Activate the Alexa component."""
|
"""Activate the Alexa component."""
|
||||||
config = config.get(DOMAIN, {})
|
|
||||||
|
@callback
|
||||||
|
def async_describe_logbook_event(event):
|
||||||
|
"""Describe a logbook event."""
|
||||||
|
data = event.data
|
||||||
|
entity_id = data["request"].get("entity_id")
|
||||||
|
|
||||||
|
if entity_id:
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
name = state.name if state else entity_id
|
||||||
|
message = f"send command {data['request']['namespace']}/{data['request']['name']} for {name}"
|
||||||
|
else:
|
||||||
|
message = (
|
||||||
|
f"send command {data['request']['namespace']}/{data['request']['name']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"name": "Amazon Alexa",
|
||||||
|
"message": message,
|
||||||
|
"entity_id": entity_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
hass.components.logbook.async_describe_event(
|
||||||
|
DOMAIN, EVENT_ALEXA_SMART_HOME, async_describe_logbook_event
|
||||||
|
)
|
||||||
|
|
||||||
|
if DOMAIN not in config:
|
||||||
|
return True
|
||||||
|
|
||||||
|
config = config[DOMAIN]
|
||||||
|
|
||||||
flash_briefings_config = config.get(CONF_FLASH_BRIEFINGS)
|
flash_briefings_config = config.get(CONF_FLASH_BRIEFINGS)
|
||||||
|
|
||||||
intent.async_setup(hass)
|
intent.async_setup(hass)
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
"""Alexa capabilities."""
|
"""Alexa capabilities."""
|
||||||
import logging
|
import logging
|
||||||
import math
|
|
||||||
|
|
||||||
from homeassistant.components import (
|
from homeassistant.components import (
|
||||||
cover,
|
cover,
|
||||||
@ -8,6 +7,7 @@ from homeassistant.components import (
|
|||||||
image_processing,
|
image_processing,
|
||||||
input_number,
|
input_number,
|
||||||
light,
|
light,
|
||||||
|
timer,
|
||||||
vacuum,
|
vacuum,
|
||||||
)
|
)
|
||||||
from homeassistant.components.alarm_control_panel import ATTR_CODE_FORMAT, FORMAT_NUMBER
|
from homeassistant.components.alarm_control_panel import ATTR_CODE_FORMAT, FORMAT_NUMBER
|
||||||
@ -26,6 +26,7 @@ from homeassistant.const import (
|
|||||||
STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
||||||
STATE_ALARM_ARMED_HOME,
|
STATE_ALARM_ARMED_HOME,
|
||||||
STATE_ALARM_ARMED_NIGHT,
|
STATE_ALARM_ARMED_NIGHT,
|
||||||
|
STATE_IDLE,
|
||||||
STATE_LOCKED,
|
STATE_LOCKED,
|
||||||
STATE_OFF,
|
STATE_OFF,
|
||||||
STATE_ON,
|
STATE_ON,
|
||||||
@ -227,7 +228,6 @@ class AlexaCapability:
|
|||||||
"""Return properties serialized for an API response."""
|
"""Return properties serialized for an API response."""
|
||||||
for prop in self.properties_supported():
|
for prop in self.properties_supported():
|
||||||
prop_name = prop["name"]
|
prop_name = prop["name"]
|
||||||
# pylint: disable=assignment-from-no-return
|
|
||||||
prop_value = self.get_property(prop_name)
|
prop_value = self.get_property(prop_name)
|
||||||
if prop_value is not None:
|
if prop_value is not None:
|
||||||
result = {
|
result = {
|
||||||
@ -365,6 +365,10 @@ class AlexaPowerController(AlexaCapability):
|
|||||||
|
|
||||||
if self.entity.domain == climate.DOMAIN:
|
if self.entity.domain == climate.DOMAIN:
|
||||||
is_on = self.entity.state != climate.HVAC_MODE_OFF
|
is_on = self.entity.state != climate.HVAC_MODE_OFF
|
||||||
|
elif self.entity.domain == vacuum.DOMAIN:
|
||||||
|
is_on = self.entity.state == vacuum.STATE_CLEANING
|
||||||
|
elif self.entity.domain == timer.DOMAIN:
|
||||||
|
is_on = self.entity.state != STATE_IDLE
|
||||||
|
|
||||||
else:
|
else:
|
||||||
is_on = self.entity.state != STATE_OFF
|
is_on = self.entity.state != STATE_OFF
|
||||||
@ -670,11 +674,8 @@ class AlexaSpeaker(AlexaCapability):
|
|||||||
current_level = self.entity.attributes.get(
|
current_level = self.entity.attributes.get(
|
||||||
media_player.ATTR_MEDIA_VOLUME_LEVEL
|
media_player.ATTR_MEDIA_VOLUME_LEVEL
|
||||||
)
|
)
|
||||||
try:
|
if current_level is not None:
|
||||||
current = math.floor(int(current_level * 100))
|
return round(float(current_level) * 100)
|
||||||
except ZeroDivisionError:
|
|
||||||
current = 0
|
|
||||||
return current
|
|
||||||
|
|
||||||
if name == "muted":
|
if name == "muted":
|
||||||
return bool(
|
return bool(
|
||||||
|
@ -53,7 +53,7 @@ class AbstractConfig(ABC):
|
|||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
await self._unsub_proactive_report
|
await self._unsub_proactive_report
|
||||||
except Exception: # pylint: disable=broad-except
|
except Exception:
|
||||||
self._unsub_proactive_report = None
|
self._unsub_proactive_report = None
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ from homeassistant.components.climate import const as climate
|
|||||||
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
|
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
|
||||||
|
|
||||||
DOMAIN = "alexa"
|
DOMAIN = "alexa"
|
||||||
|
EVENT_ALEXA_SMART_HOME = "alexa_smart_home"
|
||||||
|
|
||||||
# Flash briefing constants
|
# Flash briefing constants
|
||||||
CONF_UID = "uid"
|
CONF_UID = "uid"
|
||||||
|
@ -400,7 +400,10 @@ class CoverCapabilities(AlexaEntity):
|
|||||||
|
|
||||||
def interfaces(self):
|
def interfaces(self):
|
||||||
"""Yield the supported interfaces."""
|
"""Yield the supported interfaces."""
|
||||||
yield AlexaPowerController(self.entity)
|
device_class = self.entity.attributes.get(ATTR_DEVICE_CLASS)
|
||||||
|
if device_class != cover.DEVICE_CLASS_GARAGE:
|
||||||
|
yield AlexaPowerController(self.entity)
|
||||||
|
|
||||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
if supported & cover.SUPPORT_SET_POSITION:
|
if supported & cover.SUPPORT_SET_POSITION:
|
||||||
yield AlexaRangeController(
|
yield AlexaRangeController(
|
||||||
@ -724,6 +727,7 @@ class TimerCapabilities(AlexaEntity):
|
|||||||
def interfaces(self):
|
def interfaces(self):
|
||||||
"""Yield the supported interfaces."""
|
"""Yield the supported interfaces."""
|
||||||
yield AlexaTimeHoldController(self.entity, allow_remote_resume=True)
|
yield AlexaTimeHoldController(self.entity, allow_remote_resume=True)
|
||||||
|
yield AlexaPowerController(self.entity)
|
||||||
yield Alexa(self.entity)
|
yield Alexa(self.entity)
|
||||||
|
|
||||||
|
|
||||||
@ -738,8 +742,11 @@ class VacuumCapabilities(AlexaEntity):
|
|||||||
def interfaces(self):
|
def interfaces(self):
|
||||||
"""Yield the supported interfaces."""
|
"""Yield the supported interfaces."""
|
||||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
if (supported & vacuum.SUPPORT_TURN_ON) and (
|
if (
|
||||||
supported & vacuum.SUPPORT_TURN_OFF
|
(supported & vacuum.SUPPORT_TURN_ON) or (supported & vacuum.SUPPORT_START)
|
||||||
|
) and (
|
||||||
|
(supported & vacuum.SUPPORT_TURN_OFF)
|
||||||
|
or (supported & vacuum.SUPPORT_RETURN_HOME)
|
||||||
):
|
):
|
||||||
yield AlexaPowerController(self.entity)
|
yield AlexaPowerController(self.entity)
|
||||||
|
|
||||||
|
@ -121,6 +121,12 @@ async def async_api_turn_on(hass, config, directive, context):
|
|||||||
service = SERVICE_TURN_ON
|
service = SERVICE_TURN_ON
|
||||||
if domain == cover.DOMAIN:
|
if domain == cover.DOMAIN:
|
||||||
service = cover.SERVICE_OPEN_COVER
|
service = cover.SERVICE_OPEN_COVER
|
||||||
|
elif domain == vacuum.DOMAIN:
|
||||||
|
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
|
if not supported & vacuum.SUPPORT_TURN_ON and supported & vacuum.SUPPORT_START:
|
||||||
|
service = vacuum.SERVICE_START
|
||||||
|
elif domain == timer.DOMAIN:
|
||||||
|
service = timer.SERVICE_START
|
||||||
elif domain == media_player.DOMAIN:
|
elif domain == media_player.DOMAIN:
|
||||||
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
power_features = media_player.SUPPORT_TURN_ON | media_player.SUPPORT_TURN_OFF
|
power_features = media_player.SUPPORT_TURN_ON | media_player.SUPPORT_TURN_OFF
|
||||||
@ -149,6 +155,15 @@ async def async_api_turn_off(hass, config, directive, context):
|
|||||||
service = SERVICE_TURN_OFF
|
service = SERVICE_TURN_OFF
|
||||||
if entity.domain == cover.DOMAIN:
|
if entity.domain == cover.DOMAIN:
|
||||||
service = cover.SERVICE_CLOSE_COVER
|
service = cover.SERVICE_CLOSE_COVER
|
||||||
|
elif domain == vacuum.DOMAIN:
|
||||||
|
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
|
if (
|
||||||
|
not supported & vacuum.SUPPORT_TURN_OFF
|
||||||
|
and supported & vacuum.SUPPORT_RETURN_HOME
|
||||||
|
):
|
||||||
|
service = vacuum.SERVICE_RETURN_TO_BASE
|
||||||
|
elif domain == timer.DOMAIN:
|
||||||
|
service = timer.SERVICE_CANCEL
|
||||||
elif domain == media_player.DOMAIN:
|
elif domain == media_player.DOMAIN:
|
||||||
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
power_features = media_player.SUPPORT_TURN_ON | media_player.SUPPORT_TURN_OFF
|
power_features = media_player.SUPPORT_TURN_ON | media_player.SUPPORT_TURN_OFF
|
||||||
@ -478,8 +493,8 @@ async def async_api_select_input(hass, config, directive, context):
|
|||||||
media_input = source
|
media_input = source
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
msg = "failed to map input {} to a media source on {}".format(
|
msg = (
|
||||||
media_input, entity.entity_id
|
f"failed to map input {media_input} to a media source on {entity.entity_id}"
|
||||||
)
|
)
|
||||||
raise AlexaInvalidValueError(msg)
|
raise AlexaInvalidValueError(msg)
|
||||||
|
|
||||||
@ -1225,7 +1240,7 @@ async def async_api_adjust_range(hass, config, directive, context):
|
|||||||
service = SERVICE_SET_COVER_POSITION
|
service = SERVICE_SET_COVER_POSITION
|
||||||
current = entity.attributes.get(cover.ATTR_POSITION)
|
current = entity.attributes.get(cover.ATTR_POSITION)
|
||||||
if not current:
|
if not current:
|
||||||
msg = "Unable to determine {} current position".format(entity.entity_id)
|
msg = f"Unable to determine {entity.entity_id} current position"
|
||||||
raise AlexaInvalidValueError(msg)
|
raise AlexaInvalidValueError(msg)
|
||||||
position = response_value = min(100, max(0, range_delta + current))
|
position = response_value = min(100, max(0, range_delta + current))
|
||||||
if position == 100:
|
if position == 100:
|
||||||
@ -1241,9 +1256,7 @@ async def async_api_adjust_range(hass, config, directive, context):
|
|||||||
service = SERVICE_SET_COVER_TILT_POSITION
|
service = SERVICE_SET_COVER_TILT_POSITION
|
||||||
current = entity.attributes.get(cover.ATTR_TILT_POSITION)
|
current = entity.attributes.get(cover.ATTR_TILT_POSITION)
|
||||||
if not current:
|
if not current:
|
||||||
msg = "Unable to determine {} current tilt position".format(
|
msg = f"Unable to determine {entity.entity_id} current tilt position"
|
||||||
entity.entity_id
|
|
||||||
)
|
|
||||||
raise AlexaInvalidValueError(msg)
|
raise AlexaInvalidValueError(msg)
|
||||||
tilt_position = response_value = min(100, max(0, range_delta + current))
|
tilt_position = response_value = min(100, max(0, range_delta + current))
|
||||||
if tilt_position == 100:
|
if tilt_position == 100:
|
||||||
@ -1439,9 +1452,7 @@ async def async_api_set_eq_mode(hass, config, directive, context):
|
|||||||
if sound_mode_list and mode.lower() in sound_mode_list:
|
if sound_mode_list and mode.lower() in sound_mode_list:
|
||||||
data[media_player.const.ATTR_SOUND_MODE] = mode.lower()
|
data[media_player.const.ATTR_SOUND_MODE] = mode.lower()
|
||||||
else:
|
else:
|
||||||
msg = "failed to map sound mode {} to a mode on {}".format(
|
msg = f"failed to map sound mode {mode} to a mode on {entity.entity_id}"
|
||||||
mode, entity.entity_id
|
|
||||||
)
|
|
||||||
raise AlexaInvalidValueError(msg)
|
raise AlexaInvalidValueError(msg)
|
||||||
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
|
@ -4,5 +4,6 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/alexa",
|
"documentation": "https://www.home-assistant.io/integrations/alexa",
|
||||||
"requirements": [],
|
"requirements": [],
|
||||||
"dependencies": ["http"],
|
"dependencies": ["http"],
|
||||||
|
"after_dependencies": ["logbook"],
|
||||||
"codeowners": ["@home-assistant/cloud", "@ochlocracy"]
|
"codeowners": ["@home-assistant/cloud", "@ochlocracy"]
|
||||||
}
|
}
|
||||||
|
@ -3,15 +3,13 @@ import logging
|
|||||||
|
|
||||||
import homeassistant.core as ha
|
import homeassistant.core as ha
|
||||||
|
|
||||||
from .const import API_DIRECTIVE, API_HEADER
|
from .const import API_DIRECTIVE, API_HEADER, EVENT_ALEXA_SMART_HOME
|
||||||
from .errors import AlexaBridgeUnreachableError, AlexaError
|
from .errors import AlexaBridgeUnreachableError, AlexaError
|
||||||
from .handlers import HANDLERS
|
from .handlers import HANDLERS
|
||||||
from .messages import AlexaDirective
|
from .messages import AlexaDirective
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
EVENT_ALEXA_SMART_HOME = "alexa_smart_home"
|
|
||||||
|
|
||||||
|
|
||||||
async def async_handle_message(hass, config, request, context=None, enabled=True):
|
async def async_handle_message(hass, config, request, context=None, enabled=True):
|
||||||
"""Handle incoming API messages.
|
"""Handle incoming API messages.
|
||||||
|
@ -26,6 +26,9 @@ async def async_enable_proactive_mode(hass, smart_home_config):
|
|||||||
await smart_home_config.async_get_access_token()
|
await smart_home_config.async_get_access_token()
|
||||||
|
|
||||||
async def async_entity_state_listener(changed_entity, old_state, new_state):
|
async def async_entity_state_listener(changed_entity, old_state, new_state):
|
||||||
|
if not hass.is_running:
|
||||||
|
return
|
||||||
|
|
||||||
if not new_state:
|
if not new_state:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -87,7 +87,6 @@ class AlmondFlowHandler(config_entry_oauth2_flow.AbstractOAuth2FlowHandler):
|
|||||||
)
|
)
|
||||||
return self.async_abort(reason="cannot_connect")
|
return self.async_abort(reason="cannot_connect")
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
|
||||||
self.CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
|
self.CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
|
||||||
|
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Denne appn\u00f8gle er allerede i brug."
|
||||||
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"identifier_exists": "Applikationsn\u00f8gle og/eller API n\u00f8gle er allerede registreret",
|
"identifier_exists": "Applikationsn\u00f8gle og/eller API n\u00f8gle er allerede registreret",
|
||||||
"invalid_key": "Ugyldig API n\u00f8gle og/eller applikationsn\u00f8gle",
|
"invalid_key": "Ugyldig API n\u00f8gle og/eller applikationsn\u00f8gle",
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Esta clave API ya est\u00e1 en uso."
|
||||||
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"identifier_exists": "La clave API y/o la clave de aplicaci\u00f3n ya est\u00e1 registrada",
|
"identifier_exists": "La clave API y/o la clave de aplicaci\u00f3n ya est\u00e1 registrada",
|
||||||
"invalid_key": "Clave API y/o clave de aplicaci\u00f3n no v\u00e1lida",
|
"invalid_key": "Clave API y/o clave de aplicaci\u00f3n no v\u00e1lida",
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Questa chiave dell'app \u00e8 gi\u00e0 in uso."
|
||||||
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"identifier_exists": "API Key e/o Application Key gi\u00e0 registrata",
|
"identifier_exists": "API Key e/o Application Key gi\u00e0 registrata",
|
||||||
"invalid_key": "API Key e/o Application Key non valida",
|
"invalid_key": "API Key e/o Application Key non valida",
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "D\u00ebsen App Schl\u00ebssel g\u00ebtt scho benotzt"
|
||||||
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"identifier_exists": "Applikatioun's Schl\u00ebssel an/oder API Schl\u00ebssel ass scho registr\u00e9iert",
|
"identifier_exists": "Applikatioun's Schl\u00ebssel an/oder API Schl\u00ebssel ass scho registr\u00e9iert",
|
||||||
"invalid_key": "Ong\u00ebltegen API Schl\u00ebssel an/oder Applikatioun's Schl\u00ebssel",
|
"invalid_key": "Ong\u00ebltegen API Schl\u00ebssel an/oder Applikatioun's Schl\u00ebssel",
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "\u042d\u0442\u043e\u0442 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f."
|
||||||
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"identifier_exists": "\u041a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 API \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d.",
|
"identifier_exists": "\u041a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 API \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d.",
|
||||||
"invalid_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.",
|
"invalid_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.",
|
||||||
|
@ -10,8 +10,11 @@ from homeassistant.config_entries import SOURCE_IMPORT
|
|||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_LOCATION,
|
ATTR_LOCATION,
|
||||||
ATTR_NAME,
|
ATTR_NAME,
|
||||||
|
CONCENTRATION_PARTS_PER_MILLION,
|
||||||
CONF_API_KEY,
|
CONF_API_KEY,
|
||||||
EVENT_HOMEASSISTANT_STOP,
|
EVENT_HOMEASSISTANT_STOP,
|
||||||
|
SPEED_MILES_PER_HOUR,
|
||||||
|
UNIT_PERCENTAGE,
|
||||||
)
|
)
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
@ -23,14 +26,12 @@ from homeassistant.helpers.dispatcher import (
|
|||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.event import async_call_later
|
from homeassistant.helpers.event import async_call_later
|
||||||
|
|
||||||
from .config_flow import configured_instances
|
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_LAST_DATA,
|
ATTR_LAST_DATA,
|
||||||
ATTR_MONITORED_CONDITIONS,
|
ATTR_MONITORED_CONDITIONS,
|
||||||
CONF_APP_KEY,
|
CONF_APP_KEY,
|
||||||
DATA_CLIENT,
|
DATA_CLIENT,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
TOPIC_UPDATE,
|
|
||||||
TYPE_BINARY_SENSOR,
|
TYPE_BINARY_SENSOR,
|
||||||
TYPE_SENSOR,
|
TYPE_SENSOR,
|
||||||
)
|
)
|
||||||
@ -148,26 +149,26 @@ SENSOR_TYPES = {
|
|||||||
TYPE_BATT8: ("Battery 8", None, TYPE_BINARY_SENSOR, "battery"),
|
TYPE_BATT8: ("Battery 8", None, TYPE_BINARY_SENSOR, "battery"),
|
||||||
TYPE_BATT9: ("Battery 9", None, TYPE_BINARY_SENSOR, "battery"),
|
TYPE_BATT9: ("Battery 9", None, TYPE_BINARY_SENSOR, "battery"),
|
||||||
TYPE_BATTOUT: ("Battery", None, TYPE_BINARY_SENSOR, "battery"),
|
TYPE_BATTOUT: ("Battery", None, TYPE_BINARY_SENSOR, "battery"),
|
||||||
TYPE_CO2: ("co2", "ppm", TYPE_SENSOR, None),
|
TYPE_CO2: ("co2", CONCENTRATION_PARTS_PER_MILLION, TYPE_SENSOR, None),
|
||||||
TYPE_DAILYRAININ: ("Daily Rain", "in", TYPE_SENSOR, None),
|
TYPE_DAILYRAININ: ("Daily Rain", "in", TYPE_SENSOR, None),
|
||||||
TYPE_DEWPOINT: ("Dew Point", "°F", TYPE_SENSOR, "temperature"),
|
TYPE_DEWPOINT: ("Dew Point", "°F", TYPE_SENSOR, "temperature"),
|
||||||
TYPE_EVENTRAININ: ("Event Rain", "in", TYPE_SENSOR, None),
|
TYPE_EVENTRAININ: ("Event Rain", "in", TYPE_SENSOR, None),
|
||||||
TYPE_FEELSLIKE: ("Feels Like", "°F", TYPE_SENSOR, "temperature"),
|
TYPE_FEELSLIKE: ("Feels Like", "°F", TYPE_SENSOR, "temperature"),
|
||||||
TYPE_HOURLYRAININ: ("Hourly Rain Rate", "in/hr", TYPE_SENSOR, None),
|
TYPE_HOURLYRAININ: ("Hourly Rain Rate", "in/hr", TYPE_SENSOR, None),
|
||||||
TYPE_HUMIDITY10: ("Humidity 10", "%", TYPE_SENSOR, "humidity"),
|
TYPE_HUMIDITY10: ("Humidity 10", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"),
|
||||||
TYPE_HUMIDITY1: ("Humidity 1", "%", TYPE_SENSOR, "humidity"),
|
TYPE_HUMIDITY1: ("Humidity 1", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"),
|
||||||
TYPE_HUMIDITY2: ("Humidity 2", "%", TYPE_SENSOR, "humidity"),
|
TYPE_HUMIDITY2: ("Humidity 2", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"),
|
||||||
TYPE_HUMIDITY3: ("Humidity 3", "%", TYPE_SENSOR, "humidity"),
|
TYPE_HUMIDITY3: ("Humidity 3", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"),
|
||||||
TYPE_HUMIDITY4: ("Humidity 4", "%", TYPE_SENSOR, "humidity"),
|
TYPE_HUMIDITY4: ("Humidity 4", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"),
|
||||||
TYPE_HUMIDITY5: ("Humidity 5", "%", TYPE_SENSOR, "humidity"),
|
TYPE_HUMIDITY5: ("Humidity 5", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"),
|
||||||
TYPE_HUMIDITY6: ("Humidity 6", "%", TYPE_SENSOR, "humidity"),
|
TYPE_HUMIDITY6: ("Humidity 6", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"),
|
||||||
TYPE_HUMIDITY7: ("Humidity 7", "%", TYPE_SENSOR, "humidity"),
|
TYPE_HUMIDITY7: ("Humidity 7", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"),
|
||||||
TYPE_HUMIDITY8: ("Humidity 8", "%", TYPE_SENSOR, "humidity"),
|
TYPE_HUMIDITY8: ("Humidity 8", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"),
|
||||||
TYPE_HUMIDITY9: ("Humidity 9", "%", TYPE_SENSOR, "humidity"),
|
TYPE_HUMIDITY9: ("Humidity 9", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"),
|
||||||
TYPE_HUMIDITY: ("Humidity", "%", TYPE_SENSOR, "humidity"),
|
TYPE_HUMIDITY: ("Humidity", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"),
|
||||||
TYPE_HUMIDITYIN: ("Humidity In", "%", TYPE_SENSOR, "humidity"),
|
TYPE_HUMIDITYIN: ("Humidity In", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"),
|
||||||
TYPE_LASTRAIN: ("Last Rain", None, TYPE_SENSOR, "timestamp"),
|
TYPE_LASTRAIN: ("Last Rain", None, TYPE_SENSOR, "timestamp"),
|
||||||
TYPE_MAXDAILYGUST: ("Max Gust", "mph", TYPE_SENSOR, None),
|
TYPE_MAXDAILYGUST: ("Max Gust", SPEED_MILES_PER_HOUR, TYPE_SENSOR, None),
|
||||||
TYPE_MONTHLYRAININ: ("Monthly Rain", "in", TYPE_SENSOR, None),
|
TYPE_MONTHLYRAININ: ("Monthly Rain", "in", TYPE_SENSOR, None),
|
||||||
TYPE_RELAY10: ("Relay 10", None, TYPE_BINARY_SENSOR, "connectivity"),
|
TYPE_RELAY10: ("Relay 10", None, TYPE_BINARY_SENSOR, "connectivity"),
|
||||||
TYPE_RELAY1: ("Relay 1", None, TYPE_BINARY_SENSOR, "connectivity"),
|
TYPE_RELAY1: ("Relay 1", None, TYPE_BINARY_SENSOR, "connectivity"),
|
||||||
@ -179,16 +180,16 @@ SENSOR_TYPES = {
|
|||||||
TYPE_RELAY7: ("Relay 7", None, TYPE_BINARY_SENSOR, "connectivity"),
|
TYPE_RELAY7: ("Relay 7", None, TYPE_BINARY_SENSOR, "connectivity"),
|
||||||
TYPE_RELAY8: ("Relay 8", None, TYPE_BINARY_SENSOR, "connectivity"),
|
TYPE_RELAY8: ("Relay 8", None, TYPE_BINARY_SENSOR, "connectivity"),
|
||||||
TYPE_RELAY9: ("Relay 9", None, TYPE_BINARY_SENSOR, "connectivity"),
|
TYPE_RELAY9: ("Relay 9", None, TYPE_BINARY_SENSOR, "connectivity"),
|
||||||
TYPE_SOILHUM10: ("Soil Humidity 10", "%", TYPE_SENSOR, "humidity"),
|
TYPE_SOILHUM10: ("Soil Humidity 10", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"),
|
||||||
TYPE_SOILHUM1: ("Soil Humidity 1", "%", TYPE_SENSOR, "humidity"),
|
TYPE_SOILHUM1: ("Soil Humidity 1", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"),
|
||||||
TYPE_SOILHUM2: ("Soil Humidity 2", "%", TYPE_SENSOR, "humidity"),
|
TYPE_SOILHUM2: ("Soil Humidity 2", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"),
|
||||||
TYPE_SOILHUM3: ("Soil Humidity 3", "%", TYPE_SENSOR, "humidity"),
|
TYPE_SOILHUM3: ("Soil Humidity 3", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"),
|
||||||
TYPE_SOILHUM4: ("Soil Humidity 4", "%", TYPE_SENSOR, "humidity"),
|
TYPE_SOILHUM4: ("Soil Humidity 4", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"),
|
||||||
TYPE_SOILHUM5: ("Soil Humidity 5", "%", TYPE_SENSOR, "humidity"),
|
TYPE_SOILHUM5: ("Soil Humidity 5", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"),
|
||||||
TYPE_SOILHUM6: ("Soil Humidity 6", "%", TYPE_SENSOR, "humidity"),
|
TYPE_SOILHUM6: ("Soil Humidity 6", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"),
|
||||||
TYPE_SOILHUM7: ("Soil Humidity 7", "%", TYPE_SENSOR, "humidity"),
|
TYPE_SOILHUM7: ("Soil Humidity 7", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"),
|
||||||
TYPE_SOILHUM8: ("Soil Humidity 8", "%", TYPE_SENSOR, "humidity"),
|
TYPE_SOILHUM8: ("Soil Humidity 8", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"),
|
||||||
TYPE_SOILHUM9: ("Soil Humidity 9", "%", TYPE_SENSOR, "humidity"),
|
TYPE_SOILHUM9: ("Soil Humidity 9", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"),
|
||||||
TYPE_SOILTEMP10F: ("Soil Temp 10", "°F", TYPE_SENSOR, "temperature"),
|
TYPE_SOILTEMP10F: ("Soil Temp 10", "°F", TYPE_SENSOR, "temperature"),
|
||||||
TYPE_SOILTEMP1F: ("Soil Temp 1", "°F", TYPE_SENSOR, "temperature"),
|
TYPE_SOILTEMP1F: ("Soil Temp 1", "°F", TYPE_SENSOR, "temperature"),
|
||||||
TYPE_SOILTEMP2F: ("Soil Temp 2", "°F", TYPE_SENSOR, "temperature"),
|
TYPE_SOILTEMP2F: ("Soil Temp 2", "°F", TYPE_SENSOR, "temperature"),
|
||||||
@ -218,12 +219,12 @@ SENSOR_TYPES = {
|
|||||||
TYPE_WEEKLYRAININ: ("Weekly Rain", "in", TYPE_SENSOR, None),
|
TYPE_WEEKLYRAININ: ("Weekly Rain", "in", TYPE_SENSOR, None),
|
||||||
TYPE_WINDDIR: ("Wind Dir", "°", TYPE_SENSOR, None),
|
TYPE_WINDDIR: ("Wind Dir", "°", TYPE_SENSOR, None),
|
||||||
TYPE_WINDDIR_AVG10M: ("Wind Dir Avg 10m", "°", TYPE_SENSOR, None),
|
TYPE_WINDDIR_AVG10M: ("Wind Dir Avg 10m", "°", TYPE_SENSOR, None),
|
||||||
TYPE_WINDDIR_AVG2M: ("Wind Dir Avg 2m", "mph", TYPE_SENSOR, None),
|
TYPE_WINDDIR_AVG2M: ("Wind Dir Avg 2m", SPEED_MILES_PER_HOUR, TYPE_SENSOR, None),
|
||||||
TYPE_WINDGUSTDIR: ("Gust Dir", "°", TYPE_SENSOR, None),
|
TYPE_WINDGUSTDIR: ("Gust Dir", "°", TYPE_SENSOR, None),
|
||||||
TYPE_WINDGUSTMPH: ("Wind Gust", "mph", TYPE_SENSOR, None),
|
TYPE_WINDGUSTMPH: ("Wind Gust", SPEED_MILES_PER_HOUR, TYPE_SENSOR, None),
|
||||||
TYPE_WINDSPDMPH_AVG10M: ("Wind Avg 10m", "mph", TYPE_SENSOR, None),
|
TYPE_WINDSPDMPH_AVG10M: ("Wind Avg 10m", SPEED_MILES_PER_HOUR, TYPE_SENSOR, None),
|
||||||
TYPE_WINDSPDMPH_AVG2M: ("Wind Avg 2m", "mph", TYPE_SENSOR, None),
|
TYPE_WINDSPDMPH_AVG2M: ("Wind Avg 2m", SPEED_MILES_PER_HOUR, TYPE_SENSOR, None),
|
||||||
TYPE_WINDSPEEDMPH: ("Wind Speed", "mph", TYPE_SENSOR, None),
|
TYPE_WINDSPEEDMPH: ("Wind Speed", SPEED_MILES_PER_HOUR, TYPE_SENSOR, None),
|
||||||
TYPE_YEARLYRAININ: ("Yearly Rain", "in", TYPE_SENSOR, None),
|
TYPE_YEARLYRAININ: ("Yearly Rain", "in", TYPE_SENSOR, None),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,9 +254,6 @@ async def async_setup(hass, config):
|
|||||||
# Store config for use during entry setup:
|
# Store config for use during entry setup:
|
||||||
hass.data[DOMAIN][DATA_CONFIG] = conf
|
hass.data[DOMAIN][DATA_CONFIG] = conf
|
||||||
|
|
||||||
if conf[CONF_APP_KEY] in configured_instances(hass):
|
|
||||||
return True
|
|
||||||
|
|
||||||
hass.async_create_task(
|
hass.async_create_task(
|
||||||
hass.config_entries.flow.async_init(
|
hass.config_entries.flow.async_init(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@ -269,6 +267,11 @@ async def async_setup(hass, config):
|
|||||||
|
|
||||||
async def async_setup_entry(hass, config_entry):
|
async def async_setup_entry(hass, config_entry):
|
||||||
"""Set up the Ambient PWS as config entry."""
|
"""Set up the Ambient PWS as config entry."""
|
||||||
|
if not config_entry.unique_id:
|
||||||
|
hass.config_entries.async_update_entry(
|
||||||
|
config_entry, unique_id=config_entry.data[CONF_APP_KEY]
|
||||||
|
)
|
||||||
|
|
||||||
session = aiohttp_client.async_get_clientsession(hass)
|
session = aiohttp_client.async_get_clientsession(hass)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -378,7 +381,9 @@ 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.format(mac_address))
|
async_dispatcher_send(
|
||||||
|
self._hass, f"ambient_station_data_update_{mac_address}"
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER.debug("Resetting watchdog")
|
_LOGGER.debug("Resetting watchdog")
|
||||||
self._watchdog_listener()
|
self._watchdog_listener()
|
||||||
@ -518,7 +523,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.format(self._mac_address), update
|
self.hass, f"ambient_station_data_update_{self._mac_address}", update
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_will_remove_from_hass(self):
|
async def async_will_remove_from_hass(self):
|
||||||
|
@ -5,35 +5,29 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.const import CONF_API_KEY
|
from homeassistant.const import CONF_API_KEY
|
||||||
from homeassistant.core import callback
|
|
||||||
from homeassistant.helpers import aiohttp_client
|
from homeassistant.helpers import aiohttp_client
|
||||||
|
|
||||||
from .const import CONF_APP_KEY, DOMAIN
|
from .const import CONF_APP_KEY, DOMAIN # pylint: disable=unused-import
|
||||||
|
|
||||||
|
|
||||||
@callback
|
class AmbientStationFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
def configured_instances(hass):
|
|
||||||
"""Return a set of configured Ambient PWS instances."""
|
|
||||||
return set(
|
|
||||||
entry.data[CONF_APP_KEY] for entry in hass.config_entries.async_entries(DOMAIN)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@config_entries.HANDLERS.register(DOMAIN)
|
|
||||||
class AmbientStationFlowHandler(config_entries.ConfigFlow):
|
|
||||||
"""Handle an Ambient PWS config flow."""
|
"""Handle an Ambient PWS config flow."""
|
||||||
|
|
||||||
VERSION = 2
|
VERSION = 2
|
||||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH
|
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH
|
||||||
|
|
||||||
async def _show_form(self, errors=None):
|
def __init__(self):
|
||||||
"""Show the form to the user."""
|
"""Initialize the config flow."""
|
||||||
data_schema = vol.Schema(
|
self.data_schema = vol.Schema(
|
||||||
{vol.Required(CONF_API_KEY): str, vol.Required(CONF_APP_KEY): str}
|
{vol.Required(CONF_API_KEY): str, vol.Required(CONF_APP_KEY): str}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def _show_form(self, errors=None):
|
||||||
|
"""Show the form to the user."""
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="user", data_schema=data_schema, errors=errors if errors else {}
|
step_id="user",
|
||||||
|
data_schema=self.data_schema,
|
||||||
|
errors=errors if errors else {},
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_import(self, import_config):
|
async def async_step_import(self, import_config):
|
||||||
@ -42,12 +36,11 @@ class AmbientStationFlowHandler(config_entries.ConfigFlow):
|
|||||||
|
|
||||||
async def async_step_user(self, user_input=None):
|
async def async_step_user(self, user_input=None):
|
||||||
"""Handle the start of the config flow."""
|
"""Handle the start of the config flow."""
|
||||||
|
|
||||||
if not user_input:
|
if not user_input:
|
||||||
return await self._show_form()
|
return await self._show_form()
|
||||||
|
|
||||||
if user_input[CONF_APP_KEY] in configured_instances(self.hass):
|
await self.async_set_unique_id(user_input[CONF_APP_KEY])
|
||||||
return await self._show_form({CONF_APP_KEY: "identifier_exists"})
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
session = aiohttp_client.async_get_clientsession(self.hass)
|
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||||
client = Client(user_input[CONF_API_KEY], user_input[CONF_APP_KEY], session)
|
client = Client(user_input[CONF_API_KEY], user_input[CONF_APP_KEY], session)
|
||||||
|
@ -8,7 +8,5 @@ CONF_APP_KEY = "app_key"
|
|||||||
|
|
||||||
DATA_CLIENT = "data_client"
|
DATA_CLIENT = "data_client"
|
||||||
|
|
||||||
TOPIC_UPDATE = "ambient_station_data_update_{0}"
|
|
||||||
|
|
||||||
TYPE_BINARY_SENSOR = "binary_sensor"
|
TYPE_BINARY_SENSOR = "binary_sensor"
|
||||||
TYPE_SENSOR = "sensor"
|
TYPE_SENSOR = "sensor"
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "Ambient Weather Station",
|
"name": "Ambient Weather Station",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/ambient_station",
|
"documentation": "https://www.home-assistant.io/integrations/ambient_station",
|
||||||
"requirements": ["aioambient==1.0.2"],
|
"requirements": ["aioambient==1.0.4"],
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"codeowners": ["@bachya"]
|
"codeowners": ["@bachya"]
|
||||||
}
|
}
|
||||||
|
@ -83,7 +83,11 @@ class AmbientWeatherSensor(AmbientWeatherEntity):
|
|||||||
w_m2_brightness_val = self._ambient.stations[self._mac_address][
|
w_m2_brightness_val = self._ambient.stations[self._mac_address][
|
||||||
ATTR_LAST_DATA
|
ATTR_LAST_DATA
|
||||||
].get(TYPE_SOLARRADIATION)
|
].get(TYPE_SOLARRADIATION)
|
||||||
self._state = round(float(w_m2_brightness_val) / 0.0079)
|
|
||||||
|
if w_m2_brightness_val is None:
|
||||||
|
self._state = None
|
||||||
|
else:
|
||||||
|
self._state = round(float(w_m2_brightness_val) / 0.0079)
|
||||||
else:
|
else:
|
||||||
self._state = self._ambient.stations[self._mac_address][ATTR_LAST_DATA].get(
|
self._state = self._ambient.stations[self._mac_address][ATTR_LAST_DATA].get(
|
||||||
self._sensor_type
|
self._sensor_type
|
||||||
|
@ -11,9 +11,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"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",
|
||||||
"no_devices": "No devices found in account"
|
"no_devices": "No devices found in account"
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "This app key is already in use."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -109,7 +109,6 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-ancestors
|
|
||||||
class AmcrestChecker(Http):
|
class AmcrestChecker(Http):
|
||||||
"""amcrest.Http wrapper for catching errors."""
|
"""amcrest.Http wrapper for catching errors."""
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ class AmcrestBinarySensor(BinarySensorDevice):
|
|||||||
|
|
||||||
def __init__(self, name, device, sensor_type):
|
def __init__(self, name, device, sensor_type):
|
||||||
"""Initialize entity."""
|
"""Initialize entity."""
|
||||||
self._name = "{} {}".format(name, BINARY_SENSORS[sensor_type][0])
|
self._name = f"{name} {BINARY_SENSORS[sensor_type][0]}"
|
||||||
self._signal_name = name
|
self._signal_name = name
|
||||||
self._api = device.api
|
self._api = device.api
|
||||||
self._sensor_type = sensor_type
|
self._sensor_type = sensor_type
|
||||||
|
@ -491,9 +491,7 @@ class AmcrestCam(Camera):
|
|||||||
"""Enable or disable indicator light."""
|
"""Enable or disable indicator light."""
|
||||||
try:
|
try:
|
||||||
self._api.command(
|
self._api.command(
|
||||||
"configManager.cgi?action=setConfig&LightGlobal[0].Enable={}".format(
|
f"configManager.cgi?action=setConfig&LightGlobal[0].Enable={str(enable).lower()}"
|
||||||
str(enable).lower()
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
except AmcrestError as error:
|
except AmcrestError as error:
|
||||||
log_update_error(
|
log_update_error(
|
||||||
|
@ -6,7 +6,7 @@ def service_signal(service, ident=None):
|
|||||||
"""Encode service and identifier into signal."""
|
"""Encode service and identifier into signal."""
|
||||||
signal = f"{DOMAIN}_{service}"
|
signal = f"{DOMAIN}_{service}"
|
||||||
if ident:
|
if ident:
|
||||||
signal += "_{}".format(ident.replace(".", "_"))
|
signal += f"_{ident.replace('.', '_')}"
|
||||||
return signal
|
return signal
|
||||||
|
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import logging
|
|||||||
|
|
||||||
from amcrest import AmcrestError
|
from amcrest import AmcrestError
|
||||||
|
|
||||||
from homeassistant.const import CONF_NAME, CONF_SENSORS
|
from homeassistant.const import CONF_NAME, CONF_SENSORS, UNIT_PERCENTAGE
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ SENSOR_SDCARD = "sdcard"
|
|||||||
# Sensor types are defined like: Name, units, icon
|
# Sensor types are defined like: Name, units, icon
|
||||||
SENSORS = {
|
SENSORS = {
|
||||||
SENSOR_PTZ_PRESET: ["PTZ Preset", None, "mdi:camera-iris"],
|
SENSOR_PTZ_PRESET: ["PTZ Preset", None, "mdi:camera-iris"],
|
||||||
SENSOR_SDCARD: ["SD Used", "%", "mdi:sd"],
|
SENSOR_SDCARD: ["SD Used", UNIT_PERCENTAGE, "mdi:sd"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -45,7 +45,7 @@ class AmcrestSensor(Entity):
|
|||||||
|
|
||||||
def __init__(self, name, device, sensor_type):
|
def __init__(self, name, device, sensor_type):
|
||||||
"""Initialize a sensor for Amcrest camera."""
|
"""Initialize a sensor for Amcrest camera."""
|
||||||
self._name = "{} {}".format(name, SENSORS[sensor_type][0])
|
self._name = f"{name} {SENSORS[sensor_type][0]}"
|
||||||
self._signal_name = name
|
self._signal_name = name
|
||||||
self._api = device.api
|
self._api = device.api
|
||||||
self._sensor_type = sensor_type
|
self._sensor_type = sensor_type
|
||||||
@ -98,15 +98,21 @@ class AmcrestSensor(Entity):
|
|||||||
elif self._sensor_type == SENSOR_SDCARD:
|
elif self._sensor_type == SENSOR_SDCARD:
|
||||||
storage = self._api.storage_all
|
storage = self._api.storage_all
|
||||||
try:
|
try:
|
||||||
self._attrs["Total"] = "{:.2f} {}".format(*storage["total"])
|
self._attrs[
|
||||||
|
"Total"
|
||||||
|
] = f"{storage['total'][0]:.2f} {storage['total'][1]}"
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self._attrs["Total"] = "{} {}".format(*storage["total"])
|
self._attrs[
|
||||||
|
"Total"
|
||||||
|
] = f"{storage['total'][0]} {storage['total'][1]}"
|
||||||
try:
|
try:
|
||||||
self._attrs["Used"] = "{:.2f} {}".format(*storage["used"])
|
self._attrs[
|
||||||
|
"Used"
|
||||||
|
] = f"{storage['used'][0]:.2f} {storage['used'][1]}"
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self._attrs["Used"] = "{} {}".format(*storage["used"])
|
self._attrs["Used"] = f"{storage['used'][0]} {storage['used'][1]}"
|
||||||
try:
|
try:
|
||||||
self._state = "{:.2f}".format(storage["used_percent"])
|
self._state = f"{storage['used_percent']:.2f}"
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self._state = storage["used_percent"]
|
self._state = storage["used_percent"]
|
||||||
except AmcrestError as error:
|
except AmcrestError as error:
|
||||||
|
@ -75,9 +75,7 @@ class PwrCtrlSwitch(SwitchDevice):
|
|||||||
@property
|
@property
|
||||||
def unique_id(self):
|
def unique_id(self):
|
||||||
"""Return the unique ID of the device."""
|
"""Return the unique ID of the device."""
|
||||||
return "{device}-{switch_idx}".format(
|
return f"{self._port.device.host}-{self._port.get_index()}"
|
||||||
device=self._port.device.host, switch_idx=self._port.get_index()
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
@ -6,7 +6,14 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from homeassistant.components import apcupsd
|
from homeassistant.components import apcupsd
|
||||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||||
from homeassistant.const import CONF_RESOURCES, POWER_WATT, TEMP_CELSIUS
|
from homeassistant.const import (
|
||||||
|
CONF_RESOURCES,
|
||||||
|
POWER_WATT,
|
||||||
|
TEMP_CELSIUS,
|
||||||
|
TIME_MINUTES,
|
||||||
|
TIME_SECONDS,
|
||||||
|
UNIT_PERCENTAGE,
|
||||||
|
)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
@ -22,7 +29,7 @@ SENSOR_TYPES = {
|
|||||||
"battdate": ["Battery Replaced", "", "mdi:calendar-clock"],
|
"battdate": ["Battery Replaced", "", "mdi:calendar-clock"],
|
||||||
"battstat": ["Battery Status", "", "mdi:information-outline"],
|
"battstat": ["Battery Status", "", "mdi:information-outline"],
|
||||||
"battv": ["Battery Voltage", "V", "mdi:flash"],
|
"battv": ["Battery Voltage", "V", "mdi:flash"],
|
||||||
"bcharge": ["Battery", "%", "mdi:battery"],
|
"bcharge": ["Battery", UNIT_PERCENTAGE, "mdi:battery"],
|
||||||
"cable": ["Cable Type", "", "mdi:ethernet-cable"],
|
"cable": ["Cable Type", "", "mdi:ethernet-cable"],
|
||||||
"cumonbatt": ["Total Time on Battery", "", "mdi:timer"],
|
"cumonbatt": ["Total Time on Battery", "", "mdi:timer"],
|
||||||
"date": ["Status Date", "", "mdi:calendar-clock"],
|
"date": ["Status Date", "", "mdi:calendar-clock"],
|
||||||
@ -36,20 +43,20 @@ SENSOR_TYPES = {
|
|||||||
"firmware": ["Firmware Version", "", "mdi:information-outline"],
|
"firmware": ["Firmware Version", "", "mdi:information-outline"],
|
||||||
"hitrans": ["Transfer High", "V", "mdi:flash"],
|
"hitrans": ["Transfer High", "V", "mdi:flash"],
|
||||||
"hostname": ["Hostname", "", "mdi:information-outline"],
|
"hostname": ["Hostname", "", "mdi:information-outline"],
|
||||||
"humidity": ["Ambient Humidity", "%", "mdi:water-percent"],
|
"humidity": ["Ambient Humidity", UNIT_PERCENTAGE, "mdi:water-percent"],
|
||||||
"itemp": ["Internal Temperature", TEMP_CELSIUS, "mdi:thermometer"],
|
"itemp": ["Internal Temperature", TEMP_CELSIUS, "mdi:thermometer"],
|
||||||
"lastxfer": ["Last Transfer", "", "mdi:transfer"],
|
"lastxfer": ["Last Transfer", "", "mdi:transfer"],
|
||||||
"linefail": ["Input Voltage Status", "", "mdi:information-outline"],
|
"linefail": ["Input Voltage Status", "", "mdi:information-outline"],
|
||||||
"linefreq": ["Line Frequency", "Hz", "mdi:information-outline"],
|
"linefreq": ["Line Frequency", "Hz", "mdi:information-outline"],
|
||||||
"linev": ["Input Voltage", "V", "mdi:flash"],
|
"linev": ["Input Voltage", "V", "mdi:flash"],
|
||||||
"loadpct": ["Load", "%", "mdi:gauge"],
|
"loadpct": ["Load", UNIT_PERCENTAGE, "mdi:gauge"],
|
||||||
"loadapnt": ["Load Apparent Power", "%", "mdi:gauge"],
|
"loadapnt": ["Load Apparent Power", UNIT_PERCENTAGE, "mdi:gauge"],
|
||||||
"lotrans": ["Transfer Low", "V", "mdi:flash"],
|
"lotrans": ["Transfer Low", "V", "mdi:flash"],
|
||||||
"mandate": ["Manufacture Date", "", "mdi:calendar"],
|
"mandate": ["Manufacture Date", "", "mdi:calendar"],
|
||||||
"masterupd": ["Master Update", "", "mdi:information-outline"],
|
"masterupd": ["Master Update", "", "mdi:information-outline"],
|
||||||
"maxlinev": ["Input Voltage High", "V", "mdi:flash"],
|
"maxlinev": ["Input Voltage High", "V", "mdi:flash"],
|
||||||
"maxtime": ["Battery Timeout", "", "mdi:timer-off"],
|
"maxtime": ["Battery Timeout", "", "mdi:timer-off"],
|
||||||
"mbattchg": ["Battery Shutdown", "%", "mdi:battery-alert"],
|
"mbattchg": ["Battery Shutdown", UNIT_PERCENTAGE, "mdi:battery-alert"],
|
||||||
"minlinev": ["Input Voltage Low", "V", "mdi:flash"],
|
"minlinev": ["Input Voltage Low", "V", "mdi:flash"],
|
||||||
"mintimel": ["Shutdown Time", "", "mdi:timer"],
|
"mintimel": ["Shutdown Time", "", "mdi:timer"],
|
||||||
"model": ["Model", "", "mdi:information-outline"],
|
"model": ["Model", "", "mdi:information-outline"],
|
||||||
@ -64,7 +71,7 @@ SENSOR_TYPES = {
|
|||||||
"reg1": ["Register 1 Fault", "", "mdi:information-outline"],
|
"reg1": ["Register 1 Fault", "", "mdi:information-outline"],
|
||||||
"reg2": ["Register 2 Fault", "", "mdi:information-outline"],
|
"reg2": ["Register 2 Fault", "", "mdi:information-outline"],
|
||||||
"reg3": ["Register 3 Fault", "", "mdi:information-outline"],
|
"reg3": ["Register 3 Fault", "", "mdi:information-outline"],
|
||||||
"retpct": ["Restore Requirement", "%", "mdi:battery-alert"],
|
"retpct": ["Restore Requirement", UNIT_PERCENTAGE, "mdi:battery-alert"],
|
||||||
"selftest": ["Last Self Test", "", "mdi:calendar-clock"],
|
"selftest": ["Last Self Test", "", "mdi:calendar-clock"],
|
||||||
"sense": ["Sensitivity", "", "mdi:information-outline"],
|
"sense": ["Sensitivity", "", "mdi:information-outline"],
|
||||||
"serialno": ["Serial Number", "", "mdi:information-outline"],
|
"serialno": ["Serial Number", "", "mdi:information-outline"],
|
||||||
@ -84,16 +91,16 @@ SENSOR_TYPES = {
|
|||||||
|
|
||||||
SPECIFIC_UNITS = {"ITEMP": TEMP_CELSIUS}
|
SPECIFIC_UNITS = {"ITEMP": TEMP_CELSIUS}
|
||||||
INFERRED_UNITS = {
|
INFERRED_UNITS = {
|
||||||
" Minutes": "min",
|
" Minutes": TIME_MINUTES,
|
||||||
" Seconds": "sec",
|
" Seconds": TIME_SECONDS,
|
||||||
" Percent": "%",
|
" Percent": UNIT_PERCENTAGE,
|
||||||
" Volts": "V",
|
" Volts": "V",
|
||||||
" Ampere": "A",
|
" Ampere": "A",
|
||||||
" Volt-Ampere": "VA",
|
" Volt-Ampere": "VA",
|
||||||
" Watts": POWER_WATT,
|
" Watts": POWER_WATT,
|
||||||
" Hz": "Hz",
|
" Hz": "Hz",
|
||||||
" C": TEMP_CELSIUS,
|
" C": TEMP_CELSIUS,
|
||||||
" Percent Load Capacity": "%",
|
" Percent Load Capacity": UNIT_PERCENTAGE,
|
||||||
}
|
}
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
|
@ -26,7 +26,6 @@ from homeassistant.const import (
|
|||||||
URL_API_EVENTS,
|
URL_API_EVENTS,
|
||||||
URL_API_SERVICES,
|
URL_API_SERVICES,
|
||||||
URL_API_STATES,
|
URL_API_STATES,
|
||||||
URL_API_STATES_ENTITY,
|
|
||||||
URL_API_STREAM,
|
URL_API_STREAM,
|
||||||
URL_API_TEMPLATE,
|
URL_API_TEMPLATE,
|
||||||
__version__,
|
__version__,
|
||||||
@ -254,7 +253,7 @@ class APIEntityStateView(HomeAssistantView):
|
|||||||
status_code = HTTP_CREATED if is_new_state else 200
|
status_code = HTTP_CREATED if is_new_state else 200
|
||||||
resp = self.json(hass.states.get(entity_id), status_code)
|
resp = self.json(hass.states.get(entity_id), status_code)
|
||||||
|
|
||||||
resp.headers.add("Location", URL_API_STATES_ENTITY.format(entity_id))
|
resp.headers.add("Location", f"/api/states/{entity_id}")
|
||||||
|
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
@ -88,16 +88,15 @@ def request_configuration(hass, config, atv, credentials):
|
|||||||
try:
|
try:
|
||||||
await atv.airplay.finish_authentication(pin)
|
await atv.airplay.finish_authentication(pin)
|
||||||
hass.components.persistent_notification.async_create(
|
hass.components.persistent_notification.async_create(
|
||||||
"Authentication succeeded!<br /><br />Add the following "
|
f"Authentication succeeded!<br /><br />"
|
||||||
"to credentials: in your apple_tv configuration:<br /><br />"
|
f"Add the following to credentials: "
|
||||||
"{0}".format(credentials),
|
f"in your apple_tv configuration:<br /><br />{credentials}",
|
||||||
title=NOTIFICATION_AUTH_TITLE,
|
title=NOTIFICATION_AUTH_TITLE,
|
||||||
notification_id=NOTIFICATION_AUTH_ID,
|
notification_id=NOTIFICATION_AUTH_ID,
|
||||||
)
|
)
|
||||||
except DeviceAuthenticationError as ex:
|
except DeviceAuthenticationError as ex:
|
||||||
hass.components.persistent_notification.async_create(
|
hass.components.persistent_notification.async_create(
|
||||||
"Authentication failed! Did you enter correct PIN?<br /><br />"
|
f"Authentication failed! Did you enter correct PIN?<br /><br />Details: {ex}",
|
||||||
"Details: {0}".format(ex),
|
|
||||||
title=NOTIFICATION_AUTH_TITLE,
|
title=NOTIFICATION_AUTH_TITLE,
|
||||||
notification_id=NOTIFICATION_AUTH_ID,
|
notification_id=NOTIFICATION_AUTH_ID,
|
||||||
)
|
)
|
||||||
@ -124,9 +123,7 @@ async def scan_apple_tvs(hass):
|
|||||||
if login_id is None:
|
if login_id is None:
|
||||||
login_id = "Home Sharing disabled"
|
login_id = "Home Sharing disabled"
|
||||||
devices.append(
|
devices.append(
|
||||||
"Name: {0}<br />Host: {1}<br />Login ID: {2}".format(
|
f"Name: {atv.name}<br />Host: {atv.address}<br />Login ID: {login_id}"
|
||||||
atv.name, atv.address, login_id
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not devices:
|
if not devices:
|
||||||
|
@ -57,7 +57,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|||||||
|
|
||||||
def make_filter(callsigns: list) -> str:
|
def make_filter(callsigns: list) -> str:
|
||||||
"""Make a server-side filter from a list of callsigns."""
|
"""Make a server-side filter from a list of callsigns."""
|
||||||
return " ".join("b/{0}".format(cs.upper()) for cs in callsigns)
|
return " ".join(f"b/{sign.upper()}" for sign in callsigns)
|
||||||
|
|
||||||
|
|
||||||
def gps_accuracy(gps, posambiguity: int) -> int:
|
def gps_accuracy(gps, posambiguity: int) -> int:
|
||||||
|
@ -4,7 +4,12 @@ import logging
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||||
from homeassistant.const import CONF_MONITORED_CONDITIONS, TEMP_CELSIUS, TEMP_FAHRENHEIT
|
from homeassistant.const import (
|
||||||
|
CONF_MONITORED_CONDITIONS,
|
||||||
|
TEMP_CELSIUS,
|
||||||
|
TEMP_FAHRENHEIT,
|
||||||
|
UNIT_PERCENTAGE,
|
||||||
|
)
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
@ -14,7 +19,7 @@ from . import DOMAIN, UPDATE_TOPIC
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
TEMP_UNITS = [TEMP_CELSIUS, TEMP_FAHRENHEIT]
|
TEMP_UNITS = [TEMP_CELSIUS, TEMP_FAHRENHEIT]
|
||||||
PERCENT_UNITS = ["%", "%"]
|
PERCENT_UNITS = [UNIT_PERCENTAGE, UNIT_PERCENTAGE]
|
||||||
SALT_UNITS = ["g/L", "PPM"]
|
SALT_UNITS = ["g/L", "PPM"]
|
||||||
WATT_UNITS = ["W", "W"]
|
WATT_UNITS = ["W", "W"]
|
||||||
NO_UNITS = [None, None]
|
NO_UNITS = [None, None]
|
||||||
@ -70,7 +75,7 @@ class AquaLogicSensor(Entity):
|
|||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the sensor."""
|
"""Return the name of the sensor."""
|
||||||
return "AquaLogic {}".format(SENSOR_TYPES[self._type][0])
|
return f"AquaLogic {SENSOR_TYPES[self._type][0]}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unit_of_measurement(self):
|
def unit_of_measurement(self):
|
||||||
|
@ -70,7 +70,7 @@ class AquaLogicSwitch(SwitchDevice):
|
|||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the switch."""
|
"""Return the name of the switch."""
|
||||||
return "AquaLogic {}".format(SWITCH_TYPES[self._type])
|
return f"AquaLogic {SWITCH_TYPES[self._type]}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self):
|
def should_poll(self):
|
||||||
|
@ -44,9 +44,9 @@ def _optional_zone(value):
|
|||||||
def _zone_name_validator(config):
|
def _zone_name_validator(config):
|
||||||
for zone, zone_config in config[CONF_ZONE].items():
|
for zone, zone_config in config[CONF_ZONE].items():
|
||||||
if CONF_NAME not in zone_config:
|
if CONF_NAME not in zone_config:
|
||||||
zone_config[CONF_NAME] = "{} ({}:{}) - {}".format(
|
zone_config[
|
||||||
DEFAULT_NAME, config[CONF_HOST], config[CONF_PORT], zone
|
CONF_NAME
|
||||||
)
|
] = f"{DEFAULT_NAME} ({config[CONF_HOST]}:{config[CONF_PORT]}) - {zone}"
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
@ -140,7 +140,7 @@ class ArestSensor(Entity):
|
|||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
self.arest = arest
|
self.arest = arest
|
||||||
self._resource = resource
|
self._resource = resource
|
||||||
self._name = "{} {}".format(location.title(), name.title())
|
self._name = f"{location.title()} {name.title()}"
|
||||||
self._variable = variable
|
self._variable = variable
|
||||||
self._pin = pin
|
self._pin = pin
|
||||||
self._state = None
|
self._state = None
|
||||||
@ -204,8 +204,7 @@ class ArestData:
|
|||||||
try:
|
try:
|
||||||
if str(self._pin[0]) == "A":
|
if str(self._pin[0]) == "A":
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
"{}/analog/{}".format(self._resource, self._pin[1:]),
|
f"{self._resource,}/analog/{self._pin[1:]}", timeout=10
|
||||||
timeout=10,
|
|
||||||
)
|
)
|
||||||
self.data = {"value": response.json()["return_value"]}
|
self.data = {"value": response.json()["return_value"]}
|
||||||
except TypeError:
|
except TypeError:
|
||||||
|
@ -86,7 +86,7 @@ class ArestSwitchBase(SwitchDevice):
|
|||||||
def __init__(self, resource, location, name):
|
def __init__(self, resource, location, name):
|
||||||
"""Initialize the switch."""
|
"""Initialize the switch."""
|
||||||
self._resource = resource
|
self._resource = resource
|
||||||
self._name = "{} {}".format(location.title(), name.title())
|
self._name = f"{location.title()} {name.title()}"
|
||||||
self._state = None
|
self._state = None
|
||||||
self._available = True
|
self._available = True
|
||||||
|
|
||||||
|
@ -67,9 +67,7 @@ def setup(hass, config):
|
|||||||
except (ConnectTimeout, HTTPError) as ex:
|
except (ConnectTimeout, HTTPError) as ex:
|
||||||
_LOGGER.error("Unable to connect to Netgear Arlo: %s", str(ex))
|
_LOGGER.error("Unable to connect to Netgear Arlo: %s", str(ex))
|
||||||
hass.components.persistent_notification.create(
|
hass.components.persistent_notification.create(
|
||||||
"Error: {}<br />"
|
f"Error: {ex}<br />You will need to restart hass after fixing.",
|
||||||
"You will need to restart hass after fixing."
|
|
||||||
"".format(ex),
|
|
||||||
title=NOTIFICATION_TITLE,
|
title=NOTIFICATION_TITLE,
|
||||||
notification_id=NOTIFICATION_ID,
|
notification_id=NOTIFICATION_ID,
|
||||||
)
|
)
|
||||||
|
@ -83,8 +83,9 @@ class ArloCam(Camera):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not video:
|
if not video:
|
||||||
error_msg = "Video not found for {0}. Is it older than {1} days?".format(
|
error_msg = (
|
||||||
self.name, self._camera.min_days_vdo_cache
|
f"Video not found for {self.name}. "
|
||||||
|
f"Is it older than {self._camera.min_days_vdo_cache} days?"
|
||||||
)
|
)
|
||||||
_LOGGER.error(error_msg)
|
_LOGGER.error(error_msg)
|
||||||
return
|
return
|
||||||
|
@ -6,10 +6,12 @@ import voluptuous as vol
|
|||||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ATTRIBUTION,
|
ATTR_ATTRIBUTION,
|
||||||
|
CONCENTRATION_PARTS_PER_MILLION,
|
||||||
CONF_MONITORED_CONDITIONS,
|
CONF_MONITORED_CONDITIONS,
|
||||||
DEVICE_CLASS_HUMIDITY,
|
DEVICE_CLASS_HUMIDITY,
|
||||||
DEVICE_CLASS_TEMPERATURE,
|
DEVICE_CLASS_TEMPERATURE,
|
||||||
TEMP_CELSIUS,
|
TEMP_CELSIUS,
|
||||||
|
UNIT_PERCENTAGE,
|
||||||
)
|
)
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
@ -26,11 +28,11 @@ SENSOR_TYPES = {
|
|||||||
"last_capture": ["Last", None, "run-fast"],
|
"last_capture": ["Last", None, "run-fast"],
|
||||||
"total_cameras": ["Arlo Cameras", None, "video"],
|
"total_cameras": ["Arlo Cameras", None, "video"],
|
||||||
"captured_today": ["Captured Today", None, "file-video"],
|
"captured_today": ["Captured Today", None, "file-video"],
|
||||||
"battery_level": ["Battery Level", "%", "battery-50"],
|
"battery_level": ["Battery Level", UNIT_PERCENTAGE, "battery-50"],
|
||||||
"signal_strength": ["Signal Strength", None, "signal"],
|
"signal_strength": ["Signal Strength", None, "signal"],
|
||||||
"temperature": ["Temperature", TEMP_CELSIUS, "thermometer"],
|
"temperature": ["Temperature", TEMP_CELSIUS, "thermometer"],
|
||||||
"humidity": ["Humidity", "%", "water-percent"],
|
"humidity": ["Humidity", UNIT_PERCENTAGE, "water-percent"],
|
||||||
"air_quality": ["Air Quality", "ppm", "biohazard"],
|
"air_quality": ["Air Quality", CONCENTRATION_PARTS_PER_MILLION, "biohazard"],
|
||||||
}
|
}
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
@ -57,7 +59,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||||||
if sensor_type in ("temperature", "humidity", "air_quality"):
|
if sensor_type in ("temperature", "humidity", "air_quality"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
name = "{0} {1}".format(SENSOR_TYPES[sensor_type][0], camera.name)
|
name = f"{SENSOR_TYPES[sensor_type][0]} {camera.name}"
|
||||||
sensors.append(ArloSensor(name, camera, sensor_type))
|
sensors.append(ArloSensor(name, camera, sensor_type))
|
||||||
|
|
||||||
for base_station in arlo.base_stations:
|
for base_station in arlo.base_stations:
|
||||||
@ -65,9 +67,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||||||
sensor_type in ("temperature", "humidity", "air_quality")
|
sensor_type in ("temperature", "humidity", "air_quality")
|
||||||
and base_station.model_id == "ABC1000"
|
and base_station.model_id == "ABC1000"
|
||||||
):
|
):
|
||||||
name = "{0} {1}".format(
|
name = f"{SENSOR_TYPES[sensor_type][0]} {base_station.name}"
|
||||||
SENSOR_TYPES[sensor_type][0], base_station.name
|
|
||||||
)
|
|
||||||
sensors.append(ArloSensor(name, base_station, sensor_type))
|
sensors.append(ArloSensor(name, base_station, sensor_type))
|
||||||
|
|
||||||
add_entities(sensors, True)
|
add_entities(sensors, True)
|
||||||
@ -83,7 +83,7 @@ class ArloSensor(Entity):
|
|||||||
self._data = device
|
self._data = device
|
||||||
self._sensor_type = sensor_type
|
self._sensor_type = sensor_type
|
||||||
self._state = None
|
self._state = None
|
||||||
self._icon = "mdi:{}".format(SENSOR_TYPES.get(self._sensor_type)[2])
|
self._icon = f"mdi:{SENSOR_TYPES.get(self._sensor_type)[2]}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
@ -141,8 +141,9 @@ class ArloSensor(Entity):
|
|||||||
video = self._data.last_video
|
video = self._data.last_video
|
||||||
self._state = video.created_at_pretty("%m-%d-%Y %H:%M:%S")
|
self._state = video.created_at_pretty("%m-%d-%Y %H:%M:%S")
|
||||||
except (AttributeError, IndexError):
|
except (AttributeError, IndexError):
|
||||||
error_msg = "Video not found for {0}. Older than {1} days?".format(
|
error_msg = (
|
||||||
self.name, self._data.min_days_vdo_cache
|
f"Video not found for {self.name}. "
|
||||||
|
f"Older than {self._data.min_days_vdo_cache} days?"
|
||||||
)
|
)
|
||||||
_LOGGER.debug(error_msg)
|
_LOGGER.debug(error_msg)
|
||||||
self._state = None
|
self._state = None
|
||||||
|
@ -84,8 +84,8 @@ class ArubaDeviceScanner(DeviceScanner):
|
|||||||
def get_aruba_data(self):
|
def get_aruba_data(self):
|
||||||
"""Retrieve data from Aruba Access Point and return parsed result."""
|
"""Retrieve data from Aruba Access Point and return parsed result."""
|
||||||
|
|
||||||
connect = "ssh {}@{}"
|
connect = f"ssh {self.username}@{self.host}"
|
||||||
ssh = pexpect.spawn(connect.format(self.username, self.host))
|
ssh = pexpect.spawn(connect)
|
||||||
query = ssh.expect(
|
query = ssh.expect(
|
||||||
[
|
[
|
||||||
"password:",
|
"password:",
|
||||||
|
@ -50,7 +50,7 @@ def discover_sensors(topic, payload):
|
|||||||
|
|
||||||
|
|
||||||
def _slug(name):
|
def _slug(name):
|
||||||
return "sensor.arwn_{}".format(slugify(name))
|
return f"sensor.arwn_{slugify(name)}"
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||||
|
@ -49,8 +49,10 @@ class AsteriskCDR(Mailbox):
|
|||||||
"duration": entry["duration"],
|
"duration": entry["duration"],
|
||||||
}
|
}
|
||||||
sha = hashlib.sha256(str(entry).encode("utf-8")).hexdigest()
|
sha = hashlib.sha256(str(entry).encode("utf-8")).hexdigest()
|
||||||
msg = "Destination: {}\nApplication: {}\n Context: {}".format(
|
msg = (
|
||||||
entry["dest"], entry["application"], entry["context"]
|
f"Destination: {entry['dest']}\n"
|
||||||
|
f"Application: {entry['application']}\n "
|
||||||
|
f"Context: {entry['context']}"
|
||||||
)
|
)
|
||||||
cdr.append({"info": info, "sha": sha, "text": msg})
|
cdr.append({"info": info, "sha": sha, "text": msg})
|
||||||
self.cdr = cdr
|
self.cdr = cdr
|
||||||
|
@ -17,6 +17,8 @@ from homeassistant.helpers.discovery import async_load_platform
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CONF_DNSMASQ = "dnsmasq"
|
||||||
|
CONF_INTERFACE = "interface"
|
||||||
CONF_PUB_KEY = "pub_key"
|
CONF_PUB_KEY = "pub_key"
|
||||||
CONF_REQUIRE_IP = "require_ip"
|
CONF_REQUIRE_IP = "require_ip"
|
||||||
CONF_SENSORS = "sensors"
|
CONF_SENSORS = "sensors"
|
||||||
@ -24,7 +26,10 @@ CONF_SSH_KEY = "ssh_key"
|
|||||||
|
|
||||||
DOMAIN = "asuswrt"
|
DOMAIN = "asuswrt"
|
||||||
DATA_ASUSWRT = DOMAIN
|
DATA_ASUSWRT = DOMAIN
|
||||||
|
|
||||||
DEFAULT_SSH_PORT = 22
|
DEFAULT_SSH_PORT = 22
|
||||||
|
DEFAULT_INTERFACE = "eth0"
|
||||||
|
DEFAULT_DNSMASQ = "/var/lib/misc"
|
||||||
|
|
||||||
SECRET_GROUP = "Password or SSH Key"
|
SECRET_GROUP = "Password or SSH Key"
|
||||||
SENSOR_TYPES = ["upload_speed", "download_speed", "download", "upload"]
|
SENSOR_TYPES = ["upload_speed", "download_speed", "download", "upload"]
|
||||||
@ -45,6 +50,8 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
vol.Optional(CONF_SENSORS): vol.All(
|
vol.Optional(CONF_SENSORS): vol.All(
|
||||||
cv.ensure_list, [vol.In(SENSOR_TYPES)]
|
cv.ensure_list, [vol.In(SENSOR_TYPES)]
|
||||||
),
|
),
|
||||||
|
vol.Optional(CONF_INTERFACE, default=DEFAULT_INTERFACE): cv.string,
|
||||||
|
vol.Optional(CONF_DNSMASQ, default=DEFAULT_DNSMASQ): cv.isdir,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@ -59,13 +66,15 @@ async def async_setup(hass, config):
|
|||||||
|
|
||||||
api = AsusWrt(
|
api = AsusWrt(
|
||||||
conf[CONF_HOST],
|
conf[CONF_HOST],
|
||||||
conf.get(CONF_PORT),
|
conf[CONF_PORT],
|
||||||
conf.get(CONF_PROTOCOL) == "telnet",
|
conf[CONF_PROTOCOL] == "telnet",
|
||||||
conf[CONF_USERNAME],
|
conf[CONF_USERNAME],
|
||||||
conf.get(CONF_PASSWORD, ""),
|
conf.get(CONF_PASSWORD, ""),
|
||||||
conf.get("ssh_key", conf.get("pub_key", "")),
|
conf.get("ssh_key", conf.get("pub_key", "")),
|
||||||
conf.get(CONF_MODE),
|
conf[CONF_MODE],
|
||||||
conf.get(CONF_REQUIRE_IP),
|
conf[CONF_REQUIRE_IP],
|
||||||
|
conf[CONF_INTERFACE],
|
||||||
|
conf[CONF_DNSMASQ],
|
||||||
)
|
)
|
||||||
|
|
||||||
await api.connection.async_connect()
|
await api.connection.async_connect()
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"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.2.2"],
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"codeowners": ["@kennedyshead"]
|
"codeowners": ["@kennedyshead"]
|
||||||
}
|
}
|
||||||
|
32
homeassistant/components/august/.translations/ca.json
Normal file
32
homeassistant/components/august/.translations/ca.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "El compte ja ha estat configurat"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "No s'ha pogut connectar, torna-ho a provar",
|
||||||
|
"invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
|
||||||
|
"unknown": "Error inesperat"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"login_method": "M\u00e8tode d'inici de sessi\u00f3",
|
||||||
|
"password": "Contrasenya",
|
||||||
|
"timeout": "Temps d'espera (segons)",
|
||||||
|
"username": "Nom d'usuari"
|
||||||
|
},
|
||||||
|
"description": "Si el m\u00e8tode d'inici de sessi\u00f3 \u00e9s 'email', el nom d'usuari \u00e9s l'adre\u00e7a de correu electr\u00f2nic. Si el m\u00e8tode d'inici de sessi\u00f3 \u00e9s 'phone', el nom d'usuari \u00e9s el n\u00famero de tel\u00e8fon en el format \"+NNNNNNNNN\".",
|
||||||
|
"title": "Configuraci\u00f3 de compte August"
|
||||||
|
},
|
||||||
|
"validation": {
|
||||||
|
"data": {
|
||||||
|
"code": "Codi de verificaci\u00f3"
|
||||||
|
},
|
||||||
|
"description": "Comprova el teu {login_method} ({username}) i introdueix el codi de verificaci\u00f3 a continuaci\u00f3",
|
||||||
|
"title": "Autenticaci\u00f3 de dos factors"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "August"
|
||||||
|
}
|
||||||
|
}
|
32
homeassistant/components/august/.translations/da.json
Normal file
32
homeassistant/components/august/.translations/da.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Kontoen er allerede konfigureret"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Kunne ikke oprette forbindelse. Pr\u00f8v igen",
|
||||||
|
"invalid_auth": "Ugyldig godkendelse",
|
||||||
|
"unknown": "Uventet fejl"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"login_method": "Loginmetode",
|
||||||
|
"password": "Adgangskode",
|
||||||
|
"timeout": "Timeout (sekunder)",
|
||||||
|
"username": "Brugernavn"
|
||||||
|
},
|
||||||
|
"description": "Hvis loginmetoden er 'e-mail', er brugernavn e-mailadressen. Hvis loginmetoden er 'telefon', er brugernavn telefonnummeret i formatet '+NNNNNNNNNN'.",
|
||||||
|
"title": "Konfigurer en August-konto"
|
||||||
|
},
|
||||||
|
"validation": {
|
||||||
|
"data": {
|
||||||
|
"code": "Bekr\u00e6ftelseskode"
|
||||||
|
},
|
||||||
|
"description": "Kontroller dit {login_method} ({username}), og angiv bekr\u00e6ftelseskoden nedenfor",
|
||||||
|
"title": "Tofaktorgodkendelse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "August"
|
||||||
|
}
|
||||||
|
}
|
32
homeassistant/components/august/.translations/es.json
Normal file
32
homeassistant/components/august/.translations/es.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "La cuenta ya est\u00e1 configurada"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntalo de nuevo.",
|
||||||
|
"invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
|
||||||
|
"unknown": "Error inesperado"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"login_method": "M\u00e9todo de inicio de sesi\u00f3n",
|
||||||
|
"password": "Contrase\u00f1a",
|
||||||
|
"timeout": "Tiempo de espera (segundos)",
|
||||||
|
"username": "Usuario"
|
||||||
|
},
|
||||||
|
"description": "Si el M\u00e9todo de Inicio de Sesi\u00f3n es 'correo electr\u00f3nico', Usuario es la direcci\u00f3n de correo electr\u00f3nico. Si el M\u00e9todo de Inicio de Sesi\u00f3n es 'tel\u00e9fono', Usuario es el n\u00famero de tel\u00e9fono en formato '+NNNNNNNNN'.",
|
||||||
|
"title": "Configurar una cuenta de August"
|
||||||
|
},
|
||||||
|
"validation": {
|
||||||
|
"data": {
|
||||||
|
"code": "C\u00f3digo de verificaci\u00f3n"
|
||||||
|
},
|
||||||
|
"description": "Por favor, compruebe tu {login_method} ({username}) e introduce el c\u00f3digo de verificaci\u00f3n a continuaci\u00f3n",
|
||||||
|
"title": "Autenticaci\u00f3n de dos factores"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "August"
|
||||||
|
}
|
||||||
|
}
|
32
homeassistant/components/august/.translations/it.json
Normal file
32
homeassistant/components/august/.translations/it.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "L'account \u00e8 gi\u00e0 configurato"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Impossibile connettersi, si prega di riprovare.",
|
||||||
|
"invalid_auth": "Autenticazione non valida",
|
||||||
|
"unknown": "Errore imprevisto"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"login_method": "Metodo di accesso",
|
||||||
|
"password": "Password",
|
||||||
|
"timeout": "Timeout (in secondi)",
|
||||||
|
"username": "Nome utente"
|
||||||
|
},
|
||||||
|
"description": "Se il metodo di accesso \u00e8 \"e-mail\", il nome utente \u00e8 l'indirizzo e-mail. Se il metodo di accesso \u00e8 \"telefono\", il nome utente \u00e8 il numero di telefono nel formato \"+NNNNNNNNN\".",
|
||||||
|
"title": "Configura un account di August"
|
||||||
|
},
|
||||||
|
"validation": {
|
||||||
|
"data": {
|
||||||
|
"code": "Codice di verifica"
|
||||||
|
},
|
||||||
|
"description": "Controlla il tuo {login_method} ({username}) e inserisci il codice di verifica seguente",
|
||||||
|
"title": "Autenticazione a due fattori"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "August"
|
||||||
|
}
|
||||||
|
}
|
32
homeassistant/components/august/.translations/lb.json
Normal file
32
homeassistant/components/august/.translations/lb.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Kont ass scho konfigur\u00e9iert"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Feeler beim verbannen, prob\u00e9iert w.e.g. nach emol.",
|
||||||
|
"invalid_auth": "Ong\u00eblteg Authentifikatioun",
|
||||||
|
"unknown": "Onerwaarte Feeler"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"login_method": "Login Method",
|
||||||
|
"password": "Passwuert",
|
||||||
|
"timeout": "Z\u00e4itiwwerscheidung (sekonnen)",
|
||||||
|
"username": "Benotzernumm"
|
||||||
|
},
|
||||||
|
"description": "Wann d'Login Method 'E-Mail' ass, dannn ass de Benotzernumm d'E-Mail Adress. Wann d'Login-Method 'Telefon' ass, ass den Benotzernumm d'Telefonsnummer am Format '+ NNNNNNNNN'.",
|
||||||
|
"title": "August Kont ariichten"
|
||||||
|
},
|
||||||
|
"validation": {
|
||||||
|
"data": {
|
||||||
|
"code": "Verifikatiouns Code"
|
||||||
|
},
|
||||||
|
"description": "Pr\u00e9ift w.e.g. \u00c4re {login_method} ({username}) a gitt de Verifikatiounscode hei dr\u00ebnner an",
|
||||||
|
"title": "2-Faktor-Authentifikatioun"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "August"
|
||||||
|
}
|
||||||
|
}
|
12
homeassistant/components/august/.translations/lv.json
Normal file
12
homeassistant/components/august/.translations/lv.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"login_method": "Pieteik\u0161an\u0101s metode",
|
||||||
|
"password": "Parole"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
homeassistant/components/august/.translations/no.json
Normal file
32
homeassistant/components/august/.translations/no.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Kontoen er allerede konfigurert"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen",
|
||||||
|
"invalid_auth": "Ugyldig godkjenning",
|
||||||
|
"unknown": "Uventet feil"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"login_method": "P\u00e5loggingsmetode",
|
||||||
|
"password": "Passord",
|
||||||
|
"timeout": "Tidsavbrudd (sekunder)",
|
||||||
|
"username": "Brukernavn"
|
||||||
|
},
|
||||||
|
"description": "Hvis p\u00e5loggingsmetoden er 'e-post', er brukernavnet e-postadressen. Hvis p\u00e5loggingsmetoden er 'telefon', er brukernavn telefonnummeret i formatet '+ NNNNNNNNN'.",
|
||||||
|
"title": "Sett opp en August konto"
|
||||||
|
},
|
||||||
|
"validation": {
|
||||||
|
"data": {
|
||||||
|
"code": "Bekreftelseskode"
|
||||||
|
},
|
||||||
|
"description": "Kontroller {login_method} ( {username} ) og skriv inn bekreftelseskoden nedenfor",
|
||||||
|
"title": "To-faktor autentisering"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "August"
|
||||||
|
}
|
||||||
|
}
|
32
homeassistant/components/august/.translations/ru.json
Normal file
32
homeassistant/components/august/.translations/ru.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.",
|
||||||
|
"invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
|
||||||
|
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"login_method": "\u0421\u043f\u043e\u0441\u043e\u0431 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438",
|
||||||
|
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
|
||||||
|
"timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)",
|
||||||
|
"username": "\u041b\u043e\u0433\u0438\u043d"
|
||||||
|
},
|
||||||
|
"description": "\u0415\u0441\u043b\u0438 \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0441\u043f\u043e\u0441\u043e\u0431\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0432\u044b\u0431\u0440\u0430\u043d\u043e 'email', \u0442\u043e \u043b\u043e\u0433\u0438\u043d\u043e\u043c \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b. \u0415\u0441\u043b\u0438 \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0441\u043f\u043e\u0441\u043e\u0431\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0432\u044b\u0431\u0440\u0430\u043d\u043e 'phone', \u0442\u043e \u043b\u043e\u0433\u0438\u043d\u043e\u043c \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043d\u043e\u043c\u0435\u0440 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0430 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 '+NNNNNNNNN'.",
|
||||||
|
"title": "August"
|
||||||
|
},
|
||||||
|
"validation": {
|
||||||
|
"data": {
|
||||||
|
"code": "\u041a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f"
|
||||||
|
},
|
||||||
|
"description": "\u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 {login_method} ({username}) \u0438 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0439 \u043f\u0440\u043e\u0432\u0435\u0440\u043e\u0447\u043d\u044b\u0439 \u043a\u043e\u0434.",
|
||||||
|
"title": "\u0414\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "August"
|
||||||
|
}
|
||||||
|
}
|
32
homeassistant/components/august/.translations/zh-Hant.json
Normal file
32
homeassistant/components/august/.translations/zh-Hant.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21",
|
||||||
|
"invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
|
||||||
|
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"login_method": "\u767b\u5165\u65b9\u5f0f",
|
||||||
|
"password": "\u5bc6\u78bc",
|
||||||
|
"timeout": "\u903e\u6642\uff08\u79d2\uff09",
|
||||||
|
"username": "\u4f7f\u7528\u8005\u540d\u7a31"
|
||||||
|
},
|
||||||
|
"description": "\u5047\u5982\u767b\u5165\u65b9\u5f0f\u70ba\u90f5\u4ef6\u300cemail\u300d\u3001\u4f7f\u7528\u8005\u540d\u7a31\u70ba\u96fb\u5b50\u90f5\u4ef6\u4f4d\u5740\u3002\u5047\u5982\u767b\u5165\u65b9\u5f0f\u70ba\u96fb\u8a71\u300cphone\u300d\u3001\u5247\u4f7f\u7528\u8005\u540d\u7a31\u70ba\u5305\u542b\u570b\u78bc\u4e4b\u96fb\u8a71\u865f\u78bc\uff0c\u5982\u300c+NNNNNNNNN\u300d\u3002",
|
||||||
|
"title": "\u8a2d\u5b9a August \u5e33\u865f"
|
||||||
|
},
|
||||||
|
"validation": {
|
||||||
|
"data": {
|
||||||
|
"code": "\u9a57\u8b49\u78bc"
|
||||||
|
},
|
||||||
|
"description": "\u8acb\u78ba\u8a8d {login_method} ({username}) \u4e26\u65bc\u4e0b\u65b9\u8f38\u5165\u9a57\u8b49\u78bc",
|
||||||
|
"title": "\u5169\u6b65\u9a5f\u9a57\u8b49"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "August"
|
||||||
|
}
|
||||||
|
}
|
@ -1,67 +1,41 @@
|
|||||||
"""Support for August devices."""
|
"""Support for August devices."""
|
||||||
import asyncio
|
import asyncio
|
||||||
from datetime import timedelta
|
import itertools
|
||||||
from functools import partial
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from august.api import Api, AugustApiHTTPError
|
from aiohttp import ClientError
|
||||||
from august.authenticator import AuthenticationState, Authenticator, ValidationResult
|
from august.authenticator import ValidationResult
|
||||||
from requests import RequestException, Session
|
from august.exceptions import AugustApiAIOHTTPError
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||||
CONF_PASSWORD,
|
from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME
|
||||||
CONF_TIMEOUT,
|
from homeassistant.core import HomeAssistant
|
||||||
CONF_USERNAME,
|
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||||
EVENT_HOMEASSISTANT_STOP,
|
|
||||||
)
|
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
|
||||||
from homeassistant.helpers import discovery
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.util import Throttle, dt
|
|
||||||
|
from .activity import ActivityStream
|
||||||
|
from .const import (
|
||||||
|
AUGUST_COMPONENTS,
|
||||||
|
CONF_ACCESS_TOKEN_CACHE_FILE,
|
||||||
|
CONF_INSTALL_ID,
|
||||||
|
CONF_LOGIN_METHOD,
|
||||||
|
DATA_AUGUST,
|
||||||
|
DEFAULT_AUGUST_CONFIG_FILE,
|
||||||
|
DEFAULT_NAME,
|
||||||
|
DEFAULT_TIMEOUT,
|
||||||
|
DOMAIN,
|
||||||
|
LOGIN_METHODS,
|
||||||
|
MIN_TIME_BETWEEN_DETAIL_UPDATES,
|
||||||
|
VERIFICATION_CODE_KEY,
|
||||||
|
)
|
||||||
|
from .exceptions import InvalidAuth, RequireValidation
|
||||||
|
from .gateway import AugustGateway
|
||||||
|
from .subscriber import AugustSubscriberMixin
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
_CONFIGURING = {}
|
TWO_FA_REVALIDATE = "verify_configurator"
|
||||||
|
|
||||||
DEFAULT_TIMEOUT = 10
|
|
||||||
ACTIVITY_FETCH_LIMIT = 10
|
|
||||||
ACTIVITY_INITIAL_FETCH_LIMIT = 20
|
|
||||||
|
|
||||||
CONF_LOGIN_METHOD = "login_method"
|
|
||||||
CONF_INSTALL_ID = "install_id"
|
|
||||||
|
|
||||||
NOTIFICATION_ID = "august_notification"
|
|
||||||
NOTIFICATION_TITLE = "August Setup"
|
|
||||||
|
|
||||||
AUGUST_CONFIG_FILE = ".august.conf"
|
|
||||||
|
|
||||||
DATA_AUGUST = "august"
|
|
||||||
DOMAIN = "august"
|
|
||||||
DEFAULT_ENTITY_NAMESPACE = "august"
|
|
||||||
|
|
||||||
# Limit battery and hardware updates to 1800 seconds
|
|
||||||
# in order to reduce the number of api requests and
|
|
||||||
# avoid hitting rate limits
|
|
||||||
MIN_TIME_BETWEEN_LOCK_DETAIL_UPDATES = timedelta(seconds=1800)
|
|
||||||
|
|
||||||
# Limit locks status check to 900 seconds now that
|
|
||||||
# we get the state from the lock and unlock api calls
|
|
||||||
# and the lock and unlock activities are now captured
|
|
||||||
MIN_TIME_BETWEEN_LOCK_STATUS_UPDATES = timedelta(seconds=900)
|
|
||||||
|
|
||||||
# Doorbells need to update more frequently than locks
|
|
||||||
# since we get an image from the doorbell api
|
|
||||||
MIN_TIME_BETWEEN_DOORBELL_STATUS_UPDATES = timedelta(seconds=20)
|
|
||||||
|
|
||||||
# Activity needs to be checked more frequently as the
|
|
||||||
# doorbell motion and rings are included here
|
|
||||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10)
|
|
||||||
|
|
||||||
DEFAULT_SCAN_INTERVAL = timedelta(seconds=10)
|
|
||||||
|
|
||||||
|
|
||||||
LOGIN_METHODS = ["phone", "email"]
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema(
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
@ -78,447 +52,315 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
extra=vol.ALLOW_EXTRA,
|
extra=vol.ALLOW_EXTRA,
|
||||||
)
|
)
|
||||||
|
|
||||||
AUGUST_COMPONENTS = ["camera", "binary_sensor", "lock"]
|
|
||||||
|
|
||||||
|
async def async_request_validation(hass, config_entry, august_gateway):
|
||||||
|
"""Request a new verification code from the user."""
|
||||||
|
|
||||||
def request_configuration(hass, config, api, authenticator, token_refresh_lock):
|
#
|
||||||
"""Request configuration steps from the user."""
|
# In the future this should start a new config flow
|
||||||
|
# instead of using the legacy configurator
|
||||||
|
#
|
||||||
|
_LOGGER.error("Access token is no longer valid.")
|
||||||
configurator = hass.components.configurator
|
configurator = hass.components.configurator
|
||||||
|
entry_id = config_entry.entry_id
|
||||||
|
|
||||||
def august_configuration_callback(data):
|
async def async_august_configuration_validation_callback(data):
|
||||||
"""Run when the configuration callback is called."""
|
code = data.get(VERIFICATION_CODE_KEY)
|
||||||
|
result = await august_gateway.authenticator.async_validate_verification_code(
|
||||||
result = authenticator.validate_verification_code(data.get("verification_code"))
|
code
|
||||||
|
)
|
||||||
|
|
||||||
if result == ValidationResult.INVALID_VERIFICATION_CODE:
|
if result == ValidationResult.INVALID_VERIFICATION_CODE:
|
||||||
configurator.notify_errors(
|
configurator.async_notify_errors(
|
||||||
_CONFIGURING[DOMAIN], "Invalid verification code"
|
hass.data[DOMAIN][entry_id][TWO_FA_REVALIDATE],
|
||||||
|
"Invalid verification code, please make sure you are using the latest code and try again.",
|
||||||
)
|
)
|
||||||
elif result == ValidationResult.VALIDATED:
|
elif result == ValidationResult.VALIDATED:
|
||||||
setup_august(hass, config, api, authenticator, token_refresh_lock)
|
return await async_setup_august(hass, config_entry, august_gateway)
|
||||||
|
|
||||||
if DOMAIN not in _CONFIGURING:
|
return False
|
||||||
authenticator.send_verification_code()
|
|
||||||
|
|
||||||
conf = config[DOMAIN]
|
if TWO_FA_REVALIDATE not in hass.data[DOMAIN][entry_id]:
|
||||||
username = conf.get(CONF_USERNAME)
|
await august_gateway.authenticator.async_send_verification_code()
|
||||||
login_method = conf.get(CONF_LOGIN_METHOD)
|
|
||||||
|
|
||||||
_CONFIGURING[DOMAIN] = configurator.request_config(
|
entry_data = config_entry.data
|
||||||
NOTIFICATION_TITLE,
|
login_method = entry_data.get(CONF_LOGIN_METHOD)
|
||||||
august_configuration_callback,
|
username = entry_data.get(CONF_USERNAME)
|
||||||
description="Please check your {} ({}) and enter the verification "
|
|
||||||
|
hass.data[DOMAIN][entry_id][TWO_FA_REVALIDATE] = configurator.async_request_config(
|
||||||
|
f"{DEFAULT_NAME} ({username})",
|
||||||
|
async_august_configuration_validation_callback,
|
||||||
|
description="August must be re-verified. Please check your {} ({}) and enter the verification "
|
||||||
"code below".format(login_method, username),
|
"code below".format(login_method, username),
|
||||||
submit_caption="Verify",
|
submit_caption="Verify",
|
||||||
fields=[
|
fields=[
|
||||||
{"id": "verification_code", "name": "Verification code", "type": "string"}
|
{"id": VERIFICATION_CODE_KEY, "name": "Verification code", "type": "string"}
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
def setup_august(hass, config, api, authenticator, token_refresh_lock):
|
async def async_setup_august(hass, config_entry, august_gateway):
|
||||||
"""Set up the August component."""
|
"""Set up the August component."""
|
||||||
|
|
||||||
authentication = None
|
entry_id = config_entry.entry_id
|
||||||
|
hass.data[DOMAIN].setdefault(entry_id, {})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
authentication = authenticator.authenticate()
|
await august_gateway.async_authenticate()
|
||||||
except RequestException as ex:
|
except RequireValidation:
|
||||||
_LOGGER.error("Unable to connect to August service: %s", str(ex))
|
await async_request_validation(hass, config_entry, august_gateway)
|
||||||
|
|
||||||
hass.components.persistent_notification.create(
|
|
||||||
"Error: {}<br />"
|
|
||||||
"You will need to restart hass after fixing."
|
|
||||||
"".format(ex),
|
|
||||||
title=NOTIFICATION_TITLE,
|
|
||||||
notification_id=NOTIFICATION_ID,
|
|
||||||
)
|
|
||||||
|
|
||||||
state = authentication.state
|
|
||||||
|
|
||||||
if state == AuthenticationState.AUTHENTICATED:
|
|
||||||
if DOMAIN in _CONFIGURING:
|
|
||||||
hass.components.configurator.request_done(_CONFIGURING.pop(DOMAIN))
|
|
||||||
|
|
||||||
hass.data[DATA_AUGUST] = AugustData(
|
|
||||||
hass, api, authentication, authenticator, token_refresh_lock
|
|
||||||
)
|
|
||||||
|
|
||||||
for component in AUGUST_COMPONENTS:
|
|
||||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
|
||||||
|
|
||||||
return True
|
|
||||||
if state == AuthenticationState.BAD_PASSWORD:
|
|
||||||
_LOGGER.error("Invalid password provided")
|
|
||||||
return False
|
return False
|
||||||
if state == AuthenticationState.REQUIRES_VALIDATION:
|
except InvalidAuth:
|
||||||
request_configuration(hass, config, api, authenticator, token_refresh_lock)
|
_LOGGER.error("Password is no longer valid. Please set up August again")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# We still use the configurator to get a new 2fa code
|
||||||
|
# when needed since config_flow doesn't have a way
|
||||||
|
# to re-request if it expires
|
||||||
|
if TWO_FA_REVALIDATE in hass.data[DOMAIN][entry_id]:
|
||||||
|
hass.components.configurator.async_request_done(
|
||||||
|
hass.data[DOMAIN][entry_id].pop(TWO_FA_REVALIDATE)
|
||||||
|
)
|
||||||
|
|
||||||
|
hass.data[DOMAIN][entry_id][DATA_AUGUST] = AugustData(hass, august_gateway)
|
||||||
|
|
||||||
|
await hass.data[DOMAIN][entry_id][DATA_AUGUST].async_setup()
|
||||||
|
|
||||||
|
for component in AUGUST_COMPONENTS:
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.async_forward_entry_setup(config_entry, component)
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: dict):
|
||||||
|
"""Set up the August component from YAML."""
|
||||||
|
|
||||||
|
conf = config.get(DOMAIN)
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
|
||||||
|
if not conf:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
hass.async_create_task(
|
||||||
|
hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_IMPORT},
|
||||||
|
data={
|
||||||
|
CONF_LOGIN_METHOD: conf.get(CONF_LOGIN_METHOD),
|
||||||
|
CONF_USERNAME: conf.get(CONF_USERNAME),
|
||||||
|
CONF_PASSWORD: conf.get(CONF_PASSWORD),
|
||||||
|
CONF_INSTALL_ID: conf.get(CONF_INSTALL_ID),
|
||||||
|
CONF_ACCESS_TOKEN_CACHE_FILE: DEFAULT_AUGUST_CONFIG_FILE,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass, config):
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
"""Set up the August component."""
|
"""Set up August from a config entry."""
|
||||||
|
|
||||||
|
august_gateway = AugustGateway(hass)
|
||||||
|
|
||||||
conf = config[DOMAIN]
|
|
||||||
api_http_session = None
|
|
||||||
try:
|
try:
|
||||||
api_http_session = Session()
|
await august_gateway.async_setup(entry.data)
|
||||||
except RequestException as ex:
|
return await async_setup_august(hass, entry, august_gateway)
|
||||||
_LOGGER.warning("Creating HTTP session failed with: %s", str(ex))
|
except asyncio.TimeoutError:
|
||||||
|
raise ConfigEntryNotReady
|
||||||
|
|
||||||
api = Api(timeout=conf.get(CONF_TIMEOUT), http_session=api_http_session)
|
|
||||||
|
|
||||||
authenticator = Authenticator(
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
api,
|
"""Unload a config entry."""
|
||||||
conf.get(CONF_LOGIN_METHOD),
|
unload_ok = all(
|
||||||
conf.get(CONF_USERNAME),
|
await asyncio.gather(
|
||||||
conf.get(CONF_PASSWORD),
|
*[
|
||||||
install_id=conf.get(CONF_INSTALL_ID),
|
hass.config_entries.async_forward_entry_unload(entry, component)
|
||||||
access_token_cache_file=hass.config.path(AUGUST_CONFIG_FILE),
|
for component in AUGUST_COMPONENTS
|
||||||
|
]
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def close_http_session(event):
|
if unload_ok:
|
||||||
"""Close API sessions used to connect to August."""
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
_LOGGER.debug("Closing August HTTP sessions")
|
|
||||||
if api_http_session:
|
|
||||||
try:
|
|
||||||
api_http_session.close()
|
|
||||||
except RequestException:
|
|
||||||
pass
|
|
||||||
|
|
||||||
_LOGGER.debug("August HTTP session closed.")
|
return unload_ok
|
||||||
|
|
||||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, close_http_session)
|
|
||||||
_LOGGER.debug("Registered for Home Assistant stop event")
|
|
||||||
|
|
||||||
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(AugustSubscriberMixin):
|
||||||
"""August data object."""
|
"""August data object."""
|
||||||
|
|
||||||
def __init__(self, hass, api, authentication, authenticator, token_refresh_lock):
|
def __init__(self, hass, august_gateway):
|
||||||
"""Init August data object."""
|
"""Init August data object."""
|
||||||
|
super().__init__(hass, MIN_TIME_BETWEEN_DETAIL_UPDATES)
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._api = api
|
self._august_gateway = august_gateway
|
||||||
self._authenticator = authenticator
|
self.activity_stream = None
|
||||||
self._access_token = authentication.access_token
|
self._api = august_gateway.api
|
||||||
self._access_token_expires = authentication.access_token_expires
|
self._device_detail_by_id = {}
|
||||||
|
self._doorbells_by_id = {}
|
||||||
self._token_refresh_lock = token_refresh_lock
|
self._locks_by_id = {}
|
||||||
self._doorbells = self._api.get_doorbells(self._access_token) or []
|
|
||||||
self._locks = self._api.get_operable_locks(self._access_token) or []
|
|
||||||
self._house_ids = set()
|
self._house_ids = set()
|
||||||
for device in self._doorbells + self._locks:
|
|
||||||
self._house_ids.add(device.house_id)
|
|
||||||
|
|
||||||
self._doorbell_detail_by_id = {}
|
async def async_setup(self):
|
||||||
self._door_last_state_update_time_utc_by_id = {}
|
"""Async setup of august device data and activities."""
|
||||||
self._lock_last_status_update_time_utc_by_id = {}
|
locks = (
|
||||||
self._lock_status_by_id = {}
|
await self._api.async_get_operable_locks(self._august_gateway.access_token)
|
||||||
self._lock_detail_by_id = {}
|
or []
|
||||||
self._door_state_by_id = {}
|
)
|
||||||
self._activities_by_id = {}
|
doorbells = (
|
||||||
|
await self._api.async_get_doorbells(self._august_gateway.access_token) or []
|
||||||
|
)
|
||||||
|
|
||||||
# We check the locks right away so we can
|
self._doorbells_by_id = dict((device.device_id, device) for device in doorbells)
|
||||||
# remove inoperative ones
|
self._locks_by_id = dict((device.device_id, device) for device in locks)
|
||||||
self._update_locks_status()
|
self._house_ids = set(
|
||||||
self._update_locks_detail()
|
device.house_id for device in itertools.chain(locks, doorbells)
|
||||||
|
)
|
||||||
|
|
||||||
self._filter_inoperative_locks()
|
await self._async_refresh_device_detail_by_ids(
|
||||||
|
[device.device_id for device in itertools.chain(locks, doorbells)]
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
# We remove all devices that we are missing
|
||||||
def house_ids(self):
|
# detail as we cannot determine if they are usable.
|
||||||
"""Return a list of house_ids."""
|
# This also allows us to avoid checking for
|
||||||
return self._house_ids
|
# detail being None all over the place
|
||||||
|
self._remove_inoperative_locks()
|
||||||
|
self._remove_inoperative_doorbells()
|
||||||
|
|
||||||
|
self.activity_stream = ActivityStream(
|
||||||
|
self._hass, self._api, self._august_gateway, self._house_ids
|
||||||
|
)
|
||||||
|
await self.activity_stream.async_setup()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def doorbells(self):
|
def doorbells(self):
|
||||||
"""Return a list of doorbells."""
|
"""Return a list of py-august Doorbell objects."""
|
||||||
return self._doorbells
|
return self._doorbells_by_id.values()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def locks(self):
|
def locks(self):
|
||||||
"""Return a list of locks."""
|
"""Return a list of py-august Lock objects."""
|
||||||
return self._locks
|
return self._locks_by_id.values()
|
||||||
|
|
||||||
async def _async_refresh_access_token_if_needed(self):
|
def get_device_detail(self, device_id):
|
||||||
"""Refresh the august access token if needed."""
|
"""Return the py-august LockDetail or DoorbellDetail object for a device."""
|
||||||
if self._authenticator.should_refresh():
|
return self._device_detail_by_id[device_id]
|
||||||
async with self._token_refresh_lock:
|
|
||||||
await self._hass.async_add_executor_job(self._refresh_access_token)
|
|
||||||
|
|
||||||
def _refresh_access_token(self):
|
async def _async_refresh(self, time):
|
||||||
refreshed_authentication = self._authenticator.refresh_access_token(force=False)
|
await self._async_refresh_device_detail_by_ids(self._subscriptions.keys())
|
||||||
_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):
|
async def _async_refresh_device_detail_by_ids(self, device_ids_list):
|
||||||
"""Return a list of activities."""
|
for device_id in device_ids_list:
|
||||||
_LOGGER.debug("Getting device activities for %s", device_id)
|
if device_id in self._locks_by_id:
|
||||||
await self._async_update_device_activities()
|
await self._async_update_device_detail(
|
||||||
|
self._locks_by_id[device_id], self._api.async_get_lock_detail
|
||||||
activities = self._activities_by_id.get(device_id, [])
|
)
|
||||||
if activity_types:
|
elif device_id in self._doorbells_by_id:
|
||||||
return [a for a in activities if a.activity_type in activity_types]
|
await self._async_update_device_detail(
|
||||||
return activities
|
self._doorbells_by_id[device_id],
|
||||||
|
self._api.async_get_doorbell_detail,
|
||||||
async def async_get_latest_device_activity(self, device_id, *activity_types):
|
)
|
||||||
"""Return latest activity."""
|
_LOGGER.debug(
|
||||||
activities = await self.async_get_device_activities(device_id, *activity_types)
|
"async_signal_device_id_update (from detail updates): %s", device_id,
|
||||||
return next(iter(activities or []), None)
|
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
|
||||||
async def _async_update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT):
|
|
||||||
"""Update data object with latest from August API."""
|
|
||||||
|
|
||||||
# This is the only place we refresh the api token
|
|
||||||
await self._async_refresh_access_token_if_needed()
|
|
||||||
return await self._hass.async_add_executor_job(
|
|
||||||
partial(self._update_device_activities, limit=ACTIVITY_FETCH_LIMIT)
|
|
||||||
)
|
|
||||||
|
|
||||||
def _update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT):
|
|
||||||
_LOGGER.debug("Start retrieving device activities")
|
|
||||||
for house_id in self.house_ids:
|
|
||||||
_LOGGER.debug("Updating device activity for house id %s", house_id)
|
|
||||||
|
|
||||||
activities = self._api.get_house_activities(
|
|
||||||
self._access_token, house_id, limit=limit
|
|
||||||
)
|
)
|
||||||
|
self.async_signal_device_id_update(device_id)
|
||||||
|
|
||||||
device_ids = {a.device_id for a in activities}
|
async def _async_update_device_detail(self, device, api_call):
|
||||||
for device_id in device_ids:
|
_LOGGER.debug(
|
||||||
self._activities_by_id[device_id] = [
|
"Started retrieving detail for %s (%s)",
|
||||||
a for a in activities if a.device_id == device_id
|
device.device_name,
|
||||||
]
|
device.device_id,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER.debug("Completed retrieving device activities")
|
try:
|
||||||
|
self._device_detail_by_id[device.device_id] = await api_call(
|
||||||
|
self._august_gateway.access_token, device.device_id
|
||||||
|
)
|
||||||
|
except ClientError as ex:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Request error trying to retrieve %s details for %s. %s",
|
||||||
|
device.device_id,
|
||||||
|
device.device_name,
|
||||||
|
ex,
|
||||||
|
)
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Completed retrieving detail for %s (%s)",
|
||||||
|
device.device_name,
|
||||||
|
device.device_id,
|
||||||
|
)
|
||||||
|
|
||||||
async def async_get_doorbell_detail(self, doorbell_id):
|
def _get_device_name(self, device_id):
|
||||||
"""Return doorbell detail."""
|
"""Return doorbell or lock name as August has it stored."""
|
||||||
await self._async_update_doorbells()
|
if self._locks_by_id.get(device_id):
|
||||||
return self._doorbell_detail_by_id.get(doorbell_id)
|
return self._locks_by_id[device_id].device_name
|
||||||
|
if self._doorbells_by_id.get(device_id):
|
||||||
|
return self._doorbells_by_id[device_id].device_name
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_DOORBELL_STATUS_UPDATES)
|
async def async_lock(self, device_id):
|
||||||
async def _async_update_doorbells(self):
|
|
||||||
await self._hass.async_add_executor_job(self._update_doorbells)
|
|
||||||
|
|
||||||
def _update_doorbells(self):
|
|
||||||
detail_by_id = {}
|
|
||||||
|
|
||||||
_LOGGER.debug("Start retrieving doorbell details")
|
|
||||||
for doorbell in self._doorbells:
|
|
||||||
_LOGGER.debug("Updating doorbell status for %s", doorbell.device_name)
|
|
||||||
try:
|
|
||||||
detail_by_id[doorbell.device_id] = self._api.get_doorbell_detail(
|
|
||||||
self._access_token, doorbell.device_id
|
|
||||||
)
|
|
||||||
except RequestException as ex:
|
|
||||||
_LOGGER.error(
|
|
||||||
"Request error trying to retrieve doorbell status for %s. %s",
|
|
||||||
doorbell.device_name,
|
|
||||||
ex,
|
|
||||||
)
|
|
||||||
detail_by_id[doorbell.device_id] = None
|
|
||||||
except Exception:
|
|
||||||
detail_by_id[doorbell.device_id] = None
|
|
||||||
raise
|
|
||||||
|
|
||||||
_LOGGER.debug("Completed retrieving doorbell details")
|
|
||||||
self._doorbell_detail_by_id = detail_by_id
|
|
||||||
|
|
||||||
def update_door_state(self, lock_id, door_state, update_start_time_utc):
|
|
||||||
"""Set the door status and last status update time.
|
|
||||||
|
|
||||||
This is called when newer activity is detected on the activity feed
|
|
||||||
in order to keep the internal data in sync
|
|
||||||
"""
|
|
||||||
self._door_state_by_id[lock_id] = door_state
|
|
||||||
self._door_last_state_update_time_utc_by_id[lock_id] = update_start_time_utc
|
|
||||||
return True
|
|
||||||
|
|
||||||
def update_lock_status(self, lock_id, lock_status, update_start_time_utc):
|
|
||||||
"""Set the lock status and last status update time.
|
|
||||||
|
|
||||||
This is used when the lock, unlock apis are called
|
|
||||||
or newer activity is detected on the activity feed
|
|
||||||
in order to keep the internal data in sync
|
|
||||||
"""
|
|
||||||
self._lock_status_by_id[lock_id] = lock_status
|
|
||||||
self._lock_last_status_update_time_utc_by_id[lock_id] = update_start_time_utc
|
|
||||||
return True
|
|
||||||
|
|
||||||
def lock_has_doorsense(self, lock_id):
|
|
||||||
"""Determine if a lock has doorsense installed and can tell when the door is open or closed."""
|
|
||||||
# We do not update here since this is not expected
|
|
||||||
# to change until restart
|
|
||||||
if self._lock_detail_by_id[lock_id] is None:
|
|
||||||
return False
|
|
||||||
return self._lock_detail_by_id[lock_id].doorsense
|
|
||||||
|
|
||||||
async def async_get_lock_status(self, lock_id):
|
|
||||||
"""Return status if the door is locked or unlocked.
|
|
||||||
|
|
||||||
This is status for the lock itself.
|
|
||||||
"""
|
|
||||||
await self._async_update_locks()
|
|
||||||
return self._lock_status_by_id.get(lock_id)
|
|
||||||
|
|
||||||
async def async_get_lock_detail(self, lock_id):
|
|
||||||
"""Return lock detail."""
|
|
||||||
await self._async_update_locks()
|
|
||||||
return self._lock_detail_by_id.get(lock_id)
|
|
||||||
|
|
||||||
def get_lock_name(self, device_id):
|
|
||||||
"""Return lock name as August has it stored."""
|
|
||||||
for lock in self._locks:
|
|
||||||
if lock.device_id == device_id:
|
|
||||||
return lock.device_name
|
|
||||||
|
|
||||||
async def async_get_door_state(self, lock_id):
|
|
||||||
"""Return status if the door is open or closed.
|
|
||||||
|
|
||||||
This is the status from the door sensor.
|
|
||||||
"""
|
|
||||||
await self._async_update_locks_status()
|
|
||||||
return self._door_state_by_id.get(lock_id)
|
|
||||||
|
|
||||||
async def _async_update_locks(self):
|
|
||||||
await self._async_update_locks_status()
|
|
||||||
await self._async_update_locks_detail()
|
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_LOCK_STATUS_UPDATES)
|
|
||||||
async def _async_update_locks_status(self):
|
|
||||||
await self._hass.async_add_executor_job(self._update_locks_status)
|
|
||||||
|
|
||||||
def _update_locks_status(self):
|
|
||||||
status_by_id = {}
|
|
||||||
state_by_id = {}
|
|
||||||
lock_last_status_update_by_id = {}
|
|
||||||
door_last_state_update_by_id = {}
|
|
||||||
|
|
||||||
_LOGGER.debug("Start retrieving lock and door status")
|
|
||||||
for lock in self._locks:
|
|
||||||
update_start_time_utc = dt.utcnow()
|
|
||||||
_LOGGER.debug("Updating lock and door status for %s", lock.device_name)
|
|
||||||
try:
|
|
||||||
(
|
|
||||||
status_by_id[lock.device_id],
|
|
||||||
state_by_id[lock.device_id],
|
|
||||||
) = self._api.get_lock_status(
|
|
||||||
self._access_token, lock.device_id, door_status=True
|
|
||||||
)
|
|
||||||
# Since there is a a race condition between calling the
|
|
||||||
# lock and activity apis, we set the last update time
|
|
||||||
# BEFORE making the api call since we will compare this
|
|
||||||
# to activity later we want activity to win over stale lock/door
|
|
||||||
# state.
|
|
||||||
lock_last_status_update_by_id[lock.device_id] = update_start_time_utc
|
|
||||||
door_last_state_update_by_id[lock.device_id] = update_start_time_utc
|
|
||||||
except RequestException as ex:
|
|
||||||
_LOGGER.error(
|
|
||||||
"Request error trying to retrieve lock and door status for %s. %s",
|
|
||||||
lock.device_name,
|
|
||||||
ex,
|
|
||||||
)
|
|
||||||
status_by_id[lock.device_id] = None
|
|
||||||
state_by_id[lock.device_id] = None
|
|
||||||
except Exception:
|
|
||||||
status_by_id[lock.device_id] = None
|
|
||||||
state_by_id[lock.device_id] = None
|
|
||||||
raise
|
|
||||||
|
|
||||||
_LOGGER.debug("Completed retrieving lock and door status")
|
|
||||||
self._lock_status_by_id = status_by_id
|
|
||||||
self._door_state_by_id = state_by_id
|
|
||||||
self._door_last_state_update_time_utc_by_id = door_last_state_update_by_id
|
|
||||||
self._lock_last_status_update_time_utc_by_id = lock_last_status_update_by_id
|
|
||||||
|
|
||||||
def get_last_lock_status_update_time_utc(self, lock_id):
|
|
||||||
"""Return the last time that a lock status update was seen from the august API."""
|
|
||||||
# Since the activity api is called more frequently than
|
|
||||||
# the lock api it is possible that the lock has not
|
|
||||||
# been updated yet
|
|
||||||
if lock_id not in self._lock_last_status_update_time_utc_by_id:
|
|
||||||
return dt.utc_from_timestamp(0)
|
|
||||||
|
|
||||||
return self._lock_last_status_update_time_utc_by_id[lock_id]
|
|
||||||
|
|
||||||
def get_last_door_state_update_time_utc(self, lock_id):
|
|
||||||
"""Return the last time that a door status update was seen from the august API."""
|
|
||||||
# Since the activity api is called more frequently than
|
|
||||||
# the lock api it is possible that the door has not
|
|
||||||
# been updated yet
|
|
||||||
if lock_id not in self._door_last_state_update_time_utc_by_id:
|
|
||||||
return dt.utc_from_timestamp(0)
|
|
||||||
|
|
||||||
return self._door_last_state_update_time_utc_by_id[lock_id]
|
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_LOCK_DETAIL_UPDATES)
|
|
||||||
async def _async_update_locks_detail(self):
|
|
||||||
await self._hass.async_add_executor_job(self._update_locks_detail)
|
|
||||||
|
|
||||||
def _update_locks_detail(self):
|
|
||||||
detail_by_id = {}
|
|
||||||
|
|
||||||
_LOGGER.debug("Start retrieving locks detail")
|
|
||||||
for lock in self._locks:
|
|
||||||
try:
|
|
||||||
detail_by_id[lock.device_id] = self._api.get_lock_detail(
|
|
||||||
self._access_token, lock.device_id
|
|
||||||
)
|
|
||||||
except RequestException as ex:
|
|
||||||
_LOGGER.error(
|
|
||||||
"Request error trying to retrieve door details for %s. %s",
|
|
||||||
lock.device_name,
|
|
||||||
ex,
|
|
||||||
)
|
|
||||||
detail_by_id[lock.device_id] = None
|
|
||||||
except Exception:
|
|
||||||
detail_by_id[lock.device_id] = None
|
|
||||||
raise
|
|
||||||
|
|
||||||
_LOGGER.debug("Completed retrieving locks detail")
|
|
||||||
self._lock_detail_by_id = detail_by_id
|
|
||||||
|
|
||||||
def lock(self, device_id):
|
|
||||||
"""Lock the device."""
|
"""Lock the device."""
|
||||||
return _call_api_operation_that_requires_bridge(
|
return await self._async_call_api_op_requires_bridge(
|
||||||
self.get_lock_name(device_id),
|
device_id,
|
||||||
"lock",
|
self._api.async_lock_return_activities,
|
||||||
self._api.lock,
|
self._august_gateway.access_token,
|
||||||
self._access_token,
|
|
||||||
device_id,
|
device_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
def unlock(self, device_id):
|
async def async_unlock(self, device_id):
|
||||||
"""Unlock the device."""
|
"""Unlock the device."""
|
||||||
return _call_api_operation_that_requires_bridge(
|
return await self._async_call_api_op_requires_bridge(
|
||||||
self.get_lock_name(device_id),
|
device_id,
|
||||||
"unlock",
|
self._api.async_unlock_return_activities,
|
||||||
self._api.unlock,
|
self._august_gateway.access_token,
|
||||||
self._access_token,
|
|
||||||
device_id,
|
device_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _filter_inoperative_locks(self):
|
async def _async_call_api_op_requires_bridge(
|
||||||
|
self, device_id, func, *args, **kwargs
|
||||||
|
):
|
||||||
|
"""Call an API that requires the bridge to be online and will change the device state."""
|
||||||
|
ret = None
|
||||||
|
try:
|
||||||
|
ret = await func(*args, **kwargs)
|
||||||
|
except AugustApiAIOHTTPError as err:
|
||||||
|
device_name = self._get_device_name(device_id)
|
||||||
|
if device_name is None:
|
||||||
|
device_name = f"DeviceID: {device_id}"
|
||||||
|
raise HomeAssistantError(f"{device_name}: {err}")
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def _remove_inoperative_doorbells(self):
|
||||||
|
doorbells = list(self.doorbells)
|
||||||
|
for doorbell in doorbells:
|
||||||
|
device_id = doorbell.device_id
|
||||||
|
doorbell_is_operative = False
|
||||||
|
doorbell_detail = self._device_detail_by_id.get(device_id)
|
||||||
|
if doorbell_detail is None:
|
||||||
|
_LOGGER.info(
|
||||||
|
"The doorbell %s could not be setup because the system could not fetch details about the doorbell.",
|
||||||
|
doorbell.device_name,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
doorbell_is_operative = True
|
||||||
|
|
||||||
|
if not doorbell_is_operative:
|
||||||
|
del self._doorbells_by_id[device_id]
|
||||||
|
del self._device_detail_by_id[device_id]
|
||||||
|
|
||||||
|
def _remove_inoperative_locks(self):
|
||||||
# Remove non-operative locks as there must
|
# Remove non-operative locks as there must
|
||||||
# be a bridge (August Connect) for them to
|
# be a bridge (August Connect) for them to
|
||||||
# be usable
|
# be usable
|
||||||
operative_locks = []
|
locks = list(self.locks)
|
||||||
for lock in self._locks:
|
|
||||||
lock_detail = self._lock_detail_by_id.get(lock.device_id)
|
for lock in locks:
|
||||||
|
device_id = lock.device_id
|
||||||
|
lock_is_operative = False
|
||||||
|
lock_detail = self._device_detail_by_id.get(device_id)
|
||||||
if lock_detail is None:
|
if lock_detail is None:
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"The lock %s could not be setup because the system could not fetch details about the lock.",
|
"The lock %s could not be setup because the system could not fetch details about the lock.",
|
||||||
@ -535,19 +377,8 @@ class AugustData:
|
|||||||
lock.device_name,
|
lock.device_name,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
operative_locks.append(lock)
|
lock_is_operative = True
|
||||||
|
|
||||||
self._locks = operative_locks
|
if not lock_is_operative:
|
||||||
|
del self._locks_by_id[device_id]
|
||||||
|
del self._device_detail_by_id[device_id]
|
||||||
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
|
|
||||||
|
124
homeassistant/components/august/activity.py
Normal file
124
homeassistant/components/august/activity.py
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
"""Consume the august activity stream."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from aiohttp import ClientError
|
||||||
|
|
||||||
|
from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
|
from .const import ACTIVITY_UPDATE_INTERVAL
|
||||||
|
from .subscriber import AugustSubscriberMixin
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
ACTIVITY_STREAM_FETCH_LIMIT = 10
|
||||||
|
ACTIVITY_CATCH_UP_FETCH_LIMIT = 1000
|
||||||
|
|
||||||
|
|
||||||
|
class ActivityStream(AugustSubscriberMixin):
|
||||||
|
"""August activity stream handler."""
|
||||||
|
|
||||||
|
def __init__(self, hass, api, august_gateway, house_ids):
|
||||||
|
"""Init August activity stream object."""
|
||||||
|
super().__init__(hass, ACTIVITY_UPDATE_INTERVAL)
|
||||||
|
self._hass = hass
|
||||||
|
self._august_gateway = august_gateway
|
||||||
|
self._api = api
|
||||||
|
self._house_ids = house_ids
|
||||||
|
self._latest_activities_by_id_type = {}
|
||||||
|
self._last_update_time = None
|
||||||
|
self._abort_async_track_time_interval = None
|
||||||
|
|
||||||
|
async def async_setup(self):
|
||||||
|
"""Token refresh check and catch up the activity stream."""
|
||||||
|
await self._async_refresh(utcnow)
|
||||||
|
|
||||||
|
def get_latest_device_activity(self, device_id, activity_types):
|
||||||
|
"""Return latest activity that is one of the acitivty_types."""
|
||||||
|
if device_id not in self._latest_activities_by_id_type:
|
||||||
|
return None
|
||||||
|
|
||||||
|
latest_device_activities = self._latest_activities_by_id_type[device_id]
|
||||||
|
latest_activity = None
|
||||||
|
|
||||||
|
for activity_type in activity_types:
|
||||||
|
if activity_type in latest_device_activities:
|
||||||
|
if (
|
||||||
|
latest_activity is not None
|
||||||
|
and latest_device_activities[activity_type].activity_start_time
|
||||||
|
<= latest_activity.activity_start_time
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
latest_activity = latest_device_activities[activity_type]
|
||||||
|
|
||||||
|
return latest_activity
|
||||||
|
|
||||||
|
async def _async_refresh(self, time):
|
||||||
|
"""Update the activity stream from August."""
|
||||||
|
|
||||||
|
# This is the only place we refresh the api token
|
||||||
|
await self._august_gateway.async_refresh_access_token_if_needed()
|
||||||
|
await self._async_update_device_activities(time)
|
||||||
|
|
||||||
|
async def _async_update_device_activities(self, time):
|
||||||
|
_LOGGER.debug("Start retrieving device activities")
|
||||||
|
|
||||||
|
limit = (
|
||||||
|
ACTIVITY_STREAM_FETCH_LIMIT
|
||||||
|
if self._last_update_time
|
||||||
|
else ACTIVITY_CATCH_UP_FETCH_LIMIT
|
||||||
|
)
|
||||||
|
|
||||||
|
for house_id in self._house_ids:
|
||||||
|
_LOGGER.debug("Updating device activity for house id %s", house_id)
|
||||||
|
try:
|
||||||
|
activities = await self._api.async_get_house_activities(
|
||||||
|
self._august_gateway.access_token, house_id, limit=limit
|
||||||
|
)
|
||||||
|
except ClientError as ex:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Request error trying to retrieve activity for house id %s: %s",
|
||||||
|
house_id,
|
||||||
|
ex,
|
||||||
|
)
|
||||||
|
# Make sure we process the next house if one of them fails
|
||||||
|
continue
|
||||||
|
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Completed retrieving device activities for house id %s", house_id
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_device_ids = self._process_newer_device_activities(activities)
|
||||||
|
|
||||||
|
if updated_device_ids:
|
||||||
|
for device_id in updated_device_ids:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"async_signal_device_id_update (from activity stream): %s",
|
||||||
|
device_id,
|
||||||
|
)
|
||||||
|
self.async_signal_device_id_update(device_id)
|
||||||
|
|
||||||
|
self._last_update_time = time
|
||||||
|
|
||||||
|
def _process_newer_device_activities(self, activities):
|
||||||
|
updated_device_ids = set()
|
||||||
|
for activity in activities:
|
||||||
|
self._latest_activities_by_id_type.setdefault(activity.device_id, {})
|
||||||
|
|
||||||
|
lastest_activity = self._latest_activities_by_id_type[
|
||||||
|
activity.device_id
|
||||||
|
].get(activity.activity_type)
|
||||||
|
|
||||||
|
# Ignore activities that are older than the latest one
|
||||||
|
if (
|
||||||
|
lastest_activity
|
||||||
|
and lastest_activity.activity_start_time >= activity.activity_start_time
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
self._latest_activities_by_id_type[activity.device_id][
|
||||||
|
activity.activity_type
|
||||||
|
] = activity
|
||||||
|
|
||||||
|
updated_device_ids.add(activity.device_id)
|
||||||
|
|
||||||
|
return updated_device_ids
|
@ -2,56 +2,61 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from august.activity import ACTIVITY_ACTION_STATES, ActivityType
|
from august.activity import ActivityType
|
||||||
from august.lock import LockDoorStatus
|
from august.lock import LockDoorStatus
|
||||||
|
from august.util import update_lock_detail_from_activity
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
from homeassistant.components.binary_sensor import (
|
||||||
from homeassistant.util import dt
|
DEVICE_CLASS_CONNECTIVITY,
|
||||||
|
DEVICE_CLASS_DOOR,
|
||||||
|
DEVICE_CLASS_MOTION,
|
||||||
|
DEVICE_CLASS_OCCUPANCY,
|
||||||
|
BinarySensorDevice,
|
||||||
|
)
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||||
|
from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
from . import DATA_AUGUST
|
from .const import DATA_AUGUST, DOMAIN
|
||||||
|
from .entity import AugustEntityMixin
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=5)
|
TIME_TO_DECLARE_DETECTION = timedelta(seconds=60)
|
||||||
|
|
||||||
|
|
||||||
async def _async_retrieve_door_state(data, lock):
|
def _retrieve_online_state(data, detail):
|
||||||
"""Get the latest state of the DoorSense sensor."""
|
|
||||||
return await data.async_get_door_state(lock.device_id)
|
|
||||||
|
|
||||||
|
|
||||||
async def _async_retrieve_online_state(data, doorbell):
|
|
||||||
"""Get the latest state of the sensor."""
|
"""Get the latest state of the sensor."""
|
||||||
detail = await data.async_get_doorbell_detail(doorbell.device_id)
|
# The doorbell will go into standby mode when there is no motion
|
||||||
if detail is None:
|
# for a short while. It will wake by itself when needed so we need
|
||||||
return None
|
# to consider is available or we will not report motion or dings
|
||||||
|
|
||||||
return detail.is_online
|
return detail.is_online or detail.is_standby
|
||||||
|
|
||||||
|
|
||||||
async def _async_retrieve_motion_state(data, doorbell):
|
def _retrieve_motion_state(data, detail):
|
||||||
|
|
||||||
return await _async_activity_time_based_state(
|
return _activity_time_based_state(
|
||||||
data, doorbell, [ActivityType.DOORBELL_MOTION, ActivityType.DOORBELL_DING]
|
data,
|
||||||
|
detail.device_id,
|
||||||
|
[ActivityType.DOORBELL_MOTION, ActivityType.DOORBELL_DING],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _async_retrieve_ding_state(data, doorbell):
|
def _retrieve_ding_state(data, detail):
|
||||||
|
|
||||||
return await _async_activity_time_based_state(
|
return _activity_time_based_state(
|
||||||
data, doorbell, [ActivityType.DOORBELL_DING]
|
data, detail.device_id, [ActivityType.DOORBELL_DING]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _async_activity_time_based_state(data, doorbell, activity_types):
|
def _activity_time_based_state(data, device_id, activity_types):
|
||||||
"""Get the latest state of the sensor."""
|
"""Get the latest state of the sensor."""
|
||||||
latest = await data.async_get_latest_device_activity(
|
latest = data.activity_stream.get_latest_device_activity(device_id, activity_types)
|
||||||
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=45)
|
end = latest.activity_end_time + TIME_TO_DECLARE_DETECTION
|
||||||
return start <= datetime.now() <= end
|
return start <= datetime.now() <= end
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -59,38 +64,37 @@ async def _async_activity_time_based_state(data, doorbell, activity_types):
|
|||||||
SENSOR_NAME = 0
|
SENSOR_NAME = 0
|
||||||
SENSOR_DEVICE_CLASS = 1
|
SENSOR_DEVICE_CLASS = 1
|
||||||
SENSOR_STATE_PROVIDER = 2
|
SENSOR_STATE_PROVIDER = 2
|
||||||
|
SENSOR_STATE_IS_TIME_BASED = 3
|
||||||
|
|
||||||
# sensor_type: [name, device_class, async_state_provider]
|
# sensor_type: [name, device_class, state_provider, is_time_based]
|
||||||
SENSOR_TYPES_DOOR = {"door_open": ["Open", "door", _async_retrieve_door_state]}
|
|
||||||
|
|
||||||
SENSOR_TYPES_DOORBELL = {
|
SENSOR_TYPES_DOORBELL = {
|
||||||
"doorbell_ding": ["Ding", "occupancy", _async_retrieve_ding_state],
|
"doorbell_ding": ["Ding", DEVICE_CLASS_OCCUPANCY, _retrieve_ding_state, True],
|
||||||
"doorbell_motion": ["Motion", "motion", _async_retrieve_motion_state],
|
"doorbell_motion": ["Motion", DEVICE_CLASS_MOTION, _retrieve_motion_state, True],
|
||||||
"doorbell_online": ["Online", "connectivity", _async_retrieve_online_state],
|
"doorbell_online": [
|
||||||
|
"Online",
|
||||||
|
DEVICE_CLASS_CONNECTIVITY,
|
||||||
|
_retrieve_online_state,
|
||||||
|
False,
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
"""Set up the August binary sensors."""
|
"""Set up the August binary sensors."""
|
||||||
data = hass.data[DATA_AUGUST]
|
data = hass.data[DOMAIN][config_entry.entry_id][DATA_AUGUST]
|
||||||
devices = []
|
devices = []
|
||||||
|
|
||||||
for door in data.locks:
|
for door in data.locks:
|
||||||
for sensor_type in SENSOR_TYPES_DOOR:
|
detail = data.get_device_detail(door.device_id)
|
||||||
if not data.lock_has_doorsense(door.device_id):
|
if not detail.doorsense:
|
||||||
_LOGGER.debug(
|
|
||||||
"Not adding sensor class %s for lock %s ",
|
|
||||||
SENSOR_TYPES_DOOR[sensor_type][SENSOR_DEVICE_CLASS],
|
|
||||||
door.device_name,
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Adding sensor class %s for %s",
|
"Not adding sensor class door for lock %s because it does not have doorsense.",
|
||||||
SENSOR_TYPES_DOOR[sensor_type][SENSOR_DEVICE_CLASS],
|
|
||||||
door.device_name,
|
door.device_name,
|
||||||
)
|
)
|
||||||
devices.append(AugustDoorBinarySensor(data, sensor_type, door))
|
continue
|
||||||
|
|
||||||
|
_LOGGER.debug("Adding sensor class door for %s", door.device_name)
|
||||||
|
devices.append(AugustDoorBinarySensor(data, "door_open", door))
|
||||||
|
|
||||||
for doorbell in data.doorbells:
|
for doorbell in data.doorbells:
|
||||||
for sensor_type in SENSOR_TYPES_DOORBELL:
|
for sensor_type in SENSOR_TYPES_DOORBELL:
|
||||||
@ -104,116 +108,66 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
|
|||||||
async_add_entities(devices, True)
|
async_add_entities(devices, True)
|
||||||
|
|
||||||
|
|
||||||
class AugustDoorBinarySensor(BinarySensorDevice):
|
class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorDevice):
|
||||||
"""Representation of an August Door binary sensor."""
|
"""Representation of an August Door binary sensor."""
|
||||||
|
|
||||||
def __init__(self, data, sensor_type, door):
|
def __init__(self, data, sensor_type, device):
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
|
super().__init__(data, device)
|
||||||
self._data = data
|
self._data = data
|
||||||
self._sensor_type = sensor_type
|
self._sensor_type = sensor_type
|
||||||
self._door = door
|
self._device = device
|
||||||
self._state = None
|
self._update_from_data()
|
||||||
self._available = False
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available(self):
|
def available(self):
|
||||||
"""Return the availability of this sensor."""
|
"""Return the availability of this sensor."""
|
||||||
return self._available
|
return self._detail.bridge_is_online
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
"""Return true if the binary sensor is on."""
|
"""Return true if the binary sensor is on."""
|
||||||
return self._state
|
return self._detail.door_state == LockDoorStatus.OPEN
|
||||||
|
|
||||||
@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."""
|
||||||
return SENSOR_TYPES_DOOR[self._sensor_type][SENSOR_DEVICE_CLASS]
|
return DEVICE_CLASS_DOOR
|
||||||
|
|
||||||
@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 f"{self._device.device_name} Open"
|
||||||
self._door.device_name, SENSOR_TYPES_DOOR[self._sensor_type][SENSOR_NAME]
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_update(self):
|
@callback
|
||||||
|
def _update_from_data(self):
|
||||||
"""Get the latest state of the sensor and update activity."""
|
"""Get the latest state of the sensor and update activity."""
|
||||||
async_state_provider = SENSOR_TYPES_DOOR[self._sensor_type][
|
door_activity = self._data.activity_stream.get_latest_device_activity(
|
||||||
SENSOR_STATE_PROVIDER
|
self._device_id, [ActivityType.DOOR_OPERATION]
|
||||||
]
|
|
||||||
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
|
|
||||||
|
|
||||||
door_activity = await self._data.async_get_latest_device_activity(
|
|
||||||
self._door.device_id, ActivityType.DOOR_OPERATION
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if door_activity is not None:
|
if door_activity is not None:
|
||||||
self._sync_door_activity(door_activity)
|
update_lock_detail_from_activity(self._detail, 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 f"{self._device_id}_open"
|
||||||
self._door.device_id,
|
|
||||||
SENSOR_TYPES_DOOR[self._sensor_type][SENSOR_NAME].lower(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AugustDoorbellBinarySensor(BinarySensorDevice):
|
class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorDevice):
|
||||||
"""Representation of an August binary sensor."""
|
"""Representation of an August binary sensor."""
|
||||||
|
|
||||||
def __init__(self, data, sensor_type, doorbell):
|
def __init__(self, data, sensor_type, device):
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
|
super().__init__(data, device)
|
||||||
|
self._check_for_off_update_listener = None
|
||||||
self._data = data
|
self._data = data
|
||||||
self._sensor_type = sensor_type
|
self._sensor_type = sensor_type
|
||||||
self._doorbell = doorbell
|
self._device = device
|
||||||
self._state = None
|
self._state = None
|
||||||
self._available = False
|
self._available = False
|
||||||
|
self._update_from_data()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available(self):
|
def available(self):
|
||||||
@ -233,26 +187,68 @@ class AugustDoorbellBinarySensor(BinarySensorDevice):
|
|||||||
@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 f"{self._device.device_name} {SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_NAME]}"
|
||||||
self._doorbell.device_name,
|
|
||||||
SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_NAME],
|
@property
|
||||||
|
def _state_provider(self):
|
||||||
|
"""Return the state provider for the binary sensor."""
|
||||||
|
return SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_STATE_PROVIDER]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _is_time_based(self):
|
||||||
|
"""Return true of false if the sensor is time based."""
|
||||||
|
return SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_STATE_IS_TIME_BASED]
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _update_from_data(self):
|
||||||
|
"""Get the latest state of the sensor."""
|
||||||
|
self._cancel_any_pending_updates()
|
||||||
|
self._state = self._state_provider(self._data, self._detail)
|
||||||
|
|
||||||
|
if self._is_time_based:
|
||||||
|
self._available = _retrieve_online_state(self._data, self._detail)
|
||||||
|
self._schedule_update_to_recheck_turn_off_sensor()
|
||||||
|
else:
|
||||||
|
self._available = True
|
||||||
|
|
||||||
|
def _schedule_update_to_recheck_turn_off_sensor(self):
|
||||||
|
"""Schedule an update to recheck the sensor to see if it is ready to turn off."""
|
||||||
|
|
||||||
|
# If the sensor is already off there is nothing to do
|
||||||
|
if not self._state:
|
||||||
|
return
|
||||||
|
|
||||||
|
# self.hass is only available after setup is completed
|
||||||
|
# and we will recheck in async_added_to_hass
|
||||||
|
if not self.hass:
|
||||||
|
return
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _scheduled_update(now):
|
||||||
|
"""Timer callback for sensor update."""
|
||||||
|
self._check_for_off_update_listener = None
|
||||||
|
self._update_from_data()
|
||||||
|
|
||||||
|
self._check_for_off_update_listener = async_track_point_in_utc_time(
|
||||||
|
self.hass, _scheduled_update, utcnow() + TIME_TO_DECLARE_DETECTION
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_update(self):
|
def _cancel_any_pending_updates(self):
|
||||||
"""Get the latest state of the sensor."""
|
"""Cancel any updates to recheck a sensor to see if it is ready to turn off."""
|
||||||
async_state_provider = SENSOR_TYPES_DOORBELL[self._sensor_type][
|
if self._check_for_off_update_listener:
|
||||||
SENSOR_STATE_PROVIDER
|
_LOGGER.debug("%s: canceled pending update", self.entity_id)
|
||||||
]
|
self._check_for_off_update_listener()
|
||||||
self._state = await async_state_provider(self._data, self._doorbell)
|
self._check_for_off_update_listener = None
|
||||||
# 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
|
async def async_added_to_hass(self):
|
||||||
# to consider is available or we will not report motion or dings
|
"""Call the mixin to subscribe and setup an async_track_point_in_utc_time to turn off the sensor if needed."""
|
||||||
self._available = self._doorbell.is_online or self._doorbell.status == "standby"
|
self._schedule_update_to_recheck_turn_off_sensor()
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
|
||||||
@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 (
|
||||||
self._doorbell.device_id,
|
f"{self._device_id}_"
|
||||||
SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_NAME].lower(),
|
f"{SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_NAME].lower()}"
|
||||||
)
|
)
|
||||||
|
@ -1,18 +1,19 @@
|
|||||||
"""Support for August camera."""
|
"""Support for August doorbell camera."""
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
import requests
|
from august.activity import ActivityType
|
||||||
|
from august.util import update_doorbell_image_from_activity
|
||||||
|
|
||||||
from homeassistant.components.camera import Camera
|
from homeassistant.components.camera import Camera
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers import aiohttp_client
|
||||||
|
|
||||||
from . import DATA_AUGUST, DEFAULT_TIMEOUT
|
from .const import DATA_AUGUST, DEFAULT_NAME, DEFAULT_TIMEOUT, DOMAIN
|
||||||
|
from .entity import AugustEntityMixin
|
||||||
SCAN_INTERVAL = timedelta(seconds=10)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
"""Set up August cameras."""
|
"""Set up August cameras."""
|
||||||
data = hass.data[DATA_AUGUST]
|
data = hass.data[DOMAIN][config_entry.entry_id][DATA_AUGUST]
|
||||||
devices = []
|
devices = []
|
||||||
|
|
||||||
for doorbell in data.doorbells:
|
for doorbell in data.doorbells:
|
||||||
@ -21,14 +22,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
|
|||||||
async_add_entities(devices, True)
|
async_add_entities(devices, True)
|
||||||
|
|
||||||
|
|
||||||
class AugustCamera(Camera):
|
class AugustCamera(AugustEntityMixin, Camera):
|
||||||
"""An implementation of a August security camera."""
|
"""An implementation of a August security camera."""
|
||||||
|
|
||||||
def __init__(self, data, doorbell, timeout):
|
def __init__(self, data, device, timeout):
|
||||||
"""Initialize a August security camera."""
|
"""Initialize a August security camera."""
|
||||||
super().__init__()
|
super().__init__(data, device)
|
||||||
self._data = data
|
self._data = data
|
||||||
self._doorbell = doorbell
|
self._device = device
|
||||||
self._timeout = timeout
|
self._timeout = timeout
|
||||||
self._image_url = None
|
self._image_url = None
|
||||||
self._image_content = None
|
self._image_content = None
|
||||||
@ -36,12 +37,12 @@ class AugustCamera(Camera):
|
|||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of this device."""
|
"""Return the name of this device."""
|
||||||
return self._doorbell.device_name
|
return f"{self._device.device_name} Camera"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_recording(self):
|
def is_recording(self):
|
||||||
"""Return true if the device is recording."""
|
"""Return true if the device is recording."""
|
||||||
return self._doorbell.has_subscription
|
return self._device.has_subscription
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def motion_detection_enabled(self):
|
def motion_detection_enabled(self):
|
||||||
@ -51,31 +52,35 @@ class AugustCamera(Camera):
|
|||||||
@property
|
@property
|
||||||
def brand(self):
|
def brand(self):
|
||||||
"""Return the camera brand."""
|
"""Return the camera brand."""
|
||||||
return "August"
|
return DEFAULT_NAME
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def model(self):
|
def model(self):
|
||||||
"""Return the camera model."""
|
"""Return the camera model."""
|
||||||
return "Doorbell"
|
return self._detail.model
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _update_from_data(self):
|
||||||
|
"""Get the latest state of the sensor."""
|
||||||
|
doorbell_activity = self._data.activity_stream.get_latest_device_activity(
|
||||||
|
self._device_id, [ActivityType.DOORBELL_MOTION]
|
||||||
|
)
|
||||||
|
|
||||||
|
if doorbell_activity is not None:
|
||||||
|
update_doorbell_image_from_activity(self._detail, doorbell_activity)
|
||||||
|
|
||||||
async def async_camera_image(self):
|
async def async_camera_image(self):
|
||||||
"""Return bytes of camera image."""
|
"""Return bytes of camera image."""
|
||||||
latest = await self._data.async_get_doorbell_detail(self._doorbell.device_id)
|
self._update_from_data()
|
||||||
|
|
||||||
if self._image_url is not latest.image_url:
|
if self._image_url is not self._detail.image_url:
|
||||||
self._image_url = latest.image_url
|
self._image_url = self._detail.image_url
|
||||||
self._image_content = await self.hass.async_add_executor_job(
|
self._image_content = await self._detail.async_get_doorbell_image(
|
||||||
self._camera_image
|
aiohttp_client.async_get_clientsession(self.hass), timeout=self._timeout
|
||||||
)
|
)
|
||||||
|
|
||||||
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."""
|
||||||
return f"{self._doorbell.device_id:s}_camera"
|
return f"{self._device_id:s}_camera"
|
||||||
|
133
homeassistant/components/august/config_flow.py
Normal file
133
homeassistant/components/august/config_flow.py
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
"""Config flow for August integration."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from august.authenticator import ValidationResult
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries, core
|
||||||
|
from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
CONF_LOGIN_METHOD,
|
||||||
|
DEFAULT_TIMEOUT,
|
||||||
|
LOGIN_METHODS,
|
||||||
|
VERIFICATION_CODE_KEY,
|
||||||
|
)
|
||||||
|
from .const import DOMAIN # pylint:disable=unused-import
|
||||||
|
from .exceptions import CannotConnect, InvalidAuth, RequireValidation
|
||||||
|
from .gateway import AugustGateway
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DATA_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_LOGIN_METHOD, default="phone"): vol.In(LOGIN_METHODS),
|
||||||
|
vol.Required(CONF_USERNAME): str,
|
||||||
|
vol.Required(CONF_PASSWORD): str,
|
||||||
|
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.Coerce(int),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_validate_input(
|
||||||
|
hass: core.HomeAssistant, data, august_gateway,
|
||||||
|
):
|
||||||
|
"""Validate the user input allows us to connect.
|
||||||
|
|
||||||
|
Data has the keys from DATA_SCHEMA with values provided by the user.
|
||||||
|
|
||||||
|
Request configuration steps from the user.
|
||||||
|
"""
|
||||||
|
|
||||||
|
code = data.get(VERIFICATION_CODE_KEY)
|
||||||
|
|
||||||
|
if code is not None:
|
||||||
|
result = await august_gateway.authenticator.async_validate_verification_code(
|
||||||
|
code
|
||||||
|
)
|
||||||
|
_LOGGER.debug("Verification code validation: %s", result)
|
||||||
|
if result != ValidationResult.VALIDATED:
|
||||||
|
raise RequireValidation
|
||||||
|
|
||||||
|
try:
|
||||||
|
await august_gateway.async_authenticate()
|
||||||
|
except RequireValidation:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Requesting new verification code for %s via %s",
|
||||||
|
data.get(CONF_USERNAME),
|
||||||
|
data.get(CONF_LOGIN_METHOD),
|
||||||
|
)
|
||||||
|
if code is None:
|
||||||
|
await august_gateway.authenticator.async_send_verification_code()
|
||||||
|
raise
|
||||||
|
|
||||||
|
return {
|
||||||
|
"title": data.get(CONF_USERNAME),
|
||||||
|
"data": august_gateway.config_entry(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for August."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Store an AugustGateway()."""
|
||||||
|
self._august_gateway = None
|
||||||
|
self.user_auth_details = {}
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input=None):
|
||||||
|
"""Handle the initial step."""
|
||||||
|
if self._august_gateway is None:
|
||||||
|
self._august_gateway = AugustGateway(self.hass)
|
||||||
|
errors = {}
|
||||||
|
if user_input is not None:
|
||||||
|
await self._august_gateway.async_setup(user_input)
|
||||||
|
|
||||||
|
try:
|
||||||
|
info = await async_validate_input(
|
||||||
|
self.hass, user_input, self._august_gateway,
|
||||||
|
)
|
||||||
|
await self.async_set_unique_id(user_input[CONF_USERNAME])
|
||||||
|
return self.async_create_entry(title=info["title"], data=info["data"])
|
||||||
|
except CannotConnect:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
except InvalidAuth:
|
||||||
|
errors["base"] = "invalid_auth"
|
||||||
|
except RequireValidation:
|
||||||
|
self.user_auth_details = user_input
|
||||||
|
|
||||||
|
return await self.async_step_validation()
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
_LOGGER.exception("Unexpected exception")
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_validation(self, user_input=None):
|
||||||
|
"""Handle validation (2fa) step."""
|
||||||
|
if user_input:
|
||||||
|
return await self.async_step_user({**self.user_auth_details, **user_input})
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="validation",
|
||||||
|
data_schema=vol.Schema(
|
||||||
|
{vol.Required(VERIFICATION_CODE_KEY): vol.All(str, vol.Strip)}
|
||||||
|
),
|
||||||
|
description_placeholders={
|
||||||
|
CONF_USERNAME: self.user_auth_details.get(CONF_USERNAME),
|
||||||
|
CONF_LOGIN_METHOD: self.user_auth_details.get(CONF_LOGIN_METHOD),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_import(self, user_input):
|
||||||
|
"""Handle import."""
|
||||||
|
await self.async_set_unique_id(user_input[CONF_USERNAME])
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
|
return await self.async_step_user(user_input)
|
44
homeassistant/components/august/const.py
Normal file
44
homeassistant/components/august/const.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
"""Constants for August devices."""
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
DEFAULT_TIMEOUT = 10
|
||||||
|
|
||||||
|
CONF_ACCESS_TOKEN_CACHE_FILE = "access_token_cache_file"
|
||||||
|
CONF_LOGIN_METHOD = "login_method"
|
||||||
|
CONF_INSTALL_ID = "install_id"
|
||||||
|
|
||||||
|
VERIFICATION_CODE_KEY = "verification_code"
|
||||||
|
|
||||||
|
NOTIFICATION_ID = "august_notification"
|
||||||
|
NOTIFICATION_TITLE = "August"
|
||||||
|
|
||||||
|
DEFAULT_AUGUST_CONFIG_FILE = ".august.conf"
|
||||||
|
|
||||||
|
DATA_AUGUST = "data_august"
|
||||||
|
|
||||||
|
DEFAULT_NAME = "August"
|
||||||
|
DOMAIN = "august"
|
||||||
|
|
||||||
|
OPERATION_METHOD_AUTORELOCK = "autorelock"
|
||||||
|
OPERATION_METHOD_REMOTE = "remote"
|
||||||
|
OPERATION_METHOD_KEYPAD = "keypad"
|
||||||
|
OPERATION_METHOD_MOBILE_DEVICE = "mobile"
|
||||||
|
|
||||||
|
ATTR_OPERATION_AUTORELOCK = "autorelock"
|
||||||
|
ATTR_OPERATION_METHOD = "method"
|
||||||
|
ATTR_OPERATION_REMOTE = "remote"
|
||||||
|
ATTR_OPERATION_KEYPAD = "keypad"
|
||||||
|
|
||||||
|
# Limit battery, online, and hardware updates to hourly
|
||||||
|
# in order to reduce the number of api requests and
|
||||||
|
# avoid hitting rate limits
|
||||||
|
MIN_TIME_BETWEEN_DETAIL_UPDATES = timedelta(hours=1)
|
||||||
|
|
||||||
|
# Activity needs to be checked more frequently as the
|
||||||
|
# doorbell motion and rings are included here
|
||||||
|
ACTIVITY_UPDATE_INTERVAL = timedelta(seconds=10)
|
||||||
|
|
||||||
|
LOGIN_METHODS = ["phone", "email"]
|
||||||
|
|
||||||
|
AUGUST_COMPONENTS = ["camera", "binary_sensor", "lock", "sensor"]
|
67
homeassistant/components/august/entity.py
Normal file
67
homeassistant/components/august/entity.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
"""Base class for August entity."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
|
from . import DEFAULT_NAME, DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AugustEntityMixin(Entity):
|
||||||
|
"""Base implementation for August device."""
|
||||||
|
|
||||||
|
def __init__(self, data, device):
|
||||||
|
"""Initialize an August device."""
|
||||||
|
super().__init__()
|
||||||
|
self._data = data
|
||||||
|
self._device = device
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
"""Return False, updates are controlled via the hub."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _device_id(self):
|
||||||
|
return self._device.device_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _detail(self):
|
||||||
|
return self._data.get_device_detail(self._device.device_id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self):
|
||||||
|
"""Return the device_info of the device."""
|
||||||
|
return {
|
||||||
|
"identifiers": {(DOMAIN, self._device_id)},
|
||||||
|
"name": self._device.device_name,
|
||||||
|
"manufacturer": DEFAULT_NAME,
|
||||||
|
"sw_version": self._detail.firmware_version,
|
||||||
|
"model": self._detail.model,
|
||||||
|
}
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _update_from_data_and_write_state(self):
|
||||||
|
self._update_from_data()
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
async def async_added_to_hass(self):
|
||||||
|
"""Subscribe to updates."""
|
||||||
|
self._data.async_subscribe_device_id(
|
||||||
|
self._device_id, self._update_from_data_and_write_state
|
||||||
|
)
|
||||||
|
self._data.activity_stream.async_subscribe_device_id(
|
||||||
|
self._device_id, self._update_from_data_and_write_state
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self):
|
||||||
|
"""Undo subscription."""
|
||||||
|
self._data.async_unsubscribe_device_id(
|
||||||
|
self._device_id, self._update_from_data_and_write_state
|
||||||
|
)
|
||||||
|
self._data.activity_stream.async_unsubscribe_device_id(
|
||||||
|
self._device_id, self._update_from_data_and_write_state
|
||||||
|
)
|
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