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