Merge pull request #32932 from home-assistant/rc

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

View File

@ -29,6 +29,7 @@ omit =
homeassistant/components/airly/air_quality.py
homeassistant/components/airly/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

View File

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

View File

@ -1,53 +1,58 @@
repos:
- repo: https://github.com/psf/black
- repo: https://github.com/psf/black
rev: 19.10b0
hooks:
- id: black
- id: black
args:
- --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
- id: codespell
args:
- --ignore-words-list=hass,alot,datas,dof,dur,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing
- --skip="./.*,*.json"
- --quiet-level=2
exclude_types: [json]
- repo: https://gitlab.com/pycqa/flake8
- repo: https://gitlab.com/pycqa/flake8
rev: 3.7.9
hooks:
- id: flake8
- id: flake8
additional_dependencies:
- 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
- id: bandit
args:
- --quiet
- --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
- id: isort
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.4.0
hooks:
- id: check-json
- repo: local
- id: check-json
- 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
# committing from a GUI tool that was not launched from an activated
# shell.
- id: mypy
# Run mypy through our wrapper script in order to get the possible
# pyenv and/or virtualenv activated; it may not have been e.g. if
# committing from a GUI tool that was not launched from an activated
# shell.
- id: mypy
name: mypy
entry: script/run-in-env.sh mypy
language: script

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,6 @@
from asyncio import gather
from 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()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,16 +42,19 @@ 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:
self._device.switch_on()
return
self._device.switch_on()
def turn_off(self, **kwargs):
"""Turn off the light."""

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,23 @@
{
"config": {
"abort": {
"already_configured": "Aquesta clau API ja est\u00e0 sent utilitzada."
},
"error": {
"invalid_api_key": "Clau API inv\u00e0lida"
},
"step": {
"user": {
"data": {
"api_key": "Clau API",
"latitude": "Latitud",
"longitude": "Longitud",
"show_on_map": "Mostra al mapa l'\u00e0rea geogr\u00e0fica monitoritzada"
},
"description": "Monitoritzaci\u00f3 de la qualitat de l'aire per ubicaci\u00f3 geogr\u00e0fica.",
"title": "Configura AirVisual"
}
},
"title": "AirVisual"
}
}

View File

@ -0,0 +1,21 @@
{
"config": {
"abort": {
"already_configured": "Dieser API-Schl\u00fcssel wird bereits verwendet."
},
"error": {
"invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel"
},
"step": {
"user": {
"data": {
"api_key": "API-Schl\u00fcssel",
"latitude": "Breitengrad",
"longitude": "L\u00e4ngengrad"
},
"title": "Konfigurieren Sie AirVisual"
}
},
"title": "AirVisual"
}
}

View File

@ -0,0 +1,33 @@
{
"config": {
"abort": {
"already_configured": "This API key is already in use."
},
"error": {
"invalid_api_key": "Invalid API key"
},
"step": {
"user": {
"data": {
"api_key": "API Key",
"latitude": "Latitude",
"longitude": "Longitude"
},
"description": "Monitor air quality in a geographical location.",
"title": "Configure AirVisual"
}
},
"title": "AirVisual"
},
"options": {
"step": {
"init": {
"data": {
"show_on_map": "Show monitored geography on the map"
},
"description": "Set various options for the AirVisual integration.",
"title": "Configure AirVisual"
}
}
}
}

View File

@ -0,0 +1,23 @@
{
"config": {
"abort": {
"already_configured": "Esta clave API ya est\u00e1 en uso."
},
"error": {
"invalid_api_key": "Clave API inv\u00e1lida"
},
"step": {
"user": {
"data": {
"api_key": "Clave API",
"latitude": "Latitud",
"longitude": "Longitud",
"show_on_map": "Mostrar geograf\u00eda monitorizada en el mapa"
},
"description": "Monitorizar la calidad del aire en una ubicaci\u00f3n geogr\u00e1fica.",
"title": "Configurar AirVisual"
}
},
"title": "AirVisual"
}
}

View File

@ -0,0 +1,23 @@
{
"config": {
"abort": {
"already_configured": "Questa chiave API \u00e8 gi\u00e0 in uso."
},
"error": {
"invalid_api_key": "Chiave API non valida"
},
"step": {
"user": {
"data": {
"api_key": "Chiave API",
"latitude": "Latitudine",
"longitude": "Logitudine",
"show_on_map": "Mostra l'area geografica monitorata sulla mappa"
},
"description": "Monitorare la qualit\u00e0 dell'aria in una posizione geografica.",
"title": "Configura AirVisual"
}
},
"title": "AirVisual"
}
}

View File

@ -0,0 +1,21 @@
{
"config": {
"abort": {
"already_configured": "D\u00ebsen App Schl\u00ebssel g\u00ebtt scho benotzt"
},
"error": {
"invalid_api_key": "Ong\u00ebltegen API Schl\u00ebssel"
},
"step": {
"user": {
"data": {
"api_key": "API Schl\u00ebssel",
"latitude": "Breedegrad",
"longitude": "L\u00e4ngegrad"
},
"title": "AirVisual konfigur\u00e9ieren"
}
},
"title": "AirVisual"
}
}

View File

@ -0,0 +1,23 @@
{
"config": {
"abort": {
"already_configured": "Denne API-n\u00f8kkelen er allerede i bruk."
},
"error": {
"invalid_api_key": "Ugyldig API-n\u00f8kkel"
},
"step": {
"user": {
"data": {
"api_key": "API-n\u00f8kkel",
"latitude": "Breddegrad",
"longitude": "Lengdegrad",
"show_on_map": "Vis overv\u00e5ket geografi p\u00e5 kartet"
},
"description": "Overv\u00e5k luftkvaliteten p\u00e5 et geografisk sted.",
"title": "Konfigurer AirVisual"
}
},
"title": "AirVisual"
}
}

View File

