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