mirror of
https://github.com/home-assistant/core.git
synced 2025-07-09 22:37:11 +00:00
commit
d78e132007
10
.coveragerc
10
.coveragerc
@ -47,6 +47,7 @@ omit =
|
||||
homeassistant/components/august/*
|
||||
homeassistant/components/automatic/device_tracker.py
|
||||
homeassistant/components/avion/light.py
|
||||
homeassistant/components/azure_event_hub/*
|
||||
homeassistant/components/baidu/tts.py
|
||||
homeassistant/components/bbb_gpio/*
|
||||
homeassistant/components/bbox/device_tracker.py
|
||||
@ -171,6 +172,7 @@ omit =
|
||||
homeassistant/components/esphome/camera.py
|
||||
homeassistant/components/esphome/climate.py
|
||||
homeassistant/components/esphome/cover.py
|
||||
homeassistant/components/esphome/entry_data.py
|
||||
homeassistant/components/esphome/fan.py
|
||||
homeassistant/components/esphome/light.py
|
||||
homeassistant/components/esphome/sensor.py
|
||||
@ -250,7 +252,6 @@ omit =
|
||||
homeassistant/components/hitron_coda/device_tracker.py
|
||||
homeassistant/components/hive/*
|
||||
homeassistant/components/hlk_sw16/*
|
||||
homeassistant/components/homekit_controller/*
|
||||
homeassistant/components/homematic/*
|
||||
homeassistant/components/homematic/climate.py
|
||||
homeassistant/components/homematic/cover.py
|
||||
@ -344,6 +345,7 @@ omit =
|
||||
homeassistant/components/mastodon/notify.py
|
||||
homeassistant/components/matrix/*
|
||||
homeassistant/components/maxcube/*
|
||||
homeassistant/components/mcp23017/*
|
||||
homeassistant/components/media_extractor/*
|
||||
homeassistant/components/mediaroom/media_player.py
|
||||
homeassistant/components/message_bird/notify.py
|
||||
@ -487,6 +489,9 @@ omit =
|
||||
homeassistant/components/reddit/*
|
||||
homeassistant/components/rejseplanen/sensor.py
|
||||
homeassistant/components/remember_the_milk/__init__.py
|
||||
homeassistant/components/repetier/__init__.py
|
||||
homeassistant/components/repetier/sensor.py
|
||||
homeassistant/components/remote_rpi_gpio/*
|
||||
homeassistant/components/rest/binary_sensor.py
|
||||
homeassistant/components/rest/notify.py
|
||||
homeassistant/components/rest/switch.py
|
||||
@ -539,12 +544,14 @@ omit =
|
||||
homeassistant/components/slack/notify.py
|
||||
homeassistant/components/sma/sensor.py
|
||||
homeassistant/components/smappee/*
|
||||
homeassistant/components/smarthab/*
|
||||
homeassistant/components/smtp/notify.py
|
||||
homeassistant/components/snapcast/media_player.py
|
||||
homeassistant/components/snmp/*
|
||||
homeassistant/components/sochain/sensor.py
|
||||
homeassistant/components/socialblade/sensor.py
|
||||
homeassistant/components/solaredge/sensor.py
|
||||
homeassistant/components/solax/sensor.py
|
||||
homeassistant/components/somfy_mylink/*
|
||||
homeassistant/components/sonarr/sensor.py
|
||||
homeassistant/components/songpal/media_player.py
|
||||
@ -651,6 +658,7 @@ omit =
|
||||
homeassistant/components/waqi/sensor.py
|
||||
homeassistant/components/waterfurnace/*
|
||||
homeassistant/components/watson_iot/*
|
||||
homeassistant/components/watson_tts/tts.py
|
||||
homeassistant/components/waze_travel_time/sensor.py
|
||||
homeassistant/components/webostv/*
|
||||
homeassistant/components/wemo/*
|
||||
|
21
CODEOWNERS
21
CODEOWNERS
@ -32,6 +32,7 @@ homeassistant/components/automatic/* @armills
|
||||
homeassistant/components/automation/* @home-assistant/core
|
||||
homeassistant/components/aws/* @awarecan @robbiet480
|
||||
homeassistant/components/axis/* @kane610
|
||||
homeassistant/components/azure_event_hub/* @eavanvalkenburg
|
||||
homeassistant/components/bitcoin/* @fabaff
|
||||
homeassistant/components/bizkaibus/* @UgaitzEtxebarria
|
||||
homeassistant/components/blink/* @fronzbot
|
||||
@ -83,7 +84,7 @@ homeassistant/components/flock/* @fabaff
|
||||
homeassistant/components/flunearyou/* @bachya
|
||||
homeassistant/components/foursquare/* @robbiet480
|
||||
homeassistant/components/freebox/* @snoof85
|
||||
homeassistant/components/frontend/* @home-assistant/core
|
||||
homeassistant/components/frontend/* @home-assistant/frontend
|
||||
homeassistant/components/gearbest/* @HerrHofrat
|
||||
homeassistant/components/geniushub/* @zxdavb
|
||||
homeassistant/components/gitter/* @fabaff
|
||||
@ -131,6 +132,7 @@ homeassistant/components/kodi/* @armills
|
||||
homeassistant/components/konnected/* @heythisisnate
|
||||
homeassistant/components/lametric/* @robbiet480
|
||||
homeassistant/components/launch_library/* @ludeeus
|
||||
homeassistant/components/lcn/* @alengwenus
|
||||
homeassistant/components/lifx/* @amelchio
|
||||
homeassistant/components/lifx_cloud/* @amelchio
|
||||
homeassistant/components/lifx_legacy/* @amelchio
|
||||
@ -138,11 +140,12 @@ homeassistant/components/linux_battery/* @fabaff
|
||||
homeassistant/components/liveboxplaytv/* @pschmitt
|
||||
homeassistant/components/logger/* @home-assistant/core
|
||||
homeassistant/components/logi_circle/* @evanjd
|
||||
homeassistant/components/lovelace/* @home-assistant/core
|
||||
homeassistant/components/lovelace/* @home-assistant/frontend
|
||||
homeassistant/components/luci/* @fbradyirl
|
||||
homeassistant/components/luftdaten/* @fabaff
|
||||
homeassistant/components/mastodon/* @fabaff
|
||||
homeassistant/components/matrix/* @tinloaf
|
||||
homeassistant/components/mcp23017/* @jardiamj
|
||||
homeassistant/components/mediaroom/* @dgomes
|
||||
homeassistant/components/melissa/* @kennedyshead
|
||||
homeassistant/components/met/* @danielhiversen
|
||||
@ -173,8 +176,8 @@ homeassistant/components/openuv/* @bachya
|
||||
homeassistant/components/openweathermap/* @fabaff
|
||||
homeassistant/components/orangepi_gpio/* @pascallj
|
||||
homeassistant/components/owlet/* @oblogic7
|
||||
homeassistant/components/panel_custom/* @home-assistant/core
|
||||
homeassistant/components/panel_iframe/* @home-assistant/core
|
||||
homeassistant/components/panel_custom/* @home-assistant/frontend
|
||||
homeassistant/components/panel_iframe/* @home-assistant/frontend
|
||||
homeassistant/components/persistent_notification/* @home-assistant/core
|
||||
homeassistant/components/philips_js/* @elupus
|
||||
homeassistant/components/pi_hole/* @fabaff
|
||||
@ -190,6 +193,7 @@ homeassistant/components/qwikswitch/* @kellerza
|
||||
homeassistant/components/raincloud/* @vanstinator
|
||||
homeassistant/components/rainmachine/* @bachya
|
||||
homeassistant/components/random/* @fabaff
|
||||
homeassistant/components/repetier/* @MTrab
|
||||
homeassistant/components/rfxtrx/* @danielhiversen
|
||||
homeassistant/components/rmvtransport/* @cgtobi
|
||||
homeassistant/components/roomba/* @pschmitt
|
||||
@ -205,15 +209,17 @@ homeassistant/components/shiftr/* @fabaff
|
||||
homeassistant/components/shodan/* @fabaff
|
||||
homeassistant/components/simplisafe/* @bachya
|
||||
homeassistant/components/sma/* @kellerza
|
||||
homeassistant/components/smarthab/* @outadoc
|
||||
homeassistant/components/smartthings/* @andrewsayre
|
||||
homeassistant/components/smtp/* @fabaff
|
||||
homeassistant/components/solax/* @squishykid
|
||||
homeassistant/components/sonos/* @amelchio
|
||||
homeassistant/components/spaceapi/* @fabaff
|
||||
homeassistant/components/spider/* @peternijssen
|
||||
homeassistant/components/sql/* @dgomes
|
||||
homeassistant/components/statistics/* @fabaff
|
||||
homeassistant/components/stiebel_eltron/* @fucm
|
||||
homeassistant/components/sun/* @home-assistant/core
|
||||
homeassistant/components/sun/* @Swamp-Ig
|
||||
homeassistant/components/supla/* @mwegrzynek
|
||||
homeassistant/components/swiss_hydrological_data/* @fabaff
|
||||
homeassistant/components/swiss_public_transport/* @fabaff
|
||||
@ -253,6 +259,7 @@ homeassistant/components/velux/* @Julius2342
|
||||
homeassistant/components/version/* @fabaff
|
||||
homeassistant/components/vizio/* @raman325
|
||||
homeassistant/components/waqi/* @andrey-git
|
||||
homeassistant/components/watson_tts/* @rutkai
|
||||
homeassistant/components/weather/* @fabaff
|
||||
homeassistant/components/weblink/* @home-assistant/core
|
||||
homeassistant/components/websocket_api/* @home-assistant/core
|
||||
@ -261,14 +268,14 @@ homeassistant/components/worldclock/* @fabaff
|
||||
homeassistant/components/xfinity/* @cisasteelersfan
|
||||
homeassistant/components/xiaomi_aqara/* @danielhiversen @syssi
|
||||
homeassistant/components/xiaomi_miio/* @rytilahti @syssi
|
||||
homeassistant/components/xiaomi_tv/* @fattdev
|
||||
homeassistant/components/xiaomi_tv/* @simse
|
||||
homeassistant/components/xmpp/* @fabaff @flowolf
|
||||
homeassistant/components/yamaha_musiccast/* @jalmeroth
|
||||
homeassistant/components/yeelight/* @rytilahti @zewelor
|
||||
homeassistant/components/yeelightsunflower/* @lindsaymarkward
|
||||
homeassistant/components/yessssms/* @flowolf
|
||||
homeassistant/components/yi/* @bachya
|
||||
homeassistant/components/zeroconf/* @robbiet480
|
||||
homeassistant/components/zeroconf/* @robbiet480 @Kane610
|
||||
homeassistant/components/zha/* @dmulcahey @adminiuga
|
||||
homeassistant/components/zone/* @home-assistant/core
|
||||
homeassistant/components/zoneminder/* @rohankapoorcom
|
||||
|
@ -1,4 +1,4 @@
|
||||
Home Assistant |Build Status| |CI Status| |Coverage Status| |Chat Status|
|
||||
Home Assistant |Chat Status|
|
||||
=================================================================================
|
||||
|
||||
Home Assistant is a home automation platform running on Python 3. It is able to track and control all devices at home and offer a platform for automating control.
|
||||
@ -27,12 +27,6 @@ components <https://developers.home-assistant.io/docs/en/creating_component_inde
|
||||
If you run into issues while using Home Assistant or during development
|
||||
of a component, check the `Home Assistant help section <https://home-assistant.io/help/>`__ of our website for further help and information.
|
||||
|
||||
.. |Build Status| image:: https://travis-ci.org/home-assistant/home-assistant.svg?branch=dev
|
||||
:target: https://travis-ci.org/home-assistant/home-assistant
|
||||
.. |CI Status| image:: https://circleci.com/gh/home-assistant/home-assistant.svg?style=shield
|
||||
:target: https://circleci.com/gh/home-assistant/home-assistant
|
||||
.. |Coverage Status| image:: https://img.shields.io/coveralls/home-assistant/home-assistant.svg
|
||||
:target: https://coveralls.io/r/home-assistant/home-assistant?branch=master
|
||||
.. |Chat Status| image:: https://img.shields.io/discord/330944238910963714.svg
|
||||
:target: https://discord.gg/c5DvZ4e
|
||||
.. |screenshot-states| image:: https://raw.github.com/home-assistant/home-assistant/master/docs/screenshots.png
|
||||
|
@ -2,108 +2,20 @@
|
||||
|
||||
trigger:
|
||||
batch: true
|
||||
branches:
|
||||
include:
|
||||
- dev
|
||||
- master
|
||||
tags:
|
||||
include:
|
||||
- '*'
|
||||
pr: none
|
||||
variables:
|
||||
- name: versionBuilder
|
||||
value: '3.2'
|
||||
- name: versionWheels
|
||||
value: '0.7'
|
||||
- group: docker
|
||||
- group: wheels
|
||||
- group: github
|
||||
- group: twine
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
- job: 'Wheels'
|
||||
condition: or(eq(variables['Build.SourceBranchName'], 'dev'), eq(variables['Build.SourceBranchName'], 'master'))
|
||||
timeoutInMinutes: 360
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
strategy:
|
||||
maxParallel: 3
|
||||
matrix:
|
||||
amd64:
|
||||
buildArch: 'amd64'
|
||||
i386:
|
||||
buildArch: 'i386'
|
||||
armhf:
|
||||
buildArch: 'armhf'
|
||||
armv7:
|
||||
buildArch: 'armv7'
|
||||
aarch64:
|
||||
buildArch: 'aarch64'
|
||||
steps:
|
||||
- script: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
qemu-user-static \
|
||||
binfmt-support \
|
||||
curl
|
||||
|
||||
sudo mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc
|
||||
sudo update-binfmts --enable qemu-arm
|
||||
sudo update-binfmts --enable qemu-aarch64
|
||||
displayName: 'Initial cross build'
|
||||
- script: |
|
||||
mkdir -p .ssh
|
||||
echo -e "-----BEGIN RSA PRIVATE KEY-----\n$(wheelsSSH)\n-----END RSA PRIVATE KEY-----" >> .ssh/id_rsa
|
||||
ssh-keyscan -H $(wheelsHost) >> .ssh/known_hosts
|
||||
chmod 600 .ssh/*
|
||||
displayName: 'Install ssh key'
|
||||
- script: sudo docker pull homeassistant/$(buildArch)-wheels:$(versionWheels)
|
||||
displayName: 'Install wheels builder'
|
||||
- script: |
|
||||
cp requirements_all.txt requirements_wheels.txt
|
||||
if [ "$(Build.SourceBranchName)" == "dev" ]; then
|
||||
curl -s -o requirements_diff.txt https://raw.githubusercontent.com/home-assistant/home-assistant/master/requirements_all.txt
|
||||
else
|
||||
touch requirements_diff.txt
|
||||
fi
|
||||
|
||||
requirement_files="requirements_wheels.txt requirements_diff.txt"
|
||||
for requirement_file in ${requirement_files}; do
|
||||
sed -i "s|# pytradfri|pytradfri|g" ${requirement_file}
|
||||
sed -i "s|# pybluez|pybluez|g" ${requirement_file}
|
||||
sed -i "s|# bluepy|bluepy|g" ${requirement_file}
|
||||
sed -i "s|# beacontools|beacontools|g" ${requirement_file}
|
||||
sed -i "s|# RPi.GPIO|RPi.GPIO|g" ${requirement_file}
|
||||
sed -i "s|# raspihats|raspihats|g" ${requirement_file}
|
||||
sed -i "s|# rpi-rf|rpi-rf|g" ${requirement_file}
|
||||
sed -i "s|# blinkt|blinkt|g" ${requirement_file}
|
||||
sed -i "s|# fritzconnection|fritzconnection|g" ${requirement_file}
|
||||
sed -i "s|# pyuserinput|pyuserinput|g" ${requirement_file}
|
||||
sed -i "s|# evdev|evdev|g" ${requirement_file}
|
||||
sed -i "s|# smbus-cffi|smbus-cffi|g" ${requirement_file}
|
||||
sed -i "s|# i2csense|i2csense|g" ${requirement_file}
|
||||
sed -i "s|# python-eq3bt|python-eq3bt|g" ${requirement_file}
|
||||
sed -i "s|# pycups|pycups|g" ${requirement_file}
|
||||
sed -i "s|# homekit|homekit|g" ${requirement_file}
|
||||
sed -i "s|# decora_wifi|decora_wifi|g" ${requirement_file}
|
||||
sed -i "s|# decora|decora|g" ${requirement_file}
|
||||
sed -i "s|# PySwitchbot|PySwitchbot|g" ${requirement_file}
|
||||
sed -i "s|# pySwitchmate|pySwitchmate|g" ${requirement_file}
|
||||
sed -i "s|# face_recognition|face_recognition|g" ${requirement_file}
|
||||
done
|
||||
displayName: 'Prepare requirements files for Hass.io'
|
||||
- script: |
|
||||
sudo docker run --rm -v $(pwd):/data:ro -v $(pwd)/.ssh:/root/.ssh:rw \
|
||||
homeassistant/$(buildArch)-wheels:$(versionWheels) \
|
||||
--apk "build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;linux-headers;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev" \
|
||||
--index $(wheelsIndex) \
|
||||
--requirement requirements_wheels.txt \
|
||||
--requirement-diff requirements_diff.txt \
|
||||
--upload rsync \
|
||||
--remote wheels@$(wheelsHost):/opt/wheels
|
||||
displayName: 'Run wheels build'
|
||||
|
||||
|
||||
- job: 'VersionValidate'
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
|
@ -94,6 +94,13 @@ async def async_from_config_dict(config: Dict[str, Any],
|
||||
stop = time()
|
||||
_LOGGER.info("Home Assistant initialized in %.2fs", stop-start)
|
||||
|
||||
if sys.version_info[:3] < (3, 6, 0):
|
||||
hass.components.persistent_notification.async_create(
|
||||
"Python 3.5 support is deprecated and will "
|
||||
"be removed in the first release after August 1. Please "
|
||||
"upgrade Python.", "Python version", "python_version"
|
||||
)
|
||||
|
||||
# TEMP: warn users for invalid slugs
|
||||
# Remove after 0.94 or 1.0
|
||||
if cv.INVALID_SLUGS_FOUND or cv.INVALID_ENTITY_IDS_FOUND:
|
||||
|
@ -118,7 +118,7 @@ async def async_setup(hass, config):
|
||||
|
||||
tasks = [alert.async_update_ha_state() for alert in entities]
|
||||
if tasks:
|
||||
await asyncio.wait(tasks, loop=hass.loop)
|
||||
await asyncio.wait(tasks)
|
||||
|
||||
return True
|
||||
|
||||
|
@ -39,7 +39,7 @@ class Auth:
|
||||
self._prefs = None
|
||||
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||
|
||||
self._get_token_lock = asyncio.Lock(loop=hass.loop)
|
||||
self._get_token_lock = asyncio.Lock()
|
||||
|
||||
async def async_do_auth(self, accept_grant_code):
|
||||
"""Do authentication with an AcceptGrant code."""
|
||||
@ -97,7 +97,7 @@ class Auth:
|
||||
|
||||
try:
|
||||
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||
with async_timeout.timeout(DEFAULT_TIMEOUT, loop=self.hass.loop):
|
||||
with async_timeout.timeout(DEFAULT_TIMEOUT):
|
||||
response = await session.post(LWA_TOKEN_URI,
|
||||
headers=LWA_HEADERS,
|
||||
data=lwa_params,
|
||||
|
@ -1432,7 +1432,7 @@ async def async_send_changereport_message(hass, config, alexa_entity):
|
||||
|
||||
try:
|
||||
session = aiohttp_client.async_get_clientsession(hass)
|
||||
with async_timeout.timeout(DEFAULT_TIMEOUT, loop=hass.loop):
|
||||
with async_timeout.timeout(DEFAULT_TIMEOUT):
|
||||
response = await session.post(config.endpoint,
|
||||
headers=headers,
|
||||
json=message_serialized,
|
||||
|
23
homeassistant/components/ambiclimate/.translations/es.json
Normal file
23
homeassistant/components/ambiclimate/.translations/es.json
Normal 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"
|
||||
}
|
||||
}
|
23
homeassistant/components/ambiclimate/.translations/fr.json
Normal file
23
homeassistant/components/ambiclimate/.translations/fr.json
Normal 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"
|
||||
}
|
||||
}
|
23
homeassistant/components/ambiclimate/.translations/no.json
Normal file
23
homeassistant/components/ambiclimate/.translations/no.json
Normal 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"
|
||||
}
|
||||
}
|
23
homeassistant/components/ambiclimate/.translations/pl.json
Normal file
23
homeassistant/components/ambiclimate/.translations/pl.json
Normal 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"
|
||||
}
|
||||
}
|
23
homeassistant/components/ambiclimate/.translations/sl.json
Normal file
23
homeassistant/components/ambiclimate/.translations/sl.json
Normal 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"
|
||||
}
|
||||
}
|
23
homeassistant/components/ambiclimate/.translations/sv.json
Normal file
23
homeassistant/components/ambiclimate/.translations/sv.json
Normal 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"
|
||||
}
|
||||
}
|
@ -62,7 +62,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
|
||||
return
|
||||
|
||||
if _token_info:
|
||||
await store.async_save(token_info)
|
||||
await store.async_save(_token_info)
|
||||
token_info = _token_info
|
||||
|
||||
data_connection = ambiclimate.AmbiclimateConnection(oauth,
|
||||
|
@ -1,9 +1,10 @@
|
||||
{
|
||||
"domain": "ambiclimate",
|
||||
"name": "Ambiclimate",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/ambiclimate",
|
||||
"requirements": [
|
||||
"ambiclimate==0.1.1"
|
||||
"ambiclimate==0.1.2"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": [
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"domain": "ambient_station",
|
||||
"name": "Ambient station",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/ambient_station",
|
||||
"requirements": [
|
||||
"aioambient==0.3.0"
|
||||
|
@ -203,8 +203,7 @@ class AmcrestCam(Camera):
|
||||
"""Return the camera model."""
|
||||
return self._model
|
||||
|
||||
@property
|
||||
def stream_source(self):
|
||||
async def stream_source(self):
|
||||
"""Return the source of the stream."""
|
||||
return self._api.rtsp_url(typeno=self._resolution)
|
||||
|
||||
|
@ -233,7 +233,7 @@ async def async_setup(hass, config):
|
||||
|
||||
tasks = [async_setup_ipcamera(conf) for conf in config[DOMAIN]]
|
||||
if tasks:
|
||||
await asyncio.wait(tasks, loop=hass.loop)
|
||||
await asyncio.wait(tasks)
|
||||
|
||||
return True
|
||||
|
||||
|
@ -90,20 +90,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
||||
if CONF_ADB_SERVER_IP not in config:
|
||||
# Use "python-adb" (Python ADB implementation)
|
||||
adb_log = "using Python ADB implementation "
|
||||
if CONF_ADBKEY in config:
|
||||
aftv = setup(host, config[CONF_ADBKEY],
|
||||
device_class=config[CONF_DEVICE_CLASS])
|
||||
adb_log = " using adbkey='{0}'".format(config[CONF_ADBKEY])
|
||||
adb_log += "with adbkey='{0}'".format(config[CONF_ADBKEY])
|
||||
|
||||
else:
|
||||
aftv = setup(host, device_class=config[CONF_DEVICE_CLASS])
|
||||
adb_log = ""
|
||||
adb_log += "without adbkey authentication"
|
||||
else:
|
||||
# Use "pure-python-adb" (communicate with ADB server)
|
||||
aftv = setup(host, adb_server_ip=config[CONF_ADB_SERVER_IP],
|
||||
adb_server_port=config[CONF_ADB_SERVER_PORT],
|
||||
device_class=config[CONF_DEVICE_CLASS])
|
||||
adb_log = " using ADB server at {0}:{1}".format(
|
||||
adb_log = "using ADB server at {0}:{1}".format(
|
||||
config[CONF_ADB_SERVER_IP], config[CONF_ADB_SERVER_PORT])
|
||||
|
||||
if not aftv.available:
|
||||
@ -117,7 +118,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
else:
|
||||
device_name = 'Android TV / Fire TV device'
|
||||
|
||||
_LOGGER.warning("Could not connect to %s at %s%s",
|
||||
_LOGGER.warning("Could not connect to %s at %s %s",
|
||||
device_name, host, adb_log)
|
||||
raise PlatformNotReady
|
||||
|
||||
@ -156,10 +157,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
for target_device in target_devices:
|
||||
output = target_device.adb_command(cmd)
|
||||
|
||||
# log the output if there is any
|
||||
if output and (not isinstance(output, str) or output.strip()):
|
||||
# log the output, if there is any
|
||||
if output:
|
||||
_LOGGER.info("Output of command '%s' from '%s': %s",
|
||||
cmd, target_device.entity_id, repr(output))
|
||||
cmd, target_device.entity_id, output)
|
||||
|
||||
hass.services.register(ANDROIDTV_DOMAIN, SERVICE_ADB_COMMAND,
|
||||
service_adb_command,
|
||||
@ -224,6 +225,7 @@ class ADBDevice(MediaPlayerDevice):
|
||||
self.exceptions = (ConnectionResetError, RuntimeError)
|
||||
|
||||
# Property attributes
|
||||
self._adb_response = None
|
||||
self._available = self.aftv.available
|
||||
self._current_app = None
|
||||
self._state = None
|
||||
@ -243,6 +245,11 @@ class ADBDevice(MediaPlayerDevice):
|
||||
"""Return whether or not the ADB connection is valid."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Provide the last ADB command's response as an attribute."""
|
||||
return {'adb_response': self._adb_response}
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the device name."""
|
||||
@ -304,12 +311,24 @@ class ADBDevice(MediaPlayerDevice):
|
||||
"""Send an ADB command to an Android TV / Fire TV device."""
|
||||
key = self._keys.get(cmd)
|
||||
if key:
|
||||
return self.aftv.adb_shell('input keyevent {}'.format(key))
|
||||
self.aftv.adb_shell('input keyevent {}'.format(key))
|
||||
self._adb_response = None
|
||||
self.schedule_update_ha_state()
|
||||
return
|
||||
|
||||
if cmd == 'GET_PROPERTIES':
|
||||
return self.aftv.get_properties_dict()
|
||||
self._adb_response = str(self.aftv.get_properties_dict())
|
||||
self.schedule_update_ha_state()
|
||||
return self._adb_response
|
||||
|
||||
return self.aftv.adb_shell(cmd)
|
||||
response = self.aftv.adb_shell(cmd)
|
||||
if isinstance(response, str) and response.strip():
|
||||
self._adb_response = response.strip()
|
||||
else:
|
||||
self._adb_response = None
|
||||
|
||||
self.schedule_update_ha_state()
|
||||
return self._adb_response
|
||||
|
||||
|
||||
class AndroidTVDevice(ADBDevice):
|
||||
|
@ -47,7 +47,7 @@ async def async_setup_platform(hass, config, async_add_entities,
|
||||
hass.async_create_task(device.async_update_ha_state())
|
||||
|
||||
avr = await anthemav.Connection.create(
|
||||
host=host, port=port, loop=hass.loop,
|
||||
host=host, port=port,
|
||||
update_callback=async_anthemav_update_callback)
|
||||
|
||||
device = AnthemAVR(avr, name)
|
||||
|
@ -82,7 +82,7 @@ class APIEventStream(HomeAssistantView):
|
||||
raise Unauthorized()
|
||||
hass = request.app['hass']
|
||||
stop_obj = object()
|
||||
to_write = asyncio.Queue(loop=hass.loop)
|
||||
to_write = asyncio.Queue()
|
||||
|
||||
restrict = request.query.get('restrict')
|
||||
if restrict:
|
||||
@ -119,8 +119,7 @@ class APIEventStream(HomeAssistantView):
|
||||
|
||||
while True:
|
||||
try:
|
||||
with async_timeout.timeout(STREAM_PING_INTERVAL,
|
||||
loop=hass.loop):
|
||||
with async_timeout.timeout(STREAM_PING_INTERVAL):
|
||||
payload = await to_write.get()
|
||||
|
||||
if payload is stop_obj:
|
||||
|
@ -1,6 +1,5 @@
|
||||
"""APNS Notification platform."""
|
||||
import logging
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@ -149,7 +148,8 @@ class ApnsNotificationService(BaseNotificationService):
|
||||
self.devices = {}
|
||||
self.device_states = {}
|
||||
self.topic = topic
|
||||
if os.path.isfile(self.yaml_path):
|
||||
|
||||
try:
|
||||
self.devices = {
|
||||
str(key): ApnsDevice(
|
||||
str(key),
|
||||
@ -160,6 +160,8 @@ class ApnsNotificationService(BaseNotificationService):
|
||||
for (key, value) in
|
||||
load_yaml_config_file(self.yaml_path).items()
|
||||
}
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
tracking_ids = [
|
||||
device.full_tracking_device_id
|
||||
|
@ -167,7 +167,7 @@ async def async_setup(hass, config):
|
||||
|
||||
tasks = [_setup_atv(hass, config, conf) for conf in config.get(DOMAIN, [])]
|
||||
if tasks:
|
||||
await asyncio.wait(tasks, loop=hass.loop)
|
||||
await asyncio.wait(tasks)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SCAN, async_service_handler,
|
||||
|
@ -124,7 +124,7 @@ async def async_setup(hass, config):
|
||||
context=service_call.context))
|
||||
|
||||
if tasks:
|
||||
await asyncio.wait(tasks, loop=hass.loop)
|
||||
await asyncio.wait(tasks)
|
||||
|
||||
async def turn_onoff_service_handler(service_call):
|
||||
"""Handle automation turn on/off service calls."""
|
||||
@ -134,7 +134,7 @@ async def async_setup(hass, config):
|
||||
tasks.append(getattr(entity, method)())
|
||||
|
||||
if tasks:
|
||||
await asyncio.wait(tasks, loop=hass.loop)
|
||||
await asyncio.wait(tasks)
|
||||
|
||||
async def toggle_service_handler(service_call):
|
||||
"""Handle automation toggle service calls."""
|
||||
@ -146,7 +146,7 @@ async def async_setup(hass, config):
|
||||
tasks.append(entity.async_turn_on())
|
||||
|
||||
if tasks:
|
||||
await asyncio.wait(tasks, loop=hass.loop)
|
||||
await asyncio.wait(tasks)
|
||||
|
||||
async def reload_service_handler(service_call):
|
||||
"""Remove all automations and load new ones from config."""
|
||||
|
@ -166,14 +166,14 @@ async def _validate_aws_credentials(hass, credential):
|
||||
profile = aws_config.get(CONF_PROFILE_NAME)
|
||||
|
||||
if profile is not None:
|
||||
session = aiobotocore.AioSession(profile=profile, loop=hass.loop)
|
||||
session = aiobotocore.AioSession(profile=profile)
|
||||
del aws_config[CONF_PROFILE_NAME]
|
||||
if CONF_ACCESS_KEY_ID in aws_config:
|
||||
del aws_config[CONF_ACCESS_KEY_ID]
|
||||
if CONF_SECRET_ACCESS_KEY in aws_config:
|
||||
del aws_config[CONF_SECRET_ACCESS_KEY]
|
||||
else:
|
||||
session = aiobotocore.AioSession(loop=hass.loop)
|
||||
session = aiobotocore.AioSession()
|
||||
|
||||
if credential[CONF_VALIDATE]:
|
||||
async with session.create_client("iam", **aws_config) as client:
|
||||
|
@ -94,10 +94,10 @@ async def async_get_service(hass, config, discovery_info=None):
|
||||
if session is None:
|
||||
profile = aws_config.get(CONF_PROFILE_NAME)
|
||||
if profile is not None:
|
||||
session = aiobotocore.AioSession(profile=profile, loop=hass.loop)
|
||||
session = aiobotocore.AioSession(profile=profile)
|
||||
del aws_config[CONF_PROFILE_NAME]
|
||||
else:
|
||||
session = aiobotocore.AioSession(loop=hass.loop)
|
||||
session = aiobotocore.AioSession()
|
||||
|
||||
aws_config[CONF_REGION] = region_name
|
||||
|
||||
|
18
homeassistant/components/axis/.translations/nl.json
Normal file
18
homeassistant/components/axis/.translations/nl.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -2,7 +2,8 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Enheten \u00e4r redan konfigurerad",
|
||||
"bad_config_file": "Felaktig data fr\u00e5n config fil"
|
||||
"bad_config_file": "Felaktig data fr\u00e5n config fil",
|
||||
"link_local_address": "Link local addresses are not supported"
|
||||
},
|
||||
"error": {
|
||||
"already_configured": "Enheten \u00e4r redan konfigurerad",
|
||||
@ -17,7 +18,7 @@
|
||||
"port": "Port",
|
||||
"username": "Anv\u00e4ndarnamn"
|
||||
},
|
||||
"title": "Konfigurera Axis enhet"
|
||||
"title": "Konfigurera Axis-enhet"
|
||||
}
|
||||
},
|
||||
"title": "Axis enhet"
|
||||
|
86
homeassistant/components/axis/axis_base.py
Normal file
86
homeassistant/components/axis/axis_base.py
Normal 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)
|
@ -2,6 +2,8 @@
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from axis.event_stream import CLASS_INPUT, CLASS_OUTPUT
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.const import CONF_MAC, CONF_TRIGGER_TIME
|
||||
from homeassistant.core import callback
|
||||
@ -9,7 +11,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from .const import DOMAIN as AXIS_DOMAIN, LOGGER
|
||||
from .axis_base import AxisEventBase
|
||||
from .const import DOMAIN as AXIS_DOMAIN
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
@ -21,32 +24,21 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
def async_add_sensor(event_id):
|
||||
"""Add binary sensor from Axis device."""
|
||||
event = device.api.event.events[event_id]
|
||||
async_add_entities([AxisBinarySensor(event, device)], True)
|
||||
|
||||
if event.CLASS != CLASS_OUTPUT:
|
||||
async_add_entities([AxisBinarySensor(event, device)], True)
|
||||
|
||||
device.listeners.append(async_dispatcher_connect(
|
||||
hass, device.event_new_sensor, async_add_sensor))
|
||||
|
||||
|
||||
class AxisBinarySensor(BinarySensorDevice):
|
||||
class AxisBinarySensor(AxisEventBase, BinarySensorDevice):
|
||||
"""Representation of a binary Axis event."""
|
||||
|
||||
def __init__(self, event, device):
|
||||
"""Initialize the Axis binary sensor."""
|
||||
self.event = event
|
||||
self.device = device
|
||||
super().__init__(event, device)
|
||||
self.remove_timer = None
|
||||
self.unsub_dispatcher = None
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe sensors events."""
|
||||
self.event.register_callback(self.update_callback)
|
||||
self.unsub_dispatcher = async_dispatcher_connect(
|
||||
self.hass, self.device.event_reachable, self.update_callback)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Disconnect device object when removed."""
|
||||
self.event.remove_callback(self.update_callback)
|
||||
self.unsub_dispatcher()
|
||||
|
||||
@callback
|
||||
def update_callback(self, no_delay=False):
|
||||
@ -67,7 +59,6 @@ class AxisBinarySensor(BinarySensorDevice):
|
||||
@callback
|
||||
def _delay_update(now):
|
||||
"""Timer callback for sensor update."""
|
||||
LOGGER.debug("%s called delayed (%s sec) update", self.name, delay)
|
||||
self.async_schedule_update_ha_state()
|
||||
self.remove_timer = None
|
||||
|
||||
@ -83,32 +74,10 @@ class AxisBinarySensor(BinarySensorDevice):
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the event."""
|
||||
return '{} {} {}'.format(
|
||||
self.device.name, self.event.TYPE, self.event.id)
|
||||
if self.event.CLASS == CLASS_INPUT and self.event.id and \
|
||||
self.device.api.vapix.ports[self.event.id].name:
|
||||
return '{} {}'.format(
|
||||
self.device.name,
|
||||
self.device.api.vapix.ports[self.event.id].name)
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of the event."""
|
||||
return self.event.CLASS
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique identifier for this device."""
|
||||
return '{}-{}-{}'.format(
|
||||
self.device.serial, self.event.topic, self.event.id)
|
||||
|
||||
def available(self):
|
||||
"""Return True if device is available."""
|
||||
return self.device.available
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return a device description for device registry."""
|
||||
return {
|
||||
'identifiers': {(AXIS_DOMAIN, self.device.serial)}
|
||||
}
|
||||
return super().name
|
||||
|
@ -6,9 +6,9 @@ from homeassistant.components.mjpeg.camera import (
|
||||
from homeassistant.const import (
|
||||
CONF_AUTHENTICATION, CONF_DEVICE, CONF_HOST, CONF_MAC, CONF_NAME,
|
||||
CONF_PASSWORD, CONF_PORT, CONF_USERNAME, HTTP_DIGEST_AUTHENTICATION)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from .axis_base import AxisEntityBase
|
||||
from .const import DOMAIN as AXIS_DOMAIN
|
||||
|
||||
AXIS_IMAGE = 'http://{}:{}/axis-cgi/jpg/image.cgi'
|
||||
@ -38,65 +38,40 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
async_add_entities([AxisCamera(config, device)])
|
||||
|
||||
|
||||
class AxisCamera(MjpegCamera):
|
||||
class AxisCamera(AxisEntityBase, MjpegCamera):
|
||||
"""Representation of a Axis camera."""
|
||||
|
||||
def __init__(self, config, device):
|
||||
"""Initialize Axis Communications camera component."""
|
||||
super().__init__(config)
|
||||
self.device_config = config
|
||||
self.device = device
|
||||
self.port = device.config_entry.data[CONF_DEVICE][CONF_PORT]
|
||||
self.unsub_dispatcher = []
|
||||
AxisEntityBase.__init__(self, device)
|
||||
MjpegCamera.__init__(self, config)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe camera events."""
|
||||
self.unsub_dispatcher.append(async_dispatcher_connect(
|
||||
self.hass, self.device.event_new_address, self._new_address))
|
||||
self.unsub_dispatcher.append(async_dispatcher_connect(
|
||||
self.hass, self.device.event_reachable, self.update_callback))
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Disconnect device object when removed."""
|
||||
for unsub_dispatcher in self.unsub_dispatcher:
|
||||
unsub_dispatcher()
|
||||
await super().async_added_to_hass()
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return supported features."""
|
||||
return SUPPORT_STREAM
|
||||
|
||||
@property
|
||||
def stream_source(self):
|
||||
async def stream_source(self):
|
||||
"""Return the stream source."""
|
||||
return AXIS_STREAM.format(
|
||||
self.device.config_entry.data[CONF_DEVICE][CONF_USERNAME],
|
||||
self.device.config_entry.data[CONF_DEVICE][CONF_PASSWORD],
|
||||
self.device.host)
|
||||
|
||||
@callback
|
||||
def update_callback(self, no_delay=None):
|
||||
"""Update the cameras state."""
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if device is available."""
|
||||
return self.device.available
|
||||
|
||||
def _new_address(self):
|
||||
"""Set new device address for video stream."""
|
||||
self._mjpeg_url = AXIS_VIDEO.format(self.device.host, self.port)
|
||||
self._still_image_url = AXIS_IMAGE.format(self.device.host, self.port)
|
||||
port = self.device.config_entry.data[CONF_DEVICE][CONF_PORT]
|
||||
self._mjpeg_url = AXIS_VIDEO.format(self.device.host, port)
|
||||
self._still_image_url = AXIS_IMAGE.format(self.device.host, port)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique identifier for this device."""
|
||||
return '{}-camera'.format(self.device.serial)
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return a device description for device registry."""
|
||||
return {
|
||||
'identifiers': {(AXIS_DOMAIN, self.device.serial)}
|
||||
}
|
||||
|
@ -146,7 +146,7 @@ class AxisFlowHandler(config_entries.ConfigFlow):
|
||||
entry.data[CONF_DEVICE][CONF_HOST] = host
|
||||
self.hass.config_entries.async_update_entry(entry)
|
||||
|
||||
async def async_step_discovery(self, discovery_info):
|
||||
async def async_step_zeroconf(self, discovery_info):
|
||||
"""Prepare configuration for a discovered Axis device.
|
||||
|
||||
This flow is triggered by the discovery component.
|
||||
@ -155,6 +155,13 @@ class AxisFlowHandler(config_entries.ConfigFlow):
|
||||
return self.async_abort(reason='link_local_address')
|
||||
|
||||
serialnumber = discovery_info['properties']['macaddress']
|
||||
# pylint: disable=unsupported-assignment-operation
|
||||
self.context['macaddress'] = serialnumber
|
||||
|
||||
if any(serialnumber == flow['context']['macaddress']
|
||||
for flow in self._async_in_progress()):
|
||||
return self.async_abort(reason='already_in_progress')
|
||||
|
||||
device_entries = configured_devices(self.hass)
|
||||
|
||||
if serialnumber in device_entries:
|
||||
|
@ -83,19 +83,23 @@ class AxisNetworkDevice:
|
||||
self.product_type = self.api.vapix.params.prodtype
|
||||
|
||||
if self.config_entry.options[CONF_CAMERA]:
|
||||
|
||||
self.hass.async_create_task(
|
||||
self.hass.config_entries.async_forward_entry_setup(
|
||||
self.config_entry, 'camera'))
|
||||
|
||||
if self.config_entry.options[CONF_EVENTS]:
|
||||
task = self.hass.async_create_task(
|
||||
self.hass.config_entries.async_forward_entry_setup(
|
||||
self.config_entry, 'binary_sensor'))
|
||||
|
||||
self.api.stream.connection_status_callback = \
|
||||
self.async_connection_status_callback
|
||||
self.api.enable_events(event_callback=self.async_event_callback)
|
||||
task.add_done_callback(self.start)
|
||||
|
||||
platform_tasks = [
|
||||
self.hass.config_entries.async_forward_entry_setup(
|
||||
self.config_entry, platform)
|
||||
for platform in ['binary_sensor', 'switch']
|
||||
]
|
||||
self.hass.async_create_task(self.start(platform_tasks))
|
||||
|
||||
self.config_entry.add_update_listener(self.async_new_address_callback)
|
||||
|
||||
@ -145,9 +149,9 @@ class AxisNetworkDevice:
|
||||
if action == 'add':
|
||||
async_dispatcher_send(self.hass, self.event_new_sensor, event_id)
|
||||
|
||||
@callback
|
||||
def start(self, fut):
|
||||
"""Start the event stream."""
|
||||
async def start(self, platform_tasks):
|
||||
"""Start the event stream when all platforms are loaded."""
|
||||
await asyncio.gather(*platform_tasks)
|
||||
self.api.start()
|
||||
|
||||
@callback
|
||||
@ -157,15 +161,22 @@ class AxisNetworkDevice:
|
||||
|
||||
async def async_reset(self):
|
||||
"""Reset this device to default state."""
|
||||
self.api.stop()
|
||||
platform_tasks = []
|
||||
|
||||
if self.config_entry.options[CONF_CAMERA]:
|
||||
await self.hass.config_entries.async_forward_entry_unload(
|
||||
self.config_entry, 'camera')
|
||||
platform_tasks.append(
|
||||
self.hass.config_entries.async_forward_entry_unload(
|
||||
self.config_entry, 'camera'))
|
||||
|
||||
if self.config_entry.options[CONF_EVENTS]:
|
||||
await self.hass.config_entries.async_forward_entry_unload(
|
||||
self.config_entry, 'binary_sensor')
|
||||
self.api.stop()
|
||||
platform_tasks += [
|
||||
self.hass.config_entries.async_forward_entry_unload(
|
||||
self.config_entry, platform)
|
||||
for platform in ['binary_sensor', 'switch']
|
||||
]
|
||||
|
||||
await asyncio.gather(*platform_tasks)
|
||||
|
||||
for unsub_dispatcher in self.listeners:
|
||||
unsub_dispatcher()
|
||||
@ -185,13 +196,22 @@ async def get_device(hass, config):
|
||||
port=config[CONF_PORT], web_proto='http')
|
||||
|
||||
device.vapix.initialize_params(preload_data=False)
|
||||
device.vapix.initialize_ports()
|
||||
|
||||
try:
|
||||
with async_timeout.timeout(15):
|
||||
await hass.async_add_executor_job(
|
||||
device.vapix.params.update_brand)
|
||||
await hass.async_add_executor_job(
|
||||
device.vapix.params.update_properties)
|
||||
|
||||
await asyncio.gather(
|
||||
hass.async_add_executor_job(
|
||||
device.vapix.params.update_brand),
|
||||
|
||||
hass.async_add_executor_job(
|
||||
device.vapix.params.update_properties),
|
||||
|
||||
hass.async_add_executor_job(
|
||||
device.vapix.ports.update)
|
||||
)
|
||||
|
||||
return device
|
||||
|
||||
except axis.Unauthorized:
|
||||
|
@ -1,8 +1,10 @@
|
||||
{
|
||||
"domain": "axis",
|
||||
"name": "Axis",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/axis",
|
||||
"requirements": ["axis==22"],
|
||||
"requirements": ["axis==24"],
|
||||
"dependencies": [],
|
||||
"zeroconf": ["_axis-video._tcp.local."],
|
||||
"codeowners": ["@kane610"]
|
||||
}
|
||||
|
@ -14,6 +14,7 @@
|
||||
},
|
||||
"error": {
|
||||
"already_configured": "Device is already configured",
|
||||
"already_in_progress": "Config flow for device is already in progress.",
|
||||
"device_unavailable": "Device is not available",
|
||||
"faulty_credentials": "Bad user credentials"
|
||||
},
|
||||
|
59
homeassistant/components/axis/switch.py
Normal file
59
homeassistant/components/axis/switch.py
Normal 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
|
80
homeassistant/components/azure_event_hub/__init__.py
Normal file
80
homeassistant/components/azure_event_hub/__init__.py
Normal 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
|
8
homeassistant/components/azure_event_hub/manifest.json
Normal file
8
homeassistant/components/azure_event_hub/manifest.json
Normal 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"]
|
||||
}
|
@ -8,7 +8,7 @@ from homeassistant.helpers import (
|
||||
from homeassistant.const import (
|
||||
CONF_USERNAME, CONF_PASSWORD, CONF_NAME, CONF_SCAN_INTERVAL,
|
||||
CONF_BINARY_SENSORS, CONF_SENSORS, CONF_FILENAME,
|
||||
CONF_MONITORED_CONDITIONS, TEMP_FAHRENHEIT)
|
||||
CONF_MONITORED_CONDITIONS, CONF_MODE, CONF_OFFSET, TEMP_FAHRENHEIT)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -41,7 +41,7 @@ BINARY_SENSORS = {
|
||||
|
||||
SENSORS = {
|
||||
TYPE_TEMPERATURE: ['Temperature', TEMP_FAHRENHEIT, 'mdi:thermometer'],
|
||||
TYPE_BATTERY: ['Battery', '%', 'mdi:battery-80'],
|
||||
TYPE_BATTERY: ['Battery', '', 'mdi:battery-80'],
|
||||
TYPE_WIFI_STRENGTH: ['Wifi Signal', 'dBm', 'mdi:wifi-strength-2'],
|
||||
}
|
||||
|
||||
@ -75,6 +75,8 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
vol.Optional(CONF_BINARY_SENSORS, default={}):
|
||||
BINARY_SENSOR_SCHEMA,
|
||||
vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA,
|
||||
vol.Optional(CONF_OFFSET, default=1): int,
|
||||
vol.Optional(CONF_MODE, default=''): cv.string,
|
||||
})
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA)
|
||||
@ -87,8 +89,12 @@ def setup(hass, config):
|
||||
username = conf[CONF_USERNAME]
|
||||
password = conf[CONF_PASSWORD]
|
||||
scan_interval = conf[CONF_SCAN_INTERVAL]
|
||||
is_legacy = bool(conf[CONF_MODE] == 'legacy')
|
||||
motion_interval = conf[CONF_OFFSET]
|
||||
hass.data[BLINK_DATA] = blinkpy.Blink(username=username,
|
||||
password=password)
|
||||
password=password,
|
||||
motion_interval=motion_interval,
|
||||
legacy_subdomain=is_legacy)
|
||||
hass.data[BLINK_DATA].refresh_rate = scan_interval.total_seconds()
|
||||
hass.data[BLINK_DATA].start()
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Blink",
|
||||
"documentation": "https://www.home-assistant.io/components/blink",
|
||||
"requirements": [
|
||||
"blinkpy==0.13.1"
|
||||
"blinkpy==0.14.0"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": [
|
||||
|
@ -255,7 +255,7 @@ class BluesoundPlayer(MediaPlayerDevice):
|
||||
BluesoundPlayer._TimeoutException):
|
||||
_LOGGER.info("Node %s is offline, retrying later", self._name)
|
||||
await asyncio.sleep(
|
||||
NODE_OFFLINE_CHECK_TIMEOUT, loop=self._hass.loop)
|
||||
NODE_OFFLINE_CHECK_TIMEOUT)
|
||||
self.start_polling()
|
||||
|
||||
except CancelledError:
|
||||
@ -318,7 +318,7 @@ class BluesoundPlayer(MediaPlayerDevice):
|
||||
|
||||
try:
|
||||
websession = async_get_clientsession(self._hass)
|
||||
with async_timeout.timeout(10, loop=self._hass.loop):
|
||||
with async_timeout.timeout(10):
|
||||
response = await websession.get(url)
|
||||
|
||||
if response.status == 200:
|
||||
@ -361,7 +361,7 @@ class BluesoundPlayer(MediaPlayerDevice):
|
||||
|
||||
try:
|
||||
|
||||
with async_timeout.timeout(125, loop=self._hass.loop):
|
||||
with async_timeout.timeout(125):
|
||||
response = await self._polling_session.get(
|
||||
url, headers={CONNECTION: KEEP_ALIVE})
|
||||
|
||||
@ -378,7 +378,7 @@ class BluesoundPlayer(MediaPlayerDevice):
|
||||
self._group_name = group_name
|
||||
# the sleep is needed to make sure that the
|
||||
# devices is synced
|
||||
await asyncio.sleep(1, loop=self._hass.loop)
|
||||
await asyncio.sleep(1)
|
||||
await self.async_trigger_sync_on_all()
|
||||
elif self.is_grouped:
|
||||
# when player is grouped we need to fetch volume from
|
||||
|
@ -2,12 +2,15 @@
|
||||
import logging
|
||||
|
||||
from homeassistant.helpers.event import track_point_in_utc_time
|
||||
from homeassistant.components.device_tracker import (
|
||||
YAML_DEVICES, CONF_TRACK_NEW, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL,
|
||||
load_config, SOURCE_TYPE_BLUETOOTH_LE
|
||||
from homeassistant.components.device_tracker.legacy import (
|
||||
YAML_DEVICES, async_load_config
|
||||
)
|
||||
from homeassistant.components.device_tracker.const import (
|
||||
CONF_TRACK_NEW, CONF_SCAN_INTERVAL, SCAN_INTERVAL, SOURCE_TYPE_BLUETOOTH_LE
|
||||
)
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.util.async_ import run_coroutine_threadsafe
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -79,7 +82,10 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||
# Load all known devices.
|
||||
# We just need the devices so set consider_home and home range
|
||||
# to 0
|
||||
for device in load_config(yaml_path, hass, 0):
|
||||
for device in run_coroutine_threadsafe(
|
||||
async_load_config(yaml_path, hass, 0),
|
||||
hass.loop
|
||||
).result():
|
||||
# check if device is a valid bluetooth device
|
||||
if device.mac and device.mac[:4].upper() == BLE_PREFIX:
|
||||
if device.track:
|
||||
@ -97,7 +103,7 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||
_LOGGER.warning("No Bluetooth LE devices to track!")
|
||||
return False
|
||||
|
||||
interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
||||
interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
|
||||
|
||||
def update_ble(now):
|
||||
"""Lookup Bluetooth LE devices and update status."""
|
||||
|
@ -5,11 +5,16 @@ import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import track_point_in_utc_time
|
||||
from homeassistant.components.device_tracker import (
|
||||
YAML_DEVICES, CONF_TRACK_NEW, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL,
|
||||
load_config, PLATFORM_SCHEMA, DEFAULT_TRACK_NEW, SOURCE_TYPE_BLUETOOTH,
|
||||
DOMAIN)
|
||||
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
|
||||
from homeassistant.components.device_tracker.legacy import (
|
||||
YAML_DEVICES, async_load_config
|
||||
)
|
||||
from homeassistant.components.device_tracker.const import (
|
||||
CONF_TRACK_NEW, CONF_SCAN_INTERVAL, SCAN_INTERVAL, DEFAULT_TRACK_NEW,
|
||||
SOURCE_TYPE_BLUETOOTH, DOMAIN
|
||||
)
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.util.async_ import run_coroutine_threadsafe
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -60,7 +65,10 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||
# Load all known devices.
|
||||
# We just need the devices so set consider_home and home range
|
||||
# to 0
|
||||
for device in load_config(yaml_path, hass, 0):
|
||||
for device in run_coroutine_threadsafe(
|
||||
async_load_config(yaml_path, hass, 0),
|
||||
hass.loop
|
||||
).result():
|
||||
# Check if device is a valid bluetooth device
|
||||
if device.mac and device.mac[:3].upper() == BT_PREFIX:
|
||||
if device.track:
|
||||
@ -77,7 +85,7 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||
devs_to_track.append(dev[0])
|
||||
see_device(dev[0], dev[1])
|
||||
|
||||
interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
||||
interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
|
||||
|
||||
request_rssi = config.get(CONF_REQUEST_RSSI, False)
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
"""Support for the Broadlink RM2 Pro (only temperature) and A1 devices."""
|
||||
import binascii
|
||||
import logging
|
||||
import socket
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
@ -60,6 +59,7 @@ class BroadlinkSensor(Entity):
|
||||
"""Initialize the sensor."""
|
||||
self._name = '{} {}'.format(name, SENSOR_TYPES[sensor_type][0])
|
||||
self._state = None
|
||||
self._is_available = False
|
||||
self._type = sensor_type
|
||||
self._broadlink_data = broadlink_data
|
||||
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
|
||||
@ -74,6 +74,11 @@ class BroadlinkSensor(Entity):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self._is_available
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit this state is expressed in."""
|
||||
@ -83,8 +88,11 @@ class BroadlinkSensor(Entity):
|
||||
"""Get the latest data from the sensor."""
|
||||
self._broadlink_data.update()
|
||||
if self._broadlink_data.data is None:
|
||||
self._state = None
|
||||
self._is_available = False
|
||||
return
|
||||
self._state = self._broadlink_data.data[self._type]
|
||||
self._is_available = True
|
||||
|
||||
|
||||
class BroadlinkData:
|
||||
@ -119,8 +127,9 @@ class BroadlinkData:
|
||||
if data is not None:
|
||||
self.data = self._schema(data)
|
||||
return
|
||||
except socket.timeout as error:
|
||||
except OSError as error:
|
||||
if retry < 1:
|
||||
self.data = None
|
||||
_LOGGER.error(error)
|
||||
return
|
||||
except (vol.Invalid, vol.MultipleInvalid):
|
||||
@ -131,7 +140,7 @@ class BroadlinkData:
|
||||
def _auth(self, retry=3):
|
||||
try:
|
||||
auth = self._device.auth()
|
||||
except socket.timeout:
|
||||
except OSError:
|
||||
auth = False
|
||||
if not auth and retry > 0:
|
||||
self._connect()
|
||||
|
@ -10,9 +10,10 @@ from homeassistant.components.switch import (
|
||||
ENTITY_ID_FORMAT, PLATFORM_SCHEMA, SwitchDevice)
|
||||
from homeassistant.const import (
|
||||
CONF_COMMAND_OFF, CONF_COMMAND_ON, CONF_FRIENDLY_NAME, CONF_HOST, CONF_MAC,
|
||||
CONF_SWITCHES, CONF_TIMEOUT, CONF_TYPE)
|
||||
CONF_SWITCHES, CONF_TIMEOUT, CONF_TYPE, STATE_ON)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import Throttle, slugify
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
from . import async_setup_service, data_packet
|
||||
|
||||
@ -109,13 +110,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
broadlink_device.timeout = config.get(CONF_TIMEOUT)
|
||||
try:
|
||||
broadlink_device.auth()
|
||||
except socket.timeout:
|
||||
except OSError:
|
||||
_LOGGER.error("Failed to connect to device")
|
||||
|
||||
add_entities(switches)
|
||||
|
||||
|
||||
class BroadlinkRMSwitch(SwitchDevice):
|
||||
class BroadlinkRMSwitch(SwitchDevice, RestoreEntity):
|
||||
"""Representation of an Broadlink switch."""
|
||||
|
||||
def __init__(self, name, friendly_name, device, command_on, command_off):
|
||||
@ -126,6 +127,14 @@ class BroadlinkRMSwitch(SwitchDevice):
|
||||
self._command_on = command_on
|
||||
self._command_off = command_off
|
||||
self._device = device
|
||||
self._is_available = False
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Call when entity about to be added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
state = await self.async_get_last_state()
|
||||
if state:
|
||||
self._state = state.state == STATE_ON
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@ -137,6 +146,11 @@ class BroadlinkRMSwitch(SwitchDevice):
|
||||
"""Return true if unable to access real state of entity."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return not self.should_poll or self._is_available
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state."""
|
||||
@ -166,7 +180,7 @@ class BroadlinkRMSwitch(SwitchDevice):
|
||||
return True
|
||||
try:
|
||||
self._device.send_data(packet)
|
||||
except (socket.timeout, ValueError) as error:
|
||||
except (ValueError, OSError) as error:
|
||||
if retry < 1:
|
||||
_LOGGER.error("Error during sending a packet: %s", error)
|
||||
return False
|
||||
@ -178,7 +192,7 @@ class BroadlinkRMSwitch(SwitchDevice):
|
||||
def _auth(self, retry=2):
|
||||
try:
|
||||
auth = self._device.auth()
|
||||
except socket.timeout:
|
||||
except OSError:
|
||||
auth = False
|
||||
if retry < 1:
|
||||
_LOGGER.error("Timeout during authorization")
|
||||
@ -244,6 +258,7 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch):
|
||||
except (socket.timeout, ValueError) as error:
|
||||
if retry < 1:
|
||||
_LOGGER.error("Error during updating the state: %s", error)
|
||||
self._is_available = False
|
||||
return
|
||||
if not self._auth():
|
||||
return
|
||||
@ -252,6 +267,7 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch):
|
||||
return self._update(retry-1)
|
||||
self._state = state
|
||||
self._load_power = load_power
|
||||
self._is_available = True
|
||||
|
||||
|
||||
class BroadlinkMP1Slot(BroadlinkRMSwitch):
|
||||
@ -277,10 +293,12 @@ class BroadlinkMP1Slot(BroadlinkRMSwitch):
|
||||
except (socket.timeout, ValueError) as error:
|
||||
if retry < 1:
|
||||
_LOGGER.error("Error during sending a packet: %s", error)
|
||||
self._is_available = False
|
||||
return False
|
||||
if not self._auth():
|
||||
return False
|
||||
return self._sendpacket(packet, max(0, retry-1))
|
||||
self._is_available = True
|
||||
return True
|
||||
|
||||
@property
|
||||
@ -330,7 +348,7 @@ class BroadlinkMP1Switch:
|
||||
"""Authenticate the device."""
|
||||
try:
|
||||
auth = self._device.auth()
|
||||
except socket.timeout:
|
||||
except OSError:
|
||||
auth = False
|
||||
if not auth and retry > 0:
|
||||
return self._auth(retry-1)
|
||||
|
@ -388,7 +388,7 @@ class BrData:
|
||||
tasks.append(dev.async_update_ha_state())
|
||||
|
||||
if tasks:
|
||||
await asyncio.wait(tasks, loop=self.hass.loop)
|
||||
await asyncio.wait(tasks)
|
||||
|
||||
async def schedule_update(self, minute=1):
|
||||
"""Schedule an update after minute minutes."""
|
||||
@ -407,7 +407,7 @@ class BrData:
|
||||
resp = None
|
||||
try:
|
||||
websession = async_get_clientsession(self.hass)
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
with async_timeout.timeout(10):
|
||||
resp = await websession.get(url)
|
||||
|
||||
result[STATUS_CODE] = resp.status
|
||||
|
@ -36,7 +36,7 @@ async def async_setup(hass, config):
|
||||
hass.http.register_view(CalendarEventView(component))
|
||||
|
||||
# Doesn't work in prod builds of the frontend: home-assistant-polymer#1289
|
||||
# await hass.components.frontend.async_register_built_in_panel(
|
||||
# hass.components.frontend.async_register_built_in_panel(
|
||||
# 'calendar', 'calendar', 'hass:calendar')
|
||||
|
||||
await component.async_setup(config)
|
||||
|
@ -107,11 +107,14 @@ async def async_request_stream(hass, entity_id, fmt):
|
||||
camera = _get_camera_from_entity_id(hass, entity_id)
|
||||
camera_prefs = hass.data[DATA_CAMERA_PREFS].get(entity_id)
|
||||
|
||||
if not camera.stream_source:
|
||||
async with async_timeout.timeout(10):
|
||||
source = await camera.stream_source()
|
||||
|
||||
if not source:
|
||||
raise HomeAssistantError("{} does not support play stream service"
|
||||
.format(camera.entity_id))
|
||||
|
||||
return request_stream(hass, camera.stream_source, fmt=fmt,
|
||||
return request_stream(hass, source, fmt=fmt,
|
||||
keepalive=camera_prefs.preload_stream)
|
||||
|
||||
|
||||
@ -121,7 +124,7 @@ async def async_get_image(hass, entity_id, timeout=10):
|
||||
camera = _get_camera_from_entity_id(hass, entity_id)
|
||||
|
||||
with suppress(asyncio.CancelledError, asyncio.TimeoutError):
|
||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||
async with async_timeout.timeout(timeout):
|
||||
image = await camera.async_camera_image()
|
||||
|
||||
if image:
|
||||
@ -221,8 +224,16 @@ async def async_setup(hass, config):
|
||||
async def preload_stream(hass, _):
|
||||
for camera in component.entities:
|
||||
camera_prefs = prefs.get(camera.entity_id)
|
||||
if camera.stream_source and camera_prefs.preload_stream:
|
||||
request_stream(hass, camera.stream_source, keepalive=True)
|
||||
if not camera_prefs.preload_stream:
|
||||
continue
|
||||
|
||||
async with async_timeout.timeout(10):
|
||||
source = await camera.stream_source()
|
||||
|
||||
if not source:
|
||||
continue
|
||||
|
||||
request_stream(hass, source, keepalive=True)
|
||||
|
||||
async_when_setup(hass, DOMAIN_STREAM, preload_stream)
|
||||
|
||||
@ -328,8 +339,7 @@ class Camera(Entity):
|
||||
"""Return the interval between frames of the mjpeg stream."""
|
||||
return 0.5
|
||||
|
||||
@property
|
||||
def stream_source(self):
|
||||
async def stream_source(self):
|
||||
"""Return the source of the stream."""
|
||||
return None
|
||||
|
||||
@ -481,7 +491,7 @@ class CameraImageView(CameraView):
|
||||
async def handle(self, request, camera):
|
||||
"""Serve camera image."""
|
||||
with suppress(asyncio.CancelledError, asyncio.TimeoutError):
|
||||
with async_timeout.timeout(10, loop=request.app['hass'].loop):
|
||||
async with async_timeout.timeout(10):
|
||||
image = await camera.async_camera_image()
|
||||
|
||||
if image:
|
||||
@ -522,12 +532,10 @@ async def websocket_camera_thumbnail(hass, connection, msg):
|
||||
"""
|
||||
try:
|
||||
image = await async_get_image(hass, msg['entity_id'])
|
||||
connection.send_message(websocket_api.result_message(
|
||||
msg['id'], {
|
||||
'content_type': image.content_type,
|
||||
'content': base64.b64encode(image.content).decode('utf-8')
|
||||
}
|
||||
))
|
||||
await connection.send_big_result(msg['id'], {
|
||||
'content_type': image.content_type,
|
||||
'content': base64.b64encode(image.content).decode('utf-8')
|
||||
})
|
||||
except HomeAssistantError:
|
||||
connection.send_message(websocket_api.error_message(
|
||||
msg['id'], 'image_fetch_failed', 'Unable to fetch image'))
|
||||
@ -549,18 +557,25 @@ async def ws_camera_stream(hass, connection, msg):
|
||||
camera = _get_camera_from_entity_id(hass, entity_id)
|
||||
camera_prefs = hass.data[DATA_CAMERA_PREFS].get(entity_id)
|
||||
|
||||
if not camera.stream_source:
|
||||
async with async_timeout.timeout(10):
|
||||
source = await camera.stream_source()
|
||||
|
||||
if not source:
|
||||
raise HomeAssistantError("{} does not support play stream service"
|
||||
.format(camera.entity_id))
|
||||
|
||||
fmt = msg['format']
|
||||
url = request_stream(hass, camera.stream_source, fmt=fmt,
|
||||
url = request_stream(hass, source, fmt=fmt,
|
||||
keepalive=camera_prefs.preload_stream)
|
||||
connection.send_result(msg['id'], {'url': url})
|
||||
except HomeAssistantError as ex:
|
||||
_LOGGER.error(ex)
|
||||
_LOGGER.error("Error requesting stream: %s", ex)
|
||||
connection.send_error(
|
||||
msg['id'], 'start_stream_failed', str(ex))
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.error("Timeout getting stream source")
|
||||
connection.send_error(
|
||||
msg['id'], 'start_stream_failed', "Timeout getting stream source")
|
||||
|
||||
|
||||
@websocket_api.async_response
|
||||
@ -624,7 +639,10 @@ async def async_handle_snapshot_service(camera, service):
|
||||
|
||||
async def async_handle_play_stream_service(camera, service_call):
|
||||
"""Handle play stream services calls."""
|
||||
if not camera.stream_source:
|
||||
async with async_timeout.timeout(10):
|
||||
source = await camera.stream_source()
|
||||
|
||||
if not source:
|
||||
raise HomeAssistantError("{} does not support play stream service"
|
||||
.format(camera.entity_id))
|
||||
|
||||
@ -633,7 +651,7 @@ async def async_handle_play_stream_service(camera, service_call):
|
||||
fmt = service_call.data[ATTR_FORMAT]
|
||||
entity_ids = service_call.data[ATTR_MEDIA_PLAYER]
|
||||
|
||||
url = request_stream(hass, camera.stream_source, fmt=fmt,
|
||||
url = request_stream(hass, source, fmt=fmt,
|
||||
keepalive=camera_prefs.preload_stream)
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity_ids,
|
||||
@ -648,7 +666,10 @@ async def async_handle_play_stream_service(camera, service_call):
|
||||
|
||||
async def async_handle_record_service(camera, call):
|
||||
"""Handle stream recording service calls."""
|
||||
if not camera.stream_source:
|
||||
async with async_timeout.timeout(10):
|
||||
source = await camera.stream_source()
|
||||
|
||||
if not source:
|
||||
raise HomeAssistantError("{} does not support record service"
|
||||
.format(camera.entity_id))
|
||||
|
||||
@ -659,7 +680,7 @@ async def async_handle_record_service(camera, call):
|
||||
variables={ATTR_ENTITY_ID: camera})
|
||||
|
||||
data = {
|
||||
CONF_STREAM_SOURCE: camera.stream_source,
|
||||
CONF_STREAM_SOURCE: source,
|
||||
CONF_FILENAME: video_path,
|
||||
CONF_DURATION: call.data[CONF_DURATION],
|
||||
CONF_LOOKBACK: call.data[CONF_LOOKBACK],
|
||||
|
@ -79,7 +79,7 @@ class CanaryCamera(Camera):
|
||||
image = await asyncio.shield(ffmpeg.get_image(
|
||||
self._live_stream_session.live_stream_url,
|
||||
output_format=IMAGE_JPEG,
|
||||
extra_cmd=self._ffmpeg_arguments), loop=self.hass.loop)
|
||||
extra_cmd=self._ffmpeg_arguments))
|
||||
return image
|
||||
|
||||
async def handle_async_mjpeg_stream(self, request):
|
||||
|
@ -1,8 +1,7 @@
|
||||
"""Component to embed Google Cast."""
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
|
||||
DOMAIN = 'cast'
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
@ -23,15 +22,3 @@ async def async_setup_entry(hass, entry):
|
||||
hass.async_create_task(hass.config_entries.async_forward_entry_setup(
|
||||
entry, 'media_player'))
|
||||
return True
|
||||
|
||||
|
||||
async def _async_has_devices(hass):
|
||||
"""Return if there are devices that can be discovered."""
|
||||
from pychromecast.discovery import discover_chromecasts
|
||||
|
||||
return await hass.async_add_executor_job(discover_chromecasts)
|
||||
|
||||
|
||||
config_entry_flow.register_discovery_flow(
|
||||
DOMAIN, 'Google Cast', _async_has_devices,
|
||||
config_entries.CONN_CLASS_LOCAL_PUSH)
|
||||
|
16
homeassistant/components/cast/config_flow.py
Normal file
16
homeassistant/components/cast/config_flow.py
Normal 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)
|
3
homeassistant/components/cast/const.py
Normal file
3
homeassistant/components/cast/const.py
Normal file
@ -0,0 +1,3 @@
|
||||
"""Consts for Cast integration."""
|
||||
|
||||
DOMAIN = 'cast'
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"domain": "cast",
|
||||
"name": "Cast",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/cast",
|
||||
"requirements": [
|
||||
"pychromecast==3.2.1"
|
||||
|
@ -106,7 +106,7 @@ async def async_citybikes_request(hass, uri, schema):
|
||||
try:
|
||||
session = async_get_clientsession(hass)
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT):
|
||||
req = await session.get(DEFAULT_ENDPOINT.format(uri=uri))
|
||||
|
||||
json_response = await req.json()
|
||||
@ -181,7 +181,7 @@ class CityBikesNetworks:
|
||||
"""Initialize the networks instance."""
|
||||
self.hass = hass
|
||||
self.networks = None
|
||||
self.networks_loading = asyncio.Condition(loop=hass.loop)
|
||||
self.networks_loading = asyncio.Condition()
|
||||
|
||||
async def get_closest_network_id(self, latitude, longitude):
|
||||
"""Return the id of the network closest to provided location."""
|
||||
@ -217,7 +217,7 @@ class CityBikesNetwork:
|
||||
self.hass = hass
|
||||
self.network_id = network_id
|
||||
self.stations = []
|
||||
self.ready = asyncio.Event(loop=hass.loop)
|
||||
self.ready = asyncio.Event()
|
||||
|
||||
async def async_refresh(self, now=None):
|
||||
"""Refresh the state of the network."""
|
||||
|
@ -17,7 +17,9 @@ from homeassistant.util.aiohttp import MockRequest
|
||||
|
||||
from . import utils
|
||||
from .const import (
|
||||
CONF_ENTITY_CONFIG, CONF_FILTER, DOMAIN, DISPATCHER_REMOTE_UPDATE)
|
||||
CONF_ENTITY_CONFIG, CONF_FILTER, DOMAIN, DISPATCHER_REMOTE_UPDATE,
|
||||
PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE,
|
||||
PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA)
|
||||
from .prefs import CloudPreferences
|
||||
|
||||
|
||||
@ -98,12 +100,26 @@ class CloudClient(Interface):
|
||||
if entity.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
|
||||
return False
|
||||
|
||||
return google_conf['filter'](entity.entity_id)
|
||||
if not google_conf['filter'].empty_filter:
|
||||
return google_conf['filter'](entity.entity_id)
|
||||
|
||||
entity_configs = self.prefs.google_entity_configs
|
||||
entity_config = entity_configs.get(entity.entity_id, {})
|
||||
return entity_config.get(
|
||||
PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE)
|
||||
|
||||
def should_2fa(entity):
|
||||
"""If an entity should be checked for 2FA."""
|
||||
entity_configs = self.prefs.google_entity_configs
|
||||
entity_config = entity_configs.get(entity.entity_id, {})
|
||||
return not entity_config.get(
|
||||
PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA)
|
||||
|
||||
username = self._hass.data[DOMAIN].claims["cognito:username"]
|
||||
|
||||
self._google_config = ga_h.Config(
|
||||
should_expose=should_expose,
|
||||
should_2fa=should_2fa,
|
||||
secure_devices_pin=self._prefs.google_secure_devices_pin,
|
||||
entity_config=google_conf.get(CONF_ENTITY_CONFIG),
|
||||
agent_user_id=username,
|
||||
|
@ -8,6 +8,13 @@ PREF_ENABLE_REMOTE = 'remote_enabled'
|
||||
PREF_GOOGLE_SECURE_DEVICES_PIN = 'google_secure_devices_pin'
|
||||
PREF_CLOUDHOOKS = 'cloudhooks'
|
||||
PREF_CLOUD_USER = 'cloud_user'
|
||||
PREF_GOOGLE_ENTITY_CONFIGS = 'google_entity_configs'
|
||||
PREF_OVERRIDE_NAME = 'override_name'
|
||||
PREF_DISABLE_2FA = 'disable_2fa'
|
||||
PREF_ALIASES = 'aliases'
|
||||
PREF_SHOULD_EXPOSE = 'should_expose'
|
||||
DEFAULT_SHOULD_EXPOSE = True
|
||||
DEFAULT_DISABLE_2FA = False
|
||||
|
||||
CONF_ALEXA = 'alexa'
|
||||
CONF_ALIASES = 'aliases'
|
||||
|
@ -14,8 +14,7 @@ from homeassistant.components.http.data_validator import (
|
||||
RequestDataValidator)
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.components.alexa import smart_home as alexa_sh
|
||||
from homeassistant.components.google_assistant import (
|
||||
const as google_const)
|
||||
from homeassistant.components.google_assistant import helpers as google_helpers
|
||||
|
||||
from .const import (
|
||||
DOMAIN, REQUEST_TIMEOUT, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE,
|
||||
@ -81,6 +80,12 @@ async def async_setup(hass):
|
||||
websocket_remote_connect)
|
||||
hass.components.websocket_api.async_register_command(
|
||||
websocket_remote_disconnect)
|
||||
|
||||
hass.components.websocket_api.async_register_command(
|
||||
google_assistant_list)
|
||||
hass.components.websocket_api.async_register_command(
|
||||
google_assistant_update)
|
||||
|
||||
hass.http.register_view(GoogleActionsSyncView)
|
||||
hass.http.register_view(CloudLoginView)
|
||||
hass.http.register_view(CloudLogoutView)
|
||||
@ -164,10 +169,10 @@ class GoogleActionsSyncView(HomeAssistantView):
|
||||
cloud = hass.data[DOMAIN]
|
||||
websession = hass.helpers.aiohttp_client.async_get_clientsession()
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT):
|
||||
await hass.async_add_job(cloud.auth.check_token)
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT):
|
||||
req = await websession.post(
|
||||
cloud.google_actions_sync_url, headers={
|
||||
'authorization': cloud.id_token
|
||||
@ -192,7 +197,7 @@ class CloudLoginView(HomeAssistantView):
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT):
|
||||
await hass.async_add_job(cloud.auth.login, data['email'],
|
||||
data['password'])
|
||||
|
||||
@ -212,7 +217,7 @@ class CloudLogoutView(HomeAssistantView):
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT):
|
||||
await cloud.logout()
|
||||
|
||||
return self.json_message('ok')
|
||||
@ -234,7 +239,7 @@ class CloudRegisterView(HomeAssistantView):
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT):
|
||||
await hass.async_add_job(
|
||||
cloud.auth.register, data['email'], data['password'])
|
||||
|
||||
@ -256,7 +261,7 @@ class CloudResendConfirmView(HomeAssistantView):
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT):
|
||||
await hass.async_add_job(
|
||||
cloud.auth.resend_email_confirm, data['email'])
|
||||
|
||||
@ -278,7 +283,7 @@ class CloudForgotPasswordView(HomeAssistantView):
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT):
|
||||
await hass.async_add_job(
|
||||
cloud.auth.forgot_password, data['email'])
|
||||
|
||||
@ -320,7 +325,7 @@ async def websocket_subscription(hass, connection, msg):
|
||||
from hass_nabucasa.const import STATE_DISCONNECTED
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT):
|
||||
response = await cloud.fetch_subscription_info()
|
||||
|
||||
if response.status != 200:
|
||||
@ -411,7 +416,6 @@ def _account_data(cloud):
|
||||
'cloud': cloud.iot.state,
|
||||
'prefs': client.prefs.as_dict(),
|
||||
'google_entities': client.google_user_config['filter'].config,
|
||||
'google_domains': list(google_const.DOMAIN_TO_GOOGLE_TYPES),
|
||||
'alexa_entities': client.alexa_config.should_expose.config,
|
||||
'alexa_domains': list(alexa_sh.ENTITY_ADAPTERS),
|
||||
'remote_domain': remote.instance_domain,
|
||||
@ -448,3 +452,55 @@ async def websocket_remote_disconnect(hass, connection, msg):
|
||||
await cloud.client.prefs.async_update(remote_enabled=False)
|
||||
await cloud.remote.disconnect()
|
||||
connection.send_result(msg['id'], _account_data(cloud))
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@_require_cloud_login
|
||||
@websocket_api.async_response
|
||||
@_ws_handle_cloud_errors
|
||||
@websocket_api.websocket_command({
|
||||
'type': 'cloud/google_assistant/entities'
|
||||
})
|
||||
async def google_assistant_list(hass, connection, msg):
|
||||
"""List all google assistant entities."""
|
||||
cloud = hass.data[DOMAIN]
|
||||
entities = google_helpers.async_get_entities(
|
||||
hass, cloud.client.google_config
|
||||
)
|
||||
|
||||
result = []
|
||||
|
||||
for entity in entities:
|
||||
result.append({
|
||||
'entity_id': entity.entity_id,
|
||||
'traits': [trait.name for trait in entity.traits()],
|
||||
'might_2fa': entity.might_2fa(),
|
||||
})
|
||||
|
||||
connection.send_result(msg['id'], result)
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@_require_cloud_login
|
||||
@websocket_api.async_response
|
||||
@_ws_handle_cloud_errors
|
||||
@websocket_api.websocket_command({
|
||||
'type': 'cloud/google_assistant/entities/update',
|
||||
'entity_id': str,
|
||||
vol.Optional('should_expose'): bool,
|
||||
vol.Optional('override_name'): str,
|
||||
vol.Optional('aliases'): [str],
|
||||
vol.Optional('disable_2fa'): bool,
|
||||
})
|
||||
async def google_assistant_update(hass, connection, msg):
|
||||
"""List all google assistant entities."""
|
||||
cloud = hass.data[DOMAIN]
|
||||
changes = dict(msg)
|
||||
changes.pop('type')
|
||||
changes.pop('id')
|
||||
|
||||
await cloud.client.prefs.async_update_google_entity_config(**changes)
|
||||
|
||||
connection.send_result(
|
||||
msg['id'],
|
||||
cloud.client.prefs.google_entity_configs.get(msg['entity_id']))
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Cloud",
|
||||
"documentation": "https://www.home-assistant.io/components/cloud",
|
||||
"requirements": [
|
||||
"hass-nabucasa==0.12"
|
||||
"hass-nabucasa==0.13"
|
||||
],
|
||||
"dependencies": [
|
||||
"http",
|
||||
|
@ -4,6 +4,8 @@ from ipaddress import ip_address
|
||||
from .const import (
|
||||
DOMAIN, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, PREF_ENABLE_REMOTE,
|
||||
PREF_GOOGLE_SECURE_DEVICES_PIN, PREF_CLOUDHOOKS, PREF_CLOUD_USER,
|
||||
PREF_GOOGLE_ENTITY_CONFIGS, PREF_OVERRIDE_NAME, PREF_DISABLE_2FA,
|
||||
PREF_ALIASES, PREF_SHOULD_EXPOSE,
|
||||
InvalidTrustedNetworks)
|
||||
|
||||
STORAGE_KEY = DOMAIN
|
||||
@ -30,6 +32,7 @@ class CloudPreferences:
|
||||
PREF_ENABLE_GOOGLE: True,
|
||||
PREF_ENABLE_REMOTE: False,
|
||||
PREF_GOOGLE_SECURE_DEVICES_PIN: None,
|
||||
PREF_GOOGLE_ENTITY_CONFIGS: {},
|
||||
PREF_CLOUDHOOKS: {},
|
||||
PREF_CLOUD_USER: None,
|
||||
}
|
||||
@ -39,7 +42,7 @@ class CloudPreferences:
|
||||
async def async_update(self, *, google_enabled=_UNDEF,
|
||||
alexa_enabled=_UNDEF, remote_enabled=_UNDEF,
|
||||
google_secure_devices_pin=_UNDEF, cloudhooks=_UNDEF,
|
||||
cloud_user=_UNDEF):
|
||||
cloud_user=_UNDEF, google_entity_configs=_UNDEF):
|
||||
"""Update user preferences."""
|
||||
for key, value in (
|
||||
(PREF_ENABLE_GOOGLE, google_enabled),
|
||||
@ -48,6 +51,7 @@ class CloudPreferences:
|
||||
(PREF_GOOGLE_SECURE_DEVICES_PIN, google_secure_devices_pin),
|
||||
(PREF_CLOUDHOOKS, cloudhooks),
|
||||
(PREF_CLOUD_USER, cloud_user),
|
||||
(PREF_GOOGLE_ENTITY_CONFIGS, google_entity_configs),
|
||||
):
|
||||
if value is not _UNDEF:
|
||||
self._prefs[key] = value
|
||||
@ -57,9 +61,48 @@ class CloudPreferences:
|
||||
|
||||
await self._store.async_save(self._prefs)
|
||||
|
||||
async def async_update_google_entity_config(
|
||||
self, *, entity_id, override_name=_UNDEF, disable_2fa=_UNDEF,
|
||||
aliases=_UNDEF, should_expose=_UNDEF):
|
||||
"""Update config for a Google entity."""
|
||||
entities = self.google_entity_configs
|
||||
entity = entities.get(entity_id, {})
|
||||
|
||||
changes = {}
|
||||
for key, value in (
|
||||
(PREF_OVERRIDE_NAME, override_name),
|
||||
(PREF_DISABLE_2FA, disable_2fa),
|
||||
(PREF_ALIASES, aliases),
|
||||
(PREF_SHOULD_EXPOSE, should_expose),
|
||||
):
|
||||
if value is not _UNDEF:
|
||||
changes[key] = value
|
||||
|
||||
if not changes:
|
||||
return
|
||||
|
||||
updated_entity = {
|
||||
**entity,
|
||||
**changes,
|
||||
}
|
||||
|
||||
updated_entities = {
|
||||
**entities,
|
||||
entity_id: updated_entity,
|
||||
}
|
||||
await self.async_update(google_entity_configs=updated_entities)
|
||||
|
||||
def as_dict(self):
|
||||
"""Return dictionary version."""
|
||||
return self._prefs
|
||||
return {
|
||||
PREF_ENABLE_ALEXA: self.alexa_enabled,
|
||||
PREF_ENABLE_GOOGLE: self.google_enabled,
|
||||
PREF_ENABLE_REMOTE: self.remote_enabled,
|
||||
PREF_GOOGLE_SECURE_DEVICES_PIN: self.google_secure_devices_pin,
|
||||
PREF_GOOGLE_ENTITY_CONFIGS: self.google_entity_configs,
|
||||
PREF_CLOUDHOOKS: self.cloudhooks,
|
||||
PREF_CLOUD_USER: self.cloud_user,
|
||||
}
|
||||
|
||||
@property
|
||||
def remote_enabled(self):
|
||||
@ -89,6 +132,11 @@ class CloudPreferences:
|
||||
"""Return if Google is allowed to unlock locks."""
|
||||
return self._prefs.get(PREF_GOOGLE_SECURE_DEVICES_PIN)
|
||||
|
||||
@property
|
||||
def google_entity_configs(self):
|
||||
"""Return Google Entity configurations."""
|
||||
return self._prefs.get(PREF_GOOGLE_ENTITY_CONFIGS, {})
|
||||
|
||||
@property
|
||||
def cloudhooks(self):
|
||||
"""Return the published cloud webhooks."""
|
||||
|
@ -1,13 +1,25 @@
|
||||
"""Helper functions for cloud components."""
|
||||
from typing import Any, Dict
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp import web, payload
|
||||
|
||||
|
||||
def aiohttp_serialize_response(response: web.Response) -> Dict[str, Any]:
|
||||
"""Serialize an aiohttp response to a dictionary."""
|
||||
body = response.body
|
||||
|
||||
if body is None:
|
||||
pass
|
||||
elif isinstance(body, payload.StringPayload):
|
||||
# pylint: disable=protected-access
|
||||
body = body._value.decode(body.encoding)
|
||||
elif isinstance(body, bytes):
|
||||
body = body.decode(response.charset or 'utf-8')
|
||||
else:
|
||||
raise ValueError("Unknown payload encoding")
|
||||
|
||||
return {
|
||||
'status': response.status,
|
||||
'body': response.text,
|
||||
'body': body,
|
||||
'headers': dict(response.headers),
|
||||
}
|
||||
|
@ -106,7 +106,7 @@ class ComedHourlyPricingSensor(Entity):
|
||||
else:
|
||||
url_string += '?type=currenthouraverage'
|
||||
|
||||
with async_timeout.timeout(60, loop=self.loop):
|
||||
with async_timeout.timeout(60):
|
||||
response = await self.websession.get(url_string)
|
||||
# The API responds with MIME type 'text/html'
|
||||
text = await response.text()
|
||||
|
@ -30,7 +30,7 @@ ON_DEMAND = ('zwave',)
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the config component."""
|
||||
await hass.components.frontend.async_register_built_in_panel(
|
||||
hass.components.frontend.async_register_built_in_panel(
|
||||
'config', 'config', 'hass:settings', require_admin=True)
|
||||
|
||||
async def setup_panel(panel_name):
|
||||
@ -62,7 +62,7 @@ async def async_setup(hass, config):
|
||||
tasks.append(setup_panel(panel_name))
|
||||
|
||||
if tasks:
|
||||
await asyncio.wait(tasks, loop=hass.loop)
|
||||
await asyncio.wait(tasks)
|
||||
|
||||
return True
|
||||
|
||||
@ -92,6 +92,10 @@ class BaseEditConfigView(HomeAssistantView):
|
||||
"""Set value."""
|
||||
raise NotImplementedError
|
||||
|
||||
def _delete_value(self, hass, data, config_key):
|
||||
"""Delete value."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def get(self, request, config_key):
|
||||
"""Fetch device specific config."""
|
||||
hass = request.app['hass']
|
||||
@ -128,7 +132,27 @@ class BaseEditConfigView(HomeAssistantView):
|
||||
current = await self.read_config(hass)
|
||||
self._write_value(hass, current, config_key, data)
|
||||
|
||||
await hass.async_add_job(_write, path, current)
|
||||
await hass.async_add_executor_job(_write, path, current)
|
||||
|
||||
if self.post_write_hook is not None:
|
||||
hass.async_create_task(self.post_write_hook(hass))
|
||||
|
||||
return self.json({
|
||||
'result': 'ok',
|
||||
})
|
||||
|
||||
async def delete(self, request, config_key):
|
||||
"""Remove an entry."""
|
||||
hass = request.app['hass']
|
||||
current = await self.read_config(hass)
|
||||
value = self._get_value(hass, current, config_key)
|
||||
path = hass.config.path(self.path)
|
||||
|
||||
if value is None:
|
||||
return self.json_message('Resource not found', 404)
|
||||
|
||||
self._delete_value(hass, current, config_key)
|
||||
await hass.async_add_executor_job(_write, path, current)
|
||||
|
||||
if self.post_write_hook is not None:
|
||||
hass.async_create_task(self.post_write_hook(hass))
|
||||
@ -161,6 +185,10 @@ class EditKeyBasedConfigView(BaseEditConfigView):
|
||||
"""Set value."""
|
||||
data.setdefault(config_key, {}).update(new_value)
|
||||
|
||||
def _delete_value(self, hass, data, config_key):
|
||||
"""Delete value."""
|
||||
return data.pop(config_key)
|
||||
|
||||
|
||||
class EditIdBasedConfigView(BaseEditConfigView):
|
||||
"""Configure key based config entries."""
|
||||
@ -184,6 +212,13 @@ class EditIdBasedConfigView(BaseEditConfigView):
|
||||
|
||||
value.update(new_value)
|
||||
|
||||
def _delete_value(self, hass, data, config_key):
|
||||
"""Delete value."""
|
||||
index = next(
|
||||
idx for idx, val in enumerate(data)
|
||||
if val.get(CONF_ID) == config_key)
|
||||
data.pop(index)
|
||||
|
||||
|
||||
def _read(path):
|
||||
"""Read YAML helper."""
|
||||
|
@ -6,6 +6,7 @@ from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.exceptions import Unauthorized
|
||||
from homeassistant.helpers.data_entry_flow import (
|
||||
FlowManagerIndexView, FlowManagerResourceView)
|
||||
from homeassistant.generated import config_flows
|
||||
|
||||
|
||||
async def async_setup(hass):
|
||||
@ -172,7 +173,7 @@ class ConfigManagerAvailableFlowView(HomeAssistantView):
|
||||
|
||||
async def get(self, request):
|
||||
"""List available flow handlers."""
|
||||
return self.json(config_entries.FLOWS)
|
||||
return self.json(config_flows.FLOWS)
|
||||
|
||||
|
||||
class OptionManagerFlowIndexView(FlowManagerIndexView):
|
||||
|
@ -1,12 +1,22 @@
|
||||
"""Component to interact with Hassbian tools."""
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.config import async_check_ha_config_file
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.const import (
|
||||
CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.util import location
|
||||
|
||||
|
||||
async def async_setup(hass):
|
||||
"""Set up the Hassbian config."""
|
||||
hass.http.register_view(CheckConfigView)
|
||||
websocket_api.async_register_command(hass, websocket_update_config)
|
||||
websocket_api.async_register_command(hass, websocket_detect_config)
|
||||
return True
|
||||
|
||||
|
||||
@ -26,3 +36,62 @@ class CheckConfigView(HomeAssistantView):
|
||||
"result": state,
|
||||
"errors": errors,
|
||||
})
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.async_response
|
||||
@websocket_api.websocket_command({
|
||||
'type': 'config/core/update',
|
||||
vol.Optional('latitude'): cv.latitude,
|
||||
vol.Optional('longitude'): cv.longitude,
|
||||
vol.Optional('elevation'): int,
|
||||
vol.Optional('unit_system'): cv.unit_system,
|
||||
vol.Optional('location_name'): str,
|
||||
vol.Optional('time_zone'): cv.time_zone,
|
||||
})
|
||||
async def websocket_update_config(hass, connection, msg):
|
||||
"""Handle update core config command."""
|
||||
data = dict(msg)
|
||||
data.pop('id')
|
||||
data.pop('type')
|
||||
|
||||
try:
|
||||
await hass.config.update(**data)
|
||||
connection.send_result(msg['id'])
|
||||
except ValueError as err:
|
||||
connection.send_error(
|
||||
msg['id'], 'invalid_info', str(err)
|
||||
)
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.async_response
|
||||
@websocket_api.websocket_command({
|
||||
'type': 'config/core/detect',
|
||||
})
|
||||
async def websocket_detect_config(hass, connection, msg):
|
||||
"""Detect core config."""
|
||||
session = hass.helpers.aiohttp_client.async_get_clientsession()
|
||||
location_info = await location.async_detect_location_info(session)
|
||||
|
||||
info = {}
|
||||
|
||||
if location_info is None:
|
||||
connection.send_result(msg['id'], info)
|
||||
return
|
||||
|
||||
if location_info.use_metric:
|
||||
info['unit_system'] = CONF_UNIT_SYSTEM_METRIC
|
||||
else:
|
||||
info['unit_system'] = CONF_UNIT_SYSTEM_IMPERIAL
|
||||
|
||||
if location_info.latitude:
|
||||
info['latitude'] = location_info.latitude
|
||||
|
||||
if location_info.longitude:
|
||||
info['longitude'] = location_info.longitude
|
||||
|
||||
if location_info.time_zone:
|
||||
info['time_zone'] = location_info.time_zone
|
||||
|
||||
connection.send_result(msg['id'], info)
|
||||
|
@ -81,12 +81,7 @@ class DaikinClimate(ClimateDevice):
|
||||
self._api = api
|
||||
self._list = {
|
||||
ATTR_OPERATION_MODE: list(HA_STATE_TO_DAIKIN),
|
||||
ATTR_FAN_MODE: list(
|
||||
map(
|
||||
str.title,
|
||||
appliance.daikin_values(HA_ATTR_TO_DAIKIN[ATTR_FAN_MODE])
|
||||
)
|
||||
),
|
||||
ATTR_FAN_MODE: self._api.device.fan_rate,
|
||||
ATTR_SWING_MODE: list(
|
||||
map(
|
||||
str.title,
|
||||
@ -95,11 +90,14 @@ class DaikinClimate(ClimateDevice):
|
||||
),
|
||||
}
|
||||
|
||||
self._supported_features = (SUPPORT_AWAY_MODE | SUPPORT_ON_OFF
|
||||
self._supported_features = (SUPPORT_ON_OFF
|
||||
| SUPPORT_OPERATION_MODE
|
||||
| SUPPORT_TARGET_TEMPERATURE)
|
||||
|
||||
if self._api.device.support_fan_mode:
|
||||
if self._api.device.support_away_mode:
|
||||
self._supported_features |= SUPPORT_AWAY_MODE
|
||||
|
||||
if self._api.device.support_fan_rate:
|
||||
self._supported_features |= SUPPORT_FAN_MODE
|
||||
|
||||
if self._api.device.support_swing_mode:
|
||||
|
@ -1,9 +1,10 @@
|
||||
{
|
||||
"domain": "daikin",
|
||||
"name": "Daikin",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/daikin",
|
||||
"requirements": [
|
||||
"pydaikin==1.4.0"
|
||||
"pydaikin==1.4.6"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": [
|
||||
|
@ -26,9 +26,12 @@ async def async_setup_platform(
|
||||
async def async_setup_entry(hass, entry, async_add_entities):
|
||||
"""Set up Daikin climate based on config_entry."""
|
||||
daikin_api = hass.data[DAIKIN_DOMAIN].get(entry.entry_id)
|
||||
sensors = [ATTR_INSIDE_TEMPERATURE]
|
||||
if daikin_api.device.support_outside_temperature:
|
||||
sensors.append(ATTR_OUTSIDE_TEMPERATURE)
|
||||
async_add_entities([
|
||||
DaikinClimateSensor(daikin_api, sensor, hass.config.units)
|
||||
for sensor in SENSOR_TYPES
|
||||
for sensor in sensors
|
||||
])
|
||||
|
||||
|
||||
|
@ -27,8 +27,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
|
||||
if zones:
|
||||
async_add_entities([
|
||||
DaikinZoneSwitch(daikin_api, zone_id)
|
||||
for zone_id, name in enumerate(zones)
|
||||
if name != '-'
|
||||
for zone_id, zone in enumerate(zones) if zone != ('-', '0')
|
||||
])
|
||||
|
||||
|
||||
|
@ -103,6 +103,8 @@ class DarkSkyWeather(WeatherEntity):
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
if self._dark_sky.units is None:
|
||||
return None
|
||||
return TEMP_FAHRENHEIT if 'us' in self._dark_sky.units \
|
||||
else TEMP_CELSIUS
|
||||
|
||||
|
@ -3,12 +3,21 @@
|
||||
"abort": {
|
||||
"already_configured": "Ce pont est d\u00e9j\u00e0 configur\u00e9",
|
||||
"no_bridges": "Aucun pont deCONZ n'a \u00e9t\u00e9 d\u00e9couvert",
|
||||
"one_instance_only": "Le composant prend uniquement en charge une instance deCONZ"
|
||||
"one_instance_only": "Le composant prend uniquement en charge une instance deCONZ",
|
||||
"updated_instance": "Instance deCONZ mise \u00e0 jour avec la nouvelle adresse d'h\u00f4te"
|
||||
},
|
||||
"error": {
|
||||
"no_key": "Impossible d'obtenir une cl\u00e9 d'API"
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"data": {
|
||||
"allow_clip_sensor": "Autoriser l'importation de capteurs virtuels",
|
||||
"allow_deconz_groups": "Autoriser l'importation des groupes deCONZ"
|
||||
},
|
||||
"description": "Voulez-vous configurer Home Assistant pour qu'il se connecte \u00e0 la passerelle deCONZ fournie par l'add-on hass.io {addon} ?",
|
||||
"title": "Passerelle deCONZ Zigbee via l'add-on Hass.io"
|
||||
},
|
||||
"init": {
|
||||
"data": {
|
||||
"host": "H\u00f4te",
|
||||
|
@ -3,7 +3,8 @@
|
||||
"abort": {
|
||||
"already_configured": "Bryggan \u00e4r redan konfigurerad",
|
||||
"no_bridges": "Inga deCONZ-bryggor uppt\u00e4cktes",
|
||||
"one_instance_only": "Komponenten st\u00f6djer endast en deCONZ-instans"
|
||||
"one_instance_only": "Komponenten st\u00f6djer endast en deCONZ-instans",
|
||||
"updated_instance": "Uppdaterad deCONZ-instans med ny v\u00e4rdadress"
|
||||
},
|
||||
"error": {
|
||||
"no_key": "Det gick inte att ta emot en API-nyckel"
|
||||
@ -11,8 +12,10 @@
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"data": {
|
||||
"allow_clip_sensor": "Till\u00e5t import av virtuella sensorer"
|
||||
"allow_clip_sensor": "Till\u00e5t import av virtuella sensorer",
|
||||
"allow_deconz_groups": "Till\u00e5t import av deCONZ-grupper"
|
||||
},
|
||||
"description": "Vill du konfigurera Home Assistant f\u00f6r att ansluta till deCONZ gateway som tillhandah\u00e5lls av hass.io till\u00e4gg {addon}?",
|
||||
"title": "deCONZ Zigbee gateway via Hass.io till\u00e4gg"
|
||||
},
|
||||
"init": {
|
||||
|
@ -164,6 +164,7 @@ async def async_unload_entry(hass, config_entry):
|
||||
if not hass.data[DOMAIN]:
|
||||
hass.services.async_remove(DOMAIN, SERVICE_DECONZ)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_DEVICE_REFRESH)
|
||||
|
||||
elif gateway.master:
|
||||
await async_populate_options(hass, config_entry)
|
||||
new_master_gateway = next(iter(hass.data[DOMAIN].values()))
|
||||
|
@ -1,6 +1,8 @@
|
||||
"""Support for deCONZ binary sensors."""
|
||||
from pydeconz.sensor import Presence, Vibration
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.const import ATTR_BATTERY_LEVEL
|
||||
from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
@ -15,7 +17,7 @@ ATTR_VIBRATIONSTRENGTH = 'vibrationstrength'
|
||||
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_entities, discovery_info=None):
|
||||
"""Old way of setting up deCONZ binary sensors."""
|
||||
"""Old way of setting up deCONZ platforms."""
|
||||
pass
|
||||
|
||||
|
||||
@ -26,12 +28,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
@callback
|
||||
def async_add_sensor(sensors):
|
||||
"""Add binary sensor from deCONZ."""
|
||||
from pydeconz.sensor import DECONZ_BINARY_SENSOR
|
||||
entities = []
|
||||
|
||||
for sensor in sensors:
|
||||
|
||||
if sensor.type in DECONZ_BINARY_SENSOR and \
|
||||
if sensor.BINARY and \
|
||||
not (not gateway.allow_clip_sensor and
|
||||
sensor.type.startswith('CLIP')):
|
||||
|
||||
@ -49,16 +50,11 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorDevice):
|
||||
"""Representation of a deCONZ binary sensor."""
|
||||
|
||||
@callback
|
||||
def async_update_callback(self, reason):
|
||||
"""Update the sensor's state.
|
||||
|
||||
If reason is that state is updated,
|
||||
or reachable has changed or battery has changed.
|
||||
"""
|
||||
if reason['state'] or \
|
||||
'reachable' in reason['attr'] or \
|
||||
'battery' in reason['attr'] or \
|
||||
'on' in reason['attr']:
|
||||
def async_update_callback(self, force_update=False):
|
||||
"""Update the sensor's state."""
|
||||
changed = set(self._device.changed_keys)
|
||||
keys = {'battery', 'on', 'reachable', 'state'}
|
||||
if force_update or any(key in changed for key in keys):
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
@ -69,26 +65,33 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorDevice):
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of the sensor."""
|
||||
return self._device.sensor_class
|
||||
return self._device.SENSOR_CLASS
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon to use in the frontend."""
|
||||
return self._device.sensor_icon
|
||||
return self._device.SENSOR_ICON
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the sensor."""
|
||||
from pydeconz.sensor import PRESENCE, VIBRATION
|
||||
attr = {}
|
||||
if self._device.battery:
|
||||
attr[ATTR_BATTERY_LEVEL] = self._device.battery
|
||||
|
||||
if self._device.on is not None:
|
||||
attr[ATTR_ON] = self._device.on
|
||||
if self._device.type in PRESENCE and self._device.dark is not None:
|
||||
|
||||
if self._device.secondary_temperature is not None:
|
||||
attr[ATTR_TEMPERATURE] = self._device.secondary_temperature
|
||||
|
||||
if self._device.type in Presence.ZHATYPE and \
|
||||
self._device.dark is not None:
|
||||
attr[ATTR_DARK] = self._device.dark
|
||||
elif self._device.type in VIBRATION:
|
||||
|
||||
elif self._device.type in Vibration.ZHATYPE:
|
||||
attr[ATTR_ORIENTATION] = self._device.orientation
|
||||
attr[ATTR_TILTANGLE] = self._device.tiltangle
|
||||
attr[ATTR_VIBRATIONSTRENGTH] = self._device.vibrationstrength
|
||||
|
||||
return attr
|
||||
|
@ -1,4 +1,6 @@
|
||||
"""Support for deCONZ climate devices."""
|
||||
from pydeconz.sensor import Thermostat
|
||||
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.components.climate.const import (
|
||||
SUPPORT_ON_OFF, SUPPORT_TARGET_TEMPERATURE)
|
||||
@ -12,6 +14,12 @@ from .deconz_device import DeconzDevice
|
||||
from .gateway import get_gateway_from_config_entry
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_entities, discovery_info=None):
|
||||
"""Old way of setting up deCONZ platforms."""
|
||||
pass
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up the deCONZ climate devices.
|
||||
|
||||
@ -22,12 +30,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
@callback
|
||||
def async_add_climate(sensors):
|
||||
"""Add climate devices from deCONZ."""
|
||||
from pydeconz.sensor import THERMOSTAT
|
||||
entities = []
|
||||
|
||||
for sensor in sensors:
|
||||
|
||||
if sensor.type in THERMOSTAT and \
|
||||
if sensor.type in Thermostat.ZHATYPE and \
|
||||
not (not gateway.allow_clip_sensor and
|
||||
sensor.type.startswith('CLIP')):
|
||||
|
||||
@ -59,7 +66,7 @@ class DeconzThermostat(DeconzDevice, ClimateDevice):
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if on."""
|
||||
return self._device.on
|
||||
return self._device.state_on
|
||||
|
||||
async def async_turn_on(self):
|
||||
"""Turn on switch."""
|
||||
|
@ -4,13 +4,19 @@ import asyncio
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from pydeconz.errors import ResponseError, RequestError
|
||||
from pydeconz.utils import (
|
||||
async_discovery, async_get_api_key, async_get_bridgeid)
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_SERIAL
|
||||
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
|
||||
from .const import CONF_BRIDGEID, DEFAULT_PORT, DOMAIN
|
||||
|
||||
DECONZ_MANUFACTURERURL = 'http://www.dresden-elektronik.de'
|
||||
CONF_SERIAL = 'serial'
|
||||
|
||||
|
||||
@ -54,8 +60,6 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
|
||||
If more than one bridge is found let user choose bridge to link.
|
||||
If no bridge is found allow user to manually input configuration.
|
||||
"""
|
||||
from pydeconz.utils import async_discovery
|
||||
|
||||
if user_input is not None:
|
||||
for bridge in self.bridges:
|
||||
if bridge[CONF_HOST] == user_input[CONF_HOST]:
|
||||
@ -101,8 +105,6 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
|
||||
|
||||
async def async_step_link(self, user_input=None):
|
||||
"""Attempt to link with the deCONZ bridge."""
|
||||
from pydeconz.errors import ResponseError, RequestError
|
||||
from pydeconz.utils import async_get_api_key
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
@ -127,8 +129,6 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
|
||||
|
||||
async def _create_entry(self):
|
||||
"""Create entry for gateway."""
|
||||
from pydeconz.utils import async_get_bridgeid
|
||||
|
||||
if CONF_BRIDGEID not in self.deconz_config:
|
||||
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||
|
||||
@ -151,12 +151,12 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
|
||||
entry.data[CONF_HOST] = host
|
||||
self.hass.config_entries.async_update_entry(entry)
|
||||
|
||||
async def async_step_discovery(self, discovery_info):
|
||||
"""Prepare configuration for a discovered deCONZ bridge.
|
||||
async def async_step_ssdp(self, discovery_info):
|
||||
"""Handle a discovered deCONZ bridge."""
|
||||
if discovery_info[ATTR_MANUFACTURERURL] != DECONZ_MANUFACTURERURL:
|
||||
return self.async_abort(reason='not_deconz_bridge')
|
||||
|
||||
This flow is triggered by the discovery component.
|
||||
"""
|
||||
bridgeid = discovery_info[CONF_SERIAL]
|
||||
bridgeid = discovery_info[ATTR_SERIAL]
|
||||
gateway_entries = configured_gateways(self.hass)
|
||||
|
||||
if bridgeid in gateway_entries:
|
||||
@ -164,10 +164,17 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
|
||||
await self._update_entry(entry, discovery_info[CONF_HOST])
|
||||
return self.async_abort(reason='updated_instance')
|
||||
|
||||
# pylint: disable=unsupported-assignment-operation
|
||||
self.context[ATTR_SERIAL] = bridgeid
|
||||
|
||||
if any(bridgeid == flow['context'][ATTR_SERIAL]
|
||||
for flow in self._async_in_progress()):
|
||||
return self.async_abort(reason='already_in_progress')
|
||||
|
||||
deconz_config = {
|
||||
CONF_HOST: discovery_info[CONF_HOST],
|
||||
CONF_PORT: discovery_info[CONF_PORT],
|
||||
CONF_BRIDGEID: discovery_info[CONF_SERIAL]
|
||||
CONF_BRIDGEID: bridgeid
|
||||
}
|
||||
|
||||
return await self.async_step_import(deconz_config)
|
||||
|
@ -14,7 +14,7 @@ ZIGBEE_SPEC = ['lumi.curtain']
|
||||
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_entities, discovery_info=None):
|
||||
"""Unsupported way of setting up deCONZ covers."""
|
||||
"""Old way of setting up deCONZ platforms."""
|
||||
pass
|
||||
|
||||
|
||||
|
@ -31,7 +31,7 @@ class DeconzDevice(Entity):
|
||||
self.unsub_dispatcher()
|
||||
|
||||
@callback
|
||||
def async_update_callback(self, reason):
|
||||
def async_update_callback(self, force_update=False):
|
||||
"""Update the device's state."""
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
|
@ -2,6 +2,9 @@
|
||||
import asyncio
|
||||
import async_timeout
|
||||
|
||||
from pydeconz import DeconzSession, errors
|
||||
from pydeconz.sensor import Switch
|
||||
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.const import CONF_EVENT, CONF_HOST, CONF_ID
|
||||
from homeassistant.core import EventOrigin, callback
|
||||
@ -126,8 +129,7 @@ class DeconzGateway:
|
||||
def async_connection_status_callback(self, available):
|
||||
"""Handle signals of gateway connection status."""
|
||||
self.available = available
|
||||
async_dispatcher_send(self.hass, self.event_reachable,
|
||||
{'state': True, 'attr': 'reachable'})
|
||||
async_dispatcher_send(self.hass, self.event_reachable, True)
|
||||
|
||||
@callback
|
||||
def async_event_new_device(self, device_type):
|
||||
@ -145,9 +147,8 @@ class DeconzGateway:
|
||||
@callback
|
||||
def async_add_remote(self, sensors):
|
||||
"""Set up remote from deCONZ."""
|
||||
from pydeconz.sensor import SWITCH as DECONZ_REMOTE
|
||||
for sensor in sensors:
|
||||
if sensor.type in DECONZ_REMOTE and \
|
||||
if sensor.type in Switch.ZHATYPE and \
|
||||
not (not self.allow_clip_sensor and
|
||||
sensor.type.startswith('CLIP')):
|
||||
self.events.append(DeconzEvent(self.hass, sensor))
|
||||
@ -187,8 +188,6 @@ class DeconzGateway:
|
||||
async def get_gateway(hass, config, async_add_device_callback,
|
||||
async_connection_status_callback):
|
||||
"""Create a gateway object and verify configuration."""
|
||||
from pydeconz import DeconzSession, errors
|
||||
|
||||
session = aiohttp_client.async_get_clientsession(hass)
|
||||
|
||||
deconz = DeconzSession(hass.loop, session, **config,
|
||||
@ -232,8 +231,8 @@ class DeconzEvent:
|
||||
self._device = None
|
||||
|
||||
@callback
|
||||
def async_update_callback(self, reason):
|
||||
def async_update_callback(self, force_update=False):
|
||||
"""Fire the event if reason is that state is updated."""
|
||||
if reason['state']:
|
||||
if 'state' in self._device.changed_keys:
|
||||
data = {CONF_ID: self._id, CONF_EVENT: self._device.state}
|
||||
self._hass.bus.async_fire(self._event, data, EventOrigin.remote)
|
||||
|
@ -15,7 +15,7 @@ from .gateway import get_gateway_from_config_entry
|
||||
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_entities, discovery_info=None):
|
||||
"""Old way of setting up deCONZ lights and group."""
|
||||
"""Old way of setting up deCONZ platforms."""
|
||||
pass
|
||||
|
||||
|
||||
|
@ -1,10 +1,16 @@
|
||||
{
|
||||
"domain": "deconz",
|
||||
"name": "Deconz",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/deconz",
|
||||
"requirements": [
|
||||
"pydeconz==58"
|
||||
"pydeconz==59"
|
||||
],
|
||||
"ssdp": {
|
||||
"manufacturer": [
|
||||
"Royal Philips Electronics"
|
||||
]
|
||||
},
|
||||
"dependencies": [],
|
||||
"codeowners": [
|
||||
"@kane610"
|
||||
|
@ -9,7 +9,7 @@ from .gateway import get_gateway_from_config_entry
|
||||
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_entities, discovery_info=None):
|
||||
"""Old way of setting up deCONZ scenes."""
|
||||
"""Old way of setting up deCONZ platforms."""
|
||||
pass
|
||||
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
"""Support for deCONZ sensors."""
|
||||
from pydeconz.sensor import LightLevel, Switch
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY)
|
||||
ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.util import slugify
|
||||
@ -16,7 +18,7 @@ ATTR_EVENT_ID = 'event_id'
|
||||
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_entities, discovery_info=None):
|
||||
"""Old way of setting up deCONZ sensors."""
|
||||
"""Old way of setting up deCONZ platforms."""
|
||||
pass
|
||||
|
||||
|
||||
@ -27,17 +29,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
@callback
|
||||
def async_add_sensor(sensors):
|
||||
"""Add sensors from deCONZ."""
|
||||
from pydeconz.sensor import (
|
||||
DECONZ_SENSOR, SWITCH as DECONZ_REMOTE)
|
||||
entities = []
|
||||
|
||||
for sensor in sensors:
|
||||
|
||||
if sensor.type in DECONZ_SENSOR and \
|
||||
if not sensor.BINARY and \
|
||||
not (not gateway.allow_clip_sensor and
|
||||
sensor.type.startswith('CLIP')):
|
||||
|
||||
if sensor.type in DECONZ_REMOTE:
|
||||
if sensor.type in Switch.ZHATYPE:
|
||||
if sensor.battery:
|
||||
entities.append(DeconzBattery(sensor, gateway))
|
||||
|
||||
@ -56,16 +56,11 @@ class DeconzSensor(DeconzDevice):
|
||||
"""Representation of a deCONZ sensor."""
|
||||
|
||||
@callback
|
||||
def async_update_callback(self, reason):
|
||||
"""Update the sensor's state.
|
||||
|
||||
If reason is that state is updated,
|
||||
or reachable has changed or battery has changed.
|
||||
"""
|
||||
if reason['state'] or \
|
||||
'reachable' in reason['attr'] or \
|
||||
'battery' in reason['attr'] or \
|
||||
'on' in reason['attr']:
|
||||
def async_update_callback(self, force_update=False):
|
||||
"""Update the sensor's state."""
|
||||
changed = set(self._device.changed_keys)
|
||||
keys = {'battery', 'on', 'reachable', 'state'}
|
||||
if force_update or any(key in changed for key in keys):
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
@ -76,34 +71,42 @@ class DeconzSensor(DeconzDevice):
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of the sensor."""
|
||||
return self._device.sensor_class
|
||||
return self._device.SENSOR_CLASS
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon to use in the frontend."""
|
||||
return self._device.sensor_icon
|
||||
return self._device.SENSOR_ICON
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement of this sensor."""
|
||||
return self._device.sensor_unit
|
||||
return self._device.SENSOR_UNIT
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the sensor."""
|
||||
from pydeconz.sensor import LIGHTLEVEL
|
||||
attr = {}
|
||||
if self._device.battery:
|
||||
attr[ATTR_BATTERY_LEVEL] = self._device.battery
|
||||
|
||||
if self._device.on is not None:
|
||||
attr[ATTR_ON] = self._device.on
|
||||
if self._device.type in LIGHTLEVEL and self._device.dark is not None:
|
||||
|
||||
if self._device.secondary_temperature is not None:
|
||||
attr[ATTR_TEMPERATURE] = self._device.secondary_temperature
|
||||
|
||||
if self._device.type in LightLevel.ZHATYPE and \
|
||||
self._device.dark is not None:
|
||||
attr[ATTR_DARK] = self._device.dark
|
||||
|
||||
if self.unit_of_measurement == 'Watts':
|
||||
attr[ATTR_CURRENT] = self._device.current
|
||||
attr[ATTR_VOLTAGE] = self._device.voltage
|
||||
if self._device.sensor_class == 'daylight':
|
||||
|
||||
if self._device.SENSOR_CLASS == 'daylight':
|
||||
attr[ATTR_DAYLIGHT] = self._device.daylight
|
||||
|
||||
return attr
|
||||
|
||||
|
||||
@ -118,9 +121,11 @@ class DeconzBattery(DeconzDevice):
|
||||
self._unit_of_measurement = "%"
|
||||
|
||||
@callback
|
||||
def async_update_callback(self, reason):
|
||||
def async_update_callback(self, force_update=False):
|
||||
"""Update the battery's state, if needed."""
|
||||
if 'reachable' in reason['attr'] or 'battery' in reason['attr']:
|
||||
changed = set(self._device.changed_keys)
|
||||
keys = {'battery', 'reachable'}
|
||||
if force_update or any(key in changed for key in keys):
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
|
@ -34,9 +34,11 @@
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Bridge is already configured",
|
||||
"already_in_progress": "Config flow for bridge is already in progress.",
|
||||
"no_bridges": "No deCONZ bridges discovered",
|
||||
"updated_instance": "Updated deCONZ instance with new host address",
|
||||
"one_instance_only": "Component only supports one deCONZ instance"
|
||||
"not_deconz_bridge": "Not a deCONZ bridge",
|
||||
"one_instance_only": "Component only supports one deCONZ instance",
|
||||
"updated_instance": "Updated deCONZ instance with new host address"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ from .gateway import get_gateway_from_config_entry
|
||||
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_entities, discovery_info=None):
|
||||
"""Old way of setting up deCONZ switches."""
|
||||
"""Old way of setting up deCONZ platforms."""
|
||||
pass
|
||||
|
||||
|
||||
|
@ -15,6 +15,7 @@
|
||||
"mobile_app",
|
||||
"person",
|
||||
"script",
|
||||
"ssdp",
|
||||
"sun",
|
||||
"system_health",
|
||||
"updater",
|
||||
|
@ -5,7 +5,8 @@ from homeassistant.components.media_player.const import (
|
||||
SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY,
|
||||
SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
|
||||
SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET,
|
||||
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET)
|
||||
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
||||
SUPPORT_VOLUME_STEP)
|
||||
from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
@ -35,7 +36,7 @@ YOUTUBE_PLAYER_SUPPORT = \
|
||||
MUSIC_PLAYER_SUPPORT = \
|
||||
SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_CLEAR_PLAYLIST | \
|
||||
SUPPORT_PLAY | SUPPORT_SHUFFLE_SET | \
|
||||
SUPPORT_PLAY | SUPPORT_SHUFFLE_SET | SUPPORT_VOLUME_STEP | \
|
||||
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
|
||||
SUPPORT_SELECT_SOUND_MODE
|
||||
|
||||
@ -122,6 +123,16 @@ class AbstractDemoPlayer(MediaPlayerDevice):
|
||||
self._volume_muted = mute
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def volume_up(self):
|
||||
"""Increase volume."""
|
||||
self._volume_level = min(1.0, self._volume_level + 0.1)
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def volume_down(self):
|
||||
"""Decrease volume."""
|
||||
self._volume_level = max(0.0, self._volume_level - 0.1)
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def set_volume_level(self, volume):
|
||||
"""Set the volume level, range 0..1."""
|
||||
self._volume_level = volume
|
||||
|
@ -1,78 +1,52 @@
|
||||
"""Provide functionality to keep track of devices."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any, List, Sequence, Callable
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.setup import async_prepare_setup_platform
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.components import group, zone
|
||||
from homeassistant.components.group import (
|
||||
ATTR_ADD_ENTITIES, ATTR_ENTITIES, ATTR_OBJECT_ID, ATTR_VISIBLE,
|
||||
DOMAIN as DOMAIN_GROUP, SERVICE_SET)
|
||||
from homeassistant.components.zone.zone import async_active_zone
|
||||
from homeassistant.config import load_yaml_config_file, async_log_exception
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_per_platform, discovery
|
||||
from homeassistant.components import group
|
||||
from homeassistant.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType
|
||||
from homeassistant import util
|
||||
from homeassistant.util.async_ import run_coroutine_threadsafe
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.util.yaml import dump
|
||||
|
||||
from homeassistant.helpers.event import async_track_utc_time_change
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_GPS_ACCURACY, ATTR_ICON, ATTR_LATITUDE,
|
||||
ATTR_LONGITUDE, ATTR_NAME, CONF_ICON, CONF_MAC, CONF_NAME,
|
||||
DEVICE_DEFAULT_NAME, STATE_NOT_HOME, STATE_HOME)
|
||||
from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
from . import legacy, setup
|
||||
from .config_entry import ( # noqa # pylint: disable=unused-import
|
||||
async_setup_entry, async_unload_entry
|
||||
)
|
||||
from .legacy import DeviceScanner # noqa # pylint: disable=unused-import
|
||||
from .const import (
|
||||
ATTR_ATTRIBUTES,
|
||||
ATTR_BATTERY,
|
||||
ATTR_CONSIDER_HOME,
|
||||
ATTR_DEV_ID,
|
||||
ATTR_GPS,
|
||||
ATTR_HOST_NAME,
|
||||
ATTR_LOCATION_NAME,
|
||||
ATTR_MAC,
|
||||
ATTR_SOURCE_TYPE,
|
||||
CONF_AWAY_HIDE,
|
||||
CONF_CONSIDER_HOME,
|
||||
CONF_NEW_DEVICE_DEFAULTS,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_TRACK_NEW,
|
||||
DEFAULT_AWAY_HIDE,
|
||||
DEFAULT_CONSIDER_HOME,
|
||||
DEFAULT_TRACK_NEW,
|
||||
DOMAIN,
|
||||
PLATFORM_TYPE_LEGACY,
|
||||
SOURCE_TYPE_BLUETOOTH_LE,
|
||||
SOURCE_TYPE_BLUETOOTH,
|
||||
SOURCE_TYPE_GPS,
|
||||
SOURCE_TYPE_ROUTER,
|
||||
)
|
||||
|
||||
DOMAIN = 'device_tracker'
|
||||
GROUP_NAME_ALL_DEVICES = 'all devices'
|
||||
ENTITY_ID_ALL_DEVICES = group.ENTITY_ID_FORMAT.format('all_devices')
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
YAML_DEVICES = 'known_devices.yaml'
|
||||
|
||||
CONF_TRACK_NEW = 'track_new_devices'
|
||||
DEFAULT_TRACK_NEW = True
|
||||
CONF_NEW_DEVICE_DEFAULTS = 'new_device_defaults'
|
||||
|
||||
CONF_CONSIDER_HOME = 'consider_home'
|
||||
DEFAULT_CONSIDER_HOME = timedelta(seconds=180)
|
||||
|
||||
CONF_SCAN_INTERVAL = 'interval_seconds'
|
||||
DEFAULT_SCAN_INTERVAL = timedelta(seconds=12)
|
||||
|
||||
CONF_AWAY_HIDE = 'hide_if_away'
|
||||
DEFAULT_AWAY_HIDE = False
|
||||
|
||||
EVENT_NEW_DEVICE = 'device_tracker_new_device'
|
||||
|
||||
SERVICE_SEE = 'see'
|
||||
|
||||
ATTR_ATTRIBUTES = 'attributes'
|
||||
ATTR_BATTERY = 'battery'
|
||||
ATTR_DEV_ID = 'dev_id'
|
||||
ATTR_GPS = 'gps'
|
||||
ATTR_HOST_NAME = 'host_name'
|
||||
ATTR_LOCATION_NAME = 'location_name'
|
||||
ATTR_MAC = 'mac'
|
||||
ATTR_SOURCE_TYPE = 'source_type'
|
||||
ATTR_CONSIDER_HOME = 'consider_home'
|
||||
|
||||
SOURCE_TYPE_GPS = 'gps'
|
||||
SOURCE_TYPE_ROUTER = 'router'
|
||||
SOURCE_TYPE_BLUETOOTH = 'bluetooth'
|
||||
SOURCE_TYPE_BLUETOOTH_LE = 'bluetooth_le'
|
||||
SOURCE_TYPES = (SOURCE_TYPE_GPS, SOURCE_TYPE_ROUTER,
|
||||
SOURCE_TYPE_BLUETOOTH, SOURCE_TYPE_BLUETOOTH_LE)
|
||||
|
||||
@ -136,75 +110,29 @@ def see(hass: HomeAssistantType, mac: str = None, dev_id: str = None,
|
||||
|
||||
async def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||
"""Set up the device tracker."""
|
||||
yaml_path = hass.config.path(YAML_DEVICES)
|
||||
tracker = await legacy.get_tracker(hass, config)
|
||||
|
||||
conf = config.get(DOMAIN, [])
|
||||
conf = conf[0] if conf else {}
|
||||
consider_home = conf.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME)
|
||||
legacy_platforms = await setup.async_extract_config(hass, config)
|
||||
|
||||
defaults = conf.get(CONF_NEW_DEVICE_DEFAULTS, {})
|
||||
track_new = conf.get(CONF_TRACK_NEW)
|
||||
if track_new is None:
|
||||
track_new = defaults.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
|
||||
setup_tasks = [
|
||||
legacy_platform.async_setup_legacy(hass, tracker)
|
||||
for legacy_platform in legacy_platforms
|
||||
]
|
||||
|
||||
devices = await async_load_config(yaml_path, hass, consider_home)
|
||||
tracker = DeviceTracker(
|
||||
hass, consider_home, track_new, defaults, devices)
|
||||
|
||||
async def async_setup_platform(p_type, p_config, disc_info=None):
|
||||
"""Set up a device tracker platform."""
|
||||
platform = await async_prepare_setup_platform(
|
||||
hass, config, DOMAIN, p_type)
|
||||
if platform is None:
|
||||
return
|
||||
|
||||
_LOGGER.info("Setting up %s.%s", DOMAIN, p_type)
|
||||
try:
|
||||
scanner = None
|
||||
setup = None
|
||||
if hasattr(platform, 'async_get_scanner'):
|
||||
scanner = await platform.async_get_scanner(
|
||||
hass, {DOMAIN: p_config})
|
||||
elif hasattr(platform, 'get_scanner'):
|
||||
scanner = await hass.async_add_job(
|
||||
platform.get_scanner, hass, {DOMAIN: p_config})
|
||||
elif hasattr(platform, 'async_setup_scanner'):
|
||||
setup = await platform.async_setup_scanner(
|
||||
hass, p_config, tracker.async_see, disc_info)
|
||||
elif hasattr(platform, 'setup_scanner'):
|
||||
setup = await hass.async_add_job(
|
||||
platform.setup_scanner, hass, p_config, tracker.see,
|
||||
disc_info)
|
||||
elif hasattr(platform, 'async_setup_entry'):
|
||||
setup = await platform.async_setup_entry(
|
||||
hass, p_config, tracker.async_see)
|
||||
else:
|
||||
raise HomeAssistantError("Invalid device_tracker platform.")
|
||||
|
||||
if scanner:
|
||||
async_setup_scanner_platform(
|
||||
hass, p_config, scanner, tracker.async_see, p_type)
|
||||
return
|
||||
|
||||
if not setup:
|
||||
_LOGGER.error("Error setting up platform %s", p_type)
|
||||
return
|
||||
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Error setting up platform %s", p_type)
|
||||
|
||||
hass.data[DOMAIN] = async_setup_platform
|
||||
|
||||
setup_tasks = [async_setup_platform(p_type, p_config) for p_type, p_config
|
||||
in config_per_platform(config, DOMAIN)]
|
||||
if setup_tasks:
|
||||
await asyncio.wait(setup_tasks, loop=hass.loop)
|
||||
await asyncio.wait(setup_tasks)
|
||||
|
||||
tracker.async_setup_group()
|
||||
|
||||
async def async_platform_discovered(platform, info):
|
||||
async def async_platform_discovered(p_type, info):
|
||||
"""Load a platform."""
|
||||
await async_setup_platform(platform, {}, disc_info=info)
|
||||
platform = await setup.async_create_platform_type(
|
||||
hass, config, p_type, {})
|
||||
|
||||
if platform is None or platform.type != PLATFORM_TYPE_LEGACY:
|
||||
return
|
||||
|
||||
await platform.async_setup_legacy(hass, tracker, info)
|
||||
|
||||
discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered)
|
||||
|
||||
@ -226,537 +154,3 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||
# restore
|
||||
await tracker.async_setup_tracked_device()
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry):
|
||||
"""Set up an entry."""
|
||||
await hass.data[DOMAIN](entry.domain, entry)
|
||||
return True
|
||||
|
||||
|
||||
class DeviceTracker:
|
||||
"""Representation of a device tracker."""
|
||||
|
||||
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
|
||||
track_new: bool, defaults: dict,
|
||||
devices: Sequence) -> None:
|
||||
"""Initialize a device tracker."""
|
||||
self.hass = hass
|
||||
self.devices = {dev.dev_id: dev for dev in devices}
|
||||
self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac}
|
||||
self.consider_home = consider_home
|
||||
self.track_new = track_new if track_new is not None \
|
||||
else defaults.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
|
||||
self.defaults = defaults
|
||||
self.group = None
|
||||
self._is_updating = asyncio.Lock(loop=hass.loop)
|
||||
|
||||
for dev in devices:
|
||||
if self.devices[dev.dev_id] is not dev:
|
||||
_LOGGER.warning('Duplicate device IDs detected %s', dev.dev_id)
|
||||
if dev.mac and self.mac_to_dev[dev.mac] is not dev:
|
||||
_LOGGER.warning('Duplicate device MAC addresses detected %s',
|
||||
dev.mac)
|
||||
|
||||
def see(self, mac: str = None, dev_id: str = None, host_name: str = None,
|
||||
location_name: str = None, gps: GPSType = None,
|
||||
gps_accuracy: int = None, battery: int = None,
|
||||
attributes: dict = None, source_type: str = SOURCE_TYPE_GPS,
|
||||
picture: str = None, icon: str = None,
|
||||
consider_home: timedelta = None):
|
||||
"""Notify the device tracker that you see a device."""
|
||||
self.hass.add_job(
|
||||
self.async_see(mac, dev_id, host_name, location_name, gps,
|
||||
gps_accuracy, battery, attributes, source_type,
|
||||
picture, icon, consider_home)
|
||||
)
|
||||
|
||||
async def async_see(
|
||||
self, mac: str = None, dev_id: str = None, host_name: str = None,
|
||||
location_name: str = None, gps: GPSType = None,
|
||||
gps_accuracy: int = None, battery: int = None,
|
||||
attributes: dict = None, source_type: str = SOURCE_TYPE_GPS,
|
||||
picture: str = None, icon: str = None,
|
||||
consider_home: timedelta = None):
|
||||
"""Notify the device tracker that you see a device.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if mac is None and dev_id is None:
|
||||
raise HomeAssistantError('Neither mac or device id passed in')
|
||||
if mac is not None:
|
||||
mac = str(mac).upper()
|
||||
device = self.mac_to_dev.get(mac)
|
||||
if not device:
|
||||
dev_id = util.slugify(host_name or '') or util.slugify(mac)
|
||||
else:
|
||||
dev_id = cv.slug(str(dev_id).lower())
|
||||
device = self.devices.get(dev_id)
|
||||
|
||||
if device:
|
||||
await device.async_seen(
|
||||
host_name, location_name, gps, gps_accuracy, battery,
|
||||
attributes, source_type, consider_home)
|
||||
if device.track:
|
||||
await device.async_update_ha_state()
|
||||
return
|
||||
|
||||
# If no device can be found, create it
|
||||
dev_id = util.ensure_unique_string(dev_id, self.devices.keys())
|
||||
device = Device(
|
||||
self.hass, consider_home or self.consider_home, self.track_new,
|
||||
dev_id, mac, (host_name or dev_id).replace('_', ' '),
|
||||
picture=picture, icon=icon,
|
||||
hide_if_away=self.defaults.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE))
|
||||
self.devices[dev_id] = device
|
||||
if mac is not None:
|
||||
self.mac_to_dev[mac] = device
|
||||
|
||||
await device.async_seen(
|
||||
host_name, location_name, gps, gps_accuracy, battery, attributes,
|
||||
source_type)
|
||||
|
||||
if device.track:
|
||||
await device.async_update_ha_state()
|
||||
|
||||
# During init, we ignore the group
|
||||
if self.group and self.track_new:
|
||||
self.hass.async_create_task(
|
||||
self.hass.async_call(
|
||||
DOMAIN_GROUP, SERVICE_SET, {
|
||||
ATTR_OBJECT_ID: util.slugify(GROUP_NAME_ALL_DEVICES),
|
||||
ATTR_VISIBLE: False,
|
||||
ATTR_NAME: GROUP_NAME_ALL_DEVICES,
|
||||
ATTR_ADD_ENTITIES: [device.entity_id]}))
|
||||
|
||||
self.hass.bus.async_fire(EVENT_NEW_DEVICE, {
|
||||
ATTR_ENTITY_ID: device.entity_id,
|
||||
ATTR_HOST_NAME: device.host_name,
|
||||
ATTR_MAC: device.mac,
|
||||
})
|
||||
|
||||
# update known_devices.yaml
|
||||
self.hass.async_create_task(
|
||||
self.async_update_config(
|
||||
self.hass.config.path(YAML_DEVICES), dev_id, device)
|
||||
)
|
||||
|
||||
async def async_update_config(self, path, dev_id, device):
|
||||
"""Add device to YAML configuration file.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
async with self._is_updating:
|
||||
await self.hass.async_add_executor_job(
|
||||
update_config, self.hass.config.path(YAML_DEVICES),
|
||||
dev_id, device)
|
||||
|
||||
@callback
|
||||
def async_setup_group(self):
|
||||
"""Initialize group for all tracked devices.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
entity_ids = [dev.entity_id for dev in self.devices.values()
|
||||
if dev.track]
|
||||
|
||||
self.hass.async_create_task(
|
||||
self.hass.services.async_call(
|
||||
DOMAIN_GROUP, SERVICE_SET, {
|
||||
ATTR_OBJECT_ID: util.slugify(GROUP_NAME_ALL_DEVICES),
|
||||
ATTR_VISIBLE: False,
|
||||
ATTR_NAME: GROUP_NAME_ALL_DEVICES,
|
||||
ATTR_ENTITIES: entity_ids}))
|
||||
|
||||
@callback
|
||||
def async_update_stale(self, now: dt_util.dt.datetime):
|
||||
"""Update stale devices.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
for device in self.devices.values():
|
||||
if (device.track and device.last_update_home) and \
|
||||
device.stale(now):
|
||||
self.hass.async_create_task(device.async_update_ha_state(True))
|
||||
|
||||
async def async_setup_tracked_device(self):
|
||||
"""Set up all not exists tracked devices.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
async def async_init_single_device(dev):
|
||||
"""Init a single device_tracker entity."""
|
||||
await dev.async_added_to_hass()
|
||||
await dev.async_update_ha_state()
|
||||
|
||||
tasks = []
|
||||
for device in self.devices.values():
|
||||
if device.track and not device.last_seen:
|
||||
tasks.append(self.hass.async_create_task(
|
||||
async_init_single_device(device)))
|
||||
|
||||
if tasks:
|
||||
await asyncio.wait(tasks, loop=self.hass.loop)
|
||||
|
||||
|
||||
class Device(RestoreEntity):
|
||||
"""Represent a tracked device."""
|
||||
|
||||
host_name = None # type: str
|
||||
location_name = None # type: str
|
||||
gps = None # type: GPSType
|
||||
gps_accuracy = 0 # type: int
|
||||
last_seen = None # type: dt_util.dt.datetime
|
||||
consider_home = None # type: dt_util.dt.timedelta
|
||||
battery = None # type: int
|
||||
attributes = None # type: dict
|
||||
icon = None # type: str
|
||||
|
||||
# Track if the last update of this device was HOME.
|
||||
last_update_home = False
|
||||
_state = STATE_NOT_HOME
|
||||
|
||||
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
|
||||
track: bool, dev_id: str, mac: str, name: str = None,
|
||||
picture: str = None, gravatar: str = None, icon: str = None,
|
||||
hide_if_away: bool = False) -> None:
|
||||
"""Initialize a device."""
|
||||
self.hass = hass
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
|
||||
|
||||
# Timedelta object how long we consider a device home if it is not
|
||||
# detected anymore.
|
||||
self.consider_home = consider_home
|
||||
|
||||
# Device ID
|
||||
self.dev_id = dev_id
|
||||
self.mac = mac
|
||||
|
||||
# If we should track this device
|
||||
self.track = track
|
||||
|
||||
# Configured name
|
||||
self.config_name = name
|
||||
|
||||
# Configured picture
|
||||
if gravatar is not None:
|
||||
self.config_picture = get_gravatar_for_email(gravatar)
|
||||
else:
|
||||
self.config_picture = picture
|
||||
|
||||
self.icon = icon
|
||||
|
||||
self.away_hide = hide_if_away
|
||||
|
||||
self.source_type = None
|
||||
|
||||
self._attributes = {}
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the entity."""
|
||||
return self.config_name or self.host_name or DEVICE_DEFAULT_NAME
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def entity_picture(self):
|
||||
"""Return the picture of the device."""
|
||||
return self.config_picture
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the device state attributes."""
|
||||
attr = {
|
||||
ATTR_SOURCE_TYPE: self.source_type
|
||||
}
|
||||
|
||||
if self.gps:
|
||||
attr[ATTR_LATITUDE] = self.gps[0]
|
||||
attr[ATTR_LONGITUDE] = self.gps[1]
|
||||
attr[ATTR_GPS_ACCURACY] = self.gps_accuracy
|
||||
|
||||
if self.battery:
|
||||
attr[ATTR_BATTERY] = self.battery
|
||||
|
||||
return attr
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return device state attributes."""
|
||||
return self._attributes
|
||||
|
||||
@property
|
||||
def hidden(self):
|
||||
"""If device should be hidden."""
|
||||
return self.away_hide and self.state != STATE_HOME
|
||||
|
||||
async def async_seen(
|
||||
self, host_name: str = None, location_name: str = None,
|
||||
gps: GPSType = None, gps_accuracy=0, battery: int = None,
|
||||
attributes: dict = None,
|
||||
source_type: str = SOURCE_TYPE_GPS,
|
||||
consider_home: timedelta = None):
|
||||
"""Mark the device as seen."""
|
||||
self.source_type = source_type
|
||||
self.last_seen = dt_util.utcnow()
|
||||
self.host_name = host_name
|
||||
self.location_name = location_name
|
||||
self.consider_home = consider_home or self.consider_home
|
||||
|
||||
if battery:
|
||||
self.battery = battery
|
||||
if attributes:
|
||||
self._attributes.update(attributes)
|
||||
|
||||
self.gps = None
|
||||
|
||||
if gps is not None:
|
||||
try:
|
||||
self.gps = float(gps[0]), float(gps[1])
|
||||
self.gps_accuracy = gps_accuracy or 0
|
||||
except (ValueError, TypeError, IndexError):
|
||||
self.gps = None
|
||||
self.gps_accuracy = 0
|
||||
_LOGGER.warning(
|
||||
"Could not parse gps value for %s: %s", self.dev_id, gps)
|
||||
|
||||
# pylint: disable=not-an-iterable
|
||||
await self.async_update()
|
||||
|
||||
def stale(self, now: dt_util.dt.datetime = None):
|
||||
"""Return if device state is stale.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
return self.last_seen is None or \
|
||||
(now or dt_util.utcnow()) - self.last_seen > self.consider_home
|
||||
|
||||
def mark_stale(self):
|
||||
"""Mark the device state as stale."""
|
||||
self._state = STATE_NOT_HOME
|
||||
self.gps = None
|
||||
self.last_update_home = False
|
||||
|
||||
async def async_update(self):
|
||||
"""Update state of entity.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if not self.last_seen:
|
||||
return
|
||||
if self.location_name:
|
||||
self._state = self.location_name
|
||||
elif self.gps is not None and self.source_type == SOURCE_TYPE_GPS:
|
||||
zone_state = async_active_zone(
|
||||
self.hass, self.gps[0], self.gps[1], self.gps_accuracy)
|
||||
if zone_state is None:
|
||||
self._state = STATE_NOT_HOME
|
||||
elif zone_state.entity_id == zone.ENTITY_ID_HOME:
|
||||
self._state = STATE_HOME
|
||||
else:
|
||||
self._state = zone_state.name
|
||||
elif self.stale():
|
||||
self.mark_stale()
|
||||
else:
|
||||
self._state = STATE_HOME
|
||||
self.last_update_home = True
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Add an entity."""
|
||||
await super().async_added_to_hass()
|
||||
state = await self.async_get_last_state()
|
||||
if not state:
|
||||
return
|
||||
self._state = state.state
|
||||
self.last_update_home = (state.state == STATE_HOME)
|
||||
self.last_seen = dt_util.utcnow()
|
||||
|
||||
for attr, var in (
|
||||
(ATTR_SOURCE_TYPE, 'source_type'),
|
||||
(ATTR_GPS_ACCURACY, 'gps_accuracy'),
|
||||
(ATTR_BATTERY, 'battery'),
|
||||
):
|
||||
if attr in state.attributes:
|
||||
setattr(self, var, state.attributes[attr])
|
||||
|
||||
if ATTR_LONGITUDE in state.attributes:
|
||||
self.gps = (state.attributes[ATTR_LATITUDE],
|
||||
state.attributes[ATTR_LONGITUDE])
|
||||
|
||||
|
||||
class DeviceScanner:
|
||||
"""Device scanner object."""
|
||||
|
||||
hass = None # type: HomeAssistantType
|
||||
|
||||
def scan_devices(self) -> List[str]:
|
||||
"""Scan for devices."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_scan_devices(self) -> Any:
|
||||
"""Scan for devices.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(self.scan_devices)
|
||||
|
||||
def get_device_name(self, device: str) -> str:
|
||||
"""Get the name of a device."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_get_device_name(self, device: str) -> Any:
|
||||
"""Get the name of a device.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(self.get_device_name, device)
|
||||
|
||||
def get_extra_attributes(self, device: str) -> dict:
|
||||
"""Get the extra attributes of a device."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_get_extra_attributes(self, device: str) -> Any:
|
||||
"""Get the extra attributes of a device.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(self.get_extra_attributes, device)
|
||||
|
||||
|
||||
def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
|
||||
"""Load devices from YAML configuration file."""
|
||||
return run_coroutine_threadsafe(
|
||||
async_load_config(path, hass, consider_home), hass.loop).result()
|
||||
|
||||
|
||||
async def async_load_config(path: str, hass: HomeAssistantType,
|
||||
consider_home: timedelta):
|
||||
"""Load devices from YAML configuration file.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
dev_schema = vol.Schema({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_ICON, default=None): vol.Any(None, cv.icon),
|
||||
vol.Optional('track', default=False): cv.boolean,
|
||||
vol.Optional(CONF_MAC, default=None):
|
||||
vol.Any(None, vol.All(cv.string, vol.Upper)),
|
||||
vol.Optional(CONF_AWAY_HIDE, default=DEFAULT_AWAY_HIDE): cv.boolean,
|
||||
vol.Optional('gravatar', default=None): vol.Any(None, cv.string),
|
||||
vol.Optional('picture', default=None): vol.Any(None, cv.string),
|
||||
vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All(
|
||||
cv.time_period, cv.positive_timedelta),
|
||||
})
|
||||
try:
|
||||
result = []
|
||||
try:
|
||||
devices = await hass.async_add_job(
|
||||
load_yaml_config_file, path)
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error("Unable to load %s: %s", path, str(err))
|
||||
return []
|
||||
|
||||
for dev_id, device in devices.items():
|
||||
# Deprecated option. We just ignore it to avoid breaking change
|
||||
device.pop('vendor', None)
|
||||
try:
|
||||
device = dev_schema(device)
|
||||
device['dev_id'] = cv.slugify(dev_id)
|
||||
except vol.Invalid as exp:
|
||||
async_log_exception(exp, dev_id, devices, hass)
|
||||
else:
|
||||
result.append(Device(hass, **device))
|
||||
return result
|
||||
except (HomeAssistantError, FileNotFoundError):
|
||||
# When YAML file could not be loaded/did not contain a dict
|
||||
return []
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
|
||||
scanner: Any, async_see_device: Callable,
|
||||
platform: str):
|
||||
"""Set up the connect scanner-based platform to device tracker.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
||||
update_lock = asyncio.Lock(loop=hass.loop)
|
||||
scanner.hass = hass
|
||||
|
||||
# Initial scan of each mac we also tell about host name for config
|
||||
seen = set() # type: Any
|
||||
|
||||
async def async_device_tracker_scan(now: dt_util.dt.datetime):
|
||||
"""Handle interval matches."""
|
||||
if update_lock.locked():
|
||||
_LOGGER.warning(
|
||||
"Updating device list from %s took longer than the scheduled "
|
||||
"scan interval %s", platform, interval)
|
||||
return
|
||||
|
||||
async with update_lock:
|
||||
found_devices = await scanner.async_scan_devices()
|
||||
|
||||
for mac in found_devices:
|
||||
if mac in seen:
|
||||
host_name = None
|
||||
else:
|
||||
host_name = await scanner.async_get_device_name(mac)
|
||||
seen.add(mac)
|
||||
|
||||
try:
|
||||
extra_attributes = \
|
||||
await scanner.async_get_extra_attributes(mac)
|
||||
except NotImplementedError:
|
||||
extra_attributes = dict()
|
||||
|
||||
kwargs = {
|
||||
'mac': mac,
|
||||
'host_name': host_name,
|
||||
'source_type': SOURCE_TYPE_ROUTER,
|
||||
'attributes': {
|
||||
'scanner': scanner.__class__.__name__,
|
||||
**extra_attributes
|
||||
}
|
||||
}
|
||||
|
||||
zone_home = hass.states.get(zone.ENTITY_ID_HOME)
|
||||
if zone_home:
|
||||
kwargs['gps'] = [zone_home.attributes[ATTR_LATITUDE],
|
||||
zone_home.attributes[ATTR_LONGITUDE]]
|
||||
kwargs['gps_accuracy'] = 0
|
||||
|
||||
hass.async_create_task(async_see_device(**kwargs))
|
||||
|
||||
async_track_time_interval(hass, async_device_tracker_scan, interval)
|
||||
hass.async_create_task(async_device_tracker_scan(None))
|
||||
|
||||
|
||||
def update_config(path: str, dev_id: str, device: Device):
|
||||
"""Add device to YAML configuration file."""
|
||||
with open(path, 'a') as out:
|
||||
device = {device.dev_id: {
|
||||
ATTR_NAME: device.name,
|
||||
ATTR_MAC: device.mac,
|
||||
ATTR_ICON: device.icon,
|
||||
'picture': device.config_picture,
|
||||
'track': device.track,
|
||||
CONF_AWAY_HIDE: device.away_hide,
|
||||
}}
|
||||
out.write('\n')
|
||||
out.write(dump(device))
|
||||
|
||||
|
||||
def get_gravatar_for_email(email: str):
|
||||
"""Return an 80px Gravatar for the given email address.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
import hashlib
|
||||
url = 'https://www.gravatar.com/avatar/{}.jpg?s=80&d=wavatar'
|
||||
return url.format(hashlib.md5(email.encode('utf-8').lower()).hexdigest())
|
||||
|
114
homeassistant/components/device_tracker/config_entry.py
Normal file
114
homeassistant/components/device_tracker/config_entry.py
Normal 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
|
40
homeassistant/components/device_tracker/const.py
Normal file
40
homeassistant/components/device_tracker/const.py
Normal 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'
|
526
homeassistant/components/device_tracker/legacy.py
Normal file
526
homeassistant/components/device_tracker/legacy.py
Normal 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())
|
184
homeassistant/components/device_tracker/setup.py
Normal file
184
homeassistant/components/device_tracker/setup.py
Normal 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))
|
@ -8,9 +8,10 @@ from homeassistant.const import CONF_WEBHOOK_ID
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import intent, template, config_entry_flow
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
from .const import DOMAIN
|
||||
|
||||
DOMAIN = 'dialogflow'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SOURCE = "Home Assistant Dialogflow"
|
||||
|
||||
@ -83,16 +84,6 @@ async def async_unload_entry(hass, entry):
|
||||
async_remove_entry = config_entry_flow.webhook_async_remove_entry
|
||||
|
||||
|
||||
config_entry_flow.register_webhook_flow(
|
||||
DOMAIN,
|
||||
'Dialogflow Webhook',
|
||||
{
|
||||
'dialogflow_url': 'https://dialogflow.com/docs/fulfillment#webhook',
|
||||
'docs_url': 'https://www.home-assistant.io/components/dialogflow/'
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def dialogflow_error_response(message, error):
|
||||
"""Return a response saying the error message."""
|
||||
dialogflow_response = DialogflowResponse(message['result']['parameters'])
|
||||
|
13
homeassistant/components/dialogflow/config_flow.py
Normal file
13
homeassistant/components/dialogflow/config_flow.py
Normal 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/'
|
||||
}
|
||||
)
|
3
homeassistant/components/dialogflow/const.py
Normal file
3
homeassistant/components/dialogflow/const.py
Normal file
@ -0,0 +1,3 @@
|
||||
"""Const for DialogFlow."""
|
||||
|
||||
DOMAIN = "dialogflow"
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"domain": "dialogflow",
|
||||
"name": "Dialogflow",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/dialogflow",
|
||||
"requirements": [],
|
||||
"dependencies": [
|
||||
|
@ -46,7 +46,7 @@ class DiscordNotificationService(BaseNotificationService):
|
||||
import discord
|
||||
|
||||
discord.VoiceClient.warn_nacl = False
|
||||
discord_bot = discord.Client(loop=self.hass.loop)
|
||||
discord_bot = discord.Client()
|
||||
images = None
|
||||
|
||||
if ATTR_TARGET not in kwargs:
|
||||
|
@ -24,19 +24,14 @@ DOMAIN = 'discovery'
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=300)
|
||||
SERVICE_APPLE_TV = 'apple_tv'
|
||||
SERVICE_AXIS = 'axis'
|
||||
SERVICE_DAIKIN = 'daikin'
|
||||
SERVICE_DECONZ = 'deconz'
|
||||
SERVICE_DLNA_DMR = 'dlna_dmr'
|
||||
SERVICE_ENIGMA2 = 'enigma2'
|
||||
SERVICE_FREEBOX = 'freebox'
|
||||
SERVICE_HASS_IOS_APP = 'hass_ios'
|
||||
SERVICE_HASSIO = 'hassio'
|
||||
SERVICE_HOMEKIT = 'homekit'
|
||||
SERVICE_HEOS = 'heos'
|
||||
SERVICE_HUE = 'philips_hue'
|
||||
SERVICE_IGD = 'igd'
|
||||
SERVICE_IKEA_TRADFRI = 'ikea_tradfri'
|
||||
SERVICE_KONNECTED = 'konnected'
|
||||
SERVICE_MOBILE_APP = 'hass_mobile_app'
|
||||
SERVICE_NETGEAR = 'netgear_router'
|
||||
@ -51,15 +46,10 @@ SERVICE_WINK = 'wink'
|
||||
SERVICE_XIAOMI_GW = 'xiaomi_gw'
|
||||
|
||||
CONFIG_ENTRY_HANDLERS = {
|
||||
SERVICE_AXIS: 'axis',
|
||||
SERVICE_DAIKIN: 'daikin',
|
||||
SERVICE_DECONZ: 'deconz',
|
||||
'esphome': 'esphome',
|
||||
'google_cast': 'cast',
|
||||
SERVICE_HEOS: 'heos',
|
||||
SERVICE_HUE: 'hue',
|
||||
SERVICE_TELLDUSLIVE: 'tellduslive',
|
||||
SERVICE_IKEA_TRADFRI: 'tradfri',
|
||||
'sonos': 'sonos',
|
||||
SERVICE_IGD: 'upnp',
|
||||
}
|
||||
@ -101,12 +91,22 @@ SERVICE_HANDLERS = {
|
||||
}
|
||||
|
||||
OPTIONAL_SERVICE_HANDLERS = {
|
||||
SERVICE_HOMEKIT: ('homekit_controller', None),
|
||||
SERVICE_DLNA_DMR: ('media_player', 'dlna_dmr'),
|
||||
}
|
||||
|
||||
DEFAULT_ENABLED = list(CONFIG_ENTRY_HANDLERS) + list(SERVICE_HANDLERS)
|
||||
DEFAULT_DISABLED = list(OPTIONAL_SERVICE_HANDLERS)
|
||||
MIGRATED_SERVICE_HANDLERS = {
|
||||
'axis': None,
|
||||
'deconz': None,
|
||||
'esphome': None,
|
||||
'ikea_tradfri': None,
|
||||
'homekit': None,
|
||||
'philips_hue': None
|
||||
}
|
||||
|
||||
DEFAULT_ENABLED = list(CONFIG_ENTRY_HANDLERS) + list(SERVICE_HANDLERS) + \
|
||||
list(MIGRATED_SERVICE_HANDLERS)
|
||||
DEFAULT_DISABLED = list(OPTIONAL_SERVICE_HANDLERS) + \
|
||||
list(MIGRATED_SERVICE_HANDLERS)
|
||||
|
||||
CONF_IGNORE = 'ignore'
|
||||
CONF_ENABLE = 'enable'
|
||||
@ -153,6 +153,9 @@ async def async_setup(hass, config):
|
||||
|
||||
async def new_service_found(service, info):
|
||||
"""Handle a new service if one is found."""
|
||||
if service in MIGRATED_SERVICE_HANDLERS:
|
||||
return
|
||||
|
||||
if service in ignored_platforms:
|
||||
logger.info("Ignoring service: %s %s", service, info)
|
||||
return
|
||||
|
@ -103,7 +103,6 @@ async def async_start_event_handler(
|
||||
requester,
|
||||
listen_port=server_port,
|
||||
listen_host=server_host,
|
||||
loop=hass.loop,
|
||||
callback_url=callback_url_override)
|
||||
await server.start_server()
|
||||
_LOGGER.info(
|
||||
|
@ -63,7 +63,7 @@ class WanIpSensor(Entity):
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
self.hostname = hostname
|
||||
self.resolver = aiodns.DNSResolver(loop=self.hass.loop)
|
||||
self.resolver = aiodns.DNSResolver()
|
||||
self.resolver.nameservers = [resolver]
|
||||
self.querytype = 'AAAA' if ipv6 else 'A'
|
||||
self._state = None
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user