@ -0,0 +1,23 @@
{
"config": {
"abort": {
"already_configured": "\u042d\u0442\u043e\u0442 \u043a\u043b\u044e\u0447 API \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f."
},
"error": {
"invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API."
},
"step": {
"user": {
"data": {
"api_key": "\u041a\u043b\u044e\u0447 API",
"latitude": "\u0428\u0438\u0440\u043e\u0442\u0430",
"longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430",
"show_on_map": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0435\u043c\u0443\u044e \u043e\u0431\u043b\u0430\u0441\u0442\u044c \u043d\u0430 \u043a\u0430\u0440\u0442\u0435"
},
"description": "\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u0438\u0440\u0443\u0439\u0442\u0435 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u043e \u0432\u043e\u0437\u0434\u0443\u0445\u0430 \u0432 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u043c \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0438.",
"title": "AirVisual"
}
},
"title": "AirVisual"
}
}

View File

@ -0,0 +1,23 @@
{
"config": {
"abort": {
"already_configured": "\u6b64 API \u5bc6\u9470\u5df2\u88ab\u4f7f\u7528\u3002"
},
"error": {
"invalid_api_key": "API \u5bc6\u78bc\u7121\u6548"
},
"step": {
"user": {
"data": {
"api_key": "API \u5bc6\u9470",
"latitude": "\u7def\u5ea6",
"longitude": "\u7d93\u5ea6",
"show_on_map": "\u65bc\u5730\u5716\u4e0a\u986f\u793a\u76e3\u63a7\u4f4d\u7f6e\u3002"
},
"description": "\u4f9d\u5730\u7406\u4f4d\u7f6e\u76e3\u63a7\u7a7a\u6c23\u54c1\u8cea\u3002",
"title": "\u8a2d\u5b9a AirVisual"
}
},
"title": "AirVisual"
}
}

View File

@ -1 +1,215 @@
"""The airvisual component."""
import asyncio
import logging
from pyairvisual import Client
from pyairvisual.errors import AirVisualError, InvalidKeyError
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
CONF_API_KEY,
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_SHOW_ON_MAP,
CONF_STATE,
)
from homeassistant.core import callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
from .const import (
CONF_CITY,
CONF_COUNTRY,
CONF_GEOGRAPHIES,
DATA_CLIENT,
DEFAULT_SCAN_INTERVAL,
DOMAIN,
TOPIC_UPDATE,
)
_LOGGER = logging.getLogger(__name__)
DATA_LISTENER = "listener"
DEFAULT_OPTIONS = {CONF_SHOW_ON_MAP: True}
CONF_NODE_ID = "node_id"
GEOGRAPHY_COORDINATES_SCHEMA = vol.Schema(
{
vol.Required(CONF_LATITUDE): cv.latitude,
vol.Required(CONF_LONGITUDE): cv.longitude,
}
)
GEOGRAPHY_PLACE_SCHEMA = vol.Schema(
{
vol.Required(CONF_CITY): cv.string,
vol.Required(CONF_STATE): cv.string,
vol.Required(CONF_COUNTRY): cv.string,
}
)
CLOUD_API_SCHEMA = vol.Schema(
{
vol.Required(CONF_API_KEY): cv.string,
vol.Optional(CONF_GEOGRAPHIES, default=[]): vol.All(
cv.ensure_list,
[vol.Any(GEOGRAPHY_COORDINATES_SCHEMA, GEOGRAPHY_PLACE_SCHEMA)],
),
}
)
CONFIG_SCHEMA = vol.Schema({DOMAIN: CLOUD_API_SCHEMA}, extra=vol.ALLOW_EXTRA)
@callback
def async_get_geography_id(geography_dict):
"""Generate a unique ID from a geography dict."""
if CONF_CITY in geography_dict:
return ",".join(
(
geography_dict[CONF_CITY],
geography_dict[CONF_STATE],
geography_dict[CONF_COUNTRY],
)
)
return ",".join(
(str(geography_dict[CONF_LATITUDE]), str(geography_dict[CONF_LONGITUDE]))
)
async def async_setup(hass, config):
"""Set up the AirVisual component."""
hass.data[DOMAIN] = {}
hass.data[DOMAIN][DATA_CLIENT] = {}
hass.data[DOMAIN][DATA_LISTENER] = {}
if DOMAIN not in config:
return True
conf = config[DOMAIN]
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=conf
)
)
return True
async def async_setup_entry(hass, config_entry):
"""Set up AirVisual as config entry."""
entry_updates = {}
if not config_entry.unique_id:
# If the config entry doesn't already have a unique ID, set one:
entry_updates["unique_id"] = config_entry.data[CONF_API_KEY]
if not config_entry.options:
# If the config entry doesn't already have any options set, set defaults:
entry_updates["options"] = DEFAULT_OPTIONS
if entry_updates:
hass.config_entries.async_update_entry(config_entry, **entry_updates)
websession = aiohttp_client.async_get_clientsession(hass)
hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = AirVisualData(
hass, Client(websession, api_key=config_entry.data[CONF_API_KEY]), config_entry
)
try:
await hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id].async_update()
except InvalidKeyError:
_LOGGER.error("Invalid API key provided")
raise ConfigEntryNotReady
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, "sensor")
)
async def refresh(event_time):
"""Refresh data from AirVisual."""
await hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id].async_update()
hass.data[DOMAIN][DATA_LISTENER][config_entry.entry_id] = async_track_time_interval(
hass, refresh, DEFAULT_SCAN_INTERVAL
)
config_entry.add_update_listener(async_update_options)
return True
async def async_unload_entry(hass, config_entry):
"""Unload an AirVisual config entry."""
hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id)
remove_listener = hass.data[DOMAIN][DATA_LISTENER].pop(config_entry.entry_id)
remove_listener()
await hass.config_entries.async_forward_entry_unload(config_entry, "sensor")
return True
async def async_update_options(hass, config_entry):
"""Handle an options update."""
airvisual = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id]
airvisual.async_update_options(config_entry.options)
class AirVisualData:
"""Define a class to manage data from the AirVisual cloud API."""
def __init__(self, hass, client, config_entry):
"""Initialize."""
self._client = client
self._hass = hass
self.data = {}
self.options = config_entry.options
self.geographies = {
async_get_geography_id(geography): geography
for geography in config_entry.data[CONF_GEOGRAPHIES]
}
async def async_update(self):
"""Get new data for all locations from the AirVisual cloud API."""
tasks = []
for geography in self.geographies.values():
if CONF_CITY in geography:
tasks.append(
self._client.api.city(
geography[CONF_CITY],
geography[CONF_STATE],
geography[CONF_COUNTRY],
)
)
else:
tasks.append(
self._client.api.nearest_city(
geography[CONF_LATITUDE], geography[CONF_LONGITUDE],
)
)
results = await asyncio.gather(*tasks, return_exceptions=True)
for geography_id, result in zip(self.geographies, results):
if isinstance(result, AirVisualError):
_LOGGER.error("Error while retrieving data: %s", result)
self.data[geography_id] = {}
continue
self.data[geography_id] = result
_LOGGER.debug("Received new data")
async_dispatcher_send(self._hass, TOPIC_UPDATE)
@callback
def async_update_options(self, options):
"""Update the data manager's options."""
self.options = options
async_dispatcher_send(self._hass, TOPIC_UPDATE)

