Merge pull request #32932 from home-assistant/rc

0.107.0
This commit is contained in:
Franck Nijhof 2020-03-18 15:35:44 +01:00 committed by GitHub
commit d520a02b8c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1283 changed files with 36288 additions and 18993 deletions

View File

@ -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

View File

@ -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

View File

@ -41,6 +41,11 @@ repos:
rev: v2.4.0 rev: v2.4.0
hooks: hooks:
- id: check-json - id: check-json
- id: no-commit-to-branch
args:
- --branch=dev
- --branch=master
- --branch=rc
- repo: local - 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

View File

@ -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

View File

@ -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']))

View File

@ -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"

View File

@ -0,0 +1,12 @@
{
"config": {
"step": {
"user": {
"data": {
"password": "Parole",
"username": "E-pasta adrese"
}
}
}
}
}

View File

@ -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()

View File

@ -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."""

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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_{}"

View File

@ -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."""

View File

@ -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,15 +42,18 @@ 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):

View File

@ -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."""

View File

@ -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"]
} }

View File

@ -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

View File

@ -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}

View File

@ -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

View File

@ -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:

View File

@ -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,

View 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"
}
}

View 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"
}
}

View 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"
}
}
}
}

View 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"
}
}

View 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"
}
}

View 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"
}
}

View 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"
}
}

View 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"
}
}

View File

@ -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"
}
}

View File

@ -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)

View 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
}
),
)

View 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"

View File

@ -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": [],

View File

@ -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(
{ async def async_setup_entry(hass, entry, async_add_entities):
vol.Required(CONF_API_KEY): cv.string, """Set up AirVisual sensors based on a config entry."""
vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSOR_LOCALES)): vol.All( airvisual = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id]
cv.ensure_list, [vol.In(SENSOR_LOCALES)]
), async_add_entities(
vol.Inclusive(CONF_CITY, "city"): cv.string, [
vol.Inclusive(CONF_COUNTRY, "city"): cv.string, AirVisualSensor(airvisual, kind, name, icon, unit, locale, geography_id)
vol.Inclusive(CONF_LATITUDE, "coords"): cv.latitude, for geography_id in airvisual.data
vol.Inclusive(CONF_LONGITUDE, "coords"): cv.longitude, for locale in SENSOR_LOCALES
vol.Optional(CONF_SHOW_ON_MAP, default=True): cv.boolean, for kind, name, icon, unit in SENSORS
vol.Inclusive(CONF_STATE, "city"): cv.string, ],
vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): cv.time_period, True,
}
) )
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Configure the platform and add the sensors."""
city = config.get(CONF_CITY)
state = config.get(CONF_STATE)
country = config.get(CONF_COUNTRY)
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
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 = {}

View 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"
}
}
}
}
}

View File

@ -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,
) )

View File

@ -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."""

View File

@ -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

View File

@ -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)

View File

@ -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(

View File

@ -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

View File

@ -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"

View File

@ -400,7 +400,10 @@ class CoverCapabilities(AlexaEntity):
def interfaces(self): def interfaces(self):
"""Yield the supported interfaces.""" """Yield the supported interfaces."""
device_class = self.entity.attributes.get(ATTR_DEVICE_CLASS)
if device_class != cover.DEVICE_CLASS_GARAGE:
yield AlexaPowerController(self.entity) 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)

View File

@ -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(

View File

@ -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"]
} }

View File

@ -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.

View File

@ -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

View File

@ -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(

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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.",

View File

@ -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):

View File

@ -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)

View File

@ -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"

View File

@ -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"]
} }

View File

@ -83,6 +83,10 @@ 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)
if w_m2_brightness_val is None:
self._state = None
else:
self._state = round(float(w_m2_brightness_val) / 0.0079) 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(

View File

@ -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."
} }
} }
} }

View File

@ -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."""

View File

@ -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

View File

@ -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(

View File

@ -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

View File

@ -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:

View File

@ -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):

View File

