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

View File

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

View File

@ -2,108 +2,20 @@
trigger: trigger:
batch: true batch: true
branches:
include:
- dev
- master
tags: tags:
include: include:
- '*' - '*'
pr: none
variables: variables:
- name: versionBuilder - name: versionBuilder
value: '3.2' value: '3.2'
- name: versionWheels
value: '0.7'
- group: docker - group: docker
- group: wheels
- group: github - group: github
- group: twine - group: twine
jobs: 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' - job: 'VersionValidate'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags') 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() stop = time()
_LOGGER.info("Home Assistant initialized in %.2fs", stop-start) _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 # TEMP: warn users for invalid slugs
# Remove after 0.94 or 1.0 # Remove after 0.94 or 1.0
if cv.INVALID_SLUGS_FOUND or cv.INVALID_ENTITY_IDS_FOUND: 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] tasks = [alert.async_update_ha_state() for alert in entities]
if tasks: if tasks:
await asyncio.wait(tasks, loop=hass.loop) await asyncio.wait(tasks)
return True return True

View File

@ -39,7 +39,7 @@ class Auth:
self._prefs = None self._prefs = None
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) 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): async def async_do_auth(self, accept_grant_code):
"""Do authentication with an AcceptGrant code.""" """Do authentication with an AcceptGrant code."""
@ -97,7 +97,7 @@ class Auth:
try: try:
session = aiohttp_client.async_get_clientsession(self.hass) 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, response = await session.post(LWA_TOKEN_URI,
headers=LWA_HEADERS, headers=LWA_HEADERS,
data=lwa_params, data=lwa_params,

View File

@ -1432,7 +1432,7 @@ async def async_send_changereport_message(hass, config, alexa_entity):
try: try:
session = aiohttp_client.async_get_clientsession(hass) 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, response = await session.post(config.endpoint,
headers=headers, headers=headers,
json=message_serialized, 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 return
if _token_info: if _token_info:
await store.async_save(token_info) await store.async_save(_token_info)
token_info = _token_info token_info = _token_info
data_connection = ambiclimate.AmbiclimateConnection(oauth, data_connection = ambiclimate.AmbiclimateConnection(oauth,

View File

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

View File

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

View File

@ -203,8 +203,7 @@ class AmcrestCam(Camera):
"""Return the camera model.""" """Return the camera model."""
return self._model return self._model
@property async def stream_source(self):
def stream_source(self):
"""Return the source of the stream.""" """Return the source of the stream."""
return self._api.rtsp_url(typeno=self._resolution) 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]] tasks = [async_setup_ipcamera(conf) for conf in config[DOMAIN]]
if tasks: if tasks:
await asyncio.wait(tasks, loop=hass.loop) await asyncio.wait(tasks)
return True 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: if CONF_ADB_SERVER_IP not in config:
# Use "python-adb" (Python ADB implementation) # Use "python-adb" (Python ADB implementation)
adb_log = "using Python ADB implementation "
if CONF_ADBKEY in config: if CONF_ADBKEY in config:
aftv = setup(host, config[CONF_ADBKEY], aftv = setup(host, config[CONF_ADBKEY],
device_class=config[CONF_DEVICE_CLASS]) 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: else:
aftv = setup(host, device_class=config[CONF_DEVICE_CLASS]) aftv = setup(host, device_class=config[CONF_DEVICE_CLASS])
adb_log = "" adb_log += "without adbkey authentication"
else: else:
# Use "pure-python-adb" (communicate with ADB server) # Use "pure-python-adb" (communicate with ADB server)
aftv = setup(host, adb_server_ip=config[CONF_ADB_SERVER_IP], aftv = setup(host, adb_server_ip=config[CONF_ADB_SERVER_IP],
adb_server_port=config[CONF_ADB_SERVER_PORT], adb_server_port=config[CONF_ADB_SERVER_PORT],
device_class=config[CONF_DEVICE_CLASS]) 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]) config[CONF_ADB_SERVER_IP], config[CONF_ADB_SERVER_PORT])
if not aftv.available: if not aftv.available:
@ -117,7 +118,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
else: else:
device_name = 'Android TV / Fire TV device' 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) device_name, host, adb_log)
raise PlatformNotReady raise PlatformNotReady
@ -156,10 +157,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
for target_device in target_devices: for target_device in target_devices:
output = target_device.adb_command(cmd) output = target_device.adb_command(cmd)
# log the output if there is any # log the output, if there is any
if output and (not isinstance(output, str) or output.strip()): if output:
_LOGGER.info("Output of command '%s' from '%s': %s", _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, hass.services.register(ANDROIDTV_DOMAIN, SERVICE_ADB_COMMAND,
service_adb_command, service_adb_command,
@ -224,6 +225,7 @@ class ADBDevice(MediaPlayerDevice):
self.exceptions = (ConnectionResetError, RuntimeError) self.exceptions = (ConnectionResetError, RuntimeError)
# Property attributes # Property attributes
self._adb_response = None
self._available = self.aftv.available self._available = self.aftv.available
self._current_app = None self._current_app = None
self._state = None self._state = None
@ -243,6 +245,11 @@ class ADBDevice(MediaPlayerDevice):
"""Return whether or not the ADB connection is valid.""" """Return whether or not the ADB connection is valid."""
return self._available 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 @property
def name(self): def name(self):
"""Return the device name.""" """Return the device name."""
@ -304,12 +311,24 @@ class ADBDevice(MediaPlayerDevice):
"""Send an ADB command to an Android TV / Fire TV device.""" """Send an ADB command to an Android TV / Fire TV device."""
key = self._keys.get(cmd) key = self._keys.get(cmd)
if key: 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': 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): 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()) hass.async_create_task(device.async_update_ha_state())
avr = await anthemav.Connection.create( avr = await anthemav.Connection.create(
host=host, port=port, loop=hass.loop, host=host, port=port,
update_callback=async_anthemav_update_callback) update_callback=async_anthemav_update_callback)
device = AnthemAVR(avr, name) device = AnthemAVR(avr, name)

View File

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

View File

@ -1,6 +1,5 @@
"""APNS Notification platform.""" """APNS Notification platform."""
import logging import logging
import os
import voluptuous as vol import voluptuous as vol
@ -149,7 +148,8 @@ class ApnsNotificationService(BaseNotificationService):
self.devices = {} self.devices = {}
self.device_states = {} self.device_states = {}
self.topic = topic self.topic = topic
if os.path.isfile(self.yaml_path):
try:
self.devices = { self.devices = {
str(key): ApnsDevice( str(key): ApnsDevice(
str(key), str(key),
@ -160,6 +160,8 @@ class ApnsNotificationService(BaseNotificationService):
for (key, value) in for (key, value) in
load_yaml_config_file(self.yaml_path).items() load_yaml_config_file(self.yaml_path).items()
} }
except FileNotFoundError:
pass
tracking_ids = [ tracking_ids = [
device.full_tracking_device_id 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, [])] tasks = [_setup_atv(hass, config, conf) for conf in config.get(DOMAIN, [])]
if tasks: if tasks:
await asyncio.wait(tasks, loop=hass.loop) await asyncio.wait(tasks)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_SCAN, async_service_handler, DOMAIN, SERVICE_SCAN, async_service_handler,

View File

@ -124,7 +124,7 @@ async def async_setup(hass, config):
context=service_call.context)) context=service_call.context))
if tasks: if tasks:
await asyncio.wait(tasks, loop=hass.loop) await asyncio.wait(tasks)
async def turn_onoff_service_handler(service_call): async def turn_onoff_service_handler(service_call):
"""Handle automation turn on/off service calls.""" """Handle automation turn on/off service calls."""
@ -134,7 +134,7 @@ async def async_setup(hass, config):
tasks.append(getattr(entity, method)()) tasks.append(getattr(entity, method)())
if tasks: if tasks:
await asyncio.wait(tasks, loop=hass.loop) await asyncio.wait(tasks)
async def toggle_service_handler(service_call): async def toggle_service_handler(service_call):
"""Handle automation toggle service calls.""" """Handle automation toggle service calls."""
@ -146,7 +146,7 @@ async def async_setup(hass, config):
tasks.append(entity.async_turn_on()) tasks.append(entity.async_turn_on())
if tasks: if tasks:
await asyncio.wait(tasks, loop=hass.loop) await asyncio.wait(tasks)
async def reload_service_handler(service_call): async def reload_service_handler(service_call):
"""Remove all automations and load new ones from config.""" """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) profile = aws_config.get(CONF_PROFILE_NAME)
if profile is not None: if profile is not None:
session = aiobotocore.AioSession(profile=profile, loop=hass.loop) session = aiobotocore.AioSession(profile=profile)
del aws_config[CONF_PROFILE_NAME] del aws_config[CONF_PROFILE_NAME]
if CONF_ACCESS_KEY_ID in aws_config: if CONF_ACCESS_KEY_ID in aws_config:
del aws_config[CONF_ACCESS_KEY_ID] del aws_config[CONF_ACCESS_KEY_ID]
if CONF_SECRET_ACCESS_KEY in aws_config: if CONF_SECRET_ACCESS_KEY in aws_config:
del aws_config[CONF_SECRET_ACCESS_KEY] del aws_config[CONF_SECRET_ACCESS_KEY]
else: else:
session = aiobotocore.AioSession(loop=hass.loop) session = aiobotocore.AioSession()
if credential[CONF_VALIDATE]: if credential[CONF_VALIDATE]:
async with session.create_client("iam", **aws_config) as client: 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: if session is None:
profile = aws_config.get(CONF_PROFILE_NAME) profile = aws_config.get(CONF_PROFILE_NAME)
if profile is not None: if profile is not None:
session = aiobotocore.AioSession(profile=profile, loop=hass.loop) session = aiobotocore.AioSession(profile=profile)
del aws_config[CONF_PROFILE_NAME] del aws_config[CONF_PROFILE_NAME]
else: else:
session = aiobotocore.AioSession(loop=hass.loop) session = aiobotocore.AioSession()
aws_config[CONF_REGION] = region_name 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": { "config": {
"abort": { "abort": {
"already_configured": "Enheten \u00e4r redan konfigurerad", "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": { "error": {
"already_configured": "Enheten \u00e4r redan konfigurerad", "already_configured": "Enheten \u00e4r redan konfigurerad",
@ -17,7 +18,7 @@
"port": "Port", "port": "Port",
"username": "Anv\u00e4ndarnamn" "username": "Anv\u00e4ndarnamn"
}, },
"title": "Konfigurera Axis enhet" "title": "Konfigurera Axis-enhet"
} }
}, },
"title": "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 datetime import timedelta
from axis.event_stream import CLASS_INPUT, CLASS_OUTPUT
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.const import CONF_MAC, CONF_TRIGGER_TIME from homeassistant.const import CONF_MAC, CONF_TRIGGER_TIME
from homeassistant.core import callback 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.helpers.event import async_track_point_in_utc_time
from homeassistant.util.dt import utcnow 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): 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): def async_add_sensor(event_id):
"""Add binary sensor from Axis device.""" """Add binary sensor from Axis device."""
event = device.api.event.events[event_id] 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( device.listeners.append(async_dispatcher_connect(
hass, device.event_new_sensor, async_add_sensor)) hass, device.event_new_sensor, async_add_sensor))
class AxisBinarySensor(BinarySensorDevice): class AxisBinarySensor(AxisEventBase, BinarySensorDevice):
"""Representation of a binary Axis event.""" """Representation of a binary Axis event."""
def __init__(self, event, device): def __init__(self, event, device):
"""Initialize the Axis binary sensor.""" """Initialize the Axis binary sensor."""
self.event = event super().__init__(event, device)
self.device = device
self.remove_timer = None 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 @callback
def update_callback(self, no_delay=False): def update_callback(self, no_delay=False):
@ -67,7 +59,6 @@ class AxisBinarySensor(BinarySensorDevice):
@callback @callback
def _delay_update(now): def _delay_update(now):
"""Timer callback for sensor update.""" """Timer callback for sensor update."""
LOGGER.debug("%s called delayed (%s sec) update", self.name, delay)
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
self.remove_timer = None self.remove_timer = None
@ -83,32 +74,10 @@ class AxisBinarySensor(BinarySensorDevice):
@property @property
def name(self): def name(self):
"""Return the name of the event.""" """Return the name of the event."""
return '{} {} {}'.format( if self.event.CLASS == CLASS_INPUT and self.event.id and \
self.device.name, self.event.TYPE, self.event.id) self.device.api.vapix.ports[self.event.id].name:
return '{} {}'.format(
self.device.name,
self.device.api.vapix.ports[self.event.id].name)
@property return super().name
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)}
}

View File

@ -6,9 +6,9 @@ from homeassistant.components.mjpeg.camera import (
from homeassistant.const import ( from homeassistant.const import (
CONF_AUTHENTICATION, CONF_DEVICE, CONF_HOST, CONF_MAC, CONF_NAME, CONF_AUTHENTICATION, CONF_DEVICE, CONF_HOST, CONF_MAC, CONF_NAME,
CONF_PASSWORD, CONF_PORT, CONF_USERNAME, HTTP_DIGEST_AUTHENTICATION) CONF_PASSWORD, CONF_PORT, CONF_USERNAME, HTTP_DIGEST_AUTHENTICATION)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .axis_base import AxisEntityBase
from .const import DOMAIN as AXIS_DOMAIN from .const import DOMAIN as AXIS_DOMAIN
AXIS_IMAGE = 'http://{}:{}/axis-cgi/jpg/image.cgi' 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)]) async_add_entities([AxisCamera(config, device)])
class AxisCamera(MjpegCamera): class AxisCamera(AxisEntityBase, MjpegCamera):
"""Representation of a Axis camera.""" """Representation of a Axis camera."""
def __init__(self, config, device): def __init__(self, config, device):
"""Initialize Axis Communications camera component.""" """Initialize Axis Communications camera component."""
super().__init__(config) AxisEntityBase.__init__(self, device)
self.device_config = config MjpegCamera.__init__(self, config)
self.device = device
self.port = device.config_entry.data[CONF_DEVICE][CONF_PORT]
self.unsub_dispatcher = []
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Subscribe camera events.""" """Subscribe camera events."""
self.unsub_dispatcher.append(async_dispatcher_connect( self.unsub_dispatcher.append(async_dispatcher_connect(
self.hass, self.device.event_new_address, self._new_address)) 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: await super().async_added_to_hass()
"""Disconnect device object when removed."""
for unsub_dispatcher in self.unsub_dispatcher:
unsub_dispatcher()
@property @property
def supported_features(self): def supported_features(self):
"""Return supported features.""" """Return supported features."""
return SUPPORT_STREAM return SUPPORT_STREAM
@property async def stream_source(self):
def stream_source(self):
"""Return the stream source.""" """Return the stream source."""
return AXIS_STREAM.format( return AXIS_STREAM.format(
self.device.config_entry.data[CONF_DEVICE][CONF_USERNAME], self.device.config_entry.data[CONF_DEVICE][CONF_USERNAME],
self.device.config_entry.data[CONF_DEVICE][CONF_PASSWORD], self.device.config_entry.data[CONF_DEVICE][CONF_PASSWORD],
self.device.host) 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): def _new_address(self):
"""Set new device address for video stream.""" """Set new device address for video stream."""
self._mjpeg_url = AXIS_VIDEO.format(self.device.host, self.port) port = self.device.config_entry.data[CONF_DEVICE][CONF_PORT]
self._still_image_url = AXIS_IMAGE.format(self.device.host, self.port) self._mjpeg_url = AXIS_VIDEO.format(self.device.host, port)
self._still_image_url = AXIS_IMAGE.format(self.device.host, port)
@property @property
def unique_id(self): def unique_id(self):
"""Return a unique identifier for this device.""" """Return a unique identifier for this device."""
return '{}-camera'.format(self.device.serial) 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 entry.data[CONF_DEVICE][CONF_HOST] = host
self.hass.config_entries.async_update_entry(entry) 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. """Prepare configuration for a discovered Axis device.
This flow is triggered by the discovery component. 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') return self.async_abort(reason='link_local_address')
serialnumber = discovery_info['properties']['macaddress'] 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) device_entries = configured_devices(self.hass)
if serialnumber in device_entries: if serialnumber in device_entries:

View File

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

View File

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

View File

@ -14,6 +14,7 @@
}, },
"error": { "error": {
"already_configured": "Device is already configured", "already_configured": "Device is already configured",
"already_in_progress": "Config flow for device is already in progress.",
"device_unavailable": "Device is not available", "device_unavailable": "Device is not available",
"faulty_credentials": "Bad user credentials" "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 ( from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD, CONF_NAME, CONF_SCAN_INTERVAL, CONF_USERNAME, CONF_PASSWORD, CONF_NAME, CONF_SCAN_INTERVAL,
CONF_BINARY_SENSORS, CONF_SENSORS, CONF_FILENAME, 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__) _LOGGER = logging.getLogger(__name__)
@ -41,7 +41,7 @@ BINARY_SENSORS = {
SENSORS = { SENSORS = {
TYPE_TEMPERATURE: ['Temperature', TEMP_FAHRENHEIT, 'mdi:thermometer'], 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'], TYPE_WIFI_STRENGTH: ['Wifi Signal', 'dBm', 'mdi:wifi-strength-2'],
} }
@ -75,6 +75,8 @@ CONFIG_SCHEMA = vol.Schema(
vol.Optional(CONF_BINARY_SENSORS, default={}): vol.Optional(CONF_BINARY_SENSORS, default={}):
BINARY_SENSOR_SCHEMA, BINARY_SENSOR_SCHEMA,
vol.Optional(CONF_SENSORS, default={}): 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) extra=vol.ALLOW_EXTRA)
@ -87,8 +89,12 @@ def setup(hass, config):
username = conf[CONF_USERNAME] username = conf[CONF_USERNAME]
password = conf[CONF_PASSWORD] password = conf[CONF_PASSWORD]
scan_interval = conf[CONF_SCAN_INTERVAL] 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, 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].refresh_rate = scan_interval.total_seconds()
hass.data[BLINK_DATA].start() hass.data[BLINK_DATA].start()

View File

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

View File

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

View File

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

View File

@ -5,11 +5,16 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.components.device_tracker import ( from homeassistant.components.device_tracker import PLATFORM_SCHEMA
YAML_DEVICES, CONF_TRACK_NEW, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL, from homeassistant.components.device_tracker.legacy import (
load_config, PLATFORM_SCHEMA, DEFAULT_TRACK_NEW, SOURCE_TYPE_BLUETOOTH, YAML_DEVICES, async_load_config
DOMAIN) )
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 import homeassistant.util.dt as dt_util
from homeassistant.util.async_ import run_coroutine_threadsafe
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -60,7 +65,10 @@ def setup_scanner(hass, config, see, discovery_info=None):
# Load all known devices. # Load all known devices.
# We just need the devices so set consider_home and home range # We just need the devices so set consider_home and home range
# to 0 # 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 # Check if device is a valid bluetooth device
if device.mac and device.mac[:3].upper() == BT_PREFIX: if device.mac and device.mac[:3].upper() == BT_PREFIX:
if device.track: if device.track:
@ -77,7 +85,7 @@ def setup_scanner(hass, config, see, discovery_info=None):
devs_to_track.append(dev[0]) devs_to_track.append(dev[0])
see_device(dev[0], dev[1]) 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) 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.""" """Support for the Broadlink RM2 Pro (only temperature) and A1 devices."""
import binascii import binascii
import logging import logging
import socket
from datetime import timedelta from datetime import timedelta
import voluptuous as vol import voluptuous as vol
@ -60,6 +59,7 @@ class BroadlinkSensor(Entity):
"""Initialize the sensor.""" """Initialize the sensor."""
self._name = '{} {}'.format(name, SENSOR_TYPES[sensor_type][0]) self._name = '{} {}'.format(name, SENSOR_TYPES[sensor_type][0])
self._state = None self._state = None
self._is_available = False
self._type = sensor_type self._type = sensor_type
self._broadlink_data = broadlink_data self._broadlink_data = broadlink_data
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
@ -74,6 +74,11 @@ class BroadlinkSensor(Entity):
"""Return the state of the sensor.""" """Return the state of the sensor."""
return self._state return self._state
@property
def available(self):
"""Return True if entity is available."""
return self._is_available
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
"""Return the unit this state is expressed in.""" """Return the unit this state is expressed in."""
@ -83,8 +88,11 @@ class BroadlinkSensor(Entity):
"""Get the latest data from the sensor.""" """Get the latest data from the sensor."""
self._broadlink_data.update() self._broadlink_data.update()
if self._broadlink_data.data is None: if self._broadlink_data.data is None:
self._state = None
self._is_available = False
return return
self._state = self._broadlink_data.data[self._type] self._state = self._broadlink_data.data[self._type]
self._is_available = True
class BroadlinkData: class BroadlinkData:
@ -119,8 +127,9 @@ class BroadlinkData:
if data is not None: if data is not None:
self.data = self._schema(data) self.data = self._schema(data)
return return
except socket.timeout as error: except OSError as error:
if retry < 1: if retry < 1:
self.data = None
_LOGGER.error(error) _LOGGER.error(error)
return return
except (vol.Invalid, vol.MultipleInvalid): except (vol.Invalid, vol.MultipleInvalid):
@ -131,7 +140,7 @@ class BroadlinkData:
def _auth(self, retry=3): def _auth(self, retry=3):
try: try:
auth = self._device.auth() auth = self._device.auth()
except socket.timeout: except OSError:
auth = False auth = False
if not auth and retry > 0: if not auth and retry > 0:
self._connect() self._connect()

View File

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

View File

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

View File

@ -36,7 +36,7 @@ async def async_setup(hass, config):
hass.http.register_view(CalendarEventView(component)) hass.http.register_view(CalendarEventView(component))
# Doesn't work in prod builds of the frontend: home-assistant-polymer#1289 # 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') # 'calendar', 'calendar', 'hass:calendar')
await component.async_setup(config) 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 = _get_camera_from_entity_id(hass, entity_id)
camera_prefs = hass.data[DATA_CAMERA_PREFS].get(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" raise HomeAssistantError("{} does not support play stream service"
.format(camera.entity_id)) .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) 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) camera = _get_camera_from_entity_id(hass, entity_id)
with suppress(asyncio.CancelledError, asyncio.TimeoutError): 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() image = await camera.async_camera_image()
if image: if image:
@ -221,8 +224,16 @@ async def async_setup(hass, config):
async def preload_stream(hass, _): async def preload_stream(hass, _):
for camera in component.entities: for camera in component.entities:
camera_prefs = prefs.get(camera.entity_id) camera_prefs = prefs.get(camera.entity_id)
if camera.stream_source and camera_prefs.preload_stream: if not camera_prefs.preload_stream:
request_stream(hass, camera.stream_source, keepalive=True) 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) 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 the interval between frames of the mjpeg stream."""
return 0.5 return 0.5
@property async def stream_source(self):
def stream_source(self):
"""Return the source of the stream.""" """Return the source of the stream."""
return None return None
@ -481,7 +491,7 @@ class CameraImageView(CameraView):
async def handle(self, request, camera): async def handle(self, request, camera):
"""Serve camera image.""" """Serve camera image."""
with suppress(asyncio.CancelledError, asyncio.TimeoutError): 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() image = await camera.async_camera_image()
if image: if image:
@ -522,12 +532,10 @@ async def websocket_camera_thumbnail(hass, connection, msg):
""" """
try: try:
image = await async_get_image(hass, msg['entity_id']) image = await async_get_image(hass, msg['entity_id'])
connection.send_message(websocket_api.result_message( await connection.send_big_result(msg['id'], {
msg['id'], { 'content_type': image.content_type,
'content_type': image.content_type, 'content': base64.b64encode(image.content).decode('utf-8')
'content': base64.b64encode(image.content).decode('utf-8') })
}
))
except HomeAssistantError: except HomeAssistantError:
connection.send_message(websocket_api.error_message( connection.send_message(websocket_api.error_message(
msg['id'], 'image_fetch_failed', 'Unable to fetch image')) 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 = _get_camera_from_entity_id(hass, entity_id)
camera_prefs = hass.data[DATA_CAMERA_PREFS].get(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" raise HomeAssistantError("{} does not support play stream service"
.format(camera.entity_id)) .format(camera.entity_id))
fmt = msg['format'] fmt = msg['format']
url = request_stream(hass, camera.stream_source, fmt=fmt, url = request_stream(hass, source, fmt=fmt,
keepalive=camera_prefs.preload_stream) keepalive=camera_prefs.preload_stream)
connection.send_result(msg['id'], {'url': url}) connection.send_result(msg['id'], {'url': url})
except HomeAssistantError as ex: except HomeAssistantError as ex:
_LOGGER.error(ex) _LOGGER.error("Error requesting stream: %s", ex)
connection.send_error( connection.send_error(
msg['id'], 'start_stream_failed', str(ex)) 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 @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): async def async_handle_play_stream_service(camera, service_call):
"""Handle play stream services calls.""" """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" raise HomeAssistantError("{} does not support play stream service"
.format(camera.entity_id)) .format(camera.entity_id))
@ -633,7 +651,7 @@ async def async_handle_play_stream_service(camera, service_call):
fmt = service_call.data[ATTR_FORMAT] fmt = service_call.data[ATTR_FORMAT]
entity_ids = service_call.data[ATTR_MEDIA_PLAYER] 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) keepalive=camera_prefs.preload_stream)
data = { data = {
ATTR_ENTITY_ID: entity_ids, 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): async def async_handle_record_service(camera, call):
"""Handle stream recording service calls.""" """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" raise HomeAssistantError("{} does not support record service"
.format(camera.entity_id)) .format(camera.entity_id))
@ -659,7 +680,7 @@ async def async_handle_record_service(camera, call):
variables={ATTR_ENTITY_ID: camera}) variables={ATTR_ENTITY_ID: camera})
data = { data = {
CONF_STREAM_SOURCE: camera.stream_source, CONF_STREAM_SOURCE: source,
CONF_FILENAME: video_path, CONF_FILENAME: video_path,
CONF_DURATION: call.data[CONF_DURATION], CONF_DURATION: call.data[CONF_DURATION],
CONF_LOOKBACK: call.data[CONF_LOOKBACK], CONF_LOOKBACK: call.data[CONF_LOOKBACK],

View File

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

View File

@ -1,8 +1,7 @@
"""Component to embed Google Cast.""" """Component to embed Google Cast."""
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.helpers import config_entry_flow
DOMAIN = 'cast' from .const import DOMAIN
async def async_setup(hass, config): 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( hass.async_create_task(hass.config_entries.async_forward_entry_setup(
entry, 'media_player')) entry, 'media_player'))
return True 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", "domain": "cast",
"name": "Cast", "name": "Cast",
"config_flow": true,
"documentation": "https://www.home-assistant.io/components/cast", "documentation": "https://www.home-assistant.io/components/cast",
"requirements": [ "requirements": [
"pychromecast==3.2.1" "pychromecast==3.2.1"

View File

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

View File

@ -17,7 +17,9 @@ from homeassistant.util.aiohttp import MockRequest
from . import utils from . import utils
from .const import ( 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 from .prefs import CloudPreferences
@ -98,12 +100,26 @@ class CloudClient(Interface):
if entity.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: if entity.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
return False 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"] username = self._hass.data[DOMAIN].claims["cognito:username"]
self._google_config = ga_h.Config( self._google_config = ga_h.Config(
should_expose=should_expose, should_expose=should_expose,
should_2fa=should_2fa,
secure_devices_pin=self._prefs.google_secure_devices_pin, secure_devices_pin=self._prefs.google_secure_devices_pin,
entity_config=google_conf.get(CONF_ENTITY_CONFIG), entity_config=google_conf.get(CONF_ENTITY_CONFIG),
agent_user_id=username, 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_GOOGLE_SECURE_DEVICES_PIN = 'google_secure_devices_pin'
PREF_CLOUDHOOKS = 'cloudhooks' PREF_CLOUDHOOKS = 'cloudhooks'
PREF_CLOUD_USER = 'cloud_user' 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_ALEXA = 'alexa'
CONF_ALIASES = 'aliases' CONF_ALIASES = 'aliases'

View File

@ -14,8 +14,7 @@ from homeassistant.components.http.data_validator import (
RequestDataValidator) RequestDataValidator)
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
from homeassistant.components.alexa import smart_home as alexa_sh from homeassistant.components.alexa import smart_home as alexa_sh
from homeassistant.components.google_assistant import ( from homeassistant.components.google_assistant import helpers as google_helpers
const as google_const)
from .const import ( from .const import (
DOMAIN, REQUEST_TIMEOUT, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, DOMAIN, REQUEST_TIMEOUT, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE,
@ -81,6 +80,12 @@ async def async_setup(hass):
websocket_remote_connect) websocket_remote_connect)
hass.components.websocket_api.async_register_command( hass.components.websocket_api.async_register_command(
websocket_remote_disconnect) 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(GoogleActionsSyncView)
hass.http.register_view(CloudLoginView) hass.http.register_view(CloudLoginView)
hass.http.register_view(CloudLogoutView) hass.http.register_view(CloudLogoutView)
@ -164,10 +169,10 @@ class GoogleActionsSyncView(HomeAssistantView):
cloud = hass.data[DOMAIN] cloud = hass.data[DOMAIN]
websession = hass.helpers.aiohttp_client.async_get_clientsession() 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) 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( req = await websession.post(
cloud.google_actions_sync_url, headers={ cloud.google_actions_sync_url, headers={
'authorization': cloud.id_token 'authorization': cloud.id_token
@ -192,7 +197,7 @@ class CloudLoginView(HomeAssistantView):
hass = request.app['hass'] hass = request.app['hass']
cloud = hass.data[DOMAIN] 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'], await hass.async_add_job(cloud.auth.login, data['email'],
data['password']) data['password'])
@ -212,7 +217,7 @@ class CloudLogoutView(HomeAssistantView):
hass = request.app['hass'] hass = request.app['hass']
cloud = hass.data[DOMAIN] cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): with async_timeout.timeout(REQUEST_TIMEOUT):
await cloud.logout() await cloud.logout()
return self.json_message('ok') return self.json_message('ok')
@ -234,7 +239,7 @@ class CloudRegisterView(HomeAssistantView):
hass = request.app['hass'] hass = request.app['hass']
cloud = hass.data[DOMAIN] cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): with async_timeout.timeout(REQUEST_TIMEOUT):
await hass.async_add_job( await hass.async_add_job(
cloud.auth.register, data['email'], data['password']) cloud.auth.register, data['email'], data['password'])
@ -256,7 +261,7 @@ class CloudResendConfirmView(HomeAssistantView):
hass = request.app['hass'] hass = request.app['hass']
cloud = hass.data[DOMAIN] cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): with async_timeout.timeout(REQUEST_TIMEOUT):
await hass.async_add_job( await hass.async_add_job(
cloud.auth.resend_email_confirm, data['email']) cloud.auth.resend_email_confirm, data['email'])
@ -278,7 +283,7 @@ class CloudForgotPasswordView(HomeAssistantView):
hass = request.app['hass'] hass = request.app['hass']
cloud = hass.data[DOMAIN] cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): with async_timeout.timeout(REQUEST_TIMEOUT):
await hass.async_add_job( await hass.async_add_job(
cloud.auth.forgot_password, data['email']) cloud.auth.forgot_password, data['email'])
@ -320,7 +325,7 @@ async def websocket_subscription(hass, connection, msg):
from hass_nabucasa.const import STATE_DISCONNECTED from hass_nabucasa.const import STATE_DISCONNECTED
cloud = hass.data[DOMAIN] 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() response = await cloud.fetch_subscription_info()
if response.status != 200: if response.status != 200:
@ -411,7 +416,6 @@ def _account_data(cloud):
'cloud': cloud.iot.state, 'cloud': cloud.iot.state,
'prefs': client.prefs.as_dict(), 'prefs': client.prefs.as_dict(),
'google_entities': client.google_user_config['filter'].config, '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_entities': client.alexa_config.should_expose.config,
'alexa_domains': list(alexa_sh.ENTITY_ADAPTERS), 'alexa_domains': list(alexa_sh.ENTITY_ADAPTERS),
'remote_domain': remote.instance_domain, '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.client.prefs.async_update(remote_enabled=False)
await cloud.remote.disconnect() await cloud.remote.disconnect()
connection.send_result(msg['id'], _account_data(cloud)) 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", "name": "Cloud",
"documentation": "https://www.home-assistant.io/components/cloud", "documentation": "https://www.home-assistant.io/components/cloud",
"requirements": [ "requirements": [
"hass-nabucasa==0.12" "hass-nabucasa==0.13"
], ],
"dependencies": [ "dependencies": [
"http", "http",

View File

@ -4,6 +4,8 @@ from ipaddress import ip_address
from .const import ( from .const import (
DOMAIN, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, PREF_ENABLE_REMOTE, DOMAIN, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, PREF_ENABLE_REMOTE,
PREF_GOOGLE_SECURE_DEVICES_PIN, PREF_CLOUDHOOKS, PREF_CLOUD_USER, 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) InvalidTrustedNetworks)
STORAGE_KEY = DOMAIN STORAGE_KEY = DOMAIN
@ -30,6 +32,7 @@ class CloudPreferences:
PREF_ENABLE_GOOGLE: True, PREF_ENABLE_GOOGLE: True,
PREF_ENABLE_REMOTE: False, PREF_ENABLE_REMOTE: False,
PREF_GOOGLE_SECURE_DEVICES_PIN: None, PREF_GOOGLE_SECURE_DEVICES_PIN: None,
PREF_GOOGLE_ENTITY_CONFIGS: {},
PREF_CLOUDHOOKS: {}, PREF_CLOUDHOOKS: {},
PREF_CLOUD_USER: None, PREF_CLOUD_USER: None,
} }
@ -39,7 +42,7 @@ class CloudPreferences:
async def async_update(self, *, google_enabled=_UNDEF, async def async_update(self, *, google_enabled=_UNDEF,
alexa_enabled=_UNDEF, remote_enabled=_UNDEF, alexa_enabled=_UNDEF, remote_enabled=_UNDEF,
google_secure_devices_pin=_UNDEF, cloudhooks=_UNDEF, google_secure_devices_pin=_UNDEF, cloudhooks=_UNDEF,
cloud_user=_UNDEF): cloud_user=_UNDEF, google_entity_configs=_UNDEF):
"""Update user preferences.""" """Update user preferences."""
for key, value in ( for key, value in (
(PREF_ENABLE_GOOGLE, google_enabled), (PREF_ENABLE_GOOGLE, google_enabled),
@ -48,6 +51,7 @@ class CloudPreferences:
(PREF_GOOGLE_SECURE_DEVICES_PIN, google_secure_devices_pin), (PREF_GOOGLE_SECURE_DEVICES_PIN, google_secure_devices_pin),
(PREF_CLOUDHOOKS, cloudhooks), (PREF_CLOUDHOOKS, cloudhooks),
(PREF_CLOUD_USER, cloud_user), (PREF_CLOUD_USER, cloud_user),
(PREF_GOOGLE_ENTITY_CONFIGS, google_entity_configs),
): ):
if value is not _UNDEF: if value is not _UNDEF:
self._prefs[key] = value self._prefs[key] = value
@ -57,9 +61,48 @@ class CloudPreferences:
await self._store.async_save(self._prefs) 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): def as_dict(self):
"""Return dictionary version.""" """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 @property
def remote_enabled(self): def remote_enabled(self):
@ -89,6 +132,11 @@ class CloudPreferences:
"""Return if Google is allowed to unlock locks.""" """Return if Google is allowed to unlock locks."""
return self._prefs.get(PREF_GOOGLE_SECURE_DEVICES_PIN) 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 @property
def cloudhooks(self): def cloudhooks(self):
"""Return the published cloud webhooks.""" """Return the published cloud webhooks."""

View File

@ -1,13 +1,25 @@
"""Helper functions for cloud components.""" """Helper functions for cloud components."""
from typing import Any, Dict from typing import Any, Dict
from aiohttp import web from aiohttp import web, payload
def aiohttp_serialize_response(response: web.Response) -> Dict[str, Any]: def aiohttp_serialize_response(response: web.Response) -> Dict[str, Any]:
"""Serialize an aiohttp response to a dictionary.""" """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 { return {
'status': response.status, 'status': response.status,
'body': response.text, 'body': body,
'headers': dict(response.headers), 'headers': dict(response.headers),
} }

View File

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

View File

@ -30,7 +30,7 @@ ON_DEMAND = ('zwave',)
async def async_setup(hass, config): async def async_setup(hass, config):
"""Set up the config component.""" """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) 'config', 'config', 'hass:settings', require_admin=True)
async def setup_panel(panel_name): async def setup_panel(panel_name):
@ -62,7 +62,7 @@ async def async_setup(hass, config):
tasks.append(setup_panel(panel_name)) tasks.append(setup_panel(panel_name))
if tasks: if tasks:
await asyncio.wait(tasks, loop=hass.loop) await asyncio.wait(tasks)
return True return True
@ -92,6 +92,10 @@ class BaseEditConfigView(HomeAssistantView):
"""Set value.""" """Set value."""
raise NotImplementedError raise NotImplementedError
def _delete_value(self, hass, data, config_key):
"""Delete value."""
raise NotImplementedError
async def get(self, request, config_key): async def get(self, request, config_key):
"""Fetch device specific config.""" """Fetch device specific config."""
hass = request.app['hass'] hass = request.app['hass']
@ -128,7 +132,27 @@ class BaseEditConfigView(HomeAssistantView):
current = await self.read_config(hass) current = await self.read_config(hass)
self._write_value(hass, current, config_key, data) 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: if self.post_write_hook is not None:
hass.async_create_task(self.post_write_hook(hass)) hass.async_create_task(self.post_write_hook(hass))
@ -161,6 +185,10 @@ class EditKeyBasedConfigView(BaseEditConfigView):
"""Set value.""" """Set value."""
data.setdefault(config_key, {}).update(new_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): class EditIdBasedConfigView(BaseEditConfigView):
"""Configure key based config entries.""" """Configure key based config entries."""
@ -184,6 +212,13 @@ class EditIdBasedConfigView(BaseEditConfigView):
value.update(new_value) 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): def _read(path):
"""Read YAML helper.""" """Read YAML helper."""