View File

@ -0,0 +1,123 @@
"""Define a config flow manager for AirVisual."""
import logging
from pyairvisual import Client
from pyairvisual.errors import InvalidKeyError
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import (
CONF_API_KEY,
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_SHOW_ON_MAP,
)
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client, config_validation as cv
from .const import CONF_GEOGRAPHIES, DOMAIN # pylint: disable=unused-import
_LOGGER = logging.getLogger("homeassistant.components.airvisual")
class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle an AirVisual config flow."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
@property
def cloud_api_schema(self):
"""Return the data schema for the cloud API."""
return vol.Schema(
{
vol.Required(CONF_API_KEY): str,
vol.Required(
CONF_LATITUDE, default=self.hass.config.latitude
): cv.latitude,
vol.Required(
CONF_LONGITUDE, default=self.hass.config.longitude
): cv.longitude,
}
)
async def _async_set_unique_id(self, unique_id):
"""Set the unique ID of the config flow and abort if it already exists."""
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured()
@callback
async def _show_form(self, errors=None):
"""Show the form to the user."""
return self.async_show_form(
step_id="user", data_schema=self.cloud_api_schema, errors=errors or {},
)
@staticmethod
@callback
def async_get_options_flow(config_entry):
"""Define the config flow to handle options."""
return AirVisualOptionsFlowHandler(config_entry)
async def async_step_import(self, import_config):
"""Import a config entry from configuration.yaml."""
return await self.async_step_user(import_config)
async def async_step_user(self, user_input=None):
"""Handle the start of the config flow."""
if not user_input:
return await self._show_form()
await self._async_set_unique_id(user_input[CONF_API_KEY])
websession = aiohttp_client.async_get_clientsession(self.hass)
client = Client(websession, api_key=user_input[CONF_API_KEY])
try:
await client.api.nearest_city()
except InvalidKeyError:
return await self._show_form(errors={CONF_API_KEY: "invalid_api_key"})
data = {CONF_API_KEY: user_input[CONF_API_KEY]}
if user_input.get(CONF_GEOGRAPHIES):
data[CONF_GEOGRAPHIES] = user_input[CONF_GEOGRAPHIES]
else:
data[CONF_GEOGRAPHIES] = [
{
CONF_LATITUDE: user_input.get(
CONF_LATITUDE, self.hass.config.latitude
),
CONF_LONGITUDE: user_input.get(
CONF_LONGITUDE, self.hass.config.longitude
),
}
]
return self.async_create_entry(
title=f"Cloud API (API key: {user_input[CONF_API_KEY][:4]}...)", data=data
)
class AirVisualOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle an AirVisual options flow."""
def __init__(self, config_entry):
"""Initialize."""
self.config_entry = config_entry
async def async_step_init(self, user_input=None):
"""Manage the options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Required(
CONF_SHOW_ON_MAP,
default=self.config_entry.options.get(CONF_SHOW_ON_MAP),
): bool
}
),
)

View File

@ -0,0 +1,14 @@
"""Define AirVisual constants."""
from datetime import timedelta
DOMAIN = "airvisual"
CONF_CITY = "city"
CONF_COUNTRY = "country"
CONF_GEOGRAPHIES = "geographies"
DATA_CLIENT = "client"
DEFAULT_SCAN_INTERVAL = timedelta(minutes=10)
TOPIC_UPDATE = f"{DOMAIN}_update"

View File

@ -1,6 +1,7 @@
{
"domain": "airvisual",
"name": "AirVisual",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/airvisual",
"requirements": ["pyairvisual==3.0.1"],
"dependencies": [],

View File

@ -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
)
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)
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,
)
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 = []

View File

@ -0,0 +1,33 @@
{
"config": {
"title": "AirVisual",
"step": {
"user": {
"title": "Configure AirVisual",
"description": "Monitor air quality in a geographical location.",
"data": {
"api_key": "API Key",
"latitude": "Latitude",
"longitude": "Longitude"
}
}
},
"error": {
"invalid_api_key": "Invalid API key"
},
"abort": {
"already_configured": "This API key is already in use."
}
},
"options": {
"step": {
"init": {
"title": "Configure AirVisual",
"description": "Set various options for the AirVisual integration.",
"data": {
"show_on_map": "Show monitored geography on the map"
}
}
}
}
}

View File