@ -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(

View File

@ -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

View File

@ -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:

View File

@ -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:

View File

@ -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):

View File

@ -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):

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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,
) )

View File

@ -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

View File

@ -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

View File

@ -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:",

View File

@ -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):

View File

@ -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

View File

@ -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()

View File

@ -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"]
} }

View 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"
}
}

View 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"
}
}

View 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"
}
}

View 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"
}
}

View 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"
}
}

View File

@ -0,0 +1,12 @@
{
"config": {
"step": {
"user": {
"data": {
"login_method": "Pieteik\u0161an\u0101s metode",
"password": "Parole"
}
}
}
}
}

View 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"
}
}

View 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"
}
}

View 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"
}
}

View File

@ -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)
return False
except InvalidAuth:
_LOGGER.error("Password is no longer valid. Please set up August again")
return False
hass.components.persistent_notification.create( # We still use the configurator to get a new 2fa code
"Error: {}<br />" # when needed since config_flow doesn't have a way
"You will need to restart hass after fixing." # to re-request if it expires
"".format(ex), if TWO_FA_REVALIDATE in hass.data[DOMAIN][entry_id]:
title=NOTIFICATION_TITLE, hass.components.configurator.async_request_done(
notification_id=NOTIFICATION_ID, hass.data[DOMAIN][entry_id].pop(TWO_FA_REVALIDATE)
) )
state = authentication.state hass.data[DOMAIN][entry_id][DATA_AUGUST] = AugustData(hass, august_gateway)
if state == AuthenticationState.AUTHENTICATED: await hass.data[DOMAIN][entry_id][DATA_AUGUST].async_setup()
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: for component in AUGUST_COMPONENTS:
discovery.load_platform(hass, component, DOMAIN, {}, config) hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, component)
return True
if state == AuthenticationState.BAD_PASSWORD:
_LOGGER.error("Invalid password provided")
return False
if state == AuthenticationState.REQUIRES_VALIDATION:
request_configuration(hass, config, api, authenticator, token_refresh_lock)
return True
return False
async def async_setup(hass, config):
"""Set up the August component."""
conf = config[DOMAIN]
api_http_session = None
try:
api_http_session = Session()
except RequestException as ex:
_LOGGER.warning("Creating HTTP session failed with: %s", str(ex))
api = Api(timeout=conf.get(CONF_TIMEOUT), http_session=api_http_session)
authenticator = Authenticator(
api,
conf.get(CONF_LOGIN_METHOD),
conf.get(CONF_USERNAME),
conf.get(CONF_PASSWORD),
install_id=conf.get(CONF_INSTALL_ID),
access_token_cache_file=hass.config.path(AUGUST_CONFIG_FILE),
) )
def close_http_session(event): return True
"""Close API sessions used to connect to August."""
_LOGGER.debug("Closing August HTTP sessions")
if api_http_session: 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
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_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up August from a config entry."""
august_gateway = AugustGateway(hass)
try: try:
api_http_session.close() await august_gateway.async_setup(entry.data)
except RequestException: return await async_setup_august(hass, entry, august_gateway)
pass except asyncio.TimeoutError:
raise ConfigEntryNotReady
_LOGGER.debug("August HTTP session closed.")
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, close_http_session) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
_LOGGER.debug("Registered for Home Assistant stop event") """Unload a config entry."""
unload_ok = all(
token_refresh_lock = asyncio.Lock() await asyncio.gather(
*[
return await hass.async_add_executor_job( hass.config_entries.async_forward_entry_unload(entry, component)
setup_august, hass, config, api, authenticator, token_refresh_lock for component in AUGUST_COMPONENTS
]
)
) )
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
class AugustData: return unload_ok
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", async def _async_refresh_device_detail_by_ids(self, device_ids_list):
self._access_token_expires, for device_id in device_ids_list:
refreshed_authentication.access_token_expires, if device_id in self._locks_by_id:
await self._async_update_device_detail(
self._locks_by_id[device_id], self._api.async_get_lock_detail
) )
self._access_token = refreshed_authentication.access_token elif device_id in self._doorbells_by_id:
self._access_token_expires = refreshed_authentication.access_token_expires await self._async_update_device_detail(
self._doorbells_by_id[device_id],
self._api.async_get_doorbell_detail,
)
_LOGGER.debug(
"async_signal_device_id_update (from detail updates): %s", device_id,
)
self.async_signal_device_id_update(device_id)
async def async_get_device_activities(self, device_id, *activity_types): async def _async_update_device_detail(self, device, api_call):
"""Return a list of activities.""" _LOGGER.debug(
_LOGGER.debug("Getting device activities for %s", device_id) "Started retrieving detail for %s (%s)",
await self._async_update_device_activities() device.device_name,
device.device_id,
activities = self._activities_by_id.get(device_id, [])
if activity_types:
return [a for a in activities if a.activity_type in activity_types]
return activities
async def async_get_latest_device_activity(self, device_id, *activity_types):
"""Return latest activity."""
activities = await self.async_get_device_activities(device_id, *activity_types)
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
)
device_ids = {a.device_id for a in activities}
for device_id in device_ids:
self._activities_by_id[device_id] = [
a for a in activities if a.device_id == device_id
]
_LOGGER.debug("Completed retrieving device activities")
async def async_get_doorbell_detail(self, doorbell_id):
"""Return doorbell detail."""
await self._async_update_doorbells()
return self._doorbell_detail_by_id.get(doorbell_id)
@Throttle(MIN_TIME_BETWEEN_DOORBELL_STATUS_UPDATES)
async def _async_update_doorbells(self):
await self._hass.async_add_executor_job(self._update_doorbells)
def _update_doorbells(self):
detail_by_id = {}
_LOGGER.debug("Start retrieving doorbell details")
for doorbell in self._doorbells:
_LOGGER.debug("Updating doorbell status for %s", doorbell.device_name)
try: try:
detail_by_id[doorbell.device_id] = self._api.get_doorbell_detail( self._device_detail_by_id[device.device_id] = await api_call(
self._access_token, doorbell.device_id self._august_gateway.access_token, device.device_id
) )
except RequestException as ex: except ClientError as ex:
_LOGGER.error( _LOGGER.error(
"Request error trying to retrieve doorbell status for %s. %s", "Request error trying to retrieve %s details for %s. %s",
doorbell.device_name, device.device_id,
device.device_name,
ex, ex,
) )
detail_by_id[doorbell.device_id] = None _LOGGER.debug(
except Exception: "Completed retrieving detail for %s (%s)",
detail_by_id[doorbell.device_id] = None device.device_name,
raise device.device_id,
_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") def _get_device_name(self, device_id):
self._lock_status_by_id = status_by_id """Return doorbell or lock name as August has it stored."""
self._door_state_by_id = state_by_id if self._locks_by_id.get(device_id):
self._door_last_state_update_time_utc_by_id = door_last_state_update_by_id return self._locks_by_id[device_id].device_name
self._lock_last_status_update_time_utc_by_id = lock_last_status_update_by_id if self._doorbells_by_id.get(device_id):
return self._doorbells_by_id[device_id].device_name
def get_last_lock_status_update_time_utc(self, lock_id): async def async_lock(self, device_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

View 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

View File

@ -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( _LOGGER.debug(
"Not adding sensor class %s for lock %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,
) )
continue continue
_LOGGER.debug( _LOGGER.debug("Adding sensor class door for %s", door.device_name)
"Adding sensor class %s for %s", devices.append(AugustDoorBinarySensor(data, "door_open", door))
SENSOR_TYPES_DOOR[sensor_type][SENSOR_DEVICE_CLASS],
door.device_name,
)
devices.append(AugustDoorBinarySensor(data, sensor_type, 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()}"
) )

View File

@ -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"

View 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)

View 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"]

View 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