View File

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

View File

@ -1,12 +1,22 @@
"""Component to interact with Hassbian tools.""" """Component to interact with Hassbian tools."""
import voluptuous as vol
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from homeassistant.config import async_check_ha_config_file 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): async def async_setup(hass):
"""Set up the Hassbian config.""" """Set up the Hassbian config."""
hass.http.register_view(CheckConfigView) 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 return True
@ -26,3 +36,62 @@ class CheckConfigView(HomeAssistantView):
"result": state, "result": state,
"errors": errors, "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._api = api
self._list = { self._list = {
ATTR_OPERATION_MODE: list(HA_STATE_TO_DAIKIN), ATTR_OPERATION_MODE: list(HA_STATE_TO_DAIKIN),
ATTR_FAN_MODE: list( ATTR_FAN_MODE: self._api.device.fan_rate,
map(
str.title,
appliance.daikin_values(HA_ATTR_TO_DAIKIN[ATTR_FAN_MODE])
)
),
ATTR_SWING_MODE: list( ATTR_SWING_MODE: list(
map( map(
str.title, 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_OPERATION_MODE
| SUPPORT_TARGET_TEMPERATURE) | 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 self._supported_features |= SUPPORT_FAN_MODE
if self._api.device.support_swing_mode: if self._api.device.support_swing_mode:

View File

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

View File

@ -26,9 +26,12 @@ async def async_setup_platform(
async def async_setup_entry(hass, entry, async_add_entities): async def async_setup_entry(hass, entry, async_add_entities):
"""Set up Daikin climate based on config_entry.""" """Set up Daikin climate based on config_entry."""
daikin_api = hass.data[DAIKIN_DOMAIN].get(entry.entry_id) 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([ async_add_entities([
DaikinClimateSensor(daikin_api, sensor, hass.config.units) 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: if zones:
async_add_entities([ async_add_entities([
DaikinZoneSwitch(daikin_api, zone_id) DaikinZoneSwitch(daikin_api, zone_id)
for zone_id, name in enumerate(zones) for zone_id, zone in enumerate(zones) if zone != ('-', '0')
if name != '-'
]) ])

View File

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

View File

@ -3,12 +3,21 @@
"abort": { "abort": {
"already_configured": "Ce pont est d\u00e9j\u00e0 configur\u00e9", "already_configured": "Ce pont est d\u00e9j\u00e0 configur\u00e9",
"no_bridges": "Aucun pont deCONZ n'a \u00e9t\u00e9 d\u00e9couvert", "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": { "error": {
"no_key": "Impossible d'obtenir une cl\u00e9 d'API" "no_key": "Impossible d'obtenir une cl\u00e9 d'API"
}, },
"step": { "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": { "init": {
"data": { "data": {
"host": "H\u00f4te", "host": "H\u00f4te",

View File

@ -3,7 +3,8 @@
"abort": { "abort": {
"already_configured": "Bryggan \u00e4r redan konfigurerad", "already_configured": "Bryggan \u00e4r redan konfigurerad",
"no_bridges": "Inga deCONZ-bryggor uppt\u00e4cktes", "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": { "error": {
"no_key": "Det gick inte att ta emot en API-nyckel" "no_key": "Det gick inte att ta emot en API-nyckel"
@ -11,8 +12,10 @@
"step": { "step": {
"hassio_confirm": { "hassio_confirm": {
"data": { "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" "title": "deCONZ Zigbee gateway via Hass.io till\u00e4gg"
}, },
"init": { "init": {

View File

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

View File

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

View File

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

View File

@ -4,13 +4,19 @@ import asyncio
import async_timeout import async_timeout
import voluptuous as vol 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 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.const import CONF_API_KEY, CONF_HOST, CONF_PORT
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
from .const import CONF_BRIDGEID, DEFAULT_PORT, DOMAIN from .const import CONF_BRIDGEID, DEFAULT_PORT, DOMAIN
DECONZ_MANUFACTURERURL = 'http://www.dresden-elektronik.de'
CONF_SERIAL = 'serial' 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 more than one bridge is found let user choose bridge to link.
If no bridge is found allow user to manually input configuration. If no bridge is found allow user to manually input configuration.
""" """
from pydeconz.utils import async_discovery
if user_input is not None: if user_input is not None:
for bridge in self.bridges: for bridge in self.bridges:
if bridge[CONF_HOST] == user_input[CONF_HOST]: 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): async def async_step_link(self, user_input=None):
"""Attempt to link with the deCONZ bridge.""" """Attempt to link with the deCONZ bridge."""
from pydeconz.errors import ResponseError, RequestError
from pydeconz.utils import async_get_api_key
errors = {} errors = {}
if user_input is not None: if user_input is not None:
@ -127,8 +129,6 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
async def _create_entry(self): async def _create_entry(self):
"""Create entry for gateway.""" """Create entry for gateway."""
from pydeconz.utils import async_get_bridgeid
if CONF_BRIDGEID not in self.deconz_config: if CONF_BRIDGEID not in self.deconz_config:
session = aiohttp_client.async_get_clientsession(self.hass) session = aiohttp_client.async_get_clientsession(self.hass)
@ -151,12 +151,12 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
entry.data[CONF_HOST] = host entry.data[CONF_HOST] = host
self.hass.config_entries.async_update_entry(entry) self.hass.config_entries.async_update_entry(entry)
async def async_step_discovery(self, discovery_info): async def async_step_ssdp(self, discovery_info):
"""Prepare configuration for a discovered deCONZ bridge. """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[ATTR_SERIAL]
"""
bridgeid = discovery_info[CONF_SERIAL]
gateway_entries = configured_gateways(self.hass) gateway_entries = configured_gateways(self.hass)
if bridgeid in gateway_entries: if bridgeid in gateway_entries:
@ -164,10 +164,17 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
await self._update_entry(entry, discovery_info[CONF_HOST]) await self._update_entry(entry, discovery_info[CONF_HOST])
return self.async_abort(reason='updated_instance') 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 = { deconz_config = {
CONF_HOST: discovery_info[CONF_HOST], CONF_HOST: discovery_info[CONF_HOST],
CONF_PORT: discovery_info[CONF_PORT], CONF_PORT: discovery_info[CONF_PORT],
CONF_BRIDGEID: discovery_info[CONF_SERIAL] CONF_BRIDGEID: bridgeid
} }
return await self.async_step_import(deconz_config) return await self.async_step_import(deconz_config)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -34,9 +34,11 @@
}, },
"abort": { "abort": {
"already_configured": "Bridge is already configured", "already_configured": "Bridge is already configured",
"already_in_progress": "Config flow for bridge is already in progress.",
"no_bridges": "No deCONZ bridges discovered", "no_bridges": "No deCONZ bridges discovered",
"updated_instance": "Updated deCONZ instance with new host address", "not_deconz_bridge": "Not a deCONZ bridge",
"one_instance_only": "Component only supports one deCONZ instance" "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( async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None): hass, config, async_add_entities, discovery_info=None):
"""Old way of setting up deCONZ switches.""" """Old way of setting up deCONZ platforms."""
pass pass

View File

@ -15,6 +15,7 @@
"mobile_app", "mobile_app",
"person", "person",
"script", "script",
"ssdp",
"sun", "sun",
"system_health", "system_health",
"updater", "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_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY,
SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, 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 from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
@ -35,7 +36,7 @@ YOUTUBE_PLAYER_SUPPORT = \
MUSIC_PLAYER_SUPPORT = \ MUSIC_PLAYER_SUPPORT = \
SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_CLEAR_PLAYLIST | \ 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_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
SUPPORT_SELECT_SOUND_MODE SUPPORT_SELECT_SOUND_MODE
@ -122,6 +123,16 @@ class AbstractDemoPlayer(MediaPlayerDevice):
self._volume_muted = mute self._volume_muted = mute
self.schedule_update_ha_state() 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): def set_volume_level(self, volume):
"""Set the volume level, range 0..1.""" """Set the volume level, range 0..1."""
self._volume_level = volume self._volume_level = volume

View File

@ -1,78 +1,52 @@
"""Provide functionality to keep track of devices.""" """Provide functionality to keep track of devices."""
import asyncio import asyncio
from datetime import timedelta
import logging
from typing import Any, List, Sequence, Callable
import voluptuous as vol 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.loader import bind_hass
from homeassistant.components import group, zone from homeassistant.components import group
from homeassistant.components.group import ( from homeassistant.helpers import discovery
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
import homeassistant.helpers.config_validation as cv 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.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.helpers.event import async_track_utc_time_change
from homeassistant.const import ( from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME
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)
_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_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' 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_TYPES = (SOURCE_TYPE_GPS, SOURCE_TYPE_ROUTER,
SOURCE_TYPE_BLUETOOTH, SOURCE_TYPE_BLUETOOTH_LE) 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): async def async_setup(hass: HomeAssistantType, config: ConfigType):
"""Set up the device tracker.""" """Set up the device tracker."""
yaml_path = hass.config.path(YAML_DEVICES) tracker = await legacy.get_tracker(hass, config)
conf = config.get(DOMAIN, []) legacy_platforms = await setup.async_extract_config(hass, config)
conf = conf[0] if conf else {}
consider_home = conf.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME)
defaults = conf.get(CONF_NEW_DEVICE_DEFAULTS, {}) setup_tasks = [
track_new = conf.get(CONF_TRACK_NEW) legacy_platform.async_setup_legacy(hass, tracker)
if track_new is None: for legacy_platform in legacy_platforms
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)
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: if setup_tasks:
await asyncio.wait(setup_tasks, loop=hass.loop) await asyncio.wait(setup_tasks)
tracker.async_setup_group() tracker.async_setup_group()
async def async_platform_discovered(platform, info): async def async_platform_discovered(p_type, info):
"""Load a platform.""" """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) discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered)
@ -226,537 +154,3 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType):
# restore # restore
await tracker.async_setup_tracked_device() await tracker.async_setup_tracked_device()
return True 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.exceptions import HomeAssistantError
from homeassistant.helpers import intent, template, config_entry_flow 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" 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 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): def dialogflow_error_response(message, error):
"""Return a response saying the error message.""" """Return a response saying the error message."""
dialogflow_response = DialogflowResponse(message['result']['parameters']) 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", "domain": "dialogflow",
"name": "Dialogflow", "name": "Dialogflow",
"config_flow": true,
"documentation": "https://www.home-assistant.io/components/dialogflow", "documentation": "https://www.home-assistant.io/components/dialogflow",
"requirements": [], "requirements": [],
"dependencies": [ "dependencies": [

View File

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

View File

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

View File

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

View File

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

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