@ -53,9 +53,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
except (TypeError, KeyError, NameError, ValueError) as ex:
_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,
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,5 +4,6 @@
"documentation": "https://www.home-assistant.io/integrations/alexa",
"requirements": [],
"dependencies": ["http"],
"after_dependencies": ["logbook"],
"codeowners": ["@home-assistant/cloud", "@ochlocracy"]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -83,7 +83,11 @@ class AmbientWeatherSensor(AmbientWeatherEntity):
w_m2_brightness_val = self._ambient.stations[self._mac_address][
ATTR_LAST_DATA
].get(TYPE_SOLARRADIATION)
self._state = round(float(w_m2_brightness_val) / 0.0079)
if w_m2_brightness_val is None:
self._state = None
else:
self._state = round(float(w_m2_brightness_val) / 0.0079)
else:
self._state = self._ambient.stations[self._mac_address][ATTR_LAST_DATA].get(
self._sensor_type

View File

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

View File

@ -109,7 +109,6 @@ CONFIG_SCHEMA = vol.Schema(
)
# pylint: disable=too-many-ancestors
class AmcrestChecker(Http):
"""amcrest.Http wrapper for catching errors."""

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,32 @@
{
"config": {
"abort": {
"already_configured": "El compte ja ha estat configurat"
},
"error": {
"cannot_connect": "No s'ha pogut connectar, torna-ho a provar",
"invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
"unknown": "Error inesperat"
},
"step": {
"user": {
"data": {
"login_method": "M\u00e8tode d'inici de sessi\u00f3",
"password": "Contrasenya",
"timeout": "Temps d'espera (segons)",
"username": "Nom d'usuari"
},
"description": "Si el m\u00e8tode d'inici de sessi\u00f3 \u00e9s 'email', el nom d'usuari \u00e9s l'adre\u00e7a de correu electr\u00f2nic. Si el m\u00e8tode d'inici de sessi\u00f3 \u00e9s 'phone', el nom d'usuari \u00e9s el n\u00famero de tel\u00e8fon en el format \"+NNNNNNNNN\".",
"title": "Configuraci\u00f3 de compte August"
},
"validation": {
"data": {
"code": "Codi de verificaci\u00f3"
},
"description": "Comprova el teu {login_method} ({username}) i introdueix el codi de verificaci\u00f3 a continuaci\u00f3",
"title": "Autenticaci\u00f3 de dos factors"
}
},
"title": "August"
}
}

View File

@ -0,0 +1,32 @@
{
"config": {
"abort": {
"already_configured": "Kontoen er allerede konfigureret"
},
"error": {
"cannot_connect": "Kunne ikke oprette forbindelse. Pr\u00f8v igen",
"invalid_auth": "Ugyldig godkendelse",
"unknown": "Uventet fejl"
},
"step": {
"user": {
"data": {
"login_method": "Loginmetode",
"password": "Adgangskode",
"timeout": "Timeout (sekunder)",
"username": "Brugernavn"
},
"description": "Hvis loginmetoden er 'e-mail', er brugernavn e-mailadressen. Hvis loginmetoden er 'telefon', er brugernavn telefonnummeret i formatet '+NNNNNNNNNN'.",
"title": "Konfigurer en August-konto"
},
"validation": {
"data": {
"code": "Bekr\u00e6ftelseskode"
},
"description": "Kontroller dit {login_method} ({username}), og angiv bekr\u00e6ftelseskoden nedenfor",
"title": "Tofaktorgodkendelse"
}
},
"title": "August"
}
}

View File

@ -0,0 +1,32 @@
{
"config": {
"abort": {
"already_configured": "La cuenta ya est\u00e1 configurada"
},
"error": {
"cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntalo de nuevo.",
"invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
"unknown": "Error inesperado"
},
"step": {
"user": {
"data": {
"login_method": "M\u00e9todo de inicio de sesi\u00f3n",
"password": "Contrase\u00f1a",
"timeout": "Tiempo de espera (segundos)",
"username": "Usuario"
},
"description": "Si el M\u00e9todo de Inicio de Sesi\u00f3n es 'correo electr\u00f3nico', Usuario es la direcci\u00f3n de correo electr\u00f3nico. Si el M\u00e9todo de Inicio de Sesi\u00f3n es 'tel\u00e9fono', Usuario es el n\u00famero de tel\u00e9fono en formato '+NNNNNNNNN'.",
"title": "Configurar una cuenta de August"
},
"validation": {
"data": {
"code": "C\u00f3digo de verificaci\u00f3n"
},
"description": "Por favor, compruebe tu {login_method} ({username}) e introduce el c\u00f3digo de verificaci\u00f3n a continuaci\u00f3n",
"title": "Autenticaci\u00f3n de dos factores"
}
},
"title": "August"
}
}

View File

@ -0,0 +1,32 @@
{
"config": {
"abort": {
"already_configured": "L'account \u00e8 gi\u00e0 configurato"
},
"error": {
"cannot_connect": "Impossibile connettersi, si prega di riprovare.",
"invalid_auth": "Autenticazione non valida",
"unknown": "Errore imprevisto"
},
"step": {
"user": {
"data": {
"login_method": "Metodo di accesso",
"password": "Password",
"timeout": "Timeout (in secondi)",
"username": "Nome utente"
},
"description": "Se il metodo di accesso \u00e8 \"e-mail\", il nome utente \u00e8 l'indirizzo e-mail. Se il metodo di accesso \u00e8 \"telefono\", il nome utente \u00e8 il numero di telefono nel formato \"+NNNNNNNNN\".",
"title": "Configura un account di August"
},
"validation": {
"data": {
"code": "Codice di verifica"
},
"description": "Controlla il tuo {login_method} ({username}) e inserisci il codice di verifica seguente",
"title": "Autenticazione a due fattori"
}
},
"title": "August"
}
}

View File

@ -0,0 +1,32 @@
{
"config": {
"abort": {
"already_configured": "Kont ass scho konfigur\u00e9iert"
},
"error": {
"cannot_connect": "Feeler beim verbannen, prob\u00e9iert w.e.g. nach emol.",
"invalid_auth": "Ong\u00eblteg Authentifikatioun",
"unknown": "Onerwaarte Feeler"
},
"step": {
"user": {
"data": {
"login_method": "Login Method",
"password": "Passwuert",
"timeout": "Z\u00e4itiwwerscheidung (sekonnen)",
"username": "Benotzernumm"
},
"description": "Wann d'Login Method 'E-Mail' ass, dannn ass de Benotzernumm d'E-Mail Adress. Wann d'Login-Method 'Telefon' ass, ass den Benotzernumm d'Telefonsnummer am Format '+ NNNNNNNNN'.",
"title": "August Kont ariichten"
},
"validation": {
"data": {
"code": "Verifikatiouns Code"
},
"description": "Pr\u00e9ift w.e.g. \u00c4re {login_method} ({username}) a gitt de Verifikatiounscode hei dr\u00ebnner an",
"title": "2-Faktor-Authentifikatioun"
}
},
"title": "August"
}
}

View File

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

View File

@ -0,0 +1,32 @@
{
"config": {
"abort": {
"already_configured": "Kontoen er allerede konfigurert"
},
"error": {
"cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen",
"invalid_auth": "Ugyldig godkjenning",
"unknown": "Uventet feil"
},
"step": {
"user": {
"data": {
"login_method": "P\u00e5loggingsmetode",
"password": "Passord",
"timeout": "Tidsavbrudd (sekunder)",
"username": "Brukernavn"
},
"description": "Hvis p\u00e5loggingsmetoden er 'e-post', er brukernavnet e-postadressen. Hvis p\u00e5loggingsmetoden er 'telefon', er brukernavn telefonnummeret i formatet '+ NNNNNNNNN'.",
"title": "Sett opp en August konto"
},
"validation": {
"data": {
"code": "Bekreftelseskode"
},
"description": "Kontroller {login_method} ( {username} ) og skriv inn bekreftelseskoden nedenfor",
"title": "To-faktor autentisering"
}
},
"title": "August"
}
}

View File

@ -0,0 +1,32 @@
{
"config": {
"abort": {
"already_configured": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430."
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.",
"invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
"user": {
"data": {
"login_method": "\u0421\u043f\u043e\u0441\u043e\u0431 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)",
"username": "\u041b\u043e\u0433\u0438\u043d"
},
"description": "\u0415\u0441\u043b\u0438 \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0441\u043f\u043e\u0441\u043e\u0431\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0432\u044b\u0431\u0440\u0430\u043d\u043e 'email', \u0442\u043e \u043b\u043e\u0433\u0438\u043d\u043e\u043c \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b. \u0415\u0441\u043b\u0438 \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0441\u043f\u043e\u0441\u043e\u0431\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0432\u044b\u0431\u0440\u0430\u043d\u043e 'phone', \u0442\u043e \u043b\u043e\u0433\u0438\u043d\u043e\u043c \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043d\u043e\u043c\u0435\u0440 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0430 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 '+NNNNNNNNN'.",
"title": "August"
},
"validation": {
"data": {
"code": "\u041a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f"
},
"description": "\u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 {login_method} ({username}) \u0438 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0439 \u043f\u0440\u043e\u0432\u0435\u0440\u043e\u0447\u043d\u044b\u0439 \u043a\u043e\u0434.",
"title": "\u0414\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f"
}
},
"title": "August"
}
}

View File

@ -0,0 +1,32 @@
{
"config": {
"abort": {
"already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
"error": {
"cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21",
"invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
"step": {
"user": {
"data": {
"login_method": "\u767b\u5165\u65b9\u5f0f",
"password": "\u5bc6\u78bc",
"timeout": "\u903e\u6642\uff08\u79d2\uff09",
"username": "\u4f7f\u7528\u8005\u540d\u7a31"
},
"description": "\u5047\u5982\u767b\u5165\u65b9\u5f0f\u70ba\u90f5\u4ef6\u300cemail\u300d\u3001\u4f7f\u7528\u8005\u540d\u7a31\u70ba\u96fb\u5b50\u90f5\u4ef6\u4f4d\u5740\u3002\u5047\u5982\u767b\u5165\u65b9\u5f0f\u70ba\u96fb\u8a71\u300cphone\u300d\u3001\u5247\u4f7f\u7528\u8005\u540d\u7a31\u70ba\u5305\u542b\u570b\u78bc\u4e4b\u96fb\u8a71\u865f\u78bc\uff0c\u5982\u300c+NNNNNNNNN\u300d\u3002",
"title": "\u8a2d\u5b9a August \u5e33\u865f"
},
"validation": {
"data": {
"code": "\u9a57\u8b49\u78bc"
},
"description": "\u8acb\u78ba\u8a8d {login_method} ({username}) \u4e26\u65bc\u4e0b\u65b9\u8f38\u5165\u9a57\u8b49\u78bc",
"title": "\u5169\u6b65\u9a5f\u9a57\u8b49"
}
},
"title": "August"
}
}

View File

@ -1,67 +1,41 @@
"""Support for August devices."""
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))
hass.components.persistent_notification.create(
"Error: {}<br />"
"You will need to restart hass after fixing."
"".format(ex),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID,
)
state = authentication.state
if state == AuthenticationState.AUTHENTICATED:
if DOMAIN in _CONFIGURING:
hass.components.configurator.request_done(_CONFIGURING.pop(DOMAIN))
hass.data[DATA_AUGUST] = AugustData(
hass, api, authentication, authenticator, token_refresh_lock
)
for component in AUGUST_COMPONENTS:
discovery.load_platform(hass, component, DOMAIN, {}, config)
return True
if state == AuthenticationState.BAD_PASSWORD:
_LOGGER.error("Invalid password provided")
await august_gateway.async_authenticate()
except RequireValidation:
await async_request_validation(hass, config_entry, august_gateway)
return False
if state == AuthenticationState.REQUIRES_VALIDATION:
request_configuration(hass, config, api, authenticator, token_refresh_lock)
except InvalidAuth:
_LOGGER.error("Password is no longer valid. Please set up August again")
return False
# We still use the configurator to get a new 2fa code
# when needed since config_flow doesn't have a way
# to re-request if it expires
if TWO_FA_REVALIDATE in hass.data[DOMAIN][entry_id]:
hass.components.configurator.async_request_done(
hass.data[DOMAIN][entry_id].pop(TWO_FA_REVALIDATE)
)
hass.data[DOMAIN][entry_id][DATA_AUGUST] = AugustData(hass, august_gateway)
await hass.data[DOMAIN][entry_id][DATA_AUGUST].async_setup()
for component in AUGUST_COMPONENTS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, component)
)
return True
async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the August component from YAML."""
conf = config.get(DOMAIN)
hass.data.setdefault(DOMAIN, {})
if not conf:
return True
return False
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={
CONF_LOGIN_METHOD: conf.get(CONF_LOGIN_METHOD),
CONF_USERNAME: conf.get(CONF_USERNAME),
CONF_PASSWORD: conf.get(CONF_PASSWORD),
CONF_INSTALL_ID: conf.get(CONF_INSTALL_ID),
CONF_ACCESS_TOKEN_CACHE_FILE: DEFAULT_AUGUST_CONFIG_FILE,
},
)
)
return True
async def async_setup(hass, config):
"""Set up the August component."""
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up August from a config entry."""
august_gateway = AugustGateway(hass)
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))
await august_gateway.async_setup(entry.data)
return await async_setup_august(hass, entry, august_gateway)
except asyncio.TimeoutError:
raise ConfigEntryNotReady
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),
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
]
)
)
def close_http_session(event):
"""Close API sessions used to connect to August."""
_LOGGER.debug("Closing August HTTP sessions")
if api_http_session:
try:
api_http_session.close()
except RequestException:
pass
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
_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
)
return unload_ok
class AugustData:
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,
)
self._access_token = refreshed_authentication.access_token
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_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)
)
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
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
)
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)
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
]
async def _async_update_device_detail(self, device, api_call):
_LOGGER.debug(
"Started retrieving detail for %s (%s)",
device.device_name,
device.device_id,
)
_LOGGER.debug("Completed retrieving device activities")
try:
self._device_detail_by_id[device.device_id] = await api_call(
self._august_gateway.access_token, device.device_id
)
except ClientError as ex:
_LOGGER.error(
"Request error trying to retrieve %s details for %s. %s",
device.device_id,
device.device_name,
ex,
)
_LOGGER.debug(
"Completed retrieving detail for %s (%s)",
device.device_name,
device.device_id,
)
async def async_get_doorbell_detail(self, doorbell_id):
"""Return doorbell detail."""
await self._async_update_doorbells()
return self._doorbell_detail_by_id.get(doorbell_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
@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
)
except RequestException as ex:
_LOGGER.error(
"Request error trying to retrieve doorbell status for %s. %s",
doorbell.device_name,
ex,
)
detail_by_id[doorbell.device_id] = None
except Exception:
detail_by_id[doorbell.device_id] = None
raise
_LOGGER.debug("Completed retrieving doorbell details")
self._doorbell_detail_by_id = detail_by_id
def update_door_state(self, lock_id, door_state, update_start_time_utc):
"""Set the door status and last status update time.
This is called when newer activity is detected on the activity feed
in order to keep the internal data in sync
"""
self._door_state_by_id[lock_id] = door_state
self._door_last_state_update_time_utc_by_id[lock_id] = update_start_time_utc
return True
def update_lock_status(self, lock_id, lock_status, update_start_time_utc):
"""Set the lock status and last status update time.
This is used when the lock, unlock apis are called
or newer activity is detected on the activity feed
in order to keep the internal data in sync
"""
self._lock_status_by_id[lock_id] = lock_status
self._lock_last_status_update_time_utc_by_id[lock_id] = update_start_time_utc
return True
def lock_has_doorsense(self, lock_id):
"""Determine if a lock has doorsense installed and can tell when the door is open or closed."""
# We do not update here since this is not expected
# to change until restart
if self._lock_detail_by_id[lock_id] is None:
return False
return self._lock_detail_by_id[lock_id].doorsense
async def async_get_lock_status(self, lock_id):
"""Return status if the door is locked or unlocked.
This is status for the lock itself.
"""
await self._async_update_locks()
return self._lock_status_by_id.get(lock_id)
async def async_get_lock_detail(self, lock_id):
"""Return lock detail."""
await self._async_update_locks()
return self._lock_detail_by_id.get(lock_id)
def get_lock_name(self, device_id):
"""Return lock name as August has it stored."""
for lock in self._locks:
if lock.device_id == device_id:
return lock.device_name
async def async_get_door_state(self, lock_id):
"""Return status if the door is open or closed.
This is the status from the door sensor.
"""
await self._async_update_locks_status()
return self._door_state_by_id.get(lock_id)
async def _async_update_locks(self):
await self._async_update_locks_status()
await self._async_update_locks_detail()
@Throttle(MIN_TIME_BETWEEN_LOCK_STATUS_UPDATES)
async def _async_update_locks_status(self):
await self._hass.async_add_executor_job(self._update_locks_status)
def _update_locks_status(self):
status_by_id = {}
state_by_id = {}
lock_last_status_update_by_id = {}
door_last_state_update_by_id = {}
_LOGGER.debug("Start retrieving lock and door status")
for lock in self._locks:
update_start_time_utc = dt.utcnow()
_LOGGER.debug("Updating lock and door status for %s", lock.device_name)
try:
(
status_by_id[lock.device_id],
state_by_id[lock.device_id],
) = self._api.get_lock_status(
self._access_token, lock.device_id, door_status=True
)
# Since there is a a race condition between calling the
# lock and activity apis, we set the last update time
# BEFORE making the api call since we will compare this
# to activity later we want activity to win over stale lock/door
# state.
lock_last_status_update_by_id[lock.device_id] = update_start_time_utc
door_last_state_update_by_id[lock.device_id] = update_start_time_utc
except RequestException as ex:
_LOGGER.error(
"Request error trying to retrieve lock and door status for %s. %s",
lock.device_name,
ex,
)
status_by_id[lock.device_id] = None
state_by_id[lock.device_id] = None
except Exception:
status_by_id[lock.device_id] = None
state_by_id[lock.device_id] = None
raise
_LOGGER.debug("Completed retrieving lock and door status")
self._lock_status_by_id = status_by_id
self._door_state_by_id = state_by_id
self._door_last_state_update_time_utc_by_id = door_last_state_update_by_id
self._lock_last_status_update_time_utc_by_id = lock_last_status_update_by_id
def get_last_lock_status_update_time_utc(self, lock_id):
"""Return the last time that a lock status update was seen from the august API."""
# Since the activity api is called more frequently than
# the lock api it is possible that the lock has not
# been updated yet
if lock_id not in self._lock_last_status_update_time_utc_by_id:
return dt.utc_from_timestamp(0)
return self._lock_last_status_update_time_utc_by_id[lock_id]
def get_last_door_state_update_time_utc(self, lock_id):
"""Return the last time that a door status update was seen from the august API."""
# Since the activity api is called more frequently than
# the lock api it is possible that the door has not
# been updated yet
if lock_id not in self._door_last_state_update_time_utc_by_id:
return dt.utc_from_timestamp(0)
return self._door_last_state_update_time_utc_by_id[lock_id]
@Throttle(MIN_TIME_BETWEEN_LOCK_DETAIL_UPDATES)
async def _async_update_locks_detail(self):
await self._hass.async_add_executor_job(self._update_locks_detail)
def _update_locks_detail(self):
detail_by_id = {}
_LOGGER.debug("Start retrieving locks detail")
for lock in self._locks:
try:
detail_by_id[lock.device_id] = self._api.get_lock_detail(
self._access_token, lock.device_id
)
except RequestException as ex:
_LOGGER.error(
"Request error trying to retrieve door details for %s. %s",
lock.device_name,
ex,
)
detail_by_id[lock.device_id] = None
except Exception:
detail_by_id[lock.device_id] = None
raise
_LOGGER.debug("Completed retrieving locks detail")
self._lock_detail_by_id = detail_by_id
def lock(self, device_id):
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]

