Merge pull request #24305 from home-assistant/rc

0.94.0
This commit is contained in:
Pascal Vizeli 2019-06-05 18:35:04 +02:00 committed by GitHub
commit d78e132007
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
640 changed files with 12900 additions and 5161 deletions

View File

@ -47,6 +47,7 @@ omit =
homeassistant/components/august/*
homeassistant/components/automatic/device_tracker.py
homeassistant/components/avion/light.py
homeassistant/components/azure_event_hub/*
homeassistant/components/baidu/tts.py
homeassistant/components/bbb_gpio/*
homeassistant/components/bbox/device_tracker.py
@ -171,6 +172,7 @@ omit =
homeassistant/components/esphome/camera.py
homeassistant/components/esphome/climate.py
homeassistant/components/esphome/cover.py
homeassistant/components/esphome/entry_data.py
homeassistant/components/esphome/fan.py
homeassistant/components/esphome/light.py
homeassistant/components/esphome/sensor.py
@ -250,7 +252,6 @@ omit =
homeassistant/components/hitron_coda/device_tracker.py
homeassistant/components/hive/*
homeassistant/components/hlk_sw16/*
homeassistant/components/homekit_controller/*
homeassistant/components/homematic/*
homeassistant/components/homematic/climate.py
homeassistant/components/homematic/cover.py
@ -344,6 +345,7 @@ omit =
homeassistant/components/mastodon/notify.py
homeassistant/components/matrix/*
homeassistant/components/maxcube/*
homeassistant/components/mcp23017/*
homeassistant/components/media_extractor/*
homeassistant/components/mediaroom/media_player.py
homeassistant/components/message_bird/notify.py
@ -487,6 +489,9 @@ omit =
homeassistant/components/reddit/*
homeassistant/components/rejseplanen/sensor.py
homeassistant/components/remember_the_milk/__init__.py
homeassistant/components/repetier/__init__.py
homeassistant/components/repetier/sensor.py
homeassistant/components/remote_rpi_gpio/*
homeassistant/components/rest/binary_sensor.py
homeassistant/components/rest/notify.py
homeassistant/components/rest/switch.py
@ -539,12 +544,14 @@ omit =
homeassistant/components/slack/notify.py
homeassistant/components/sma/sensor.py
homeassistant/components/smappee/*
homeassistant/components/smarthab/*
homeassistant/components/smtp/notify.py
homeassistant/components/snapcast/media_player.py
homeassistant/components/snmp/*
homeassistant/components/sochain/sensor.py
homeassistant/components/socialblade/sensor.py
homeassistant/components/solaredge/sensor.py
homeassistant/components/solax/sensor.py
homeassistant/components/somfy_mylink/*
homeassistant/components/sonarr/sensor.py
homeassistant/components/songpal/media_player.py
@ -651,6 +658,7 @@ omit =
homeassistant/components/waqi/sensor.py
homeassistant/components/waterfurnace/*
homeassistant/components/watson_iot/*
homeassistant/components/watson_tts/tts.py
homeassistant/components/waze_travel_time/sensor.py
homeassistant/components/webostv/*
homeassistant/components/wemo/*

View File

@ -32,6 +32,7 @@ homeassistant/components/automatic/* @armills
homeassistant/components/automation/* @home-assistant/core
homeassistant/components/aws/* @awarecan @robbiet480
homeassistant/components/axis/* @kane610
homeassistant/components/azure_event_hub/* @eavanvalkenburg
homeassistant/components/bitcoin/* @fabaff
homeassistant/components/bizkaibus/* @UgaitzEtxebarria
homeassistant/components/blink/* @fronzbot
@ -83,7 +84,7 @@ homeassistant/components/flock/* @fabaff
homeassistant/components/flunearyou/* @bachya
homeassistant/components/foursquare/* @robbiet480
homeassistant/components/freebox/* @snoof85
homeassistant/components/frontend/* @home-assistant/core
homeassistant/components/frontend/* @home-assistant/frontend
homeassistant/components/gearbest/* @HerrHofrat
homeassistant/components/geniushub/* @zxdavb
homeassistant/components/gitter/* @fabaff
@ -131,6 +132,7 @@ homeassistant/components/kodi/* @armills
homeassistant/components/konnected/* @heythisisnate
homeassistant/components/lametric/* @robbiet480
homeassistant/components/launch_library/* @ludeeus
homeassistant/components/lcn/* @alengwenus
homeassistant/components/lifx/* @amelchio
homeassistant/components/lifx_cloud/* @amelchio
homeassistant/components/lifx_legacy/* @amelchio
@ -138,11 +140,12 @@ homeassistant/components/linux_battery/* @fabaff
homeassistant/components/liveboxplaytv/* @pschmitt
homeassistant/components/logger/* @home-assistant/core
homeassistant/components/logi_circle/* @evanjd
homeassistant/components/lovelace/* @home-assistant/core
homeassistant/components/lovelace/* @home-assistant/frontend
homeassistant/components/luci/* @fbradyirl
homeassistant/components/luftdaten/* @fabaff
homeassistant/components/mastodon/* @fabaff
homeassistant/components/matrix/* @tinloaf
homeassistant/components/mcp23017/* @jardiamj
homeassistant/components/mediaroom/* @dgomes
homeassistant/components/melissa/* @kennedyshead
homeassistant/components/met/* @danielhiversen
@ -173,8 +176,8 @@ homeassistant/components/openuv/* @bachya
homeassistant/components/openweathermap/* @fabaff
homeassistant/components/orangepi_gpio/* @pascallj
homeassistant/components/owlet/* @oblogic7
homeassistant/components/panel_custom/* @home-assistant/core
homeassistant/components/panel_iframe/* @home-assistant/core
homeassistant/components/panel_custom/* @home-assistant/frontend
homeassistant/components/panel_iframe/* @home-assistant/frontend
homeassistant/components/persistent_notification/* @home-assistant/core
homeassistant/components/philips_js/* @elupus
homeassistant/components/pi_hole/* @fabaff
@ -190,6 +193,7 @@ homeassistant/components/qwikswitch/* @kellerza
homeassistant/components/raincloud/* @vanstinator
homeassistant/components/rainmachine/* @bachya
homeassistant/components/random/* @fabaff
homeassistant/components/repetier/* @MTrab
homeassistant/components/rfxtrx/* @danielhiversen
homeassistant/components/rmvtransport/* @cgtobi
homeassistant/components/roomba/* @pschmitt
@ -205,15 +209,17 @@ homeassistant/components/shiftr/* @fabaff
homeassistant/components/shodan/* @fabaff
homeassistant/components/simplisafe/* @bachya
homeassistant/components/sma/* @kellerza
homeassistant/components/smarthab/* @outadoc
homeassistant/components/smartthings/* @andrewsayre
homeassistant/components/smtp/* @fabaff
homeassistant/components/solax/* @squishykid
homeassistant/components/sonos/* @amelchio
homeassistant/components/spaceapi/* @fabaff
homeassistant/components/spider/* @peternijssen
homeassistant/components/sql/* @dgomes
homeassistant/components/statistics/* @fabaff
homeassistant/components/stiebel_eltron/* @fucm
homeassistant/components/sun/* @home-assistant/core
homeassistant/components/sun/* @Swamp-Ig
homeassistant/components/supla/* @mwegrzynek
homeassistant/components/swiss_hydrological_data/* @fabaff
homeassistant/components/swiss_public_transport/* @fabaff
@ -253,6 +259,7 @@ homeassistant/components/velux/* @Julius2342
homeassistant/components/version/* @fabaff
homeassistant/components/vizio/* @raman325
homeassistant/components/waqi/* @andrey-git
homeassistant/components/watson_tts/* @rutkai
homeassistant/components/weather/* @fabaff
homeassistant/components/weblink/* @home-assistant/core
homeassistant/components/websocket_api/* @home-assistant/core
@ -261,14 +268,14 @@ homeassistant/components/worldclock/* @fabaff
homeassistant/components/xfinity/* @cisasteelersfan
homeassistant/components/xiaomi_aqara/* @danielhiversen @syssi
homeassistant/components/xiaomi_miio/* @rytilahti @syssi
homeassistant/components/xiaomi_tv/* @fattdev
homeassistant/components/xiaomi_tv/* @simse
homeassistant/components/xmpp/* @fabaff @flowolf
homeassistant/components/yamaha_musiccast/* @jalmeroth
homeassistant/components/yeelight/* @rytilahti @zewelor
homeassistant/components/yeelightsunflower/* @lindsaymarkward
homeassistant/components/yessssms/* @flowolf
homeassistant/components/yi/* @bachya
homeassistant/components/zeroconf/* @robbiet480
homeassistant/components/zeroconf/* @robbiet480 @Kane610
homeassistant/components/zha/* @dmulcahey @adminiuga
homeassistant/components/zone/* @home-assistant/core
homeassistant/components/zoneminder/* @rohankapoorcom

View File

@ -1,4 +1,4 @@
Home Assistant |Build Status| |CI Status| |Coverage Status| |Chat Status|
Home Assistant |Chat Status|
=================================================================================
Home Assistant is a home automation platform running on Python 3. It is able to track and control all devices at home and offer a platform for automating control.
@ -27,12 +27,6 @@ components <https://developers.home-assistant.io/docs/en/creating_component_inde
If you run into issues while using Home Assistant or during development
of a component, check the `Home Assistant help section <https://home-assistant.io/help/>`__ of our website for further help and information.
.. |Build Status| image:: https://travis-ci.org/home-assistant/home-assistant.svg?branch=dev
:target: https://travis-ci.org/home-assistant/home-assistant
.. |CI Status| image:: https://circleci.com/gh/home-assistant/home-assistant.svg?style=shield
:target: https://circleci.com/gh/home-assistant/home-assistant
.. |Coverage Status| image:: https://img.shields.io/coveralls/home-assistant/home-assistant.svg
:target: https://coveralls.io/r/home-assistant/home-assistant?branch=master
.. |Chat Status| image:: https://img.shields.io/discord/330944238910963714.svg
:target: https://discord.gg/c5DvZ4e
.. |screenshot-states| image:: https://raw.github.com/home-assistant/home-assistant/master/docs/screenshots.png

View File

@ -2,108 +2,20 @@
trigger:
batch: true
branches:
include:
- dev
- master
tags:
include:
- '*'
pr: none
variables:
- name: versionBuilder
value: '3.2'
- name: versionWheels
value: '0.7'
- group: docker
- group: wheels
- group: github
- group: twine
jobs:
- job: 'Wheels'
condition: or(eq(variables['Build.SourceBranchName'], 'dev'), eq(variables['Build.SourceBranchName'], 'master'))
timeoutInMinutes: 360
pool:
vmImage: 'ubuntu-latest'
strategy:
maxParallel: 3
matrix:
amd64:
buildArch: 'amd64'
i386:
buildArch: 'i386'
armhf:
buildArch: 'armhf'
armv7:
buildArch: 'armv7'
aarch64:
buildArch: 'aarch64'
steps:
- script: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
qemu-user-static \
binfmt-support \
curl
sudo mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc
sudo update-binfmts --enable qemu-arm
sudo update-binfmts --enable qemu-aarch64
displayName: 'Initial cross build'
- script: |
mkdir -p .ssh
echo -e "-----BEGIN RSA PRIVATE KEY-----\n$(wheelsSSH)\n-----END RSA PRIVATE KEY-----" >> .ssh/id_rsa
ssh-keyscan -H $(wheelsHost) >> .ssh/known_hosts
chmod 600 .ssh/*
displayName: 'Install ssh key'
- script: sudo docker pull homeassistant/$(buildArch)-wheels:$(versionWheels)
displayName: 'Install wheels builder'
- script: |
cp requirements_all.txt requirements_wheels.txt
if [ "$(Build.SourceBranchName)" == "dev" ]; then
curl -s -o requirements_diff.txt https://raw.githubusercontent.com/home-assistant/home-assistant/master/requirements_all.txt
else
touch requirements_diff.txt
fi
requirement_files="requirements_wheels.txt requirements_diff.txt"
for requirement_file in ${requirement_files}; do
sed -i "s|# pytradfri|pytradfri|g" ${requirement_file}
sed -i "s|# pybluez|pybluez|g" ${requirement_file}
sed -i "s|# bluepy|bluepy|g" ${requirement_file}
sed -i "s|# beacontools|beacontools|g" ${requirement_file}
sed -i "s|# RPi.GPIO|RPi.GPIO|g" ${requirement_file}
sed -i "s|# raspihats|raspihats|g" ${requirement_file}
sed -i "s|# rpi-rf|rpi-rf|g" ${requirement_file}
sed -i "s|# blinkt|blinkt|g" ${requirement_file}
sed -i "s|# fritzconnection|fritzconnection|g" ${requirement_file}
sed -i "s|# pyuserinput|pyuserinput|g" ${requirement_file}
sed -i "s|# evdev|evdev|g" ${requirement_file}
sed -i "s|# smbus-cffi|smbus-cffi|g" ${requirement_file}
sed -i "s|# i2csense|i2csense|g" ${requirement_file}
sed -i "s|# python-eq3bt|python-eq3bt|g" ${requirement_file}
sed -i "s|# pycups|pycups|g" ${requirement_file}
sed -i "s|# homekit|homekit|g" ${requirement_file}
sed -i "s|# decora_wifi|decora_wifi|g" ${requirement_file}
sed -i "s|# decora|decora|g" ${requirement_file}
sed -i "s|# PySwitchbot|PySwitchbot|g" ${requirement_file}
sed -i "s|# pySwitchmate|pySwitchmate|g" ${requirement_file}
sed -i "s|# face_recognition|face_recognition|g" ${requirement_file}
done
displayName: 'Prepare requirements files for Hass.io'
- script: |
sudo docker run --rm -v $(pwd):/data:ro -v $(pwd)/.ssh:/root/.ssh:rw \
homeassistant/$(buildArch)-wheels:$(versionWheels) \
--apk "build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;linux-headers;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev" \
--index $(wheelsIndex) \
--requirement requirements_wheels.txt \
--requirement-diff requirements_diff.txt \
--upload rsync \
--remote wheels@$(wheelsHost):/opt/wheels
displayName: 'Run wheels build'
- job: 'VersionValidate'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')

View File

@ -94,6 +94,13 @@ async def async_from_config_dict(config: Dict[str, Any],
stop = time()
_LOGGER.info("Home Assistant initialized in %.2fs", stop-start)
if sys.version_info[:3] < (3, 6, 0):
hass.components.persistent_notification.async_create(
"Python 3.5 support is deprecated and will "
"be removed in the first release after August 1. Please "
"upgrade Python.", "Python version", "python_version"
)
# TEMP: warn users for invalid slugs
# Remove after 0.94 or 1.0
if cv.INVALID_SLUGS_FOUND or cv.INVALID_ENTITY_IDS_FOUND:

View File

@ -118,7 +118,7 @@ async def async_setup(hass, config):
tasks = [alert.async_update_ha_state() for alert in entities]
if tasks:
await asyncio.wait(tasks, loop=hass.loop)
await asyncio.wait(tasks)
return True

View File

@ -39,7 +39,7 @@ class Auth:
self._prefs = None
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
self._get_token_lock = asyncio.Lock(loop=hass.loop)
self._get_token_lock = asyncio.Lock()
async def async_do_auth(self, accept_grant_code):
"""Do authentication with an AcceptGrant code."""
@ -97,7 +97,7 @@ class Auth:
try:
session = aiohttp_client.async_get_clientsession(self.hass)
with async_timeout.timeout(DEFAULT_TIMEOUT, loop=self.hass.loop):
with async_timeout.timeout(DEFAULT_TIMEOUT):
response = await session.post(LWA_TOKEN_URI,
headers=LWA_HEADERS,
data=lwa_params,

View File

@ -1432,7 +1432,7 @@ async def async_send_changereport_message(hass, config, alexa_entity):
try:
session = aiohttp_client.async_get_clientsession(hass)
with async_timeout.timeout(DEFAULT_TIMEOUT, loop=hass.loop):
with async_timeout.timeout(DEFAULT_TIMEOUT):
response = await session.post(config.endpoint,
headers=headers,
json=message_serialized,

View File

@ -0,0 +1,23 @@
{
"config": {
"abort": {
"access_token": "Error desconocido al generar un token de acceso.",
"already_setup": "La cuenta de Ambiclimate est\u00e1 configurada.",
"no_config": "Es necesario configurar Ambiclimate antes de poder autenticarse con \u00e9l. [Por favor, lee las instrucciones](https://www.home-assistant.io/components/ambiclimate/)."
},
"create_entry": {
"default": "Autenticado correctamente con Ambiclimate"
},
"error": {
"follow_link": "Accede al enlace e identif\u00edcate antes de pulsar Enviar.",
"no_token": "No autenticado con Ambiclimate"
},
"step": {
"auth": {
"description": "Accede al siguiente [enlace]({authorization_url}) y <b>permite</b> el acceso a tu cuenta de Ambiclimate, despu\u00e9s vuelve y pulsa en <b>enviar</b> a continuaci\u00f3n.\n(Aseg\u00farate que la url de devoluci\u00f3n de llamada es {cb_url})",
"title": "Autenticaci\u00f3n de Ambiclimate"
}
},
"title": "Ambiclimate"
}
}

View File

@ -0,0 +1,23 @@
{
"config": {
"abort": {
"access_token": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'un jeton d'acc\u00e8s.",
"already_setup": "Le compte Ambiclimate est configur\u00e9.",
"no_config": "Vous devez configurer Ambiclimate avant de pouvoir vous authentifier aupr\u00e8s de celui-ci. [Veuillez lire les instructions] (https://www.home-assistant.io/components/ambiclimate/)."
},
"create_entry": {
"default": "Authentifi\u00e9 avec succ\u00e8s avec Ambiclimate"
},
"error": {
"follow_link": "Veuillez suivre le lien et vous authentifier avant d'appuyer sur Soumettre.",
"no_token": "Non authentifi\u00e9 avec Ambiclimate"
},
"step": {
"auth": {
"description": "Suivez ce [lien] ( {authorization_url} ) et <b> Autorisez </b> l'acc\u00e8s \u00e0 votre compte Ambiclimate, puis revenez et appuyez sur <b> Envoyer </b> ci-dessous. \n (Assurez-vous que l'URL de rappel sp\u00e9cifi\u00e9 est {cb_url} )",
"title": "Authentifier Ambiclimate"
}
},
"title": "Ambiclimate"
}
}

View File

@ -0,0 +1,23 @@
{
"config": {
"abort": {
"access_token": "Ukjent feil ved oppretting av tilgangstoken.",
"already_setup": "Ambiclimate-kontoen er konfigurert.",
"no_config": "Du m\u00e5 konfigurere Ambiclimate f\u00f8r du kan autentisere med den. [Vennligst les instruksjonene](https://www.home-assistant.io/components/ambiclimate/)."
},
"create_entry": {
"default": "Vellykket autentisering med Ambiclimate"
},
"error": {
"follow_link": "Vennligst f\u00f8lg lenken og godkjen f\u00f8r du trykker p\u00e5 Send",
"no_token": "Ikke autentisert med Ambiclimate"
},
"step": {
"auth": {
"description": "Vennligst f\u00f8lg denne [linken]({authorization_url}) og <b>Tillat</b> tilgang til din Ambiclimate konto, og kom s\u00e5 tilbake og trykk <b>Send</b> nedenfor.\n(Kontroller at den angitte URL-adressen for tilbakeringing er {cb_url})",
"title": "Autensiere Ambiclimate"
}
},
"title": "Ambiclimate"
}
}

View File

@ -0,0 +1,23 @@
{
"config": {
"abort": {
"access_token": "Nieznany b\u0142\u0105d podczas generowania tokena dost\u0119pu.",
"already_setup": "Konto Ambiclimate jest skonfigurowane.",
"no_config": "Musisz skonfigurowa\u0107 Ambiclimate, zanim b\u0119dziesz m\u00f3g\u0142 si\u0119 z nim uwierzytelni\u0107. [Przeczytaj instrukcj\u0119](https://www.home-assistant.io/components/ambiclimate/)."
},
"create_entry": {
"default": "Pomy\u015blnie uwierzytelniono z Ambiclimate"
},
"error": {
"follow_link": "Prosz\u0119 klikn\u0105\u0107 link i uwierzytelni\u0107 przed naci\u015bni\u0119ciem przycisku Prze\u015blij",
"no_token": "Nie uwierzytelniony z Ambiclimate"
},
"step": {
"auth": {
"description": "Kliknij poni\u017cszy [link]({authorization_url}) i <b>Zezw\u00f3l</b> na dost\u0119p do swojego konta Ambiclimate, a nast\u0119pnie wr\u00f3\u0107 i naci\u015bnij <b>Prze\u015blij</b> poni\u017cej. \n(Upewnij si\u0119, \u017ce podany adres URL to {cb_url})",
"title": "Uwierzytelnienie Ambiclimate"
}
},
"title": "Ambiclimate"
}
}

View File

@ -0,0 +1,23 @@
{
"config": {
"abort": {
"access_token": "Neznana napaka pri ustvarjanju \u017eetona za dostop.",
"already_setup": "Ra\u010dun Ambiclimate je konfiguriran.",
"no_config": "Ambiclimat morate konfigurirati, preden lahko z njo preverjate pristnost. [Preberite navodila] (https://www.home-assistant.io/components/ambiclimate/)."
},
"create_entry": {
"default": "Uspe\u0161no overjeno z funkcijo Ambiclimate"
},
"error": {
"follow_link": "Preden pritisnete Po\u0161lji, sledite povezavi in preverite pristnost",
"no_token": "Ni overjeno z Ambiclimate"
},
"step": {
"auth": {
"description": "Sledite temu povezavi ( {authorization_url} in <b> Dovoli </b> dostopu do svojega ra\u010duna Ambiclimate, nato se vrnite in pritisnite <b> Po\u0161lji </b> spodaj. \n (Poskrbite, da je dolo\u010den url za povratni klic {cb_url} )",
"title": "Overi Ambiclimate"
}
},
"title": "Ambiclimate"
}
}

View File

@ -0,0 +1,23 @@
{
"config": {
"abort": {
"access_token": "Ok\u00e4nt fel vid generering av \u00e5tkomsttoken.",
"already_setup": "Ambiclientkontot \u00e4r konfigurerat",
"no_config": "Du m\u00e5ste konfigurera Ambiclimate innan du kan autentisera med den. [V\u00e4nligen l\u00e4s instruktionerna] (https://www.home-assistant.io/components/ambiclimate/)."
},
"create_entry": {
"default": "Lyckad autentisering med Ambiclimate"
},
"error": {
"follow_link": "V\u00e4nligen f\u00f6lj l\u00e4nken och autentisera dig innan du trycker p\u00e5 Skicka",
"no_token": "Inte autentiserad med Ambiclimate"
},
"step": {
"auth": {
"description": "V\u00e4nligen f\u00f6lj denna [l\u00e4nk] ({authorization_url}) och <b> till\u00e5ta </b> till g\u00e5ng till ditt Ambiclimate konto, kom sedan tillbaka och tryck p\u00e5 <b> Skicka </b> nedan.\n(Kontrollera att den angivna callback url \u00e4r {cb_url})",
"title": "Autentisera Ambiclimate"
}
},
"title": "Ambiclimate"
}
}

View File

@ -62,7 +62,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
return
if _token_info:
await store.async_save(token_info)
await store.async_save(_token_info)
token_info = _token_info
data_connection = ambiclimate.AmbiclimateConnection(oauth,

View File

@ -1,9 +1,10 @@
{
"domain": "ambiclimate",
"name": "Ambiclimate",
"config_flow": true,
"documentation": "https://www.home-assistant.io/components/ambiclimate",
"requirements": [
"ambiclimate==0.1.1"
"ambiclimate==0.1.2"
],
"dependencies": [],
"codeowners": [

View File

@ -1,6 +1,7 @@
{
"domain": "ambient_station",
"name": "Ambient station",
"config_flow": true,
"documentation": "https://www.home-assistant.io/components/ambient_station",
"requirements": [
"aioambient==0.3.0"

View File

@ -203,8 +203,7 @@ class AmcrestCam(Camera):
"""Return the camera model."""
return self._model
@property
def stream_source(self):
async def stream_source(self):
"""Return the source of the stream."""
return self._api.rtsp_url(typeno=self._resolution)

View File

@ -233,7 +233,7 @@ async def async_setup(hass, config):
tasks = [async_setup_ipcamera(conf) for conf in config[DOMAIN]]
if tasks:
await asyncio.wait(tasks, loop=hass.loop)
await asyncio.wait(tasks)
return True

View File

@ -90,20 +90,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
if CONF_ADB_SERVER_IP not in config:
# Use "python-adb" (Python ADB implementation)
adb_log = "using Python ADB implementation "
if CONF_ADBKEY in config:
aftv = setup(host, config[CONF_ADBKEY],
device_class=config[CONF_DEVICE_CLASS])
adb_log = " using adbkey='{0}'".format(config[CONF_ADBKEY])
adb_log += "with adbkey='{0}'".format(config[CONF_ADBKEY])
else:
aftv = setup(host, device_class=config[CONF_DEVICE_CLASS])
adb_log = ""
adb_log += "without adbkey authentication"
else:
# Use "pure-python-adb" (communicate with ADB server)
aftv = setup(host, adb_server_ip=config[CONF_ADB_SERVER_IP],
adb_server_port=config[CONF_ADB_SERVER_PORT],
device_class=config[CONF_DEVICE_CLASS])
adb_log = " using ADB server at {0}:{1}".format(
adb_log = "using ADB server at {0}:{1}".format(
config[CONF_ADB_SERVER_IP], config[CONF_ADB_SERVER_PORT])
if not aftv.available:
@ -117,7 +118,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
else:
device_name = 'Android TV / Fire TV device'
_LOGGER.warning("Could not connect to %s at %s%s",
_LOGGER.warning("Could not connect to %s at %s %s",
device_name, host, adb_log)
raise PlatformNotReady
@ -156,10 +157,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
for target_device in target_devices:
output = target_device.adb_command(cmd)
# log the output if there is any
if output and (not isinstance(output, str) or output.strip()):
# log the output, if there is any
if output:
_LOGGER.info("Output of command '%s' from '%s': %s",
cmd, target_device.entity_id, repr(output))
cmd, target_device.entity_id, output)
hass.services.register(ANDROIDTV_DOMAIN, SERVICE_ADB_COMMAND,
service_adb_command,
@ -224,6 +225,7 @@ class ADBDevice(MediaPlayerDevice):
self.exceptions = (ConnectionResetError, RuntimeError)
# Property attributes
self._adb_response = None
self._available = self.aftv.available
self._current_app = None
self._state = None
@ -243,6 +245,11 @@ class ADBDevice(MediaPlayerDevice):
"""Return whether or not the ADB connection is valid."""
return self._available
@property
def device_state_attributes(self):
"""Provide the last ADB command's response as an attribute."""
return {'adb_response': self._adb_response}
@property
def name(self):
"""Return the device name."""
@ -304,12 +311,24 @@ class ADBDevice(MediaPlayerDevice):
"""Send an ADB command to an Android TV / Fire TV device."""
key = self._keys.get(cmd)
if key:
return self.aftv.adb_shell('input keyevent {}'.format(key))
self.aftv.adb_shell('input keyevent {}'.format(key))
self._adb_response = None
self.schedule_update_ha_state()
return
if cmd == 'GET_PROPERTIES':
return self.aftv.get_properties_dict()
self._adb_response = str(self.aftv.get_properties_dict())
self.schedule_update_ha_state()
return self._adb_response
return self.aftv.adb_shell(cmd)
response = self.aftv.adb_shell(cmd)
if isinstance(response, str) and response.strip():
self._adb_response = response.strip()
else:
self._adb_response = None
self.schedule_update_ha_state()
return self._adb_response
class AndroidTVDevice(ADBDevice):

View File

@ -47,7 +47,7 @@ async def async_setup_platform(hass, config, async_add_entities,
hass.async_create_task(device.async_update_ha_state())
avr = await anthemav.Connection.create(
host=host, port=port, loop=hass.loop,
host=host, port=port,
update_callback=async_anthemav_update_callback)
device = AnthemAVR(avr, name)

View File

@ -82,7 +82,7 @@ class APIEventStream(HomeAssistantView):
raise Unauthorized()
hass = request.app['hass']
stop_obj = object()
to_write = asyncio.Queue(loop=hass.loop)
to_write = asyncio.Queue()
restrict = request.query.get('restrict')
if restrict:
@ -119,8 +119,7 @@ class APIEventStream(HomeAssistantView):
while True:
try:
with async_timeout.timeout(STREAM_PING_INTERVAL,
loop=hass.loop):
with async_timeout.timeout(STREAM_PING_INTERVAL):
payload = await to_write.get()
if payload is stop_obj:

View File

@ -1,6 +1,5 @@
"""APNS Notification platform."""
import logging
import os
import voluptuous as vol
@ -149,7 +148,8 @@ class ApnsNotificationService(BaseNotificationService):
self.devices = {}
self.device_states = {}
self.topic = topic
if os.path.isfile(self.yaml_path):
try:
self.devices = {
str(key): ApnsDevice(
str(key),
@ -160,6 +160,8 @@ class ApnsNotificationService(BaseNotificationService):
for (key, value) in
load_yaml_config_file(self.yaml_path).items()
}
except FileNotFoundError:
pass
tracking_ids = [
device.full_tracking_device_id

View File

@ -167,7 +167,7 @@ async def async_setup(hass, config):
tasks = [_setup_atv(hass, config, conf) for conf in config.get(DOMAIN, [])]
if tasks:
await asyncio.wait(tasks, loop=hass.loop)
await asyncio.wait(tasks)
hass.services.async_register(
DOMAIN, SERVICE_SCAN, async_service_handler,

View File

@ -124,7 +124,7 @@ async def async_setup(hass, config):
context=service_call.context))
if tasks:
await asyncio.wait(tasks, loop=hass.loop)
await asyncio.wait(tasks)
async def turn_onoff_service_handler(service_call):
"""Handle automation turn on/off service calls."""
@ -134,7 +134,7 @@ async def async_setup(hass, config):
tasks.append(getattr(entity, method)())
if tasks:
await asyncio.wait(tasks, loop=hass.loop)
await asyncio.wait(tasks)
async def toggle_service_handler(service_call):
"""Handle automation toggle service calls."""
@ -146,7 +146,7 @@ async def async_setup(hass, config):
tasks.append(entity.async_turn_on())
if tasks:
await asyncio.wait(tasks, loop=hass.loop)
await asyncio.wait(tasks)
async def reload_service_handler(service_call):
"""Remove all automations and load new ones from config."""

View File

@ -166,14 +166,14 @@ async def _validate_aws_credentials(hass, credential):
profile = aws_config.get(CONF_PROFILE_NAME)
if profile is not None:
session = aiobotocore.AioSession(profile=profile, loop=hass.loop)
session = aiobotocore.AioSession(profile=profile)
del aws_config[CONF_PROFILE_NAME]
if CONF_ACCESS_KEY_ID in aws_config:
del aws_config[CONF_ACCESS_KEY_ID]
if CONF_SECRET_ACCESS_KEY in aws_config:
del aws_config[CONF_SECRET_ACCESS_KEY]
else:
session = aiobotocore.AioSession(loop=hass.loop)
session = aiobotocore.AioSession()
if credential[CONF_VALIDATE]:
async with session.create_client("iam", **aws_config) as client:

View File

@ -94,10 +94,10 @@ async def async_get_service(hass, config, discovery_info=None):
if session is None:
profile = aws_config.get(CONF_PROFILE_NAME)
if profile is not None:
session = aiobotocore.AioSession(profile=profile, loop=hass.loop)
session = aiobotocore.AioSession(profile=profile)
del aws_config[CONF_PROFILE_NAME]
else:
session = aiobotocore.AioSession(loop=hass.loop)
session = aiobotocore.AioSession()
aws_config[CONF_REGION] = region_name

View File

@ -0,0 +1,18 @@
{
"config": {
"error": {
"device_unavailable": "Apparaat is niet beschikbaar",
"faulty_credentials": "Ongeldige gebruikersreferenties"
},
"step": {
"user": {
"data": {
"host": "Host",
"password": "Wachtwoord",
"port": "Poort",
"username": "Gebruikersnaam"
}
}
}
}
}

View File

@ -2,7 +2,8 @@
"config": {
"abort": {
"already_configured": "Enheten \u00e4r redan konfigurerad",
"bad_config_file": "Felaktig data fr\u00e5n config fil"
"bad_config_file": "Felaktig data fr\u00e5n config fil",
"link_local_address": "Link local addresses are not supported"
},
"error": {
"already_configured": "Enheten \u00e4r redan konfigurerad",
@ -17,7 +18,7 @@
"port": "Port",
"username": "Anv\u00e4ndarnamn"
},
"title": "Konfigurera Axis enhet"
"title": "Konfigurera Axis-enhet"
}
},
"title": "Axis enhet"

View File

@ -0,0 +1,86 @@
"""Base classes for Axis entities."""
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from .const import DOMAIN as AXIS_DOMAIN
class AxisEntityBase(Entity):
"""Base common to all Axis entities."""
def __init__(self, device):
"""Initialize the Axis event."""
self.device = device
self.unsub_dispatcher = []
async def async_added_to_hass(self):
"""Subscribe device events."""
self.unsub_dispatcher.append(async_dispatcher_connect(
self.hass, self.device.event_reachable, self.update_callback))
async def async_will_remove_from_hass(self) -> None:
"""Unsubscribe device events when removed."""
for unsub_dispatcher in self.unsub_dispatcher:
unsub_dispatcher()
@property
def available(self):
"""Return True if device is available."""
return self.device.available
@property
def device_info(self):
"""Return a device description for device registry."""
return {
'identifiers': {(AXIS_DOMAIN, self.device.serial)}
}
@callback
def update_callback(self, no_delay=None):
"""Update the entities state."""
self.async_schedule_update_ha_state()
class AxisEventBase(AxisEntityBase):
"""Base common to all Axis entities from event stream."""
def __init__(self, event, device):
"""Initialize the Axis event."""
super().__init__(device)
self.event = event
async def async_added_to_hass(self) -> None:
"""Subscribe sensors events."""
self.event.register_callback(self.update_callback)
await super().async_added_to_hass()
async def async_will_remove_from_hass(self) -> None:
"""Disconnect device object when removed."""
self.event.remove_callback(self.update_callback)
await super().async_will_remove_from_hass()
@property
def device_class(self):
"""Return the class of the event."""
return self.event.CLASS
@property
def name(self):
"""Return the name of the event."""
return '{} {} {}'.format(
self.device.name, self.event.TYPE, self.event.id)
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def unique_id(self):
"""Return a unique identifier for this device."""
return '{}-{}-{}'.format(
self.device.serial, self.event.topic, self.event.id)

View File

@ -2,6 +2,8 @@
from datetime import timedelta
from axis.event_stream import CLASS_INPUT, CLASS_OUTPUT
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.const import CONF_MAC, CONF_TRIGGER_TIME
from homeassistant.core import callback
@ -9,7 +11,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util.dt import utcnow
from .const import DOMAIN as AXIS_DOMAIN, LOGGER
from .axis_base import AxisEventBase
from .const import DOMAIN as AXIS_DOMAIN
async def async_setup_entry(hass, config_entry, async_add_entities):
@ -21,32 +24,21 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
def async_add_sensor(event_id):
"""Add binary sensor from Axis device."""
event = device.api.event.events[event_id]
async_add_entities([AxisBinarySensor(event, device)], True)
if event.CLASS != CLASS_OUTPUT:
async_add_entities([AxisBinarySensor(event, device)], True)
device.listeners.append(async_dispatcher_connect(
hass, device.event_new_sensor, async_add_sensor))
class AxisBinarySensor(BinarySensorDevice):
class AxisBinarySensor(AxisEventBase, BinarySensorDevice):
"""Representation of a binary Axis event."""
def __init__(self, event, device):
"""Initialize the Axis binary sensor."""
self.event = event
self.device = device
super().__init__(event, device)
self.remove_timer = None
self.unsub_dispatcher = None
async def async_added_to_hass(self):
"""Subscribe sensors events."""
self.event.register_callback(self.update_callback)
self.unsub_dispatcher = async_dispatcher_connect(
self.hass, self.device.event_reachable, self.update_callback)
async def async_will_remove_from_hass(self) -> None:
"""Disconnect device object when removed."""
self.event.remove_callback(self.update_callback)
self.unsub_dispatcher()
@callback
def update_callback(self, no_delay=False):
@ -67,7 +59,6 @@ class AxisBinarySensor(BinarySensorDevice):
@callback
def _delay_update(now):
"""Timer callback for sensor update."""
LOGGER.debug("%s called delayed (%s sec) update", self.name, delay)
self.async_schedule_update_ha_state()
self.remove_timer = None
@ -83,32 +74,10 @@ class AxisBinarySensor(BinarySensorDevice):
@property
def name(self):
"""Return the name of the event."""
return '{} {} {}'.format(
self.device.name, self.event.TYPE, self.event.id)
if self.event.CLASS == CLASS_INPUT and self.event.id and \
self.device.api.vapix.ports[self.event.id].name:
return '{} {}'.format(
self.device.name,
self.device.api.vapix.ports[self.event.id].name)
@property
def device_class(self):
"""Return the class of the event."""
return self.event.CLASS
@property
def unique_id(self):
"""Return a unique identifier for this device."""
return '{}-{}-{}'.format(
self.device.serial, self.event.topic, self.event.id)
def available(self):
"""Return True if device is available."""
return self.device.available
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def device_info(self):
"""Return a device description for device registry."""
return {
'identifiers': {(AXIS_DOMAIN, self.device.serial)}
}
return super().name

View File

@ -6,9 +6,9 @@ from homeassistant.components.mjpeg.camera import (
from homeassistant.const import (
CONF_AUTHENTICATION, CONF_DEVICE, CONF_HOST, CONF_MAC, CONF_NAME,
CONF_PASSWORD, CONF_PORT, CONF_USERNAME, HTTP_DIGEST_AUTHENTICATION)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .axis_base import AxisEntityBase
from .const import DOMAIN as AXIS_DOMAIN
AXIS_IMAGE = 'http://{}:{}/axis-cgi/jpg/image.cgi'
@ -38,65 +38,40 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities([AxisCamera(config, device)])
class AxisCamera(MjpegCamera):
class AxisCamera(AxisEntityBase, MjpegCamera):
"""Representation of a Axis camera."""
def __init__(self, config, device):
"""Initialize Axis Communications camera component."""
super().__init__(config)
self.device_config = config
self.device = device
self.port = device.config_entry.data[CONF_DEVICE][CONF_PORT]
self.unsub_dispatcher = []
AxisEntityBase.__init__(self, device)
MjpegCamera.__init__(self, config)
async def async_added_to_hass(self):
"""Subscribe camera events."""
self.unsub_dispatcher.append(async_dispatcher_connect(
self.hass, self.device.event_new_address, self._new_address))
self.unsub_dispatcher.append(async_dispatcher_connect(
self.hass, self.device.event_reachable, self.update_callback))
async def async_will_remove_from_hass(self) -> None:
"""Disconnect device object when removed."""
for unsub_dispatcher in self.unsub_dispatcher:
unsub_dispatcher()
await super().async_added_to_hass()
@property
def supported_features(self):
"""Return supported features."""
return SUPPORT_STREAM
@property
def stream_source(self):
async def stream_source(self):
"""Return the stream source."""
return AXIS_STREAM.format(
self.device.config_entry.data[CONF_DEVICE][CONF_USERNAME],
self.device.config_entry.data[CONF_DEVICE][CONF_PASSWORD],
self.device.host)
@callback
def update_callback(self, no_delay=None):
"""Update the cameras state."""
self.async_schedule_update_ha_state()
@property
def available(self):
"""Return True if device is available."""
return self.device.available
def _new_address(self):
"""Set new device address for video stream."""
self._mjpeg_url = AXIS_VIDEO.format(self.device.host, self.port)
self._still_image_url = AXIS_IMAGE.format(self.device.host, self.port)
port = self.device.config_entry.data[CONF_DEVICE][CONF_PORT]
self._mjpeg_url = AXIS_VIDEO.format(self.device.host, port)
self._still_image_url = AXIS_IMAGE.format(self.device.host, port)
@property
def unique_id(self):
"""Return a unique identifier for this device."""
return '{}-camera'.format(self.device.serial)
@property
def device_info(self):
"""Return a device description for device registry."""
return {
'identifiers': {(AXIS_DOMAIN, self.device.serial)}
}

View File

@ -146,7 +146,7 @@ class AxisFlowHandler(config_entries.ConfigFlow):
entry.data[CONF_DEVICE][CONF_HOST] = host
self.hass.config_entries.async_update_entry(entry)
async def async_step_discovery(self, discovery_info):
async def async_step_zeroconf(self, discovery_info):
"""Prepare configuration for a discovered Axis device.
This flow is triggered by the discovery component.
@ -155,6 +155,13 @@ class AxisFlowHandler(config_entries.ConfigFlow):
return self.async_abort(reason='link_local_address')
serialnumber = discovery_info['properties']['macaddress']
# pylint: disable=unsupported-assignment-operation
self.context['macaddress'] = serialnumber
if any(serialnumber == flow['context']['macaddress']
for flow in self._async_in_progress()):
return self.async_abort(reason='already_in_progress')
device_entries = configured_devices(self.hass)
if serialnumber in device_entries:

View File

@ -83,19 +83,23 @@ class AxisNetworkDevice:
self.product_type = self.api.vapix.params.prodtype
if self.config_entry.options[CONF_CAMERA]:
self.hass.async_create_task(
self.hass.config_entries.async_forward_entry_setup(
self.config_entry, 'camera'))
if self.config_entry.options[CONF_EVENTS]:
task = self.hass.async_create_task(
self.hass.config_entries.async_forward_entry_setup(
self.config_entry, 'binary_sensor'))
self.api.stream.connection_status_callback = \
self.async_connection_status_callback
self.api.enable_events(event_callback=self.async_event_callback)
task.add_done_callback(self.start)
platform_tasks = [
self.hass.config_entries.async_forward_entry_setup(
self.config_entry, platform)
for platform in ['binary_sensor', 'switch']
]
self.hass.async_create_task(self.start(platform_tasks))
self.config_entry.add_update_listener(self.async_new_address_callback)
@ -145,9 +149,9 @@ class AxisNetworkDevice:
if action == 'add':
async_dispatcher_send(self.hass, self.event_new_sensor, event_id)
@callback
def start(self, fut):
"""Start the event stream."""
async def start(self, platform_tasks):
"""Start the event stream when all platforms are loaded."""
await asyncio.gather(*platform_tasks)
self.api.start()
@callback
@ -157,15 +161,22 @@ class AxisNetworkDevice:
async def async_reset(self):
"""Reset this device to default state."""
self.api.stop()
platform_tasks = []
if self.config_entry.options[CONF_CAMERA]:
await self.hass.config_entries.async_forward_entry_unload(
self.config_entry, 'camera')
platform_tasks.append(
self.hass.config_entries.async_forward_entry_unload(
self.config_entry, 'camera'))
if self.config_entry.options[CONF_EVENTS]:
await self.hass.config_entries.async_forward_entry_unload(
self.config_entry, 'binary_sensor')
self.api.stop()
platform_tasks += [
self.hass.config_entries.async_forward_entry_unload(
self.config_entry, platform)
for platform in ['binary_sensor', 'switch']
]
await asyncio.gather(*platform_tasks)
for unsub_dispatcher in self.listeners:
unsub_dispatcher()
@ -185,13 +196,22 @@ async def get_device(hass, config):
port=config[CONF_PORT], web_proto='http')
device.vapix.initialize_params(preload_data=False)
device.vapix.initialize_ports()
try:
with async_timeout.timeout(15):
await hass.async_add_executor_job(
device.vapix.params.update_brand)
await hass.async_add_executor_job(
device.vapix.params.update_properties)
await asyncio.gather(
hass.async_add_executor_job(
device.vapix.params.update_brand),
hass.async_add_executor_job(
device.vapix.params.update_properties),
hass.async_add_executor_job(
device.vapix.ports.update)
)
return device
except axis.Unauthorized:

View File

@ -1,8 +1,10 @@
{
"domain": "axis",
"name": "Axis",
"config_flow": true,
"documentation": "https://www.home-assistant.io/components/axis",
"requirements": ["axis==22"],
"requirements": ["axis==24"],
"dependencies": [],
"zeroconf": ["_axis-video._tcp.local."],
"codeowners": ["@kane610"]
}

View File

@ -14,6 +14,7 @@
},
"error": {
"already_configured": "Device is already configured",
"already_in_progress": "Config flow for device is already in progress.",
"device_unavailable": "Device is not available",
"faulty_credentials": "Bad user credentials"
},

View File

@ -0,0 +1,59 @@
"""Support for Axis switches."""
from axis.event_stream import CLASS_OUTPUT
from homeassistant.components.switch import SwitchDevice
from homeassistant.const import CONF_MAC
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .axis_base import AxisEventBase
from .const import DOMAIN as AXIS_DOMAIN
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up a Axis switch."""
serial_number = config_entry.data[CONF_MAC]
device = hass.data[AXIS_DOMAIN][serial_number]
@callback
def async_add_switch(event_id):
"""Add switch from Axis device."""
event = device.api.event.events[event_id]
if event.CLASS == CLASS_OUTPUT:
async_add_entities([AxisSwitch(event, device)], True)
device.listeners.append(async_dispatcher_connect(
hass, device.event_new_sensor, async_add_switch))
class AxisSwitch(AxisEventBase, SwitchDevice):
"""Representation of a Axis switch."""
@property
def is_on(self):
"""Return true if event is active."""
return self.event.is_tripped
async def async_turn_on(self, **kwargs):
"""Turn on switch."""
action = '/'
await self.hass.async_add_executor_job(
self.device.api.vapix.ports[self.event.id].action, action)
async def async_turn_off(self, **kwargs):
"""Turn off switch."""
action = '\\'
await self.hass.async_add_executor_job(
self.device.api.vapix.ports[self.event.id].action, action)
@property
def name(self):
"""Return the name of the event."""
if self.event.id and self.device.api.vapix.ports[self.event.id].name:
return '{} {}'.format(
self.device.name,
self.device.api.vapix.ports[self.event.id].name)
return super().name

View File

@ -0,0 +1,80 @@
"""Support for Azure Event Hubs."""
import json
import logging
from typing import Any, Dict
import voluptuous as vol
from azure.eventhub import EventData, EventHubClientAsync
from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, STATE_UNAVAILABLE,
STATE_UNKNOWN)
from homeassistant.core import Event, HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entityfilter import FILTER_SCHEMA
from homeassistant.helpers.json import JSONEncoder
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'azure_event_hub'
CONF_EVENT_HUB_NAMESPACE = 'event_hub_namespace'
CONF_EVENT_HUB_INSTANCE_NAME = 'event_hub_instance_name'
CONF_EVENT_HUB_SAS_POLICY = 'event_hub_sas_policy'
CONF_EVENT_HUB_SAS_KEY = 'event_hub_sas_key'
CONF_FILTER = 'filter'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_EVENT_HUB_NAMESPACE): cv.string,
vol.Required(CONF_EVENT_HUB_INSTANCE_NAME): cv.string,
vol.Required(CONF_EVENT_HUB_SAS_POLICY): cv.string,
vol.Required(CONF_EVENT_HUB_SAS_KEY): cv.string,
vol.Required(CONF_FILTER): FILTER_SCHEMA,
}),
}, extra=vol.ALLOW_EXTRA)
async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]):
"""Activate Azure EH component."""
config = yaml_config[DOMAIN]
event_hub_address = "amqps://{}.servicebus.windows.net/{}".format(
config[CONF_EVENT_HUB_NAMESPACE],
config[CONF_EVENT_HUB_INSTANCE_NAME])
entities_filter = config[CONF_FILTER]
client = EventHubClientAsync(
event_hub_address,
debug=True,
username=config[CONF_EVENT_HUB_SAS_POLICY],
password=config[CONF_EVENT_HUB_SAS_KEY])
async_sender = client.add_async_sender()
await client.run_async()
encoder = JSONEncoder()
async def async_send_to_event_hub(event: Event):
"""Send states to Event Hub."""
state = event.data.get('new_state')
if (state is None
or state.state in (STATE_UNKNOWN, '', STATE_UNAVAILABLE)
or not entities_filter(state.entity_id)):
return
event_data = EventData(
json.dumps(
obj=state.as_dict(),
default=encoder.encode
).encode('utf-8')
)
await async_sender.send(event_data)
async def async_shutdown(event: Event):
"""Shut down the client."""
await client.stop_async()
hass.bus.async_listen(EVENT_STATE_CHANGED, async_send_to_event_hub)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_shutdown)
return True

View File

@ -0,0 +1,8 @@
{
"domain": "azure_event_hub",
"name": "Azure Event Hub",
"documentation": "https://www.home-assistant.io/components/azure_event_hub",
"requirements": ["azure-eventhub==1.3.1"],
"dependencies": [],
"codeowners": ["@eavanvalkenburg"]
}

View File

@ -8,7 +8,7 @@ from homeassistant.helpers import (
from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD, CONF_NAME, CONF_SCAN_INTERVAL,
CONF_BINARY_SENSORS, CONF_SENSORS, CONF_FILENAME,
CONF_MONITORED_CONDITIONS, TEMP_FAHRENHEIT)
CONF_MONITORED_CONDITIONS, CONF_MODE, CONF_OFFSET, TEMP_FAHRENHEIT)
_LOGGER = logging.getLogger(__name__)
@ -41,7 +41,7 @@ BINARY_SENSORS = {
SENSORS = {
TYPE_TEMPERATURE: ['Temperature', TEMP_FAHRENHEIT, 'mdi:thermometer'],
TYPE_BATTERY: ['Battery', '%', 'mdi:battery-80'],
TYPE_BATTERY: ['Battery', '', 'mdi:battery-80'],
TYPE_WIFI_STRENGTH: ['Wifi Signal', 'dBm', 'mdi:wifi-strength-2'],
}
@ -75,6 +75,8 @@ CONFIG_SCHEMA = vol.Schema(
vol.Optional(CONF_BINARY_SENSORS, default={}):
BINARY_SENSOR_SCHEMA,
vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA,
vol.Optional(CONF_OFFSET, default=1): int,
vol.Optional(CONF_MODE, default=''): cv.string,
})
},
extra=vol.ALLOW_EXTRA)
@ -87,8 +89,12 @@ def setup(hass, config):
username = conf[CONF_USERNAME]
password = conf[CONF_PASSWORD]
scan_interval = conf[CONF_SCAN_INTERVAL]
is_legacy = bool(conf[CONF_MODE] == 'legacy')
motion_interval = conf[CONF_OFFSET]
hass.data[BLINK_DATA] = blinkpy.Blink(username=username,
password=password)
password=password,
motion_interval=motion_interval,
legacy_subdomain=is_legacy)
hass.data[BLINK_DATA].refresh_rate = scan_interval.total_seconds()
hass.data[BLINK_DATA].start()

View File

@ -3,7 +3,7 @@
"name": "Blink",
"documentation": "https://www.home-assistant.io/components/blink",
"requirements": [
"blinkpy==0.13.1"
"blinkpy==0.14.0"
],
"dependencies": [],
"codeowners": [

View File

@ -255,7 +255,7 @@ class BluesoundPlayer(MediaPlayerDevice):
BluesoundPlayer._TimeoutException):
_LOGGER.info("Node %s is offline, retrying later", self._name)
await asyncio.sleep(
NODE_OFFLINE_CHECK_TIMEOUT, loop=self._hass.loop)
NODE_OFFLINE_CHECK_TIMEOUT)
self.start_polling()
except CancelledError:
@ -318,7 +318,7 @@ class BluesoundPlayer(MediaPlayerDevice):
try:
websession = async_get_clientsession(self._hass)
with async_timeout.timeout(10, loop=self._hass.loop):
with async_timeout.timeout(10):
response = await websession.get(url)
if response.status == 200:
@ -361,7 +361,7 @@ class BluesoundPlayer(MediaPlayerDevice):
try:
with async_timeout.timeout(125, loop=self._hass.loop):
with async_timeout.timeout(125):
response = await self._polling_session.get(
url, headers={CONNECTION: KEEP_ALIVE})
@ -378,7 +378,7 @@ class BluesoundPlayer(MediaPlayerDevice):
self._group_name = group_name
# the sleep is needed to make sure that the
# devices is synced
await asyncio.sleep(1, loop=self._hass.loop)
await asyncio.sleep(1)
await self.async_trigger_sync_on_all()
elif self.is_grouped:
# when player is grouped we need to fetch volume from

View File

@ -2,12 +2,15 @@
import logging
from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.components.device_tracker import (
YAML_DEVICES, CONF_TRACK_NEW, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL,
load_config, SOURCE_TYPE_BLUETOOTH_LE
from homeassistant.components.device_tracker.legacy import (
YAML_DEVICES, async_load_config
)
from homeassistant.components.device_tracker.const import (
CONF_TRACK_NEW, CONF_SCAN_INTERVAL, SCAN_INTERVAL, SOURCE_TYPE_BLUETOOTH_LE
)
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
import homeassistant.util.dt as dt_util
from homeassistant.util.async_ import run_coroutine_threadsafe
_LOGGER = logging.getLogger(__name__)
@ -79,7 +82,10 @@ def setup_scanner(hass, config, see, discovery_info=None):
# Load all known devices.
# We just need the devices so set consider_home and home range
# to 0
for device in load_config(yaml_path, hass, 0):
for device in run_coroutine_threadsafe(
async_load_config(yaml_path, hass, 0),
hass.loop
).result():
# check if device is a valid bluetooth device
if device.mac and device.mac[:4].upper() == BLE_PREFIX:
if device.track:
@ -97,7 +103,7 @@ def setup_scanner(hass, config, see, discovery_info=None):
_LOGGER.warning("No Bluetooth LE devices to track!")
return False
interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
def update_ble(now):
"""Lookup Bluetooth LE devices and update status."""

View File

@ -5,11 +5,16 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.components.device_tracker import (
YAML_DEVICES, CONF_TRACK_NEW, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL,
load_config, PLATFORM_SCHEMA, DEFAULT_TRACK_NEW, SOURCE_TYPE_BLUETOOTH,
DOMAIN)
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
from homeassistant.components.device_tracker.legacy import (
YAML_DEVICES, async_load_config
)
from homeassistant.components.device_tracker.const import (
CONF_TRACK_NEW, CONF_SCAN_INTERVAL, SCAN_INTERVAL, DEFAULT_TRACK_NEW,
SOURCE_TYPE_BLUETOOTH, DOMAIN
)
import homeassistant.util.dt as dt_util
from homeassistant.util.async_ import run_coroutine_threadsafe
_LOGGER = logging.getLogger(__name__)
@ -60,7 +65,10 @@ def setup_scanner(hass, config, see, discovery_info=None):
# Load all known devices.
# We just need the devices so set consider_home and home range
# to 0
for device in load_config(yaml_path, hass, 0):
for device in run_coroutine_threadsafe(
async_load_config(yaml_path, hass, 0),
hass.loop
).result():
# Check if device is a valid bluetooth device
if device.mac and device.mac[:3].upper() == BT_PREFIX:
if device.track:
@ -77,7 +85,7 @@ def setup_scanner(hass, config, see, discovery_info=None):
devs_to_track.append(dev[0])
see_device(dev[0], dev[1])
interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
request_rssi = config.get(CONF_REQUEST_RSSI, False)

View File

@ -1,7 +1,6 @@
"""Support for the Broadlink RM2 Pro (only temperature) and A1 devices."""
import binascii
import logging
import socket
from datetime import timedelta
import voluptuous as vol
@ -60,6 +59,7 @@ class BroadlinkSensor(Entity):
"""Initialize the sensor."""
self._name = '{} {}'.format(name, SENSOR_TYPES[sensor_type][0])
self._state = None
self._is_available = False
self._type = sensor_type
self._broadlink_data = broadlink_data
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
@ -74,6 +74,11 @@ class BroadlinkSensor(Entity):
"""Return the state of the sensor."""
return self._state
@property
def available(self):
"""Return True if entity is available."""
return self._is_available
@property
def unit_of_measurement(self):
"""Return the unit this state is expressed in."""
@ -83,8 +88,11 @@ class BroadlinkSensor(Entity):
"""Get the latest data from the sensor."""
self._broadlink_data.update()
if self._broadlink_data.data is None:
self._state = None
self._is_available = False
return
self._state = self._broadlink_data.data[self._type]
self._is_available = True
class BroadlinkData:
@ -119,8 +127,9 @@ class BroadlinkData:
if data is not None:
self.data = self._schema(data)
return
except socket.timeout as error:
except OSError as error:
if retry < 1:
self.data = None
_LOGGER.error(error)
return
except (vol.Invalid, vol.MultipleInvalid):
@ -131,7 +140,7 @@ class BroadlinkData:
def _auth(self, retry=3):
try:
auth = self._device.auth()
except socket.timeout:
except OSError:
auth = False
if not auth and retry > 0:
self._connect()

View File

@ -10,9 +10,10 @@ from homeassistant.components.switch import (
ENTITY_ID_FORMAT, PLATFORM_SCHEMA, SwitchDevice)
from homeassistant.const import (
CONF_COMMAND_OFF, CONF_COMMAND_ON, CONF_FRIENDLY_NAME, CONF_HOST, CONF_MAC,
CONF_SWITCHES, CONF_TIMEOUT, CONF_TYPE)
CONF_SWITCHES, CONF_TIMEOUT, CONF_TYPE, STATE_ON)
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle, slugify
from homeassistant.helpers.restore_state import RestoreEntity
from . import async_setup_service, data_packet
@ -109,13 +110,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
broadlink_device.timeout = config.get(CONF_TIMEOUT)
try:
broadlink_device.auth()
except socket.timeout:
except OSError:
_LOGGER.error("Failed to connect to device")
add_entities(switches)
class BroadlinkRMSwitch(SwitchDevice):
class BroadlinkRMSwitch(SwitchDevice, RestoreEntity):
"""Representation of an Broadlink switch."""
def __init__(self, name, friendly_name, device, command_on, command_off):
@ -126,6 +127,14 @@ class BroadlinkRMSwitch(SwitchDevice):
self._command_on = command_on
self._command_off = command_off
self._device = device
self._is_available = False
async def async_added_to_hass(self):
"""Call when entity about to be added to hass."""
await super().async_added_to_hass()
state = await self.async_get_last_state()
if state:
self._state = state.state == STATE_ON
@property
def name(self):
@ -137,6 +146,11 @@ class BroadlinkRMSwitch(SwitchDevice):
"""Return true if unable to access real state of entity."""
return True
@property
def available(self):
"""Return True if entity is available."""
return not self.should_poll or self._is_available
@property
def should_poll(self):
"""Return the polling state."""
@ -166,7 +180,7 @@ class BroadlinkRMSwitch(SwitchDevice):
return True
try:
self._device.send_data(packet)
except (socket.timeout, ValueError) as error:
except (ValueError, OSError) as error:
if retry < 1:
_LOGGER.error("Error during sending a packet: %s", error)
return False
@ -178,7 +192,7 @@ class BroadlinkRMSwitch(SwitchDevice):
def _auth(self, retry=2):
try:
auth = self._device.auth()
except socket.timeout:
except OSError:
auth = False
if retry < 1:
_LOGGER.error("Timeout during authorization")
@ -244,6 +258,7 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch):
except (socket.timeout, ValueError) as error:
if retry < 1:
_LOGGER.error("Error during updating the state: %s", error)
self._is_available = False
return
if not self._auth():
return
@ -252,6 +267,7 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch):
return self._update(retry-1)
self._state = state
self._load_power = load_power
self._is_available = True
class BroadlinkMP1Slot(BroadlinkRMSwitch):
@ -277,10 +293,12 @@ class BroadlinkMP1Slot(BroadlinkRMSwitch):
except (socket.timeout, ValueError) as error:
if retry < 1:
_LOGGER.error("Error during sending a packet: %s", error)
self._is_available = False
return False
if not self._auth():
return False
return self._sendpacket(packet, max(0, retry-1))
self._is_available = True
return True
@property
@ -330,7 +348,7 @@ class BroadlinkMP1Switch:
"""Authenticate the device."""
try:
auth = self._device.auth()
except socket.timeout:
except OSError:
auth = False
if not auth and retry > 0:
return self._auth(retry-1)

View File

@ -388,7 +388,7 @@ class BrData:
tasks.append(dev.async_update_ha_state())
if tasks:
await asyncio.wait(tasks, loop=self.hass.loop)
await asyncio.wait(tasks)
async def schedule_update(self, minute=1):
"""Schedule an update after minute minutes."""
@ -407,7 +407,7 @@ class BrData:
resp = None
try:
websession = async_get_clientsession(self.hass)
with async_timeout.timeout(10, loop=self.hass.loop):
with async_timeout.timeout(10):
resp = await websession.get(url)
result[STATUS_CODE] = resp.status

View File

@ -36,7 +36,7 @@ async def async_setup(hass, config):
hass.http.register_view(CalendarEventView(component))
# Doesn't work in prod builds of the frontend: home-assistant-polymer#1289
# await hass.components.frontend.async_register_built_in_panel(
# hass.components.frontend.async_register_built_in_panel(
# 'calendar', 'calendar', 'hass:calendar')
await component.async_setup(config)

View File

@ -107,11 +107,14 @@ async def async_request_stream(hass, entity_id, fmt):
camera = _get_camera_from_entity_id(hass, entity_id)
camera_prefs = hass.data[DATA_CAMERA_PREFS].get(entity_id)
if not camera.stream_source:
async with async_timeout.timeout(10):
source = await camera.stream_source()
if not source:
raise HomeAssistantError("{} does not support play stream service"
.format(camera.entity_id))
return request_stream(hass, camera.stream_source, fmt=fmt,
return request_stream(hass, source, fmt=fmt,
keepalive=camera_prefs.preload_stream)
@ -121,7 +124,7 @@ async def async_get_image(hass, entity_id, timeout=10):
camera = _get_camera_from_entity_id(hass, entity_id)
with suppress(asyncio.CancelledError, asyncio.TimeoutError):
with async_timeout.timeout(timeout, loop=hass.loop):
async with async_timeout.timeout(timeout):
image = await camera.async_camera_image()
if image:
@ -221,8 +224,16 @@ async def async_setup(hass, config):
async def preload_stream(hass, _):
for camera in component.entities:
camera_prefs = prefs.get(camera.entity_id)
if camera.stream_source and camera_prefs.preload_stream:
request_stream(hass, camera.stream_source, keepalive=True)
if not camera_prefs.preload_stream:
continue
async with async_timeout.timeout(10):
source = await camera.stream_source()
if not source:
continue
request_stream(hass, source, keepalive=True)
async_when_setup(hass, DOMAIN_STREAM, preload_stream)
@ -328,8 +339,7 @@ class Camera(Entity):
"""Return the interval between frames of the mjpeg stream."""
return 0.5
@property
def stream_source(self):
async def stream_source(self):
"""Return the source of the stream."""
return None
@ -481,7 +491,7 @@ class CameraImageView(CameraView):
async def handle(self, request, camera):
"""Serve camera image."""
with suppress(asyncio.CancelledError, asyncio.TimeoutError):
with async_timeout.timeout(10, loop=request.app['hass'].loop):
async with async_timeout.timeout(10):
image = await camera.async_camera_image()
if image:
@ -522,12 +532,10 @@ async def websocket_camera_thumbnail(hass, connection, msg):
"""
try:
image = await async_get_image(hass, msg['entity_id'])
connection.send_message(websocket_api.result_message(
msg['id'], {
'content_type': image.content_type,
'content': base64.b64encode(image.content).decode('utf-8')
}
))
await connection.send_big_result(msg['id'], {
'content_type': image.content_type,
'content': base64.b64encode(image.content).decode('utf-8')
})
except HomeAssistantError:
connection.send_message(websocket_api.error_message(
msg['id'], 'image_fetch_failed', 'Unable to fetch image'))
@ -549,18 +557,25 @@ async def ws_camera_stream(hass, connection, msg):
camera = _get_camera_from_entity_id(hass, entity_id)
camera_prefs = hass.data[DATA_CAMERA_PREFS].get(entity_id)
if not camera.stream_source:
async with async_timeout.timeout(10):
source = await camera.stream_source()
if not source:
raise HomeAssistantError("{} does not support play stream service"
.format(camera.entity_id))
fmt = msg['format']
url = request_stream(hass, camera.stream_source, fmt=fmt,
url = request_stream(hass, source, fmt=fmt,
keepalive=camera_prefs.preload_stream)
connection.send_result(msg['id'], {'url': url})
except HomeAssistantError as ex:
_LOGGER.error(ex)
_LOGGER.error("Error requesting stream: %s", ex)
connection.send_error(
msg['id'], 'start_stream_failed', str(ex))
except asyncio.TimeoutError:
_LOGGER.error("Timeout getting stream source")
connection.send_error(
msg['id'], 'start_stream_failed', "Timeout getting stream source")
@websocket_api.async_response
@ -624,7 +639,10 @@ async def async_handle_snapshot_service(camera, service):
async def async_handle_play_stream_service(camera, service_call):
"""Handle play stream services calls."""
if not camera.stream_source:
async with async_timeout.timeout(10):
source = await camera.stream_source()
if not source:
raise HomeAssistantError("{} does not support play stream service"
.format(camera.entity_id))
@ -633,7 +651,7 @@ async def async_handle_play_stream_service(camera, service_call):
fmt = service_call.data[ATTR_FORMAT]
entity_ids = service_call.data[ATTR_MEDIA_PLAYER]
url = request_stream(hass, camera.stream_source, fmt=fmt,
url = request_stream(hass, source, fmt=fmt,
keepalive=camera_prefs.preload_stream)
data = {
ATTR_ENTITY_ID: entity_ids,
@ -648,7 +666,10 @@ async def async_handle_play_stream_service(camera, service_call):
async def async_handle_record_service(camera, call):
"""Handle stream recording service calls."""
if not camera.stream_source:
async with async_timeout.timeout(10):
source = await camera.stream_source()
if not source:
raise HomeAssistantError("{} does not support record service"
.format(camera.entity_id))
@ -659,7 +680,7 @@ async def async_handle_record_service(camera, call):
variables={ATTR_ENTITY_ID: camera})
data = {
CONF_STREAM_SOURCE: camera.stream_source,
CONF_STREAM_SOURCE: source,
CONF_FILENAME: video_path,
CONF_DURATION: call.data[CONF_DURATION],
CONF_LOOKBACK: call.data[CONF_LOOKBACK],

View File

@ -79,7 +79,7 @@ class CanaryCamera(Camera):
image = await asyncio.shield(ffmpeg.get_image(
self._live_stream_session.live_stream_url,
output_format=IMAGE_JPEG,
extra_cmd=self._ffmpeg_arguments), loop=self.hass.loop)
extra_cmd=self._ffmpeg_arguments))
return image
async def handle_async_mjpeg_stream(self, request):

View File

@ -1,8 +1,7 @@
"""Component to embed Google Cast."""
from homeassistant import config_entries
from homeassistant.helpers import config_entry_flow
DOMAIN = 'cast'
from .const import DOMAIN
async def async_setup(hass, config):
@ -23,15 +22,3 @@ async def async_setup_entry(hass, entry):
hass.async_create_task(hass.config_entries.async_forward_entry_setup(
entry, 'media_player'))
return True
async def _async_has_devices(hass):
"""Return if there are devices that can be discovered."""
from pychromecast.discovery import discover_chromecasts
return await hass.async_add_executor_job(discover_chromecasts)
config_entry_flow.register_discovery_flow(
DOMAIN, 'Google Cast', _async_has_devices,
config_entries.CONN_CLASS_LOCAL_PUSH)

View File

@ -0,0 +1,16 @@
"""Config flow for Cast."""
from homeassistant.helpers import config_entry_flow
from homeassistant import config_entries
from .const import DOMAIN
async def _async_has_devices(hass):
"""Return if there are devices that can be discovered."""
from pychromecast.discovery import discover_chromecasts
return await hass.async_add_executor_job(discover_chromecasts)
config_entry_flow.register_discovery_flow(
DOMAIN, 'Google Cast', _async_has_devices,
config_entries.CONN_CLASS_LOCAL_PUSH)

View File

@ -0,0 +1,3 @@
"""Consts for Cast integration."""
DOMAIN = 'cast'

View File

@ -1,6 +1,7 @@
{
"domain": "cast",
"name": "Cast",
"config_flow": true,
"documentation": "https://www.home-assistant.io/components/cast",
"requirements": [
"pychromecast==3.2.1"

View File

@ -106,7 +106,7 @@ async def async_citybikes_request(hass, uri, schema):
try:
session = async_get_clientsession(hass)
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
with async_timeout.timeout(REQUEST_TIMEOUT):
req = await session.get(DEFAULT_ENDPOINT.format(uri=uri))
json_response = await req.json()
@ -181,7 +181,7 @@ class CityBikesNetworks:
"""Initialize the networks instance."""
self.hass = hass
self.networks = None
self.networks_loading = asyncio.Condition(loop=hass.loop)
self.networks_loading = asyncio.Condition()
async def get_closest_network_id(self, latitude, longitude):
"""Return the id of the network closest to provided location."""
@ -217,7 +217,7 @@ class CityBikesNetwork:
self.hass = hass
self.network_id = network_id
self.stations = []
self.ready = asyncio.Event(loop=hass.loop)
self.ready = asyncio.Event()
async def async_refresh(self, now=None):
"""Refresh the state of the network."""

View File

@ -17,7 +17,9 @@ from homeassistant.util.aiohttp import MockRequest
from . import utils
from .const import (
CONF_ENTITY_CONFIG, CONF_FILTER, DOMAIN, DISPATCHER_REMOTE_UPDATE)
CONF_ENTITY_CONFIG, CONF_FILTER, DOMAIN, DISPATCHER_REMOTE_UPDATE,
PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE,
PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA)
from .prefs import CloudPreferences
@ -98,12 +100,26 @@ class CloudClient(Interface):
if entity.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
return False
return google_conf['filter'](entity.entity_id)
if not google_conf['filter'].empty_filter:
return google_conf['filter'](entity.entity_id)
entity_configs = self.prefs.google_entity_configs
entity_config = entity_configs.get(entity.entity_id, {})
return entity_config.get(
PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE)
def should_2fa(entity):
"""If an entity should be checked for 2FA."""
entity_configs = self.prefs.google_entity_configs
entity_config = entity_configs.get(entity.entity_id, {})
return not entity_config.get(
PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA)
username = self._hass.data[DOMAIN].claims["cognito:username"]
self._google_config = ga_h.Config(
should_expose=should_expose,
should_2fa=should_2fa,
secure_devices_pin=self._prefs.google_secure_devices_pin,
entity_config=google_conf.get(CONF_ENTITY_CONFIG),
agent_user_id=username,

View File

@ -8,6 +8,13 @@ PREF_ENABLE_REMOTE = 'remote_enabled'
PREF_GOOGLE_SECURE_DEVICES_PIN = 'google_secure_devices_pin'
PREF_CLOUDHOOKS = 'cloudhooks'
PREF_CLOUD_USER = 'cloud_user'
PREF_GOOGLE_ENTITY_CONFIGS = 'google_entity_configs'
PREF_OVERRIDE_NAME = 'override_name'
PREF_DISABLE_2FA = 'disable_2fa'
PREF_ALIASES = 'aliases'
PREF_SHOULD_EXPOSE = 'should_expose'
DEFAULT_SHOULD_EXPOSE = True
DEFAULT_DISABLE_2FA = False
CONF_ALEXA = 'alexa'
CONF_ALIASES = 'aliases'

View File

@ -14,8 +14,7 @@ from homeassistant.components.http.data_validator import (
RequestDataValidator)
from homeassistant.components import websocket_api
from homeassistant.components.alexa import smart_home as alexa_sh
from homeassistant.components.google_assistant import (
const as google_const)
from homeassistant.components.google_assistant import helpers as google_helpers
from .const import (
DOMAIN, REQUEST_TIMEOUT, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE,
@ -81,6 +80,12 @@ async def async_setup(hass):
websocket_remote_connect)
hass.components.websocket_api.async_register_command(
websocket_remote_disconnect)
hass.components.websocket_api.async_register_command(
google_assistant_list)
hass.components.websocket_api.async_register_command(
google_assistant_update)
hass.http.register_view(GoogleActionsSyncView)
hass.http.register_view(CloudLoginView)
hass.http.register_view(CloudLogoutView)
@ -164,10 +169,10 @@ class GoogleActionsSyncView(HomeAssistantView):
cloud = hass.data[DOMAIN]
websession = hass.helpers.aiohttp_client.async_get_clientsession()
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
with async_timeout.timeout(REQUEST_TIMEOUT):
await hass.async_add_job(cloud.auth.check_token)
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
with async_timeout.timeout(REQUEST_TIMEOUT):
req = await websession.post(
cloud.google_actions_sync_url, headers={
'authorization': cloud.id_token
@ -192,7 +197,7 @@ class CloudLoginView(HomeAssistantView):
hass = request.app['hass']
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
with async_timeout.timeout(REQUEST_TIMEOUT):
await hass.async_add_job(cloud.auth.login, data['email'],
data['password'])
@ -212,7 +217,7 @@ class CloudLogoutView(HomeAssistantView):
hass = request.app['hass']
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
with async_timeout.timeout(REQUEST_TIMEOUT):
await cloud.logout()
return self.json_message('ok')
@ -234,7 +239,7 @@ class CloudRegisterView(HomeAssistantView):
hass = request.app['hass']
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
with async_timeout.timeout(REQUEST_TIMEOUT):
await hass.async_add_job(
cloud.auth.register, data['email'], data['password'])
@ -256,7 +261,7 @@ class CloudResendConfirmView(HomeAssistantView):
hass = request.app['hass']
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
with async_timeout.timeout(REQUEST_TIMEOUT):
await hass.async_add_job(
cloud.auth.resend_email_confirm, data['email'])
@ -278,7 +283,7 @@ class CloudForgotPasswordView(HomeAssistantView):
hass = request.app['hass']
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
with async_timeout.timeout(REQUEST_TIMEOUT):
await hass.async_add_job(
cloud.auth.forgot_password, data['email'])
@ -320,7 +325,7 @@ async def websocket_subscription(hass, connection, msg):
from hass_nabucasa.const import STATE_DISCONNECTED
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
with async_timeout.timeout(REQUEST_TIMEOUT):
response = await cloud.fetch_subscription_info()
if response.status != 200:
@ -411,7 +416,6 @@ def _account_data(cloud):
'cloud': cloud.iot.state,
'prefs': client.prefs.as_dict(),
'google_entities': client.google_user_config['filter'].config,
'google_domains': list(google_const.DOMAIN_TO_GOOGLE_TYPES),
'alexa_entities': client.alexa_config.should_expose.config,
'alexa_domains': list(alexa_sh.ENTITY_ADAPTERS),
'remote_domain': remote.instance_domain,
@ -448,3 +452,55 @@ async def websocket_remote_disconnect(hass, connection, msg):
await cloud.client.prefs.async_update(remote_enabled=False)
await cloud.remote.disconnect()
connection.send_result(msg['id'], _account_data(cloud))
@websocket_api.require_admin
@_require_cloud_login
@websocket_api.async_response
@_ws_handle_cloud_errors
@websocket_api.websocket_command({
'type': 'cloud/google_assistant/entities'
})
async def google_assistant_list(hass, connection, msg):
"""List all google assistant entities."""
cloud = hass.data[DOMAIN]
entities = google_helpers.async_get_entities(
hass, cloud.client.google_config
)
result = []
for entity in entities:
result.append({
'entity_id': entity.entity_id,
'traits': [trait.name for trait in entity.traits()],
'might_2fa': entity.might_2fa(),
})
connection.send_result(msg['id'], result)
@websocket_api.require_admin
@_require_cloud_login
@websocket_api.async_response
@_ws_handle_cloud_errors
@websocket_api.websocket_command({
'type': 'cloud/google_assistant/entities/update',
'entity_id': str,
vol.Optional('should_expose'): bool,
vol.Optional('override_name'): str,
vol.Optional('aliases'): [str],
vol.Optional('disable_2fa'): bool,
})
async def google_assistant_update(hass, connection, msg):
"""List all google assistant entities."""
cloud = hass.data[DOMAIN]
changes = dict(msg)
changes.pop('type')
changes.pop('id')
await cloud.client.prefs.async_update_google_entity_config(**changes)
connection.send_result(
msg['id'],
cloud.client.prefs.google_entity_configs.get(msg['entity_id']))

View File

@ -3,7 +3,7 @@
"name": "Cloud",
"documentation": "https://www.home-assistant.io/components/cloud",
"requirements": [
"hass-nabucasa==0.12"
"hass-nabucasa==0.13"
],
"dependencies": [
"http",

View File

@ -4,6 +4,8 @@ from ipaddress import ip_address
from .const import (
DOMAIN, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, PREF_ENABLE_REMOTE,
PREF_GOOGLE_SECURE_DEVICES_PIN, PREF_CLOUDHOOKS, PREF_CLOUD_USER,
PREF_GOOGLE_ENTITY_CONFIGS, PREF_OVERRIDE_NAME, PREF_DISABLE_2FA,
PREF_ALIASES, PREF_SHOULD_EXPOSE,
InvalidTrustedNetworks)
STORAGE_KEY = DOMAIN
@ -30,6 +32,7 @@ class CloudPreferences:
PREF_ENABLE_GOOGLE: True,
PREF_ENABLE_REMOTE: False,
PREF_GOOGLE_SECURE_DEVICES_PIN: None,
PREF_GOOGLE_ENTITY_CONFIGS: {},
PREF_CLOUDHOOKS: {},
PREF_CLOUD_USER: None,
}
@ -39,7 +42,7 @@ class CloudPreferences:
async def async_update(self, *, google_enabled=_UNDEF,
alexa_enabled=_UNDEF, remote_enabled=_UNDEF,
google_secure_devices_pin=_UNDEF, cloudhooks=_UNDEF,
cloud_user=_UNDEF):
cloud_user=_UNDEF, google_entity_configs=_UNDEF):
"""Update user preferences."""
for key, value in (
(PREF_ENABLE_GOOGLE, google_enabled),
@ -48,6 +51,7 @@ class CloudPreferences:
(PREF_GOOGLE_SECURE_DEVICES_PIN, google_secure_devices_pin),
(PREF_CLOUDHOOKS, cloudhooks),
(PREF_CLOUD_USER, cloud_user),
(PREF_GOOGLE_ENTITY_CONFIGS, google_entity_configs),
):
if value is not _UNDEF:
self._prefs[key] = value
@ -57,9 +61,48 @@ class CloudPreferences:
await self._store.async_save(self._prefs)
async def async_update_google_entity_config(
self, *, entity_id, override_name=_UNDEF, disable_2fa=_UNDEF,
aliases=_UNDEF, should_expose=_UNDEF):
"""Update config for a Google entity."""
entities = self.google_entity_configs
entity = entities.get(entity_id, {})
changes = {}
for key, value in (
(PREF_OVERRIDE_NAME, override_name),
(PREF_DISABLE_2FA, disable_2fa),
(PREF_ALIASES, aliases),
(PREF_SHOULD_EXPOSE, should_expose),
):
if value is not _UNDEF:
changes[key] = value
if not changes:
return
updated_entity = {
**entity,
**changes,
}
updated_entities = {
**entities,
entity_id: updated_entity,
}
await self.async_update(google_entity_configs=updated_entities)
def as_dict(self):
"""Return dictionary version."""
return self._prefs
return {
PREF_ENABLE_ALEXA: self.alexa_enabled,
PREF_ENABLE_GOOGLE: self.google_enabled,
PREF_ENABLE_REMOTE: self.remote_enabled,
PREF_GOOGLE_SECURE_DEVICES_PIN: self.google_secure_devices_pin,
PREF_GOOGLE_ENTITY_CONFIGS: self.google_entity_configs,
PREF_CLOUDHOOKS: self.cloudhooks,
PREF_CLOUD_USER: self.cloud_user,
}
@property
def remote_enabled(self):
@ -89,6 +132,11 @@ class CloudPreferences:
"""Return if Google is allowed to unlock locks."""
return self._prefs.get(PREF_GOOGLE_SECURE_DEVICES_PIN)
@property
def google_entity_configs(self):
"""Return Google Entity configurations."""
return self._prefs.get(PREF_GOOGLE_ENTITY_CONFIGS, {})
@property
def cloudhooks(self):
"""Return the published cloud webhooks."""

View File

@ -1,13 +1,25 @@
"""Helper functions for cloud components."""
from typing import Any, Dict
from aiohttp import web
from aiohttp import web, payload
def aiohttp_serialize_response(response: web.Response) -> Dict[str, Any]:
"""Serialize an aiohttp response to a dictionary."""
body = response.body
if body is None:
pass
elif isinstance(body, payload.StringPayload):
# pylint: disable=protected-access
body = body._value.decode(body.encoding)
elif isinstance(body, bytes):
body = body.decode(response.charset or 'utf-8')
else:
raise ValueError("Unknown payload encoding")
return {
'status': response.status,
'body': response.text,
'body': body,
'headers': dict(response.headers),
}

View File

@ -106,7 +106,7 @@ class ComedHourlyPricingSensor(Entity):
else:
url_string += '?type=currenthouraverage'
with async_timeout.timeout(60, loop=self.loop):
with async_timeout.timeout(60):
response = await self.websession.get(url_string)
# The API responds with MIME type 'text/html'
text = await response.text()

View File

@ -30,7 +30,7 @@ ON_DEMAND = ('zwave',)
async def async_setup(hass, config):
"""Set up the config component."""
await hass.components.frontend.async_register_built_in_panel(
hass.components.frontend.async_register_built_in_panel(
'config', 'config', 'hass:settings', require_admin=True)
async def setup_panel(panel_name):
@ -62,7 +62,7 @@ async def async_setup(hass, config):
tasks.append(setup_panel(panel_name))
if tasks:
await asyncio.wait(tasks, loop=hass.loop)
await asyncio.wait(tasks)
return True
@ -92,6 +92,10 @@ class BaseEditConfigView(HomeAssistantView):
"""Set value."""
raise NotImplementedError
def _delete_value(self, hass, data, config_key):
"""Delete value."""
raise NotImplementedError
async def get(self, request, config_key):
"""Fetch device specific config."""
hass = request.app['hass']
@ -128,7 +132,27 @@ class BaseEditConfigView(HomeAssistantView):
current = await self.read_config(hass)
self._write_value(hass, current, config_key, data)
await hass.async_add_job(_write, path, current)
await hass.async_add_executor_job(_write, path, current)
if self.post_write_hook is not None:
hass.async_create_task(self.post_write_hook(hass))
return self.json({
'result': 'ok',
})
async def delete(self, request, config_key):
"""Remove an entry."""
hass = request.app['hass']
current = await self.read_config(hass)
value = self._get_value(hass, current, config_key)
path = hass.config.path(self.path)
if value is None:
return self.json_message('Resource not found', 404)
self._delete_value(hass, current, config_key)
await hass.async_add_executor_job(_write, path, current)
if self.post_write_hook is not None:
hass.async_create_task(self.post_write_hook(hass))
@ -161,6 +185,10 @@ class EditKeyBasedConfigView(BaseEditConfigView):
"""Set value."""
data.setdefault(config_key, {}).update(new_value)
def _delete_value(self, hass, data, config_key):
"""Delete value."""
return data.pop(config_key)
class EditIdBasedConfigView(BaseEditConfigView):
"""Configure key based config entries."""
@ -184,6 +212,13 @@ class EditIdBasedConfigView(BaseEditConfigView):
value.update(new_value)
def _delete_value(self, hass, data, config_key):
"""Delete value."""
index = next(
idx for idx, val in enumerate(data)
if val.get(CONF_ID) == config_key)
data.pop(index)
def _read(path):
"""Read YAML helper."""

View File

@ -6,6 +6,7 @@ from homeassistant.components.http import HomeAssistantView
from homeassistant.exceptions import Unauthorized
from homeassistant.helpers.data_entry_flow import (
FlowManagerIndexView, FlowManagerResourceView)
from homeassistant.generated import config_flows
async def async_setup(hass):
@ -172,7 +173,7 @@ class ConfigManagerAvailableFlowView(HomeAssistantView):
async def get(self, request):
"""List available flow handlers."""
return self.json(config_entries.FLOWS)
return self.json(config_flows.FLOWS)
class OptionManagerFlowIndexView(FlowManagerIndexView):

View File

@ -1,12 +1,22 @@
"""Component to interact with Hassbian tools."""
import voluptuous as vol
from homeassistant.components.http import HomeAssistantView
from homeassistant.config import async_check_ha_config_file
from homeassistant.components import websocket_api
from homeassistant.const import (
CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL
)
from homeassistant.helpers import config_validation as cv
from homeassistant.util import location
async def async_setup(hass):
"""Set up the Hassbian config."""
hass.http.register_view(CheckConfigView)
websocket_api.async_register_command(hass, websocket_update_config)
websocket_api.async_register_command(hass, websocket_detect_config)
return True
@ -26,3 +36,62 @@ class CheckConfigView(HomeAssistantView):
"result": state,
"errors": errors,
})
@websocket_api.require_admin
@websocket_api.async_response
@websocket_api.websocket_command({
'type': 'config/core/update',
vol.Optional('latitude'): cv.latitude,
vol.Optional('longitude'): cv.longitude,
vol.Optional('elevation'): int,
vol.Optional('unit_system'): cv.unit_system,
vol.Optional('location_name'): str,
vol.Optional('time_zone'): cv.time_zone,
})
async def websocket_update_config(hass, connection, msg):
"""Handle update core config command."""
data = dict(msg)
data.pop('id')
data.pop('type')
try:
await hass.config.update(**data)
connection.send_result(msg['id'])
except ValueError as err:
connection.send_error(
msg['id'], 'invalid_info', str(err)
)
@websocket_api.require_admin
@websocket_api.async_response
@websocket_api.websocket_command({
'type': 'config/core/detect',
})
async def websocket_detect_config(hass, connection, msg):
"""Detect core config."""
session = hass.helpers.aiohttp_client.async_get_clientsession()
location_info = await location.async_detect_location_info(session)
info = {}
if location_info is None:
connection.send_result(msg['id'], info)
return
if location_info.use_metric:
info['unit_system'] = CONF_UNIT_SYSTEM_METRIC
else:
info['unit_system'] = CONF_UNIT_SYSTEM_IMPERIAL
if location_info.latitude:
info['latitude'] = location_info.latitude
if location_info.longitude:
info['longitude'] = location_info.longitude
if location_info.time_zone:
info['time_zone'] = location_info.time_zone
connection.send_result(msg['id'], info)

View File

@ -81,12 +81,7 @@ class DaikinClimate(ClimateDevice):
self._api = api
self._list = {
ATTR_OPERATION_MODE: list(HA_STATE_TO_DAIKIN),
ATTR_FAN_MODE: list(
map(
str.title,
appliance.daikin_values(HA_ATTR_TO_DAIKIN[ATTR_FAN_MODE])
)
),
ATTR_FAN_MODE: self._api.device.fan_rate,
ATTR_SWING_MODE: list(
map(
str.title,
@ -95,11 +90,14 @@ class DaikinClimate(ClimateDevice):
),
}
self._supported_features = (SUPPORT_AWAY_MODE | SUPPORT_ON_OFF
self._supported_features = (SUPPORT_ON_OFF
| SUPPORT_OPERATION_MODE
| SUPPORT_TARGET_TEMPERATURE)
if self._api.device.support_fan_mode:
if self._api.device.support_away_mode:
self._supported_features |= SUPPORT_AWAY_MODE
if self._api.device.support_fan_rate:
self._supported_features |= SUPPORT_FAN_MODE
if self._api.device.support_swing_mode:

View File

@ -1,9 +1,10 @@
{
"domain": "daikin",
"name": "Daikin",
"config_flow": true,
"documentation": "https://www.home-assistant.io/components/daikin",
"requirements": [
"pydaikin==1.4.0"
"pydaikin==1.4.6"
],
"dependencies": [],
"codeowners": [

View File

@ -26,9 +26,12 @@ async def async_setup_platform(
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up Daikin climate based on config_entry."""
daikin_api = hass.data[DAIKIN_DOMAIN].get(entry.entry_id)
sensors = [ATTR_INSIDE_TEMPERATURE]
if daikin_api.device.support_outside_temperature:
sensors.append(ATTR_OUTSIDE_TEMPERATURE)
async_add_entities([
DaikinClimateSensor(daikin_api, sensor, hass.config.units)
for sensor in SENSOR_TYPES
for sensor in sensors
])

View File

@ -27,8 +27,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
if zones:
async_add_entities([
DaikinZoneSwitch(daikin_api, zone_id)
for zone_id, name in enumerate(zones)
if name != '-'
for zone_id, zone in enumerate(zones) if zone != ('-', '0')
])

View File

@ -103,6 +103,8 @@ class DarkSkyWeather(WeatherEntity):
@property
def temperature_unit(self):
"""Return the unit of measurement."""
if self._dark_sky.units is None:
return None
return TEMP_FAHRENHEIT if 'us' in self._dark_sky.units \
else TEMP_CELSIUS

View File

@ -3,12 +3,21 @@
"abort": {
"already_configured": "Ce pont est d\u00e9j\u00e0 configur\u00e9",
"no_bridges": "Aucun pont deCONZ n'a \u00e9t\u00e9 d\u00e9couvert",
"one_instance_only": "Le composant prend uniquement en charge une instance deCONZ"
"one_instance_only": "Le composant prend uniquement en charge une instance deCONZ",
"updated_instance": "Instance deCONZ mise \u00e0 jour avec la nouvelle adresse d'h\u00f4te"
},
"error": {
"no_key": "Impossible d'obtenir une cl\u00e9 d'API"
},
"step": {
"hassio_confirm": {
"data": {
"allow_clip_sensor": "Autoriser l'importation de capteurs virtuels",
"allow_deconz_groups": "Autoriser l'importation des groupes deCONZ"
},
"description": "Voulez-vous configurer Home Assistant pour qu'il se connecte \u00e0 la passerelle deCONZ fournie par l'add-on hass.io {addon} ?",
"title": "Passerelle deCONZ Zigbee via l'add-on Hass.io"
},
"init": {
"data": {
"host": "H\u00f4te",

View File

@ -3,7 +3,8 @@
"abort": {
"already_configured": "Bryggan \u00e4r redan konfigurerad",
"no_bridges": "Inga deCONZ-bryggor uppt\u00e4cktes",
"one_instance_only": "Komponenten st\u00f6djer endast en deCONZ-instans"
"one_instance_only": "Komponenten st\u00f6djer endast en deCONZ-instans",
"updated_instance": "Uppdaterad deCONZ-instans med ny v\u00e4rdadress"
},
"error": {
"no_key": "Det gick inte att ta emot en API-nyckel"
@ -11,8 +12,10 @@
"step": {
"hassio_confirm": {
"data": {
"allow_clip_sensor": "Till\u00e5t import av virtuella sensorer"
"allow_clip_sensor": "Till\u00e5t import av virtuella sensorer",
"allow_deconz_groups": "Till\u00e5t import av deCONZ-grupper"
},
"description": "Vill du konfigurera Home Assistant f\u00f6r att ansluta till deCONZ gateway som tillhandah\u00e5lls av hass.io till\u00e4gg {addon}?",
"title": "deCONZ Zigbee gateway via Hass.io till\u00e4gg"
},
"init": {

View File

@ -164,6 +164,7 @@ async def async_unload_entry(hass, config_entry):
if not hass.data[DOMAIN]:
hass.services.async_remove(DOMAIN, SERVICE_DECONZ)
hass.services.async_remove(DOMAIN, SERVICE_DEVICE_REFRESH)
elif gateway.master:
await async_populate_options(hass, config_entry)
new_master_gateway = next(iter(hass.data[DOMAIN].values()))

View File

@ -1,6 +1,8 @@
"""Support for deCONZ binary sensors."""
from pydeconz.sensor import Presence, Vibration
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@ -15,7 +17,7 @@ ATTR_VIBRATIONSTRENGTH = 'vibrationstrength'
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
"""Old way of setting up deCONZ binary sensors."""
"""Old way of setting up deCONZ platforms."""
pass
@ -26,12 +28,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
@callback
def async_add_sensor(sensors):
"""Add binary sensor from deCONZ."""
from pydeconz.sensor import DECONZ_BINARY_SENSOR
entities = []
for sensor in sensors:
if sensor.type in DECONZ_BINARY_SENSOR and \
if sensor.BINARY and \
not (not gateway.allow_clip_sensor and
sensor.type.startswith('CLIP')):
@ -49,16 +50,11 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorDevice):
"""Representation of a deCONZ binary sensor."""
@callback
def async_update_callback(self, reason):
"""Update the sensor's state.
If reason is that state is updated,
or reachable has changed or battery has changed.
"""
if reason['state'] or \
'reachable' in reason['attr'] or \
'battery' in reason['attr'] or \
'on' in reason['attr']:
def async_update_callback(self, force_update=False):
"""Update the sensor's state."""
changed = set(self._device.changed_keys)
keys = {'battery', 'on', 'reachable', 'state'}
if force_update or any(key in changed for key in keys):
self.async_schedule_update_ha_state()
@property
@ -69,26 +65,33 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorDevice):
@property
def device_class(self):
"""Return the class of the sensor."""
return self._device.sensor_class
return self._device.SENSOR_CLASS
@property
def icon(self):
"""Return the icon to use in the frontend."""
return self._device.sensor_icon
return self._device.SENSOR_ICON
@property
def device_state_attributes(self):
"""Return the state attributes of the sensor."""
from pydeconz.sensor import PRESENCE, VIBRATION
attr = {}
if self._device.battery:
attr[ATTR_BATTERY_LEVEL] = self._device.battery
if self._device.on is not None:
attr[ATTR_ON] = self._device.on
if self._device.type in PRESENCE and self._device.dark is not None:
if self._device.secondary_temperature is not None:
attr[ATTR_TEMPERATURE] = self._device.secondary_temperature
if self._device.type in Presence.ZHATYPE and \
self._device.dark is not None:
attr[ATTR_DARK] = self._device.dark
elif self._device.type in VIBRATION:
elif self._device.type in Vibration.ZHATYPE:
attr[ATTR_ORIENTATION] = self._device.orientation
attr[ATTR_TILTANGLE] = self._device.tiltangle
attr[ATTR_VIBRATIONSTRENGTH] = self._device.vibrationstrength
return attr

View File

@ -1,4 +1,6 @@
"""Support for deCONZ climate devices."""
from pydeconz.sensor import Thermostat
from homeassistant.components.climate import ClimateDevice
from homeassistant.components.climate.const import (
SUPPORT_ON_OFF, SUPPORT_TARGET_TEMPERATURE)
@ -12,6 +14,12 @@ from .deconz_device import DeconzDevice
from .gateway import get_gateway_from_config_entry
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
"""Old way of setting up deCONZ platforms."""
pass
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the deCONZ climate devices.
@ -22,12 +30,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
@callback
def async_add_climate(sensors):
"""Add climate devices from deCONZ."""
from pydeconz.sensor import THERMOSTAT
entities = []
for sensor in sensors:
if sensor.type in THERMOSTAT and \
if sensor.type in Thermostat.ZHATYPE and \
not (not gateway.allow_clip_sensor and
sensor.type.startswith('CLIP')):
@ -59,7 +66,7 @@ class DeconzThermostat(DeconzDevice, ClimateDevice):
@property
def is_on(self):
"""Return true if on."""
return self._device.on
return self._device.state_on
async def async_turn_on(self):
"""Turn on switch."""

View File

@ -4,13 +4,19 @@ import asyncio
import async_timeout
import voluptuous as vol
from pydeconz.errors import ResponseError, RequestError
from pydeconz.utils import (
async_discovery, async_get_api_key, async_get_bridgeid)
from homeassistant import config_entries
from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_SERIAL
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client
from .const import CONF_BRIDGEID, DEFAULT_PORT, DOMAIN
DECONZ_MANUFACTURERURL = 'http://www.dresden-elektronik.de'
CONF_SERIAL = 'serial'
@ -54,8 +60,6 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
If more than one bridge is found let user choose bridge to link.
If no bridge is found allow user to manually input configuration.
"""
from pydeconz.utils import async_discovery
if user_input is not None:
for bridge in self.bridges:
if bridge[CONF_HOST] == user_input[CONF_HOST]:
@ -101,8 +105,6 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
async def async_step_link(self, user_input=None):
"""Attempt to link with the deCONZ bridge."""
from pydeconz.errors import ResponseError, RequestError
from pydeconz.utils import async_get_api_key
errors = {}
if user_input is not None:
@ -127,8 +129,6 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
async def _create_entry(self):
"""Create entry for gateway."""
from pydeconz.utils import async_get_bridgeid
if CONF_BRIDGEID not in self.deconz_config:
session = aiohttp_client.async_get_clientsession(self.hass)
@ -151,12 +151,12 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
entry.data[CONF_HOST] = host
self.hass.config_entries.async_update_entry(entry)
async def async_step_discovery(self, discovery_info):
"""Prepare configuration for a discovered deCONZ bridge.
async def async_step_ssdp(self, discovery_info):
"""Handle a discovered deCONZ bridge."""
if discovery_info[ATTR_MANUFACTURERURL] != DECONZ_MANUFACTURERURL:
return self.async_abort(reason='not_deconz_bridge')
This flow is triggered by the discovery component.
"""
bridgeid = discovery_info[CONF_SERIAL]
bridgeid = discovery_info[ATTR_SERIAL]
gateway_entries = configured_gateways(self.hass)
if bridgeid in gateway_entries:
@ -164,10 +164,17 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
await self._update_entry(entry, discovery_info[CONF_HOST])
return self.async_abort(reason='updated_instance')
# pylint: disable=unsupported-assignment-operation
self.context[ATTR_SERIAL] = bridgeid
if any(bridgeid == flow['context'][ATTR_SERIAL]
for flow in self._async_in_progress()):
return self.async_abort(reason='already_in_progress')
deconz_config = {
CONF_HOST: discovery_info[CONF_HOST],
CONF_PORT: discovery_info[CONF_PORT],
CONF_BRIDGEID: discovery_info[CONF_SERIAL]
CONF_BRIDGEID: bridgeid
}
return await self.async_step_import(deconz_config)

View File

@ -14,7 +14,7 @@ ZIGBEE_SPEC = ['lumi.curtain']
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
"""Unsupported way of setting up deCONZ covers."""
"""Old way of setting up deCONZ platforms."""
pass

View File

@ -31,7 +31,7 @@ class DeconzDevice(Entity):
self.unsub_dispatcher()
@callback
def async_update_callback(self, reason):
def async_update_callback(self, force_update=False):
"""Update the device's state."""
self.async_schedule_update_ha_state()

View File

@ -2,6 +2,9 @@
import asyncio
import async_timeout
from pydeconz import DeconzSession, errors
from pydeconz.sensor import Switch
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.const import CONF_EVENT, CONF_HOST, CONF_ID
from homeassistant.core import EventOrigin, callback
@ -126,8 +129,7 @@ class DeconzGateway:
def async_connection_status_callback(self, available):
"""Handle signals of gateway connection status."""
self.available = available
async_dispatcher_send(self.hass, self.event_reachable,
{'state': True, 'attr': 'reachable'})
async_dispatcher_send(self.hass, self.event_reachable, True)
@callback
def async_event_new_device(self, device_type):
@ -145,9 +147,8 @@ class DeconzGateway:
@callback
def async_add_remote(self, sensors):
"""Set up remote from deCONZ."""
from pydeconz.sensor import SWITCH as DECONZ_REMOTE
for sensor in sensors:
if sensor.type in DECONZ_REMOTE and \
if sensor.type in Switch.ZHATYPE and \
not (not self.allow_clip_sensor and
sensor.type.startswith('CLIP')):
self.events.append(DeconzEvent(self.hass, sensor))
@ -187,8 +188,6 @@ class DeconzGateway:
async def get_gateway(hass, config, async_add_device_callback,
async_connection_status_callback):
"""Create a gateway object and verify configuration."""
from pydeconz import DeconzSession, errors
session = aiohttp_client.async_get_clientsession(hass)
deconz = DeconzSession(hass.loop, session, **config,
@ -232,8 +231,8 @@ class DeconzEvent:
self._device = None
@callback
def async_update_callback(self, reason):
def async_update_callback(self, force_update=False):
"""Fire the event if reason is that state is updated."""
if reason['state']:
if 'state' in self._device.changed_keys:
data = {CONF_ID: self._id, CONF_EVENT: self._device.state}
self._hass.bus.async_fire(self._event, data, EventOrigin.remote)

View File

@ -15,7 +15,7 @@ from .gateway import get_gateway_from_config_entry
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
"""Old way of setting up deCONZ lights and group."""
"""Old way of setting up deCONZ platforms."""
pass

View File

@ -1,10 +1,16 @@
{
"domain": "deconz",
"name": "Deconz",
"config_flow": true,
"documentation": "https://www.home-assistant.io/components/deconz",
"requirements": [
"pydeconz==58"
"pydeconz==59"
],
"ssdp": {
"manufacturer": [
"Royal Philips Electronics"
]
},
"dependencies": [],
"codeowners": [
"@kane610"

View File

@ -9,7 +9,7 @@ from .gateway import get_gateway_from_config_entry
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
"""Old way of setting up deCONZ scenes."""
"""Old way of setting up deCONZ platforms."""
pass

View File

@ -1,6 +1,8 @@
"""Support for deCONZ sensors."""
from pydeconz.sensor import LightLevel, Switch
from homeassistant.const import (
ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY)
ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util import slugify
@ -16,7 +18,7 @@ ATTR_EVENT_ID = 'event_id'
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
"""Old way of setting up deCONZ sensors."""
"""Old way of setting up deCONZ platforms."""
pass
@ -27,17 +29,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
@callback
def async_add_sensor(sensors):
"""Add sensors from deCONZ."""
from pydeconz.sensor import (
DECONZ_SENSOR, SWITCH as DECONZ_REMOTE)
entities = []
for sensor in sensors:
if sensor.type in DECONZ_SENSOR and \
if not sensor.BINARY and \
not (not gateway.allow_clip_sensor and
sensor.type.startswith('CLIP')):
if sensor.type in DECONZ_REMOTE:
if sensor.type in Switch.ZHATYPE:
if sensor.battery:
entities.append(DeconzBattery(sensor, gateway))
@ -56,16 +56,11 @@ class DeconzSensor(DeconzDevice):
"""Representation of a deCONZ sensor."""
@callback
def async_update_callback(self, reason):
"""Update the sensor's state.
If reason is that state is updated,
or reachable has changed or battery has changed.
"""
if reason['state'] or \
'reachable' in reason['attr'] or \
'battery' in reason['attr'] or \
'on' in reason['attr']:
def async_update_callback(self, force_update=False):
"""Update the sensor's state."""
changed = set(self._device.changed_keys)
keys = {'battery', 'on', 'reachable', 'state'}
if force_update or any(key in changed for key in keys):
self.async_schedule_update_ha_state()
@property
@ -76,34 +71,42 @@ class DeconzSensor(DeconzDevice):
@property
def device_class(self):
"""Return the class of the sensor."""
return self._device.sensor_class
return self._device.SENSOR_CLASS
@property
def icon(self):
"""Return the icon to use in the frontend."""
return self._device.sensor_icon
return self._device.SENSOR_ICON
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this sensor."""
return self._device.sensor_unit
return self._device.SENSOR_UNIT
@property
def device_state_attributes(self):
"""Return the state attributes of the sensor."""
from pydeconz.sensor import LIGHTLEVEL
attr = {}
if self._device.battery:
attr[ATTR_BATTERY_LEVEL] = self._device.battery
if self._device.on is not None:
attr[ATTR_ON] = self._device.on
if self._device.type in LIGHTLEVEL and self._device.dark is not None:
if self._device.secondary_temperature is not None:
attr[ATTR_TEMPERATURE] = self._device.secondary_temperature
if self._device.type in LightLevel.ZHATYPE and \
self._device.dark is not None:
attr[ATTR_DARK] = self._device.dark
if self.unit_of_measurement == 'Watts':
attr[ATTR_CURRENT] = self._device.current
attr[ATTR_VOLTAGE] = self._device.voltage
if self._device.sensor_class == 'daylight':
if self._device.SENSOR_CLASS == 'daylight':
attr[ATTR_DAYLIGHT] = self._device.daylight
return attr
@ -118,9 +121,11 @@ class DeconzBattery(DeconzDevice):
self._unit_of_measurement = "%"
@callback
def async_update_callback(self, reason):
def async_update_callback(self, force_update=False):
"""Update the battery's state, if needed."""
if 'reachable' in reason['attr'] or 'battery' in reason['attr']:
changed = set(self._device.changed_keys)
keys = {'battery', 'reachable'}
if force_update or any(key in changed for key in keys):
self.async_schedule_update_ha_state()
@property

View File

@ -34,9 +34,11 @@
},
"abort": {
"already_configured": "Bridge is already configured",
"already_in_progress": "Config flow for bridge is already in progress.",
"no_bridges": "No deCONZ bridges discovered",
"updated_instance": "Updated deCONZ instance with new host address",
"one_instance_only": "Component only supports one deCONZ instance"
"not_deconz_bridge": "Not a deCONZ bridge",
"one_instance_only": "Component only supports one deCONZ instance",
"updated_instance": "Updated deCONZ instance with new host address"
}
}
}

View File

@ -10,7 +10,7 @@ from .gateway import get_gateway_from_config_entry
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
"""Old way of setting up deCONZ switches."""
"""Old way of setting up deCONZ platforms."""
pass

View File

@ -15,6 +15,7 @@
"mobile_app",
"person",
"script",
"ssdp",
"sun",
"system_health",
"updater",

View File

@ -5,7 +5,8 @@ from homeassistant.components.media_player.const import (
SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY,
SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET,
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET)
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_VOLUME_STEP)
from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING
import homeassistant.util.dt as dt_util
@ -35,7 +36,7 @@ YOUTUBE_PLAYER_SUPPORT = \
MUSIC_PLAYER_SUPPORT = \
SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_CLEAR_PLAYLIST | \
SUPPORT_PLAY | SUPPORT_SHUFFLE_SET | \
SUPPORT_PLAY | SUPPORT_SHUFFLE_SET | SUPPORT_VOLUME_STEP | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
SUPPORT_SELECT_SOUND_MODE
@ -122,6 +123,16 @@ class AbstractDemoPlayer(MediaPlayerDevice):
self._volume_muted = mute
self.schedule_update_ha_state()
def volume_up(self):
"""Increase volume."""
self._volume_level = min(1.0, self._volume_level + 0.1)
self.schedule_update_ha_state()
def volume_down(self):
"""Decrease volume."""
self._volume_level = max(0.0, self._volume_level - 0.1)
self.schedule_update_ha_state()
def set_volume_level(self, volume):
"""Set the volume level, range 0..1."""
self._volume_level = volume

View File

@ -1,78 +1,52 @@
"""Provide functionality to keep track of devices."""
import asyncio
from datetime import timedelta
import logging
from typing import Any, List, Sequence, Callable
import voluptuous as vol
from homeassistant.setup import async_prepare_setup_platform
from homeassistant.core import callback
from homeassistant.loader import bind_hass
from homeassistant.components import group, zone
from homeassistant.components.group import (
ATTR_ADD_ENTITIES, ATTR_ENTITIES, ATTR_OBJECT_ID, ATTR_VISIBLE,
DOMAIN as DOMAIN_GROUP, SERVICE_SET)
from homeassistant.components.zone.zone import async_active_zone
from homeassistant.config import load_yaml_config_file, async_log_exception
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_per_platform, discovery
from homeassistant.components import group
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType
from homeassistant import util
from homeassistant.util.async_ import run_coroutine_threadsafe
import homeassistant.util.dt as dt_util
from homeassistant.util.yaml import dump
from homeassistant.helpers.event import async_track_utc_time_change
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_GPS_ACCURACY, ATTR_ICON, ATTR_LATITUDE,
ATTR_LONGITUDE, ATTR_NAME, CONF_ICON, CONF_MAC, CONF_NAME,
DEVICE_DEFAULT_NAME, STATE_NOT_HOME, STATE_HOME)
from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME
_LOGGER = logging.getLogger(__name__)
from . import legacy, setup
from .config_entry import ( # noqa # pylint: disable=unused-import
async_setup_entry, async_unload_entry
)
from .legacy import DeviceScanner # noqa # pylint: disable=unused-import
from .const import (
ATTR_ATTRIBUTES,
ATTR_BATTERY,
ATTR_CONSIDER_HOME,
ATTR_DEV_ID,
ATTR_GPS,
ATTR_HOST_NAME,
ATTR_LOCATION_NAME,
ATTR_MAC,
ATTR_SOURCE_TYPE,
CONF_AWAY_HIDE,
CONF_CONSIDER_HOME,
CONF_NEW_DEVICE_DEFAULTS,
CONF_SCAN_INTERVAL,
CONF_TRACK_NEW,
DEFAULT_AWAY_HIDE,
DEFAULT_CONSIDER_HOME,
DEFAULT_TRACK_NEW,
DOMAIN,
PLATFORM_TYPE_LEGACY,
SOURCE_TYPE_BLUETOOTH_LE,
SOURCE_TYPE_BLUETOOTH,
SOURCE_TYPE_GPS,
SOURCE_TYPE_ROUTER,
)
DOMAIN = 'device_tracker'
GROUP_NAME_ALL_DEVICES = 'all devices'
ENTITY_ID_ALL_DEVICES = group.ENTITY_ID_FORMAT.format('all_devices')
ENTITY_ID_FORMAT = DOMAIN + '.{}'
YAML_DEVICES = 'known_devices.yaml'
CONF_TRACK_NEW = 'track_new_devices'
DEFAULT_TRACK_NEW = True
CONF_NEW_DEVICE_DEFAULTS = 'new_device_defaults'
CONF_CONSIDER_HOME = 'consider_home'
DEFAULT_CONSIDER_HOME = timedelta(seconds=180)
CONF_SCAN_INTERVAL = 'interval_seconds'
DEFAULT_SCAN_INTERVAL = timedelta(seconds=12)
CONF_AWAY_HIDE = 'hide_if_away'
DEFAULT_AWAY_HIDE = False
EVENT_NEW_DEVICE = 'device_tracker_new_device'
SERVICE_SEE = 'see'
ATTR_ATTRIBUTES = 'attributes'
ATTR_BATTERY = 'battery'
ATTR_DEV_ID = 'dev_id'
ATTR_GPS = 'gps'
ATTR_HOST_NAME = 'host_name'
ATTR_LOCATION_NAME = 'location_name'
ATTR_MAC = 'mac'
ATTR_SOURCE_TYPE = 'source_type'
ATTR_CONSIDER_HOME = 'consider_home'
SOURCE_TYPE_GPS = 'gps'
SOURCE_TYPE_ROUTER = 'router'
SOURCE_TYPE_BLUETOOTH = 'bluetooth'
SOURCE_TYPE_BLUETOOTH_LE = 'bluetooth_le'
SOURCE_TYPES = (SOURCE_TYPE_GPS, SOURCE_TYPE_ROUTER,
SOURCE_TYPE_BLUETOOTH, SOURCE_TYPE_BLUETOOTH_LE)
@ -136,75 +110,29 @@ def see(hass: HomeAssistantType, mac: str = None, dev_id: str = None,
async def async_setup(hass: HomeAssistantType, config: ConfigType):
"""Set up the device tracker."""
yaml_path = hass.config.path(YAML_DEVICES)
tracker = await legacy.get_tracker(hass, config)
conf = config.get(DOMAIN, [])
conf = conf[0] if conf else {}
consider_home = conf.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME)
legacy_platforms = await setup.async_extract_config(hass, config)
defaults = conf.get(CONF_NEW_DEVICE_DEFAULTS, {})
track_new = conf.get(CONF_TRACK_NEW)
if track_new is None:
track_new = defaults.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
setup_tasks = [
legacy_platform.async_setup_legacy(hass, tracker)
for legacy_platform in legacy_platforms
]
devices = await async_load_config(yaml_path, hass, consider_home)
tracker = DeviceTracker(
hass, consider_home, track_new, defaults, devices)
async def async_setup_platform(p_type, p_config, disc_info=None):
"""Set up a device tracker platform."""
platform = await async_prepare_setup_platform(
hass, config, DOMAIN, p_type)
if platform is None:
return
_LOGGER.info("Setting up %s.%s", DOMAIN, p_type)
try:
scanner = None
setup = None
if hasattr(platform, 'async_get_scanner'):
scanner = await platform.async_get_scanner(
hass, {DOMAIN: p_config})
elif hasattr(platform, 'get_scanner'):
scanner = await hass.async_add_job(
platform.get_scanner, hass, {DOMAIN: p_config})
elif hasattr(platform, 'async_setup_scanner'):
setup = await platform.async_setup_scanner(
hass, p_config, tracker.async_see, disc_info)
elif hasattr(platform, 'setup_scanner'):
setup = await hass.async_add_job(
platform.setup_scanner, hass, p_config, tracker.see,
disc_info)
elif hasattr(platform, 'async_setup_entry'):
setup = await platform.async_setup_entry(
hass, p_config, tracker.async_see)
else:
raise HomeAssistantError("Invalid device_tracker platform.")
if scanner:
async_setup_scanner_platform(
hass, p_config, scanner, tracker.async_see, p_type)
return
if not setup:
_LOGGER.error("Error setting up platform %s", p_type)
return
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Error setting up platform %s", p_type)
hass.data[DOMAIN] = async_setup_platform
setup_tasks = [async_setup_platform(p_type, p_config) for p_type, p_config
in config_per_platform(config, DOMAIN)]
if setup_tasks:
await asyncio.wait(setup_tasks, loop=hass.loop)
await asyncio.wait(setup_tasks)
tracker.async_setup_group()
async def async_platform_discovered(platform, info):
async def async_platform_discovered(p_type, info):
"""Load a platform."""
await async_setup_platform(platform, {}, disc_info=info)
platform = await setup.async_create_platform_type(
hass, config, p_type, {})
if platform is None or platform.type != PLATFORM_TYPE_LEGACY:
return
await platform.async_setup_legacy(hass, tracker, info)
discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered)
@ -226,537 +154,3 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType):
# restore
await tracker.async_setup_tracked_device()
return True
async def async_setup_entry(hass, entry):
"""Set up an entry."""
await hass.data[DOMAIN](entry.domain, entry)
return True
class DeviceTracker:
"""Representation of a device tracker."""
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
track_new: bool, defaults: dict,
devices: Sequence) -> None:
"""Initialize a device tracker."""
self.hass = hass
self.devices = {dev.dev_id: dev for dev in devices}
self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac}
self.consider_home = consider_home
self.track_new = track_new if track_new is not None \
else defaults.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
self.defaults = defaults
self.group = None
self._is_updating = asyncio.Lock(loop=hass.loop)
for dev in devices:
if self.devices[dev.dev_id] is not dev:
_LOGGER.warning('Duplicate device IDs detected %s', dev.dev_id)
if dev.mac and self.mac_to_dev[dev.mac] is not dev:
_LOGGER.warning('Duplicate device MAC addresses detected %s',
dev.mac)
def see(self, mac: str = None, dev_id: str = None, host_name: str = None,
location_name: str = None, gps: GPSType = None,
gps_accuracy: int = None, battery: int = None,
attributes: dict = None, source_type: str = SOURCE_TYPE_GPS,
picture: str = None, icon: str = None,
consider_home: timedelta = None):
"""Notify the device tracker that you see a device."""
self.hass.add_job(
self.async_see(mac, dev_id, host_name, location_name, gps,
gps_accuracy, battery, attributes, source_type,
picture, icon, consider_home)
)
async def async_see(
self, mac: str = None, dev_id: str = None, host_name: str = None,
location_name: str = None, gps: GPSType = None,
gps_accuracy: int = None, battery: int = None,
attributes: dict = None, source_type: str = SOURCE_TYPE_GPS,
picture: str = None, icon: str = None,
consider_home: timedelta = None):
"""Notify the device tracker that you see a device.
This method is a coroutine.
"""
if mac is None and dev_id is None:
raise HomeAssistantError('Neither mac or device id passed in')
if mac is not None:
mac = str(mac).upper()
device = self.mac_to_dev.get(mac)
if not device:
dev_id = util.slugify(host_name or '') or util.slugify(mac)
else:
dev_id = cv.slug(str(dev_id).lower())
device = self.devices.get(dev_id)
if device:
await device.async_seen(
host_name, location_name, gps, gps_accuracy, battery,
attributes, source_type, consider_home)
if device.track:
await device.async_update_ha_state()
return
# If no device can be found, create it
dev_id = util.ensure_unique_string(dev_id, self.devices.keys())
device = Device(
self.hass, consider_home or self.consider_home, self.track_new,
dev_id, mac, (host_name or dev_id).replace('_', ' '),
picture=picture, icon=icon,
hide_if_away=self.defaults.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE))
self.devices[dev_id] = device
if mac is not None:
self.mac_to_dev[mac] = device
await device.async_seen(
host_name, location_name, gps, gps_accuracy, battery, attributes,
source_type)
if device.track:
await device.async_update_ha_state()
# During init, we ignore the group
if self.group and self.track_new:
self.hass.async_create_task(
self.hass.async_call(
DOMAIN_GROUP, SERVICE_SET, {
ATTR_OBJECT_ID: util.slugify(GROUP_NAME_ALL_DEVICES),
ATTR_VISIBLE: False,
ATTR_NAME: GROUP_NAME_ALL_DEVICES,
ATTR_ADD_ENTITIES: [device.entity_id]}))
self.hass.bus.async_fire(EVENT_NEW_DEVICE, {
ATTR_ENTITY_ID: device.entity_id,
ATTR_HOST_NAME: device.host_name,
ATTR_MAC: device.mac,
})
# update known_devices.yaml
self.hass.async_create_task(
self.async_update_config(
self.hass.config.path(YAML_DEVICES), dev_id, device)
)
async def async_update_config(self, path, dev_id, device):
"""Add device to YAML configuration file.
This method is a coroutine.
"""
async with self._is_updating:
await self.hass.async_add_executor_job(
update_config, self.hass.config.path(YAML_DEVICES),
dev_id, device)
@callback
def async_setup_group(self):
"""Initialize group for all tracked devices.
This method must be run in the event loop.
"""
entity_ids = [dev.entity_id for dev in self.devices.values()
if dev.track]
self.hass.async_create_task(
self.hass.services.async_call(
DOMAIN_GROUP, SERVICE_SET, {
ATTR_OBJECT_ID: util.slugify(GROUP_NAME_ALL_DEVICES),
ATTR_VISIBLE: False,
ATTR_NAME: GROUP_NAME_ALL_DEVICES,
ATTR_ENTITIES: entity_ids}))
@callback
def async_update_stale(self, now: dt_util.dt.datetime):
"""Update stale devices.
This method must be run in the event loop.
"""
for device in self.devices.values():
if (device.track and device.last_update_home) and \
device.stale(now):
self.hass.async_create_task(device.async_update_ha_state(True))
async def async_setup_tracked_device(self):
"""Set up all not exists tracked devices.
This method is a coroutine.
"""
async def async_init_single_device(dev):
"""Init a single device_tracker entity."""
await dev.async_added_to_hass()
await dev.async_update_ha_state()
tasks = []
for device in self.devices.values():
if device.track and not device.last_seen:
tasks.append(self.hass.async_create_task(
async_init_single_device(device)))
if tasks:
await asyncio.wait(tasks, loop=self.hass.loop)
class Device(RestoreEntity):
"""Represent a tracked device."""
host_name = None # type: str
location_name = None # type: str
gps = None # type: GPSType
gps_accuracy = 0 # type: int
last_seen = None # type: dt_util.dt.datetime
consider_home = None # type: dt_util.dt.timedelta
battery = None # type: int
attributes = None # type: dict
icon = None # type: str
# Track if the last update of this device was HOME.
last_update_home = False
_state = STATE_NOT_HOME
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
track: bool, dev_id: str, mac: str, name: str = None,
picture: str = None, gravatar: str = None, icon: str = None,
hide_if_away: bool = False) -> None:
"""Initialize a device."""
self.hass = hass
self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
# Timedelta object how long we consider a device home if it is not
# detected anymore.
self.consider_home = consider_home
# Device ID
self.dev_id = dev_id
self.mac = mac
# If we should track this device
self.track = track
# Configured name
self.config_name = name
# Configured picture
if gravatar is not None:
self.config_picture = get_gravatar_for_email(gravatar)
else:
self.config_picture = picture
self.icon = icon
self.away_hide = hide_if_away
self.source_type = None
self._attributes = {}
@property
def name(self):
"""Return the name of the entity."""
return self.config_name or self.host_name or DEVICE_DEFAULT_NAME
@property
def state(self):
"""Return the state of the device."""
return self._state
@property
def entity_picture(self):
"""Return the picture of the device."""
return self.config_picture
@property
def state_attributes(self):
"""Return the device state attributes."""
attr = {
ATTR_SOURCE_TYPE: self.source_type
}
if self.gps:
attr[ATTR_LATITUDE] = self.gps[0]
attr[ATTR_LONGITUDE] = self.gps[1]
attr[ATTR_GPS_ACCURACY] = self.gps_accuracy
if self.battery:
attr[ATTR_BATTERY] = self.battery
return attr
@property
def device_state_attributes(self):
"""Return device state attributes."""
return self._attributes
@property
def hidden(self):
"""If device should be hidden."""
return self.away_hide and self.state != STATE_HOME
async def async_seen(
self, host_name: str = None, location_name: str = None,
gps: GPSType = None, gps_accuracy=0, battery: int = None,
attributes: dict = None,
source_type: str = SOURCE_TYPE_GPS,
consider_home: timedelta = None):
"""Mark the device as seen."""
self.source_type = source_type
self.last_seen = dt_util.utcnow()
self.host_name = host_name
self.location_name = location_name
self.consider_home = consider_home or self.consider_home
if battery:
self.battery = battery
if attributes:
self._attributes.update(attributes)
self.gps = None
if gps is not None:
try:
self.gps = float(gps[0]), float(gps[1])
self.gps_accuracy = gps_accuracy or 0
except (ValueError, TypeError, IndexError):
self.gps = None
self.gps_accuracy = 0
_LOGGER.warning(
"Could not parse gps value for %s: %s", self.dev_id, gps)
# pylint: disable=not-an-iterable
await self.async_update()
def stale(self, now: dt_util.dt.datetime = None):
"""Return if device state is stale.
Async friendly.
"""
return self.last_seen is None or \
(now or dt_util.utcnow()) - self.last_seen > self.consider_home
def mark_stale(self):
"""Mark the device state as stale."""
self._state = STATE_NOT_HOME
self.gps = None
self.last_update_home = False
async def async_update(self):
"""Update state of entity.
This method is a coroutine.
"""
if not self.last_seen:
return
if self.location_name:
self._state = self.location_name
elif self.gps is not None and self.source_type == SOURCE_TYPE_GPS:
zone_state = async_active_zone(
self.hass, self.gps[0], self.gps[1], self.gps_accuracy)
if zone_state is None:
self._state = STATE_NOT_HOME
elif zone_state.entity_id == zone.ENTITY_ID_HOME:
self._state = STATE_HOME
else:
self._state = zone_state.name
elif self.stale():
self.mark_stale()
else:
self._state = STATE_HOME
self.last_update_home = True
async def async_added_to_hass(self):
"""Add an entity."""
await super().async_added_to_hass()
state = await self.async_get_last_state()
if not state:
return
self._state = state.state
self.last_update_home = (state.state == STATE_HOME)
self.last_seen = dt_util.utcnow()
for attr, var in (
(ATTR_SOURCE_TYPE, 'source_type'),
(ATTR_GPS_ACCURACY, 'gps_accuracy'),
(ATTR_BATTERY, 'battery'),
):
if attr in state.attributes:
setattr(self, var, state.attributes[attr])
if ATTR_LONGITUDE in state.attributes:
self.gps = (state.attributes[ATTR_LATITUDE],
state.attributes[ATTR_LONGITUDE])
class DeviceScanner:
"""Device scanner object."""
hass = None # type: HomeAssistantType
def scan_devices(self) -> List[str]:
"""Scan for devices."""
raise NotImplementedError()
def async_scan_devices(self) -> Any:
"""Scan for devices.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.async_add_job(self.scan_devices)
def get_device_name(self, device: str) -> str:
"""Get the name of a device."""
raise NotImplementedError()
def async_get_device_name(self, device: str) -> Any:
"""Get the name of a device.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.async_add_job(self.get_device_name, device)
def get_extra_attributes(self, device: str) -> dict:
"""Get the extra attributes of a device."""
raise NotImplementedError()
def async_get_extra_attributes(self, device: str) -> Any:
"""Get the extra attributes of a device.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.async_add_job(self.get_extra_attributes, device)
def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
"""Load devices from YAML configuration file."""
return run_coroutine_threadsafe(
async_load_config(path, hass, consider_home), hass.loop).result()
async def async_load_config(path: str, hass: HomeAssistantType,
consider_home: timedelta):
"""Load devices from YAML configuration file.
This method is a coroutine.
"""
dev_schema = vol.Schema({
vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_ICON, default=None): vol.Any(None, cv.icon),
vol.Optional('track', default=False): cv.boolean,
vol.Optional(CONF_MAC, default=None):
vol.Any(None, vol.All(cv.string, vol.Upper)),
vol.Optional(CONF_AWAY_HIDE, default=DEFAULT_AWAY_HIDE): cv.boolean,
vol.Optional('gravatar', default=None): vol.Any(None, cv.string),
vol.Optional('picture', default=None): vol.Any(None, cv.string),
vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All(
cv.time_period, cv.positive_timedelta),
})
try:
result = []
try:
devices = await hass.async_add_job(
load_yaml_config_file, path)
except HomeAssistantError as err:
_LOGGER.error("Unable to load %s: %s", path, str(err))
return []
for dev_id, device in devices.items():
# Deprecated option. We just ignore it to avoid breaking change
device.pop('vendor', None)
try:
device = dev_schema(device)
device['dev_id'] = cv.slugify(dev_id)
except vol.Invalid as exp:
async_log_exception(exp, dev_id, devices, hass)
else:
result.append(Device(hass, **device))
return result
except (HomeAssistantError, FileNotFoundError):
# When YAML file could not be loaded/did not contain a dict
return []
@callback
def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
scanner: Any, async_see_device: Callable,
platform: str):
"""Set up the connect scanner-based platform to device tracker.
This method must be run in the event loop.
"""
interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
update_lock = asyncio.Lock(loop=hass.loop)
scanner.hass = hass
# Initial scan of each mac we also tell about host name for config
seen = set() # type: Any
async def async_device_tracker_scan(now: dt_util.dt.datetime):
"""Handle interval matches."""
if update_lock.locked():
_LOGGER.warning(
"Updating device list from %s took longer than the scheduled "
"scan interval %s", platform, interval)
return
async with update_lock:
found_devices = await scanner.async_scan_devices()
for mac in found_devices:
if mac in seen:
host_name = None
else:
host_name = await scanner.async_get_device_name(mac)
seen.add(mac)
try:
extra_attributes = \
await scanner.async_get_extra_attributes(mac)
except NotImplementedError:
extra_attributes = dict()
kwargs = {
'mac': mac,
'host_name': host_name,
'source_type': SOURCE_TYPE_ROUTER,
'attributes': {
'scanner': scanner.__class__.__name__,
**extra_attributes
}
}
zone_home = hass.states.get(zone.ENTITY_ID_HOME)
if zone_home:
kwargs['gps'] = [zone_home.attributes[ATTR_LATITUDE],
zone_home.attributes[ATTR_LONGITUDE]]
kwargs['gps_accuracy'] = 0
hass.async_create_task(async_see_device(**kwargs))
async_track_time_interval(hass, async_device_tracker_scan, interval)
hass.async_create_task(async_device_tracker_scan(None))
def update_config(path: str, dev_id: str, device: Device):
"""Add device to YAML configuration file."""
with open(path, 'a') as out:
device = {device.dev_id: {
ATTR_NAME: device.name,
ATTR_MAC: device.mac,
ATTR_ICON: device.icon,
'picture': device.config_picture,
'track': device.track,
CONF_AWAY_HIDE: device.away_hide,
}}
out.write('\n')
out.write(dump(device))
def get_gravatar_for_email(email: str):
"""Return an 80px Gravatar for the given email address.
Async friendly.
"""
import hashlib
url = 'https://www.gravatar.com/avatar/{}.jpg?s=80&d=wavatar'
return url.format(hashlib.md5(email.encode('utf-8').lower()).hexdigest())

View File

@ -0,0 +1,114 @@
"""Code to set up a device tracker platform using a config entry."""
from typing import Optional
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.const import (
STATE_NOT_HOME,
STATE_HOME,
ATTR_GPS_ACCURACY,
ATTR_LATITUDE,
ATTR_LONGITUDE,
ATTR_BATTERY_LEVEL,
)
from homeassistant.components import zone
from .const import (
ATTR_SOURCE_TYPE,
DOMAIN,
LOGGER,
)
async def async_setup_entry(hass, entry):
"""Set up an entry."""
component = hass.data.get(DOMAIN) # type: Optional[EntityComponent]
if component is None:
component = hass.data[DOMAIN] = EntityComponent(
LOGGER, DOMAIN, hass
)
return await component.async_setup_entry(entry)
async def async_unload_entry(hass, entry):
"""Unload an entry."""
return await hass.data[DOMAIN].async_unload_entry(entry)
class DeviceTrackerEntity(Entity):
"""Represent a tracked device."""
@property
def battery_level(self):
"""Return the battery level of the device.
Percentage from 0-100.
"""
return None
@property
def location_accuracy(self):
"""Return the location accuracy of the device.
Value in meters.
"""
return 0
@property
def location_name(self) -> str:
"""Return a location name for the current location of the device."""
return None
@property
def latitude(self) -> float:
"""Return latitude value of the device."""
return NotImplementedError
@property
def longitude(self) -> float:
"""Return longitude value of the device."""
return NotImplementedError
@property
def source_type(self):
"""Return the source type, eg gps or router, of the device."""
raise NotImplementedError
@property
def state(self):
"""Return the state of the device."""
if self.location_name:
return self.location_name
if self.latitude is not None:
zone_state = zone.async_active_zone(
self.hass, self.latitude, self.longitude,
self.location_accuracy)
if zone_state is None:
state = STATE_NOT_HOME
elif zone_state.entity_id == zone.ENTITY_ID_HOME:
state = STATE_HOME
else:
state = zone_state.name
return state
return None
@property
def state_attributes(self):
"""Return the device state attributes."""
attr = {
ATTR_SOURCE_TYPE: self.source_type
}
if self.latitude is not None:
attr[ATTR_LATITUDE] = self.latitude
attr[ATTR_LONGITUDE] = self.longitude
attr[ATTR_GPS_ACCURACY] = self.location_accuracy
if self.battery_level:
attr[ATTR_BATTERY_LEVEL] = self.battery_level
return attr

View File

@ -0,0 +1,40 @@
"""Device tracker constants."""
from datetime import timedelta
import logging
LOGGER = logging.getLogger(__package__)
DOMAIN = 'device_tracker'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
PLATFORM_TYPE_LEGACY = 'legacy'
PLATFORM_TYPE_ENTITY = 'entity_platform'
SOURCE_TYPE_GPS = 'gps'
SOURCE_TYPE_ROUTER = 'router'
SOURCE_TYPE_BLUETOOTH = 'bluetooth'
SOURCE_TYPE_BLUETOOTH_LE = 'bluetooth_le'
CONF_SCAN_INTERVAL = 'interval_seconds'
SCAN_INTERVAL = timedelta(seconds=12)
CONF_TRACK_NEW = 'track_new_devices'
DEFAULT_TRACK_NEW = True
CONF_AWAY_HIDE = 'hide_if_away'
DEFAULT_AWAY_HIDE = False
CONF_CONSIDER_HOME = 'consider_home'
DEFAULT_CONSIDER_HOME = timedelta(seconds=180)
CONF_NEW_DEVICE_DEFAULTS = 'new_device_defaults'
ATTR_ATTRIBUTES = 'attributes'
ATTR_BATTERY = 'battery'
ATTR_DEV_ID = 'dev_id'
ATTR_GPS = 'gps'
ATTR_HOST_NAME = 'host_name'
ATTR_LOCATION_NAME = 'location_name'
ATTR_MAC = 'mac'
ATTR_SOURCE_TYPE = 'source_type'
ATTR_CONSIDER_HOME = 'consider_home'

View File

@ -0,0 +1,526 @@
"""Legacy device tracker classes."""
import asyncio
from datetime import timedelta
from typing import Any, List, Sequence
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components import zone
from homeassistant.components.group import (
ATTR_ADD_ENTITIES, ATTR_ENTITIES, ATTR_OBJECT_ID, ATTR_VISIBLE,
DOMAIN as DOMAIN_GROUP, SERVICE_SET)
from homeassistant.components.zone import async_active_zone
from homeassistant.config import load_yaml_config_file, async_log_exception
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import GPSType, HomeAssistantType
from homeassistant import util
import homeassistant.util.dt as dt_util
from homeassistant.util.yaml import dump
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_GPS_ACCURACY, ATTR_ICON, ATTR_LATITUDE,
ATTR_LONGITUDE, ATTR_NAME, CONF_ICON, CONF_MAC, CONF_NAME,
DEVICE_DEFAULT_NAME, STATE_NOT_HOME, STATE_HOME)
from .const import (
ATTR_BATTERY,
ATTR_HOST_NAME,
ATTR_MAC,
ATTR_SOURCE_TYPE,
CONF_AWAY_HIDE,
CONF_CONSIDER_HOME,
CONF_NEW_DEVICE_DEFAULTS,
CONF_TRACK_NEW,
DEFAULT_AWAY_HIDE,
DEFAULT_CONSIDER_HOME,
DEFAULT_TRACK_NEW,
DOMAIN,
ENTITY_ID_FORMAT,
LOGGER,
SOURCE_TYPE_GPS,
)
YAML_DEVICES = 'known_devices.yaml'
GROUP_NAME_ALL_DEVICES = 'all devices'
EVENT_NEW_DEVICE = 'device_tracker_new_device'
async def get_tracker(hass, config):
"""Create a tracker."""
yaml_path = hass.config.path(YAML_DEVICES)
conf = config.get(DOMAIN, [])
conf = conf[0] if conf else {}
consider_home = conf.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME)
defaults = conf.get(CONF_NEW_DEVICE_DEFAULTS, {})
track_new = conf.get(CONF_TRACK_NEW)
if track_new is None:
track_new = defaults.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
devices = await async_load_config(yaml_path, hass, consider_home)
tracker = DeviceTracker(
hass, consider_home, track_new, defaults, devices)
return tracker
class DeviceTracker:
"""Representation of a device tracker."""
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
track_new: bool, defaults: dict,
devices: Sequence) -> None:
"""Initialize a device tracker."""
self.hass = hass
self.devices = {dev.dev_id: dev for dev in devices}
self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac}
self.consider_home = consider_home
self.track_new = track_new if track_new is not None \
else defaults.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
self.defaults = defaults
self.group = None
self._is_updating = asyncio.Lock()
for dev in devices:
if self.devices[dev.dev_id] is not dev:
LOGGER.warning('Duplicate device IDs detected %s', dev.dev_id)
if dev.mac and self.mac_to_dev[dev.mac] is not dev:
LOGGER.warning('Duplicate device MAC addresses detected %s',
dev.mac)
def see(self, mac: str = None, dev_id: str = None, host_name: str = None,
location_name: str = None, gps: GPSType = None,
gps_accuracy: int = None, battery: int = None,
attributes: dict = None, source_type: str = SOURCE_TYPE_GPS,
picture: str = None, icon: str = None,
consider_home: timedelta = None):
"""Notify the device tracker that you see a device."""
self.hass.add_job(
self.async_see(mac, dev_id, host_name, location_name, gps,
gps_accuracy, battery, attributes, source_type,
picture, icon, consider_home)
)
async def async_see(
self, mac: str = None, dev_id: str = None, host_name: str = None,
location_name: str = None, gps: GPSType = None,
gps_accuracy: int = None, battery: int = None,
attributes: dict = None, source_type: str = SOURCE_TYPE_GPS,
picture: str = None, icon: str = None,
consider_home: timedelta = None):
"""Notify the device tracker that you see a device.
This method is a coroutine.
"""
if mac is None and dev_id is None:
raise HomeAssistantError('Neither mac or device id passed in')
if mac is not None:
mac = str(mac).upper()
device = self.mac_to_dev.get(mac)
if not device:
dev_id = util.slugify(host_name or '') or util.slugify(mac)
else:
dev_id = cv.slug(str(dev_id).lower())
device = self.devices.get(dev_id)
if device:
await device.async_seen(
host_name, location_name, gps, gps_accuracy, battery,
attributes, source_type, consider_home)
if device.track:
await device.async_update_ha_state()
return
# If no device can be found, create it
dev_id = util.ensure_unique_string(dev_id, self.devices.keys())
device = Device(
self.hass, consider_home or self.consider_home, self.track_new,
dev_id, mac, (host_name or dev_id).replace('_', ' '),
picture=picture, icon=icon,
hide_if_away=self.defaults.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE))
self.devices[dev_id] = device
if mac is not None:
self.mac_to_dev[mac] = device
await device.async_seen(
host_name, location_name, gps, gps_accuracy, battery, attributes,
source_type)
if device.track:
await device.async_update_ha_state()
# During init, we ignore the group
if self.group and self.track_new:
self.hass.async_create_task(
self.hass.async_call(
DOMAIN_GROUP, SERVICE_SET, {
ATTR_OBJECT_ID: util.slugify(GROUP_NAME_ALL_DEVICES),
ATTR_VISIBLE: False,
ATTR_NAME: GROUP_NAME_ALL_DEVICES,
ATTR_ADD_ENTITIES: [device.entity_id]}))
self.hass.bus.async_fire(EVENT_NEW_DEVICE, {
ATTR_ENTITY_ID: device.entity_id,
ATTR_HOST_NAME: device.host_name,
ATTR_MAC: device.mac,
})
# update known_devices.yaml
self.hass.async_create_task(
self.async_update_config(
self.hass.config.path(YAML_DEVICES), dev_id, device)
)
async def async_update_config(self, path, dev_id, device):
"""Add device to YAML configuration file.
This method is a coroutine.
"""
async with self._is_updating:
await self.hass.async_add_executor_job(
update_config, self.hass.config.path(YAML_DEVICES),
dev_id, device)
@callback
def async_setup_group(self):
"""Initialize group for all tracked devices.
This method must be run in the event loop.
"""
entity_ids = [dev.entity_id for dev in self.devices.values()
if dev.track]
self.hass.async_create_task(
self.hass.services.async_call(
DOMAIN_GROUP, SERVICE_SET, {
ATTR_OBJECT_ID: util.slugify(GROUP_NAME_ALL_DEVICES),
ATTR_VISIBLE: False,
ATTR_NAME: GROUP_NAME_ALL_DEVICES,
ATTR_ENTITIES: entity_ids}))
@callback
def async_update_stale(self, now: dt_util.dt.datetime):
"""Update stale devices.
This method must be run in the event loop.
"""
for device in self.devices.values():
if (device.track and device.last_update_home) and \
device.stale(now):
self.hass.async_create_task(device.async_update_ha_state(True))
async def async_setup_tracked_device(self):
"""Set up all not exists tracked devices.
This method is a coroutine.
"""
async def async_init_single_device(dev):
"""Init a single device_tracker entity."""
await dev.async_added_to_hass()
await dev.async_update_ha_state()
tasks = []
for device in self.devices.values():
if device.track and not device.last_seen:
tasks.append(self.hass.async_create_task(
async_init_single_device(device)))
if tasks:
await asyncio.wait(tasks)
class Device(RestoreEntity):
"""Represent a tracked device."""
host_name = None # type: str
location_name = None # type: str
gps = None # type: GPSType
gps_accuracy = 0 # type: int
last_seen = None # type: dt_util.dt.datetime
consider_home = None # type: dt_util.dt.timedelta
battery = None # type: int
attributes = None # type: dict
icon = None # type: str
# Track if the last update of this device was HOME.
last_update_home = False
_state = STATE_NOT_HOME
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
track: bool, dev_id: str, mac: str, name: str = None,
picture: str = None, gravatar: str = None, icon: str = None,
hide_if_away: bool = False) -> None:
"""Initialize a device."""
self.hass = hass
self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
# Timedelta object how long we consider a device home if it is not
# detected anymore.
self.consider_home = consider_home
# Device ID
self.dev_id = dev_id
self.mac = mac
# If we should track this device
self.track = track
# Configured name
self.config_name = name
# Configured picture
if gravatar is not None:
self.config_picture = get_gravatar_for_email(gravatar)
else:
self.config_picture = picture
self.icon = icon
self.away_hide = hide_if_away
self.source_type = None
self._attributes = {}
@property
def name(self):
"""Return the name of the entity."""
return self.config_name or self.host_name or DEVICE_DEFAULT_NAME
@property
def state(self):
"""Return the state of the device."""
return self._state
@property
def entity_picture(self):
"""Return the picture of the device."""
return self.config_picture
@property
def state_attributes(self):
"""Return the device state attributes."""
attr = {
ATTR_SOURCE_TYPE: self.source_type
}
if self.gps:
attr[ATTR_LATITUDE] = self.gps[0]
attr[ATTR_LONGITUDE] = self.gps[1]
attr[ATTR_GPS_ACCURACY] = self.gps_accuracy
if self.battery:
attr[ATTR_BATTERY] = self.battery
return attr
@property
def device_state_attributes(self):
"""Return device state attributes."""
return self._attributes
@property
def hidden(self):
"""If device should be hidden."""
return self.away_hide and self.state != STATE_HOME
async def async_seen(
self, host_name: str = None, location_name: str = None,
gps: GPSType = None, gps_accuracy=0, battery: int = None,
attributes: dict = None,
source_type: str = SOURCE_TYPE_GPS,
consider_home: timedelta = None):
"""Mark the device as seen."""
self.source_type = source_type
self.last_seen = dt_util.utcnow()
self.host_name = host_name
self.location_name = location_name
self.consider_home = consider_home or self.consider_home
if battery:
self.battery = battery
if attributes:
self._attributes.update(attributes)
self.gps = None
if gps is not None:
try:
self.gps = float(gps[0]), float(gps[1])
self.gps_accuracy = gps_accuracy or 0
except (ValueError, TypeError, IndexError):
self.gps = None
self.gps_accuracy = 0
LOGGER.warning(
"Could not parse gps value for %s: %s", self.dev_id, gps)
# pylint: disable=not-an-iterable
await self.async_update()
def stale(self, now: dt_util.dt.datetime = None):
"""Return if device state is stale.
Async friendly.
"""
return self.last_seen is None or \
(now or dt_util.utcnow()) - self.last_seen > self.consider_home
def mark_stale(self):
"""Mark the device state as stale."""
self._state = STATE_NOT_HOME
self.gps = None
self.last_update_home = False
async def async_update(self):
"""Update state of entity.
This method is a coroutine.
"""
if not self.last_seen:
return
if self.location_name:
self._state = self.location_name
elif self.gps is not None and self.source_type == SOURCE_TYPE_GPS:
zone_state = async_active_zone(
self.hass, self.gps[0], self.gps[1], self.gps_accuracy)
if zone_state is None:
self._state = STATE_NOT_HOME
elif zone_state.entity_id == zone.ENTITY_ID_HOME:
self._state = STATE_HOME
else:
self._state = zone_state.name
elif self.stale():
self.mark_stale()
else:
self._state = STATE_HOME
self.last_update_home = True
async def async_added_to_hass(self):
"""Add an entity."""
await super().async_added_to_hass()
state = await self.async_get_last_state()
if not state:
return
self._state = state.state
self.last_update_home = (state.state == STATE_HOME)
self.last_seen = dt_util.utcnow()
for attr, var in (
(ATTR_SOURCE_TYPE, 'source_type'),
(ATTR_GPS_ACCURACY, 'gps_accuracy'),
(ATTR_BATTERY, 'battery'),
):
if attr in state.attributes:
setattr(self, var, state.attributes[attr])
if ATTR_LONGITUDE in state.attributes:
self.gps = (state.attributes[ATTR_LATITUDE],
state.attributes[ATTR_LONGITUDE])
class DeviceScanner:
"""Device scanner object."""
hass = None # type: HomeAssistantType
def scan_devices(self) -> List[str]:
"""Scan for devices."""
raise NotImplementedError()
def async_scan_devices(self) -> Any:
"""Scan for devices.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.async_add_job(self.scan_devices)
def get_device_name(self, device: str) -> str:
"""Get the name of a device."""
raise NotImplementedError()
def async_get_device_name(self, device: str) -> Any:
"""Get the name of a device.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.async_add_job(self.get_device_name, device)
def get_extra_attributes(self, device: str) -> dict:
"""Get the extra attributes of a device."""
raise NotImplementedError()
def async_get_extra_attributes(self, device: str) -> Any:
"""Get the extra attributes of a device.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.async_add_job(self.get_extra_attributes, device)
async def async_load_config(path: str, hass: HomeAssistantType,
consider_home: timedelta):
"""Load devices from YAML configuration file.
This method is a coroutine.
"""
dev_schema = vol.Schema({
vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_ICON, default=None): vol.Any(None, cv.icon),
vol.Optional('track', default=False): cv.boolean,
vol.Optional(CONF_MAC, default=None):
vol.Any(None, vol.All(cv.string, vol.Upper)),
vol.Optional(CONF_AWAY_HIDE, default=DEFAULT_AWAY_HIDE): cv.boolean,
vol.Optional('gravatar', default=None): vol.Any(None, cv.string),
vol.Optional('picture', default=None): vol.Any(None, cv.string),
vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All(
cv.time_period, cv.positive_timedelta),
})
result = []
try:
devices = await hass.async_add_job(
load_yaml_config_file, path)
except HomeAssistantError as err:
LOGGER.error("Unable to load %s: %s", path, str(err))
return []
except FileNotFoundError:
return []
for dev_id, device in devices.items():
# Deprecated option. We just ignore it to avoid breaking change
device.pop('vendor', None)
try:
device = dev_schema(device)
device['dev_id'] = cv.slugify(dev_id)
except vol.Invalid as exp:
async_log_exception(exp, dev_id, devices, hass)
else:
result.append(Device(hass, **device))
return result
def update_config(path: str, dev_id: str, device: Device):
"""Add device to YAML configuration file."""
with open(path, 'a') as out:
device = {device.dev_id: {
ATTR_NAME: device.name,
ATTR_MAC: device.mac,
ATTR_ICON: device.icon,
'picture': device.config_picture,
'track': device.track,
CONF_AWAY_HIDE: device.away_hide,
}}
out.write('\n')
out.write(dump(device))
def get_gravatar_for_email(email: str):
"""Return an 80px Gravatar for the given email address.
Async friendly.
"""
import hashlib
url = 'https://www.gravatar.com/avatar/{}.jpg?s=80&d=wavatar'
return url.format(hashlib.md5(email.encode('utf-8').lower()).hexdigest())

View File

@ -0,0 +1,184 @@
"""Device tracker helpers."""
import asyncio
from typing import Dict, Any, Callable, Optional
from types import ModuleType
import attr
from homeassistant.core import callback
from homeassistant.setup import async_prepare_setup_platform
from homeassistant.helpers import config_per_platform
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.util import dt as dt_util
from homeassistant.const import (
ATTR_LATITUDE,
ATTR_LONGITUDE,
)
from .const import (
DOMAIN,
PLATFORM_TYPE_LEGACY,
CONF_SCAN_INTERVAL,
SCAN_INTERVAL,
SOURCE_TYPE_ROUTER,
LOGGER,
)
@attr.s
class DeviceTrackerPlatform:
"""Class to hold platform information."""
LEGACY_SETUP = (
'async_get_scanner',
'get_scanner',
'async_setup_scanner',
'setup_scanner',
)
name = attr.ib(type=str)
platform = attr.ib(type=ModuleType)
config = attr.ib(type=Dict)
@property
def type(self):
"""Return platform type."""
for methods, platform_type in (
(self.LEGACY_SETUP, PLATFORM_TYPE_LEGACY),
):
for meth in methods:
if hasattr(self.platform, meth):
return platform_type
return None
async def async_setup_legacy(self, hass, tracker, discovery_info=None):
"""Set up a legacy platform."""
LOGGER.info("Setting up %s.%s", DOMAIN, self.type)
try:
scanner = None
setup = None
if hasattr(self.platform, 'async_get_scanner'):
scanner = await self.platform.async_get_scanner(
hass, {DOMAIN: self.config})
elif hasattr(self.platform, 'get_scanner'):
scanner = await hass.async_add_job(
self.platform.get_scanner, hass, {DOMAIN: self.config})
elif hasattr(self.platform, 'async_setup_scanner'):
setup = await self.platform.async_setup_scanner(
hass, self.config, tracker.async_see, discovery_info)
elif hasattr(self.platform, 'setup_scanner'):
setup = await hass.async_add_job(
self.platform.setup_scanner, hass, self.config,
tracker.see, discovery_info)
else:
raise HomeAssistantError(
"Invalid legacy device_tracker platform.")
if scanner:
async_setup_scanner_platform(
hass, self.config, scanner, tracker.async_see, self.type)
return
if not setup:
LOGGER.error("Error setting up platform %s", self.type)
return
except Exception: # pylint: disable=broad-except
LOGGER.exception("Error setting up platform %s", self.type)
async def async_extract_config(hass, config):
"""Extract device tracker config and split between legacy and modern."""
legacy = []
for platform in await asyncio.gather(*[
async_create_platform_type(hass, config, p_type, p_config)
for p_type, p_config in config_per_platform(config, DOMAIN)
]):
if platform is None:
continue
if platform.type == PLATFORM_TYPE_LEGACY:
legacy.append(platform)
else:
raise ValueError("Unable to determine type for {}: {}".format(
platform.name, platform.type))
return legacy
async def async_create_platform_type(hass, config, p_type, p_config) \
-> Optional[DeviceTrackerPlatform]:
"""Determine type of platform."""
platform = await async_prepare_setup_platform(
hass, config, DOMAIN, p_type)
if platform is None:
return None
return DeviceTrackerPlatform(p_type, platform, p_config)
@callback
def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
scanner: Any, async_see_device: Callable,
platform: str):
"""Set up the connect scanner-based platform to device tracker.
This method must be run in the event loop.
"""
interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
update_lock = asyncio.Lock()
scanner.hass = hass
# Initial scan of each mac we also tell about host name for config
seen = set() # type: Any
async def async_device_tracker_scan(now: dt_util.dt.datetime):
"""Handle interval matches."""
if update_lock.locked():
LOGGER.warning(
"Updating device list from %s took longer than the scheduled "
"scan interval %s", platform, interval)
return
async with update_lock:
found_devices = await scanner.async_scan_devices()
for mac in found_devices:
if mac in seen:
host_name = None
else:
host_name = await scanner.async_get_device_name(mac)
seen.add(mac)
try:
extra_attributes = \
await scanner.async_get_extra_attributes(mac)
except NotImplementedError:
extra_attributes = dict()
kwargs = {
'mac': mac,
'host_name': host_name,
'source_type': SOURCE_TYPE_ROUTER,
'attributes': {
'scanner': scanner.__class__.__name__,
**extra_attributes
}
}
zone_home = hass.states.get(hass.components.zone.ENTITY_ID_HOME)
if zone_home:
kwargs['gps'] = [zone_home.attributes[ATTR_LATITUDE],
zone_home.attributes[ATTR_LONGITUDE]]
kwargs['gps_accuracy'] = 0
hass.async_create_task(async_see_device(**kwargs))
async_track_time_interval(hass, async_device_tracker_scan, interval)
hass.async_create_task(async_device_tracker_scan(None))

View File

@ -8,9 +8,10 @@ from homeassistant.const import CONF_WEBHOOK_ID
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import intent, template, config_entry_flow
_LOGGER = logging.getLogger(__name__)
from .const import DOMAIN
DOMAIN = 'dialogflow'
_LOGGER = logging.getLogger(__name__)
SOURCE = "Home Assistant Dialogflow"
@ -83,16 +84,6 @@ async def async_unload_entry(hass, entry):
async_remove_entry = config_entry_flow.webhook_async_remove_entry
config_entry_flow.register_webhook_flow(
DOMAIN,
'Dialogflow Webhook',
{
'dialogflow_url': 'https://dialogflow.com/docs/fulfillment#webhook',
'docs_url': 'https://www.home-assistant.io/components/dialogflow/'
}
)
def dialogflow_error_response(message, error):
"""Return a response saying the error message."""
dialogflow_response = DialogflowResponse(message['result']['parameters'])

View File

@ -0,0 +1,13 @@
"""Config flow for DialogFlow."""
from homeassistant.helpers import config_entry_flow
from .const import DOMAIN
config_entry_flow.register_webhook_flow(
DOMAIN,
'Dialogflow Webhook',
{
'dialogflow_url': 'https://dialogflow.com/docs/fulfillment#webhook',
'docs_url': 'https://www.home-assistant.io/components/dialogflow/'
}
)

View File

@ -0,0 +1,3 @@
"""Const for DialogFlow."""
DOMAIN = "dialogflow"

View File

@ -1,6 +1,7 @@
{
"domain": "dialogflow",
"name": "Dialogflow",
"config_flow": true,
"documentation": "https://www.home-assistant.io/components/dialogflow",
"requirements": [],
"dependencies": [

View File

@ -46,7 +46,7 @@ class DiscordNotificationService(BaseNotificationService):
import discord
discord.VoiceClient.warn_nacl = False
discord_bot = discord.Client(loop=self.hass.loop)
discord_bot = discord.Client()
images = None
if ATTR_TARGET not in kwargs:

View File

@ -24,19 +24,14 @@ DOMAIN = 'discovery'
SCAN_INTERVAL = timedelta(seconds=300)
SERVICE_APPLE_TV = 'apple_tv'
SERVICE_AXIS = 'axis'
SERVICE_DAIKIN = 'daikin'
SERVICE_DECONZ = 'deconz'
SERVICE_DLNA_DMR = 'dlna_dmr'
SERVICE_ENIGMA2 = 'enigma2'
SERVICE_FREEBOX = 'freebox'
SERVICE_HASS_IOS_APP = 'hass_ios'
SERVICE_HASSIO = 'hassio'
SERVICE_HOMEKIT = 'homekit'
SERVICE_HEOS = 'heos'
SERVICE_HUE = 'philips_hue'
SERVICE_IGD = 'igd'
SERVICE_IKEA_TRADFRI = 'ikea_tradfri'
SERVICE_KONNECTED = 'konnected'
SERVICE_MOBILE_APP = 'hass_mobile_app'
SERVICE_NETGEAR = 'netgear_router'
@ -51,15 +46,10 @@ SERVICE_WINK = 'wink'
SERVICE_XIAOMI_GW = 'xiaomi_gw'
CONFIG_ENTRY_HANDLERS = {
SERVICE_AXIS: 'axis',
SERVICE_DAIKIN: 'daikin',
SERVICE_DECONZ: 'deconz',
'esphome': 'esphome',
'google_cast': 'cast',
SERVICE_HEOS: 'heos',
SERVICE_HUE: 'hue',
SERVICE_TELLDUSLIVE: 'tellduslive',
SERVICE_IKEA_TRADFRI: 'tradfri',
'sonos': 'sonos',
SERVICE_IGD: 'upnp',
}
@ -101,12 +91,22 @@ SERVICE_HANDLERS = {
}
OPTIONAL_SERVICE_HANDLERS = {
SERVICE_HOMEKIT: ('homekit_controller', None),
SERVICE_DLNA_DMR: ('media_player', 'dlna_dmr'),
}
DEFAULT_ENABLED = list(CONFIG_ENTRY_HANDLERS) + list(SERVICE_HANDLERS)
DEFAULT_DISABLED = list(OPTIONAL_SERVICE_HANDLERS)
MIGRATED_SERVICE_HANDLERS = {
'axis': None,
'deconz': None,
'esphome': None,
'ikea_tradfri': None,
'homekit': None,
'philips_hue': None
}
DEFAULT_ENABLED = list(CONFIG_ENTRY_HANDLERS) + list(SERVICE_HANDLERS) + \
list(MIGRATED_SERVICE_HANDLERS)
DEFAULT_DISABLED = list(OPTIONAL_SERVICE_HANDLERS) + \
list(MIGRATED_SERVICE_HANDLERS)
CONF_IGNORE = 'ignore'
CONF_ENABLE = 'enable'
@ -153,6 +153,9 @@ async def async_setup(hass, config):
async def new_service_found(service, info):
"""Handle a new service if one is found."""
if service in MIGRATED_SERVICE_HANDLERS:
return
if service in ignored_platforms:
logger.info("Ignoring service: %s %s", service, info)
return

View File

@ -103,7 +103,6 @@ async def async_start_event_handler(
requester,
listen_port=server_port,
listen_host=server_host,
loop=hass.loop,
callback_url=callback_url_override)
await server.start_server()
_LOGGER.info(

View File

@ -63,7 +63,7 @@ class WanIpSensor(Entity):
self.hass = hass
self._name = name
self.hostname = hostname
self.resolver = aiodns.DNSResolver(loop=self.hass.loop)
self.resolver = aiodns.DNSResolver()
self.resolver.nameservers = [resolver]
self.querytype = 'AAAA' if ipv6 else 'A'
self._state = None

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