View File

@ -0,0 +1,124 @@
"""Consume the august activity stream."""
import logging
from aiohttp import ClientError
from homeassistant.util.dt import utcnow
from .const import ACTIVITY_UPDATE_INTERVAL
from .subscriber import AugustSubscriberMixin
_LOGGER = logging.getLogger(__name__)
ACTIVITY_STREAM_FETCH_LIMIT = 10
ACTIVITY_CATCH_UP_FETCH_LIMIT = 1000
class ActivityStream(AugustSubscriberMixin):
"""August activity stream handler."""
def __init__(self, hass, api, august_gateway, house_ids):
"""Init August activity stream object."""
super().__init__(hass, ACTIVITY_UPDATE_INTERVAL)
self._hass = hass
self._august_gateway = august_gateway
self._api = api
self._house_ids = house_ids
self._latest_activities_by_id_type = {}
self._last_update_time = None
self._abort_async_track_time_interval = None
async def async_setup(self):
"""Token refresh check and catch up the activity stream."""
await self._async_refresh(utcnow)
def get_latest_device_activity(self, device_id, activity_types):
"""Return latest activity that is one of the acitivty_types."""
if device_id not in self._latest_activities_by_id_type:
return None
latest_device_activities = self._latest_activities_by_id_type[device_id]
latest_activity = None
for activity_type in activity_types:
if activity_type in latest_device_activities:
if (
latest_activity is not None
and latest_device_activities[activity_type].activity_start_time
<= latest_activity.activity_start_time
):
continue
latest_activity = latest_device_activities[activity_type]
return latest_activity
async def _async_refresh(self, time):
"""Update the activity stream from August."""
# This is the only place we refresh the api token
await self._august_gateway.async_refresh_access_token_if_needed()
await self._async_update_device_activities(time)
async def _async_update_device_activities(self, time):
_LOGGER.debug("Start retrieving device activities")
limit = (
ACTIVITY_STREAM_FETCH_LIMIT
if self._last_update_time
else ACTIVITY_CATCH_UP_FETCH_LIMIT
)
for house_id in self._house_ids:
_LOGGER.debug("Updating device activity for house id %s", house_id)
try:
activities = await self._api.async_get_house_activities(
self._august_gateway.access_token, house_id, limit=limit
)
except ClientError as ex:
_LOGGER.error(
"Request error trying to retrieve activity for house id %s: %s",
house_id,
ex,
)
# Make sure we process the next house if one of them fails
continue
_LOGGER.debug(
"Completed retrieving device activities for house id %s", house_id
)
updated_device_ids = self._process_newer_device_activities(activities)
if updated_device_ids:
for device_id in updated_device_ids:
_LOGGER.debug(
"async_signal_device_id_update (from activity stream): %s",
device_id,
)
self.async_signal_device_id_update(device_id)
self._last_update_time = time
def _process_newer_device_activities(self, activities):
updated_device_ids = set()
for activity in activities:
self._latest_activities_by_id_type.setdefault(activity.device_id, {})
lastest_activity = self._latest_activities_by_id_type[
activity.device_id
].get(activity.activity_type)
# Ignore activities that are older than the latest one
if (
lastest_activity
and lastest_activity.activity_start_time >= activity.activity_start_time
):
continue
self._latest_activities_by_id_type[activity.device_id][
activity.activity_type
] = activity
updated_device_ids.add(activity.device_id)
return updated_device_ids

View File

@ -2,56 +2,61 @@
from datetime import datetime, timedelta
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):
_LOGGER.debug(
"Not adding sensor class %s for lock %s ",
SENSOR_TYPES_DOOR[sensor_type][SENSOR_DEVICE_CLASS],
door.device_name,
)
continue
detail = data.get_device_detail(door.device_id)
if not detail.doorsense:
_LOGGER.debug(
"Adding sensor class %s for %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,
)
devices.append(AugustDoorBinarySensor(data, sensor_type, door))
continue
_LOGGER.debug("Adding sensor class door for %s", door.device_name)
devices.append(AugustDoorBinarySensor(data, "door_open", door))
for doorbell in data.doorbells:
for 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()}"
)

View File

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

View File

@ -0,0 +1,133 @@
"""Config flow for August integration."""
import logging
from august.authenticator import ValidationResult
import voluptuous as vol
from homeassistant import config_entries, core
from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME
from .const import (
CONF_LOGIN_METHOD,
DEFAULT_TIMEOUT,
LOGIN_METHODS,
VERIFICATION_CODE_KEY,
)
from .const import DOMAIN # pylint:disable=unused-import
from .exceptions import CannotConnect, InvalidAuth, RequireValidation
from .gateway import AugustGateway
_LOGGER = logging.getLogger(__name__)
DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_LOGIN_METHOD, default="phone"): vol.In(LOGIN_METHODS),
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.Coerce(int),
}
)
async def async_validate_input(
hass: core.HomeAssistant, data, august_gateway,
):
"""Validate the user input allows us to connect.
Data has the keys from DATA_SCHEMA with values provided by the user.
Request configuration steps from the user.
"""
code = data.get(VERIFICATION_CODE_KEY)
if code is not None:
result = await august_gateway.authenticator.async_validate_verification_code(
code
)
_LOGGER.debug("Verification code validation: %s", result)
if result != ValidationResult.VALIDATED:
raise RequireValidation
try:
await august_gateway.async_authenticate()
except RequireValidation:
_LOGGER.debug(
"Requesting new verification code for %s via %s",
data.get(CONF_USERNAME),
data.get(CONF_LOGIN_METHOD),
)
if code is None:
await august_gateway.authenticator.async_send_verification_code()
raise
return {
"title": data.get(CONF_USERNAME),
"data": august_gateway.config_entry(),
}
class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for August."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
def __init__(self):
"""Store an AugustGateway()."""
self._august_gateway = None
self.user_auth_details = {}
super().__init__()
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
if self._august_gateway is None:
self._august_gateway = AugustGateway(self.hass)
errors = {}
if user_input is not None:
await self._august_gateway.async_setup(user_input)
try:
info = await async_validate_input(
self.hass, user_input, self._august_gateway,
)
await self.async_set_unique_id(user_input[CONF_USERNAME])
return self.async_create_entry(title=info["title"], data=info["data"])
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except RequireValidation:
self.user_auth_details = user_input
return await self.async_step_validation()
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)
async def async_step_validation(self, user_input=None):
"""Handle validation (2fa) step."""
if user_input:
return await self.async_step_user({**self.user_auth_details, **user_input})
return self.async_show_form(
step_id="validation",
data_schema=vol.Schema(
{vol.Required(VERIFICATION_CODE_KEY): vol.All(str, vol.Strip)}
),
description_placeholders={
CONF_USERNAME: self.user_auth_details.get(CONF_USERNAME),
CONF_LOGIN_METHOD: self.user_auth_details.get(CONF_LOGIN_METHOD),
},
)
async def async_step_import(self, user_input):
"""Handle import."""
await self.async_set_unique_id(user_input[CONF_USERNAME])
self._abort_if_unique_id_configured()
return await self.async_step_user(user_input)

View File

@ -0,0 +1,44 @@
"""Constants for August devices."""
from datetime import timedelta
DEFAULT_TIMEOUT = 10
CONF_ACCESS_TOKEN_CACHE_FILE = "access_token_cache_file"
CONF_LOGIN_METHOD = "login_method"
CONF_INSTALL_ID = "install_id"
VERIFICATION_CODE_KEY = "verification_code"
NOTIFICATION_ID = "august_notification"
NOTIFICATION_TITLE = "August"
DEFAULT_AUGUST_CONFIG_FILE = ".august.conf"
DATA_AUGUST = "data_august"
DEFAULT_NAME = "August"
DOMAIN = "august"
OPERATION_METHOD_AUTORELOCK = "autorelock"
OPERATION_METHOD_REMOTE = "remote"
OPERATION_METHOD_KEYPAD = "keypad"
OPERATION_METHOD_MOBILE_DEVICE = "mobile"
ATTR_OPERATION_AUTORELOCK = "autorelock"
ATTR_OPERATION_METHOD = "method"
ATTR_OPERATION_REMOTE = "remote"
ATTR_OPERATION_KEYPAD = "keypad"
# Limit battery, online, and hardware updates to hourly
# in order to reduce the number of api requests and
# avoid hitting rate limits
MIN_TIME_BETWEEN_DETAIL_UPDATES = timedelta(hours=1)
# Activity needs to be checked more frequently as the
# doorbell motion and rings are included here
ACTIVITY_UPDATE_INTERVAL = timedelta(seconds=10)
LOGIN_METHODS = ["phone", "email"]
AUGUST_COMPONENTS = ["camera", "binary_sensor", "lock", "sensor"]

View File

@ -0,0 +1,67 @@
"""Base class for August entity."""
import logging
from homeassistant.core import callback
from homeassistant.helpers.entity import Entity
from . import DEFAULT_NAME, DOMAIN
_LOGGER = logging.getLogger(__name__)
class AugustEntityMixin(Entity):
"""Base implementation for August device."""
def __init__(self, data, device):
"""Initialize an August device."""
super().__init__()
self._data = data
self._device = device
@property
def should_poll(self):
"""Return False, updates are controlled via the hub."""
return False
@property
def _device_id(self):
return self._device.device_id
@property
def _detail(self):
return self._data.get_device_detail(self._device.device_id)
@property
def device_info(self):
"""Return the device_info of the device."""
return {
"identifiers": {(DOMAIN, self._device_id)},
"name": self._device.device_name,
"manufacturer": DEFAULT_NAME,
"sw_version": self._detail.firmware_version,
"model": self._detail.model,
}
@callback
def _update_from_data_and_write_state(self):
self._update_from_data()
self.async_write_ha_state()
async def async_added_to_hass(self):
"""Subscribe to updates."""
self._data.async_subscribe_device_id(
self._device_id, self._update_from_data_and_write_state
)
self._data.activity_stream.async_subscribe_device_id(
self._device_id, self._update_from_data_and_write_state
)
async def async_will_remove_from_hass(self):
"""Undo subscription."""
self._data.async_unsubscribe_device_id(
self._device_id, self._update_from_data_and_write_state
)
self._data.activity_stream.async_unsubscribe_device_id(
self._device_id, self._update_from_data_and_write_state
)

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