From 1061c369f1665966cc9244b3e283c3b255896f7b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 5 Oct 2018 19:54:15 +0200 Subject: [PATCH 001/265] Bumped version to 0.81.0.dev0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 93ab0deeba2..e9fb301f0db 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 80 +MINOR_VERSION = 81 PATCH_VERSION = '0.dev0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) From 5e7d4a57a3b0bd402ab69e3c557cdbd99f18e5c4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 5 Oct 2018 20:21:09 +0200 Subject: [PATCH 002/265] Fix incorrect yaml in hangouts (#17169) --- homeassistant/components/hangouts/services.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/hangouts/services.yaml b/homeassistant/components/hangouts/services.yaml index ded324d2de9..d07f1d65688 100644 --- a/homeassistant/components/hangouts/services.yaml +++ b/homeassistant/components/hangouts/services.yaml @@ -12,5 +12,4 @@ send_message: example: '[{"text":"test", "is_bold": false, "is_italic": false, "is_strikethrough": false, "is_underline": false, "parse_str": false, "link_target": "http://google.com"}, ...]' data: description: Other options ['image_file' / 'image_url'] - example: '{ "image_file": "file" }' or '{ "image_url": "url" }' - + example: '{ "image_file": "file" } or { "image_url": "url" }' From 7598067b559838be6c8393dd36a2d50e69854950 Mon Sep 17 00:00:00 2001 From: kennedyshead Date: Fri, 5 Oct 2018 20:48:20 +0200 Subject: [PATCH 003/265] Adding myself as melissa owner (#17157) --- CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CODEOWNERS b/CODEOWNERS index 91d5fd67670..77853434c6c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -110,6 +110,8 @@ homeassistant/components/konnected.py @heythisisnate homeassistant/components/*/konnected.py @heythisisnate homeassistant/components/matrix.py @tinloaf homeassistant/components/*/matrix.py @tinloaf +homeassistant/components/melissa.py @kennedyshead +homeassistant/components/*/melissa.py @kennedyshead homeassistant/components/openuv/* @bachya homeassistant/components/*/openuv.py @bachya homeassistant/components/qwikswitch.py @kellerza From 13106a9b55c0dddb87d5a482ca9a1904911158b1 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 5 Oct 2018 21:45:14 +0200 Subject: [PATCH 004/265] Update core, add myself and introduce grouping --- CODEOWNERS | 114 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 103 insertions(+), 11 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 77853434c6c..2799ae326c4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -2,48 +2,56 @@ # when the code that they own is touched. # https://github.com/blog/2392-introducing-code-owners +# Home Assistant Core setup.py @home-assistant/core homeassistant/*.py @home-assistant/core homeassistant/helpers/* @home-assistant/core homeassistant/util/* @home-assistant/core homeassistant/components/api.py @home-assistant/core +homeassistant/components/auth/* @home-assistant/core homeassistant/components/automation/* @home-assistant/core +homeassistant/components/cloud/* @home-assistant/core +homeassistant/components/config/* @home-assistant/core homeassistant/components/configurator.py @home-assistant/core -homeassistant/components/group.py @home-assistant/core +homeassistant/components/conversation/* @home-assistant/core +homeassistant/components/frontend/* @home-assistant/core +homeassistant/components/group/* @home-assistant/core homeassistant/components/history.py @home-assistant/core homeassistant/components/http/* @home-assistant/core homeassistant/components/input_*.py @home-assistant/core homeassistant/components/introduction.py @home-assistant/core homeassistant/components/logger.py @home-assistant/core +homeassistant/components/lovelace/* @home-assistant/core homeassistant/components/mqtt/* @home-assistant/core homeassistant/components/panel_custom.py @home-assistant/core homeassistant/components/panel_iframe.py @home-assistant/core -homeassistant/components/persistent_notification.py @home-assistant/core +homeassistant/components/onboarding/* @home-assistant/core +homeassistant/components/persistent_notification/* @home-assistant/core homeassistant/components/scene/__init__.py @home-assistant/core homeassistant/components/scene/hass.py @home-assistant/core homeassistant/components/script.py @home-assistant/core homeassistant/components/shell_command.py @home-assistant/core homeassistant/components/sun.py @home-assistant/core homeassistant/components/updater.py @home-assistant/core -homeassistant/components/weblink.py @home-assistant/core +homeassistant/components/weblink/* @home-assistant/core homeassistant/components/websocket_api.py @home-assistant/core -homeassistant/components/zone.py @home-assistant/core +homeassistant/components/zone/* @home-assistant/core -# HomeAssistant developer Teams +# Home Assistant Developer Teams Dockerfile @home-assistant/docker virtualization/Docker/* @home-assistant/docker homeassistant/components/zwave/* @home-assistant/z-wave homeassistant/components/*/zwave.py @home-assistant/z-wave -homeassistant/components/hassio.py @home-assistant/hassio +homeassistant/components/hassio/* @home-assistant/hassio -# Individual components +# Individual platforms homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt homeassistant/components/alarm_control_panel/manual_mqtt.py @colinodell homeassistant/components/alarm_control_panel/simplisafe.py @bachya homeassistant/components/binary_sensor/hikvision.py @mezz64 -homeassistant/components/bmw_connected_drive.py @ChristianKuehnel +homeassistant/components/binary_sensor/threshold.py @fabaff homeassistant/components/camera/yi.py @bachya homeassistant/components/climate/ephember.py @ttroy50 homeassistant/components/climate/eq3btsmart.py @rytilahti @@ -54,6 +62,7 @@ homeassistant/components/device_tracker/automatic.py @armills homeassistant/components/device_tracker/huawei_router.py @abmantis homeassistant/components/device_tracker/tile.py @bachya homeassistant/components/history_graph.py @andrey-git +homeassistant/components/influx.py @fabaff homeassistant/components/light/lifx.py @amelchio homeassistant/components/light/lifx_legacy.py @amelchio homeassistant/components/light/tplink.py @rytilahti @@ -65,76 +74,159 @@ homeassistant/components/media_player/kodi.py @armills homeassistant/components/media_player/liveboxplaytv.py @pschmitt homeassistant/components/media_player/mediaroom.py @dgomes homeassistant/components/media_player/monoprice.py @etsinko +homeassistant/components/media_player/mpd.py @fabaff homeassistant/components/media_player/sonos.py @amelchio homeassistant/components/media_player/xiaomi_tv.py @fattdev homeassistant/components/media_player/yamaha_musiccast.py @jalmeroth +homeassistant/components/no_ip.py @fabaff +homeassistant/components/notify/file.py @fabaff +homeassistant/components/notify/flock.py @fabaff +homeassistant/components/notify/instapush.py @fabaff +homeassistant/components/notify/mastodon.py @fabaff +homeassistant/components/notify/smtp.py @fabaff +homeassistant/components/notify/syslog.py @fabaff +homeassistant/components/notify/xmpp.py @fabaff homeassistant/components/plant.py @ChristianKuehnel homeassistant/components/scene/lifx_cloud.py @amelchio homeassistant/components/sensor/airvisual.py @bachya +homeassistant/components/sensor/alpha_vantage.py @fabaff +homeassistant/components/sensor/bitcoin.py @fabaff +homeassistant/components/sensor/cpuspeed.py @fabaff +homeassistant/components/sensor/cups.py @fabaff +homeassistant/components/sensor/darksky.py @fabaff +homeassistant/components/sensor/file.py @fabaff homeassistant/components/sensor/filter.py @dgomes +homeassistant/components/sensor/fixer.py @fabaff homeassistant/components/sensor/gearbest.py @HerrHofrat +homeassistant/components/sensor/gitter.py @fabaff +homeassistant/components/sensor/glances.py @fabaff +homeassistant/components/sensor/gpsd.py @fabaff homeassistant/components/sensor/irish_rail_transport.py @ttroy50 homeassistant/components/sensor/jewish_calendar.py @tsvi +homeassistant/components/sensor/linux_battery.py @fabaff +homeassistant/components/sensor/luftdaten.py @fabaff homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel +homeassistant/components/sensor/min_max.py @fabaff +homeassistant/components/sensor/moon.py @fabaff +homeassistant/components/sensor/netdata.py @fabaff homeassistant/components/sensor/nsw_fuel_station.py @nickw444 +homeassistant/components/sensor/pi_hole.py @fabaff homeassistant/components/sensor/pollen.py @bachya +homeassistant/components/sensor/pvoutput.py @fabaff homeassistant/components/sensor/qnap.py @colinodell +homeassistant/components/sensor/serial.py @fabaff homeassistant/components/sensor/sma.py @kellerza homeassistant/components/sensor/sql.py @dgomes +homeassistant/components/sensor/swiss*.py @fabaff homeassistant/components/sensor/sytadin.py @gautric +homeassistant/components/sensor/time_data.py @fabaff +homeassistant/components/sensor/version.py @fabaff homeassistant/components/sensor/waqi.py @andrey-git +homeassistant/components/sensor/worldclock.py @fabaff +homeassistant/components/shiftr.py @fabaff +homeassistant/components/spaceapi.py @fabaff homeassistant/components/switch/tplink.py @rytilahti homeassistant/components/vacuum/roomba.py @pschmitt +homeassistant/components/weather/__init__.py @fabaff +homeassistant/components/weather/darksky.py @fabaff +homeassistant/components/weather/demo.py @fabaff +homeassistant/components/weather/openweathermap.py @fabaff homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi +# A +homeassistant/components/arduino.py @fabaff +homeassistant/components/*/arduino.py @fabaff +homeassistant/components/*/arest.py @fabaff homeassistant/components/*/axis.py @kane610 + +# B homeassistant/components/blink/* @fronzbot homeassistant/components/*/blink.py @fronzbot +homeassistant/components/bmw_connected_drive.py @ChristianKuehnel homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel homeassistant/components/*/broadlink.py @danielhiversen + +# C +homeassistant/components/counter/* @fabaff + +# D homeassistant/components/*/deconz.py @kane610 +homeassistant/components/digital_ocean.py @fabaff +homeassistant/components/*/digital_ocean.py @fabaff +homeassistant/components/dweet.py @fabaff +homeassistant/components/*/dweet.py @fabaff + +# E homeassistant/components/ecovacs.py @OverloadUT homeassistant/components/*/ecovacs.py @OverloadUT -homeassistant/components/edp_redy.py @abmantis homeassistant/components/*/edp_redy.py @abmantis +homeassistant/components/edp_redy.py @abmantis homeassistant/components/eight_sleep.py @mezz64 homeassistant/components/*/eight_sleep.py @mezz64 + +# H homeassistant/components/hive.py @Rendili @KJonline homeassistant/components/*/hive.py @Rendili @KJonline homeassistant/components/homekit/* @cdce8p homeassistant/components/huawei_lte.py @scop homeassistant/components/*/huawei_lte.py @scop + +# K homeassistant/components/knx.py @Julius2342 homeassistant/components/*/knx.py @Julius2342 homeassistant/components/konnected.py @heythisisnate homeassistant/components/*/konnected.py @heythisisnate + +# M homeassistant/components/matrix.py @tinloaf homeassistant/components/*/matrix.py @tinloaf homeassistant/components/melissa.py @kennedyshead homeassistant/components/*/melissa.py @kennedyshead +homeassistant/components/*/mystrom.py @fabaff + +# U homeassistant/components/openuv/* @bachya homeassistant/components/*/openuv.py @bachya + +# Q homeassistant/components/qwikswitch.py @kellerza homeassistant/components/*/qwikswitch.py @kellerza + +# R homeassistant/components/rainmachine/* @bachya homeassistant/components/*/rainmachine.py @bachya +homeassistant/components/*/random.py @fabaff homeassistant/components/*/rfxtrx.py @danielhiversen + +# T homeassistant/components/tahoma.py @philklei homeassistant/components/*/tahoma.py @philklei -homeassistant/components/tesla.py @zabuldon -homeassistant/components/*/tesla.py @zabuldon homeassistant/components/tellduslive.py @molobrakos @fredrike homeassistant/components/*/tellduslive.py @molobrakos @fredrike +homeassistant/components/tesla.py @zabuldon +homeassistant/components/*/tesla.py @zabuldon +homeassistant/components/thethingsnetwork.py @fabaff +homeassistant/components/*/thethingsnetwork.py @fabaff homeassistant/components/tibber/* @danielhiversen homeassistant/components/*/tibber.py @danielhiversen +homeassistant/components/tradfri/* @ggravlingen homeassistant/components/*/tradfri.py @ggravlingen + +# U homeassistant/components/upcloud.py @scop homeassistant/components/*/upcloud.py @scop + +# V homeassistant/components/velux.py @Julius2342 homeassistant/components/*/velux.py @Julius2342 + +# X homeassistant/components/*/xiaomi_aqara.py @danielhiversen @syssi homeassistant/components/*/xiaomi_miio.py @rytilahti @syssi + +# Z homeassistant/components/zoneminder.py @rohankapoorcom homeassistant/components/*/zoneminder.py @rohankapoorcom +# Other code homeassistant/scripts/check_config.py @kellerza From bed1b96f5aca64a6deee0cc1eac4087e103e4e42 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 5 Oct 2018 21:46:26 +0200 Subject: [PATCH 005/265] Revert "Update core, add myself and introduce grouping" This reverts commit 13106a9b55c0dddb87d5a482ca9a1904911158b1. --- CODEOWNERS | 114 ++++++----------------------------------------------- 1 file changed, 11 insertions(+), 103 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 2799ae326c4..77853434c6c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -2,56 +2,48 @@ # when the code that they own is touched. # https://github.com/blog/2392-introducing-code-owners -# Home Assistant Core setup.py @home-assistant/core homeassistant/*.py @home-assistant/core homeassistant/helpers/* @home-assistant/core homeassistant/util/* @home-assistant/core homeassistant/components/api.py @home-assistant/core -homeassistant/components/auth/* @home-assistant/core homeassistant/components/automation/* @home-assistant/core -homeassistant/components/cloud/* @home-assistant/core -homeassistant/components/config/* @home-assistant/core homeassistant/components/configurator.py @home-assistant/core -homeassistant/components/conversation/* @home-assistant/core -homeassistant/components/frontend/* @home-assistant/core -homeassistant/components/group/* @home-assistant/core +homeassistant/components/group.py @home-assistant/core homeassistant/components/history.py @home-assistant/core homeassistant/components/http/* @home-assistant/core homeassistant/components/input_*.py @home-assistant/core homeassistant/components/introduction.py @home-assistant/core homeassistant/components/logger.py @home-assistant/core -homeassistant/components/lovelace/* @home-assistant/core homeassistant/components/mqtt/* @home-assistant/core homeassistant/components/panel_custom.py @home-assistant/core homeassistant/components/panel_iframe.py @home-assistant/core -homeassistant/components/onboarding/* @home-assistant/core -homeassistant/components/persistent_notification/* @home-assistant/core +homeassistant/components/persistent_notification.py @home-assistant/core homeassistant/components/scene/__init__.py @home-assistant/core homeassistant/components/scene/hass.py @home-assistant/core homeassistant/components/script.py @home-assistant/core homeassistant/components/shell_command.py @home-assistant/core homeassistant/components/sun.py @home-assistant/core homeassistant/components/updater.py @home-assistant/core -homeassistant/components/weblink/* @home-assistant/core +homeassistant/components/weblink.py @home-assistant/core homeassistant/components/websocket_api.py @home-assistant/core -homeassistant/components/zone/* @home-assistant/core +homeassistant/components/zone.py @home-assistant/core -# Home Assistant Developer Teams +# HomeAssistant developer Teams Dockerfile @home-assistant/docker virtualization/Docker/* @home-assistant/docker homeassistant/components/zwave/* @home-assistant/z-wave homeassistant/components/*/zwave.py @home-assistant/z-wave -homeassistant/components/hassio/* @home-assistant/hassio +homeassistant/components/hassio.py @home-assistant/hassio -# Individual platforms +# Individual components homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt homeassistant/components/alarm_control_panel/manual_mqtt.py @colinodell homeassistant/components/alarm_control_panel/simplisafe.py @bachya homeassistant/components/binary_sensor/hikvision.py @mezz64 -homeassistant/components/binary_sensor/threshold.py @fabaff +homeassistant/components/bmw_connected_drive.py @ChristianKuehnel homeassistant/components/camera/yi.py @bachya homeassistant/components/climate/ephember.py @ttroy50 homeassistant/components/climate/eq3btsmart.py @rytilahti @@ -62,7 +54,6 @@ homeassistant/components/device_tracker/automatic.py @armills homeassistant/components/device_tracker/huawei_router.py @abmantis homeassistant/components/device_tracker/tile.py @bachya homeassistant/components/history_graph.py @andrey-git -homeassistant/components/influx.py @fabaff homeassistant/components/light/lifx.py @amelchio homeassistant/components/light/lifx_legacy.py @amelchio homeassistant/components/light/tplink.py @rytilahti @@ -74,159 +65,76 @@ homeassistant/components/media_player/kodi.py @armills homeassistant/components/media_player/liveboxplaytv.py @pschmitt homeassistant/components/media_player/mediaroom.py @dgomes homeassistant/components/media_player/monoprice.py @etsinko -homeassistant/components/media_player/mpd.py @fabaff homeassistant/components/media_player/sonos.py @amelchio homeassistant/components/media_player/xiaomi_tv.py @fattdev homeassistant/components/media_player/yamaha_musiccast.py @jalmeroth -homeassistant/components/no_ip.py @fabaff -homeassistant/components/notify/file.py @fabaff -homeassistant/components/notify/flock.py @fabaff -homeassistant/components/notify/instapush.py @fabaff -homeassistant/components/notify/mastodon.py @fabaff -homeassistant/components/notify/smtp.py @fabaff -homeassistant/components/notify/syslog.py @fabaff -homeassistant/components/notify/xmpp.py @fabaff homeassistant/components/plant.py @ChristianKuehnel homeassistant/components/scene/lifx_cloud.py @amelchio homeassistant/components/sensor/airvisual.py @bachya -homeassistant/components/sensor/alpha_vantage.py @fabaff -homeassistant/components/sensor/bitcoin.py @fabaff -homeassistant/components/sensor/cpuspeed.py @fabaff -homeassistant/components/sensor/cups.py @fabaff -homeassistant/components/sensor/darksky.py @fabaff -homeassistant/components/sensor/file.py @fabaff homeassistant/components/sensor/filter.py @dgomes -homeassistant/components/sensor/fixer.py @fabaff homeassistant/components/sensor/gearbest.py @HerrHofrat -homeassistant/components/sensor/gitter.py @fabaff -homeassistant/components/sensor/glances.py @fabaff -homeassistant/components/sensor/gpsd.py @fabaff homeassistant/components/sensor/irish_rail_transport.py @ttroy50 homeassistant/components/sensor/jewish_calendar.py @tsvi -homeassistant/components/sensor/linux_battery.py @fabaff -homeassistant/components/sensor/luftdaten.py @fabaff homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel -homeassistant/components/sensor/min_max.py @fabaff -homeassistant/components/sensor/moon.py @fabaff -homeassistant/components/sensor/netdata.py @fabaff homeassistant/components/sensor/nsw_fuel_station.py @nickw444 -homeassistant/components/sensor/pi_hole.py @fabaff homeassistant/components/sensor/pollen.py @bachya -homeassistant/components/sensor/pvoutput.py @fabaff homeassistant/components/sensor/qnap.py @colinodell -homeassistant/components/sensor/serial.py @fabaff homeassistant/components/sensor/sma.py @kellerza homeassistant/components/sensor/sql.py @dgomes -homeassistant/components/sensor/swiss*.py @fabaff homeassistant/components/sensor/sytadin.py @gautric -homeassistant/components/sensor/time_data.py @fabaff -homeassistant/components/sensor/version.py @fabaff homeassistant/components/sensor/waqi.py @andrey-git -homeassistant/components/sensor/worldclock.py @fabaff -homeassistant/components/shiftr.py @fabaff -homeassistant/components/spaceapi.py @fabaff homeassistant/components/switch/tplink.py @rytilahti homeassistant/components/vacuum/roomba.py @pschmitt -homeassistant/components/weather/__init__.py @fabaff -homeassistant/components/weather/darksky.py @fabaff -homeassistant/components/weather/demo.py @fabaff -homeassistant/components/weather/openweathermap.py @fabaff homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi -# A -homeassistant/components/arduino.py @fabaff -homeassistant/components/*/arduino.py @fabaff -homeassistant/components/*/arest.py @fabaff homeassistant/components/*/axis.py @kane610 - -# B homeassistant/components/blink/* @fronzbot homeassistant/components/*/blink.py @fronzbot -homeassistant/components/bmw_connected_drive.py @ChristianKuehnel homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel homeassistant/components/*/broadlink.py @danielhiversen - -# C -homeassistant/components/counter/* @fabaff - -# D homeassistant/components/*/deconz.py @kane610 -homeassistant/components/digital_ocean.py @fabaff -homeassistant/components/*/digital_ocean.py @fabaff -homeassistant/components/dweet.py @fabaff -homeassistant/components/*/dweet.py @fabaff - -# E homeassistant/components/ecovacs.py @OverloadUT homeassistant/components/*/ecovacs.py @OverloadUT -homeassistant/components/*/edp_redy.py @abmantis homeassistant/components/edp_redy.py @abmantis +homeassistant/components/*/edp_redy.py @abmantis homeassistant/components/eight_sleep.py @mezz64 homeassistant/components/*/eight_sleep.py @mezz64 - -# H homeassistant/components/hive.py @Rendili @KJonline homeassistant/components/*/hive.py @Rendili @KJonline homeassistant/components/homekit/* @cdce8p homeassistant/components/huawei_lte.py @scop homeassistant/components/*/huawei_lte.py @scop - -# K homeassistant/components/knx.py @Julius2342 homeassistant/components/*/knx.py @Julius2342 homeassistant/components/konnected.py @heythisisnate homeassistant/components/*/konnected.py @heythisisnate - -# M homeassistant/components/matrix.py @tinloaf homeassistant/components/*/matrix.py @tinloaf homeassistant/components/melissa.py @kennedyshead homeassistant/components/*/melissa.py @kennedyshead -homeassistant/components/*/mystrom.py @fabaff - -# U homeassistant/components/openuv/* @bachya homeassistant/components/*/openuv.py @bachya - -# Q homeassistant/components/qwikswitch.py @kellerza homeassistant/components/*/qwikswitch.py @kellerza - -# R homeassistant/components/rainmachine/* @bachya homeassistant/components/*/rainmachine.py @bachya -homeassistant/components/*/random.py @fabaff homeassistant/components/*/rfxtrx.py @danielhiversen - -# T homeassistant/components/tahoma.py @philklei homeassistant/components/*/tahoma.py @philklei -homeassistant/components/tellduslive.py @molobrakos @fredrike -homeassistant/components/*/tellduslive.py @molobrakos @fredrike homeassistant/components/tesla.py @zabuldon homeassistant/components/*/tesla.py @zabuldon -homeassistant/components/thethingsnetwork.py @fabaff -homeassistant/components/*/thethingsnetwork.py @fabaff +homeassistant/components/tellduslive.py @molobrakos @fredrike +homeassistant/components/*/tellduslive.py @molobrakos @fredrike homeassistant/components/tibber/* @danielhiversen homeassistant/components/*/tibber.py @danielhiversen -homeassistant/components/tradfri/* @ggravlingen homeassistant/components/*/tradfri.py @ggravlingen - -# U homeassistant/components/upcloud.py @scop homeassistant/components/*/upcloud.py @scop - -# V homeassistant/components/velux.py @Julius2342 homeassistant/components/*/velux.py @Julius2342 - -# X homeassistant/components/*/xiaomi_aqara.py @danielhiversen @syssi homeassistant/components/*/xiaomi_miio.py @rytilahti @syssi - -# Z homeassistant/components/zoneminder.py @rohankapoorcom homeassistant/components/*/zoneminder.py @rohankapoorcom -# Other code homeassistant/scripts/check_config.py @kellerza From a66db5935901c96549423352fc8818c20c49dc6e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 5 Oct 2018 23:07:27 +0200 Subject: [PATCH 006/265] Fix data used for logbook (#17172) * Fix data used for logbook * Lint --- homeassistant/components/logbook.py | 7 ++++--- tests/components/test_logbook.py | 12 ++++++------ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index e282a133f1d..9e66c8d3aca 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -265,16 +265,17 @@ def humanify(hass, events): elif event.event_type == EVENT_ALEXA_SMART_HOME: data = event.data - entity_id = data.get('entity_id') + entity_id = data['request'].get('entity_id') if entity_id: state = hass.states.get(entity_id) name = state.name if state else entity_id message = "send command {}/{} for {}".format( - data['namespace'], data['name'], name) + data['request']['namespace'], + data['request']['name'], name) else: message = "send command {}/{}".format( - data['namespace'], data['name']) + data['request']['namespace'], data['request']['name']) yield { 'when': event.time_fired, diff --git a/tests/components/test_logbook.py b/tests/components/test_logbook.py index 9ccb8f58a87..8e7c2299731 100644 --- a/tests/components/test_logbook.py +++ b/tests/components/test_logbook.py @@ -565,20 +565,20 @@ async def test_humanify_alexa_event(hass): }) results = list(logbook.humanify(hass, [ - ha.Event(EVENT_ALEXA_SMART_HOME, { + ha.Event(EVENT_ALEXA_SMART_HOME, {'request': { 'namespace': 'Alexa.Discovery', 'name': 'Discover', - }), - ha.Event(EVENT_ALEXA_SMART_HOME, { + }}), + ha.Event(EVENT_ALEXA_SMART_HOME, {'request': { 'namespace': 'Alexa.PowerController', 'name': 'TurnOn', 'entity_id': 'light.kitchen' - }), - ha.Event(EVENT_ALEXA_SMART_HOME, { + }}), + ha.Event(EVENT_ALEXA_SMART_HOME, {'request': { 'namespace': 'Alexa.PowerController', 'name': 'TurnOn', 'entity_id': 'light.non_existing' - }), + }}), ])) From 07d90c6c55541ac270ca23b0e64d40dc60293b18 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Fri, 5 Oct 2018 23:09:55 +0200 Subject: [PATCH 007/265] Fix device_tracker service call & cleanup (#17173) * Bugfix group service - device_tracker * Cleanup --- homeassistant/components/alert.py | 6 ++-- .../components/device_sun_light_trigger.py | 20 ++++++----- .../components/device_tracker/__init__.py | 30 ++++++++-------- homeassistant/components/notify/telegram.py | 2 +- homeassistant/components/switch/flux.py | 36 ++++++++++--------- 5 files changed, 51 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/alert.py b/homeassistant/components/alert.py index 72689b30138..e224351f9db 100644 --- a/homeassistant/components/alert.py +++ b/homeassistant/components/alert.py @@ -10,6 +10,8 @@ import logging import voluptuous as vol +from homeassistant.components.notify import ( + ATTR_MESSAGE, DOMAIN as DOMAIN_NOTIFY) from homeassistant.const import ( CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, CONF_STATE, STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID) @@ -204,7 +206,7 @@ class Alert(ToggleEntity): self._send_done_message = True for target in self._notifiers: await self.hass.services.async_call( - 'notify', target, {'message': self._name}) + DOMAIN_NOTIFY, target, {ATTR_MESSAGE: self._name}) await self._schedule_notify() async def _notify_done_message(self, *args): @@ -213,7 +215,7 @@ class Alert(ToggleEntity): self._send_done_message = False for target in self._notifiers: await self.hass.services.async_call( - 'notify', target, {'message': self._done_message}) + DOMAIN_NOTIFY, target, {ATTR_MESSAGE: self._done_message}) async def async_turn_on(self, **kwargs): """Async Unacknowledge alert.""" diff --git a/homeassistant/components/device_sun_light_trigger.py b/homeassistant/components/device_sun_light_trigger.py index cd81b3a01ad..40a602056bf 100644 --- a/homeassistant/components/device_sun_light_trigger.py +++ b/homeassistant/components/device_sun_light_trigger.py @@ -11,9 +11,11 @@ import voluptuous as vol from homeassistant.core import callback import homeassistant.util.dt as dt_util +from homeassistant.components.light import ( + ATTR_PROFILE, ATTR_TRANSITION, DOMAIN as DOMAIN_LIGHT) from homeassistant.const import ( - SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_HOME, STATE_NOT_HOME) -from homeassistant.components.light import DOMAIN as DOMAIN_LIGHT + ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_HOME, + STATE_NOT_HOME) from homeassistant.helpers.event import ( async_track_point_in_utc_time, async_track_state_change) from homeassistant.helpers.sun import is_up, get_astral_event_next @@ -88,10 +90,10 @@ async def async_setup(hass, config): return hass.async_create_task( hass.services.async_call( - DOMAIN_LIGHT, SERVICE_TURN_ON, dict( - entity_id=light_id, - transition=LIGHT_TRANSITION_TIME.seconds, - profile=light_profile))) + DOMAIN_LIGHT, SERVICE_TURN_ON, { + ATTR_ENTITY_ID: light_id, + ATTR_TRANSITION: LIGHT_TRANSITION_TIME.seconds, + ATTR_PROFILE: light_profile})) def async_turn_on_factory(light_id): """Generate turn on callbacks as factory.""" @@ -144,7 +146,7 @@ async def async_setup(hass, config): hass.async_create_task( hass.services.async_call( DOMAIN_LIGHT, SERVICE_TURN_ON, - dict(entity_id=light_ids, profile=light_profile))) + {ATTR_ENTITY_ID: light_ids, ATTR_PROFILE: light_profile})) # Are we in the time span were we would turn on the lights # if someone would be home? @@ -160,7 +162,7 @@ async def async_setup(hass, config): hass.async_create_task( hass.services.async_call( DOMAIN_LIGHT, SERVICE_TURN_ON, - dict(entity_id=light_id))) + {ATTR_ENTITY_ID: light_id})) else: # If this light didn't happen to be turned on yet so @@ -184,7 +186,7 @@ async def async_setup(hass, config): "Everyone has left but there are lights on. Turning them off") hass.async_create_task( hass.services.async_call( - DOMAIN_LIGHT, SERVICE_TURN_OFF, dict(entity_id=light_ids))) + DOMAIN_LIGHT, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: light_ids})) async_track_state_change( hass, device_group, turn_off_lights_when_all_leave, diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index a9cc122a6f9..cbf32b4cd5a 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -15,7 +15,9 @@ from homeassistant.setup import async_prepare_setup_platform from homeassistant.core import callback from homeassistant.loader import bind_hass from homeassistant.components import group, zone -from homeassistant.components.group import DOMAIN as DOMAIN_GROUP, SERVICE_SET +from homeassistant.components.group import ( + ATTR_ADD_ENTITIES, ATTR_ENTITIES, ATTR_OBJECT_ID, ATTR_VISIBLE, + DOMAIN as DOMAIN_GROUP, SERVICE_SET) from homeassistant.components.zone.zone import async_active_zone from homeassistant.config import load_yaml_config_file, async_log_exception from homeassistant.exceptions import HomeAssistantError @@ -32,9 +34,9 @@ from homeassistant.util.yaml import dump from homeassistant.helpers.event import async_track_utc_time_change from homeassistant.const import ( - ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_NAME, CONF_MAC, - DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_ID, - CONF_ICON, ATTR_ICON, ATTR_NAME) + 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__) @@ -317,11 +319,11 @@ class DeviceTracker: if self.group and self.track_new: self.hass.async_create_task( self.hass.async_call( - DOMAIN_GROUP, SERVICE_SET, dict( - object_id=util.slugify(GROUP_NAME_ALL_DEVICES), - visible=False, - name=GROUP_NAME_ALL_DEVICES, - add=[device.entity_id]))) + 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, @@ -356,11 +358,11 @@ class DeviceTracker: self.hass.async_create_task( self.hass.services.async_call( - DOMAIN_GROUP, SERVICE_SET, dict( - object_id=util.slugify(GROUP_NAME_ALL_DEVICES), - visible=False, - name=GROUP_NAME_ALL_DEVICES, - entities=entity_ids))) + 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): diff --git a/homeassistant/components/notify/telegram.py b/homeassistant/components/notify/telegram.py index b012506acd9..1dff82fa2cd 100644 --- a/homeassistant/components/notify/telegram.py +++ b/homeassistant/components/notify/telegram.py @@ -47,7 +47,7 @@ class TelegramNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a user.""" - service_data = dict(target=kwargs.get(ATTR_TARGET, self._chat_id)) + service_data = {ATTR_TARGET: kwargs.get(ATTR_TARGET, self._chat_id)} if ATTR_TITLE in kwargs: service_data.update({ATTR_TITLE: kwargs.get(ATTR_TITLE)}) if message: diff --git a/homeassistant/components/switch/flux.py b/homeassistant/components/switch/flux.py index 793d6ab91d0..c541c37b5e7 100644 --- a/homeassistant/components/switch/flux.py +++ b/homeassistant/components/switch/flux.py @@ -13,10 +13,12 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( - is_on, DOMAIN as LIGHT_DOMAIN, VALID_TRANSITION, ATTR_TRANSITION) + is_on, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION, + ATTR_WHITE_VALUE, ATTR_XY_COLOR, DOMAIN as LIGHT_DOMAIN, VALID_TRANSITION) from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.const import ( - CONF_NAME, CONF_PLATFORM, CONF_LIGHTS, CONF_MODE, SERVICE_TURN_ON) + ATTR_ENTITY_ID, CONF_NAME, CONF_PLATFORM, CONF_LIGHTS, CONF_MODE, + SERVICE_TURN_ON) from homeassistant.helpers.event import track_time_change from homeassistant.helpers.sun import get_astral_event_date from homeassistant.util import slugify @@ -70,12 +72,12 @@ def set_lights_xy(hass, lights, x_val, y_val, brightness, transition): for light in lights: if is_on(hass, light): hass.services.call( - LIGHT_DOMAIN, SERVICE_TURN_ON, dict( - xy_color=[x_val, y_val], - brightness=brightness, - transition=transition, - white_value=brightness, - entity_id=light)) + LIGHT_DOMAIN, SERVICE_TURN_ON, { + ATTR_XY_COLOR: [x_val, y_val], + ATTR_BRIGHTNESS: brightness, + ATTR_TRANSITION: transition, + ATTR_WHITE_VALUE: brightness, + ATTR_ENTITY_ID: light}) def set_lights_temp(hass, lights, mired, brightness, transition): @@ -83,11 +85,11 @@ def set_lights_temp(hass, lights, mired, brightness, transition): for light in lights: if is_on(hass, light): hass.services.call( - LIGHT_DOMAIN, SERVICE_TURN_ON, dict( - color_temp=int(mired), - brightness=brightness, - transition=transition, - entity_id=light)) + LIGHT_DOMAIN, SERVICE_TURN_ON, { + ATTR_COLOR_TEMP: int(mired), + ATTR_BRIGHTNESS: brightness, + ATTR_TRANSITION: transition, + ATTR_ENTITY_ID: light}) def set_lights_rgb(hass, lights, rgb, transition): @@ -95,10 +97,10 @@ def set_lights_rgb(hass, lights, rgb, transition): for light in lights: if is_on(hass, light): hass.services.call( - LIGHT_DOMAIN, SERVICE_TURN_ON, dict( - rgb_color=rgb, - transition=transition, - entity_id=light)) + LIGHT_DOMAIN, SERVICE_TURN_ON, { + ATTR_RGB_COLOR: rgb, + ATTR_TRANSITION: transition, + ATTR_ENTITY_ID: light}) def setup_platform(hass, config, add_entities, discovery_info=None): From 169abe637cfc2e3f3f16c32defc83212af7dfc5d Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 6 Oct 2018 07:16:06 +0200 Subject: [PATCH 008/265] Update core, add myself and introduce grouping (#17175) --- CODEOWNERS | 114 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 103 insertions(+), 11 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 77853434c6c..2799ae326c4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -2,48 +2,56 @@ # when the code that they own is touched. # https://github.com/blog/2392-introducing-code-owners +# Home Assistant Core setup.py @home-assistant/core homeassistant/*.py @home-assistant/core homeassistant/helpers/* @home-assistant/core homeassistant/util/* @home-assistant/core homeassistant/components/api.py @home-assistant/core +homeassistant/components/auth/* @home-assistant/core homeassistant/components/automation/* @home-assistant/core +homeassistant/components/cloud/* @home-assistant/core +homeassistant/components/config/* @home-assistant/core homeassistant/components/configurator.py @home-assistant/core -homeassistant/components/group.py @home-assistant/core +homeassistant/components/conversation/* @home-assistant/core +homeassistant/components/frontend/* @home-assistant/core +homeassistant/components/group/* @home-assistant/core homeassistant/components/history.py @home-assistant/core homeassistant/components/http/* @home-assistant/core homeassistant/components/input_*.py @home-assistant/core homeassistant/components/introduction.py @home-assistant/core homeassistant/components/logger.py @home-assistant/core +homeassistant/components/lovelace/* @home-assistant/core homeassistant/components/mqtt/* @home-assistant/core homeassistant/components/panel_custom.py @home-assistant/core homeassistant/components/panel_iframe.py @home-assistant/core -homeassistant/components/persistent_notification.py @home-assistant/core +homeassistant/components/onboarding/* @home-assistant/core +homeassistant/components/persistent_notification/* @home-assistant/core homeassistant/components/scene/__init__.py @home-assistant/core homeassistant/components/scene/hass.py @home-assistant/core homeassistant/components/script.py @home-assistant/core homeassistant/components/shell_command.py @home-assistant/core homeassistant/components/sun.py @home-assistant/core homeassistant/components/updater.py @home-assistant/core -homeassistant/components/weblink.py @home-assistant/core +homeassistant/components/weblink/* @home-assistant/core homeassistant/components/websocket_api.py @home-assistant/core -homeassistant/components/zone.py @home-assistant/core +homeassistant/components/zone/* @home-assistant/core -# HomeAssistant developer Teams +# Home Assistant Developer Teams Dockerfile @home-assistant/docker virtualization/Docker/* @home-assistant/docker homeassistant/components/zwave/* @home-assistant/z-wave homeassistant/components/*/zwave.py @home-assistant/z-wave -homeassistant/components/hassio.py @home-assistant/hassio +homeassistant/components/hassio/* @home-assistant/hassio -# Individual components +# Individual platforms homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt homeassistant/components/alarm_control_panel/manual_mqtt.py @colinodell homeassistant/components/alarm_control_panel/simplisafe.py @bachya homeassistant/components/binary_sensor/hikvision.py @mezz64 -homeassistant/components/bmw_connected_drive.py @ChristianKuehnel +homeassistant/components/binary_sensor/threshold.py @fabaff homeassistant/components/camera/yi.py @bachya homeassistant/components/climate/ephember.py @ttroy50 homeassistant/components/climate/eq3btsmart.py @rytilahti @@ -54,6 +62,7 @@ homeassistant/components/device_tracker/automatic.py @armills homeassistant/components/device_tracker/huawei_router.py @abmantis homeassistant/components/device_tracker/tile.py @bachya homeassistant/components/history_graph.py @andrey-git +homeassistant/components/influx.py @fabaff homeassistant/components/light/lifx.py @amelchio homeassistant/components/light/lifx_legacy.py @amelchio homeassistant/components/light/tplink.py @rytilahti @@ -65,76 +74,159 @@ homeassistant/components/media_player/kodi.py @armills homeassistant/components/media_player/liveboxplaytv.py @pschmitt homeassistant/components/media_player/mediaroom.py @dgomes homeassistant/components/media_player/monoprice.py @etsinko +homeassistant/components/media_player/mpd.py @fabaff homeassistant/components/media_player/sonos.py @amelchio homeassistant/components/media_player/xiaomi_tv.py @fattdev homeassistant/components/media_player/yamaha_musiccast.py @jalmeroth +homeassistant/components/no_ip.py @fabaff +homeassistant/components/notify/file.py @fabaff +homeassistant/components/notify/flock.py @fabaff +homeassistant/components/notify/instapush.py @fabaff +homeassistant/components/notify/mastodon.py @fabaff +homeassistant/components/notify/smtp.py @fabaff +homeassistant/components/notify/syslog.py @fabaff +homeassistant/components/notify/xmpp.py @fabaff homeassistant/components/plant.py @ChristianKuehnel homeassistant/components/scene/lifx_cloud.py @amelchio homeassistant/components/sensor/airvisual.py @bachya +homeassistant/components/sensor/alpha_vantage.py @fabaff +homeassistant/components/sensor/bitcoin.py @fabaff +homeassistant/components/sensor/cpuspeed.py @fabaff +homeassistant/components/sensor/cups.py @fabaff +homeassistant/components/sensor/darksky.py @fabaff +homeassistant/components/sensor/file.py @fabaff homeassistant/components/sensor/filter.py @dgomes +homeassistant/components/sensor/fixer.py @fabaff homeassistant/components/sensor/gearbest.py @HerrHofrat +homeassistant/components/sensor/gitter.py @fabaff +homeassistant/components/sensor/glances.py @fabaff +homeassistant/components/sensor/gpsd.py @fabaff homeassistant/components/sensor/irish_rail_transport.py @ttroy50 homeassistant/components/sensor/jewish_calendar.py @tsvi +homeassistant/components/sensor/linux_battery.py @fabaff +homeassistant/components/sensor/luftdaten.py @fabaff homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel +homeassistant/components/sensor/min_max.py @fabaff +homeassistant/components/sensor/moon.py @fabaff +homeassistant/components/sensor/netdata.py @fabaff homeassistant/components/sensor/nsw_fuel_station.py @nickw444 +homeassistant/components/sensor/pi_hole.py @fabaff homeassistant/components/sensor/pollen.py @bachya +homeassistant/components/sensor/pvoutput.py @fabaff homeassistant/components/sensor/qnap.py @colinodell +homeassistant/components/sensor/serial.py @fabaff homeassistant/components/sensor/sma.py @kellerza homeassistant/components/sensor/sql.py @dgomes +homeassistant/components/sensor/swiss*.py @fabaff homeassistant/components/sensor/sytadin.py @gautric +homeassistant/components/sensor/time_data.py @fabaff +homeassistant/components/sensor/version.py @fabaff homeassistant/components/sensor/waqi.py @andrey-git +homeassistant/components/sensor/worldclock.py @fabaff +homeassistant/components/shiftr.py @fabaff +homeassistant/components/spaceapi.py @fabaff homeassistant/components/switch/tplink.py @rytilahti homeassistant/components/vacuum/roomba.py @pschmitt +homeassistant/components/weather/__init__.py @fabaff +homeassistant/components/weather/darksky.py @fabaff +homeassistant/components/weather/demo.py @fabaff +homeassistant/components/weather/openweathermap.py @fabaff homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi +# A +homeassistant/components/arduino.py @fabaff +homeassistant/components/*/arduino.py @fabaff +homeassistant/components/*/arest.py @fabaff homeassistant/components/*/axis.py @kane610 + +# B homeassistant/components/blink/* @fronzbot homeassistant/components/*/blink.py @fronzbot +homeassistant/components/bmw_connected_drive.py @ChristianKuehnel homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel homeassistant/components/*/broadlink.py @danielhiversen + +# C +homeassistant/components/counter/* @fabaff + +# D homeassistant/components/*/deconz.py @kane610 +homeassistant/components/digital_ocean.py @fabaff +homeassistant/components/*/digital_ocean.py @fabaff +homeassistant/components/dweet.py @fabaff +homeassistant/components/*/dweet.py @fabaff + +# E homeassistant/components/ecovacs.py @OverloadUT homeassistant/components/*/ecovacs.py @OverloadUT -homeassistant/components/edp_redy.py @abmantis homeassistant/components/*/edp_redy.py @abmantis +homeassistant/components/edp_redy.py @abmantis homeassistant/components/eight_sleep.py @mezz64 homeassistant/components/*/eight_sleep.py @mezz64 + +# H homeassistant/components/hive.py @Rendili @KJonline homeassistant/components/*/hive.py @Rendili @KJonline homeassistant/components/homekit/* @cdce8p homeassistant/components/huawei_lte.py @scop homeassistant/components/*/huawei_lte.py @scop + +# K homeassistant/components/knx.py @Julius2342 homeassistant/components/*/knx.py @Julius2342 homeassistant/components/konnected.py @heythisisnate homeassistant/components/*/konnected.py @heythisisnate + +# M homeassistant/components/matrix.py @tinloaf homeassistant/components/*/matrix.py @tinloaf homeassistant/components/melissa.py @kennedyshead homeassistant/components/*/melissa.py @kennedyshead +homeassistant/components/*/mystrom.py @fabaff + +# U homeassistant/components/openuv/* @bachya homeassistant/components/*/openuv.py @bachya + +# Q homeassistant/components/qwikswitch.py @kellerza homeassistant/components/*/qwikswitch.py @kellerza + +# R homeassistant/components/rainmachine/* @bachya homeassistant/components/*/rainmachine.py @bachya +homeassistant/components/*/random.py @fabaff homeassistant/components/*/rfxtrx.py @danielhiversen + +# T homeassistant/components/tahoma.py @philklei homeassistant/components/*/tahoma.py @philklei -homeassistant/components/tesla.py @zabuldon -homeassistant/components/*/tesla.py @zabuldon homeassistant/components/tellduslive.py @molobrakos @fredrike homeassistant/components/*/tellduslive.py @molobrakos @fredrike +homeassistant/components/tesla.py @zabuldon +homeassistant/components/*/tesla.py @zabuldon +homeassistant/components/thethingsnetwork.py @fabaff +homeassistant/components/*/thethingsnetwork.py @fabaff homeassistant/components/tibber/* @danielhiversen homeassistant/components/*/tibber.py @danielhiversen +homeassistant/components/tradfri/* @ggravlingen homeassistant/components/*/tradfri.py @ggravlingen + +# U homeassistant/components/upcloud.py @scop homeassistant/components/*/upcloud.py @scop + +# V homeassistant/components/velux.py @Julius2342 homeassistant/components/*/velux.py @Julius2342 + +# X homeassistant/components/*/xiaomi_aqara.py @danielhiversen @syssi homeassistant/components/*/xiaomi_miio.py @rytilahti @syssi + +# Z homeassistant/components/zoneminder.py @rohankapoorcom homeassistant/components/*/zoneminder.py @rohankapoorcom +# Other code homeassistant/scripts/check_config.py @kellerza From 75e236de73573f880cdb1ff349b1af25632e9a63 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 6 Oct 2018 09:54:03 +0200 Subject: [PATCH 009/265] Add myself to more sensors (#17185) --- CODEOWNERS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CODEOWNERS b/CODEOWNERS index 2799ae326c4..6cdfa6a45d5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -114,9 +114,12 @@ homeassistant/components/sensor/pi_hole.py @fabaff homeassistant/components/sensor/pollen.py @bachya homeassistant/components/sensor/pvoutput.py @fabaff homeassistant/components/sensor/qnap.py @colinodell +homeassistant/components/sensor/scrape.py @fabaff homeassistant/components/sensor/serial.py @fabaff +homeassistant/components/sensor/shodan.py @fabaff homeassistant/components/sensor/sma.py @kellerza homeassistant/components/sensor/sql.py @dgomes +homeassistant/components/sensor/statistics.py @fabaff homeassistant/components/sensor/swiss*.py @fabaff homeassistant/components/sensor/sytadin.py @gautric homeassistant/components/sensor/time_data.py @fabaff From 8683eeb9086b1bd62290a04b8251a378af4f6b19 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sat, 6 Oct 2018 14:32:54 +0200 Subject: [PATCH 010/265] Upgrade aiolifx_effects to 0.2.1 (#17188) --- homeassistant/components/light/lifx.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index 17b9f104f68..9dcd2ae4cc2 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -30,7 +30,7 @@ import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['aiolifx==0.6.3', 'aiolifx_effects==0.2.0'] +REQUIREMENTS = ['aiolifx==0.6.3', 'aiolifx_effects==0.2.1'] UDP_BROADCAST_PORT = 56700 diff --git a/requirements_all.txt b/requirements_all.txt index faadd0f5016..d2c2527d354 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -110,7 +110,7 @@ aioimaplib==0.7.13 aiolifx==0.6.3 # homeassistant.components.light.lifx -aiolifx_effects==0.2.0 +aiolifx_effects==0.2.1 # homeassistant.components.scene.hunterdouglas_powerview aiopvapi==1.5.4 From 760047f9644455d5559647541870320c6c95173a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Sandstr=C3=B6m?= Date: Sat, 6 Oct 2018 20:03:22 +0200 Subject: [PATCH 011/265] Verisure standard config for scan interval (#17192) * verisure configurable polling * fix indentation --- homeassistant/components/verisure.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/verisure.py b/homeassistant/components/verisure.py index 3876ff41c37..016547697b9 100644 --- a/homeassistant/components/verisure.py +++ b/homeassistant/components/verisure.py @@ -10,8 +10,8 @@ from datetime import timedelta import voluptuous as vol -from homeassistant.const import (CONF_PASSWORD, CONF_USERNAME, - EVENT_HOMEASSISTANT_STOP) +from homeassistant.const import (CONF_PASSWORD, CONF_SCAN_INTERVAL, + CONF_USERNAME, EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers import discovery from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv @@ -32,10 +32,12 @@ CONF_MOUSE = 'mouse' CONF_SMARTPLUGS = 'smartplugs' CONF_THERMOMETERS = 'thermometers' CONF_SMARTCAM = 'smartcam' -CONF_POLLING_RATE = 'polling_rate' DOMAIN = 'verisure' +MIN_SCAN_INTERVAL = timedelta(minutes=1) +DEFAULT_SCAN_INTERVAL = timedelta(minutes=1) + SERVICE_CAPTURE_SMARTCAM = 'capture_smartcam' HUB = None @@ -54,8 +56,8 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_SMARTPLUGS, default=True): cv.boolean, vol.Optional(CONF_THERMOMETERS, default=True): cv.boolean, vol.Optional(CONF_SMARTCAM, default=True): cv.boolean, - vol.Optional(CONF_POLLING_RATE, default=1): vol.All( - vol.Coerce(int), vol.Range(min=1)), + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): ( + vol.All(cv.time_period, vol.Clamp(min=MIN_SCAN_INTERVAL))), }), }, extra=vol.ALLOW_EXTRA) @@ -69,8 +71,8 @@ def setup(hass, config): import verisure global HUB HUB = VerisureHub(config[DOMAIN], verisure) - HUB.update_overview = Throttle(timedelta( - minutes=config[DOMAIN][CONF_POLLING_RATE]))(HUB.update_overview) + HUB.update_overview = Throttle( + config[DOMAIN][CONF_SCAN_INTERVAL])(HUB.update_overview) if not HUB.login(): return False hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, From 9285831fa11f370adf0fe21e7d477cda68e83d8c Mon Sep 17 00:00:00 2001 From: Guy Khmelnitsky Date: Sat, 6 Oct 2018 21:46:20 +0300 Subject: [PATCH 012/265] Upgrade boto3 to 1.9.16 (#17140) --- homeassistant/components/notify/aws_lambda.py | 2 +- homeassistant/components/notify/aws_sns.py | 2 +- homeassistant/components/notify/aws_sqs.py | 2 +- homeassistant/components/tts/amazon_polly.py | 2 +- requirements_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/notify/aws_lambda.py b/homeassistant/components/notify/aws_lambda.py index 8a3cb900f4b..28fedf6434d 100644 --- a/homeassistant/components/notify/aws_lambda.py +++ b/homeassistant/components/notify/aws_lambda.py @@ -17,7 +17,7 @@ from homeassistant.components.notify import ( import homeassistant.helpers.config_validation as cv from homeassistant.helpers.json import JSONEncoder -REQUIREMENTS = ['boto3==1.4.7'] +REQUIREMENTS = ['boto3==1.9.16'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/notify/aws_sns.py b/homeassistant/components/notify/aws_sns.py index 7ecf5a7cc7f..065898bcb85 100644 --- a/homeassistant/components/notify/aws_sns.py +++ b/homeassistant/components/notify/aws_sns.py @@ -17,7 +17,7 @@ from homeassistant.components.notify import ( import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ["boto3==1.4.7"] +REQUIREMENTS = ["boto3==1.9.16"] CONF_REGION = 'region_name' CONF_ACCESS_KEY_ID = 'aws_access_key_id' diff --git a/homeassistant/components/notify/aws_sqs.py b/homeassistant/components/notify/aws_sqs.py index 30b673846e7..78e71bde97a 100644 --- a/homeassistant/components/notify/aws_sqs.py +++ b/homeassistant/components/notify/aws_sqs.py @@ -16,7 +16,7 @@ from homeassistant.components.notify import ( import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ["boto3==1.4.7"] +REQUIREMENTS = ["boto3==1.9.16"] CONF_REGION = 'region_name' CONF_ACCESS_KEY_ID = 'aws_access_key_id' diff --git a/homeassistant/components/tts/amazon_polly.py b/homeassistant/components/tts/amazon_polly.py index d59331984b7..b6250f9d0e9 100644 --- a/homeassistant/components/tts/amazon_polly.py +++ b/homeassistant/components/tts/amazon_polly.py @@ -11,7 +11,7 @@ from homeassistant.components.tts import Provider, PLATFORM_SCHEMA import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['boto3==1.4.7'] +REQUIREMENTS = ['boto3==1.9.16'] CONF_REGION = 'region_name' CONF_ACCESS_KEY_ID = 'aws_access_key_id' diff --git a/requirements_all.txt b/requirements_all.txt index d2c2527d354..495a5f7158c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -200,7 +200,7 @@ blockchain==1.4.4 # homeassistant.components.notify.aws_sns # homeassistant.components.notify.aws_sqs # homeassistant.components.tts.amazon_polly -boto3==1.4.7 +boto3==1.9.16 # homeassistant.scripts.credstash botocore==1.7.34 From 5d6562a73f00409668deaa8fd325f5bda7e7da6b Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Sat, 6 Oct 2018 23:30:07 +0200 Subject: [PATCH 013/265] Bugfix switch flux - light service call (#17187) * Bugfix switch flux - light service call * Change x_val and y_val test --- homeassistant/components/switch/flux.py | 38 +++++++++++++++---------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/switch/flux.py b/homeassistant/components/switch/flux.py index c541c37b5e7..05e0497155a 100644 --- a/homeassistant/components/switch/flux.py +++ b/homeassistant/components/switch/flux.py @@ -71,36 +71,44 @@ def set_lights_xy(hass, lights, x_val, y_val, brightness, transition): """Set color of array of lights.""" for light in lights: if is_on(hass, light): + service_data = {ATTR_ENTITY_ID: light} + if x_val is not None and y_val is not None: + service_data[ATTR_XY_COLOR] = [x_val, y_val] + if brightness is not None: + service_data[ATTR_BRIGHTNESS] = brightness + service_data[ATTR_WHITE_VALUE] = brightness + if transition is not None: + service_data[ATTR_TRANSITION] = transition hass.services.call( - LIGHT_DOMAIN, SERVICE_TURN_ON, { - ATTR_XY_COLOR: [x_val, y_val], - ATTR_BRIGHTNESS: brightness, - ATTR_TRANSITION: transition, - ATTR_WHITE_VALUE: brightness, - ATTR_ENTITY_ID: light}) + LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) def set_lights_temp(hass, lights, mired, brightness, transition): """Set color of array of lights.""" for light in lights: if is_on(hass, light): + service_data = {ATTR_ENTITY_ID: light} + if mired is not None: + service_data[ATTR_COLOR_TEMP] = int(mired) + if brightness is not None: + service_data[ATTR_BRIGHTNESS] = brightness + if transition is not None: + service_data[ATTR_TRANSITION] = transition hass.services.call( - LIGHT_DOMAIN, SERVICE_TURN_ON, { - ATTR_COLOR_TEMP: int(mired), - ATTR_BRIGHTNESS: brightness, - ATTR_TRANSITION: transition, - ATTR_ENTITY_ID: light}) + LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) def set_lights_rgb(hass, lights, rgb, transition): """Set color of array of lights.""" for light in lights: if is_on(hass, light): + service_data = {ATTR_ENTITY_ID: light} + if rgb is not None: + service_data[ATTR_RGB_COLOR] = rgb + if transition is not None: + service_data[ATTR_TRANSITION] = transition hass.services.call( - LIGHT_DOMAIN, SERVICE_TURN_ON, { - ATTR_RGB_COLOR: rgb, - ATTR_TRANSITION: transition, - ATTR_ENTITY_ID: light}) + LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) def setup_platform(hass, config, add_entities, discovery_info=None): From 8fda70537736db9a73c0a863800d6bb4df67f5fc Mon Sep 17 00:00:00 2001 From: Philip Rosenberg-Watt Date: Sat, 6 Oct 2018 17:38:25 -0600 Subject: [PATCH 014/265] Fix Todoist custom project update (#17115) * Fix Todoist custom project update Custom projects were not refreshing the API state and were using local/stored state. This resulted in invalid tasks being retained upon update. This change resets the local Todoist API state, syncs it, and then continues normal update operation(s) on the Todoist project data object. * Remove blank line after docstring * Update logging call * Simplify logging --- homeassistant/components/calendar/todoist.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/calendar/todoist.py b/homeassistant/components/calendar/todoist.py index b5eaed4e6c9..a0a3457667f 100644 --- a/homeassistant/components/calendar/todoist.py +++ b/homeassistant/components/calendar/todoist.py @@ -518,6 +518,8 @@ class TodoistProjectData: def update(self): """Get the latest data.""" if self._id is None: + self._api.reset_state() + self._api.sync() project_task_data = [ task for task in self._api.state[TASKS] if not self._project_id_whitelist or @@ -527,6 +529,7 @@ class TodoistProjectData: # If we have no data, we can just return right away. if not project_task_data: + _LOGGER.debug("No data for %s", self._name) self.event = None return True @@ -541,6 +544,8 @@ class TodoistProjectData: if not project_tasks: # We had no valid tasks + _LOGGER.debug("No valid tasks for %s", self._name) + self.event = None return True # Make sure the task collection is reset to prevent an From c8266c669258e9c605a34369d98c494189835351 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Sandstr=C3=B6m?= Date: Sun, 7 Oct 2018 12:33:16 +0200 Subject: [PATCH 015/265] vsure version 1.5.0 (#17209) --- homeassistant/components/verisure.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/verisure.py b/homeassistant/components/verisure.py index 016547697b9..2c8c34fa67d 100644 --- a/homeassistant/components/verisure.py +++ b/homeassistant/components/verisure.py @@ -16,7 +16,7 @@ from homeassistant.helpers import discovery from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['vsure==1.3.7', 'jsonpath==0.75'] +REQUIREMENTS = ['vsure==1.5.0', 'jsonpath==0.75'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 495a5f7158c..30bf763e6b2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1484,7 +1484,7 @@ volkszaehler==0.1.2 volvooncall==0.4.0 # homeassistant.components.verisure -vsure==1.3.7 +vsure==1.5.0 # homeassistant.components.sensor.vasttrafik vtjp==0.1.14 From 6e81ae096ed60dcbb1345339623313c323147576 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sun, 7 Oct 2018 12:35:44 +0200 Subject: [PATCH 016/265] Disallow list/dict for string configuration (#17202) --- homeassistant/helpers/config_validation.py | 9 ++++++--- tests/helpers/test_config_validation.py | 8 +++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index bb4dcf6a55f..9ce4b6b166d 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -335,9 +335,12 @@ def slugify(value): def string(value: Any) -> str: """Coerce value to string, except for None.""" - if value is not None: - return str(value) - raise vol.Invalid('string value is None') + if value is None: + raise vol.Invalid('string value is None') + if isinstance(value, (list, dict)): + raise vol.Invalid('value should be a string') + + return str(value) def temperature_unit(value) -> str: diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index ab575c61789..cfd84dbc3b3 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -356,9 +356,15 @@ def test_string(): """Test string validation.""" schema = vol.Schema(cv.string) - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(vol.Invalid): schema(None) + with pytest.raises(vol.Invalid): + schema([]) + + with pytest.raises(vol.Invalid): + schema({}) + for value in (True, 1, 'hello'): schema(value) From 592e1dc96ab327f8840ea8e37fdc20deca19b749 Mon Sep 17 00:00:00 2001 From: Brian Towles Date: Sun, 7 Oct 2018 06:12:33 -0500 Subject: [PATCH 017/265] Enable new registry rename for Insteon (#17171) * Enable new registry rename for Insteon * Segment unique_id from name --- homeassistant/components/insteon/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index 924baeaa560..b3bb6e73b94 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -465,6 +465,16 @@ class InsteonEntity(Entity): """Return the INSTEON group that the entity responds to.""" return self._insteon_device_state.group + @property + def unique_id(self) -> str: + """Return a unique ID.""" + if self._insteon_device_state.group == 0x01: + uid = self._insteon_device.id + else: + uid = '{:s}_{:d}'.format(self._insteon_device.id, + self._insteon_device_state.group) + return uid + @property def name(self): """Return the name of the node (used for Entity_ID).""" From 1d7d82fde5a9d9961bb752875ab46dc545caf432 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Sun, 7 Oct 2018 13:14:37 +0200 Subject: [PATCH 018/265] Fix aliases support for RFLink sensors (#17190) --- homeassistant/components/sensor/rflink.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/rflink.py b/homeassistant/components/sensor/rflink.py index e01c441be84..c33c99c5828 100644 --- a/homeassistant/components/sensor/rflink.py +++ b/homeassistant/components/sensor/rflink.py @@ -67,9 +67,14 @@ def devices_from_config(domain_config, hass=None): device = RflinkSensor(device_id, hass, **config) devices.append(device) - # Register entity to listen to incoming rflink events + # Register entity (and aliases) to listen to incoming rflink events hass.data[DATA_ENTITY_LOOKUP][ EVENT_KEY_SENSOR][device_id].append(device) + aliases = config.get(CONF_ALIASES) + if aliases: + for _id in aliases: + hass.data[DATA_ENTITY_LOOKUP][ + EVENT_KEY_SENSOR][_id].append(device) return devices From c1ed9edd263adb1ca6140d89c36c3a8a4510b584 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sun, 7 Oct 2018 21:00:12 +0200 Subject: [PATCH 019/265] Add forecast for Met.no (#17109) test met met no 0.3.0 fix line length fix line length --- homeassistant/components/weather/met.py | 107 ++++-------------------- requirements_all.txt | 2 +- 2 files changed, 19 insertions(+), 90 deletions(-) diff --git a/homeassistant/components/weather/met.py b/homeassistant/components/weather/met.py index f888af2e909..bab6624e9d0 100644 --- a/homeassistant/components/weather/met.py +++ b/homeassistant/components/weather/met.py @@ -16,9 +16,9 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.event import (async_track_utc_time_change, async_call_later) -from homeassistant.util import dt as dt_util +import homeassistant.util.dt as dt_util -REQUIREMENTS = ['pyMetno==0.2.0'] +REQUIREMENTS = ['pyMetno==0.3.0'] _LOGGER = logging.getLogger(__name__) @@ -26,49 +26,6 @@ CONF_ATTRIBUTION = "Weather forecast from met.no, delivered " \ "by the Norwegian Meteorological Institute." DEFAULT_NAME = "Met.no" -# https://api.met.no/weatherapi/weathericon/_/documentation/#___top -CONDITIONS = {1: 'sunny', - 2: 'partlycloudy', - 3: 'partlycloudy', - 4: 'cloudy', - 5: 'rainy', - 6: 'lightning-rainy', - 7: 'snowy-rainy', - 8: 'snowy', - 9: 'rainy', - 10: 'rainy', - 11: 'lightning-rainy', - 12: 'snowy-rainy', - 13: 'snowy', - 14: 'snowy', - 15: 'fog', - 20: 'lightning-rainy', - 21: 'lightning-rainy', - 22: 'lightning-rainy', - 23: 'lightning-rainy', - 24: 'lightning-rainy', - 25: 'lightning-rainy', - 26: 'lightning-rainy', - 27: 'lightning-rainy', - 28: 'lightning-rainy', - 29: 'lightning-rainy', - 30: 'lightning-rainy', - 31: 'lightning-rainy', - 32: 'lightning-rainy', - 33: 'lightning-rainy', - 34: 'lightning-rainy', - 40: 'rainy', - 41: 'rainy', - 42: 'snowy-rainy', - 43: 'snowy-rainy', - 44: 'snowy', - 45: 'snowy', - 46: 'rainy', - 47: 'snowy-rainy', - 48: 'snowy-rainy', - 49: 'snowy', - 50: 'snowy', - } URL = 'https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/1.9/' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -113,12 +70,8 @@ class MetWeather(WeatherEntity): clientsession, URL ) - self._temperature = None - self._condition = None - self._pressure = None - self._humidity = None - self._wind_speed = None - self._wind_bearing = None + self._current_weather_data = {} + self._forecast_data = None async def async_added_to_hass(self): """Start fetching data.""" @@ -145,38 +98,9 @@ class MetWeather(WeatherEntity): async def _update(self, *_): """Get the latest data from Met.no.""" - import metno - if self._weather_data is None: - return - - now = dt_util.utcnow() - - ordered_entries = [] - for time_entry in self._weather_data.data['product']['time']: - valid_from = dt_util.parse_datetime(time_entry['@from']) - valid_to = dt_util.parse_datetime(time_entry['@to']) - - if now >= valid_to: - # Has already passed. Never select this. - continue - - average_dist = (abs((valid_to - now).total_seconds()) + - abs((valid_from - now).total_seconds())) - - ordered_entries.append((average_dist, time_entry)) - - if not ordered_entries: - return - ordered_entries.sort(key=lambda item: item[0]) - - self._temperature = metno.get_forecast('temperature', ordered_entries) - self._condition = CONDITIONS.get(metno.get_forecast('symbol', - ordered_entries)) - self._pressure = metno.get_forecast('pressure', ordered_entries) - self._humidity = metno.get_forecast('humidity', ordered_entries) - self._wind_speed = metno.get_forecast('windSpeed', ordered_entries) - self._wind_bearing = metno.get_forecast('windDirection', - ordered_entries) + self._current_weather_data = self._weather_data.get_current_weather() + time_zone = dt_util.DEFAULT_TIME_ZONE + self._forecast_data = self._weather_data.get_forecast(time_zone) self.async_schedule_update_ha_state() @property @@ -187,12 +111,12 @@ class MetWeather(WeatherEntity): @property def condition(self): """Return the current condition.""" - return self._condition + return self._current_weather_data.get('condition') @property def temperature(self): """Return the temperature.""" - return self._temperature + return self._current_weather_data.get('temperature') @property def temperature_unit(self): @@ -202,24 +126,29 @@ class MetWeather(WeatherEntity): @property def pressure(self): """Return the pressure.""" - return self._pressure + return self._current_weather_data.get('pressure') @property def humidity(self): """Return the humidity.""" - return self._humidity + return self._current_weather_data.get('humidity') @property def wind_speed(self): """Return the wind speed.""" - return self._wind_speed + return self._current_weather_data.get('wind_speed') @property def wind_bearing(self): """Return the wind direction.""" - return self._wind_bearing + return self._current_weather_data.get('wind_bearing') @property def attribution(self): """Return the attribution.""" return CONF_ATTRIBUTION + + @property + def forecast(self): + """Return the forecast array.""" + return self._forecast_data diff --git a/requirements_all.txt b/requirements_all.txt index 30bf763e6b2..fb24b5600f2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -759,7 +759,7 @@ pyCEC==0.4.13 pyHS100==0.3.3 # homeassistant.components.weather.met -pyMetno==0.2.0 +pyMetno==0.3.0 # homeassistant.components.rfxtrx pyRFXtrx==0.23 From 06a64c0167f5a38fc6ad331dad1124ebb94f6d89 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Sun, 7 Oct 2018 15:45:36 -0400 Subject: [PATCH 020/265] Add support for ElkM1 alarm/automation panel (#16952) * Add support for ElkM1 alarm/automation panel. * fix lines too long * Address PR comments * Fix hound ci errors * Changes for PR comments * Use vol.Range for checking range value * Address PR comments * Fix lint errors * Added elkm1-lib requirement * Update coverage to exclude elk * Fix flake8 errors * Fix flake8 error * Cleanup config parsing * Add housecode converter * fix PR comments * fix syntax error * Fix PR comment --- .coveragerc | 3 + .../components/alarm_control_panel/elkm1.py | 200 +++++++++++++++++ .../alarm_control_panel/services.yaml | 52 +++++ homeassistant/components/elkm1/__init__.py | 212 ++++++++++++++++++ requirements_all.txt | 3 + 5 files changed, 470 insertions(+) create mode 100644 homeassistant/components/alarm_control_panel/elkm1.py create mode 100644 homeassistant/components/elkm1/__init__.py diff --git a/.coveragerc b/.coveragerc index d5296455981..02c5481c23a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -102,6 +102,9 @@ omit = homeassistant/components/egardia.py homeassistant/components/*/egardia.py + homeassistant/components/elkm1/* + homeassistant/components/*/elkm1.py + homeassistant/components/enocean.py homeassistant/components/*/enocean.py diff --git a/homeassistant/components/alarm_control_panel/elkm1.py b/homeassistant/components/alarm_control_panel/elkm1.py new file mode 100644 index 00000000000..8026a3736fb --- /dev/null +++ b/homeassistant/components/alarm_control_panel/elkm1.py @@ -0,0 +1,200 @@ +""" +Each ElkM1 area will be created as a separate alarm_control_panel in HASS. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/alarm_control_panel.elkm1/ +""" + +import voluptuous as vol +import homeassistant.components.alarm_control_panel as alarm +from homeassistant.const import ( + ATTR_CODE, ATTR_ENTITY_ID, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMING, STATE_ALARM_DISARMED, + STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED) +from homeassistant.components.elkm1 import ( + DOMAIN as ELK_DOMAIN, create_elk_entities, ElkEntity) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, async_dispatcher_send) + +DEPENDENCIES = [ELK_DOMAIN] + +SIGNAL_ARM_ENTITY = 'elkm1_arm' +SIGNAL_DISPLAY_MESSAGE = 'elkm1_display_message' + +ELK_ALARM_SERVICE_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID, default=[]): cv.entity_ids, + vol.Required(ATTR_CODE): vol.All(vol.Coerce(int), vol.Range(0, 999999)), +}) + +DISPLAY_MESSAGE_SERVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID, default=[]): cv.entity_ids, + vol.Optional('clear', default=2): vol.In([0, 1, 2]), + vol.Optional('beep', default=False): cv.boolean, + vol.Optional('timeout', default=0): vol.Range(min=0, max=65535), + vol.Optional('line1', default=''): cv.string, + vol.Optional('line2', default=''): cv.string, +}) + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info): + """Set up the ElkM1 alarm platform.""" + elk = hass.data[ELK_DOMAIN]['elk'] + entities = create_elk_entities(hass, elk.areas, 'area', ElkArea, []) + async_add_entities(entities, True) + + def _dispatch(signal, entity_ids, *args): + for entity_id in entity_ids: + async_dispatcher_send( + hass, '{}_{}'.format(signal, entity_id), *args) + + def _arm_service(service): + entity_ids = service.data.get(ATTR_ENTITY_ID, []) + arm_level = _arm_services().get(service.service) + args = (arm_level, service.data.get(ATTR_CODE)) + _dispatch(SIGNAL_ARM_ENTITY, entity_ids, *args) + + for service in _arm_services(): + hass.services.async_register( + alarm.DOMAIN, service, _arm_service, ELK_ALARM_SERVICE_SCHEMA) + + def _display_message_service(service): + entity_ids = service.data.get(ATTR_ENTITY_ID, []) + data = service.data + args = (data['clear'], data['beep'], data['timeout'], + data['line1'], data['line2']) + _dispatch(SIGNAL_DISPLAY_MESSAGE, entity_ids, *args) + + hass.services.async_register( + alarm.DOMAIN, 'elkm1_alarm_display_message', + _display_message_service, DISPLAY_MESSAGE_SERVICE_SCHEMA) + + +def _arm_services(): + from elkm1_lib.const import ArmLevel + + return { + 'elkm1_alarm_arm_vacation': ArmLevel.ARMED_VACATION.value, + 'elkm1_alarm_arm_home_instant': ArmLevel.ARMED_STAY_INSTANT.value, + 'elkm1_alarm_arm_night_instant': ArmLevel.ARMED_NIGHT_INSTANT.value, + } + + +class ElkArea(ElkEntity, alarm.AlarmControlPanel): + """Representation of an Area / Partition within the ElkM1 alarm panel.""" + + def __init__(self, element, elk, elk_data): + """Initialize Area as Alarm Control Panel.""" + super().__init__('alarm_control_panel', element, elk, elk_data) + self._changed_by_entity_id = '' + + async def async_added_to_hass(self): + """Register callback for ElkM1 changes.""" + await super().async_added_to_hass() + for keypad in self._elk.keypads: + keypad.add_callback(self._watch_keypad) + async_dispatcher_connect( + self.hass, '{}_{}'.format(SIGNAL_ARM_ENTITY, self.entity_id), + self._arm_service) + async_dispatcher_connect( + self.hass, '{}_{}'.format(SIGNAL_DISPLAY_MESSAGE, self.entity_id), + self._display_message) + + def _watch_keypad(self, keypad, changeset): + if keypad.area != self._element.index: + return + if changeset.get('last_user') is not None: + self._changed_by_entity_id = self.hass.data[ + ELK_DOMAIN]['keypads'].get(keypad.index, '') + self.async_schedule_update_ha_state(True) + + @property + def code_format(self): + """Return the alarm code format.""" + return '^[0-9]{4}([0-9]{2})?$' + + @property + def state(self): + """Return the state of the element.""" + return self._state + + @property + def device_state_attributes(self): + """Attributes of the area.""" + from elkm1_lib.const import AlarmState, ArmedStatus, ArmUpState + + attrs = self.initial_attrs() + elmt = self._element + attrs['is_exit'] = elmt.is_exit + attrs['timer1'] = elmt.timer1 + attrs['timer2'] = elmt.timer2 + if elmt.armed_status is not None: + attrs['armed_status'] = \ + ArmedStatus(elmt.armed_status).name.lower() + if elmt.arm_up_state is not None: + attrs['arm_up_state'] = ArmUpState(elmt.arm_up_state).name.lower() + if elmt.alarm_state is not None: + attrs['alarm_state'] = AlarmState(elmt.alarm_state).name.lower() + attrs['changed_by_entity_id'] = self._changed_by_entity_id + return attrs + + def _element_changed(self, element, changeset): + from elkm1_lib.const import ArmedStatus + + elk_state_to_hass_state = { + ArmedStatus.DISARMED.value: STATE_ALARM_DISARMED, + ArmedStatus.ARMED_AWAY.value: STATE_ALARM_ARMED_AWAY, + ArmedStatus.ARMED_STAY.value: STATE_ALARM_ARMED_HOME, + ArmedStatus.ARMED_STAY_INSTANT.value: STATE_ALARM_ARMED_HOME, + ArmedStatus.ARMED_TO_NIGHT.value: STATE_ALARM_ARMED_NIGHT, + ArmedStatus.ARMED_TO_NIGHT_INSTANT.value: STATE_ALARM_ARMED_NIGHT, + ArmedStatus.ARMED_TO_VACATION.value: STATE_ALARM_ARMED_AWAY, + } + + if self._element.alarm_state is None: + self._state = None + elif self._area_is_in_alarm_state(): + self._state = STATE_ALARM_TRIGGERED + elif self._entry_exit_timer_is_running(): + self._state = STATE_ALARM_ARMING \ + if self._element.is_exit else STATE_ALARM_PENDING + else: + self._state = elk_state_to_hass_state[self._element.armed_status] + + def _entry_exit_timer_is_running(self): + return self._element.timer1 > 0 or self._element.timer2 > 0 + + def _area_is_in_alarm_state(self): + from elkm1_lib.const import AlarmState + + return self._element.alarm_state >= AlarmState.FIRE_ALARM.value + + async def async_alarm_disarm(self, code=None): + """Send disarm command.""" + self._element.disarm(int(code)) + + async def async_alarm_arm_home(self, code=None): + """Send arm home command.""" + from elkm1_lib.const import ArmLevel + + self._element.arm(ArmLevel.ARMED_STAY.value, int(code)) + + async def async_alarm_arm_away(self, code=None): + """Send arm away command.""" + from elkm1_lib.const import ArmLevel + + self._element.arm(ArmLevel.ARMED_AWAY.value, int(code)) + + async def async_alarm_arm_night(self, code=None): + """Send arm night command.""" + from elkm1_lib.const import ArmLevel + + self._element.arm(ArmLevel.ARMED_NIGHT.value, int(code)) + + async def _arm_service(self, arm_level, code): + self._element.arm(arm_level, code) + + async def _display_message(self, clear, beep, timeout, line1, line2): + """Display a message on all keypads for the area.""" + self._element.display_message(clear, beep, timeout, line1, line2) diff --git a/homeassistant/components/alarm_control_panel/services.yaml b/homeassistant/components/alarm_control_panel/services.yaml index 391de2033c7..7918631464f 100644 --- a/homeassistant/components/alarm_control_panel/services.yaml +++ b/homeassistant/components/alarm_control_panel/services.yaml @@ -79,3 +79,55 @@ ifttt_push_alarm_state: state: description: The state to which the alarm control panel has to be set. example: 'armed_night' + +elkm1_alarm_arm_vacation: + description: Arm the ElkM1 in vacation mode. + fields: + entity_id: + description: Name of alarm control panel to arm. + example: 'alarm_control_panel.main' + code: + description: An code to arm the alarm control panel. + example: 1234 + +elkm1_alarm_arm_home_instant: + description: Arm the ElkM1 in home instant mode. + fields: + entity_id: + description: Name of alarm control panel to arm. + example: 'alarm_control_panel.main' + code: + description: An code to arm the alarm control panel. + example: 1234 + +elkm1_alarm_arm_night_instant: + description: Arm the ElkM1 in night instant mode. + fields: + entity_id: + description: Name of alarm control panel to arm. + example: 'alarm_control_panel.main' + code: + description: An code to arm the alarm control panel. + example: 1234 + +elkm1_alarm_display_message: + description: Display a message on all of the ElkM1 keypads for an area. + fields: + entity_id: + description: Name of alarm control panel to display messages on. + example: 'alarm_control_panel.main' + clear: + description: 0=clear message, 1=clear message with * key, 2=Display until timeout; default 2 + example: 1 + beep: + description: 0=no beep, 1=beep; default 0 + example: 1 + timeout: + description: Time to display message, 0=forever, max 65535, default 0 + example: 4242 + line1: + description: Up to 16 characters of text (truncated if too long). Default blank. + example: The answer to life, + line2: + description: Up to 16 characters of text (truncated if too long). Default blank. + example: the universe, and everything. diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py new file mode 100644 index 00000000000..505280d4f26 --- /dev/null +++ b/homeassistant/components/elkm1/__init__.py @@ -0,0 +1,212 @@ +""" +Support the ElkM1 Gold and ElkM1 EZ8 alarm / integration panels. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/elkm1/ +""" + +import logging +import re + +import voluptuous as vol +from homeassistant.const import ( + CONF_EXCLUDE, CONF_HOST, CONF_INCLUDE, CONF_PASSWORD, + CONF_TEMPERATURE_UNIT, CONF_USERNAME, TEMP_FAHRENHEIT) +from homeassistant.core import HomeAssistant, callback # noqa +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import discovery +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import ConfigType # noqa + +DOMAIN = "elkm1" + +REQUIREMENTS = ['elkm1-lib==0.7.10'] + +CONF_AREA = 'area' +CONF_COUNTER = 'counter' +CONF_KEYPAD = 'keypad' +CONF_OUTPUT = 'output' +CONF_SETTING = 'setting' +CONF_TASK = 'task' +CONF_THERMOSTAT = 'thermostat' +CONF_PLC = 'plc' +CONF_ZONE = 'zone' +CONF_ENABLED = 'enabled' + +_LOGGER = logging.getLogger(__name__) + +SUPPORTED_DOMAINS = ['alarm_control_panel'] + + +def _host_validator(config): + """Validate that a host is properly configured.""" + if config[CONF_HOST].startswith('elks://'): + if CONF_USERNAME not in config or CONF_PASSWORD not in config: + raise vol.Invalid("Specify username and password for elks://") + elif not config[CONF_HOST].startswith('elk://') and not config[ + CONF_HOST].startswith('serial://'): + raise vol.Invalid("Invalid host URL") + return config + + +def _elk_range_validator(rng): + def _housecode_to_int(val): + match = re.search(r'^([a-p])(0[1-9]|1[0-6]|[1-9])$', val.lower()) + if match: + return (ord(match.group(1)) - ord('a')) * 16 + int(match.group(2)) + raise vol.Invalid("Invalid range") + + def _elk_value(val): + return int(val) if val.isdigit() else _housecode_to_int(val) + + vals = [s.strip() for s in str(rng).split('-')] + start = _elk_value(vals[0]) + end = start if len(vals) == 1 else _elk_value(vals[1]) + return (start, end) + + +CONFIG_SCHEMA_SUBDOMAIN = vol.Schema({ + vol.Optional(CONF_ENABLED, default=True): cv.boolean, + vol.Optional(CONF_INCLUDE, default=[]): [_elk_range_validator], + vol.Optional(CONF_EXCLUDE, default=[]): [_elk_range_validator], +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_USERNAME, default=''): cv.string, + vol.Optional(CONF_PASSWORD, default=''): cv.string, + vol.Optional(CONF_TEMPERATURE_UNIT, default=TEMP_FAHRENHEIT): + cv.temperature_unit, + vol.Optional(CONF_AREA): CONFIG_SCHEMA_SUBDOMAIN, + vol.Optional(CONF_COUNTER): CONFIG_SCHEMA_SUBDOMAIN, + vol.Optional(CONF_KEYPAD): CONFIG_SCHEMA_SUBDOMAIN, + vol.Optional(CONF_OUTPUT): CONFIG_SCHEMA_SUBDOMAIN, + vol.Optional(CONF_PLC): CONFIG_SCHEMA_SUBDOMAIN, + vol.Optional(CONF_SETTING): CONFIG_SCHEMA_SUBDOMAIN, + vol.Optional(CONF_TASK): CONFIG_SCHEMA_SUBDOMAIN, + vol.Optional(CONF_THERMOSTAT): CONFIG_SCHEMA_SUBDOMAIN, + vol.Optional(CONF_ZONE): CONFIG_SCHEMA_SUBDOMAIN, + }, + _host_validator, + ) +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: + """Set up the Elk M1 platform.""" + from elkm1_lib.const import Max + import elkm1_lib as elkm1 + + configs = { + CONF_AREA: Max.AREAS.value, + CONF_COUNTER: Max.COUNTERS.value, + CONF_KEYPAD: Max.KEYPADS.value, + CONF_OUTPUT: Max.OUTPUTS.value, + CONF_PLC: Max.LIGHTS.value, + CONF_SETTING: Max.SETTINGS.value, + CONF_TASK: Max.TASKS.value, + CONF_THERMOSTAT: Max.THERMOSTATS.value, + CONF_ZONE: Max.ZONES.value, + } + + def _included(ranges, set_to, values): + for rng in ranges: + if not rng[0] <= rng[1] <= len(values): + raise vol.Invalid("Invalid range {}".format(rng)) + values[rng[0]-1:rng[1]] = [set_to] * (rng[1] - rng[0] + 1) + + conf = hass_config[DOMAIN] + config = {'temperature_unit': conf[CONF_TEMPERATURE_UNIT]} + config['panel'] = {'enabled': True, 'included': [True]} + + for item, max_ in configs.items(): + config[item] = {'enabled': conf[item][CONF_ENABLED], + 'included': [not conf[item]['include']] * max_} + try: + _included(conf[item]['include'], True, config[item]['included']) + _included(conf[item]['exclude'], False, config[item]['included']) + except (ValueError, vol.Invalid) as err: + _LOGGER.error("Config item: %s; %s", item, err) + return False + + elk = elkm1.Elk({'url': conf[CONF_HOST], 'userid': conf[CONF_USERNAME], + 'password': conf[CONF_PASSWORD]}) + elk.connect() + + hass.data[DOMAIN] = {'elk': elk, 'config': config, 'keypads': {}} + for component in SUPPORTED_DOMAINS: + hass.async_create_task( + discovery.async_load_platform(hass, component, DOMAIN)) + + return True + + +def create_elk_entities(hass, elk_elements, element_type, class_, entities): + """Create the ElkM1 devices of a particular class.""" + elk_data = hass.data[DOMAIN] + if elk_data['config'][element_type]['enabled']: + elk = elk_data['elk'] + for element in elk_elements: + if elk_data['config'][element_type]['included'][element.index]: + entities.append(class_(element, elk, elk_data)) + return entities + + +class ElkEntity(Entity): + """Base class for all Elk entities.""" + + def __init__(self, platform, element, elk, elk_data): + """Initialize the base of all Elk devices.""" + self._elk = elk + self._element = element + self._state = None + self._temperature_unit = elk_data['config']['temperature_unit'] + self._unique_id = 'elkm1_{}'.format( + self._element.default_name('_').lower()) + + @property + def name(self): + """Name of the element.""" + return self._element.name + + @property + def unique_id(self): + """Return unique id of the element.""" + return self._unique_id + + @property + def should_poll(self) -> bool: + """Don't poll this device.""" + return False + + @property + def device_state_attributes(self): + """Return the default attributes of the element.""" + return {**self._element.as_dict(), **self.initial_attrs()} + + @property + def available(self): + """Is the entity available to be updated.""" + return self._elk.is_connected() + + def initial_attrs(self): + """Return the underlying element's attributes as a dict.""" + attrs = {} + attrs['index'] = self._element.index + 1 + return attrs + + def _element_changed(self, element, changeset): + raise NotImplementedError() + + @callback + def _element_callback(self, element, changeset): + """Handle callback from an Elk element that has changed.""" + self._element_changed(element, changeset) + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Register callback for ElkM1 changes and update entity state.""" + self._element.add_callback(self._element_callback) + self._element_callback(self._element, {}) diff --git a/requirements_all.txt b/requirements_all.txt index fb24b5600f2..5c5415d9ec6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -321,6 +321,9 @@ einder==0.3.1 # homeassistant.components.sensor.eliqonline eliqonline==1.0.14 +# homeassistant.components.elkm1 +elkm1-lib==0.7.10 + # homeassistant.components.enocean enocean==0.40 From 086c71525ed55491de19bdb08fde78b507c0ad66 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sun, 7 Oct 2018 23:14:53 +0200 Subject: [PATCH 021/265] Add config entry for LIFX (#17201) * Add config entry for LIFX * Use list for dependencies * Obsolete the platform config * Use DOMAIN * Use async_create_task --- CODEOWNERS | 5 +- .../components/lifx/.translations/en.json | 15 +++ homeassistant/components/lifx/__init__.py | 97 +++++++++++++++++++ homeassistant/components/lifx/strings.json | 15 +++ homeassistant/components/light/lifx.py | 49 +++++----- homeassistant/config_entries.py | 1 + requirements_all.txt | 2 +- 7 files changed, 160 insertions(+), 24 deletions(-) create mode 100644 homeassistant/components/lifx/.translations/en.json create mode 100644 homeassistant/components/lifx/__init__.py create mode 100644 homeassistant/components/lifx/strings.json diff --git a/CODEOWNERS b/CODEOWNERS index 6cdfa6a45d5..603a6467d5b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -63,7 +63,6 @@ homeassistant/components/device_tracker/huawei_router.py @abmantis homeassistant/components/device_tracker/tile.py @bachya homeassistant/components/history_graph.py @andrey-git homeassistant/components/influx.py @fabaff -homeassistant/components/light/lifx.py @amelchio homeassistant/components/light/lifx_legacy.py @amelchio homeassistant/components/light/tplink.py @rytilahti homeassistant/components/light/yeelight.py @rytilahti @@ -180,6 +179,10 @@ homeassistant/components/*/knx.py @Julius2342 homeassistant/components/konnected.py @heythisisnate homeassistant/components/*/konnected.py @heythisisnate +# L +homeassistant/components/lifx.py @amelchio +homeassistant/components/*/lifx.py @amelchio + # M homeassistant/components/matrix.py @tinloaf homeassistant/components/*/matrix.py @tinloaf diff --git a/homeassistant/components/lifx/.translations/en.json b/homeassistant/components/lifx/.translations/en.json new file mode 100644 index 00000000000..64fdc7516ea --- /dev/null +++ b/homeassistant/components/lifx/.translations/en.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No LIFX devices found on the network.", + "single_instance_allowed": "Only a single configuration of LIFX is possible." + }, + "step": { + "confirm": { + "description": "Do you want to set up LIFX?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/__init__.py b/homeassistant/components/lifx/__init__.py new file mode 100644 index 00000000000..85e249eb934 --- /dev/null +++ b/homeassistant/components/lifx/__init__.py @@ -0,0 +1,97 @@ +"""Component to embed LIFX.""" +import asyncio +import socket + +import async_timeout +import voluptuous as vol +import homeassistant.helpers.config_validation as cv + +from homeassistant import config_entries +from homeassistant.helpers import config_entry_flow +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN + + +DOMAIN = 'lifx' +REQUIREMENTS = ['aiolifx==0.6.3'] + +CONF_SERVER = 'server' +CONF_BROADCAST = 'broadcast' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: { + LIGHT_DOMAIN: { + vol.Optional(CONF_SERVER): cv.string, + vol.Optional(CONF_BROADCAST): cv.string, + } + } +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the LIFX component.""" + conf = config.get(DOMAIN) + + hass.data[DOMAIN] = conf or {} + + if conf is not None: + hass.async_create_task(hass.config_entries.flow.async_init( + DOMAIN, context={'source': config_entries.SOURCE_IMPORT})) + + return True + + +async def async_setup_entry(hass, entry): + """Set up LIFX from a config entry.""" + hass.async_create_task(hass.config_entries.async_forward_entry_setup( + entry, LIGHT_DOMAIN)) + return True + + +async def _async_has_devices(hass): + """Return if there are devices that can be discovered.""" + import aiolifx + + manager = DiscoveryManager() + lifx_discovery = aiolifx.LifxDiscovery(hass.loop, manager) + coro = hass.loop.create_datagram_endpoint( + lambda: lifx_discovery, + family=socket.AF_INET) + hass.async_create_task(coro) + + has_devices = await manager.found_devices() + lifx_discovery.cleanup() + + return has_devices + + +config_entry_flow.register_discovery_flow( + DOMAIN, 'LIFX', _async_has_devices, config_entries.CONN_CLASS_LOCAL_POLL) + + +class DiscoveryManager: + """Temporary LIFX manager for discovering any bulb.""" + + def __init__(self): + """Initialize the manager.""" + self._event = asyncio.Event() + + async def found_devices(self): + """Return whether any device could be discovered.""" + try: + async with async_timeout.timeout(2): + await self._event.wait() + + # Let bulbs recover from the discovery + await asyncio.sleep(1) + + return True + except asyncio.TimeoutError: + return False + + def register(self, bulb): + """Handle aiolifx detected bulb.""" + self._event.set() + + def unregister(self, bulb): + """Handle aiolifx disappearing bulbs.""" + pass diff --git a/homeassistant/components/lifx/strings.json b/homeassistant/components/lifx/strings.json new file mode 100644 index 00000000000..300c9b628f3 --- /dev/null +++ b/homeassistant/components/lifx/strings.json @@ -0,0 +1,15 @@ +{ + "config": { + "title": "LIFX", + "step": { + "confirm": { + "title": "LIFX", + "description": "Do you want to set up LIFX?" + } + }, + "abort": { + "single_instance_allowed": "Only a single configuration of LIFX is possible.", + "no_devices_found": "No LIFX devices found on the network." + } + } +} diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index 9dcd2ae4cc2..87b3b02dd16 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.lifx/ """ import asyncio +import socket from datetime import timedelta from functools import partial import logging @@ -17,10 +18,12 @@ from homeassistant import util from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, ATTR_KELVIN, ATTR_RGB_COLOR, ATTR_TRANSITION, - ATTR_XY_COLOR, COLOR_GROUP, DOMAIN, LIGHT_TURN_ON_SCHEMA, PLATFORM_SCHEMA, + ATTR_XY_COLOR, COLOR_GROUP, DOMAIN, LIGHT_TURN_ON_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_TRANSITION, VALID_BRIGHTNESS, VALID_BRIGHTNESS_PCT, Light, preprocess_turn_on_alternatives) +from homeassistant.components.lifx import ( + DOMAIN as LIFX_DOMAIN, CONF_SERVER, CONF_BROADCAST) from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -30,23 +33,16 @@ import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['aiolifx==0.6.3', 'aiolifx_effects==0.2.1'] +DEPENDENCIES = ['lifx'] +REQUIREMENTS = ['aiolifx_effects==0.2.1'] -UDP_BROADCAST_PORT = 56700 +SCAN_INTERVAL = timedelta(seconds=10) DISCOVERY_INTERVAL = 60 MESSAGE_TIMEOUT = 1.0 MESSAGE_RETRIES = 8 UNAVAILABLE_GRACE = 90 -CONF_SERVER = 'server' -CONF_BROADCAST = 'broadcast' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_SERVER, default='0.0.0.0'): cv.string, - vol.Optional(CONF_BROADCAST, default='255.255.255.255'): cv.string, -}) - SERVICE_LIFX_SET_STATE = 'lifx_set_state' ATTR_INFRARED = 'infrared' @@ -138,24 +134,33 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the LIFX platform.""" + """Set up the LIFX light platform. Obsolete.""" + _LOGGER.warning('LIFX no longer works with light platform configuration.') + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up LIFX from a config entry.""" if sys.platform == 'win32': _LOGGER.warning("The lifx platform is known to not work on Windows. " "Consider using the lifx_legacy platform instead") - server_addr = config.get(CONF_SERVER) + config = hass.data[LIFX_DOMAIN].get(DOMAIN, {}) lifx_manager = LIFXManager(hass, async_add_entities) - lifx_discovery = aiolifx().LifxDiscovery( - hass.loop, - lifx_manager, - discovery_interval=DISCOVERY_INTERVAL, - broadcast_ip=config.get(CONF_BROADCAST)) - coro = hass.loop.create_datagram_endpoint( - lambda: lifx_discovery, local_addr=(server_addr, UDP_BROADCAST_PORT)) + broadcast_ip = config.get(CONF_BROADCAST) + kwargs = {'discovery_interval': DISCOVERY_INTERVAL} + if broadcast_ip: + kwargs['broadcast_ip'] = broadcast_ip + lifx_discovery = aiolifx().LifxDiscovery(hass.loop, lifx_manager, **kwargs) - hass.async_add_job(coro) + kwargs = {'family': socket.AF_INET} + local_addr = config.get(CONF_SERVER) + if local_addr is not None: + kwargs['local_addr'] = (local_addr, 0) + coro = hass.loop.create_datagram_endpoint(lambda: lifx_discovery, **kwargs) + + hass.async_create_task(coro) @callback def cleanup(event): @@ -225,7 +230,7 @@ class LIFXManager: for light in self.service_to_entities(service): if service.service == SERVICE_LIFX_SET_STATE: task = light.set_state(**service.data) - tasks.append(self.hass.async_add_job(task)) + tasks.append(self.hass.async_create_task(task)) if tasks: await asyncio.wait(tasks, loop=self.hass.loop) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 1cc2e1362af..a5c44c30ce7 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -142,6 +142,7 @@ FLOWS = [ 'hue', 'ifttt', 'ios', + 'lifx', 'mqtt', 'nest', 'openuv', diff --git a/requirements_all.txt b/requirements_all.txt index 5c5415d9ec6..01d02379371 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -106,7 +106,7 @@ aiohue==1.5.0 # homeassistant.components.sensor.imap aioimaplib==0.7.13 -# homeassistant.components.light.lifx +# homeassistant.components.lifx aiolifx==0.6.3 # homeassistant.components.light.lifx From c12bbddc0be79e8ef5a8c07701a31a3d8f274eb6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 7 Oct 2018 23:26:23 +0200 Subject: [PATCH 022/265] Update frontend to 20181007.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 9a5e7c05a56..27904faec1a 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20181005.0'] +REQUIREMENTS = ['home-assistant-frontend==20181007.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 01d02379371..0efae2f7fcc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -461,7 +461,7 @@ hole==0.3.0 holidays==0.9.7 # homeassistant.components.frontend -home-assistant-frontend==20181005.0 +home-assistant-frontend==20181007.0 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 55b8733fff1..39eed8510e6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -90,7 +90,7 @@ hdate==0.6.3 holidays==0.9.7 # homeassistant.components.frontend -home-assistant-frontend==20181005.0 +home-assistant-frontend==20181007.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From 2de1193fd96cb8e41f738122d27cf57518c91025 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 7 Oct 2018 23:26:46 +0200 Subject: [PATCH 023/265] Update translations --- .../components/hangouts/.translations/no.json | 2 ++ .../components/hue/.translations/no.json | 2 +- .../components/mqtt/.translations/no.json | 7 +++++ .../components/upnp/.translations/ko.json | 2 +- .../components/upnp/.translations/no.json | 29 +++++++++++++++++++ 5 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/upnp/.translations/no.json diff --git a/homeassistant/components/hangouts/.translations/no.json b/homeassistant/components/hangouts/.translations/no.json index c2cdb93c005..d75092da759 100644 --- a/homeassistant/components/hangouts/.translations/no.json +++ b/homeassistant/components/hangouts/.translations/no.json @@ -14,6 +14,7 @@ "data": { "2fa": "2FA Pin" }, + "description": "Tom", "title": "Tofaktorautentisering" }, "user": { @@ -21,6 +22,7 @@ "email": "E-postadresse", "password": "Passord" }, + "description": "Tom", "title": "Google Hangouts p\u00e5logging" } }, diff --git a/homeassistant/components/hue/.translations/no.json b/homeassistant/components/hue/.translations/no.json index 309e9f6a299..02dd6ef7128 100644 --- a/homeassistant/components/hue/.translations/no.json +++ b/homeassistant/components/hue/.translations/no.json @@ -24,6 +24,6 @@ "title": "Link Hub" } }, - "title": "Philips Hue Bridge" + "title": "Philips Hue" } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/no.json b/homeassistant/components/mqtt/.translations/no.json index 412efd3e107..b3f1e4740b9 100644 --- a/homeassistant/components/mqtt/.translations/no.json +++ b/homeassistant/components/mqtt/.translations/no.json @@ -17,6 +17,13 @@ }, "description": "Vennligst oppgi tilkoblingsinformasjonen for din MQTT megler.", "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "Aktiver oppdagelse" + }, + "description": "Vil du konfigurere Home Assistant til \u00e5 koble til MQTT megler gitt av hass.io tillegget {addon}?", + "title": "MQTT megler via Hass.io tillegg" } }, "title": "MQTT" diff --git a/homeassistant/components/upnp/.translations/ko.json b/homeassistant/components/upnp/.translations/ko.json index 4da5bedb9b3..0dd7a16de0b 100644 --- a/homeassistant/components/upnp/.translations/ko.json +++ b/homeassistant/components/upnp/.translations/ko.json @@ -6,7 +6,7 @@ "no_sensors_or_port_mapping": "\ucd5c\uc18c\ud55c \uc13c\uc11c \ud639\uc740 \ud3ec\ud2b8 \ub9e4\ud551\uc744 \ud65c\uc131\ud654 \ud574\uc57c \ud569\ub2c8\ub2e4" }, "error": { - "other": "\uc5d0\ub7ec" + "other": "\ub2e4\ub978" }, "step": { "init": { diff --git a/homeassistant/components/upnp/.translations/no.json b/homeassistant/components/upnp/.translations/no.json new file mode 100644 index 00000000000..fbb1b4afc75 --- /dev/null +++ b/homeassistant/components/upnp/.translations/no.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP / IGD er allerede konfigurert", + "no_devices_discovered": "Ingen UPnP / IGDs oppdaget" + }, + "error": { + "few": "f\u00e5", + "many": "mange", + "one": "en", + "other": "andre", + "two": "to", + "zero": "ingen" + }, + "step": { + "init": { + "title": "UPnP / IGD" + }, + "user": { + "data": { + "enable_sensors": "Legg til trafikk sensorer", + "igd": "UPnP / IGD" + }, + "title": "Konfigurasjonsalternativer for UPnP / IGD" + } + }, + "title": "UPnP / IGD" + } +} \ No newline at end of file From e922dd10ba727cc91ed0bf734e95ab6aa7ce8321 Mon Sep 17 00:00:00 2001 From: Martin Berg <2682426+mbrrg@users.noreply.github.com> Date: Sun, 7 Oct 2018 23:30:09 +0200 Subject: [PATCH 024/265] Init sub-components using global var. (#17220) --- .../components/alarm_control_panel/spc.py | 12 ++-- homeassistant/components/binary_sensor/spc.py | 14 ++--- homeassistant/components/spc.py | 9 +-- .../alarm_control_panel/test_spc.py | 58 ------------------- tests/components/binary_sensor/test_spc.py | 55 ------------------ tests/components/test_spc.py | 3 +- 6 files changed, 13 insertions(+), 138 deletions(-) delete mode 100644 tests/components/alarm_control_panel/test_spc.py delete mode 100644 tests/components/binary_sensor/test_spc.py diff --git a/homeassistant/components/alarm_control_panel/spc.py b/homeassistant/components/alarm_control_panel/spc.py index 9150518022f..2345717d835 100644 --- a/homeassistant/components/alarm_control_panel/spc.py +++ b/homeassistant/components/alarm_control_panel/spc.py @@ -9,8 +9,7 @@ import logging import homeassistant.components.alarm_control_panel as alarm from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.core import callback -from homeassistant.components.spc import ( - ATTR_DISCOVER_AREAS, DATA_API, SIGNAL_UPDATE_ALARM) +from homeassistant.components.spc import (DATA_API, SIGNAL_UPDATE_ALARM) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED) @@ -37,12 +36,9 @@ def _get_alarm_state(area): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the SPC alarm control panel platform.""" - if (discovery_info is None or - discovery_info[ATTR_DISCOVER_AREAS] is None): - return - - async_add_entities([SpcAlarm(area=area, api=hass.data[DATA_API]) - for area in discovery_info[ATTR_DISCOVER_AREAS]]) + api = hass.data[DATA_API] + async_add_entities([SpcAlarm(area=area, api=api) + for area in api.areas.values()]) class SpcAlarm(alarm.AlarmControlPanel): diff --git a/homeassistant/components/binary_sensor/spc.py b/homeassistant/components/binary_sensor/spc.py index c1be72db374..25baf503399 100644 --- a/homeassistant/components/binary_sensor/spc.py +++ b/homeassistant/components/binary_sensor/spc.py @@ -9,8 +9,7 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.core import callback -from homeassistant.components.spc import ( - ATTR_DISCOVER_DEVICES, SIGNAL_UPDATE_SENSOR) +from homeassistant.components.spc import (DATA_API, SIGNAL_UPDATE_SENSOR) _LOGGER = logging.getLogger(__name__) @@ -27,13 +26,10 @@ def _get_device_class(zone_type): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the SPC binary sensor.""" - if (discovery_info is None or - discovery_info[ATTR_DISCOVER_DEVICES] is None): - return - - async_add_entities(SpcBinarySensor(zone) - for zone in discovery_info[ATTR_DISCOVER_DEVICES] - if _get_device_class(zone.type)) + api = hass.data[DATA_API] + async_add_entities([SpcBinarySensor(zone) + for zone in api.zones.values() + if _get_device_class(zone.type)]) class SpcBinarySensor(BinarySensorDevice): diff --git a/homeassistant/components/spc.py b/homeassistant/components/spc.py index b00a4aeed2c..0771608f88e 100644 --- a/homeassistant/components/spc.py +++ b/homeassistant/components/spc.py @@ -16,9 +16,6 @@ REQUIREMENTS = ['pyspcwebgw==0.4.0'] _LOGGER = logging.getLogger(__name__) -ATTR_DISCOVER_DEVICES = 'devices' -ATTR_DISCOVER_AREAS = 'areas' - CONF_WS_URL = 'ws_url' CONF_API_URL = 'api_url' @@ -66,13 +63,11 @@ async def async_setup(hass, config): # add sensor devices for each zone (typically motion/fire/door sensors) hass.async_create_task(discovery.async_load_platform( - hass, 'binary_sensor', DOMAIN, - {ATTR_DISCOVER_DEVICES: spc.zones.values()}, config)) + hass, 'binary_sensor', DOMAIN)) # create a separate alarm panel for each area hass.async_create_task(discovery.async_load_platform( - hass, 'alarm_control_panel', DOMAIN, - {ATTR_DISCOVER_AREAS: spc.areas.values()}, config)) + hass, 'alarm_control_panel', DOMAIN)) # start listening for incoming events over websocket spc.start() diff --git a/tests/components/alarm_control_panel/test_spc.py b/tests/components/alarm_control_panel/test_spc.py deleted file mode 100644 index b1078e1b14f..00000000000 --- a/tests/components/alarm_control_panel/test_spc.py +++ /dev/null @@ -1,58 +0,0 @@ -"""Tests for Vanderbilt SPC alarm control panel platform.""" -from homeassistant.components.alarm_control_panel import spc -from homeassistant.const import ( - STATE_ALARM_ARMED_AWAY, STATE_ALARM_DISARMED) -from homeassistant.components.spc import (DATA_API) - - -async def test_setup_platform(hass): - """Test adding areas as separate alarm control panel devices.""" - added_entities = [] - - def add_entities(entities): - nonlocal added_entities - added_entities = list(entities) - - area_defs = [{ - 'id': '1', - 'name': 'House', - 'mode': '3', - 'last_set_time': '1485759851', - 'last_set_user_id': '1', - 'last_set_user_name': 'Pelle', - 'last_unset_time': '1485800564', - 'last_unset_user_id': '1', - 'last_unset_user_name': 'Lisa', - 'last_alarm': '1478174896' - }, { - 'id': '3', - 'name': 'Garage', - 'mode': '0', - 'last_set_time': '1483705803', - 'last_set_user_id': '9998', - 'last_set_user_name': 'Pelle', - 'last_unset_time': '1483705808', - 'last_unset_user_id': '9998', - 'last_unset_user_name': 'Lisa' - }] - - from pyspcwebgw import Area - - areas = [Area(gateway=None, spc_area=a) for a in area_defs] - - hass.data[DATA_API] = None - - await spc.async_setup_platform(hass=hass, - config={}, - async_add_entities=add_entities, - discovery_info={'areas': areas}) - - assert len(added_entities) == 2 - - assert added_entities[0].name == 'House' - assert added_entities[0].state == STATE_ALARM_ARMED_AWAY - assert added_entities[0].changed_by == 'Pelle' - - assert added_entities[1].name == 'Garage' - assert added_entities[1].state == STATE_ALARM_DISARMED - assert added_entities[1].changed_by == 'Lisa' diff --git a/tests/components/binary_sensor/test_spc.py b/tests/components/binary_sensor/test_spc.py deleted file mode 100644 index ec0886aeed8..00000000000 --- a/tests/components/binary_sensor/test_spc.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Tests for Vanderbilt SPC binary sensor platform.""" -from homeassistant.components.binary_sensor import spc - - -async def test_setup_platform(hass): - """Test autodiscovery of supported device types.""" - added_entities = [] - - zone_defs = [{ - 'id': '1', - 'type': '3', - 'zone_name': 'Kitchen smoke', - 'area': '1', - 'area_name': 'House', - 'input': '0', - 'status': '0', - }, { - 'id': '3', - 'type': '0', - 'zone_name': 'Hallway PIR', - 'area': '1', - 'area_name': 'House', - 'input': '0', - 'status': '0', - }, { - 'id': '5', - 'type': '1', - 'zone_name': 'Front door', - 'area': '1', - 'area_name': 'House', - 'input': '1', - 'status': '0', - }] - - def add_entities(entities): - nonlocal added_entities - added_entities = list(entities) - - from pyspcwebgw import Zone - - zones = [Zone(area=None, spc_zone=z) for z in zone_defs] - - await spc.async_setup_platform(hass=hass, - config={}, - async_add_entities=add_entities, - discovery_info={'devices': zones}) - - assert len(added_entities) == 3 - assert added_entities[0].device_class == 'smoke' - assert added_entities[0].state == 'off' - assert added_entities[1].device_class == 'motion' - assert added_entities[1].state == 'off' - assert added_entities[2].device_class == 'opening' - assert added_entities[2].state == 'on' - assert all(d.hidden for d in added_entities) diff --git a/tests/components/test_spc.py b/tests/components/test_spc.py index d4bedda4e96..bcbf970a48b 100644 --- a/tests/components/test_spc.py +++ b/tests/components/test_spc.py @@ -59,7 +59,8 @@ async def test_update_alarm_device(hass): return_value=mock_coro(True)): assert await async_setup_component(hass, 'spc', config) is True - await hass.async_block_till_done() + await hass.async_block_till_done() + entity_id = 'alarm_control_panel.house' assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY From 59d78b060f7b933cdc70e5da3928adb300984777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Mon, 8 Oct 2018 08:36:32 +0200 Subject: [PATCH 025/265] danielhiversen as codeowner for met.no (#17232) --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/CODEOWNERS b/CODEOWNERS index 603a6467d5b..b3ba8ff564d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -132,6 +132,7 @@ homeassistant/components/vacuum/roomba.py @pschmitt homeassistant/components/weather/__init__.py @fabaff homeassistant/components/weather/darksky.py @fabaff homeassistant/components/weather/demo.py @fabaff +homeassistant/components/weather/met.py @danielhiversen homeassistant/components/weather/openweathermap.py @fabaff homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi From 4b7f85518fafa7a600a8964df9268ae2e065e1ce Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 8 Oct 2018 09:30:40 +0200 Subject: [PATCH 026/265] Prevent accidental device reg override (#17136) --- homeassistant/helpers/entity_platform.py | 27 +++++++++------ tests/helpers/test_entity_platform.py | 42 ++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 10 deletions(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index f2913e37339..99aa10013ab 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -275,17 +275,24 @@ class EntityPlatform: device_info = entity.device_info if config_entry_id is not None and device_info is not None: + processed_dev_info = { + 'config_entry_id': config_entry_id + } + for key in ( + 'connections', + 'identifiers', + 'manufacturer', + 'model', + 'name', + 'sw_version', + 'via_hub', + ): + if key in device_info: + processed_dev_info[key] = device_info[key] + device = device_registry.async_get_or_create( - config_entry_id=config_entry_id, - connections=device_info.get('connections') or set(), - identifiers=device_info.get('identifiers') or set(), - manufacturer=device_info.get('manufacturer'), - model=device_info.get('model'), - name=device_info.get('name'), - sw_version=device_info.get('sw_version'), - via_hub=device_info.get('via_hub')) - if device: - device_id = device.id + **processed_dev_info) + device_id = device.id else: device_id = None diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 631d446d186..97d6a0f5b98 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -728,3 +728,45 @@ async def test_device_info_called(hass): assert device.name == 'test-name' assert device.sw_version == 'test-sw' assert device.hub_device_id == hub.id + + +async def test_device_info_not_overrides(hass): + """Test device info is forwarded correctly.""" + registry = await hass.helpers.device_registry.async_get_registry() + device = registry.async_get_or_create( + config_entry_id='bla', + connections={('mac', 'abcd')}, + manufacturer='test-manufacturer', + model='test-model' + ) + + assert device.manufacturer == 'test-manufacturer' + assert device.model == 'test-model' + + async def async_setup_entry(hass, config_entry, async_add_entities): + """Mock setup entry method.""" + async_add_entities([ + MockEntity(unique_id='qwer', device_info={ + 'connections': {('mac', 'abcd')}, + }), + ]) + return True + + platform = MockPlatform( + async_setup_entry=async_setup_entry + ) + config_entry = MockConfigEntry(entry_id='super-mock-id') + entity_platform = MockEntityPlatform( + hass, + platform_name=config_entry.domain, + platform=platform + ) + + assert await entity_platform.async_setup_entry(config_entry) + await hass.async_block_till_done() + + device2 = registry.async_get_device(set(), {('mac', 'abcd')}) + assert device2 is not None + assert device.id == device2.id + assert device2.manufacturer == 'test-manufacturer' + assert device2.model == 'test-model' From 315f83e1ea79c129ca08dffa614d15136a0b7135 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Mon, 8 Oct 2018 09:32:01 +0200 Subject: [PATCH 027/265] Add some new model names of Xiaomi Aqara devices (#17234) * Add additional model name of the Xiaomi Aqara Button (WXKG11LM) * Add additional model name of the Xiaomi Aqara Wireless Switch (WXKG02LM, WXKG03LM) * Bump PyXiaomiGateway version --- homeassistant/components/binary_sensor/xiaomi_aqara.py | 9 ++++++--- homeassistant/components/xiaomi_aqara.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/binary_sensor/xiaomi_aqara.py b/homeassistant/components/binary_sensor/xiaomi_aqara.py index 730b662b90b..63a7c6b68a3 100644 --- a/homeassistant/components/binary_sensor/xiaomi_aqara.py +++ b/homeassistant/components/binary_sensor/xiaomi_aqara.py @@ -36,21 +36,24 @@ def setup_platform(hass, config, add_entities, discovery_info=None): elif model in ['natgas', 'sensor_natgas']: devices.append(XiaomiNatgasSensor(device, gateway)) elif model in ['switch', 'sensor_switch', - 'sensor_switch.aq2', 'sensor_switch.aq3']: + 'sensor_switch.aq2', 'sensor_switch.aq3', + 'remote.b1acn01']: if 'proto' not in device or int(device['proto'][0:1]) == 1: data_key = 'status' else: data_key = 'button_0' devices.append(XiaomiButton(device, 'Switch', data_key, hass, gateway)) - elif model in ['86sw1', 'sensor_86sw1', 'sensor_86sw1.aq1']: + elif model in ['86sw1', 'sensor_86sw1', 'sensor_86sw1.aq1', + 'remote.b186acn01']: if 'proto' not in device or int(device['proto'][0:1]) == 1: data_key = 'channel_0' else: data_key = 'button_0' devices.append(XiaomiButton(device, 'Wall Switch', data_key, hass, gateway)) - elif model in ['86sw2', 'sensor_86sw2', 'sensor_86sw2.aq1']: + elif model in ['86sw2', 'sensor_86sw2', 'sensor_86sw2.aq1', + 'remote.b286acn01']: if 'proto' not in device or int(device['proto'][0:1]) == 1: data_key_left = 'channel_0' data_key_right = 'channel_1' diff --git a/homeassistant/components/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara.py index 27414a64150..aa2102ca805 100644 --- a/homeassistant/components/xiaomi_aqara.py +++ b/homeassistant/components/xiaomi_aqara.py @@ -22,7 +22,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow from homeassistant.util import slugify -REQUIREMENTS = ['PyXiaomiGateway==0.11.0'] +REQUIREMENTS = ['PyXiaomiGateway==0.11.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 0efae2f7fcc..6cb8a94b1ad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -58,7 +58,7 @@ PyRMVtransport==0.1 PySwitchbot==0.3 # homeassistant.components.xiaomi_aqara -PyXiaomiGateway==0.11.0 +PyXiaomiGateway==0.11.1 # homeassistant.components.rpi_gpio # RPi.GPIO==0.6.1 From 849665b9ca2eba19b505bdd9a63906cb97e75cf2 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 8 Oct 2018 09:32:14 +0200 Subject: [PATCH 028/265] Pushed to version 0.7.6 of denonavr library to add more sound modes (#17227) --- homeassistant/components/media_player/denonavr.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/denonavr.py b/homeassistant/components/media_player/denonavr.py index 296548dd3c2..bf934311303 100644 --- a/homeassistant/components/media_player/denonavr.py +++ b/homeassistant/components/media_player/denonavr.py @@ -21,7 +21,7 @@ from homeassistant.const import ( STATE_PAUSED, STATE_PLAYING) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['denonavr==0.7.5'] +REQUIREMENTS = ['denonavr==0.7.6'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 6cb8a94b1ad..e9bc6de55d7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -282,7 +282,7 @@ defusedxml==0.5.0 deluge-client==1.4.0 # homeassistant.components.media_player.denonavr -denonavr==0.7.5 +denonavr==0.7.6 # homeassistant.components.media_player.directv directpy==0.5 From a54e24224508b22522b2a67e298e796b36e4514b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 8 Oct 2018 10:20:18 +0200 Subject: [PATCH 029/265] Fix SPC (#17236) --- homeassistant/components/alarm_control_panel/spc.py | 2 ++ homeassistant/components/binary_sensor/spc.py | 2 ++ homeassistant/components/spc.py | 4 ++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/spc.py b/homeassistant/components/alarm_control_panel/spc.py index 2345717d835..b4c49d4d190 100644 --- a/homeassistant/components/alarm_control_panel/spc.py +++ b/homeassistant/components/alarm_control_panel/spc.py @@ -36,6 +36,8 @@ def _get_alarm_state(area): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the SPC alarm control panel platform.""" + if discovery_info is None: + return api = hass.data[DATA_API] async_add_entities([SpcAlarm(area=area, api=api) for area in api.areas.values()]) diff --git a/homeassistant/components/binary_sensor/spc.py b/homeassistant/components/binary_sensor/spc.py index 25baf503399..baa25266804 100644 --- a/homeassistant/components/binary_sensor/spc.py +++ b/homeassistant/components/binary_sensor/spc.py @@ -26,6 +26,8 @@ def _get_device_class(zone_type): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the SPC binary sensor.""" + if discovery_info is None: + return api = hass.data[DATA_API] async_add_entities([SpcBinarySensor(zone) for zone in api.zones.values() diff --git a/homeassistant/components/spc.py b/homeassistant/components/spc.py index 0771608f88e..5aa987bd0a8 100644 --- a/homeassistant/components/spc.py +++ b/homeassistant/components/spc.py @@ -63,11 +63,11 @@ async def async_setup(hass, config): # add sensor devices for each zone (typically motion/fire/door sensors) hass.async_create_task(discovery.async_load_platform( - hass, 'binary_sensor', DOMAIN)) + hass, 'binary_sensor', DOMAIN, {})) # create a separate alarm panel for each area hass.async_create_task(discovery.async_load_platform( - hass, 'alarm_control_panel', DOMAIN)) + hass, 'alarm_control_panel', DOMAIN, {})) # start listening for incoming events over websocket spc.start() From ff4204244b28361aba0aa0c1a827cd40fd7b2425 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Mon, 8 Oct 2018 10:37:27 +0200 Subject: [PATCH 030/265] Fix data_key and power_consumed attribute of the Aqara Wall Switch (Closes: #16457) (#17235) --- homeassistant/components/switch/xiaomi_aqara.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switch/xiaomi_aqara.py b/homeassistant/components/switch/xiaomi_aqara.py index 17265d5dfa2..2d2ba244ba0 100644 --- a/homeassistant/components/switch/xiaomi_aqara.py +++ b/homeassistant/components/switch/xiaomi_aqara.py @@ -16,6 +16,7 @@ ATTR_IN_USE = 'in_use' LOAD_POWER = 'load_power' POWER_CONSUMED = 'power_consumed' +ENERGY_CONSUMED = 'energy_consumed' IN_USE = 'inuse' @@ -57,8 +58,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): 'channel_1', False, gateway)) elif model in ['86plug', 'ctrl_86plug', 'ctrl_86plug.aq1']: + if 'proto' not in device or int(device['proto'][0:1]) == 1: + data_key = 'status' + else: + data_key = 'channel_0' devices.append(XiaomiGenericSwitch(device, 'Wall Plug', - 'status', True, gateway)) + data_key, True, gateway)) add_entities(devices) @@ -122,8 +127,12 @@ class XiaomiGenericSwitch(XiaomiDevice, SwitchDevice): self._in_use = int(data[IN_USE]) if not self._in_use: self._load_power = 0 - if POWER_CONSUMED in data: - self._power_consumed = round(float(data[POWER_CONSUMED]), 2) + + for key in [POWER_CONSUMED, ENERGY_CONSUMED]: + if key in data: + self._power_consumed = round(float(data[key]), 2) + break + if LOAD_POWER in data: self._load_power = round(float(data[LOAD_POWER]), 2) From f2d8f3bcb86a02870de966a940117337005fc6ed Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Mon, 8 Oct 2018 04:38:07 -0400 Subject: [PATCH 031/265] Water heater support (#17058) * Moved econet to water_heater * Wink and Econet are now in water heater! * Removed away mode from econet and added demo water heater * Added demo tests * Updated coveragerc * Fix lint issues. * updated requirements all * Requirements all actually updated. * Reset wink and econet and fixed service. * Reset wink and econet to the correct dev state * Reset requirements_all and .coveragerc and removed the new econet and wink water_heater files * Removed @bind_hass service methods * Actually reset the .coverage file * Fixed the tests * Addressed @MartinHjelmare's comments * Removed unused import * Switched to async_add_executor_job * Fixed lint * Removed is_on * Added celsius demo water heater and tests. * Removed metric import --- .../components/water_heater/__init__.py | 263 ++++++++++++++++++ homeassistant/components/water_heater/demo.py | 110 ++++++++ .../components/water_heater/services.yaml | 29 ++ tests/components/water_heater/__init__.py | 1 + tests/components/water_heater/common.py | 51 ++++ tests/components/water_heater/test_demo.py | 118 ++++++++ 6 files changed, 572 insertions(+) create mode 100644 homeassistant/components/water_heater/__init__.py create mode 100644 homeassistant/components/water_heater/demo.py create mode 100644 homeassistant/components/water_heater/services.yaml create mode 100644 tests/components/water_heater/__init__.py create mode 100644 tests/components/water_heater/common.py create mode 100644 tests/components/water_heater/test_demo.py diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py new file mode 100644 index 00000000000..92dbebc4421 --- /dev/null +++ b/homeassistant/components/water_heater/__init__.py @@ -0,0 +1,263 @@ +""" +Provides functionality to interact with water heater devices. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/water_heater/ +""" +from datetime import timedelta +import logging +import functools as ft + +import voluptuous as vol + +from homeassistant.helpers.temperature import display_temp as show_temp +from homeassistant.util.temperature import convert as convert_temperature +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa +import homeassistant.helpers.config_validation as cv +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_TURN_ON, SERVICE_TURN_OFF, + STATE_ON, STATE_OFF, TEMP_CELSIUS, PRECISION_WHOLE, + PRECISION_TENTHS, TEMP_FAHRENHEIT) + +DEFAULT_MIN_TEMP = 110 +DEFAULT_MAX_TEMP = 140 + +DOMAIN = 'water_heater' + +ENTITY_ID_FORMAT = DOMAIN + '.{}' +SCAN_INTERVAL = timedelta(seconds=60) + +SERVICE_SET_AWAY_MODE = 'set_away_mode' +SERVICE_SET_TEMPERATURE = 'set_temperature' +SERVICE_SET_OPERATION_MODE = 'set_operation_mode' + +STATE_ECO = 'eco' +STATE_ELECTRIC = 'electric' +STATE_PERFORMANCE = 'performance' +STATE_HIGH_DEMAND = 'high_demand' +STATE_HEAT_PUMP = 'heat_pump' +STATE_GAS = 'gas' + +SUPPORT_TARGET_TEMPERATURE = 1 +SUPPORT_OPERATION_MODE = 2 +SUPPORT_AWAY_MODE = 4 + +ATTR_MAX_TEMP = 'max_temp' +ATTR_MIN_TEMP = 'min_temp' +ATTR_AWAY_MODE = 'away_mode' +ATTR_OPERATION_MODE = 'operation_mode' +ATTR_OPERATION_LIST = 'operation_list' + +CONVERTIBLE_ATTRIBUTE = [ + ATTR_TEMPERATURE, +] + +_LOGGER = logging.getLogger(__name__) + +ON_OFF_SERVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, +}) + +SET_AWAY_MODE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_AWAY_MODE): cv.boolean, +}) +SET_TEMPERATURE_SCHEMA = vol.Schema(vol.All( + { + vol.Required(ATTR_TEMPERATURE, 'temperature'): vol.Coerce(float), + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_OPERATION_MODE): cv.string, + } +)) +SET_OPERATION_MODE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_OPERATION_MODE): cv.string, +}) + + +async def async_setup(hass, config): + """Set up water_heater devices.""" + component = hass.data[DOMAIN] = \ + EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) + await component.async_setup(config) + + component.async_register_entity_service( + SERVICE_SET_AWAY_MODE, SET_AWAY_MODE_SCHEMA, + async_service_away_mode + ) + component.async_register_entity_service( + SERVICE_SET_TEMPERATURE, SET_TEMPERATURE_SCHEMA, + async_service_temperature_set + ) + component.async_register_entity_service( + SERVICE_SET_OPERATION_MODE, SET_OPERATION_MODE_SCHEMA, + 'async_set_operation_mode' + ) + component.async_register_entity_service( + SERVICE_TURN_OFF, ON_OFF_SERVICE_SCHEMA, + 'async_turn_off' + ) + component.async_register_entity_service( + SERVICE_TURN_ON, ON_OFF_SERVICE_SCHEMA, + 'async_turn_on' + ) + + return True + + +async def async_setup_entry(hass, entry): + """Set up a config entry.""" + return await hass.data[DOMAIN].async_setup_entry(entry) + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + return await hass.data[DOMAIN].async_unload_entry(entry) + + +class WaterHeaterDevice(Entity): + """Representation of a water_heater device.""" + + @property + def state(self): + """Return the current state.""" + return self.current_operation + + @property + def precision(self): + """Return the precision of the system.""" + if self.hass.config.units.temperature_unit == TEMP_CELSIUS: + return PRECISION_TENTHS + return PRECISION_WHOLE + + @property + def state_attributes(self): + """Return the optional state attributes.""" + data = { + ATTR_MIN_TEMP: show_temp( + self.hass, self.min_temp, self.temperature_unit, + self.precision), + ATTR_MAX_TEMP: show_temp( + self.hass, self.max_temp, self.temperature_unit, + self.precision), + ATTR_TEMPERATURE: show_temp( + self.hass, self.target_temperature, self.temperature_unit, + self.precision), + } + + supported_features = self.supported_features + + if supported_features & SUPPORT_OPERATION_MODE: + data[ATTR_OPERATION_MODE] = self.current_operation + if self.operation_list: + data[ATTR_OPERATION_LIST] = self.operation_list + + if supported_features & SUPPORT_AWAY_MODE: + is_away = self.is_away_mode_on + data[ATTR_AWAY_MODE] = STATE_ON if is_away else STATE_OFF + + return data + + @property + def temperature_unit(self): + """Return the unit of measurement used by the platform.""" + raise NotImplementedError + + @property + def current_operation(self): + """Return current operation ie. eco, electric, performance, ...""" + return None + + @property + def operation_list(self): + """Return the list of available operation modes.""" + return None + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return None + + @property + def is_away_mode_on(self): + """Return true if away mode is on.""" + return None + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + raise NotImplementedError() + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + await self.hass.async_add_executor_job( + ft.partial(self.set_temperature, **kwargs)) + + def set_operation_mode(self, operation_mode): + """Set new target operation mode.""" + raise NotImplementedError() + + async def async_set_operation_mode(self, operation_mode): + """Set new target operation mode.""" + await self.hass.async_add_executor_job(self.set_operation_mode, + operation_mode) + + def turn_away_mode_on(self): + """Turn away mode on.""" + raise NotImplementedError() + + async def async_turn_away_mode_on(self): + """Turn away mode on.""" + await self.hass.async_add_executor_job(self.turn_away_mode_on) + + def turn_away_mode_off(self): + """Turn away mode off.""" + raise NotImplementedError() + + async def async_turn_away_mode_off(self): + """Turn away mode off.""" + await self.hass.async_add_executor_job(self.turn_away_mode_off) + + @property + def supported_features(self): + """Return the list of supported features.""" + raise NotImplementedError() + + @property + def min_temp(self): + """Return the minimum temperature.""" + return convert_temperature(DEFAULT_MIN_TEMP, TEMP_FAHRENHEIT, + self.temperature_unit) + + @property + def max_temp(self): + """Return the maximum temperature.""" + return convert_temperature(DEFAULT_MAX_TEMP, TEMP_FAHRENHEIT, + self.temperature_unit) + + +async def async_service_away_mode(entity, service): + """Handle away mode service.""" + if service.data[ATTR_AWAY_MODE]: + await entity.async_turn_away_mode_on() + else: + await entity.async_turn_away_mode_off() + + +async def async_service_temperature_set(entity, service): + """Handle set temperature service.""" + hass = entity.hass + kwargs = {} + + for value, temp in service.data.items(): + if value in CONVERTIBLE_ATTRIBUTE: + kwargs[value] = convert_temperature( + temp, + hass.config.units.temperature_unit, + entity.temperature_unit + ) + else: + kwargs[value] = temp + + await entity.async_set_temperature(**kwargs) diff --git a/homeassistant/components/water_heater/demo.py b/homeassistant/components/water_heater/demo.py new file mode 100644 index 00000000000..89b86c12af4 --- /dev/null +++ b/homeassistant/components/water_heater/demo.py @@ -0,0 +1,110 @@ +""" +Demo platform that offers a fake water_heater device. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/demo/ +""" +from homeassistant.components.water_heater import ( + WaterHeaterDevice, + SUPPORT_TARGET_TEMPERATURE, + SUPPORT_AWAY_MODE, + SUPPORT_OPERATION_MODE) +from homeassistant.const import TEMP_FAHRENHEIT, ATTR_TEMPERATURE, TEMP_CELSIUS + +SUPPORT_FLAGS_HEATER = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | + SUPPORT_AWAY_MODE) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Demo water_heater devices.""" + add_entities([ + DemoWaterHeater('Demo Water Heater', 119, + TEMP_FAHRENHEIT, False, 'eco'), + DemoWaterHeater('Demo Water Heater Celsius', 45, + TEMP_CELSIUS, True, 'eco') + + ]) + + +class DemoWaterHeater(WaterHeaterDevice): + """Representation of a demo water_heater device.""" + + def __init__(self, name, target_temperature, unit_of_measurement, + away, current_operation): + """Initialize the water_heater device.""" + self._name = name + self._support_flags = SUPPORT_FLAGS_HEATER + if target_temperature is not None: + self._support_flags = \ + self._support_flags | SUPPORT_TARGET_TEMPERATURE + if away is not None: + self._support_flags = self._support_flags | SUPPORT_AWAY_MODE + if current_operation is not None: + self._support_flags = self._support_flags | SUPPORT_OPERATION_MODE + self._target_temperature = target_temperature + self._unit_of_measurement = unit_of_measurement + self._away = away + self._current_operation = current_operation + self._operation_list = ['eco', 'electric', 'performance', + 'high_demand', 'heat_pump', 'gas', + 'off'] + + @property + def supported_features(self): + """Return the list of supported features.""" + return self._support_flags + + @property + def should_poll(self): + """Return the polling state.""" + return False + + @property + def name(self): + """Return the name of the water_heater device.""" + return self._name + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return self._unit_of_measurement + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._target_temperature + + @property + def current_operation(self): + """Return current operation ie. heat, cool, idle.""" + return self._current_operation + + @property + def operation_list(self): + """Return the list of available operation modes.""" + return self._operation_list + + @property + def is_away_mode_on(self): + """Return if away mode is on.""" + return self._away + + def set_temperature(self, **kwargs): + """Set new target temperatures.""" + self._target_temperature = kwargs.get(ATTR_TEMPERATURE) + self.schedule_update_ha_state() + + def set_operation_mode(self, operation_mode): + """Set new operation mode.""" + self._current_operation = operation_mode + self.schedule_update_ha_state() + + def turn_away_mode_on(self): + """Turn away mode on.""" + self._away = True + self.schedule_update_ha_state() + + def turn_away_mode_off(self): + """Turn away mode off.""" + self._away = False + self.schedule_update_ha_state() diff --git a/homeassistant/components/water_heater/services.yaml b/homeassistant/components/water_heater/services.yaml new file mode 100644 index 00000000000..959d1ca9790 --- /dev/null +++ b/homeassistant/components/water_heater/services.yaml @@ -0,0 +1,29 @@ +# Describes the format for available water_heater services + +set_away_mode: + description: Turn away mode on/off for water_heater device. + fields: + entity_id: + description: Name(s) of entities to change. + example: 'water_heater.water_heater' + away_mode: + description: New value of away mode. + example: true +set_temperature: + description: Set target temperature of water_heater device. + fields: + entity_id: + description: Name(s) of entities to change. + example: 'water_heater.water_heater' + temperature: + description: New target temperature for water heater. + example: 25 +set_operation_mode: + description: Set operation mode for water_heater device. + fields: + entity_id: + description: Name(s) of entities to change. + example: 'water_heater.water_heater' + operation_mode: + description: New value of operation mode. + example: eco diff --git a/tests/components/water_heater/__init__.py b/tests/components/water_heater/__init__.py new file mode 100644 index 00000000000..673119bf16e --- /dev/null +++ b/tests/components/water_heater/__init__.py @@ -0,0 +1 @@ +"""The tests for water heater component.""" diff --git a/tests/components/water_heater/common.py b/tests/components/water_heater/common.py new file mode 100644 index 00000000000..34173e7f110 --- /dev/null +++ b/tests/components/water_heater/common.py @@ -0,0 +1,51 @@ +"""Collection of helper methods. + +All containing methods are legacy helpers that should not be used by new +components. Instead call the service directly. +""" +from homeassistant.components.water_heater import ( + _LOGGER, ATTR_AWAY_MODE, + ATTR_OPERATION_MODE, DOMAIN, SERVICE_SET_AWAY_MODE, + SERVICE_SET_TEMPERATURE, SERVICE_SET_OPERATION_MODE) +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_TEMPERATURE) +from homeassistant.loader import bind_hass + + +@bind_hass +def set_away_mode(hass, away_mode, entity_id=None): + """Turn all or specified water_heater devices away mode on.""" + data = { + ATTR_AWAY_MODE: away_mode + } + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_SET_AWAY_MODE, data) + + +@bind_hass +def set_temperature(hass, temperature=None, entity_id=None, + operation_mode=None): + """Set new target temperature.""" + kwargs = { + key: value for key, value in [ + (ATTR_TEMPERATURE, temperature), + (ATTR_ENTITY_ID, entity_id), + (ATTR_OPERATION_MODE, operation_mode) + ] if value is not None + } + _LOGGER.debug("set_temperature start data=%s", kwargs) + hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, kwargs) + + +@bind_hass +def set_operation_mode(hass, operation_mode, entity_id=None): + """Set new target operation mode.""" + data = {ATTR_OPERATION_MODE: operation_mode} + + if entity_id is not None: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_SET_OPERATION_MODE, data) diff --git a/tests/components/water_heater/test_demo.py b/tests/components/water_heater/test_demo.py new file mode 100644 index 00000000000..14fe57de99c --- /dev/null +++ b/tests/components/water_heater/test_demo.py @@ -0,0 +1,118 @@ +"""The tests for the demo water_heater component.""" +import unittest + +from homeassistant.util.unit_system import ( + IMPERIAL_SYSTEM +) +from homeassistant.setup import setup_component +from homeassistant.components import water_heater + +from tests.common import get_test_home_assistant +from tests.components.water_heater import common + + +ENTITY_WATER_HEATER = 'water_heater.demo_water_heater' +ENTITY_WATER_HEATER_CELSIUS = 'water_heater.demo_water_heater_celsius' + + +class TestDemowater_heater(unittest.TestCase): + """Test the demo water_heater.""" + + def setUp(self): # pylint: disable=invalid-name + """Set up things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.hass.config.units = IMPERIAL_SYSTEM + self.assertTrue(setup_component(self.hass, water_heater.DOMAIN, { + 'water_heater': { + 'platform': 'demo', + }})) + + def tearDown(self): # pylint: disable=invalid-name + """Stop down everything that was started.""" + self.hass.stop() + + def test_setup_params(self): + """Test the initial parameters.""" + state = self.hass.states.get(ENTITY_WATER_HEATER) + self.assertEqual(119, state.attributes.get('temperature')) + self.assertEqual('off', state.attributes.get('away_mode')) + self.assertEqual("eco", state.attributes.get('operation_mode')) + + def test_default_setup_params(self): + """Test the setup with default parameters.""" + state = self.hass.states.get(ENTITY_WATER_HEATER) + self.assertEqual(110, state.attributes.get('min_temp')) + self.assertEqual(140, state.attributes.get('max_temp')) + + def test_set_only_target_temp_bad_attr(self): + """Test setting the target temperature without required attribute.""" + state = self.hass.states.get(ENTITY_WATER_HEATER) + self.assertEqual(119, state.attributes.get('temperature')) + common.set_temperature(self.hass, None, ENTITY_WATER_HEATER) + self.hass.block_till_done() + self.assertEqual(119, state.attributes.get('temperature')) + + def test_set_only_target_temp(self): + """Test the setting of the target temperature.""" + state = self.hass.states.get(ENTITY_WATER_HEATER) + self.assertEqual(119, state.attributes.get('temperature')) + common.set_temperature(self.hass, 110, ENTITY_WATER_HEATER) + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_WATER_HEATER) + self.assertEqual(110, state.attributes.get('temperature')) + + def test_set_operation_bad_attr_and_state(self): + """Test setting operation mode without required attribute. + + Also check the state. + """ + state = self.hass.states.get(ENTITY_WATER_HEATER) + self.assertEqual("eco", state.attributes.get('operation_mode')) + self.assertEqual("eco", state.state) + common.set_operation_mode(self.hass, None, ENTITY_WATER_HEATER) + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_WATER_HEATER) + self.assertEqual("eco", state.attributes.get('operation_mode')) + self.assertEqual("eco", state.state) + + def test_set_operation(self): + """Test setting of new operation mode.""" + state = self.hass.states.get(ENTITY_WATER_HEATER) + self.assertEqual("eco", state.attributes.get('operation_mode')) + self.assertEqual("eco", state.state) + common.set_operation_mode(self.hass, "electric", ENTITY_WATER_HEATER) + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_WATER_HEATER) + self.assertEqual("electric", state.attributes.get('operation_mode')) + self.assertEqual("electric", state.state) + + def test_set_away_mode_bad_attr(self): + """Test setting the away mode without required attribute.""" + state = self.hass.states.get(ENTITY_WATER_HEATER) + self.assertEqual('off', state.attributes.get('away_mode')) + common.set_away_mode(self.hass, None, ENTITY_WATER_HEATER) + self.hass.block_till_done() + self.assertEqual('off', state.attributes.get('away_mode')) + + def test_set_away_mode_on(self): + """Test setting the away mode on/true.""" + common.set_away_mode(self.hass, True, ENTITY_WATER_HEATER) + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_WATER_HEATER) + self.assertEqual('on', state.attributes.get('away_mode')) + + def test_set_away_mode_off(self): + """Test setting the away mode off/false.""" + common.set_away_mode(self.hass, False, ENTITY_WATER_HEATER_CELSIUS) + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_WATER_HEATER_CELSIUS) + self.assertEqual('off', state.attributes.get('away_mode')) + + def test_set_only_target_temp_with_convert(self): + """Test the setting of the target temperature.""" + state = self.hass.states.get(ENTITY_WATER_HEATER_CELSIUS) + self.assertEqual(113, state.attributes.get('temperature')) + common.set_temperature(self.hass, 114, ENTITY_WATER_HEATER_CELSIUS) + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_WATER_HEATER_CELSIUS) + self.assertEqual(114, state.attributes.get('temperature')) From ee5e1fa35563b69aa2ccd078f8ad457ef70579cf Mon Sep 17 00:00:00 2001 From: MatteGary Date: Mon, 8 Oct 2018 10:49:54 +0200 Subject: [PATCH 032/265] Daikin Climate - Better integration with Climate base component (#16913) * Daikin Climate - Better integration with Climate base component Made some modification in order to better integrate the Daikin AC Component with the base Climate Component. Benefits are: Support localization for Operation Mode Support for Homekit Integration (if the AC is turned On, now the status is updated in Homekit) * Build bug fixing * Bug fixing in Set Operation_Mode functionality Fixed to .title() functionality in matching the Operation_Mode * Fix useless code * Fix hound bug * Bug fixing Change in list of Operation Mode * Trailing white space fix * Compile fixing * Whitespace fix --- homeassistant/components/climate/daikin.py | 27 ++++++++++++++-------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/climate/daikin.py b/homeassistant/components/climate/daikin.py index 6743bf034dc..66e380ad68d 100644 --- a/homeassistant/components/climate/daikin.py +++ b/homeassistant/components/climate/daikin.py @@ -40,6 +40,15 @@ HA_STATE_TO_DAIKIN = { STATE_OFF: 'off', } +DAIKIN_TO_HA_STATE = { + 'fan': STATE_FAN_ONLY, + 'dry': STATE_DRY, + 'cool': STATE_COOL, + 'hot': STATE_HEAT, + 'auto': STATE_AUTO, + 'off': STATE_OFF, +} + HA_ATTR_TO_DAIKIN = { ATTR_OPERATION_MODE: 'mode', ATTR_FAN_MODE: 'f_rate', @@ -75,9 +84,7 @@ class DaikinClimate(ClimateDevice): self._api = api self._force_refresh = False self._list = { - ATTR_OPERATION_MODE: list( - map(str.title, set(HA_STATE_TO_DAIKIN.values())) - ), + ATTR_OPERATION_MODE: list(HA_STATE_TO_DAIKIN), ATTR_FAN_MODE: list( map( str.title, @@ -136,11 +143,11 @@ class DaikinClimate(ClimateDevice): elif key == ATTR_OPERATION_MODE: # Daikin can return also internal states auto-1 or auto-7 # and we need to translate them as AUTO - value = re.sub( - '[^a-z]', - '', - self._api.device.represent(daikin_attr)[1] - ).title() + daikin_mode = re.sub( + '[^a-z]', '', + self._api.device.represent(daikin_attr)[1]) + ha_mode = DAIKIN_TO_HA_STATE.get(daikin_mode) + value = ha_mode if value is None: _LOGGER.error("Invalid value requested for key %s", key) @@ -167,8 +174,8 @@ class DaikinClimate(ClimateDevice): daikin_attr = HA_ATTR_TO_DAIKIN.get(attr) if daikin_attr is not None: - if value.title() in self._list[attr]: - values[daikin_attr] = value.lower() + if value in self._list[attr]: + values[daikin_attr] = HA_STATE_TO_DAIKIN[value] else: _LOGGER.error("Invalid value %s for %s", attr, value) From 3f498bd04236fe9c63e048406520dca09101d2fc Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 8 Oct 2018 10:59:43 +0200 Subject: [PATCH 033/265] Fix potential MQTT discovery race condition (#17208) * Fix potential MQTT discovery race condition * Rename data key --- homeassistant/components/mqtt/discovery.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index fdb7948e4bf..a762978a330 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -4,6 +4,7 @@ Support for MQTT discovery. For more details about this component, please refer to the documentation at https://home-assistant.io/components/mqtt/#discovery """ +import asyncio import json import logging import re @@ -51,6 +52,7 @@ CONFIG_ENTRY_PLATFORMS = { } ALREADY_DISCOVERED = 'mqtt_discovered_components' +DATA_CONFIG_ENTRY_LOCK = 'mqtt_config_entry_lock' CONFIG_ENTRY_IS_SETUP = 'mqtt_config_entry_is_setup' MQTT_DISCOVERY_UPDATED = 'mqtt_discovery_updated_{}' MQTT_DISCOVERY_NEW = 'mqtt_discovery_new_{}_{}' @@ -119,14 +121,16 @@ async def async_start(hass: HomeAssistantType, discovery_topic, hass_config, return config_entries_key = '{}.{}'.format(component, platform) - if config_entries_key not in hass.data[CONFIG_ENTRY_IS_SETUP]: - hass.data[CONFIG_ENTRY_IS_SETUP].add(config_entries_key) - await hass.config_entries.async_forward_entry_setup( - config_entry, component) + async with hass.data[DATA_CONFIG_ENTRY_LOCK]: + if config_entries_key not in hass.data[CONFIG_ENTRY_IS_SETUP]: + await hass.config_entries.async_forward_entry_setup( + config_entry, component) + hass.data[CONFIG_ENTRY_IS_SETUP].add(config_entries_key) async_dispatcher_send(hass, MQTT_DISCOVERY_NEW.format( component, platform), payload) + hass.data[DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock() hass.data[CONFIG_ENTRY_IS_SETUP] = set() await mqtt.async_subscribe( From 71a0274b1223f3f766202c94855b925105390a9a Mon Sep 17 00:00:00 2001 From: Daniel Lashua Date: Mon, 8 Oct 2018 05:08:46 -0500 Subject: [PATCH 034/265] Add Support for Xiaomi Vibration Sensor (#16422) --- .../components/binary_sensor/xiaomi_aqara.py | 38 +++++++++++++++++++ .../components/sensor/xiaomi_aqara.py | 12 ++++++ 2 files changed, 50 insertions(+) diff --git a/homeassistant/components/binary_sensor/xiaomi_aqara.py b/homeassistant/components/binary_sensor/xiaomi_aqara.py index 63a7c6b68a3..2b4934ff824 100644 --- a/homeassistant/components/binary_sensor/xiaomi_aqara.py +++ b/homeassistant/components/binary_sensor/xiaomi_aqara.py @@ -68,6 +68,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): 'dual_channel', hass, gateway)) elif model in ['cube', 'sensor_cube', 'sensor_cube.aqgl01']: devices.append(XiaomiCube(device, hass, gateway)) + elif model in ['vibration', 'vibration.aq1']: + devices.append(XiaomiVibration(device, 'Vibration', + 'status', gateway)) + else: + _LOGGER.warning('Unmapped Device Model %s', model) + add_entities(devices) @@ -314,6 +320,38 @@ class XiaomiSmokeSensor(XiaomiBinarySensor): return False +class XiaomiVibration(XiaomiBinarySensor): + """Representation of a Xiaomi Vibration Sensor.""" + + def __init__(self, device, name, data_key, xiaomi_hub): + """Initialize the XiaomiVibration.""" + self._last_action = None + super().__init__(device, name, xiaomi_hub, data_key, None) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + attrs = {ATTR_LAST_ACTION: self._last_action} + attrs.update(super().device_state_attributes) + return attrs + + def parse_data(self, data, raw_data): + """Parse data sent by gateway.""" + value = data.get(self._data_key) + if value not in ('vibrate', 'tilt', 'free_fall'): + _LOGGER.warning("Unsupported movement_type detected: %s", + value) + return False + + self.hass.bus.fire('xiaomi_aqara.movement', { + 'entity_id': self.entity_id, + 'movement_type': value + }) + self._last_action = value + + return True + + class XiaomiButton(XiaomiBinarySensor): """Representation of a Xiaomi Button.""" diff --git a/homeassistant/components/sensor/xiaomi_aqara.py b/homeassistant/components/sensor/xiaomi_aqara.py index 31366fe0097..33517e957b9 100644 --- a/homeassistant/components/sensor/xiaomi_aqara.py +++ b/homeassistant/components/sensor/xiaomi_aqara.py @@ -41,6 +41,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None): elif device['model'] in ['gateway', 'gateway.v3', 'acpartner.v3']: devices.append(XiaomiSensor(device, 'Illumination', 'illumination', gateway)) + elif device['model'] in ['vibration']: + devices.append(XiaomiSensor(device, 'Bed Activity', + 'bed_activity', gateway)) + devices.append(XiaomiSensor(device, 'Tilt Angle', + 'final_tilt_angle', gateway)) + devices.append(XiaomiSensor(device, 'Coordination', + 'coordination', gateway)) + else: + _LOGGER.warning("Unmapped Device Model ") add_entities(devices) @@ -84,6 +93,9 @@ class XiaomiSensor(XiaomiDevice): value = data.get(self._data_key) if value is None: return False + if self._data_key in ['coordination', 'status']: + self._state = value + return True value = float(value) if self._data_key in ['temperature', 'humidity', 'pressure']: value /= 100 From af2402ea592f24b36db28361043e7efc740f7bb2 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 8 Oct 2018 12:53:30 +0200 Subject: [PATCH 035/265] Implement base for MQTT device registry integration (#16943) * Implement base for MQTT device registry integration * Lint * Lint * Address comments * Lint * Lint * Address comments * Only add keys if specified See https://github.com/home-assistant/home-assistant/pull/17136#discussion_r223267185 --- homeassistant/components/mqtt/__init__.py | 66 ++++++++++++++++++++++- homeassistant/components/sensor/mqtt.py | 12 +++-- tests/components/mqtt/test_init.py | 41 ++++++++++++++ tests/components/sensor/test_mqtt.py | 39 ++++++++++++++ 4 files changed, 153 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 3e25563e9ba..2f1895019dd 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -21,7 +21,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import ( CONF_PASSWORD, CONF_PAYLOAD, CONF_PORT, CONF_PROTOCOL, CONF_USERNAME, - CONF_VALUE_TEMPLATE, EVENT_HOMEASSISTANT_STOP) + CONF_VALUE_TEMPLATE, EVENT_HOMEASSISTANT_STOP, CONF_NAME) from homeassistant.core import Event, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv @@ -73,6 +73,12 @@ CONF_PAYLOAD_NOT_AVAILABLE = 'payload_not_available' CONF_QOS = 'qos' CONF_RETAIN = 'retain' +CONF_IDENTIFIERS = 'identifiers' +CONF_CONNECTIONS = 'connections' +CONF_MANUFACTURER = 'manufacturer' +CONF_MODEL = 'model' +CONF_SW_VERSION = 'sw_version' + PROTOCOL_31 = '3.1' PROTOCOL_311 = '3.1.1' @@ -144,6 +150,15 @@ def valid_publish_topic(value: Any) -> str: return value +def validate_device_has_at_least_one_identifier(value: ConfigType) -> \ + ConfigType: + """Validate that a device info entry has at least one identifying value.""" + if not value.get(CONF_IDENTIFIERS) and not value.get(CONF_CONNECTIONS): + raise vol.Invalid("Device must have at least one identifying value in " + "'identifiers' and/or 'connections'") + return value + + _VALID_QOS_SCHEMA = vol.All(vol.Coerce(int), vol.In([0, 1, 2])) CLIENT_KEY_AUTH_MSG = 'client_key and client_cert must both be present in ' \ @@ -198,6 +213,17 @@ MQTT_AVAILABILITY_SCHEMA = vol.Schema({ default=DEFAULT_PAYLOAD_NOT_AVAILABLE): cv.string, }) +MQTT_ENTITY_DEVICE_INFO_SCHEMA = vol.All(vol.Schema({ + vol.Optional(CONF_IDENTIFIERS, default=list): + vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_CONNECTIONS, default=list): + vol.All(cv.ensure_list, [vol.All(vol.Length(2), [cv.string])]), + vol.Optional(CONF_MANUFACTURER): cv.string, + vol.Optional(CONF_MODEL): cv.string, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_SW_VERSION): cv.string, +}), validate_device_has_at_least_one_identifier) + MQTT_BASE_PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(SCHEMA_BASE) # Sensor type platforms subscribe to MQTT events @@ -866,3 +892,41 @@ class MqttDiscoveryUpdate(Entity): self.hass, MQTT_DISCOVERY_UPDATED.format(self._discovery_hash), discovery_callback) + + +class MqttEntityDeviceInfo(Entity): + """Mixin used for mqtt platforms that support the device registry.""" + + def __init__(self, device_config: Optional[ConfigType]) -> None: + """Initialize the device mixin.""" + self._device_config = device_config + + @property + def device_info(self): + """Return a device description for device registry.""" + if not self._device_config: + return None + + info = { + 'identifiers': { + (DOMAIN, id_) + for id_ in self._device_config[CONF_IDENTIFIERS] + }, + 'connections': { + tuple(x) for x in self._device_config[CONF_CONNECTIONS] + } + } + + if CONF_MANUFACTURER in self._device_config: + info['manufacturer'] = self._device_config[CONF_MANUFACTURER] + + if CONF_MODEL in self._device_config: + info['model'] = self._device_config[CONF_MODEL] + + if CONF_NAME in self._device_config: + info['name'] = self._device_config[CONF_NAME] + + if CONF_SW_VERSION in self._device_config: + info['sw_version'] = self._device_config[CONF_SW_VERSION] + + return info diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/sensor/mqtt.py index fe0b77b2024..225ed07a622 100644 --- a/homeassistant/components/sensor/mqtt.py +++ b/homeassistant/components/sensor/mqtt.py @@ -16,12 +16,12 @@ from homeassistant.components import sensor from homeassistant.components.mqtt import ( ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, - MqttAvailability, MqttDiscoveryUpdate) + MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo) from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA from homeassistant.const import ( CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, STATE_UNKNOWN, - CONF_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_DEVICE_CLASS) + CONF_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_DEVICE_CLASS, CONF_DEVICE) from homeassistant.helpers.entity import Entity from homeassistant.components import mqtt import homeassistant.helpers.config_validation as cv @@ -51,6 +51,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ # Integrations shouldn't never expose unique_id through configuration # this here is an exception because MQTT is a msg transport, not a protocol vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @@ -95,22 +96,25 @@ async def _async_setup_entity(hass: HomeAssistantType, config: ConfigType, config.get(CONF_AVAILABILITY_TOPIC), config.get(CONF_PAYLOAD_AVAILABLE), config.get(CONF_PAYLOAD_NOT_AVAILABLE), + config.get(CONF_DEVICE), discovery_hash, )]) -class MqttSensor(MqttAvailability, MqttDiscoveryUpdate, Entity): +class MqttSensor(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, + Entity): """Representation of a sensor that can be updated using MQTT.""" def __init__(self, name, state_topic, qos, unit_of_measurement, force_update, expire_after, icon, device_class: Optional[str], value_template, json_attributes, unique_id: Optional[str], availability_topic, payload_available, payload_not_available, - discovery_hash): + device_config: Optional[ConfigType], discovery_hash): """Initialize the sensor.""" MqttAvailability.__init__(self, availability_topic, qos, payload_available, payload_not_available) MqttDiscoveryUpdate.__init__(self, discovery_hash) + MqttEntityDeviceInfo.__init__(self, device_config) self._state = STATE_UNKNOWN self._name = name self._state_topic = state_topic diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 831bcaa1d24..045a411a271 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -190,6 +190,47 @@ class TestMQTTComponent(unittest.TestCase): # Topic names beginning with $ SHOULD NOT be used, but can mqtt.valid_publish_topic('$SYS/') + def test_entity_device_info_schema(self): + """Test MQTT entity device info validation.""" + # just identifier + mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA({ + 'identifiers': ['abcd'] + }) + mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA({ + 'identifiers': 'abcd' + }) + # just connection + mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA({ + 'connections': [ + ['mac', '02:5b:26:a8:dc:12'], + ] + }) + # full device info + mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA({ + 'identifiers': ['helloworld', 'hello'], + 'connections': [ + ["mac", "02:5b:26:a8:dc:12"], + ["zigbee", "zigbee_id"], + ], + 'manufacturer': 'Whatever', + 'name': 'Beer', + 'model': 'Glass', + 'sw_version': '0.1-beta', + }) + # no identifiers + self.assertRaises(vol.Invalid, mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, { + 'manufacturer': 'Whatever', + 'name': 'Beer', + 'model': 'Glass', + 'sw_version': '0.1-beta', + }) + # empty identifiers + self.assertRaises(vol.Invalid, mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, { + 'identifiers': [], + 'connections': [], + 'name': 'Beer', + }) + # pylint: disable=invalid-name class TestMQTTCallbacks(unittest.TestCase): diff --git a/tests/components/sensor/test_mqtt.py b/tests/components/sensor/test_mqtt.py index 4f70c37e04f..873de5a9bd6 100644 --- a/tests/components/sensor/test_mqtt.py +++ b/tests/components/sensor/test_mqtt.py @@ -1,4 +1,5 @@ """The tests for the MQTT sensor platform.""" +import json import unittest from datetime import timedelta, datetime @@ -411,3 +412,41 @@ async def test_discovery_removal_sensor(hass, mqtt_mock, caplog): await hass.async_block_till_done() state = hass.states.get('sensor.beer') assert state is None + + +async def test_entity_device_info_with_identifier(hass, mqtt_mock): + """Test MQTT sensor device registry integration.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, 'homeassistant', {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + data = json.dumps({ + 'platform': 'mqtt', + 'name': 'Test 1', + 'state_topic': 'test-topic', + 'device': { + 'identifiers': ['helloworld'], + 'connections': [ + ["mac", "02:5b:26:a8:dc:12"], + ], + 'manufacturer': 'Whatever', + 'name': 'Beer', + 'model': 'Glass', + 'sw_version': '0.1-beta', + }, + 'unique_id': 'veryunique' + }) + async_fire_mqtt_message(hass, 'homeassistant/sensor/bla/config', + data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + device = registry.async_get_device({('mqtt', 'helloworld')}, set()) + assert device is not None + assert device.identifiers == {('mqtt', 'helloworld')} + assert device.connections == {('mac', "02:5b:26:a8:dc:12")} + assert device.manufacturer == 'Whatever' + assert device.name == 'Beer' + assert device.model == 'Glass' + assert device.sw_version == '0.1-beta' From d5f5273c3162a22e0568b359acde16a88322e54d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 8 Oct 2018 12:53:51 +0200 Subject: [PATCH 036/265] Guard for bad device info (#17238) --- homeassistant/helpers/entity_platform.py | 6 +++--- tests/helpers/test_entity_platform.py | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 99aa10013ab..3ab45577236 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -273,6 +273,7 @@ class EntityPlatform: config_entry_id = None device_info = entity.device_info + device_id = None if config_entry_id is not None and device_info is not None: processed_dev_info = { @@ -292,9 +293,8 @@ class EntityPlatform: device = device_registry.async_get_or_create( **processed_dev_info) - device_id = device.id - else: - device_id = None + if device: + device_id = device.id entry = entity_registry.async_get_or_create( self.domain, self.platform_name, entity.unique_id, diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 97d6a0f5b98..e985771e486 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -719,6 +719,8 @@ async def test_device_info_called(hass): assert await entity_platform.async_setup_entry(config_entry) await hass.async_block_till_done() + assert len(hass.states.async_entity_ids()) == 2 + device = registry.async_get_device({('hue', '1234')}, set()) assert device is not None assert device.identifiers == {('hue', '1234')} From 394ffc40b60418e8709f460675566f770de628af Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Mon, 8 Oct 2018 22:32:16 +1100 Subject: [PATCH 037/265] updated georss-client library to 0.3 (#17239) --- homeassistant/components/sensor/geo_rss_events.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/geo_rss_events.py b/homeassistant/components/sensor/geo_rss_events.py index 2a9041df357..5085e113e92 100644 --- a/homeassistant/components/sensor/geo_rss_events.py +++ b/homeassistant/components/sensor/geo_rss_events.py @@ -19,7 +19,7 @@ from homeassistant.const import ( STATE_UNKNOWN, CONF_UNIT_OF_MEASUREMENT, CONF_NAME, CONF_RADIUS, CONF_URL) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['georss_client==0.1'] +REQUIREMENTS = ['georss_client==0.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index e9bc6de55d7..027acec7029 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -401,7 +401,7 @@ geizhals==0.0.7 geojson_client==0.1 # homeassistant.components.sensor.geo_rss_events -georss_client==0.1 +georss_client==0.3 # homeassistant.components.sensor.gitter gitterpy==0.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 39eed8510e6..b4e995d580c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -72,7 +72,7 @@ gTTS-token==1.1.2 geojson_client==0.1 # homeassistant.components.sensor.geo_rss_events -georss_client==0.1 +georss_client==0.3 # homeassistant.components.ffmpeg ha-ffmpeg==1.9 From 27f0331f0f56e0aa0658b42ae38b80fbc8b211f0 Mon Sep 17 00:00:00 2001 From: Matt Schmitt Date: Mon, 8 Oct 2018 08:19:23 -0400 Subject: [PATCH 038/265] MyQ cover return unknown state if not available (#17207) * Add additional supported states * Use get method for lookup * Return None if unable to get status --- homeassistant/components/cover/myq.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/cover/myq.py b/homeassistant/components/cover/myq.py index 78b6f891f11..5ceb4260d0c 100644 --- a/homeassistant/components/cover/myq.py +++ b/homeassistant/components/cover/myq.py @@ -11,8 +11,8 @@ import voluptuous as vol from homeassistant.components.cover import ( CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN) from homeassistant.const import ( - CONF_PASSWORD, CONF_TYPE, CONF_USERNAME, STATE_CLOSED, STATE_OPEN, - STATE_CLOSING, STATE_OPENING) + CONF_PASSWORD, CONF_TYPE, CONF_USERNAME, STATE_CLOSED, STATE_CLOSING, + STATE_OPEN, STATE_OPENING) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['pymyq==0.0.15'] @@ -23,8 +23,8 @@ DEFAULT_NAME = 'myq' MYQ_TO_HASS = { 'closed': STATE_CLOSED, - 'open': STATE_OPEN, 'closing': STATE_CLOSING, + 'open': STATE_OPEN, 'opening': STATE_OPENING } @@ -76,7 +76,7 @@ class MyQDevice(CoverDevice): self.myq = myq self.device_id = device['deviceid'] self._name = device['name'] - self._status = STATE_CLOSED + self._status = None @property def device_class(self): @@ -96,17 +96,19 @@ class MyQDevice(CoverDevice): @property def is_closed(self): """Return true if cover is closed, else False.""" - return MYQ_TO_HASS[self._status] == STATE_CLOSED + if self._status in [None, False]: + return None + return MYQ_TO_HASS.get(self._status) == STATE_CLOSED @property def is_closing(self): """Return if the cover is closing or not.""" - return MYQ_TO_HASS[self._status] == STATE_CLOSING + return MYQ_TO_HASS.get(self._status) == STATE_CLOSING @property def is_opening(self): """Return if the cover is opening or not.""" - return MYQ_TO_HASS[self._status] == STATE_OPENING + return MYQ_TO_HASS.get(self._status) == STATE_OPENING def close_cover(self, **kwargs): """Issue close command to cover.""" From 744dd42ad3269de8f0f55fc9d25719edb6067e0d Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 8 Oct 2018 14:44:00 +0200 Subject: [PATCH 039/265] Add device registry to MQTT binary sensor (#17243) --- .../components/binary_sensor/mqtt.py | 12 ++++-- tests/components/binary_sensor/test_mqtt.py | 39 +++++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/binary_sensor/mqtt.py index baaf6a9a567..deca5bea61a 100644 --- a/homeassistant/components/binary_sensor/mqtt.py +++ b/homeassistant/components/binary_sensor/mqtt.py @@ -15,11 +15,11 @@ from homeassistant.components.binary_sensor import ( BinarySensorDevice, DEVICE_CLASSES_SCHEMA) from homeassistant.const import ( CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON, - CONF_PAYLOAD_OFF, CONF_DEVICE_CLASS) + CONF_PAYLOAD_OFF, CONF_DEVICE_CLASS, CONF_DEVICE) from homeassistant.components.mqtt import ( ATTR_DISCOVERY_HASH, CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, - MqttAvailability, MqttDiscoveryUpdate) + MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo) from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -44,6 +44,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ # Integrations shouldn't never expose unique_id through configuration # this here is an exception because MQTT is a msg transport, not a protocol vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @@ -86,22 +87,25 @@ async def _async_setup_entity(hass, config, async_add_entities, config.get(CONF_PAYLOAD_NOT_AVAILABLE), value_template, config.get(CONF_UNIQUE_ID), + config.get(CONF_DEVICE), discovery_hash, )]) class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate, - BinarySensorDevice): + MqttEntityDeviceInfo, BinarySensorDevice): """Representation a binary sensor that is updated by MQTT.""" def __init__(self, name, state_topic, availability_topic, device_class, qos, force_update, payload_on, payload_off, payload_available, payload_not_available, value_template, - unique_id: Optional[str], discovery_hash): + unique_id: Optional[str], device_config: Optional[ConfigType], + discovery_hash): """Initialize the MQTT binary sensor.""" MqttAvailability.__init__(self, availability_topic, qos, payload_available, payload_not_available) MqttDiscoveryUpdate.__init__(self, discovery_hash) + MqttEntityDeviceInfo.__init__(self, device_config) self._name = name self._state = None self._state_topic = state_topic diff --git a/tests/components/binary_sensor/test_mqtt.py b/tests/components/binary_sensor/test_mqtt.py index 84619ce4ee6..0496affebf3 100644 --- a/tests/components/binary_sensor/test_mqtt.py +++ b/tests/components/binary_sensor/test_mqtt.py @@ -1,4 +1,5 @@ """The tests for the MQTT binary sensor platform.""" +import json import unittest import homeassistant.core as ha @@ -250,3 +251,41 @@ async def test_discovery_removal_binary_sensor(hass, mqtt_mock, caplog): await hass.async_block_till_done() state = hass.states.get('binary_sensor.beer') assert state is None + + +async def test_entity_device_info_with_identifier(hass, mqtt_mock): + """Test MQTT binary sensor device registry integration.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, 'homeassistant', {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + data = json.dumps({ + 'platform': 'mqtt', + 'name': 'Test 1', + 'state_topic': 'test-topic', + 'device': { + 'identifiers': ['helloworld'], + 'connections': [ + ["mac", "02:5b:26:a8:dc:12"], + ], + 'manufacturer': 'Whatever', + 'name': 'Beer', + 'model': 'Glass', + 'sw_version': '0.1-beta', + }, + 'unique_id': 'veryunique' + }) + async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/config', + data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + device = registry.async_get_device({('mqtt', 'helloworld')}, set()) + assert device is not None + assert device.identifiers == {('mqtt', 'helloworld')} + assert device.connections == {('mac', "02:5b:26:a8:dc:12")} + assert device.manufacturer == 'Whatever' + assert device.name == 'Beer' + assert device.model == 'Glass' + assert device.sw_version == '0.1-beta' From c41ef65da60e59c069057d0c480cb4e1d6640805 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 8 Oct 2018 14:57:07 +0200 Subject: [PATCH 040/265] Add device registry to MQTT switches (#17244) --- homeassistant/components/switch/mqtt.py | 13 +++++--- tests/components/switch/test_mqtt.py | 40 +++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/switch/mqtt.py b/homeassistant/components/switch/mqtt.py index bb57f179340..ad2b963629e 100644 --- a/homeassistant/components/switch/mqtt.py +++ b/homeassistant/components/switch/mqtt.py @@ -14,12 +14,12 @@ from homeassistant.components.mqtt import ( ATTR_DISCOVERY_HASH, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_AVAILABILITY_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, MqttAvailability, - MqttDiscoveryUpdate) + MqttDiscoveryUpdate, MqttEntityDeviceInfo) from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW from homeassistant.components.switch import SwitchDevice from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_OFF, - CONF_PAYLOAD_ON, CONF_ICON, STATE_ON) + CONF_PAYLOAD_ON, CONF_ICON, STATE_ON, CONF_DEVICE) from homeassistant.components import mqtt, switch import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -47,6 +47,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_STATE_OFF): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, + vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @@ -94,13 +95,15 @@ async def _async_setup_entity(hass, config, async_add_entities, config.get(CONF_PAYLOAD_NOT_AVAILABLE), config.get(CONF_UNIQUE_ID), value_template, + config.get(CONF_DEVICE), discovery_hash, ) async_add_entities([newswitch]) -class MqttSwitch(MqttAvailability, MqttDiscoveryUpdate, SwitchDevice): +class MqttSwitch(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, + SwitchDevice): """Representation of a switch that can be toggled using MQTT.""" def __init__(self, name, icon, @@ -108,11 +111,13 @@ class MqttSwitch(MqttAvailability, MqttDiscoveryUpdate, SwitchDevice): qos, retain, payload_on, payload_off, state_on, state_off, optimistic, payload_available, payload_not_available, unique_id: Optional[str], - value_template, discovery_hash): + value_template, device_config: Optional[ConfigType], + discovery_hash): """Initialize the MQTT switch.""" MqttAvailability.__init__(self, availability_topic, qos, payload_available, payload_not_available) MqttDiscoveryUpdate.__init__(self, discovery_hash) + MqttEntityDeviceInfo.__init__(self, device_config) self._state = False self._name = name self._icon = icon diff --git a/tests/components/switch/test_mqtt.py b/tests/components/switch/test_mqtt.py index 5ad233de284..3552ec0dc2a 100644 --- a/tests/components/switch/test_mqtt.py +++ b/tests/components/switch/test_mqtt.py @@ -1,4 +1,5 @@ """The tests for the MQTT switch platform.""" +import json import unittest from unittest.mock import patch @@ -337,3 +338,42 @@ async def test_discovery_removal_switch(hass, mqtt_mock, caplog): state = hass.states.get('switch.beer') assert state is None + + +async def test_entity_device_info_with_identifier(hass, mqtt_mock): + """Test MQTT switch device registry integration.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, 'homeassistant', {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + data = json.dumps({ + 'platform': 'mqtt', + 'name': 'Test 1', + 'state_topic': 'test-topic', + 'command_topic': 'test-command-topic', + 'device': { + 'identifiers': ['helloworld'], + 'connections': [ + ["mac", "02:5b:26:a8:dc:12"], + ], + 'manufacturer': 'Whatever', + 'name': 'Beer', + 'model': 'Glass', + 'sw_version': '0.1-beta', + }, + 'unique_id': 'veryunique' + }) + async_fire_mqtt_message(hass, 'homeassistant/switch/bla/config', + data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + device = registry.async_get_device({('mqtt', 'helloworld')}, set()) + assert device is not None + assert device.identifiers == {('mqtt', 'helloworld')} + assert device.connections == {('mac', "02:5b:26:a8:dc:12")} + assert device.manufacturer == 'Whatever' + assert device.name == 'Beer' + assert device.model == 'Glass' + assert device.sw_version == '0.1-beta' From 9290f245bfd51d1c3b9bd81053c0116a49c90d25 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 8 Oct 2018 14:59:04 +0200 Subject: [PATCH 041/265] Convert MQTT fan to config entry (#17247) --- homeassistant/components/fan/mqtt.py | 28 ++++++++++++++++------ homeassistant/components/mqtt/discovery.py | 1 + tests/components/fan/test_mqtt.py | 5 ++-- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/fan/mqtt.py b/homeassistant/components/fan/mqtt.py index 3e1ad2704e7..bc45754f698 100644 --- a/homeassistant/components/fan/mqtt.py +++ b/homeassistant/components/fan/mqtt.py @@ -10,7 +10,7 @@ from typing import Optional import voluptuous as vol from homeassistant.core import callback -from homeassistant.components import mqtt +from homeassistant.components import fan, mqtt from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, CONF_STATE, STATE_ON, STATE_OFF, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON) @@ -19,11 +19,13 @@ from homeassistant.components.mqtt import ( CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, MqttAvailability, MqttDiscoveryUpdate) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, FanEntity, SUPPORT_SET_SPEED, SUPPORT_OSCILLATE, SPEED_OFF, ATTR_SPEED) +from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW _LOGGER = logging.getLogger(__name__) @@ -82,14 +84,26 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None): - """Set up the MQTT fan platform.""" - if discovery_info is not None: - config = PLATFORM_SCHEMA(discovery_info) + """Set up MQTT fan through configuration.yaml.""" + await _async_setup_entity(hass, config, async_add_entities) - discovery_hash = None - if discovery_info is not None and ATTR_DISCOVERY_HASH in discovery_info: - discovery_hash = discovery_info[ATTR_DISCOVERY_HASH] +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up MQTT fan dynamically through MQTT discovery.""" + async def async_discover(discovery_payload): + """Discover and add a MQTT fan.""" + config = PLATFORM_SCHEMA(discovery_payload) + await _async_setup_entity(hass, config, async_add_entities, + discovery_payload[ATTR_DISCOVERY_HASH]) + + async_dispatcher_connect( + hass, MQTT_DISCOVERY_NEW.format(fan.DOMAIN, 'mqtt'), + async_discover) + + +async def _async_setup_entity(hass, config, async_add_entities, + discovery_hash=None): + """Set up the MQTT fan.""" async_add_entities([MqttFan( config.get(CONF_NAME), { diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index a762978a330..62f500d9952 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -49,6 +49,7 @@ CONFIG_ENTRY_PLATFORMS = { 'switch': ['mqtt'], 'climate': ['mqtt'], 'alarm_control_panel': ['mqtt'], + 'fan': ['mqtt'], } ALREADY_DISCOVERED = 'mqtt_discovered_components' diff --git a/tests/components/fan/test_mqtt.py b/tests/components/fan/test_mqtt.py index 7434e5aa1c9..feb2ff6904d 100644 --- a/tests/components/fan/test_mqtt.py +++ b/tests/components/fan/test_mqtt.py @@ -8,7 +8,7 @@ from homeassistant.const import ATTR_ASSUMED_STATE, STATE_UNAVAILABLE from tests.common import ( mock_mqtt_component, async_fire_mqtt_message, fire_mqtt_message, - get_test_home_assistant, async_mock_mqtt_component) + get_test_home_assistant, async_mock_mqtt_component, MockConfigEntry) class TestMqttFan(unittest.TestCase): @@ -108,7 +108,8 @@ class TestMqttFan(unittest.TestCase): async def test_discovery_removal_fan(hass, mqtt_mock, caplog): """Test removal of discovered fan.""" - await async_start(hass, 'homeassistant', {}) + entry = MockConfigEntry(domain='mqtt') + await async_start(hass, 'homeassistant', {}, entry) data = ( '{ "name": "Beer",' ' "command_topic": "test_topic" }' From 42fb886d7161da7e983305f4e31bb7ea9808051c Mon Sep 17 00:00:00 2001 From: emontnemery Date: Mon, 8 Oct 2018 15:36:57 +0200 Subject: [PATCH 042/265] Add support for HS color to mqtt light (#16958) * Add support for HS color to mqtt light * Warn if hs state update is invalid --- homeassistant/components/light/mqtt.py | 57 +++++++++++++++++- homeassistant/const.py | 1 + tests/components/light/test_mqtt.py | 82 ++++++++++++++++++++++++-- 3 files changed, 133 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/light/mqtt.py b/homeassistant/components/light/mqtt.py index 3b095aa4bfd..1c01278197d 100644 --- a/homeassistant/components/light/mqtt.py +++ b/homeassistant/components/light/mqtt.py @@ -15,7 +15,7 @@ from homeassistant.components.light import ( ATTR_WHITE_VALUE, Light, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_COLOR, SUPPORT_WHITE_VALUE) from homeassistant.const import ( - CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, CONF_NAME, + CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, CONF_HS, CONF_NAME, CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, STATE_ON, CONF_RGB, CONF_STATE, CONF_VALUE_TEMPLATE, CONF_WHITE_VALUE, CONF_XY) from homeassistant.components.mqtt import ( @@ -44,6 +44,9 @@ CONF_EFFECT_COMMAND_TOPIC = 'effect_command_topic' CONF_EFFECT_LIST = 'effect_list' CONF_EFFECT_STATE_TOPIC = 'effect_state_topic' CONF_EFFECT_VALUE_TEMPLATE = 'effect_value_template' +CONF_HS_COMMAND_TOPIC = 'hs_command_topic' +CONF_HS_STATE_TOPIC = 'hs_state_topic' +CONF_HS_VALUE_TEMPLATE = 'hs_value_template' CONF_RGB_COMMAND_TEMPLATE = 'rgb_command_template' CONF_RGB_COMMAND_TOPIC = 'rgb_command_topic' CONF_RGB_STATE_TOPIC = 'rgb_state_topic' @@ -82,6 +85,9 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_EFFECT_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_EFFECT_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_HS_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_HS_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_HS_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, @@ -143,6 +149,8 @@ async def _async_setup_entity(hass, config, async_add_entities, CONF_COMMAND_TOPIC, CONF_EFFECT_COMMAND_TOPIC, CONF_EFFECT_STATE_TOPIC, + CONF_HS_COMMAND_TOPIC, + CONF_HS_STATE_TOPIC, CONF_RGB_COMMAND_TOPIC, CONF_RGB_STATE_TOPIC, CONF_STATE_TOPIC, @@ -156,6 +164,7 @@ async def _async_setup_entity(hass, config, async_add_entities, CONF_BRIGHTNESS: config.get(CONF_BRIGHTNESS_VALUE_TEMPLATE), CONF_COLOR_TEMP: config.get(CONF_COLOR_TEMP_VALUE_TEMPLATE), CONF_EFFECT: config.get(CONF_EFFECT_VALUE_TEMPLATE), + CONF_HS: config.get(CONF_HS_VALUE_TEMPLATE), CONF_RGB: config.get(CONF_RGB_VALUE_TEMPLATE), CONF_RGB_COMMAND_TEMPLATE: config.get(CONF_RGB_COMMAND_TEMPLATE), CONF_STATE: config.get(CONF_STATE_VALUE_TEMPLATE), @@ -207,6 +216,8 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, Light): optimistic or topic[CONF_COLOR_TEMP_STATE_TOPIC] is None) self._optimistic_effect = ( optimistic or topic[CONF_EFFECT_STATE_TOPIC] is None) + self._optimistic_hs = \ + optimistic or topic[CONF_HS_STATE_TOPIC] is None self._optimistic_white_value = ( optimistic or topic[CONF_WHITE_VALUE_STATE_TOPIC] is None) self._optimistic_xy = \ @@ -232,6 +243,8 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, Light): self._supported_features |= ( topic[CONF_EFFECT_COMMAND_TOPIC] is not None and SUPPORT_EFFECT) + self._supported_features |= ( + topic[CONF_HS_COMMAND_TOPIC] is not None and SUPPORT_COLOR) self._supported_features |= ( topic[CONF_WHITE_VALUE_COMMAND_TOPIC] is not None and SUPPORT_WHITE_VALUE) @@ -374,6 +387,33 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, Light): else: self._effect = None + @callback + def hs_received(topic, payload, qos): + """Handle new MQTT messages for hs color.""" + payload = templates[CONF_HS](payload) + if not payload: + _LOGGER.debug("Ignoring empty hs message from '%s'", topic) + return + + try: + hs_color = [float(val) for val in payload.split(',', 2)] + self._hs = hs_color + self.async_schedule_update_ha_state() + except ValueError: + _LOGGER.debug("Failed to parse hs state update: '%s'", + payload) + + if self._topic[CONF_HS_STATE_TOPIC] is not None: + await mqtt.async_subscribe( + self.hass, self._topic[CONF_HS_STATE_TOPIC], hs_received, + self._qos) + self._hs = (0, 0) + if self._optimistic_hs and last_state\ + and last_state.attributes.get(ATTR_HS_COLOR): + self._hs = last_state.attributes.get(ATTR_HS_COLOR) + elif self._topic[CONF_HS_COMMAND_TOPIC] is not None: + self._hs = (0, 0) + @callback def white_value_received(topic, payload, qos): """Handle new MQTT messages for white value.""" @@ -403,7 +443,7 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, Light): @callback def xy_received(topic, payload, qos): - """Handle new MQTT messages for color.""" + """Handle new MQTT messages for xy color.""" payload = templates[CONF_XY](payload) if not payload: _LOGGER.debug("Ignoring empty xy-color message from '%s'", @@ -539,6 +579,19 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, Light): self._hs = kwargs[ATTR_HS_COLOR] should_update = True + if ATTR_HS_COLOR in kwargs and \ + self._topic[CONF_HS_COMMAND_TOPIC] is not None: + + hs_color = kwargs[ATTR_HS_COLOR] + mqtt.async_publish( + self.hass, self._topic[CONF_HS_COMMAND_TOPIC], + '{},{}'.format(*hs_color), self._qos, + self._retain) + + if self._optimistic_hs: + self._hs = kwargs[ATTR_HS_COLOR] + should_update = True + if ATTR_HS_COLOR in kwargs and \ self._topic[CONF_XY_COMMAND_TOPIC] is not None: diff --git a/homeassistant/const.py b/homeassistant/const.py index e9fb301f0db..361299181ac 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -82,6 +82,7 @@ CONF_FRIENDLY_NAME_TEMPLATE = 'friendly_name_template' CONF_HEADERS = 'headers' CONF_HOST = 'host' CONF_HOSTS = 'hosts' +CONF_HS = 'hs' CONF_ICON = 'icon' CONF_ICON_TEMPLATE = 'icon_template' CONF_INCLUDE = 'include' diff --git a/tests/components/light/test_mqtt.py b/tests/components/light/test_mqtt.py index 118cdb3c995..2b23be101c7 100644 --- a/tests/components/light/test_mqtt.py +++ b/tests/components/light/test_mqtt.py @@ -137,6 +137,21 @@ light: payload_on: "on" payload_off: "off" +Configuration for HS Version with brightness: + +light: + platform: mqtt + name: "Office Light HS" + state_topic: "office/hs1/light/status" + command_topic: "office/hs1/light/switch" + brightness_state_topic: "office/hs1/brightness/status" + brightness_command_topic: "office/hs1/brightness/set" + hs_state_topic: "office/hs1/hs/status" + hs_command_topic: "office/hs1/hs/set" + qos: 0 + payload_on: "on" + payload_off: "off" + """ import unittest from unittest import mock @@ -180,7 +195,7 @@ class TestLightMQTT(unittest.TestCase): }) self.assertIsNone(self.hass.states.get('light.test')) - def test_no_color_brightness_color_temp_white_xy_if_no_topics(self): + def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics(self): """Test if there is no color and brightness if no topic.""" with assert_setup_component(1, light.DOMAIN): assert setup_component(self.hass, light.DOMAIN, { @@ -197,6 +212,7 @@ class TestLightMQTT(unittest.TestCase): self.assertIsNone(state.attributes.get('rgb_color')) self.assertIsNone(state.attributes.get('brightness')) self.assertIsNone(state.attributes.get('color_temp')) + self.assertIsNone(state.attributes.get('hs_color')) self.assertIsNone(state.attributes.get('white_value')) self.assertIsNone(state.attributes.get('xy_color')) @@ -208,6 +224,7 @@ class TestLightMQTT(unittest.TestCase): self.assertIsNone(state.attributes.get('rgb_color')) self.assertIsNone(state.attributes.get('brightness')) self.assertIsNone(state.attributes.get('color_temp')) + self.assertIsNone(state.attributes.get('hs_color')) self.assertIsNone(state.attributes.get('white_value')) self.assertIsNone(state.attributes.get('xy_color')) @@ -226,6 +243,8 @@ class TestLightMQTT(unittest.TestCase): 'color_temp_command_topic': 'test_light_rgb/color_temp/set', 'effect_state_topic': 'test_light_rgb/effect/status', 'effect_command_topic': 'test_light_rgb/effect/set', + 'hs_state_topic': 'test_light_rgb/hs/status', + 'hs_command_topic': 'test_light_rgb/hs/set', 'white_value_state_topic': 'test_light_rgb/white_value/status', 'white_value_command_topic': 'test_light_rgb/white_value/set', 'xy_state_topic': 'test_light_rgb/xy/status', @@ -244,6 +263,7 @@ class TestLightMQTT(unittest.TestCase): self.assertIsNone(state.attributes.get('brightness')) self.assertIsNone(state.attributes.get('color_temp')) self.assertIsNone(state.attributes.get('effect')) + self.assertIsNone(state.attributes.get('hs_color')) self.assertIsNone(state.attributes.get('white_value')) self.assertIsNone(state.attributes.get('xy_color')) self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) @@ -257,6 +277,7 @@ class TestLightMQTT(unittest.TestCase): self.assertEqual(255, state.attributes.get('brightness')) self.assertEqual(150, state.attributes.get('color_temp')) self.assertEqual('none', state.attributes.get('effect')) + self.assertEqual((0, 0), state.attributes.get('hs_color')) self.assertEqual(255, state.attributes.get('white_value')) self.assertEqual((0.323, 0.329), state.attributes.get('xy_color')) @@ -309,6 +330,14 @@ class TestLightMQTT(unittest.TestCase): self.assertEqual((255, 255, 255), light_state.attributes.get('rgb_color')) + fire_mqtt_message(self.hass, 'test_light_rgb/hs/status', + '200,50') + self.hass.block_till_done() + + light_state = self.hass.states.get('light.test') + self.assertEqual((200, 50), + light_state.attributes.get('hs_color')) + fire_mqtt_message(self.hass, 'test_light_rgb/xy/status', '0.675,0.322') self.hass.block_till_done() @@ -412,7 +441,7 @@ class TestLightMQTT(unittest.TestCase): light_state.attributes['white_value']) def test_controlling_state_via_topic_with_templates(self): - """Test the setting og the state with a template.""" + """Test the setting of the state with a template.""" config = {light.DOMAIN: { 'platform': 'mqtt', 'name': 'test', @@ -422,11 +451,13 @@ class TestLightMQTT(unittest.TestCase): 'rgb_command_topic': 'test_light_rgb/rgb/set', 'color_temp_command_topic': 'test_light_rgb/color_temp/set', 'effect_command_topic': 'test_light_rgb/effect/set', + 'hs_command_topic': 'test_light_rgb/hs/set', 'white_value_command_topic': 'test_light_rgb/white_value/set', 'xy_command_topic': 'test_light_rgb/xy/set', 'brightness_state_topic': 'test_light_rgb/brightness/status', 'color_temp_state_topic': 'test_light_rgb/color_temp/status', 'effect_state_topic': 'test_light_rgb/effect/status', + 'hs_state_topic': 'test_light_rgb/hs/status', 'rgb_state_topic': 'test_light_rgb/rgb/status', 'white_value_state_topic': 'test_light_rgb/white_value/status', 'xy_state_topic': 'test_light_rgb/xy/status', @@ -434,6 +465,7 @@ class TestLightMQTT(unittest.TestCase): 'brightness_value_template': '{{ value_json.hello }}', 'color_temp_value_template': '{{ value_json.hello }}', 'effect_value_template': '{{ value_json.hello }}', + 'hs_value_template': '{{ value_json.hello | join(",") }}', 'rgb_value_template': '{{ value_json.hello | join(",") }}', 'white_value_template': '{{ value_json.hello }}', 'xy_value_template': '{{ value_json.hello | join(",") }}', @@ -459,17 +491,28 @@ class TestLightMQTT(unittest.TestCase): '{"hello": "rainbow"}') fire_mqtt_message(self.hass, 'test_light_rgb/white_value/status', '{"hello": "75"}') - fire_mqtt_message(self.hass, 'test_light_rgb/xy/status', - '{"hello": [0.123,0.123]}') self.hass.block_till_done() state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) self.assertEqual(50, state.attributes.get('brightness')) - self.assertEqual((0, 123, 255), state.attributes.get('rgb_color')) + self.assertEqual((84, 169, 255), state.attributes.get('rgb_color')) self.assertEqual(300, state.attributes.get('color_temp')) self.assertEqual('rainbow', state.attributes.get('effect')) self.assertEqual(75, state.attributes.get('white_value')) + + fire_mqtt_message(self.hass, 'test_light_rgb/hs/status', + '{"hello": [100,50]}') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual((100, 50), state.attributes.get('hs_color')) + + fire_mqtt_message(self.hass, 'test_light_rgb/xy/status', + '{"hello": [0.123,0.123]}') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') self.assertEqual((0.14, 0.131), state.attributes.get('xy_color')) def test_sending_mqtt_commands_and_optimistic(self): @@ -482,6 +525,7 @@ class TestLightMQTT(unittest.TestCase): 'rgb_command_topic': 'test_light_rgb/rgb/set', 'color_temp_command_topic': 'test_light_rgb/color_temp/set', 'effect_command_topic': 'test_light_rgb/effect/set', + 'hs_command_topic': 'test_light_rgb/hs/set', 'white_value_command_topic': 'test_light_rgb/white_value/set', 'xy_command_topic': 'test_light_rgb/xy/set', 'effect_list': ['colorloop', 'random'], @@ -529,6 +573,8 @@ class TestLightMQTT(unittest.TestCase): self.mock_publish.reset_mock() common.turn_on(self.hass, 'light.test', brightness=50, xy_color=[0.123, 0.123]) + common.turn_on(self.hass, 'light.test', + brightness=50, hs_color=[359, 78]) common.turn_on(self.hass, 'light.test', rgb_color=[255, 128, 0], white_value=80) self.hass.block_till_done() @@ -537,6 +583,7 @@ class TestLightMQTT(unittest.TestCase): mock.call('test_light_rgb/set', 'on', 2, False), mock.call('test_light_rgb/rgb/set', '255,128,0', 2, False), mock.call('test_light_rgb/brightness/set', 50, 2, False), + mock.call('test_light_rgb/hs/set', '359.0,78.0', 2, False), mock.call('test_light_rgb/white_value/set', 80, 2, False), mock.call('test_light_rgb/xy/set', '0.14,0.131', 2, False), ], any_order=True) @@ -545,6 +592,7 @@ class TestLightMQTT(unittest.TestCase): self.assertEqual(STATE_ON, state.state) self.assertEqual((255, 128, 0), state.attributes['rgb_color']) self.assertEqual(50, state.attributes['brightness']) + self.assertEqual((30.118, 100), state.attributes['hs_color']) self.assertEqual(80, state.attributes['white_value']) self.assertEqual((0.611, 0.375), state.attributes['xy_color']) @@ -652,6 +700,30 @@ class TestLightMQTT(unittest.TestCase): self.assertEqual(STATE_ON, state.state) self.assertEqual('none', state.attributes.get('effect')) + def test_show_hs_if_only_command_topic(self): + """Test the hs if only a command topic is present.""" + config = {light.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'hs_command_topic': 'test_light_rgb/hs/set', + 'command_topic': 'test_light_rgb/set', + 'state_topic': 'test_light_rgb/status', + }} + + with assert_setup_component(1, light.DOMAIN): + assert setup_component(self.hass, light.DOMAIN, config) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + self.assertIsNone(state.attributes.get('hs_color')) + + fire_mqtt_message(self.hass, 'test_light_rgb/status', 'ON') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + self.assertEqual((0, 0), state.attributes.get('hs_color')) + def test_show_white_value_if_only_command_topic(self): """Test the white_value if only a command topic is present.""" config = {light.DOMAIN: { From b637b48bd8684f1937d2030778e3ed399a44ae4d Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Tue, 9 Oct 2018 01:13:08 +1100 Subject: [PATCH 043/265] emptying device state attributes if the update from the feed fails (#17249) --- homeassistant/components/sensor/geo_rss_events.py | 1 + tests/components/sensor/test_geo_rss_events.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/homeassistant/components/sensor/geo_rss_events.py b/homeassistant/components/sensor/geo_rss_events.py index 5085e113e92..22b4c71a705 100644 --- a/homeassistant/components/sensor/geo_rss_events.py +++ b/homeassistant/components/sensor/geo_rss_events.py @@ -145,3 +145,4 @@ class GeoRssServiceSensor(Entity): # If no events were found due to an error then just set state to # zero. self._state = 0 + self._state_attributes = {} diff --git a/tests/components/sensor/test_geo_rss_events.py b/tests/components/sensor/test_geo_rss_events.py index 21538d458bc..3362f799392 100644 --- a/tests/components/sensor/test_geo_rss_events.py +++ b/tests/components/sensor/test_geo_rss_events.py @@ -123,6 +123,10 @@ class TestGeoRssServiceUpdater(unittest.TestCase): assert len(all_states) == 1 state = self.hass.states.get("sensor.event_service_any") assert int(state.state) == 0 + assert state.attributes == { + ATTR_FRIENDLY_NAME: "Event Service Any", + ATTR_UNIT_OF_MEASUREMENT: "Events", + ATTR_ICON: "mdi:alert"} @mock.patch('georss_client.generic_feed.GenericFeed') def test_setup_with_categories(self, mock_feed): From dd55ff87c8e12b36a39e35b951f16adb2499412b Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 8 Oct 2018 16:13:44 +0200 Subject: [PATCH 044/265] Add device registry to MQTT cover (#17245) * Add device registry to MQTT cover * Fix tests --- homeassistant/components/cover/mqtt.py | 13 ++++-- tests/components/cover/test_mqtt.py | 56 ++++++++++++++++++++++---- 2 files changed, 57 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py index cbc8fbee274..92a7fac1d33 100644 --- a/homeassistant/components/cover/mqtt.py +++ b/homeassistant/components/cover/mqtt.py @@ -19,12 +19,12 @@ from homeassistant.components.cover import ( from homeassistant.exceptions import TemplateError from homeassistant.const import ( CONF_NAME, CONF_VALUE_TEMPLATE, CONF_OPTIMISTIC, STATE_OPEN, - STATE_CLOSED, STATE_UNKNOWN) + STATE_CLOSED, STATE_UNKNOWN, CONF_DEVICE) from homeassistant.components.mqtt import ( ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, valid_publish_topic, valid_subscribe_topic, - MqttAvailability, MqttDiscoveryUpdate) + MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo) from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -96,6 +96,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_TILT_INVERT_STATE, default=DEFAULT_TILT_INVERT_STATE): cv.boolean, vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @@ -155,11 +156,13 @@ async def _async_setup_entity(hass, config, async_add_entities, config.get(CONF_POSITION_TOPIC), set_position_template, config.get(CONF_UNIQUE_ID), + config.get(CONF_DEVICE), discovery_hash )]) -class MqttCover(MqttAvailability, MqttDiscoveryUpdate, CoverDevice): +class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, + CoverDevice): """Representation of a cover that can be controlled using MQTT.""" def __init__(self, name, state_topic, command_topic, availability_topic, @@ -169,11 +172,13 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, CoverDevice): optimistic, value_template, tilt_open_position, tilt_closed_position, tilt_min, tilt_max, tilt_optimistic, tilt_invert, position_topic, set_position_template, - unique_id: Optional[str], discovery_hash): + unique_id: Optional[str], device_config: Optional[ConfigType], + discovery_hash): """Initialize the cover.""" MqttAvailability.__init__(self, availability_topic, qos, payload_available, payload_not_available) MqttDiscoveryUpdate.__init__(self, discovery_hash) + MqttEntityDeviceInfo.__init__(self, device_config) self._position = None self._state = None self._name = name diff --git a/tests/components/cover/test_mqtt.py b/tests/components/cover/test_mqtt.py index 355f620520a..282b1d2873f 100644 --- a/tests/components/cover/test_mqtt.py +++ b/tests/components/cover/test_mqtt.py @@ -1,4 +1,5 @@ """The tests for the MQTT cover platform.""" +import json import unittest from homeassistant.components import cover, mqtt @@ -615,7 +616,7 @@ class TestCoverMQTT(unittest.TestCase): 'tilt-command-topic', 'tilt-status-topic', 0, False, 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, False, None, 100, 0, 0, 100, False, False, None, None, None, - None) + None, None) self.assertEqual(44, mqtt_cover.find_percentage_in_range(44)) @@ -626,7 +627,7 @@ class TestCoverMQTT(unittest.TestCase): 'tilt-command-topic', 'tilt-status-topic', 0, False, 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, False, None, 180, 80, 80, 180, False, False, None, None, None, - None) + None, None) self.assertEqual(40, mqtt_cover.find_percentage_in_range(120)) @@ -637,7 +638,7 @@ class TestCoverMQTT(unittest.TestCase): 'tilt-command-topic', 'tilt-status-topic', 0, False, 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, False, None, 100, 0, 0, 100, False, True, None, None, None, - None) + None, None) self.assertEqual(56, mqtt_cover.find_percentage_in_range(44)) @@ -648,7 +649,7 @@ class TestCoverMQTT(unittest.TestCase): 'tilt-command-topic', 'tilt-status-topic', 0, False, 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, False, None, 180, 80, 80, 180, False, True, None, None, None, - None) + None, None) self.assertEqual(60, mqtt_cover.find_percentage_in_range(120)) @@ -659,7 +660,7 @@ class TestCoverMQTT(unittest.TestCase): 'tilt-command-topic', 'tilt-status-topic', 0, False, 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, False, None, 100, 0, 0, 100, False, False, None, None, None, - None) + None, None) self.assertEqual(44, mqtt_cover.find_in_range_from_percent(44)) @@ -670,7 +671,7 @@ class TestCoverMQTT(unittest.TestCase): 'tilt-command-topic', 'tilt-status-topic', 0, False, 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, False, None, 180, 80, 80, 180, False, False, None, None, None, - None) + None, None) self.assertEqual(120, mqtt_cover.find_in_range_from_percent(40)) @@ -681,7 +682,7 @@ class TestCoverMQTT(unittest.TestCase): 'tilt-command-topic', 'tilt-status-topic', 0, False, 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, False, None, 100, 0, 0, 100, False, True, None, None, None, - None) + None, None) self.assertEqual(44, mqtt_cover.find_in_range_from_percent(56)) @@ -692,7 +693,7 @@ class TestCoverMQTT(unittest.TestCase): 'tilt-command-topic', 'tilt-status-topic', 0, False, 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, False, None, 180, 80, 80, 180, False, True, None, None, None, - None) + None, None) self.assertEqual(120, mqtt_cover.find_in_range_from_percent(60)) @@ -810,3 +811,42 @@ async def test_unique_id(hass): await hass.async_block_till_done() assert len(hass.states.async_entity_ids(cover.DOMAIN)) == 1 + + +async def test_entity_device_info_with_identifier(hass, mqtt_mock): + """Test MQTT cover device registry integration.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, 'homeassistant', {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + data = json.dumps({ + 'platform': 'mqtt', + 'name': 'Test 1', + 'state_topic': 'test-topic', + 'command_topic': 'test-command-topic', + 'device': { + 'identifiers': ['helloworld'], + 'connections': [ + ["mac", "02:5b:26:a8:dc:12"], + ], + 'manufacturer': 'Whatever', + 'name': 'Beer', + 'model': 'Glass', + 'sw_version': '0.1-beta', + }, + 'unique_id': 'veryunique' + }) + async_fire_mqtt_message(hass, 'homeassistant/cover/bla/config', + data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + device = registry.async_get_device({('mqtt', 'helloworld')}, set()) + assert device is not None + assert device.identifiers == {('mqtt', 'helloworld')} + assert device.connections == {('mac', "02:5b:26:a8:dc:12")} + assert device.manufacturer == 'Whatever' + assert device.name == 'Beer' + assert device.model == 'Glass' + assert device.sw_version == '0.1-beta' From c3b1121d7761c9816c5544eaa0429e9802ec6e68 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 8 Oct 2018 16:35:38 +0200 Subject: [PATCH 045/265] Add group foundation (#16935) Add group foundation --- homeassistant/auth/__init__.py | 3 + homeassistant/auth/auth_store.py | 59 +++++++++++++++++- homeassistant/auth/models.py | 10 +++ homeassistant/components/config/auth.py | 1 + tests/auth/test_auth_store.py | 82 +++++++++++++++++++++++++ tests/common.py | 27 +++++++- tests/components/config/test_auth.py | 9 ++- 7 files changed, 186 insertions(+), 5 deletions(-) create mode 100644 tests/auth/test_auth_store.py diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index c6f978640f6..e19498026d1 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -130,13 +130,16 @@ class AuthManager: name=name, system_generated=True, is_active=True, + groups=[], ) async def async_create_user(self, name: str) -> models.User: """Create a user.""" + group = (await self._store.async_get_groups())[0] kwargs = { 'name': name, 'is_active': True, + 'groups': [group] } # type: Dict[str, Any] if await self._user_should_be_owner(): diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index 54c34d8ec2c..572393dc444 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -13,6 +13,7 @@ from . import models STORAGE_VERSION = 1 STORAGE_KEY = 'auth' +INITIAL_GROUP_NAME = 'All Access' class AuthStore: @@ -28,9 +29,18 @@ class AuthStore: """Initialize the auth store.""" self.hass = hass self._users = None # type: Optional[Dict[str, models.User]] + self._groups = None # type: Optional[Dict[str, models.Group]] self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY, private=True) + async def async_get_groups(self) -> List[models.Group]: + """Retrieve all users.""" + if self._groups is None: + await self._async_load() + assert self._groups is not None + + return list(self._groups.values()) + async def async_get_users(self) -> List[models.User]: """Retrieve all users.""" if self._users is None: @@ -51,14 +61,20 @@ class AuthStore: self, name: Optional[str], is_owner: Optional[bool] = None, is_active: Optional[bool] = None, system_generated: Optional[bool] = None, - credentials: Optional[models.Credentials] = None) -> models.User: + credentials: Optional[models.Credentials] = None, + groups: Optional[List[models.Group]] = None) -> models.User: """Create a new user.""" if self._users is None: await self._async_load() - assert self._users is not None + + assert self._users is not None + assert self._groups is not None kwargs = { - 'name': name + 'name': name, + # Until we get group management, we just put everyone in the + # same group. + 'groups': groups or [], } # type: Dict[str, Any] if is_owner is not None: @@ -219,19 +235,36 @@ class AuthStore: return users = OrderedDict() # type: Dict[str, models.User] + groups = OrderedDict() # type: Dict[str, models.Group] # When creating objects we mention each attribute explicetely. This # prevents crashing if user rolls back HA version after a new property # was added. + for group_dict in data.get('groups', []): + groups[group_dict['id']] = models.Group( + name=group_dict['name'], + id=group_dict['id'], + ) + + migrate_group = None + + if not groups: + migrate_group = models.Group(name=INITIAL_GROUP_NAME) + groups[migrate_group.id] = migrate_group + for user_dict in data['users']: users[user_dict['id']] = models.User( name=user_dict['name'], + groups=[groups[group_id] for group_id + in user_dict.get('group_ids', [])], id=user_dict['id'], is_owner=user_dict['is_owner'], is_active=user_dict['is_active'], system_generated=user_dict['system_generated'], ) + if migrate_group is not None and not user_dict['system_generated']: + users[user_dict['id']].groups = [migrate_group] for cred_dict in data['credentials']: users[cred_dict['user_id']].credentials.append(models.Credentials( @@ -286,6 +319,7 @@ class AuthStore: ) users[rt_dict['user_id']].refresh_tokens[token.id] = token + self._groups = groups self._users = users @callback @@ -300,10 +334,12 @@ class AuthStore: def _data_to_save(self) -> Dict: """Return the data to store.""" assert self._users is not None + assert self._groups is not None users = [ { 'id': user.id, + 'group_ids': [group.id for group in user.groups], 'is_owner': user.is_owner, 'is_active': user.is_active, 'name': user.name, @@ -312,6 +348,14 @@ class AuthStore: for user in self._users.values() ] + groups = [ + { + 'name': group.name, + 'id': group.id, + } + for group in self._groups.values() + ] + credentials = [ { 'id': credential.id, @@ -348,6 +392,7 @@ class AuthStore: return { 'users': users, + 'groups': groups, 'credentials': credentials, 'refresh_tokens': refresh_tokens, } @@ -355,3 +400,11 @@ class AuthStore: def _set_defaults(self) -> None: """Set default values for auth store.""" self._users = OrderedDict() # type: Dict[str, models.User] + + # Add default group + all_access_group = models.Group(name=INITIAL_GROUP_NAME) + + groups = OrderedDict() # type: Dict[str, models.Group] + groups[all_access_group.id] = all_access_group + + self._groups = groups diff --git a/homeassistant/auth/models.py b/homeassistant/auth/models.py index bd00ca72b83..7305e0e77b2 100644 --- a/homeassistant/auth/models.py +++ b/homeassistant/auth/models.py @@ -14,6 +14,14 @@ TOKEN_TYPE_SYSTEM = 'system' TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = 'long_lived_access_token' +@attr.s(slots=True) +class Group: + """A group.""" + + name = attr.ib(type=str) # type: Optional[str] + id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex) + + @attr.s(slots=True) class User: """A user.""" @@ -24,6 +32,8 @@ class User: is_active = attr.ib(type=bool, default=False) system_generated = attr.ib(type=bool, default=False) + groups = attr.ib(type=List, factory=list, cmp=False) # type: List[Group] + # List of credentials of a user. credentials = attr.ib( type=list, factory=list, cmp=False diff --git a/homeassistant/components/config/auth.py b/homeassistant/components/config/auth.py index fb60b4075ef..ec83918e9f0 100644 --- a/homeassistant/components/config/auth.py +++ b/homeassistant/components/config/auth.py @@ -92,6 +92,7 @@ def _user_info(user): 'is_owner': user.is_owner, 'is_active': user.is_active, 'system_generated': user.system_generated, + 'group_ids': [group.id for group in user.groups], 'credentials': [ { 'type': c.auth_provider_type, diff --git a/tests/auth/test_auth_store.py b/tests/auth/test_auth_store.py new file mode 100644 index 00000000000..a3bdbab93d7 --- /dev/null +++ b/tests/auth/test_auth_store.py @@ -0,0 +1,82 @@ +"""Tests for the auth store.""" +from homeassistant.auth import auth_store + + +async def test_loading_old_data_format(hass, hass_storage): + """Test we correctly load an old data format.""" + hass_storage[auth_store.STORAGE_KEY] = { + 'version': 1, + 'data': { + 'credentials': [], + 'users': [ + { + "id": "user-id", + "is_active": True, + "is_owner": True, + "name": "Paulus", + "system_generated": False, + }, + { + "id": "system-id", + "is_active": True, + "is_owner": True, + "name": "Hass.io", + "system_generated": True, + } + ], + "refresh_tokens": [ + { + "access_token_expiration": 1800.0, + "client_id": "http://localhost:8123/", + "created_at": "2018-10-03T13:43:19.774637+00:00", + "id": "user-token-id", + "jwt_key": "some-key", + "last_used_at": "2018-10-03T13:43:19.774712+00:00", + "token": "some-token", + "user_id": "user-id" + }, + { + "access_token_expiration": 1800.0, + "client_id": None, + "created_at": "2018-10-03T13:43:19.774637+00:00", + "id": "system-token-id", + "jwt_key": "some-key", + "last_used_at": "2018-10-03T13:43:19.774712+00:00", + "token": "some-token", + "user_id": "system-id" + }, + { + "access_token_expiration": 1800.0, + "client_id": "http://localhost:8123/", + "created_at": "2018-10-03T13:43:19.774637+00:00", + "id": "hidden-because-no-jwt-id", + "last_used_at": "2018-10-03T13:43:19.774712+00:00", + "token": "some-token", + "user_id": "user-id" + }, + ] + } + } + + store = auth_store.AuthStore(hass) + groups = await store.async_get_groups() + assert len(groups) == 1 + group = groups[0] + assert group.name == "All Access" + + users = await store.async_get_users() + assert len(users) == 2 + + owner, system = users + + assert owner.system_generated is False + assert owner.groups == [group] + assert len(owner.refresh_tokens) == 1 + owner_token = list(owner.refresh_tokens.values())[0] + assert owner_token.id == 'user-token-id' + + assert system.system_generated is True + assert system.groups == [] + assert len(system.refresh_tokens) == 1 + system_token = list(system.refresh_tokens.values())[0] + assert system_token.id == 'system-token-id' diff --git a/tests/common.py b/tests/common.py index ee181cfa2e9..cfc29a7f441 100644 --- a/tests/common.py +++ b/tests/common.py @@ -345,17 +345,42 @@ def mock_device_registry(hass, mock_entries=None): return registry +class MockGroup(auth_models.Group): + """Mock a group in Home Assistant.""" + + def __init__(self, id=None, name='Mock Group'): + """Mock a group.""" + kwargs = { + 'name': name + } + if id is not None: + kwargs['id'] = id + + super().__init__(**kwargs) + + def add_to_hass(self, hass): + """Test helper to add entry to hass.""" + return self.add_to_auth_manager(hass.auth) + + def add_to_auth_manager(self, auth_mgr): + """Test helper to add entry to hass.""" + ensure_auth_manager_loaded(auth_mgr) + auth_mgr._store._groups[self.id] = self + return self + + class MockUser(auth_models.User): """Mock a user in Home Assistant.""" def __init__(self, id=None, is_owner=False, is_active=True, - name='Mock User', system_generated=False): + name='Mock User', system_generated=False, groups=None): """Initialize mock user.""" kwargs = { 'is_owner': is_owner, 'is_active': is_active, 'name': name, 'system_generated': system_generated, + 'groups': groups or [], } if id is not None: kwargs['id'] = id diff --git a/tests/components/config/test_auth.py b/tests/components/config/test_auth.py index cd04eedf08e..f7e348e8476 100644 --- a/tests/components/config/test_auth.py +++ b/tests/components/config/test_auth.py @@ -6,7 +6,7 @@ import pytest from homeassistant.auth import models as auth_models from homeassistant.components.config import auth as auth_config -from tests.common import MockUser, CLIENT_ID +from tests.common import MockGroup, MockUser, CLIENT_ID @pytest.fixture(autouse=True) @@ -39,10 +39,13 @@ async def test_list_requires_owner(hass, hass_ws_client, hass_access_token): async def test_list(hass, hass_ws_client): """Test get users.""" + group = MockGroup().add_to_hass(hass) + owner = MockUser( id='abc', name='Test Owner', is_owner=True, + groups=[group], ).add_to_hass(hass) owner.credentials.append(auth_models.Credentials( @@ -61,6 +64,7 @@ async def test_list(hass, hass_ws_client): id='hij', name='Inactive User', is_active=False, + groups=[group], ).add_to_hass(hass) refresh_token = await hass.auth.async_create_refresh_token( @@ -83,6 +87,7 @@ async def test_list(hass, hass_ws_client): 'is_owner': True, 'is_active': True, 'system_generated': False, + 'group_ids': [group.id for group in owner.groups], 'credentials': [{'type': 'homeassistant'}] } assert data[1] == { @@ -91,6 +96,7 @@ async def test_list(hass, hass_ws_client): 'is_owner': False, 'is_active': True, 'system_generated': True, + 'group_ids': [], 'credentials': [], } assert data[2] == { @@ -99,6 +105,7 @@ async def test_list(hass, hass_ws_client): 'is_owner': False, 'is_active': False, 'system_generated': False, + 'group_ids': [group.id for group in inactive.groups], 'credentials': [], } From 9380fca97ea2c111752dc044cde29eaa2b69a1ae Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Mon, 8 Oct 2018 11:30:27 -0400 Subject: [PATCH 046/265] Add Elk light platform (#17222) * Add Elk light platform. * Add ElkM1 light code. Doh. * Fix PR comments. * Fix hound errors. * Fix PR comment. * Move state from base to class(s) where used --- .../components/alarm_control_panel/elkm1.py | 6 +- homeassistant/components/elkm1/__init__.py | 5 +- homeassistant/components/light/elkm1.py | 59 +++++++++++++++++++ 3 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/light/elkm1.py diff --git a/homeassistant/components/alarm_control_panel/elkm1.py b/homeassistant/components/alarm_control_panel/elkm1.py index 8026a3736fb..a01898ac959 100644 --- a/homeassistant/components/alarm_control_panel/elkm1.py +++ b/homeassistant/components/alarm_control_panel/elkm1.py @@ -38,8 +38,11 @@ DISPLAY_MESSAGE_SERVICE_SCHEMA = vol.Schema({ async def async_setup_platform(hass, config, async_add_entities, - discovery_info): + discovery_info=None): """Set up the ElkM1 alarm platform.""" + if discovery_info is None: + return + elk = hass.data[ELK_DOMAIN]['elk'] entities = create_elk_entities(hass, elk.areas, 'area', ElkArea, []) async_add_entities(entities, True) @@ -88,6 +91,7 @@ class ElkArea(ElkEntity, alarm.AlarmControlPanel): """Initialize Area as Alarm Control Panel.""" super().__init__('alarm_control_panel', element, elk, elk_data) self._changed_by_entity_id = '' + self._state = None async def async_added_to_hass(self): """Register callback for ElkM1 changes.""" diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 505280d4f26..2dd85e02215 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -35,7 +35,7 @@ CONF_ENABLED = 'enabled' _LOGGER = logging.getLogger(__name__) -SUPPORTED_DOMAINS = ['alarm_control_panel'] +SUPPORTED_DOMAINS = ['alarm_control_panel', 'light'] def _host_validator(config): @@ -138,7 +138,7 @@ async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: hass.data[DOMAIN] = {'elk': elk, 'config': config, 'keypads': {}} for component in SUPPORTED_DOMAINS: hass.async_create_task( - discovery.async_load_platform(hass, component, DOMAIN)) + discovery.async_load_platform(hass, component, DOMAIN, {})) return True @@ -161,7 +161,6 @@ class ElkEntity(Entity): """Initialize the base of all Elk devices.""" self._elk = elk self._element = element - self._state = None self._temperature_unit = elk_data['config']['temperature_unit'] self._unique_id = 'elkm1_{}'.format( self._element.default_name('_').lower()) diff --git a/homeassistant/components/light/elkm1.py b/homeassistant/components/light/elkm1.py new file mode 100644 index 00000000000..c6cb138877b --- /dev/null +++ b/homeassistant/components/light/elkm1.py @@ -0,0 +1,59 @@ +""" +Support for control of ElkM1 lighting (X10, UPB, etc). + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.elkm1/ +""" + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) +from homeassistant.components.elkm1 import ( + DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities) + +DEPENDENCIES = [ELK_DOMAIN] + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the Elk light platform.""" + if discovery_info is None: + return + elk = hass.data[ELK_DOMAIN]['elk'] + async_add_entities( + create_elk_entities(hass, elk.lights, 'plc', ElkLight, []), True) + + +class ElkLight(ElkEntity, Light): + """Elk lighting device.""" + + def __init__(self, element, elk, elk_data): + """Initialize light.""" + super().__init__('light', element, elk, elk_data) + self._brightness = self._element.status + + @property + def brightness(self): + """Get the brightness.""" + return self._brightness + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS + + @property + def is_on(self) -> bool: + """Get the current brightness.""" + return self._brightness != 0 + + def _element_changed(self, element, changeset): + status = self._element.status if self._element.status != 1 else 100 + self._brightness = round(status * 2.55) + + async def async_turn_on(self, **kwargs): + """Turn on the light.""" + self._element.level(round(kwargs.get(ATTR_BRIGHTNESS, 255) / 2.55)) + + async def async_turn_off(self, **kwargs): + """Turn off the light.""" + self._element.level(0) From 68472b8241e8fb3b269617010e7db85c092e56ad Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 8 Oct 2018 20:16:37 +0200 Subject: [PATCH 047/265] Add a webhook automation trigger (#17246) --- .../components/automation/webhook.py | 54 +++++++++++++ tests/components/automation/test_webhook.py | 75 +++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 homeassistant/components/automation/webhook.py create mode 100644 tests/components/automation/test_webhook.py diff --git a/homeassistant/components/automation/webhook.py b/homeassistant/components/automation/webhook.py new file mode 100644 index 00000000000..2c9c331cdc5 --- /dev/null +++ b/homeassistant/components/automation/webhook.py @@ -0,0 +1,54 @@ +""" +Offer webhook triggered automation rules. + +For more details about this automation rule, please refer to the documentation +at https://home-assistant.io/docs/automation/trigger/#webhook-trigger +""" +from functools import partial +import logging + +from aiohttp import hdrs +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.const import CONF_PLATFORM +import homeassistant.helpers.config_validation as cv + +DEPENDENCIES = ('webhook',) + +_LOGGER = logging.getLogger(__name__) +CONF_WEBHOOK_ID = 'webhook_id' + +TRIGGER_SCHEMA = vol.Schema({ + vol.Required(CONF_PLATFORM): 'webhook', + vol.Required(CONF_WEBHOOK_ID): cv.string, +}) + + +async def _handle_webhook(action, hass, webhook_id, request): + """Handle incoming webhook.""" + result = { + 'platform': 'webhook', + 'webhook_id': webhook_id, + } + + if 'json' in request.headers.get(hdrs.CONTENT_TYPE, ''): + result['json'] = await request.json() + else: + result['data'] = await request.post() + + hass.async_run_job(action, {'trigger': result}) + + +async def async_trigger(hass, config, action): + """Trigger based on incoming webhooks.""" + webhook_id = config.get(CONF_WEBHOOK_ID) + hass.components.webhook.async_register( + webhook_id, partial(_handle_webhook, action)) + + @callback + def unregister(): + """Unregister webhook.""" + hass.components.webhook.async_unregister(webhook_id) + + return unregister diff --git a/tests/components/automation/test_webhook.py b/tests/components/automation/test_webhook.py new file mode 100644 index 00000000000..a6cde395583 --- /dev/null +++ b/tests/components/automation/test_webhook.py @@ -0,0 +1,75 @@ +"""The tests for the webhook automation trigger.""" +from homeassistant.core import callback +from homeassistant.setup import async_setup_component + + +async def test_webhook_json(hass, aiohttp_client): + """Test triggering with a JSON webhook.""" + events = [] + + @callback + def store_event(event): + """Helepr to store events.""" + events.append(event) + + hass.bus.async_listen('test_success', store_event) + + assert await async_setup_component(hass, 'automation', { + 'automation': { + 'trigger': { + 'platform': 'webhook', + 'webhook_id': 'json_webhook' + }, + 'action': { + 'event': 'test_success', + 'event_data_template': { + 'hello': 'yo {{ trigger.json.hello }}', + } + } + } + }) + + client = await aiohttp_client(hass.http.app) + + await client.post('/api/webhook/json_webhook', json={ + 'hello': 'world' + }) + + assert len(events) == 1 + assert events[0].data['hello'] == 'yo world' + + +async def test_webhook_post(hass, aiohttp_client): + """Test triggering with a POST webhook.""" + events = [] + + @callback + def store_event(event): + """Helepr to store events.""" + events.append(event) + + hass.bus.async_listen('test_success', store_event) + + assert await async_setup_component(hass, 'automation', { + 'automation': { + 'trigger': { + 'platform': 'webhook', + 'webhook_id': 'post_webhook' + }, + 'action': { + 'event': 'test_success', + 'event_data_template': { + 'hello': 'yo {{ trigger.data.hello }}', + } + } + } + }) + + client = await aiohttp_client(hass.http.app) + + await client.post('/api/webhook/post_webhook', data={ + 'hello': 'world' + }) + + assert len(events) == 1 + assert events[0].data['hello'] == 'yo world' From 59ec469722e784a90f03732c0a37f65aca2e9dbf Mon Sep 17 00:00:00 2001 From: damarco Date: Mon, 8 Oct 2018 20:23:26 +0200 Subject: [PATCH 048/265] Use only_cache parameter in binary_sensor.zha.Remote (#16711) --- homeassistant/components/binary_sensor/zha.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/binary_sensor/zha.py b/homeassistant/components/binary_sensor/zha.py index aa07a673c97..fa24ed89980 100644 --- a/homeassistant/components/binary_sensor/zha.py +++ b/homeassistant/components/binary_sensor/zha.py @@ -253,5 +253,9 @@ class Remote(zha.Entity, BinarySensorDevice): """Retrieve latest state.""" from zigpy.zcl.clusters.general import OnOff result = await zha.safe_read( - self._endpoint.out_clusters[OnOff.cluster_id], ['on_off']) + self._endpoint.out_clusters[OnOff.cluster_id], + ['on_off'], + allow_cache=False, + only_cache=(not self._initialized) + ) self._state = result.get('on_off', self._state) From 0c0184973bca27ac4003986a82e9395bd94e50fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jari=20Ylim=C3=A4inen?= Date: Mon, 8 Oct 2018 21:24:25 +0300 Subject: [PATCH 049/265] Add configurable temperature step for MQTT climate component (#16201) * Add configurable temperature step * Remove temp step from climate component --- homeassistant/components/climate/mqtt.py | 9 ++++++--- tests/components/climate/test_mqtt.py | 13 +++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/climate/mqtt.py b/homeassistant/components/climate/mqtt.py index 79c49db7955..ff9b78135fd 100644 --- a/homeassistant/components/climate/mqtt.py +++ b/homeassistant/components/climate/mqtt.py @@ -75,6 +75,7 @@ CONF_SEND_IF_OFF = 'send_if_off' CONF_MIN_TEMP = 'min_temp' CONF_MAX_TEMP = 'max_temp' +CONF_TEMP_STEP = 'temp_step' SCHEMA_BASE = CLIMATE_PLATFORM_SCHEMA.extend(MQTT_BASE_PLATFORM_SCHEMA.schema) PLATFORM_SCHEMA = SCHEMA_BASE.extend({ @@ -124,7 +125,8 @@ PLATFORM_SCHEMA = SCHEMA_BASE.extend({ vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string, vol.Optional(CONF_MIN_TEMP, default=DEFAULT_MIN_TEMP): vol.Coerce(float), - vol.Optional(CONF_MAX_TEMP, default=DEFAULT_MAX_TEMP): vol.Coerce(float) + vol.Optional(CONF_MAX_TEMP, default=DEFAULT_MAX_TEMP): vol.Coerce(float), + vol.Optional(CONF_TEMP_STEP, default=1.0): vol.Coerce(float) }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @@ -213,6 +215,7 @@ async def _async_setup_entity(hass, config, async_add_entities, config.get(CONF_PAYLOAD_NOT_AVAILABLE), config.get(CONF_MIN_TEMP), config.get(CONF_MAX_TEMP), + config.get(CONF_TEMP_STEP), discovery_hash, )]) @@ -226,7 +229,7 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice): current_swing_mode, current_operation, aux, send_if_off, payload_on, payload_off, availability_topic, payload_available, payload_not_available, - min_temp, max_temp, discovery_hash): + min_temp, max_temp, temp_step, discovery_hash): """Initialize the climate device.""" MqttAvailability.__init__(self, availability_topic, qos, payload_available, payload_not_available) @@ -249,7 +252,7 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice): self._fan_list = fan_mode_list self._operation_list = mode_list self._swing_list = swing_mode_list - self._target_temperature_step = 1 + self._target_temperature_step = temp_step self._send_if_off = send_if_off self._payload_on = payload_on self._payload_off = payload_off diff --git a/tests/components/climate/test_mqtt.py b/tests/components/climate/test_mqtt.py index c63dbf26690..f330efd99b8 100644 --- a/tests/components/climate/test_mqtt.py +++ b/tests/components/climate/test_mqtt.py @@ -653,6 +653,19 @@ class TestMQTTClimate(unittest.TestCase): self.assertIsInstance(max_temp, float) self.assertEqual(60, max_temp) + def test_temp_step_custom(self): + """Test a custom temp step.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config['climate']['temp_step'] = 0.01 + + assert setup_component(self.hass, climate.DOMAIN, config) + + state = self.hass.states.get(ENTITY_CLIMATE) + temp_step = state.attributes.get('target_temp_step') + + self.assertIsInstance(temp_step, float) + self.assertEqual(0.01, temp_step) + async def test_discovery_removal_climate(hass, mqtt_mock, caplog): """Test removal of discovered climate.""" From 68d72931c43c974d0f629d0872ca6a0bd7e11b99 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 8 Oct 2018 20:50:24 +0200 Subject: [PATCH 050/265] block external IP (#17248) * block external IP * Update __init__.py --- .../components/emulated_hue/__init__.py | 15 ++++++++++-- .../components/emulated_hue/hue_api.py | 23 +++++++++++++++++++ tests/components/emulated_hue/test_hue_api.py | 10 ++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index 8a67b933b9f..5f1d61dd602 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -18,6 +18,8 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.deprecation import get_deprecated import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json, save_json +from homeassistant.components.http import real_ip + from .hue_api import ( HueUsernameView, HueAllLightsStateView, HueOneLightStateView, HueOneLightChangeView, HueGroupView) @@ -81,12 +83,20 @@ ATTR_EMULATED_HUE_NAME = 'emulated_hue_name' ATTR_EMULATED_HUE_HIDDEN = 'emulated_hue_hidden' -def setup(hass, yaml_config): +async def async_setup(hass, yaml_config): """Activate the emulated_hue component.""" config = Config(hass, yaml_config.get(DOMAIN, {})) app = web.Application() app['hass'] = hass + + real_ip.setup_real_ip(app, False, []) + # We misunderstood the startup signal. You're not allowed to change + # anything during startup. Temp workaround. + # pylint: disable=protected-access + app._on_startup.freeze() + await app.startup() + handler = None server = None @@ -131,7 +141,8 @@ def setup(hass, yaml_config): hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, stop_emulated_hue_bridge) - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_emulated_hue_bridge) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, + start_emulated_hue_bridge) return True diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index c6fa622513b..3699a45ef30 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -20,6 +20,9 @@ from homeassistant.components.fan import ( SPEED_MEDIUM, SPEED_HIGH ) from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http.const import KEY_REAL_IP +from homeassistant.util.network import is_local + _LOGGER = logging.getLogger(__name__) @@ -46,6 +49,10 @@ class HueUsernameView(HomeAssistantView): return self.json_message('devicetype not specified', HTTP_BAD_REQUEST) + if not is_local(request[KEY_REAL_IP]): + return self.json_message('only local IPs allowed', + HTTP_BAD_REQUEST) + return self.json([{'success': {'username': '12345678901234567890'}}]) @@ -63,6 +70,10 @@ class HueGroupView(HomeAssistantView): @core.callback def put(self, request, username): """Process a request to make the Logitech Pop working.""" + if not is_local(request[KEY_REAL_IP]): + return self.json_message('only local IPs allowed', + HTTP_BAD_REQUEST) + return self.json([{ 'error': { 'address': '/groups/0/action/scene', @@ -86,6 +97,10 @@ class HueAllLightsStateView(HomeAssistantView): @core.callback def get(self, request, username): """Process a request to get the list of available lights.""" + if not is_local(request[KEY_REAL_IP]): + return self.json_message('only local IPs allowed', + HTTP_BAD_REQUEST) + hass = request.app['hass'] json_response = {} @@ -114,6 +129,10 @@ class HueOneLightStateView(HomeAssistantView): @core.callback def get(self, request, username, entity_id): """Process a request to get the state of an individual light.""" + if not is_local(request[KEY_REAL_IP]): + return self.json_message('only local IPs allowed', + HTTP_BAD_REQUEST) + hass = request.app['hass'] entity_id = self.config.number_to_entity_id(entity_id) entity = hass.states.get(entity_id) @@ -146,6 +165,10 @@ class HueOneLightChangeView(HomeAssistantView): async def put(self, request, username, entity_number): """Process a request to set the state of an individual light.""" + if not is_local(request[KEY_REAL_IP]): + return self.json_message('only local IPs allowed', + HTTP_BAD_REQUEST) + config = self.config hass = request.app['hass'] entity_id = config.number_to_entity_id(entity_number) diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 3920a45ddf6..8582f5b38cf 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -1,6 +1,7 @@ """The tests for the emulated Hue component.""" import asyncio import json +from ipaddress import ip_address from unittest.mock import patch from aiohttp.hdrs import CONTENT_TYPE @@ -484,3 +485,12 @@ def perform_put_light_state(hass_hue, client, entity_id, is_on, yield from hass_hue.async_block_till_done() return result + + +async def test_external_ip_blocked(hue_client): + """Test external IP blocked.""" + with patch('homeassistant.components.http.real_ip.ip_address', + return_value=ip_address('45.45.45.45')): + result = await hue_client.get('/api/username/lights') + + assert result.status == 400 From 139376665948d2b036004417e2b087ae5c5e7e97 Mon Sep 17 00:00:00 2001 From: Nick Horvath Date: Mon, 8 Oct 2018 16:15:38 -0400 Subject: [PATCH 051/265] Thermoworks Smoke Sensor (#16139) * Add support for monitoring the temperature of a thermoworks smoke thermometer. * Use string formatting. * Add line break. * Add error handling for authentication. * Fix linting errors. * Fix quotes. * Bump thermoworks_smoke library version. * Review changes for @MartinHjelmare * Add unique id attribute and change battery attribute to the standard "battery_level". * requested changes to snake case and monitored conditions * fix lint error * exclude firmware from state attrs. rename original_unit to unit_of_min_max so it's more clear what it is for. * add device_info * add regex validator for exclude * undo device info stuff * remove serial number from attributes even though other components are allowed to have it... * exclude firmware --- .coveragerc | 1 + .../components/sensor/thermoworks_smoke.py | 180 ++++++++++++++++++ requirements_all.txt | 6 + 3 files changed, 187 insertions(+) create mode 100644 homeassistant/components/sensor/thermoworks_smoke.py diff --git a/.coveragerc b/.coveragerc index 02c5481c23a..6be00bdd54f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -779,6 +779,7 @@ omit = homeassistant/components/sensor/tank_utility.py homeassistant/components/sensor/ted5000.py homeassistant/components/sensor/temper.py + homeassistant/components/sensor/thermoworks_smoke.py homeassistant/components/sensor/time_date.py homeassistant/components/sensor/torque.py homeassistant/components/sensor/trafikverket_weatherstation.py diff --git a/homeassistant/components/sensor/thermoworks_smoke.py b/homeassistant/components/sensor/thermoworks_smoke.py new file mode 100644 index 00000000000..56ed138e221 --- /dev/null +++ b/homeassistant/components/sensor/thermoworks_smoke.py @@ -0,0 +1,180 @@ +""" +Support for getting the state of a Thermoworks Smoke Thermometer. + +Requires Smoke Gateway Wifi with an internet connection. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.thermoworks_smoke/ +""" +import logging + +from requests import RequestException +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import TEMP_FAHRENHEIT, CONF_EMAIL, CONF_PASSWORD,\ + CONF_MONITORED_CONDITIONS, CONF_EXCLUDE, ATTR_BATTERY_LEVEL +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['thermoworks_smoke==0.1.6', 'stringcase==1.2.0'] + +_LOGGER = logging.getLogger(__name__) + +PROBE_1 = 'probe1' +PROBE_2 = 'probe2' +PROBE_1_MIN = 'probe1_min' +PROBE_1_MAX = 'probe1_max' +PROBE_2_MIN = 'probe2_min' +PROBE_2_MAX = 'probe2_max' +BATTERY_LEVEL = 'battery' +FIRMWARE = 'firmware' + +SERIAL_REGEX = '^(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$' + +# map types to labels +SENSOR_TYPES = { + PROBE_1: 'Probe 1', + PROBE_2: 'Probe 2', + PROBE_1_MIN: 'Probe 1 Min', + PROBE_1_MAX: 'Probe 1 Max', + PROBE_2_MIN: 'Probe 2 Min', + PROBE_2_MAX: 'Probe 2 Max', +} + +# exclude these keys from thermoworks data +EXCLUDE_KEYS = [ + FIRMWARE +] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_EMAIL): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_MONITORED_CONDITIONS, default=[PROBE_1, PROBE_2]): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.Optional(CONF_EXCLUDE, default=[]): + vol.All(cv.ensure_list, [cv.matches_regex(SERIAL_REGEX)]), +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the thermoworks sensor.""" + import thermoworks_smoke + from requests.exceptions import HTTPError + + email = config[CONF_EMAIL] + password = config[CONF_PASSWORD] + monitored_variables = config[CONF_MONITORED_CONDITIONS] + excluded = config[CONF_EXCLUDE] + + try: + mgr = thermoworks_smoke.initialize_app(email, password, True, excluded) + + # list of sensor devices + dev = [] + + # get list of registered devices + for serial in mgr.serials(): + for variable in monitored_variables: + dev.append(ThermoworksSmokeSensor(variable, serial, mgr)) + + add_entities(dev, True) + except HTTPError as error: + msg = "{}".format(error.strerror) + if 'EMAIL_NOT_FOUND' in msg or \ + 'INVALID_PASSWORD' in msg: + _LOGGER.error("Invalid email and password combination") + else: + _LOGGER.error(msg) + + +class ThermoworksSmokeSensor(Entity): + """Implementation of a thermoworks smoke sensor.""" + + def __init__(self, sensor_type, serial, mgr): + """Initialize the sensor.""" + self._name = "{name} {sensor}".format( + name=mgr.name(serial), sensor=SENSOR_TYPES[sensor_type]) + self.type = sensor_type + self._state = None + self._attributes = {} + self._unit_of_measurement = TEMP_FAHRENHEIT + self._unique_id = "{serial}-{type}".format( + serial=serial, type=sensor_type) + self.serial = serial + self.mgr = mgr + self.update_unit() + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def unique_id(self): + """Return the unique id for the sensor.""" + return self._unique_id + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attributes + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this sensor.""" + return self._unit_of_measurement + + def update_unit(self): + """Set the units from the data.""" + if PROBE_2 in self.type: + self._unit_of_measurement = self.mgr.units(self.serial, PROBE_2) + else: + self._unit_of_measurement = self.mgr.units(self.serial, PROBE_1) + + def update(self): + """Get the monitored data from firebase.""" + from stringcase import camelcase, snakecase + try: + values = self.mgr.data(self.serial) + + # set state from data based on type of sensor + self._state = values.get(camelcase(self.type)) + + # set units + self.update_unit() + + # set basic attributes for all sensors + self._attributes = { + 'time': values['time'], + 'localtime': values['localtime'] + } + + # set extended attributes for main probe sensors + if self.type in [PROBE_1, PROBE_2]: + for key, val in values.items(): + # add all attributes that don't contain any probe name + # or contain a matching probe name + if ( + (self.type == PROBE_1 and key.find(PROBE_2) == -1) + or + (self.type == PROBE_2 and key.find(PROBE_1) == -1) + ): + if key == BATTERY_LEVEL: + key = ATTR_BATTERY_LEVEL + else: + # strip probe label and convert to snake_case + key = snakecase(key.replace(self.type, '')) + # add to attrs + if key and key not in EXCLUDE_KEYS: + self._attributes[key] = val + # store actual unit because attributes are not converted + self._attributes['unit_of_min_max'] = self._unit_of_measurement + + except (RequestException, ValueError, KeyError): + _LOGGER.warning("Could not update status for %s", self.name) diff --git a/requirements_all.txt b/requirements_all.txt index 027acec7029..3cac497446a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1401,6 +1401,9 @@ statsd==3.2.1 # homeassistant.components.sensor.steam_online steamodd==4.21 +# homeassistant.components.sensor.thermoworks_smoke +stringcase==1.2.0 + # homeassistant.components.ecovacs sucks==0.9.3 @@ -1434,6 +1437,9 @@ temperusb==1.5.3 # homeassistant.components.tesla teslajsonpy==0.0.23 +# homeassistant.components.sensor.thermoworks_smoke +thermoworks_smoke==0.1.6 + # homeassistant.components.thingspeak thingspeak==0.4.1 From 56a43436d7d61cbfd8673e1c37960e3dd6f5db18 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Mon, 8 Oct 2018 22:33:06 +0200 Subject: [PATCH 052/265] Bump python-miio requirement (#17260) --- homeassistant/components/device_tracker/xiaomi_miio.py | 2 +- homeassistant/components/fan/xiaomi_miio.py | 2 +- homeassistant/components/light/xiaomi_miio.py | 2 +- homeassistant/components/remote/xiaomi_miio.py | 2 +- homeassistant/components/sensor/xiaomi_miio.py | 2 +- homeassistant/components/switch/xiaomi_miio.py | 2 +- homeassistant/components/vacuum/xiaomi_miio.py | 2 +- requirements_all.txt | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/device_tracker/xiaomi_miio.py b/homeassistant/components/device_tracker/xiaomi_miio.py index b1cfc0aed4a..060849a3268 100644 --- a/homeassistant/components/device_tracker/xiaomi_miio.py +++ b/homeassistant/components/device_tracker/xiaomi_miio.py @@ -20,7 +20,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)), }) -REQUIREMENTS = ['python-miio==0.4.1', 'construct==2.9.41'] +REQUIREMENTS = ['python-miio==0.4.2', 'construct==2.9.41'] def get_scanner(hass, config): diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py index a66e833b4b2..fdbf06818b9 100644 --- a/homeassistant/components/fan/xiaomi_miio.py +++ b/homeassistant/components/fan/xiaomi_miio.py @@ -49,7 +49,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'zhimi.humidifier.ca1']), }) -REQUIREMENTS = ['python-miio==0.4.1', 'construct==2.9.41'] +REQUIREMENTS = ['python-miio==0.4.2', 'construct==2.9.41'] ATTR_MODEL = 'model' diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py index 51c36fc2dd0..335389586b7 100644 --- a/homeassistant/components/light/xiaomi_miio.py +++ b/homeassistant/components/light/xiaomi_miio.py @@ -42,7 +42,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'philips.light.candle2']), }) -REQUIREMENTS = ['python-miio==0.4.1', 'construct==2.9.41'] +REQUIREMENTS = ['python-miio==0.4.2', 'construct==2.9.41'] # The light does not accept cct values < 1 CCT_MIN = 1 diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py index 7cd588683de..6d82da9e9fd 100644 --- a/homeassistant/components/remote/xiaomi_miio.py +++ b/homeassistant/components/remote/xiaomi_miio.py @@ -22,7 +22,7 @@ from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.util.dt import utcnow -REQUIREMENTS = ['python-miio==0.4.1', 'construct==2.9.41'] +REQUIREMENTS = ['python-miio==0.4.2', 'construct==2.9.41'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/xiaomi_miio.py b/homeassistant/components/sensor/xiaomi_miio.py index 15af57bf46b..4cdf166cf99 100644 --- a/homeassistant/components/sensor/xiaomi_miio.py +++ b/homeassistant/components/sensor/xiaomi_miio.py @@ -25,7 +25,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) -REQUIREMENTS = ['python-miio==0.4.1', 'construct==2.9.41'] +REQUIREMENTS = ['python-miio==0.4.2', 'construct==2.9.41'] ATTR_POWER = 'power' ATTR_CHARGING = 'charging' diff --git a/homeassistant/components/switch/xiaomi_miio.py b/homeassistant/components/switch/xiaomi_miio.py index 821de5bf647..2ac009f4334 100644 --- a/homeassistant/components/switch/xiaomi_miio.py +++ b/homeassistant/components/switch/xiaomi_miio.py @@ -39,7 +39,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'chuangmi.plug.v3']), }) -REQUIREMENTS = ['python-miio==0.4.1', 'construct==2.9.41'] +REQUIREMENTS = ['python-miio==0.4.2', 'construct==2.9.41'] ATTR_POWER = 'power' ATTR_TEMPERATURE = 'temperature' diff --git a/homeassistant/components/vacuum/xiaomi_miio.py b/homeassistant/components/vacuum/xiaomi_miio.py index d2da4f3b6ac..abfbb342418 100644 --- a/homeassistant/components/vacuum/xiaomi_miio.py +++ b/homeassistant/components/vacuum/xiaomi_miio.py @@ -21,7 +21,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-miio==0.4.1', 'construct==2.9.41'] +REQUIREMENTS = ['python-miio==0.4.2', 'construct==2.9.41'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 3cac497446a..08a3ce1149a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1147,7 +1147,7 @@ python-juicenet==0.0.5 # homeassistant.components.sensor.xiaomi_miio # homeassistant.components.switch.xiaomi_miio # homeassistant.components.vacuum.xiaomi_miio -python-miio==0.4.1 +python-miio==0.4.2 # homeassistant.components.media_player.mpd python-mpd2==1.0.0 From 540d22d603605b0ee39c932bff42f2b5212f2e49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomas=20Hellstr=C3=B6m?= Date: Mon, 8 Oct 2018 23:54:55 +0200 Subject: [PATCH 053/265] Swedish weather institute weather component (#16717) * SMHI Component * Clean up typos * Fixed default values first config to home location (tests will follow) * Fixed tests and removed unused function * Minor fixup after comments from @kane610 * add support for precipitation in forecast * Removed old async_step_init not needed. --- .../components/smhi/.translations/en.json | 19 + .../components/smhi/.translations/sv.json | 19 + homeassistant/components/smhi/__init__.py | 39 + homeassistant/components/smhi/config_flow.py | 124 ++ homeassistant/components/smhi/const.py | 12 + homeassistant/components/smhi/strings.json | 19 + homeassistant/components/weather/__init__.py | 13 +- homeassistant/components/weather/smhi.py | 243 +++ homeassistant/config_entries.py | 1 + requirements_all.txt | 5 + requirements_test_all.txt | 5 + script/gen_requirements_all.py | 1 + tests/components/smhi/__init__.py | 1 + tests/components/smhi/common.py | 11 + tests/components/smhi/test_config_flow.py | 276 +++ tests/components/smhi/test_init.py | 39 + tests/components/weather/test_smhi.py | 292 +++ tests/fixtures/smhi.json | 1599 +++++++++++++++++ 18 files changed, 2716 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/smhi/.translations/en.json create mode 100644 homeassistant/components/smhi/.translations/sv.json create mode 100644 homeassistant/components/smhi/__init__.py create mode 100644 homeassistant/components/smhi/config_flow.py create mode 100644 homeassistant/components/smhi/const.py create mode 100644 homeassistant/components/smhi/strings.json create mode 100644 homeassistant/components/weather/smhi.py create mode 100644 tests/components/smhi/__init__.py create mode 100644 tests/components/smhi/common.py create mode 100644 tests/components/smhi/test_config_flow.py create mode 100644 tests/components/smhi/test_init.py create mode 100644 tests/components/weather/test_smhi.py create mode 100644 tests/fixtures/smhi.json diff --git a/homeassistant/components/smhi/.translations/en.json b/homeassistant/components/smhi/.translations/en.json new file mode 100644 index 00000000000..1a995a64a32 --- /dev/null +++ b/homeassistant/components/smhi/.translations/en.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Name already exists", + "wrong_location": "Location in Sweden only" + }, + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Name" + }, + "title": "Location in Sweden" + } + }, + "title": "Swedish weather service (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/sv.json b/homeassistant/components/smhi/.translations/sv.json new file mode 100644 index 00000000000..30d647ac2c4 --- /dev/null +++ b/homeassistant/components/smhi/.translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Namnet finns redan", + "wrong_location": "Endast plats i Sverige" + }, + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Namn" + }, + "title": "Plats i Sverige" + } + }, + "title": "SMHI svenskt väder" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/__init__.py b/homeassistant/components/smhi/__init__.py new file mode 100644 index 00000000000..d0e4b6ef487 --- /dev/null +++ b/homeassistant/components/smhi/__init__.py @@ -0,0 +1,39 @@ +""" +Component for the swedish weather institute weather service. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/smhi/ +""" +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import Config, HomeAssistant + +# Have to import for config_flow to work +# even if they are not used here +from .config_flow import smhi_locations # noqa: F401 +from .const import DOMAIN # noqa: F401 + +REQUIREMENTS = ['smhi-pkg==1.0.4'] + +DEFAULT_NAME = 'smhi' + + +async def async_setup(hass: HomeAssistant, config: Config) -> bool: + """Set up configured smhi.""" + # We allow setup only through config flow type of config + return True + + +async def async_setup_entry(hass: HomeAssistant, + config_entry: ConfigEntry) -> bool: + """Set up smhi forecast as config entry.""" + hass.async_create_task(hass.config_entries.async_forward_entry_setup( + config_entry, 'weather')) + return True + + +async def async_unload_entry(hass: HomeAssistant, + config_entry: ConfigEntry) -> bool: + """Unload a config entry.""" + await hass.config_entries.async_forward_entry_unload( + config_entry, 'weather') + return True diff --git a/homeassistant/components/smhi/config_flow.py b/homeassistant/components/smhi/config_flow.py new file mode 100644 index 00000000000..3c9875ab797 --- /dev/null +++ b/homeassistant/components/smhi/config_flow.py @@ -0,0 +1,124 @@ +"""Config flow to configure smhi component. + +First time the user creates the configuration and +a valid location is set in the hass configuration yaml +it will use that location and use it as default values. + +Additional locations can be added in config form. +The input location will be checked by invoking +the API. Exception will be thrown if the location +is not supported by the API (Swedish locations only) +""" +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant import config_entries, data_entry_flow +from homeassistant.const import (CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import aiohttp_client +from homeassistant.util import slugify + +from .const import DOMAIN, HOME_LOCATION_NAME + +REQUIREMENTS = ['smhi-pkg==1.0.4'] + + +@callback +def smhi_locations(hass: HomeAssistant): + """Return configurations of SMHI component.""" + return set((slugify(entry.data[CONF_NAME])) for + entry in hass.config_entries.async_entries(DOMAIN)) + + +@config_entries.HANDLERS.register(DOMAIN) +class SmhiFlowHandler(data_entry_flow.FlowHandler): + """Config flow for SMHI component.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self) -> None: + """Initialize SMHI forecast configuration flow.""" + self._errors = {} + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + self._errors = {} + + if user_input is not None: + is_ok = await self._check_location( + user_input[CONF_LONGITUDE], + user_input[CONF_LATITUDE] + ) + if is_ok: + name = slugify(user_input[CONF_NAME]) + if not self._name_in_configuration_exists(name): + return self.async_create_entry( + title=user_input[CONF_NAME], + data=user_input, + ) + + self._errors[CONF_NAME] = 'name_exists' + else: + self._errors['base'] = 'wrong_location' + + # If hass config has the location set and + # is a valid coordinate the default location + # is set as default values in the form + if not smhi_locations(self.hass): + if await self._homeassistant_location_exists(): + return await self._show_config_form( + name=HOME_LOCATION_NAME, + latitude=self.hass.config.latitude, + longitude=self.hass.config.longitude + ) + + return await self._show_config_form() + + async def _homeassistant_location_exists(self) -> bool: + """Return true if default location is set and is valid.""" + if self.hass.config.latitude != 0.0 and \ + self.hass.config.longitude != 0.0: + # Return true if valid location + if await self._check_location( + self.hass.config.longitude, + self.hass.config.latitude): + return True + return False + + def _name_in_configuration_exists(self, name: str) -> bool: + """Return True if name exists in configuration.""" + if name in smhi_locations(self.hass): + return True + return False + + async def _show_config_form(self, + name: str = None, + latitude: str = None, + longitude: str = None): + """Show the configuration form to edit location data.""" + return self.async_show_form( + step_id='user', + data_schema=vol.Schema({ + vol.Required(CONF_NAME, default=name): str, + vol.Required(CONF_LATITUDE, default=latitude): cv.latitude, + vol.Required(CONF_LONGITUDE, default=longitude): cv.longitude + }), + errors=self._errors, + ) + + async def _check_location(self, longitude: str, latitude: str) -> bool: + """Return true if location is ok.""" + from smhi.smhi_lib import Smhi, SmhiForecastException + try: + session = aiohttp_client.async_get_clientsession(self.hass) + smhi_api = Smhi(longitude, latitude, session=session) + + await smhi_api.async_get_forecast() + + return True + except SmhiForecastException: + # The API will throw an exception if faulty location + pass + + return False diff --git a/homeassistant/components/smhi/const.py b/homeassistant/components/smhi/const.py new file mode 100644 index 00000000000..49e0f295873 --- /dev/null +++ b/homeassistant/components/smhi/const.py @@ -0,0 +1,12 @@ +"""Constants in smhi component.""" +import logging +from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN + +HOME_LOCATION_NAME = 'Home' + +ATTR_SMHI_CLOUDINESS = 'cloudiness' +DOMAIN = 'smhi' +LOGGER = logging.getLogger('homeassistant.components.smhi') +ENTITY_ID_SENSOR_FORMAT = WEATHER_DOMAIN + ".smhi_{}" +ENTITY_ID_SENSOR_FORMAT_HOME = ENTITY_ID_SENSOR_FORMAT.format( + HOME_LOCATION_NAME) diff --git a/homeassistant/components/smhi/strings.json b/homeassistant/components/smhi/strings.json new file mode 100644 index 00000000000..dbf1172b7d6 --- /dev/null +++ b/homeassistant/components/smhi/strings.json @@ -0,0 +1,19 @@ +{ + "config": { + "title": "Swedish weather service (SMHI)", + "step": { + "user": { + "title": "Location in Sweden", + "data": { + "name": "Name", + "latitude": "Latitude", + "longitude": "Longitude" + } + } + }, + "error": { + "name_exists": "Name already exists", + "wrong_location": "Location Sweden only" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index f9a8f1fbbe4..725c7f609a7 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -40,12 +40,21 @@ ATTR_WEATHER_WIND_SPEED = 'wind_speed' async def async_setup(hass, config): """Set up the weather component.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) - + component = hass.data[DOMAIN] = EntityComponent(_LOGGER, DOMAIN, hass) await component.async_setup(config) return True +async def async_setup_entry(hass, entry): + """Set up a config entry.""" + return await hass.data[DOMAIN].async_setup_entry(entry) + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + return await hass.data[DOMAIN].async_unload_entry(entry) + + class WeatherEntity(Entity): """ABC for weather data.""" diff --git a/homeassistant/components/weather/smhi.py b/homeassistant/components/weather/smhi.py new file mode 100644 index 00000000000..c24d3f8f091 --- /dev/null +++ b/homeassistant/components/weather/smhi.py @@ -0,0 +1,243 @@ +"""Support for the Swedish weather institute weather service. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/weather.smhi/ +""" + + +import asyncio +import logging +from datetime import timedelta +from typing import Dict, List + +import aiohttp +import async_timeout + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_LATITUDE, CONF_LONGITUDE, + CONF_NAME, TEMP_CELSIUS) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import aiohttp_client +from homeassistant.util import dt, Throttle + +from homeassistant.components.weather import ( + WeatherEntity, ATTR_FORECAST_CONDITION, ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, + ATTR_FORECAST_PRECIPITATION) + +from homeassistant.components.smhi.const import ( + ENTITY_ID_SENSOR_FORMAT, ATTR_SMHI_CLOUDINESS) + +DEPENDENCIES = ['smhi'] +REQUIREMENTS = ['smhi-pkg==1.0.4'] + +_LOGGER = logging.getLogger(__name__) + +# Used to map condition from API results +CONDITION_CLASSES = { + 'cloudy': [5, 6], + 'fog': [7], + 'hail': [], + 'lightning': [21], + 'lightning-rainy': [11], + 'partlycloudy': [3, 4], + 'pouring': [10, 20], + 'rainy': [8, 9, 18, 19], + 'snowy': [15, 16, 17, 25, 26, 27], + 'snowy-rainy': [12, 13, 14, 22, 23, 24], + 'sunny': [1, 2], + 'windy': [], + 'windy-variant': [], + 'exceptional': [], +} + + +# 5 minutes between retrying connect to API again +RETRY_TIMEOUT = 5*60 + +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=31) + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Old way of setting up components. + + Can only be called when a user accidentally mentions smhi in the + config. In that case it will be ignored. + """ + pass + + +async def async_setup_entry(hass: HomeAssistant, + config_entry: ConfigEntry, + config_entries) -> bool: + """Add a weather entity from map location.""" + location = config_entry.data + name = location[CONF_NAME] + + session = aiohttp_client.async_get_clientsession(hass) + + entity = SmhiWeather(name, location[CONF_LATITUDE], + location[CONF_LONGITUDE], + session=session) + entity.entity_id = ENTITY_ID_SENSOR_FORMAT.format(name) + + config_entries([entity], True) + return True + + +class SmhiWeather(WeatherEntity): + """Representation of a weather entity.""" + + def __init__(self, name: str, latitude: str, + longitude: str, + session: aiohttp.ClientSession = None) -> None: + """Initialize the SMHI weather entity.""" + from smhi import Smhi + + self._name = name + self._latitude = latitude + self._longitude = longitude + self._forecasts = None + self._fail_count = 0 + self._smhi_api = Smhi(self._longitude, self._latitude, + session=session) + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + async def async_update(self) -> None: + """Refresh the forecast data from SMHI weather API.""" + from smhi.smhi_lib import SmhiForecastException + + def fail(): + self._fail_count += 1 + if self._fail_count < 3: + self.hass.helpers.event.async_call_later( + RETRY_TIMEOUT, self.retry_update()) + + try: + with async_timeout.timeout(10, loop=self.hass.loop): + self._forecasts = await self.get_weather_forecast() + self._fail_count = 0 + + except (asyncio.TimeoutError, SmhiForecastException): + _LOGGER.error("Failed to connect to SMHI API, " + "retry in 5 minutes") + fail() + + async def retry_update(self): + """Retry refresh weather forecast.""" + self.async_update() + + async def get_weather_forecast(self) -> []: + """Return the current forecasts from SMHI API.""" + return await self._smhi_api.async_get_forecast() + + @property + def name(self) -> str: + """Return the name of the sensor.""" + return self._name + + @property + def temperature(self) -> int: + """Return the temperature.""" + if self._forecasts is not None: + return self._forecasts[0].temperature + return None + + @property + def temperature_unit(self) -> str: + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def humidity(self) -> int: + """Return the humidity.""" + if self._forecasts is not None: + return self._forecasts[0].humidity + return None + + @property + def wind_speed(self) -> float: + """Return the wind speed.""" + if self._forecasts is not None: + # Convert from m/s to km/h + return round(self._forecasts[0].wind_speed*18/5) + return None + + @property + def wind_bearing(self) -> int: + """Return the wind bearing.""" + if self._forecasts is not None: + return self._forecasts[0].wind_direction + return None + + @property + def visibility(self) -> float: + """Return the visibility.""" + if self._forecasts is not None: + return self._forecasts[0].horizontal_visibility + return None + + @property + def pressure(self) -> int: + """Return the pressure.""" + if self._forecasts is not None: + return self._forecasts[0].pressure + return None + + @property + def cloudiness(self) -> int: + """Return the cloudiness.""" + if self._forecasts is not None: + return self._forecasts[0].cloudiness + return None + + @property + def condition(self) -> str: + """Return the weather condition.""" + if self._forecasts is None: + return None + return next(( + k for k, v in CONDITION_CLASSES.items() + if self._forecasts[0].symbol in v), None) + + @property + def attribution(self) -> str: + """Return the attribution.""" + return 'Swedish weather institute (SMHI)' + + @property + def forecast(self) -> List: + """Return the forecast.""" + if self._forecasts is None: + return None + + data = [] + for forecast in self._forecasts: + condition = next(( + k for k, v in CONDITION_CLASSES.items() + if forecast.symbol in v), None) + + # Only get mid day forecasts + if forecast.valid_time.hour == 12: + data.append({ + ATTR_FORECAST_TIME: + dt.as_local(forecast.valid_time), + ATTR_FORECAST_TEMP: + forecast.temperature_max, + ATTR_FORECAST_TEMP_LOW: + forecast.temperature_min, + ATTR_FORECAST_PRECIPITATION: + round(forecast.mean_precipitation*24), + ATTR_FORECAST_CONDITION: + condition + }) + + return data + + @property + def device_state_attributes(self) -> Dict: + """Return SMHI specific attributes.""" + if self.cloudiness: + return {ATTR_SMHI_CLOUDINESS: self.cloudiness} diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index a5c44c30ce7..56d4d24eea2 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -146,6 +146,7 @@ FLOWS = [ 'mqtt', 'nest', 'openuv', + 'smhi', 'sonos', 'tradfri', 'zone', diff --git a/requirements_all.txt b/requirements_all.txt index 08a3ce1149a..21b50dc4b95 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1366,6 +1366,11 @@ smappy==0.2.16 # homeassistant.components.sensor.htu21d # smbus-cffi==0.5.1 +# homeassistant.components.smhi +# homeassistant.components.smhi.config_flow +# homeassistant.components.weather.smhi +smhi-pkg==1.0.4 + # homeassistant.components.media_player.snapcast snapcast==2.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b4e995d580c..754a8947a70 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -211,6 +211,11 @@ rxv==0.5.1 # homeassistant.components.sleepiq sleepyq==0.6 +# homeassistant.components.smhi +# homeassistant.components.smhi.config_flow +# homeassistant.components.weather.smhi +smhi-pkg==1.0.4 + # homeassistant.components.climate.honeywell somecomfort==0.5.2 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 9c0323bf5ca..9c695054ffc 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -96,6 +96,7 @@ TEST_REQUIREMENTS = ( 'ring_doorbell', 'rxv', 'sleepyq', + 'smhi-pkg', 'somecomfort', 'sqlalchemy', 'statsd', diff --git a/tests/components/smhi/__init__.py b/tests/components/smhi/__init__.py new file mode 100644 index 00000000000..100b1f1bbb1 --- /dev/null +++ b/tests/components/smhi/__init__.py @@ -0,0 +1 @@ +"""Tests for the SMHI component.""" diff --git a/tests/components/smhi/common.py b/tests/components/smhi/common.py new file mode 100644 index 00000000000..ecf904ac9c9 --- /dev/null +++ b/tests/components/smhi/common.py @@ -0,0 +1,11 @@ +"""Common test utilities.""" +from unittest.mock import Mock + + +class AsyncMock(Mock): + """Implements Mock async.""" + + # pylint: disable=W0235 + async def __call__(self, *args, **kwargs): + """Hack for async support for Mock.""" + return super(AsyncMock, self).__call__(*args, **kwargs) diff --git a/tests/components/smhi/test_config_flow.py b/tests/components/smhi/test_config_flow.py new file mode 100644 index 00000000000..b4e543231d9 --- /dev/null +++ b/tests/components/smhi/test_config_flow.py @@ -0,0 +1,276 @@ +"""Tests for SMHI config flow.""" +from unittest.mock import Mock, patch + +from smhi.smhi_lib import Smhi as SmhiApi, SmhiForecastException + +from tests.common import mock_coro + +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.components.smhi import config_flow + + +# pylint: disable=W0212 +async def test_homeassistant_location_exists() -> None: + """Test if homeassistant location exists it should return True.""" + hass = Mock() + flow = config_flow.SmhiFlowHandler() + flow.hass = hass + with patch.object(flow, '_check_location', + return_value=mock_coro(True)): + # Test exists + hass.config.location_name = 'Home' + hass.config.latitude = 17.8419 + hass.config.longitude = 59.3262 + + assert await flow._homeassistant_location_exists() is True + + # Test not exists + hass.config.location_name = None + hass.config.latitude = 0 + hass.config.longitude = 0 + + assert await flow._homeassistant_location_exists() is False + + +async def test_name_in_configuration_exists() -> None: + """Test if home location exists in configuration.""" + hass = Mock() + flow = config_flow.SmhiFlowHandler() + flow.hass = hass + + # Test exists + hass.config.location_name = 'Home' + hass.config.latitude = 17.8419 + hass.config.longitude = 59.3262 + + # Check not exists + with patch.object(config_flow, 'smhi_locations', + return_value={ + 'test': 'something', 'test2': 'something else' + }): + + assert flow._name_in_configuration_exists('no_exist_name') is False + + # Check exists + with patch.object(config_flow, 'smhi_locations', + return_value={ + 'test': 'something', 'name_exist': 'config' + }): + + assert flow._name_in_configuration_exists('name_exist') is True + + +def test_smhi_locations(hass) -> None: + """Test return empty set.""" + locations = config_flow.smhi_locations(hass) + assert not locations + + +async def test_show_config_form() -> None: + """Test show configuration form.""" + hass = Mock() + flow = config_flow.SmhiFlowHandler() + flow.hass = hass + + result = await flow._show_config_form() + + assert result['type'] == 'form' + assert result['step_id'] == 'user' + + +async def test_show_config_form_default_values() -> None: + """Test show configuration form.""" + hass = Mock() + flow = config_flow.SmhiFlowHandler() + flow.hass = hass + + result = await flow._show_config_form( + name="test", latitude='65', longitude='17') + + assert result['type'] == 'form' + assert result['step_id'] == 'user' + + +async def test_flow_with_home_location(hass) -> None: + """Test config flow . + + Tests the flow when a default location is configured + then it should return a form with default values + """ + flow = config_flow.SmhiFlowHandler() + flow.hass = hass + + with patch.object(flow, '_check_location', + return_value=mock_coro(True)): + hass.config.location_name = 'Home' + hass.config.latitude = 17.8419 + hass.config.longitude = 59.3262 + + result = await flow.async_step_user() + assert result['type'] == 'form' + assert result['step_id'] == 'user' + + +async def test_flow_show_form() -> None: + """Test show form scenarios first time. + + Test when the form should show when no configurations exists + """ + hass = Mock() + flow = config_flow.SmhiFlowHandler() + flow.hass = hass + + # Test show form when home assistant config exists and + # home is already configured, then new config is allowed + with \ + patch.object(flow, '_show_config_form', + return_value=mock_coro()) as config_form, \ + patch.object(flow, '_homeassistant_location_exists', + return_value=mock_coro(True)), \ + patch.object(config_flow, 'smhi_locations', + return_value={ + 'test': 'something', 'name_exist': 'config' + }): + await flow.async_step_user() + assert len(config_form.mock_calls) == 1 + + # Test show form when home assistant config not and + # home is not configured + with \ + patch.object(flow, '_show_config_form', + return_value=mock_coro()) as config_form, \ + patch.object(flow, '_homeassistant_location_exists', + return_value=mock_coro(False)), \ + patch.object(config_flow, 'smhi_locations', + return_value={ + 'test': 'something', 'name_exist': 'config' + }): + + await flow.async_step_user() + assert len(config_form.mock_calls) == 1 + + +async def test_flow_show_form_name_exists() -> None: + """Test show form if name already exists. + + Test when the form should show when no configurations exists + """ + hass = Mock() + flow = config_flow.SmhiFlowHandler() + flow.hass = hass + test_data = {'name': 'home', CONF_LONGITUDE: '0', CONF_LATITUDE: '0'} + # Test show form when home assistant config exists and + # home is already configured, then new config is allowed + with \ + patch.object(flow, '_show_config_form', + return_value=mock_coro()) as config_form, \ + patch.object(flow, '_name_in_configuration_exists', + return_value=True), \ + patch.object(config_flow, 'smhi_locations', + return_value={ + 'test': 'something', 'name_exist': 'config' + }), \ + patch.object(flow, '_check_location', + return_value=mock_coro(True)): + + await flow.async_step_user(user_input=test_data) + + assert len(config_form.mock_calls) == 1 + assert len(flow._errors) == 1 + + +async def test_flow_entry_created_from_user_input() -> None: + """Test that create data from user input. + + Test when the form should show when no configurations exists + """ + hass = Mock() + flow = config_flow.SmhiFlowHandler() + flow.hass = hass + + test_data = {'name': 'home', CONF_LONGITUDE: '0', CONF_LATITUDE: '0'} + + # Test that entry created when user_input name not exists + with \ + patch.object(flow, '_show_config_form', + return_value=mock_coro()) as config_form, \ + patch.object(flow, '_name_in_configuration_exists', + return_value=False), \ + patch.object(flow, '_homeassistant_location_exists', + return_value=mock_coro(False)), \ + patch.object(config_flow, 'smhi_locations', + return_value={ + 'test': 'something', 'name_exist': 'config' + }), \ + patch.object(flow, '_check_location', + return_value=mock_coro(True)): + + result = await flow.async_step_user(user_input=test_data) + + assert result['type'] == 'create_entry' + assert result['data'] == test_data + assert not config_form.mock_calls + + +async def test_flow_entry_created_user_input_faulty() -> None: + """Test that create data from user input and are faulty. + + Test when the form should show when user puts faulty location + in the config gui. Then the form should show with error + """ + hass = Mock() + flow = config_flow.SmhiFlowHandler() + flow.hass = hass + + test_data = {'name': 'home', CONF_LONGITUDE: '0', CONF_LATITUDE: '0'} + + # Test that entry created when user_input name not exists + with \ + patch.object(flow, '_check_location', + return_value=mock_coro(True)), \ + patch.object(flow, '_show_config_form', + return_value=mock_coro()) as config_form, \ + patch.object(flow, '_name_in_configuration_exists', + return_value=False), \ + patch.object(flow, '_homeassistant_location_exists', + return_value=mock_coro(False)), \ + patch.object(config_flow, 'smhi_locations', + return_value={ + 'test': 'something', 'name_exist': 'config' + }), \ + patch.object(flow, '_check_location', + return_value=mock_coro(False)): + + await flow.async_step_user(user_input=test_data) + + assert len(config_form.mock_calls) == 1 + assert len(flow._errors) == 1 + + +async def test_check_location_correct() -> None: + """Test check location when correct input.""" + hass = Mock() + flow = config_flow.SmhiFlowHandler() + flow.hass = hass + + with \ + patch.object(config_flow.aiohttp_client, 'async_get_clientsession'),\ + patch.object(SmhiApi, 'async_get_forecast', + return_value=mock_coro()): + + assert await flow._check_location('58', '17') is True + + +async def test_check_location_faulty() -> None: + """Test check location when faulty input.""" + hass = Mock() + flow = config_flow.SmhiFlowHandler() + flow.hass = hass + + with \ + patch.object(config_flow.aiohttp_client, + 'async_get_clientsession'), \ + patch.object(SmhiApi, 'async_get_forecast', + side_effect=SmhiForecastException()): + + assert await flow._check_location('58', '17') is False diff --git a/tests/components/smhi/test_init.py b/tests/components/smhi/test_init.py new file mode 100644 index 00000000000..2b0dbaabafd --- /dev/null +++ b/tests/components/smhi/test_init.py @@ -0,0 +1,39 @@ +"""Test SMHI component setup process.""" +from unittest.mock import Mock + +from homeassistant.components import smhi + +from .common import AsyncMock + +TEST_CONFIG = { + "config": { + "name": "0123456789ABCDEF", + "longitude": "62.0022", + "latitude": "17.0022" + } +} + + +async def test_setup_always_return_true() -> None: + """Test async_setup always returns True.""" + hass = Mock() + # Returns true with empty config + assert await smhi.async_setup(hass, {}) is True + + # Returns true with a config provided + assert await smhi.async_setup(hass, TEST_CONFIG) is True + + +async def test_forward_async_setup_entry() -> None: + """Test that it will forward setup entry.""" + hass = Mock() + + assert await smhi.async_setup_entry(hass, {}) is True + assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 1 + + +async def test_forward_async_unload_entry() -> None: + """Test that it will forward unload entry.""" + hass = AsyncMock() + assert await smhi.async_unload_entry(hass, {}) is True + assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 1 diff --git a/tests/components/weather/test_smhi.py b/tests/components/weather/test_smhi.py new file mode 100644 index 00000000000..11a5028842b --- /dev/null +++ b/tests/components/weather/test_smhi.py @@ -0,0 +1,292 @@ +"""Test for the smhi weather entity.""" +import asyncio +import logging +from datetime import datetime, timezone +from unittest.mock import Mock, patch + +from homeassistant.components.weather import ( + ATTR_FORECAST_CONDITION, ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, + ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, + ATTR_FORECAST_TEMP_LOW, ATTR_WEATHER_VISIBILITY, ATTR_WEATHER_ATTRIBUTION, + ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED, + ATTR_FORECAST_PRECIPITATION, smhi as weather_smhi, + DOMAIN as WEATHER_DOMAIN) +from homeassistant.const import TEMP_CELSIUS +from homeassistant.core import HomeAssistant + +from tests.common import load_fixture, MockConfigEntry + +from homeassistant.components.smhi.const import ATTR_SMHI_CLOUDINESS + +_LOGGER = logging.getLogger(__name__) + +TEST_CONFIG = { + "name": "test", + "longitude": "17.84197", + "latitude": "59.32624" +} + + +async def test_setup_hass(hass: HomeAssistant, aioclient_mock) -> None: + """Test for successfully setting up the smhi platform. + + This test are deeper integrated with the core. Since only + config_flow is used the component are setup with + "async_forward_entry_setup". The actual result are tested + with the entity state rather than "per function" unity tests + """ + from smhi.smhi_lib import APIURL_TEMPLATE + + uri = APIURL_TEMPLATE.format( + TEST_CONFIG['longitude'], TEST_CONFIG['latitude']) + api_response = load_fixture('smhi.json') + aioclient_mock.get(uri, text=api_response) + + entry = MockConfigEntry(domain='smhi', data=TEST_CONFIG) + + await hass.config_entries.async_forward_entry_setup(entry, WEATHER_DOMAIN) + assert aioclient_mock.call_count == 1 + + # Testing the actual entity state for + # deeper testing than normal unity test + state = hass.states.get('weather.smhi_test') + + assert state.state == 'sunny' + assert state.attributes[ATTR_SMHI_CLOUDINESS] == 50 + assert state.attributes[ATTR_WEATHER_ATTRIBUTION].find('SMHI') >= 0 + assert state.attributes[ATTR_WEATHER_HUMIDITY] == 55 + assert state.attributes[ATTR_WEATHER_PRESSURE] == 1024 + assert state.attributes[ATTR_WEATHER_TEMPERATURE] == 17 + assert state.attributes[ATTR_WEATHER_VISIBILITY] == 50 + assert state.attributes[ATTR_WEATHER_WIND_SPEED] == 7 + assert state.attributes[ATTR_WEATHER_WIND_BEARING] == 134 + _LOGGER.error(state.attributes) + assert len(state.attributes['forecast']) == 1 + + forecast = state.attributes['forecast'][0] + assert forecast[ATTR_FORECAST_TIME] == datetime(2018, 9, 2, 12, 0, + tzinfo=timezone.utc) + assert forecast[ATTR_FORECAST_TEMP] == 20 + assert forecast[ATTR_FORECAST_TEMP_LOW] == 6 + assert forecast[ATTR_FORECAST_PRECIPITATION] == 0 + assert forecast[ATTR_FORECAST_CONDITION] == 'partlycloudy' + + +async def test_setup_plattform(hass): + """Test that setup plattform does nothing.""" + assert await weather_smhi.async_setup_platform(hass, None, None) is None + + +def test_properties_no_data(hass: HomeAssistant) -> None: + """Test properties when no API data available.""" + weather = weather_smhi.SmhiWeather('name', '10', '10') + weather.hass = hass + + assert weather.name == 'name' + assert weather.should_poll is True + assert weather.temperature is None + assert weather.humidity is None + assert weather.wind_speed is None + assert weather.wind_bearing is None + assert weather.visibility is None + assert weather.pressure is None + assert weather.cloudiness is None + assert weather.condition is None + assert weather.forecast is None + assert weather.temperature_unit == TEMP_CELSIUS + + +# pylint: disable=W0212 +def test_properties_unknown_symbol() -> None: + """Test behaviour when unknown symbol from API.""" + hass = Mock() + data = Mock() + data.temperature = 5 + data.mean_precipitation = 1 + data.humidity = 5 + data.wind_speed = 10 + data.wind_direction = 180 + data.horizontal_visibility = 6 + data.pressure = 1008 + data.cloudiness = 52 + data.symbol = 100 # Faulty symbol + data.valid_time = datetime(2018, 1, 1, 0, 1, 2) + + data2 = Mock() + data2.temperature = 5 + data2.mean_precipitation = 1 + data2.humidity = 5 + data2.wind_speed = 10 + data2.wind_direction = 180 + data2.horizontal_visibility = 6 + data2.pressure = 1008 + data2.cloudiness = 52 + data2.symbol = 100 # Faulty symbol + data2.valid_time = datetime(2018, 1, 1, 12, 1, 2) + + data3 = Mock() + data3.temperature = 5 + data3.mean_precipitation = 1 + data3.humidity = 5 + data3.wind_speed = 10 + data3.wind_direction = 180 + data3.horizontal_visibility = 6 + data3.pressure = 1008 + data3.cloudiness = 52 + data3.symbol = 100 # Faulty symbol + data3.valid_time = datetime(2018, 1, 2, 12, 1, 2) + + testdata = [ + data, + data2, + data3 + ] + + weather = weather_smhi.SmhiWeather('name', '10', '10') + weather.hass = hass + weather._forecasts = testdata + assert weather.condition is None + forecast = weather.forecast[0] + assert forecast[ATTR_FORECAST_CONDITION] is None + + +# pylint: disable=W0212 +async def test_refresh_weather_forecast_exceeds_retries(hass) -> None: + """Test the refresh weather forecast function.""" + from smhi.smhi_lib import SmhiForecastException + + with \ + patch.object(hass.helpers.event, 'async_call_later') as call_later, \ + patch.object(weather_smhi.SmhiWeather, 'get_weather_forecast', + side_effect=SmhiForecastException()): + + weather = weather_smhi.SmhiWeather( + 'name', '17.0022', '62.0022') + weather.hass = hass + weather._fail_count = 2 + + await weather.async_update() + assert weather._forecasts is None + assert not call_later.mock_calls + + +async def test_refresh_weather_forecast_timeout(hass) -> None: + """Test timeout exception.""" + weather = weather_smhi.SmhiWeather( + 'name', '17.0022', '62.0022') + weather.hass = hass + + with \ + patch.object(hass.helpers.event, 'async_call_later') as call_later, \ + patch.object(weather_smhi.SmhiWeather, 'retry_update'), \ + patch.object(weather_smhi.SmhiWeather, 'get_weather_forecast', + side_effect=asyncio.TimeoutError): + + await weather.async_update() + assert len(call_later.mock_calls) == 1 + # Assert we are going to wait RETRY_TIMEOUT seconds + assert call_later.mock_calls[0][1][0] == weather_smhi.RETRY_TIMEOUT + + +async def test_refresh_weather_forecast_exception() -> None: + """Test any exception.""" + from smhi.smhi_lib import SmhiForecastException + + hass = Mock() + weather = weather_smhi.SmhiWeather( + 'name', '17.0022', '62.0022') + weather.hass = hass + + with \ + patch.object(hass.helpers.event, 'async_call_later') as call_later, \ + patch.object(weather_smhi, 'async_timeout'), \ + patch.object(weather_smhi.SmhiWeather, 'retry_update'), \ + patch.object(weather_smhi.SmhiWeather, 'get_weather_forecast', + side_effect=SmhiForecastException()): + + hass.async_add_job = Mock() + call_later = hass.helpers.event.async_call_later + + await weather.async_update() + assert len(call_later.mock_calls) == 1 + # Assert we are going to wait RETRY_TIMEOUT seconds + assert call_later.mock_calls[0][1][0] == weather_smhi.RETRY_TIMEOUT + + +async def test_retry_update(): + """Test retry function of refresh forecast.""" + hass = Mock() + weather = weather_smhi.SmhiWeather( + 'name', '17.0022', '62.0022') + weather.hass = hass + + with patch.object(weather_smhi.SmhiWeather, + 'async_update') as update: + await weather.retry_update() + assert len(update.mock_calls) == 1 + + +def test_condition_class(): + """Test condition class.""" + def get_condition(index: int) -> str: + """Return condition given index.""" + return [k for k, v in weather_smhi.CONDITION_CLASSES.items() + if index in v][0] + + # SMHI definitions as follows, see + # http://opendata.smhi.se/apidocs/metfcst/parameters.html + + # 1. Clear sky + assert get_condition(1) == 'sunny' + # 2. Nearly clear sky + assert get_condition(2) == 'sunny' + # 3. Variable cloudiness + assert get_condition(3) == 'partlycloudy' + # 4. Halfclear sky + assert get_condition(4) == 'partlycloudy' + # 5. Cloudy sky + assert get_condition(5) == 'cloudy' + # 6. Overcast + assert get_condition(6) == 'cloudy' + # 7. Fog + assert get_condition(7) == 'fog' + # 8. Light rain showers + assert get_condition(8) == 'rainy' + # 9. Moderate rain showers + assert get_condition(9) == 'rainy' + # 18. Light rain + assert get_condition(18) == 'rainy' + # 19. Moderate rain + assert get_condition(19) == 'rainy' + # 10. Heavy rain showers + assert get_condition(10) == 'pouring' + # 20. Heavy rain + assert get_condition(20) == 'pouring' + # 21. Thunder + assert get_condition(21) == 'lightning' + # 11. Thunderstorm + assert get_condition(11) == 'lightning-rainy' + # 15. Light snow showers + assert get_condition(15) == 'snowy' + # 16. Moderate snow showers + assert get_condition(16) == 'snowy' + # 17. Heavy snow showers + assert get_condition(17) == 'snowy' + # 25. Light snowfall + assert get_condition(25) == 'snowy' + # 26. Moderate snowfall + assert get_condition(26) == 'snowy' + # 27. Heavy snowfall + assert get_condition(27) == 'snowy' + # 12. Light sleet showers + assert get_condition(12) == 'snowy-rainy' + # 13. Moderate sleet showers + assert get_condition(13) == 'snowy-rainy' + # 14. Heavy sleet showers + assert get_condition(14) == 'snowy-rainy' + # 22. Light sleet + assert get_condition(22) == 'snowy-rainy' + # 23. Moderate sleet + assert get_condition(23) == 'snowy-rainy' + # 24. Heavy sleet + assert get_condition(24) == 'snowy-rainy' diff --git a/tests/fixtures/smhi.json b/tests/fixtures/smhi.json new file mode 100644 index 00000000000..f66cc546018 --- /dev/null +++ b/tests/fixtures/smhi.json @@ -0,0 +1,1599 @@ +{ + "approvedTime": "2018-09-01T14:06:18Z", + "referenceTime": "2018-09-01T14:00:00Z", + "geometry": { + "type": "Point", + "coordinates": [ + [ + 16.024394, + 63.341937 + ] + ] + }, + "timeSeries": [ + { + "validTime": "2018-09-01T15:00:00Z", + "parameters": [ + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [ + -9 + ] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [ + 1 + ] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [ + 0 + ] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [ + 2 + ] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [ + 0 + ] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [ + 1 + ] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [ + 4 + ] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [ + 0 + ] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [ + 0 + ] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [ + 0 + ] + }, + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [ + 1024.6 + ] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [ + 17 + ] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [ + 50 + ] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [ + 134 + ] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [ + 1.9 + ] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [ + 55 + ] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [ + 33 + ] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [ + 4.7 + ] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [ + 1 + ] + } + ] + }, + { + "validTime": "2018-09-02T00:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [ + 1026 + ] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [ + 6 + ] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [ + 12 + ] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [ + 214 + ] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [ + 0.7 + ] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [ + 87 + ] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [ + 0 + ] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [ + 0 + ] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [ + 0 + ] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [ + 0 + ] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [ + 0 + ] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [ + 1.5 + ] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [ + 0 + ] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [ + 0 + ] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [ + -9 + ] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [ + 0 + ] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [ + 0 + ] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [ + 0 + ] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [ + 1 + ] + } + ] + }, + { + "validTime": "2018-09-02T11:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [ + 1026.6 + ] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [ + 19.8 + ] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [ + 50 + ] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [ + 201 + ] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [ + 1.8 + ] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [ + 43 + ] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [ + 0 + ] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [ + 6 + ] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [ + 0 + ] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [ + 0 + ] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [ + 6 + ] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [ + 5.2 + ] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [ + 0 + ] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [ + 0 + ] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [ + -9 + ] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [ + 0 + ] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [ + 0 + ] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [ + 0 + ] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [ + 3 + ] + } + ] + }, + { + "validTime": "2018-09-02T12:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [ + 1026.5 + ] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [ + 20.6 + ] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [ + 50 + ] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [ + 203 + ] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [ + 1.7 + ] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [ + 43 + ] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [ + 0 + ] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [ + 9 + ] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [ + 0 + ] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [ + 0 + ] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [ + 6 + ] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [ + 5.1 + ] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [ + 0 + ] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [ + 0 + ] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [ + -9 + ] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [ + 0 + ] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [ + 0 + ] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [ + 0 + ] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [ + 3 + ] + } + ] + }, + { + "validTime": "2018-09-02T23:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [ + 1026 + ] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [ + 9.3 + ] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [ + 19.4 + ] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [ + 95 + ] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [ + 0.5 + ] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [ + 75 + ] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [ + 1 + ] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [ + 0 + ] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [ + 0 + ] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [ + 0 + ] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [ + 0 + ] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [ + 1.1 + ] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [ + 0 + ] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [ + 0 + ] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [ + -9 + ] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [ + 0 + ] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [ + 0 + ] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [ + 0 + ] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [ + 1 + ] + } + ] + }, + { + "validTime": "2018-09-03T00:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [ + 1025.9 + ] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [ + 8.5 + ] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [ + 50 + ] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [ + 104 + ] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [ + 0.5 + ] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [ + 73 + ] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [ + 1 + ] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [ + 0 + ] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [ + 0 + ] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [ + 0 + ] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [ + 0 + ] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [ + 1.1 + ] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [ + 0 + ] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [ + 0 + ] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [ + -9 + ] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [ + 0 + ] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [ + 0 + ] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [ + 0 + ] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [ + 1 + ] + } + ] + }, + { + "validTime": "2018-09-03T01:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [ + 1025.6 + ] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [ + 8 + ] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [ + 50 + ] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [ + 116 + ] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [ + 0.3 + ] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [ + 74 + ] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [ + 1 + ] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [ + 0 + ] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [ + 0 + ] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [ + 0 + ] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [ + 0 + ] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [ + 1 + ] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [ + 0 + ] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [ + 0 + ] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [ + -9 + ] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [ + 0 + ] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [ + 0 + ] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [ + 0 + ] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [ + 1 + ] + } + ] + }, + { + "validTime": "2018-09-04T12:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [ + 1020.5 + ] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [ + 19.2 + ] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [ + 50 + ] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [ + 353 + ] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [ + 1.4 + ] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [ + 60 + ] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [ + 0 + ] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [ + 7 + ] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [ + 3 + ] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [ + 5 + ] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [ + 4 + ] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [ + 4.7 + ] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [ + 0 + ] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [ + 0 + ] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [ + -9 + ] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [ + 0 + ] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [ + 0 + ] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [ + 0 + ] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [ + 4 + ] + } + ] + }, + { + "validTime": "2018-09-04T18:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [ + 1021.5 + ] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [ + 14.3 + ] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [ + 50 + ] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [ + 333 + ] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [ + 2.3 + ] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [ + 81 + ] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [ + 0 + ] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [ + 4 + ] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [ + 1 + ] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [ + 4 + ] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [ + 0 + ] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [ + 4.5 + ] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [ + 0 + ] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [ + 0.2 + ] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [ + 0 + ] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [ + 4 + ] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [ + 0 + ] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [ + 0 + ] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [ + 3 + ] + } + ] + } + ] +} \ No newline at end of file From 5db7d702c8e52180be73c5544508c41e5b3edfcb Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Tue, 9 Oct 2018 10:06:42 +0200 Subject: [PATCH 054/265] Remove warning on script delay (#17264) * Remove warning on script delay * Use suppress --- homeassistant/helpers/script.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index ac53a3e32a2..5e660ba7b7f 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -1,6 +1,7 @@ """Helpers to execute scripts.""" import logging +from contextlib import suppress from itertools import islice from typing import Optional, Sequence @@ -95,7 +96,9 @@ class Script(): def async_script_delay(now): """Handle delay.""" # pylint: disable=cell-var-from-loop - self._async_remove_listener() + with suppress(ValueError): + self._async_listener.remove(unsub) + self.hass.async_create_task( self.async_run(variables, context)) @@ -240,7 +243,8 @@ class Script(): @callback def async_script_timeout(now): """Call after timeout is retrieve.""" - self._async_remove_listener() + with suppress(ValueError): + self._async_listener.remove(unsub) # Check if we want to continue to execute # the script after the timeout From 4455a287fcf121451f593f71220a7526b704727e Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Tue, 9 Oct 2018 10:07:30 +0200 Subject: [PATCH 055/265] Add defaults, fixing #17229 (#17261) --- homeassistant/components/upnp/config_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index 885f2f64211..f695e3ada75 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -125,8 +125,8 @@ class UpnpFlowHandler(data_entry_flow.FlowHandler): data_schema=vol.Schema( OrderedDict([ (vol.Required('name'), vol.In(names)), - (vol.Optional('enable_sensors'), bool), - (vol.Optional('enable_port_mapping'), bool), + (vol.Optional('enable_sensors', default=False), bool), + (vol.Optional('enable_port_mapping', default=False), bool), ]) )) From 882c4b73ae98c56613bdbea9946292c53b09a686 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Tue, 9 Oct 2018 10:11:14 +0200 Subject: [PATCH 056/265] Fix ambient light state of the Philips Eyecare Lamp (Closes: #16269) (#17259) --- homeassistant/components/light/xiaomi_miio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py index 335389586b7..cefd4922bd0 100644 --- a/homeassistant/components/light/xiaomi_miio.py +++ b/homeassistant/components/light/xiaomi_miio.py @@ -713,7 +713,7 @@ class XiaomiPhilipsEyecareLampAmbientLight(XiaomiPhilipsAbstractLight): _LOGGER.debug("Got new state: %s", state) self._available = True - self._state = state.eyecare + self._state = state.ambient self._brightness = ceil((255 / 100.0) * state.ambient_brightness) except DeviceException as ex: From 757ba3b60e5349f2e977731ba96aed5b99db1af7 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Tue, 9 Oct 2018 10:11:34 +0200 Subject: [PATCH 057/265] Add basic support of the Philips Zhirui desk lamp (philips.light.mono1) (#17258) --- homeassistant/components/light/xiaomi_miio.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py index cefd4922bd0..ce1e504b54c 100644 --- a/homeassistant/components/light/xiaomi_miio.py +++ b/homeassistant/components/light/xiaomi_miio.py @@ -39,7 +39,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'philips.light.zyceiling', 'philips.light.bulb', 'philips.light.candle', - 'philips.light.candle2']), + 'philips.light.candle2', + 'philips.light.mono1']), }) REQUIREMENTS = ['python-miio==0.4.2', 'construct==2.9.41'] @@ -157,6 +158,12 @@ async def async_setup_platform(hass, config, async_add_entities, device = XiaomiPhilipsBulb(name, light, model, unique_id) devices.append(device) hass.data[DATA_KEY][host] = device + elif model == 'philips.light.mono1': + from miio import PhilipsBulb + light = PhilipsBulb(host, token) + device = XiaomiPhilipsGenericLight(name, light, model, unique_id) + devices.append(device) + hass.data[DATA_KEY][host] = device else: _LOGGER.error( 'Unsupported device found! Please create an issue at ' From 0c34c50d2fa2c7a35e4aa87c6759736b96785eed Mon Sep 17 00:00:00 2001 From: marcolertora Date: Tue, 9 Oct 2018 10:13:03 +0200 Subject: [PATCH 058/265] Added lumitek/ankuoo recswitch component (#15764) * Added lumitek/ankuoo recswitch component * cosmetics * remove callback * cosmetics * update requirements pyrecswitch==1.0.2 * add in .coveragerc --- .coveragerc | 1 + homeassistant/components/switch/recswitch.py | 101 +++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 105 insertions(+) create mode 100644 homeassistant/components/switch/recswitch.py diff --git a/.coveragerc b/.coveragerc index 6be00bdd54f..801932b19fb 100644 --- a/.coveragerc +++ b/.coveragerc @@ -820,6 +820,7 @@ omit = homeassistant/components/switch/pulseaudio_loopback.py homeassistant/components/switch/rainbird.py homeassistant/components/switch/rest.py + homeassistant/components/switch/recswitch.py homeassistant/components/switch/rpi_rf.py homeassistant/components/switch/snmp.py homeassistant/components/switch/switchbot.py diff --git a/homeassistant/components/switch/recswitch.py b/homeassistant/components/switch/recswitch.py new file mode 100644 index 00000000000..636c302cea1 --- /dev/null +++ b/homeassistant/components/switch/recswitch.py @@ -0,0 +1,101 @@ +""" +Support for Ankuoo RecSwitch MS6126 devices. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.recswitch/ +""" + +import logging + +import voluptuous as vol + +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME +import homeassistant.helpers.config_validation as cv + + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['pyrecswitch==1.0.2'] + +DEFAULT_NAME = 'RecSwitch {0}' + +DATA_RSN = 'RSN' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_MAC): vol.All(cv.string, vol.Upper), + vol.Optional(CONF_NAME): cv.string, +}) + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the device.""" + from pyrecswitch import RSNetwork + + host = config[CONF_HOST] + mac_address = config[CONF_MAC] + device_name = config.get(CONF_NAME) + + if not hass.data.get(DATA_RSN): + hass.data[DATA_RSN] = RSNetwork() + job = hass.data[DATA_RSN].create_datagram_endpoint(loop=hass.loop) + hass.async_create_task(job) + + device = hass.data[DATA_RSN].register_device(mac_address, host) + async_add_entities([RecSwitchSwitch(device, device_name, mac_address)]) + + +class RecSwitchSwitch(SwitchDevice): + """Representation of a recswitch device.""" + + def __init__(self, device, device_name, mac_address): + """Initialize a recswitch device.""" + self.gpio_state = False + self.device = device + self.device_name = device_name + self.mac_address = mac_address + if not self.device_name: + self.device_name = DEFAULT_NAME.format(self.mac_address) + + @property + def unique_id(self): + """Return the switch unique ID.""" + return self.mac_address + + @property + def name(self): + """Return the switch name.""" + return self.device_name + + @property + def is_on(self): + """Return true if switch is on.""" + return self.gpio_state + + async def async_turn_on(self, **kwargs): + """Turn on the switch.""" + await self.async_set_gpio_status(True) + + async def async_turn_off(self, **kwargs): + """Turn off the switch.""" + await self.async_set_gpio_status(False) + + async def async_set_gpio_status(self, status): + """Set the switch status.""" + from pyrecswitch import RSNetworkError + try: + ret = await self.device.set_gpio_status(status) + self.gpio_state = ret.state + except RSNetworkError as error: + _LOGGER.error('Setting status to %s: %r', self.name, error) + + async def async_update(self): + """Update the current switch status.""" + from pyrecswitch import RSNetworkError + try: + ret = await self.device.get_gpio_status() + self.gpio_state = ret.state + except RSNetworkError as error: + _LOGGER.error('Reading status from %s: %r', self.name, error) diff --git a/requirements_all.txt b/requirements_all.txt index 21b50dc4b95..1643897927d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1051,6 +1051,9 @@ pyqwikswitch==0.8 # homeassistant.components.rainbird pyrainbird==0.1.6 +# homeassistant.components.switch.recswitch +pyrecswitch==1.0.2 + # homeassistant.components.sabnzbd pysabnzbd==1.0.1 From 9190fe1c212293a9f93c0dd0c554402a42db8a92 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 9 Oct 2018 10:13:42 +0200 Subject: [PATCH 059/265] Add device registry to MQTT fan (#17250) --- homeassistant/components/fan/mqtt.py | 13 ++++++--- tests/components/fan/test_mqtt.py | 40 ++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fan/mqtt.py b/homeassistant/components/fan/mqtt.py index bc45754f698..1ff04cd913a 100644 --- a/homeassistant/components/fan/mqtt.py +++ b/homeassistant/components/fan/mqtt.py @@ -13,11 +13,12 @@ from homeassistant.core import callback from homeassistant.components import fan, mqtt from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, CONF_STATE, STATE_ON, STATE_OFF, - CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON) + CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, CONF_DEVICE) from homeassistant.components.mqtt import ( ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, - CONF_QOS, CONF_RETAIN, MqttAvailability, MqttDiscoveryUpdate) + CONF_QOS, CONF_RETAIN, MqttAvailability, MqttDiscoveryUpdate, + MqttEntityDeviceInfo) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import HomeAssistantType, ConfigType @@ -79,6 +80,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ SPEED_MEDIUM, SPEED_HIGH]): cv.ensure_list, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @@ -138,21 +140,24 @@ async def _async_setup_entity(hass, config, async_add_entities, config.get(CONF_PAYLOAD_AVAILABLE), config.get(CONF_PAYLOAD_NOT_AVAILABLE), config.get(CONF_UNIQUE_ID), + config.get(CONF_DEVICE), discovery_hash, )]) -class MqttFan(MqttAvailability, MqttDiscoveryUpdate, FanEntity): +class MqttFan(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, + FanEntity): """A MQTT fan component.""" def __init__(self, name, topic, templates, qos, retain, payload, speed_list, optimistic, availability_topic, payload_available, payload_not_available, unique_id: Optional[str], - discovery_hash): + device_config: Optional[ConfigType], discovery_hash): """Initialize the MQTT fan.""" MqttAvailability.__init__(self, availability_topic, qos, payload_available, payload_not_available) MqttDiscoveryUpdate.__init__(self, discovery_hash) + MqttEntityDeviceInfo.__init__(self, device_config) self._name = name self._topic = topic self._qos = qos diff --git a/tests/components/fan/test_mqtt.py b/tests/components/fan/test_mqtt.py index feb2ff6904d..e2742eeba7d 100644 --- a/tests/components/fan/test_mqtt.py +++ b/tests/components/fan/test_mqtt.py @@ -1,4 +1,5 @@ """Test MQTT fans.""" +import json import unittest from homeassistant.setup import setup_component, async_setup_component @@ -151,3 +152,42 @@ async def test_unique_id(hass): await hass.async_block_till_done() assert len(hass.states.async_entity_ids(fan.DOMAIN)) == 1 + + +async def test_entity_device_info_with_identifier(hass, mqtt_mock): + """Test MQTT fan device registry integration.""" + entry = MockConfigEntry(domain='mqtt') + entry.add_to_hass(hass) + await async_start(hass, 'homeassistant', {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + data = json.dumps({ + 'platform': 'mqtt', + 'name': 'Test 1', + 'state_topic': 'test-topic', + 'command_topic': 'test-command-topic', + 'device': { + 'identifiers': ['helloworld'], + 'connections': [ + ["mac", "02:5b:26:a8:dc:12"], + ], + 'manufacturer': 'Whatever', + 'name': 'Beer', + 'model': 'Glass', + 'sw_version': '0.1-beta', + }, + 'unique_id': 'veryunique' + }) + async_fire_mqtt_message(hass, 'homeassistant/fan/bla/config', + data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + device = registry.async_get_device({('mqtt', 'helloworld')}, set()) + assert device is not None + assert device.identifiers == {('mqtt', 'helloworld')} + assert device.connections == {('mac', "02:5b:26:a8:dc:12")} + assert device.manufacturer == 'Whatever' + assert device.name == 'Beer' + assert device.model == 'Glass' + assert device.sw_version == '0.1-beta' From 26cf5acd5ba457982ff8a0ec60832473a7d4f510 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 9 Oct 2018 10:14:18 +0200 Subject: [PATCH 060/265] Make async_track_time_change smarter (#17199) * Make async_track_time_change smarter * Move to util/dt * Remove unnecessary check * Lint * Remove tzinfo check * Remove check * Add comment about async_track_point_in_utc_time * Fix typing check * Lint --- homeassistant/helpers/event.py | 66 +++++---- homeassistant/util/dt.py | 165 ++++++++++++++++++++- tests/common.py | 2 +- tests/helpers/test_event.py | 263 ++++++++++++++++++++++----------- tests/util/test_dt.py | 125 ++++++++++++++++ 5 files changed, 502 insertions(+), 119 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 05555e8b5c6..1c28e2878e9 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -322,13 +322,13 @@ track_sunset = threaded_listener_factory(async_track_sunset) @callback @bind_hass -def async_track_utc_time_change(hass, action, year=None, month=None, day=None, +def async_track_utc_time_change(hass, action, hour=None, minute=None, second=None, local=False): """Add a listener that will fire if time matches a pattern.""" # We do not have to wrap the function with time pattern matching logic # if no pattern given - if all(val is None for val in (year, month, day, hour, minute, second)): + if all(val is None for val in (hour, minute, second)): @callback def time_change_listener(event): """Fire every time event that comes in.""" @@ -336,24 +336,45 @@ def async_track_utc_time_change(hass, action, year=None, month=None, day=None, return hass.bus.async_listen(EVENT_TIME_CHANGED, time_change_listener) - pmp = _process_time_match - year, month, day = pmp(year), pmp(month), pmp(day) - hour, minute, second = pmp(hour), pmp(minute), pmp(second) + matching_seconds = dt_util.parse_time_expression(second, 0, 59) + matching_minutes = dt_util.parse_time_expression(minute, 0, 59) + matching_hours = dt_util.parse_time_expression(hour, 0, 23) + + next_time = None + + def calculate_next(now): + """Calculate and set the next time the trigger should fire.""" + nonlocal next_time + + localized_now = dt_util.as_local(now) if local else now + next_time = dt_util.find_next_time_expression_time( + localized_now, matching_seconds, matching_minutes, + matching_hours) + + # Make sure rolling back the clock doesn't prevent the timer from + # triggering. + last_now = None @callback def pattern_time_change_listener(event): """Listen for matching time_changed events.""" + nonlocal next_time, last_now + now = event.data[ATTR_NOW] - if local: - now = dt_util.as_local(now) + if last_now is None or now < last_now: + # Time rolled back or next time not yet calculated + calculate_next(now) - # pylint: disable=too-many-boolean-expressions - if second(now.second) and minute(now.minute) and hour(now.hour) and \ - day(now.day) and month(now.month) and year(now.year): + last_now = now - hass.async_run_job(action, now) + if next_time <= now: + hass.async_run_job(action, event.data[ATTR_NOW]) + calculate_next(now + timedelta(seconds=1)) + # We can't use async_track_point_in_utc_time here because it would + # break in the case that the system time abruptly jumps backwards. + # Our custom last_now logic takes care of resolving that scenario. return hass.bus.async_listen(EVENT_TIME_CHANGED, pattern_time_change_listener) @@ -363,11 +384,10 @@ track_utc_time_change = threaded_listener_factory(async_track_utc_time_change) @callback @bind_hass -def async_track_time_change(hass, action, year=None, month=None, day=None, - hour=None, minute=None, second=None): +def async_track_time_change(hass, action, hour=None, minute=None, second=None): """Add a listener that will fire if UTC time matches a pattern.""" - return async_track_utc_time_change(hass, action, year, month, day, hour, - minute, second, local=True) + return async_track_utc_time_change(hass, action, hour, minute, second, + local=True) track_time_change = threaded_listener_factory(async_track_time_change) @@ -383,19 +403,3 @@ def _process_state_match(parameter): parameter = tuple(parameter) return lambda state: state in parameter - - -def _process_time_match(parameter): - """Wrap parameter in a tuple if it is not one and returns it.""" - if parameter is None or parameter == MATCH_ALL: - return lambda _: True - - if isinstance(parameter, str) and parameter.startswith('/'): - parameter = float(parameter[1:]) - return lambda time: time % parameter == 0 - - if isinstance(parameter, str) or not hasattr(parameter, '__iter__'): - return lambda time: time == parameter - - parameter = tuple(parameter) - return lambda time: time in parameter diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 5d4b10454a7..b3f7cdd434c 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -1,10 +1,14 @@ """Helper methods to handle the time in Home Assistant.""" import datetime as dt import re -from typing import Any, Dict, Union, Optional, Tuple # noqa pylint: disable=unused-import +from typing import (Any, Union, Optional, # noqa pylint: disable=unused-import + Tuple, List, cast, Dict) import pytz import pytz.exceptions as pytzexceptions +import pytz.tzinfo as pytzinfo # noqa pylint: disable=unused-import + +from homeassistant.const import MATCH_ALL DATE_STR_FORMAT = "%Y-%m-%d" UTC = pytz.utc @@ -209,3 +213,162 @@ def get_age(date: dt.datetime) -> str: return formatn(minute, 'minute') return formatn(second, 'second') + + +def parse_time_expression(parameter: Any, min_value: int, max_value: int) \ + -> List[int]: + """Parse the time expression part and return a list of times to match.""" + if parameter is None or parameter == MATCH_ALL: + res = [x for x in range(min_value, max_value + 1)] + elif isinstance(parameter, str) and parameter.startswith('/'): + parameter = float(parameter[1:]) + res = [x for x in range(min_value, max_value + 1) + if x % parameter == 0] + elif not hasattr(parameter, '__iter__'): + res = [int(parameter)] + else: + res = list(sorted(int(x) for x in parameter)) + + for val in res: + if val < min_value or val > max_value: + raise ValueError( + "Time expression '{}': parameter {} out of range ({} to {})" + "".format(parameter, val, min_value, max_value) + ) + + return res + + +# pylint: disable=redefined-outer-name +def find_next_time_expression_time(now: dt.datetime, + seconds: List[int], minutes: List[int], + hours: List[int]) -> dt.datetime: + """Find the next datetime from now for which the time expression matches. + + The algorithm looks at each time unit separately and tries to find the + next one that matches for each. If any of them would roll over, all + time units below that are reset to the first matching value. + + Timezones are also handled (the tzinfo of the now object is used), + including daylight saving time. + """ + if not seconds or not minutes or not hours: + raise ValueError("Cannot find a next time: Time expression never " + "matches!") + + def _lower_bound(arr: List[int], cmp: int) -> Optional[int]: + """Return the first value in arr greater or equal to cmp. + + Return None if no such value exists. + """ + left = 0 + right = len(arr) + while left < right: + mid = (left + right) // 2 + if arr[mid] < cmp: + left = mid + 1 + else: + right = mid + + if left == len(arr): + return None + return arr[left] + + result = now.replace(microsecond=0) + + # Match next second + next_second = _lower_bound(seconds, result.second) + if next_second is None: + # No second to match in this minute. Roll-over to next minute. + next_second = seconds[0] + result += dt.timedelta(minutes=1) + + result = result.replace(second=next_second) + + # Match next minute + next_minute = _lower_bound(minutes, result.minute) + if next_minute != result.minute: + # We're in the next minute. Seconds needs to be reset. + result = result.replace(second=seconds[0]) + + if next_minute is None: + # No minute to match in this hour. Roll-over to next hour. + next_minute = minutes[0] + result += dt.timedelta(hours=1) + + result = result.replace(minute=next_minute) + + # Match next hour + next_hour = _lower_bound(hours, result.hour) + if next_hour != result.hour: + # We're in the next hour. Seconds+minutes needs to be reset. + result.replace(second=seconds[0], minute=minutes[0]) + + if next_hour is None: + # No minute to match in this day. Roll-over to next day. + next_hour = hours[0] + result += dt.timedelta(days=1) + + result = result.replace(hour=next_hour) + + if result.tzinfo is None: + return result + + # Now we need to handle timezones. We will make this datetime object + # "naive" first and then re-convert it to the target timezone. + # This is so that we can call pytz's localize and handle DST changes. + tzinfo = result.tzinfo # type: pytzinfo.DstTzInfo + result = result.replace(tzinfo=None) + + try: + result = tzinfo.localize(result, is_dst=None) + except pytzexceptions.AmbiguousTimeError: + # This happens when we're leaving daylight saving time and local + # clocks are rolled back. In this case, we want to trigger + # on both the DST and non-DST time. So when "now" is in the DST + # use the DST-on time, and if not, use the DST-off time. + use_dst = bool(now.dst()) + result = tzinfo.localize(result, is_dst=use_dst) + except pytzexceptions.NonExistentTimeError: + # This happens when we're entering daylight saving time and local + # clocks are rolled forward, thus there are local times that do + # not exist. In this case, we want to trigger on the next time + # that *does* exist. + # In the worst case, this will run through all the seconds in the + # time shift, but that's max 3600 operations for once per year + result = result.replace(tzinfo=tzinfo) + dt.timedelta(seconds=1) + return find_next_time_expression_time(result, seconds, minutes, hours) + + result_dst = cast(dt.timedelta, result.dst()) + now_dst = cast(dt.timedelta, now.dst()) + if result_dst >= now_dst: + return result + + # Another edge-case when leaving DST: + # When now is in DST and ambiguous *and* the next trigger time we *should* + # trigger is ambiguous and outside DST, the excepts above won't catch it. + # For example: if triggering on 2:30 and now is 28.10.2018 2:30 (in DST) + # we should trigger next on 28.10.2018 2:30 (out of DST), but our + # algorithm above would produce 29.10.2018 2:30 (out of DST) + + # Step 1: Check if now is ambiguous + try: + tzinfo.localize(now.replace(tzinfo=None), is_dst=None) + return result + except pytzexceptions.AmbiguousTimeError: + pass + + # Step 2: Check if result of (now - DST) is ambiguous. + check = now - now_dst + check_result = find_next_time_expression_time( + check, seconds, minutes, hours) + try: + tzinfo.localize(check_result.replace(tzinfo=None), is_dst=None) + return result + except pytzexceptions.AmbiguousTimeError: + pass + + # OK, edge case does apply. We must override the DST to DST-off + check_result = tzinfo.localize(check_result.replace(tzinfo=None), + is_dst=False) + return check_result diff --git a/tests/common.py b/tests/common.py index cfc29a7f441..ce80746be4e 100644 --- a/tests/common.py +++ b/tests/common.py @@ -251,7 +251,7 @@ fire_mqtt_message = threadsafe_callback_factory(async_fire_mqtt_message) @ha.callback def async_fire_time_changed(hass, time): """Fire a time changes event.""" - hass.bus.async_fire(EVENT_TIME_CHANGED, {'now': time}) + hass.bus.async_fire(EVENT_TIME_CHANGED, {'now': date_util.as_utc(time)}) fire_time_changed = threadsafe_callback_factory(async_fire_time_changed) diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 5b57ca75d51..cb586698302 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -85,38 +85,6 @@ class TestEventHelpers(unittest.TestCase): self.hass.block_till_done() self.assertEqual(2, len(runs)) - def test_track_time_change(self): - """Test tracking time change.""" - wildcard_runs = [] - specific_runs = [] - - unsub = track_time_change(self.hass, lambda x: wildcard_runs.append(1)) - unsub_utc = track_utc_time_change( - self.hass, lambda x: specific_runs.append(1), second=[0, 30]) - - self._send_time_changed(datetime(2014, 5, 24, 12, 0, 0)) - self.hass.block_till_done() - self.assertEqual(1, len(specific_runs)) - self.assertEqual(1, len(wildcard_runs)) - - self._send_time_changed(datetime(2014, 5, 24, 12, 0, 15)) - self.hass.block_till_done() - self.assertEqual(1, len(specific_runs)) - self.assertEqual(2, len(wildcard_runs)) - - self._send_time_changed(datetime(2014, 5, 24, 12, 0, 30)) - self.hass.block_till_done() - self.assertEqual(2, len(specific_runs)) - self.assertEqual(3, len(wildcard_runs)) - - unsub() - unsub_utc() - - self._send_time_changed(datetime(2014, 5, 24, 12, 0, 30)) - self.hass.block_till_done() - self.assertEqual(2, len(specific_runs)) - self.assertEqual(3, len(wildcard_runs)) - def test_track_state_change(self): """Test track_state_change.""" # 2 lists to track how often our callbacks get called @@ -526,12 +494,64 @@ class TestEventHelpers(unittest.TestCase): """Send a time changed event.""" self.hass.bus.fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: now}) + +class TestTrackTimeChange(unittest.TestCase): + """Test track time change methods.""" + + def setUp(self): + """Set up the tests.""" + self.orig_default_time_zone = dt_util.DEFAULT_TIME_ZONE + self.hass = get_test_home_assistant() + + def tearDown(self): + """Stop everything that was started.""" + dt_util.set_default_time_zone(self.orig_default_time_zone) + self.hass.stop() + + def _send_time_changed(self, now): + """Send a time changed event.""" + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: now}) + + def test_track_time_change(self): + """Test tracking time change.""" + wildcard_runs = [] + specific_runs = [] + + unsub = track_time_change(self.hass, + lambda x: wildcard_runs.append(1)) + unsub_utc = track_utc_time_change( + self.hass, lambda x: specific_runs.append(1), second=[0, 30]) + + self._send_time_changed(datetime(2014, 5, 24, 12, 0, 0)) + self.hass.block_till_done() + self.assertEqual(1, len(specific_runs)) + self.assertEqual(1, len(wildcard_runs)) + + self._send_time_changed(datetime(2014, 5, 24, 12, 0, 15)) + self.hass.block_till_done() + self.assertEqual(1, len(specific_runs)) + self.assertEqual(2, len(wildcard_runs)) + + self._send_time_changed(datetime(2014, 5, 24, 12, 0, 30)) + self.hass.block_till_done() + self.assertEqual(2, len(specific_runs)) + self.assertEqual(3, len(wildcard_runs)) + + unsub() + unsub_utc() + + self._send_time_changed(datetime(2014, 5, 24, 12, 0, 30)) + self.hass.block_till_done() + self.assertEqual(2, len(specific_runs)) + self.assertEqual(3, len(wildcard_runs)) + def test_periodic_task_minute(self): """Test periodic tasks per minute.""" specific_runs = [] unsub = track_utc_time_change( - self.hass, lambda x: specific_runs.append(1), minute='/5') + self.hass, lambda x: specific_runs.append(1), minute='/5', + second=0) self._send_time_changed(datetime(2014, 5, 24, 12, 0, 0)) self.hass.block_till_done() @@ -556,7 +576,8 @@ class TestEventHelpers(unittest.TestCase): specific_runs = [] unsub = track_utc_time_change( - self.hass, lambda x: specific_runs.append(1), hour='/2') + self.hass, lambda x: specific_runs.append(1), hour='/2', + minute=0, second=0) self._send_time_changed(datetime(2014, 5, 24, 22, 0, 0)) self.hass.block_till_done() @@ -566,7 +587,7 @@ class TestEventHelpers(unittest.TestCase): self.hass.block_till_done() self.assertEqual(1, len(specific_runs)) - self._send_time_changed(datetime(2014, 5, 24, 0, 0, 0)) + self._send_time_changed(datetime(2014, 5, 25, 0, 0, 0)) self.hass.block_till_done() self.assertEqual(2, len(specific_runs)) @@ -584,68 +605,138 @@ class TestEventHelpers(unittest.TestCase): self.hass.block_till_done() self.assertEqual(3, len(specific_runs)) - def test_periodic_task_day(self): - """Test periodic tasks per day.""" - specific_runs = [] - - unsub = track_utc_time_change( - self.hass, lambda x: specific_runs.append(1), day='/2') - - self._send_time_changed(datetime(2014, 5, 2, 0, 0, 0)) - self.hass.block_till_done() - self.assertEqual(1, len(specific_runs)) - - self._send_time_changed(datetime(2014, 5, 3, 12, 0, 0)) - self.hass.block_till_done() - self.assertEqual(1, len(specific_runs)) - - self._send_time_changed(datetime(2014, 5, 4, 0, 0, 0)) - self.hass.block_till_done() - self.assertEqual(2, len(specific_runs)) - - unsub() - - self._send_time_changed(datetime(2014, 5, 4, 0, 0, 0)) - self.hass.block_till_done() - self.assertEqual(2, len(specific_runs)) - - def test_periodic_task_year(self): - """Test periodic tasks per year.""" - specific_runs = [] - - unsub = track_utc_time_change( - self.hass, lambda x: specific_runs.append(1), year='/2') - - self._send_time_changed(datetime(2014, 5, 2, 0, 0, 0)) - self.hass.block_till_done() - self.assertEqual(1, len(specific_runs)) - - self._send_time_changed(datetime(2015, 5, 2, 0, 0, 0)) - self.hass.block_till_done() - self.assertEqual(1, len(specific_runs)) - - self._send_time_changed(datetime(2016, 5, 2, 0, 0, 0)) - self.hass.block_till_done() - self.assertEqual(2, len(specific_runs)) - - unsub() - - self._send_time_changed(datetime(2016, 5, 2, 0, 0, 0)) - self.hass.block_till_done() - self.assertEqual(2, len(specific_runs)) - def test_periodic_task_wrong_input(self): """Test periodic tasks with wrong input.""" specific_runs = [] with pytest.raises(ValueError): track_utc_time_change( - self.hass, lambda x: specific_runs.append(1), year='/two') + self.hass, lambda x: specific_runs.append(1), hour='/two') self._send_time_changed(datetime(2014, 5, 2, 0, 0, 0)) self.hass.block_till_done() self.assertEqual(0, len(specific_runs)) + def test_periodic_task_clock_rollback(self): + """Test periodic tasks with the time rolling backwards.""" + specific_runs = [] + + unsub = track_utc_time_change( + self.hass, lambda x: specific_runs.append(1), hour='/2', minute=0, + second=0) + + self._send_time_changed(datetime(2014, 5, 24, 22, 0, 0)) + self.hass.block_till_done() + self.assertEqual(1, len(specific_runs)) + + self._send_time_changed(datetime(2014, 5, 24, 23, 0, 0)) + self.hass.block_till_done() + self.assertEqual(1, len(specific_runs)) + + self._send_time_changed(datetime(2014, 5, 24, 22, 0, 0)) + self.hass.block_till_done() + self.assertEqual(2, len(specific_runs)) + + self._send_time_changed(datetime(2014, 5, 24, 0, 0, 0)) + self.hass.block_till_done() + self.assertEqual(3, len(specific_runs)) + + self._send_time_changed(datetime(2014, 5, 25, 2, 0, 0)) + self.hass.block_till_done() + self.assertEqual(4, len(specific_runs)) + + unsub() + + self._send_time_changed(datetime(2014, 5, 25, 2, 0, 0)) + self.hass.block_till_done() + self.assertEqual(4, len(specific_runs)) + + def test_periodic_task_duplicate_time(self): + """Test periodic tasks not triggering on duplicate time.""" + specific_runs = [] + + unsub = track_utc_time_change( + self.hass, lambda x: specific_runs.append(1), hour='/2', minute=0, + second=0) + + self._send_time_changed(datetime(2014, 5, 24, 22, 0, 0)) + self.hass.block_till_done() + self.assertEqual(1, len(specific_runs)) + + self._send_time_changed(datetime(2014, 5, 24, 22, 0, 0)) + self.hass.block_till_done() + self.assertEqual(1, len(specific_runs)) + + self._send_time_changed(datetime(2014, 5, 25, 0, 0, 0)) + self.hass.block_till_done() + self.assertEqual(2, len(specific_runs)) + + unsub() + + def test_periodic_task_entering_dst(self): + """Test periodic task behavior when entering dst.""" + tz = dt_util.get_time_zone('Europe/Vienna') + dt_util.set_default_time_zone(tz) + specific_runs = [] + + unsub = track_time_change( + self.hass, lambda x: specific_runs.append(1), hour=2, minute=30, + second=0) + + self._send_time_changed( + tz.localize(datetime(2018, 3, 25, 1, 50, 0))) + self.hass.block_till_done() + self.assertEqual(0, len(specific_runs)) + + self._send_time_changed( + tz.localize(datetime(2018, 3, 25, 3, 50, 0))) + self.hass.block_till_done() + self.assertEqual(0, len(specific_runs)) + + self._send_time_changed( + tz.localize(datetime(2018, 3, 26, 1, 50, 0))) + self.hass.block_till_done() + self.assertEqual(0, len(specific_runs)) + + self._send_time_changed( + tz.localize(datetime(2018, 3, 26, 2, 50, 0))) + self.hass.block_till_done() + self.assertEqual(1, len(specific_runs)) + + unsub() + + def test_periodic_task_leaving_dst(self): + """Test periodic task behavior when leaving dst.""" + tz = dt_util.get_time_zone('Europe/Vienna') + dt_util.set_default_time_zone(tz) + specific_runs = [] + + unsub = track_time_change( + self.hass, lambda x: specific_runs.append(1), hour=2, minute=30, + second=0) + + self._send_time_changed( + tz.localize(datetime(2018, 10, 28, 2, 5, 0), is_dst=False)) + self.hass.block_till_done() + self.assertEqual(0, len(specific_runs)) + + self._send_time_changed( + tz.localize(datetime(2018, 10, 28, 2, 55, 0), is_dst=False)) + self.hass.block_till_done() + self.assertEqual(1, len(specific_runs)) + + self._send_time_changed( + tz.localize(datetime(2018, 10, 28, 2, 5, 0), is_dst=True)) + self.hass.block_till_done() + self.assertEqual(1, len(specific_runs)) + + self._send_time_changed( + tz.localize(datetime(2018, 10, 28, 2, 55, 0), is_dst=True)) + self.hass.block_till_done() + self.assertEqual(2, len(specific_runs)) + + unsub() + def test_call_later(self): """Test calling an action later.""" def action(): pass diff --git a/tests/util/test_dt.py b/tests/util/test_dt.py index d670917c055..35a83de6bfb 100644 --- a/tests/util/test_dt.py +++ b/tests/util/test_dt.py @@ -164,3 +164,128 @@ class TestDateUtil(unittest.TestCase): diff = dt_util.now() - timedelta(minutes=365*60*24) self.assertEqual(dt_util.get_age(diff), "1 year") + + def test_parse_time_expression(self): + """Test parse_time_expression.""" + self.assertEqual( + [x for x in range(60)], + dt_util.parse_time_expression('*', 0, 59) + ) + self.assertEqual( + [x for x in range(60)], + dt_util.parse_time_expression(None, 0, 59) + ) + + self.assertEqual( + [x for x in range(0, 60, 5)], + dt_util.parse_time_expression('/5', 0, 59) + ) + + self.assertEqual( + [1, 2, 3], + dt_util.parse_time_expression([2, 1, 3], 0, 59) + ) + + self.assertEqual( + [x for x in range(24)], + dt_util.parse_time_expression('*', 0, 23) + ) + + self.assertEqual( + [42], + dt_util.parse_time_expression(42, 0, 59) + ) + + self.assertRaises(ValueError, dt_util.parse_time_expression, 61, 0, 60) + + def test_find_next_time_expression_time_basic(self): + """Test basic stuff for find_next_time_expression_time.""" + def find(dt, hour, minute, second): + """Call test_find_next_time_expression_time.""" + seconds = dt_util.parse_time_expression(second, 0, 59) + minutes = dt_util.parse_time_expression(minute, 0, 59) + hours = dt_util.parse_time_expression(hour, 0, 23) + + return dt_util.find_next_time_expression_time( + dt, seconds, minutes, hours) + + self.assertEqual( + datetime(2018, 10, 7, 10, 30, 0), + find(datetime(2018, 10, 7, 10, 20, 0), '*', '/30', 0) + ) + + self.assertEqual( + datetime(2018, 10, 7, 10, 30, 0), + find(datetime(2018, 10, 7, 10, 30, 0), '*', '/30', 0) + ) + + self.assertEqual( + datetime(2018, 10, 7, 12, 30, 30), + find(datetime(2018, 10, 7, 10, 30, 0), '/3', '/30', [30, 45]) + ) + + self.assertEqual( + datetime(2018, 10, 8, 5, 0, 0), + find(datetime(2018, 10, 7, 10, 30, 0), 5, 0, 0) + ) + + def test_find_next_time_expression_time_dst(self): + """Test daylight saving time for find_next_time_expression_time.""" + tz = dt_util.get_time_zone('Europe/Vienna') + dt_util.set_default_time_zone(tz) + + def find(dt, hour, minute, second): + """Call test_find_next_time_expression_time.""" + seconds = dt_util.parse_time_expression(second, 0, 59) + minutes = dt_util.parse_time_expression(minute, 0, 59) + hours = dt_util.parse_time_expression(hour, 0, 23) + + return dt_util.find_next_time_expression_time( + dt, seconds, minutes, hours) + + # Entering DST, clocks are rolled forward + self.assertEqual( + tz.localize(datetime(2018, 3, 26, 2, 30, 0)), + find(tz.localize(datetime(2018, 3, 25, 1, 50, 0)), 2, 30, 0) + ) + + self.assertEqual( + tz.localize(datetime(2018, 3, 26, 2, 30, 0)), + find(tz.localize(datetime(2018, 3, 25, 3, 50, 0)), 2, 30, 0) + ) + + self.assertEqual( + tz.localize(datetime(2018, 3, 26, 2, 30, 0)), + find(tz.localize(datetime(2018, 3, 26, 1, 50, 0)), 2, 30, 0) + ) + + # Leaving DST, clocks are rolled back + self.assertEqual( + tz.localize(datetime(2018, 10, 28, 2, 30, 0), is_dst=False), + find(tz.localize(datetime(2018, 10, 28, 2, 5, 0), is_dst=False), + 2, 30, 0) + ) + + self.assertEqual( + tz.localize(datetime(2018, 10, 28, 2, 30, 0), is_dst=False), + find(tz.localize(datetime(2018, 10, 28, 2, 55, 0), is_dst=True), + 2, 30, 0) + ) + + self.assertEqual( + tz.localize(datetime(2018, 10, 28, 4, 30, 0), is_dst=False), + find(tz.localize(datetime(2018, 10, 28, 2, 55, 0), is_dst=True), + 4, 30, 0) + ) + + self.assertEqual( + tz.localize(datetime(2018, 10, 28, 2, 30, 0), is_dst=True), + find(tz.localize(datetime(2018, 10, 28, 2, 5, 0), is_dst=True), + 2, 30, 0) + ) + + self.assertEqual( + tz.localize(datetime(2018, 10, 29, 2, 30, 0)), + find(tz.localize(datetime(2018, 10, 28, 2, 55, 0), is_dst=False), + 2, 30, 0) + ) From 9d56730b8de62c5aa1e16d25fe8b0054479c3065 Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Tue, 9 Oct 2018 10:14:55 +0200 Subject: [PATCH 061/265] Add optional "all" parameter for groups (#17179) * Added optional mode parameter * Cleanup * Using boolean configuration * Fix invalid syntax * Added tests for all-parameter * Grammar * Lint * Docstrings * Better description --- homeassistant/components/group/__init__.py | 34 +++++++++++++++----- homeassistant/components/group/services.yaml | 3 ++ tests/components/group/test_init.py | 30 +++++++++++++++++ 3 files changed, 59 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 39fd7567c98..4dd3571e69c 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -30,6 +30,7 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}' CONF_ENTITIES = 'entities' CONF_VIEW = 'view' CONF_CONTROL = 'control' +CONF_ALL = 'all' ATTR_ADD_ENTITIES = 'add_entities' ATTR_AUTO = 'auto' @@ -39,6 +40,7 @@ ATTR_OBJECT_ID = 'object_id' ATTR_ORDER = 'order' ATTR_VIEW = 'view' ATTR_VISIBLE = 'visible' +ATTR_ALL = 'all' SERVICE_SET_VISIBILITY = 'set_visibility' SERVICE_SET = 'set' @@ -60,6 +62,7 @@ SET_SERVICE_SCHEMA = vol.Schema({ vol.Optional(ATTR_ICON): cv.string, vol.Optional(ATTR_CONTROL): CONTROL_TYPES, vol.Optional(ATTR_VISIBLE): cv.boolean, + vol.Optional(ATTR_ALL): cv.boolean, vol.Exclusive(ATTR_ENTITIES, 'entities'): cv.entity_ids, vol.Exclusive(ATTR_ADD_ENTITIES, 'entities'): cv.entity_ids, }) @@ -85,6 +88,7 @@ GROUP_SCHEMA = vol.Schema({ CONF_NAME: cv.string, CONF_ICON: cv.icon, CONF_CONTROL: CONTROL_TYPES, + CONF_ALL: cv.boolean, }) CONFIG_SCHEMA = vol.Schema({ @@ -223,6 +227,7 @@ async def async_setup(hass, config): object_id=object_id, entity_ids=entity_ids, user_defined=False, + mode=service.data.get(ATTR_ALL), **extra_arg ) return @@ -265,6 +270,10 @@ async def async_setup(hass, config): group.view = service.data[ATTR_VIEW] need_update = True + if ATTR_ALL in service.data: + group.mode = all if service.data[ATTR_ALL] else any + need_update = True + if need_update: await group.async_update_ha_state() @@ -310,19 +319,21 @@ async def _async_process_config(hass, config, component): icon = conf.get(CONF_ICON) view = conf.get(CONF_VIEW) control = conf.get(CONF_CONTROL) + mode = conf.get(CONF_ALL) # Don't create tasks and await them all. The order is important as # groups get a number based on creation order. await Group.async_create_group( hass, name, entity_ids, icon=icon, view=view, - control=control, object_id=object_id) + control=control, object_id=object_id, mode=mode) class Group(Entity): """Track a group of entity ids.""" def __init__(self, hass, name, order=None, visible=True, icon=None, - view=False, control=None, user_defined=True, entity_ids=None): + view=False, control=None, user_defined=True, entity_ids=None, + mode=None): """Initialize a group. This Object has factory function for creation. @@ -341,6 +352,9 @@ class Group(Entity): self.visible = visible self.control = control self.user_defined = user_defined + self.mode = any + if mode: + self.mode = all self._order = order self._assumed_state = False self._async_unsub_state_changed = None @@ -348,18 +362,19 @@ class Group(Entity): @staticmethod def create_group(hass, name, entity_ids=None, user_defined=True, visible=True, icon=None, view=False, control=None, - object_id=None): + object_id=None, mode=None): """Initialize a group.""" return run_coroutine_threadsafe( Group.async_create_group( hass, name, entity_ids, user_defined, visible, icon, view, - control, object_id), + control, object_id, mode), hass.loop).result() @staticmethod async def async_create_group(hass, name, entity_ids=None, user_defined=True, visible=True, icon=None, - view=False, control=None, object_id=None): + view=False, control=None, object_id=None, + mode=None): """Initialize a group. This method must be run in the event loop. @@ -368,7 +383,7 @@ class Group(Entity): hass, name, order=len(hass.states.async_entity_ids(DOMAIN)), visible=visible, icon=icon, view=view, control=control, - user_defined=user_defined, entity_ids=entity_ids + user_defined=user_defined, entity_ids=entity_ids, mode=mode ) group.entity_id = async_generate_entity_id( @@ -557,13 +572,16 @@ class Group(Entity): if gr_on is None: return + # pylint: disable=too-many-boolean-expressions if tr_state is None or ((gr_state == gr_on and tr_state.state == gr_off) or + (gr_state == gr_off and + tr_state.state == gr_on) or tr_state.state not in (gr_on, gr_off)): if states is None: states = self._tracking_states - if any(state.state == gr_on for state in states): + if self.mode(state.state == gr_on for state in states): self._state = gr_on else: self._state = gr_off @@ -576,7 +594,7 @@ class Group(Entity): if states is None: states = self._tracking_states - self._assumed_state = any( + self._assumed_state = self.mode( state.attributes.get(ATTR_ASSUMED_STATE) for state in states) diff --git a/homeassistant/components/group/services.yaml b/homeassistant/components/group/services.yaml index f51f8b909d4..68c2f04f064 100644 --- a/homeassistant/components/group/services.yaml +++ b/homeassistant/components/group/services.yaml @@ -40,6 +40,9 @@ set: add_entities: description: List of members they will change on group listening. example: domain.entity_id1, domain.entity_id2 + all: + description: Enable this option if the group should only turn on when all entities are on. + example: True remove: description: Remove a user group. diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index 55c8a7778cb..104d1427dc9 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -108,6 +108,36 @@ class TestComponentsGroup(unittest.TestCase): group_state = self.hass.states.get(test_group.entity_id) self.assertEqual(STATE_ON, group_state.state) + def test_allgroup_stays_off_if_all_are_off_and_one_turns_on(self): + """Group with all: true, stay off if one device turns on.""" + self.hass.states.set('light.Bowl', STATE_OFF) + self.hass.states.set('light.Ceiling', STATE_OFF) + test_group = group.Group.create_group( + self.hass, 'init_group', ['light.Bowl', 'light.Ceiling'], False, + mode=True) + + # Turn one on + self.hass.states.set('light.Ceiling', STATE_ON) + self.hass.block_till_done() + + group_state = self.hass.states.get(test_group.entity_id) + self.assertEqual(STATE_OFF, group_state.state) + + def test_allgroup_turn_on_if_last_turns_on(self): + """Group with all: true, turn on if all devices are on.""" + self.hass.states.set('light.Bowl', STATE_ON) + self.hass.states.set('light.Ceiling', STATE_OFF) + test_group = group.Group.create_group( + self.hass, 'init_group', ['light.Bowl', 'light.Ceiling'], False, + mode=True) + + # Turn one on + self.hass.states.set('light.Ceiling', STATE_ON) + self.hass.block_till_done() + + group_state = self.hass.states.get(test_group.entity_id) + self.assertEqual(STATE_ON, group_state.state) + def test_is_on(self): """Test is_on method.""" self.hass.states.set('light.Bowl', STATE_ON) From 6bf3f9e74806e57182ac08992389238e3bb493a2 Mon Sep 17 00:00:00 2001 From: definitio <37266727+definitio@users.noreply.github.com> Date: Tue, 9 Oct 2018 13:24:39 +0300 Subject: [PATCH 062/265] Fix mpd timeout error (#17254) * Increase mpd client timeout * Update mpd.py --- homeassistant/components/media_player/mpd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py index 8953215f44e..b5eecd3d403 100644 --- a/homeassistant/components/media_player/mpd.py +++ b/homeassistant/components/media_player/mpd.py @@ -79,7 +79,7 @@ class MpdDevice(MediaPlayerDevice): # set up MPD client self._client = mpd.MPDClient() - self._client.timeout = 5 + self._client.timeout = 30 self._client.idletimeout = None def _connect(self): From 5167658a1d569c8a488c7674385d50e1205e9d67 Mon Sep 17 00:00:00 2001 From: damarco Date: Tue, 9 Oct 2018 12:53:02 +0200 Subject: [PATCH 063/265] Add support for zha custom cluster mappings (#16714) * Add support for custom cluster mappings * Refactor sub_component mapping --- homeassistant/components/sensor/zha.py | 4 +++- homeassistant/components/zha/__init__.py | 12 +++++++++++- homeassistant/components/zha/const.py | 10 +++++++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/zha.py b/homeassistant/components/sensor/zha.py index 0d5b40d1d98..9a9de0d6cf2 100644 --- a/homeassistant/components/sensor/zha.py +++ b/homeassistant/components/sensor/zha.py @@ -36,7 +36,9 @@ async def make_sensor(discovery_info): from zigpy.zcl.clusters.smartenergy import Metering from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement in_clusters = discovery_info['in_clusters'] - if RelativeHumidity.cluster_id in in_clusters: + if 'sub_component' in discovery_info: + sensor = discovery_info['sub_component'](**discovery_info) + elif RelativeHumidity.cluster_id in in_clusters: sensor = RelativeHumiditySensor(**discovery_info) elif TemperatureMeasurement.cluster_id in in_clusters: sensor = TemperatureSensor(**discovery_info) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 2e74e079d5f..8cea746f89a 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -276,15 +276,23 @@ class ApplicationListener: device_classes, discovery_attr, is_new_join): """Try to set up an entity from a "bare" cluster.""" + import homeassistant.components.zha.const as zha_const if cluster.cluster_id in profile_clusters: return - component = None + component = sub_component = None for cluster_type, candidate_component in device_classes.items(): if isinstance(cluster, cluster_type): component = candidate_component break + for signature, comp in zha_const.CUSTOM_CLUSTER_MAPPINGS.items(): + if (isinstance(endpoint.device, signature[0]) and + cluster.cluster_id == signature[1]): + component = comp[0] + sub_component = comp[1] + break + if component is None: return @@ -301,6 +309,8 @@ class ApplicationListener: 'entity_suffix': '_{}'.format(cluster.cluster_id), } discovery_info[discovery_attr] = {cluster.cluster_id: cluster} + if sub_component: + discovery_info.update({'sub_component': sub_component}) self._hass.data[DISCOVERY_KEY][cluster_key] = discovery_info await discovery.async_load_platform( diff --git a/homeassistant/components/zha/const.py b/homeassistant/components/zha/const.py index 37c7f5592a0..0b3e926fadc 100644 --- a/homeassistant/components/zha/const.py +++ b/homeassistant/components/zha/const.py @@ -3,6 +3,7 @@ DEVICE_CLASS = {} SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {} SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {} +CUSTOM_CLUSTER_MAPPINGS = {} COMPONENT_CLUSTERS = {} @@ -12,8 +13,9 @@ def populate_data(): These cannot be module level, as importing bellows must be done in a in a function. """ - from zigpy import zcl + from zigpy import zcl, quirks from zigpy.profiles import PROFILES, zha, zll + from homeassistant.components.sensor import zha as sensor_zha DEVICE_CLASS[zha.PROFILE_ID] = { zha.DeviceType.ON_OFF_SWITCH: 'binary_sensor', @@ -58,6 +60,12 @@ def populate_data(): zcl.clusters.general.OnOff: 'binary_sensor', }) + # A map of device/cluster to component/sub-component + CUSTOM_CLUSTER_MAPPINGS.update({ + (quirks.smartthings.SmartthingsTemperatureHumiditySensor, 64581): + ('sensor', sensor_zha.RelativeHumiditySensor) + }) + # A map of hass components to all Zigbee clusters it could use for profile_id, classes in DEVICE_CLASS.items(): profile = PROFILES[profile_id] From cf249e3e5eba22674b73e4e32a27334ae5d0345a Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Tue, 9 Oct 2018 10:30:55 -0400 Subject: [PATCH 064/265] Z-Wave Config Entry Support (#17119) * Initial Z-Wave Config Entry Support * Use conf.get() for config import * Uncomment test * Re-add line breaks * tabs -> space * Unused import cleanup & lint fixes * Remove unused config flow link step * Address comments * Remove unused import * Fix tests * Check for valid usb_path * Test for Z-Stick in config flow * Pass config dir to ZWaveOption * Auto-generate Network Key if none provided * Test fixes * Address comments & more start network service registration * add_executor_job for options.lock() --- .../components/zwave/.translations/en.json | 22 +++++ homeassistant/components/zwave/__init__.py | 79 +++++++++------ homeassistant/components/zwave/config_flow.py | 95 +++++++++++++++++++ homeassistant/components/zwave/const.py | 12 +++ homeassistant/components/zwave/strings.json | 22 +++++ homeassistant/config_entries.py | 1 + tests/components/zwave/test_init.py | 36 ++++--- 7 files changed, 227 insertions(+), 40 deletions(-) create mode 100644 homeassistant/components/zwave/.translations/en.json create mode 100644 homeassistant/components/zwave/config_flow.py create mode 100644 homeassistant/components/zwave/strings.json diff --git a/homeassistant/components/zwave/.translations/en.json b/homeassistant/components/zwave/.translations/en.json new file mode 100644 index 00000000000..081d5c858cb --- /dev/null +++ b/homeassistant/components/zwave/.translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave is already configured", + "one_instance_only": "Component only supports one Z-Wave instance" + }, + "error": { + "option_error": "Z-Wave validation failed. Is the path to the USB stick correct?" + }, + "step": { + "user": { + "data": { + "network_key": "Network Key (leave blank to auto-generate)", + "usb_path": "USB Path" + }, + "description": "See https://www.home-assistant.io/docs/z-wave/installation/ for information on the configuration variables", + "title": "Set up Z-Wave" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index fa78f719557..d48cac6a1e2 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -11,6 +11,7 @@ from pprint import pprint import voluptuous as vol +from homeassistant import config_entries from homeassistant.core import callback, CoreState from homeassistant.loader import get_platform from homeassistant.helpers import discovery @@ -28,7 +29,13 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send) from . import const -from .const import DOMAIN, DATA_DEVICES, DATA_NETWORK, DATA_ENTITY_VALUES +from . import config_flow # noqa # pylint: disable=unused-import +from .const import ( + CONF_AUTOHEAL, CONF_DEBUG, CONF_POLLING_INTERVAL, + CONF_USB_STICK_PATH, CONF_CONFIG_PATH, CONF_NETWORK_KEY, + DEFAULT_CONF_AUTOHEAL, DEFAULT_CONF_USB_STICK_PATH, + DEFAULT_POLLING_INTERVAL, DEFAULT_DEBUG, DOMAIN, + DATA_DEVICES, DATA_NETWORK, DATA_ENTITY_VALUES) from .node_entity import ZWaveBaseEntity, ZWaveNodeEntity from . import workaround from .discovery_schemas import DISCOVERY_SCHEMAS @@ -40,12 +47,10 @@ REQUIREMENTS = ['pydispatcher==2.0.5', 'python_openzwave==0.4.9'] _LOGGER = logging.getLogger(__name__) CLASS_ID = 'class_id' -CONF_AUTOHEAL = 'autoheal' -CONF_DEBUG = 'debug' + +ATTR_POWER = 'power_consumption' + CONF_POLLING_INTENSITY = 'polling_intensity' -CONF_POLLING_INTERVAL = 'polling_interval' -CONF_USB_STICK_PATH = 'usb_path' -CONF_CONFIG_PATH = 'config_path' CONF_IGNORED = 'ignored' CONF_INVERT_OPENCLOSE_BUTTONS = 'invert_openclose_buttons' CONF_REFRESH_VALUE = 'refresh_value' @@ -53,14 +58,9 @@ CONF_REFRESH_DELAY = 'delay' CONF_DEVICE_CONFIG = 'device_config' CONF_DEVICE_CONFIG_GLOB = 'device_config_glob' CONF_DEVICE_CONFIG_DOMAIN = 'device_config_domain' -CONF_NETWORK_KEY = 'network_key' -ATTR_POWER = 'power_consumption' +DATA_ZWAVE_CONFIG = 'zwave_config' -DEFAULT_CONF_AUTOHEAL = True -DEFAULT_CONF_USB_STICK_PATH = '/zwaveusbstick' -DEFAULT_POLLING_INTERVAL = 60000 -DEFAULT_DEBUG = False DEFAULT_CONF_IGNORED = False DEFAULT_CONF_INVERT_OPENCLOSE_BUTTONS = False DEFAULT_CONF_REFRESH_VALUE = False @@ -230,7 +230,27 @@ async def async_setup_platform(hass, config, async_add_entities, async def async_setup(hass, config): - """Set up Z-Wave. + """Set up Z-Wave components.""" + if DOMAIN not in config: + return True + + conf = config[DOMAIN] + hass.data[DATA_ZWAVE_CONFIG] = conf + + if not hass.config_entries.async_entries(DOMAIN): + hass.async_create_task(hass.config_entries.flow.async_init( + DOMAIN, context={'source': config_entries.SOURCE_IMPORT}, + data={ + CONF_USB_STICK_PATH: conf[CONF_USB_STICK_PATH], + CONF_NETWORK_KEY: conf.get(CONF_NETWORK_KEY), + } + )) + + return True + + +async def async_setup_entry(hass, config_entry): + """Set up Z-Wave from a config entry. Will automatically load components to support devices found on the network. """ @@ -240,27 +260,31 @@ async def async_setup(hass, config): from openzwave.network import ZWaveNetwork from openzwave.group import ZWaveGroup + config = {} + if DATA_ZWAVE_CONFIG in hass.data: + config = hass.data[DATA_ZWAVE_CONFIG] + # Load configuration - use_debug = config[DOMAIN].get(CONF_DEBUG) - autoheal = config[DOMAIN].get(CONF_AUTOHEAL) + use_debug = config.get(CONF_DEBUG, DEFAULT_DEBUG) + autoheal = config.get(CONF_AUTOHEAL, + DEFAULT_CONF_AUTOHEAL) device_config = EntityValues( - config[DOMAIN][CONF_DEVICE_CONFIG], - config[DOMAIN][CONF_DEVICE_CONFIG_DOMAIN], - config[DOMAIN][CONF_DEVICE_CONFIG_GLOB]) + config.get(CONF_DEVICE_CONFIG), + config.get(CONF_DEVICE_CONFIG_DOMAIN), + config.get(CONF_DEVICE_CONFIG_GLOB)) # Setup options options = ZWaveOption( - config[DOMAIN].get(CONF_USB_STICK_PATH), + config_entry.data[CONF_USB_STICK_PATH], user_path=hass.config.config_dir, - config_path=config[DOMAIN].get(CONF_CONFIG_PATH)) + config_path=config.get(CONF_CONFIG_PATH)) options.set_console_output(use_debug) - if CONF_NETWORK_KEY in config[DOMAIN]: - options.addOption("NetworkKey", config[DOMAIN][CONF_NETWORK_KEY]) - - options.lock() + if CONF_NETWORK_KEY in config_entry.data: + options.addOption("NetworkKey", config_entry.data[CONF_NETWORK_KEY]) + await hass.async_add_executor_job(options.lock) network = hass.data[DATA_NETWORK] = ZWaveNetwork(options, autostart=False) hass.data[DATA_DEVICES] = {} hass.data[DATA_ENTITY_VALUES] = [] @@ -666,7 +690,7 @@ async def async_setup(hass, config): def _finalize_start(): """Perform final initializations after Z-Wave network is awaked.""" polling_interval = convert( - config[DOMAIN].get(CONF_POLLING_INTERVAL), int) + config.get(CONF_POLLING_INTERVAL), int) if polling_interval is not None: network.set_poll_interval(polling_interval, False) @@ -691,8 +715,6 @@ async def async_setup(hass, config): test_network) hass.services.register(DOMAIN, const.SERVICE_STOP_NETWORK, stop_network) - hass.services.register(DOMAIN, const.SERVICE_START_NETWORK, - start_zwave) hass.services.register(DOMAIN, const.SERVICE_RENAME_NODE, rename_node, schema=RENAME_NODE_SCHEMA) hass.services.register(DOMAIN, const.SERVICE_RENAME_VALUE, @@ -752,6 +774,9 @@ async def async_setup(hass, config): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_zwave) + hass.services.async_register(DOMAIN, const.SERVICE_START_NETWORK, + start_zwave) + return True diff --git a/homeassistant/components/zwave/config_flow.py b/homeassistant/components/zwave/config_flow.py new file mode 100644 index 00000000000..2b853ffa81d --- /dev/null +++ b/homeassistant/components/zwave/config_flow.py @@ -0,0 +1,95 @@ +"""Config flow to configure Z-Wave.""" +from collections import OrderedDict +import logging + +import voluptuous as vol + +from homeassistant import config_entries + +from .const import ( + CONF_USB_STICK_PATH, CONF_NETWORK_KEY, + DEFAULT_CONF_USB_STICK_PATH, DOMAIN) + +_LOGGER = logging.getLogger(__name__) + + +@config_entries.HANDLERS.register(DOMAIN) +class ZwaveFlowHandler(config_entries.ConfigFlow): + """Handle a Z-Wave config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + def __init__(self): + """Initialize the Z-Wave config flow.""" + self.usb_path = CONF_USB_STICK_PATH + + async def async_step_user(self, user_input=None): + """Handle a flow start.""" + if self._async_current_entries(): + return self.async_abort(reason='one_instance_only') + + errors = {} + + fields = OrderedDict() + fields[vol.Required(CONF_USB_STICK_PATH, + default=DEFAULT_CONF_USB_STICK_PATH)] = str + fields[vol.Optional(CONF_NETWORK_KEY)] = str + + if user_input is not None: + # Check if USB path is valid + from openzwave.option import ZWaveOption + from openzwave.object import ZWaveException + + try: + from functools import partial + # pylint: disable=unused-variable + option = await self.hass.async_add_executor_job( # noqa: F841 + partial(ZWaveOption, + user_input[CONF_USB_STICK_PATH], + user_path=self.hass.config.config_dir) + ) + except ZWaveException: + errors['base'] = 'option_error' + return self.async_show_form( + step_id='user', + data_schema=vol.Schema(fields), + errors=errors + ) + + if user_input.get(CONF_NETWORK_KEY) is None: + # Generate a random key + from random import choice + key = str() + for i in range(16): + key += '0x' + key += choice('1234567890ABCDEF') + key += choice('1234567890ABCDEF') + if i < 15: + key += ', ' + user_input[CONF_NETWORK_KEY] = key + + return self.async_create_entry( + title='Z-Wave', + data={ + CONF_USB_STICK_PATH: user_input[CONF_USB_STICK_PATH], + CONF_NETWORK_KEY: user_input[CONF_NETWORK_KEY], + }, + ) + + return self.async_show_form( + step_id='user', data_schema=vol.Schema(fields) + ) + + async def async_step_import(self, info): + """Import existing configuration from Z-Wave.""" + if self._async_current_entries(): + return self.async_abort(reason='already_setup') + + return self.async_create_entry( + title="Z-Wave (import from configuration.yaml)", + data={ + CONF_USB_STICK_PATH: info.get(CONF_USB_STICK_PATH), + CONF_NETWORK_KEY: info.get(CONF_NETWORK_KEY), + }, + ) diff --git a/homeassistant/components/zwave/const.py b/homeassistant/components/zwave/const.py index b84f0287349..fece48655df 100644 --- a/homeassistant/components/zwave/const.py +++ b/homeassistant/components/zwave/const.py @@ -22,6 +22,18 @@ ATTR_VALUE_INSTANCE = "value_instance" NETWORK_READY_WAIT_SECS = 300 NODE_READY_WAIT_SECS = 30 +CONF_AUTOHEAL = 'autoheal' +CONF_DEBUG = 'debug' +CONF_POLLING_INTERVAL = 'polling_interval' +CONF_USB_STICK_PATH = 'usb_path' +CONF_CONFIG_PATH = 'config_path' +CONF_NETWORK_KEY = 'network_key' + +DEFAULT_CONF_AUTOHEAL = True +DEFAULT_CONF_USB_STICK_PATH = '/zwaveusbstick' +DEFAULT_POLLING_INTERVAL = 60000 +DEFAULT_DEBUG = False + DISCOVERY_DEVICE = 'device' DATA_DEVICES = 'zwave_devices' diff --git a/homeassistant/components/zwave/strings.json b/homeassistant/components/zwave/strings.json new file mode 100644 index 00000000000..0ac55e46791 --- /dev/null +++ b/homeassistant/components/zwave/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "title": "Z-Wave", + "step": { + "user": { + "title": "Set up Z-Wave", + "description": "See https://www.home-assistant.io/docs/z-wave/installation/ for information on the configuration variables", + "data": { + "usb_path": "USB Path", + "network_key": "Network Key (leave blank to auto-generate)" + } + } + }, + "error": { + "option_error": "Z-Wave validation failed. Is the path to the USB stick correct?" + }, + "abort": { + "already_configured": "Z-Wave is already configured", + "one_instance_only": "Component only supports one Z-Wave instance" + } + } +} \ No newline at end of file diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 56d4d24eea2..053aa079617 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -151,6 +151,7 @@ FLOWS = [ 'tradfri', 'zone', 'upnp', + 'zwave' ] diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index a2290d8aabf..ef330b48f72 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -2,6 +2,7 @@ import asyncio from collections import OrderedDict from datetime import datetime +from pytz import utc import unittest from unittest.mock import patch, MagicMock @@ -22,13 +23,6 @@ from tests.common import ( from tests.mock.zwave import MockNetwork, MockNode, MockValue, MockEntityValues -@asyncio.coroutine -def test_missing_openzwave(hass): - """Test that missing openzwave lib stops setup.""" - result = yield from async_setup_component(hass, 'zwave', {'zwave': {}}) - assert not result - - @asyncio.coroutine def test_valid_device_config(hass, mock_openzwave): """Test valid device config.""" @@ -41,6 +35,7 @@ def test_valid_device_config(hass, mock_openzwave): 'zwave': { 'device_config': device_config }}) + yield from hass.async_block_till_done() assert result @@ -57,6 +52,7 @@ def test_invalid_device_config(hass, mock_openzwave): 'zwave': { 'device_config': device_config }}) + yield from hass.async_block_till_done() assert not result @@ -81,6 +77,7 @@ def test_network_options(hass, mock_openzwave): 'usb_path': 'mock_usb_path', 'config_path': 'mock_config_path', }}) + yield from hass.async_block_till_done() assert result @@ -92,14 +89,16 @@ def test_network_options(hass, mock_openzwave): @asyncio.coroutine def test_auto_heal_midnight(hass, mock_openzwave): """Test network auto-heal at midnight.""" - assert (yield from async_setup_component(hass, 'zwave', { + yield from async_setup_component(hass, 'zwave', { 'zwave': { 'autoheal': True, - }})) + }}) + yield from hass.async_block_till_done() + network = hass.data[zwave.DATA_NETWORK] assert not network.heal.called - time = datetime(2017, 5, 6, 0, 0, 0) + time = utc.localize(datetime(2017, 5, 6, 0, 0, 0)) async_fire_time_changed(hass, time) yield from hass.async_block_till_done() assert network.heal.called @@ -109,14 +108,16 @@ def test_auto_heal_midnight(hass, mock_openzwave): @asyncio.coroutine def test_auto_heal_disabled(hass, mock_openzwave): """Test network auto-heal disabled.""" - assert (yield from async_setup_component(hass, 'zwave', { + yield from async_setup_component(hass, 'zwave', { 'zwave': { 'autoheal': False, - }})) + }}) + yield from hass.async_block_till_done() + network = hass.data[zwave.DATA_NETWORK] assert not network.heal.called - time = datetime(2017, 5, 6, 0, 0, 0) + time = utc.localize(datetime(2017, 5, 6, 0, 0, 0)) async_fire_time_changed(hass, time) yield from hass.async_block_till_done() assert not network.heal.called @@ -215,6 +216,7 @@ def test_node_discovery(hass, mock_openzwave): with patch('pydispatch.dispatcher.connect', new=mock_connect): yield from async_setup_component(hass, 'zwave', {'zwave': {}}) + yield from hass.async_block_till_done() assert len(mock_receivers) == 1 @@ -235,6 +237,7 @@ async def test_unparsed_node_discovery(hass, mock_openzwave): with patch('pydispatch.dispatcher.connect', new=mock_connect): await async_setup_component(hass, 'zwave', {'zwave': {}}) + await hass.async_block_till_done() assert len(mock_receivers) == 1 @@ -282,6 +285,7 @@ def test_node_ignored(hass, mock_openzwave): 'zwave.mock_node': { 'ignored': True, }}}}) + yield from hass.async_block_till_done() assert len(mock_receivers) == 1 @@ -303,6 +307,7 @@ def test_value_discovery(hass, mock_openzwave): with patch('pydispatch.dispatcher.connect', new=mock_connect): yield from async_setup_component(hass, 'zwave', {'zwave': {}}) + yield from hass.async_block_till_done() assert len(mock_receivers) == 1 @@ -328,6 +333,7 @@ def test_value_discovery_existing_entity(hass, mock_openzwave): with patch('pydispatch.dispatcher.connect', new=mock_connect): yield from async_setup_component(hass, 'zwave', {'zwave': {}}) + yield from hass.async_block_till_done() assert len(mock_receivers) == 1 @@ -373,6 +379,7 @@ def test_power_schemes(hass, mock_openzwave): with patch('pydispatch.dispatcher.connect', new=mock_connect): yield from async_setup_component(hass, 'zwave', {'zwave': {}}) + yield from hass.async_block_till_done() assert len(mock_receivers) == 1 @@ -415,6 +422,7 @@ def test_network_ready(hass, mock_openzwave): with patch('pydispatch.dispatcher.connect', new=mock_connect): yield from async_setup_component(hass, 'zwave', {'zwave': {}}) + yield from hass.async_block_till_done() assert len(mock_receivers) == 1 @@ -442,6 +450,7 @@ def test_network_complete(hass, mock_openzwave): with patch('pydispatch.dispatcher.connect', new=mock_connect): yield from async_setup_component(hass, 'zwave', {'zwave': {}}) + yield from hass.async_block_till_done() assert len(mock_receivers) == 1 @@ -469,6 +478,7 @@ def test_network_complete_some_dead(hass, mock_openzwave): with patch('pydispatch.dispatcher.connect', new=mock_connect): yield from async_setup_component(hass, 'zwave', {'zwave': {}}) + yield from hass.async_block_till_done() assert len(mock_receivers) == 1 From e903f7ffdafc7f5e0797f654c9b8314529412775 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 9 Oct 2018 16:54:38 +0200 Subject: [PATCH 065/265] Manual updates (#17278) --- homeassistant/components/__init__.py | 15 +++++++++++++ homeassistant/helpers/entity.py | 2 +- homeassistant/helpers/entity_component.py | 26 +++++++++++++++++++++++ tests/components/test_init.py | 14 ++++++++++++ tests/helpers/test_entity_component.py | 16 ++++++++++++++ 5 files changed, 72 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index e2701ee37f1..bdb89dd60fa 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -12,6 +12,8 @@ import itertools as it import logging from typing import Awaitable +import voluptuous as vol + import homeassistant.core as ha import homeassistant.config as conf_util from homeassistant.exceptions import HomeAssistantError @@ -21,11 +23,16 @@ from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART, RESTART_EXIT_CODE) +from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) SERVICE_RELOAD_CORE_CONFIG = 'reload_core_config' SERVICE_CHECK_CONFIG = 'check_config' +SERVICE_UPDATE_ENTITY = 'update_entity' +SCHEMA_UPDATE_ENTITY = vol.Schema({ + ATTR_ENTITY_ID: cv.entity_id +}) def is_on(hass, entity_id=None): @@ -133,12 +140,20 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]: if call.service == SERVICE_HOMEASSISTANT_RESTART: hass.async_create_task(hass.async_stop(RESTART_EXIT_CODE)) + async def async_handle_update_service(call): + """Service handler for updating an entity.""" + await hass.helpers.entity_component.async_update_entity( + call.data[ATTR_ENTITY_ID]) + hass.services.async_register( ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service) hass.services.async_register( ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service) hass.services.async_register( ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service) + hass.services.async_register( + ha.DOMAIN, SERVICE_UPDATE_ENTITY, async_handle_update_service, + schema=SCHEMA_UPDATE_ENTITY) async def async_handle_reload_config(call): """Service handler for reloading core config.""" diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 60fd661a765..987bdeae6ca 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -346,7 +346,7 @@ class Entity: if hasattr(self, 'async_update'): await self.async_update() elif hasattr(self, 'update'): - await self.hass.async_add_job(self.update) + await self.hass.async_add_executor_job(self.update) finally: self._update_staged = False if warning: diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index c2ab8722c97..982c92510a9 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -2,6 +2,7 @@ import asyncio from datetime import timedelta from itertools import chain +import logging from homeassistant import config as conf_util from homeassistant.setup import async_prepare_setup_platform @@ -11,10 +12,33 @@ from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, discovery from homeassistant.helpers.service import extract_entity_ids +from homeassistant.loader import bind_hass from homeassistant.util import slugify from .entity_platform import EntityPlatform DEFAULT_SCAN_INTERVAL = timedelta(seconds=15) +DATA_INSTANCES = 'entity_components' + + +@bind_hass +async def async_update_entity(hass, entity_id): + """Trigger an update for an entity.""" + domain = entity_id.split('.', 1)[0] + entity_comp = hass.data.get(DATA_INSTANCES, {}).get(domain) + + if entity_comp is None: + logging.getLogger(__name__).warning( + 'Forced update failed. Component for %s not loaded.', entity_id) + return + + entity = entity_comp.get_entity(entity_id) + + if entity is None: + logging.getLogger(__name__).warning( + 'Forced update failed. Entity %s not found.', entity_id) + return + + await entity.async_update_ha_state(True) class EntityComponent: @@ -45,6 +69,8 @@ class EntityComponent: self.async_add_entities = self._platforms[domain].async_add_entities self.add_entities = self._platforms[domain].add_entities + hass.data.setdefault(DATA_INSTANCES, {})[domain] = self + @property def entities(self): """Return an iterable that returns all entities.""" diff --git a/tests/components/test_init.py b/tests/components/test_init.py index 68396f5abcb..b9152bbdd6a 100644 --- a/tests/components/test_init.py +++ b/tests/components/test_init.py @@ -355,3 +355,17 @@ async def test_turn_on_to_not_block_for_domains_without_service(hass): 'light', 'turn_on', {'entity_id': ['light.bla', 'light.test']}, True) assert mock_call.call_args_list[1][0] == ( 'sensor', 'turn_on', {'entity_id': ['sensor.bla']}, False) + + +async def test_entity_update(hass): + """Test being able to call entity update.""" + await comps.async_setup(hass, {}) + + with patch('homeassistant.helpers.entity_component.async_update_entity', + return_value=mock_coro()) as mock_update: + await hass.services.async_call('homeassistant', 'update_entity', { + 'entity_id': 'light.kitchen' + }, blocking=True) + + assert len(mock_update.mock_calls) == 1 + assert mock_update.mock_calls[0][1][1] == 'light.kitchen' diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 1a0c248383b..c853d0b3447 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -415,3 +415,19 @@ async def test_unload_entry_fails_if_never_loaded(hass): with pytest.raises(ValueError): await component.async_unload_entry(entry) + + +async def test_update_entity(hass): + """Test that we can update an entity with the helper.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + entity = MockEntity() + entity.async_update_ha_state = Mock(return_value=mock_coro()) + await component.async_add_entities([entity]) + + # Called as part of async_add_entities + assert len(entity.async_update_ha_state.mock_calls) == 1 + + await hass.helpers.entity_component.async_update_entity(entity.entity_id) + + assert len(entity.async_update_ha_state.mock_calls) == 2 + assert entity.async_update_ha_state.mock_calls[-1][1][0] is True From a99ba0a1d4de64c270edf356a391cb8c087eec8d Mon Sep 17 00:00:00 2001 From: Markus Nigbur Date: Tue, 9 Oct 2018 19:18:46 +0200 Subject: [PATCH 066/265] Bumped fints component to version 1.0.1 (#17280) --- homeassistant/components/sensor/fints.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/fints.py b/homeassistant/components/sensor/fints.py index 1704c13b5aa..e5dae70070b 100644 --- a/homeassistant/components/sensor/fints.py +++ b/homeassistant/components/sensor/fints.py @@ -15,7 +15,7 @@ from homeassistant.const import CONF_USERNAME, CONF_PIN, CONF_URL, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['fints==0.2.1'] +REQUIREMENTS = ['fints==1.0.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 1643897927d..3581eb3b9e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -363,7 +363,7 @@ fedexdeliverymanager==1.0.6 feedparser==5.2.1 # homeassistant.components.sensor.fints -fints==0.2.1 +fints==1.0.1 # homeassistant.components.sensor.fitbit fitbit==0.3.0 From 2aeb0efc7c232c3bcfd725a6df7b4dcbddf111e2 Mon Sep 17 00:00:00 2001 From: dickesW <42518853+dickesW@users.noreply.github.com> Date: Tue, 9 Oct 2018 19:19:21 +0200 Subject: [PATCH 067/265] Fixed Temperature for HMIP-WeatherStation Plus/Basic (#17216) --- homeassistant/components/homematic/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 927f86b590d..0ce3e5c4419 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -97,7 +97,9 @@ HM_IGNORE_DISCOVERY_NODE = [ ] HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS = { - 'ACTUAL_TEMPERATURE': ['IPAreaThermostat', 'IPWeatherSensor'], + 'ACTUAL_TEMPERATURE': [ + 'IPAreaThermostat', 'IPWeatherSensor', + 'IPWeatherSensorPlus', 'IPWeatherSensorBasic'], } HM_ATTRIBUTE_SUPPORT = { From fc67f5eef3c69a2e1fe693d0c519c1a1c3e991ce Mon Sep 17 00:00:00 2001 From: mvn23 Date: Tue, 9 Oct 2018 21:06:24 +0200 Subject: [PATCH 068/265] Rewrite opentherm_gw to a component (#17133) * Rewrite opentherm_gw to a component which loads the opentherm_gw climate platform. * Add OpenTherm Gateway sensor platform. * Remove library imports from platforms (use hass.data instead) * Update .coveragerc * Update docstrings to use new component documentation url * Add OpenTherm Gateway binary sensor support. Fix houndci findings. * Revert "Add OpenTherm Gateway binary sensor support." This reverts commit 5711dc4c25edc7352ba8bdf14875c79f0bf51d11. * Revert "Add OpenTherm Gateway sensor platform." This reverts commit b3505ed5611d52842c1ea623cd04a13ef2bb9a88. * Remove import from platform, use hass.data instead. Update .coveragerc Update docstrings Update requirements_all.txt General code cleanup * Fix review findings. Avoid using hass.data within connect_and_subscribe. --- .coveragerc | 4 +- .../components/climate/opentherm_gw.py | 81 ++++++++----------- homeassistant/components/opentherm_gw.py | 74 +++++++++++++++++ requirements_all.txt | 2 +- 4 files changed, 111 insertions(+), 50 deletions(-) create mode 100644 homeassistant/components/opentherm_gw.py diff --git a/.coveragerc b/.coveragerc index 801932b19fb..459b59f3d39 100644 --- a/.coveragerc +++ b/.coveragerc @@ -248,6 +248,9 @@ omit = homeassistant/components/opencv.py homeassistant/components/*/opencv.py + homeassistant/components/opentherm_gw.py + homeassistant/components/climate/opentherm_gw.py + homeassistant/components/openuv/__init__.py homeassistant/components/*/openuv.py @@ -438,7 +441,6 @@ omit = homeassistant/components/climate/honeywell.py homeassistant/components/climate/knx.py homeassistant/components/climate/oem.py - homeassistant/components/climate/opentherm_gw.py homeassistant/components/climate/proliphix.py homeassistant/components/climate/radiotherm.py homeassistant/components/climate/sensibo.py diff --git a/homeassistant/components/climate/opentherm_gw.py b/homeassistant/components/climate/opentherm_gw.py index 00049d26b7f..6dc52e6acc7 100644 --- a/homeassistant/components/climate/opentherm_gw.py +++ b/homeassistant/components/climate/opentherm_gw.py @@ -1,34 +1,23 @@ """ -Support for OpenTherm Gateway devices. +Support for OpenTherm Gateway climate devices. -For more details about this component, please refer to the documentation at +For more details about this platform, please refer to the documentation at http://home-assistant.io/components/climate.opentherm_gw/ """ import logging -import voluptuous as vol - -from homeassistant.components.climate import (ClimateDevice, PLATFORM_SCHEMA, - STATE_IDLE, STATE_HEAT, - STATE_COOL, +from homeassistant.components.climate import (ClimateDevice, STATE_IDLE, + STATE_HEAT, STATE_COOL, SUPPORT_TARGET_TEMPERATURE) -from homeassistant.const import (ATTR_TEMPERATURE, CONF_DEVICE, CONF_NAME, - PRECISION_HALVES, PRECISION_TENTHS, - TEMP_CELSIUS, PRECISION_WHOLE) -import homeassistant.helpers.config_validation as cv +from homeassistant.components.opentherm_gw import ( + CONF_FLOOR_TEMP, CONF_PRECISION, DATA_DEVICE, DATA_GW_VARS, + DATA_OPENTHERM_GW, SIGNAL_OPENTHERM_GW_UPDATE) +from homeassistant.const import (ATTR_TEMPERATURE, CONF_NAME, PRECISION_HALVES, + PRECISION_TENTHS, PRECISION_WHOLE, + TEMP_CELSIUS) +from homeassistant.helpers.dispatcher import async_dispatcher_connect -REQUIREMENTS = ['pyotgw==0.1b0'] - -CONF_FLOOR_TEMP = "floor_temperature" -CONF_PRECISION = 'precision' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_DEVICE): cv.string, - vol.Optional(CONF_NAME, default="OpenTherm Gateway"): cv.string, - vol.Optional(CONF_PRECISION): vol.In([PRECISION_TENTHS, PRECISION_HALVES, - PRECISION_WHOLE]), - vol.Optional(CONF_FLOOR_TEMP, default=False): cv.boolean, -}) +DEPENDENCIES = ['opentherm_gw'] SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE) _LOGGER = logging.getLogger(__name__) @@ -37,19 +26,17 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the opentherm_gw device.""" - gateway = OpenThermGateway(config) + gateway = OpenThermGateway(hass, discovery_info) async_add_entities([gateway]) class OpenThermGateway(ClimateDevice): """Representation of a climate device.""" - def __init__(self, config): - """Initialize the sensor.""" - import pyotgw - self.pyotgw = pyotgw - self.gateway = self.pyotgw.pyotgw() - self._device = config[CONF_DEVICE] + def __init__(self, hass, config): + """Initialize the device.""" + self._gateway = hass.data[DATA_OPENTHERM_GW][DATA_DEVICE] + self._gw_vars = hass.data[DATA_OPENTHERM_GW][DATA_GW_VARS] self.friendly_name = config.get(CONF_NAME) self.floor_temp = config.get(CONF_FLOOR_TEMP) self.temp_precision = config.get(CONF_PRECISION) @@ -63,40 +50,38 @@ class OpenThermGateway(ClimateDevice): async def async_added_to_hass(self): """Connect to the OpenTherm Gateway device.""" - await self.gateway.connect(self.hass.loop, self._device) - self.gateway.subscribe(self.receive_report) - _LOGGER.debug("Connected to %s on %s", self.friendly_name, - self._device) + _LOGGER.debug("Added device %s", self.friendly_name) + async_dispatcher_connect(self.hass, SIGNAL_OPENTHERM_GW_UPDATE, + self.receive_report) async def receive_report(self, status): """Receive and handle a new report from the Gateway.""" - _LOGGER.debug("Received report: %s", status) - ch_active = status.get(self.pyotgw.DATA_SLAVE_CH_ACTIVE) - flame_on = status.get(self.pyotgw.DATA_SLAVE_FLAME_ON) - cooling_active = status.get(self.pyotgw.DATA_SLAVE_COOLING_ACTIVE) + ch_active = status.get(self._gw_vars.DATA_SLAVE_CH_ACTIVE) + flame_on = status.get(self._gw_vars.DATA_SLAVE_FLAME_ON) + cooling_active = status.get(self._gw_vars.DATA_SLAVE_COOLING_ACTIVE) if ch_active and flame_on: self._current_operation = STATE_HEAT elif cooling_active: self._current_operation = STATE_COOL else: self._current_operation = STATE_IDLE - self._current_temperature = status.get(self.pyotgw.DATA_ROOM_TEMP) + self._current_temperature = status.get(self._gw_vars.DATA_ROOM_TEMP) - temp = status.get(self.pyotgw.DATA_ROOM_SETPOINT_OVRD) + temp = status.get(self._gw_vars.DATA_ROOM_SETPOINT_OVRD) if temp is None: - temp = status.get(self.pyotgw.DATA_ROOM_SETPOINT) + temp = status.get(self._gw_vars.DATA_ROOM_SETPOINT) self._target_temperature = temp # GPIO mode 5: 0 == Away # GPIO mode 6: 1 == Away - gpio_a_state = status.get(self.pyotgw.OTGW_GPIO_A) + gpio_a_state = status.get(self._gw_vars.OTGW_GPIO_A) if gpio_a_state == 5: self._away_mode_a = 0 elif gpio_a_state == 6: self._away_mode_a = 1 else: self._away_mode_a = None - gpio_b_state = status.get(self.pyotgw.OTGW_GPIO_B) + gpio_b_state = status.get(self._gw_vars.OTGW_GPIO_B) if gpio_b_state == 5: self._away_mode_b = 0 elif gpio_b_state == 6: @@ -104,11 +89,11 @@ class OpenThermGateway(ClimateDevice): else: self._away_mode_b = None if self._away_mode_a is not None: - self._away_state_a = (status.get(self.pyotgw.OTGW_GPIO_A_STATE) == - self._away_mode_a) + self._away_state_a = (status.get( + self._gw_vars.OTGW_GPIO_A_STATE) == self._away_mode_a) if self._away_mode_b is not None: - self._away_state_b = (status.get(self.pyotgw.OTGW_GPIO_B_STATE) == - self._away_mode_b) + self._away_state_b = (status.get( + self._gw_vars.OTGW_GPIO_B_STATE) == self._away_mode_b) self.async_schedule_update_ha_state() @property @@ -170,7 +155,7 @@ class OpenThermGateway(ClimateDevice): """Set new target temperature.""" if ATTR_TEMPERATURE in kwargs: temp = float(kwargs[ATTR_TEMPERATURE]) - self._target_temperature = await self.gateway.set_target_temp( + self._target_temperature = await self._gateway.set_target_temp( temp) self.async_schedule_update_ha_state() diff --git a/homeassistant/components/opentherm_gw.py b/homeassistant/components/opentherm_gw.py new file mode 100644 index 00000000000..7bc2bbeaa8a --- /dev/null +++ b/homeassistant/components/opentherm_gw.py @@ -0,0 +1,74 @@ +""" +Support for OpenTherm Gateway devices. + +For more details about this component, please refer to the documentation at +http://home-assistant.io/components/opentherm_gw/ +""" +import logging + +import voluptuous as vol + +from homeassistant.const import (CONF_DEVICE, CONF_NAME, PRECISION_HALVES, + PRECISION_TENTHS, PRECISION_WHOLE) +from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.dispatcher import async_dispatcher_send + +import homeassistant.helpers.config_validation as cv + +DOMAIN = 'opentherm_gw' + +CONF_CLIMATE = 'climate' +CONF_FLOOR_TEMP = 'floor_temperature' +CONF_PRECISION = 'precision' + +DATA_DEVICE = 'device' +DATA_GW_VARS = 'gw_vars' +DATA_OPENTHERM_GW = 'opentherm_gw' + +SIGNAL_OPENTHERM_GW_UPDATE = 'opentherm_gw_update' + +CLIMATE_SCHEMA = vol.Schema({ + vol.Optional(CONF_NAME, default="OpenTherm Gateway"): cv.string, + vol.Optional(CONF_PRECISION): vol.In([PRECISION_TENTHS, PRECISION_HALVES, + PRECISION_WHOLE]), + vol.Optional(CONF_FLOOR_TEMP, default=False): cv.boolean, +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_DEVICE): cv.string, + vol.Optional(CONF_CLIMATE, default={}): CLIMATE_SCHEMA, + }), +}, extra=vol.ALLOW_EXTRA) + +REQUIREMENTS = ['pyotgw==0.1b0'] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass, config): + """Set up the OpenTherm Gateway component.""" + import pyotgw + conf = config[DOMAIN] + gateway = pyotgw.pyotgw() + hass.data[DATA_OPENTHERM_GW] = { + DATA_DEVICE: gateway, + DATA_GW_VARS: pyotgw.vars, + } + hass.async_create_task(connect_and_subscribe( + hass, conf[CONF_DEVICE], gateway)) + hass.async_create_task(async_load_platform( + hass, 'climate', DOMAIN, conf.get(CONF_CLIMATE))) + return True + + +async def connect_and_subscribe(hass, device_path, gateway): + """Connect to serial device and subscribe report handler.""" + await gateway.connect(hass.loop, device_path) + _LOGGER.debug("Connected to OpenTherm Gateway at %s", device_path) + + async def handle_report(status): + """Handle reports from the OpenTherm Gateway.""" + _LOGGER.debug("Received report: %s", status) + async_dispatcher_send(hass, SIGNAL_OPENTHERM_GW_UPDATE, status) + gateway.subscribe(handle_report) diff --git a/requirements_all.txt b/requirements_all.txt index 3581eb3b9e5..9804d3022d4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1027,7 +1027,7 @@ pyoppleio==1.0.5 # homeassistant.components.iota pyota==2.0.5 -# homeassistant.components.climate.opentherm_gw +# homeassistant.components.opentherm_gw pyotgw==0.1b0 # homeassistant.auth.mfa_modules.notify From 26f2e3dd8b9a5a8ce604605735898367f2e48f0c Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Tue, 9 Oct 2018 15:43:59 -0400 Subject: [PATCH 069/265] Fix samsung bug (#17285) --- homeassistant/components/media_player/samsungtv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py index 3573b28b4e3..3a66aa66dc0 100644 --- a/homeassistant/components/media_player/samsungtv.py +++ b/homeassistant/components/media_player/samsungtv.py @@ -30,7 +30,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'Samsung TV Remote' DEFAULT_PORT = 55000 -DEFAULT_TIMEOUT = 0 +DEFAULT_TIMEOUT = 1 KEY_PRESS_TIMEOUT = 1.2 KNOWN_DEVICES_KEY = 'samsungtv_known_devices' From 2cd99e5a976ed3db78f007204bcece0fe352643a Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 9 Oct 2018 22:37:50 +0200 Subject: [PATCH 070/265] Upgrade shodan to 1.10.4 (#17292) --- homeassistant/components/sensor/shodan.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/shodan.py b/homeassistant/components/sensor/shodan.py index bd74bcaeb2c..1cce17cf64a 100644 --- a/homeassistant/components/sensor/shodan.py +++ b/homeassistant/components/sensor/shodan.py @@ -14,7 +14,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['shodan==1.10.2'] +REQUIREMENTS = ['shodan==1.10.4'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 9804d3022d4..86fbbced661 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1335,7 +1335,7 @@ sense_energy==0.4.2 sharp_aquos_rc==0.3.2 # homeassistant.components.sensor.shodan -shodan==1.10.2 +shodan==1.10.4 # homeassistant.components.notify.simplepush simplepush==1.1.4 From 5022cf8a6cc6c8078472a64f4726113468598e99 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 10 Oct 2018 00:02:07 +0200 Subject: [PATCH 071/265] Upgrade locationsharinglib to 3.0.6 (#17294) --- homeassistant/components/device_tracker/google_maps.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/google_maps.py b/homeassistant/components/device_tracker/google_maps.py index 77f499dcf6b..aec1dcff355 100644 --- a/homeassistant/components/device_tracker/google_maps.py +++ b/homeassistant/components/device_tracker/google_maps.py @@ -19,7 +19,7 @@ from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.typing import ConfigType from homeassistant.util import slugify, dt as dt_util -REQUIREMENTS = ['locationsharinglib==3.0.3'] +REQUIREMENTS = ['locationsharinglib==3.0.6'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 86fbbced661..0370373e818 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -564,7 +564,7 @@ liveboxplaytv==2.0.2 lmnotify==0.0.4 # homeassistant.components.device_tracker.google_maps -locationsharinglib==3.0.3 +locationsharinglib==3.0.6 # homeassistant.components.logi_circle logi_circle==0.1.7 From 8310f4a1cf592af7dea593803aab3ff6ff224b91 Mon Sep 17 00:00:00 2001 From: hanzoh Date: Wed, 10 Oct 2018 02:53:49 +0200 Subject: [PATCH 072/265] Add valve level to HmIP thermostat attributes (#17297) --- homeassistant/components/homematic/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 0ce3e5c4419..dd47f2581e5 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -110,6 +110,7 @@ HM_ATTRIBUTE_SUPPORT = { 'RSSI_PEER': ['rssi', {}], 'RSSI_DEVICE': ['rssi', {}], 'VALVE_STATE': ['valve', {}], + 'LEVEL': ['level', {}], 'BATTERY_STATE': ['battery', {}], 'CONTROL_MODE': ['mode', { 0: 'Auto', From cfc175d71d15dbeda7975638da5ef205aa11a9b5 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Wed, 10 Oct 2018 08:10:42 +0200 Subject: [PATCH 073/265] Make rmvtransport async (#17225) * Make rmvtransport async * Make rmv transport async * Make async tests * Update rmvtransport module version * Remove unnecessary import * Make rmvtransport async * Make rmv transport async * Make async tests * Update rmvtransport module version * Remove unnecessary import * Update requirements * Remove async loop * Fix wrong import * Fix stupidness * Remove unnecessary import * Bump upstream version * Don't store the session * Refactor tests * Add test for no data * Fix linter issues * Fix stale docstring * Fix stale docstring * Remove unnecessary test code * Remove unnecessary import * Add configurable timeout * Remove global variable --- .../components/sensor/rmvtransport.py | 56 ++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/sensor/test_rmvtransport.py | 148 +++++++++--------- 4 files changed, 114 insertions(+), 94 deletions(-) diff --git a/homeassistant/components/sensor/rmvtransport.py b/homeassistant/components/sensor/rmvtransport.py index 0916765e12d..79ec8c7a5e7 100644 --- a/homeassistant/components/sensor/rmvtransport.py +++ b/homeassistant/components/sensor/rmvtransport.py @@ -5,15 +5,18 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.rmvtransport/ """ import logging +from datetime import timedelta import voluptuous as vol import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import Entity from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import (CONF_NAME, ATTR_ATTRIBUTION) +from homeassistant.util import Throttle -REQUIREMENTS = ['PyRMVtransport==0.1'] +REQUIREMENTS = ['PyRMVtransport==0.1.3'] _LOGGER = logging.getLogger(__name__) @@ -26,6 +29,7 @@ CONF_LINES = 'lines' CONF_PRODUCTS = 'products' CONF_TIME_OFFSET = 'time_offset' CONF_MAX_JOURNEYS = 'max_journeys' +CONF_TIMEOUT = 'timeout' DEFAULT_NAME = 'RMV Journey' @@ -46,6 +50,8 @@ ICONS = { } ATTRIBUTION = "Data provided by opendata.rmv.de" +SCAN_INTERVAL = timedelta(seconds=60) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_NEXT_DEPARTURE): [{ vol.Required(CONF_STATION): cv.string, @@ -59,16 +65,23 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.All(cv.ensure_list, [vol.In(VALID_PRODUCTS)]), vol.Optional(CONF_TIME_OFFSET, default=0): cv.positive_int, vol.Optional(CONF_MAX_JOURNEYS, default=5): cv.positive_int, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string}] + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string}], + vol.Optional(CONF_TIMEOUT, default=10): cv.positive_int }) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the RMV departure sensor.""" + timeout = config.get(CONF_TIMEOUT) + + session = async_get_clientsession(hass) + sensors = [] for next_departure in config.get(CONF_NEXT_DEPARTURE): sensors.append( RMVDepartureSensor( + session, next_departure[CONF_STATION], next_departure.get(CONF_DESTINATIONS), next_departure.get(CONF_DIRECTIONS), @@ -76,21 +89,23 @@ def setup_platform(hass, config, add_entities, discovery_info=None): next_departure.get(CONF_PRODUCTS), next_departure.get(CONF_TIME_OFFSET), next_departure.get(CONF_MAX_JOURNEYS), - next_departure.get(CONF_NAME))) - add_entities(sensors, True) + next_departure.get(CONF_NAME), + timeout)) + async_add_entities(sensors, True) class RMVDepartureSensor(Entity): """Implementation of an RMV departure sensor.""" - def __init__(self, station, destinations, directions, - lines, products, time_offset, max_journeys, name): + def __init__(self, session, station, destinations, directions, lines, + products, time_offset, max_journeys, name, timeout): """Initialize the sensor.""" self._station = station self._name = name self._state = None - self.data = RMVDepartureData(station, destinations, directions, lines, - products, time_offset, max_journeys) + self.data = RMVDepartureData(session, station, destinations, + directions, lines, products, time_offset, + max_journeys, timeout) self._icon = ICONS[None] @property @@ -134,9 +149,10 @@ class RMVDepartureSensor(Entity): """Return the unit this state is expressed in.""" return "min" - def update(self): + async def async_update(self): """Get the latest data and update the state.""" - self.data.update() + await self.data.async_update() + if not self.data.departures: self._state = None self._icon = ICONS[None] @@ -151,10 +167,11 @@ class RMVDepartureSensor(Entity): class RMVDepartureData: """Pull data from the opendata.rmv.de web page.""" - def __init__(self, station_id, destinations, directions, - lines, products, time_offset, max_journeys): + def __init__(self, session, station_id, destinations, directions, lines, + products, time_offset, max_journeys, timeout): """Initialize the sensor.""" - import RMVtransport + from RMVtransport import RMVtransport + self.station = None self._station_id = station_id self._destinations = destinations @@ -163,15 +180,16 @@ class RMVDepartureData: self._products = products self._time_offset = time_offset self._max_journeys = max_journeys - self.rmv = RMVtransport.RMVtransport() + self.rmv = RMVtransport(session, timeout) self.departures = [] - def update(self): + @Throttle(SCAN_INTERVAL) + async def async_update(self): """Update the connection data.""" try: - _data = self.rmv.get_departures(self._station_id, - products=self._products, - maxJourneys=50) + _data = await self.rmv.get_departures(self._station_id, + products=self._products, + maxJourneys=50) except ValueError: self.departures = [] _LOGGER.warning("Returned data not understood") diff --git a/requirements_all.txt b/requirements_all.txt index 0370373e818..68bb64844c4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -52,7 +52,7 @@ PyMata==2.14 PyQRCode==1.2.1 # homeassistant.components.sensor.rmvtransport -PyRMVtransport==0.1 +PyRMVtransport==0.1.3 # homeassistant.components.switch.switchbot PySwitchbot==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 754a8947a70..229655bdea7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -22,7 +22,7 @@ requests_mock==1.5.2 HAP-python==2.2.2 # homeassistant.components.sensor.rmvtransport -PyRMVtransport==0.1 +PyRMVtransport==0.1.3 # homeassistant.components.notify.yessssms YesssSMS==0.2.3 diff --git a/tests/components/sensor/test_rmvtransport.py b/tests/components/sensor/test_rmvtransport.py index 9db19ecde49..d917edf0029 100644 --- a/tests/components/sensor/test_rmvtransport.py +++ b/tests/components/sensor/test_rmvtransport.py @@ -1,14 +1,17 @@ """The tests for the rmvtransport platform.""" -import unittest -from unittest.mock import patch import datetime +from unittest.mock import patch -from homeassistant.setup import setup_component +from homeassistant.setup import async_setup_component -from tests.common import get_test_home_assistant +from tests.common import mock_coro -VALID_CONFIG_MINIMAL = {'sensor': {'platform': 'rmvtransport', - 'next_departure': [{'station': '3000010'}]}} + +VALID_CONFIG_MINIMAL = {'sensor': { + 'platform': 'rmvtransport', + 'next_departure': [ + {'station': '3000010'} + ]}} VALID_CONFIG_NAME = {'sensor': { 'platform': 'rmvtransport', @@ -41,8 +44,7 @@ VALID_CONFIG_DEST = {'sensor': { ]}} -def get_departuresMock(stationId, maxJourneys, - products): # pylint: disable=invalid-name +def get_departures_mock(): """Mock rmvtransport departures loading.""" data = {'station': 'Frankfurt (Main) Hauptbahnhof', 'stationId': '3000010', 'filter': '11111111111', 'journeys': [ @@ -97,77 +99,77 @@ def get_departuresMock(stationId, maxJourneys, return data -def get_errDeparturesMock(stationId, maxJourneys, - products): # pylint: disable=invalid-name - """Mock rmvtransport departures erroneous loading.""" - raise ValueError +def get_no_departures_mock(): + """Mock no departures in results.""" + data = {'station': 'Frankfurt (Main) Hauptbahnhof', + 'stationId': '3000010', + 'filter': '11111111111', + 'journeys': []} + return data -class TestRMVtransportSensor(unittest.TestCase): - """Test the rmvtransport sensor.""" +async def test_rmvtransport_min_config(hass): + """Test minimal rmvtransport configuration.""" + with patch('RMVtransport.RMVtransport.get_departures', + return_value=mock_coro(get_departures_mock())): + assert await async_setup_component(hass, 'sensor', + VALID_CONFIG_MINIMAL) is True - def setUp(self): - """Set up things to run when tests begin.""" - self.hass = get_test_home_assistant() - self.config = VALID_CONFIG_MINIMAL - self.reference = {} - self.entities = [] + state = hass.states.get('sensor.frankfurt_main_hauptbahnhof') + assert state.state == '7' + assert state.attributes['departure_time'] == \ + datetime.datetime(2018, 8, 6, 14, 21) + assert state.attributes['direction'] == \ + 'Frankfurt (Main) Hugo-Junkers-Straße/Schleife' + assert state.attributes['product'] == 'Tram' + assert state.attributes['line'] == 12 + assert state.attributes['icon'] == 'mdi:tram' + assert state.attributes['friendly_name'] == 'Frankfurt (Main) Hauptbahnhof' - def tearDown(self): - """Stop everything that was started.""" - self.hass.stop() - @patch('RMVtransport.RMVtransport.get_departures', - side_effect=get_departuresMock) - def test_rmvtransport_min_config(self, mock_get_departures): - """Test minimal rmvtransport configuration.""" - assert setup_component(self.hass, 'sensor', VALID_CONFIG_MINIMAL) - state = self.hass.states.get('sensor.frankfurt_main_hauptbahnhof') - self.assertEqual(state.state, '7') - self.assertEqual(state.attributes['departure_time'], - datetime.datetime(2018, 8, 6, 14, 21)) - self.assertEqual(state.attributes['direction'], - 'Frankfurt (Main) Hugo-Junkers-Straße/Schleife') - self.assertEqual(state.attributes['product'], 'Tram') - self.assertEqual(state.attributes['line'], 12) - self.assertEqual(state.attributes['icon'], 'mdi:tram') - self.assertEqual(state.attributes['friendly_name'], - 'Frankfurt (Main) Hauptbahnhof') +async def test_rmvtransport_name_config(hass): + """Test custom name configuration.""" + with patch('RMVtransport.RMVtransport.get_departures', + return_value=mock_coro(get_departures_mock())): + assert await async_setup_component(hass, 'sensor', VALID_CONFIG_NAME) - @patch('RMVtransport.RMVtransport.get_departures', - side_effect=get_departuresMock) - def test_rmvtransport_name_config(self, mock_get_departures): - """Test custom name configuration.""" - assert setup_component(self.hass, 'sensor', VALID_CONFIG_NAME) - state = self.hass.states.get('sensor.my_station') - self.assertEqual(state.attributes['friendly_name'], 'My Station') + state = hass.states.get('sensor.my_station') + assert state.attributes['friendly_name'] == 'My Station' - @patch('RMVtransport.RMVtransport.get_departures', - side_effect=get_errDeparturesMock) - def test_rmvtransport_err_config(self, mock_get_departures): - """Test erroneous rmvtransport configuration.""" - assert setup_component(self.hass, 'sensor', VALID_CONFIG_MINIMAL) - @patch('RMVtransport.RMVtransport.get_departures', - side_effect=get_departuresMock) - def test_rmvtransport_misc_config(self, mock_get_departures): - """Test misc configuration.""" - assert setup_component(self.hass, 'sensor', VALID_CONFIG_MISC) - state = self.hass.states.get('sensor.frankfurt_main_hauptbahnhof') - self.assertEqual(state.attributes['friendly_name'], - 'Frankfurt (Main) Hauptbahnhof') - self.assertEqual(state.attributes['line'], 21) +async def test_rmvtransport_misc_config(hass): + """Test misc configuration.""" + with patch('RMVtransport.RMVtransport.get_departures', + return_value=mock_coro(get_departures_mock())): + assert await async_setup_component(hass, 'sensor', VALID_CONFIG_MISC) - @patch('RMVtransport.RMVtransport.get_departures', - side_effect=get_departuresMock) - def test_rmvtransport_dest_config(self, mock_get_departures): - """Test misc configuration.""" - assert setup_component(self.hass, 'sensor', VALID_CONFIG_DEST) - state = self.hass.states.get('sensor.frankfurt_main_hauptbahnhof') - self.assertEqual(state.state, '11') - self.assertEqual(state.attributes['direction'], - 'Frankfurt (Main) Hugo-Junkers-Straße/Schleife') - self.assertEqual(state.attributes['line'], 12) - self.assertEqual(state.attributes['minutes'], 11) - self.assertEqual(state.attributes['departure_time'], - datetime.datetime(2018, 8, 6, 14, 25)) + state = hass.states.get('sensor.frankfurt_main_hauptbahnhof') + assert state.attributes['friendly_name'] == 'Frankfurt (Main) Hauptbahnhof' + assert state.attributes['line'] == 21 + + +async def test_rmvtransport_dest_config(hass): + """Test destination configuration.""" + with patch('RMVtransport.RMVtransport.get_departures', + return_value=mock_coro(get_departures_mock())): + assert await async_setup_component(hass, 'sensor', VALID_CONFIG_DEST) + + state = hass.states.get('sensor.frankfurt_main_hauptbahnhof') + assert state.state == '11' + assert state.attributes['direction'] == \ + 'Frankfurt (Main) Hugo-Junkers-Straße/Schleife' + assert state.attributes['line'] == 12 + assert state.attributes['minutes'] == 11 + assert state.attributes['departure_time'] == \ + datetime.datetime(2018, 8, 6, 14, 25) + + +async def test_rmvtransport_no_departures(hass): + """Test for no departures.""" + with patch('RMVtransport.RMVtransport.get_departures', + return_value=mock_coro(get_no_departures_mock())): + assert await async_setup_component(hass, 'sensor', + VALID_CONFIG_MINIMAL) + + state = hass.states.get('sensor.frankfurt_main_hauptbahnhof') + assert not state From 419725e1a9026afe63640426ac14ae5124d91cc1 Mon Sep 17 00:00:00 2001 From: Colby Rome Date: Wed, 10 Oct 2018 02:23:31 -0400 Subject: [PATCH 074/265] Add Verizon Fios Quantum Gateway device_tracker platform (#17023) * wrote quantum_gateway.py * ran gen_requirements script * fixed linting errors, added docstrings * update .coveragerc * fixed typo * add myself to contributors * single quotes for single words * added error handling to prevent stacktrace * updated my pypi library * houndci fixes - added RequestException * added password to config schema --- .coveragerc | 1 + CODEOWNERS | 1 + .../device_tracker/quantum_gateway.py | 69 +++++++++++++++++++ requirements_all.txt | 3 + 4 files changed, 74 insertions(+) create mode 100644 homeassistant/components/device_tracker/quantum_gateway.py diff --git a/.coveragerc b/.coveragerc index 459b59f3d39..297c7edb9a7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -483,6 +483,7 @@ omit = homeassistant/components/device_tracker/netgear.py homeassistant/components/device_tracker/nmap_tracker.py homeassistant/components/device_tracker/ping.py + homeassistant/components/device_tracker/quantum_gateway.py homeassistant/components/device_tracker/ritassist.py homeassistant/components/device_tracker/sky_hub.py homeassistant/components/device_tracker/snmp.py diff --git a/CODEOWNERS b/CODEOWNERS index b3ba8ff564d..93fd0418833 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -60,6 +60,7 @@ homeassistant/components/cover/group.py @cdce8p homeassistant/components/cover/template.py @PhracturedBlue homeassistant/components/device_tracker/automatic.py @armills homeassistant/components/device_tracker/huawei_router.py @abmantis +homeassistant/components/device_tracker/quantum_gateway.py @cisasteelersfan homeassistant/components/device_tracker/tile.py @bachya homeassistant/components/history_graph.py @andrey-git homeassistant/components/influx.py @fabaff diff --git a/homeassistant/components/device_tracker/quantum_gateway.py b/homeassistant/components/device_tracker/quantum_gateway.py new file mode 100644 index 00000000000..a06794f9179 --- /dev/null +++ b/homeassistant/components/device_tracker/quantum_gateway.py @@ -0,0 +1,69 @@ +""" +Support for Verizon FiOS Quantum Gateways. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.quantum_gateway/ +""" +import logging + +from requests.exceptions import RequestException +import voluptuous as vol + +from homeassistant.components.device_tracker import (DOMAIN, PLATFORM_SCHEMA, + DeviceScanner) +from homeassistant.const import (CONF_HOST, CONF_PASSWORD) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['quantum-gateway==0.0.3'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_HOST = 'myfiosgateway.com' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string +}) + + +def get_scanner(hass, config): + """Validate the configuration and return a Quantum Gateway scanner.""" + scanner = QuantumGatewayDeviceScanner(config[DOMAIN]) + + return scanner if scanner.success_init else None + + +class QuantumGatewayDeviceScanner(DeviceScanner): + """This class queries a Quantum Gateway.""" + + def __init__(self, config): + """Initialize the scanner.""" + from quantum_gateway import QuantumGatewayScanner + + self.host = config[CONF_HOST] + self.password = config[CONF_PASSWORD] + _LOGGER.debug('Initializing') + + try: + self.quantum = QuantumGatewayScanner(self.host, self.password) + self.success_init = self.quantum.success_init + except RequestException: + self.success_init = False + _LOGGER.error("Unable to connect to gateway. Check host.") + + if not self.success_init: + _LOGGER.error("Unable to login to gateway. Check password and " + "host.") + + def scan_devices(self): + """Scan for new devices and return a list of found MACs.""" + connected_devices = [] + try: + connected_devices = self.quantum.scan_devices() + except RequestException: + _LOGGER.error("Unable to scan devices. Check connection to router") + return connected_devices + + def get_device_name(self, device): + """Return the name of the given device or None if we don't know.""" + return self.quantum.get_device_name(device) diff --git a/requirements_all.txt b/requirements_all.txt index 68bb64844c4..2ef85e05166 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1264,6 +1264,9 @@ pyzabbix==0.7.4 # homeassistant.components.sensor.qnap qnapstats==0.2.7 +# homeassistant.components.device_tracker.quantum_gateway +quantum-gateway==0.0.3 + # homeassistant.components.rachio rachiopy==0.1.3 From 670c75e844ddfdcf3fec316186d45376c3d966a1 Mon Sep 17 00:00:00 2001 From: Markus Nigbur Date: Wed, 10 Oct 2018 11:49:24 +0200 Subject: [PATCH 075/265] Added resolve_state to template distance function (#17290) _resolve_state was already used in the "closest" function, to allow for states and entity ids --- homeassistant/helpers/template.py | 29 ++++++++++++++++++----------- tests/helpers/test_template.py | 26 ++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index c68aa311998..4650a4d92c2 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -367,18 +367,9 @@ class TemplateMethods: while to_process: value = to_process.pop(0) + point_state = self._resolve_state(value) - if isinstance(value, State): - latitude = value.attributes.get(ATTR_LATITUDE) - longitude = value.attributes.get(ATTR_LONGITUDE) - - if latitude is None or longitude is None: - _LOGGER.warning( - "Distance:State does not contains a location: %s", - value) - return None - - else: + if point_state is None: # We expect this and next value to be lat&lng if not to_process: _LOGGER.warning( @@ -395,6 +386,22 @@ class TemplateMethods: "longitude: %s, %s", value, value_2) return None + else: + if not loc_helper.has_location(point_state): + _LOGGER.warning( + "distance:State does not contain valid location: %s", + point_state) + return None + + latitude = point_state.attributes.get(ATTR_LATITUDE) + longitude = point_state.attributes.get(ATTR_LONGITUDE) + + if latitude is None or longitude is None: + _LOGGER.warning( + "Distance:State does not contains a location: %s", + value) + return None + locations.append((latitude, longitude)) if len(locations) == 1: diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index dc8106e0ed3..2ead38ba345 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -682,6 +682,32 @@ class TestHelpersTemplate(unittest.TestCase): 'None', tpl.render()) + def test_distance_function_with_2_entity_ids(self): + """Test distance function with 2 entity ids.""" + self.hass.states.set('test.object', 'happy', { + 'latitude': 32.87336, + 'longitude': -117.22943, + }) + self.hass.states.set('test.object_2', 'happy', { + 'latitude': self.hass.config.latitude, + 'longitude': self.hass.config.longitude, + }) + tpl = template.Template( + '{{ distance("test.object", "test.object_2") | round }}', + self.hass) + self.assertEqual('187', tpl.render()) + + def test_distance_function_with_1_entity_1_coord(self): + """Test distance function with 1 entity_id and 1 coord.""" + self.hass.states.set('test.object', 'happy', { + 'latitude': self.hass.config.latitude, + 'longitude': self.hass.config.longitude, + }) + tpl = template.Template( + '{{ distance("test.object", "32.87336", "-117.22943") | round }}', + self.hass) + self.assertEqual('187', tpl.render()) + def test_closest_function_home_vs_domain(self): """Test closest function home vs domain.""" self.hass.states.set('test_domain.object', 'happy', { From 78c38749ab309227da6491310e0963710c730175 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Wed, 10 Oct 2018 12:16:32 +0200 Subject: [PATCH 076/265] Xiaomi Vacuum: keep error state active after erroring (#16562) * Check for got_error to keep consistent error reporting * reword a comment --- homeassistant/components/vacuum/xiaomi_miio.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/vacuum/xiaomi_miio.py b/homeassistant/components/vacuum/xiaomi_miio.py index abfbb342418..97a285f90f5 100644 --- a/homeassistant/components/vacuum/xiaomi_miio.py +++ b/homeassistant/components/vacuum/xiaomi_miio.py @@ -179,6 +179,10 @@ class MiroboVacuum(StateVacuumDevice): def state(self): """Return the status of the vacuum cleaner.""" if self.vacuum_state is not None: + # The vacuum reverts back to an idle state after erroring out. + # We want to keep returning an error until it has been cleared. + if self.vacuum_state.got_error: + return STATE_ERROR try: return STATE_CODE_TO_STATE[int(self.vacuum_state.state_code)] except KeyError: From 707b7c202d28cfeabc9ad4ebd8fde4a67122b0d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 10 Oct 2018 13:17:11 +0300 Subject: [PATCH 077/265] Narrow scope of various pylint inline disables (#15364) * Narrow scope of various pylint inline disables * Whitespace tweaks --- homeassistant/components/apcupsd.py | 3 +-- homeassistant/components/calendar/google.py | 1 + homeassistant/components/climate/eq3btsmart.py | 5 ++--- homeassistant/components/cover/zwave.py | 2 -- homeassistant/components/device_tracker/fritz.py | 3 +-- .../components/device_tracker/unifi_direct.py | 3 +-- homeassistant/components/graphite.py | 3 +-- .../components/homekit_controller/__init__.py | 12 ++++-------- homeassistant/components/homematic/__init__.py | 3 +-- .../components/image_processing/dlib_face_detect.py | 3 +-- homeassistant/components/keyboard.py | 3 +-- homeassistant/components/keyboard_remote.py | 4 +++- .../components/light/homekit_controller.py | 3 +-- homeassistant/components/media_player/sonos.py | 5 ++--- homeassistant/components/raspihats.py | 7 +++++-- homeassistant/components/rpi_gpio.py | 13 ++++++------- homeassistant/components/scsgate.py | 6 ++---- homeassistant/components/sensor/bh1750.py | 5 ++--- homeassistant/components/sensor/bme280.py | 5 ++--- homeassistant/components/sensor/bme680.py | 4 ++-- homeassistant/components/sensor/dht.py | 3 +-- .../components/sensor/eddystone_temperature.py | 3 +-- homeassistant/components/sensor/filter.py | 4 ++-- .../components/sensor/fritzbox_callmonitor.py | 3 +-- homeassistant/components/sensor/htu21d.py | 5 ++--- .../components/switch/homekit_controller.py | 3 +-- homeassistant/components/tts/amazon_polly.py | 1 - homeassistant/components/updater.py | 2 +- homeassistant/monkey_patch.py | 4 ++-- homeassistant/util/async_.py | 6 ++---- 30 files changed, 52 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/apcupsd.py b/homeassistant/components/apcupsd.py index 8808cee79a3..79b88378169 100644 --- a/homeassistant/components/apcupsd.py +++ b/homeassistant/components/apcupsd.py @@ -49,10 +49,9 @@ def setup(hass, config): # It doesn't really matter why we're not able to get the status, just that # we can't. - # pylint: disable=broad-except try: DATA.update(no_throttle=True) - except Exception: + except Exception: # pylint: disable=broad-except _LOGGER.exception("Failure while testing APCUPSd status retrieval.") return False return True diff --git a/homeassistant/components/calendar/google.py b/homeassistant/components/calendar/google.py index 041b98dc24b..abb4fd28dd4 100644 --- a/homeassistant/components/calendar/google.py +++ b/homeassistant/components/calendar/google.py @@ -68,6 +68,7 @@ class GoogleCalendarData: self.event = None def _prepare_query(self): + # pylint: disable=import-error from httplib2 import ServerNotFoundError try: diff --git a/homeassistant/components/climate/eq3btsmart.py b/homeassistant/components/climate/eq3btsmart.py index 904d8222e88..eae7bc1d9b9 100644 --- a/homeassistant/components/climate/eq3btsmart.py +++ b/homeassistant/components/climate/eq3btsmart.py @@ -53,14 +53,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices) -# pylint: disable=import-error class EQ3BTSmartThermostat(ClimateDevice): """Representation of an eQ-3 Bluetooth Smart thermostat.""" def __init__(self, _mac, _name): """Initialize the thermostat.""" # We want to avoid name clash with this module. - import eq3bt as eq3 + import eq3bt as eq3 # pylint: disable=import-error self.modes = { eq3.Mode.Open: STATE_ON, @@ -176,7 +175,7 @@ class EQ3BTSmartThermostat(ClimateDevice): def update(self): """Update the data from the thermostat.""" - from bluepy.btle import BTLEException + from bluepy.btle import BTLEException # pylint: disable=import-error try: self._thermostat.update() except BTLEException as ex: diff --git a/homeassistant/components/cover/zwave.py b/homeassistant/components/cover/zwave.py index 258087702e0..ade44faeab5 100644 --- a/homeassistant/components/cover/zwave.py +++ b/homeassistant/components/cover/zwave.py @@ -4,8 +4,6 @@ Support for Z-Wave cover components. For more details about this platform, please refer to the documentation https://home-assistant.io/components/cover.zwave/ """ -# Because we do not compile openzwave on CI -# pylint: disable=import-error import logging from homeassistant.components.cover import ( DOMAIN, SUPPORT_OPEN, SUPPORT_CLOSE, ATTR_POSITION) diff --git a/homeassistant/components/device_tracker/fritz.py b/homeassistant/components/device_tracker/fritz.py index 8c9d1988a71..75e280fe908 100644 --- a/homeassistant/components/device_tracker/fritz.py +++ b/homeassistant/components/device_tracker/fritz.py @@ -43,8 +43,7 @@ class FritzBoxScanner(DeviceScanner): self.password = config[CONF_PASSWORD] self.success_init = True - # pylint: disable=import-error - import fritzconnection as fc + import fritzconnection as fc # pylint: disable=import-error # Establish a connection to the FRITZ!Box. try: diff --git a/homeassistant/components/device_tracker/unifi_direct.py b/homeassistant/components/device_tracker/unifi_direct.py index 228443fe22b..3b5dcc8bac2 100644 --- a/homeassistant/components/device_tracker/unifi_direct.py +++ b/homeassistant/components/device_tracker/unifi_direct.py @@ -86,10 +86,9 @@ class UnifiDeviceScanner(DeviceScanner): def _disconnect(self): """Disconnect the current SSH connection.""" - # pylint: disable=broad-except try: self.ssh.logout() - except Exception: + except Exception: # pylint: disable=broad-except pass finally: self.ssh = None diff --git a/homeassistant/components/graphite.py b/homeassistant/components/graphite.py index 2b768bc3786..26cd80d8da2 100644 --- a/homeassistant/components/graphite.py +++ b/homeassistant/components/graphite.py @@ -144,8 +144,7 @@ class GraphiteFeeder(threading.Thread): try: self._report_attributes( event.data['entity_id'], event.data['new_state']) - # pylint: disable=broad-except - except Exception: + except Exception: # pylint: disable=broad-except # Catch this so we can avoid the thread dying and # make it visible. _LOGGER.exception("Failed to process STATE_CHANGED event") diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 5431dd4a61a..ebb4a2db9cb 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -65,8 +65,7 @@ def homekit_http_send(self, message_body=None, encode_chunked=False): def get_serial(accessory): """Obtain the serial number of a HomeKit device.""" - # pylint: disable=import-error - import homekit + import homekit # pylint: disable=import-error for service in accessory['services']: if homekit.ServicesTypes.get_short(service['type']) != \ 'accessory-information': @@ -85,8 +84,7 @@ class HKDevice(): def __init__(self, hass, host, port, model, hkid, config_num, config): """Initialise a generic HomeKit device.""" - # pylint: disable=import-error - import homekit + import homekit # pylint: disable=import-error _LOGGER.info("Setting up Homekit device %s", model) self.hass = hass @@ -132,8 +130,7 @@ class HKDevice(): def accessory_setup(self): """Handle setup of a HomeKit accessory.""" - # pylint: disable=import-error - import homekit + import homekit # pylint: disable=import-error try: data = self.get_json('/accessories') @@ -185,8 +182,7 @@ class HKDevice(): def device_config_callback(self, callback_data): """Handle initial pairing.""" - # pylint: disable=import-error - import homekit + import homekit # pylint: disable=import-error pairing_id = str(uuid.uuid4()) code = callback_data.get('code').strip() try: diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index dd47f2581e5..4d9d0c2f670 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -778,8 +778,7 @@ class HMDevice(Entity): # Link events from pyhomematic self._subscribe_homematic_events() self._available = not self._hmdevice.UNREACH - # pylint: disable=broad-except - except Exception as err: + except Exception as err: # pylint: disable=broad-except self._connected = False _LOGGER.error("Exception while linking %s: %s", self._address, str(err)) diff --git a/homeassistant/components/image_processing/dlib_face_detect.py b/homeassistant/components/image_processing/dlib_face_detect.py index d3b4e14f4de..cb9ea5ff5f9 100644 --- a/homeassistant/components/image_processing/dlib_face_detect.py +++ b/homeassistant/components/image_processing/dlib_face_detect.py @@ -58,8 +58,7 @@ class DlibFaceDetectEntity(ImageProcessingFaceEntity): def process_image(self, image): """Process image.""" - # pylint: disable=import-error - import face_recognition + import face_recognition # pylint: disable=import-error fak_file = io.BytesIO(image) fak_file.name = 'snapshot.jpg' diff --git a/homeassistant/components/keyboard.py b/homeassistant/components/keyboard.py index 58bb1fa5f42..16253ba271a 100644 --- a/homeassistant/components/keyboard.py +++ b/homeassistant/components/keyboard.py @@ -20,8 +20,7 @@ TAP_KEY_SCHEMA = vol.Schema({}) def setup(hass, config): """Listen for keyboard events.""" - # pylint: disable=import-error - import pykeyboard + import pykeyboard # pylint: disable=import-error keyboard = pykeyboard.PyKeyboard() keyboard.special_key_assignment() diff --git a/homeassistant/components/keyboard_remote.py b/homeassistant/components/keyboard_remote.py index 9a7cc7caecb..ffc92f1949a 100644 --- a/homeassistant/components/keyboard_remote.py +++ b/homeassistant/components/keyboard_remote.py @@ -4,7 +4,6 @@ Receive signals from a keyboard and use it as a remote control. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/keyboard_remote/ """ -# pylint: disable=import-error import threading import logging import os @@ -90,6 +89,7 @@ class KeyboardRemoteThread(threading.Thread): id_folder = '/dev/input/by-id/' if os.path.isdir(id_folder): + # pylint: disable=import-error from evdev import InputDevice, list_devices device_names = [InputDevice(file_name).name for file_name in list_devices()] @@ -104,6 +104,7 @@ class KeyboardRemoteThread(threading.Thread): def _get_keyboard_device(self): """Get the keyboard device.""" + # pylint: disable=import-error from evdev import InputDevice, list_devices if self.device_name: devices = [InputDevice(file_name) for file_name in list_devices()] @@ -121,6 +122,7 @@ class KeyboardRemoteThread(threading.Thread): def run(self): """Run the loop of the KeyboardRemote.""" + # pylint: disable=import-error from evdev import categorize, ecodes if self.dev is not None: diff --git a/homeassistant/components/light/homekit_controller.py b/homeassistant/components/light/homekit_controller.py index dd24e3cfb2e..b874bc49f0e 100644 --- a/homeassistant/components/light/homekit_controller.py +++ b/homeassistant/components/light/homekit_controller.py @@ -38,8 +38,7 @@ class HomeKitLight(HomeKitEntity, Light): def update_characteristics(self, characteristics): """Synchronise light state with Home Assistant.""" - # pylint: disable=import-error - import homekit + import homekit # pylint: disable=import-error for characteristic in characteristics: ctype = characteristic['type'] diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 41ca1b4e85e..28ff269f400 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -447,16 +447,15 @@ class SonosDevice(MediaPlayerDevice): """Set available favorites.""" # SoCo 0.16 raises a generic Exception on invalid xml in favorites. # Filter those out now so our list is safe to use. - # pylint: disable=broad-except try: self._favorites = [] for fav in self.soco.music_library.get_sonos_favorites(): try: if fav.reference.get_uri(): self._favorites.append(fav) - except Exception: + except Exception: # pylint: disable=broad-except _LOGGER.debug("Ignoring invalid favorite '%s'", fav.title) - except Exception: + except Exception: # pylint: disable=broad-except _LOGGER.debug("Ignoring invalid favorite list") def _radio_artwork(self, url): diff --git a/homeassistant/components/raspihats.py b/homeassistant/components/raspihats.py index f43263bf4bf..5e834cdf7ec 100644 --- a/homeassistant/components/raspihats.py +++ b/homeassistant/components/raspihats.py @@ -4,7 +4,6 @@ Support for controlling raspihats boards. For more details about this component, please refer to the documentation at https://home-assistant.io/components/raspihats/ """ -# pylint: disable=import-error,no-name-in-module import logging import threading import time @@ -125,7 +124,7 @@ class I2CHatsManager(threading.Thread): with self._lock: i2c_hat = self._i2c_hats.get(address) if i2c_hat is None: - # pylint: disable=import-error + # pylint: disable=import-error,no-name-in-module import raspihats.i2c_hats as module constructor = getattr(module, board) i2c_hat = constructor(address) @@ -143,6 +142,7 @@ class I2CHatsManager(threading.Thread): def run(self): """Keep alive for I2C-HATs.""" + # pylint: disable=import-error,no-name-in-module from raspihats.i2c_hats import ResponseException _LOGGER.info(log_message(self, "starting")) @@ -205,6 +205,7 @@ class I2CHatsManager(threading.Thread): def read_di(self, address, channel): """Read a value from a I2C-HAT digital input.""" + # pylint: disable=import-error,no-name-in-module from raspihats.i2c_hats import ResponseException with self._lock: @@ -217,6 +218,7 @@ class I2CHatsManager(threading.Thread): def write_dq(self, address, channel, value): """Write a value to a I2C-HAT digital output.""" + # pylint: disable=import-error,no-name-in-module from raspihats.i2c_hats import ResponseException with self._lock: @@ -228,6 +230,7 @@ class I2CHatsManager(threading.Thread): def read_dq(self, address, channel): """Read a value from a I2C-HAT digital output.""" + # pylint: disable=import-error,no-name-in-module from raspihats.i2c_hats import ResponseException with self._lock: diff --git a/homeassistant/components/rpi_gpio.py b/homeassistant/components/rpi_gpio.py index 824ec46d636..09521803aa9 100644 --- a/homeassistant/components/rpi_gpio.py +++ b/homeassistant/components/rpi_gpio.py @@ -4,7 +4,6 @@ Support for controlling GPIO pins of a Raspberry Pi. For more details about this component, please refer to the documentation at https://home-assistant.io/components/rpi_gpio/ """ -# pylint: disable=import-error import logging from homeassistant.const import ( @@ -19,7 +18,7 @@ DOMAIN = 'rpi_gpio' def setup(hass, config): """Set up the Raspberry PI GPIO component.""" - from RPi import GPIO + from RPi import GPIO # pylint: disable=import-error def cleanup_gpio(event): """Stuff to do before stopping.""" @@ -36,32 +35,32 @@ def setup(hass, config): def setup_output(port): """Set up a GPIO as output.""" - from RPi import GPIO + from RPi import GPIO # pylint: disable=import-error GPIO.setup(port, GPIO.OUT) def setup_input(port, pull_mode): """Set up a GPIO as input.""" - from RPi import GPIO + from RPi import GPIO # pylint: disable=import-error GPIO.setup(port, GPIO.IN, GPIO.PUD_DOWN if pull_mode == 'DOWN' else GPIO.PUD_UP) def write_output(port, value): """Write a value to a GPIO.""" - from RPi import GPIO + from RPi import GPIO # pylint: disable=import-error GPIO.output(port, value) def read_input(port): """Read a value from a GPIO.""" - from RPi import GPIO + from RPi import GPIO # pylint: disable=import-error return GPIO.input(port) def edge_detect(port, event_callback, bounce): """Add detection for RISING and FALLING events.""" - from RPi import GPIO + from RPi import GPIO # pylint: disable=import-error GPIO.add_event_detect( port, GPIO.BOTH, diff --git a/homeassistant/components/scsgate.py b/homeassistant/components/scsgate.py index dcea69cbb48..79bf4e217c9 100644 --- a/homeassistant/components/scsgate.py +++ b/homeassistant/components/scsgate.py @@ -42,11 +42,10 @@ def setup(hass, config): device = config[DOMAIN][CONF_DEVICE] global SCSGATE - # pylint: disable=broad-except try: SCSGATE = SCSGate(device=device, logger=_LOGGER) SCSGATE.start() - except Exception as exception: + except Exception as exception: # pylint: disable=broad-except _LOGGER.error("Cannot setup SCSGate component: %s", exception) return False @@ -101,10 +100,9 @@ class SCSGate: if new_device_activated: self._activate_next_device() - # pylint: disable=broad-except try: self._devices[message.entity].process_event(message) - except Exception as exception: + except Exception as exception: # pylint: disable=broad-except msg = "Exception while processing event: {}".format(exception) self._logger.error(msg) else: diff --git a/homeassistant/components/sensor/bh1750.py b/homeassistant/components/sensor/bh1750.py index fb0a0116818..592a6acbe58 100644 --- a/homeassistant/components/sensor/bh1750.py +++ b/homeassistant/components/sensor/bh1750.py @@ -64,12 +64,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=import-error async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the BH1750 sensor.""" - import smbus - from i2csense.bh1750 import BH1750 + import smbus # pylint: disable=import-error + from i2csense.bh1750 import BH1750 # pylint: disable=import-error name = config.get(CONF_NAME) bus_number = config.get(CONF_I2C_BUS) diff --git a/homeassistant/components/sensor/bme280.py b/homeassistant/components/sensor/bme280.py index f67dace817e..a6b773040ef 100644 --- a/homeassistant/components/sensor/bme280.py +++ b/homeassistant/components/sensor/bme280.py @@ -79,12 +79,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=import-error async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the BME280 sensor.""" - import smbus - from i2csense.bme280 import BME280 + import smbus # pylint: disable=import-error + from i2csense.bme280 import BME280 # pylint: disable=import-error SENSOR_TYPES[SENSOR_TEMP][1] = hass.config.units.temperature_unit name = config.get(CONF_NAME) diff --git a/homeassistant/components/sensor/bme680.py b/homeassistant/components/sensor/bme680.py index c4e8baf6c05..cbcb7f1080e 100644 --- a/homeassistant/components/sensor/bme680.py +++ b/homeassistant/components/sensor/bme680.py @@ -115,15 +115,15 @@ async def async_setup_platform(hass, config, async_add_entities, return -# pylint: disable=import-error, no-member def _setup_bme680(config): """Set up and configure the BME680 sensor.""" - from smbus import SMBus + from smbus import SMBus # pylint: disable=import-error import bme680 sensor_handler = None sensor = None try: + # pylint: disable=no-member i2c_address = config.get(CONF_I2C_ADDRESS) bus = SMBus(config.get(CONF_I2C_BUS)) sensor = bme680.BME680(i2c_address, bus) diff --git a/homeassistant/components/sensor/dht.py b/homeassistant/components/sensor/dht.py index 387a555219d..a3af5631a9c 100644 --- a/homeassistant/components/sensor/dht.py +++ b/homeassistant/components/sensor/dht.py @@ -53,8 +53,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the DHT sensor.""" - # pylint: disable=import-error - import Adafruit_DHT + import Adafruit_DHT # pylint: disable=import-error SENSOR_TYPES[SENSOR_TEMPERATURE][1] = hass.config.units.temperature_unit available_sensors = { diff --git a/homeassistant/components/sensor/eddystone_temperature.py b/homeassistant/components/sensor/eddystone_temperature.py index 9e8dc33314a..ef362e17aa6 100644 --- a/homeassistant/components/sensor/eddystone_temperature.py +++ b/homeassistant/components/sensor/eddystone_temperature.py @@ -138,8 +138,7 @@ class Monitor: additional_info['namespace'], additional_info['instance'], packet.temperature) - # pylint: disable=import-error - from beacontools import ( + from beacontools import ( # pylint: disable=import-error BeaconScanner, EddystoneFilter, EddystoneTLMFrame) device_filters = [EddystoneFilter(d.namespace, d.instance) for d in devices] diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py index 1c6e857b92b..76aab42497e 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -63,7 +63,6 @@ FILTER_SCHEMA = vol.Schema({ default=DEFAULT_PRECISION): vol.Coerce(int), }) -# pylint: disable=redefined-builtin FILTER_OUTLIER_SCHEMA = FILTER_SCHEMA.extend({ vol.Required(CONF_FILTER_NAME): FILTER_NAME_OUTLIER, vol.Optional(CONF_FILTER_WINDOW_SIZE, @@ -446,7 +445,8 @@ class TimeSMAFilter(Filter): variant (enum): type of argorithm used to connect discrete values """ - def __init__(self, window_size, precision, entity, type): + def __init__(self, window_size, precision, entity, + type): # pylint: disable=redefined-builtin """Initialize Filter.""" super().__init__(FILTER_NAME_TIME_SMA, window_size, precision, entity) self._time_window = window_size diff --git a/homeassistant/components/sensor/fritzbox_callmonitor.py b/homeassistant/components/sensor/fritzbox_callmonitor.py index c60d06da039..317416a15b8 100644 --- a/homeassistant/components/sensor/fritzbox_callmonitor.py +++ b/homeassistant/components/sensor/fritzbox_callmonitor.py @@ -239,8 +239,7 @@ class FritzBoxPhonebook: self.number_dict = None self.prefixes = prefixes or [] - # pylint: disable=import-error - import fritzconnection as fc + import fritzconnection as fc # pylint: disable=import-error # Establish a connection to the FRITZ!Box. self.fph = fc.FritzPhonebook( address=self.host, user=self.username, password=self.password) diff --git a/homeassistant/components/sensor/htu21d.py b/homeassistant/components/sensor/htu21d.py index ae2555f57f9..4f8665b2011 100644 --- a/homeassistant/components/sensor/htu21d.py +++ b/homeassistant/components/sensor/htu21d.py @@ -38,12 +38,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=import-error async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the HTU21D sensor.""" - import smbus - from i2csense.htu21d import HTU21D + import smbus # pylint: disable=import-error + from i2csense.htu21d import HTU21D # pylint: disable=import-error name = config.get(CONF_NAME) bus_number = config.get(CONF_I2C_BUS) diff --git a/homeassistant/components/switch/homekit_controller.py b/homeassistant/components/switch/homekit_controller.py index a3db6060fcf..374e59aa77b 100644 --- a/homeassistant/components/switch/homekit_controller.py +++ b/homeassistant/components/switch/homekit_controller.py @@ -32,8 +32,7 @@ class HomeKitSwitch(HomeKitEntity, SwitchDevice): def update_characteristics(self, characteristics): """Synchronise the switch state with Home Assistant.""" - # pylint: disable=import-error - import homekit + import homekit # pylint: disable=import-error for characteristic in characteristics: ctype = characteristic['type'] diff --git a/homeassistant/components/tts/amazon_polly.py b/homeassistant/components/tts/amazon_polly.py index b6250f9d0e9..7b3fe4ef04e 100644 --- a/homeassistant/components/tts/amazon_polly.py +++ b/homeassistant/components/tts/amazon_polly.py @@ -87,7 +87,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def get_engine(hass, config): """Set up Amazon Polly speech component.""" - # pylint: disable=import-error output_format = config.get(CONF_OUTPUT_FORMAT) sample_rate = config.get(CONF_SAMPLE_RATE, DEFAULT_SAMPLE_RATES[output_format]) diff --git a/homeassistant/components/updater.py b/homeassistant/components/updater.py index 4e64e3be2e6..6f7b75cf549 100644 --- a/homeassistant/components/updater.py +++ b/homeassistant/components/updater.py @@ -4,9 +4,9 @@ Support to check for available updates. For more details about this component, please refer to the documentation at https://home-assistant.io/components/updater/ """ -# pylint: disable=no-name-in-module, import-error import asyncio from datetime import timedelta +# pylint: disable=import-error,no-name-in-module from distutils.version import StrictVersion import json import logging diff --git a/homeassistant/monkey_patch.py b/homeassistant/monkey_patch.py index edd25817f5a..52efa586c7f 100644 --- a/homeassistant/monkey_patch.py +++ b/homeassistant/monkey_patch.py @@ -25,7 +25,7 @@ from typing import Any def patch_weakref_tasks() -> None: """Replace weakref.WeakSet to address Python 3 bug.""" - # pylint: disable=no-self-use, protected-access, bare-except + # pylint: disable=no-self-use, protected-access import asyncio.tasks class IgnoreCalls: @@ -38,7 +38,7 @@ def patch_weakref_tasks() -> None: asyncio.tasks.Task._all_tasks = IgnoreCalls() # type: ignore try: del asyncio.tasks.Task.__del__ - except: # noqa: E722 + except: # noqa: E722 pylint: disable=bare-except pass diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index 04456b8cb2f..0185128abac 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -145,8 +145,7 @@ def run_coroutine_threadsafe( """Handle the call to the coroutine.""" try: _chain_future(ensure_future(coro, loop=loop), future) - # pylint: disable=broad-except - except Exception as exc: + except Exception as exc: # pylint: disable=broad-except if future.set_running_or_notify_cancel(): future.set_exception(exc) else: @@ -194,8 +193,7 @@ def run_callback_threadsafe(loop: AbstractEventLoop, callback: Callable, """Run callback and store result.""" try: future.set_result(callback(*args)) - # pylint: disable=broad-except - except Exception as exc: + except Exception as exc: # pylint: disable=broad-except if future.set_running_or_notify_cancel(): future.set_exception(exc) else: From e5c3a4be80b2ef682afddf3adba3a1c97893566a Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 10 Oct 2018 13:46:03 +0200 Subject: [PATCH 078/265] Fix and clean haveibeenpwned (#17306) * Move first forced data fetching and update to async_added_to_hass. * Clean up code. --- .../components/sensor/haveibeenpwned.py | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/sensor/haveibeenpwned.py b/homeassistant/components/sensor/haveibeenpwned.py index 9428eaea00e..4d651ea81c7 100644 --- a/homeassistant/components/sensor/haveibeenpwned.py +++ b/homeassistant/components/sensor/haveibeenpwned.py @@ -42,24 +42,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): devices = [] for email in emails: - devices.append(HaveIBeenPwnedSensor(data, hass, email)) + devices.append(HaveIBeenPwnedSensor(data, email)) add_entities(devices) - # To make sure we get initial data for the sensors ignoring the normal - # throttle of 15 minutes but using an update throttle of 5 seconds - for sensor in devices: - sensor.update_nothrottle() - class HaveIBeenPwnedSensor(Entity): """Implementation of a HaveIBeenPwned sensor.""" - def __init__(self, data, hass, email): + def __init__(self, data, email): """Initialize the HaveIBeenPwned sensor.""" self._state = None self._data = data - self._hass = hass self._email = email self._unit_of_measurement = "Breaches" @@ -95,6 +89,12 @@ class HaveIBeenPwnedSensor(Entity): return val + async def async_added_to_hass(self): + """Get initial data.""" + # To make sure we get initial data for the sensors ignoring the normal + # throttle of 15 minutes but using an update throttle of 5 seconds + self.hass.async_add_executor_job(self.update_nothrottle) + def update_nothrottle(self, dummy=None): """Update sensor without throttle.""" self._data.update_no_throttle() @@ -106,13 +106,12 @@ class HaveIBeenPwnedSensor(Entity): # normal using update if self._email not in self._data.data: track_point_in_time( - self._hass, self.update_nothrottle, + self.hass, self.update_nothrottle, dt_util.now() + MIN_TIME_BETWEEN_FORCED_UPDATES) return - if self._email in self._data.data: - self._state = len(self._data.data[self._email]) - self.schedule_update_ha_state() + self._state = len(self._data.data[self._email]) + self.schedule_update_ha_state() def update(self): """Update data and see if it contains data for our email.""" From a1dac28e4b2110fdf5b09ea8ff87dbee56a8ea83 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 10 Oct 2018 13:49:15 +0200 Subject: [PATCH 079/265] Template sensors to not track all state changes (#17276) * Disable template sensor match all * Only manual update template sensors that match all --- homeassistant/components/sensor/template.py | 26 +++++- tests/components/sensor/test_template.py | 92 +++++++++++++++++++-- 2 files changed, 107 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py index 77b3759d5fc..dbe92c2b3ba 100644 --- a/homeassistant/components/sensor/template.py +++ b/homeassistant/components/sensor/template.py @@ -57,22 +57,36 @@ async def async_setup_platform(hass, config, async_add_entities, entity_ids = set() manual_entity_ids = device_config.get(ATTR_ENTITY_ID) + invalid_templates = [] - for template in (state_template, icon_template, - entity_picture_template, friendly_name_template): + for tpl_name, template in ( + (CONF_VALUE_TEMPLATE, state_template), + (CONF_ICON_TEMPLATE, icon_template), + (CONF_ENTITY_PICTURE_TEMPLATE, entity_picture_template), + (CONF_FRIENDLY_NAME_TEMPLATE, friendly_name_template), + ): if template is None: continue template.hass = hass - if entity_ids == MATCH_ALL or manual_entity_ids is not None: + if manual_entity_ids is not None: continue template_entity_ids = template.extract_entities() if template_entity_ids == MATCH_ALL: entity_ids = MATCH_ALL - else: + # Cut off _template from name + invalid_templates.append(tpl_name[:-9]) + elif entity_ids != MATCH_ALL: entity_ids |= set(template_entity_ids) + if invalid_templates: + _LOGGER.warning( + 'Template sensor %s has no entity ids configured to track nor' + ' were we able to extract the entities to track from the %s ' + 'template(s). This entity will only be able to be updated ' + 'manually.', device, ', '.join(invalid_templates)) + if manual_entity_ids is not None: entity_ids = manual_entity_ids elif entity_ids != MATCH_ALL: @@ -123,6 +137,10 @@ class SensorTemplate(Entity): async def async_added_to_hass(self): """Register callbacks.""" + # We don't render on every update + if self._entities == MATCH_ALL: + return + @callback def template_sensor_state_listener(entity, old_state, new_state): """Handle device state changes.""" diff --git a/tests/components/sensor/test_template.py b/tests/components/sensor/test_template.py index 2211f092d7b..159a5c34d37 100644 --- a/tests/components/sensor/test_template.py +++ b/tests/components/sensor/test_template.py @@ -1,5 +1,5 @@ """The test for the Template sensor platform.""" -from homeassistant.setup import setup_component +from homeassistant.setup import setup_component, async_setup_component from tests.common import get_test_home_assistant, assert_setup_component @@ -52,7 +52,8 @@ class TestTemplateSensor: 'platform': 'template', 'sensors': { 'test_template_sensor': { - 'value_template': "State", + 'value_template': + "{{ states.sensor.test_state.state }}", 'icon_template': "{% if states.sensor.test_state.state == " "'Works' %}" @@ -82,7 +83,8 @@ class TestTemplateSensor: 'platform': 'template', 'sensors': { 'test_template_sensor': { - 'value_template': "State", + 'value_template': + "{{ states.sensor.test_state.state }}", 'entity_picture_template': "{% if states.sensor.test_state.state == " "'Works' %}" @@ -112,7 +114,8 @@ class TestTemplateSensor: 'platform': 'template', 'sensors': { 'test_template_sensor': { - 'value_template': "State", + 'value_template': + "{{ states.sensor.test_state.state }}", 'friendly_name_template': "It {{ states.sensor.test_state.state }}." } @@ -276,7 +279,8 @@ class TestTemplateSensor: 'platform': 'template', 'sensors': { 'test': { - 'value_template': '{{ foo }}', + 'value_template': + '{{ states.sensor.test_sensor.state }}', 'device_class': 'foobarnotreal', }, }, @@ -291,10 +295,14 @@ class TestTemplateSensor: 'platform': 'template', 'sensors': { 'test1': { - 'value_template': '{{ foo }}', + 'value_template': + '{{ states.sensor.test_sensor.state }}', 'device_class': 'temperature', }, - 'test2': {'value_template': '{{ foo }}'}, + 'test2': { + 'value_template': + '{{ states.sensor.test_sensor.state }}' + }, } } }) @@ -304,3 +312,73 @@ class TestTemplateSensor: assert state.attributes['device_class'] == 'temperature' state = self.hass.states.get('sensor.test2') assert 'device_class' not in state.attributes + + +async def test_no_template_match_all(hass, caplog): + """Test that we do not allow sensors that match on all.""" + await async_setup_component(hass, 'sensor', { + 'sensor': { + 'platform': 'template', + 'sensors': { + 'invalid_state': { + 'value_template': '{{ 1 + 1 }}', + }, + 'invalid_icon': { + 'value_template': + '{{ states.sensor.test_sensor.state }}', + 'icon_template': '{{ 1 + 1 }}', + }, + 'invalid_entity_picture': { + 'value_template': + '{{ states.sensor.test_sensor.state }}', + 'entity_picture_template': '{{ 1 + 1 }}', + }, + 'invalid_friendly_name': { + 'value_template': + '{{ states.sensor.test_sensor.state }}', + 'friendly_name_template': '{{ 1 + 1 }}', + }, + } + } + }) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 4 + assert ('Template sensor invalid_state has no entity ids ' + 'configured to track nor were we able to extract the entities to ' + 'track from the value template') in caplog.text + assert ('Template sensor invalid_icon has no entity ids ' + 'configured to track nor were we able to extract the entities to ' + 'track from the icon template') in caplog.text + assert ('Template sensor invalid_entity_picture has no entity ids ' + 'configured to track nor were we able to extract the entities to ' + 'track from the entity_picture template') in caplog.text + assert ('Template sensor invalid_friendly_name has no entity ids ' + 'configured to track nor were we able to extract the entities to ' + 'track from the friendly_name template') in caplog.text + + assert hass.states.get('sensor.invalid_state').state == 'unknown' + assert hass.states.get('sensor.invalid_icon').state == 'unknown' + assert hass.states.get('sensor.invalid_entity_picture').state == 'unknown' + assert hass.states.get('sensor.invalid_friendly_name').state == 'unknown' + + hass.states.async_set('sensor.test_sensor', 'hello') + await hass.async_block_till_done() + + assert hass.states.get('sensor.invalid_state').state == 'unknown' + assert hass.states.get('sensor.invalid_icon').state == 'unknown' + assert hass.states.get('sensor.invalid_entity_picture').state == 'unknown' + assert hass.states.get('sensor.invalid_friendly_name').state == 'unknown' + + await hass.helpers.entity_component.async_update_entity( + 'sensor.invalid_state') + await hass.helpers.entity_component.async_update_entity( + 'sensor.invalid_icon') + await hass.helpers.entity_component.async_update_entity( + 'sensor.invalid_entity_picture') + await hass.helpers.entity_component.async_update_entity( + 'sensor.invalid_friendly_name') + + assert hass.states.get('sensor.invalid_state').state == '2' + assert hass.states.get('sensor.invalid_icon').state == 'hello' + assert hass.states.get('sensor.invalid_entity_picture').state == 'hello' + assert hass.states.get('sensor.invalid_friendly_name').state == 'hello' From 83dd961fdedda6e238b5fe11a4174f7629b889b3 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 10 Oct 2018 13:50:11 +0200 Subject: [PATCH 080/265] Fix hassio discovery (#17275) * Update discovery.py * Update test_discovery.py * Update test_discovery.py * Update test_discovery.py * Update test_discovery.py * Update test_discovery.py * Update test_discovery.py * Fix tests * fix lint --- homeassistant/components/hassio/discovery.py | 9 ++-- tests/components/hassio/conftest.py | 2 + tests/components/hassio/test_discovery.py | 53 ++++++++++++++++++++ 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hassio/discovery.py b/homeassistant/components/hassio/discovery.py index ba5c8c3f789..3c5242607c1 100644 --- a/homeassistant/components/hassio/discovery.py +++ b/homeassistant/components/hassio/discovery.py @@ -5,7 +5,7 @@ import logging from aiohttp import web from aiohttp.web_exceptions import HTTPServiceUnavailable -from homeassistant.core import callback +from homeassistant.core import callback, CoreState from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.components.http import HomeAssistantView @@ -40,8 +40,11 @@ def async_setup_discovery(hass, hassio, config): if jobs: await asyncio.wait(jobs) - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, async_discovery_start_handler) + if hass.state == CoreState.running: + hass.async_create_task(async_discovery_start_handler(None)) + else: + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, async_discovery_start_handler) hass.http.register_view(hassio_discovery) diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py index fb3a172a45c..f9ad1c578de 100644 --- a/tests/components/hassio/conftest.py +++ b/tests/components/hassio/conftest.py @@ -4,6 +4,7 @@ from unittest.mock import patch, Mock import pytest +from homeassistant.core import CoreState from homeassistant.setup import async_setup_component from homeassistant.components.hassio.handler import HassIO, HassioAPIError @@ -33,6 +34,7 @@ def hassio_client(hassio_env, hass, aiohttp_client): patch('homeassistant.components.hassio.HassIO.' 'get_homeassistant_info', Mock(side_effect=HassioAPIError())): + hass.state = CoreState.starting hass.loop.run_until_complete(async_setup_component(hass, 'hassio', { 'http': { 'api_password': API_PASSWORD diff --git a/tests/components/hassio/test_discovery.py b/tests/components/hassio/test_discovery.py index 98d0835c102..c8926a1cd18 100644 --- a/tests/components/hassio/test_discovery.py +++ b/tests/components/hassio/test_discovery.py @@ -1,6 +1,8 @@ """Test config flow.""" from unittest.mock import patch, Mock +from homeassistant.setup import async_setup_component +from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.const import EVENT_HOMEASSISTANT_START, HTTP_HEADER_HA_AUTH from tests.common import mock_coro @@ -29,6 +31,8 @@ async def test_hassio_discovery_startup(hass, aioclient_mock, hassio_client): 'result': 'ok', 'data': {'name': "Mosquitto Test"} }) + assert aioclient_mock.call_count == 0 + with patch('homeassistant.components.mqtt.' 'config_flow.FlowHandler.async_step_hassio', Mock(return_value=mock_coro({"type": "abort"}))) as mock_mqtt: @@ -44,6 +48,55 @@ async def test_hassio_discovery_startup(hass, aioclient_mock, hassio_client): }) +async def test_hassio_discovery_startup_done(hass, aioclient_mock, + hassio_client): + """Test startup and discovery with hass discovery.""" + aioclient_mock.get( + "http://127.0.0.1/discovery", json={ + 'result': 'ok', 'data': {'discovery': [ + { + "service": "mqtt", "uuid": "test", + "addon": "mosquitto", "config": + { + 'broker': 'mock-broker', + 'port': 1883, + 'username': 'mock-user', + 'password': 'mock-pass', + 'protocol': '3.1.1' + } + } + ]}}) + aioclient_mock.get( + "http://127.0.0.1/addons/mosquitto/info", json={ + 'result': 'ok', 'data': {'name': "Mosquitto Test"} + }) + + with patch('homeassistant.components.hassio.HassIO.update_hass_api', + Mock(return_value=mock_coro({"result": "ok"}))), \ + patch('homeassistant.components.hassio.HassIO.' + 'get_homeassistant_info', + Mock(side_effect=HassioAPIError())), \ + patch('homeassistant.components.mqtt.' + 'config_flow.FlowHandler.async_step_hassio', + Mock(return_value=mock_coro({"type": "abort"})) + ) as mock_mqtt: + await hass.async_start() + await async_setup_component(hass, 'hassio', { + 'http': { + 'api_password': API_PASSWORD + } + }) + await hass.async_block_till_done() + + assert aioclient_mock.call_count == 2 + assert mock_mqtt.called + mock_mqtt.assert_called_with({ + 'broker': 'mock-broker', 'port': 1883, 'username': 'mock-user', + 'password': 'mock-pass', 'protocol': '3.1.1', + 'addon': 'Mosquitto Test', + }) + + async def test_hassio_discovery_webhook(hass, aioclient_mock, hassio_client): """Test discovery webhook.""" aioclient_mock.get( From 7f896bfb402a80504e30e2f024942e6d3dd17d20 Mon Sep 17 00:00:00 2001 From: definitio <37266727+definitio@users.noreply.github.com> Date: Wed, 10 Oct 2018 15:06:53 +0300 Subject: [PATCH 081/265] WIP: Don't set initial values for MQTT HVAC in non-optimistic mode (#17268) * Don't set initial temperature in non-optimistic mode * Fix tests * Don't set initial values in non-optimistic mode For fan mode, current operation and swing mode * Fix tests again --- homeassistant/components/climate/mqtt.py | 15 +++++++++---- tests/components/climate/test_mqtt.py | 28 ++++++++++++------------ 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/climate/mqtt.py b/homeassistant/components/climate/mqtt.py index ff9b78135fd..b107710fea5 100644 --- a/homeassistant/components/climate/mqtt.py +++ b/homeassistant/components/climate/mqtt.py @@ -240,15 +240,22 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice): self._value_templates = value_templates self._qos = qos self._retain = retain - self._target_temperature = target_temperature + # set to None in non-optimistic mode + self._target_temperature = self._current_fan_mode = \ + self._current_operation = self._current_swing_mode = None + if self._topic[CONF_TEMPERATURE_STATE_TOPIC] is None: + self._target_temperature = target_temperature self._unit_of_measurement = hass.config.units.temperature_unit self._away = away self._hold = hold self._current_temperature = None - self._current_fan_mode = current_fan_mode - self._current_operation = current_operation + if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None: + self._current_fan_mode = current_fan_mode + if self._topic[CONF_MODE_STATE_TOPIC] is None: + self._current_operation = current_operation self._aux = aux - self._current_swing_mode = current_swing_mode + if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None: + self._current_swing_mode = current_swing_mode self._fan_list = fan_mode_list self._operation_list = mode_list self._swing_list = swing_mode_list diff --git a/tests/components/climate/test_mqtt.py b/tests/components/climate/test_mqtt.py index f330efd99b8..16fe0a6639d 100644 --- a/tests/components/climate/test_mqtt.py +++ b/tests/components/climate/test_mqtt.py @@ -119,14 +119,14 @@ class TestMQTTClimate(unittest.TestCase): assert setup_component(self.hass, climate.DOMAIN, config) state = self.hass.states.get(ENTITY_CLIMATE) - self.assertEqual("off", state.attributes.get('operation_mode')) - self.assertEqual("off", state.state) + self.assertEqual(None, state.attributes.get('operation_mode')) + self.assertEqual("unknown", state.state) common.set_operation_mode(self.hass, "cool", ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) - self.assertEqual("off", state.attributes.get('operation_mode')) - self.assertEqual("off", state.state) + self.assertEqual(None, state.attributes.get('operation_mode')) + self.assertEqual("unknown", state.state) fire_mqtt_message(self.hass, 'mode-state', 'cool') self.hass.block_till_done() @@ -189,12 +189,12 @@ class TestMQTTClimate(unittest.TestCase): assert setup_component(self.hass, climate.DOMAIN, config) state = self.hass.states.get(ENTITY_CLIMATE) - self.assertEqual("low", state.attributes.get('fan_mode')) + self.assertEqual(None, state.attributes.get('fan_mode')) common.set_fan_mode(self.hass, 'high', ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) - self.assertEqual("low", state.attributes.get('fan_mode')) + self.assertEqual(None, state.attributes.get('fan_mode')) fire_mqtt_message(self.hass, 'fan-state', 'high') self.hass.block_till_done() @@ -237,12 +237,12 @@ class TestMQTTClimate(unittest.TestCase): assert setup_component(self.hass, climate.DOMAIN, config) state = self.hass.states.get(ENTITY_CLIMATE) - self.assertEqual("off", state.attributes.get('swing_mode')) + self.assertEqual(None, state.attributes.get('swing_mode')) common.set_swing_mode(self.hass, 'on', ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) - self.assertEqual("off", state.attributes.get('swing_mode')) + self.assertEqual(None, state.attributes.get('swing_mode')) fire_mqtt_message(self.hass, 'swing-state', 'on') self.hass.block_till_done() @@ -310,14 +310,14 @@ class TestMQTTClimate(unittest.TestCase): assert setup_component(self.hass, climate.DOMAIN, config) state = self.hass.states.get(ENTITY_CLIMATE) - self.assertEqual(21, state.attributes.get('temperature')) + self.assertEqual(None, state.attributes.get('temperature')) common.set_operation_mode(self.hass, 'heat', ENTITY_CLIMATE) self.hass.block_till_done() common.set_temperature(self.hass, temperature=47, entity_id=ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) - self.assertEqual(21, state.attributes.get('temperature')) + self.assertEqual(None, state.attributes.get('temperature')) fire_mqtt_message(self.hass, 'temperature-state', '1701') self.hass.block_till_done() @@ -539,28 +539,28 @@ class TestMQTTClimate(unittest.TestCase): # Operation Mode state = self.hass.states.get(ENTITY_CLIMATE) - self.assertEqual("off", state.attributes.get('operation_mode')) + self.assertEqual(None, state.attributes.get('operation_mode')) fire_mqtt_message(self.hass, 'mode-state', '"cool"') self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("cool", state.attributes.get('operation_mode')) # Fan Mode - self.assertEqual("low", state.attributes.get('fan_mode')) + self.assertEqual(None, state.attributes.get('fan_mode')) fire_mqtt_message(self.hass, 'fan-state', '"high"') self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('high', state.attributes.get('fan_mode')) # Swing Mode - self.assertEqual("off", state.attributes.get('swing_mode')) + self.assertEqual(None, state.attributes.get('swing_mode')) fire_mqtt_message(self.hass, 'swing-state', '"on"') self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("on", state.attributes.get('swing_mode')) # Temperature - with valid value - self.assertEqual(21, state.attributes.get('temperature')) + self.assertEqual(None, state.attributes.get('temperature')) fire_mqtt_message(self.hass, 'temperature-state', '"1031"') self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) From ad4d5666fef56f4f79d366ef95061ff0b0451d10 Mon Sep 17 00:00:00 2001 From: Joshi <42069141+Joshi425@users.noreply.github.com> Date: Wed, 10 Oct 2018 14:07:33 +0200 Subject: [PATCH 082/265] Yamaha AVR update and change Sound Mode only on main_zone (#17241) * Add support for sound_mode for Yamaha rxv media_player * Catch ParseError Exeption on surround_program for unsupported models * Catch all Exeptions from rxv * only get sound mode list / current sound mode on main_zone --- homeassistant/components/media_player/yamaha.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/media_player/yamaha.py b/homeassistant/components/media_player/yamaha.py index be61560d52b..101dfc2bc53 100644 --- a/homeassistant/components/media_player/yamaha.py +++ b/homeassistant/components/media_player/yamaha.py @@ -12,9 +12,10 @@ import voluptuous as vol from homeassistant.components.media_player import ( DOMAIN, MEDIA_PLAYER_SCHEMA, MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, - SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOURCE, - SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, - SUPPORT_VOLUME_SET, MediaPlayerDevice) + SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_STOP, + SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + SUPPORT_SELECT_SOUND_MODE, MediaPlayerDevice) + from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, STATE_IDLE, STATE_OFF, STATE_ON, STATE_PLAYING) @@ -184,8 +185,12 @@ class YamahaDevice(MediaPlayerDevice): self._playback_support = self.receiver.get_playback_support() self._is_playback_supported = self.receiver.is_playback_supported( self._current_source) - self._sound_mode = self.receiver.surround_program - self._sound_mode_list = self.receiver.surround_programs() + if self._zone == "Main_Zone": + self._sound_mode = self.receiver.surround_program + self._sound_mode_list = self.receiver.surround_programs() + else: + self._sound_mode = None + self._sound_mode_list = None def build_source_list(self): """Build the source list.""" From 40e0966d7ff1505f8fb4da5e004c6622e5997899 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 10 Oct 2018 14:07:51 +0200 Subject: [PATCH 083/265] Hassio auth (#17274) * Create auth.py * Update auth.py * Update auth.py * Update __init__.py * Update auth.py * Update auth.py * Update auth.py * Update auth.py * Update auth.py * Update auth.py * Update auth.py * Update auth.py * Update auth.py * Add tests * Update test_auth.py * Update auth.py * Update test_auth.py * Update auth.py --- homeassistant/components/hassio/__init__.py | 4 + homeassistant/components/hassio/auth.py | 75 ++++++++++++++++ tests/components/hassio/test_auth.py | 95 +++++++++++++++++++++ 3 files changed, 174 insertions(+) create mode 100644 homeassistant/components/hassio/auth.py create mode 100644 tests/components/hassio/test_auth.py diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index b97d748d864..9516675480a 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -19,6 +19,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.loader import bind_hass from homeassistant.util.dt import utcnow +from .auth import async_setup_auth from .handler import HassIO, HassioAPIError from .discovery import async_setup_discovery from .http import HassIOView @@ -280,4 +281,7 @@ async def async_setup(hass, config): # Init discovery Hass.io feature async_setup_discovery(hass, hassio, config) + # Init auth Hass.io feature + async_setup_auth(hass) + return True diff --git a/homeassistant/components/hassio/auth.py b/homeassistant/components/hassio/auth.py new file mode 100644 index 00000000000..73ef5aa29cc --- /dev/null +++ b/homeassistant/components/hassio/auth.py @@ -0,0 +1,75 @@ +"""Implement the auth feature from Hass.io for Add-ons.""" +import logging +from ipaddress import ip_address +import os + +from aiohttp import web +from aiohttp.web_exceptions import ( + HTTPForbidden, HTTPNotFound, HTTPUnauthorized) +import voluptuous as vol + +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.exceptions import HomeAssistantError +from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http.const import KEY_REAL_IP +from homeassistant.components.http.data_validator import RequestDataValidator + +_LOGGER = logging.getLogger(__name__) + +ATTR_USERNAME = 'username' +ATTR_PASSWORD = 'password' + + +SCHEMA_API_AUTH = vol.Schema({ + vol.Required(ATTR_USERNAME): cv.string, + vol.Required(ATTR_PASSWORD): cv.string, +}) + + +@callback +def async_setup_auth(hass): + """Auth setup.""" + hassio_auth = HassIOAuth(hass) + hass.http.register_view(hassio_auth) + + +class HassIOAuth(HomeAssistantView): + """Hass.io view to handle base part.""" + + name = "api:hassio_auth" + url = "/api/hassio_auth" + + def __init__(self, hass): + """Initialize WebView.""" + self.hass = hass + + @RequestDataValidator(SCHEMA_API_AUTH) + async def post(self, request, data): + """Handle new discovery requests.""" + hassio_ip = os.environ['HASSIO'].split(':')[0] + if request[KEY_REAL_IP] != ip_address(hassio_ip): + _LOGGER.error( + "Invalid auth request from %s", request[KEY_REAL_IP]) + raise HTTPForbidden() + + await self._check_login(data[ATTR_USERNAME], data[ATTR_PASSWORD]) + return web.Response(status=200) + + def _get_provider(self): + """Return Homeassistant auth provider.""" + for prv in self.hass.auth.auth_providers: + if prv.type == 'homeassistant': + return prv + + _LOGGER.error("Can't find Home Assistant auth.") + raise HTTPNotFound() + + async def _check_login(self, username, password): + """Check User credentials.""" + provider = self._get_provider() + + try: + await provider.async_validate_login(username, password) + except HomeAssistantError: + raise HTTPUnauthorized() from None diff --git a/tests/components/hassio/test_auth.py b/tests/components/hassio/test_auth.py new file mode 100644 index 00000000000..50d9488a19c --- /dev/null +++ b/tests/components/hassio/test_auth.py @@ -0,0 +1,95 @@ +"""The tests for the hassio component.""" +from unittest.mock import patch, Mock + +from homeassistant.const import HTTP_HEADER_HA_AUTH +from homeassistant.exceptions import HomeAssistantError + +from tests.common import mock_coro, register_auth_provider +from . import API_PASSWORD + + +async def test_login_success(hass, hassio_client): + """Test no auth needed for .""" + await register_auth_provider(hass, {'type': 'homeassistant'}) + + with patch('homeassistant.auth.providers.homeassistant.' + 'HassAuthProvider.async_validate_login', + Mock(return_value=mock_coro())) as mock_login: + resp = await hassio_client.post( + '/api/hassio_auth', + json={ + "username": "test", + "password": "123456" + }, + headers={ + HTTP_HEADER_HA_AUTH: API_PASSWORD + } + ) + + # Check we got right response + assert resp.status == 200 + mock_login.assert_called_with("test", "123456") + + +async def test_login_error(hass, hassio_client): + """Test no auth needed for error.""" + await register_auth_provider(hass, {'type': 'homeassistant'}) + + with patch('homeassistant.auth.providers.homeassistant.' + 'HassAuthProvider.async_validate_login', + Mock(side_effect=HomeAssistantError())) as mock_login: + resp = await hassio_client.post( + '/api/hassio_auth', + json={ + "username": "test", + "password": "123456" + }, + headers={ + HTTP_HEADER_HA_AUTH: API_PASSWORD + } + ) + + # Check we got right response + assert resp.status == 401 + mock_login.assert_called_with("test", "123456") + + +async def test_login_no_data(hass, hassio_client): + """Test auth with no data -> error.""" + await register_auth_provider(hass, {'type': 'homeassistant'}) + + with patch('homeassistant.auth.providers.homeassistant.' + 'HassAuthProvider.async_validate_login', + Mock(side_effect=HomeAssistantError())) as mock_login: + resp = await hassio_client.post( + '/api/hassio_auth', + headers={ + HTTP_HEADER_HA_AUTH: API_PASSWORD + } + ) + + # Check we got right response + assert resp.status == 400 + assert not mock_login.called + + +async def test_login_no_username(hass, hassio_client): + """Test auth with no username in data -> error.""" + await register_auth_provider(hass, {'type': 'homeassistant'}) + + with patch('homeassistant.auth.providers.homeassistant.' + 'HassAuthProvider.async_validate_login', + Mock(side_effect=HomeAssistantError())) as mock_login: + resp = await hassio_client.post( + '/api/hassio_auth', + json={ + "password": "123456" + }, + headers={ + HTTP_HEADER_HA_AUTH: API_PASSWORD + } + ) + + # Check we got right response + assert resp.status == 400 + assert not mock_login.called From 052c0944254ddab425c4e5666ee9f4b6d99ac44a Mon Sep 17 00:00:00 2001 From: Paul Annekov Date: Wed, 10 Oct 2018 15:24:30 +0300 Subject: [PATCH 084/265] fixed 'on_startup() takes 0 positional arguments but 1 was given' (#17295) --- homeassistant/components/sensor/miflora.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/miflora.py b/homeassistant/components/sensor/miflora.py index a097974fbfd..74bb8261609 100644 --- a/homeassistant/components/sensor/miflora.py +++ b/homeassistant/components/sensor/miflora.py @@ -106,7 +106,7 @@ class MiFloraSensor(Entity): async def async_added_to_hass(self): """Set initial state.""" @callback - def on_startup(): + def on_startup(_): self.async_schedule_update_ha_state(True) self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, on_startup) From d16e6c85248b5ec510efd115ea1b571ae417c972 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 10 Oct 2018 14:25:21 +0200 Subject: [PATCH 085/265] Update translations --- .../components/auth/.translations/fr.json | 16 ++++++++-- .../cast/.translations/zh-Hant.json | 2 +- .../components/deconz/.translations/ru.json | 2 +- .../homematicip_cloud/.translations/ru.json | 2 +- .../components/ifttt/.translations/fr.json | 18 ++++++++++++ .../components/ifttt/.translations/sl.json | 18 ++++++++++++ .../components/lifx/.translations/ca.json | 15 ++++++++++ .../components/lifx/.translations/fr.json | 15 ++++++++++ .../components/lifx/.translations/ko.json | 15 ++++++++++ .../components/lifx/.translations/lb.json | 15 ++++++++++ .../components/lifx/.translations/nl.json | 15 ++++++++++ .../components/lifx/.translations/pl.json | 15 ++++++++++ .../components/lifx/.translations/ru.json | 15 ++++++++++ .../components/lifx/.translations/sl.json | 15 ++++++++++ .../lifx/.translations/zh-Hant.json | 15 ++++++++++ .../components/mqtt/.translations/fr.json | 9 +++++- .../components/mqtt/.translations/sl.json | 7 +++++ .../components/smhi/.translations/ca.json | 19 ++++++++++++ .../components/smhi/.translations/en.json | 2 +- .../components/smhi/.translations/ko.json | 19 ++++++++++++ .../components/smhi/.translations/lb.json | 19 ++++++++++++ .../components/smhi/.translations/nl.json | 19 ++++++++++++ .../components/smhi/.translations/pl.json | 19 ++++++++++++ .../components/smhi/.translations/ru.json | 19 ++++++++++++ .../components/smhi/.translations/sl.json | 19 ++++++++++++ .../smhi/.translations/zh-Hant.json | 19 ++++++++++++ .../components/sonos/.translations/ko.json | 2 +- .../components/sonos/.translations/pl.json | 2 +- .../sonos/.translations/zh-Hant.json | 2 +- .../components/upnp/.translations/fr.json | 23 +++++++++++++++ .../components/upnp/.translations/nl.json | 6 +++- .../components/upnp/.translations/sl.json | 29 +++++++++++++++++++ .../components/zwave/.translations/ca.json | 17 +++++++++++ .../components/zwave/.translations/ko.json | 22 ++++++++++++++ .../components/zwave/.translations/lb.json | 22 ++++++++++++++ .../components/zwave/.translations/nl.json | 22 ++++++++++++++ .../components/zwave/.translations/ru.json | 22 ++++++++++++++ 37 files changed, 521 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/ifttt/.translations/fr.json create mode 100644 homeassistant/components/ifttt/.translations/sl.json create mode 100644 homeassistant/components/lifx/.translations/ca.json create mode 100644 homeassistant/components/lifx/.translations/fr.json create mode 100644 homeassistant/components/lifx/.translations/ko.json create mode 100644 homeassistant/components/lifx/.translations/lb.json create mode 100644 homeassistant/components/lifx/.translations/nl.json create mode 100644 homeassistant/components/lifx/.translations/pl.json create mode 100644 homeassistant/components/lifx/.translations/ru.json create mode 100644 homeassistant/components/lifx/.translations/sl.json create mode 100644 homeassistant/components/lifx/.translations/zh-Hant.json create mode 100644 homeassistant/components/smhi/.translations/ca.json create mode 100644 homeassistant/components/smhi/.translations/ko.json create mode 100644 homeassistant/components/smhi/.translations/lb.json create mode 100644 homeassistant/components/smhi/.translations/nl.json create mode 100644 homeassistant/components/smhi/.translations/pl.json create mode 100644 homeassistant/components/smhi/.translations/ru.json create mode 100644 homeassistant/components/smhi/.translations/sl.json create mode 100644 homeassistant/components/smhi/.translations/zh-Hant.json create mode 100644 homeassistant/components/upnp/.translations/fr.json create mode 100644 homeassistant/components/upnp/.translations/sl.json create mode 100644 homeassistant/components/zwave/.translations/ca.json create mode 100644 homeassistant/components/zwave/.translations/ko.json create mode 100644 homeassistant/components/zwave/.translations/lb.json create mode 100644 homeassistant/components/zwave/.translations/nl.json create mode 100644 homeassistant/components/zwave/.translations/ru.json diff --git a/homeassistant/components/auth/.translations/fr.json b/homeassistant/components/auth/.translations/fr.json index 85540314af0..cf0a1888495 100644 --- a/homeassistant/components/auth/.translations/fr.json +++ b/homeassistant/components/auth/.translations/fr.json @@ -1,11 +1,23 @@ { "mfa_setup": { "notify": { + "abort": { + "no_available_service": "Aucun service de notification disponible." + }, + "error": { + "invalid_code": "Code invalide. Veuillez essayer \u00e0 nouveau." + }, "step": { + "init": { + "description": "Veuillez s\u00e9lectionner l'un des services de notification:", + "title": "Configurer un mot de passe \u00e0 usage unique d\u00e9livr\u00e9 par le composant notify" + }, "setup": { - "description": "Un mot de passe unique a \u00e9t\u00e9 envoy\u00e9 par **notify.{notify_service}**. Veuillez le saisir ci-dessous :" + "description": "Un mot de passe unique a \u00e9t\u00e9 envoy\u00e9 par **notify.{notify_service}**. Veuillez le saisir ci-dessous :", + "title": "V\u00e9rifier la configuration" } - } + }, + "title": "Notifier un mot de passe unique" }, "totp": { "error": { diff --git a/homeassistant/components/cast/.translations/zh-Hant.json b/homeassistant/components/cast/.translations/zh-Hant.json index 711ac320397..d5383fb1a2b 100644 --- a/homeassistant/components/cast/.translations/zh-Hant.json +++ b/homeassistant/components/cast/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Google Cast \u8a2d\u5099\u3002", + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Google Cast \u88dd\u7f6e\u3002", "single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21 Google Cast \u5373\u53ef\u3002" }, "step": { diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index a9b66314f31..4cbc9594ead 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -28,6 +28,6 @@ "title": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0434\u043b\u044f deCONZ" } }, - "title": "deCONZ" + "title": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0448\u043b\u044e\u0437 deCONZ" } } \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/ru.json b/homeassistant/components/homematicip_cloud/.translations/ru.json index 6a0ea612fe8..ae67c616f3f 100644 --- a/homeassistant/components/homematicip_cloud/.translations/ru.json +++ b/homeassistant/components/homematicip_cloud/.translations/ru.json @@ -18,7 +18,7 @@ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043a\u0430\u043a \u043f\u0440\u0435\u0444\u0438\u043a\u0441 \u0434\u043b\u044f \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u044f \u0432\u0441\u0435\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432)", "pin": "PIN-\u043a\u043e\u0434 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)" }, - "title": "\u0412\u044b\u0431\u0438\u0440\u0438\u0442\u0435 \u0442\u043e\u0447\u043a\u0443 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 HomematicIP" + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u043e\u0447\u043a\u0443 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 HomematicIP" }, "link": { "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u0441\u0438\u043d\u044e\u044e \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u0442\u043e\u0447\u043a\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0438 \u043a\u043d\u043e\u043f\u043a\u0443 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438, \u0447\u0442\u043e\u0431\u044b \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c HomematicIP \u0432 Home Assistant. \n\n ![\u0420\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043a\u043d\u043e\u043f\u043a\u0438](/static/images/config_flows/config_homematicip_cloud.png)", diff --git a/homeassistant/components/ifttt/.translations/fr.json b/homeassistant/components/ifttt/.translations/fr.json new file mode 100644 index 00000000000..d083a624d70 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/fr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Votre instance Home Assistant doit \u00eatre accessible \u00e0 partir d'Internet pour recevoir les messages IFTTT.", + "one_instance_allowed": "Une seule instance est n\u00e9cessaire." + }, + "create_entry": { + "default": "Pour envoyer des \u00e9v\u00e9nements \u00e0 Home Assistant, vous devez utiliser l'action \"Effectuer une demande Web\" \u00e0 partir de [l'applet IFTTT Webhook] ( {applet_url} ). \n\n Remplissez les informations suivantes: \n\n - URL: ` {webhook_url} ` \n - M\u00e9thode: POST \n - Type de contenu: application / json \n\n Voir [la documentation] ( {docs_url} ) pour savoir comment configurer les automatisations pour g\u00e9rer les donn\u00e9es entrantes." + }, + "step": { + "user": { + "description": "\u00cates-vous s\u00fbr de vouloir configurer IFTTT?", + "title": "Configurer l'applet IFTTT Webhook" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/sl.json b/homeassistant/components/ifttt/.translations/sl.json new file mode 100644 index 00000000000..f5cc1dc572e --- /dev/null +++ b/homeassistant/components/ifttt/.translations/sl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Va\u0161 Home Assistent mora biti dostopek prek interneta, da boste lahko prejemali IFTTT sporo\u010dila.", + "one_instance_allowed": "Potrebna je samo ena instanca." + }, + "create_entry": { + "default": "\u010ce \u017eelite poslati dogodke Home Assistent-u, boste morali uporabiti akcijo \u00bbNaredi spletno zahtevo\u00ab iz orodja [IFTTT Webhook applet] ( {applet_url} ). \n\n Izpolnite naslednje podatke: \n\n - URL: ` {webhook_url} ` \n - Metoda: POST \n - Vrsta vsebine: application/json \n\n Poglejte si [dokumentacijo] ( {docs_url} ) o tem, kako konfigurirati avtomatizacijo za obdelavo dohodnih podatkov." + }, + "step": { + "user": { + "description": "Ali ste prepri\u010dani, da \u017eelite nastaviti IFTTT?", + "title": "Nastavite IFTTT Webhook Applet" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/ca.json b/homeassistant/components/lifx/.translations/ca.json new file mode 100644 index 00000000000..b3896d49e1d --- /dev/null +++ b/homeassistant/components/lifx/.translations/ca.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No s'han trobat dispositius LIFX a la xarxa.", + "single_instance_allowed": "Nom\u00e9s \u00e9s possible una \u00fanica configuraci\u00f3 de LIFX." + }, + "step": { + "confirm": { + "description": "Voleu configurar LIFX?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/fr.json b/homeassistant/components/lifx/.translations/fr.json new file mode 100644 index 00000000000..96a264fa6b2 --- /dev/null +++ b/homeassistant/components/lifx/.translations/fr.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Aucun p\u00e9riph\u00e9rique LIFX trouv\u00e9 sur le r\u00e9seau.", + "single_instance_allowed": "Une seule configuration de LIFX est possible." + }, + "step": { + "confirm": { + "description": "Voulez-vous configurer LIFX?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/ko.json b/homeassistant/components/lifx/.translations/ko.json new file mode 100644 index 00000000000..c795c54badb --- /dev/null +++ b/homeassistant/components/lifx/.translations/ko.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "LIFX \uc7a5\uce58\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "single_instance_allowed": "\ud558\ub098\uc758 LIFX \ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + }, + "step": { + "confirm": { + "description": "LIFX \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/lb.json b/homeassistant/components/lifx/.translations/lb.json new file mode 100644 index 00000000000..2e033280e46 --- /dev/null +++ b/homeassistant/components/lifx/.translations/lb.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Keng LIFX Apparater am Netzwierk fonnt.", + "single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vun LIFX ass erlaabt." + }, + "step": { + "confirm": { + "description": "Soll LIFX konfigur\u00e9iert ginn?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/nl.json b/homeassistant/components/lifx/.translations/nl.json new file mode 100644 index 00000000000..a23502729d6 --- /dev/null +++ b/homeassistant/components/lifx/.translations/nl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Geen LIFX-apparaten gevonden op het netwerk.", + "single_instance_allowed": "Slechts een enkele configuratie van LIFX is mogelijk." + }, + "step": { + "confirm": { + "description": "Wilt u LIFX instellen?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/pl.json b/homeassistant/components/lifx/.translations/pl.json new file mode 100644 index 00000000000..f13c0b54bbd --- /dev/null +++ b/homeassistant/components/lifx/.translations/pl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nie znaleziono w sieci urz\u0105dze\u0144 LIFX.", + "single_instance_allowed": "Wymagana jest tylko jedna konfiguracja LIFX." + }, + "step": { + "confirm": { + "description": "Czy chcesz skonfigurowa\u0107 LIFX?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/ru.json b/homeassistant/components/lifx/.translations/ru.json new file mode 100644 index 00000000000..5ad351b7a90 --- /dev/null +++ b/homeassistant/components/lifx/.translations/ru.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 LIFX \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430" + }, + "step": { + "confirm": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c LIFX?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/sl.json b/homeassistant/components/lifx/.translations/sl.json new file mode 100644 index 00000000000..492bf9010dd --- /dev/null +++ b/homeassistant/components/lifx/.translations/sl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "V omre\u017eju ni najdenih naprav LIFX.", + "single_instance_allowed": "Mo\u017ena je samo ena konfiguracija LIFX-a." + }, + "step": { + "confirm": { + "description": "Ali \u017eelite nastaviti LIFX?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/zh-Hant.json b/homeassistant/components/lifx/.translations/zh-Hant.json new file mode 100644 index 00000000000..4c66f0d0133 --- /dev/null +++ b/homeassistant/components/lifx/.translations/zh-Hant.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 LIFX \u88dd\u7f6e\u3002", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 LIFX\u3002" + }, + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a LIFX\uff1f", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/fr.json b/homeassistant/components/mqtt/.translations/fr.json index 916b4fdaf39..648c2f972d7 100644 --- a/homeassistant/components/mqtt/.translations/fr.json +++ b/homeassistant/components/mqtt/.translations/fr.json @@ -10,13 +10,20 @@ "broker": { "data": { "broker": "Broker", - "discovery": "Activer la d\u00e9couverte automatique", + "discovery": "Activer la d\u00e9couverte", "password": "Mot de passe", "port": "Port", "username": "Nom d'utilisateur" }, "description": "Veuillez entrer les informations de connexion de votre broker MQTT.", "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "Activer la d\u00e9couverte" + }, + "description": "Vous voulez configurer Home Assistant pour vous connecter au broker MQTT fourni par l\u2019Add-on hass.io {addon} ?", + "title": "MQTT Broker via le module compl\u00e9mentaire Hass.io" } }, "title": "MQTT" diff --git a/homeassistant/components/mqtt/.translations/sl.json b/homeassistant/components/mqtt/.translations/sl.json index a12498ac4c2..d8d331449a2 100644 --- a/homeassistant/components/mqtt/.translations/sl.json +++ b/homeassistant/components/mqtt/.translations/sl.json @@ -17,6 +17,13 @@ }, "description": "Prosimo vnesite informacije o povezavi va\u0161ega MQTT posrednika.", "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "Omogo\u010di odkrivanje" + }, + "description": "\u017delite konfigurirati Home Assistent-a za povezavo s posrednikom MQTT, ki ga ponuja hass.io add-on {addon} ?", + "title": "MQTT Broker prek dodatka Hass.io" } }, "title": "MQTT" diff --git a/homeassistant/components/smhi/.translations/ca.json b/homeassistant/components/smhi/.translations/ca.json new file mode 100644 index 00000000000..23b6a2934f0 --- /dev/null +++ b/homeassistant/components/smhi/.translations/ca.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "El nom ja existeix", + "wrong_location": "Ubicaci\u00f3 nom\u00e9s a Su\u00e8cia" + }, + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nom" + }, + "title": "Ubicaci\u00f3 a Su\u00e8cia" + } + }, + "title": "Servei meteorol\u00f2gic suec (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/en.json b/homeassistant/components/smhi/.translations/en.json index 1a995a64a32..6aa256d87d4 100644 --- a/homeassistant/components/smhi/.translations/en.json +++ b/homeassistant/components/smhi/.translations/en.json @@ -2,7 +2,7 @@ "config": { "error": { "name_exists": "Name already exists", - "wrong_location": "Location in Sweden only" + "wrong_location": "Location Sweden only" }, "step": { "user": { diff --git a/homeassistant/components/smhi/.translations/ko.json b/homeassistant/components/smhi/.translations/ko.json new file mode 100644 index 00000000000..f307fa1ad23 --- /dev/null +++ b/homeassistant/components/smhi/.translations/ko.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "\uc774\ub984\uc774 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4", + "wrong_location": "\uc2a4\uc6e8\ub374 \uc9c0\uc5ed \uc804\uc6a9\uc785\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "latitude": "\uc704\ub3c4", + "longitude": "\uacbd\ub3c4", + "name": "\uc774\ub984" + }, + "title": "\uc2a4\uc6e8\ub374 \uc9c0\uc5ed \uc704\uce58" + } + }, + "title": "\uc2a4\uc6e8\ub374 \uae30\uc0c1 \uc11c\ube44\uc2a4 (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/lb.json b/homeassistant/components/smhi/.translations/lb.json new file mode 100644 index 00000000000..46abfd2677f --- /dev/null +++ b/homeassistant/components/smhi/.translations/lb.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Numm g\u00ebtt et schonn", + "wrong_location": "N\u00ebmmen Uertschaften an Schweden" + }, + "step": { + "user": { + "data": { + "latitude": "Breedegrad", + "longitude": "L\u00e4ngegrad", + "name": "Numm" + }, + "title": "Uertschaft an Schweden" + } + }, + "title": "Schwedeschen Wieder D\u00e9ngscht (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/nl.json b/homeassistant/components/smhi/.translations/nl.json new file mode 100644 index 00000000000..88edc116e74 --- /dev/null +++ b/homeassistant/components/smhi/.translations/nl.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Naam bestaat al", + "wrong_location": "Locatie alleen Zweden" + }, + "step": { + "user": { + "data": { + "latitude": "Breedtegraad", + "longitude": "Lengtegraad", + "name": "Naam" + }, + "title": "Locatie in Zweden" + } + }, + "title": "Zweedse weerdienst (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/pl.json b/homeassistant/components/smhi/.translations/pl.json new file mode 100644 index 00000000000..21973cd54b6 --- /dev/null +++ b/homeassistant/components/smhi/.translations/pl.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Nazwa ju\u017c istnieje", + "wrong_location": "Lokalizacja w Szwecji" + }, + "step": { + "user": { + "data": { + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna", + "name": "Nazwa" + }, + "title": "Lokalizacja w Szwecji" + } + }, + "title": "Szwedzka us\u0142uga pogodowa (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/ru.json b/homeassistant/components/smhi/.translations/ru.json new file mode 100644 index 00000000000..012bb74c568 --- /dev/null +++ b/homeassistant/components/smhi/.translations/ru.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "\u0418\u043c\u044f \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442", + "wrong_location": "\u0422\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0428\u0432\u0435\u0446\u0438\u0438" + }, + "step": { + "user": { + "data": { + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "title": "\u041c\u0435\u0441\u0442\u043e\u043d\u0430\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435 \u0432 \u0428\u0432\u0435\u0446\u0438\u0438" + } + }, + "title": "\u0428\u0432\u0435\u0434\u0441\u043a\u0430\u044f \u043c\u0435\u0442\u0435\u043e\u0440\u043e\u043b\u043e\u0433\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u0441\u043b\u0443\u0436\u0431\u0430 (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/sl.json b/homeassistant/components/smhi/.translations/sl.json new file mode 100644 index 00000000000..94c3750f06f --- /dev/null +++ b/homeassistant/components/smhi/.translations/sl.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Ime \u017ee obstaja", + "wrong_location": "Lokacija le na \u0160vedskem" + }, + "step": { + "user": { + "data": { + "latitude": "Zemljepisna \u0161irina", + "longitude": "Zemljepisna dol\u017eina", + "name": "Ime" + }, + "title": "Lokacija na \u0160vedskem" + } + }, + "title": "\u0160vedska vremenska slu\u017eba (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/zh-Hant.json b/homeassistant/components/smhi/.translations/zh-Hant.json new file mode 100644 index 00000000000..b982baac2f8 --- /dev/null +++ b/homeassistant/components/smhi/.translations/zh-Hant.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "\u8a72\u540d\u7a31\u5df2\u5b58\u5728", + "wrong_location": "\u50c5\u9650\u745e\u5178\u5ea7\u6a19" + }, + "step": { + "user": { + "data": { + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6", + "name": "\u540d\u7a31" + }, + "title": "\u745e\u5178\u5ea7\u6a19" + } + }, + "title": "\u745e\u5178\u6c23\u8c61\u670d\u52d9\uff08SMHI\uff09" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/ko.json b/homeassistant/components/sonos/.translations/ko.json index 89933f57425..0b2e2a1875c 100644 --- a/homeassistant/components/sonos/.translations/ko.json +++ b/homeassistant/components/sonos/.translations/ko.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Sonos\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Sonos \ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Sonos" } }, diff --git a/homeassistant/components/sonos/.translations/pl.json b/homeassistant/components/sonos/.translations/pl.json index 2a0c526b9a6..a45cb4e9824 100644 --- a/homeassistant/components/sonos/.translations/pl.json +++ b/homeassistant/components/sonos/.translations/pl.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Chcesz skonfigurowa\u0107 Sonos?", + "description": "Czy chcesz skonfigurowa\u0107 Sonos?", "title": "Sonos" } }, diff --git a/homeassistant/components/sonos/.translations/zh-Hant.json b/homeassistant/components/sonos/.translations/zh-Hant.json index c6fb13c3605..520a29b7602 100644 --- a/homeassistant/components/sonos/.translations/zh-Hant.json +++ b/homeassistant/components/sonos/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Sonos \u8a2d\u5099\u3002", + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Sonos \u88dd\u7f6e\u3002", "single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21 Sonos \u5373\u53ef\u3002" }, "step": { diff --git a/homeassistant/components/upnp/.translations/fr.json b/homeassistant/components/upnp/.translations/fr.json new file mode 100644 index 00000000000..3eac9577890 --- /dev/null +++ b/homeassistant/components/upnp/.translations/fr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP / IGD est d\u00e9j\u00e0 configur\u00e9", + "no_devices_discovered": "Aucun UPnP / IGD d\u00e9couvert", + "no_sensors_or_port_mapping": "Activer au moins les capteurs ou la cartographie des ports" + }, + "step": { + "init": { + "title": "UPnP / IGD" + }, + "user": { + "data": { + "enable_port_mapping": "Activer le mappage de port pour Home Assistant", + "enable_sensors": "Ajouter des capteurs de trafic", + "igd": "UPnP / IGD" + }, + "title": "Options de configuration pour UPnP / IGD" + } + }, + "title": "UPnP / IGD" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/nl.json b/homeassistant/components/upnp/.translations/nl.json index c6356709170..647eb647f24 100644 --- a/homeassistant/components/upnp/.translations/nl.json +++ b/homeassistant/components/upnp/.translations/nl.json @@ -6,10 +6,14 @@ "no_sensors_or_port_mapping": "Schakel ten minste sensoren of poorttoewijzing in" }, "step": { + "init": { + "title": "UPnP/IGD" + }, "user": { "data": { "enable_port_mapping": "Poorttoewijzing voor Home Assistant inschakelen", - "enable_sensors": "Voeg verkeerssensoren toe" + "enable_sensors": "Voeg verkeerssensoren toe", + "igd": "UPnP/IGD" }, "title": "Configuratiemogelijkheden voor de UPnP/IGD" } diff --git a/homeassistant/components/upnp/.translations/sl.json b/homeassistant/components/upnp/.translations/sl.json new file mode 100644 index 00000000000..20debe7f09a --- /dev/null +++ b/homeassistant/components/upnp/.translations/sl.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP/IGD je \u017ee konfiguriran", + "no_devices_discovered": "Ni odkritih UPnP/IGD naprav", + "no_sensors_or_port_mapping": "Omogo\u010dite vsaj senzorje ali preslikavo vrat (port mapping)" + }, + "error": { + "few": "nekaj", + "one": "ena", + "other": "ve\u010d", + "two": "dve" + }, + "step": { + "init": { + "title": "UPnP/IGD" + }, + "user": { + "data": { + "enable_port_mapping": "Omogo\u010dajo preslikavo vrat (port mapping) za Home Assistent-a", + "enable_sensors": "Dodaj prometne senzorje", + "igd": "UPnP/IGD" + }, + "title": "Mo\u017enosti konfiguracije za UPnP/IGD" + } + }, + "title": "UPnP/IGD" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/ca.json b/homeassistant/components/zwave/.translations/ca.json new file mode 100644 index 00000000000..8c39ca58201 --- /dev/null +++ b/homeassistant/components/zwave/.translations/ca.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave ja est\u00e0 configurat", + "one_instance_only": "El component nom\u00e9s admet una inst\u00e0ncia de Z-Wave" + }, + "step": { + "user": { + "data": { + "network_key": "Clau de xarxa (deixeu-ho en blanc per generar-la autom\u00e0ticament)" + }, + "title": "Configureu Z-Wave" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/ko.json b/homeassistant/components/zwave/.translations/ko.json new file mode 100644 index 00000000000..43103de3d51 --- /dev/null +++ b/homeassistant/components/zwave/.translations/ko.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave \uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "one_instance_only": "\uad6c\uc131\uc694\uc18c\ub294 \ud558\ub098\uc758 Z-Wave \uc778\uc2a4\ud134\uc2a4\ub9cc \uc9c0\uc6d0\ud569\ub2c8\ub2e4" + }, + "error": { + "option_error": "Z-Wave \uc720\ud6a8\uc131 \uac80\uc0ac\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4. USB \uc2a4\ud2f1\uc758 \uacbd\ub85c\uac00 \uc815\ud655\ud569\ub2c8\uae4c?" + }, + "step": { + "user": { + "data": { + "network_key": "\ub124\ud2b8\uc6cc\ud06c \ud0a4 (\uacf5\ub780\uc73c\ub85c \ube44\uc6cc\ub450\uba74 \uc790\ub3d9 \uc0dd\uc131\ud569\ub2c8\ub2e4)", + "usb_path": "USB \uacbd\ub85c" + }, + "description": "\uad6c\uc131 \ubcc0\uc218\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 https://www.home-assistant.io/docs/z-wave/installation/ \uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694", + "title": "Z-Wave \uc124\uc815" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/lb.json b/homeassistant/components/zwave/.translations/lb.json new file mode 100644 index 00000000000..84b6d8aa67d --- /dev/null +++ b/homeassistant/components/zwave/.translations/lb.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave ass scho konfigur\u00e9iert", + "one_instance_only": "Komponent \u00ebnnerst\u00ebtzt n\u00ebmmen eng Z-Wave Instanz" + }, + "error": { + "option_error": "Z-Wave Validatioun net g\u00eblteg. Ass de Pad zum USB Stick richteg?" + }, + "step": { + "user": { + "data": { + "network_key": "Netzwierk Schl\u00ebssel (eidel loossen fir een automatesch z'erstellen)", + "usb_path": "USB Pad" + }, + "description": "Lies op https://www.home-assistant.io/docs/z-wave/installation/ fir weider Informatiounen iwwert d'Konfiguratioun vun den Variabelen", + "title": "Z-Wave konfigur\u00e9ieren" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/nl.json b/homeassistant/components/zwave/.translations/nl.json new file mode 100644 index 00000000000..0b700b895fd --- /dev/null +++ b/homeassistant/components/zwave/.translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave is al geconfigureerd", + "one_instance_only": "Component ondersteunt slechts \u00e9\u00e9n Z-Wave-instantie" + }, + "error": { + "option_error": "Z-Wave-validatie mislukt. Is het pad naar de USB-stick correct?" + }, + "step": { + "user": { + "data": { + "network_key": "Netwerksleutel (laat leeg om automatisch te genereren)", + "usb_path": "USB-pad" + }, + "description": "Zie https://www.home-assistant.io/docs/z-wave/installation/ voor informatie over de configuratievariabelen", + "title": "Stel Z-Wave in" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/ru.json b/homeassistant/components/zwave/.translations/ru.json new file mode 100644 index 00000000000..457bfd3baa8 --- /dev/null +++ b/homeassistant/components/zwave/.translations/ru.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 Z-Wave" + }, + "error": { + "option_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 Z-Wave. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u0443\u0442\u044c \u043a USB-\u043d\u0430\u043a\u043e\u043f\u0438\u0442\u0435\u043b\u044e." + }, + "step": { + "user": { + "data": { + "network_key": "\u041a\u043b\u044e\u0447 \u0441\u0435\u0442\u0438 (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u0435 \u043f\u0443\u0441\u0442\u044b\u043c \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0439 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438)", + "usb_path": "\u041f\u0443\u0442\u044c \u043a USB" + }, + "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439](https://www.home-assistant.io/docs/z-wave/installation/) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0445 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Z-Wave" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file From 9d4dbd7d9732c703891d0c38c08d0c16551a7382 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 10 Oct 2018 16:02:03 +0200 Subject: [PATCH 086/265] ABC config entries (#17309) --- homeassistant/config_entries.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 053aa079617..e4719b3ed78 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -136,8 +136,8 @@ HANDLERS = Registry() # Components that have config flows. In future we will auto-generate this list. FLOWS = [ 'cast', - 'hangouts', 'deconz', + 'hangouts', 'homematicip_cloud', 'hue', 'ifttt', @@ -149,8 +149,8 @@ FLOWS = [ 'smhi', 'sonos', 'tradfri', - 'zone', 'upnp', + 'zone', 'zwave' ] From 99c6622ee2d054f3942b4b2520174d42ab880055 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Wed, 10 Oct 2018 17:59:55 +0200 Subject: [PATCH 087/265] Add direction configuration (#17308) --- homeassistant/components/sensor/rmvtransport.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sensor/rmvtransport.py b/homeassistant/components/sensor/rmvtransport.py index 79ec8c7a5e7..f9bd65c1a74 100644 --- a/homeassistant/components/sensor/rmvtransport.py +++ b/homeassistant/components/sensor/rmvtransport.py @@ -24,7 +24,7 @@ CONF_NEXT_DEPARTURE = 'next_departure' CONF_STATION = 'station' CONF_DESTINATIONS = 'destinations' -CONF_DIRECTIONS = 'directions' +CONF_DIRECTION = 'direction' CONF_LINES = 'lines' CONF_PRODUCTS = 'products' CONF_TIME_OFFSET = 'time_offset' @@ -57,8 +57,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_STATION): cv.string, vol.Optional(CONF_DESTINATIONS, default=[]): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_DIRECTIONS, default=[]): - vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_DIRECTION): cv.string, vol.Optional(CONF_LINES, default=[]): vol.All(cv.ensure_list, [cv.positive_int, cv.string]), vol.Optional(CONF_PRODUCTS, default=VALID_PRODUCTS): @@ -84,7 +83,7 @@ async def async_setup_platform(hass, config, async_add_entities, session, next_departure[CONF_STATION], next_departure.get(CONF_DESTINATIONS), - next_departure.get(CONF_DIRECTIONS), + next_departure.get(CONF_DIRECTION), next_departure.get(CONF_LINES), next_departure.get(CONF_PRODUCTS), next_departure.get(CONF_TIME_OFFSET), @@ -97,14 +96,14 @@ async def async_setup_platform(hass, config, async_add_entities, class RMVDepartureSensor(Entity): """Implementation of an RMV departure sensor.""" - def __init__(self, session, station, destinations, directions, lines, + def __init__(self, session, station, destinations, direction, lines, products, time_offset, max_journeys, name, timeout): """Initialize the sensor.""" self._station = station self._name = name self._state = None self.data = RMVDepartureData(session, station, destinations, - directions, lines, products, time_offset, + direction, lines, products, time_offset, max_journeys, timeout) self._icon = ICONS[None] @@ -167,7 +166,7 @@ class RMVDepartureSensor(Entity): class RMVDepartureData: """Pull data from the opendata.rmv.de web page.""" - def __init__(self, session, station_id, destinations, directions, lines, + def __init__(self, session, station_id, destinations, direction, lines, products, time_offset, max_journeys, timeout): """Initialize the sensor.""" from RMVtransport import RMVtransport @@ -175,7 +174,7 @@ class RMVDepartureData: self.station = None self._station_id = station_id self._destinations = destinations - self._directions = directions + self._direction = direction self._lines = lines self._products = products self._time_offset = time_offset @@ -189,6 +188,7 @@ class RMVDepartureData: try: _data = await self.rmv.get_departures(self._station_id, products=self._products, + directionId=self._direction, maxJourneys=50) except ValueError: self.departures = [] From c434ad6af57d758e2c36cd7f31eb2d4c6222a720 Mon Sep 17 00:00:00 2001 From: zhumuht <40521367+zhumuht@users.noreply.github.com> Date: Thu, 11 Oct 2018 00:56:00 +0800 Subject: [PATCH 088/265] fix_broadlink_sp2_show_energy (#17271) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix_broadlink_sp2_show_energy Signed-off-by: 朱海涛 * fix_broadlink_sp2_show_energy Signed-off-by: 朱海涛 * fix_broadlink_sp2_show_energy Signed-off-by: 朱海涛 * fix_broadlink_sp2_show_energy Signed-off-by: 朱海涛 * fix_broadlink_sp2_show_energy Signed-off-by: 朱海涛 --- homeassistant/components/switch/broadlink.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/homeassistant/components/switch/broadlink.py b/homeassistant/components/switch/broadlink.py index 0562292acec..685402611a0 100644 --- a/homeassistant/components/switch/broadlink.py +++ b/homeassistant/components/switch/broadlink.py @@ -260,6 +260,7 @@ class BroadlinkSP1Switch(BroadlinkRMSwitch): super().__init__(friendly_name, friendly_name, device, None, None) self._command_on = 1 self._command_off = 0 + self._load_power = None def _sendpacket(self, packet, retry=2): """Send packet to device.""" @@ -288,6 +289,14 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch): """Return the polling state.""" return True + @property + def current_power_w(self): + """Return the current power usage in Watt.""" + try: + return round(self._load_power, 2) + except (ValueError, TypeError): + return None + def update(self): """Synchronize state with switch.""" self._update() @@ -296,6 +305,7 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch): """Update the state of the device.""" try: state = self._device.check_power() + load_power = self._device.get_energy() except (socket.timeout, ValueError) as error: if retry < 1: _LOGGER.error(error) @@ -306,6 +316,7 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch): if state is None and retry > 0: return self._update(retry-1) self._state = state + self._load_power = load_power class BroadlinkMP1Slot(BroadlinkRMSwitch): From 93e3596e5a2fbb47e416c5451f6c048795aabf26 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Wed, 10 Oct 2018 13:05:19 -0400 Subject: [PATCH 089/265] Add Elk-M1 switch and scene platforms (#17256) * Add Elk-M1 switch platform. * Fix travis error. * Fix very annoying lint error. * Fix PR comments. * Fix comment. * Fix lint errors. * Fix PR comments. * Fix PR Apologize. Going too fast. You should not have to find those. --- .../components/alarm_control_panel/elkm1.py | 2 +- homeassistant/components/elkm1/__init__.py | 6 +-- homeassistant/components/light/elkm1.py | 2 +- homeassistant/components/scene/elkm1.py | 31 ++++++++++++++ homeassistant/components/switch/elkm1.py | 40 +++++++++++++++++++ 5 files changed, 76 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/scene/elkm1.py create mode 100644 homeassistant/components/switch/elkm1.py diff --git a/homeassistant/components/alarm_control_panel/elkm1.py b/homeassistant/components/alarm_control_panel/elkm1.py index a01898ac959..7b8d2e4ac42 100644 --- a/homeassistant/components/alarm_control_panel/elkm1.py +++ b/homeassistant/components/alarm_control_panel/elkm1.py @@ -89,7 +89,7 @@ class ElkArea(ElkEntity, alarm.AlarmControlPanel): def __init__(self, element, elk, elk_data): """Initialize Area as Alarm Control Panel.""" - super().__init__('alarm_control_panel', element, elk, elk_data) + super().__init__(element, elk, elk_data) self._changed_by_entity_id = '' self._state = None diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 2dd85e02215..da322232bdf 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -35,7 +35,7 @@ CONF_ENABLED = 'enabled' _LOGGER = logging.getLogger(__name__) -SUPPORTED_DOMAINS = ['alarm_control_panel', 'light'] +SUPPORTED_DOMAINS = ['alarm_control_panel', 'light', 'scene', 'switch'] def _host_validator(config): @@ -157,7 +157,7 @@ def create_elk_entities(hass, elk_elements, element_type, class_, entities): class ElkEntity(Entity): """Base class for all Elk entities.""" - def __init__(self, platform, element, elk, elk_data): + def __init__(self, element, elk, elk_data): """Initialize the base of all Elk devices.""" self._elk = elk self._element = element @@ -197,7 +197,7 @@ class ElkEntity(Entity): return attrs def _element_changed(self, element, changeset): - raise NotImplementedError() + pass @callback def _element_callback(self, element, changeset): diff --git a/homeassistant/components/light/elkm1.py b/homeassistant/components/light/elkm1.py index c6cb138877b..707aedbb161 100644 --- a/homeassistant/components/light/elkm1.py +++ b/homeassistant/components/light/elkm1.py @@ -28,7 +28,7 @@ class ElkLight(ElkEntity, Light): def __init__(self, element, elk, elk_data): """Initialize light.""" - super().__init__('light', element, elk, elk_data) + super().__init__(element, elk, elk_data) self._brightness = self._element.status @property diff --git a/homeassistant/components/scene/elkm1.py b/homeassistant/components/scene/elkm1.py new file mode 100644 index 00000000000..47dd17a56ae --- /dev/null +++ b/homeassistant/components/scene/elkm1.py @@ -0,0 +1,31 @@ +""" +Support for control of ElkM1 tasks ("macros"). + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/scene.elkm1/ +""" + + +from homeassistant.components.elkm1 import ( + DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities) +from homeassistant.components.scene import Scene + +DEPENDENCIES = [ELK_DOMAIN] + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Create the Elk-M1 scene platform.""" + if discovery_info is None: + return + elk = hass.data[ELK_DOMAIN]['elk'] + entities = create_elk_entities(hass, elk.tasks, 'task', ElkTask, []) + async_add_entities(entities, True) + + +class ElkTask(ElkEntity, Scene): + """Elk-M1 task as scene.""" + + async def async_activate(self): + """Activate the task.""" + self._element.activate() diff --git a/homeassistant/components/switch/elkm1.py b/homeassistant/components/switch/elkm1.py new file mode 100644 index 00000000000..a838e1b948e --- /dev/null +++ b/homeassistant/components/switch/elkm1.py @@ -0,0 +1,40 @@ +""" +Support for control of ElkM1 outputs (relays). + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.elkm1/ +""" + + +from homeassistant.components.elkm1 import ( + DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities) +from homeassistant.components.switch import SwitchDevice + +DEPENDENCIES = [ELK_DOMAIN] + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Create the Elk-M1 switch platform.""" + if discovery_info is None: + return + elk = hass.data[ELK_DOMAIN]['elk'] + entities = create_elk_entities(hass, elk.outputs, 'output', ElkOutput, []) + async_add_entities(entities, True) + + +class ElkOutput(ElkEntity, SwitchDevice): + """Elk output as switch.""" + + @property + def is_on(self) -> bool: + """Get the current output status.""" + return self._element.output_on + + async def async_turn_on(self, **kwargs): + """Turn on the output.""" + self._element.turn_on(0) + + async def async_turn_off(self, **kwargs): + """Turn off the output.""" + self._element.turn_off() From 8d9da4e7b921fe094a452c5db49574517e285fa7 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 10 Oct 2018 23:52:45 +0200 Subject: [PATCH 090/265] Upgrade construct to 2.9.45 (#16362) --- homeassistant/components/climate/eq3btsmart.py | 2 +- .../components/device_tracker/xiaomi_miio.py | 14 +++++++------- homeassistant/components/fan/xiaomi_miio.py | 12 ++++++------ homeassistant/components/light/xiaomi_miio.py | 17 ++++++++--------- homeassistant/components/remote/xiaomi_miio.py | 2 +- .../components/sensor/eddystone_temperature.py | 16 ++++++++-------- homeassistant/components/sensor/xiaomi_miio.py | 10 +++++----- homeassistant/components/switch/xiaomi_miio.py | 14 +++++++------- homeassistant/components/vacuum/xiaomi_miio.py | 2 +- requirements_all.txt | 2 +- 10 files changed, 45 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/climate/eq3btsmart.py b/homeassistant/components/climate/eq3btsmart.py index eae7bc1d9b9..b88500f7fb2 100644 --- a/homeassistant/components/climate/eq3btsmart.py +++ b/homeassistant/components/climate/eq3btsmart.py @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_MAC, CONF_DEVICES, TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_HALVES) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-eq3bt==0.1.9', 'construct==2.9.41'] +REQUIREMENTS = ['python-eq3bt==0.1.9', 'construct==2.9.45'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/device_tracker/xiaomi_miio.py b/homeassistant/components/device_tracker/xiaomi_miio.py index 060849a3268..d8767a4cd46 100644 --- a/homeassistant/components/device_tracker/xiaomi_miio.py +++ b/homeassistant/components/device_tracker/xiaomi_miio.py @@ -8,10 +8,12 @@ import logging import voluptuous as vol +from homeassistant.components.device_tracker import ( + DOMAIN, PLATFORM_SCHEMA, DeviceScanner) +from homeassistant.const import CONF_HOST, CONF_TOKEN import homeassistant.helpers.config_validation as cv -from homeassistant.components.device_tracker import (DOMAIN, PLATFORM_SCHEMA, - DeviceScanner) -from homeassistant.const import (CONF_HOST, CONF_TOKEN) + +REQUIREMENTS = ['python-miio==0.4.2', 'construct==2.9.45'] _LOGGER = logging.getLogger(__name__) @@ -20,8 +22,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)), }) -REQUIREMENTS = ['python-miio==0.4.2', 'construct==2.9.41'] - def get_scanner(hass, config): """Return a Xiaomi MiIO device scanner.""" @@ -56,7 +56,7 @@ class XiaomiMiioDeviceScanner(DeviceScanner): self.device = device async def async_scan_devices(self): - """Scan for devices and return a list containing found device ids.""" + """Scan for devices and return a list containing found device IDs.""" from miio import DeviceException devices = [] @@ -68,7 +68,7 @@ class XiaomiMiioDeviceScanner(DeviceScanner): devices.append(device['mac']) except DeviceException as ex: - _LOGGER.error("Got exception while fetching the state: %s", ex) + _LOGGER.error("Unable to fetch the state: %s", ex) return devices diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py index fdbf06818b9..67a12442629 100644 --- a/homeassistant/components/fan/xiaomi_miio.py +++ b/homeassistant/components/fan/xiaomi_miio.py @@ -11,13 +11,15 @@ import logging import voluptuous as vol -from homeassistant.components.fan import (FanEntity, PLATFORM_SCHEMA, - SUPPORT_SET_SPEED, DOMAIN, ) -from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN, - ATTR_ENTITY_ID, ) +from homeassistant.components.fan import ( + DOMAIN, PLATFORM_SCHEMA, SUPPORT_SET_SPEED, FanEntity) +from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +REQUIREMENTS = ['python-miio==0.4.2', 'construct==2.9.45'] + _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'Xiaomi Miio Device' @@ -49,8 +51,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'zhimi.humidifier.ca1']), }) -REQUIREMENTS = ['python-miio==0.4.2', 'construct==2.9.41'] - ATTR_MODEL = 'model' # Air Purifier diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py index ce1e504b54c..26a63b2f16b 100644 --- a/homeassistant/components/light/xiaomi_miio.py +++ b/homeassistant/components/light/xiaomi_miio.py @@ -5,23 +5,24 @@ For more details about this platform, please refer to the documentation https://home-assistant.io/components/light.xiaomi_miio/ """ import asyncio +import datetime +from datetime import timedelta from functools import partial import logging from math import ceil -from datetime import timedelta -import datetime import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( - PLATFORM_SCHEMA, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, - ATTR_COLOR_TEMP, SUPPORT_COLOR_TEMP, Light, ATTR_ENTITY_ID, DOMAIN, ) - -from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN, ) + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_ENTITY_ID, DOMAIN, PLATFORM_SCHEMA, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, Light) +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv from homeassistant.util import dt +REQUIREMENTS = ['python-miio==0.4.2', 'construct==2.9.45'] + _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'Xiaomi Philips Light' @@ -43,8 +44,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'philips.light.mono1']), }) -REQUIREMENTS = ['python-miio==0.4.2', 'construct==2.9.41'] - # The light does not accept cct values < 1 CCT_MIN = 1 CCT_MAX = 100 diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py index 6d82da9e9fd..5a914fc3652 100644 --- a/homeassistant/components/remote/xiaomi_miio.py +++ b/homeassistant/components/remote/xiaomi_miio.py @@ -22,7 +22,7 @@ from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.util.dt import utcnow -REQUIREMENTS = ['python-miio==0.4.2', 'construct==2.9.41'] +REQUIREMENTS = ['python-miio==0.4.2', 'construct==2.9.45'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/eddystone_temperature.py b/homeassistant/components/sensor/eddystone_temperature.py index ef362e17aa6..ae3d498d30c 100644 --- a/homeassistant/components/sensor/eddystone_temperature.py +++ b/homeassistant/components/sensor/eddystone_temperature.py @@ -11,14 +11,14 @@ import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, TEMP_CELSIUS, STATE_UNKNOWN, EVENT_HOMEASSISTANT_STOP, - EVENT_HOMEASSISTANT_START) + CONF_NAME, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, + STATE_UNKNOWN, TEMP_CELSIUS) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['beacontools[scan]==1.2.3', 'construct==2.9.41'] +REQUIREMENTS = ['beacontools[scan]==1.2.3', 'construct==2.9.45'] _LOGGER = logging.getLogger(__name__) @@ -43,12 +43,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Validate configuration, create devices and start monitoring thread.""" bt_device_id = config.get("bt_device_id") - beacons = config.get("beacons") + beacons = config.get(CONF_BEACONS) devices = [] for dev_name, properties in beacons.items(): - namespace = get_from_conf(properties, "namespace", 20) - instance = get_from_conf(properties, "instance", 12) + namespace = get_from_conf(properties, CONF_NAMESPACE, 20) + instance = get_from_conf(properties, CONF_INSTANCE, 12) name = properties.get(CONF_NAME, dev_name) if instance is None or namespace is None: diff --git a/homeassistant/components/sensor/xiaomi_miio.py b/homeassistant/components/sensor/xiaomi_miio.py index 4cdf166cf99..dd0785120ea 100644 --- a/homeassistant/components/sensor/xiaomi_miio.py +++ b/homeassistant/components/sensor/xiaomi_miio.py @@ -8,11 +8,13 @@ import logging import voluptuous as vol +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN) -from homeassistant.exceptions import PlatformNotReady + +REQUIREMENTS = ['python-miio==0.4.2', 'construct==2.9.45'] _LOGGER = logging.getLogger(__name__) @@ -25,8 +27,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) -REQUIREMENTS = ['python-miio==0.4.2', 'construct==2.9.41'] - ATTR_POWER = 'power' ATTR_CHARGING = 'charging' ATTR_BATTERY_LEVEL = 'battery_level' diff --git a/homeassistant/components/switch/xiaomi_miio.py b/homeassistant/components/switch/xiaomi_miio.py index 2ac009f4334..0152615109c 100644 --- a/homeassistant/components/switch/xiaomi_miio.py +++ b/homeassistant/components/switch/xiaomi_miio.py @@ -10,12 +10,14 @@ import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA, - DOMAIN, ) -from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN, - ATTR_ENTITY_ID, ) +from homeassistant.components.switch import ( + DOMAIN, PLATFORM_SCHEMA, SwitchDevice) +from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN) from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['python-miio==0.4.2', 'construct==2.9.45'] _LOGGER = logging.getLogger(__name__) @@ -39,8 +41,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'chuangmi.plug.v3']), }) -REQUIREMENTS = ['python-miio==0.4.2', 'construct==2.9.41'] - ATTR_POWER = 'power' ATTR_TEMPERATURE = 'temperature' ATTR_LOAD_POWER = 'load_power' diff --git a/homeassistant/components/vacuum/xiaomi_miio.py b/homeassistant/components/vacuum/xiaomi_miio.py index 97a285f90f5..d1d45f5ecd2 100644 --- a/homeassistant/components/vacuum/xiaomi_miio.py +++ b/homeassistant/components/vacuum/xiaomi_miio.py @@ -21,7 +21,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-miio==0.4.2', 'construct==2.9.41'] +REQUIREMENTS = ['python-miio==0.4.2', 'construct==2.9.45'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 2ef85e05166..e0118145c4a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -253,7 +253,7 @@ concord232==0.15 # homeassistant.components.sensor.xiaomi_miio # homeassistant.components.switch.xiaomi_miio # homeassistant.components.vacuum.xiaomi_miio -construct==2.9.41 +construct==2.9.45 # homeassistant.scripts.credstash # credstash==1.14.0 From 3f87d413813de84935ea67b5212c55348524447f Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 11 Oct 2018 01:02:00 +0200 Subject: [PATCH 091/265] Fix auth for hass.io (#17318) * Update test_auth.py * Update auth.py * Update test_auth.py --- homeassistant/components/hassio/auth.py | 5 ++--- tests/components/hassio/test_auth.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/hassio/auth.py b/homeassistant/components/hassio/auth.py index 73ef5aa29cc..951110271d4 100644 --- a/homeassistant/components/hassio/auth.py +++ b/homeassistant/components/hassio/auth.py @@ -4,8 +4,7 @@ from ipaddress import ip_address import os from aiohttp import web -from aiohttp.web_exceptions import ( - HTTPForbidden, HTTPNotFound, HTTPUnauthorized) +from aiohttp.web_exceptions import HTTPForbidden, HTTPNotFound import voluptuous as vol from homeassistant.core import callback @@ -72,4 +71,4 @@ class HassIOAuth(HomeAssistantView): try: await provider.async_validate_login(username, password) except HomeAssistantError: - raise HTTPUnauthorized() from None + raise HTTPForbidden() from None diff --git a/tests/components/hassio/test_auth.py b/tests/components/hassio/test_auth.py index 50d9488a19c..b3a6ae223f9 100644 --- a/tests/components/hassio/test_auth.py +++ b/tests/components/hassio/test_auth.py @@ -50,7 +50,7 @@ async def test_login_error(hass, hassio_client): ) # Check we got right response - assert resp.status == 401 + assert resp.status == 403 mock_login.assert_called_with("test", "123456") From 7e8973a3159a1f87529ba88fbc2b4178e35fa0d0 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 11 Oct 2018 07:43:15 +0200 Subject: [PATCH 092/265] Update file header (#17317) --- homeassistant/components/sensor/gitlab_ci.py | 64 +++++++++++--------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/sensor/gitlab_ci.py b/homeassistant/components/sensor/gitlab_ci.py index ceb5f75cace..1e55a7d6997 100644 --- a/homeassistant/components/sensor/gitlab_ci.py +++ b/homeassistant/components/sensor/gitlab_ci.py @@ -1,4 +1,9 @@ -"""Module for retrieving latest GitLab CI job information.""" +""" +Sensor for retrieving latest GitLab CI job information. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.gitlab_ci/ +""" from datetime import timedelta import logging @@ -11,38 +16,41 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -CONF_GITLAB_ID = 'gitlab_id' -CONF_ATTRIBUTION = "Information provided by https://gitlab.com/" - -ICON_HAPPY = 'mdi:emoticon-happy' -ICON_SAD = 'mdi:emoticon-happy' -ICON_OTHER = 'mdi:git' - -ATTR_BUILD_ID = 'build id' -ATTR_BUILD_STATUS = 'build_status' -ATTR_BUILD_STARTED = 'build_started' -ATTR_BUILD_FINISHED = 'build_finished' -ATTR_BUILD_DURATION = 'build_duration' -ATTR_BUILD_COMMIT_ID = 'commit id' -ATTR_BUILD_COMMIT_DATE = 'commit date' -ATTR_BUILD_BRANCH = 'build branch' - -SCAN_INTERVAL = timedelta(seconds=300) +REQUIREMENTS = ['python-gitlab==1.6.0'] _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_TOKEN): cv.string, - vol.Required(CONF_GITLAB_ID): cv.string, - vol.Optional(CONF_NAME, default='GitLab CI Status'): cv.string, - vol.Optional(CONF_URL, default='https://gitlab.com'): cv.string -}) +ATTR_BUILD_BRANCH = 'build branch' +ATTR_BUILD_COMMIT_DATE = 'commit date' +ATTR_BUILD_COMMIT_ID = 'commit id' +ATTR_BUILD_DURATION = 'build_duration' +ATTR_BUILD_FINISHED = 'build_finished' +ATTR_BUILD_ID = 'build id' +ATTR_BUILD_STARTED = 'build_started' +ATTR_BUILD_STATUS = 'build_status' -REQUIREMENTS = ['python-gitlab==1.6.0'] +CONF_ATTRIBUTION = "Information provided by https://gitlab.com/" +CONF_GITLAB_ID = 'gitlab_id' + +DEFAULT_NAME = 'GitLab CI Status' +DEFAULT_URL = 'https://gitlab.com' + +ICON_HAPPY = 'mdi:emoticon-happy' +ICON_OTHER = 'mdi:git' +ICON_SAD = 'mdi:emoticon-happy' + +SCAN_INTERVAL = timedelta(seconds=300) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_GITLAB_ID): cv.string, + vol.Required(CONF_TOKEN): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_URL, default=DEFAULT_URL): cv.string +}) def setup_platform(hass, config, add_entities, discovery_info=None): - """Sensor platform setup.""" + """Set up the GitLab sensor platform.""" _name = config.get(CONF_NAME) _interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) _url = config.get(CONF_URL) @@ -58,10 +66,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class GitLabSensor(Entity): - """Representation of a Sensor.""" + """Representation of a GitLab sensor.""" def __init__(self, gitlab_data, name): - """Initialize the sensor.""" + """Initialize the GitLab sensor.""" self._available = False self._state = None self._started_at = None From d7cd1a2b4b1c14e1c41d42228aa1e32f5b492882 Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Thu, 11 Oct 2018 00:38:31 -0700 Subject: [PATCH 093/265] Implement ZoneMinder run states (#17198) --- .coveragerc | 2 +- CODEOWNERS | 2 +- homeassistant/components/sensor/zoneminder.py | 24 +++++++++++++++++++ .../{zoneminder.py => zoneminder/__init__.py} | 18 ++++++++++++-- .../components/zoneminder/services.yaml | 6 +++++ requirements_all.txt | 2 +- 6 files changed, 49 insertions(+), 5 deletions(-) rename homeassistant/components/{zoneminder.py => zoneminder/__init__.py} (78%) create mode 100644 homeassistant/components/zoneminder/services.yaml diff --git a/.coveragerc b/.coveragerc index 297c7edb9a7..a4f25bd73f8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -385,7 +385,7 @@ omit = homeassistant/components/zigbee.py homeassistant/components/*/zigbee.py - homeassistant/components/zoneminder.py + homeassistant/components/zoneminder/* homeassistant/components/*/zoneminder.py homeassistant/components/tuya.py diff --git a/CODEOWNERS b/CODEOWNERS index 93fd0418833..ed8d8531a6a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -233,7 +233,7 @@ homeassistant/components/*/xiaomi_aqara.py @danielhiversen @syssi homeassistant/components/*/xiaomi_miio.py @rytilahti @syssi # Z -homeassistant/components/zoneminder.py @rohankapoorcom +homeassistant/components/zoneminder/ @rohankapoorcom homeassistant/components/*/zoneminder.py @rohankapoorcom # Other code diff --git a/homeassistant/components/sensor/zoneminder.py b/homeassistant/components/sensor/zoneminder.py index d4164bbf721..8e4b57f1f38 100644 --- a/homeassistant/components/sensor/zoneminder.py +++ b/homeassistant/components/sensor/zoneminder.py @@ -54,6 +54,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for sensor in config[CONF_MONITORED_CONDITIONS]: sensors.append(ZMSensorEvents(monitor, include_archived, sensor)) + sensors.append(ZMSensorRunState(zm_client)) add_entities(sensors) @@ -114,3 +115,26 @@ class ZMSensorEvents(Entity): """Update the sensor.""" self._state = self._monitor.get_events( self.time_period, self._include_archived) + + +class ZMSensorRunState(Entity): + """Get the ZoneMinder run state.""" + + def __init__(self, client): + """Initialize run state sensor.""" + self._state = None + self._client = client + + @property + def name(self): + """Return the name of the sensor.""" + return 'Run State' + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + def update(self): + """Update the sensor.""" + self._state = self._client.get_active_state() diff --git a/homeassistant/components/zoneminder.py b/homeassistant/components/zoneminder/__init__.py similarity index 78% rename from homeassistant/components/zoneminder.py rename to homeassistant/components/zoneminder/__init__.py index 3f6d8ba7fcf..e5d0c7a5a92 100644 --- a/homeassistant/components/zoneminder.py +++ b/homeassistant/components/zoneminder/__init__.py @@ -10,12 +10,12 @@ import voluptuous as vol from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_PATH, CONF_SSL, CONF_USERNAME, - CONF_VERIFY_SSL) + CONF_VERIFY_SSL, ATTR_NAME) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['zm-py==0.0.5'] +REQUIREMENTS = ['zm-py==0.1.0'] CONF_PATH_ZMS = 'path_zms' @@ -38,6 +38,11 @@ CONFIG_SCHEMA = vol.Schema({ }) }, extra=vol.ALLOW_EXTRA) +SERVICE_SET_RUN_STATE = 'set_run_state' +SET_RUN_STATE_SCHEMA = vol.Schema({ + vol.Required(ATTR_NAME): cv.string +}) + def setup(hass, config): """Set up the ZoneMinder component.""" @@ -57,4 +62,13 @@ def setup(hass, config): conf.get(CONF_PATH_ZMS), conf.get(CONF_VERIFY_SSL)) + def set_active_state(call): + """Set the ZoneMinder run state to the given state name.""" + return hass.data[DOMAIN].set_active_state(call.data[ATTR_NAME]) + + hass.services.register( + DOMAIN, SERVICE_SET_RUN_STATE, set_active_state, + schema=SET_RUN_STATE_SCHEMA + ) + return hass.data[DOMAIN].login() diff --git a/homeassistant/components/zoneminder/services.yaml b/homeassistant/components/zoneminder/services.yaml new file mode 100644 index 00000000000..e6346d2f384 --- /dev/null +++ b/homeassistant/components/zoneminder/services.yaml @@ -0,0 +1,6 @@ +set_run_state: + description: Set the ZoneMinder run state + fields: + name: + description: The string name of the ZoneMinder run state to set as active. + example: 'Home' \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index e0118145c4a..e24a1f5681d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1594,4 +1594,4 @@ zigpy-xbee==0.1.1 zigpy==0.2.0 # homeassistant.components.zoneminder -zm-py==0.0.5 +zm-py==0.1.0 From c6c5d40056124cd50a9d0a535742de165ea9a1fc Mon Sep 17 00:00:00 2001 From: Dav0815 <35415680+Dav0815@users.noreply.github.com> Date: Thu, 11 Oct 2018 17:44:17 +1000 Subject: [PATCH 094/265] Transport NSW (#17242) * Resubmission of development * Clean up * Finishing touch and clean up * Remove not needed error check --- .../components/sensor/transport_nsw.py | 125 ++++++++++++++++++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/sensor/test_transport_nsw.py | 50 +++++++ 5 files changed, 182 insertions(+) create mode 100644 homeassistant/components/sensor/transport_nsw.py create mode 100644 tests/components/sensor/test_transport_nsw.py diff --git a/homeassistant/components/sensor/transport_nsw.py b/homeassistant/components/sensor/transport_nsw.py new file mode 100644 index 00000000000..08a2907748c --- /dev/null +++ b/homeassistant/components/sensor/transport_nsw.py @@ -0,0 +1,125 @@ +""" +Transport NSW (AU) sensor to query next leave event for a specified stop. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.transport_nsw/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import (CONF_NAME, CONF_API_KEY, ATTR_ATTRIBUTION) + +REQUIREMENTS = ['PyTransportNSW==0.0.8'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_STOP_ID = 'stop_id' +ATTR_ROUTE = 'route' +ATTR_DUE_IN = 'due' +ATTR_DELAY = 'delay' +ATTR_REAL_TIME = 'real_time' + +CONF_ATTRIBUTION = "Data provided by Transport NSW" +CONF_STOP_ID = 'stop_id' +CONF_ROUTE = 'route' + +DEFAULT_NAME = "Next Bus" +ICON = "mdi:bus" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_STOP_ID): cv.string, + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_ROUTE, default=""): cv.string, +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Transport NSW sensor.""" + stop_id = config[CONF_STOP_ID] + api_key = config[CONF_API_KEY] + route = config.get(CONF_ROUTE) + name = config.get(CONF_NAME) + + data = PublicTransportData(stop_id, route, api_key) + add_entities([TransportNSWSensor(data, stop_id, name)], True) + + +class TransportNSWSensor(Entity): + """Implementation of an Transport NSW sensor.""" + + def __init__(self, data, stop_id, name): + """Initialize the sensor.""" + self.data = data + self._name = name + self._stop_id = stop_id + self._times = self._state = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self._times is not None: + return { + ATTR_DUE_IN: self._times[ATTR_DUE_IN], + ATTR_STOP_ID: self._stop_id, + ATTR_ROUTE: self._times[ATTR_ROUTE], + ATTR_DELAY: self._times[ATTR_DELAY], + ATTR_REAL_TIME: self._times[ATTR_REAL_TIME], + ATTR_ATTRIBUTION: CONF_ATTRIBUTION + } + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return 'min' + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return ICON + + def update(self): + """Get the latest data from Transport NSW and update the states.""" + self.data.update() + self._times = self.data.info + self._state = self._times[ATTR_DUE_IN] + + +class PublicTransportData: + """The Class for handling the data retrieval.""" + + def __init__(self, stop_id, route, api_key): + """Initialize the data object.""" + import TransportNSW + self._stop_id = stop_id + self._route = route + self._api_key = api_key + self.info = {ATTR_ROUTE: self._route, + ATTR_DUE_IN: 'n/a', + ATTR_DELAY: 'n/a', + ATTR_REAL_TIME: 'n/a'} + self.tnsw = TransportNSW.TransportNSW() + + def update(self): + """Get the next leave time.""" + _data = self.tnsw.get_departures(self._stop_id, + self._route, + self._api_key) + self.info = {ATTR_ROUTE: _data['route'], + ATTR_DUE_IN: _data['due'], + ATTR_DELAY: _data['delay'], + ATTR_REAL_TIME: _data['real_time']} diff --git a/requirements_all.txt b/requirements_all.txt index e24a1f5681d..81a46a5b107 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -57,6 +57,9 @@ PyRMVtransport==0.1.3 # homeassistant.components.switch.switchbot PySwitchbot==0.3 +# homeassistant.components.sensor.transport_nsw +PyTransportNSW==0.0.8 + # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.11.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 229655bdea7..e6d9a6f3fe5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -24,6 +24,9 @@ HAP-python==2.2.2 # homeassistant.components.sensor.rmvtransport PyRMVtransport==0.1.3 +# homeassistant.components.sensor.transport_nsw +PyTransportNSW==0.0.8 + # homeassistant.components.notify.yessssms YesssSMS==0.2.3 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 9c695054ffc..eef77b9ec81 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -84,6 +84,7 @@ TEST_REQUIREMENTS = ( 'pysonos', 'pyqwikswitch', 'PyRMVtransport', + 'PyTransportNSW', 'pyspcwebgw', 'python-forecastio', 'python-nest', diff --git a/tests/components/sensor/test_transport_nsw.py b/tests/components/sensor/test_transport_nsw.py new file mode 100644 index 00000000000..fe933272962 --- /dev/null +++ b/tests/components/sensor/test_transport_nsw.py @@ -0,0 +1,50 @@ +"""The tests for the Transport NSW (AU) sensor platform.""" +import unittest +from unittest.mock import patch + +from homeassistant.setup import setup_component + +from tests.common import get_test_home_assistant + +VALID_CONFIG = {'sensor': { + 'platform': 'transport_nsw', + 'stop_id': '209516', + 'route': '199', + 'api_key': 'YOUR_API_KEY'} + } + + +def get_departuresMock(_stop_id, route, api_key): + """Mock TransportNSW departures loading.""" + data = { + 'stop_id': '209516', + 'route': '199', + 'due': 16, + 'delay': 6, + 'real_time': 'y' + } + return data + + +class TestRMVtransportSensor(unittest.TestCase): + """Test the TransportNSW sensor.""" + + def setUp(self): + """Set up things to run when tests begin.""" + self.hass = get_test_home_assistant() + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + @patch('TransportNSW.TransportNSW.get_departures', + side_effect=get_departuresMock) + def test_transportnsw_config(self, mock_get_departures): + """Test minimal TransportNSW configuration.""" + assert setup_component(self.hass, 'sensor', VALID_CONFIG) + state = self.hass.states.get('sensor.next_bus') + self.assertEqual(state.state, '16') + self.assertEqual(state.attributes['stop_id'], '209516') + self.assertEqual(state.attributes['route'], '199') + self.assertEqual(state.attributes['delay'], 6) + self.assertEqual(state.attributes['real_time'], 'y') From 9fa7906aef86e018bb58ca36a1e5271a23c6bf89 Mon Sep 17 00:00:00 2001 From: Fabien Piuzzi Date: Thu, 11 Oct 2018 09:52:13 +0200 Subject: [PATCH 095/265] Made it possible to define multiple Octoprint printers (#16519) * Made it possible to define multiple octoprint printers * style fix * Added configuration option for octoprint port * SSL support in octoprint platform configuration * Octoprint component now auto loads sensor and binary_sensor platforms * preliminary support for auto discovery of octoprint servers * Moved sensors and binary sensors configuration into main octoprint configuration * Using base_url as the key for storing api in the octoprint component * made sure to not supersede the platforms' domains * bugfix: continue setting up other printers if one fails * flake8 style correction * Added icons to sensors * Fail platform setup if no printers were successfully added * Simplified custom validator --- .../components/binary_sensor/octoprint.py | 40 +++---- homeassistant/components/discovery.py | 2 + homeassistant/components/octoprint.py | 108 +++++++++++++++--- homeassistant/components/sensor/octoprint.py | 44 +++---- 4 files changed, 125 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/binary_sensor/octoprint.py b/homeassistant/components/binary_sensor/octoprint.py index 3dd1ee2be8c..285495c03a0 100644 --- a/homeassistant/components/binary_sensor/octoprint.py +++ b/homeassistant/components/binary_sensor/octoprint.py @@ -7,45 +7,33 @@ https://home-assistant.io/components/binary_sensor.octoprint/ import logging import requests -import voluptuous as vol -from homeassistant.const import CONF_NAME, CONF_MONITORED_CONDITIONS -from homeassistant.components.binary_sensor import ( - BinarySensorDevice, PLATFORM_SCHEMA) -import homeassistant.helpers.config_validation as cv +from homeassistant.components.octoprint import (BINARY_SENSOR_TYPES, + DOMAIN as COMPONENT_DOMAIN) +from homeassistant.components.binary_sensor import BinarySensorDevice _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['octoprint'] -DOMAIN = "octoprint" -DEFAULT_NAME = 'OctoPrint' - -SENSOR_TYPES = { - # API Endpoint, Group, Key, unit - 'Printing': ['printer', 'state', 'printing', None], - 'Printing Error': ['printer', 'state', 'error', None] -} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, -}) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the available OctoPrint binary sensors.""" - octoprint_api = hass.data[DOMAIN]["api"] - name = config.get(CONF_NAME) - monitored_conditions = config.get( - CONF_MONITORED_CONDITIONS, SENSOR_TYPES.keys()) + if discovery_info is None: + return + + name = discovery_info['name'] + base_url = discovery_info['base_url'] + monitored_conditions = discovery_info['sensors'] + octoprint_api = hass.data[COMPONENT_DOMAIN][base_url] devices = [] for octo_type in monitored_conditions: new_sensor = OctoPrintBinarySensor( - octoprint_api, octo_type, SENSOR_TYPES[octo_type][2], - name, SENSOR_TYPES[octo_type][3], SENSOR_TYPES[octo_type][0], - SENSOR_TYPES[octo_type][1], 'flags') + octoprint_api, octo_type, BINARY_SENSOR_TYPES[octo_type][2], + name, BINARY_SENSOR_TYPES[octo_type][3], + BINARY_SENSOR_TYPES[octo_type][0], + BINARY_SENSOR_TYPES[octo_type][1], 'flags') devices.append(new_sensor) add_entities(devices, True) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 0640eb262cd..36f41e15a47 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -43,6 +43,7 @@ SERVICE_DAIKIN = 'daikin' SERVICE_SABNZBD = 'sabnzbd' SERVICE_SAMSUNG_PRINTER = 'samsung_printer' SERVICE_HOMEKIT = 'homekit' +SERVICE_OCTOPRINT = 'octoprint' CONFIG_ENTRY_HANDLERS = { SERVICE_DECONZ: 'deconz', @@ -67,6 +68,7 @@ SERVICE_HANDLERS = { SERVICE_SABNZBD: ('sabnzbd', None), SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'), SERVICE_KONNECTED: ('konnected', None), + SERVICE_OCTOPRINT: ('octoprint', None), 'panasonic_viera': ('media_player', 'panasonic_viera'), 'plex_mediaserver': ('media_player', 'plex'), 'roku': ('media_player', 'roku'), diff --git a/homeassistant/components/octoprint.py b/homeassistant/components/octoprint.py index ff52ad94d8b..2a39ac2c44a 100644 --- a/homeassistant/components/octoprint.py +++ b/homeassistant/components/octoprint.py @@ -11,43 +11,117 @@ import requests import voluptuous as vol from aiohttp.hdrs import CONTENT_TYPE -from homeassistant.const import CONF_API_KEY, CONF_HOST, CONTENT_TYPE_JSON +from homeassistant.components.discovery import SERVICE_OCTOPRINT +from homeassistant.const import ( + CONF_API_KEY, CONF_HOST, CONTENT_TYPE_JSON, CONF_NAME, CONF_PORT, + CONF_SSL, TEMP_CELSIUS, CONF_MONITORED_CONDITIONS, CONF_SENSORS, + CONF_BINARY_SENSORS) +from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform +from homeassistant.util import slugify as util_slugify _LOGGER = logging.getLogger(__name__) DOMAIN = 'octoprint' CONF_NUMBER_OF_TOOLS = 'number_of_tools' CONF_BED = 'bed' +DEFAULT_NAME = 'OctoPrint' + + +def has_all_unique_names(value): + """Validate that printers have an unique name.""" + names = [util_slugify(printer['name']) for printer in value] + vol.Schema(vol.Unique())(names) + return value + + +BINARY_SENSOR_TYPES = { + # API Endpoint, Group, Key, unit + 'Printing': ['printer', 'state', 'printing', None], + 'Printing Error': ['printer', 'state', 'error', None] +} + +BINARY_SENSOR_SCHEMA = vol.Schema({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(BINARY_SENSOR_TYPES)): + vol.All(cv.ensure_list, [vol.In(BINARY_SENSOR_TYPES)]), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + +SENSOR_TYPES = { + # API Endpoint, Group, Key, unit, icon + 'Temperatures': ['printer', 'temperature', '*', TEMP_CELSIUS], + 'Current State': ['printer', 'state', 'text', None, 'mdi:printer-3d'], + 'Job Percentage': ['job', 'progress', 'completion', '%', + 'mdi:file-percent'], + 'Time Remaining': ['job', 'progress', 'printTimeLeft', 'seconds', + 'mdi:clock-end'], + 'Time Elapsed': ['job', 'progress', 'printTime', 'seconds', + 'mdi:clock-start'], +} + +SENSOR_SCHEMA = vol.Schema({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ + DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_SSL, default=False): cv.boolean, + vol.Optional(CONF_PORT, default=80): cv.port, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NUMBER_OF_TOOLS, default=0): cv.positive_int, - vol.Optional(CONF_BED, default=False): cv.boolean - }), + vol.Optional(CONF_BED, default=False): cv.boolean, + vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, + vol.Optional(CONF_BINARY_SENSORS, default={}): BINARY_SENSOR_SCHEMA + })], has_all_unique_names), }, extra=vol.ALLOW_EXTRA) def setup(hass, config): """Set up the OctoPrint component.""" - base_url = 'http://{}/api/'.format(config[DOMAIN][CONF_HOST]) - api_key = config[DOMAIN][CONF_API_KEY] - number_of_tools = config[DOMAIN][CONF_NUMBER_OF_TOOLS] - bed = config[DOMAIN][CONF_BED] + printers = hass.data[DOMAIN] = {} + success = False - hass.data[DOMAIN] = {"api": None} + def device_discovered(service, info): + """Get called when an Octoprint server has been discovered.""" + _LOGGER.debug('Found an Octoprint server: %s', info) - try: - octoprint_api = OctoPrintAPI(base_url, api_key, bed, number_of_tools) - hass.data[DOMAIN]["api"] = octoprint_api - octoprint_api.get('printer') - octoprint_api.get('job') - except requests.exceptions.RequestException as conn_err: - _LOGGER.error("Error setting up OctoPrint API: %r", conn_err) + discovery.listen(hass, SERVICE_OCTOPRINT, device_discovered) - return True + for printer in config[DOMAIN]: + name = printer[CONF_NAME] + ssl = 's' if printer[CONF_SSL] else '' + base_url = 'http{}://{}:{}/api/'.format(ssl, + printer[CONF_HOST], + printer[CONF_PORT]) + api_key = printer[CONF_API_KEY] + number_of_tools = printer[CONF_NUMBER_OF_TOOLS] + bed = printer[CONF_BED] + try: + octoprint_api = OctoPrintAPI(base_url, api_key, bed, + number_of_tools) + printers[base_url] = octoprint_api + octoprint_api.get('printer') + octoprint_api.get('job') + except requests.exceptions.RequestException as conn_err: + _LOGGER.error("Error setting up OctoPrint API: %r", conn_err) + continue + + sensors = printer[CONF_SENSORS][CONF_MONITORED_CONDITIONS] + load_platform(hass, 'sensor', DOMAIN, {'name': name, + 'base_url': base_url, + 'sensors': sensors}) + b_sensors = printer[CONF_BINARY_SENSORS][CONF_MONITORED_CONDITIONS] + load_platform(hass, 'binary_sensor', DOMAIN, {'name': name, + 'base_url': base_url, + 'sensors': b_sensors}) + success = True + + return success class OctoPrintAPI: diff --git a/homeassistant/components/sensor/octoprint.py b/homeassistant/components/sensor/octoprint.py index d42828c9f55..8170b97c4c8 100644 --- a/homeassistant/components/sensor/octoprint.py +++ b/homeassistant/components/sensor/octoprint.py @@ -7,42 +7,28 @@ https://home-assistant.io/components/sensor.octoprint/ import logging import requests -import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - TEMP_CELSIUS, CONF_NAME, CONF_MONITORED_CONDITIONS) +from homeassistant.components.octoprint import (SENSOR_TYPES, + DOMAIN as COMPONENT_DOMAIN) +from homeassistant.const import (TEMP_CELSIUS) from homeassistant.helpers.entity import Entity -import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['octoprint'] -DOMAIN = "octoprint" -DEFAULT_NAME = 'OctoPrint' NOTIFICATION_ID = 'octoprint_notification' NOTIFICATION_TITLE = 'OctoPrint sensor setup error' -SENSOR_TYPES = { - 'Temperatures': ['printer', 'temperature', '*', TEMP_CELSIUS], - 'Current State': ['printer', 'state', 'text', None], - 'Job Percentage': ['job', 'progress', 'completion', '%'], - 'Time Remaining': ['job', 'progress', 'printTimeLeft', 'seconds'], - 'Time Elapsed': ['job', 'progress', 'printTime', 'seconds'], -} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, -}) - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the available OctoPrint sensors.""" - octoprint_api = hass.data[DOMAIN]["api"] - name = config.get(CONF_NAME) - monitored_conditions = config.get(CONF_MONITORED_CONDITIONS) + if discovery_info is None: + return + + name = discovery_info['name'] + base_url = discovery_info['base_url'] + monitored_conditions = discovery_info['sensors'] + octoprint_api = hass.data[COMPONENT_DOMAIN][base_url] tools = octoprint_api.get_tools() if "Temperatures" in monitored_conditions: @@ -72,7 +58,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): new_sensor = OctoPrintSensor( octoprint_api, octo_type, SENSOR_TYPES[octo_type][2], name, SENSOR_TYPES[octo_type][3], SENSOR_TYPES[octo_type][0], - SENSOR_TYPES[octo_type][1]) + SENSOR_TYPES[octo_type][1], None, SENSOR_TYPES[octo_type][4]) devices.append(new_sensor) add_entities(devices, True) @@ -81,7 +67,7 @@ class OctoPrintSensor(Entity): """Representation of an OctoPrint sensor.""" def __init__(self, api, condition, sensor_type, sensor_name, unit, - endpoint, group, tool=None): + endpoint, group, tool=None, icon=None): """Initialize a new OctoPrint sensor.""" self.sensor_name = sensor_name if tool is None: @@ -96,6 +82,7 @@ class OctoPrintSensor(Entity): self.api_endpoint = endpoint self.api_group = group self.api_tool = tool + self._icon = icon _LOGGER.debug("Created OctoPrint sensor %r", self) @property @@ -128,3 +115,8 @@ class OctoPrintSensor(Entity): except requests.exceptions.ConnectionError: # Error calling the api, already logged in api.update() return + + @property + def icon(self): + """Icon to use in the frontend.""" + return self._icon From ef2c8b2e5ba43f08b85ab578bb2c7f83a9ffb3ba Mon Sep 17 00:00:00 2001 From: George Marshall Date: Thu, 11 Oct 2018 00:59:16 -0700 Subject: [PATCH 096/265] Update python_openzwave==0.4.10 (#17323) --- homeassistant/components/zwave/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index d48cac6a1e2..b0ea5201812 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -42,7 +42,7 @@ from .discovery_schemas import DISCOVERY_SCHEMAS from .util import (check_node_schema, check_value_schema, node_name, check_has_unique_id, is_node_parsed) -REQUIREMENTS = ['pydispatcher==2.0.5', 'python_openzwave==0.4.9'] +REQUIREMENTS = ['pydispatcher==2.0.5', 'python_openzwave==0.4.10'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 81a46a5b107..39aaebac11a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1208,7 +1208,7 @@ python-wink==1.10.1 python_opendata_transport==0.1.4 # homeassistant.components.zwave -python_openzwave==0.4.9 +python_openzwave==0.4.10 # homeassistant.components.egardia pythonegardia==1.0.39 From 58af332d21c1581280a1200edeee9dc63b2bab28 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 11 Oct 2018 10:37:34 +0200 Subject: [PATCH 097/265] Allow tradfri groups for new imported entries (#17310) * Clean up leftover config schema option * Allow import groups via new config yaml setup * Fix and add test * Add a test without groups for legacy import * Change default import groups to False * Fix I/O in test --- homeassistant/components/tradfri/__init__.py | 7 +- .../components/tradfri/config_flow.py | 5 +- tests/components/tradfri/conftest.py | 12 +++ tests/components/tradfri/test_config_flow.py | 96 +++++++++++++++---- tests/components/tradfri/test_init.py | 2 +- 5 files changed, 96 insertions(+), 26 deletions(-) create mode 100644 tests/components/tradfri/conftest.py diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index e9ee8d0898b..ba13b8d511a 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -25,11 +25,11 @@ CONFIG_FILE = '.tradfri_psk.conf' KEY_GATEWAY = 'tradfri_gateway' KEY_API = 'tradfri_api' CONF_ALLOW_TRADFRI_GROUPS = 'allow_tradfri_groups' -DEFAULT_ALLOW_TRADFRI_GROUPS = True +DEFAULT_ALLOW_TRADFRI_GROUPS = False CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ - vol.Inclusive(CONF_HOST, 'gateway'): cv.string, + vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_ALLOW_TRADFRI_GROUPS, default=DEFAULT_ALLOW_TRADFRI_GROUPS): cv.boolean, }) @@ -64,13 +64,14 @@ async def async_setup(hass, config): )) host = conf.get(CONF_HOST) + import_groups = conf[CONF_ALLOW_TRADFRI_GROUPS] if host is None or host in configured_hosts or host in legacy_hosts: return True hass.async_create_task(hass.config_entries.flow.async_init( DOMAIN, context={'source': config_entries.SOURCE_IMPORT}, - data={'host': host} + data={CONF_HOST: host, CONF_IMPORT_GROUPS: import_groups} )) return True diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index a3452e50c4d..2e24fde8294 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -34,6 +34,7 @@ class FlowHandler(config_entries.ConfigFlow): def __init__(self): """Initialize flow.""" self._host = None + self._import_groups = False async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" @@ -52,7 +53,8 @@ class FlowHandler(config_entries.ConfigFlow): # We don't ask for import group anymore as group state # is not reliable, don't want to show that to the user. - auth[CONF_IMPORT_GROUPS] = False + # But we still allow specifying import group via config yaml. + auth[CONF_IMPORT_GROUPS] = self._import_groups return await self._entry_from_data(auth) @@ -97,6 +99,7 @@ class FlowHandler(config_entries.ConfigFlow): # Happens if user has host directly in configuration.yaml if 'key' not in user_input: self._host = user_input['host'] + self._import_groups = user_input[CONF_IMPORT_GROUPS] return await self.async_step_auth() try: diff --git a/tests/components/tradfri/conftest.py b/tests/components/tradfri/conftest.py new file mode 100644 index 00000000000..9a5745264b7 --- /dev/null +++ b/tests/components/tradfri/conftest.py @@ -0,0 +1,12 @@ +"""Common tradfri test fixtures.""" +from unittest.mock import patch + +import pytest + + +@pytest.fixture +def mock_gateway_info(): + """Mock get_gateway_info.""" + with patch('homeassistant.components.tradfri.config_flow.' + 'get_gateway_info') as mock_gateway: + yield mock_gateway diff --git a/tests/components/tradfri/test_config_flow.py b/tests/components/tradfri/test_config_flow.py index 99566356f61..6756a01bbc7 100644 --- a/tests/components/tradfri/test_config_flow.py +++ b/tests/components/tradfri/test_config_flow.py @@ -17,14 +17,6 @@ def mock_auth(): yield mock_auth -@pytest.fixture -def mock_gateway_info(): - """Mock get_gateway_info.""" - with patch('homeassistant.components.tradfri.config_flow.' - 'get_gateway_info') as mock_gateway: - yield mock_gateway - - @pytest.fixture def mock_entry_setup(): """Mock entry setup.""" @@ -125,34 +117,65 @@ async def test_discovery_connection(hass, mock_auth, mock_entry_setup): } -async def test_import_connection(hass, mock_gateway_info, mock_entry_setup): +async def test_import_connection(hass, mock_auth, mock_entry_setup): """Test a connection via import.""" - mock_gateway_info.side_effect = \ - lambda hass, host, identity, key: mock_coro({ - 'host': host, - 'identity': identity, - 'key': key, - 'gateway_id': 'mock-gateway' - }) + mock_auth.side_effect = lambda hass, host, code: mock_coro({ + 'host': host, + 'gateway_id': 'bla', + 'identity': 'mock-iden', + 'key': 'mock-key', + }) - result = await hass.config_entries.flow.async_init( + flow = await hass.config_entries.flow.async_init( 'tradfri', context={'source': 'import'}, data={ 'host': '123.123.123.123', - 'identity': 'mock-iden', - 'key': 'mock-key', 'import_groups': True }) + result = await hass.config_entries.flow.async_configure(flow['flow_id'], { + 'security_code': 'abcd', + }) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result['result'].data == { 'host': '123.123.123.123', - 'gateway_id': 'mock-gateway', + 'gateway_id': 'bla', 'identity': 'mock-iden', 'key': 'mock-key', 'import_groups': True } - assert len(mock_gateway_info.mock_calls) == 1 + assert len(mock_entry_setup.mock_calls) == 1 + + +async def test_import_connection_no_groups(hass, mock_auth, mock_entry_setup): + """Test a connection via import and no groups allowed.""" + mock_auth.side_effect = lambda hass, host, code: mock_coro({ + 'host': host, + 'gateway_id': 'bla', + 'identity': 'mock-iden', + 'key': 'mock-key', + }) + + flow = await hass.config_entries.flow.async_init( + 'tradfri', context={'source': 'import'}, data={ + 'host': '123.123.123.123', + 'import_groups': False + }) + + result = await hass.config_entries.flow.async_configure(flow['flow_id'], { + 'security_code': 'abcd', + }) + + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result['result'].data == { + 'host': '123.123.123.123', + 'gateway_id': 'bla', + 'identity': 'mock-iden', + 'key': 'mock-key', + 'import_groups': False + } + assert len(mock_entry_setup.mock_calls) == 1 @@ -187,6 +210,37 @@ async def test_import_connection_legacy(hass, mock_gateway_info, assert len(mock_entry_setup.mock_calls) == 1 +async def test_import_connection_legacy_no_groups( + hass, mock_gateway_info, mock_entry_setup): + """Test a connection via legacy import and no groups allowed.""" + mock_gateway_info.side_effect = \ + lambda hass, host, identity, key: mock_coro({ + 'host': host, + 'identity': identity, + 'key': key, + 'gateway_id': 'mock-gateway' + }) + + result = await hass.config_entries.flow.async_init( + 'tradfri', context={'source': 'import'}, data={ + 'host': '123.123.123.123', + 'key': 'mock-key', + 'import_groups': False + }) + + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result['result'].data == { + 'host': '123.123.123.123', + 'gateway_id': 'mock-gateway', + 'identity': 'homeassistant', + 'key': 'mock-key', + 'import_groups': False + } + + assert len(mock_gateway_info.mock_calls) == 1 + assert len(mock_entry_setup.mock_calls) == 1 + + async def test_discovery_duplicate_aborted(hass): """Test a duplicate discovery host is ignored.""" MockConfigEntry( diff --git a/tests/components/tradfri/test_init.py b/tests/components/tradfri/test_init.py index 4527e87f605..800c7b72ee6 100644 --- a/tests/components/tradfri/test_init.py +++ b/tests/components/tradfri/test_init.py @@ -58,7 +58,7 @@ async def test_config_json_host_not_imported(hass): assert len(mock_init.mock_calls) == 0 -async def test_config_json_host_imported(hass): +async def test_config_json_host_imported(hass, mock_gateway_info): """Test that we import a configured host.""" with patch('homeassistant.components.tradfri.load_json', return_value={'mock-host': {'key': 'some-info'}}): From cffb70431186547d1920182e72499a6b3fdbfec0 Mon Sep 17 00:00:00 2001 From: uchagani Date: Thu, 11 Oct 2018 04:55:22 -0400 Subject: [PATCH 098/265] Enable BMW component to be unit system aware (#17197) * Enable BMW component to be unit system aware * lint fixes * use constants for config entries * remove configuration from component and rely only on HA config of unit_system * remove unused import * update code to reflect feedback * lint fixes * remove unnecessary comments * rework return statement to satisfy pylint * more lint fixes * add tests for volume utils * lint fixes * more lint fixes * remove unnecessary comments --- .../binary_sensor/bmw_connected_drive.py | 13 ++-- .../bmw_connected_drive/__init__.py | 3 +- .../components/sensor/bmw_connected_drive.py | 60 ++++++++++++++----- homeassistant/util/unit_system.py | 8 +++ homeassistant/util/volume.py | 45 ++++++++++++++ tests/util/test_volume.py | 49 +++++++++++++++ 6 files changed, 158 insertions(+), 20 deletions(-) create mode 100644 homeassistant/util/volume.py create mode 100644 tests/util/test_volume.py diff --git a/homeassistant/components/binary_sensor/bmw_connected_drive.py b/homeassistant/components/binary_sensor/bmw_connected_drive.py index 3fe8136c93b..f8855b2e28b 100644 --- a/homeassistant/components/binary_sensor/bmw_connected_drive.py +++ b/homeassistant/components/binary_sensor/bmw_connected_drive.py @@ -8,6 +8,7 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN +from homeassistant.const import LENGTH_KILOMETERS DEPENDENCIES = ['bmw_connected_drive'] @@ -117,7 +118,8 @@ class BMWConnectedDriveSensor(BinarySensorDevice): result['lights_parking'] = vehicle_state.parking_lights.value elif self._attribute == 'condition_based_services': for report in vehicle_state.condition_based_services: - result.update(self._format_cbs_report(report)) + result.update( + self._format_cbs_report(report)) elif self._attribute == 'check_control_messages': check_control_messages = vehicle_state.check_control_messages if not check_control_messages: @@ -175,8 +177,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice): self._state = (vehicle_state._attributes['connectionStatus'] == 'CONNECTED') - @staticmethod - def _format_cbs_report(report): + def _format_cbs_report(self, report): result = {} service_type = report.service_type.lower().replace('_', ' ') result['{} status'.format(service_type)] = report.state.value @@ -184,8 +185,10 @@ class BMWConnectedDriveSensor(BinarySensorDevice): result['{} date'.format(service_type)] = \ report.due_date.strftime('%Y-%m-%d') if report.due_distance is not None: - result['{} distance'.format(service_type)] = \ - '{} km'.format(report.due_distance) + distance = round(self.hass.config.units.length( + report.due_distance, LENGTH_KILOMETERS)) + result['{} distance'.format(service_type)] = '{} {}'.format( + distance, self.hass.config.units.length_unit) return result def update_callback(self): diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index dce5961d70d..40f2b91045a 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -9,7 +9,7 @@ import logging import voluptuous as vol -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD) from homeassistant.helpers import discovery from homeassistant.helpers.event import track_utc_time_change import homeassistant.helpers.config_validation as cv @@ -85,6 +85,7 @@ def setup_account(account_config: dict, hass, name: str) \ password = account_config[CONF_PASSWORD] region = account_config[CONF_REGION] read_only = account_config[CONF_READ_ONLY] + _LOGGER.debug('Adding new account %s', name) cd_account = BMWConnectedDriveAccount(username, password, region, name, read_only) diff --git a/homeassistant/components/sensor/bmw_connected_drive.py b/homeassistant/components/sensor/bmw_connected_drive.py index 964a8a4cb16..a7ee5724d19 100644 --- a/homeassistant/components/sensor/bmw_connected_drive.py +++ b/homeassistant/components/sensor/bmw_connected_drive.py @@ -9,18 +9,32 @@ import logging from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level +from homeassistant.const import (CONF_UNIT_SYSTEM_IMPERIAL, VOLUME_LITERS, + VOLUME_GALLONS, LENGTH_KILOMETERS, + LENGTH_MILES) DEPENDENCIES = ['bmw_connected_drive'] _LOGGER = logging.getLogger(__name__) -ATTR_TO_HA = { - 'mileage': ['mdi:speedometer', 'km'], - 'remaining_range_total': ['mdi:ruler', 'km'], - 'remaining_range_electric': ['mdi:ruler', 'km'], - 'remaining_range_fuel': ['mdi:ruler', 'km'], - 'max_range_electric': ['mdi:ruler', 'km'], - 'remaining_fuel': ['mdi:gas-station', 'l'], +ATTR_TO_HA_METRIC = { + 'mileage': ['mdi:speedometer', LENGTH_KILOMETERS], + 'remaining_range_total': ['mdi:ruler', LENGTH_KILOMETERS], + 'remaining_range_electric': ['mdi:ruler', LENGTH_KILOMETERS], + 'remaining_range_fuel': ['mdi:ruler', LENGTH_KILOMETERS], + 'max_range_electric': ['mdi:ruler', LENGTH_KILOMETERS], + 'remaining_fuel': ['mdi:gas-station', VOLUME_LITERS], + 'charging_time_remaining': ['mdi:update', 'h'], + 'charging_status': ['mdi:battery-charging', None] +} + +ATTR_TO_HA_IMPERIAL = { + 'mileage': ['mdi:speedometer', LENGTH_MILES], + 'remaining_range_total': ['mdi:ruler', LENGTH_MILES], + 'remaining_range_electric': ['mdi:ruler', LENGTH_MILES], + 'remaining_range_fuel': ['mdi:ruler', LENGTH_MILES], + 'max_range_electric': ['mdi:ruler', LENGTH_MILES], + 'remaining_fuel': ['mdi:gas-station', VOLUME_GALLONS], 'charging_time_remaining': ['mdi:update', 'h'], 'charging_status': ['mdi:battery-charging', None] } @@ -28,6 +42,11 @@ ATTR_TO_HA = { def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the BMW sensors.""" + if hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: + attribute_info = ATTR_TO_HA_IMPERIAL + else: + attribute_info = ATTR_TO_HA_METRIC + accounts = hass.data[BMW_DOMAIN] _LOGGER.debug('Found BMW accounts: %s', ', '.join([a.name for a in accounts])) @@ -36,9 +55,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for vehicle in account.account.vehicles: for attribute_name in vehicle.drive_train_attributes: device = BMWConnectedDriveSensor(account, vehicle, - attribute_name) + attribute_name, + attribute_info) devices.append(device) - device = BMWConnectedDriveSensor(account, vehicle, 'mileage') + device = BMWConnectedDriveSensor(account, vehicle, 'mileage', + attribute_info) devices.append(device) add_entities(devices, True) @@ -46,7 +67,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class BMWConnectedDriveSensor(Entity): """Representation of a BMW vehicle sensor.""" - def __init__(self, account, vehicle, attribute: str): + def __init__(self, account, vehicle, attribute: str, attribute_info): """Constructor.""" self._vehicle = vehicle self._account = account @@ -54,6 +75,7 @@ class BMWConnectedDriveSensor(Entity): self._state = None self._name = '{} {}'.format(self._vehicle.name, self._attribute) self._unique_id = '{}-{}'.format(self._vehicle.vin, self._attribute) + self._attribute_info = attribute_info @property def should_poll(self) -> bool: @@ -78,14 +100,14 @@ class BMWConnectedDriveSensor(Entity): """Icon to use in the frontend, if any.""" from bimmer_connected.state import ChargingState vehicle_state = self._vehicle.state - charging_state = vehicle_state.charging_status in \ - [ChargingState.CHARGING] + charging_state = vehicle_state.charging_status in [ + ChargingState.CHARGING] if self._attribute == 'charging_level_hv': return icon_for_battery_level( battery_level=vehicle_state.charging_level_hv, charging=charging_state) - icon, _ = ATTR_TO_HA.get(self._attribute, [None, None]) + icon, _ = self._attribute_info.get(self._attribute, [None, None]) return icon @property @@ -100,7 +122,7 @@ class BMWConnectedDriveSensor(Entity): @property def unit_of_measurement(self) -> str: """Get the unit of measurement.""" - _, unit = ATTR_TO_HA.get(self._attribute, [None, None]) + _, unit = self._attribute_info.get(self._attribute, [None, None]) return unit @property @@ -116,6 +138,16 @@ class BMWConnectedDriveSensor(Entity): vehicle_state = self._vehicle.state if self._attribute == 'charging_status': self._state = getattr(vehicle_state, self._attribute).value + elif self.unit_of_measurement == VOLUME_GALLONS: + value = getattr(vehicle_state, self._attribute) + value_converted = self.hass.config.units.volume(value, + VOLUME_LITERS) + self._state = round(value_converted) + elif self.unit_of_measurement == LENGTH_MILES: + value = getattr(vehicle_state, self._attribute) + value_converted = self.hass.config.units.length(value, + LENGTH_KILOMETERS) + self._state = round(value_converted) else: self._state = getattr(vehicle_state, self._attribute) diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py index 5a8f515c3ad..5f6d202b5e9 100644 --- a/homeassistant/util/unit_system.py +++ b/homeassistant/util/unit_system.py @@ -13,6 +13,7 @@ from homeassistant.const import ( TEMPERATURE, UNIT_NOT_RECOGNIZED_TEMPLATE) from homeassistant.util import temperature as temperature_util from homeassistant.util import distance as distance_util +from homeassistant.util import volume as volume_util _LOGGER = logging.getLogger(__name__) @@ -108,6 +109,13 @@ class UnitSystem: return distance_util.convert(length, from_unit, self.length_unit) + def volume(self, volume: Optional[float], from_unit: str) -> float: + """Convert the given volume to this unit system.""" + if not isinstance(volume, Number): + raise TypeError('{} is not a numeric value.'.format(str(volume))) + + return volume_util.convert(volume, from_unit, self.volume_unit) + def as_dict(self) -> dict: """Convert the unit system to a dictionary.""" return { diff --git a/homeassistant/util/volume.py b/homeassistant/util/volume.py new file mode 100644 index 00000000000..154fb3d2c8b --- /dev/null +++ b/homeassistant/util/volume.py @@ -0,0 +1,45 @@ +"""Volume conversion util functions.""" + +import logging +from numbers import Number +from homeassistant.const import (VOLUME_LITERS, VOLUME_MILLILITERS, + VOLUME_GALLONS, VOLUME_FLUID_OUNCE, + VOLUME, UNIT_NOT_RECOGNIZED_TEMPLATE) + +_LOGGER = logging.getLogger(__name__) + +VALID_UNITS = [VOLUME_LITERS, VOLUME_MILLILITERS, VOLUME_GALLONS, + VOLUME_FLUID_OUNCE] + + +def __liter_to_gallon(liter: float) -> float: + """Convert a volume measurement in Liter to Gallon.""" + return liter * .2642 + + +def __gallon_to_liter(gallon: float) -> float: + """Convert a volume measurement in Gallon to Liter.""" + return gallon * 3.785 + + +def convert(volume: float, from_unit: str, to_unit: str) -> float: + """Convert a temperature from one unit to another.""" + if from_unit not in VALID_UNITS: + raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(from_unit, + VOLUME)) + if to_unit not in VALID_UNITS: + raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, VOLUME)) + + if not isinstance(volume, Number): + raise TypeError('{} is not of numeric type'.format(volume)) + + if from_unit == to_unit: + return volume + + result = volume + if from_unit == VOLUME_LITERS and to_unit == VOLUME_GALLONS: + result = __liter_to_gallon(volume) + elif from_unit == VOLUME_GALLONS and to_unit == VOLUME_LITERS: + result = __gallon_to_liter(volume) + + return result diff --git a/tests/util/test_volume.py b/tests/util/test_volume.py new file mode 100644 index 00000000000..e78e099d7d7 --- /dev/null +++ b/tests/util/test_volume.py @@ -0,0 +1,49 @@ +"""Test homeassistant volume utility functions.""" + +import unittest +import homeassistant.util.volume as volume_util +from homeassistant.const import (VOLUME_LITERS, VOLUME_MILLILITERS, + VOLUME_GALLONS, VOLUME_FLUID_OUNCE) + +INVALID_SYMBOL = 'bob' +VALID_SYMBOL = VOLUME_LITERS + + +class TestVolumeUtil(unittest.TestCase): + """Test the volume utility functions.""" + + def test_convert_same_unit(self): + """Test conversion from any unit to same unit.""" + self.assertEqual(2, volume_util.convert(2, VOLUME_LITERS, + VOLUME_LITERS)) + self.assertEqual(3, volume_util.convert(3, VOLUME_MILLILITERS, + VOLUME_MILLILITERS)) + self.assertEqual(4, volume_util.convert(4, VOLUME_GALLONS, + VOLUME_GALLONS)) + self.assertEqual(5, volume_util.convert(5, VOLUME_FLUID_OUNCE, + VOLUME_FLUID_OUNCE)) + + def test_convert_invalid_unit(self): + """Test exception is thrown for invalid units.""" + with self.assertRaises(ValueError): + volume_util.convert(5, INVALID_SYMBOL, VALID_SYMBOL) + + with self.assertRaises(ValueError): + volume_util.convert(5, VALID_SYMBOL, INVALID_SYMBOL) + + def test_convert_nonnumeric_value(self): + """Test exception is thrown for nonnumeric type.""" + with self.assertRaises(TypeError): + volume_util.convert('a', VOLUME_GALLONS, VOLUME_LITERS) + + def test_convert_from_liters(self): + """Test conversion from liters to other units.""" + liters = 5 + self.assertEqual(volume_util.convert(liters, VOLUME_LITERS, + VOLUME_GALLONS), 1.321) + + def test_convert_from_gallons(self): + """Test conversion from gallons to other units.""" + gallons = 5 + self.assertEqual(volume_util.convert(gallons, VOLUME_GALLONS, + VOLUME_LITERS), 18.925) From f5d3aa182647d7b3d2fb530b764b85e441c40dad Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 11 Oct 2018 10:55:38 +0200 Subject: [PATCH 099/265] Hass.io auth/sso part2 (#17324) * Update discovery.py * Create const.py * Update auth.py * Update const.py * Update const.py * Update http.py * Update handler.py * Update auth.py * Update auth.py * Update test_auth.py --- homeassistant/components/hassio/auth.py | 8 ++--- homeassistant/components/hassio/const.py | 12 +++++++ homeassistant/components/hassio/discovery.py | 10 ++---- homeassistant/components/hassio/handler.py | 4 +-- homeassistant/components/hassio/http.py | 3 +- tests/components/hassio/test_auth.py | 34 ++++++++++++++++++-- 6 files changed, 54 insertions(+), 17 deletions(-) create mode 100644 homeassistant/components/hassio/const.py diff --git a/homeassistant/components/hassio/auth.py b/homeassistant/components/hassio/auth.py index 951110271d4..4be3ba9956c 100644 --- a/homeassistant/components/hassio/auth.py +++ b/homeassistant/components/hassio/auth.py @@ -14,16 +14,16 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.const import KEY_REAL_IP from homeassistant.components.http.data_validator import RequestDataValidator -_LOGGER = logging.getLogger(__name__) +from .const import ATTR_USERNAME, ATTR_PASSWORD, ATTR_ADDON -ATTR_USERNAME = 'username' -ATTR_PASSWORD = 'password' +_LOGGER = logging.getLogger(__name__) SCHEMA_API_AUTH = vol.Schema({ vol.Required(ATTR_USERNAME): cv.string, vol.Required(ATTR_PASSWORD): cv.string, -}) + vol.Required(ATTR_ADDON): cv.string, +}, extra=vol.ALLOW_EXTRA) @callback diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py new file mode 100644 index 00000000000..c539169ebe3 --- /dev/null +++ b/homeassistant/components/hassio/const.py @@ -0,0 +1,12 @@ +"""Hass.io const variables.""" + +ATTR_DISCOVERY = 'discovery' +ATTR_ADDON = 'addon' +ATTR_NAME = 'name' +ATTR_SERVICE = 'service' +ATTR_CONFIG = 'config' +ATTR_UUID = 'uuid' +ATTR_USERNAME = 'username' +ATTR_PASSWORD = 'password' + +X_HASSIO = 'X-HASSIO-KEY' diff --git a/homeassistant/components/hassio/discovery.py b/homeassistant/components/hassio/discovery.py index 3c5242607c1..4c7c5a6597f 100644 --- a/homeassistant/components/hassio/discovery.py +++ b/homeassistant/components/hassio/discovery.py @@ -10,16 +10,12 @@ from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.components.http import HomeAssistantView from .handler import HassioAPIError +from .const import ( + ATTR_DISCOVERY, ATTR_ADDON, ATTR_NAME, ATTR_SERVICE, ATTR_CONFIG, + ATTR_UUID) _LOGGER = logging.getLogger(__name__) -ATTR_DISCOVERY = 'discovery' -ATTR_ADDON = 'addon' -ATTR_NAME = 'name' -ATTR_SERVICE = 'service' -ATTR_CONFIG = 'config' -ATTR_UUID = 'uuid' - @callback def async_setup_discovery(hass, hassio, config): diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 7c450b49bcc..91019776eeb 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -16,9 +16,9 @@ from homeassistant.components.http import ( CONF_SSL_CERTIFICATE) from homeassistant.const import CONF_TIME_ZONE, SERVER_PORT -_LOGGER = logging.getLogger(__name__) +from .const import X_HASSIO -X_HASSIO = 'X-HASSIO-KEY' +_LOGGER = logging.getLogger(__name__) class HassioAPIError(RuntimeError): diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index abd1c16ba8b..c3bd18fa9bb 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -18,9 +18,10 @@ from aiohttp.web_exceptions import HTTPBadGateway from homeassistant.const import CONTENT_TYPE_TEXT_PLAIN from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView +from .const import X_HASSIO + _LOGGER = logging.getLogger(__name__) -X_HASSIO = 'X-HASSIO-KEY' NO_TIMEOUT = re.compile( r'^(?:' diff --git a/tests/components/hassio/test_auth.py b/tests/components/hassio/test_auth.py index b3a6ae223f9..fdf3230dedc 100644 --- a/tests/components/hassio/test_auth.py +++ b/tests/components/hassio/test_auth.py @@ -19,7 +19,8 @@ async def test_login_success(hass, hassio_client): '/api/hassio_auth', json={ "username": "test", - "password": "123456" + "password": "123456", + "addon": "samba", }, headers={ HTTP_HEADER_HA_AUTH: API_PASSWORD @@ -42,7 +43,8 @@ async def test_login_error(hass, hassio_client): '/api/hassio_auth', json={ "username": "test", - "password": "123456" + "password": "123456", + "addon": "samba", }, headers={ HTTP_HEADER_HA_AUTH: API_PASSWORD @@ -83,7 +85,8 @@ async def test_login_no_username(hass, hassio_client): resp = await hassio_client.post( '/api/hassio_auth', json={ - "password": "123456" + "password": "123456", + "addon": "samba", }, headers={ HTTP_HEADER_HA_AUTH: API_PASSWORD @@ -93,3 +96,28 @@ async def test_login_no_username(hass, hassio_client): # Check we got right response assert resp.status == 400 assert not mock_login.called + + +async def test_login_success_extra(hass, hassio_client): + """Test auth with extra data.""" + await register_auth_provider(hass, {'type': 'homeassistant'}) + + with patch('homeassistant.auth.providers.homeassistant.' + 'HassAuthProvider.async_validate_login', + Mock(return_value=mock_coro())) as mock_login: + resp = await hassio_client.post( + '/api/hassio_auth', + json={ + "username": "test", + "password": "123456", + "addon": "samba", + "path": "/share", + }, + headers={ + HTTP_HEADER_HA_AUTH: API_PASSWORD + } + ) + + # Check we got right response + assert resp.status == 200 + mock_login.assert_called_with("test", "123456") From ebff253cc9e8d2bd613e0652c2db12bc6f8029e2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 11 Oct 2018 11:38:35 +0200 Subject: [PATCH 100/265] still update sensor on startup (#17319) --- homeassistant/components/sensor/template.py | 10 ++++------ tests/components/sensor/test_template.py | 21 ++++++++++++++++----- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py index dbe92c2b3ba..3fa45935617 100644 --- a/homeassistant/components/sensor/template.py +++ b/homeassistant/components/sensor/template.py @@ -137,10 +137,6 @@ class SensorTemplate(Entity): async def async_added_to_hass(self): """Register callbacks.""" - # We don't render on every update - if self._entities == MATCH_ALL: - return - @callback def template_sensor_state_listener(entity, old_state, new_state): """Handle device state changes.""" @@ -149,8 +145,10 @@ class SensorTemplate(Entity): @callback def template_sensor_startup(event): """Update template on startup.""" - async_track_state_change( - self.hass, self._entities, template_sensor_state_listener) + if self._entities != MATCH_ALL: + # Track state change only for valid templates + async_track_state_change( + self.hass, self._entities, template_sensor_state_listener) self.async_schedule_update_ha_state(True) diff --git a/tests/components/sensor/test_template.py b/tests/components/sensor/test_template.py index 159a5c34d37..b0b1ea22285 100644 --- a/tests/components/sensor/test_template.py +++ b/tests/components/sensor/test_template.py @@ -1,4 +1,5 @@ """The test for the Template sensor platform.""" +from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.setup import setup_component, async_setup_component from tests.common import get_test_home_assistant, assert_setup_component @@ -316,6 +317,8 @@ class TestTemplateSensor: async def test_no_template_match_all(hass, caplog): """Test that we do not allow sensors that match on all.""" + hass.states.async_set('sensor.test_sensor', 'startup') + await async_setup_component(hass, 'sensor', { 'sensor': { 'platform': 'template', @@ -342,7 +345,7 @@ async def test_no_template_match_all(hass, caplog): } }) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_all()) == 5 assert ('Template sensor invalid_state has no entity ids ' 'configured to track nor were we able to extract the entities to ' 'track from the value template') in caplog.text @@ -361,13 +364,21 @@ async def test_no_template_match_all(hass, caplog): assert hass.states.get('sensor.invalid_entity_picture').state == 'unknown' assert hass.states.get('sensor.invalid_friendly_name').state == 'unknown' + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + assert hass.states.get('sensor.invalid_state').state == '2' + assert hass.states.get('sensor.invalid_icon').state == 'startup' + assert hass.states.get('sensor.invalid_entity_picture').state == 'startup' + assert hass.states.get('sensor.invalid_friendly_name').state == 'startup' + hass.states.async_set('sensor.test_sensor', 'hello') await hass.async_block_till_done() - assert hass.states.get('sensor.invalid_state').state == 'unknown' - assert hass.states.get('sensor.invalid_icon').state == 'unknown' - assert hass.states.get('sensor.invalid_entity_picture').state == 'unknown' - assert hass.states.get('sensor.invalid_friendly_name').state == 'unknown' + assert hass.states.get('sensor.invalid_state').state == '2' + assert hass.states.get('sensor.invalid_icon').state == 'startup' + assert hass.states.get('sensor.invalid_entity_picture').state == 'startup' + assert hass.states.get('sensor.invalid_friendly_name').state == 'startup' await hass.helpers.entity_component.async_update_entity( 'sensor.invalid_state') From 2a35a3901e50872af32ce3abfcdeed7edf8beeed Mon Sep 17 00:00:00 2001 From: Nikolay Vasilchuk Date: Thu, 11 Oct 2018 13:53:54 +0300 Subject: [PATCH 101/265] Template Lock (#17288) * Template Lock component * Tests * CI Fix * Don't track templates if they have result in MATCH_ALL * async/await * houndci-bot review fix --- homeassistant/components/lock/template.py | 141 ++++++++++ tests/components/lock/test_template.py | 309 ++++++++++++++++++++++ 2 files changed, 450 insertions(+) create mode 100644 homeassistant/components/lock/template.py create mode 100644 tests/components/lock/test_template.py diff --git a/homeassistant/components/lock/template.py b/homeassistant/components/lock/template.py new file mode 100644 index 00000000000..e395cc508ad --- /dev/null +++ b/homeassistant/components/lock/template.py @@ -0,0 +1,141 @@ +""" +Support for locks which integrates with other components. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/lock.template/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv + +from homeassistant.core import callback +from homeassistant.components.lock import (LockDevice, PLATFORM_SCHEMA) +from homeassistant.const import ( + CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE, + EVENT_HOMEASSISTANT_START, STATE_ON, STATE_LOCKED, MATCH_ALL) +from homeassistant.exceptions import TemplateError +from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.script import Script + +_LOGGER = logging.getLogger(__name__) + +CONF_LOCK = 'lock' +CONF_UNLOCK = 'unlock' + +DEFAULT_NAME = 'Template Lock' +DEFAULT_OPTIMISTIC = False + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_LOCK): cv.SCRIPT_SCHEMA, + vol.Required(CONF_UNLOCK): cv.SCRIPT_SCHEMA, + vol.Required(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean +}) + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the Template lock.""" + name = config.get(CONF_NAME) + value_template = config.get(CONF_VALUE_TEMPLATE) + value_template.hass = hass + value_template_entity_ids = value_template.extract_entities() + + if value_template_entity_ids == MATCH_ALL: + _LOGGER.warning( + 'Template lock %s has no entity ids configured to track nor ' + 'were we able to extract the entities to track from the %s ' + 'template. This entity will only be able to be updated ' + 'manually.', name, CONF_VALUE_TEMPLATE) + + async_add_devices([TemplateLock( + hass, + name, + value_template, + value_template_entity_ids, + config.get(CONF_LOCK), + config.get(CONF_UNLOCK), + config.get(CONF_OPTIMISTIC) + )]) + + +class TemplateLock(LockDevice): + """Representation of a template lock.""" + + def __init__(self, hass, name, value_template, entity_ids, + command_lock, command_unlock, optimistic): + """Initialize the lock.""" + self._state = None + self._hass = hass + self._name = name + self._state_template = value_template + self._state_entities = entity_ids + self._command_lock = Script(hass, command_lock) + self._command_unlock = Script(hass, command_unlock) + self._optimistic = optimistic + + async def async_added_to_hass(self): + """Register callbacks.""" + @callback + def template_lock_state_listener(entity, old_state, new_state): + """Handle target device state changes.""" + self.async_schedule_update_ha_state(True) + + @callback + def template_lock_startup(event): + """Update template on startup.""" + if self._state_entities != MATCH_ALL: + # Track state change only for valid templates + async_track_state_change( + self._hass, self._state_entities, + template_lock_state_listener) + self.async_schedule_update_ha_state(True) + + self._hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, template_lock_startup) + + @property + def assumed_state(self): + """Return true if we do optimistic updates.""" + return self._optimistic + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return the name of the lock.""" + return self._name + + @property + def is_locked(self): + """Return true if lock is locked.""" + return self._state + + async def async_update(self): + """Update the state from the template.""" + try: + self._state = self._state_template.async_render().lower() in ( + 'true', STATE_ON, STATE_LOCKED) + except TemplateError as ex: + self._state = None + _LOGGER.error('Could not render template %s: %s', self._name, ex) + + async def async_lock(self, **kwargs): + """Lock the device.""" + if self._optimistic: + self._state = True + self.async_schedule_update_ha_state() + await self._command_lock.async_run() + + async def async_unlock(self, **kwargs): + """Unlock the device.""" + if self._optimistic: + self._state = False + self.async_schedule_update_ha_state() + await self._command_unlock.async_run() diff --git a/tests/components/lock/test_template.py b/tests/components/lock/test_template.py new file mode 100644 index 00000000000..7b67a68bde1 --- /dev/null +++ b/tests/components/lock/test_template.py @@ -0,0 +1,309 @@ +"""The tests for the Template lock platform.""" +import logging + +from homeassistant.core import callback +from homeassistant import setup +from homeassistant.components import lock +from homeassistant.const import STATE_ON, STATE_OFF + +from tests.common import (get_test_home_assistant, + assert_setup_component) + +_LOGGER = logging.getLogger(__name__) + + +class TestTemplateLock: + """Test the Template lock.""" + + hass = None + calls = None + # pylint: disable=invalid-name + + def setup_method(self, method): + """Set up things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.calls = [] + + @callback + def record_call(service): + """Track function calls.""" + self.calls.append(service) + + self.hass.services.register('test', 'automation', record_call) + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + + def test_template_state(self): + """Test template.""" + with assert_setup_component(1, 'lock'): + assert setup.setup_component(self.hass, 'lock', { + 'lock': { + 'platform': 'template', + 'name': 'Test template lock', + 'value_template': + "{{ states.switch.test_state.state }}", + 'lock': { + 'service': 'switch.turn_on', + 'entity_id': 'switch.test_state' + }, + 'unlock': { + 'service': 'switch.turn_off', + 'entity_id': 'switch.test_state' + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + self.hass.states.set('switch.test_state', STATE_ON) + self.hass.block_till_done() + + state = self.hass.states.get('lock.test_template_lock') + assert state.state == lock.STATE_LOCKED + + self.hass.states.set('switch.test_state', STATE_OFF) + self.hass.block_till_done() + + state = self.hass.states.get('lock.test_template_lock') + assert state.state == lock.STATE_UNLOCKED + + def test_template_state_boolean_on(self): + """Test the setting of the state with boolean on.""" + with assert_setup_component(1, 'lock'): + assert setup.setup_component(self.hass, 'lock', { + 'lock': { + 'platform': 'template', + 'value_template': + "{{ 1 == 1 }}", + 'lock': { + 'service': 'switch.turn_on', + 'entity_id': 'switch.test_state' + }, + 'unlock': { + 'service': 'switch.turn_off', + 'entity_id': 'switch.test_state' + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + state = self.hass.states.get('lock.template_lock') + assert state.state == lock.STATE_LOCKED + + def test_template_state_boolean_off(self): + """Test the setting of the state with off.""" + with assert_setup_component(1, 'lock'): + assert setup.setup_component(self.hass, 'lock', { + 'lock': { + 'platform': 'template', + 'value_template': + "{{ 1 == 2 }}", + 'lock': { + 'service': 'switch.turn_on', + 'entity_id': 'switch.test_state' + }, + 'unlock': { + 'service': 'switch.turn_off', + 'entity_id': 'switch.test_state' + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + state = self.hass.states.get('lock.template_lock') + assert state.state == lock.STATE_UNLOCKED + + def test_template_syntax_error(self): + """Test templating syntax error.""" + with assert_setup_component(0, 'lock'): + assert setup.setup_component(self.hass, 'lock', { + 'lock': { + 'platform': 'template', + 'value_template': + "{% if rubbish %}", + 'lock': { + 'service': 'switch.turn_on', + 'entity_id': 'switch.test_state' + }, + 'unlock': { + 'service': 'switch.turn_off', + 'entity_id': 'switch.test_state' + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + assert self.hass.states.all() == [] + + def test_invalid_name_does_not_create(self): + """Test invalid name.""" + with assert_setup_component(0, 'lock'): + assert setup.setup_component(self.hass, 'lock', { + 'switch': { + 'platform': 'lock', + 'name': '{{%}', + 'value_template': + "{{ rubbish }", + 'lock': { + 'service': 'switch.turn_on', + 'entity_id': 'switch.test_state' + }, + 'unlock': { + 'service': 'switch.turn_off', + 'entity_id': 'switch.test_state' + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + assert self.hass.states.all() == [] + + def test_invalid_lock_does_not_create(self): + """Test invalid lock.""" + with assert_setup_component(0, 'lock'): + assert setup.setup_component(self.hass, 'lock', { + 'lock': { + 'platform': 'template', + 'value_template': "Invalid" + } + }) + + self.hass.start() + self.hass.block_till_done() + + assert self.hass.states.all() == [] + + def test_missing_template_does_not_create(self): + """Test missing template.""" + with assert_setup_component(0, 'lock'): + assert setup.setup_component(self.hass, 'lock', { + 'lock': { + 'platform': 'template', + 'not_value_template': + "{{ states.switch.test_state.state }}", + 'lock': { + 'service': 'switch.turn_on', + 'entity_id': 'switch.test_state' + }, + 'unlock': { + 'service': 'switch.turn_off', + 'entity_id': 'switch.test_state' + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + assert self.hass.states.all() == [] + + def test_no_template_match_all(self, caplog): + """Test that we do not allow locks that match on all.""" + with assert_setup_component(1, 'lock'): + assert setup.setup_component(self.hass, 'lock', { + 'lock': { + 'platform': 'template', + 'value_template': '{{ 1 + 1 }}', + 'lock': { + 'service': 'switch.turn_on', + 'entity_id': 'switch.test_state' + }, + 'unlock': { + 'service': 'switch.turn_off', + 'entity_id': 'switch.test_state' + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + state = self.hass.states.get('lock.template_lock') + assert state.state == lock.STATE_UNLOCKED + + assert ('Template lock Template Lock has no entity ids configured ' + 'to track nor were we able to extract the entities to track ' + 'from the value_template template. This entity will only ' + 'be able to be updated manually.') in caplog.text + + self.hass.states.set('lock.template_lock', lock.STATE_LOCKED) + self.hass.block_till_done() + state = self.hass.states.get('lock.template_lock') + assert state.state == lock.STATE_LOCKED + + def test_lock_action(self): + """Test lock action.""" + assert setup.setup_component(self.hass, 'lock', { + 'lock': { + 'platform': 'template', + 'value_template': + "{{ states.switch.test_state.state }}", + 'lock': { + 'service': 'test.automation' + }, + 'unlock': { + 'service': 'switch.turn_off', + 'entity_id': 'switch.test_state' + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + self.hass.states.set('switch.test_state', STATE_OFF) + self.hass.block_till_done() + + state = self.hass.states.get('lock.template_lock') + assert state.state == lock.STATE_UNLOCKED + + self.hass.services.call(lock.DOMAIN, lock.SERVICE_LOCK, { + lock.ATTR_ENTITY_ID: 'lock.template_lock' + }) + self.hass.block_till_done() + + assert len(self.calls) == 1 + + def test_unlock_action(self): + """Test unlock action.""" + assert setup.setup_component(self.hass, 'lock', { + 'lock': { + 'platform': 'template', + 'value_template': + "{{ states.switch.test_state.state }}", + 'lock': { + 'service': 'switch.turn_on', + 'entity_id': 'switch.test_state' + }, + 'unlock': { + 'service': 'test.automation' + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + self.hass.states.set('switch.test_state', STATE_ON) + self.hass.block_till_done() + + state = self.hass.states.get('lock.template_lock') + assert state.state == lock.STATE_LOCKED + + self.hass.services.call(lock.DOMAIN, lock.SERVICE_UNLOCK, { + lock.ATTR_ENTITY_ID: 'lock.template_lock' + }) + self.hass.block_till_done() + + assert len(self.calls) == 1 From ed45dff6e89f4b9b0bacecd586d4f149d4d9db0a Mon Sep 17 00:00:00 2001 From: Karim Geiger Date: Thu, 11 Oct 2018 13:25:48 +0200 Subject: [PATCH 102/265] Implement turn_off and turn_on actions for eq3btsmart (#17168) * Implement turn_off and turn_on actions for eq3btsmart This commit implements the turn_off and turn_on methods for eq3btsmart. Turning the device off will set the thermostat to "OFF". Turning it on will set it to "AUTO". * Add missing support flags for on/off feature * Fix line length --- homeassistant/components/climate/eq3btsmart.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/climate/eq3btsmart.py b/homeassistant/components/climate/eq3btsmart.py index b88500f7fb2..bb0a9d4b810 100644 --- a/homeassistant/components/climate/eq3btsmart.py +++ b/homeassistant/components/climate/eq3btsmart.py @@ -10,7 +10,8 @@ import voluptuous as vol from homeassistant.components.climate import ( STATE_ON, STATE_OFF, STATE_AUTO, PLATFORM_SCHEMA, ClimateDevice, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE) + SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE, + SUPPORT_ON_OFF) from homeassistant.const import ( CONF_MAC, CONF_DEVICES, TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_HALVES) import homeassistant.helpers.config_validation as cv @@ -39,7 +40,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | - SUPPORT_AWAY_MODE) + SUPPORT_AWAY_MODE | SUPPORT_ON_OFF) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -150,6 +151,14 @@ class EQ3BTSmartThermostat(ClimateDevice): """Return if we are away.""" return self.current_operation == STATE_AWAY + def turn_on(self): + """Turn device on.""" + self.set_operation_mode(STATE_AUTO) + + def turn_off(self): + """Turn device off.""" + self.set_operation_mode(STATE_OFF) + @property def min_temp(self): """Return the minimum temperature.""" From 44477f3d322b3e31c7d94141b7de01faf64e507a Mon Sep 17 00:00:00 2001 From: Nikolay Vasilchuk Date: Thu, 11 Oct 2018 15:15:04 +0300 Subject: [PATCH 103/265] Logbook: filter by entity and period (#17095) * Filter logbook by entity_id * Filter logbook by period * Simple test * houndci-bot review * Tests * Test fix * Test Fix --- homeassistant/components/logbook.py | 19 ++++-- tests/components/test_logbook.py | 89 ++++++++++++++++++++++++++++- 2 files changed, 103 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 9e66c8d3aca..5cbd2b9432b 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -133,14 +133,21 @@ class LogbookView(HomeAssistantView): else: datetime = dt_util.start_of_local_day() - start_day = dt_util.as_utc(datetime) - end_day = start_day + timedelta(days=1) + period = request.query.get('period') + if period is None: + period = 1 + else: + period = int(period) + + entity_id = request.query.get('entity') + start_day = dt_util.as_utc(datetime) - timedelta(days=period - 1) + end_day = start_day + timedelta(days=period) hass = request.app['hass'] def json_events(): """Fetch events and generate JSON.""" return self.json(list( - _get_events(hass, self.config, start_day, end_day))) + _get_events(hass, self.config, start_day, end_day, entity_id))) return await hass.async_add_job(json_events) @@ -288,7 +295,7 @@ def humanify(hass, events): } -def _get_events(hass, config, start_day, end_day): +def _get_events(hass, config, start_day, end_day, entity_id=None): """Get events for a period of time.""" from homeassistant.components.recorder.models import Events, States from homeassistant.components.recorder.util import ( @@ -302,6 +309,10 @@ def _get_events(hass, config, start_day, end_day): & (Events.time_fired < end_day)) \ .filter((States.last_updated == States.last_changed) | (States.state_id.is_(None))) + + if entity_id is not None: + query = query.filter(States.entity_id == entity_id.lower()) + events = execute(query) return humanify(hass, _exclude_events(events, config)) diff --git a/tests/components/test_logbook.py b/tests/components/test_logbook.py index 8e7c2299731..3bb3ae57c68 100644 --- a/tests/components/test_logbook.py +++ b/tests/components/test_logbook.py @@ -1,7 +1,7 @@ """The tests for the logbook component.""" # pylint: disable=protected-access,invalid-name import logging -from datetime import timedelta +from datetime import (timedelta, datetime) import unittest from homeassistant.components import sun @@ -558,6 +558,93 @@ async def test_logbook_view(hass, aiohttp_client): assert response.status == 200 +async def test_logbook_view_period_entity(hass, aiohttp_client): + """Test the logbook view with period and entity.""" + await hass.async_add_job(init_recorder_component, hass) + await async_setup_component(hass, 'logbook', {}) + await hass.components.recorder.wait_connection_ready() + await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + + entity_id_test = 'switch.test' + hass.states.async_set(entity_id_test, STATE_OFF) + hass.states.async_set(entity_id_test, STATE_ON) + entity_id_second = 'switch.second' + hass.states.async_set(entity_id_second, STATE_OFF) + hass.states.async_set(entity_id_second, STATE_ON) + await hass.async_block_till_done() + await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + + client = await aiohttp_client(hass.http.app) + + # Today time 00:00:00 + start = dt_util.utcnow().date() + start_date = datetime(start.year, start.month, start.day) + + # Test today entries without filters + response = await client.get( + '/api/logbook/{}'.format(start_date.isoformat())) + assert response.status == 200 + json = await response.json() + assert len(json) == 2 + assert json[0]['entity_id'] == entity_id_test + assert json[1]['entity_id'] == entity_id_second + + # Test today entries with filter by period + response = await client.get( + '/api/logbook/{}?period=1'.format(start_date.isoformat())) + assert response.status == 200 + json = await response.json() + assert len(json) == 2 + assert json[0]['entity_id'] == entity_id_test + assert json[1]['entity_id'] == entity_id_second + + # Test today entries with filter by entity_id + response = await client.get( + '/api/logbook/{}?entity=switch.test'.format( + start_date.isoformat())) + assert response.status == 200 + json = await response.json() + assert len(json) == 1 + assert json[0]['entity_id'] == entity_id_test + + # Test entries for 3 days with filter by entity_id + response = await client.get( + '/api/logbook/{}?period=3&entity=switch.test'.format( + start_date.isoformat())) + assert response.status == 200 + json = await response.json() + assert len(json) == 1 + assert json[0]['entity_id'] == entity_id_test + + # Tomorrow time 00:00:00 + start = (dt_util.utcnow() + timedelta(days=1)).date() + start_date = datetime(start.year, start.month, start.day) + + # Test tomorrow entries without filters + response = await client.get( + '/api/logbook/{}'.format(start_date.isoformat())) + assert response.status == 200 + json = await response.json() + assert len(json) == 0 + + # Test tomorrow entries with filter by entity_id + response = await client.get( + '/api/logbook/{}?entity=switch.test'.format( + start_date.isoformat())) + assert response.status == 200 + json = await response.json() + assert len(json) == 0 + + # Test entries from tomorrow to 3 days ago with filter by entity_id + response = await client.get( + '/api/logbook/{}?period=3&entity=switch.test'.format( + start_date.isoformat())) + assert response.status == 200 + json = await response.json() + assert len(json) == 1 + assert json[0]['entity_id'] == entity_id_test + + async def test_humanify_alexa_event(hass): """Test humanifying Alexa event.""" hass.states.async_set('light.kitchen', 'on', { From 61bf4d8a29d359f8e4066c760d01beb69a504b54 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 11 Oct 2018 17:06:51 +0200 Subject: [PATCH 104/265] Add user events (#17328) --- homeassistant/auth/__init__.py | 31 +++++++++++++-- tests/auth/test_init.py | 72 ++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 3 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index e19498026d1..e584d5b70e5 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -16,6 +16,9 @@ from . import auth_store, models from .mfa_modules import auth_mfa_module_from_config, MultiFactorAuthModule from .providers import auth_provider_from_config, AuthProvider, LoginFlow +EVENT_USER_ADDED = 'user_added' +EVENT_USER_REMOVED = 'user_removed' + _LOGGER = logging.getLogger(__name__) _MfaModuleDict = Dict[str, MultiFactorAuthModule] _ProviderKey = Tuple[str, Optional[str]] @@ -126,13 +129,19 @@ class AuthManager: async def async_create_system_user(self, name: str) -> models.User: """Create a system user.""" - return await self._store.async_create_user( + user = await self._store.async_create_user( name=name, system_generated=True, is_active=True, groups=[], ) + self.hass.bus.async_fire(EVENT_USER_ADDED, { + 'user_id': user.id + }) + + return user + async def async_create_user(self, name: str) -> models.User: """Create a user.""" group = (await self._store.async_get_groups())[0] @@ -145,7 +154,13 @@ class AuthManager: if await self._user_should_be_owner(): kwargs['is_owner'] = True - return await self._store.async_create_user(**kwargs) + user = await self._store.async_create_user(**kwargs) + + self.hass.bus.async_fire(EVENT_USER_ADDED, { + 'user_id': user.id + }) + + return user async def async_get_or_create_user(self, credentials: models.Credentials) \ -> models.User: @@ -165,12 +180,18 @@ class AuthManager: info = await auth_provider.async_user_meta_for_credentials( credentials) - return await self._store.async_create_user( + user = await self._store.async_create_user( credentials=credentials, name=info.name, is_active=info.is_active, ) + self.hass.bus.async_fire(EVENT_USER_ADDED, { + 'user_id': user.id + }) + + return user + async def async_link_user(self, user: models.User, credentials: models.Credentials) -> None: """Link credentials to an existing user.""" @@ -188,6 +209,10 @@ class AuthManager: await self._store.async_remove_user(user) + self.hass.bus.async_fire(EVENT_USER_REMOVED, { + 'user_id': user.id + }) + async def async_activate_user(self, user: models.User) -> None: """Activate a user.""" await self._store.async_activate_user(user) diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py index 8fd9b8930e4..f6086db7516 100644 --- a/tests/auth/test_init.py +++ b/tests/auth/test_init.py @@ -10,6 +10,7 @@ from homeassistant import auth, data_entry_flow from homeassistant.auth import ( models as auth_models, auth_store, const as auth_const) from homeassistant.auth.const import MFA_SESSION_EXPIRATION +from homeassistant.core import callback from homeassistant.util import dt as dt_util from tests.common import ( MockUser, ensure_auth_manager_loaded, flush_store, CLIENT_ID) @@ -138,6 +139,14 @@ async def test_auth_manager_from_config_auth_modules(mock_hass): async def test_create_new_user(hass): """Test creating new user.""" + events = [] + + @callback + def user_added(event): + events.append(event) + + hass.bus.async_listen('user_added', user_added) + manager = await auth.auth_manager_from_config(hass, [{ 'type': 'insecure_example', 'users': [{ @@ -160,6 +169,10 @@ async def test_create_new_user(hass): assert user.is_owner is False assert user.name == 'Test Name' + await hass.async_block_till_done() + assert len(events) == 1 + assert events[0].data['user_id'] == user.id + async def test_login_as_existing_user(mock_hass): """Test login as existing user.""" @@ -331,6 +344,14 @@ async def test_cannot_retrieve_expired_access_token(hass): async def test_generating_system_user(hass): """Test that we can add a system user.""" + events = [] + + @callback + def user_added(event): + events.append(event) + + hass.bus.async_listen('user_added', user_added) + manager = await auth.auth_manager_from_config(hass, [], []) user = await manager.async_create_system_user('Hass.io') token = await manager.async_create_refresh_token(user) @@ -338,6 +359,10 @@ async def test_generating_system_user(hass): assert token is not None assert token.client_id is None + await hass.async_block_till_done() + assert len(events) == 1 + assert events[0].data['user_id'] == user.id + async def test_refresh_token_requires_client_for_user(hass): """Test create refresh token for a user with client_id.""" @@ -797,3 +822,50 @@ async def test_enable_mfa_for_user(hass, hass_storage): # disable mfa for user don't enabled just silent fail await manager.async_disable_user_mfa(user, 'insecure_example') + + +async def test_async_remove_user(hass): + """Test removing a user.""" + events = [] + + @callback + def user_removed(event): + events.append(event) + + hass.bus.async_listen('user_removed', user_removed) + + manager = await auth.auth_manager_from_config(hass, [{ + 'type': 'insecure_example', + 'users': [{ + 'username': 'test-user', + 'password': 'test-pass', + 'name': 'Test Name' + }] + }], []) + hass.auth = manager + ensure_auth_manager_loaded(manager) + + # Add fake user with credentials for example auth provider. + user = MockUser( + id='mock-user', + is_owner=False, + is_active=False, + name='Paulus', + ).add_to_auth_manager(manager) + user.credentials.append(auth_models.Credentials( + id='mock-id', + auth_provider_type='insecure_example', + auth_provider_id=None, + data={'username': 'test-user'}, + is_new=False, + )) + assert len(user.credentials) == 1 + + await hass.auth.async_remove_user(user) + + assert len(await manager.async_get_users()) == 0 + assert len(user.credentials) == 0 + + await hass.async_block_till_done() + assert len(events) == 1 + assert events[0].data['user_id'] == user.id From 5961f2f5772b0122f56ef555acdb481f525e3fbc Mon Sep 17 00:00:00 2001 From: emontnemery Date: Thu, 11 Oct 2018 19:14:23 +0200 Subject: [PATCH 105/265] Add support for off_delay to MQTT binary_sensor (#16993) * Add support for off_delay to MQTT binary_sensor * Fix debounce, add testcase * Make off_delay number of seconds instead of timedelta * Update mqtt.py * Fix testcase, remove CONF_OFF_DELAY from const.py --- .../components/binary_sensor/mqtt.py | 25 ++++++++- tests/components/binary_sensor/test_mqtt.py | 51 ++++++++++++++++++- 2 files changed, 72 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/binary_sensor/mqtt.py index deca5bea61a..beaeb9ce21b 100644 --- a/homeassistant/components/binary_sensor/mqtt.py +++ b/homeassistant/components/binary_sensor/mqtt.py @@ -23,11 +23,13 @@ from homeassistant.components.mqtt import ( from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect +import homeassistant.helpers.event as evt from homeassistant.helpers.typing import HomeAssistantType, ConfigType _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'MQTT Binary sensor' +CONF_OFF_DELAY = 'off_delay' CONF_UNIQUE_ID = 'unique_id' DEFAULT_PAYLOAD_OFF = 'OFF' DEFAULT_PAYLOAD_ON = 'ON' @@ -41,6 +43,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, + vol.Optional(CONF_OFF_DELAY): + vol.All(vol.Coerce(int), vol.Range(min=0)), # Integrations shouldn't never expose unique_id through configuration # this here is an exception because MQTT is a msg transport, not a protocol vol.Optional(CONF_UNIQUE_ID): cv.string, @@ -81,6 +85,7 @@ async def _async_setup_entity(hass, config, async_add_entities, config.get(CONF_DEVICE_CLASS), config.get(CONF_QOS), config.get(CONF_FORCE_UPDATE), + config.get(CONF_OFF_DELAY), config.get(CONF_PAYLOAD_ON), config.get(CONF_PAYLOAD_OFF), config.get(CONF_PAYLOAD_AVAILABLE), @@ -97,8 +102,8 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate, """Representation a binary sensor that is updated by MQTT.""" def __init__(self, name, state_topic, availability_topic, device_class, - qos, force_update, payload_on, payload_off, payload_available, - payload_not_available, value_template, + qos, force_update, off_delay, payload_on, payload_off, + payload_available, payload_not_available, value_template, unique_id: Optional[str], device_config: Optional[ConfigType], discovery_hash): """Initialize the MQTT binary sensor.""" @@ -114,9 +119,11 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate, self._payload_off = payload_off self._qos = qos self._force_update = force_update + self._off_delay = off_delay self._template = value_template self._unique_id = unique_id self._discovery_hash = discovery_hash + self._delay_listener = None async def async_added_to_hass(self): """Subscribe mqtt events.""" @@ -139,6 +146,20 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate, self._name, self._state_topic) return + if (self._state and self._off_delay is not None): + @callback + def off_delay_listener(now): + """Switch device off after a delay.""" + self._delay_listener = None + self._state = False + self.async_schedule_update_ha_state() + + if self._delay_listener is not None: + self._delay_listener() + + self._delay_listener = evt.async_call_later( + self.hass, self._off_delay, off_delay_listener) + self.async_schedule_update_ha_state() await mqtt.async_subscribe( diff --git a/tests/components/binary_sensor/test_mqtt.py b/tests/components/binary_sensor/test_mqtt.py index 0496affebf3..8e9e7dcf301 100644 --- a/tests/components/binary_sensor/test_mqtt.py +++ b/tests/components/binary_sensor/test_mqtt.py @@ -1,6 +1,8 @@ """The tests for the MQTT binary sensor platform.""" import json import unittest +from unittest.mock import Mock +from datetime import timedelta import homeassistant.core as ha from homeassistant.setup import setup_component, async_setup_component @@ -10,10 +12,12 @@ from homeassistant.components.mqtt.discovery import async_start from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE +import homeassistant.util.dt as dt_util + from tests.common import ( get_test_home_assistant, fire_mqtt_message, async_fire_mqtt_message, - mock_component, mock_mqtt_component, async_mock_mqtt_component, - MockConfigEntry) + fire_time_changed, mock_component, mock_mqtt_component, + async_mock_mqtt_component, MockConfigEntry) class TestSensorMQTT(unittest.TestCase): @@ -22,6 +26,7 @@ class TestSensorMQTT(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() + self.hass.config_entries._async_schedule_save = Mock() mock_mqtt_component(self.hass) def tearDown(self): # pylint: disable=invalid-name @@ -209,6 +214,48 @@ class TestSensorMQTT(unittest.TestCase): self.hass.block_till_done() self.assertEqual(2, len(events)) + def test_off_delay(self): + """Test off_delay option.""" + mock_component(self.hass, 'mqtt') + assert setup_component(self.hass, binary_sensor.DOMAIN, { + binary_sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'payload_on': 'ON', + 'payload_off': 'OFF', + 'off_delay': 30, + 'force_update': True + } + }) + + events = [] + + @ha.callback + def callback(event): + """Verify event got called.""" + events.append(event) + + self.hass.bus.listen(EVENT_STATE_CHANGED, callback) + + fire_mqtt_message(self.hass, 'test-topic', 'ON') + self.hass.block_till_done() + state = self.hass.states.get('binary_sensor.test') + self.assertEqual(STATE_ON, state.state) + self.assertEqual(1, len(events)) + + fire_mqtt_message(self.hass, 'test-topic', 'ON') + self.hass.block_till_done() + state = self.hass.states.get('binary_sensor.test') + self.assertEqual(STATE_ON, state.state) + self.assertEqual(2, len(events)) + + fire_time_changed(self.hass, dt_util.utcnow() + timedelta(seconds=30)) + self.hass.block_till_done() + state = self.hass.states.get('binary_sensor.test') + self.assertEqual(STATE_OFF, state.state) + self.assertEqual(3, len(events)) + async def test_unique_id(hass): """Test unique id option only creates one sensor per unique_id.""" From 61f7a39748d6e1930df58f2cfd65dd248e80d1e4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 11 Oct 2018 19:24:25 +0200 Subject: [PATCH 106/265] Add permissions foundation (#16890) * Add permission foundation * Address comments * typing * False > True * Convert more lambdas * Use constants * Remove support for False * Fix only allow True --- homeassistant/auth/auth_store.py | 26 ++- homeassistant/auth/models.py | 24 +++ homeassistant/auth/permissions.py | 252 ++++++++++++++++++++++++++++++ tests/auth/test_init.py | 1 + tests/auth/test_models.py | 34 ++++ tests/auth/test_permissions.py | 198 +++++++++++++++++++++++ tests/common.py | 6 +- 7 files changed, 532 insertions(+), 9 deletions(-) create mode 100644 homeassistant/auth/permissions.py create mode 100644 tests/auth/test_models.py create mode 100644 tests/auth/test_permissions.py diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index 572393dc444..8c328bfe13e 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -10,6 +10,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.util import dt as dt_util from . import models +from .permissions import DEFAULT_POLICY STORAGE_VERSION = 1 STORAGE_KEY = 'auth' @@ -245,12 +246,16 @@ class AuthStore: groups[group_dict['id']] = models.Group( name=group_dict['name'], id=group_dict['id'], + policy=group_dict.get('policy', DEFAULT_POLICY), ) migrate_group = None if not groups: - migrate_group = models.Group(name=INITIAL_GROUP_NAME) + migrate_group = models.Group( + name=INITIAL_GROUP_NAME, + policy=DEFAULT_POLICY + ) groups[migrate_group.id] = migrate_group for user_dict in data['users']: @@ -348,13 +353,17 @@ class AuthStore: for user in self._users.values() ] - groups = [ - { + groups = [] + for group in self._groups.values(): + g_dict = { 'name': group.name, 'id': group.id, - } - for group in self._groups.values() - ] + } # type: Dict[str, Any] + + if group.policy is not DEFAULT_POLICY: + g_dict['policy'] = group.policy + + groups.append(g_dict) credentials = [ { @@ -402,7 +411,10 @@ class AuthStore: self._users = OrderedDict() # type: Dict[str, models.User] # Add default group - all_access_group = models.Group(name=INITIAL_GROUP_NAME) + all_access_group = models.Group( + name=INITIAL_GROUP_NAME, + policy=DEFAULT_POLICY, + ) groups = OrderedDict() # type: Dict[str, models.Group] groups[all_access_group.id] = all_access_group diff --git a/homeassistant/auth/models.py b/homeassistant/auth/models.py index 7305e0e77b2..fc35f1398db 100644 --- a/homeassistant/auth/models.py +++ b/homeassistant/auth/models.py @@ -7,6 +7,7 @@ import attr from homeassistant.util import dt as dt_util +from . import permissions as perm_mdl from .util import generate_secret TOKEN_TYPE_NORMAL = 'normal' @@ -19,6 +20,7 @@ class Group: """A group.""" name = attr.ib(type=str) # type: Optional[str] + policy = attr.ib(type=perm_mdl.PolicyType) id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex) @@ -44,6 +46,28 @@ class User: type=dict, factory=dict, cmp=False ) # type: Dict[str, RefreshToken] + _permissions = attr.ib( + type=perm_mdl.PolicyPermissions, + init=False, + cmp=False, + default=None, + ) + + @property + def permissions(self) -> perm_mdl.AbstractPermissions: + """Return permissions object for user.""" + if self.is_owner: + return perm_mdl.OwnerPermissions + + if self._permissions is not None: + return self._permissions + + self._permissions = perm_mdl.PolicyPermissions( + perm_mdl.merge_policies([ + group.policy for group in self.groups])) + + return self._permissions + @attr.s(slots=True) class RefreshToken: diff --git a/homeassistant/auth/permissions.py b/homeassistant/auth/permissions.py new file mode 100644 index 00000000000..82de61da7f9 --- /dev/null +++ b/homeassistant/auth/permissions.py @@ -0,0 +1,252 @@ +"""Permissions for Home Assistant.""" +from typing import ( # noqa: F401 + cast, Any, Callable, Dict, List, Mapping, Set, Tuple, Union) + +import voluptuous as vol + +from homeassistant.core import State + +CategoryType = Union[Mapping[str, 'CategoryType'], bool, None] +PolicyType = Mapping[str, CategoryType] + + +# Default policy if group has no policy applied. +DEFAULT_POLICY = { + "entities": True +} # type: PolicyType + +CAT_ENTITIES = 'entities' +ENTITY_DOMAINS = 'domains' +ENTITY_ENTITY_IDS = 'entity_ids' + +VALUES_SCHEMA = vol.Any(True, vol.Schema({ + str: True +})) + +ENTITY_POLICY_SCHEMA = vol.Any(True, vol.Schema({ + vol.Optional(ENTITY_DOMAINS): VALUES_SCHEMA, + vol.Optional(ENTITY_ENTITY_IDS): VALUES_SCHEMA, +})) + +POLICY_SCHEMA = vol.Schema({ + vol.Optional(CAT_ENTITIES): ENTITY_POLICY_SCHEMA +}) + + +class AbstractPermissions: + """Default permissions class.""" + + def check_entity(self, entity_id: str, *keys: str) -> bool: + """Test if we can access entity.""" + raise NotImplementedError + + def filter_states(self, states: List[State]) -> List[State]: + """Filter a list of states for what the user is allowed to see.""" + raise NotImplementedError + + +class PolicyPermissions(AbstractPermissions): + """Handle permissions.""" + + def __init__(self, policy: PolicyType) -> None: + """Initialize the permission class.""" + self._policy = policy + self._compiled = {} # type: Dict[str, Callable[..., bool]] + + def check_entity(self, entity_id: str, *keys: str) -> bool: + """Test if we can access entity.""" + func = self._policy_func(CAT_ENTITIES, _compile_entities) + return func(entity_id, keys) + + def filter_states(self, states: List[State]) -> List[State]: + """Filter a list of states for what the user is allowed to see.""" + func = self._policy_func(CAT_ENTITIES, _compile_entities) + keys = ('read',) + return [entity for entity in states if func(entity.entity_id, keys)] + + def _policy_func(self, category: str, + compile_func: Callable[[CategoryType], Callable]) \ + -> Callable[..., bool]: + """Get a policy function.""" + func = self._compiled.get(category) + + if func: + return func + + func = self._compiled[category] = compile_func( + self._policy.get(category)) + return func + + def __eq__(self, other: Any) -> bool: + """Equals check.""" + # pylint: disable=protected-access + return (isinstance(other, PolicyPermissions) and + other._policy == self._policy) + + +class _OwnerPermissions(AbstractPermissions): + """Owner permissions.""" + + # pylint: disable=no-self-use + + def check_entity(self, entity_id: str, *keys: str) -> bool: + """Test if we can access entity.""" + return True + + def filter_states(self, states: List[State]) -> List[State]: + """Filter a list of states for what the user is allowed to see.""" + return states + + +OwnerPermissions = _OwnerPermissions() # pylint: disable=invalid-name + + +def _compile_entities(policy: CategoryType) \ + -> Callable[[str, Tuple[str]], bool]: + """Compile policy into a function that tests policy.""" + # None, Empty Dict, False + if not policy: + def apply_policy_deny_all(entity_id: str, keys: Tuple[str]) -> bool: + """Decline all.""" + return False + + return apply_policy_deny_all + + if policy is True: + def apply_policy_allow_all(entity_id: str, keys: Tuple[str]) -> bool: + """Approve all.""" + return True + + return apply_policy_allow_all + + assert isinstance(policy, dict) + + domains = policy.get(ENTITY_DOMAINS) + entity_ids = policy.get(ENTITY_ENTITY_IDS) + + funcs = [] # type: List[Callable[[str, Tuple[str]], Union[None, bool]]] + + # The order of these functions matter. The more precise are at the top. + # If a function returns None, they cannot handle it. + # If a function returns a boolean, that's the result to return. + + # Setting entity_ids to a boolean is final decision for permissions + # So return right away. + if isinstance(entity_ids, bool): + def apply_entity_id_policy(entity_id: str, keys: Tuple[str]) -> bool: + """Test if allowed entity_id.""" + return entity_ids # type: ignore + + return apply_entity_id_policy + + if entity_ids is not None: + def allowed_entity_id(entity_id: str, keys: Tuple[str]) \ + -> Union[None, bool]: + """Test if allowed entity_id.""" + return entity_ids.get(entity_id) # type: ignore + + funcs.append(allowed_entity_id) + + if isinstance(domains, bool): + def allowed_domain(entity_id: str, keys: Tuple[str]) \ + -> Union[None, bool]: + """Test if allowed domain.""" + return domains + + funcs.append(allowed_domain) + + elif domains is not None: + def allowed_domain(entity_id: str, keys: Tuple[str]) \ + -> Union[None, bool]: + """Test if allowed domain.""" + domain = entity_id.split(".", 1)[0] + return domains.get(domain) # type: ignore + + funcs.append(allowed_domain) + + # Can happen if no valid subcategories specified + if not funcs: + def apply_policy_deny_all_2(entity_id: str, keys: Tuple[str]) -> bool: + """Decline all.""" + return False + + return apply_policy_deny_all_2 + + if len(funcs) == 1: + func = funcs[0] + + def apply_policy_func(entity_id: str, keys: Tuple[str]) -> bool: + """Apply a single policy function.""" + return func(entity_id, keys) is True + + return apply_policy_func + + def apply_policy_funcs(entity_id: str, keys: Tuple[str]) -> bool: + """Apply several policy functions.""" + for func in funcs: + result = func(entity_id, keys) + if result is not None: + return result + return False + + return apply_policy_funcs + + +def merge_policies(policies: List[PolicyType]) -> PolicyType: + """Merge policies.""" + new_policy = {} # type: Dict[str, CategoryType] + seen = set() # type: Set[str] + for policy in policies: + for category in policy: + if category in seen: + continue + seen.add(category) + new_policy[category] = _merge_policies([ + policy.get(category) for policy in policies]) + cast(PolicyType, new_policy) + return new_policy + + +def _merge_policies(sources: List[CategoryType]) -> CategoryType: + """Merge a policy.""" + # When merging policies, the most permissive wins. + # This means we order it like this: + # True > Dict > None + # + # True: allow everything + # Dict: specify more granular permissions + # None: no opinion + # + # If there are multiple sources with a dict as policy, we recursively + # merge each key in the source. + + policy = None # type: CategoryType + seen = set() # type: Set[str] + for source in sources: + if source is None: + continue + + # A source that's True will always win. Shortcut return. + if source is True: + return True + + assert isinstance(source, dict) + + if policy is None: + policy = {} + + assert isinstance(policy, dict) + + for key in source: + if key in seen: + continue + seen.add(key) + + key_sources = [] + for src in sources: + if isinstance(src, dict): + key_sources.append(src.get(key)) + + policy[key] = _merge_policies(key_sources) + + return policy diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py index f6086db7516..4357ba1b1de 100644 --- a/tests/auth/test_init.py +++ b/tests/auth/test_init.py @@ -302,6 +302,7 @@ async def test_saving_loading(hass, hass_storage): store2 = auth_store.AuthStore(hass) users = await store2.async_get_users() assert len(users) == 1 + assert users[0].permissions == user.permissions assert users[0] == user assert len(users[0].refresh_tokens) == 2 for r_token in users[0].refresh_tokens.values(): diff --git a/tests/auth/test_models.py b/tests/auth/test_models.py new file mode 100644 index 00000000000..c84bdc7390b --- /dev/null +++ b/tests/auth/test_models.py @@ -0,0 +1,34 @@ +"""Tests for the auth models.""" +from homeassistant.auth import models, permissions + + +def test_owner_fetching_owner_permissions(): + """Test we fetch the owner permissions for an owner user.""" + group = models.Group(name="Test Group", policy={}) + owner = models.User(name="Test User", groups=[group], is_owner=True) + assert owner.permissions is permissions.OwnerPermissions + + +def test_permissions_merged(): + """Test we merge the groups permissions.""" + group = models.Group(name="Test Group", policy={ + 'entities': { + 'domains': { + 'switch': True + } + } + }) + group2 = models.Group(name="Test Group", policy={ + 'entities': { + 'entity_ids': { + 'light.kitchen': True + } + } + }) + user = models.User(name="Test User", groups=[group, group2]) + # Make sure we cache instance + assert user.permissions is user.permissions + + assert user.permissions.check_entity('switch.bla') is True + assert user.permissions.check_entity('light.kitchen') is True + assert user.permissions.check_entity('light.not_kitchen') is False diff --git a/tests/auth/test_permissions.py b/tests/auth/test_permissions.py new file mode 100644 index 00000000000..71582dc281d --- /dev/null +++ b/tests/auth/test_permissions.py @@ -0,0 +1,198 @@ +"""Tests for the auth permission system.""" +import pytest +import voluptuous as vol + +from homeassistant.core import State +from homeassistant.auth import permissions + + +def test_entities_none(): + """Test entity ID policy.""" + policy = None + compiled = permissions._compile_entities(policy) + assert compiled('light.kitchen', []) is False + + +def test_entities_empty(): + """Test entity ID policy.""" + policy = {} + permissions.ENTITY_POLICY_SCHEMA(policy) + compiled = permissions._compile_entities(policy) + assert compiled('light.kitchen', []) is False + + +def test_entities_false(): + """Test entity ID policy.""" + policy = False + with pytest.raises(vol.Invalid): + permissions.ENTITY_POLICY_SCHEMA(policy) + + +def test_entities_true(): + """Test entity ID policy.""" + policy = True + permissions.ENTITY_POLICY_SCHEMA(policy) + compiled = permissions._compile_entities(policy) + assert compiled('light.kitchen', []) is True + + +def test_entities_domains_true(): + """Test entity ID policy.""" + policy = { + 'domains': True + } + permissions.ENTITY_POLICY_SCHEMA(policy) + compiled = permissions._compile_entities(policy) + assert compiled('light.kitchen', []) is True + + +def test_entities_domains_domain_true(): + """Test entity ID policy.""" + policy = { + 'domains': { + 'light': True + } + } + permissions.ENTITY_POLICY_SCHEMA(policy) + compiled = permissions._compile_entities(policy) + assert compiled('light.kitchen', []) is True + assert compiled('switch.kitchen', []) is False + + +def test_entities_domains_domain_false(): + """Test entity ID policy.""" + policy = { + 'domains': { + 'light': False + } + } + with pytest.raises(vol.Invalid): + permissions.ENTITY_POLICY_SCHEMA(policy) + + +def test_entities_entity_ids_true(): + """Test entity ID policy.""" + policy = { + 'entity_ids': True + } + permissions.ENTITY_POLICY_SCHEMA(policy) + compiled = permissions._compile_entities(policy) + assert compiled('light.kitchen', []) is True + + +def test_entities_entity_ids_false(): + """Test entity ID policy.""" + policy = { + 'entity_ids': False + } + with pytest.raises(vol.Invalid): + permissions.ENTITY_POLICY_SCHEMA(policy) + + +def test_entities_entity_ids_entity_id_true(): + """Test entity ID policy.""" + policy = { + 'entity_ids': { + 'light.kitchen': True + } + } + permissions.ENTITY_POLICY_SCHEMA(policy) + compiled = permissions._compile_entities(policy) + assert compiled('light.kitchen', []) is True + assert compiled('switch.kitchen', []) is False + + +def test_entities_entity_ids_entity_id_false(): + """Test entity ID policy.""" + policy = { + 'entity_ids': { + 'light.kitchen': False + } + } + with pytest.raises(vol.Invalid): + permissions.ENTITY_POLICY_SCHEMA(policy) + + +def test_policy_perm_filter_states(): + """Test filtering entitites.""" + states = [ + State('light.kitchen', 'on'), + State('light.living_room', 'off'), + State('light.balcony', 'on'), + ] + perm = permissions.PolicyPermissions({ + 'entities': { + 'entity_ids': { + 'light.kitchen': True, + 'light.balcony': True, + } + } + }) + filtered = perm.filter_states(states) + assert len(filtered) == 2 + assert filtered == [states[0], states[2]] + + +def test_owner_permissions(): + """Test owner permissions access all.""" + assert permissions.OwnerPermissions.check_entity('light.kitchen') + states = [ + State('light.kitchen', 'on'), + State('light.living_room', 'off'), + State('light.balcony', 'on'), + ] + assert permissions.OwnerPermissions.filter_states(states) == states + + +def test_default_policy_allow_all(): + """Test that the default policy is to allow all entity actions.""" + perm = permissions.PolicyPermissions(permissions.DEFAULT_POLICY) + assert perm.check_entity('light.kitchen') + states = [ + State('light.kitchen', 'on'), + State('light.living_room', 'off'), + State('light.balcony', 'on'), + ] + assert perm.filter_states(states) == states + + +def test_merging_permissions_true_rules_dict(): + """Test merging policy with two entities.""" + policy1 = { + 'something_else': True, + 'entities': { + 'entity_ids': { + 'light.kitchen': True, + } + } + } + policy2 = { + 'entities': { + 'entity_ids': True + } + } + assert permissions.merge_policies([policy1, policy2]) == { + 'something_else': True, + 'entities': { + 'entity_ids': True + } + } + + +def test_merging_permissions_multiple_subcategories(): + """Test merging policy with two entities.""" + policy1 = { + 'entities': None + } + policy2 = { + 'entities': { + 'entity_ids': True, + } + } + policy3 = { + 'entities': True + } + assert permissions.merge_policies([policy1, policy2]) == policy2 + assert permissions.merge_policies([policy1, policy3]) == policy3 + + assert permissions.merge_policies([policy2, policy3]) == policy3 diff --git a/tests/common.py b/tests/common.py index ce80746be4e..44f934e4cb3 100644 --- a/tests/common.py +++ b/tests/common.py @@ -348,10 +348,12 @@ def mock_device_registry(hass, mock_entries=None): class MockGroup(auth_models.Group): """Mock a group in Home Assistant.""" - def __init__(self, id=None, name='Mock Group'): + def __init__(self, id=None, name='Mock Group', + policy=auth_store.DEFAULT_POLICY): """Mock a group.""" kwargs = { - 'name': name + 'name': name, + 'policy': policy, } if id is not None: kwargs['id'] = id From 6df3c480b3afe4a0ac35ff632e86faf2982be8b1 Mon Sep 17 00:00:00 2001 From: Mister Wil <1091741+MisterWil@users.noreply.github.com> Date: Thu, 11 Oct 2018 13:00:51 -0700 Subject: [PATCH 107/265] Bump version of abodepy to 0.14.0 (#17336) --- homeassistant/components/abode.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/abode.py b/homeassistant/components/abode.py index 64bedb4ac7c..99bc026a532 100644 --- a/homeassistant/components/abode.py +++ b/homeassistant/components/abode.py @@ -18,7 +18,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['abodepy==0.13.1'] +REQUIREMENTS = ['abodepy==0.14.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 39aaebac11a..63f2430a330 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -82,7 +82,7 @@ WazeRouteCalculator==0.6 YesssSMS==0.2.3 # homeassistant.components.abode -abodepy==0.13.1 +abodepy==0.14.0 # homeassistant.components.media_player.frontier_silicon afsapi==0.0.4 From 0ea5a73e8dbbd65067370cfa99a142b577c0a08f Mon Sep 17 00:00:00 2001 From: Zhong Jianxin Date: Fri, 12 Oct 2018 14:34:32 +0800 Subject: [PATCH 108/265] Fix motion sensor in Aqara LAN protocol V2 (#17240) --- .../components/binary_sensor/xiaomi_aqara.py | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/binary_sensor/xiaomi_aqara.py b/homeassistant/components/binary_sensor/xiaomi_aqara.py index 2b4934ff824..e082c886f03 100644 --- a/homeassistant/components/binary_sensor/xiaomi_aqara.py +++ b/homeassistant/components/binary_sensor/xiaomi_aqara.py @@ -4,6 +4,8 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.xiaomi_aqara import (PY_XIAOMI_GATEWAY, XiaomiDevice) +from homeassistant.core import callback +from homeassistant.helpers.event import async_call_later _LOGGER = logging.getLogger(__name__) @@ -153,6 +155,7 @@ class XiaomiMotionSensor(XiaomiBinarySensor): """Initialize the XiaomiMotionSensor.""" self._hass = hass self._no_motion_since = 0 + self._unsub_set_no_motion = None if 'proto' not in device or int(device['proto'][0:1]) == 1: data_key = 'status' else: @@ -167,6 +170,13 @@ class XiaomiMotionSensor(XiaomiBinarySensor): attrs.update(super().device_state_attributes) return attrs + @callback + def _async_set_no_motion(self, now): + """Set state to False.""" + self._unsub_set_no_motion = None + self._state = False + self.async_schedule_update_ha_state() + def parse_data(self, data, raw_data): """Parse data sent by gateway.""" if raw_data['cmd'] == 'heartbeat': @@ -188,11 +198,20 @@ class XiaomiMotionSensor(XiaomiBinarySensor): return False if value == MOTION: - self._should_poll = True - if self.entity_id is not None: - self._hass.bus.fire('motion', { - 'entity_id': self.entity_id - }) + if self._data_key == 'motion_status': + if self._unsub_set_no_motion: + self._unsub_set_no_motion() + self._unsub_set_no_motion = async_call_later( + self._hass, + 180, + self._async_set_no_motion + ) + else: + self._should_poll = True + if self.entity_id is not None: + self._hass.bus.fire('motion', { + 'entity_id': self.entity_id + }) self._no_motion_since = 0 if self._state: From 1c23a36f46f276aef36b43eb364eb7ad2ec0cce1 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Thu, 11 Oct 2018 23:40:45 -0700 Subject: [PATCH 109/265] Set botvac availability (#17350) * Set botvac availability * Lint * Reduce availability calls per review comments --- homeassistant/components/vacuum/neato.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vacuum/neato.py b/homeassistant/components/vacuum/neato.py index 29db94de762..aa446f8cd20 100644 --- a/homeassistant/components/vacuum/neato.py +++ b/homeassistant/components/vacuum/neato.py @@ -64,6 +64,8 @@ class NeatoConnectedVacuum(StateVacuumDevice): self.clean_battery_end = None self.clean_suspension_charge_count = None self.clean_suspension_time = None + self._available = False + self._battery_level = None def update(self): """Update the states of Neato Vacuums.""" @@ -71,12 +73,12 @@ class NeatoConnectedVacuum(StateVacuumDevice): self.neato.update_robots() try: self._state = self.robot.state + self._available = True except (requests.exceptions.ConnectionError, requests.exceptions.HTTPError) as ex: _LOGGER.warning("Neato connection error: %s", ex) self._state = None - self._clean_state = STATE_ERROR - self._status_state = 'Robot Offline' + self._available = False return _LOGGER.debug('self._state=%s', self._state) if self._state['state'] == 1: @@ -127,6 +129,8 @@ class NeatoConnectedVacuum(StateVacuumDevice): self.clean_battery_end = ( self._mapdata[self.robot.serial]['maps'][0]['run_charge_at_end']) + self._battery_level = self._state['details']['charge'] + @property def name(self): """Return the name of the device.""" @@ -140,7 +144,12 @@ class NeatoConnectedVacuum(StateVacuumDevice): @property def battery_level(self): """Return the battery level of the vacuum cleaner.""" - return self._state['details']['charge'] + return self._battery_level + + @property + def available(self): + """Return if the robot is available.""" + return self._available @property def state(self): From 7bb60068d7a38fd45b20ecb95dca920bcee9043e Mon Sep 17 00:00:00 2001 From: shred86 <32663154+shred86@users.noreply.github.com> Date: Thu, 11 Oct 2018 22:47:14 -0800 Subject: [PATCH 110/265] Color control for Abode RGB lights (#17347) * Color control support for Abode lights * Updated add_devices to add_entities * Update line length * Changed elif to if for pylint warning --- homeassistant/components/light/abode.py | 33 ++++++++++++++++--------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/light/abode.py b/homeassistant/components/light/abode.py index 69314b63a4b..397d61f3073 100644 --- a/homeassistant/components/light/abode.py +++ b/homeassistant/components/light/abode.py @@ -8,9 +8,10 @@ import logging from math import ceil from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_HS_COLOR, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light) -import homeassistant.util.color as color_util + ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_COLOR_TEMP, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, Light) +from homeassistant.util.color import ( + color_temperature_kelvin_to_mired, color_temperature_mired_to_kelvin) DEPENDENCIES = ['abode'] @@ -45,10 +46,13 @@ class AbodeLight(AbodeDevice, Light): def turn_on(self, **kwargs): """Turn on the light.""" - if (ATTR_HS_COLOR in kwargs and - self._device.is_dimmable and self._device.has_color): - self._device.set_color(color_util.color_hs_to_RGB( - *kwargs[ATTR_HS_COLOR])) + if ATTR_COLOR_TEMP in kwargs and self._device.is_color_capable: + self._device.set_color_temp( + int(color_temperature_mired_to_kelvin( + kwargs[ATTR_COLOR_TEMP]))) + + if ATTR_HS_COLOR in kwargs and self._device.is_color_capable: + self._device.set_color(kwargs[ATTR_HS_COLOR]) if ATTR_BRIGHTNESS in kwargs and self._device.is_dimmable: # Convert HASS brightness (0-255) to Abode brightness (0-99) @@ -77,18 +81,23 @@ class AbodeLight(AbodeDevice, Light): # Convert Abode brightness (0-99) to HASS brightness (0-255) return ceil(brightness * 255 / 99.0) + @property + def color_temp(self): + """Return the color temp of the light.""" + if self._device.has_color: + return color_temperature_kelvin_to_mired(self._device.color_temp) + @property def hs_color(self): """Return the color of the light.""" - if self._device.is_dimmable and self._device.has_color: - return color_util.color_RGB_to_hs(*self._device.color) + if self._device.has_color: + return self._device.color @property def supported_features(self): """Flag supported features.""" - if self._device.is_dimmable and self._device.has_color: - return SUPPORT_BRIGHTNESS | SUPPORT_COLOR + if self._device.is_dimmable and self._device.is_color_capable: + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_COLOR_TEMP if self._device.is_dimmable: return SUPPORT_BRIGHTNESS - return 0 From b2789d988344e61a05e72f487bba7868697760e1 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Fri, 12 Oct 2018 08:51:16 +0200 Subject: [PATCH 111/265] Support abbreviations in discovery topic (#16635) * Support abbreviations in discovery topic * Add abbreviations for all words. Add testcase. Add missing docstring. * Add missing abbreviations * Support topic prefix * Update test case * Restrict topic prefix * Fix merge * Simplify abbreviations expanding, assume TOPIC_PREFIX is one character long * Support abbreviated keys instead of words * Remove redundant abbreviations * Remove extra spaces in abbreviation list * Make topic prefix less restrictive * Make topix prefix a bit more restrictive again --- homeassistant/components/mqtt/discovery.py | 139 +++++++++++++++++++-- tests/components/mqtt/test_discovery.py | 34 +++++ 2 files changed, 165 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 62f500d9952..b8c8627c038 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -58,6 +58,114 @@ CONFIG_ENTRY_IS_SETUP = 'mqtt_config_entry_is_setup' MQTT_DISCOVERY_UPDATED = 'mqtt_discovery_updated_{}' MQTT_DISCOVERY_NEW = 'mqtt_discovery_new_{}_{}' +TOPIC_BASE = '~' + +ABBREVIATIONS = { + 'aux_cmd_t': 'aux_command_topic', + 'aux_stat_tpl': 'aux_state_template', + 'aux_stat_t': 'aux_state_topic', + 'avty_t': 'availability_topic', + 'away_mode_cmd_t': 'away_mode_command_topic', + 'away_mode_stat_tpl': 'away_mode_state_template', + 'away_mode_stat_t': 'away_mode_state_topic', + 'bri_cmd_t': 'brightness_command_topic', + 'bri_scl': 'brightness_scale', + 'bri_stat_t': 'brightness_state_topic', + 'bri_val_tpl': 'brightness_value_template', + 'clr_temp_cmd_t': 'color_temp_command_topic', + 'clr_temp_stat_t': 'color_temp_state_topic', + 'clr_temp_val_tpl': 'color_temp_value_template', + 'cmd_t': 'command_topic', + 'curr_temp_t': 'current_temperature_topic', + 'dev_cla': 'device_class', + 'fx_cmd_t': 'effect_command_topic', + 'fx_list': 'effect_list', + 'fx_stat_t': 'effect_state_topic', + 'fx_val_tpl': 'effect_value_template', + 'exp_aft': 'expire_after', + 'fan_mode_cmd_t': 'fan_mode_command_topic', + 'fan_mode_stat_tpl': 'fan_mode_state_template', + 'fan_mode_stat_t': 'fan_mode_state_topic', + 'frc_upd': 'force_update', + 'hold_cmd_t': 'hold_command_topic', + 'hold_stat_tpl': 'hold_state_template', + 'hold_stat_t': 'hold_state_topic', + 'ic': 'icon', + 'init': 'initial', + 'json_attr': 'json_attributes', + 'max_temp': 'max_temp', + 'min_temp': 'min_temp', + 'mode_cmd_t': 'mode_command_topic', + 'mode_stat_tpl': 'mode_state_template', + 'mode_stat_t': 'mode_state_topic', + 'name': 'name', + 'on_cmd_type': 'on_command_type', + 'opt': 'optimistic', + 'osc_cmd_t': 'oscillation_command_topic', + 'osc_stat_t': 'oscillation_state_topic', + 'osc_val_tpl': 'oscillation_value_template', + 'pl_arm_away': 'payload_arm_away', + 'pl_arm_home': 'payload_arm_home', + 'pl_avail': 'payload_available', + 'pl_cls': 'payload_close', + 'pl_disarm': 'payload_disarm', + 'pl_hi_spd': 'payload_high_speed', + 'pl_lock': 'payload_lock', + 'pl_lo_spd': 'payload_low_speed', + 'pl_med_spd': 'payload_medium_speed', + 'pl_not_avail': 'payload_not_available', + 'pl_off': 'payload_off', + 'pl_on': 'payload_on', + 'pl_open': 'payload_open', + 'pl_osc_off': 'payload_oscillation_off', + 'pl_osc_on': 'payload_oscillation_on', + 'pl_stop': 'payload_stop', + 'pl_unlk': 'payload_unlock', + 'pow_cmd_t': 'power_command_topic', + 'ret': 'retain', + 'rgb_cmd_tpl': 'rgb_command_template', + 'rgb_cmd_t': 'rgb_command_topic', + 'rgb_stat_t': 'rgb_state_topic', + 'rgb_val_tpl': 'rgb_value_template', + 'send_if_off': 'send_if_off', + 'set_pos_tpl': 'set_position_template', + 'set_pos_t': 'set_position_topic', + 'spd_cmd_t': 'speed_command_topic', + 'spd_stat_t': 'speed_state_topic', + 'spd_val_tpl': 'speed_value_template', + 'spds': 'speeds', + 'stat_clsd': 'state_closed', + 'stat_off': 'state_off', + 'stat_on': 'state_on', + 'stat_open': 'state_open', + 'stat_t': 'state_topic', + 'stat_val_tpl': 'state_value_template', + 'swing_mode_cmd_t': 'swing_mode_command_topic', + 'swing_mode_stat_tpl': 'swing_mode_state_template', + 'swing_mode_stat_t': 'swing_mode_state_topic', + 'temp_cmd_t': 'temperature_command_topic', + 'temp_stat_tpl': 'temperature_state_template', + 'temp_stat_t': 'temperature_state_topic', + 'tilt_clsd_val': 'tilt_closed_value', + 'tilt_cmd_t': 'tilt_command_topic', + 'tilt_inv_stat': 'tilt_invert_state', + 'tilt_max': 'tilt_max', + 'tilt_min': 'tilt_min', + 'tilt_opnd_val': 'tilt_opened_value', + 'tilt_status_opt': 'tilt_status_optimistic', + 'tilt_status_t': 'tilt_status_topic', + 't': 'topic', + 'uniq_id': 'unique_id', + 'unit_of_meas': 'unit_of_measurement', + 'val_tpl': 'value_template', + 'whit_val_cmd_t': 'white_value_command_topic', + 'whit_val_stat_t': 'white_value_state_topic', + 'whit_val_tpl': 'white_value_template', + 'xy_cmd_t': 'xy_command_topic', + 'xy_stat_t': 'xy_state_topic', + 'xy_val_tpl': 'xy_value_template', +} + async def async_start(hass: HomeAssistantType, discovery_topic, hass_config, config_entry=None) -> bool: @@ -75,6 +183,29 @@ async def async_start(hass: HomeAssistantType, discovery_topic, hass_config, _LOGGER.warning("Component %s is not supported", component) return + if payload: + try: + payload = json.loads(payload) + except ValueError: + _LOGGER.warning("Unable to parse JSON %s: '%s'", + object_id, payload) + return + + payload = dict(payload) + + for key in list(payload.keys()): + abbreviated_key = key + key = ABBREVIATIONS.get(key, key) + payload[key] = payload.pop(abbreviated_key) + + if TOPIC_BASE in payload: + base = payload[TOPIC_BASE] + for key, value in payload.items(): + if value[0] == TOPIC_BASE and key.endswith('_topic'): + payload[key] = "{}{}".format(base, value[1:]) + if value[-1] == TOPIC_BASE and key.endswith('_topic'): + payload[key] = "{}{}".format(value[:-1], base) + # If present, the node_id will be included in the discovered object id discovery_id = '_'.join((node_id, object_id)) if node_id else object_id @@ -91,14 +222,6 @@ async def async_start(hass: HomeAssistantType, discovery_topic, hass_config, hass, MQTT_DISCOVERY_UPDATED.format(discovery_hash), payload) elif payload: # Add component - try: - payload = json.loads(payload) - except ValueError: - _LOGGER.warning("Unable to parse JSON %s: '%s'", - object_id, payload) - return - - payload = dict(payload) platform = payload.get(CONF_PLATFORM, 'mqtt') if platform not in ALLOWED_PLATFORMS.get(component, []): _LOGGER.warning("Platform %s (component %s) is not allowed", diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 36b022de7a6..dd3ab2e6f7a 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -5,6 +5,7 @@ from unittest.mock import patch from homeassistant.components import mqtt from homeassistant.components.mqtt.discovery import async_start, \ ALREADY_DISCOVERED +from homeassistant.const import STATE_ON, STATE_OFF from tests.common import async_fire_mqtt_message, mock_coro, MockConfigEntry @@ -208,3 +209,36 @@ def test_non_duplicate_discovery(hass, mqtt_mock, caplog): assert state_duplicate is None assert 'Component has already been discovered: ' \ 'binary_sensor bla' in caplog.text + + +@asyncio.coroutine +def test_discovery_expansion(hass, mqtt_mock, caplog): + """Test expansion of abbreviated discovery payload.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + + yield from async_start(hass, 'homeassistant', {}, entry) + + data = ( + '{ "~": "some/base/topic",' + ' "name": "DiscoveryExpansionTest1",' + ' "stat_t": "test_topic/~",' + ' "cmd_t": "~/test_topic" }' + ) + + async_fire_mqtt_message( + hass, 'homeassistant/switch/bla/config', data) + yield from hass.async_block_till_done() + + state = hass.states.get('switch.DiscoveryExpansionTest1') + assert state is not None + assert state.name == 'DiscoveryExpansionTest1' + assert ('switch', 'bla') in hass.data[ALREADY_DISCOVERED] + assert state.state == STATE_OFF + + async_fire_mqtt_message(hass, 'test_topic/some/base/topic', + 'ON') + yield from hass.async_block_till_done() + yield from hass.async_block_till_done() + + state = hass.states.get('switch.DiscoveryExpansionTest1') + assert state.state == STATE_ON From 241d87e9d3a77ba7179c1ba11553db57d8ec2d48 Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Fri, 12 Oct 2018 09:30:35 +0200 Subject: [PATCH 112/265] Add exception handling to dnsip sensor (#17332) * Add exception handling to dnsip sensor * Refactor import * Fix exception --- homeassistant/components/sensor/dnsip.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/dnsip.py b/homeassistant/components/sensor/dnsip.py index 7b0d54cd934..c3ec5fd4ce2 100644 --- a/homeassistant/components/sensor/dnsip.py +++ b/homeassistant/components/sensor/dnsip.py @@ -87,8 +87,13 @@ class WanIpSensor(Entity): async def async_update(self): """Get the current DNS IP address for hostname.""" - response = await self.resolver.query(self.hostname, - self.querytype) + from aiodns.error import DNSError + try: + response = await self.resolver.query(self.hostname, + self.querytype) + except DNSError as err: + _LOGGER.warning("Exception while resolving host: %s", err) + response = None if response: self._state = response[0].host else: From cb3d62eeeffe8cc965104d6e78f6f9cce4a3b166 Mon Sep 17 00:00:00 2001 From: Martin Mois Date: Fri, 12 Oct 2018 09:36:52 +0200 Subject: [PATCH 113/265] notify.homematic (#16973) * Add notify.homematic_signalgen * Update homematic_signalgen.py, test_homematic_signalgen.py * Added new files to .coveragerc * Fixed review comments from houndci-bot * Fixed pylint errors * Regenerate requirements_test_all.txt by script/gen_requirements_all.py * Fix flake8 warnings * Renamed notify.homematic_signalgen to notify.homematic and made it generic * Update .coveragerc and requirements_test_all.txt * Removed the terms signal generator from the sources. --- .coveragerc | 1 + homeassistant/components/notify/homematic.py | 74 +++++++++++++++++++ requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/notify/test_homematic.py | 78 ++++++++++++++++++++ 5 files changed, 157 insertions(+) create mode 100644 homeassistant/components/notify/homematic.py create mode 100644 tests/components/notify/test_homematic.py diff --git a/.coveragerc b/.coveragerc index a4f25bd73f8..8e9ed417d7b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -610,6 +610,7 @@ omit = homeassistant/components/notify/gntp.py homeassistant/components/notify/group.py homeassistant/components/notify/hipchat.py + homeassistant/components/notify/homematic.py homeassistant/components/notify/instapush.py homeassistant/components/notify/kodi.py homeassistant/components/notify/lannouncer.py diff --git a/homeassistant/components/notify/homematic.py b/homeassistant/components/notify/homematic.py new file mode 100644 index 00000000000..2587bac8b6c --- /dev/null +++ b/homeassistant/components/notify/homematic.py @@ -0,0 +1,74 @@ +""" +Notification support for Homematic. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.homematic/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.notify import ( + BaseNotificationService, PLATFORM_SCHEMA, ATTR_DATA) +import homeassistant.helpers.config_validation as cv +from homeassistant.components.homematic import ( + DOMAIN, SERVICE_SET_DEVICE_VALUE, ATTR_ADDRESS, ATTR_CHANNEL, ATTR_PARAM, + ATTR_VALUE, ATTR_INTERFACE) +import homeassistant.helpers.template as template_helper + +_LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ["homematic"] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(ATTR_ADDRESS): vol.All(cv.string, vol.Upper), + vol.Required(ATTR_CHANNEL): vol.Coerce(int), + vol.Required(ATTR_PARAM): vol.All(cv.string, vol.Upper), + vol.Required(ATTR_VALUE): cv.match_all, + vol.Optional(ATTR_INTERFACE): cv.string, +}) + + +def get_service(hass, config, discovery_info=None): + """Get the Homematic notification service.""" + data = { + ATTR_ADDRESS: config[ATTR_ADDRESS], + ATTR_CHANNEL: config[ATTR_CHANNEL], + ATTR_PARAM: config[ATTR_PARAM], + ATTR_VALUE: config[ATTR_VALUE] + } + if ATTR_INTERFACE in config: + data[ATTR_INTERFACE] = config[ATTR_INTERFACE] + + return HomematicNotificationService(hass, data) + + +class HomematicNotificationService(BaseNotificationService): + """Implement the notification service for Homematic.""" + + def __init__(self, hass, data): + """Initialize the service.""" + self.hass = hass + self.data = data + + def send_message(self, message="", **kwargs): + """Send a notification to the device.""" + attr_data = kwargs.get(ATTR_DATA) + if attr_data is not None: + if 'address' in attr_data: + self.data[ATTR_ADDRESS] = attr_data['address'] + if 'channel' in attr_data: + self.data[ATTR_CHANNEL] = attr_data['channel'] + if 'param' in attr_data: + self.data[ATTR_PARAM] = attr_data['param'] + if 'value' in attr_data: + self.data[ATTR_VALUE] = attr_data['value'] + if 'interface' in attr_data: + self.data[ATTR_INTERFACE] = attr_data['interface'] + + if self.data[ATTR_VALUE] is not None: + templ = template_helper.Template(self.data[ATTR_VALUE], self.hass) + self.data[ATTR_VALUE] = template_helper.render_complex(templ, None) + + _LOGGER.debug("Calling service: domain=%s;service=%s;data=%s", + DOMAIN, SERVICE_SET_DEVICE_VALUE, str(self.data)) + self.hass.services.call(DOMAIN, SERVICE_SET_DEVICE_VALUE, self.data) diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e6d9a6f3fe5..280b77d6ebd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -153,6 +153,9 @@ pydeconz==47 # homeassistant.components.zwave pydispatcher==2.0.5 +# homeassistant.components.homematic +pyhomematic==0.1.50 + # homeassistant.components.litejet pylitejet==0.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index eef77b9ec81..fd161898acc 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -76,6 +76,7 @@ TEST_REQUIREMENTS = ( 'pyblackbird', 'pydeconz', 'pydispatcher', + 'pyhomematic', 'pylitejet', 'pymonoprice', 'pynx584', diff --git a/tests/components/notify/test_homematic.py b/tests/components/notify/test_homematic.py new file mode 100644 index 00000000000..2ea98fc020b --- /dev/null +++ b/tests/components/notify/test_homematic.py @@ -0,0 +1,78 @@ +"""The tests for the Homematic notification platform.""" + +import unittest + +from homeassistant.setup import setup_component +import homeassistant.components.notify as notify_comp +from tests.common import assert_setup_component, get_test_home_assistant + + +class TestHomematicNotify(unittest.TestCase): + """Test the Homematic notifications.""" + + def setUp(self): # pylint: disable=invalid-name + """Set up things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def tearDown(self): # pylint: disable=invalid-name + """Stop down everything that was started.""" + self.hass.stop() + + def test_setup_full(self): + """Test valid configuration.""" + setup_component(self.hass, 'homematic', { + 'homematic': { + 'hosts': { + 'ccu2': { + 'host': '127.0.0.1' + } + } + } + }) + with assert_setup_component(1) as handle_config: + assert setup_component(self.hass, 'notify', { + 'notify': { + 'name': 'test', + 'platform': 'homematic', + 'address': 'NEQXXXXXXX', + 'channel': 2, + 'param': 'SUBMIT', + 'value': '1,1,108000,2', + 'interface': 'my-interface'} + }) + assert handle_config[notify_comp.DOMAIN] + + def test_setup_without_optional(self): + """Test valid configuration without optional.""" + setup_component(self.hass, 'homematic', { + 'homematic': { + 'hosts': { + 'ccu2': { + 'host': '127.0.0.1' + } + } + } + }) + with assert_setup_component(1) as handle_config: + assert setup_component(self.hass, 'notify', { + 'notify': { + 'name': 'test', + 'platform': 'homematic', + 'address': 'NEQXXXXXXX', + 'channel': 2, + 'param': 'SUBMIT', + 'value': '1,1,108000,2'} + }) + assert handle_config[notify_comp.DOMAIN] + + def test_bad_config(self): + """Test invalid configuration.""" + config = { + notify_comp.DOMAIN: { + 'name': 'test', + 'platform': 'homematic' + } + } + with assert_setup_component(0) as handle_config: + assert setup_component(self.hass, notify_comp.DOMAIN, config) + assert not handle_config[notify_comp.DOMAIN] From 5a00cc5afcfb6cba0d56167429c5a273d6b33492 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Fri, 12 Oct 2018 11:35:33 +0200 Subject: [PATCH 114/265] Provide an individual color temperature range per Yeelight model (#17305) * Provide an individual color temperature range per Yeelight model * Fix lint * Bump yeelight version * Remove unused const * Enable SUPPORT_COLOR_TEMP for BulbType.WhiteTemp --- homeassistant/components/light/yeelight.py | 57 ++++++++++++++-------- requirements_all.txt | 2 +- 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/light/yeelight.py b/homeassistant/components/light/yeelight.py index b14b1f96e69..7efd62e3de5 100644 --- a/homeassistant/components/light/yeelight.py +++ b/homeassistant/components/light/yeelight.py @@ -20,7 +20,7 @@ from homeassistant.components.light import ( import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util -REQUIREMENTS = ['yeelight==0.4.0'] +REQUIREMENTS = ['yeelight==0.4.3'] _LOGGER = logging.getLogger(__name__) @@ -35,6 +35,7 @@ LEGACY_DEVICE_TYPE_MAP = { DEFAULT_NAME = 'Yeelight' DEFAULT_TRANSITION = 350 +CONF_MODEL = 'model' CONF_TRANSITION = 'transition' CONF_SAVE_ON_CHANGE = 'save_on_change' CONF_MODE_MUSIC = 'use_music_mode' @@ -46,6 +47,7 @@ DEVICE_SCHEMA = vol.Schema({ vol.Optional(CONF_TRANSITION, default=DEFAULT_TRANSITION): cv.positive_int, vol.Optional(CONF_MODE_MUSIC, default=False): cv.boolean, vol.Optional(CONF_SAVE_ON_CHANGE, default=False): cv.boolean, + vol.Optional(CONF_MODEL): cv.string, }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -55,15 +57,14 @@ SUPPORT_YEELIGHT = (SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION | SUPPORT_FLASH) +SUPPORT_YEELIGHT_WHITE_TEMP = (SUPPORT_YEELIGHT | + SUPPORT_COLOR_TEMP) + SUPPORT_YEELIGHT_RGB = (SUPPORT_YEELIGHT | SUPPORT_COLOR | SUPPORT_EFFECT | SUPPORT_COLOR_TEMP) -YEELIGHT_MIN_KELVIN = YEELIGHT_MAX_KELVIN = 2700 -YEELIGHT_RGB_MIN_KELVIN = 1700 -YEELIGHT_RGB_MAX_KELVIN = 6500 - EFFECT_DISCO = "Disco" EFFECT_TEMP = "Slow Temp" EFFECT_STROBE = "Strobe epilepsy!" @@ -132,23 +133,26 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.debug("Adding autodetected %s", discovery_info['hostname']) device_type = discovery_info['device_type'] - device_type = LEGACY_DEVICE_TYPE_MAP.get(device_type, device_type) + legacy_device_type = LEGACY_DEVICE_TYPE_MAP.get(device_type, + device_type) # Not using hostname, as it seems to vary. - name = "yeelight_%s_%s" % (device_type, + name = "yeelight_%s_%s" % (legacy_device_type, discovery_info['properties']['mac']) - host = discovery_info['host'] - device = {'name': name, 'ipaddr': host} + device = {'name': name, 'ipaddr': discovery_info['host']} - light = YeelightLight(device, DEVICE_SCHEMA({})) + light = YeelightLight(device, DEVICE_SCHEMA({CONF_MODEL: device_type})) lights.append(light) - hass.data[DATA_KEY][host] = light + hass.data[DATA_KEY][name] = light else: - for host, device_config in config[CONF_DEVICES].items(): - device = {'name': device_config[CONF_NAME], 'ipaddr': host} + for ipaddr, device_config in config[CONF_DEVICES].items(): + name = device_config[CONF_NAME] + _LOGGER.debug("Adding configured %s", name) + + device = {'name': name, 'ipaddr': ipaddr} light = YeelightLight(device, device_config) lights.append(light) - hass.data[DATA_KEY][host] = light + hass.data[DATA_KEY][name] = light add_entities(lights, True) @@ -194,6 +198,10 @@ class YeelightLight(Light): self._is_on = None self._hs = None + self._model = config.get('model') + self._min_mireds = None + self._max_mireds = None + @property def available(self) -> bool: """Return if bulb is available.""" @@ -232,16 +240,12 @@ class YeelightLight(Light): @property def min_mireds(self): """Return minimum supported color temperature.""" - if self.supported_features & SUPPORT_COLOR_TEMP: - return kelvin_to_mired(YEELIGHT_RGB_MAX_KELVIN) - return kelvin_to_mired(YEELIGHT_MAX_KELVIN) + return self._min_mireds @property def max_mireds(self): """Return maximum supported color temperature.""" - if self.supported_features & SUPPORT_COLOR_TEMP: - return kelvin_to_mired(YEELIGHT_RGB_MIN_KELVIN) - return kelvin_to_mired(YEELIGHT_MIN_KELVIN) + return self._max_mireds def _get_hs_from_properties(self): rgb = self._properties.get('rgb', None) @@ -279,7 +283,8 @@ class YeelightLight(Light): import yeelight if self._bulb_device is None: try: - self._bulb_device = yeelight.Bulb(self._ipaddr) + self._bulb_device = yeelight.Bulb(self._ipaddr, + model=self._model) self._bulb_device.get_properties() # force init for type self._available = True @@ -305,6 +310,15 @@ class YeelightLight(Light): if self._bulb_device.bulb_type == yeelight.BulbType.Color: self._supported_features = SUPPORT_YEELIGHT_RGB + elif self._bulb_device.bulb_type == yeelight.BulbType.WhiteTemp: + self._supported_features = SUPPORT_YEELIGHT_WHITE_TEMP + + if self._min_mireds is None: + model_specs = self._bulb.get_model_specs() + self._min_mireds = \ + kelvin_to_mired(model_specs['color_temp']['max']) + self._max_mireds = \ + kelvin_to_mired(model_specs['color_temp']['min']) self._is_on = self._properties.get('power') == 'on' @@ -500,5 +514,6 @@ class YeelightLight(Light): import yeelight try: self._bulb.set_power_mode(yeelight.enums.PowerMode[mode.upper()]) + self.async_schedule_update_ha_state(True) except yeelight.BulbException as ex: _LOGGER.error("Unable to set the power mode: %s", ex) diff --git a/requirements_all.txt b/requirements_all.txt index 63f2430a330..569cb783f7f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1570,7 +1570,7 @@ yahooweather==0.10 yalesmartalarmclient==0.1.4 # homeassistant.components.light.yeelight -yeelight==0.4.0 +yeelight==0.4.3 # homeassistant.components.light.yeelightsunflower yeelightsunflower==0.0.10 From 7b28963a8861c466a99a937749506b2f80fc8094 Mon Sep 17 00:00:00 2001 From: Jedmeng Date: Fri, 12 Oct 2018 20:50:20 +0800 Subject: [PATCH 115/265] Fix setting opple light color temperature (#17359) --- homeassistant/components/light/opple.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/light/opple.py b/homeassistant/components/light/opple.py index 66850d04406..fb503d33d31 100644 --- a/homeassistant/components/light/opple.py +++ b/homeassistant/components/light/opple.py @@ -110,7 +110,7 @@ class OppleLight(Light): self._device.brightness = kwargs[ATTR_BRIGHTNESS] if ATTR_COLOR_TEMP in kwargs and \ - self.brightness != kwargs[ATTR_COLOR_TEMP]: + self.color_temp != kwargs[ATTR_COLOR_TEMP]: color_temp = mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]) self._device.color_temperature = color_temp From e00ed84d84b1026bb5500c64bf6d31c7e8b6db12 Mon Sep 17 00:00:00 2001 From: kennedyshead Date: Fri, 12 Oct 2018 14:51:03 +0200 Subject: [PATCH 116/265] The ping command will not detect device in standby as off (#17358) --- .../components/media_player/samsungtv.py | 21 +-------- .../components/media_player/test_samsungtv.py | 44 +++---------------- 2 files changed, 7 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py index 3a66aa66dc0..3a255ad0ad2 100644 --- a/homeassistant/components/media_player/samsungtv.py +++ b/homeassistant/components/media_player/samsungtv.py @@ -8,8 +8,6 @@ import asyncio from datetime import timedelta import logging import socket -import subprocess -import sys import voluptuous as vol @@ -124,24 +122,7 @@ class SamsungTVDevice(MediaPlayerDevice): def update(self): """Update state of device.""" - if sys.platform == 'win32': - timeout_arg = '-w {}000'.format(self._config['timeout']) - _ping_cmd = [ - 'ping', '-n 3', timeout_arg, self._config['host']] - else: - timeout_arg = '-W{}'.format(self._config['timeout']) - _ping_cmd = [ - 'ping', '-n', '-q', - '-c3', timeout_arg, self._config['host']] - - ping = subprocess.Popen( - _ping_cmd, - stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) - try: - ping.communicate() - self._state = STATE_ON if ping.returncode == 0 else STATE_OFF - except subprocess.CalledProcessError: - self._state = STATE_OFF + self.send_key("KEY") def get_remote(self): """Create or return a remote control instance.""" diff --git a/tests/components/media_player/test_samsungtv.py b/tests/components/media_player/test_samsungtv.py index 5551a86df05..45db09dc662 100644 --- a/tests/components/media_player/test_samsungtv.py +++ b/tests/components/media_player/test_samsungtv.py @@ -2,7 +2,6 @@ import asyncio import unittest from unittest.mock import call, patch, MagicMock -from subprocess import CalledProcessError from asynctest import mock @@ -100,50 +99,19 @@ class TestSamsungTv(unittest.TestCase): mocked_warn.assert_called_once_with("Cannot determine device") add_entities.assert_not_called() - @mock.patch( - 'homeassistant.components.media_player.samsungtv.subprocess.Popen' - ) - def test_update_on(self, mocked_popen): + def test_update_on(self): """Testing update tv on.""" - ping = mock.Mock() - mocked_popen.return_value = ping - ping.returncode = 0 self.device.update() self.assertEqual(STATE_ON, self.device._state) - @mock.patch( - 'homeassistant.components.media_player.samsungtv.subprocess.Popen' - ) - def test_update_off(self, mocked_popen): + def test_update_off(self): """Testing update tv off.""" - ping = mock.Mock() - mocked_popen.return_value = ping - ping.returncode = 1 + _remote = mock.Mock() + _remote.control = mock.Mock( + side_effect=OSError('Boom')) + self.device.get_remote = mock.Mock(return_value=_remote) self.device.update() self.assertEqual(STATE_OFF, self.device._state) - ping = mock.Mock() - ping.communicate = mock.Mock( - side_effect=CalledProcessError("BOOM", None)) - mocked_popen.return_value = ping - self.device.update() - self.assertEqual(STATE_OFF, self.device._state) - - @mock.patch( - 'homeassistant.components.media_player.samsungtv.subprocess.Popen' - ) - def test_timeout(self, mocked_popen): - """Test timeout use.""" - ping = mock.Mock() - mocked_popen.return_value = ping - ping.returncode = 0 - self.device.update() - expected_timeout = self.device._config['timeout'] - timeout_arg = '-W{}'.format(expected_timeout) - ping_command = [ - 'ping', '-n', '-q', '-c3', timeout_arg, 'fake'] - expected_call = call(ping_command, stderr=-3, stdout=-1) - self.assertEqual(mocked_popen.call_args, expected_call) - self.assertEqual(STATE_ON, self.device._state) def test_send_key(self): """Test for send key.""" From 397c4336bc9cc2df7d508d22c48661ef04d5c9ee Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 12 Oct 2018 14:57:29 +0200 Subject: [PATCH 117/265] Update frontend --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 27904faec1a..c06f659573e 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20181007.0'] +REQUIREMENTS = ['home-assistant-frontend==20181012.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 569cb783f7f..ba7d05fc7e3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -464,7 +464,7 @@ hole==0.3.0 holidays==0.9.7 # homeassistant.components.frontend -home-assistant-frontend==20181007.0 +home-assistant-frontend==20181012.0 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 280b77d6ebd..db67e27cc61 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -93,7 +93,7 @@ hdate==0.6.3 holidays==0.9.7 # homeassistant.components.frontend -home-assistant-frontend==20181007.0 +home-assistant-frontend==20181012.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From 6971e84ddf4245c92a9b07d3d44c9c6696f880a4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 12 Oct 2018 14:58:09 +0200 Subject: [PATCH 118/265] Update translations --- .../components/auth/.translations/sv.json | 9 +++++-- .../deconz/.translations/zh-Hant.json | 2 +- .../components/hangouts/.translations/sv.json | 2 ++ .../components/ifttt/.translations/sv.json | 13 +++++++++ .../ifttt/.translations/zh-Hant.json | 4 +-- .../components/ios/.translations/sv.json | 3 +++ .../components/lifx/.translations/sv.json | 15 +++++++++++ .../lifx/.translations/zh-Hans.json | 15 +++++++++++ .../components/mqtt/.translations/sv.json | 22 +++++++++++++-- .../components/smhi/.translations/sv.json | 4 +-- .../smhi/.translations/zh-Hans.json | 19 +++++++++++++ .../components/tradfri/.translations/sv.json | 5 +++- .../components/upnp/.translations/sv.json | 27 +++++++++++++++++++ .../components/zwave/.translations/ca.json | 7 ++++- .../components/zwave/.translations/sl.json | 22 +++++++++++++++ .../components/zwave/.translations/sv.json | 22 +++++++++++++++ .../zwave/.translations/zh-Hans.json | 22 +++++++++++++++ .../zwave/.translations/zh-Hant.json | 22 +++++++++++++++ 18 files changed, 224 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/lifx/.translations/sv.json create mode 100644 homeassistant/components/lifx/.translations/zh-Hans.json create mode 100644 homeassistant/components/smhi/.translations/zh-Hans.json create mode 100644 homeassistant/components/upnp/.translations/sv.json create mode 100644 homeassistant/components/zwave/.translations/sl.json create mode 100644 homeassistant/components/zwave/.translations/sv.json create mode 100644 homeassistant/components/zwave/.translations/zh-Hans.json create mode 100644 homeassistant/components/zwave/.translations/zh-Hant.json diff --git a/homeassistant/components/auth/.translations/sv.json b/homeassistant/components/auth/.translations/sv.json index 604ae3c4fe5..9246a88c512 100644 --- a/homeassistant/components/auth/.translations/sv.json +++ b/homeassistant/components/auth/.translations/sv.json @@ -8,11 +8,16 @@ "invalid_code": "Ogiltig kod, var god f\u00f6rs\u00f6k igen." }, "step": { + "init": { + "description": "Var god v\u00e4lj en av notifieringstj\u00e4nsterna:", + "title": "Konfigurera ett eng\u00e5ngsl\u00f6senord levererat genom notifieringskomponenten" + }, "setup": { "description": "Ett eng\u00e5ngsl\u00f6senord har skickats av **notify.{notify_service}**. V\u00e4nligen ange det nedan:", - "title": "Verifiera installationen" + "title": "Verifiera inst\u00e4llningen" } - } + }, + "title": "Meddela eng\u00e5ngsl\u00f6senord" }, "totp": { "error": { diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json index 5cd1a14d499..524f68d41bc 100644 --- a/homeassistant/components/deconz/.translations/zh-Hant.json +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Bridge \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "no_bridges": "\u672a\u641c\u5c0b\u5230 deCONZ Bridfe", - "one_instance_only": "\u7d44\u4ef6\u50c5\u652f\u63f4\u4e00\u7d44 deCONZ \u5be6\u4f8b" + "one_instance_only": "\u7d44\u4ef6\u50c5\u652f\u63f4\u4e00\u7d44 deCONZ \u7269\u4ef6" }, "error": { "no_key": "\u7121\u6cd5\u53d6\u5f97 API key" diff --git a/homeassistant/components/hangouts/.translations/sv.json b/homeassistant/components/hangouts/.translations/sv.json index 90bf4e97712..ae03fdbf722 100644 --- a/homeassistant/components/hangouts/.translations/sv.json +++ b/homeassistant/components/hangouts/.translations/sv.json @@ -14,6 +14,7 @@ "data": { "2fa": "2FA Pinkod" }, + "description": "Missing english translation", "title": "Tv\u00e5faktorsautentisering" }, "user": { @@ -21,6 +22,7 @@ "email": "E-postadress", "password": "L\u00f6senord" }, + "description": "Missing english translation", "title": "Google Hangouts-inloggning" } }, diff --git a/homeassistant/components/ifttt/.translations/sv.json b/homeassistant/components/ifttt/.translations/sv.json index 077956287b3..883bb042822 100644 --- a/homeassistant/components/ifttt/.translations/sv.json +++ b/homeassistant/components/ifttt/.translations/sv.json @@ -1,5 +1,18 @@ { "config": { + "abort": { + "not_internet_accessible": "Din Home Assistant instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot IFTTT meddelanden.", + "one_instance_allowed": "Endast en enda instans \u00e4r n\u00f6dv\u00e4ndig." + }, + "create_entry": { + "default": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du anv\u00e4nda \u00e5tg\u00e4rden \"G\u00f6r en webbf\u00f6rfr\u00e5gan\" fr\u00e5n [IFTTT Webhook applet] ( {applet_url} ).\n\n Fyll i f\u00f6ljande information:\n \n - URL: ` {webhook_url} `\n - Metod: POST\n - Inneh\u00e5llstyp: application / json\n\n Se [dokumentationen] ( {docs_url} ) om hur du konfigurerar automatiseringar f\u00f6r att hantera inkommande data." + }, + "step": { + "user": { + "description": "\u00c4r du s\u00e4ker p\u00e5 att du vill st\u00e4lla in IFTTT?", + "title": "St\u00e4lla in IFTTT Webhook Applet" + } + }, "title": "IFTTT" } } \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/zh-Hant.json b/homeassistant/components/ifttt/.translations/zh-Hant.json index 5c75beddbe1..8610351f43b 100644 --- a/homeassistant/components/ifttt/.translations/zh-Hant.json +++ b/homeassistant/components/ifttt/.translations/zh-Hant.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 IFTTT \u8a0a\u606f\u3002", - "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u5be6\u4f8b\u5373\u53ef\u3002" + "not_internet_accessible": "Home Assistant \u7269\u4ef6\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 IFTTT \u8a0a\u606f\u3002", + "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "create_entry": { "default": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u8981\u7531 [IFTTT Webhook applet]({applet_url}) \u547c\u53eb\u300c\u9032\u884c Web \u8acb\u6c42\u300d\u52d5\u4f5c\u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u95dc\u65bc\u5982\u4f55\u50b3\u5165\u8cc7\u6599\u81ea\u52d5\u5316\u8a2d\u5b9a\uff0c\u8acb\u53c3\u95b1[\u6587\u4ef6]({docs_url})\u4ee5\u9032\u884c\u4e86\u89e3\u3002" diff --git a/homeassistant/components/ios/.translations/sv.json b/homeassistant/components/ios/.translations/sv.json index 6806f9bab90..5a605ed8987 100644 --- a/homeassistant/components/ios/.translations/sv.json +++ b/homeassistant/components/ios/.translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "Endast en enda konfiguration av Home Assistant iOS \u00e4r n\u00f6dv\u00e4ndig." + }, "step": { "confirm": { "description": "Vill du konfigurera Home Assistants iOS komponent?", diff --git a/homeassistant/components/lifx/.translations/sv.json b/homeassistant/components/lifx/.translations/sv.json new file mode 100644 index 00000000000..a935e209bb4 --- /dev/null +++ b/homeassistant/components/lifx/.translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Inga LIFX enheter hittas i n\u00e4tverket.", + "single_instance_allowed": "Endast en enda konfiguration av LIFX \u00e4r m\u00f6jlig." + }, + "step": { + "confirm": { + "description": "Vill du st\u00e4lla in LIFX?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/zh-Hans.json b/homeassistant/components/lifx/.translations/zh-Hans.json new file mode 100644 index 00000000000..bc9375d807d --- /dev/null +++ b/homeassistant/components/lifx/.translations/zh-Hans.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u6ca1\u6709\u5728\u7f51\u7edc\u4e0a\u627e\u5230 LIFX \u8bbe\u5907\u3002", + "single_instance_allowed": "LIFX \u53ea\u80fd\u914d\u7f6e\u4e00\u6b21\u3002" + }, + "step": { + "confirm": { + "description": "\u60a8\u60f3\u8981\u914d\u7f6e LIFX \u5417\uff1f", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/sv.json b/homeassistant/components/mqtt/.translations/sv.json index 7cf6d75b9c1..70e3720038d 100644 --- a/homeassistant/components/mqtt/.translations/sv.json +++ b/homeassistant/components/mqtt/.translations/sv.json @@ -1,13 +1,31 @@ { "config": { + "abort": { + "single_instance_allowed": "Endast en enda konfiguration av MQTT \u00e4r till\u00e5ten." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta till broker." + }, "step": { "broker": { "data": { + "broker": "Broker", + "discovery": "Aktivera uppt\u00e4ckt", "password": "L\u00f6senord", "port": "Port", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "V\u00e4nligen ange anslutningsinformationen f\u00f6r din MQTT broker.", + "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "Aktivera uppt\u00e4ckt" + }, + "description": "Vill du konfigurera Home Assistant f\u00f6r att ansluta till MQTT Broker som tillhandah\u00e5lls av hass.io-till\u00e4gget {addon} ?", + "title": "MQTT Broker via Hass.io till\u00e4gg" } - } + }, + "title": "MQTT" } } \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/sv.json b/homeassistant/components/smhi/.translations/sv.json index 30d647ac2c4..69073a0eb73 100644 --- a/homeassistant/components/smhi/.translations/sv.json +++ b/homeassistant/components/smhi/.translations/sv.json @@ -2,7 +2,7 @@ "config": { "error": { "name_exists": "Namnet finns redan", - "wrong_location": "Endast plats i Sverige" + "wrong_location": "Plats i Sverige endast" }, "step": { "user": { @@ -14,6 +14,6 @@ "title": "Plats i Sverige" } }, - "title": "SMHI svenskt väder" + "title": "Svensk v\u00e4derservice (SMHI)" } } \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/zh-Hans.json b/homeassistant/components/smhi/.translations/zh-Hans.json new file mode 100644 index 00000000000..a70bb7a6722 --- /dev/null +++ b/homeassistant/components/smhi/.translations/zh-Hans.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "\u540d\u79f0\u5df2\u5b58\u5728", + "wrong_location": "\u4ec5\u9650\u745e\u5178\u7684\u4f4d\u7f6e" + }, + "step": { + "user": { + "data": { + "latitude": "\u7eac\u5ea6", + "longitude": "\u7ecf\u5ea6", + "name": "\u540d\u79f0" + }, + "title": "\u5728\u745e\u5178\u7684\u4f4d\u7f6e" + } + }, + "title": "\u745e\u5178\u6c14\u8c61\u670d\u52a1\uff08SMHI\uff09" + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/.translations/sv.json b/homeassistant/components/tradfri/.translations/sv.json index ffe8bff22b4..34799050539 100644 --- a/homeassistant/components/tradfri/.translations/sv.json +++ b/homeassistant/components/tradfri/.translations/sv.json @@ -4,11 +4,14 @@ "already_configured": "Bryggan \u00e4r redan konfigurerad" }, "error": { - "cannot_connect": "Det gick inte att ansluta till gatewayen." + "cannot_connect": "Det gick inte att ansluta till gatewayen.", + "invalid_key": "Misslyckades med att registrera den angivna nyckeln. Om det h\u00e4r h\u00e4nder, f\u00f6rs\u00f6k starta om gatewayen igen.", + "timeout": "Timeout vid valididering av kod" }, "step": { "auth": { "data": { + "host": "V\u00e4rd", "security_code": "S\u00e4kerhetskod" }, "description": "Du kan hitta s\u00e4kerhetskoden p\u00e5 baksidan av din gateway.", diff --git a/homeassistant/components/upnp/.translations/sv.json b/homeassistant/components/upnp/.translations/sv.json new file mode 100644 index 00000000000..63c63781845 --- /dev/null +++ b/homeassistant/components/upnp/.translations/sv.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP/IGD \u00e4r redan konfigurerad", + "no_devices_discovered": "Inga UPnP/IGDs uppt\u00e4cktes", + "no_sensors_or_port_mapping": "Aktivera minst sensorer eller portmappning" + }, + "error": { + "one": "En", + "other": "Andra" + }, + "step": { + "init": { + "title": "UPnP/IGD" + }, + "user": { + "data": { + "enable_port_mapping": "Aktivera portmappning f\u00f6r Home Assistant", + "enable_sensors": "L\u00e4gg till trafiksensorer", + "igd": "UPnP/IGD" + }, + "title": "Konfigurationsalternativ f\u00f6r UPnP/IGD" + } + }, + "title": "UPnP/IGD" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/ca.json b/homeassistant/components/zwave/.translations/ca.json index 8c39ca58201..b617a902374 100644 --- a/homeassistant/components/zwave/.translations/ca.json +++ b/homeassistant/components/zwave/.translations/ca.json @@ -4,11 +4,16 @@ "already_configured": "Z-Wave ja est\u00e0 configurat", "one_instance_only": "El component nom\u00e9s admet una inst\u00e0ncia de Z-Wave" }, + "error": { + "option_error": "Ha fallat la validaci\u00f3 de Z-Wave. \u00c9s correcta la ruta al port on hi ha la mem\u00f2ria USB?" + }, "step": { "user": { "data": { - "network_key": "Clau de xarxa (deixeu-ho en blanc per generar-la autom\u00e0ticament)" + "network_key": "Clau de xarxa (deixeu-ho en blanc per generar-la autom\u00e0ticament)", + "usb_path": "Ruta del port USB" }, + "description": "Consulteu https://www.home-assistant.io/docs/z-wave/installation/ per obtenir informaci\u00f3 sobre les variables de configuraci\u00f3", "title": "Configureu Z-Wave" } }, diff --git a/homeassistant/components/zwave/.translations/sl.json b/homeassistant/components/zwave/.translations/sl.json new file mode 100644 index 00000000000..fa799d1ed36 --- /dev/null +++ b/homeassistant/components/zwave/.translations/sl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave je \u017ee konfiguriran", + "one_instance_only": "Komponente podpirajo le eno Z-Wave instanco" + }, + "error": { + "option_error": "Potrjevanje Z-Wave ni uspelo. Ali je pot do USB klju\u010da pravilna?" + }, + "step": { + "user": { + "data": { + "network_key": "Omre\u017eni klju\u010d (pustite prazno za samodejno generiranje)", + "usb_path": "USB Pot" + }, + "description": "Za informacije o konfiguracijskih spremenljivka si oglejte https://www.home-assistant.io/docs/z-wave/installation/", + "title": "Nastavite Z-Wave" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/sv.json b/homeassistant/components/zwave/.translations/sv.json new file mode 100644 index 00000000000..508652a1784 --- /dev/null +++ b/homeassistant/components/zwave/.translations/sv.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave \u00e4r redan konfigurerat", + "one_instance_only": "Komponenten st\u00f6der endast en Z-Wave-instans" + }, + "error": { + "option_error": "Z-Wave-valideringen misslyckades. \u00c4r s\u00f6kv\u00e4gen till USB-minnet korrekt?" + }, + "step": { + "user": { + "data": { + "network_key": "N\u00e4tverksnyckel (l\u00e4mna blank f\u00f6r automatisk generering)", + "usb_path": "USB-s\u00f6kv\u00e4g" + }, + "description": "Se https://www.home-assistant.io/docs/z-wave/installation/ f\u00f6r information om konfigurationsvariabler", + "title": "St\u00e4lla in Z-Wave" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/zh-Hans.json b/homeassistant/components/zwave/.translations/zh-Hans.json new file mode 100644 index 00000000000..2c72ce72c60 --- /dev/null +++ b/homeassistant/components/zwave/.translations/zh-Hans.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave \u5df2\u914d\u7f6e\u5b8c\u6210", + "one_instance_only": "\u7ec4\u4ef6\u53ea\u652f\u6301\u4e00\u4e2a Z-Wave \u5b9e\u4f8b" + }, + "error": { + "option_error": "Z-Wave \u9a8c\u8bc1\u5931\u8d25\u3002 USB \u68d2\u7684\u8def\u5f84\u662f\u5426\u6b63\u786e\uff1f" + }, + "step": { + "user": { + "data": { + "network_key": "\u7f51\u7edc\u5bc6\u94a5\uff08\u7559\u7a7a\u5c06\u81ea\u52a8\u751f\u6210\uff09", + "usb_path": "USB \u8def\u5f84" + }, + "description": "\u6709\u5173\u914d\u7f6e\u7684\u4fe1\u606f\uff0c\u8bf7\u53c2\u9605 https://www.home-assistant.io/docs/z-wave/installation/", + "title": "\u8bbe\u7f6e Z-Wave" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/zh-Hant.json b/homeassistant/components/zwave/.translations/zh-Hant.json new file mode 100644 index 00000000000..2a84e8b3fd6 --- /dev/null +++ b/homeassistant/components/zwave/.translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "one_instance_only": "\u7d44\u4ef6\u50c5\u652f\u63f4\u4e00\u7d44 Z-Wave \u7269\u4ef6" + }, + "error": { + "option_error": "Z-Wave \u9a57\u8b49\u5931\u6557\uff0c\u8acb\u78ba\u5b9a USB \u96a8\u8eab\u789f\u8def\u5f91\u6b63\u78ba\uff1f" + }, + "step": { + "user": { + "data": { + "network_key": "\u7db2\u8def\u5bc6\u9470\uff08\u4fdd\u7559\u7a7a\u767d\u5c07\u6703\u81ea\u52d5\u7522\u751f\uff09", + "usb_path": "USB \u8def\u5f91" + }, + "description": "\u95dc\u65bc\u8a2d\u5b9a\u8b8a\u6578\u8cc7\u8a0a\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/docs/z-wave/installation/", + "title": "\u8a2d\u5b9a Z-Wave" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file From 1f863830e1c6ed29af34a24dbadce9725a3c4583 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Sat, 13 Oct 2018 01:48:02 +1100 Subject: [PATCH 119/265] Adding source attribute to geo location platforms (#17339) * added source attribute to all geo_location platforms * amended test cases * constant moved and source method now forces subclasses to override --- homeassistant/components/geo_location/__init__.py | 8 ++++++++ homeassistant/components/geo_location/demo.py | 7 +++++++ .../components/geo_location/geo_json_events.py | 6 ++++++ tests/components/geo_location/test_geo_json_events.py | 10 +++++++--- tests/components/geo_location/test_init.py | 4 ++++ 5 files changed, 32 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/geo_location/__init__.py b/homeassistant/components/geo_location/__init__.py index 66753aad221..54aebc3591b 100644 --- a/homeassistant/components/geo_location/__init__.py +++ b/homeassistant/components/geo_location/__init__.py @@ -19,6 +19,7 @@ from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa _LOGGER = logging.getLogger(__name__) ATTR_DISTANCE = 'distance' +ATTR_SOURCE = 'source' DOMAIN = 'geo_location' ENTITY_ID_FORMAT = DOMAIN + '.{}' GROUP_NAME_ALL_EVENTS = 'All Geo Location Events' @@ -43,6 +44,11 @@ class GeoLocationEvent(Entity): return round(self.distance, 1) return None + @property + def source(self) -> str: + """Return source value of this external event.""" + raise NotImplementedError + @property def distance(self) -> Optional[float]: """Return distance value of this external event.""" @@ -66,4 +72,6 @@ class GeoLocationEvent(Entity): data[ATTR_LATITUDE] = round(self.latitude, 5) if self.longitude is not None: data[ATTR_LONGITUDE] = round(self.longitude, 5) + if self.source is not None: + data[ATTR_SOURCE] = self.source return data diff --git a/homeassistant/components/geo_location/demo.py b/homeassistant/components/geo_location/demo.py index ddec369e696..5e76e5cdf9a 100644 --- a/homeassistant/components/geo_location/demo.py +++ b/homeassistant/components/geo_location/demo.py @@ -26,6 +26,8 @@ EVENT_NAMES = ["Bushfire", "Hazard Reduction", "Grass Fire", "Burn off", "Cyclone", "Waterspout", "Dust Storm", "Blizzard", "Ice Storm", "Earthquake", "Tsunami"] +SOURCE = 'demo' + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Demo geo locations.""" @@ -100,6 +102,11 @@ class DemoGeoLocationEvent(GeoLocationEvent): self._longitude = longitude self._unit_of_measurement = unit_of_measurement + @property + def source(self) -> str: + """Return source value of this external event.""" + return SOURCE + @property def name(self) -> Optional[str]: """Return the name of the event.""" diff --git a/homeassistant/components/geo_location/geo_json_events.py b/homeassistant/components/geo_location/geo_json_events.py index bb17fb2450e..fc40dddb9b8 100644 --- a/homeassistant/components/geo_location/geo_json_events.py +++ b/homeassistant/components/geo_location/geo_json_events.py @@ -31,6 +31,7 @@ DEFAULT_RADIUS_IN_KM = 20.0 DEFAULT_UNIT_OF_MEASUREMENT = "km" SCAN_INTERVAL = timedelta(minutes=5) +SOURCE = 'geo_json_events' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_URL): cv.string, @@ -162,6 +163,11 @@ class GeoJsonLocationEvent(GeoLocationEvent): self._longitude = feed_entry.coordinates[1] self.external_id = feed_entry.external_id + @property + def source(self) -> str: + """Return source value of this external event.""" + return SOURCE + @property def name(self) -> Optional[str]: """Return the name of the entity.""" diff --git a/tests/components/geo_location/test_geo_json_events.py b/tests/components/geo_location/test_geo_json_events.py index 5ce508289dd..dbaf71a6509 100644 --- a/tests/components/geo_location/test_geo_json_events.py +++ b/tests/components/geo_location/test_geo_json_events.py @@ -4,6 +4,7 @@ from unittest import mock from unittest.mock import patch, MagicMock from homeassistant.components import geo_location +from homeassistant.components.geo_location import ATTR_SOURCE from homeassistant.components.geo_location.geo_json_events import \ SCAN_INTERVAL, ATTR_EXTERNAL_ID from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_START, \ @@ -84,7 +85,8 @@ class TestGeoJsonPlatform(unittest.TestCase): assert state.attributes == { ATTR_EXTERNAL_ID: "1234", ATTR_LATITUDE: -31.0, ATTR_LONGITUDE: 150.0, ATTR_FRIENDLY_NAME: "Title 1", - ATTR_UNIT_OF_MEASUREMENT: "km"} + ATTR_UNIT_OF_MEASUREMENT: "km", + ATTR_SOURCE: 'geo_json_events'} self.assertAlmostEqual(float(state.state), 15.5) state = self.hass.states.get("geo_location.title_2") @@ -93,7 +95,8 @@ class TestGeoJsonPlatform(unittest.TestCase): assert state.attributes == { ATTR_EXTERNAL_ID: "2345", ATTR_LATITUDE: -31.1, ATTR_LONGITUDE: 150.1, ATTR_FRIENDLY_NAME: "Title 2", - ATTR_UNIT_OF_MEASUREMENT: "km"} + ATTR_UNIT_OF_MEASUREMENT: "km", + ATTR_SOURCE: 'geo_json_events'} self.assertAlmostEqual(float(state.state), 20.5) state = self.hass.states.get("geo_location.title_3") @@ -102,7 +105,8 @@ class TestGeoJsonPlatform(unittest.TestCase): assert state.attributes == { ATTR_EXTERNAL_ID: "3456", ATTR_LATITUDE: -31.2, ATTR_LONGITUDE: 150.2, ATTR_FRIENDLY_NAME: "Title 3", - ATTR_UNIT_OF_MEASUREMENT: "km"} + ATTR_UNIT_OF_MEASUREMENT: "km", + ATTR_SOURCE: 'geo_json_events'} self.assertAlmostEqual(float(state.state), 25.5) # Simulate an update - one existing, one new entry, diff --git a/tests/components/geo_location/test_init.py b/tests/components/geo_location/test_init.py index 54efe977bf9..09030354901 100644 --- a/tests/components/geo_location/test_init.py +++ b/tests/components/geo_location/test_init.py @@ -1,4 +1,6 @@ """The tests for the geo location component.""" +import pytest + from homeassistant.components import geo_location from homeassistant.components.geo_location import GeoLocationEvent from homeassistant.setup import async_setup_component @@ -18,3 +20,5 @@ async def test_event(hass): assert entity.distance is None assert entity.latitude is None assert entity.longitude is None + with pytest.raises(NotImplementedError): + assert entity.source is None From 401e22fc0c5c6232c089e6dcfa57d7ee63c9fceb Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 12 Oct 2018 11:07:47 -0600 Subject: [PATCH 120/265] Add config entry for SimpliSafe (#17148) * Initial move into component * Base functionality in place * Starting tests in place * All config entry tests in place * Made default scan interval more obvious and removed extra logging * Inherit default scan interval from alarm_control_panel * Updated coveragerc and CODEOWNERS * Member-requested changes * Hound * Updated requirements * Updated tests * Member-requested changes * Owner-requested changes * Constant cleanup * Fixed config flow test * Owner-requested updates * Updated requirements * Using async_will_remove_from_hass * Corrected scan interval logic * Fixed tests * Owner-requested changes * Additional logging * Owner-requested changes --- .coveragerc | 4 +- CODEOWNERS | 7 +- .../alarm_control_panel/simplisafe.py | 135 +++++++---------- .../simplisafe/.translations/en.json | 19 +++ .../components/simplisafe/__init__.py | 143 ++++++++++++++++++ .../components/simplisafe/config_flow.py | 80 ++++++++++ homeassistant/components/simplisafe/const.py | 10 ++ .../components/simplisafe/strings.json | 19 +++ homeassistant/config_entries.py | 1 + requirements_all.txt | 4 +- requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/simplisafe/__init__.py | 1 + .../components/simplisafe/test_config_flow.py | 120 +++++++++++++++ 14 files changed, 461 insertions(+), 86 deletions(-) create mode 100644 homeassistant/components/simplisafe/.translations/en.json create mode 100644 homeassistant/components/simplisafe/__init__.py create mode 100644 homeassistant/components/simplisafe/config_flow.py create mode 100644 homeassistant/components/simplisafe/const.py create mode 100644 homeassistant/components/simplisafe/strings.json create mode 100644 tests/components/simplisafe/__init__.py create mode 100644 tests/components/simplisafe/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 8e9ed417d7b..cf56182fd6d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -290,6 +290,9 @@ omit = homeassistant/components/scsgate.py homeassistant/components/*/scsgate.py + homeassistant/components/simplisafe/__init__.py + homeassistant/components/*/simplisafe.py + homeassistant/components/sisyphus.py homeassistant/components/*/sisyphus.py @@ -401,7 +404,6 @@ omit = homeassistant/components/alarm_control_panel/ifttt.py homeassistant/components/alarm_control_panel/manual_mqtt.py homeassistant/components/alarm_control_panel/nx584.py - homeassistant/components/alarm_control_panel/simplisafe.py homeassistant/components/alarm_control_panel/totalconnect.py homeassistant/components/alarm_control_panel/yale_smart_alarm.py homeassistant/components/apiai.py diff --git a/CODEOWNERS b/CODEOWNERS index ed8d8531a6a..9343407f06f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -49,7 +49,6 @@ homeassistant/components/hassio/* @home-assistant/hassio # Individual platforms homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt homeassistant/components/alarm_control_panel/manual_mqtt.py @colinodell -homeassistant/components/alarm_control_panel/simplisafe.py @bachya homeassistant/components/binary_sensor/hikvision.py @mezz64 homeassistant/components/binary_sensor/threshold.py @fabaff homeassistant/components/camera/yi.py @bachya @@ -192,7 +191,7 @@ homeassistant/components/melissa.py @kennedyshead homeassistant/components/*/melissa.py @kennedyshead homeassistant/components/*/mystrom.py @fabaff -# U +# O homeassistant/components/openuv/* @bachya homeassistant/components/*/openuv.py @bachya @@ -206,6 +205,10 @@ homeassistant/components/*/rainmachine.py @bachya homeassistant/components/*/random.py @fabaff homeassistant/components/*/rfxtrx.py @danielhiversen +# S +homeassistant/components/simplisafe/* @bachya +homeassistant/components/*/simplisafe.py @bachya + # T homeassistant/components/tahoma.py @philklei homeassistant/components/*/tahoma.py @philklei diff --git a/homeassistant/components/alarm_control_panel/simplisafe.py b/homeassistant/components/alarm_control_panel/simplisafe.py index 34c68f26c2a..cdcdf07c982 100644 --- a/homeassistant/components/alarm_control_panel/simplisafe.py +++ b/homeassistant/components/alarm_control_panel/simplisafe.py @@ -1,5 +1,5 @@ """ -Interfaces with SimpliSafe alarm control panel. +This platform provides alarm control functionality for SimpliSafe. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel.simplisafe/ @@ -7,86 +7,44 @@ https://home-assistant.io/components/alarm_control_panel.simplisafe/ import logging import re -import voluptuous as vol - -from homeassistant.components.alarm_control_panel import ( - PLATFORM_SCHEMA, AlarmControlPanel) +from homeassistant.components.alarm_control_panel import AlarmControlPanel +from homeassistant.components.simplisafe.const import ( + DATA_CLIENT, DOMAIN, TOPIC_UPDATE) from homeassistant.const import ( - CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED) -from homeassistant.helpers import aiohttp_client, config_validation as cv -from homeassistant.util.json import load_json, save_json - -REQUIREMENTS = ['simplisafe-python==3.1.2'] + CONF_CODE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, + STATE_ALARM_DISARMED) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect _LOGGER = logging.getLogger(__name__) -ATTR_ALARM_ACTIVE = "alarm_active" -ATTR_TEMPERATURE = "temperature" - -DATA_FILE = '.simplisafe' - -DEFAULT_NAME = 'SimpliSafe' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_CODE): cv.string, -}) +ATTR_ALARM_ACTIVE = 'alarm_active' +ATTR_TEMPERATURE = 'temperature' async def async_setup_platform( hass, config, async_add_entities, discovery_info=None): - """Set up the SimpliSafe platform.""" - from simplipy import API - from simplipy.errors import SimplipyError + """Set up a SimpliSafe alarm control panel based on existing config.""" + pass - username = config[CONF_USERNAME] - password = config[CONF_PASSWORD] - name = config.get(CONF_NAME) - code = config.get(CONF_CODE) - websession = aiohttp_client.async_get_clientsession(hass) - - config_data = await hass.async_add_executor_job( - load_json, hass.config.path(DATA_FILE)) - - try: - if config_data: - try: - simplisafe = await API.login_via_token( - config_data['refresh_token'], websession) - _LOGGER.debug('Logging in with refresh token') - except SimplipyError: - _LOGGER.info('Refresh token expired; attempting credentials') - simplisafe = await API.login_via_credentials( - username, password, websession) - else: - simplisafe = await API.login_via_credentials( - username, password, websession) - _LOGGER.debug('Logging in with credentials') - except SimplipyError as err: - _LOGGER.error("There was an error during setup: %s", err) - return - - config_data = {'refresh_token': simplisafe.refresh_token} - await hass.async_add_executor_job( - save_json, hass.config.path(DATA_FILE), config_data) - - systems = await simplisafe.get_systems() - async_add_entities( - [SimpliSafeAlarm(system, name, code) for system in systems], True) +async def async_setup_entry(hass, entry, async_add_entities): + """Set up a SimpliSafe alarm control panel based on a config entry.""" + systems = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] + async_add_entities([ + SimpliSafeAlarm(system, entry.data.get(CONF_CODE)) + for system in systems + ], True) class SimpliSafeAlarm(AlarmControlPanel): """Representation of a SimpliSafe alarm.""" - def __init__(self, system, name, code): + def __init__(self, system, code): """Initialize the SimpliSafe alarm.""" + self._async_unsub_dispatcher_connect = None self._attrs = {} - self._code = str(code) if code else None - self._name = name + self._code = code self._system = system self._state = None @@ -98,9 +56,7 @@ class SimpliSafeAlarm(AlarmControlPanel): @property def name(self): """Return the name of the device.""" - if self._name: - return self._name - return 'Alarm {}'.format(self._system.system_id) + return self._system.address @property def code_format(self): @@ -128,6 +84,21 @@ class SimpliSafeAlarm(AlarmControlPanel): _LOGGER.warning("Wrong code entered for %s", state) return check + async def async_added_to_hass(self): + """Register callbacks.""" + @callback + def update(): + """Update the state.""" + self.async_schedule_update_ha_state(True) + + self._async_unsub_dispatcher_connect = async_dispatcher_connect( + self.hass, TOPIC_UPDATE, update) + + async def async_will_remove_from_hass(self) -> None: + """Disconnect dispatcher listener when removed.""" + if self._async_unsub_dispatcher_connect: + self._async_unsub_dispatcher_connect() + async def async_alarm_disarm(self, code=None): """Send disarm command.""" if not self._validate_code(code, 'disarming'): @@ -151,22 +122,24 @@ class SimpliSafeAlarm(AlarmControlPanel): async def async_update(self): """Update alarm status.""" - await self._system.update() + from simplipy.system import SystemStates - if self._system.state == self._system.SystemStates.off: - self._state = STATE_ALARM_DISARMED - elif self._system.state in ( - self._system.SystemStates.home, - self._system.SystemStates.home_count): - self._state = STATE_ALARM_ARMED_HOME - elif self._system.state in ( - self._system.SystemStates.away, - self._system.SystemStates.away_count, - self._system.SystemStates.exit_delay): - self._state = STATE_ALARM_ARMED_AWAY - else: - self._state = None + await self._system.update() self._attrs[ATTR_ALARM_ACTIVE] = self._system.alarm_going_off if self._system.temperature: self._attrs[ATTR_TEMPERATURE] = self._system.temperature + + if self._system.state == SystemStates.error: + return + + if self._system.state == SystemStates.off: + self._state = STATE_ALARM_DISARMED + elif self._system.state in (SystemStates.home, + SystemStates.home_count): + self._state = STATE_ALARM_ARMED_HOME + elif self._system.state in (SystemStates.away, SystemStates.away_count, + SystemStates.exit_delay): + self._state = STATE_ALARM_ARMED_AWAY + else: + self._state = None diff --git a/homeassistant/components/simplisafe/.translations/en.json b/homeassistant/components/simplisafe/.translations/en.json new file mode 100644 index 00000000000..b000335af8f --- /dev/null +++ b/homeassistant/components/simplisafe/.translations/en.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Account already registered", + "invalid_credentials": "Invalid credentials" + }, + "step": { + "user": { + "data": { + "code": "Code (for Home Assistant)", + "password": "Password", + "username": "Email Address" + }, + "title": "Fill in your information" + } + }, + "title": "SimpliSafe" + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py new file mode 100644 index 00000000000..91ffb3dbde4 --- /dev/null +++ b/homeassistant/components/simplisafe/__init__.py @@ -0,0 +1,143 @@ +""" +Support for SimpliSafe alarm systems. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/simplisafe/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import ( + CONF_CODE, CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_TOKEN, CONF_USERNAME) +from homeassistant.core import callback +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval + +from homeassistant.helpers import config_validation as cv + +from .config_flow import configured_instances +from .const import DATA_CLIENT, DEFAULT_SCAN_INTERVAL, DOMAIN, TOPIC_UPDATE + +REQUIREMENTS = ['simplisafe-python==3.1.7'] + +_LOGGER = logging.getLogger(__name__) + +CONF_ACCOUNTS = 'accounts' + +DATA_LISTENER = 'listener' + +ACCOUNT_CONFIG_SCHEMA = vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_CODE): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): + cv.time_period +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Optional(CONF_ACCOUNTS): + vol.All(cv.ensure_list, [ACCOUNT_CONFIG_SCHEMA]), + }), +}, extra=vol.ALLOW_EXTRA) + + +@callback +def _async_save_refresh_token(hass, config_entry, token): + hass.config_entries.async_update_entry( + config_entry, data={ + **config_entry.data, CONF_TOKEN: token + }) + + +async def async_setup(hass, config): + """Set up the SimpliSafe component.""" + hass.data[DOMAIN] = {} + hass.data[DOMAIN][DATA_CLIENT] = {} + hass.data[DOMAIN][DATA_LISTENER] = {} + + if DOMAIN not in config: + return True + + conf = config[DOMAIN] + + for account in conf[CONF_ACCOUNTS]: + if account[CONF_USERNAME] in configured_instances(hass): + continue + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={'source': SOURCE_IMPORT}, + data={ + CONF_USERNAME: account[CONF_USERNAME], + CONF_PASSWORD: account[CONF_PASSWORD], + CONF_CODE: account.get(CONF_CODE), + CONF_SCAN_INTERVAL: account[CONF_SCAN_INTERVAL], + })) + + return True + + +async def async_setup_entry(hass, config_entry): + """Set up SimpliSafe as config entry.""" + from simplipy import API + from simplipy.errors import SimplipyError + + websession = aiohttp_client.async_get_clientsession(hass) + + try: + simplisafe = await API.login_via_token( + config_entry.data[CONF_TOKEN], websession) + except SimplipyError as err: + if 403 in str(err): + _LOGGER.error('Invalid credentials provided') + return False + + _LOGGER.error('Config entry failed: %s', err) + raise ConfigEntryNotReady + + _async_save_refresh_token(hass, config_entry, simplisafe.refresh_token) + + systems = await simplisafe.get_systems() + hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = systems + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup( + config_entry, 'alarm_control_panel')) + + async def refresh(event_time): + """Refresh data from the SimpliSafe account.""" + for system in systems: + _LOGGER.debug('Updating system data: %s', system.system_id) + await system.update() + async_dispatcher_send(hass, TOPIC_UPDATE.format(system.system_id)) + + if system.api.refresh_token_dirty: + _async_save_refresh_token( + hass, config_entry, system.api.refresh_token) + + hass.data[DOMAIN][DATA_LISTENER][ + config_entry.entry_id] = async_track_time_interval( + hass, + refresh, + timedelta(seconds=config_entry.data[CONF_SCAN_INTERVAL])) + + return True + + +async def async_unload_entry(hass, entry): + """Unload a SimpliSafe config entry.""" + await hass.config_entries.async_forward_entry_unload( + entry, 'alarm_control_panel') + + hass.data[DOMAIN][DATA_CLIENT].pop(entry.entry_id) + remove_listener = hass.data[DOMAIN][DATA_LISTENER].pop(entry.entry_id) + remove_listener() + + return True diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py new file mode 100644 index 00000000000..0a59dcb3e1d --- /dev/null +++ b/homeassistant/components/simplisafe/config_flow.py @@ -0,0 +1,80 @@ +"""Config flow to configure the SimpliSafe component.""" + +from collections import OrderedDict + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.core import callback +from homeassistant.const import ( + CONF_CODE, CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_TOKEN, CONF_USERNAME) +from homeassistant.helpers import aiohttp_client + +from .const import DEFAULT_SCAN_INTERVAL, DOMAIN + + +@callback +def configured_instances(hass): + """Return a set of configured SimpliSafe instances.""" + return set( + entry.data[CONF_USERNAME] + for entry in hass.config_entries.async_entries(DOMAIN)) + + +@config_entries.HANDLERS.register(DOMAIN) +class SimpliSafeFlowHandler(config_entries.ConfigFlow): + """Handle a SimpliSafe config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self): + """Initialize the config flow.""" + self.data_schema = OrderedDict() + self.data_schema[vol.Required(CONF_USERNAME)] = str + self.data_schema[vol.Required(CONF_PASSWORD)] = str + self.data_schema[vol.Optional(CONF_CODE)] = str + + async def _show_form(self, errors=None): + """Show the form to the user.""" + return self.async_show_form( + step_id='user', + data_schema=vol.Schema(self.data_schema), + errors=errors if errors else {}, + ) + + async def async_step_import(self, import_config): + """Import a config entry from configuration.yaml.""" + return await self.async_step_user(import_config) + + async def async_step_user(self, user_input=None): + """Handle the start of the config flow.""" + from simplipy import API + from simplipy.errors import SimplipyError + + if not user_input: + return await self._show_form() + + if user_input[CONF_USERNAME] in configured_instances(self.hass): + return await self._show_form({CONF_USERNAME: 'identifier_exists'}) + + username = user_input[CONF_USERNAME] + websession = aiohttp_client.async_get_clientsession(self.hass) + + try: + simplisafe = await API.login_via_credentials( + username, user_input[CONF_PASSWORD], websession) + except SimplipyError: + return await self._show_form({'base': 'invalid_credentials'}) + + scan_interval = user_input.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + + return self.async_create_entry( + title=user_input[CONF_USERNAME], + data={ + CONF_USERNAME: username, + CONF_TOKEN: simplisafe.refresh_token, + CONF_SCAN_INTERVAL: scan_interval.seconds, + }, + ) diff --git a/homeassistant/components/simplisafe/const.py b/homeassistant/components/simplisafe/const.py new file mode 100644 index 00000000000..437197878e0 --- /dev/null +++ b/homeassistant/components/simplisafe/const.py @@ -0,0 +1,10 @@ +"""Define constants for the SimpliSafe component.""" +from datetime import timedelta + +DOMAIN = 'simplisafe' + +DATA_CLIENT = 'client' + +DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) + +TOPIC_UPDATE = 'update_{0}' diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json new file mode 100644 index 00000000000..5df0cf400d4 --- /dev/null +++ b/homeassistant/components/simplisafe/strings.json @@ -0,0 +1,19 @@ +{ + "config": { + "title": "SimpliSafe", + "step": { + "user": { + "title": "Fill in your information", + "data": { + "username": "Email Address", + "password": "Password", + "code": "Code (for Home Assistant)" + } + } + }, + "error": { + "identifier_exists": "Account already registered", + "invalid_credentials": "Invalid credentials" + } + } +} diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index e4719b3ed78..a4f28b63fb1 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -146,6 +146,7 @@ FLOWS = [ 'mqtt', 'nest', 'openuv', + 'simplisafe', 'smhi', 'sonos', 'tradfri', diff --git a/requirements_all.txt b/requirements_all.txt index ba7d05fc7e3..0542a0ddf80 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1346,8 +1346,8 @@ shodan==1.10.4 # homeassistant.components.notify.simplepush simplepush==1.1.4 -# homeassistant.components.alarm_control_panel.simplisafe -simplisafe-python==3.1.2 +# homeassistant.components.simplisafe +simplisafe-python==3.1.7 # homeassistant.components.sisyphus sisyphus-control==2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index db67e27cc61..e449a9ae98a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -214,6 +214,9 @@ ring_doorbell==0.2.1 # homeassistant.components.media_player.yamaha rxv==0.5.1 +# homeassistant.components.simplisafe +simplisafe-python==3.1.7 + # homeassistant.components.sleepiq sleepyq==0.6 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index fd161898acc..fd8d673f633 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -97,6 +97,7 @@ TEST_REQUIREMENTS = ( 'rflink', 'ring_doorbell', 'rxv', + 'simplisafe-python', 'sleepyq', 'smhi-pkg', 'somecomfort', diff --git a/tests/components/simplisafe/__init__.py b/tests/components/simplisafe/__init__.py new file mode 100644 index 00000000000..b1cc391eec9 --- /dev/null +++ b/tests/components/simplisafe/__init__.py @@ -0,0 +1 @@ +"""Define tests for the SimpliSafe component.""" diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py new file mode 100644 index 00000000000..63b932ee681 --- /dev/null +++ b/tests/components/simplisafe/test_config_flow.py @@ -0,0 +1,120 @@ +"""Define tests for the SimpliSafe config flow.""" +import json +from datetime import timedelta +from unittest.mock import mock_open, patch, MagicMock, PropertyMock + +from homeassistant import data_entry_flow +from homeassistant.components.simplisafe import DOMAIN, config_flow +from homeassistant.const import ( + CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_TOKEN, CONF_USERNAME) + +from tests.common import MockConfigEntry, mock_coro + + +def mock_api(): + """Mock SimpliSafe API class.""" + api = MagicMock() + type(api).refresh_token = PropertyMock(return_value='12345abc') + return api + + +async def test_duplicate_error(hass): + """Test that errors are shown when duplicates are added.""" + conf = { + CONF_USERNAME: 'user@email.com', + CONF_PASSWORD: 'password', + } + + MockConfigEntry(domain=DOMAIN, data=conf).add_to_hass(hass) + flow = config_flow.SimpliSafeFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=conf) + assert result['errors'] == {CONF_USERNAME: 'identifier_exists'} + + +async def test_invalid_credentials(hass): + """Test that invalid credentials throws an error.""" + from simplipy.errors import SimplipyError + conf = { + CONF_USERNAME: 'user@email.com', + CONF_PASSWORD: 'password', + } + + flow = config_flow.SimpliSafeFlowHandler() + flow.hass = hass + + with patch('simplipy.API.login_via_credentials', + return_value=mock_coro(exception=SimplipyError)): + result = await flow.async_step_user(user_input=conf) + assert result['errors'] == {'base': 'invalid_credentials'} + + +async def test_show_form(hass): + """Test that the form is served with no input.""" + flow = config_flow.SimpliSafeFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=None) + + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'user' + + +async def test_step_import(hass): + """Test that the import step works.""" + conf = { + CONF_USERNAME: 'user@email.com', + CONF_PASSWORD: 'password', + } + + flow = config_flow.SimpliSafeFlowHandler() + flow.hass = hass + + mop = mock_open(read_data=json.dumps({'refresh_token': '12345'})) + + with patch('simplipy.API.login_via_credentials', + return_value=mock_coro(return_value=mock_api())): + with patch('homeassistant.util.json.open', mop, create=True): + with patch('homeassistant.util.json.os.open', return_value=0): + with patch('homeassistant.util.json.os.replace'): + result = await flow.async_step_import(import_config=conf) + + assert result[ + 'type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result['title'] == 'user@email.com' + assert result['data'] == { + CONF_USERNAME: 'user@email.com', + CONF_TOKEN: '12345abc', + CONF_SCAN_INTERVAL: 30, + } + + +async def test_step_user(hass): + """Test that the user step works.""" + conf = { + CONF_USERNAME: 'user@email.com', + CONF_PASSWORD: 'password', + CONF_SCAN_INTERVAL: timedelta(seconds=90), + } + + flow = config_flow.SimpliSafeFlowHandler() + flow.hass = hass + + mop = mock_open(read_data=json.dumps({'refresh_token': '12345'})) + + with patch('simplipy.API.login_via_credentials', + return_value=mock_coro(return_value=mock_api())): + with patch('homeassistant.util.json.open', mop, create=True): + with patch('homeassistant.util.json.os.open', return_value=0): + with patch('homeassistant.util.json.os.replace'): + result = await flow.async_step_user(user_input=conf) + + assert result[ + 'type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result['title'] == 'user@email.com' + assert result['data'] == { + CONF_USERNAME: 'user@email.com', + CONF_TOKEN: '12345abc', + CONF_SCAN_INTERVAL: 90, + } From 434d1d7d63d2a56d1c474e4ff0063a590b8b83d1 Mon Sep 17 00:00:00 2001 From: Tom French Date: Fri, 12 Oct 2018 19:04:52 +0100 Subject: [PATCH 121/265] Added option to use a location other than home (#17340) --- .../components/sensor/geo_rss_events.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sensor/geo_rss_events.py b/homeassistant/components/sensor/geo_rss_events.py index 22b4c71a705..60ae9730d80 100644 --- a/homeassistant/components/sensor/geo_rss_events.py +++ b/homeassistant/components/sensor/geo_rss_events.py @@ -16,7 +16,8 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - STATE_UNKNOWN, CONF_UNIT_OF_MEASUREMENT, CONF_NAME, CONF_RADIUS, CONF_URL) + STATE_UNKNOWN, CONF_UNIT_OF_MEASUREMENT, CONF_NAME, + CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS, CONF_URL) from homeassistant.helpers.entity import Entity REQUIREMENTS = ['georss_client==0.3'] @@ -40,6 +41,8 @@ SCAN_INTERVAL = timedelta(minutes=5) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_URL): cv.string, + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS_IN_KM): vol.Coerce(float), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_CATEGORIES, default=[]): @@ -51,8 +54,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the GeoRSS component.""" - home_latitude = hass.config.latitude - home_longitude = hass.config.longitude + latitude = config.get(CONF_LATITUDE, hass.config.latitude) + longitude = config.get(CONF_LONGITUDE, hass.config.longitude) url = config.get(CONF_URL) radius_in_km = config.get(CONF_RADIUS) name = config.get(CONF_NAME) @@ -60,18 +63,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT) _LOGGER.debug("latitude=%s, longitude=%s, url=%s, radius=%s", - home_latitude, home_longitude, url, radius_in_km) + latitude, longitude, url, radius_in_km) # Create all sensors based on categories. devices = [] if not categories: - device = GeoRssServiceSensor((home_latitude, home_longitude), url, + device = GeoRssServiceSensor((latitude, longitude), url, radius_in_km, None, name, unit_of_measurement) devices.append(device) else: for category in categories: - device = GeoRssServiceSensor((home_latitude, home_longitude), url, + device = GeoRssServiceSensor((latitude, longitude), url, radius_in_km, category, name, unit_of_measurement) devices.append(device) @@ -81,7 +84,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class GeoRssServiceSensor(Entity): """Representation of a Sensor.""" - def __init__(self, home_coordinates, url, radius, category, service_name, + def __init__(self, coordinates, url, radius, category, service_name, unit_of_measurement): """Initialize the sensor.""" self._category = category @@ -90,7 +93,7 @@ class GeoRssServiceSensor(Entity): self._state_attributes = None self._unit_of_measurement = unit_of_measurement from georss_client.generic_feed import GenericFeed - self._feed = GenericFeed(home_coordinates, url, filter_radius=radius, + self._feed = GenericFeed(coordinates, url, filter_radius=radius, filter_categories=None if not category else [category]) From 3cf6c76f8b32b37cc59c1dc5a596da920b20bbe2 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Fri, 12 Oct 2018 15:32:35 -0700 Subject: [PATCH 122/265] Add unique_id for Lightify (#17377) --- homeassistant/components/light/osramlightify.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/homeassistant/components/light/osramlightify.py b/homeassistant/components/light/osramlightify.py index 244a233c517..a49e12c76a6 100644 --- a/homeassistant/components/light/osramlightify.py +++ b/homeassistant/components/light/osramlightify.py @@ -231,6 +231,11 @@ class OsramLightifyLight(Luminary): self._luminary.temp()) self._brightness = int(self._luminary.lum() * 2.55) + @property + def unique_id(self): + """Return a unique ID.""" + return self._light_id + class OsramLightifyGroup(Luminary): """Representation of an Osram Lightify Group.""" @@ -240,6 +245,7 @@ class OsramLightifyGroup(Luminary): self._bridge = bridge self._light_ids = [] super().__init__(group, update_lights) + self._unique_id = '{}'.format(self._light_ids) def _get_state(self): """Get state of group.""" @@ -260,3 +266,8 @@ class OsramLightifyGroup(Luminary): else: self._temperature = color_temperature_kelvin_to_mired(o_temp) self._state = light.on() + + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id From d3672f36fbde77ee46ff4a0f65780067c940fa82 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Fri, 12 Oct 2018 15:33:13 -0700 Subject: [PATCH 123/265] Add unique_id for Neato (#17369) * Add unique_id for Neato * Only send the serial per review comments --- homeassistant/components/camera/neato.py | 5 +++++ homeassistant/components/switch/neato.py | 6 ++++++ homeassistant/components/vacuum/neato.py | 6 ++++++ 3 files changed, 17 insertions(+) diff --git a/homeassistant/components/camera/neato.py b/homeassistant/components/camera/neato.py index b080dbbae10..4df423344bb 100644 --- a/homeassistant/components/camera/neato.py +++ b/homeassistant/components/camera/neato.py @@ -63,3 +63,8 @@ class NeatoCleaningMap(Camera): def name(self): """Return the name of this camera.""" return self._robot_name + + @property + def unique_id(self): + """Return unique ID.""" + return self._robot_serial diff --git a/homeassistant/components/switch/neato.py b/homeassistant/components/switch/neato.py index d9850c1589a..0b49cb71ba2 100644 --- a/homeassistant/components/switch/neato.py +++ b/homeassistant/components/switch/neato.py @@ -52,6 +52,7 @@ class NeatoConnectedSwitch(ToggleEntity): self._state = None self._schedule_state = None self._clean_state = None + self._robot_serial = self.robot.serial def update(self): """Update the states of Neato switches.""" @@ -83,6 +84,11 @@ class NeatoConnectedSwitch(ToggleEntity): """Return True if entity is available.""" return self._state + @property + def unique_id(self): + """Return a unique ID.""" + return self._robot_serial + @property def is_on(self): """Return true if switch is on.""" diff --git a/homeassistant/components/vacuum/neato.py b/homeassistant/components/vacuum/neato.py index aa446f8cd20..53f83a1de1d 100644 --- a/homeassistant/components/vacuum/neato.py +++ b/homeassistant/components/vacuum/neato.py @@ -66,6 +66,7 @@ class NeatoConnectedVacuum(StateVacuumDevice): self.clean_suspension_time = None self._available = False self._battery_level = None + self._robot_serial = self.robot.serial def update(self): """Update the states of Neato Vacuums.""" @@ -156,6 +157,11 @@ class NeatoConnectedVacuum(StateVacuumDevice): """Return the status of the vacuum cleaner.""" return self._clean_state + @property + def unique_id(self): + """Return a unique ID.""" + return self._robot_serial + @property def device_state_attributes(self): """Return the state attributes of the vacuum cleaner.""" From 5f16f3c3a66af1912a8f083377dbfe5d0ec14485 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Sat, 13 Oct 2018 01:23:00 -0700 Subject: [PATCH 124/265] Add unique_id for Bloomsky (#17383) * Add unique_id for Bloomsky * Add bloomsky camera unique ID --- homeassistant/components/binary_sensor/bloomsky.py | 6 ++++++ homeassistant/components/camera/bloomsky.py | 5 +++++ homeassistant/components/sensor/bloomsky.py | 6 ++++++ 3 files changed, 17 insertions(+) diff --git a/homeassistant/components/binary_sensor/bloomsky.py b/homeassistant/components/binary_sensor/bloomsky.py index ecffb3accf3..971941f4dd6 100644 --- a/homeassistant/components/binary_sensor/bloomsky.py +++ b/homeassistant/components/binary_sensor/bloomsky.py @@ -50,6 +50,12 @@ class BloomSkySensor(BinarySensorDevice): self._sensor_name = sensor_name self._name = '{} {}'.format(device['DeviceName'], sensor_name) self._state = None + self._unique_id = '{}-{}'.format(self._device_id, self._sensor_name) + + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id @property def name(self): diff --git a/homeassistant/components/camera/bloomsky.py b/homeassistant/components/camera/bloomsky.py index 01e20e3ccd3..1c9266ca3a7 100644 --- a/homeassistant/components/camera/bloomsky.py +++ b/homeassistant/components/camera/bloomsky.py @@ -53,6 +53,11 @@ class BloomSkyCamera(Camera): return self._last_image + @property + def unique_id(self): + """Return a unique ID.""" + return self._id + @property def name(self): """Return the name of this BloomSky device.""" diff --git a/homeassistant/components/sensor/bloomsky.py b/homeassistant/components/sensor/bloomsky.py index 8926848102c..02cd456107f 100644 --- a/homeassistant/components/sensor/bloomsky.py +++ b/homeassistant/components/sensor/bloomsky.py @@ -63,6 +63,12 @@ class BloomSkySensor(Entity): self._sensor_name = sensor_name self._name = '{} {}'.format(device['DeviceName'], sensor_name) self._state = None + self._unique_id = '{}-{}'.format(self._device_id, self._sensor_name) + + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id @property def name(self): From d9d27733d1885de0723f91558973c038be0386ec Mon Sep 17 00:00:00 2001 From: Martin Berg <2682426+mbrrg@users.noreply.github.com> Date: Sat, 13 Oct 2018 10:30:49 +0200 Subject: [PATCH 125/265] Fix arm/disarm calls. (#17381) --- homeassistant/components/alarm_control_panel/spc.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/spc.py b/homeassistant/components/alarm_control_panel/spc.py index b4c49d4d190..7adbb616774 100644 --- a/homeassistant/components/alarm_control_panel/spc.py +++ b/homeassistant/components/alarm_control_panel/spc.py @@ -85,19 +85,23 @@ class SpcAlarm(alarm.AlarmControlPanel): async def async_alarm_disarm(self, code=None): """Send disarm command.""" from pyspcwebgw.const import AreaMode - self._api.change_mode(area=self._area, new_mode=AreaMode.UNSET) + await self._api.change_mode(area=self._area, + new_mode=AreaMode.UNSET) async def async_alarm_arm_home(self, code=None): """Send arm home command.""" from pyspcwebgw.const import AreaMode - self._api.change_mode(area=self._area, new_mode=AreaMode.PART_SET_A) + await self._api.change_mode(area=self._area, + new_mode=AreaMode.PART_SET_A) async def async_alarm_arm_night(self, code=None): """Send arm home command.""" from pyspcwebgw.const import AreaMode - self._api.change_mode(area=self._area, new_mode=AreaMode.PART_SET_B) + await self._api.change_mode(area=self._area, + new_mode=AreaMode.PART_SET_B) async def async_alarm_arm_away(self, code=None): """Send arm away command.""" from pyspcwebgw.const import AreaMode - self._api.change_mode(area=self._area, new_mode=AreaMode.FULL_SET) + await self._api.change_mode(area=self._area, + new_mode=AreaMode.FULL_SET) From db536797be28707d954e45361649cdad257fd1ce Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Sat, 13 Oct 2018 04:33:34 -0400 Subject: [PATCH 126/265] Bump insteonplm version to 0.15.0 (#17384) --- homeassistant/components/insteon/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index b3bb6e73b94..3980503a1ac 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -19,7 +19,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['insteonplm==0.14.2'] +REQUIREMENTS = ['insteonplm==0.15.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 0542a0ddf80..348902b7098 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -501,7 +501,7 @@ ihcsdk==2.2.0 influxdb==5.0.0 # homeassistant.components.insteon -insteonplm==0.14.2 +insteonplm==0.15.0 # homeassistant.components.sensor.iperf3 iperf3==0.1.10 From d4b092706a8ecb60364d161f899f07634391fda2 Mon Sep 17 00:00:00 2001 From: Florian Klien Date: Sat, 13 Oct 2018 10:37:42 +0200 Subject: [PATCH 127/265] XMPP async (#17283) * new lib dependencies, working old xmpp * non working aioxmpp * reverting to sync xmpp will try slixmpp instead of aioxmpp reasons: echo bot example of aioxmpp had blocking behavior (slixmpp echo bot works fine) closer API to sleekxmpp less dependencies than aioxmpp * first working slixmpp version * DEBUG messages, changed MUC call the joinMUC method changed from sleekxmpp to slixmpp added debug messages better name for cleanup callback * flake8 * little cleanup, tested MUC * requirements_all * dependencies managed by slixmpp, removed debug messages * resource configurable by user, requirements updated * changed __init__ parameter code format * removed trailing dots from LOG messages * changed super call to python3 format --- homeassistant/components/notify/xmpp.py | 65 ++++++++++++++----------- requirements_all.txt | 15 ++---- 2 files changed, 39 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/notify/xmpp.py b/homeassistant/components/notify/xmpp.py index c5678dff351..1f4417e07b5 100644 --- a/homeassistant/components/notify/xmpp.py +++ b/homeassistant/components/notify/xmpp.py @@ -12,12 +12,9 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService) from homeassistant.const import ( - CONF_PASSWORD, CONF_SENDER, CONF_RECIPIENT, CONF_ROOM) + CONF_PASSWORD, CONF_SENDER, CONF_RECIPIENT, CONF_ROOM, CONF_RESOURCE) -REQUIREMENTS = ['sleekxmpp==1.3.2', - 'dnspython3==1.15.0', - 'pyasn1==0.3.7', - 'pyasn1-modules==0.1.5'] +REQUIREMENTS = ['slixmpp==1.4.0'] _LOGGER = logging.getLogger(__name__) @@ -31,84 +28,94 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_TLS, default=True): cv.boolean, vol.Optional(CONF_VERIFY, default=True): cv.boolean, vol.Optional(CONF_ROOM, default=''): cv.string, + vol.Optional(CONF_RESOURCE, default="home-assistant"): cv.string, }) -def get_service(hass, config, discovery_info=None): +async def async_get_service(hass, config, discovery_info=None): """Get the Jabber (XMPP) notification service.""" return XmppNotificationService( - config.get(CONF_SENDER), config.get(CONF_PASSWORD), - config.get(CONF_RECIPIENT), config.get(CONF_TLS), - config.get(CONF_VERIFY), config.get(CONF_ROOM)) + config.get(CONF_SENDER), config.get(CONF_RESOURCE), + config.get(CONF_PASSWORD), config.get(CONF_RECIPIENT), + config.get(CONF_TLS), config.get(CONF_VERIFY), + config.get(CONF_ROOM), hass.loop) class XmppNotificationService(BaseNotificationService): """Implement the notification service for Jabber (XMPP).""" - def __init__(self, sender, password, recipient, tls, verify, room): + def __init__(self, sender, resource, password, + recipient, tls, verify, room, loop): """Initialize the service.""" + self._loop = loop self._sender = sender + self._resource = resource self._password = password self._recipient = recipient self._tls = tls self._verify = verify self._room = room - def send_message(self, message="", **kwargs): + async def async_send_message(self, message="", **kwargs): """Send a message to a user.""" title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) data = '{}: {}'.format(title, message) if title else message - send_message('{}/home-assistant'.format(self._sender), - self._password, self._recipient, self._tls, - self._verify, self._room, data) + await async_send_message( + '{}/{}'.format(self._sender, self._resource), + self._password, self._recipient, self._tls, + self._verify, self._room, self._loop, data) -def send_message(sender, password, recipient, use_tls, - verify_certificate, room, message): +async def async_send_message(sender, password, recipient, use_tls, + verify_certificate, room, loop, message): """Send a message over XMPP.""" - import sleekxmpp + import slixmpp - class SendNotificationBot(sleekxmpp.ClientXMPP): + class SendNotificationBot(slixmpp.ClientXMPP): """Service for sending Jabber (XMPP) messages.""" def __init__(self): """Initialize the Jabber Bot.""" - super(SendNotificationBot, self).__init__(sender, password) + super().__init__(sender, password) - self.use_tls = use_tls + # need hass.loop!! + self.loop = loop + + self.force_starttls = use_tls self.use_ipv6 = False - self.add_event_handler('failed_auth', self.check_credentials) + self.add_event_handler( + 'failed_auth', self.disconnect_on_login_fail) self.add_event_handler('session_start', self.start) + if room: self.register_plugin('xep_0045') # MUC if not verify_certificate: self.add_event_handler('ssl_invalid_cert', self.discard_ssl_invalid_cert) - self.connect(use_tls=self.use_tls, use_ssl=False) - self.process() + self.connect(force_starttls=self.force_starttls, use_ssl=False) def start(self, event): """Start the communication and sends the message.""" - self.send_presence() self.get_roster() - + self.send_presence() if room: - _LOGGER.debug("Joining room %s.", room) - self.plugin['xep_0045'].joinMUC(room, sender, wait=True) + _LOGGER.debug("Joining room %s", room) + self.plugin['xep_0045'].join_muc(room, sender, wait=True) self.send_message(mto=room, mbody=message, mtype='groupchat') else: self.send_message(mto=recipient, mbody=message, mtype='chat') self.disconnect(wait=True) - def check_credentials(self, event): + def disconnect_on_login_fail(self, event): """Disconnect from the server if credentials are invalid.""" + _LOGGER.warning('Login failed') self.disconnect() @staticmethod def discard_ssl_invalid_cert(event): """Do nothing if ssl certificate is invalid.""" - _LOGGER.info('Ignoring invalid ssl certificate as requested.') + _LOGGER.info('Ignoring invalid ssl certificate as requested') SendNotificationBot() diff --git a/requirements_all.txt b/requirements_all.txt index 348902b7098..ea13e493858 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -302,9 +302,6 @@ distro==1.3.0 # homeassistant.components.switch.digitalloggers dlipower==0.7.165 -# homeassistant.components.notify.xmpp -dnspython3==1.15.0 - # homeassistant.components.sensor.dovado dovado==0.4.1 @@ -797,12 +794,6 @@ pyalarmdotcom==0.3.2 # homeassistant.components.arlo pyarlo==0.2.0 -# homeassistant.components.notify.xmpp -pyasn1-modules==0.1.5 - -# homeassistant.components.notify.xmpp -pyasn1==0.3.7 - # homeassistant.components.netatmo pyatmo==1.2 @@ -1358,12 +1349,12 @@ skybellpy==0.1.2 # homeassistant.components.notify.slack slacker==0.9.65 -# homeassistant.components.notify.xmpp -sleekxmpp==1.3.2 - # homeassistant.components.sleepiq sleepyq==0.6 +# homeassistant.components.notify.xmpp +slixmpp==1.4.0 + # homeassistant.components.smappee smappy==0.2.16 From 70281a148bf590a568a18cb460f401ea4721ed24 Mon Sep 17 00:00:00 2001 From: Tommy Jonsson Date: Sat, 13 Oct 2018 10:54:35 +0200 Subject: [PATCH 128/265] Fix hangout.send_message requiring data key (#17393) --- homeassistant/components/hangouts/hangouts_bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hangouts/hangouts_bot.py b/homeassistant/components/hangouts/hangouts_bot.py index 8747bff9ba7..bceedb1acfa 100644 --- a/homeassistant/components/hangouts/hangouts_bot.py +++ b/homeassistant/components/hangouts/hangouts_bot.py @@ -304,7 +304,7 @@ class HangoutsBot: """Handle the send_message service.""" await self._async_send_message(service.data[ATTR_MESSAGE], service.data[ATTR_TARGET], - service.data[ATTR_DATA]) + service.data.get(ATTR_DATA, {})) async def async_handle_update_users_and_conversations(self, _=None): """Handle the update_users_and_conversations service.""" From e6d002c3774476e332866e12b363b9bd468605aa Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Sat, 13 Oct 2018 14:29:12 +0200 Subject: [PATCH 129/265] Update to async-upnp-client==0.12.5 (#17401) --- homeassistant/components/media_player/dlna_dmr.py | 2 +- homeassistant/components/upnp/__init__.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_player/dlna_dmr.py b/homeassistant/components/media_player/dlna_dmr.py index 8aab4bfa43a..25a729aed6d 100644 --- a/homeassistant/components/media_player/dlna_dmr.py +++ b/homeassistant/components/media_player/dlna_dmr.py @@ -25,7 +25,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.util import get_local_ip -REQUIREMENTS = ['async-upnp-client==0.12.4'] +REQUIREMENTS = ['async-upnp-client==0.12.5'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index f70fbcc4d20..bf9f8a4746d 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -30,7 +30,7 @@ from .config_flow import ensure_domain_data from .device import Device -REQUIREMENTS = ['async-upnp-client==0.12.4'] +REQUIREMENTS = ['async-upnp-client==0.12.5'] DEPENDENCIES = ['http'] NOTIFICATION_ID = 'upnp_notification' diff --git a/requirements_all.txt b/requirements_all.txt index ea13e493858..ab7c2104874 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -150,7 +150,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.upnp # homeassistant.components.media_player.dlna_dmr -async-upnp-client==0.12.4 +async-upnp-client==0.12.5 # homeassistant.components.light.avion # avion==0.7 From 9c178cf48895a4c43196eb01d3777eaa734c734b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 13 Oct 2018 15:31:53 +0300 Subject: [PATCH 130/265] Add unique id to syncthru sensors (#17399) --- homeassistant/components/sensor/syncthru.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/homeassistant/components/sensor/syncthru.py b/homeassistant/components/sensor/syncthru.py index 45a529012e3..862efb63fd7 100644 --- a/homeassistant/components/sensor/syncthru.py +++ b/homeassistant/components/sensor/syncthru.py @@ -115,6 +115,13 @@ class SyncThruSensor(Entity): self._name = name self._icon = 'mdi:printer' self._unit_of_measurement = None + self._id_suffix = '' + + @property + def unique_id(self): + """Return unique ID for the sensor.""" + serial = self.syncthru.serial_number() + return serial + self._id_suffix if serial else super().unique_id @property def name(self): @@ -145,6 +152,11 @@ class SyncThruSensor(Entity): class SyncThruMainSensor(SyncThruSensor): """Implementation of the main sensor, monitoring the general state.""" + def __init__(self, syncthru, name): + """Initialize the sensor.""" + super().__init__(syncthru, name) + self._id_suffix = '_main' + def update(self): """Get the latest data from SyncThru and update the state.""" self.syncthru.update() @@ -160,6 +172,7 @@ class SyncThruTonerSensor(SyncThruSensor): self._name = "{} Toner {}".format(name, color) self._color = color self._unit_of_measurement = '%' + self._id_suffix = '_toner_{}'.format(color) def update(self): """Get the latest data from SyncThru and update the state.""" @@ -180,6 +193,7 @@ class SyncThruDrumSensor(SyncThruSensor): self._name = "{} Drum {}".format(name, color) self._color = color self._unit_of_measurement = '%' + self._id_suffix = '_drum_{}'.format(color) def update(self): """Get the latest data from SyncThru and update the state.""" @@ -199,6 +213,7 @@ class SyncThruInputTraySensor(SyncThruSensor): super().__init__(syncthru, name) self._name = "{} Tray {}".format(name, number) self._number = number + self._id_suffix = '_tray_{}'.format(number) def update(self): """Get the latest data from SyncThru and update the state.""" @@ -220,6 +235,7 @@ class SyncThruOutputTraySensor(SyncThruSensor): super().__init__(syncthru, name) self._name = "{} Output Tray {}".format(name, number) self._number = number + self._id_suffix = '_output_tray_{}'.format(number) def update(self): """Get the latest data from SyncThru and update the state.""" From 78e29cd3fa25fe2a7c0d5c9fc87dfa8208dff9c9 Mon Sep 17 00:00:00 2001 From: Keiran S Date: Sun, 14 Oct 2018 06:03:30 +1100 Subject: [PATCH 131/265] Add AWS Route53 Dynamic DNS support (#17072) * Add AWS Route53 dynamic DNS support to Home Assistant * Remove line breaks --- .coveragerc | 1 + homeassistant/components/route53.py | 113 ++++++++++++++++++++++++++++ requirements_all.txt | 4 + 3 files changed, 118 insertions(+) create mode 100644 homeassistant/components/route53.py diff --git a/.coveragerc b/.coveragerc index cf56182fd6d..df73e2066b7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -645,6 +645,7 @@ omit = homeassistant/components/remember_the_milk/__init__.py homeassistant/components/remote/harmony.py homeassistant/components/remote/itach.py + homeassistant/components/route53.py homeassistant/components/scene/hunterdouglas_powerview.py homeassistant/components/scene/lifx_cloud.py homeassistant/components/sensor/airvisual.py diff --git a/homeassistant/components/route53.py b/homeassistant/components/route53.py new file mode 100644 index 00000000000..f88a15b72b8 --- /dev/null +++ b/homeassistant/components/route53.py @@ -0,0 +1,113 @@ +""" +Update the IP addresses of your Route53 DNS records. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/route53/ +""" +from datetime import timedelta +import logging + +import voluptuous as vol + +from homeassistant.const import CONF_DOMAIN, CONF_ZONE +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import track_time_interval + +REQUIREMENTS = ['boto3==1.9.16', 'ipify==1.0.0'] + +_LOGGER = logging.getLogger(__name__) + +CONF_ACCESS_KEY_ID = 'aws_access_key_id' +CONF_SECRET_ACCESS_KEY = 'aws_secret_access_key' +CONF_RECORDS = 'records' + +DOMAIN = 'route53' + +INTERVAL = timedelta(minutes=60) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_ACCESS_KEY_ID): cv.string, + vol.Required(CONF_DOMAIN): cv.string, + vol.Required(CONF_RECORDS): vol.All(cv.ensure_list, [cv.string]), + vol.Required(CONF_SECRET_ACCESS_KEY): cv.string, + vol.Required(CONF_ZONE): cv.string, + }) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Route53 component.""" + domain = config[DOMAIN][CONF_DOMAIN] + records = config[DOMAIN][CONF_RECORDS] + zone = config[DOMAIN][CONF_ZONE] + aws_access_key_id = config[DOMAIN][CONF_ACCESS_KEY_ID] + aws_secret_access_key = config[DOMAIN][CONF_SECRET_ACCESS_KEY] + + def update_records_interval(now): + """Set up recurring update.""" + _update_route53( + aws_access_key_id, aws_secret_access_key, zone, domain, records) + + def update_records_service(now): + """Set up service for manual trigger.""" + _update_route53( + aws_access_key_id, aws_secret_access_key, zone, domain, records) + + track_time_interval(hass, update_records_interval, INTERVAL) + + hass.services.register(DOMAIN, 'update_records', update_records_service) + return True + + +def _update_route53( + aws_access_key_id, aws_secret_access_key, zone, domain, records): + import boto3 + from ipify import get_ip + from ipify import exceptions + + _LOGGER.debug("Starting update for zone %s", zone) + + client = boto3.client( + DOMAIN, + aws_access_key_id=aws_access_key_id, + aws_secret_access_key=aws_secret_access_key, + ) + + # Get the IP Address and build an array of changes + try: + ipaddress = get_ip() + + except exceptions.ConnectionError: + _LOGGER.warning("Unable to reach the ipify service") + return + + except exceptions.ServiceError: + _LOGGER.warning("Unable to complete the ipfy request") + return + + changes = [] + for record in records: + _LOGGER.debug("Processing record: %s", record) + + changes.append({ + 'Action': 'UPSERT', + 'ResourceRecordSet': { + 'Name': '{}.{}'.format(record, domain), + 'Type': 'A', + 'TTL': 300, + 'ResourceRecords': [ + {'Value': ipaddress}, + ], + } + }) + + _LOGGER.debug("Submitting the following changes to Route53") + _LOGGER.debug(changes) + + response = client.change_resource_record_sets( + HostedZoneId=zone, ChangeBatch={'Changes': changes}) + _LOGGER.debug("Response is %s", response) + + if response['ResponseMetadata']['HTTPStatusCode'] != 200: + _LOGGER.warning(response) diff --git a/requirements_all.txt b/requirements_all.txt index ab7c2104874..5793dc4946b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -199,6 +199,7 @@ blockchain==1.4.4 # homeassistant.components.sensor.bme680 # bme680==1.0.4 +# homeassistant.components.route53 # homeassistant.components.notify.aws_lambda # homeassistant.components.notify.aws_sns # homeassistant.components.notify.aws_sqs @@ -503,6 +504,9 @@ insteonplm==0.15.0 # homeassistant.components.sensor.iperf3 iperf3==0.1.10 +# homeassistant.components.route53 +ipify==1.0.0 + # homeassistant.components.verisure jsonpath==0.75 From ef8253c549deea37efd0ed640f72158a45a54ce7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20Bun=C4=8Di=C4=87?= Date: Sat, 13 Oct 2018 21:04:51 +0200 Subject: [PATCH 132/265] Added ssl and verify_ssl parameters in ddwrt device tracker component (#17406) * Added ssl and verify_ssl parameters in ddwrt device tracker component * Set defaults in validation --- .../components/device_tracker/ddwrt.py | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/device_tracker/ddwrt.py b/homeassistant/components/device_tracker/ddwrt.py index 539d4fde5ef..cf8c8e1779b 100644 --- a/homeassistant/components/device_tracker/ddwrt.py +++ b/homeassistant/components/device_tracker/ddwrt.py @@ -10,20 +10,26 @@ import re import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ( + CONF_HOST, CONF_PASSWORD, CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) _DDWRT_DATA_REGEX = re.compile(r'\{(\w+)::([^\}]*)\}') _MAC_REGEX = re.compile(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})') +DEFAULT_SSL = False +DEFAULT_VERIFY_SSL = True + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, }) @@ -39,7 +45,9 @@ class DdWrtDeviceScanner(DeviceScanner): """This class queries a wireless router running DD-WRT firmware.""" def __init__(self, config): - """Initialize the scanner.""" + """Initialize the DD-WRT scanner.""" + self.protocol = 'https' if config[CONF_SSL] else 'http' + self.verify_ssl = config[CONF_VERIFY_SSL] self.host = config[CONF_HOST] self.username = config[CONF_USERNAME] self.password = config[CONF_PASSWORD] @@ -48,7 +56,8 @@ class DdWrtDeviceScanner(DeviceScanner): self.mac2name = {} # Test the router is accessible - url = 'http://{}/Status_Wireless.live.asp'.format(self.host) + url = '{}://{}/Status_Wireless.live.asp'.format( + self.protocol, self.host) data = self.get_ddwrt_data(url) if not data: raise ConnectionError('Cannot connect to DD-Wrt router') @@ -63,7 +72,8 @@ class DdWrtDeviceScanner(DeviceScanner): """Return the name of the given device or None if we don't know.""" # If not initialised and not already scanned and not found. if device not in self.mac2name: - url = 'http://{}/Status_Lan.live.asp'.format(self.host) + url = '{}://{}/Status_Lan.live.asp'.format( + self.protocol, self.host) data = self.get_ddwrt_data(url) if not data: @@ -98,7 +108,8 @@ class DdWrtDeviceScanner(DeviceScanner): """ _LOGGER.info("Checking ARP") - url = 'http://{}/Status_Wireless.live.asp'.format(self.host) + url = '{}://{}/Status_Wireless.live.asp'.format( + self.protocol, self.host) data = self.get_ddwrt_data(url) if not data: @@ -125,7 +136,8 @@ class DdWrtDeviceScanner(DeviceScanner): """Retrieve data from DD-WRT and return parsed result.""" try: response = requests.get( - url, auth=(self.username, self.password), timeout=4) + url, auth=(self.username, self.password), + timeout=4, verify=self.verify_ssl) except requests.exceptions.Timeout: _LOGGER.exception("Connection to the router timed out") return @@ -142,5 +154,4 @@ class DdWrtDeviceScanner(DeviceScanner): def _parse_ddwrt_response(data_str): """Parse the DD-WRT data format.""" return { - key: val for key, val in _DDWRT_DATA_REGEX - .findall(data_str)} + key: val for key, val in _DDWRT_DATA_REGEX.findall(data_str)} From 3ca3fe7015e9ff3fda16033311fd6ad1c9d73c89 Mon Sep 17 00:00:00 2001 From: Dan Klaffenbach Date: Sat, 13 Oct 2018 21:08:06 +0200 Subject: [PATCH 133/265] homematic: Support additional property for sabotage detection (#17407) At least HM-Sec-Sir-WM uses ERROR_SABOTAGE, see: pyhomematic.devicetypes.actors.RFSiren --- homeassistant/components/homematic/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 4d9d0c2f670..635be9ed172 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -106,6 +106,7 @@ HM_ATTRIBUTE_SUPPORT = { 'LOWBAT': ['battery', {0: 'High', 1: 'Low'}], 'LOW_BAT': ['battery', {0: 'High', 1: 'Low'}], 'ERROR': ['sabotage', {0: 'No', 1: 'Yes'}], + 'ERROR_SABOTAGE': ['sabotage', {0: 'No', 1: 'Yes'}], 'SABOTAGE': ['sabotage', {0: 'No', 1: 'Yes'}], 'RSSI_PEER': ['rssi', {}], 'RSSI_DEVICE': ['rssi', {}], From d0da26516644ddf7e39cf7b3ee388db12be313c8 Mon Sep 17 00:00:00 2001 From: J4nsen Date: Sat, 13 Oct 2018 21:09:10 +0200 Subject: [PATCH 134/265] Fix netio component (#17411) * bump pynetio to 0.1.9.1 to actually use provided credentials. * cast to int to fix TypeError * update requirements_all.txt --- homeassistant/components/switch/netio.py | 14 +++++++------- requirements_all.txt | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/switch/netio.py b/homeassistant/components/switch/netio.py index fc6086f9897..4492697406d 100644 --- a/homeassistant/components/switch/netio.py +++ b/homeassistant/components/switch/netio.py @@ -19,7 +19,7 @@ from homeassistant.const import ( from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pynetio==0.1.6'] +REQUIREMENTS = ['pynetio==0.1.9.1'] _LOGGER = logging.getLogger(__name__) @@ -151,15 +151,15 @@ class NetioSwitch(SwitchDevice): def _set(self, value): val = list('uuuu') - val[self.outlet - 1] = '1' if value else '0' + val[int(self.outlet) - 1] = '1' if value else '0' self.netio.get('port list %s' % ''.join(val)) - self.netio.states[self.outlet - 1] = value + self.netio.states[int(self.outlet) - 1] = value self.schedule_update_ha_state() @property def is_on(self): """Return the switch's status.""" - return self.netio.states[self.outlet - 1] + return self.netio.states[int(self.outlet) - 1] def update(self): """Update the state.""" @@ -176,14 +176,14 @@ class NetioSwitch(SwitchDevice): @property def current_power_w(self): """Return actual power.""" - return self.netio.consumptions[self.outlet - 1] + return self.netio.consumptions[int(self.outlet) - 1] @property def cumulated_consumption_kwh(self): """Return the total enerygy consumption since start_date.""" - return self.netio.cumulated_consumptions[self.outlet - 1] + return self.netio.cumulated_consumptions[int(self.outlet) - 1] @property def start_date(self): """Point in time when the energy accumulation started.""" - return self.netio.start_dates[self.outlet - 1] + return self.netio.start_dates[int(self.outlet) - 1] diff --git a/requirements_all.txt b/requirements_all.txt index 5793dc4946b..e278a012acb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1004,7 +1004,7 @@ pynello==1.5.1 pynetgear==0.4.2 # homeassistant.components.switch.netio -pynetio==0.1.6 +pynetio==0.1.9.1 # homeassistant.components.lock.nuki pynuki==1.3.1 From 0dd3640c783057762d5376e779b17af277d33ed1 Mon Sep 17 00:00:00 2001 From: noxhirsch <30938717+noxhirsch@users.noreply.github.com> Date: Sat, 13 Oct 2018 22:58:41 +0200 Subject: [PATCH 135/265] Adding support for HmIP-SLO (outdoor brightness sensor) (#17413) * Add IPBrightnessSensor * Add ILLUMINATION unit & icon * Update homematic.py * Added missing entry --- homeassistant/components/homematic/__init__.py | 2 +- homeassistant/components/sensor/homematic.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 635be9ed172..c87e2926ae8 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -76,7 +76,7 @@ HM_DEVICE_TYPES = { 'IPSmoke', 'RFSiren', 'PresenceIP', 'IPAreaThermostat', 'IPWeatherSensor', 'RotaryHandleSensorIP', 'IPPassageSensor', 'IPKeySwitchPowermeter', 'IPThermostatWall230V', 'IPWeatherSensorPlus', - 'IPWeatherSensorBasic'], + 'IPWeatherSensorBasic', 'IPBrightnessSensor'], DISCOVER_CLIMATE: [ 'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2', 'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall', diff --git a/homeassistant/components/sensor/homematic.py b/homeassistant/components/sensor/homematic.py index 8495286c143..26fa76d94a9 100644 --- a/homeassistant/components/sensor/homematic.py +++ b/homeassistant/components/sensor/homematic.py @@ -48,6 +48,10 @@ HM_UNIT_HA_CAST = { 'GAS_POWER': 'm3', 'GAS_ENERGY_COUNTER': 'm3', 'LUX': 'lx', + 'CURRENT_ILLUMINATION': 'lx', + 'AVERAGE_ILLUMINATION': 'lx', + 'LOWEST_ILLUMINATION': 'lx', + 'HIGHEST_ILLUMINATION': 'lx', 'RAIN_COUNTER': 'mm', 'WIND_SPEED': 'km/h', 'WIND_DIRECTION': '°', @@ -64,6 +68,10 @@ HM_ICON_HA_CAST = { 'TEMPERATURE': 'mdi:thermometer', 'ACTUAL_TEMPERATURE': 'mdi:thermometer', 'LUX': 'mdi:weather-sunny', + 'CURRENT_ILLUMINATION': 'mdi:weather-sunny', + 'AVERAGE_ILLUMINATION': 'mdi:weather-sunny', + 'LOWEST_ILLUMINATION': 'mdi:weather-sunny', + 'HIGHEST_ILLUMINATION': 'mdi:weather-sunny', 'BRIGHTNESS': 'mdi:invert-colors', 'POWER': 'mdi:flash-red-eye', 'CURRENT': 'mdi:flash-red-eye', From f23708ce6fb9406610e8dd3b033bd91df84f7702 Mon Sep 17 00:00:00 2001 From: Fredrik Baberg Date: Sat, 13 Oct 2018 23:02:00 +0200 Subject: [PATCH 136/265] Update Vagrant Windows support (#17205) * Update Vagrantfile with Hyper-V support * Correction to name. * Exclude files in Vagrant provision tests --- virtualization/vagrant/Vagrantfile | 8 +++++ virtualization/vagrant/provision.bat | 50 ++++++++++++++++++++++++++++ virtualization/vagrant/provision.sh | 4 +++ 3 files changed, 62 insertions(+) create mode 100644 virtualization/vagrant/provision.bat diff --git a/virtualization/vagrant/Vagrantfile b/virtualization/vagrant/Vagrantfile index e50c4e6de00..d3974d51a7a 100644 --- a/virtualization/vagrant/Vagrantfile +++ b/virtualization/vagrant/Vagrantfile @@ -13,4 +13,12 @@ Vagrant.configure(2) do |config| vb.cpus = 2 vb.customize ['modifyvm', :id, '--memory', '1024'] end + config.vm.provider :hyperv do |h, override| + override.vm.box = "generic/debian9" + override.vm.hostname = "contrib-stretch" + h.vmname = "home-assistant" + h.cpus = 2 + h.memory = 1024 + h.maxmemory = 1024 + end end diff --git a/virtualization/vagrant/provision.bat b/virtualization/vagrant/provision.bat new file mode 100644 index 00000000000..c8174e939a1 --- /dev/null +++ b/virtualization/vagrant/provision.bat @@ -0,0 +1,50 @@ +@echo off +call:main %* +goto:eof + +:usage +echo.############################################################ +echo. +echo.Use `./provision.bat` to interact with HASS. E.g: +echo. +echo.- setup the environment: `./provision.bat start` +echo.- restart HASS process: `./provision.bat restart` +echo.- run test suit: `./provision.bat tests` +echo.- destroy the host and start anew: `./provision.bat recreate` +echo. +echo.Official documentation at https://home-assistant.io/docs/installation/vagrant/ +echo. +echo.############################################################' +goto:eof + +:main +if "%*"=="setup" ( + if exist setup_done del setup_done + vagrant up --provision + copy /y nul setup_done +) else ( +if "%*"=="tests" ( + copy /y nul run_tests + vagrant provision +) else ( +if "%*"=="restart" ( + copy /y nul restart + vagrant provision +) else ( +if "%*"=="start" ( + vagrant up --provision +) else ( +if "%*"=="stop" ( + vagrant halt +) else ( +if "%*"=="destroy" ( + vagrant destroy -f +) else ( +if "%*"=="recreate" ( + if exist setup_done del setup_done + if exist restart del restart + vagrant destroy -f + vagrant up --provision +) else ( + call:usage +))))))) diff --git a/virtualization/vagrant/provision.sh b/virtualization/vagrant/provision.sh index d4ef4e0b446..1d2eecddc73 100755 --- a/virtualization/vagrant/provision.sh +++ b/virtualization/vagrant/provision.sh @@ -76,6 +76,10 @@ run_tests() { rsync -a --delete \ --exclude='*.tox' \ --exclude='*.git' \ + --exclude='.vagrant' \ + --exclude='lib64' \ + --exclude='bin/python' \ + --exclude='bin/python3' \ /home-assistant/ /home-assistant-tests/ cd /home-assistant-tests && tox || true echo '############################################################' From 6a8eb8d0a156ea34b01764505b98a4c86f01e6c2 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Sat, 13 Oct 2018 17:16:44 -0400 Subject: [PATCH 137/265] Moved econet from climate to water heater (#17322) * Moved econet from climate to water heater * Updated .coveragerc * Fixed requirements_all.txt --- .coveragerc | 2 +- .../components/climate/services.yaml | 20 ----------------- .../{climate => water_heater}/econet.py | 8 +++---- .../components/water_heater/services.yaml | 22 +++++++++++++++++++ requirements_all.txt | 2 +- 5 files changed, 28 insertions(+), 26 deletions(-) rename homeassistant/components/{climate => water_heater}/econet.py (96%) diff --git a/.coveragerc b/.coveragerc index df73e2066b7..fd40e588bf7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -434,7 +434,6 @@ omit = homeassistant/components/camera/xeoma.py homeassistant/components/camera/xiaomi.py homeassistant/components/camera/yi.py - homeassistant/components/climate/econet.py homeassistant/components/climate/ephember.py homeassistant/components/climate/eq3btsmart.py homeassistant/components/climate/flexit.py @@ -844,6 +843,7 @@ omit = homeassistant/components/tts/picotts.py homeassistant/components/vacuum/mqtt.py homeassistant/components/vacuum/roomba.py + homeassistant/components/water_heater/econet.py homeassistant/components/watson_iot.py homeassistant/components/weather/bom.py homeassistant/components/weather/buienradar.py diff --git a/homeassistant/components/climate/services.yaml b/homeassistant/components/climate/services.yaml index fbb21962c6e..e2a42770cb2 100644 --- a/homeassistant/components/climate/services.yaml +++ b/homeassistant/components/climate/services.yaml @@ -123,26 +123,6 @@ nuheat_resume_program: description: Name(s) of entities to change. example: 'climate.kitchen' -econet_add_vacation: - description: Add a vacation to your water heater. - fields: - entity_id: - description: Name(s) of entities to change. - example: 'climate.water_heater' - start_date: - description: The timestamp of when the vacation should start. (Optional, defaults to now) - example: 1513186320 - end_date: - description: The timestamp of when the vacation should end. - example: 1513445520 - -econet_delete_vacation: - description: Delete your existing vacation from your water heater. - fields: - entity_id: - description: Name(s) of entities to change. - example: 'climate.water_heater' - sensibo_assume_state: description: Set Sensibo device to external state. fields: diff --git a/homeassistant/components/climate/econet.py b/homeassistant/components/water_heater/econet.py similarity index 96% rename from homeassistant/components/climate/econet.py rename to homeassistant/components/water_heater/econet.py index 8be640c37e1..6af8ea43fa6 100644 --- a/homeassistant/components/climate/econet.py +++ b/homeassistant/components/water_heater/econet.py @@ -2,17 +2,17 @@ Support for Rheem EcoNet water heaters. For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/climate.econet/ +https://home-assistant.io/components/water_heater.econet/ """ import datetime import logging import voluptuous as vol -from homeassistant.components.climate import ( +from homeassistant.components.water_heater import ( DOMAIN, PLATFORM_SCHEMA, STATE_ECO, STATE_ELECTRIC, STATE_GAS, STATE_HEAT_PUMP, STATE_HIGH_DEMAND, STATE_OFF, STATE_PERFORMANCE, - SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, ClimateDevice) + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, WaterHeaterDevice) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, CONF_PASSWORD, CONF_USERNAME, TEMP_FAHRENHEIT) @@ -109,7 +109,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): schema=DELETE_VACATION_SCHEMA) -class EcoNetWaterHeater(ClimateDevice): +class EcoNetWaterHeater(WaterHeaterDevice): """Representation of an EcoNet water heater.""" def __init__(self, water_heater): diff --git a/homeassistant/components/water_heater/services.yaml b/homeassistant/components/water_heater/services.yaml index 959d1ca9790..72a3f909fbb 100644 --- a/homeassistant/components/water_heater/services.yaml +++ b/homeassistant/components/water_heater/services.yaml @@ -9,6 +9,7 @@ set_away_mode: away_mode: description: New value of away mode. example: true + set_temperature: description: Set target temperature of water_heater device. fields: @@ -18,6 +19,7 @@ set_temperature: temperature: description: New target temperature for water heater. example: 25 + set_operation_mode: description: Set operation mode for water_heater device. fields: @@ -27,3 +29,23 @@ set_operation_mode: operation_mode: description: New value of operation mode. example: eco + +econet_add_vacation: + description: Add a vacation to your water heater. + fields: + entity_id: + description: Name(s) of entities to change. + example: 'water_heater.econet' + start_date: + description: The timestamp of when the vacation should start. (Optional, defaults to now) + example: 1513186320 + end_date: + description: The timestamp of when the vacation should end. + example: 1513445520 + +econet_delete_vacation: + description: Delete your existing vacation from your water heater. + fields: + entity_id: + description: Name(s) of entities to change. + example: 'water_heater.econet' \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index e278a012acb..fe5d467062e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -857,7 +857,7 @@ pydukeenergy==0.0.6 # homeassistant.components.sensor.ebox pyebox==1.1.4 -# homeassistant.components.climate.econet +# homeassistant.components.water_heater.econet pyeconet==0.0.6 # homeassistant.components.switch.edimax From a3021128799245278ad7b2befb8ff5f1fcb67180 Mon Sep 17 00:00:00 2001 From: 333ryan18 Date: Sun, 14 Oct 2018 03:25:24 -0400 Subject: [PATCH 138/265] Bump Totalconnect (#17418) * Update totalconnect.py * Update requirements_all.txt --- homeassistant/components/alarm_control_panel/totalconnect.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/totalconnect.py b/homeassistant/components/alarm_control_panel/totalconnect.py index f594a798dce..2989bb1be37 100644 --- a/homeassistant/components/alarm_control_panel/totalconnect.py +++ b/homeassistant/components/alarm_control_panel/totalconnect.py @@ -18,7 +18,7 @@ from homeassistant.const import ( STATE_ALARM_ARMED_CUSTOM_BYPASS) -REQUIREMENTS = ['total_connect_client==0.18'] +REQUIREMENTS = ['total_connect_client==0.20'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index fe5d467062e..9b7f7a5c352 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1462,7 +1462,7 @@ todoist-python==7.0.17 toonlib==1.0.2 # homeassistant.components.alarm_control_panel.totalconnect -total_connect_client==0.18 +total_connect_client==0.20 # homeassistant.components.device_tracker.tplink tplink==0.2.1 From f1988594412f97e2543150118e353a405f92c036 Mon Sep 17 00:00:00 2001 From: Brant Knudson Date: Sun, 14 Oct 2018 02:26:34 -0500 Subject: [PATCH 139/265] Upgrade pyvera to 0.2.45 (#17419) This release adds support for garage door openers. A garage door opener will show up as a switch. --- homeassistant/components/vera.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vera.py b/homeassistant/components/vera.py index 5bc6260c0a7..fb8af0d8855 100644 --- a/homeassistant/components/vera.py +++ b/homeassistant/components/vera.py @@ -19,7 +19,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, CONF_LIGHTS, CONF_EXCLUDE) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyvera==0.2.44'] +REQUIREMENTS = ['pyvera==0.2.45'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 9b7f7a5c352..f7dc780a0a8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1236,7 +1236,7 @@ pyuptimerobot==0.0.5 # pyuserinput==0.1.11 # homeassistant.components.vera -pyvera==0.2.44 +pyvera==0.2.45 # homeassistant.components.switch.vesync pyvesync==0.1.1 From fccaf7f919415c211d7abcd9c0128968f4c0d466 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Sun, 14 Oct 2018 23:11:26 +1100 Subject: [PATCH 140/265] NSW Rural Fire Service platform (#16802) * initial integration with nsw rural fire service feed * improved test coverage * updated requirements * grouped imports * removed debug print statement * moved manager's startup code into separate call * simplified feed update code * simplified feed update code * simplified device state attribute code * added source to conform with pr #17339 * fixed lint * refactored how entities are managed * fixed pylint * simplified signalling --- .../nsw_rural_fire_service_feed.py | 284 ++++++++++++++++++ requirements_all.txt | 1 + requirements_test_all.txt | 1 + .../test_nsw_rural_fire_service_feed.py | 175 +++++++++++ 4 files changed, 461 insertions(+) create mode 100644 homeassistant/components/geo_location/nsw_rural_fire_service_feed.py create mode 100644 tests/components/geo_location/test_nsw_rural_fire_service_feed.py diff --git a/homeassistant/components/geo_location/nsw_rural_fire_service_feed.py b/homeassistant/components/geo_location/nsw_rural_fire_service_feed.py new file mode 100644 index 00000000000..52f261ba858 --- /dev/null +++ b/homeassistant/components/geo_location/nsw_rural_fire_service_feed.py @@ -0,0 +1,284 @@ +""" +NSW Rural Fire Service Feed platform. + +Retrieves current events (bush fires, grass fires, etc.) in GeoJSON format, +and displays information on events filtered by distance and category to the +HA instance's location. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/geo_location/nsw_rural_fire_service_feed/ +""" +import logging +from datetime import timedelta +from typing import Optional + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.geo_location import GeoLocationEvent, \ + PLATFORM_SCHEMA +from homeassistant.const import CONF_RADIUS, CONF_SCAN_INTERVAL, \ + EVENT_HOMEASSISTANT_START, ATTR_ATTRIBUTION +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import dispatcher_send, \ + async_dispatcher_connect +from homeassistant.helpers.event import track_time_interval + +REQUIREMENTS = ['geojson_client==0.1'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_CATEGORY = 'category' +ATTR_COUNCIL_AREA = 'council_area' +ATTR_EXTERNAL_ID = 'external_id' +ATTR_FIRE = 'fire' +ATTR_LOCATION = 'location' +ATTR_PUBLICATION_DATE = 'publication_date' +ATTR_RESPONSIBLE_AGENCY = 'responsible_agency' +ATTR_SIZE = 'size' +ATTR_STATUS = 'status' +ATTR_TYPE = 'type' + +CONF_CATEGORIES = 'categories' + +DEFAULT_RADIUS_IN_KM = 20.0 +DEFAULT_UNIT_OF_MEASUREMENT = "km" + +SCAN_INTERVAL = timedelta(minutes=5) + +SIGNAL_DELETE_ENTITY = 'nsw_rural_fire_service_feed_delete_{}' +SIGNAL_UPDATE_ENTITY = 'nsw_rural_fire_service_feed_update_{}' + +SOURCE = 'nsw_rural_fire_service_feed' + +VALID_CATEGORIES = ['Emergency Warning', 'Watch and Act', 'Advice', + 'Not Applicable'] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS_IN_KM): + vol.Coerce(float), + vol.Optional(CONF_CATEGORIES, default=[]): + vol.All(cv.ensure_list, [vol.In(VALID_CATEGORIES)]) +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the GeoJSON Events platform.""" + scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) + radius_in_km = config[CONF_RADIUS] + categories = config.get(CONF_CATEGORIES) + # Initialize the entity manager. + feed = NswRuralFireServiceFeedManager(hass, add_entities, scan_interval, + radius_in_km, categories) + + def start_feed_manager(event): + """Start feed manager.""" + feed.startup() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_feed_manager) + + +class NswRuralFireServiceFeedManager: + """Feed Manager for NSW Rural Fire Service GeoJSON feed.""" + + def __init__(self, hass, add_entities, scan_interval, radius_in_km, + categories): + """Initialize the GeoJSON Feed Manager.""" + from geojson_client.nsw_rural_fire_service_feed \ + import NswRuralFireServiceFeed + self._hass = hass + self._feed = NswRuralFireServiceFeed((hass.config.latitude, + hass.config.longitude), + filter_radius=radius_in_km, + filter_categories=categories) + self._add_entities = add_entities + self._scan_interval = scan_interval + self.feed_entries = {} + self._managed_external_ids = set() + + def startup(self): + """Start up this manager.""" + self._update() + self._init_regular_updates() + + def _init_regular_updates(self): + """Schedule regular updates at the specified interval.""" + track_time_interval(self._hass, lambda now: self._update(), + self._scan_interval) + + def _update(self): + """Update the feed and then update connected entities.""" + import geojson_client + status, feed_entries = self._feed.update() + if status == geojson_client.UPDATE_OK: + _LOGGER.debug("Data retrieved %s", feed_entries) + # Keep a copy of all feed entries for future lookups by entities. + self.feed_entries = {entry.external_id: entry + for entry in feed_entries} + # For entity management the external ids from the feed are used. + feed_external_ids = set(self.feed_entries) + remove_external_ids = self._managed_external_ids.difference( + feed_external_ids) + self._remove_entities(remove_external_ids) + update_external_ids = self._managed_external_ids.intersection( + feed_external_ids) + self._update_entities(update_external_ids) + create_external_ids = feed_external_ids.difference( + self._managed_external_ids) + self._generate_new_entities(create_external_ids) + elif status == geojson_client.UPDATE_OK_NO_DATA: + _LOGGER.debug("Update successful, but no data received from %s", + self._feed) + else: + _LOGGER.warning("Update not successful, no data received from %s", + self._feed) + # Remove all entities. + self._remove_entities(self._managed_external_ids.copy()) + + def _generate_new_entities(self, external_ids): + """Generate new entities for events.""" + new_entities = [] + for external_id in external_ids: + new_entity = NswRuralFireServiceLocationEvent(self, external_id) + _LOGGER.debug("New entity added %s", external_id) + new_entities.append(new_entity) + self._managed_external_ids.add(external_id) + # Add new entities to HA. + self._add_entities(new_entities, True) + + def _update_entities(self, external_ids): + """Update entities.""" + for external_id in external_ids: + _LOGGER.debug("Existing entity found %s", external_id) + dispatcher_send(self._hass, + SIGNAL_UPDATE_ENTITY.format(external_id)) + + def _remove_entities(self, external_ids): + """Remove entities.""" + for external_id in external_ids: + _LOGGER.debug("Entity not current anymore %s", external_id) + self._managed_external_ids.remove(external_id) + dispatcher_send(self._hass, + SIGNAL_DELETE_ENTITY.format(external_id)) + + +class NswRuralFireServiceLocationEvent(GeoLocationEvent): + """This represents an external event with GeoJSON data.""" + + def __init__(self, feed_manager, external_id): + """Initialize entity with data from feed entry.""" + self._feed_manager = feed_manager + self._external_id = external_id + self._name = None + self._distance = None + self._latitude = None + self._longitude = None + self._attribution = None + self._category = None + self._publication_date = None + self._location = None + self._council_area = None + self._status = None + self._type = None + self._fire = None + self._size = None + self._responsible_agency = None + + async def async_added_to_hass(self): + """Call when entity is added to hass.""" + async_dispatcher_connect( + self.hass, SIGNAL_DELETE_ENTITY.format(self._external_id), + self._delete_callback) + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_ENTITY.format(self._external_id), + self._update_callback) + + @callback + def _delete_callback(self): + """Remove this entity.""" + self.hass.async_create_task(self.async_remove()) + + @callback + def _update_callback(self): + """Call update method.""" + self.async_schedule_update_ha_state(True) + + @property + def should_poll(self): + """No polling needed for GeoJSON location events.""" + return False + + async def async_update(self): + """Update this entity from the data held in the feed manager.""" + _LOGGER.debug("Updating %s", self._external_id) + feed_entry = self._feed_manager.feed_entries.get(self._external_id) + if feed_entry: + self._update_from_feed(feed_entry) + + def _update_from_feed(self, feed_entry): + """Update the internal state from the provided feed entry.""" + self._name = feed_entry.title + self._distance = feed_entry.distance_to_home + self._latitude = feed_entry.coordinates[0] + self._longitude = feed_entry.coordinates[1] + self._attribution = feed_entry.attribution + self._category = feed_entry.category + self._publication_date = feed_entry.publication_date + self._location = feed_entry.location + self._council_area = feed_entry.council_area + self._status = feed_entry.status + self._type = feed_entry.type + self._fire = feed_entry.fire + self._size = feed_entry.size + self._responsible_agency = feed_entry.responsible_agency + + @property + def source(self) -> str: + """Return source value of this external event.""" + return SOURCE + + @property + def name(self) -> Optional[str]: + """Return the name of the entity.""" + return self._name + + @property + def distance(self) -> Optional[float]: + """Return distance value of this external event.""" + return self._distance + + @property + def latitude(self) -> Optional[float]: + """Return latitude value of this external event.""" + return self._latitude + + @property + def longitude(self) -> Optional[float]: + """Return longitude value of this external event.""" + return self._longitude + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return DEFAULT_UNIT_OF_MEASUREMENT + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + attributes = {} + for key, value in ( + (ATTR_EXTERNAL_ID, self._external_id), + (ATTR_CATEGORY, self._category), + (ATTR_LOCATION, self._location), + (ATTR_ATTRIBUTION, self._attribution), + (ATTR_PUBLICATION_DATE, self._publication_date), + (ATTR_COUNCIL_AREA, self._council_area), + (ATTR_STATUS, self._status), + (ATTR_TYPE, self._type), + (ATTR_FIRE, self._fire), + (ATTR_SIZE, self._size), + (ATTR_RESPONSIBLE_AGENCY, self._responsible_agency), + ): + if value or isinstance(value, bool): + attributes[key] = value + return attributes diff --git a/requirements_all.txt b/requirements_all.txt index f7dc780a0a8..63f1a451049 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -399,6 +399,7 @@ gearbest_parser==1.0.7 geizhals==0.0.7 # homeassistant.components.geo_location.geo_json_events +# homeassistant.components.geo_location.nsw_rural_fire_service_feed geojson_client==0.1 # homeassistant.components.sensor.geo_rss_events diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e449a9ae98a..9ad85eb8de6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -72,6 +72,7 @@ foobot_async==0.3.1 gTTS-token==1.1.2 # homeassistant.components.geo_location.geo_json_events +# homeassistant.components.geo_location.nsw_rural_fire_service_feed geojson_client==0.1 # homeassistant.components.sensor.geo_rss_events diff --git a/tests/components/geo_location/test_nsw_rural_fire_service_feed.py b/tests/components/geo_location/test_nsw_rural_fire_service_feed.py new file mode 100644 index 00000000000..92c4bae1931 --- /dev/null +++ b/tests/components/geo_location/test_nsw_rural_fire_service_feed.py @@ -0,0 +1,175 @@ +"""The tests for the geojson platform.""" +import datetime +import unittest +from unittest import mock +from unittest.mock import patch, MagicMock + +from homeassistant.components import geo_location +from homeassistant.components.geo_location import ATTR_SOURCE +from homeassistant.components.geo_location.nsw_rural_fire_service_feed import \ + ATTR_EXTERNAL_ID, SCAN_INTERVAL, ATTR_CATEGORY, ATTR_FIRE, ATTR_LOCATION, \ + ATTR_COUNCIL_AREA, ATTR_STATUS, ATTR_TYPE, ATTR_SIZE, \ + ATTR_RESPONSIBLE_AGENCY, ATTR_PUBLICATION_DATE +from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_START, \ + CONF_RADIUS, ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_FRIENDLY_NAME, \ + ATTR_UNIT_OF_MEASUREMENT, ATTR_ATTRIBUTION +from homeassistant.setup import setup_component +from tests.common import get_test_home_assistant, assert_setup_component, \ + fire_time_changed +import homeassistant.util.dt as dt_util + +URL = 'http://geo.json.local/geo_json_events.json' +CONFIG = { + geo_location.DOMAIN: [ + { + 'platform': 'nsw_rural_fire_service_feed', + CONF_URL: URL, + CONF_RADIUS: 200 + } + ] +} + + +class TestGeoJsonPlatform(unittest.TestCase): + """Test the geojson platform.""" + + def setUp(self): + """Initialize values for this testcase class.""" + self.hass = get_test_home_assistant() + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + @staticmethod + def _generate_mock_feed_entry(external_id, title, distance_to_home, + coordinates, category=None, location=None, + attribution=None, publication_date=None, + council_area=None, status=None, + entry_type=None, fire=True, size=None, + responsible_agency=None): + """Construct a mock feed entry for testing purposes.""" + feed_entry = MagicMock() + feed_entry.external_id = external_id + feed_entry.title = title + feed_entry.distance_to_home = distance_to_home + feed_entry.coordinates = coordinates + feed_entry.category = category + feed_entry.location = location + feed_entry.attribution = attribution + feed_entry.publication_date = publication_date + feed_entry.council_area = council_area + feed_entry.status = status + feed_entry.type = entry_type + feed_entry.fire = fire + feed_entry.size = size + feed_entry.responsible_agency = responsible_agency + return feed_entry + + @mock.patch('geojson_client.nsw_rural_fire_service_feed.' + 'NswRuralFireServiceFeed') + def test_setup(self, mock_feed): + """Test the general setup of the platform.""" + # Set up some mock feed entries for this test. + mock_entry_1 = self._generate_mock_feed_entry( + '1234', 'Title 1', 15.5, (-31.0, 150.0), category='Category 1', + location='Location 1', attribution='Attribution 1', + publication_date=datetime.datetime(2018, 9, 22, 8, 0, + tzinfo=datetime.timezone.utc), + council_area='Council Area 1', status='Status 1', + entry_type='Type 1', size='Size 1', responsible_agency='Agency 1') + mock_entry_2 = self._generate_mock_feed_entry('2345', 'Title 2', 20.5, + (-31.1, 150.1), + fire=False) + mock_entry_3 = self._generate_mock_feed_entry('3456', 'Title 3', 25.5, + (-31.2, 150.2)) + mock_entry_4 = self._generate_mock_feed_entry('4567', 'Title 4', 12.5, + (-31.3, 150.3)) + mock_feed.return_value.update.return_value = 'OK', [mock_entry_1, + mock_entry_2, + mock_entry_3] + + utcnow = dt_util.utcnow() + # Patching 'utcnow' to gain more control over the timed update. + with patch('homeassistant.util.dt.utcnow', return_value=utcnow): + with assert_setup_component(1, geo_location.DOMAIN): + self.assertTrue(setup_component(self.hass, geo_location.DOMAIN, + CONFIG)) + # Artificially trigger update. + self.hass.bus.fire(EVENT_HOMEASSISTANT_START) + # Collect events. + self.hass.block_till_done() + + all_states = self.hass.states.all() + assert len(all_states) == 3 + + state = self.hass.states.get("geo_location.title_1") + self.assertIsNotNone(state) + assert state.name == "Title 1" + assert state.attributes == { + ATTR_EXTERNAL_ID: "1234", ATTR_LATITUDE: -31.0, + ATTR_LONGITUDE: 150.0, ATTR_FRIENDLY_NAME: "Title 1", + ATTR_CATEGORY: "Category 1", ATTR_LOCATION: "Location 1", + ATTR_ATTRIBUTION: "Attribution 1", + ATTR_PUBLICATION_DATE: + datetime.datetime(2018, 9, 22, 8, 0, + tzinfo=datetime.timezone.utc), + ATTR_FIRE: True, + ATTR_COUNCIL_AREA: 'Council Area 1', + ATTR_STATUS: 'Status 1', ATTR_TYPE: 'Type 1', + ATTR_SIZE: 'Size 1', ATTR_RESPONSIBLE_AGENCY: 'Agency 1', + ATTR_UNIT_OF_MEASUREMENT: "km", + ATTR_SOURCE: 'nsw_rural_fire_service_feed'} + self.assertAlmostEqual(float(state.state), 15.5) + + state = self.hass.states.get("geo_location.title_2") + self.assertIsNotNone(state) + assert state.name == "Title 2" + assert state.attributes == { + ATTR_EXTERNAL_ID: "2345", ATTR_LATITUDE: -31.1, + ATTR_LONGITUDE: 150.1, ATTR_FRIENDLY_NAME: "Title 2", + ATTR_FIRE: False, + ATTR_UNIT_OF_MEASUREMENT: "km", + ATTR_SOURCE: 'nsw_rural_fire_service_feed'} + self.assertAlmostEqual(float(state.state), 20.5) + + state = self.hass.states.get("geo_location.title_3") + self.assertIsNotNone(state) + assert state.name == "Title 3" + assert state.attributes == { + ATTR_EXTERNAL_ID: "3456", ATTR_LATITUDE: -31.2, + ATTR_LONGITUDE: 150.2, ATTR_FRIENDLY_NAME: "Title 3", + ATTR_FIRE: True, + ATTR_UNIT_OF_MEASUREMENT: "km", + ATTR_SOURCE: 'nsw_rural_fire_service_feed'} + self.assertAlmostEqual(float(state.state), 25.5) + + # Simulate an update - one existing, one new entry, + # one outdated entry + mock_feed.return_value.update.return_value = 'OK', [ + mock_entry_1, mock_entry_4, mock_entry_3] + fire_time_changed(self.hass, utcnow + SCAN_INTERVAL) + self.hass.block_till_done() + + all_states = self.hass.states.all() + assert len(all_states) == 3 + + # Simulate an update - empty data, but successful update, + # so no changes to entities. + mock_feed.return_value.update.return_value = 'OK_NO_DATA', None + # mock_restdata.return_value.data = None + fire_time_changed(self.hass, utcnow + + 2 * SCAN_INTERVAL) + self.hass.block_till_done() + + all_states = self.hass.states.all() + assert len(all_states) == 3 + + # Simulate an update - empty data, removes all entities + mock_feed.return_value.update.return_value = 'ERROR', None + fire_time_changed(self.hass, utcnow + + 2 * SCAN_INTERVAL) + self.hass.block_till_done() + + all_states = self.hass.states.all() + assert len(all_states) == 0 From 5ac0469ef99800da76016c7ab2fab581c9c9eab2 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 14 Oct 2018 17:08:17 +0200 Subject: [PATCH 141/265] Upgrade python-telegram-bot to 11.1.0 (#17441) --- homeassistant/components/telegram_bot/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 40724a1ee86..ddd8337aeda 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -21,7 +21,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import TemplateError from homeassistant.setup import async_prepare_setup_platform -REQUIREMENTS = ['python-telegram-bot==11.0.0'] +REQUIREMENTS = ['python-telegram-bot==11.1.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 63f1a451049..f7d46d633dd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1186,7 +1186,7 @@ python-synology==0.2.0 python-tado==0.2.3 # homeassistant.components.telegram_bot -python-telegram-bot==11.0.0 +python-telegram-bot==11.1.0 # homeassistant.components.sensor.twitch python-twitch-client==0.6.0 From daf48a3b1fdfe382c2435b91663278ad60d06079 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 14 Oct 2018 17:10:46 +0200 Subject: [PATCH 142/265] Minor updates (#17437) --- .../components/geo_location/__init__.py | 13 ++-- homeassistant/components/geo_location/demo.py | 8 +-- .../geo_location/geo_json_events.py | 37 +++++----- .../nsw_rural_fire_service_feed.py | 70 +++++++++---------- 4 files changed, 63 insertions(+), 65 deletions(-) diff --git a/homeassistant/components/geo_location/__init__.py b/homeassistant/components/geo_location/__init__.py index 54aebc3591b..495c9e1744b 100644 --- a/homeassistant/components/geo_location/__init__.py +++ b/homeassistant/components/geo_location/__init__.py @@ -1,33 +1,34 @@ """ Geo Location component. -This component covers platforms that deal with external events that contain -a geo location related to the installed HA instance. - For more details about this component, please refer to the documentation at https://home-assistant.io/components/geo_location/ """ -import logging from datetime import timedelta +import logging from typing import Optional from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa _LOGGER = logging.getLogger(__name__) ATTR_DISTANCE = 'distance' ATTR_SOURCE = 'source' + DOMAIN = 'geo_location' + ENTITY_ID_FORMAT = DOMAIN + '.{}' + GROUP_NAME_ALL_EVENTS = 'All Geo Location Events' + SCAN_INTERVAL = timedelta(seconds=60) async def async_setup(hass, config): - """Set up this component.""" + """Set up the Geo Location component.""" component = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_EVENTS) await component.async_setup(config) diff --git a/homeassistant/components/geo_location/demo.py b/homeassistant/components/geo_location/demo.py index 5e76e5cdf9a..6c5cc2fe147 100644 --- a/homeassistant/components/geo_location/demo.py +++ b/homeassistant/components/geo_location/demo.py @@ -4,10 +4,10 @@ Demo platform for the geo location component. For more details about this platform, please refer to the documentation https://home-assistant.io/components/demo/ """ -import logging -import random from datetime import timedelta -from math import pi, cos, sin, radians +import logging +from math import cos, pi, radians, sin +import random from typing import Optional from homeassistant.components.geo_location import GeoLocationEvent @@ -16,7 +16,7 @@ from homeassistant.helpers.event import track_time_interval _LOGGER = logging.getLogger(__name__) AVG_KM_PER_DEGREE = 111.0 -DEFAULT_UNIT_OF_MEASUREMENT = "km" +DEFAULT_UNIT_OF_MEASUREMENT = 'km' DEFAULT_UPDATE_INTERVAL = timedelta(minutes=1) MAX_RADIUS_IN_KM = 50 NUMBER_OF_DEMO_DEVICES = 5 diff --git a/homeassistant/components/geo_location/geo_json_events.py b/homeassistant/components/geo_location/geo_json_events.py index fc40dddb9b8..4665f8f2361 100644 --- a/homeassistant/components/geo_location/geo_json_events.py +++ b/homeassistant/components/geo_location/geo_json_events.py @@ -1,24 +1,20 @@ """ Generic GeoJSON events platform. -Retrieves current events (typically incidents or alerts) in GeoJSON format, and -displays information on events filtered by distance to the HA instance's -location. - For more details about this platform, please refer to the documentation at https://home-assistant.io/components/geo_location/geo_json_events/ """ -import logging from datetime import timedelta +import logging from typing import Optional import voluptuous as vol +from homeassistant.components.geo_location import ( + PLATFORM_SCHEMA, GeoLocationEvent) +from homeassistant.const import ( + CONF_RADIUS, CONF_SCAN_INTERVAL, CONF_URL, EVENT_HOMEASSISTANT_START) import homeassistant.helpers.config_validation as cv -from homeassistant.components.geo_location import GeoLocationEvent -from homeassistant.const import CONF_RADIUS, CONF_URL, CONF_SCAN_INTERVAL, \ - EVENT_HOMEASSISTANT_START -from homeassistant.components.geo_location import PLATFORM_SCHEMA from homeassistant.helpers.event import track_time_interval REQUIREMENTS = ['geojson_client==0.1'] @@ -28,15 +24,15 @@ _LOGGER = logging.getLogger(__name__) ATTR_EXTERNAL_ID = 'external_id' DEFAULT_RADIUS_IN_KM = 20.0 -DEFAULT_UNIT_OF_MEASUREMENT = "km" +DEFAULT_UNIT_OF_MEASUREMENT = 'km' SCAN_INTERVAL = timedelta(minutes=5) + SOURCE = 'geo_json_events' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_URL): cv.string, - vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS_IN_KM): - vol.Coerce(float), + vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS_IN_KM): vol.Coerce(float), }) @@ -56,8 +52,9 @@ class GeoJsonFeedManager: """Initialize the GeoJSON Feed Manager.""" from geojson_client.generic_feed import GenericFeed self._hass = hass - self._feed = GenericFeed((hass.config.latitude, hass.config.longitude), - filter_radius=radius_in_km, url=url) + self._feed = GenericFeed( + (hass.config.latitude, hass.config.longitude), + filter_radius=radius_in_km, url=url) self._add_entities = add_entities self._scan_interval = scan_interval self._feed_entries = [] @@ -68,8 +65,8 @@ class GeoJsonFeedManager: def _init_regular_updates(self): """Schedule regular updates at the specified interval.""" - track_time_interval(self._hass, lambda now: self._update(), - self._scan_interval) + track_time_interval( + self._hass, lambda now: self._update(), self._scan_interval) def _update(self): """Update the feed and then update connected entities.""" @@ -82,11 +79,11 @@ class GeoJsonFeedManager: keep_entries = self._update_or_remove_entities(feed_entries) self._generate_new_entities(keep_entries) elif status == geojson_client.UPDATE_OK_NO_DATA: - _LOGGER.debug("Update successful, but no data received from %s", - self._feed) + _LOGGER.debug( + "Update successful, but no data received from %s", self._feed) else: - _LOGGER.warning("Update not successful, no data received from %s", - self._feed) + _LOGGER.warning( + "Update not successful, no data received from %s", self._feed) # Remove all entities. self._update_or_remove_entities([]) diff --git a/homeassistant/components/geo_location/nsw_rural_fire_service_feed.py b/homeassistant/components/geo_location/nsw_rural_fire_service_feed.py index 52f261ba858..d3b13abe704 100644 --- a/homeassistant/components/geo_location/nsw_rural_fire_service_feed.py +++ b/homeassistant/components/geo_location/nsw_rural_fire_service_feed.py @@ -1,27 +1,24 @@ """ NSW Rural Fire Service Feed platform. -Retrieves current events (bush fires, grass fires, etc.) in GeoJSON format, -and displays information on events filtered by distance and category to the -HA instance's location. - For more details about this platform, please refer to the documentation at https://home-assistant.io/components/geo_location/nsw_rural_fire_service_feed/ """ -import logging from datetime import timedelta +import logging from typing import Optional import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.geo_location import GeoLocationEvent, \ - PLATFORM_SCHEMA -from homeassistant.const import CONF_RADIUS, CONF_SCAN_INTERVAL, \ - EVENT_HOMEASSISTANT_START, ATTR_ATTRIBUTION +from homeassistant.components.geo_location import ( + PLATFORM_SCHEMA, GeoLocationEvent) +from homeassistant.const import ( + ATTR_ATTRIBUTION, ATTR_LOCATION, CONF_RADIUS, CONF_SCAN_INTERVAL, + EVENT_HOMEASSISTANT_START) from homeassistant.core import callback -from homeassistant.helpers.dispatcher import dispatcher_send, \ - async_dispatcher_connect +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, dispatcher_send) from homeassistant.helpers.event import track_time_interval REQUIREMENTS = ['geojson_client==0.1'] @@ -32,7 +29,6 @@ ATTR_CATEGORY = 'category' ATTR_COUNCIL_AREA = 'council_area' ATTR_EXTERNAL_ID = 'external_id' ATTR_FIRE = 'fire' -ATTR_LOCATION = 'location' ATTR_PUBLICATION_DATE = 'publication_date' ATTR_RESPONSIBLE_AGENCY = 'responsible_agency' ATTR_SIZE = 'size' @@ -42,7 +38,7 @@ ATTR_TYPE = 'type' CONF_CATEGORIES = 'categories' DEFAULT_RADIUS_IN_KM = 20.0 -DEFAULT_UNIT_OF_MEASUREMENT = "km" +DEFAULT_UNIT_OF_MEASUREMENT = 'km' SCAN_INTERVAL = timedelta(minutes=5) @@ -51,14 +47,17 @@ SIGNAL_UPDATE_ENTITY = 'nsw_rural_fire_service_feed_update_{}' SOURCE = 'nsw_rural_fire_service_feed' -VALID_CATEGORIES = ['Emergency Warning', 'Watch and Act', 'Advice', - 'Not Applicable'] +VALID_CATEGORIES = [ + 'Advice', + 'Emergency Warning', + 'Not Applicable', + 'Watch and Act', +] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS_IN_KM): - vol.Coerce(float), vol.Optional(CONF_CATEGORIES, default=[]): - vol.All(cv.ensure_list, [vol.In(VALID_CATEGORIES)]) + vol.All(cv.ensure_list, [vol.In(VALID_CATEGORIES)]), + vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS_IN_KM): vol.Coerce(float), }) @@ -68,8 +67,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): radius_in_km = config[CONF_RADIUS] categories = config.get(CONF_CATEGORIES) # Initialize the entity manager. - feed = NswRuralFireServiceFeedManager(hass, add_entities, scan_interval, - radius_in_km, categories) + feed = NswRuralFireServiceFeedManager( + hass, add_entities, scan_interval, radius_in_km, categories) def start_feed_manager(event): """Start feed manager.""" @@ -86,11 +85,11 @@ class NswRuralFireServiceFeedManager: """Initialize the GeoJSON Feed Manager.""" from geojson_client.nsw_rural_fire_service_feed \ import NswRuralFireServiceFeed + self._hass = hass - self._feed = NswRuralFireServiceFeed((hass.config.latitude, - hass.config.longitude), - filter_radius=radius_in_km, - filter_categories=categories) + self._feed = NswRuralFireServiceFeed( + (hass.config.latitude, hass.config.longitude), + filter_radius=radius_in_km, filter_categories=categories) self._add_entities = add_entities self._scan_interval = scan_interval self.feed_entries = {} @@ -103,12 +102,13 @@ class NswRuralFireServiceFeedManager: def _init_regular_updates(self): """Schedule regular updates at the specified interval.""" - track_time_interval(self._hass, lambda now: self._update(), - self._scan_interval) + track_time_interval( + self._hass, lambda now: self._update(), self._scan_interval) def _update(self): """Update the feed and then update connected entities.""" import geojson_client + status, feed_entries = self._feed.update() if status == geojson_client.UPDATE_OK: _LOGGER.debug("Data retrieved %s", feed_entries) @@ -127,11 +127,11 @@ class NswRuralFireServiceFeedManager: self._managed_external_ids) self._generate_new_entities(create_external_ids) elif status == geojson_client.UPDATE_OK_NO_DATA: - _LOGGER.debug("Update successful, but no data received from %s", - self._feed) + _LOGGER.debug( + "Update successful, but no data received from %s", self._feed) else: - _LOGGER.warning("Update not successful, no data received from %s", - self._feed) + _LOGGER.warning( + "Update not successful, no data received from %s", self._feed) # Remove all entities. self._remove_entities(self._managed_external_ids.copy()) @@ -150,16 +150,16 @@ class NswRuralFireServiceFeedManager: """Update entities.""" for external_id in external_ids: _LOGGER.debug("Existing entity found %s", external_id) - dispatcher_send(self._hass, - SIGNAL_UPDATE_ENTITY.format(external_id)) + dispatcher_send( + self._hass, SIGNAL_UPDATE_ENTITY.format(external_id)) def _remove_entities(self, external_ids): """Remove entities.""" for external_id in external_ids: _LOGGER.debug("Entity not current anymore %s", external_id) self._managed_external_ids.remove(external_id) - dispatcher_send(self._hass, - SIGNAL_DELETE_ENTITY.format(external_id)) + dispatcher_send( + self._hass, SIGNAL_DELETE_ENTITY.format(external_id)) class NswRuralFireServiceLocationEvent(GeoLocationEvent): From 253e154a79c5efda82fc202783f639eb227265e9 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 14 Oct 2018 17:11:39 +0200 Subject: [PATCH 143/265] Minor updates (#17436) --- homeassistant/components/habitica/__init__.py | 92 +++++++++---------- 1 file changed, 43 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index 44b9e392157..8e77c6bf50b 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -1,55 +1,54 @@ """ The Habitica API component. -For more details about this platform, please refer to the documentation at +For more details about this component, please refer to the documentation at https://home-assistant.io/components/habitica/ """ - -import logging from collections import namedtuple +import logging import voluptuous as vol -from homeassistant.const import \ - CONF_NAME, CONF_URL, CONF_SENSORS, CONF_PATH, CONF_API_KEY + +from homeassistant.const import ( + CONF_API_KEY, CONF_NAME, CONF_PATH, CONF_SENSORS, CONF_URL) +from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers import \ - config_validation as cv, discovery REQUIREMENTS = ['habitipy==0.2.0'] -_LOGGER = logging.getLogger(__name__) -DOMAIN = "habitica" -CONF_API_USER = "api_user" +_LOGGER = logging.getLogger(__name__) + +CONF_API_USER = 'api_user' + +DEFAULT_URL = 'https://habitica.com' +DOMAIN = 'habitica' ST = SensorType = namedtuple('SensorType', [ - "name", "icon", "unit", "path" + 'name', 'icon', 'unit', 'path' ]) SENSORS_TYPES = { - 'name': ST('Name', None, '', ["profile", "name"]), - 'hp': ST('HP', 'mdi:heart', 'HP', ["stats", "hp"]), - 'maxHealth': ST('max HP', 'mdi:heart', 'HP', ["stats", "maxHealth"]), - 'mp': ST('Mana', 'mdi:auto-fix', 'MP', ["stats", "mp"]), - 'maxMP': ST('max Mana', 'mdi:auto-fix', 'MP', ["stats", "maxMP"]), - 'exp': ST('EXP', 'mdi:star', 'EXP', ["stats", "exp"]), + 'name': ST('Name', None, '', ['profile', 'name']), + 'hp': ST('HP', 'mdi:heart', 'HP', ['stats', 'hp']), + 'maxHealth': ST('max HP', 'mdi:heart', 'HP', ['stats', 'maxHealth']), + 'mp': ST('Mana', 'mdi:auto-fix', 'MP', ['stats', 'mp']), + 'maxMP': ST('max Mana', 'mdi:auto-fix', 'MP', ['stats', 'maxMP']), + 'exp': ST('EXP', 'mdi:star', 'EXP', ['stats', 'exp']), 'toNextLevel': ST( - 'Next Lvl', 'mdi:star', 'EXP', ["stats", "toNextLevel"]), + 'Next Lvl', 'mdi:star', 'EXP', ['stats', 'toNextLevel']), 'lvl': ST( - 'Lvl', 'mdi:arrow-up-bold-circle-outline', 'Lvl', ["stats", "lvl"]), - 'gp': ST('Gold', 'mdi:coin', 'Gold', ["stats", "gp"]), - 'class': ST('Class', 'mdi:sword', '', ["stats", "class"]) + 'Lvl', 'mdi:arrow-up-bold-circle-outline', 'Lvl', ['stats', 'lvl']), + 'gp': ST('Gold', 'mdi:coin', 'Gold', ['stats', 'gp']), + 'class': ST('Class', 'mdi:sword', '', ['stats', 'class']) } INSTANCE_SCHEMA = vol.Schema({ - vol.Optional(CONF_URL, default='https://habitica.com'): cv.url, + vol.Optional(CONF_URL, default=DEFAULT_URL): cv.url, vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_API_USER): cv.string, vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_SENSORS, default=list(SENSORS_TYPES)): - vol.All( - cv.ensure_list, - vol.Unique(), - [vol.In(list(SENSORS_TYPES))]) + vol.All(cv.ensure_list, vol.Unique(), [vol.In(list(SENSORS_TYPES))]), }) has_unique_values = vol.Schema(vol.Unique()) # pylint: disable=invalid-name @@ -57,7 +56,7 @@ has_unique_values = vol.Schema(vol.Unique()) # pylint: disable=invalid-name def has_all_unique_users(value): - """Validate that all `api_user`s are unique.""" + """Validate that all API users are unique.""" api_users = [user[CONF_API_USER] for user in value] has_unique_values(api_users) return value @@ -75,9 +74,7 @@ def has_all_unique_users_names(value): INSTANCE_LIST_SCHEMA = vol.All( - cv.ensure_list, - has_all_unique_users, - has_all_unique_users_names, + cv.ensure_list, has_all_unique_users, has_all_unique_users_names, [INSTANCE_SCHEMA]) CONFIG_SCHEMA = vol.Schema({ @@ -87,23 +84,24 @@ CONFIG_SCHEMA = vol.Schema({ SERVICE_API_CALL = 'api_call' ATTR_NAME = CONF_NAME ATTR_PATH = CONF_PATH -ATTR_ARGS = "args" -EVENT_API_CALL_SUCCESS = "{0}_{1}_{2}".format( - DOMAIN, SERVICE_API_CALL, "success") +ATTR_ARGS = 'args' +EVENT_API_CALL_SUCCESS = '{0}_{1}_{2}'.format( + DOMAIN, SERVICE_API_CALL, 'success') SERVICE_API_CALL_SCHEMA = vol.Schema({ vol.Required(ATTR_NAME): str, vol.Required(ATTR_PATH): vol.All(cv.ensure_list, [str]), - vol.Optional(ATTR_ARGS): dict + vol.Optional(ATTR_ARGS): dict, }) async def async_setup(hass, config): - """Set up the habitica service.""" + """Set up the Habitica service.""" + from habitipy.aio import HabitipyAsync + conf = config[DOMAIN] data = hass.data[DOMAIN] = {} websession = async_get_clientsession(hass) - from habitipy.aio import HabitipyAsync class HAHabitipyAsync(HabitipyAsync): """Closure API class to hold session.""" @@ -116,7 +114,7 @@ async def async_setup(hass, config): username = instance[CONF_API_USER] password = instance[CONF_API_KEY] name = instance.get(CONF_NAME) - config_dict = {"url": url, "login": username, "password": password} + config_dict = {'url': url, 'login': username, 'password': password} api = HAHabitipyAsync(config_dict) user = await api.user.get() if name is None: @@ -125,34 +123,30 @@ async def async_setup(hass, config): if CONF_SENSORS in instance: hass.async_create_task( discovery.async_load_platform( - hass, "sensor", DOMAIN, - {"name": name, "sensors": instance[CONF_SENSORS]}, - config)) + hass, 'sensor', DOMAIN, + {'name': name, 'sensors': instance[CONF_SENSORS]}, config)) async def handle_api_call(call): name = call.data[ATTR_NAME] path = call.data[ATTR_PATH] api = hass.data[DOMAIN].get(name) if api is None: - _LOGGER.error( - "API_CALL: User '%s' not configured", name) + _LOGGER.error("API_CALL: User '%s' not configured", name) return try: for element in path: api = api[element] except KeyError: _LOGGER.error( - "API_CALL: Path %s is invalid" - " for api on '{%s}' element", path, element) + "API_CALL: Path %s is invalid for API on '{%s}' element", + path, element) return kwargs = call.data.get(ATTR_ARGS, {}) data = await api(**kwargs) - hass.bus.async_fire(EVENT_API_CALL_SUCCESS, { - "name": name, "path": path, "data": data - }) + hass.bus.async_fire( + EVENT_API_CALL_SUCCESS, {'name': name, 'path': path, 'data': data}) hass.services.async_register( - DOMAIN, SERVICE_API_CALL, - handle_api_call, + DOMAIN, SERVICE_API_CALL, handle_api_call, schema=SERVICE_API_CALL_SCHEMA) return True From 7b0c88c54b3e5f12cbf0062a73d363120e0da517 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 14 Oct 2018 17:12:34 +0200 Subject: [PATCH 144/265] Update docstrings (#17435) --- homeassistant/components/sensor/api_streams.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/api_streams.py b/homeassistant/components/sensor/api_streams.py index ac9b15754d0..1fbef2c5896 100644 --- a/homeassistant/components/sensor/api_streams.py +++ b/homeassistant/components/sensor/api_streams.py @@ -2,7 +2,7 @@ Entity to track connections to stream API. For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.api_stream/ +https://home-assistant.io/components/sensor.api_streams/ """ import logging @@ -47,9 +47,9 @@ class StreamHandler(logging.Handler): self.entity.schedule_update_ha_state() -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up the API stream platform.""" +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Set up the API streams platform.""" entity = APICount() handler = StreamHandler(entity) @@ -68,7 +68,7 @@ async def async_setup_platform(hass, config, async_add_entities, class APICount(Entity): - """Entity to represent how many people are connected to stream API.""" + """Entity to represent how many people are connected to the stream API.""" def __init__(self): """Initialize the API count.""" From 9305ea9a6b530b52e2bd3641327d4a558a354c9c Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 14 Oct 2018 17:13:25 +0200 Subject: [PATCH 145/265] Upgrade numpy to 1.15.2 (#17431) --- homeassistant/components/binary_sensor/trend.py | 2 +- homeassistant/components/image_processing/opencv.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/binary_sensor/trend.py b/homeassistant/components/binary_sensor/trend.py index 0b168e45b4d..d332c668703 100644 --- a/homeassistant/components/binary_sensor/trend.py +++ b/homeassistant/components/binary_sensor/trend.py @@ -22,7 +22,7 @@ from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.event import async_track_state_change from homeassistant.util import utcnow -REQUIREMENTS = ['numpy==1.15.1'] +REQUIREMENTS = ['numpy==1.15.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/image_processing/opencv.py b/homeassistant/components/image_processing/opencv.py index 1e5d38b638e..062c18bb730 100644 --- a/homeassistant/components/image_processing/opencv.py +++ b/homeassistant/components/image_processing/opencv.py @@ -16,7 +16,7 @@ from homeassistant.components.image_processing import ( from homeassistant.core import split_entity_id import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['numpy==1.15.1'] +REQUIREMENTS = ['numpy==1.15.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index f7d46d633dd..5dbeda711a4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -650,7 +650,7 @@ nuheat==0.3.0 # homeassistant.components.binary_sensor.trend # homeassistant.components.image_processing.opencv -numpy==1.15.1 +numpy==1.15.2 # homeassistant.components.google oauth2client==4.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9ad85eb8de6..c9cfe401b90 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -115,7 +115,7 @@ mficlient==0.3.0 # homeassistant.components.binary_sensor.trend # homeassistant.components.image_processing.opencv -numpy==1.15.1 +numpy==1.15.2 # homeassistant.components.mqtt # homeassistant.components.shiftr From d4061b73b00c00e6f4072d09b6f75bcd353be668 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 14 Oct 2018 17:14:06 +0200 Subject: [PATCH 146/265] Upgrade youtube_dl to 2018.10.05 (#17429) --- homeassistant/components/media_extractor.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py index ffcb0f6ab95..401fd729f9c 100644 --- a/homeassistant/components/media_extractor.py +++ b/homeassistant/components/media_extractor.py @@ -14,7 +14,7 @@ from homeassistant.components.media_player import ( SERVICE_PLAY_MEDIA) from homeassistant.helpers import config_validation as cv -REQUIREMENTS = ['youtube_dl==2018.09.26'] +REQUIREMENTS = ['youtube_dl==2018.10.05'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 5dbeda711a4..1f53fda6199 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1572,7 +1572,7 @@ yeelight==0.4.3 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2018.09.26 +youtube_dl==2018.10.05 # homeassistant.components.light.zengge zengge==0.2 From ed683d8c2c82fcc6d0734c415971601fa76ab6aa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 14 Oct 2018 19:17:30 +0200 Subject: [PATCH 147/265] Update frontend to 20181014.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index c06f659573e..aa9406d9c62 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20181012.0'] +REQUIREMENTS = ['home-assistant-frontend==20181014.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 1f53fda6199..4a23bc4b6b6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -463,7 +463,7 @@ hole==0.3.0 holidays==0.9.7 # homeassistant.components.frontend -home-assistant-frontend==20181012.0 +home-assistant-frontend==20181014.0 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c9cfe401b90..9c5c0d3fa61 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -94,7 +94,7 @@ hdate==0.6.3 holidays==0.9.7 # homeassistant.components.frontend -home-assistant-frontend==20181012.0 +home-assistant-frontend==20181014.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From a401be9b1bbec7ed3bc07fe0b747c365ebbf83ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sun, 14 Oct 2018 20:22:20 +0200 Subject: [PATCH 148/265] New climate device (#17313) * initial version of millheater * Remove unused imports * Add some comments * separate lib * fix review comments --- .coveragerc | 1 + homeassistant/components/climate/mill.py | 153 +++++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 157 insertions(+) create mode 100644 homeassistant/components/climate/mill.py diff --git a/.coveragerc b/.coveragerc index fd40e588bf7..8a52a625341 100644 --- a/.coveragerc +++ b/.coveragerc @@ -441,6 +441,7 @@ omit = homeassistant/components/climate/homematic.py homeassistant/components/climate/honeywell.py homeassistant/components/climate/knx.py + homeassistant/components/climate/mill.py homeassistant/components/climate/oem.py homeassistant/components/climate/proliphix.py homeassistant/components/climate/radiotherm.py diff --git a/homeassistant/components/climate/mill.py b/homeassistant/components/climate/mill.py new file mode 100644 index 00000000000..5185cf115e2 --- /dev/null +++ b/homeassistant/components/climate/mill.py @@ -0,0 +1,153 @@ +""" +Support for mill wifi-enabled home heaters. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/climate.mill/ +""" + +import logging + +import voluptuous as vol +from homeassistant.components.climate import ( + ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_FAN_MODE, SUPPORT_ON_OFF) +from homeassistant.const import ( + ATTR_TEMPERATURE, CONF_PASSWORD, CONF_USERNAME, + STATE_ON, STATE_OFF, TEMP_CELSIUS) +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +REQUIREMENTS = ['millheater==0.1.1'] + +_LOGGER = logging.getLogger(__name__) + +MAX_TEMP = 35 +MIN_TEMP = 5 +SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | + SUPPORT_FAN_MODE | SUPPORT_ON_OFF) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, +}) + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the Mill heater.""" + from mill import Mill + mill_data_connection = Mill(config[CONF_USERNAME], + config[CONF_PASSWORD], + websession=async_get_clientsession(hass)) + if not await mill_data_connection.connect(): + _LOGGER.error("Failed to connect to Mill") + return + + await mill_data_connection.update_heaters() + + dev = [] + for heater in mill_data_connection.heaters.values(): + dev.append(MillHeater(heater, mill_data_connection)) + async_add_entities(dev) + + +class MillHeater(ClimateDevice): + """Representation of a Mill Thermostat device.""" + + def __init__(self, heater, mill_data_connection): + """Initialize the thermostat.""" + self._heater = heater + self._conn = mill_data_connection + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS + + @property + def available(self): + """Return True if entity is available.""" + return self._heater.device_status == 0 # weird api choice + + @property + def unique_id(self): + """Return a unique ID.""" + return self._heater.device_id + + @property + def name(self): + """Return the name of the entity.""" + return self._heater.name + + @property + def temperature_unit(self): + """Return the unit of measurement which this thermostat uses.""" + return TEMP_CELSIUS + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._heater.set_temp + + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + return 1 + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._heater.current_temp + + @property + def current_fan_mode(self): + """Return the fan setting.""" + return STATE_ON if self._heater.fan_status == 1 else STATE_OFF + + @property + def fan_list(self): + """List of available fan modes.""" + return [STATE_ON, STATE_OFF] + + @property + def is_on(self): + """Return true if heater is on.""" + return self._heater.power_status == 1 + + @property + def min_temp(self): + """Return the minimum temperature.""" + return MIN_TEMP + + @property + def max_temp(self): + """Return the maximum temperature.""" + return MAX_TEMP + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature is None: + return + await self._conn.set_heater_temp(self._heater.device_id, + int(temperature)) + + async def async_set_fan_mode(self, fan_mode): + """Set new target fan mode.""" + fan_status = 1 if fan_mode == STATE_ON else 0 + await self._conn.heater_control(self._heater.device_id, + fan_status=fan_status) + + async def async_turn_on(self): + """Turn Mill unit on.""" + await self._conn.heater_control(self._heater.device_id, + power_status=1) + + async def async_turn_off(self): + """Turn Mill unit off.""" + await self._conn.heater_control(self._heater.device_id, + power_status=0) + + async def async_update(self): + """Retrieve latest state.""" + self._heater = await self._conn.update_device(self._heater.device_id) diff --git a/requirements_all.txt b/requirements_all.txt index 4a23bc4b6b6..decc8f63f4a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -602,6 +602,9 @@ mficlient==0.3.0 # homeassistant.components.sensor.miflora miflora==0.4.0 +# homeassistant.components.climate.mill +millheater==0.1.1 + # homeassistant.components.sensor.mitemp_bt mitemp_bt==0.0.1 From 1187e0aea5a11f6eb2c1b8c447292dd50f445aac Mon Sep 17 00:00:00 2001 From: kennedyshead Date: Sun, 14 Oct 2018 21:33:18 +0200 Subject: [PATCH 149/265] Remove day as a conf option (#17452) --- homeassistant/components/sensor/speedtest.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/speedtest.py b/homeassistant/components/sensor/speedtest.py index ee6cad61e20..a08eec56e17 100644 --- a/homeassistant/components/sensor/speedtest.py +++ b/homeassistant/components/sensor/speedtest.py @@ -32,7 +32,6 @@ CONF_ATTRIBUTION = "Data retrieved from Speedtest by Ookla" CONF_SECOND = 'second' CONF_MINUTE = 'minute' CONF_HOUR = 'hour' -CONF_DAY = 'day' CONF_SERVER_ID = 'server_id' CONF_MANUAL = 'manual' @@ -47,8 +46,6 @@ SENSOR_TYPES = { PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_MONITORED_CONDITIONS): vol.All(cv.ensure_list, [vol.In(list(SENSOR_TYPES))]), - vol.Optional(CONF_DAY): - vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(1, 31))]), vol.Optional(CONF_HOUR): vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(0, 23))]), vol.Optional(CONF_MANUAL, default=False): cv.boolean, @@ -156,8 +153,7 @@ class SpeedtestData: if not config.get(CONF_MANUAL): track_time_change( hass, self.update, second=config.get(CONF_SECOND), - minute=config.get(CONF_MINUTE), hour=config.get(CONF_HOUR), - day=config.get(CONF_DAY)) + minute=config.get(CONF_MINUTE), hour=config.get(CONF_HOUR)) def update(self, now): """Get the latest data from speedtest.net.""" From b022dde622cb18853694314950ee35639a14225c Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 14 Oct 2018 14:26:44 -0600 Subject: [PATCH 150/265] Bumped simplisafe-python to 3.1.11 (#17454) * Bumped simplisafe-python to 3.1.10 * Updated requirements * Updated requirements --- homeassistant/components/simplisafe/__init__.py | 11 +++++------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 91ffb3dbde4..df7fe6beda7 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -23,7 +23,7 @@ from homeassistant.helpers import config_validation as cv from .config_flow import configured_instances from .const import DATA_CLIENT, DEFAULT_SCAN_INTERVAL, DOMAIN, TOPIC_UPDATE -REQUIREMENTS = ['simplisafe-python==3.1.7'] +REQUIREMENTS = ['simplisafe-python==3.1.11'] _LOGGER = logging.getLogger(__name__) @@ -87,18 +87,17 @@ async def async_setup(hass, config): async def async_setup_entry(hass, config_entry): """Set up SimpliSafe as config entry.""" from simplipy import API - from simplipy.errors import SimplipyError + from simplipy.errors import InvalidCredentialsError, SimplipyError websession = aiohttp_client.async_get_clientsession(hass) try: simplisafe = await API.login_via_token( config_entry.data[CONF_TOKEN], websession) + except InvalidCredentialsError: + _LOGGER.error('Invalid credentials provided') + return False except SimplipyError as err: - if 403 in str(err): - _LOGGER.error('Invalid credentials provided') - return False - _LOGGER.error('Config entry failed: %s', err) raise ConfigEntryNotReady diff --git a/requirements_all.txt b/requirements_all.txt index decc8f63f4a..7b4427e9618 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1346,7 +1346,7 @@ shodan==1.10.4 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==3.1.7 +simplisafe-python==3.1.11 # homeassistant.components.sisyphus sisyphus-control==2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9c5c0d3fa61..57d9d5d871f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -216,7 +216,7 @@ ring_doorbell==0.2.1 rxv==0.5.1 # homeassistant.components.simplisafe -simplisafe-python==3.1.7 +simplisafe-python==3.1.11 # homeassistant.components.sleepiq sleepyq==0.6 From 3edcc9420a345f80f3d2af9b0cdb431795cd4e78 Mon Sep 17 00:00:00 2001 From: "Craig J. Midwinter" Date: Sun, 14 Oct 2018 16:51:15 -0500 Subject: [PATCH 151/265] Update pysher version (#17455) --- homeassistant/components/goalfeed.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/goalfeed.py b/homeassistant/components/goalfeed.py index f360d4ffba9..1a571960bc7 100644 --- a/homeassistant/components/goalfeed.py +++ b/homeassistant/components/goalfeed.py @@ -12,7 +12,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -REQUIREMENTS = ['pysher==0.2.0'] +REQUIREMENTS = ['pysher==1.0.4'] DOMAIN = 'goalfeed' diff --git a/requirements_all.txt b/requirements_all.txt index 7b4427e9618..e0b6f561c53 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1072,7 +1072,7 @@ pyserial==3.1.1 pysesame==0.1.0 # homeassistant.components.goalfeed -pysher==0.2.0 +pysher==1.0.4 # homeassistant.components.sensor.sma pysma==0.2 From c5905ee5caa8464bd68fc12503afbfc89c4c6332 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Mon, 15 Oct 2018 00:55:11 +0300 Subject: [PATCH 152/265] Show torah reading during weekdays (#17447) * Add support for showing torah reading on weekdays as well * Update docstrings for test functions --- .../components/sensor/jewish_calendar.py | 7 +++++- .../components/sensor/test_jewish_calendar.py | 24 ++++++++++++++----- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sensor/jewish_calendar.py b/homeassistant/components/sensor/jewish_calendar.py index 1de45d6145e..ad024547d34 100644 --- a/homeassistant/components/sensor/jewish_calendar.py +++ b/homeassistant/components/sensor/jewish_calendar.py @@ -4,6 +4,7 @@ Platform to retrieve Jewish calendar information for Home Assistant. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.jewish_calendar/ """ +from datetime import timedelta import logging import voluptuous as vol @@ -107,16 +108,20 @@ class JewishCalSensor(Entity): import hdate today = dt_util.now().date() + upcoming_saturday = today + timedelta((12 - today.weekday()) % 7) date = hdate.HDate( today, diaspora=self.diaspora, hebrew=self._hebrew) + upcoming_shabbat = hdate.HDate( + upcoming_saturday, diaspora=self.diaspora, hebrew=self._hebrew) if self.type == 'date': self._state = hdate.date.get_hebrew_date( date.h_day, date.h_month, date.h_year, hebrew=self._hebrew) elif self.type == 'weekly_portion': self._state = hdate.date.get_parashe( - date.get_reading(self.diaspora), hebrew=self._hebrew) + upcoming_shabbat.get_reading(self.diaspora), + hebrew=self._hebrew) elif self.type == 'holiday_name': try: description = next( diff --git a/tests/components/sensor/test_jewish_calendar.py b/tests/components/sensor/test_jewish_calendar.py index b67e340a9aa..4357de26554 100644 --- a/tests/components/sensor/test_jewish_calendar.py +++ b/tests/components/sensor/test_jewish_calendar.py @@ -85,7 +85,7 @@ class TestJewishCalenderSensor(unittest.TestCase): self.assertEqual(sensor.state, "כ\"ג באלול ה\' תשע\"ח") def test_jewish_calendar_sensor_holiday_name(self): - """Test Jewish calendar sensor date output in hebrew.""" + """Test Jewish calendar sensor holiday name output in hebrew.""" test_time = dt(2018, 9, 10) sensor = JewishCalSensor( name='test', language='hebrew', sensor_type='holiday_name', @@ -97,7 +97,7 @@ class TestJewishCalenderSensor(unittest.TestCase): self.assertEqual(sensor.state, "א\' ראש השנה") def test_jewish_calendar_sensor_holiday_name_english(self): - """Test Jewish calendar sensor date output in hebrew.""" + """Test Jewish calendar sensor holiday name output in english.""" test_time = dt(2018, 9, 10) sensor = JewishCalSensor( name='test', language='english', sensor_type='holiday_name', @@ -109,7 +109,7 @@ class TestJewishCalenderSensor(unittest.TestCase): self.assertEqual(sensor.state, "Rosh Hashana I") def test_jewish_calendar_sensor_holyness(self): - """Test Jewish calendar sensor date output in hebrew.""" + """Test Jewish calendar sensor holyness value.""" test_time = dt(2018, 9, 10) sensor = JewishCalSensor( name='test', language='hebrew', sensor_type='holyness', @@ -121,7 +121,7 @@ class TestJewishCalenderSensor(unittest.TestCase): self.assertEqual(sensor.state, 1) def test_jewish_calendar_sensor_torah_reading(self): - """Test Jewish calendar sensor date output in hebrew.""" + """Test Jewish calendar sensor torah reading in hebrew.""" test_time = dt(2018, 9, 8) sensor = JewishCalSensor( name='test', language='hebrew', sensor_type='weekly_portion', @@ -133,7 +133,7 @@ class TestJewishCalenderSensor(unittest.TestCase): self.assertEqual(sensor.state, "פרשת נצבים") def test_jewish_calendar_sensor_first_stars_ny(self): - """Test Jewish calendar sensor date output in hebrew.""" + """Test Jewish calendar sensor first stars time in NY, US.""" test_time = dt(2018, 9, 8) sensor = JewishCalSensor( name='test', language='hebrew', sensor_type='first_stars', @@ -145,7 +145,7 @@ class TestJewishCalenderSensor(unittest.TestCase): self.assertEqual(sensor.state, time(19, 48)) def test_jewish_calendar_sensor_first_stars_jerusalem(self): - """Test Jewish calendar sensor date output in hebrew.""" + """Test Jewish calendar sensor first stars time in Jerusalem, IL.""" test_time = dt(2018, 9, 8) sensor = JewishCalSensor( name='test', language='hebrew', sensor_type='first_stars', @@ -155,3 +155,15 @@ class TestJewishCalenderSensor(unittest.TestCase): run_coroutine_threadsafe( sensor.async_update(), self.hass.loop).result() self.assertEqual(sensor.state, time(19, 21)) + + def test_jewish_calendar_sensor_torah_reading_weekday(self): + """Test the sensor showing torah reading also on weekdays.""" + test_time = dt(2018, 10, 14) + sensor = JewishCalSensor( + name='test', language='hebrew', sensor_type='weekly_portion', + latitude=self.TEST_LATITUDE, longitude=self.TEST_LONGITUDE, + timezone="Asia/Jerusalem", diaspora=False) + with patch('homeassistant.util.dt.now', return_value=test_time): + run_coroutine_threadsafe( + sensor.async_update(), self.hass.loop).result() + self.assertEqual(sensor.state, "פרשת לך לך") From a71cc67efb05cf343df048885b451581b8cf0bd0 Mon Sep 17 00:00:00 2001 From: Julius Mittenzwei Date: Mon, 15 Oct 2018 03:29:36 +0200 Subject: [PATCH 153/265] Fix NoEntitySpecifiedError during knx startup (#17366) * Potential fix for #13699 * removed uneccessary initialization of hass * removed hass from signature --- homeassistant/components/binary_sensor/knx.py | 12 +++++++----- homeassistant/components/climate/knx.py | 12 +++++++----- homeassistant/components/cover/knx.py | 13 +++++++------ homeassistant/components/light/knx.py | 12 +++++++----- homeassistant/components/sensor/knx.py | 12 +++++++----- homeassistant/components/switch/knx.py | 12 +++++++----- 6 files changed, 42 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/binary_sensor/knx.py b/homeassistant/components/binary_sensor/knx.py index d0707b0f067..a89d5d1c945 100644 --- a/homeassistant/components/binary_sensor/knx.py +++ b/homeassistant/components/binary_sensor/knx.py @@ -69,7 +69,7 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities): entities = [] for device_name in discovery_info[ATTR_DISCOVER_DEVICES]: device = hass.data[DATA_KNX].xknx.devices[device_name] - entities.append(KNXBinarySensor(hass, device)) + entities.append(KNXBinarySensor(device)) async_add_entities(entities) @@ -87,7 +87,7 @@ def async_add_entities_config(hass, config, async_add_entities): reset_after=config.get(CONF_RESET_AFTER)) hass.data[DATA_KNX].xknx.devices.add(binary_sensor) - entity = KNXBinarySensor(hass, binary_sensor) + entity = KNXBinarySensor(binary_sensor) automations = config.get(CONF_AUTOMATION) if automations is not None: for automation in automations: @@ -103,11 +103,9 @@ def async_add_entities_config(hass, config, async_add_entities): class KNXBinarySensor(BinarySensorDevice): """Representation of a KNX binary sensor.""" - def __init__(self, hass, device): + def __init__(self, device): """Initialize of KNX binary sensor.""" self.device = device - self.hass = hass - self.async_register_callbacks() self.automations = [] @callback @@ -118,6 +116,10 @@ class KNXBinarySensor(BinarySensorDevice): await self.async_update_ha_state() self.device.register_device_updated_cb(after_update_callback) + async def async_added_to_hass(self): + """Store register state change callback.""" + self.async_register_callbacks() + @property def name(self): """Return the name of the KNX device.""" diff --git a/homeassistant/components/climate/knx.py b/homeassistant/components/climate/knx.py index 4eada356653..b52bd4b418d 100644 --- a/homeassistant/components/climate/knx.py +++ b/homeassistant/components/climate/knx.py @@ -75,7 +75,7 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities): entities = [] for device_name in discovery_info[ATTR_DISCOVER_DEVICES]: device = hass.data[DATA_KNX].xknx.devices[device_name] - entities.append(KNXClimate(hass, device)) + entities.append(KNXClimate(device)) async_add_entities(entities) @@ -110,17 +110,15 @@ def async_add_entities_config(hass, config, async_add_entities): group_address_operation_mode_comfort=config.get( CONF_OPERATION_MODE_COMFORT_ADDRESS)) hass.data[DATA_KNX].xknx.devices.add(climate) - async_add_entities([KNXClimate(hass, climate)]) + async_add_entities([KNXClimate(climate)]) class KNXClimate(ClimateDevice): """Representation of a KNX climate device.""" - def __init__(self, hass, device): + def __init__(self, device): """Initialize of a KNX climate device.""" self.device = device - self.hass = hass - self.async_register_callbacks() @property def supported_features(self): @@ -137,6 +135,10 @@ class KNXClimate(ClimateDevice): await self.async_update_ha_state() self.device.register_device_updated_cb(after_update_callback) + async def async_added_to_hass(self): + """Store register state change callback.""" + self.async_register_callbacks() + @property def name(self): """Return the name of the KNX device.""" diff --git a/homeassistant/components/cover/knx.py b/homeassistant/components/cover/knx.py index 43a87fab367..4173db5f450 100644 --- a/homeassistant/components/cover/knx.py +++ b/homeassistant/components/cover/knx.py @@ -64,7 +64,7 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities): entities = [] for device_name in discovery_info[ATTR_DISCOVER_DEVICES]: device = hass.data[DATA_KNX].xknx.devices[device_name] - entities.append(KNXCover(hass, device)) + entities.append(KNXCover(device)) async_add_entities(entities) @@ -88,18 +88,15 @@ def async_add_entities_config(hass, config, async_add_entities): invert_angle=config.get(CONF_INVERT_ANGLE)) hass.data[DATA_KNX].xknx.devices.add(cover) - async_add_entities([KNXCover(hass, cover)]) + async_add_entities([KNXCover(cover)]) class KNXCover(CoverDevice): """Representation of a KNX cover.""" - def __init__(self, hass, device): + def __init__(self, device): """Initialize the cover.""" self.device = device - self.hass = hass - self.async_register_callbacks() - self._unsubscribe_auto_updater = None @callback @@ -110,6 +107,10 @@ class KNXCover(CoverDevice): await self.async_update_ha_state() self.device.register_device_updated_cb(after_update_callback) + async def async_added_to_hass(self): + """Store register state change callback.""" + self.async_register_callbacks() + @property def name(self): """Return the name of the KNX device.""" diff --git a/homeassistant/components/light/knx.py b/homeassistant/components/light/knx.py index 778d2fac59c..a1423cc6682 100644 --- a/homeassistant/components/light/knx.py +++ b/homeassistant/components/light/knx.py @@ -52,7 +52,7 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities): entities = [] for device_name in discovery_info[ATTR_DISCOVER_DEVICES]: device = hass.data[DATA_KNX].xknx.devices[device_name] - entities.append(KNXLight(hass, device)) + entities.append(KNXLight(device)) async_add_entities(entities) @@ -71,17 +71,15 @@ def async_add_entities_config(hass, config, async_add_entities): group_address_color=config.get(CONF_COLOR_ADDRESS), group_address_color_state=config.get(CONF_COLOR_STATE_ADDRESS)) hass.data[DATA_KNX].xknx.devices.add(light) - async_add_entities([KNXLight(hass, light)]) + async_add_entities([KNXLight(light)]) class KNXLight(Light): """Representation of a KNX light.""" - def __init__(self, hass, device): + def __init__(self, device): """Initialize of KNX light.""" self.device = device - self.hass = hass - self.async_register_callbacks() @callback def async_register_callbacks(self): @@ -91,6 +89,10 @@ class KNXLight(Light): await self.async_update_ha_state() self.device.register_device_updated_cb(after_update_callback) + async def async_added_to_hass(self): + """Store register state change callback.""" + self.async_register_callbacks() + @property def name(self): """Return the name of the KNX device.""" diff --git a/homeassistant/components/sensor/knx.py b/homeassistant/components/sensor/knx.py index ec506189c12..c096e15192d 100644 --- a/homeassistant/components/sensor/knx.py +++ b/homeassistant/components/sensor/knx.py @@ -42,7 +42,7 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities): entities = [] for device_name in discovery_info[ATTR_DISCOVER_DEVICES]: device = hass.data[DATA_KNX].xknx.devices[device_name] - entities.append(KNXSensor(hass, device)) + entities.append(KNXSensor(device)) async_add_entities(entities) @@ -56,17 +56,15 @@ def async_add_entities_config(hass, config, async_add_entities): group_address=config.get(CONF_ADDRESS), value_type=config.get(CONF_TYPE)) hass.data[DATA_KNX].xknx.devices.add(sensor) - async_add_entities([KNXSensor(hass, sensor)]) + async_add_entities([KNXSensor(sensor)]) class KNXSensor(Entity): """Representation of a KNX sensor.""" - def __init__(self, hass, device): + def __init__(self, device): """Initialize of a KNX sensor.""" self.device = device - self.hass = hass - self.async_register_callbacks() @callback def async_register_callbacks(self): @@ -76,6 +74,10 @@ class KNXSensor(Entity): await self.async_update_ha_state() self.device.register_device_updated_cb(after_update_callback) + async def async_added_to_hass(self): + """Store register state change callback.""" + self.async_register_callbacks() + @property def name(self): """Return the name of the KNX device.""" diff --git a/homeassistant/components/switch/knx.py b/homeassistant/components/switch/knx.py index 678a8d4775f..26b9f77028d 100644 --- a/homeassistant/components/switch/knx.py +++ b/homeassistant/components/switch/knx.py @@ -41,7 +41,7 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities): entities = [] for device_name in discovery_info[ATTR_DISCOVER_DEVICES]: device = hass.data[DATA_KNX].xknx.devices[device_name] - entities.append(KNXSwitch(hass, device)) + entities.append(KNXSwitch(device)) async_add_entities(entities) @@ -55,17 +55,15 @@ def async_add_entities_config(hass, config, async_add_entities): group_address=config.get(CONF_ADDRESS), group_address_state=config.get(CONF_STATE_ADDRESS)) hass.data[DATA_KNX].xknx.devices.add(switch) - async_add_entities([KNXSwitch(hass, switch)]) + async_add_entities([KNXSwitch(switch)]) class KNXSwitch(SwitchDevice): """Representation of a KNX switch.""" - def __init__(self, hass, device): + def __init__(self, device): """Initialize of KNX switch.""" self.device = device - self.hass = hass - self.async_register_callbacks() @callback def async_register_callbacks(self): @@ -75,6 +73,10 @@ class KNXSwitch(SwitchDevice): await self.async_update_ha_state() self.device.register_device_updated_cb(after_update_callback) + async def async_added_to_hass(self): + """Store register state change callback.""" + self.async_register_callbacks() + @property def name(self): """Return the name of the KNX device.""" From 3b0db291ddb4f5019f14af6548c6b381e4bb796d Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 15 Oct 2018 10:31:49 +0200 Subject: [PATCH 154/265] Bugfix eventstream with EOF on end (#17465) --- homeassistant/components/api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index 0fbb4de39f1..cbe404537eb 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -141,6 +141,8 @@ class APIEventStream(HomeAssistantView): _LOGGER.debug("STREAM %s RESPONSE CLOSED", id(stop_obj)) unsub_stream() + return response + class APIConfigView(HomeAssistantView): """View to handle Configuration requests.""" From 879924fea44114d6275bf544bb72d590a5ef6658 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Mon, 15 Oct 2018 20:17:46 +1100 Subject: [PATCH 155/265] refactored to make its code structure similar to nsw_rural_fire_service_feed platform (#17461) --- .../geo_location/geo_json_events.py | 139 +++++++++++------- 1 file changed, 85 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/geo_location/geo_json_events.py b/homeassistant/components/geo_location/geo_json_events.py index 4665f8f2361..7c3f228a4c9 100644 --- a/homeassistant/components/geo_location/geo_json_events.py +++ b/homeassistant/components/geo_location/geo_json_events.py @@ -14,7 +14,10 @@ from homeassistant.components.geo_location import ( PLATFORM_SCHEMA, GeoLocationEvent) from homeassistant.const import ( CONF_RADIUS, CONF_SCAN_INTERVAL, CONF_URL, EVENT_HOMEASSISTANT_START) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, dispatcher_send) from homeassistant.helpers.event import track_time_interval REQUIREMENTS = ['geojson_client==0.1'] @@ -28,6 +31,9 @@ DEFAULT_UNIT_OF_MEASUREMENT = 'km' SCAN_INTERVAL = timedelta(minutes=5) +SIGNAL_DELETE_ENTITY = 'geo_json_events_delete_{}' +SIGNAL_UPDATE_ENTITY = 'geo_json_events_update_{}' + SOURCE = 'geo_json_events' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -42,7 +48,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) radius_in_km = config[CONF_RADIUS] # Initialize the entity manager. - GeoJsonFeedManager(hass, add_entities, scan_interval, url, radius_in_km) + feed = GeoJsonFeedManager(hass, add_entities, scan_interval, url, + radius_in_km) + + def start_feed_manager(event): + """Start feed manager.""" + feed.startup() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_feed_manager) class GeoJsonFeedManager: @@ -51,16 +64,19 @@ class GeoJsonFeedManager: def __init__(self, hass, add_entities, scan_interval, url, radius_in_km): """Initialize the GeoJSON Feed Manager.""" from geojson_client.generic_feed import GenericFeed + self._hass = hass self._feed = GenericFeed( (hass.config.latitude, hass.config.longitude), filter_radius=radius_in_km, url=url) self._add_entities = add_entities self._scan_interval = scan_interval - self._feed_entries = [] - self._managed_entities = [] - hass.bus.listen_once( - EVENT_HOMEASSISTANT_START, lambda _: self._update()) + self.feed_entries = {} + self._managed_external_ids = set() + + def startup(self): + """Start up this manager.""" + self._update() self._init_regular_updates() def _init_regular_updates(self): @@ -71,13 +87,24 @@ class GeoJsonFeedManager: def _update(self): """Update the feed and then update connected entities.""" import geojson_client + status, feed_entries = self._feed.update() if status == geojson_client.UPDATE_OK: _LOGGER.debug("Data retrieved %s", feed_entries) # Keep a copy of all feed entries for future lookups by entities. - self._feed_entries = feed_entries.copy() - keep_entries = self._update_or_remove_entities(feed_entries) - self._generate_new_entities(keep_entries) + self.feed_entries = {entry.external_id: entry + for entry in feed_entries} + # For entity management the external ids from the feed are used. + feed_external_ids = set(self.feed_entries) + remove_external_ids = self._managed_external_ids.difference( + feed_external_ids) + self._remove_entities(remove_external_ids) + update_external_ids = self._managed_external_ids.intersection( + feed_external_ids) + self._update_entities(update_external_ids) + create_external_ids = feed_external_ids.difference( + self._managed_external_ids) + self._generate_new_entities(create_external_ids) elif status == geojson_client.UPDATE_OK_NO_DATA: _LOGGER.debug( "Update successful, but no data received from %s", self._feed) @@ -85,61 +112,65 @@ class GeoJsonFeedManager: _LOGGER.warning( "Update not successful, no data received from %s", self._feed) # Remove all entities. - self._update_or_remove_entities([]) + self._remove_entities(self._managed_external_ids.copy()) - def _update_or_remove_entities(self, feed_entries): - """Update existing entries and remove obsolete entities.""" - _LOGGER.debug("Entries for updating: %s", feed_entries) - remove_entry = None - # Remove obsolete entities for events that have disappeared - managed_entities = self._managed_entities.copy() - for entity in managed_entities: - # Remove entry from previous iteration - if applicable. - if remove_entry: - feed_entries.remove(remove_entry) - remove_entry = None - for entry in feed_entries: - if entity.external_id == entry.external_id: - # Existing entity - update details. - _LOGGER.debug("Existing entity found %s", entity) - remove_entry = entry - entity.schedule_update_ha_state(True) - break - else: - # Remove obsolete entity. - _LOGGER.debug("Entity not current anymore %s", entity) - self._managed_entities.remove(entity) - self._hass.add_job(entity.async_remove()) - # Remove entry from very last iteration - if applicable. - if remove_entry: - feed_entries.remove(remove_entry) - # Return the remaining entries that new entities must be created for. - return feed_entries - - def _generate_new_entities(self, entries): + def _generate_new_entities(self, external_ids): """Generate new entities for events.""" new_entities = [] - for entry in entries: - new_entity = GeoJsonLocationEvent(self, entry) - _LOGGER.debug("New entity added %s", new_entity) + for external_id in external_ids: + new_entity = GeoJsonLocationEvent(self, external_id) + _LOGGER.debug("New entity added %s", external_id) new_entities.append(new_entity) - # Add new entities to HA and keep track of them in this manager. + self._managed_external_ids.add(external_id) + # Add new entities to HA. self._add_entities(new_entities, True) - self._managed_entities.extend(new_entities) - def get_feed_entry(self, external_id): - """Return a feed entry identified by external id.""" - return next((entry for entry in self._feed_entries - if entry.external_id == external_id), None) + def _update_entities(self, external_ids): + """Update entities.""" + for external_id in external_ids: + _LOGGER.debug("Existing entity found %s", external_id) + dispatcher_send( + self._hass, SIGNAL_UPDATE_ENTITY.format(external_id)) + + def _remove_entities(self, external_ids): + """Remove entities.""" + for external_id in external_ids: + _LOGGER.debug("Entity not current anymore %s", external_id) + self._managed_external_ids.remove(external_id) + dispatcher_send( + self._hass, SIGNAL_DELETE_ENTITY.format(external_id)) class GeoJsonLocationEvent(GeoLocationEvent): """This represents an external event with GeoJSON data.""" - def __init__(self, feed_manager, feed_entry): + def __init__(self, feed_manager, external_id): """Initialize entity with data from feed entry.""" self._feed_manager = feed_manager - self._update_from_feed(feed_entry) + self._external_id = external_id + self._name = None + self._distance = None + self._latitude = None + self._longitude = None + + async def async_added_to_hass(self): + """Call when entity is added to hass.""" + async_dispatcher_connect( + self.hass, SIGNAL_DELETE_ENTITY.format(self._external_id), + self._delete_callback) + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_ENTITY.format(self._external_id), + self._update_callback) + + @callback + def _delete_callback(self): + """Remove this entity.""" + self.hass.async_create_task(self.async_remove()) + + @callback + def _update_callback(self): + """Call update method.""" + self.async_schedule_update_ha_state(True) @property def should_poll(self): @@ -148,7 +179,8 @@ class GeoJsonLocationEvent(GeoLocationEvent): async def async_update(self): """Update this entity from the data held in the feed manager.""" - feed_entry = self._feed_manager.get_feed_entry(self.external_id) + _LOGGER.debug("Updating %s", self._external_id) + feed_entry = self._feed_manager.feed_entries.get(self._external_id) if feed_entry: self._update_from_feed(feed_entry) @@ -158,7 +190,6 @@ class GeoJsonLocationEvent(GeoLocationEvent): self._distance = feed_entry.distance_to_home self._latitude = feed_entry.coordinates[0] self._longitude = feed_entry.coordinates[1] - self.external_id = feed_entry.external_id @property def source(self) -> str: @@ -194,6 +225,6 @@ class GeoJsonLocationEvent(GeoLocationEvent): def device_state_attributes(self): """Return the device state attributes.""" attributes = {} - if self.external_id: - attributes[ATTR_EXTERNAL_ID] = self.external_id + if self._external_id: + attributes[ATTR_EXTERNAL_ID] = self._external_id return attributes From bd450ee9ffdb7e7aa765070a1c86118cde6f9e1b Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Mon, 15 Oct 2018 02:18:21 -0700 Subject: [PATCH 156/265] Migrate CONF_WEBHOOK_ID to homeassistant.const (#17460) * Migrate CONF_WEBHOOK_ID to homeassistant.const * Switch over all instances of webhook_id to the const * Switch last instance of webhook_id to the const * automation: conf constants for conf * webhook: conf constants for conf --- homeassistant/components/automation/webhook.py | 3 +-- homeassistant/components/ifttt/__init__.py | 6 +++--- homeassistant/const.py | 1 + 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/automation/webhook.py b/homeassistant/components/automation/webhook.py index 2c9c331cdc5..345b0fe3249 100644 --- a/homeassistant/components/automation/webhook.py +++ b/homeassistant/components/automation/webhook.py @@ -11,13 +11,12 @@ from aiohttp import hdrs import voluptuous as vol from homeassistant.core import callback -from homeassistant.const import CONF_PLATFORM +from homeassistant.const import CONF_PLATFORM, CONF_WEBHOOK_ID import homeassistant.helpers.config_validation as cv DEPENDENCIES = ('webhook',) _LOGGER = logging.getLogger(__name__) -CONF_WEBHOOK_ID = 'webhook_id' TRIGGER_SCHEMA = vol.Schema({ vol.Required(CONF_PLATFORM): 'webhook', diff --git a/homeassistant/components/ifttt/__init__.py b/homeassistant/components/ifttt/__init__.py index 60748d6ff13..76f01ad0aca 100644 --- a/homeassistant/components/ifttt/__init__.py +++ b/homeassistant/components/ifttt/__init__.py @@ -14,6 +14,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant import config_entries +from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.util.network import is_local REQUIREMENTS = ['pyfttt==0.3'] @@ -29,7 +30,6 @@ ATTR_VALUE2 = 'value2' ATTR_VALUE3 = 'value3' CONF_KEY = 'key' -CONF_WEBHOOK_ID = 'webhook_id' DOMAIN = 'ifttt' @@ -91,13 +91,13 @@ async def handle_webhook(hass, webhook_id, request): async def async_setup_entry(hass, entry): """Configure based on config entry.""" hass.components.webhook.async_register( - entry.data['webhook_id'], handle_webhook) + entry.data[CONF_WEBHOOK_ID], handle_webhook) return True async def async_unload_entry(hass, entry): """Unload a config entry.""" - hass.components.webhook.async_unregister(entry.data['webhook_id']) + hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) return True diff --git a/homeassistant/const.py b/homeassistant/const.py index 361299181ac..c01a435ca85 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -146,6 +146,7 @@ CONF_URL = 'url' CONF_USERNAME = 'username' CONF_VALUE_TEMPLATE = 'value_template' CONF_VERIFY_SSL = 'verify_ssl' +CONF_WEBHOOK_ID = 'webhook_id' CONF_WEEKDAY = 'weekday' CONF_WHITELIST = 'whitelist' CONF_WHITELIST_EXTERNAL_DIRS = 'whitelist_external_dirs' From 4fce0518382d4d8273c37ade406ec2e04c0ba935 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 15 Oct 2018 12:22:49 +0300 Subject: [PATCH 157/265] Add RSRQ, RSRP, and SINR to huawei_lte default sensors (#17425) These are important LTE signal monitoring values. --- homeassistant/components/sensor/huawei_lte.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/sensor/huawei_lte.py b/homeassistant/components/sensor/huawei_lte.py index f5a21999ab8..092f9e777d1 100644 --- a/homeassistant/components/sensor/huawei_lte.py +++ b/homeassistant/components/sensor/huawei_lte.py @@ -28,7 +28,10 @@ DEFAULT_NAME_TEMPLATE = 'Huawei {}: {}' DEFAULT_SENSORS = [ "device_information.WanIPAddress", + "device_signal.rsrq", + "device_signal.rsrp", "device_signal.rssi", + "device_signal.sinr", ] SENSOR_META = { From 31981fde7e8cdd9bbb8c13cc351258d1fef9ea4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 15 Oct 2018 12:25:38 +0300 Subject: [PATCH 158/265] Make dicttoxml logging less verbose (#17446) --- homeassistant/components/huawei_lte.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/huawei_lte.py b/homeassistant/components/huawei_lte.py index ad134d8c60e..e8d3bc8b3a1 100644 --- a/homeassistant/components/huawei_lte.py +++ b/homeassistant/components/huawei_lte.py @@ -21,6 +21,9 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) +# dicttoxml (used by huawei-lte-api) has uselessly verbose INFO level. +logging.getLogger('dicttoxml').setLevel(logging.WARNING) + REQUIREMENTS = ['huawei-lte-api==1.0.16'] MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) From 0bf10b0b0930a8d5cb7833318b66bdce3b371311 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Mon, 15 Oct 2018 11:34:36 +0200 Subject: [PATCH 159/265] Fire an event when timer gets out of sync (#17398) --- homeassistant/const.py | 2 ++ homeassistant/core.py | 13 ++++++++----- tests/test_core.py | 15 ++++++++------- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c01a435ca85..4dcc171d35c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -168,6 +168,7 @@ EVENT_SERVICE_REGISTERED = 'service_registered' EVENT_SERVICE_REMOVED = 'service_removed' EVENT_LOGBOOK_ENTRY = 'logbook_entry' EVENT_THEMES_UPDATED = 'themes_updated' +EVENT_TIMER_OUT_OF_SYNC = 'timer_out_of_sync' # #### DEVICE CLASSES #### DEVICE_CLASS_BATTERY = 'battery' @@ -216,6 +217,7 @@ ATTR_CREDENTIALS = 'credentials' ATTR_NOW = 'now' ATTR_DATE = 'date' ATTR_TIME = 'time' +ATTR_SECONDS = 'seconds' # Contains domain, service for a SERVICE_CALL event ATTR_DOMAIN = 'domain' diff --git a/homeassistant/core.py b/homeassistant/core.py index d1f811502e0..1754a8b5014 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -29,11 +29,11 @@ from voluptuous.humanize import humanize_error from homeassistant.const import ( ATTR_DOMAIN, ATTR_FRIENDLY_NAME, ATTR_NOW, ATTR_SERVICE, - ATTR_SERVICE_CALL_ID, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, + ATTR_SERVICE_CALL_ID, ATTR_SERVICE_DATA, ATTR_SECONDS, EVENT_CALL_SERVICE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, + EVENT_HOMEASSISTANT_CLOSE, EVENT_SERVICE_REMOVED, EVENT_SERVICE_EXECUTED, EVENT_SERVICE_REGISTERED, EVENT_STATE_CHANGED, - EVENT_TIME_CHANGED, MATCH_ALL, EVENT_HOMEASSISTANT_CLOSE, - EVENT_SERVICE_REMOVED, __version__) + EVENT_TIME_CHANGED, EVENT_TIMER_OUT_OF_SYNC, MATCH_ALL, __version__) from homeassistant import loader from homeassistant.exceptions import ( HomeAssistantError, InvalidEntityFormatError, InvalidStateError) @@ -1297,8 +1297,11 @@ def _async_create_timer(hass: HomeAssistant) -> None: hass.bus.async_fire(EVENT_TIME_CHANGED, {ATTR_NOW: now}) - if monotonic() > target + 1: - _LOGGER.error('Timer got out of sync. Resetting') + # If we are more than a second late, a tick was missed + late = monotonic() - target + if late > 1: + hass.bus.async_fire(EVENT_TIMER_OUT_OF_SYNC, + {ATTR_SECONDS: late}) schedule_tick(now) diff --git a/tests/test_core.py b/tests/test_core.py index d88257abfb4..7ab624447c5 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -19,9 +19,9 @@ import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import (METRIC_SYSTEM) from homeassistant.const import ( __version__, EVENT_STATE_CHANGED, ATTR_FRIENDLY_NAME, CONF_UNIT_SYSTEM, - ATTR_NOW, EVENT_TIME_CHANGED, EVENT_HOMEASSISTANT_STOP, - EVENT_HOMEASSISTANT_CLOSE, EVENT_SERVICE_REGISTERED, EVENT_SERVICE_REMOVED, - EVENT_SERVICE_EXECUTED) + ATTR_NOW, EVENT_TIME_CHANGED, EVENT_TIMER_OUT_OF_SYNC, ATTR_SECONDS, + EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_CLOSE, + EVENT_SERVICE_REGISTERED, EVENT_SERVICE_REMOVED, EVENT_SERVICE_EXECUTED) from tests.common import get_test_home_assistant, async_mock_service @@ -916,12 +916,13 @@ def test_timer_out_of_sync(mock_monotonic, loop): delay, callback, target = hass.loop.call_later.mock_calls[0][1] - with patch.object(ha, '_LOGGER', MagicMock()) as mock_logger, \ - patch('homeassistant.core.dt_util.utcnow', - return_value=datetime(2018, 12, 31, 3, 4, 8, 200000)): + with patch('homeassistant.core.dt_util.utcnow', + return_value=datetime(2018, 12, 31, 3, 4, 8, 200000)): callback(target) - assert len(mock_logger.error.mock_calls) == 1 + event_type, event_data = hass.bus.async_fire.mock_calls[1][1] + assert event_type == EVENT_TIMER_OUT_OF_SYNC + assert abs(event_data[ATTR_SECONDS] - 2.433333) < 0.001 assert len(funcs) == 2 fire_time_event, stop_timer = funcs From ac79ff9e243348f0488bf019ab9d5076246c83bb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 15 Oct 2018 11:38:49 +0200 Subject: [PATCH 160/265] Add context to scripts run by template entities (#17329) --- homeassistant/components/cover/template.py | 22 ++++++++++++--------- homeassistant/components/fan/template.py | 11 ++++++----- homeassistant/components/light/template.py | 6 +++--- homeassistant/components/lock/template.py | 4 ++-- homeassistant/components/switch/template.py | 4 ++-- 5 files changed, 26 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/cover/template.py b/homeassistant/components/cover/template.py index e02cdc32319..f64e4ae7a3f 100644 --- a/homeassistant/components/cover/template.py +++ b/homeassistant/components/cover/template.py @@ -278,9 +278,10 @@ class CoverTemplate(CoverDevice): async def async_open_cover(self, **kwargs): """Move the cover up.""" if self._open_script: - await self._open_script.async_run() + await self._open_script.async_run(context=self._context) elif self._position_script: - await self._position_script.async_run({"position": 100}) + await self._position_script.async_run( + {"position": 100}, context=self._context) if self._optimistic: self._position = 100 self.async_schedule_update_ha_state() @@ -288,9 +289,10 @@ class CoverTemplate(CoverDevice): async def async_close_cover(self, **kwargs): """Move the cover down.""" if self._close_script: - await self._close_script.async_run() + await self._close_script.async_run(context=self._context) elif self._position_script: - await self._position_script.async_run({"position": 0}) + await self._position_script.async_run( + {"position": 0}, context=self._context) if self._optimistic: self._position = 0 self.async_schedule_update_ha_state() @@ -298,20 +300,21 @@ class CoverTemplate(CoverDevice): async def async_stop_cover(self, **kwargs): """Fire the stop action.""" if self._stop_script: - await self._stop_script.async_run() + await self._stop_script.async_run(context=self._context) async def async_set_cover_position(self, **kwargs): """Set cover position.""" self._position = kwargs[ATTR_POSITION] await self._position_script.async_run( - {"position": self._position}) + {"position": self._position}, context=self._context) if self._optimistic: self.async_schedule_update_ha_state() async def async_open_cover_tilt(self, **kwargs): """Tilt the cover open.""" self._tilt_value = 100 - await self._tilt_script.async_run({"tilt": self._tilt_value}) + await self._tilt_script.async_run( + {"tilt": self._tilt_value}, context=self._context) if self._tilt_optimistic: self.async_schedule_update_ha_state() @@ -319,14 +322,15 @@ class CoverTemplate(CoverDevice): """Tilt the cover closed.""" self._tilt_value = 0 await self._tilt_script.async_run( - {"tilt": self._tilt_value}) + {"tilt": self._tilt_value}, context=self._context) if self._tilt_optimistic: self.async_schedule_update_ha_state() async def async_set_cover_tilt_position(self, **kwargs): """Move the cover tilt to a specific position.""" self._tilt_value = kwargs[ATTR_TILT_POSITION] - await self._tilt_script.async_run({"tilt": self._tilt_value}) + await self._tilt_script.async_run( + {"tilt": self._tilt_value}, context=self._context) if self._tilt_optimistic: self.async_schedule_update_ha_state() diff --git a/homeassistant/components/fan/template.py b/homeassistant/components/fan/template.py index ff25afb792a..a2f33d40e48 100644 --- a/homeassistant/components/fan/template.py +++ b/homeassistant/components/fan/template.py @@ -224,7 +224,7 @@ class TemplateFan(FanEntity): # pylint: disable=arguments-differ async def async_turn_on(self, speed: str = None) -> None: """Turn on the fan.""" - await self._on_script.async_run() + await self._on_script.async_run(context=self._context) self._state = STATE_ON if speed is not None: @@ -233,7 +233,7 @@ class TemplateFan(FanEntity): # pylint: disable=arguments-differ async def async_turn_off(self) -> None: """Turn off the fan.""" - await self._off_script.async_run() + await self._off_script.async_run(context=self._context) self._state = STATE_OFF async def async_set_speed(self, speed: str) -> None: @@ -243,7 +243,8 @@ class TemplateFan(FanEntity): if speed in self._speed_list: self._speed = speed - await self._set_speed_script.async_run({ATTR_SPEED: speed}) + await self._set_speed_script.async_run( + {ATTR_SPEED: speed}, context=self._context) else: _LOGGER.error( 'Received invalid speed: %s. Expected: %s.', @@ -257,7 +258,7 @@ class TemplateFan(FanEntity): if oscillating in _VALID_OSC: self._oscillating = oscillating await self._set_oscillating_script.async_run( - {ATTR_OSCILLATING: oscillating}) + {ATTR_OSCILLATING: oscillating}, context=self._context) else: _LOGGER.error( 'Received invalid oscillating value: %s. Expected: %s.', @@ -271,7 +272,7 @@ class TemplateFan(FanEntity): if direction in _VALID_DIRECTIONS: self._direction = direction await self._set_direction_script.async_run( - {ATTR_DIRECTION: direction}) + {ATTR_DIRECTION: direction}, context=self._context) else: _LOGGER.error( 'Received invalid direction: %s. Expected: %s.', diff --git a/homeassistant/components/light/template.py b/homeassistant/components/light/template.py index 8aff85c6001..2447dabe3c7 100644 --- a/homeassistant/components/light/template.py +++ b/homeassistant/components/light/template.py @@ -215,8 +215,8 @@ class LightTemplate(Light): optimistic_set = True if ATTR_BRIGHTNESS in kwargs and self._level_script: - self.hass.async_create_task(self._level_script.async_run( - {"brightness": kwargs[ATTR_BRIGHTNESS]})) + await self._level_script.async_run( + {"brightness": kwargs[ATTR_BRIGHTNESS]}, context=self._context) else: await self._on_script.async_run() @@ -225,7 +225,7 @@ class LightTemplate(Light): async def async_turn_off(self, **kwargs): """Turn the light off.""" - await self._off_script.async_run() + await self._off_script.async_run(context=self._context) if self._template is None: self._state = False self.async_schedule_update_ha_state() diff --git a/homeassistant/components/lock/template.py b/homeassistant/components/lock/template.py index e395cc508ad..527af4c5b85 100644 --- a/homeassistant/components/lock/template.py +++ b/homeassistant/components/lock/template.py @@ -131,11 +131,11 @@ class TemplateLock(LockDevice): if self._optimistic: self._state = True self.async_schedule_update_ha_state() - await self._command_lock.async_run() + await self._command_lock.async_run(context=self._context) async def async_unlock(self, **kwargs): """Unlock the device.""" if self._optimistic: self._state = False self.async_schedule_update_ha_state() - await self._command_unlock.async_run() + await self._command_unlock.async_run(context=self._context) diff --git a/homeassistant/components/switch/template.py b/homeassistant/components/switch/template.py index 724fcbf6075..51cea68f6b3 100644 --- a/homeassistant/components/switch/template.py +++ b/homeassistant/components/switch/template.py @@ -151,11 +151,11 @@ class SwitchTemplate(SwitchDevice): async def async_turn_on(self, **kwargs): """Fire the on action.""" - await self._on_script.async_run() + await self._on_script.async_run(context=self._context) async def async_turn_off(self, **kwargs): """Fire the off action.""" - await self._off_script.async_run() + await self._off_script.async_run(context=self._context) async def async_update(self): """Update the state from the template.""" From 1cbb5b8e51e849dfa37185fc133c599170196ec0 Mon Sep 17 00:00:00 2001 From: kennedyshead Date: Mon, 15 Oct 2018 11:42:27 +0200 Subject: [PATCH 161/265] State is set to UNKNOWN rather than ON in order to make UI have an play/pause button (#17357) --- .../components/media_player/samsungtv.py | 9 ++++----- .../components/media_player/test_samsungtv.py | 18 +++++++++--------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py index 3a255ad0ad2..45d158a8653 100644 --- a/homeassistant/components/media_player/samsungtv.py +++ b/homeassistant/components/media_player/samsungtv.py @@ -17,8 +17,7 @@ from homeassistant.components.media_player import ( SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, MediaPlayerDevice) from homeassistant.const import ( - CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT, CONF_TIMEOUT, STATE_OFF, - STATE_ON, STATE_UNKNOWN) + CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT, CONF_TIMEOUT, STATE_OFF) import homeassistant.helpers.config_validation as cv from homeassistant.util import dt as dt_util @@ -100,7 +99,7 @@ class SamsungTVDevice(MediaPlayerDevice): self._muted = False # Assume that the TV is in Play mode self._playing = True - self._state = STATE_UNKNOWN + self._state = None self._remote = None # Mark the end of a shutdown command (need to wait 15 seconds before # sending the next command to avoid turning the TV back ON). @@ -149,11 +148,11 @@ class SamsungTVDevice(MediaPlayerDevice): BrokenPipeError): # BrokenPipe can occur when the commands is sent to fast self._remote = None - self._state = STATE_ON + self._state = None except (self._exceptions_class.UnhandledResponse, self._exceptions_class.AccessDenied): # We got a response so it's on. - self._state = STATE_ON + self._state = None self._remote = None _LOGGER.debug("Failed sending command %s", key, exc_info=True) return diff --git a/tests/components/media_player/test_samsungtv.py b/tests/components/media_player/test_samsungtv.py index 45db09dc662..dda6562af77 100644 --- a/tests/components/media_player/test_samsungtv.py +++ b/tests/components/media_player/test_samsungtv.py @@ -12,8 +12,8 @@ from homeassistant.components.media_player import SUPPORT_TURN_ON, \ MEDIA_TYPE_CHANNEL, MEDIA_TYPE_URL from homeassistant.components.media_player.samsungtv import setup_platform, \ CONF_TIMEOUT, SamsungTVDevice, SUPPORT_SAMSUNGTV -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, STATE_ON, \ - CONF_MAC, STATE_OFF +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_MAC, \ + STATE_OFF from tests.common import MockDependency from homeassistant.util import dt as dt_util from datetime import timedelta @@ -102,7 +102,7 @@ class TestSamsungTv(unittest.TestCase): def test_update_on(self): """Testing update tv on.""" self.device.update() - self.assertEqual(STATE_ON, self.device._state) + self.assertEqual(None, self.device._state) def test_update_off(self): """Testing update tv off.""" @@ -116,7 +116,7 @@ class TestSamsungTv(unittest.TestCase): def test_send_key(self): """Test for send key.""" self.device.send_key('KEY_POWER') - self.assertEqual(STATE_ON, self.device._state) + self.assertEqual(None, self.device._state) def test_send_key_broken_pipe(self): """Testing broken pipe Exception.""" @@ -126,7 +126,7 @@ class TestSamsungTv(unittest.TestCase): self.device.get_remote = mock.Mock(return_value=_remote) self.device.send_key('HELLO') self.assertIsNone(self.device._remote) - self.assertEqual(STATE_ON, self.device._state) + self.assertEqual(None, self.device._state) def test_send_key_connection_closed_retry_succeed(self): """Test retry on connection closed.""" @@ -137,7 +137,7 @@ class TestSamsungTv(unittest.TestCase): self.device.get_remote = mock.Mock(return_value=_remote) command = 'HELLO' self.device.send_key(command) - self.assertEqual(STATE_ON, self.device._state) + self.assertEqual(None, self.device._state) # verify that _remote.control() get called twice because of retry logic expected = [mock.call(command), mock.call(command)] @@ -152,7 +152,7 @@ class TestSamsungTv(unittest.TestCase): self.device.get_remote = mock.Mock(return_value=_remote) self.device.send_key('HELLO') self.assertIsNone(self.device._remote) - self.assertEqual(STATE_ON, self.device._state) + self.assertEqual(None, self.device._state) def test_send_key_os_error(self): """Testing broken pipe Exception.""" @@ -177,8 +177,8 @@ class TestSamsungTv(unittest.TestCase): def test_state(self): """Test for state property.""" - self.device._state = STATE_ON - self.assertEqual(STATE_ON, self.device.state) + self.device._state = None + self.assertEqual(None, self.device.state) self.device._state = STATE_OFF self.assertEqual(STATE_OFF, self.device.state) From 22bf4d07838eaf2779043f233e05562f4c504f15 Mon Sep 17 00:00:00 2001 From: Adam <22942687+SilvrrGIT@users.noreply.github.com> Date: Mon, 15 Oct 2018 04:43:27 -0500 Subject: [PATCH 162/265] Re-assign conditions (#17364) --- homeassistant/components/weather/yweather.py | 28 ++++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/weather/yweather.py b/homeassistant/components/weather/yweather.py index 505c287a99e..567b8e235a8 100644 --- a/homeassistant/components/weather/yweather.py +++ b/homeassistant/components/weather/yweather.py @@ -31,21 +31,21 @@ DEFAULT_NAME = 'Yweather' SCAN_INTERVAL = timedelta(minutes=10) CONDITION_CLASSES = { - 'clear-night': [31], - 'cloudy': [26, 27, 28, 29, 30], - 'fog': [19, 20, 21, 22, 23], - 'hail': [17, 18, 35], - 'lightning': [37], - 'lightning-rainy': [3, 4, 38, 39, 47], - 'partlycloudy': [44], - 'pouring': [40, 45], - 'rainy': [9, 11, 12], - 'snowy': [8, 13, 14, 15, 16, 41, 42, 43], - 'snowy-rainy': [5, 6, 7, 10, 46], - 'sunny': [32, 33, 34, 25, 36], - 'windy': [24], + 'clear-night': [31, 33], + 'cloudy': [26, 27, 28], + 'fog': [20, 21], + 'hail': [17, 35], + 'lightning': [], + 'lightning-rainy': [3, 4, 37, 38, 39, 45, 47], + 'partlycloudy': [29, 30, 44], + 'pouring': [], + 'rainy': [9, 10, 11, 12, 40], + 'snowy': [8, 13, 14, 15, 16, 41, 42, 43, 46], + 'snowy-rainy': [5, 6, 7, 18], + 'sunny': [25, 32, 34, 36], + 'windy': [23, 24], 'windy-variant': [], - 'exceptional': [0, 1, 2], + 'exceptional': [0, 1, 2, 19, 22], } From e985f302476ef2ebc7180bf53b8451f159c88948 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 15 Oct 2018 11:48:36 +0200 Subject: [PATCH 163/265] Fix websocket API (#17471) --- homeassistant/components/websocket_api/http.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index 87f25c9b3ef..13be503a009 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -187,3 +187,5 @@ class WebSocketHandler: self._logger.debug("Disconnected") else: self._logger.warning("Disconnected: %s", disconnect_warn) + + return wsock From 373e3b12d8e0b6b8d969af9dd1f8873cb5a46196 Mon Sep 17 00:00:00 2001 From: Julius Mittenzwei Date: Mon, 15 Oct 2018 12:16:40 +0200 Subject: [PATCH 164/265] Switched to async_fire (#17472) --- homeassistant/components/knx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/knx.py b/homeassistant/components/knx.py index b15963fa6bd..6aef3ea4ec5 100644 --- a/homeassistant/components/knx.py +++ b/homeassistant/components/knx.py @@ -240,7 +240,7 @@ class KNXModule: async def telegram_received_cb(self, telegram): """Call invoked after a KNX telegram was received.""" - self.hass.bus.fire('knx_event', { + self.hass.bus.async_fire('knx_event', { 'address': str(telegram.group_address), 'data': telegram.payload.value }) From d6752d2270585be292a4c362ef8aa5df798a6424 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Mon, 15 Oct 2018 12:24:21 +0200 Subject: [PATCH 165/265] Fix rangefilter (#17473) * Fix rangefilter RangeFilter would break for lower or upper bounds of 0, evaluating to False and thus not being handled correctly as bounds * Add test for zero bounds --- homeassistant/components/sensor/filter.py | 8 +++++--- tests/components/sensor/test_filter.py | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py index 76aab42497e..e93795c3668 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -347,7 +347,7 @@ class RangeFilter(Filter): """ def __init__(self, entity, - lower_bound, upper_bound): + lower_bound=None, upper_bound=None): """Initialize Filter.""" super().__init__(FILTER_NAME_RANGE, entity=entity) self._lower_bound = lower_bound @@ -356,7 +356,8 @@ class RangeFilter(Filter): def _filter_state(self, new_state): """Implement the range filter.""" - if self._upper_bound and new_state.state > self._upper_bound: + if (self._upper_bound is not None + and new_state.state > self._upper_bound): self._stats_internal['erasures_up'] += 1 @@ -365,7 +366,8 @@ class RangeFilter(Filter): self._entity, new_state) new_state.state = self._upper_bound - elif self._lower_bound and new_state.state < self._lower_bound: + elif (self._lower_bound is not None + and new_state.state < self._lower_bound): self._stats_internal['erasures_low'] += 1 diff --git a/tests/components/sensor/test_filter.py b/tests/components/sensor/test_filter.py index b0683b04aa0..433d1aa2512 100644 --- a/tests/components/sensor/test_filter.py +++ b/tests/components/sensor/test_filter.py @@ -149,6 +149,23 @@ class TestFilterSensor(unittest.TestCase): else: self.assertEqual(unf, filtered.state) + def test_range_zero(self): + """Test if range filter works with zeroes as bounds.""" + lower = 0 + upper = 0 + filt = RangeFilter(entity=None, + lower_bound=lower, + upper_bound=upper) + for unf_state in self.values: + unf = float(unf_state.state) + filtered = filt.filter_state(unf_state) + if unf < lower: + self.assertEqual(lower, filtered.state) + elif unf > upper: + self.assertEqual(upper, filtered.state) + else: + self.assertEqual(unf, filtered.state) + def test_throttle(self): """Test if lowpass filter works.""" filt = ThrottleFilter(window_size=3, From 0904ff45fe4b73c201345db01cbc6082bebbdbe4 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 15 Oct 2018 13:26:09 +0200 Subject: [PATCH 166/265] Cleanup HM Notify platform (#17355) * Cleanup HM Notify platform * Fix python 3.5.4 * Update homematic.py * Update homematic.py --- homeassistant/components/notify/homematic.py | 21 ++++---------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/notify/homematic.py b/homeassistant/components/notify/homematic.py index 2587bac8b6c..2897123c690 100644 --- a/homeassistant/components/notify/homematic.py +++ b/homeassistant/components/notify/homematic.py @@ -52,23 +52,10 @@ class HomematicNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a notification to the device.""" - attr_data = kwargs.get(ATTR_DATA) - if attr_data is not None: - if 'address' in attr_data: - self.data[ATTR_ADDRESS] = attr_data['address'] - if 'channel' in attr_data: - self.data[ATTR_CHANNEL] = attr_data['channel'] - if 'param' in attr_data: - self.data[ATTR_PARAM] = attr_data['param'] - if 'value' in attr_data: - self.data[ATTR_VALUE] = attr_data['value'] - if 'interface' in attr_data: - self.data[ATTR_INTERFACE] = attr_data['interface'] + data = {**self.data, **kwargs.get(ATTR_DATA, {})} - if self.data[ATTR_VALUE] is not None: + if data.get(ATTR_VALUE) is not None: templ = template_helper.Template(self.data[ATTR_VALUE], self.hass) - self.data[ATTR_VALUE] = template_helper.render_complex(templ, None) + data[ATTR_VALUE] = template_helper.render_complex(templ, None) - _LOGGER.debug("Calling service: domain=%s;service=%s;data=%s", - DOMAIN, SERVICE_SET_DEVICE_VALUE, str(self.data)) - self.hass.services.call(DOMAIN, SERVICE_SET_DEVICE_VALUE, self.data) + self.hass.services.call(DOMAIN, SERVICE_SET_DEVICE_VALUE, data) From 2ceb4d2d1eba05ab685c914618bf6bf8f3ec7300 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Mon, 15 Oct 2018 15:35:33 +0200 Subject: [PATCH 167/265] Refactor RFLink component (#17402) * Start refactor of RFLink component * alias _id not added correctly Aliases for sensor not added correctly And some debug traces. * Update rflink.py * Cleanup, fix review comments * Call event handlers directly when processing initial event * Use hass.async_create_task when adding discovered device * Review comments * Review comments --- homeassistant/components/cover/rflink.py | 36 ++----- homeassistant/components/light/rflink.py | 66 ++++--------- homeassistant/components/rflink.py | 112 ++++++++++++++++------ homeassistant/components/sensor/rflink.py | 82 +++++++++------- homeassistant/components/switch/rflink.py | 56 ++++------- 5 files changed, 168 insertions(+), 184 deletions(-) diff --git a/homeassistant/components/cover/rflink.py b/homeassistant/components/cover/rflink.py index 41a4c2af045..353cccc7d4f 100644 --- a/homeassistant/components/cover/rflink.py +++ b/homeassistant/components/cover/rflink.py @@ -9,8 +9,9 @@ import logging import voluptuous as vol from homeassistant.components.rflink import ( - DATA_ENTITY_GROUP_LOOKUP, DATA_ENTITY_LOOKUP, - DEVICE_DEFAULTS_SCHEMA, EVENT_KEY_COMMAND, RflinkCommand) + CONF_ALIASES, CONF_DEVICE_DEFAULTS, CONF_DEVICES, CONF_FIRE_EVENT, + CONF_GROUP, CONF_GROUP_ALIASES, CONF_NOGROUP_ALIASES, + CONF_SIGNAL_REPETITIONS, DEVICE_DEFAULTS_SCHEMA, RflinkCommand) from homeassistant.components.cover import ( CoverDevice, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv @@ -22,19 +23,6 @@ DEPENDENCIES = ['rflink'] _LOGGER = logging.getLogger(__name__) -CONF_ALIASES = 'aliases' -CONF_GROUP_ALIASES = 'group_aliases' -CONF_GROUP = 'group' -CONF_NOGROUP_ALIASES = 'nogroup_aliases' -CONF_DEVICE_DEFAULTS = 'device_defaults' -CONF_DEVICES = 'devices' -CONF_AUTOMATIC_ADD = 'automatic_add' -CONF_FIRE_EVENT = 'fire_event' -CONF_IGNORE_DEVICES = 'ignore_devices' -CONF_RECONNECT_INTERVAL = 'reconnect_interval' -CONF_SIGNAL_REPETITIONS = 'signal_repetitions' -CONF_WAIT_FOR_ACK = 'wait_for_ack' - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_DEVICE_DEFAULTS, default=DEVICE_DEFAULTS_SCHEMA({})): DEVICE_DEFAULTS_SCHEMA, @@ -55,33 +43,21 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -def devices_from_config(domain_config, hass=None): +def devices_from_config(domain_config): """Parse configuration and add Rflink cover devices.""" devices = [] for device_id, config in domain_config[CONF_DEVICES].items(): device_config = dict(domain_config[CONF_DEVICE_DEFAULTS], **config) - device = RflinkCover(device_id, hass, **device_config) + device = RflinkCover(device_id, **device_config) devices.append(device) - # Register entity (and aliases) to listen to incoming rflink events - # Device id and normal aliases respond to normal and group command - hass.data[DATA_ENTITY_LOOKUP][ - EVENT_KEY_COMMAND][device_id].append(device) - if config[CONF_GROUP]: - hass.data[DATA_ENTITY_GROUP_LOOKUP][ - EVENT_KEY_COMMAND][device_id].append(device) - for _id in config[CONF_ALIASES]: - hass.data[DATA_ENTITY_LOOKUP][ - EVENT_KEY_COMMAND][_id].append(device) - hass.data[DATA_ENTITY_GROUP_LOOKUP][ - EVENT_KEY_COMMAND][_id].append(device) return devices async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Rflink cover platform.""" - async_add_entities(devices_from_config(config, hass)) + async_add_entities(devices_from_config(config)) class RflinkCover(RflinkCommand, CoverDevice): diff --git a/homeassistant/components/light/rflink.py b/homeassistant/components/light/rflink.py index d9f9dd589ec..885239a51c3 100644 --- a/homeassistant/components/light/rflink.py +++ b/homeassistant/components/light/rflink.py @@ -7,18 +7,16 @@ https://home-assistant.io/components/light.rflink/ import logging from homeassistant.components.light import ( - ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) + ATTR_BRIGHTNESS, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, Light) from homeassistant.components.rflink import ( CONF_ALIASES, CONF_ALIASSES, CONF_AUTOMATIC_ADD, CONF_DEVICE_DEFAULTS, CONF_DEVICES, CONF_FIRE_EVENT, CONF_GROUP, CONF_GROUP_ALIASES, CONF_GROUP_ALIASSES, CONF_IGNORE_DEVICES, CONF_NOGROUP_ALIASES, CONF_NOGROUP_ALIASSES, CONF_SIGNAL_REPETITIONS, DATA_DEVICE_REGISTER, - DATA_ENTITY_GROUP_LOOKUP, DATA_ENTITY_LOOKUP, DEVICE_DEFAULTS_SCHEMA, - DOMAIN, EVENT_KEY_COMMAND, EVENT_KEY_ID, SwitchableRflinkDevice, cv, + DEVICE_DEFAULTS_SCHEMA, + EVENT_KEY_COMMAND, EVENT_KEY_ID, SwitchableRflinkDevice, cv, remove_deprecated, vol) -from homeassistant.const import ( - CONF_NAME, CONF_PLATFORM, CONF_TYPE, STATE_UNKNOWN) -from homeassistant.helpers.deprecation import get_deprecated +from homeassistant.const import (CONF_NAME, CONF_TYPE) DEPENDENCIES = ['rflink'] @@ -29,14 +27,13 @@ TYPE_SWITCHABLE = 'switchable' TYPE_HYBRID = 'hybrid' TYPE_TOGGLE = 'toggle' -PLATFORM_SCHEMA = vol.Schema({ - vol.Required(CONF_PLATFORM): DOMAIN, +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_IGNORE_DEVICES): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_DEVICE_DEFAULTS, default=DEVICE_DEFAULTS_SCHEMA({})): - DEVICE_DEFAULTS_SCHEMA, + DEVICE_DEFAULTS_SCHEMA, vol.Optional(CONF_AUTOMATIC_ADD, default=True): cv.boolean, - vol.Optional(CONF_DEVICES, default={}): vol.Schema({ - cv.string: { + vol.Optional(CONF_DEVICES, default={}): { + cv.string: vol.Schema({ vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_TYPE): vol.Any(TYPE_DIMMABLE, TYPE_SWITCHABLE, @@ -57,9 +54,9 @@ PLATFORM_SCHEMA = vol.Schema({ vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_NOGROUP_ALIASSES): vol.All(cv.ensure_list, [cv.string]), - }, - }), -}) + }) + }, +}, extra=vol.ALLOW_EXTRA) def entity_type_for_device_id(device_id): @@ -98,7 +95,7 @@ def entity_class_for_type(entity_type): return entity_device_mapping.get(entity_type, RflinkLight) -def devices_from_config(domain_config, hass=None): +def devices_from_config(domain_config): """Parse configuration and add Rflink light devices.""" devices = [] for device_id, config in domain_config[CONF_DEVICES].items(): @@ -124,40 +121,16 @@ def devices_from_config(domain_config, hass=None): "repetitions. Please set 'dimmable' or 'switchable' " "type explicitly in configuration", device_id) - device = entity_class(device_id, hass, **device_config) + device = entity_class(device_id, **device_config) devices.append(device) - # Register entity (and aliases) to listen to incoming rflink events - - # Device id and normal aliases respond to normal and group command - hass.data[DATA_ENTITY_LOOKUP][ - EVENT_KEY_COMMAND][device_id].append(device) - if config[CONF_GROUP]: - hass.data[DATA_ENTITY_GROUP_LOOKUP][ - EVENT_KEY_COMMAND][device_id].append(device) - for _id in get_deprecated(config, CONF_ALIASES, CONF_ALIASSES): - hass.data[DATA_ENTITY_LOOKUP][ - EVENT_KEY_COMMAND][_id].append(device) - hass.data[DATA_ENTITY_GROUP_LOOKUP][ - EVENT_KEY_COMMAND][_id].append(device) - # group_aliases only respond to group commands - for _id in get_deprecated( - config, CONF_GROUP_ALIASES, CONF_GROUP_ALIASSES): - hass.data[DATA_ENTITY_GROUP_LOOKUP][ - EVENT_KEY_COMMAND][_id].append(device) - # nogroup_aliases only respond to normal commands - for _id in get_deprecated( - config, CONF_NOGROUP_ALIASES, CONF_NOGROUP_ALIASSES): - hass.data[DATA_ENTITY_LOOKUP][ - EVENT_KEY_COMMAND][_id].append(device) - return devices async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Rflink light platform.""" - async_add_entities(devices_from_config(config, hass)) + async_add_entities(devices_from_config(config)) async def add_new_device(event): """Check if device is known, otherwise add to list of known devices.""" @@ -167,16 +140,9 @@ async def async_setup_platform(hass, config, async_add_entities, entity_class = entity_class_for_type(entity_type) device_config = config[CONF_DEVICE_DEFAULTS] - device = entity_class(device_id, hass, **device_config) + device = entity_class(device_id, initial_event=event, **device_config) async_add_entities([device]) - # Register entity to listen to incoming Rflink events - hass.data[DATA_ENTITY_LOOKUP][ - EVENT_KEY_COMMAND][device_id].append(device) - - # Schedule task to process event after entity is created - hass.async_add_job(device.handle_event, event) - if config[CONF_AUTOMATIC_ADD]: hass.data[DATA_DEVICE_REGISTER][EVENT_KEY_COMMAND] = add_new_device @@ -277,7 +243,7 @@ class ToggleRflinkLight(SwitchableRflinkDevice, Light): if command == 'on': # if the state is unknown or false, it gets set as true # if the state is true, it gets set as false - self._state = self._state in [STATE_UNKNOWN, False] + self._state = self._state in [None, False] async def async_turn_on(self, **kwargs): """Turn the device on.""" diff --git a/homeassistant/components/rflink.py b/homeassistant/components/rflink.py index a8aeca273d6..b75a14968cd 100644 --- a/homeassistant/components/rflink.py +++ b/homeassistant/components/rflink.py @@ -6,7 +6,6 @@ https://home-assistant.io/components/rflink/ """ import asyncio from collections import defaultdict -import functools as ft import logging import async_timeout @@ -14,7 +13,7 @@ import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, CONF_COMMAND, CONF_HOST, CONF_PORT, - EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN) + EVENT_HOMEASSISTANT_STOP) from homeassistant.core import CoreState, callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv @@ -68,6 +67,9 @@ DOMAIN = 'rflink' SERVICE_SEND_COMMAND = 'send_command' SIGNAL_AVAILABILITY = 'rflink_device_available' +SIGNAL_HANDLE_EVENT = 'rflink_handle_event_{}' + +TMP_ENTITY = 'tmp.{}' DEVICE_DEFAULTS_SCHEMA = vol.Schema({ vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean, @@ -153,28 +155,38 @@ async def async_setup(hass, config): return # Lookup entities who registered this device id as device id or alias - event_id = event.get('id', None) + event_id = event.get(EVENT_KEY_ID, None) is_group_event = (event_type == EVENT_KEY_COMMAND and event[EVENT_KEY_COMMAND] in RFLINK_GROUP_COMMANDS) if is_group_event: - entities = hass.data[DATA_ENTITY_GROUP_LOOKUP][event_type].get( + entity_ids = hass.data[DATA_ENTITY_GROUP_LOOKUP][event_type].get( event_id, []) else: - entities = hass.data[DATA_ENTITY_LOOKUP][event_type][event_id] + entity_ids = hass.data[DATA_ENTITY_LOOKUP][event_type][event_id] - if entities: + _LOGGER.debug('entity_ids: %s', entity_ids) + if entity_ids: # Propagate event to every entity matching the device id - for entity in entities: - _LOGGER.debug('passing event to %s', entities) - entity.handle_event(event) - else: - _LOGGER.debug('device_id not known, adding new device') - + for entity in entity_ids: + _LOGGER.debug('passing event to %s', entity) + async_dispatcher_send(hass, + SIGNAL_HANDLE_EVENT.format(entity), + event) + elif not is_group_event: # If device is not yet known, register with platform (if loaded) if event_type in hass.data[DATA_DEVICE_REGISTER]: - hass.async_run_job( - hass.data[DATA_DEVICE_REGISTER][event_type], event) + _LOGGER.debug('device_id not known, adding new device') + # Add bogus event_id first to avoid race if we get another + # event before the device is created + # Any additional events recevied before the device has been + # created will thus be ignored. + hass.data[DATA_ENTITY_LOOKUP][event_type][ + event_id].append(TMP_ENTITY.format(event_id)) + hass.async_create_task( + hass.data[DATA_DEVICE_REGISTER][event_type](event)) + else: + _LOGGER.debug('device_id not known and automatic add disabled') # When connecting to tcp host instead of serial port (optional) host = config[DOMAIN].get(CONF_HOST) @@ -192,7 +204,7 @@ async def async_setup(hass, config): # If HA is not stopping, initiate new connection if hass.state != CoreState.stopping: _LOGGER.warning('disconnected from Rflink, reconnecting') - hass.async_add_job(connect) + hass.async_create_task(connect()) async def connect(): """Set up connection and hook it into HA for reconnect/shutdown.""" @@ -242,7 +254,7 @@ async def async_setup(hass, config): _LOGGER.info('Connected to Rflink') - hass.async_add_job(connect) + hass.async_create_task(connect()) return True @@ -253,26 +265,31 @@ class RflinkDevice(Entity): """ platform = None - _state = STATE_UNKNOWN + _state = None _available = True - def __init__(self, device_id, hass, name=None, aliases=None, group=True, - group_aliases=None, nogroup_aliases=None, fire_event=False, + def __init__(self, device_id, initial_event=None, name=None, aliases=None, + group=True, group_aliases=None, nogroup_aliases=None, + fire_event=False, signal_repetitions=DEFAULT_SIGNAL_REPETITIONS): """Initialize the device.""" - self.hass = hass - # Rflink specific attributes for every component type + self._initial_event = initial_event self._device_id = device_id if name: self._name = name else: self._name = device_id + self._aliases = aliases + self._group = group + self._group_aliases = group_aliases + self._nogroup_aliases = nogroup_aliases self._should_fire_event = fire_event self._signal_repetitions = signal_repetitions - def handle_event(self, event): + @callback + def handle_event_callback(self, event): """Handle incoming event for device type.""" # Call platform specific event handler self._handle_event(event) @@ -283,7 +300,7 @@ class RflinkDevice(Entity): # Put command onto bus for user to subscribe to if self._should_fire_event and identify_event_type( event) == EVENT_KEY_COMMAND: - self.hass.bus.fire(EVENT_BUTTON_PRESSED, { + self.hass.bus.async_fire(EVENT_BUTTON_PRESSED, { ATTR_ENTITY_ID: self.entity_id, ATTR_STATE: event[EVENT_KEY_COMMAND], }) @@ -314,7 +331,7 @@ class RflinkDevice(Entity): @property def assumed_state(self): """Assume device state until first device event sets state.""" - return self._state is STATE_UNKNOWN + return self._state is None @property def available(self): @@ -322,15 +339,52 @@ class RflinkDevice(Entity): return self._available @callback - def set_availability(self, availability): + def _availability_callback(self, availability): """Update availability state.""" self._available = availability self.async_schedule_update_ha_state() async def async_added_to_hass(self): """Register update callback.""" + # Remove temporary bogus entity_id if added + tmp_entity = TMP_ENTITY.format(self._device_id) + if tmp_entity in self.hass.data[DATA_ENTITY_LOOKUP][ + EVENT_KEY_SENSOR][self._device_id]: + self.hass.data[DATA_ENTITY_LOOKUP][ + EVENT_KEY_SENSOR][self._device_id].remove(tmp_entity) + + # Register id and aliases + self.hass.data[DATA_ENTITY_LOOKUP][ + EVENT_KEY_COMMAND][self._device_id].append(self.entity_id) + if self._group: + self.hass.data[DATA_ENTITY_GROUP_LOOKUP][ + EVENT_KEY_COMMAND][self._device_id].append(self.entity_id) + # aliases respond to both normal and group commands (allon/alloff) + if self._aliases: + for _id in self._aliases: + self.hass.data[DATA_ENTITY_LOOKUP][ + EVENT_KEY_COMMAND][_id].append(self.entity_id) + self.hass.data[DATA_ENTITY_GROUP_LOOKUP][ + EVENT_KEY_COMMAND][_id].append(self.entity_id) + # group_aliases only respond to group commands (allon/alloff) + if self._group_aliases: + for _id in self._group_aliases: + self.hass.data[DATA_ENTITY_GROUP_LOOKUP][ + EVENT_KEY_COMMAND][_id].append(self.entity_id) + # nogroup_aliases only respond to normal commands + if self._nogroup_aliases: + for _id in self._nogroup_aliases: + self.hass.data[DATA_ENTITY_LOOKUP][ + EVENT_KEY_COMMAND][_id].append(self.entity_id) async_dispatcher_connect(self.hass, SIGNAL_AVAILABILITY, - self.set_availability) + self._availability_callback) + async_dispatcher_connect(self.hass, + SIGNAL_HANDLE_EVENT.format(self.entity_id), + self.handle_event_callback) + + # Process the initial event now that the entity is created + if self._initial_event: + self.handle_event_callback(self._initial_event) class RflinkCommand(RflinkDevice): @@ -388,7 +442,7 @@ class RflinkCommand(RflinkDevice): cmd = 'on' # if the state is unknown or false, it gets set as true # if the state is true, it gets set as false - self._state = self._state in [STATE_UNKNOWN, False] + self._state = self._state in [None, False] # Cover options for RFlink elif command == 'close_cover': @@ -439,8 +493,8 @@ class RflinkCommand(RflinkDevice): # Rflink protocol/transport handles asynchronous writing of buffer # to serial/tcp device. Does not wait for command send # confirmation. - self.hass.async_add_job(ft.partial( - self._protocol.send_command, self._device_id, cmd)) + self.hass.async_create_task(self._protocol.send_command( + self._device_id, cmd)) if repetitions > 1: self._repetition_task = self.hass.async_create_task( diff --git a/homeassistant/components/sensor/rflink.py b/homeassistant/components/sensor/rflink.py index c33c99c5828..a401eebeec6 100644 --- a/homeassistant/components/sensor/rflink.py +++ b/homeassistant/components/sensor/rflink.py @@ -4,16 +4,18 @@ Support for Rflink sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.rflink/ """ -from functools import partial import logging from homeassistant.components.rflink import ( CONF_ALIASES, CONF_ALIASSES, CONF_AUTOMATIC_ADD, CONF_DEVICES, - DATA_DEVICE_REGISTER, DATA_ENTITY_LOOKUP, DOMAIN, EVENT_KEY_ID, - EVENT_KEY_SENSOR, EVENT_KEY_UNIT, RflinkDevice, cv, remove_deprecated, vol) + DATA_DEVICE_REGISTER, DATA_ENTITY_LOOKUP, EVENT_KEY_ID, + EVENT_KEY_SENSOR, EVENT_KEY_UNIT, RflinkDevice, cv, remove_deprecated, vol, + SIGNAL_AVAILABILITY, SIGNAL_HANDLE_EVENT, TMP_ENTITY) +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA) from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, CONF_PLATFORM, - CONF_UNIT_OF_MEASUREMENT) + ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, CONF_UNIT_OF_MEASUREMENT) +from homeassistant.helpers.dispatcher import (async_dispatcher_connect) DEPENDENCIES = ['rflink'] @@ -27,11 +29,10 @@ SENSOR_ICONS = { CONF_SENSOR_TYPE = 'sensor_type' -PLATFORM_SCHEMA = vol.Schema({ - vol.Required(CONF_PLATFORM): DOMAIN, +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_AUTOMATIC_ADD, default=True): cv.boolean, - vol.Optional(CONF_DEVICES, default={}): vol.Schema({ - cv.string: { + vol.Optional(CONF_DEVICES, default={}): { + cv.string: vol.Schema({ vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_SENSOR_TYPE): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, @@ -40,9 +41,9 @@ PLATFORM_SCHEMA = vol.Schema({ # deprecated config options vol.Optional(CONF_ALIASSES): vol.All(cv.ensure_list, [cv.string]), - }, - }), -}) + }) + }, +}, extra=vol.ALLOW_EXTRA) def lookup_unit_for_sensor_type(sensor_type): @@ -56,7 +57,7 @@ def lookup_unit_for_sensor_type(sensor_type): return UNITS.get(field_abbrev.get(sensor_type)) -def devices_from_config(domain_config, hass=None): +def devices_from_config(domain_config): """Parse configuration and add Rflink sensor devices.""" devices = [] for device_id, config in domain_config[CONF_DEVICES].items(): @@ -64,41 +65,26 @@ def devices_from_config(domain_config, hass=None): config[ATTR_UNIT_OF_MEASUREMENT] = lookup_unit_for_sensor_type( config[CONF_SENSOR_TYPE]) remove_deprecated(config) - device = RflinkSensor(device_id, hass, **config) + device = RflinkSensor(device_id, **config) devices.append(device) - # Register entity (and aliases) to listen to incoming rflink events - hass.data[DATA_ENTITY_LOOKUP][ - EVENT_KEY_SENSOR][device_id].append(device) - aliases = config.get(CONF_ALIASES) - if aliases: - for _id in aliases: - hass.data[DATA_ENTITY_LOOKUP][ - EVENT_KEY_SENSOR][_id].append(device) return devices async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Rflink platform.""" - async_add_entities(devices_from_config(config, hass)) + async_add_entities(devices_from_config(config)) async def add_new_device(event): """Check if device is known, otherwise create device entity.""" device_id = event[EVENT_KEY_ID] - rflinksensor = partial(RflinkSensor, device_id, hass) - device = rflinksensor(event[EVENT_KEY_SENSOR], event[EVENT_KEY_UNIT]) + device = RflinkSensor(device_id, event[EVENT_KEY_SENSOR], + event[EVENT_KEY_UNIT], initial_event=event) # Add device entity async_add_entities([device]) - # Register entity to listen to incoming rflink events - hass.data[DATA_ENTITY_LOOKUP][ - EVENT_KEY_SENSOR][device_id].append(device) - - # Schedule task to process event after entity is created - hass.async_add_job(device.handle_event, event) - if config[CONF_AUTOMATIC_ADD]: hass.data[DATA_DEVICE_REGISTER][EVENT_KEY_SENSOR] = add_new_device @@ -106,17 +92,43 @@ async def async_setup_platform(hass, config, async_add_entities, class RflinkSensor(RflinkDevice): """Representation of a Rflink sensor.""" - def __init__(self, device_id, hass, sensor_type, unit_of_measurement, - **kwargs): + def __init__(self, device_id, sensor_type, unit_of_measurement, + initial_event=None, **kwargs): """Handle sensor specific args and super init.""" self._sensor_type = sensor_type self._unit_of_measurement = unit_of_measurement - super().__init__(device_id, hass, **kwargs) + super().__init__(device_id, initial_event=initial_event, **kwargs) def _handle_event(self, event): """Domain specific event handler.""" self._state = event['value'] + async def async_added_to_hass(self): + """Register update callback.""" + # Remove temporary bogus entity_id if added + tmp_entity = TMP_ENTITY.format(self._device_id) + if tmp_entity in self.hass.data[DATA_ENTITY_LOOKUP][ + EVENT_KEY_SENSOR][self._device_id]: + self.hass.data[DATA_ENTITY_LOOKUP][ + EVENT_KEY_SENSOR][self._device_id].remove(tmp_entity) + + # Register id and aliases + self.hass.data[DATA_ENTITY_LOOKUP][ + EVENT_KEY_SENSOR][self._device_id].append(self.entity_id) + if self._aliases: + for _id in self._aliases: + self.hass.data[DATA_ENTITY_LOOKUP][ + EVENT_KEY_SENSOR][_id].append(self.entity_id) + async_dispatcher_connect(self.hass, SIGNAL_AVAILABILITY, + self._availability_callback) + async_dispatcher_connect(self.hass, + SIGNAL_HANDLE_EVENT.format(self.entity_id), + self.handle_event_callback) + + # Process the initial event now that the entity is created + if self._initial_event: + self.handle_event_callback(self._initial_event) + @property def unit_of_measurement(self): """Return measurement unit.""" diff --git a/homeassistant/components/switch/rflink.py b/homeassistant/components/switch/rflink.py index 2bbe3e3f03d..1f217b1c39c 100644 --- a/homeassistant/components/switch/rflink.py +++ b/homeassistant/components/switch/rflink.py @@ -10,23 +10,22 @@ from homeassistant.components.rflink import ( CONF_ALIASES, CONF_ALIASSES, CONF_DEVICE_DEFAULTS, CONF_DEVICES, CONF_FIRE_EVENT, CONF_GROUP, CONF_GROUP_ALIASES, CONF_GROUP_ALIASSES, CONF_NOGROUP_ALIASES, CONF_NOGROUP_ALIASSES, CONF_SIGNAL_REPETITIONS, - DATA_ENTITY_GROUP_LOOKUP, DATA_ENTITY_LOOKUP, DEVICE_DEFAULTS_SCHEMA, - DOMAIN, EVENT_KEY_COMMAND, SwitchableRflinkDevice, cv, remove_deprecated, - vol) -from homeassistant.components.switch import SwitchDevice -from homeassistant.const import CONF_NAME, CONF_PLATFORM -from homeassistant.helpers.deprecation import get_deprecated + DEVICE_DEFAULTS_SCHEMA, SwitchableRflinkDevice, cv, + remove_deprecated, vol) +from homeassistant.components.switch import ( + PLATFORM_SCHEMA, SwitchDevice) + +from homeassistant.const import CONF_NAME DEPENDENCIES = ['rflink'] _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = vol.Schema({ - vol.Required(CONF_PLATFORM): DOMAIN, +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_DEVICE_DEFAULTS, default=DEVICE_DEFAULTS_SCHEMA({})): - DEVICE_DEFAULTS_SCHEMA, - vol.Optional(CONF_DEVICES, default={}): vol.Schema({ - cv.string: { + DEVICE_DEFAULTS_SCHEMA, + vol.Optional(CONF_DEVICES, default={}): { + cv.string: vol.Schema({ vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_ALIASES, default=[]): vol.All(cv.ensure_list, [cv.string]), @@ -44,50 +43,27 @@ PLATFORM_SCHEMA = vol.Schema({ vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_NOGROUP_ALIASSES): vol.All(cv.ensure_list, [cv.string]), - }, - }), -}) + }) + }, +}, extra=vol.ALLOW_EXTRA) -def devices_from_config(domain_config, hass=None): +def devices_from_config(domain_config): """Parse configuration and add Rflink switch devices.""" devices = [] for device_id, config in domain_config[CONF_DEVICES].items(): device_config = dict(domain_config[CONF_DEVICE_DEFAULTS], **config) remove_deprecated(device_config) - device = RflinkSwitch(device_id, hass, **device_config) + device = RflinkSwitch(device_id, **device_config) devices.append(device) - # Register entity (and aliases) to listen to incoming rflink events - # Device id and normal aliases respond to normal and group command - hass.data[DATA_ENTITY_LOOKUP][ - EVENT_KEY_COMMAND][device_id].append(device) - if config[CONF_GROUP]: - hass.data[DATA_ENTITY_GROUP_LOOKUP][ - EVENT_KEY_COMMAND][device_id].append(device) - for _id in get_deprecated(config, CONF_ALIASES, CONF_ALIASSES): - hass.data[DATA_ENTITY_LOOKUP][ - EVENT_KEY_COMMAND][_id].append(device) - hass.data[DATA_ENTITY_GROUP_LOOKUP][ - EVENT_KEY_COMMAND][_id].append(device) - # group_aliases only respond to group commands - for _id in get_deprecated( - config, CONF_GROUP_ALIASES, CONF_GROUP_ALIASSES): - hass.data[DATA_ENTITY_GROUP_LOOKUP][ - EVENT_KEY_COMMAND][_id].append(device) - # nogroup_aliases only respond to normal commands - for _id in get_deprecated( - config, CONF_NOGROUP_ALIASES, CONF_NOGROUP_ALIASSES): - hass.data[DATA_ENTITY_LOOKUP][ - EVENT_KEY_COMMAND][_id].append(device) - return devices async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Rflink platform.""" - async_add_entities(devices_from_config(config, hass)) + async_add_entities(devices_from_config(config)) class RflinkSwitch(SwitchableRflinkDevice, SwitchDevice): From 6feacbbfe15d6cd8d2fb3fd6b397bdaa3a141434 Mon Sep 17 00:00:00 2001 From: Dougal Matthews Date: Mon, 15 Oct 2018 18:11:12 +0100 Subject: [PATCH 168/265] Include the name of the Volumio media player in errors (#17481) When you have multiple Volumio media players it can be hard to determine which one has a problem without this information. --- homeassistant/components/media_player/volumio.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/volumio.py b/homeassistant/components/media_player/volumio.py index de0f726c2ce..686bfe17a4c 100644 --- a/homeassistant/components/media_player/volumio.py +++ b/homeassistant/components/media_player/volumio.py @@ -113,7 +113,9 @@ class Volumio(MediaPlayerDevice): return False except (asyncio.TimeoutError, aiohttp.ClientError) as error: - _LOGGER.error("Failed communicating with Volumio: %s", type(error)) + _LOGGER.error( + "Failed communicating with Volumio '%s': %s", + self._name, type(error)) return False try: From 9718a17351d005d55d71cfc9c71cdd981f453794 Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Mon, 15 Oct 2018 20:34:03 +0200 Subject: [PATCH 169/265] Fix HomeMatic availability detection (#17341) * Fix availability detection --- homeassistant/components/homematic/__init__.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index c87e2926ae8..d723f1fab28 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -798,11 +798,8 @@ class HMDevice(Entity): has_changed = True # Availability has changed - if attribute == 'UNREACH': - self._available = not bool(value) - has_changed = True - elif not self.available: - self._available = False + if self.available != (not self._hmdevice.UNREACH): + self._available = not self._hmdevice.UNREACH has_changed = True # If it has changed data point, update HASS @@ -812,7 +809,6 @@ class HMDevice(Entity): def _subscribe_homematic_events(self): """Subscribe all required events to handle job.""" channels_to_sub = set() - channels_to_sub.add(0) # Add channel 0 for UNREACH # Push data to channels_to_sub from hmdevice metadata for metadata in (self._hmdevice.SENSORNODE, self._hmdevice.BINARYNODE, From 1a5048baaf0f2f29dab7f9b75b3c259e183467fe Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Mon, 15 Oct 2018 20:46:33 +0200 Subject: [PATCH 170/265] Add device info for LIFX (#17330) * Add device info for LIFX * Address review comments --- homeassistant/components/light/lifx.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index 87b3b02dd16..49f2c56826f 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -27,6 +27,7 @@ from homeassistant.components.lifx import ( from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +import homeassistant.helpers.device_registry as dr from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.service import extract_entity_ids import homeassistant.util.color as color_util @@ -397,6 +398,26 @@ class LIFXLight(Light): self.postponed_update = None self.lock = asyncio.Lock() + @property + def device_info(self): + """Return information about the device.""" + info = { + 'identifiers': { + (LIFX_DOMAIN, self.unique_id) + }, + 'name': self.name, + 'connections': { + (dr.CONNECTION_NETWORK_MAC, self.bulb.mac_addr) + }, + 'manufacturer': 'LIFX', + } + + model = aiolifx().products.product_map.get(self.bulb.product) + if model is not None: + info['model'] = model + + return info + @property def available(self): """Return the availability of the bulb.""" From 73197c9a6c9ab722bac718ba9d3aefd4ad50cf91 Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Mon, 15 Oct 2018 21:15:26 +0200 Subject: [PATCH 171/265] Update pyhomematic to 0.1.51 (#17491) --- homeassistant/components/homematic/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index d723f1fab28..4343bcfbc08 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -18,7 +18,7 @@ from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyhomematic==0.1.50'] +REQUIREMENTS = ['pyhomematic==0.1.51'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index e0b6f561c53..d693b50b040 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -917,7 +917,7 @@ pyhik==0.1.8 pyhiveapi==0.2.14 # homeassistant.components.homematic -pyhomematic==0.1.50 +pyhomematic==0.1.51 # homeassistant.components.sensor.hydroquebec pyhydroquebec==2.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 57d9d5d871f..cdc597456e9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,7 +155,7 @@ pydeconz==47 pydispatcher==2.0.5 # homeassistant.components.homematic -pyhomematic==0.1.50 +pyhomematic==0.1.51 # homeassistant.components.litejet pylitejet==0.1 From 29c2b2fe79c9b6f26157e65d3f9cf036f138d0fd Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 15 Oct 2018 13:21:21 -0600 Subject: [PATCH 172/265] Clean up OpenUV config flow (#17349) * Cleaned up OpenUV config flow * Added proper listener removal * Added proper exception * Unnecessary exception message * Moved API key error to correct place * Member-requested changes (part 1) * Hound * Member-requested changes (part 2) * Cleanup * Fixed tests --- .../components/binary_sensor/openuv.py | 22 +++-- homeassistant/components/openuv/__init__.py | 94 +++++++++---------- .../components/openuv/config_flow.py | 76 +++++++++------ homeassistant/components/openuv/const.py | 3 + homeassistant/components/sensor/openuv.py | 22 +++-- tests/components/openuv/test_config_flow.py | 66 ++++++++----- 6 files changed, 158 insertions(+), 125 deletions(-) diff --git a/homeassistant/components/binary_sensor/openuv.py b/homeassistant/components/binary_sensor/openuv.py index bd6e4d1d5dc..3e9bb0b0bc3 100644 --- a/homeassistant/components/binary_sensor/openuv.py +++ b/homeassistant/components/binary_sensor/openuv.py @@ -50,12 +50,12 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorDevice): """Initialize the sensor.""" super().__init__(openuv) + self._async_unsub_dispatcher_connect = None self._entry_id = entry_id self._icon = icon self._latitude = openuv.client.latitude self._longitude = openuv.client.longitude self._name = name - self._dispatch_remove = None self._sensor_type = sensor_type self._state = None @@ -80,16 +80,20 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorDevice): return '{0}_{1}_{2}'.format( self._latitude, self._longitude, self._sensor_type) - @callback - def _update_data(self): - """Update the state.""" - self.async_schedule_update_ha_state(True) - async def async_added_to_hass(self): """Register callbacks.""" - self._dispatch_remove = async_dispatcher_connect( - self.hass, TOPIC_UPDATE, self._update_data) - self.async_on_remove(self._dispatch_remove) + @callback + def update(): + """Update the state.""" + self.async_schedule_update_ha_state(True) + + self._async_unsub_dispatcher_connect = async_dispatcher_connect( + self.hass, TOPIC_UPDATE, update) + + async def async_will_remove_from_hass(self): + """Disconnect dispatcher listener when removed.""" + if self._async_unsub_dispatcher_connect: + self._async_unsub_dispatcher_connect() async def async_update(self): """Update the state.""" diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index 8485e1e3201..a45d9ceb0d6 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -14,13 +14,14 @@ from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_API_KEY, CONF_BINARY_SENSORS, CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_MONITORED_CONDITIONS, CONF_SCAN_INTERVAL, CONF_SENSORS) +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval from .config_flow import configured_instances -from .const import DOMAIN +from .const import DEFAULT_SCAN_INTERVAL, DOMAIN REQUIREMENTS = ['pyopenuv==1.0.4'] _LOGGER = logging.getLogger(__name__) @@ -31,7 +32,6 @@ DATA_PROTECTION_WINDOW = 'protection_window' DATA_UV = 'uv' DEFAULT_ATTRIBUTION = 'Data provided by OpenUV' -DEFAULT_SCAN_INTERVAL = timedelta(minutes=30) NOTIFICATION_ID = 'openuv_notification' NOTIFICATION_TITLE = 'OpenUV Component Setup' @@ -85,18 +85,17 @@ SENSOR_SCHEMA = vol.Schema({ }) CONFIG_SCHEMA = vol.Schema({ - DOMAIN: - vol.Schema({ - vol.Required(CONF_API_KEY): cv.string, - vol.Optional(CONF_ELEVATION): float, - vol.Optional(CONF_LATITUDE): cv.latitude, - vol.Optional(CONF_LONGITUDE): cv.longitude, - vol.Optional(CONF_BINARY_SENSORS, default={}): - BINARY_SENSOR_SCHEMA, - vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, - vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): - cv.time_period, - }) + DOMAIN: vol.Schema({ + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_ELEVATION): float, + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_BINARY_SENSORS, default={}): + BINARY_SENSOR_SCHEMA, + vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): + cv.time_period, + }) }, extra=vol.ALLOW_EXTRA) @@ -110,27 +109,26 @@ async def async_setup(hass, config): return True conf = config[DOMAIN] - latitude = conf.get(CONF_LATITUDE, hass.config.latitude) - longitude = conf.get(CONF_LONGITUDE, hass.config.longitude) - elevation = conf.get(CONF_ELEVATION, hass.config.elevation) + latitude = conf.get(CONF_LATITUDE) + longitude = conf.get(CONF_LONGITUDE) identifier = '{0}, {1}'.format(latitude, longitude) + if identifier in configured_instances(hass): + return True - if identifier not in configured_instances(hass): - hass.async_add_job( - hass.config_entries.flow.async_init( - DOMAIN, - context={'source': SOURCE_IMPORT}, - data={ - CONF_API_KEY: conf[CONF_API_KEY], - CONF_LATITUDE: latitude, - CONF_LONGITUDE: longitude, - CONF_ELEVATION: elevation, - CONF_BINARY_SENSORS: conf[CONF_BINARY_SENSORS], - CONF_SENSORS: conf[CONF_SENSORS], - })) - - hass.data[DOMAIN][CONF_SCAN_INTERVAL] = conf[CONF_SCAN_INTERVAL] + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={'source': SOURCE_IMPORT}, + data={ + CONF_API_KEY: conf[CONF_API_KEY], + CONF_LATITUDE: latitude, + CONF_LONGITUDE: longitude, + CONF_ELEVATION: conf.get(CONF_ELEVATION), + CONF_BINARY_SENSORS: conf[CONF_BINARY_SENSORS], + CONF_SENSORS: conf[CONF_SENSORS], + CONF_SCAN_INTERVAL: conf[CONF_SCAN_INTERVAL], + })) return True @@ -145,11 +143,10 @@ async def async_setup_entry(hass, config_entry): openuv = OpenUV( Client( config_entry.data[CONF_API_KEY], - config_entry.data.get(CONF_LATITUDE, hass.config.latitude), - config_entry.data.get(CONF_LONGITUDE, hass.config.longitude), + config_entry.data[CONF_LATITUDE], + config_entry.data[CONF_LONGITUDE], websession, - altitude=config_entry.data.get( - CONF_ELEVATION, hass.config.elevation)), + altitude=config_entry.data[CONF_ELEVATION]), config_entry.data.get(CONF_BINARY_SENSORS, {}).get( CONF_MONITORED_CONDITIONS, list(BINARY_SENSORS)), config_entry.data.get(CONF_SENSORS, {}).get( @@ -157,20 +154,14 @@ async def async_setup_entry(hass, config_entry): await openuv.async_update() hass.data[DOMAIN][DATA_OPENUV_CLIENT][config_entry.entry_id] = openuv except OpenUvError as err: - _LOGGER.error('An error occurred: %s', str(err)) - hass.components.persistent_notification.create( - 'Error: {0}
' - 'You will need to restart hass after fixing.' - ''.format(err), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) - return False + _LOGGER.error('Config entry failed: %s', err) + raise ConfigEntryNotReady for component in ('binary_sensor', 'sensor'): hass.async_create_task(hass.config_entries.async_forward_entry_setup( config_entry, component)) - async def refresh_sensors(event_time): + async def refresh(event_time): """Refresh OpenUV data.""" _LOGGER.debug('Refreshing OpenUV data') await openuv.async_update() @@ -178,24 +169,25 @@ async def async_setup_entry(hass, config_entry): hass.data[DOMAIN][DATA_OPENUV_LISTENER][ config_entry.entry_id] = async_track_time_interval( - hass, refresh_sensors, - hass.data[DOMAIN].get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)) + hass, + refresh, + timedelta(seconds=config_entry.data[CONF_SCAN_INTERVAL])) return True async def async_unload_entry(hass, config_entry): """Unload an OpenUV config entry.""" - for component in ('binary_sensor', 'sensor'): - await hass.config_entries.async_forward_entry_unload( - config_entry, component) - hass.data[DOMAIN][DATA_OPENUV_CLIENT].pop(config_entry.entry_id) remove_listener = hass.data[DOMAIN][DATA_OPENUV_LISTENER].pop( config_entry.entry_id) remove_listener() + for component in ('binary_sensor', 'sensor'): + await hass.config_entries.async_forward_entry_unload( + config_entry, component) + return True diff --git a/homeassistant/components/openuv/config_flow.py b/homeassistant/components/openuv/config_flow.py index 6d7ae0f65bd..27ffe5c3985 100644 --- a/homeassistant/components/openuv/config_flow.py +++ b/homeassistant/components/openuv/config_flow.py @@ -1,16 +1,15 @@ """Config flow to configure the OpenUV component.""" -from collections import OrderedDict - import voluptuous as vol from homeassistant import config_entries from homeassistant.core import callback from homeassistant.const import ( - CONF_API_KEY, CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE) + CONF_API_KEY, CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, + CONF_SCAN_INTERVAL) from homeassistant.helpers import aiohttp_client, config_validation as cv -from .const import DOMAIN +from .const import DEFAULT_SCAN_INTERVAL, DOMAIN @callback @@ -33,6 +32,24 @@ class OpenUvFlowHandler(config_entries.ConfigFlow): """Initialize the config flow.""" pass + async def _show_form(self, errors=None): + """Show the form to the user.""" + data_schema = vol.Schema({ + vol.Required(CONF_API_KEY): str, + vol.Optional(CONF_LATITUDE, default=self.hass.config.latitude): + cv.latitude, + vol.Optional(CONF_LONGITUDE, default=self.hass.config.longitude): + cv.longitude, + vol.Optional(CONF_ELEVATION, default=self.hass.config.elevation): + vol.Coerce(float), + }) + + return self.async_show_form( + step_id='user', + data_schema=data_schema, + errors=errors if errors else {}, + ) + async def async_step_import(self, import_config): """Import a config entry from configuration.yaml.""" return await self.async_step_user(import_config) @@ -41,34 +58,31 @@ class OpenUvFlowHandler(config_entries.ConfigFlow): """Handle the start of the config flow.""" from pyopenuv.util import validate_api_key - errors = {} + if not user_input: + return await self._show_form() - if user_input is not None: - identifier = '{0}, {1}'.format( - user_input.get(CONF_LATITUDE, self.hass.config.latitude), - user_input.get(CONF_LONGITUDE, self.hass.config.longitude)) + latitude = user_input[CONF_LATITUDE] + longitude = user_input[CONF_LONGITUDE] + elevation = user_input[CONF_ELEVATION] - if identifier in configured_instances(self.hass): - errors['base'] = 'identifier_exists' - else: - websession = aiohttp_client.async_get_clientsession(self.hass) - api_key_validation = await validate_api_key( - user_input[CONF_API_KEY], websession) - if api_key_validation: - return self.async_create_entry( - title=identifier, - data=user_input, - ) - errors['base'] = 'invalid_api_key' + identifier = '{0}, {1}'.format(latitude, longitude) + if identifier in configured_instances(self.hass): + return await self._show_form({CONF_LATITUDE: 'identifier_exists'}) - data_schema = OrderedDict() - data_schema[vol.Required(CONF_API_KEY)] = str - data_schema[vol.Optional(CONF_LATITUDE)] = cv.latitude - data_schema[vol.Optional(CONF_LONGITUDE)] = cv.longitude - data_schema[vol.Optional(CONF_ELEVATION)] = vol.Coerce(float) + websession = aiohttp_client.async_get_clientsession(self.hass) + api_key_validation = await validate_api_key( + user_input[CONF_API_KEY], websession) - return self.async_show_form( - step_id='user', - data_schema=vol.Schema(data_schema), - errors=errors, - ) + if not api_key_validation: + return await self._show_form({CONF_API_KEY: 'invalid_api_key'}) + + scan_interval = user_input.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + user_input.update({ + CONF_LATITUDE: latitude, + CONF_LONGITUDE: longitude, + CONF_ELEVATION: elevation, + CONF_SCAN_INTERVAL: scan_interval.seconds, + }) + + return self.async_create_entry(title=identifier, data=user_input) diff --git a/homeassistant/components/openuv/const.py b/homeassistant/components/openuv/const.py index 1aa3d2abcaa..16623e45642 100644 --- a/homeassistant/components/openuv/const.py +++ b/homeassistant/components/openuv/const.py @@ -1,3 +1,6 @@ """Define constants for the OpenUV component.""" +from datetime import timedelta DOMAIN = 'openuv' + +DEFAULT_SCAN_INTERVAL = timedelta(minutes=30) diff --git a/homeassistant/components/sensor/openuv.py b/homeassistant/components/sensor/openuv.py index 22712aa306b..63527db42a6 100644 --- a/homeassistant/components/sensor/openuv.py +++ b/homeassistant/components/sensor/openuv.py @@ -64,7 +64,7 @@ class OpenUvSensor(OpenUvEntity): """Initialize the sensor.""" super().__init__(openuv) - self._dispatch_remove = None + self._async_unsub_dispatcher_connect = None self._entry_id = entry_id self._icon = icon self._latitude = openuv.client.latitude @@ -100,16 +100,20 @@ class OpenUvSensor(OpenUvEntity): """Return the unit the value is expressed in.""" return self._unit - @callback - def _update_data(self): - """Update the state.""" - self.async_schedule_update_ha_state(True) - async def async_added_to_hass(self): """Register callbacks.""" - self._dispatch_remove = async_dispatcher_connect( - self.hass, TOPIC_UPDATE, self._update_data) - self.async_on_remove(self._dispatch_remove) + @callback + def update(): + """Update the state.""" + self.async_schedule_update_ha_state(True) + + self._async_unsub_dispatcher_connect = async_dispatcher_connect( + self.hass, TOPIC_UPDATE, update) + + async def async_will_remove_from_hass(self): + """Disconnect dispatcher listener when removed.""" + if self._async_unsub_dispatcher_connect: + self._async_unsub_dispatcher_connect() async def async_update(self): """Update the state.""" diff --git a/tests/components/openuv/test_config_flow.py b/tests/components/openuv/test_config_flow.py index 07946fbbc09..60aa990333f 100644 --- a/tests/components/openuv/test_config_flow.py +++ b/tests/components/openuv/test_config_flow.py @@ -1,10 +1,12 @@ """Define tests for the OpenUV config flow.""" +from datetime import timedelta from unittest.mock import patch from homeassistant import data_entry_flow from homeassistant.components.openuv import DOMAIN, config_flow from homeassistant.const import ( - CONF_API_KEY, CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE) + CONF_API_KEY, CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, + CONF_SCAN_INTERVAL) from tests.common import MockConfigEntry, mock_coro @@ -23,7 +25,7 @@ async def test_duplicate_error(hass): flow.hass = hass result = await flow.async_step_user(user_input=conf) - assert result['errors'] == {'base': 'identifier_exists'} + assert result['errors'] == {CONF_LATITUDE: 'identifier_exists'} async def test_invalid_api_key(hass): @@ -41,7 +43,7 @@ async def test_invalid_api_key(hass): with patch('pyopenuv.util.validate_api_key', return_value=mock_coro(False)): result = await flow.async_step_user(user_input=conf) - assert result['errors'] == {'base': 'invalid_api_key'} + assert result['errors'] == {CONF_API_KEY: 'invalid_api_key'} async def test_show_form(hass): @@ -57,25 +59,6 @@ async def test_show_form(hass): async def test_step_import(hass): """Test that the import step works.""" - conf = { - CONF_API_KEY: '12345abcde', - } - - flow = config_flow.OpenUvFlowHandler() - flow.hass = hass - - with patch('pyopenuv.util.validate_api_key', - return_value=mock_coro(True)): - result = await flow.async_step_import(import_config=conf) - - assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result['title'] == '{0}, {1}'.format( - hass.config.latitude, hass.config.longitude) - assert result['data'] == conf - - -async def test_step_user(hass): - """Test that the user step works.""" conf = { CONF_API_KEY: '12345abcde', CONF_ELEVATION: 59.1234, @@ -86,11 +69,44 @@ async def test_step_user(hass): flow = config_flow.OpenUvFlowHandler() flow.hass = hass + with patch('pyopenuv.util.validate_api_key', + return_value=mock_coro(True)): + result = await flow.async_step_import(import_config=conf) + + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result['title'] == '39.128712, -104.9812612' + assert result['data'] == { + CONF_API_KEY: '12345abcde', + CONF_ELEVATION: 59.1234, + CONF_LATITUDE: 39.128712, + CONF_LONGITUDE: -104.9812612, + CONF_SCAN_INTERVAL: 1800, + } + + +async def test_step_user(hass): + """Test that the user step works.""" + conf = { + CONF_API_KEY: '12345abcde', + CONF_ELEVATION: 59.1234, + CONF_LATITUDE: 39.128712, + CONF_LONGITUDE: -104.9812612, + CONF_SCAN_INTERVAL: timedelta(minutes=5) + } + + flow = config_flow.OpenUvFlowHandler() + flow.hass = hass + with patch('pyopenuv.util.validate_api_key', return_value=mock_coro(True)): result = await flow.async_step_user(user_input=conf) assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result['title'] == '{0}, {1}'.format( - conf[CONF_LATITUDE], conf[CONF_LONGITUDE]) - assert result['data'] == conf + assert result['title'] == '39.128712, -104.9812612' + assert result['data'] == { + CONF_API_KEY: '12345abcde', + CONF_ELEVATION: 59.1234, + CONF_LATITUDE: 39.128712, + CONF_LONGITUDE: -104.9812612, + CONF_SCAN_INTERVAL: 300, + } From 993a02b8c4f131003dbf7bd335ee3bb8b538cd80 Mon Sep 17 00:00:00 2001 From: Alexander Lyon Date: Mon, 15 Oct 2018 23:18:59 +0100 Subject: [PATCH 173/265] Fix the sabnzbd component api error (#17014) * Bump pysabnzbd version number * Pass hass aiohttp session to pysabnzbd --- homeassistant/components/sabnzbd.py | 9 ++++++--- requirements_all.txt | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sabnzbd.py b/homeassistant/components/sabnzbd.py index 380867a3285..25fce22c641 100644 --- a/homeassistant/components/sabnzbd.py +++ b/homeassistant/components/sabnzbd.py @@ -15,11 +15,12 @@ from homeassistant.const import ( CONF_HOST, CONF_API_KEY, CONF_NAME, CONF_PORT, CONF_SENSORS, CONF_SSL) from homeassistant.core import callback from homeassistant.helpers import discovery +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval from homeassistant.util.json import load_json, save_json -REQUIREMENTS = ['pysabnzbd==1.0.1'] +REQUIREMENTS = ['pysabnzbd==1.1.0'] _LOGGER = logging.getLogger(__name__) @@ -102,7 +103,8 @@ async def async_configure_sabnzbd(hass, config, use_ssl, name=DEFAULT_NAME, hass.config.path(CONFIG_FILE)) api_key = conf.get(base_url, {}).get(CONF_API_KEY, '') - sab_api = SabnzbdApi(base_url, api_key) + sab_api = SabnzbdApi(base_url, api_key, + session=async_get_clientsession(hass)) if await async_check_sabnzbd(sab_api): async_setup_sabnzbd(hass, sab_api, config, name) else: @@ -188,7 +190,8 @@ def async_request_configuration(hass, config, host): async def async_configuration_callback(data): """Handle configuration changes.""" api_key = data.get(CONF_API_KEY) - sab_api = SabnzbdApi(host, api_key) + sab_api = SabnzbdApi(host, api_key, + session=async_get_clientsession(hass)) if not await async_check_sabnzbd(sab_api): return diff --git a/requirements_all.txt b/requirements_all.txt index d693b50b040..70106787f61 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1057,7 +1057,7 @@ pyrainbird==0.1.6 pyrecswitch==1.0.2 # homeassistant.components.sabnzbd -pysabnzbd==1.0.1 +pysabnzbd==1.1.0 # homeassistant.components.climate.sensibo pysensibo==1.0.3 From a9389d2d43006e7cdbe9cf78d10fd66ba9df17e6 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 15 Oct 2018 22:54:32 -0600 Subject: [PATCH 174/265] Bumps simplisafe-python to 3.1.12 (#17509) * Bumps simplisafe-python to 3.1.12 * Updated requirements --- homeassistant/components/simplisafe/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index df7fe6beda7..de6277c2ef1 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -23,7 +23,7 @@ from homeassistant.helpers import config_validation as cv from .config_flow import configured_instances from .const import DATA_CLIENT, DEFAULT_SCAN_INTERVAL, DOMAIN, TOPIC_UPDATE -REQUIREMENTS = ['simplisafe-python==3.1.11'] +REQUIREMENTS = ['simplisafe-python==3.1.12'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 70106787f61..3a488d16414 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1346,7 +1346,7 @@ shodan==1.10.4 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==3.1.11 +simplisafe-python==3.1.12 # homeassistant.components.sisyphus sisyphus-control==2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cdc597456e9..51f6de977ef 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -216,7 +216,7 @@ ring_doorbell==0.2.1 rxv==0.5.1 # homeassistant.components.simplisafe -simplisafe-python==3.1.11 +simplisafe-python==3.1.12 # homeassistant.components.sleepiq sleepyq==0.6 From dc7e5e3af48cfc34757bce17ae915ab1cecd95d2 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Tue, 16 Oct 2018 01:06:00 -0700 Subject: [PATCH 175/265] Add unique_id for Ring (#17497) --- homeassistant/components/binary_sensor/ring.py | 6 ++++++ homeassistant/components/camera/ring.py | 5 +++++ homeassistant/components/sensor/ring.py | 6 ++++++ 3 files changed, 17 insertions(+) diff --git a/homeassistant/components/binary_sensor/ring.py b/homeassistant/components/binary_sensor/ring.py index 3945eb5c926..5a65917f40b 100644 --- a/homeassistant/components/binary_sensor/ring.py +++ b/homeassistant/components/binary_sensor/ring.py @@ -73,6 +73,7 @@ class RingBinarySensor(BinarySensorDevice): SENSOR_TYPES.get(self._sensor_type)[0]) self._device_class = SENSOR_TYPES.get(self._sensor_type)[2] self._state = None + self._unique_id = '{}-{}'.format(self._data.id, self._sensor_type) @property def name(self): @@ -89,6 +90,11 @@ class RingBinarySensor(BinarySensorDevice): """Return the class of the binary sensor.""" return self._device_class + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id + @property def device_state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/camera/ring.py b/homeassistant/components/camera/ring.py index ae886bd0669..eafb3066e48 100644 --- a/homeassistant/components/camera/ring.py +++ b/homeassistant/components/camera/ring.py @@ -97,6 +97,11 @@ class RingCam(Camera): """Return the name of this camera.""" return self._name + @property + def unique_id(self): + """Return a unique ID.""" + return self._camera.id + @property def device_state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/sensor/ring.py b/homeassistant/components/sensor/ring.py index 408971c60e1..92c033241e0 100644 --- a/homeassistant/components/sensor/ring.py +++ b/homeassistant/components/sensor/ring.py @@ -100,6 +100,7 @@ class RingSensor(Entity): self._data.name, SENSOR_TYPES.get(self._sensor_type)[0]) self._state = STATE_UNKNOWN self._tz = str(hass.config.time_zone) + self._unique_id = '{}-{}'.format(self._data.id, self._sensor_type) @property def name(self): @@ -111,6 +112,11 @@ class RingSensor(Entity): """Return the state of the sensor.""" return self._state + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id + @property def device_state_attributes(self): """Return the state attributes.""" From 0c0c471447c5a14076ad6463ea412cdd6fbe4eaa Mon Sep 17 00:00:00 2001 From: Nikolay Vasilchuk Date: Tue, 16 Oct 2018 11:22:57 +0300 Subject: [PATCH 176/265] Fix: Connection pool of Request object is smaller than optimal value (8) (#17483) --- homeassistant/components/telegram_bot/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index ddd8337aeda..28bc7a1ad0d 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -309,10 +309,10 @@ def initialize_bot(p_config): proxy_params = p_config.get(CONF_PROXY_PARAMS) if proxy_url is not None: - request = Request(con_pool_size=4, proxy_url=proxy_url, + request = Request(con_pool_size=8, proxy_url=proxy_url, urllib3_proxy_kwargs=proxy_params) else: - request = Request(con_pool_size=4) + request = Request(con_pool_size=8) return Bot(token=api_key, request=request) From a7950937058903808d87256537d40d3b0b7c93f2 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 16 Oct 2018 10:35:35 +0200 Subject: [PATCH 177/265] UniFi POE control (#17011) * First commit * Feature complete? * Add dependency * Move setting poe mode logic to library * Use guard clauses * Bump requirement to 2 * Simplify saving switches with poe off * Store and use poe mode * Fix indentation * Fix flake8 * Configuration future proofing * Bump dependency to v3 * Add first test * Proper use of defaults with config flow (thanks helto) * Appease hound * Make sure there can't be duplicate entries of combination host+site * More tests * More tests * 98% coverage of controller * Fix hound comments * Config flow step init not necessary * Use async_current_entries to check if host and site for controller is used * Remove storing/restoring poe off devices to slim PR * First batch of switch tests * More switch tests. * Small improvements and clean up * Make tests pass Don't name device in device registry * Dont process clients that belong to non-UniFi POE switches * Allow selection of site from a list in config flow * Fix double blank lines in method * Update codeowners --- CODEOWNERS | 2 + homeassistant/components/switch/unifi.py | 230 ++++++++++++ .../components/unifi/.translations/en.json | 26 ++ homeassistant/components/unifi/__init__.py | 186 ++++++++++ homeassistant/components/unifi/const.py | 12 + homeassistant/components/unifi/controller.py | 131 +++++++ homeassistant/components/unifi/errors.py | 26 ++ homeassistant/components/unifi/strings.json | 26 ++ homeassistant/config_entries.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/switch/test_unifi.py | 345 ++++++++++++++++++ tests/components/unifi/__init__.py | 1 + tests/components/unifi/test_controller.py | 266 ++++++++++++++ tests/components/unifi/test_init.py | 330 +++++++++++++++++ 16 files changed, 1589 insertions(+) create mode 100644 homeassistant/components/switch/unifi.py create mode 100644 homeassistant/components/unifi/.translations/en.json create mode 100644 homeassistant/components/unifi/__init__.py create mode 100644 homeassistant/components/unifi/const.py create mode 100644 homeassistant/components/unifi/controller.py create mode 100644 homeassistant/components/unifi/errors.py create mode 100644 homeassistant/components/unifi/strings.json create mode 100644 tests/components/switch/test_unifi.py create mode 100644 tests/components/unifi/__init__.py create mode 100644 tests/components/unifi/test_controller.py create mode 100644 tests/components/unifi/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index 9343407f06f..40a33c66b78 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -224,6 +224,8 @@ homeassistant/components/tradfri/* @ggravlingen homeassistant/components/*/tradfri.py @ggravlingen # U +homeassistant/components/unifi.py @kane610 +homeassistant/components/switch/unifi.py @kane610 homeassistant/components/upcloud.py @scop homeassistant/components/*/upcloud.py @scop diff --git a/homeassistant/components/switch/unifi.py b/homeassistant/components/switch/unifi.py new file mode 100644 index 00000000000..dc02068c4a8 --- /dev/null +++ b/homeassistant/components/switch/unifi.py @@ -0,0 +1,230 @@ +""" +Support for devices connected to UniFi POE. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.unifi/ +""" + +import asyncio +import logging + +from datetime import timedelta + +import async_timeout + +from homeassistant.components import unifi +from homeassistant.components.switch import SwitchDevice +from homeassistant.components.unifi.const import ( + CONF_CONTROLLER, CONF_SITE_ID, CONTROLLER_ID, DOMAIN) +from homeassistant.const import CONF_HOST +from homeassistant.core import callback +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC + +DEPENDENCIES = [DOMAIN] +SCAN_INTERVAL = timedelta(seconds=15) + +LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Component doesn't support configuration through configuration.yaml.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up switches for UniFi component. + + Switches are controlling network switch ports with Poe. + """ + controller_id = CONTROLLER_ID.format( + host=config_entry.data[CONF_CONTROLLER][CONF_HOST], + site=config_entry.data[CONF_CONTROLLER][CONF_SITE_ID] + ) + controller = hass.data[unifi.DOMAIN][controller_id] + switches = {} + + progress = None + update_progress = set() + + async def request_update(object_id): + """Request an update.""" + nonlocal progress + update_progress.add(object_id) + + if progress is not None: + return await progress + + progress = asyncio.ensure_future(update_controller()) + result = await progress + progress = None + update_progress.clear() + return result + + async def update_controller(): + """Update the values of the controller.""" + tasks = [async_update_items( + controller, async_add_entities, request_update, + switches, update_progress + )] + await asyncio.wait(tasks) + + await update_controller() + + +async def async_update_items(controller, async_add_entities, + request_controller_update, switches, + progress_waiting): + """Update POE port state from the controller.""" + import aiounifi + + @callback + def update_switch_state(): + """Tell switches to reload state.""" + for client_id, client in switches.items(): + if client_id not in progress_waiting: + client.async_schedule_update_ha_state() + + try: + with async_timeout.timeout(4): + await controller.api.clients.update() + await controller.api.devices.update() + + except aiounifi.LoginRequired: + try: + with async_timeout.timeout(5): + await controller.api.login() + except (asyncio.TimeoutError, aiounifi.AiounifiException): + if controller.available: + controller.available = False + update_switch_state() + return + + except (asyncio.TimeoutError, aiounifi.AiounifiException): + if controller.available: + LOGGER.error('Unable to reach controller %s', controller.host) + controller.available = False + update_switch_state() + return + + if not controller.available: + LOGGER.info('Reconnected to controller %s', controller.host) + controller.available = True + + new_switches = [] + devices = controller.api.devices + for client_id in controller.api.clients: + + if client_id in progress_waiting: + continue + + if client_id in switches: + LOGGER.debug("Updating UniFi switch %s (%s)", + switches[client_id].entity_id, + switches[client_id].client.mac) + switches[client_id].async_schedule_update_ha_state() + continue + + client = controller.api.clients[client_id] + # Network device with active POE + if not client.is_wired or client.sw_mac not in devices or \ + not devices[client.sw_mac].ports[client.sw_port].port_poe or \ + not devices[client.sw_mac].ports[client.sw_port].poe_enable: + continue + + # Multiple POE-devices on same port means non UniFi POE driven switch + multi_clients_on_port = False + for client2 in controller.api.clients.values(): + if client.mac != client2.mac and \ + client.sw_mac == client2.sw_mac and \ + client.sw_port == client2.sw_port: + multi_clients_on_port = True + break + + if multi_clients_on_port: + continue + + switches[client_id] = UniFiSwitch( + client, controller, request_controller_update) + new_switches.append(switches[client_id]) + LOGGER.debug("New UniFi switch %s (%s)", client.hostname, client.mac) + + if new_switches: + async_add_entities(new_switches) + + +class UniFiSwitch(SwitchDevice): + """Representation of a client that uses POE.""" + + def __init__(self, client, controller, request_controller_update): + """Set up switch.""" + self.client = client + self.controller = controller + self.poe_mode = None + if self.port.poe_mode != 'off': + self.poe_mode = self.port.poe_mode + self.async_request_controller_update = request_controller_update + + async def async_update(self): + """Synchronize state with controller.""" + await self.async_request_controller_update(self.client.mac) + + @property + def name(self): + """Return the name of the switch.""" + return self.client.hostname + + @property + def unique_id(self): + """Return a unique identifier for this switch.""" + return 'poe-{}'.format(self.client.mac) + + @property + def is_on(self): + """Return true if POE is active.""" + return self.port.poe_mode != 'off' + + @property + def available(self): + """Return if switch is available.""" + return self.controller.available or \ + self.client.sw_mac in self.controller.api.devices + + async def async_turn_on(self, **kwargs): + """Enable POE for client.""" + await self.device.async_set_port_poe_mode( + self.client.sw_port, self.poe_mode) + + async def async_turn_off(self, **kwargs): + """Disable POE for client.""" + await self.device.async_set_port_poe_mode(self.client.sw_port, 'off') + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + attributes = { + 'power': self.port.poe_power, + 'received': self.client.wired_rx_bytes / 1000000, + 'sent': self.client.wired_tx_bytes / 1000000, + 'switch': self.client.sw_mac, + 'port': self.client.sw_port, + 'poe_mode': self.poe_mode + } + return attributes + + @property + def device_info(self): + """Return a device description for device registry.""" + return { + 'connections': {(CONNECTION_NETWORK_MAC, self.client.mac)} + } + + @property + def device(self): + """Shortcut to the switch that client is connected to.""" + return self.controller.api.devices[self.client.sw_mac] + + @property + def port(self): + """Shortcut to the switch port that client is connected to.""" + return self.device.ports[self.client.sw_port] diff --git a/homeassistant/components/unifi/.translations/en.json b/homeassistant/components/unifi/.translations/en.json new file mode 100644 index 00000000000..938ac058d22 --- /dev/null +++ b/homeassistant/components/unifi/.translations/en.json @@ -0,0 +1,26 @@ +{ + "config": { + "title": "UniFi Controller", + "step": { + "user": { + "title": "Set up UniFi Controller", + "data": { + "host": "Host", + "username": "User name", + "password": "Password", + "port": "Port", + "site": "Site ID", + "verify_ssl": "Controller using proper certificate" + } + } + }, + "error": { + "faulty_credentials": "Bad user credentials", + "service_unavailable": "No service available" + }, + "abort": { + "already_configured": "Controller site is already configured", + "user_privilege": "User needs to be administrator" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py new file mode 100644 index 00000000000..26b60aecf42 --- /dev/null +++ b/homeassistant/components/unifi/__init__.py @@ -0,0 +1,186 @@ +""" +Support for devices connected to UniFi POE. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/unifi/ +""" + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ( + CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME, CONF_VERIFY_SSL) +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC + +from .const import (CONF_CONTROLLER, CONF_POE_CONTROL, CONF_SITE_ID, + CONTROLLER_ID, DOMAIN, LOGGER) +from .controller import UniFiController, get_controller +from .errors import ( + AlreadyConfigured, AuthenticationRequired, CannotConnect, UserLevel) + +DEFAULT_PORT = 8443 +DEFAULT_SITE_ID = 'default' +DEFAULT_VERIFY_SSL = False + +REQUIREMENTS = ['aiounifi==3'] + + +async def async_setup(hass, config): + """Component doesn't support configuration through configuration.yaml.""" + return True + + +async def async_setup_entry(hass, config_entry): + """Set up the UniFi component.""" + controller = UniFiController(hass, config_entry) + + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} + controller_id = CONTROLLER_ID.format( + host=config_entry.data[CONF_CONTROLLER][CONF_HOST], + site=config_entry.data[CONF_CONTROLLER][CONF_SITE_ID] + ) + + if not await controller.async_setup(): + return False + + hass.data[DOMAIN][controller_id] = controller + + if controller.mac is None: + return True + + device_registry = await \ + hass.helpers.device_registry.async_get_registry() + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(CONNECTION_NETWORK_MAC, controller.mac)}, + manufacturer='Ubiquiti', + model="UniFi Controller", + name="UniFi Controller", + # sw_version=config.raw['swversion'], + ) + + return True + + +async def async_unload_entry(hass, config_entry): + """Unload a config entry.""" + controller_id = CONTROLLER_ID.format( + host=config_entry.data[CONF_CONTROLLER][CONF_HOST], + site=config_entry.data[CONF_CONTROLLER][CONF_SITE_ID] + ) + controller = hass.data[DOMAIN].pop(controller_id) + return await controller.async_reset() + + +@config_entries.HANDLERS.register(DOMAIN) +class UnifiFlowHandler(config_entries.ConfigFlow): + """Handle a UniFi config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialize the UniFi flow.""" + self.config = None + self.desc = None + self.sites = None + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + errors = {} + + if user_input is not None: + + try: + self.config = { + CONF_HOST: user_input[CONF_HOST], + CONF_USERNAME: user_input[CONF_USERNAME], + CONF_PASSWORD: user_input[CONF_PASSWORD], + CONF_PORT: user_input.get(CONF_PORT), + CONF_VERIFY_SSL: user_input.get(CONF_VERIFY_SSL), + CONF_SITE_ID: DEFAULT_SITE_ID, + } + controller = await get_controller(self.hass, **self.config) + + self.sites = await controller.sites() + + return await self.async_step_site() + + except AuthenticationRequired: + errors['base'] = 'faulty_credentials' + + except CannotConnect: + errors['base'] = 'service_unavailable' + + except Exception: # pylint: disable=broad-except + LOGGER.error( + 'Unknown error connecting with UniFi Controller at %s', + user_input[CONF_HOST]) + return self.async_abort(reason='unknown') + + return self.async_show_form( + step_id='user', + data_schema=vol.Schema({ + vol.Required(CONF_HOST): str, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): int, + vol.Optional( + CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): bool, + }), + errors=errors, + ) + + async def async_step_site(self, user_input=None): + """Select site to control.""" + errors = {} + + if user_input is not None: + + try: + desc = user_input.get(CONF_SITE_ID, self.desc) + for site in self.sites.values(): + if desc == site['desc']: + if site['role'] != 'admin': + raise UserLevel + self.config[CONF_SITE_ID] = site['name'] + break + + for entry in self._async_current_entries(): + controller = entry.data[CONF_CONTROLLER] + if controller[CONF_HOST] == self.config[CONF_HOST] and \ + controller[CONF_SITE_ID] == self.config[CONF_SITE_ID]: + raise AlreadyConfigured + + data = { + CONF_CONTROLLER: self.config, + CONF_POE_CONTROL: True + } + + return self.async_create_entry( + title=desc, + data=data + ) + + except AlreadyConfigured: + return self.async_abort(reason='already_configured') + + except UserLevel: + return self.async_abort(reason='user_privilege') + + if len(self.sites) == 1: + self.desc = next(iter(self.sites.values()))['desc'] + return await self.async_step_site(user_input={}) + + sites = [] + for site in self.sites.values(): + sites.append(site['desc']) + + return self.async_show_form( + step_id='site', + data_schema=vol.Schema({ + vol.Required(CONF_SITE_ID): vol.In(sites) + }), + errors=errors, + ) diff --git a/homeassistant/components/unifi/const.py b/homeassistant/components/unifi/const.py new file mode 100644 index 00000000000..7250feec799 --- /dev/null +++ b/homeassistant/components/unifi/const.py @@ -0,0 +1,12 @@ +"""Constants for the UniFi component.""" + +import logging + +LOGGER = logging.getLogger('homeassistant.components.unifi') +DOMAIN = 'unifi' + +CONTROLLER_ID = '{host}-{site}' + +CONF_CONTROLLER = 'controller' +CONF_POE_CONTROL = 'poe_control' +CONF_SITE_ID = 'site' diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py new file mode 100644 index 00000000000..9e21956536f --- /dev/null +++ b/homeassistant/components/unifi/controller.py @@ -0,0 +1,131 @@ +"""UniFi Controller abstraction.""" + +import asyncio +import async_timeout + +from aiohttp import CookieJar + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST +from homeassistant.helpers import aiohttp_client + +from .const import CONF_CONTROLLER, CONF_POE_CONTROL, LOGGER +from .errors import AuthenticationRequired, CannotConnect + + +class UniFiController: + """Manages a single UniFi Controller.""" + + def __init__(self, hass, config_entry): + """Initialize the system.""" + self.hass = hass + self.config_entry = config_entry + self.available = True + self.api = None + self.progress = None + self._cancel_retry_setup = None + + @property + def host(self): + """Return the host of this controller.""" + return self.config_entry.data[CONF_CONTROLLER][CONF_HOST] + + @property + def mac(self): + """Return the mac address of this controller.""" + for client in self.api.clients.values(): + if self.host == client.ip: + return client.mac + return None + + async def async_setup(self, tries=0): + """Set up a UniFi controller.""" + hass = self.hass + + try: + self.api = await get_controller( + self.hass, **self.config_entry.data[CONF_CONTROLLER]) + await self.api.initialize() + + except CannotConnect: + retry_delay = 2 ** (tries + 1) + LOGGER.error("Error connecting to the UniFi controller. Retrying " + "in %d seconds", retry_delay) + + async def retry_setup(_now): + """Retry setup.""" + if await self.async_setup(tries + 1): + # This feels hacky, we should find a better way to do this + self.config_entry.state = config_entries.ENTRY_STATE_LOADED + + self._cancel_retry_setup = hass.helpers.event.async_call_later( + retry_delay, retry_setup) + + return False + + except Exception: # pylint: disable=broad-except + LOGGER.error( + 'Unknown error connecting with UniFi controller.') + return False + + if self.config_entry.data[CONF_POE_CONTROL]: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup( + self.config_entry, 'switch')) + + return True + + async def async_reset(self): + """Reset this controller to default state. + + Will cancel any scheduled setup retry and will unload + the config entry. + """ + # If we have a retry scheduled, we were never setup. + if self._cancel_retry_setup is not None: + self._cancel_retry_setup() + self._cancel_retry_setup = None + return True + + # If the authentication was wrong. + if self.api is None: + return True + + if self.config_entry.data[CONF_POE_CONTROL]: + return await self.hass.config_entries.async_forward_entry_unload( + self.config_entry, 'switch') + return True + + +async def get_controller( + hass, host, username, password, port, site, verify_ssl): + """Create a controller object and verify authentication.""" + import aiounifi + + if verify_ssl: + session = aiohttp_client.async_get_clientsession(hass) + else: + session = aiohttp_client.async_create_clientsession( + hass, verify_ssl=verify_ssl, cookie_jar=CookieJar(unsafe=True)) + + controller = aiounifi.Controller( + host, username=username, password=password, port=port, site=site, + websession=session + ) + + try: + with async_timeout.timeout(5): + await controller.login() + return controller + + except aiounifi.Unauthorized: + LOGGER.warning("Connected to UniFi at %s but not registered.", host) + raise AuthenticationRequired + + except (asyncio.TimeoutError, aiounifi.RequestError): + LOGGER.error("Error connecting to the UniFi controller at %s", host) + raise CannotConnect + + except aiounifi.AiounifiException: + LOGGER.exception('Unknown UniFi communication error occurred') + raise AuthenticationRequired diff --git a/homeassistant/components/unifi/errors.py b/homeassistant/components/unifi/errors.py new file mode 100644 index 00000000000..c90c4956312 --- /dev/null +++ b/homeassistant/components/unifi/errors.py @@ -0,0 +1,26 @@ +"""Errors for the UniFi component.""" +from homeassistant.exceptions import HomeAssistantError + + +class UnifiException(HomeAssistantError): + """Base class for UniFi exceptions.""" + + +class AlreadyConfigured(UnifiException): + """Controller is already configured.""" + + +class AuthenticationRequired(UnifiException): + """Unknown error occurred.""" + + +class CannotConnect(UnifiException): + """Unable to connect to the controller.""" + + +class LoginRequired(UnifiException): + """Component got logged out.""" + + +class UserLevel(UnifiException): + """User level too low.""" diff --git a/homeassistant/components/unifi/strings.json b/homeassistant/components/unifi/strings.json new file mode 100644 index 00000000000..938ac058d22 --- /dev/null +++ b/homeassistant/components/unifi/strings.json @@ -0,0 +1,26 @@ +{ + "config": { + "title": "UniFi Controller", + "step": { + "user": { + "title": "Set up UniFi Controller", + "data": { + "host": "Host", + "username": "User name", + "password": "Password", + "port": "Port", + "site": "Site ID", + "verify_ssl": "Controller using proper certificate" + } + } + }, + "error": { + "faulty_credentials": "Bad user credentials", + "service_unavailable": "No service available" + }, + "abort": { + "already_configured": "Controller site is already configured", + "user_privilege": "User needs to be administrator" + } + } +} \ No newline at end of file diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index a4f28b63fb1..c1c0fbbf775 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -150,6 +150,7 @@ FLOWS = [ 'smhi', 'sonos', 'tradfri', + 'unifi', 'upnp', 'zone', 'zwave' diff --git a/requirements_all.txt b/requirements_all.txt index 3a488d16414..3c2d1df2545 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -118,6 +118,9 @@ aiolifx_effects==0.2.1 # homeassistant.components.scene.hunterdouglas_powerview aiopvapi==1.5.4 +# homeassistant.components.unifi +aiounifi==3 + # homeassistant.components.cover.aladdin_connect aladdin_connect==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 51f6de977ef..9dd81e1a6c9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -40,6 +40,9 @@ aiohttp_cors==0.7.0 # homeassistant.components.hue aiohue==1.5.0 +# homeassistant.components.unifi +aiounifi==3 + # homeassistant.components.notify.apns apns2==0.3.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index fd8d673f633..47d11dff582 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -40,6 +40,7 @@ TEST_REQUIREMENTS = ( 'aioautomatic', 'aiohttp_cors', 'aiohue', + 'aiounifi', 'apns2', 'caldav', 'coinmarketcap', diff --git a/tests/components/switch/test_unifi.py b/tests/components/switch/test_unifi.py new file mode 100644 index 00000000000..f50bda34883 --- /dev/null +++ b/tests/components/switch/test_unifi.py @@ -0,0 +1,345 @@ +"""UniFi POE control platform tests.""" +from collections import deque +from unittest.mock import Mock + +import pytest + +import aiounifi +from aiounifi.clients import Clients +from aiounifi.devices import Devices + +from homeassistant import config_entries +from homeassistant.components import unifi +from homeassistant.setup import async_setup_component + +import homeassistant.components.switch as switch + +from tests.common import mock_coro + +CLIENT_1 = { + 'hostname': 'client_1', + 'ip': '10.0.0.1', + 'is_wired': True, + 'mac': '00:00:00:00:00:01', + 'name': 'POE Client 1', + 'oui': 'Producer', + 'sw_mac': '00:00:00:00:01:01', + 'sw_port': 1, + 'wired-rx_bytes': 1234000000, + 'wired-tx_bytes': 5678000000 +} +CLIENT_2 = { + 'hostname': 'client_2', + 'ip': '10.0.0.2', + 'is_wired': True, + 'mac': '00:00:00:00:00:02', + 'name': 'POE Client 2', + 'oui': 'Producer', + 'sw_mac': '00:00:00:00:01:01', + 'sw_port': 2, + 'wired-rx_bytes': 1234000000, + 'wired-tx_bytes': 5678000000 +} +CLIENT_3 = { + 'hostname': 'client_3', + 'ip': '10.0.0.3', + 'is_wired': True, + 'mac': '00:00:00:00:00:03', + 'name': 'Non-POE Client 3', + 'oui': 'Producer', + 'sw_mac': '00:00:00:00:01:01', + 'sw_port': 3, + 'wired-rx_bytes': 1234000000, + 'wired-tx_bytes': 5678000000 +} +CLIENT_4 = { + 'hostname': 'client_4', + 'ip': '10.0.0.4', + 'is_wired': True, + 'mac': '00:00:00:00:00:04', + 'name': 'Non-POE Client 4', + 'oui': 'Producer', + 'sw_mac': '00:00:00:00:01:01', + 'sw_port': 4, + 'wired-rx_bytes': 1234000000, + 'wired-tx_bytes': 5678000000 +} +POE_SWITCH_CLIENTS = [ + { + 'hostname': 'client_1', + 'ip': '10.0.0.1', + 'is_wired': True, + 'mac': '00:00:00:00:00:01', + 'name': 'POE Client 1', + 'oui': 'Producer', + 'sw_mac': '00:00:00:00:01:01', + 'sw_port': 1, + 'wired-rx_bytes': 1234000000, + 'wired-tx_bytes': 5678000000 + }, + { + 'hostname': 'client_2', + 'ip': '10.0.0.2', + 'is_wired': True, + 'mac': '00:00:00:00:00:02', + 'name': 'POE Client 2', + 'oui': 'Producer', + 'sw_mac': '00:00:00:00:01:01', + 'sw_port': 1, + 'wired-rx_bytes': 1234000000, + 'wired-tx_bytes': 5678000000 + } +] + +DEVICE_1 = { + 'device_id': 'mock-id', + 'ip': '10.0.1.1', + 'mac': '00:00:00:00:01:01', + 'type': 'usw', + 'name': 'mock-name', + 'portconf_id': '', + 'port_table': [ + { + 'media': 'GE', + 'name': 'Port 1', + 'port_idx': 1, + 'poe_class': 'Class 4', + 'poe_enable': True, + 'poe_mode': 'auto', + 'poe_power': '2.56', + 'poe_voltage': '53.40', + 'portconf_id': '1a1', + 'port_poe': True, + 'up': True + }, + { + 'media': 'GE', + 'name': 'Port 2', + 'port_idx': 2, + 'poe_class': 'Class 4', + 'poe_enable': True, + 'poe_mode': 'auto', + 'poe_power': '2.56', + 'poe_voltage': '53.40', + 'portconf_id': '1a2', + 'port_poe': True, + 'up': True + }, + { + 'media': 'GE', + 'name': 'Port 3', + 'port_idx': 3, + 'poe_class': 'Unknown', + 'poe_enable': False, + 'poe_mode': 'off', + 'poe_power': '0.00', + 'poe_voltage': '0.00', + 'portconf_id': '1a3', + 'port_poe': False, + 'up': True + }, + { + 'media': 'GE', + 'name': 'Port 4', + 'port_idx': 4, + 'poe_class': 'Unknown', + 'poe_enable': False, + 'poe_mode': 'auto', + 'poe_power': '0.00', + 'poe_voltage': '0.00', + 'portconf_id': '1a4', + 'port_poe': True, + 'up': True + } + ] +} + +CONTROLLER_DATA = { + unifi.CONF_HOST: 'mock-host', + unifi.CONF_USERNAME: 'mock-user', + unifi.CONF_PASSWORD: 'mock-pswd', + unifi.CONF_PORT: 1234, + unifi.CONF_SITE_ID: 'mock-site', + unifi.CONF_VERIFY_SSL: True +} + +ENTRY_CONFIG = { + unifi.CONF_CONTROLLER: CONTROLLER_DATA, + unifi.CONF_POE_CONTROL: True +} + +CONTROLLER_ID = unifi.CONTROLLER_ID.format(host='mock-host', site='mock-site') + + +@pytest.fixture +def mock_controller(hass): + """Mock a UniFi Controller.""" + controller = Mock( + available=True, + api=Mock(), + spec=unifi.UniFiController + ) + controller.mock_requests = [] + + controller.mock_client_responses = deque() + controller.mock_device_responses = deque() + + async def mock_request(method, path, **kwargs): + kwargs['method'] = method + kwargs['path'] = path + controller.mock_requests.append(kwargs) + if path == 's/{site}/stat/sta': + return controller.mock_client_responses.popleft() + if path == 's/{site}/stat/device': + return controller.mock_device_responses.popleft() + return None + + controller.api.clients = Clients({}, mock_request) + controller.api.devices = Devices({}, mock_request) + + return controller + + +async def setup_controller(hass, mock_controller): + """Load the UniFi switch platform with the provided controller.""" + hass.config.components.add(unifi.DOMAIN) + hass.data[unifi.DOMAIN] = {CONTROLLER_ID: mock_controller} + config_entry = config_entries.ConfigEntry( + 1, unifi.DOMAIN, 'Mock Title', ENTRY_CONFIG, 'test', + config_entries.CONN_CLASS_LOCAL_POLL) + await hass.config_entries.async_forward_entry_setup(config_entry, 'switch') + # To flush out the service call to update the group + await hass.async_block_till_done() + + +async def test_platform_manually_configured(hass): + """Test that we do not discover anything or try to set up a bridge.""" + assert await async_setup_component(hass, switch.DOMAIN, { + 'switch': { + 'platform': 'unifi' + } + }) is True + assert unifi.DOMAIN not in hass.data + + +async def test_no_clients(hass, mock_controller): + """Test the update_clients function when no clients are found.""" + mock_controller.mock_client_responses.append({}) + await setup_controller(hass, mock_controller) + assert len(mock_controller.mock_requests) == 2 + assert not hass.states.async_all() + + +async def test_switches(hass, mock_controller): + """Test the update_items function with some lights.""" + mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_4]) + mock_controller.mock_device_responses.append([DEVICE_1]) + await setup_controller(hass, mock_controller) + assert len(mock_controller.mock_requests) == 2 + # 1 All Lights group, 2 lights + assert len(hass.states.async_all()) == 2 + + switch_1 = hass.states.get('switch.client_1') + assert switch_1 is not None + assert switch_1.state == 'on' + assert switch_1.attributes['power'] == '2.56' + assert switch_1.attributes['received'] == 1234 + assert switch_1.attributes['sent'] == 5678 + assert switch_1.attributes['switch'] == '00:00:00:00:01:01' + assert switch_1.attributes['port'] == 1 + assert switch_1.attributes['poe_mode'] == 'auto' + + switch = hass.states.get('switch.client_4') + assert switch is None + + +async def test_new_client_discovered(hass, mock_controller): + """Test if 2nd update has a new client.""" + mock_controller.mock_client_responses.append([CLIENT_1]) + mock_controller.mock_device_responses.append([DEVICE_1]) + + await setup_controller(hass, mock_controller) + assert len(mock_controller.mock_requests) == 2 + assert len(hass.states.async_all()) == 2 + + mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_2]) + mock_controller.mock_device_responses.append([DEVICE_1]) + + # Calling a service will trigger the updates to run + await hass.services.async_call('switch', 'turn_off', { + 'entity_id': 'switch.client_1' + }, blocking=True) + # 2x light update, 1 turn on request + assert len(mock_controller.mock_requests) == 5 + assert len(hass.states.async_all()) == 3 + + switch = hass.states.get('switch.client_2') + assert switch is not None + assert switch.state == 'on' + + +async def test_failed_update_successful_login(hass, mock_controller): + """Running update can login when requested.""" + mock_controller.available = False + mock_controller.api.clients.update = Mock() + mock_controller.api.clients.update.side_effect = aiounifi.LoginRequired + mock_controller.api.login = Mock() + mock_controller.api.login.return_value = mock_coro() + + await setup_controller(hass, mock_controller) + assert len(mock_controller.mock_requests) == 0 + + assert mock_controller.available is True + + +async def test_failed_update_failed_login(hass, mock_controller): + """Running update can handle a failed login.""" + mock_controller.api.clients.update = Mock() + mock_controller.api.clients.update.side_effect = aiounifi.LoginRequired + mock_controller.api.login = Mock() + mock_controller.api.login.side_effect = aiounifi.AiounifiException + + await setup_controller(hass, mock_controller) + assert len(mock_controller.mock_requests) == 0 + + assert mock_controller.available is False + + +async def test_failed_update_unreachable_controller(hass, mock_controller): + """Running update can handle a unreachable controller.""" + mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_2]) + mock_controller.mock_device_responses.append([DEVICE_1]) + + await setup_controller(hass, mock_controller) + + mock_controller.api.clients.update = Mock() + mock_controller.api.clients.update.side_effect = aiounifi.AiounifiException + + # Calling a service will trigger the updates to run + await hass.services.async_call('switch', 'turn_off', { + 'entity_id': 'switch.client_1' + }, blocking=True) + # 2x light update, 1 turn on request + assert len(mock_controller.mock_requests) == 3 + assert len(hass.states.async_all()) == 3 + + assert mock_controller.available is False + + +async def test_ignore_multiple_poe_clients_on_same_port(hass, mock_controller): + """Ignore when there are multiple POE driven clients on same port. + + If there is a non-UniFi switch powered by POE, + clients will be transparently marked as having POE as well. + """ + mock_controller.mock_client_responses.append(POE_SWITCH_CLIENTS) + mock_controller.mock_device_responses.append([DEVICE_1]) + await setup_controller(hass, mock_controller) + assert len(mock_controller.mock_requests) == 2 + # 1 All Lights group, 2 lights + assert len(hass.states.async_all()) == 0 + + switch_1 = hass.states.get('switch.client_1') + switch_2 = hass.states.get('switch.client_2') + assert switch_1 is None + assert switch_2 is None diff --git a/tests/components/unifi/__init__.py b/tests/components/unifi/__init__.py new file mode 100644 index 00000000000..e75b2778d2b --- /dev/null +++ b/tests/components/unifi/__init__.py @@ -0,0 +1 @@ +"""Tests for the UniFi component.""" diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py new file mode 100644 index 00000000000..b3b222d902a --- /dev/null +++ b/tests/components/unifi/test_controller.py @@ -0,0 +1,266 @@ +"""Test UniFi Controller.""" +from unittest.mock import Mock, patch + +from homeassistant.components import unifi +from homeassistant.components.unifi import controller, errors + +from tests.common import mock_coro + +CONTROLLER_DATA = { + unifi.CONF_HOST: '1.2.3.4', + unifi.CONF_USERNAME: 'username', + unifi.CONF_PASSWORD: 'password', + unifi.CONF_PORT: 1234, + unifi.CONF_SITE_ID: 'site', + unifi.CONF_VERIFY_SSL: True +} + +ENTRY_CONFIG = { + unifi.CONF_CONTROLLER: CONTROLLER_DATA, + unifi.CONF_POE_CONTROL: True + } + + +async def test_controller_setup(): + """Successful setup.""" + hass = Mock() + entry = Mock() + entry.data = ENTRY_CONFIG + api = Mock() + api.initialize.return_value = mock_coro(True) + + unifi_controller = controller.UniFiController(hass, entry) + + with patch.object(controller, 'get_controller', + return_value=mock_coro(api)): + assert await unifi_controller.async_setup() is True + + assert unifi_controller.api is api + assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 1 + assert hass.config_entries.async_forward_entry_setup.mock_calls[0][1] == \ + (entry, 'switch') + + +async def test_controller_host(): + """Config entry host and controller host are the same.""" + hass = Mock() + entry = Mock() + entry.data = ENTRY_CONFIG + + unifi_controller = controller.UniFiController(hass, entry) + + assert unifi_controller.host == '1.2.3.4' + + +async def test_controller_mac(): + """Test that it is possible to identify controller mac.""" + hass = Mock() + entry = Mock() + entry.data = ENTRY_CONFIG + client = Mock() + client.ip = '1.2.3.4' + client.mac = '00:11:22:33:44:55' + api = Mock() + api.initialize.return_value = mock_coro(True) + api.clients = {'client1': client} + + unifi_controller = controller.UniFiController(hass, entry) + + with patch.object(controller, 'get_controller', + return_value=mock_coro(api)): + assert await unifi_controller.async_setup() is True + + assert unifi_controller.mac == '00:11:22:33:44:55' + + +async def test_controller_no_mac(): + """Test that it works to not find the controllers mac.""" + hass = Mock() + entry = Mock() + entry.data = ENTRY_CONFIG + client = Mock() + client.ip = '5.6.7.8' + api = Mock() + api.initialize.return_value = mock_coro(True) + api.clients = {'client1': client} + + unifi_controller = controller.UniFiController(hass, entry) + + with patch.object(controller, 'get_controller', + return_value=mock_coro(api)): + assert await unifi_controller.async_setup() is True + + assert unifi_controller.mac is None + + +async def test_controller_not_accessible(): + """Retry to login gets scheduled when connection fails.""" + hass = Mock() + entry = Mock() + entry.data = ENTRY_CONFIG + api = Mock() + api.initialize.return_value = mock_coro(True) + + unifi_controller = controller.UniFiController(hass, entry) + + with patch.object(controller, 'get_controller', + side_effect=errors.CannotConnect): + assert await unifi_controller.async_setup() is False + + assert len(hass.helpers.event.async_call_later.mock_calls) == 1 + # Assert we are going to wait 2 seconds + assert hass.helpers.event.async_call_later.mock_calls[0][1][0] == 2 + + +async def test_controller_unknown_error(): + """Unknown errors are handled.""" + hass = Mock() + entry = Mock() + entry.data = ENTRY_CONFIG + api = Mock() + api.initialize.return_value = mock_coro(True) + + unifi_controller = controller.UniFiController(hass, entry) + + with patch.object(controller, 'get_controller', side_effect=Exception): + assert await unifi_controller.async_setup() is False + + assert not hass.helpers.event.async_call_later.mock_calls + + +async def test_reset_cancels_retry_setup(): + """Resetting a controller while we're waiting to retry setup.""" + hass = Mock() + entry = Mock() + entry.data = ENTRY_CONFIG + + unifi_controller = controller.UniFiController(hass, entry) + + with patch.object(controller, 'get_controller', + side_effect=errors.CannotConnect): + assert await unifi_controller.async_setup() is False + + mock_call_later = hass.helpers.event.async_call_later + + assert len(mock_call_later.mock_calls) == 1 + + assert await unifi_controller.async_reset() + + assert len(mock_call_later.mock_calls) == 2 + assert len(mock_call_later.return_value.mock_calls) == 1 + + +async def test_reset_if_entry_had_wrong_auth(): + """Calling reset when the entry contains wrong auth.""" + hass = Mock() + entry = Mock() + entry.data = ENTRY_CONFIG + + unifi_controller = controller.UniFiController(hass, entry) + + with patch.object(controller, 'get_controller', + side_effect=errors.AuthenticationRequired): + assert await unifi_controller.async_setup() is False + + assert not hass.async_add_job.mock_calls + + assert await unifi_controller.async_reset() + + +async def test_reset_unloads_entry_if_setup(): + """Calling reset when the entry has been setup.""" + hass = Mock() + entry = Mock() + entry.data = ENTRY_CONFIG + api = Mock() + api.initialize.return_value = mock_coro(True) + + unifi_controller = controller.UniFiController(hass, entry) + + with patch.object(controller, 'get_controller', + return_value=mock_coro(api)): + assert await unifi_controller.async_setup() is True + + assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 1 + + hass.config_entries.async_forward_entry_unload.return_value = \ + mock_coro(True) + assert await unifi_controller.async_reset() + + assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 1 + + +async def test_reset_unloads_entry_without_poe_control(): + """Calling reset while the entry has been setup.""" + hass = Mock() + entry = Mock() + entry.data = dict(ENTRY_CONFIG) + entry.data[unifi.CONF_POE_CONTROL] = False + api = Mock() + api.initialize.return_value = mock_coro(True) + + unifi_controller = controller.UniFiController(hass, entry) + + with patch.object(controller, 'get_controller', + return_value=mock_coro(api)): + assert await unifi_controller.async_setup() is True + + assert not hass.config_entries.async_forward_entry_setup.mock_calls + + hass.config_entries.async_forward_entry_unload.return_value = \ + mock_coro(True) + assert await unifi_controller.async_reset() + + assert not hass.config_entries.async_forward_entry_unload.mock_calls + + +async def test_get_controller(hass): + """Successful call.""" + with patch('aiounifi.Controller.login', return_value=mock_coro()): + assert await controller.get_controller(hass, **CONTROLLER_DATA) + + +async def test_get_controller_verify_ssl_false(hass): + """Successful call with verify ssl set to false.""" + controller_data = dict(CONTROLLER_DATA) + controller_data[unifi.CONF_VERIFY_SSL] = False + with patch('aiounifi.Controller.login', return_value=mock_coro()): + assert await controller.get_controller(hass, **controller_data) + + +async def test_get_controller_login_failed(hass): + """Check that get_controller can handle a failed login.""" + import aiounifi + result = None + with patch('aiounifi.Controller.login', side_effect=aiounifi.Unauthorized): + try: + result = await controller.get_controller(hass, **CONTROLLER_DATA) + except errors.AuthenticationRequired: + pass + assert result is None + + +async def test_get_controller_controller_unavailable(hass): + """Check that get_controller can handle controller being unavailable.""" + import aiounifi + result = None + with patch('aiounifi.Controller.login', + side_effect=aiounifi.RequestError): + try: + result = await controller.get_controller(hass, **CONTROLLER_DATA) + except errors.CannotConnect: + pass + assert result is None + + +async def test_get_controller_unknown_error(hass): + """Check that get_controller can handle unkown errors.""" + import aiounifi + result = None + with patch('aiounifi.Controller.login', + side_effect=aiounifi.AiounifiException): + try: + result = await controller.get_controller(hass, **CONTROLLER_DATA) + except errors.AuthenticationRequired: + pass + assert result is None diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py new file mode 100644 index 00000000000..400dd3fd93e --- /dev/null +++ b/tests/components/unifi/test_init.py @@ -0,0 +1,330 @@ +"""Test UniFi setup process.""" +from unittest.mock import Mock, patch + +from homeassistant.components import unifi +from homeassistant.setup import async_setup_component + +from tests.common import mock_coro, MockConfigEntry + + +async def test_setup_with_no_config(hass): + """Test that we do not discover anything or try to set up a bridge.""" + assert await async_setup_component(hass, unifi.DOMAIN, {}) is True + assert unifi.DOMAIN not in hass.data + + +async def test_successful_config_entry(hass): + """Test that configured options for a host are loaded via config entry.""" + entry = MockConfigEntry(domain=unifi.DOMAIN, data={ + 'controller': { + 'host': '0.0.0.0', + 'username': 'user', + 'password': 'pass', + 'port': 80, + 'site': 'default', + 'verify_ssl': True + }, + 'poe_control': True + }) + entry.add_to_hass(hass) + mock_registry = Mock() + with patch.object(unifi, 'UniFiController') as mock_controller, \ + patch('homeassistant.helpers.device_registry.async_get_registry', + return_value=mock_coro(mock_registry)): + mock_controller.return_value.async_setup.return_value = mock_coro(True) + mock_controller.return_value.mac = '00:11:22:33:44:55' + assert await unifi.async_setup_entry(hass, entry) is True + + assert len(mock_controller.mock_calls) == 2 + p_hass, p_entry = mock_controller.mock_calls[0][1] + + assert p_hass is hass + assert p_entry is entry + + assert len(mock_registry.mock_calls) == 1 + assert mock_registry.mock_calls[0][2] == { + 'config_entry_id': entry.entry_id, + 'connections': { + ('mac', '00:11:22:33:44:55') + }, + 'manufacturer': 'Ubiquiti', + 'model': "UniFi Controller", + 'name': "UniFi Controller", + } + + +async def test_controller_fail_setup(hass): + """Test that configured options for a host are loaded via config entry.""" + entry = MockConfigEntry(domain=unifi.DOMAIN, data={ + 'controller': { + 'host': '0.0.0.0', + 'username': 'user', + 'password': 'pass', + 'port': 80, + 'site': 'default', + 'verify_ssl': True + }, + 'poe_control': True + }) + entry.add_to_hass(hass) + + with patch.object(unifi, 'UniFiController') as mock_cntrlr: + mock_cntrlr.return_value.async_setup.return_value = mock_coro(False) + assert await unifi.async_setup_entry(hass, entry) is False + + controller_id = unifi.CONTROLLER_ID.format( + host='0.0.0.0', site='default' + ) + assert controller_id not in hass.data[unifi.DOMAIN] + + +async def test_controller_no_mac(hass): + """Test that configured options for a host are loaded via config entry.""" + entry = MockConfigEntry(domain=unifi.DOMAIN, data={ + 'controller': { + 'host': '0.0.0.0', + 'username': 'user', + 'password': 'pass', + 'port': 80, + 'site': 'default', + 'verify_ssl': True + }, + 'poe_control': True + }) + entry.add_to_hass(hass) + mock_registry = Mock() + with patch.object(unifi, 'UniFiController') as mock_controller, \ + patch('homeassistant.helpers.device_registry.async_get_registry', + return_value=mock_coro(mock_registry)): + mock_controller.return_value.async_setup.return_value = mock_coro(True) + mock_controller.return_value.mac = None + assert await unifi.async_setup_entry(hass, entry) is True + + assert len(mock_controller.mock_calls) == 2 + + assert len(mock_registry.mock_calls) == 0 + + +async def test_unload_entry(hass): + """Test being able to unload an entry.""" + entry = MockConfigEntry(domain=unifi.DOMAIN, data={ + 'controller': { + 'host': '0.0.0.0', + 'username': 'user', + 'password': 'pass', + 'port': 80, + 'site': 'default', + 'verify_ssl': True + }, + 'poe_control': True + }) + entry.add_to_hass(hass) + + with patch.object(unifi, 'UniFiController') as mock_controller, \ + patch('homeassistant.helpers.device_registry.async_get_registry', + return_value=mock_coro(Mock())): + mock_controller.return_value.async_setup.return_value = mock_coro(True) + mock_controller.return_value.mac = '00:11:22:33:44:55' + assert await unifi.async_setup_entry(hass, entry) is True + + assert len(mock_controller.return_value.mock_calls) == 1 + + mock_controller.return_value.async_reset.return_value = mock_coro(True) + assert await unifi.async_unload_entry(hass, entry) + assert len(mock_controller.return_value.async_reset.mock_calls) == 1 + assert hass.data[unifi.DOMAIN] == {} + + +async def test_flow_works(hass, aioclient_mock): + """Test config flow.""" + flow = unifi.UnifiFlowHandler() + flow.hass = hass + + with patch('aiounifi.Controller') as mock_controller: + def mock_constructor(host, username, password, port, site, websession): + """Fake the controller constructor.""" + mock_controller.host = host + mock_controller.username = username + mock_controller.password = password + mock_controller.port = port + mock_controller.site = site + return mock_controller + + mock_controller.side_effect = mock_constructor + mock_controller.login.return_value = mock_coro() + mock_controller.sites.return_value = mock_coro({ + 'site1': {'name': 'default', 'role': 'admin', 'desc': 'site name'} + }) + + await flow.async_step_user(user_input={ + unifi.CONF_HOST: '1.2.3.4', + unifi.CONF_USERNAME: 'username', + unifi.CONF_PASSWORD: 'password', + unifi.CONF_PORT: 1234, + unifi.CONF_VERIFY_SSL: True + }) + + result = await flow.async_step_site(user_input={}) + + assert mock_controller.host == '1.2.3.4' + assert len(mock_controller.login.mock_calls) == 1 + assert len(mock_controller.sites.mock_calls) == 1 + + assert result['type'] == 'create_entry' + assert result['title'] == 'site name' + assert result['data'] == { + unifi.CONF_CONTROLLER: { + unifi.CONF_HOST: '1.2.3.4', + unifi.CONF_USERNAME: 'username', + unifi.CONF_PASSWORD: 'password', + unifi.CONF_PORT: 1234, + unifi.CONF_SITE_ID: 'default', + unifi.CONF_VERIFY_SSL: True + }, + unifi.CONF_POE_CONTROL: True + } + + +async def test_controller_multiple_sites(hass): + """Test config flow.""" + flow = unifi.UnifiFlowHandler() + flow.hass = hass + + flow.config = { + unifi.CONF_HOST: '1.2.3.4', + unifi.CONF_USERNAME: 'username', + unifi.CONF_PASSWORD: 'password', + } + flow.sites = { + 'site1': { + 'name': 'default', 'role': 'admin', 'desc': 'site name' + }, + 'site2': { + 'name': 'site2', 'role': 'admin', 'desc': 'site2 name' + } + } + + result = await flow.async_step_site() + + assert result['type'] == 'form' + assert result['step_id'] == 'site' + + assert result['data_schema']({'site': 'site name'}) + assert result['data_schema']({'site': 'site2 name'}) + + +async def test_controller_site_already_configured(hass): + """Test config flow.""" + flow = unifi.UnifiFlowHandler() + flow.hass = hass + + entry = MockConfigEntry(domain=unifi.DOMAIN, data={ + 'controller': { + 'host': '1.2.3.4', + 'site': 'default', + } + }) + entry.add_to_hass(hass) + + flow.config = { + unifi.CONF_HOST: '1.2.3.4', + unifi.CONF_USERNAME: 'username', + unifi.CONF_PASSWORD: 'password', + } + flow.desc = 'site name' + flow.sites = { + 'site1': { + 'name': 'default', 'role': 'admin', 'desc': 'site name' + } + } + + result = await flow.async_step_site() + + assert result['type'] == 'abort' + + +async def test_user_permissions_low(hass, aioclient_mock): + """Test config flow.""" + flow = unifi.UnifiFlowHandler() + flow.hass = hass + + with patch('aiounifi.Controller') as mock_controller: + def mock_constructor(host, username, password, port, site, websession): + """Fake the controller constructor.""" + mock_controller.host = host + mock_controller.username = username + mock_controller.password = password + mock_controller.port = port + mock_controller.site = site + return mock_controller + + mock_controller.side_effect = mock_constructor + mock_controller.login.return_value = mock_coro() + mock_controller.sites.return_value = mock_coro({ + 'site1': {'name': 'default', 'role': 'viewer', 'desc': 'site name'} + }) + + await flow.async_step_user(user_input={ + unifi.CONF_HOST: '1.2.3.4', + unifi.CONF_USERNAME: 'username', + unifi.CONF_PASSWORD: 'password', + unifi.CONF_PORT: 1234, + unifi.CONF_VERIFY_SSL: True + }) + + result = await flow.async_step_site(user_input={}) + + assert result['type'] == 'abort' + + +async def test_user_credentials_faulty(hass, aioclient_mock): + """Test config flow.""" + flow = unifi.UnifiFlowHandler() + flow.hass = hass + + with patch.object(unifi, 'get_controller', + side_effect=unifi.errors.AuthenticationRequired): + result = await flow.async_step_user({ + unifi.CONF_HOST: '1.2.3.4', + unifi.CONF_USERNAME: 'username', + unifi.CONF_PASSWORD: 'password', + unifi.CONF_SITE_ID: 'default', + }) + + assert result['type'] == 'form' + assert result['errors'] == {'base': 'faulty_credentials'} + + +async def test_controller_is_unavailable(hass, aioclient_mock): + """Test config flow.""" + flow = unifi.UnifiFlowHandler() + flow.hass = hass + + with patch.object(unifi, 'get_controller', + side_effect=unifi.errors.CannotConnect): + result = await flow.async_step_user({ + unifi.CONF_HOST: '1.2.3.4', + unifi.CONF_USERNAME: 'username', + unifi.CONF_PASSWORD: 'password', + unifi.CONF_SITE_ID: 'default', + }) + + assert result['type'] == 'form' + assert result['errors'] == {'base': 'service_unavailable'} + + +async def test_controller_unkown_problem(hass, aioclient_mock): + """Test config flow.""" + flow = unifi.UnifiFlowHandler() + flow.hass = hass + + with patch.object(unifi, 'get_controller', + side_effect=Exception): + result = await flow.async_step_user({ + unifi.CONF_HOST: '1.2.3.4', + unifi.CONF_USERNAME: 'username', + unifi.CONF_PASSWORD: 'password', + unifi.CONF_SITE_ID: 'default', + }) + + assert result['type'] == 'abort' From 71ab8a9b1aaeae83f4675bfbd9a11639c994326e Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Tue, 16 Oct 2018 05:27:01 -0400 Subject: [PATCH 178/265] Moved Wink water heater from climate to water heater. (#17504) * Moved Wink water heater from climate to water heater. * Remove deprecated states from Tuya * Update toon.py * Update toon.py * Lint --- homeassistant/components/climate/__init__.py | 5 - homeassistant/components/climate/toon.py | 6 +- homeassistant/components/climate/tuya.py | 8 +- homeassistant/components/climate/wink.py | 112 +-------------- homeassistant/components/water_heater/wink.py | 136 ++++++++++++++++++ homeassistant/components/wink/__init__.py | 2 +- 6 files changed, 145 insertions(+), 124 deletions(-) create mode 100644 homeassistant/components/water_heater/wink.py diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 98483c454bc..a165521f0bd 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -48,11 +48,6 @@ STATE_MANUAL = 'manual' STATE_DRY = 'dry' STATE_FAN_ONLY = 'fan_only' STATE_ECO = 'eco' -STATE_ELECTRIC = 'electric' -STATE_PERFORMANCE = 'performance' -STATE_HIGH_DEMAND = 'high_demand' -STATE_HEAT_PUMP = 'heat_pump' -STATE_GAS = 'gas' SUPPORT_TARGET_TEMPERATURE = 1 SUPPORT_TARGET_TEMPERATURE_HIGH = 2 diff --git a/homeassistant/components/climate/toon.py b/homeassistant/components/climate/toon.py index e759e922ee1..5972ff52a8b 100644 --- a/homeassistant/components/climate/toon.py +++ b/homeassistant/components/climate/toon.py @@ -8,7 +8,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/climate.toon/ """ from homeassistant.components.climate import ( - ATTR_TEMPERATURE, STATE_COOL, STATE_ECO, STATE_HEAT, STATE_PERFORMANCE, + ATTR_TEMPERATURE, STATE_COOL, STATE_ECO, STATE_HEAT, STATE_AUTO, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, ClimateDevice) import homeassistant.components.toon as toon_main from homeassistant.const import TEMP_CELSIUS @@ -34,7 +34,7 @@ class ThermostatDevice(ClimateDevice): self._temperature = None self._setpoint = None self._operation_list = [ - STATE_PERFORMANCE, + STATE_AUTO, STATE_HEAT, STATE_ECO, STATE_COOL, @@ -84,7 +84,7 @@ class ThermostatDevice(ClimateDevice): def set_operation_mode(self, operation_mode): """Set new operation mode.""" toonlib_values = { - STATE_PERFORMANCE: 'Comfort', + STATE_AUTO: 'Comfort', STATE_HEAT: 'Home', STATE_ECO: 'Away', STATE_COOL: 'Sleep', diff --git a/homeassistant/components/climate/tuya.py b/homeassistant/components/climate/tuya.py index 2da46fee15d..4548867a45e 100644 --- a/homeassistant/components/climate/tuya.py +++ b/homeassistant/components/climate/tuya.py @@ -7,8 +7,7 @@ https://home-assistant.io/components/climate.tuya/ from homeassistant.components.climate import ( ATTR_TEMPERATURE, ENTITY_ID_FORMAT, STATE_AUTO, STATE_COOL, STATE_ECO, - STATE_ELECTRIC, STATE_FAN_ONLY, STATE_GAS, STATE_HEAT, STATE_HEAT_PUMP, - STATE_HIGH_DEMAND, STATE_PERFORMANCE, SUPPORT_FAN_MODE, SUPPORT_ON_OFF, + STATE_FAN_ONLY, STATE_HEAT, SUPPORT_FAN_MODE, SUPPORT_ON_OFF, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, ClimateDevice) from homeassistant.components.fan import SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH from homeassistant.components.tuya import DATA_TUYA, TuyaDevice @@ -23,13 +22,8 @@ HA_STATE_TO_TUYA = { STATE_AUTO: 'auto', STATE_COOL: 'cold', STATE_ECO: 'eco', - STATE_ELECTRIC: 'electric', STATE_FAN_ONLY: 'wind', - STATE_GAS: 'gas', STATE_HEAT: 'hot', - STATE_HEAT_PUMP: 'heat_pump', - STATE_HIGH_DEMAND: 'high_demand', - STATE_PERFORMANCE: 'performance', } TUYA_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_TUYA.items()} diff --git a/homeassistant/components/climate/wink.py b/homeassistant/components/climate/wink.py index cb6204d3ba3..7e5230ba3c7 100644 --- a/homeassistant/components/climate/wink.py +++ b/homeassistant/components/climate/wink.py @@ -1,5 +1,5 @@ """ -Support for Wink thermostats, Air Conditioners, and Water Heaters. +Support for Wink thermostats and Air Conditioners. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/climate.wink/ @@ -8,9 +8,9 @@ import logging from homeassistant.components.climate import ( ATTR_CURRENT_HUMIDITY, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - ATTR_TEMPERATURE, STATE_AUTO, STATE_COOL, STATE_ECO, STATE_ELECTRIC, - STATE_FAN_ONLY, STATE_GAS, STATE_HEAT, STATE_HEAT_PUMP, STATE_HIGH_DEMAND, - STATE_PERFORMANCE, SUPPORT_AUX_HEAT, SUPPORT_AWAY_MODE, SUPPORT_FAN_MODE, + ATTR_TEMPERATURE, STATE_AUTO, STATE_COOL, STATE_ECO, + STATE_FAN_ONLY, STATE_HEAT, SUPPORT_AUX_HEAT, + SUPPORT_AWAY_MODE, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW, ClimateDevice) @@ -24,11 +24,9 @@ _LOGGER = logging.getLogger(__name__) ATTR_ECO_TARGET = 'eco_target' ATTR_EXTERNAL_TEMPERATURE = 'external_temperature' ATTR_OCCUPIED = 'occupied' -ATTR_RHEEM_TYPE = 'rheem_type' ATTR_SCHEDULE_ENABLED = 'schedule_enabled' ATTR_SMART_TEMPERATURE = 'smart_temperature' ATTR_TOTAL_CONSUMPTION = 'total_consumption' -ATTR_VACATION_MODE = 'vacation_mode' ATTR_HEAT_ON = 'heat_on' ATTR_COOL_ON = 'cool_on' @@ -42,14 +40,9 @@ HA_STATE_TO_WINK = { STATE_AUTO: 'auto', STATE_COOL: 'cool_only', STATE_ECO: 'eco', - STATE_ELECTRIC: 'electric_only', STATE_FAN_ONLY: 'fan_only', - STATE_GAS: 'gas', STATE_HEAT: 'heat_only', - STATE_HEAT_PUMP: 'heat_pump', - STATE_HIGH_DEMAND: 'high_demand', STATE_OFF: 'off', - STATE_PERFORMANCE: 'performance', } WINK_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_WINK.items()} @@ -62,9 +55,6 @@ SUPPORT_FLAGS_THERMOSTAT = ( SUPPORT_FLAGS_AC = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | SUPPORT_FAN_MODE) -SUPPORT_FLAGS_HEATER = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | - SUPPORT_AWAY_MODE) - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Wink climate devices.""" @@ -77,10 +67,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _id = climate.object_id() + climate.name() if _id not in hass.data[DOMAIN]['unique_ids']: add_entities([WinkAC(climate, hass)]) - for water_heater in pywink.get_water_heaters(): - _id = water_heater.object_id() + water_heater.name() - if _id not in hass.data[DOMAIN]['unique_ids']: - add_entities([WinkWaterHeater(water_heater, hass)]) class WinkThermostat(WinkDevice, ClimateDevice): @@ -504,93 +490,3 @@ class WinkAC(WinkDevice, ClimateDevice): elif fan_mode == SPEED_HIGH: speed = 1.0 self.wink.set_ac_fan_speed(speed) - - -class WinkWaterHeater(WinkDevice, ClimateDevice): - """Representation of a Wink water heater.""" - - @property - def supported_features(self): - """Return the list of supported features.""" - return SUPPORT_FLAGS_HEATER - - @property - def temperature_unit(self): - """Return the unit of measurement.""" - # The Wink API always returns temp in Celsius - return TEMP_CELSIUS - - @property - def device_state_attributes(self): - """Return the optional device state attributes.""" - data = {} - data[ATTR_VACATION_MODE] = self.wink.vacation_mode_enabled() - data[ATTR_RHEEM_TYPE] = self.wink.rheem_type() - - return data - - @property - def current_operation(self): - """ - Return current operation one of the following. - - ["eco", "performance", "heat_pump", - "high_demand", "electric_only", "gas] - """ - if not self.wink.is_on(): - current_op = STATE_OFF - else: - current_op = WINK_STATE_TO_HA.get(self.wink.current_mode()) - if current_op is None: - current_op = STATE_UNKNOWN - return current_op - - @property - def operation_list(self): - """List of available operation modes.""" - op_list = ['off'] - modes = self.wink.modes() - for mode in modes: - if mode == 'aux': - continue - ha_mode = WINK_STATE_TO_HA.get(mode) - if ha_mode is not None: - op_list.append(ha_mode) - else: - error = "Invalid operation mode mapping. " + mode + \ - " doesn't map. Please report this." - _LOGGER.error(error) - return op_list - - def set_temperature(self, **kwargs): - """Set new target temperature.""" - target_temp = kwargs.get(ATTR_TEMPERATURE) - self.wink.set_temperature(target_temp) - - def set_operation_mode(self, operation_mode): - """Set operation mode.""" - op_mode_to_set = HA_STATE_TO_WINK.get(operation_mode) - self.wink.set_operation_mode(op_mode_to_set) - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self.wink.current_set_point() - - def turn_away_mode_on(self): - """Turn away on.""" - self.wink.set_vacation_mode(True) - - def turn_away_mode_off(self): - """Turn away off.""" - self.wink.set_vacation_mode(False) - - @property - def min_temp(self): - """Return the minimum temperature.""" - return self.wink.min_set_point() - - @property - def max_temp(self): - """Return the maximum temperature.""" - return self.wink.max_set_point() diff --git a/homeassistant/components/water_heater/wink.py b/homeassistant/components/water_heater/wink.py new file mode 100644 index 00000000000..a840baf980a --- /dev/null +++ b/homeassistant/components/water_heater/wink.py @@ -0,0 +1,136 @@ +""" +Support for Wink water heaters. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/water_heater.wink/ +""" +import logging + +from homeassistant.components.water_heater import ( + ATTR_TEMPERATURE, STATE_ECO, STATE_ELECTRIC, + STATE_PERFORMANCE, SUPPORT_AWAY_MODE, STATE_HEAT_PUMP, + STATE_GAS, STATE_HIGH_DEMAND, + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, + WaterHeaterDevice) +from homeassistant.components.wink import DOMAIN, WinkDevice +from homeassistant.const import ( + STATE_OFF, STATE_UNKNOWN, TEMP_CELSIUS) + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_FLAGS_HEATER = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | + SUPPORT_AWAY_MODE) + +ATTR_RHEEM_TYPE = 'rheem_type' +ATTR_VACATION_MODE = 'vacation_mode' + +HA_STATE_TO_WINK = { + STATE_ECO: 'eco', + STATE_ELECTRIC: 'electric_only', + STATE_GAS: 'gas', + STATE_HEAT_PUMP: 'heat_pump', + STATE_HIGH_DEMAND: 'high_demand', + STATE_OFF: 'off', + STATE_PERFORMANCE: 'performance', +} + +WINK_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_WINK.items()} + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Wink water heater devices.""" + import pywink + for water_heater in pywink.get_water_heaters(): + _id = water_heater.object_id() + water_heater.name() + if _id not in hass.data[DOMAIN]['unique_ids']: + add_entities([WinkWaterHeater(water_heater, hass)]) + + +class WinkWaterHeater(WinkDevice, WaterHeaterDevice): + """Representation of a Wink water heater.""" + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS_HEATER + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + # The Wink API always returns temp in Celsius + return TEMP_CELSIUS + + @property + def device_state_attributes(self): + """Return the optional device state attributes.""" + data = {} + data[ATTR_VACATION_MODE] = self.wink.vacation_mode_enabled() + data[ATTR_RHEEM_TYPE] = self.wink.rheem_type() + + return data + + @property + def current_operation(self): + """ + Return current operation one of the following. + + ["eco", "performance", "heat_pump", + "high_demand", "electric_only", "gas] + """ + if not self.wink.is_on(): + current_op = STATE_OFF + else: + current_op = WINK_STATE_TO_HA.get(self.wink.current_mode()) + if current_op is None: + current_op = STATE_UNKNOWN + return current_op + + @property + def operation_list(self): + """List of available operation modes.""" + op_list = ['off'] + modes = self.wink.modes() + for mode in modes: + if mode == 'aux': + continue + ha_mode = WINK_STATE_TO_HA.get(mode) + if ha_mode is not None: + op_list.append(ha_mode) + else: + error = "Invalid operation mode mapping. " + mode + \ + " doesn't map. Please report this." + _LOGGER.error(error) + return op_list + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + target_temp = kwargs.get(ATTR_TEMPERATURE) + self.wink.set_temperature(target_temp) + + def set_operation_mode(self, operation_mode): + """Set operation mode.""" + op_mode_to_set = HA_STATE_TO_WINK.get(operation_mode) + self.wink.set_operation_mode(op_mode_to_set) + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self.wink.current_set_point() + + def turn_away_mode_on(self): + """Turn away on.""" + self.wink.set_vacation_mode(True) + + def turn_away_mode_off(self): + """Turn away off.""" + self.wink.set_vacation_mode(False) + + @property + def min_temp(self): + """Return the minimum temperature.""" + return self.wink.min_set_point() + + @property + def max_temp(self): + """Return the maximum temperature.""" + return self.wink.max_set_point() diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index d21ccc18c93..3db044c4d1b 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -177,7 +177,7 @@ DIAL_STATE_SCHEMA = vol.Schema({ WINK_COMPONENTS = [ 'binary_sensor', 'sensor', 'light', 'switch', 'lock', 'cover', 'climate', - 'fan', 'alarm_control_panel', 'scene' + 'fan', 'alarm_control_panel', 'scene', 'water_heater' ] WINK_HUBS = [] From fee87cd6ed859d0b4f2d44619a4c0a4c88ec80f3 Mon Sep 17 00:00:00 2001 From: ehendrix23 Date: Tue, 16 Oct 2018 05:32:53 -0600 Subject: [PATCH 179/265] Add LogBook support to HomeKit (#17180) --- .../components/homekit/accessories.py | 31 +++++++++++++-- homeassistant/components/homekit/const.py | 7 ++++ .../components/homekit/type_covers.py | 8 ++-- homeassistant/components/homekit/type_fans.py | 6 +-- .../components/homekit/type_lights.py | 11 ++++-- .../components/homekit/type_locks.py | 2 +- .../components/homekit/type_media_players.py | 8 ++-- .../homekit/type_security_systems.py | 2 +- .../components/homekit/type_switches.py | 6 +-- .../components/homekit/type_thermostats.py | 26 +++++++++---- homeassistant/components/logbook.py | 32 +++++++++++++--- tests/components/homekit/conftest.py | 13 +++++++ tests/components/homekit/test_accessories.py | 38 ++++++++++++++++++- tests/components/homekit/test_type_covers.py | 29 ++++++++++++-- tests/components/homekit/test_type_fans.py | 19 ++++++++-- tests/components/homekit/test_type_lights.py | 24 ++++++++++-- tests/components/homekit/test_type_locks.py | 11 +++++- .../homekit/test_type_media_players.py | 22 +++++++++-- .../homekit/test_type_security_systems.py | 15 +++++++- .../components/homekit/test_type_switches.py | 20 ++++++++-- .../homekit/test_type_thermostats.py | 31 ++++++++++++--- tests/components/test_logbook.py | 32 ++++++++++++++++ 22 files changed, 327 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index adf5273b639..2c41885e311 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -9,7 +9,8 @@ from pyhap.accessory_driver import AccessoryDriver from pyhap.const import CATEGORY_OTHER from homeassistant.const import ( - __version__, ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL) + __version__, ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, + ATTR_SERVICE) from homeassistant.core import callback as ha_callback from homeassistant.core import split_entity_id from homeassistant.helpers.event import ( @@ -17,9 +18,10 @@ from homeassistant.helpers.event import ( from homeassistant.util import dt as dt_util from .const import ( - BRIDGE_MODEL, BRIDGE_SERIAL_NUMBER, CHAR_BATTERY_LEVEL, - CHAR_CHARGING_STATE, CHAR_STATUS_LOW_BATTERY, DEBOUNCE_TIMEOUT, - MANUFACTURER, SERV_BATTERY_SERVICE) + ATTR_DISPLAY_NAME, ATTR_VALUE, BRIDGE_MODEL, BRIDGE_SERIAL_NUMBER, + CHAR_BATTERY_LEVEL, CHAR_CHARGING_STATE, CHAR_STATUS_LOW_BATTERY, + DEBOUNCE_TIMEOUT, EVENT_HOMEKIT_CHANGED, MANUFACTURER, + SERV_BATTERY_SERVICE) from .util import ( convert_to_float, show_setup_message, dismiss_setup_message) @@ -137,6 +139,27 @@ class HomeAccessory(Accessory): """ raise NotImplementedError() + def call_service(self, domain, service, service_data, value=None): + """Fire event and call service for changes from HomeKit.""" + self.hass.add_job( + self.async_call_service, domain, service, service_data, value) + + async def async_call_service(self, domain, service, service_data, + value=None): + """Fire event and call service for changes from HomeKit. + + This method must be run in the event loop. + """ + event_data = { + ATTR_ENTITY_ID: self.entity_id, + ATTR_DISPLAY_NAME: self.display_name, + ATTR_SERVICE: service, + ATTR_VALUE: value + } + + self.hass.bus.async_fire(EVENT_HOMEKIT_CHANGED, event_data) + await self.hass.services.async_call(domain, service, service_data) + class HomeBridge(Bridge): """Adapter class for Bridge.""" diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 617dd3f4f22..6c143f7f0da 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -5,6 +5,10 @@ DOMAIN = 'homekit' HOMEKIT_FILE = '.homekit.state' HOMEKIT_NOTIFY_ID = 4663548 +# #### Attributes #### +ATTR_DISPLAY_NAME = 'display_name' +ATTR_VALUE = 'value' + # #### Config #### CONF_AUTO_START = 'auto_start' CONF_ENTITY_CONFIG = 'entity_config' @@ -22,6 +26,9 @@ FEATURE_PLAY_PAUSE = 'play_pause' FEATURE_PLAY_STOP = 'play_stop' FEATURE_TOGGLE_MUTE = 'toggle_mute' +# #### HomeKit Component Event #### +EVENT_HOMEKIT_CHANGED = 'homekit_state_change' + # #### HomeKit Component Services #### SERVICE_HOMEKIT_START = 'start' diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index cf0620a4e30..787e0e52b1d 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -47,10 +47,10 @@ class GarageDoorOpener(HomeAccessory): params = {ATTR_ENTITY_ID: self.entity_id} if value == 0: self.char_current_state.set_value(3) - self.hass.services.call(DOMAIN, SERVICE_OPEN_COVER, params) + self.call_service(DOMAIN, SERVICE_OPEN_COVER, params) elif value == 1: self.char_current_state.set_value(2) - self.hass.services.call(DOMAIN, SERVICE_CLOSE_COVER, params) + self.call_service(DOMAIN, SERVICE_CLOSE_COVER, params) def update_state(self, new_state): """Update cover state after state changed.""" @@ -88,7 +88,7 @@ class WindowCovering(HomeAccessory): self.homekit_target = value params = {ATTR_ENTITY_ID: self.entity_id, ATTR_POSITION: value} - self.hass.services.call(DOMAIN, SERVICE_SET_COVER_POSITION, params) + self.call_service(DOMAIN, SERVICE_SET_COVER_POSITION, params, value) def update_state(self, new_state): """Update cover position after state changed.""" @@ -143,7 +143,7 @@ class WindowCoveringBasic(HomeAccessory): service, position = (SERVICE_CLOSE_COVER, 0) params = {ATTR_ENTITY_ID: self.entity_id} - self.hass.services.call(DOMAIN, service, params) + self.call_service(DOMAIN, service, params) # Snap the current/target position to the expected final position. self.char_current_position.set_value(position) diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index aa44b11fefb..49eb525aa51 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -61,7 +61,7 @@ class Fan(HomeAccessory): self._flag[CHAR_ACTIVE] = True service = SERVICE_TURN_ON if value == 1 else SERVICE_TURN_OFF params = {ATTR_ENTITY_ID: self.entity_id} - self.hass.services.call(DOMAIN, service, params) + self.call_service(DOMAIN, service, params) def set_direction(self, value): """Set state if call came from HomeKit.""" @@ -69,7 +69,7 @@ class Fan(HomeAccessory): self._flag[CHAR_ROTATION_DIRECTION] = True direction = DIRECTION_REVERSE if value == 1 else DIRECTION_FORWARD params = {ATTR_ENTITY_ID: self.entity_id, ATTR_DIRECTION: direction} - self.hass.services.call(DOMAIN, SERVICE_SET_DIRECTION, params) + self.call_service(DOMAIN, SERVICE_SET_DIRECTION, params, direction) def set_oscillating(self, value): """Set state if call came from HomeKit.""" @@ -78,7 +78,7 @@ class Fan(HomeAccessory): oscillating = True if value == 1 else False params = {ATTR_ENTITY_ID: self.entity_id, ATTR_OSCILLATING: oscillating} - self.hass.services.call(DOMAIN, SERVICE_OSCILLATE, params) + self.call_service(DOMAIN, SERVICE_OSCILLATE, params, oscillating) def update_state(self, new_state): """Update fan after state change.""" diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index da012799602..b8125e24eba 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -83,7 +83,7 @@ class Light(HomeAccessory): self._flag[CHAR_ON] = True params = {ATTR_ENTITY_ID: self.entity_id} service = SERVICE_TURN_ON if value == 1 else SERVICE_TURN_OFF - self.hass.services.call(DOMAIN, service, params) + self.call_service(DOMAIN, service, params) @debounce def set_brightness(self, value): @@ -94,14 +94,16 @@ class Light(HomeAccessory): self.set_state(0) # Turn off light return params = {ATTR_ENTITY_ID: self.entity_id, ATTR_BRIGHTNESS_PCT: value} - self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params) + self.call_service(DOMAIN, SERVICE_TURN_ON, params, + "brightness at {}%".format(value)) def set_color_temperature(self, value): """Set color temperature if call came from HomeKit.""" _LOGGER.debug('%s: Set color temp to %s', self.entity_id, value) self._flag[CHAR_COLOR_TEMPERATURE] = True params = {ATTR_ENTITY_ID: self.entity_id, ATTR_COLOR_TEMP: value} - self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params) + self.call_service(DOMAIN, SERVICE_TURN_ON, params, + "color temperature at {}".format(value)) def set_saturation(self, value): """Set saturation if call came from HomeKit.""" @@ -126,7 +128,8 @@ class Light(HomeAccessory): self._flag.update({ CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True}) params = {ATTR_ENTITY_ID: self.entity_id, ATTR_HS_COLOR: color} - self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params) + self.call_service(DOMAIN, SERVICE_TURN_ON, params, + "set color at {}".format(color)) def update_state(self, new_state): """Update light after state change.""" diff --git a/homeassistant/components/homekit/type_locks.py b/homeassistant/components/homekit/type_locks.py index 05ab6c6f822..a3d9c3b6fac 100644 --- a/homeassistant/components/homekit/type_locks.py +++ b/homeassistant/components/homekit/type_locks.py @@ -54,7 +54,7 @@ class Lock(HomeAccessory): params = {ATTR_ENTITY_ID: self.entity_id} if self._code: params[ATTR_CODE] = self._code - self.hass.services.call(DOMAIN, service, params) + self.call_service(DOMAIN, service, params) def update_state(self, new_state): """Update lock after state changed.""" diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py index ec41b9fd618..09088871fd2 100644 --- a/homeassistant/components/homekit/type_media_players.py +++ b/homeassistant/components/homekit/type_media_players.py @@ -76,7 +76,7 @@ class MediaPlayer(HomeAccessory): self._flag[FEATURE_ON_OFF] = True service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF params = {ATTR_ENTITY_ID: self.entity_id} - self.hass.services.call(DOMAIN, service, params) + self.call_service(DOMAIN, service, params) def set_play_pause(self, value): """Move switch state to value if call came from HomeKit.""" @@ -85,7 +85,7 @@ class MediaPlayer(HomeAccessory): self._flag[FEATURE_PLAY_PAUSE] = True service = SERVICE_MEDIA_PLAY if value else SERVICE_MEDIA_PAUSE params = {ATTR_ENTITY_ID: self.entity_id} - self.hass.services.call(DOMAIN, service, params) + self.call_service(DOMAIN, service, params) def set_play_stop(self, value): """Move switch state to value if call came from HomeKit.""" @@ -94,7 +94,7 @@ class MediaPlayer(HomeAccessory): self._flag[FEATURE_PLAY_STOP] = True service = SERVICE_MEDIA_PLAY if value else SERVICE_MEDIA_STOP params = {ATTR_ENTITY_ID: self.entity_id} - self.hass.services.call(DOMAIN, service, params) + self.call_service(DOMAIN, service, params) def set_toggle_mute(self, value): """Move switch state to value if call came from HomeKit.""" @@ -103,7 +103,7 @@ class MediaPlayer(HomeAccessory): self._flag[FEATURE_TOGGLE_MUTE] = True params = {ATTR_ENTITY_ID: self.entity_id, ATTR_MEDIA_VOLUME_MUTED: value} - self.hass.services.call(DOMAIN, SERVICE_VOLUME_MUTE, params) + self.call_service(DOMAIN, SERVICE_VOLUME_MUTE, params) def update_state(self, new_state): """Update switch state after state changed.""" diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index a7d36720cab..206da3e2889 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -59,7 +59,7 @@ class SecuritySystem(HomeAccessory): params = {ATTR_ENTITY_ID: self.entity_id} if self._alarm_code: params[ATTR_CODE] = self._alarm_code - self.hass.services.call(DOMAIN, service, params) + self.call_service(DOMAIN, service, params) def update_state(self, new_state): """Update security state after state changed.""" diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 82a5d68d644..1090205ff56 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -51,7 +51,7 @@ class Outlet(HomeAccessory): self.flag_target_state = True params = {ATTR_ENTITY_ID: self.entity_id} service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF - self.hass.services.call(DOMAIN, service, params) + self.call_service(DOMAIN, service, params) def update_state(self, new_state): """Update switch state after state changed.""" @@ -84,7 +84,7 @@ class Switch(HomeAccessory): self.flag_target_state = True params = {ATTR_ENTITY_ID: self.entity_id} service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF - self.hass.services.call(self._domain, service, params) + self.call_service(self._domain, service, params) def update_state(self, new_state): """Update switch state after state changed.""" @@ -123,7 +123,7 @@ class Valve(HomeAccessory): self.char_in_use.set_value(value) params = {ATTR_ENTITY_ID: self.entity_id} service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF - self.hass.services.call(DOMAIN, service, params) + self.call_service(DOMAIN, service, params) def update_state(self, new_state): """Update switch state after state changed.""" diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 8517122f6a8..a7344995021 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -122,12 +122,13 @@ class Thermostat(HomeAccessory): if self.support_power_state is True: params = {ATTR_ENTITY_ID: self.entity_id} if hass_value == STATE_OFF: - self.hass.services.call(DOMAIN, SERVICE_TURN_OFF, params) + self.call_service(DOMAIN, SERVICE_TURN_OFF, params) return self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params) params = {ATTR_ENTITY_ID: self.entity_id, ATTR_OPERATION_MODE: hass_value} - self.hass.services.call(DOMAIN, SERVICE_SET_OPERATION_MODE, params) + self.call_service( + DOMAIN, SERVICE_SET_OPERATION_MODE, params, hass_value) @debounce def set_cooling_threshold(self, value): @@ -136,11 +137,14 @@ class Thermostat(HomeAccessory): self.entity_id, value) self.coolingthresh_flag_target_state = True low = self.char_heating_thresh_temp.value + temperature = temperature_to_states(value, self._unit) params = { ATTR_ENTITY_ID: self.entity_id, - ATTR_TARGET_TEMP_HIGH: temperature_to_states(value, self._unit), + ATTR_TARGET_TEMP_HIGH: temperature, ATTR_TARGET_TEMP_LOW: temperature_to_states(low, self._unit)} - self.hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, params) + self.call_service(DOMAIN, SERVICE_SET_TEMPERATURE, params, + "cooling threshold {}{}".format(temperature, + self._unit)) @debounce def set_heating_threshold(self, value): @@ -149,11 +153,14 @@ class Thermostat(HomeAccessory): self.entity_id, value) self.heatingthresh_flag_target_state = True high = self.char_cooling_thresh_temp.value + temperature = temperature_to_states(value, self._unit) params = { ATTR_ENTITY_ID: self.entity_id, ATTR_TARGET_TEMP_HIGH: temperature_to_states(high, self._unit), - ATTR_TARGET_TEMP_LOW: temperature_to_states(value, self._unit)} - self.hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, params) + ATTR_TARGET_TEMP_LOW: temperature} + self.call_service(DOMAIN, SERVICE_SET_TEMPERATURE, params, + "heating threshold {}{}".format(temperature, + self._unit)) @debounce def set_target_temperature(self, value): @@ -161,10 +168,13 @@ class Thermostat(HomeAccessory): _LOGGER.debug('%s: Set target temperature to %.2f°C', self.entity_id, value) self.temperature_flag_target_state = True + temperature = temperature_to_states(value, self._unit) params = { ATTR_ENTITY_ID: self.entity_id, - ATTR_TEMPERATURE: temperature_to_states(value, self._unit)} - self.hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, params) + ATTR_TEMPERATURE: temperature} + self.call_service(DOMAIN, SERVICE_SET_TEMPERATURE, params, + "target {}{}".format(temperature, + self._unit)) def update_state(self, new_state): """Update security state after state changed.""" diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 5cbd2b9432b..5bd7ed0d2f5 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -14,13 +14,16 @@ from homeassistant.loader import bind_hass from homeassistant.components import sun from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( - ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_HIDDEN, ATTR_NAME, CONF_EXCLUDE, - CONF_INCLUDE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - EVENT_LOGBOOK_ENTRY, EVENT_STATE_CHANGED, HTTP_BAD_REQUEST, STATE_NOT_HOME, - STATE_OFF, STATE_ON) + ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_HIDDEN, ATTR_NAME, ATTR_SERVICE, + CONF_EXCLUDE, CONF_INCLUDE, EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, EVENT_LOGBOOK_ENTRY, EVENT_STATE_CHANGED, + HTTP_BAD_REQUEST, STATE_NOT_HOME, STATE_OFF, STATE_ON) from homeassistant.core import ( DOMAIN as HA_DOMAIN, State, callback, split_entity_id) from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME +from homeassistant.components.homekit.const import ( + ATTR_DISPLAY_NAME, ATTR_VALUE, DOMAIN as DOMAIN_HOMEKIT, + EVENT_HOMEKIT_CHANGED) import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util @@ -56,7 +59,7 @@ CONFIG_SCHEMA = vol.Schema({ ALL_EVENT_TYPES = [ EVENT_STATE_CHANGED, EVENT_LOGBOOK_ENTRY, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - EVENT_ALEXA_SMART_HOME + EVENT_ALEXA_SMART_HOME, EVENT_HOMEKIT_CHANGED ] LOG_MESSAGE_SCHEMA = vol.Schema({ @@ -294,6 +297,25 @@ def humanify(hass, events): 'context_user_id': event.context.user_id } + elif event.event_type == EVENT_HOMEKIT_CHANGED: + data = event.data + entity_id = data.get(ATTR_ENTITY_ID) + value = data.get(ATTR_VALUE) + + value_msg = " to {}".format(value) if value else '' + message = "send command {}{} for {}".format( + data[ATTR_SERVICE], value_msg, data[ATTR_DISPLAY_NAME]) + + yield { + 'when': event.time_fired, + 'name': 'HomeKit', + 'message': message, + 'domain': DOMAIN_HOMEKIT, + 'entity_id': entity_id, + 'context_id': event.context.id, + 'context_user_id': event.context.user_id + } + def _get_events(hass, config, start_day, end_day, entity_id=None): """Get events for a period of time.""" diff --git a/tests/components/homekit/conftest.py b/tests/components/homekit/conftest.py index 55e02de7526..326845cf74f 100644 --- a/tests/components/homekit/conftest.py +++ b/tests/components/homekit/conftest.py @@ -3,6 +3,9 @@ from unittest.mock import patch import pytest +from homeassistant.components.homekit.const import EVENT_HOMEKIT_CHANGED +from homeassistant.core import callback + from pyhap.accessory_driver import AccessoryDriver @@ -14,3 +17,13 @@ def hk_driver(): patch('pyhap.accessory_driver.HAPServer'), \ patch('pyhap.accessory_driver.AccessoryDriver.publish'): return AccessoryDriver(pincode=b'123-45-678', address='127.0.0.1') + + +@pytest.fixture +def events(hass): + """Yield caught homekit_changed events.""" + events = [] + hass.bus.async_listen( + EVENT_HOMEKIT_CHANGED, + callback(lambda e: events.append(e))) + yield events diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index edb1c7175f8..15ab6d7413e 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -10,14 +10,17 @@ import pytest from homeassistant.components.homekit.accessories import ( debounce, HomeAccessory, HomeBridge, HomeDriver) from homeassistant.components.homekit.const import ( + ATTR_DISPLAY_NAME, ATTR_VALUE, BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, CHAR_FIRMWARE_REVISION, CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER, MANUFACTURER, SERV_ACCESSORY_INFO) from homeassistant.const import ( - __version__, ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, ATTR_NOW, - EVENT_TIME_CHANGED) + __version__, ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, + ATTR_SERVICE, ATTR_NOW, EVENT_TIME_CHANGED) import homeassistant.util.dt as dt_util +from tests.common import async_mock_service + async def test_debounce(hass): """Test add_timeout decorator function.""" @@ -146,6 +149,37 @@ async def test_battery_service(hass, hk_driver): assert acc._char_charging.value == 0 +async def test_call_service(hass, hk_driver, events): + """Test call_service method.""" + entity_id = 'homekit.accessory' + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + + acc = HomeAccessory(hass, hk_driver, 'Home Accessory', entity_id, 2, None) + call_service = async_mock_service(hass, 'cover', 'open_cover') + + test_domain = 'cover' + test_service = 'open_cover' + test_value = 'value' + + await acc.async_call_service( + test_domain, test_service, {ATTR_ENTITY_ID: entity_id}, test_value) + await hass.async_block_till_done() + + assert len(events) == 1 + assert events[0].data == { + ATTR_ENTITY_ID: acc.entity_id, + ATTR_DISPLAY_NAME: acc.display_name, + ATTR_SERVICE: test_service, + ATTR_VALUE: test_value + } + + assert len(call_service) == 1 + assert call_service[0].domain == test_domain + assert call_service[0].service == test_service + assert call_service[0].data == {ATTR_ENTITY_ID: entity_id} + + def test_home_bridge(hk_driver): """Test HomeBridge class.""" bridge = HomeBridge('hass', hk_driver, BRIDGE_NAME) diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index 04ed5df5702..c32abaef0dd 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -5,6 +5,7 @@ import pytest from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN, SUPPORT_STOP) +from homeassistant.components.homekit.const import ATTR_VALUE from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_CLOSED, STATE_OPEN, STATE_UNAVAILABLE, STATE_UNKNOWN) @@ -28,7 +29,7 @@ def cls(): patcher.stop() -async def test_garage_door_open_close(hass, hk_driver, cls): +async def test_garage_door_open_close(hass, hk_driver, cls, events): """Test if accessory and HA are updated accordingly.""" entity_id = 'cover.garage_door' @@ -73,6 +74,8 @@ async def test_garage_door_open_close(hass, hk_driver, cls): assert call_close_cover[0].data[ATTR_ENTITY_ID] == entity_id assert acc.char_current_state.value == 2 assert acc.char_target_state.value == 1 + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] is None hass.states.async_set(entity_id, STATE_CLOSED) await hass.async_block_till_done() @@ -83,9 +86,11 @@ async def test_garage_door_open_close(hass, hk_driver, cls): assert call_open_cover[0].data[ATTR_ENTITY_ID] == entity_id assert acc.char_current_state.value == 3 assert acc.char_target_state.value == 0 + assert len(events) == 2 + assert events[-1].data[ATTR_VALUE] is None -async def test_window_set_cover_position(hass, hk_driver, cls): +async def test_window_set_cover_position(hass, hk_driver, cls, events): """Test if accessory and HA are updated accordingly.""" entity_id = 'cover.window' @@ -123,6 +128,8 @@ async def test_window_set_cover_position(hass, hk_driver, cls): assert call_set_cover_position[0].data[ATTR_POSITION] == 25 assert acc.char_current_position.value == 50 assert acc.char_target_position.value == 25 + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] == 25 await hass.async_add_job(acc.char_target_position.client_update_value, 75) await hass.async_block_till_done() @@ -131,9 +138,11 @@ async def test_window_set_cover_position(hass, hk_driver, cls): assert call_set_cover_position[1].data[ATTR_POSITION] == 75 assert acc.char_current_position.value == 50 assert acc.char_target_position.value == 75 + assert len(events) == 2 + assert events[-1].data[ATTR_VALUE] == 75 -async def test_window_open_close(hass, hk_driver, cls): +async def test_window_open_close(hass, hk_driver, cls, events): """Test if accessory and HA are updated accordingly.""" entity_id = 'cover.window' @@ -178,6 +187,8 @@ async def test_window_open_close(hass, hk_driver, cls): assert acc.char_current_position.value == 0 assert acc.char_target_position.value == 0 assert acc.char_position_state.value == 2 + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] is None await hass.async_add_job(acc.char_target_position.client_update_value, 90) await hass.async_block_till_done() @@ -186,6 +197,8 @@ async def test_window_open_close(hass, hk_driver, cls): assert acc.char_current_position.value == 100 assert acc.char_target_position.value == 100 assert acc.char_position_state.value == 2 + assert len(events) == 2 + assert events[-1].data[ATTR_VALUE] is None await hass.async_add_job(acc.char_target_position.client_update_value, 55) await hass.async_block_till_done() @@ -194,9 +207,11 @@ async def test_window_open_close(hass, hk_driver, cls): assert acc.char_current_position.value == 100 assert acc.char_target_position.value == 100 assert acc.char_position_state.value == 2 + assert len(events) == 3 + assert events[-1].data[ATTR_VALUE] is None -async def test_window_open_close_stop(hass, hk_driver, cls): +async def test_window_open_close_stop(hass, hk_driver, cls, events): """Test if accessory and HA are updated accordingly.""" entity_id = 'cover.window' @@ -217,6 +232,8 @@ async def test_window_open_close_stop(hass, hk_driver, cls): assert acc.char_current_position.value == 0 assert acc.char_target_position.value == 0 assert acc.char_position_state.value == 2 + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] is None await hass.async_add_job(acc.char_target_position.client_update_value, 90) await hass.async_block_till_done() @@ -225,6 +242,8 @@ async def test_window_open_close_stop(hass, hk_driver, cls): assert acc.char_current_position.value == 100 assert acc.char_target_position.value == 100 assert acc.char_position_state.value == 2 + assert len(events) == 2 + assert events[-1].data[ATTR_VALUE] is None await hass.async_add_job(acc.char_target_position.client_update_value, 55) await hass.async_block_till_done() @@ -233,3 +252,5 @@ async def test_window_open_close_stop(hass, hk_driver, cls): assert acc.char_current_position.value == 50 assert acc.char_target_position.value == 50 assert acc.char_position_state.value == 2 + assert len(events) == 3 + assert events[-1].data[ATTR_VALUE] is None diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index 87a481ff06f..27b6cec0790 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -6,6 +6,7 @@ import pytest from homeassistant.components.fan import ( ATTR_DIRECTION, ATTR_OSCILLATING, DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN, SUPPORT_DIRECTION, SUPPORT_OSCILLATE) +from homeassistant.components.homekit.const import ATTR_VALUE from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF, STATE_UNKNOWN) @@ -26,7 +27,7 @@ def cls(): patcher.stop() -async def test_fan_basic(hass, hk_driver, cls): +async def test_fan_basic(hass, hk_driver, cls, events): """Test fan with char state.""" entity_id = 'fan.demo' @@ -62,6 +63,8 @@ async def test_fan_basic(hass, hk_driver, cls): await hass.async_block_till_done() assert call_turn_on assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] is None hass.states.async_set(entity_id, STATE_ON) await hass.async_block_till_done() @@ -70,9 +73,11 @@ async def test_fan_basic(hass, hk_driver, cls): await hass.async_block_till_done() assert call_turn_off assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id + assert len(events) == 2 + assert events[-1].data[ATTR_VALUE] is None -async def test_fan_direction(hass, hk_driver, cls): +async def test_fan_direction(hass, hk_driver, cls, events): """Test fan with direction.""" entity_id = 'fan.demo' @@ -101,15 +106,19 @@ async def test_fan_direction(hass, hk_driver, cls): assert call_set_direction[0] assert call_set_direction[0].data[ATTR_ENTITY_ID] == entity_id assert call_set_direction[0].data[ATTR_DIRECTION] == DIRECTION_FORWARD + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] == DIRECTION_FORWARD await hass.async_add_job(acc.char_direction.client_update_value, 1) await hass.async_block_till_done() assert call_set_direction[1] assert call_set_direction[1].data[ATTR_ENTITY_ID] == entity_id assert call_set_direction[1].data[ATTR_DIRECTION] == DIRECTION_REVERSE + assert len(events) == 2 + assert events[-1].data[ATTR_VALUE] == DIRECTION_REVERSE -async def test_fan_oscillate(hass, hk_driver, cls): +async def test_fan_oscillate(hass, hk_driver, cls, events): """Test fan with oscillate.""" entity_id = 'fan.demo' @@ -136,9 +145,13 @@ async def test_fan_oscillate(hass, hk_driver, cls): assert call_oscillate[0] assert call_oscillate[0].data[ATTR_ENTITY_ID] == entity_id assert call_oscillate[0].data[ATTR_OSCILLATING] is False + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] is False await hass.async_add_job(acc.char_swing.client_update_value, 1) await hass.async_block_till_done() assert call_oscillate[1] assert call_oscillate[1].data[ATTR_ENTITY_ID] == entity_id assert call_oscillate[1].data[ATTR_OSCILLATING] is True + assert len(events) == 2 + assert events[-1].data[ATTR_VALUE] is True diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index aab6274f484..540d9a73f48 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -3,6 +3,7 @@ from collections import namedtuple import pytest +from homeassistant.components.homekit.const import ATTR_VALUE from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, ATTR_HS_COLOR, DOMAIN, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR) @@ -26,7 +27,7 @@ def cls(): patcher.stop() -async def test_light_basic(hass, hk_driver, cls): +async def test_light_basic(hass, hk_driver, cls, events): """Test light with char state.""" entity_id = 'light.demo' @@ -62,6 +63,8 @@ async def test_light_basic(hass, hk_driver, cls): await hass.async_block_till_done() assert call_turn_on assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] is None hass.states.async_set(entity_id, STATE_ON) await hass.async_block_till_done() @@ -70,11 +73,14 @@ async def test_light_basic(hass, hk_driver, cls): await hass.async_block_till_done() assert call_turn_off assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id + assert len(events) == 2 + assert events[-1].data[ATTR_VALUE] is None -async def test_light_brightness(hass, hk_driver, cls): +async def test_light_brightness(hass, hk_driver, cls, events): """Test light with brightness.""" entity_id = 'light.demo' + event_value = "brightness at " hass.states.async_set(entity_id, STATE_ON, { ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS: 255}) @@ -101,6 +107,8 @@ async def test_light_brightness(hass, hk_driver, cls): assert call_turn_on[0] assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id assert call_turn_on[0].data[ATTR_BRIGHTNESS_PCT] == 20 + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] == "{}20%".format(event_value) await hass.async_add_job(acc.char_on.client_update_value, 1) await hass.async_add_job(acc.char_brightness.client_update_value, 40) @@ -108,15 +116,19 @@ async def test_light_brightness(hass, hk_driver, cls): assert call_turn_on[1] assert call_turn_on[1].data[ATTR_ENTITY_ID] == entity_id assert call_turn_on[1].data[ATTR_BRIGHTNESS_PCT] == 40 + assert len(events) == 2 + assert events[-1].data[ATTR_VALUE] == "{}40%".format(event_value) await hass.async_add_job(acc.char_on.client_update_value, 1) await hass.async_add_job(acc.char_brightness.client_update_value, 0) await hass.async_block_till_done() assert call_turn_off assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id + assert len(events) == 3 + assert events[-1].data[ATTR_VALUE] is None -async def test_light_color_temperature(hass, hk_driver, cls): +async def test_light_color_temperature(hass, hk_driver, cls, events): """Test light with color temperature.""" entity_id = 'light.demo' @@ -141,9 +153,11 @@ async def test_light_color_temperature(hass, hk_driver, cls): assert call_turn_on assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id assert call_turn_on[0].data[ATTR_COLOR_TEMP] == 250 + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] == "color temperature at 250" -async def test_light_rgb_color(hass, hk_driver, cls): +async def test_light_rgb_color(hass, hk_driver, cls, events): """Test light with rgb_color.""" entity_id = 'light.demo' @@ -170,3 +184,5 @@ async def test_light_rgb_color(hass, hk_driver, cls): assert call_turn_on assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id assert call_turn_on[0].data[ATTR_HS_COLOR] == (145, 75) + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] == "set color at (145, 75)" diff --git a/tests/components/homekit/test_type_locks.py b/tests/components/homekit/test_type_locks.py index 8f18a591019..e7e52c65559 100644 --- a/tests/components/homekit/test_type_locks.py +++ b/tests/components/homekit/test_type_locks.py @@ -1,6 +1,7 @@ """Test different accessory types: Locks.""" import pytest +from homeassistant.components.homekit.const import ATTR_VALUE from homeassistant.components.homekit.type_locks import Lock from homeassistant.components.lock import DOMAIN from homeassistant.const import ( @@ -9,7 +10,7 @@ from homeassistant.const import ( from tests.common import async_mock_service -async def test_lock_unlock(hass, hk_driver): +async def test_lock_unlock(hass, hk_driver, events): """Test if accessory and HA are updated accordingly.""" code = '1234' config = {ATTR_CODE: code} @@ -56,6 +57,8 @@ async def test_lock_unlock(hass, hk_driver): assert call_lock[0].data[ATTR_ENTITY_ID] == entity_id assert call_lock[0].data[ATTR_CODE] == code assert acc.char_target_state.value == 1 + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] is None await hass.async_add_job(acc.char_target_state.client_update_value, 0) await hass.async_block_till_done() @@ -63,10 +66,12 @@ async def test_lock_unlock(hass, hk_driver): assert call_unlock[0].data[ATTR_ENTITY_ID] == entity_id assert call_unlock[0].data[ATTR_CODE] == code assert acc.char_target_state.value == 0 + assert len(events) == 2 + assert events[-1].data[ATTR_VALUE] is None @pytest.mark.parametrize('config', [{}, {ATTR_CODE: None}]) -async def test_no_code(hass, hk_driver, config): +async def test_no_code(hass, hk_driver, config, events): """Test accessory if lock doesn't require a code.""" entity_id = 'lock.kitchen_door' @@ -83,3 +88,5 @@ async def test_no_code(hass, hk_driver, config): assert call_lock[0].data[ATTR_ENTITY_ID] == entity_id assert ATTR_CODE not in call_lock[0].data assert acc.char_target_state.value == 1 + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] is None diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py index 681cbba7252..299570a6923 100644 --- a/tests/components/homekit/test_type_media_players.py +++ b/tests/components/homekit/test_type_media_players.py @@ -1,8 +1,8 @@ """Test different accessory types: Media Players.""" from homeassistant.components.homekit.const import ( - CONF_FEATURE_LIST, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP, - FEATURE_TOGGLE_MUTE) + ATTR_VALUE, CONF_FEATURE_LIST, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, + FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE) from homeassistant.components.homekit.type_media_players import MediaPlayer from homeassistant.components.media_player import ( ATTR_MEDIA_VOLUME_MUTED, DOMAIN) @@ -13,7 +13,7 @@ from homeassistant.const import ( from tests.common import async_mock_service -async def test_media_player_set_state(hass, hk_driver): +async def test_media_player_set_state(hass, hk_driver, events): """Test if accessory and HA are updated accordingly.""" config = {CONF_FEATURE_LIST: { FEATURE_ON_OFF: None, FEATURE_PLAY_PAUSE: None, @@ -69,36 +69,48 @@ async def test_media_player_set_state(hass, hk_driver): await hass.async_block_till_done() assert call_turn_on assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] is None await hass.async_add_job(acc.chars[FEATURE_ON_OFF] .client_update_value, False) await hass.async_block_till_done() assert call_turn_off assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id + assert len(events) == 2 + assert events[-1].data[ATTR_VALUE] is None await hass.async_add_job(acc.chars[FEATURE_PLAY_PAUSE] .client_update_value, True) await hass.async_block_till_done() assert call_media_play assert call_media_play[0].data[ATTR_ENTITY_ID] == entity_id + assert len(events) == 3 + assert events[-1].data[ATTR_VALUE] is None await hass.async_add_job(acc.chars[FEATURE_PLAY_PAUSE] .client_update_value, False) await hass.async_block_till_done() assert call_media_pause assert call_media_pause[0].data[ATTR_ENTITY_ID] == entity_id + assert len(events) == 4 + assert events[-1].data[ATTR_VALUE] is None await hass.async_add_job(acc.chars[FEATURE_PLAY_STOP] .client_update_value, True) await hass.async_block_till_done() assert call_media_play assert call_media_play[1].data[ATTR_ENTITY_ID] == entity_id + assert len(events) == 5 + assert events[-1].data[ATTR_VALUE] is None await hass.async_add_job(acc.chars[FEATURE_PLAY_STOP] .client_update_value, False) await hass.async_block_till_done() assert call_media_stop assert call_media_stop[0].data[ATTR_ENTITY_ID] == entity_id + assert len(events) == 6 + assert events[-1].data[ATTR_VALUE] is None await hass.async_add_job(acc.chars[FEATURE_TOGGLE_MUTE] .client_update_value, True) @@ -106,6 +118,8 @@ async def test_media_player_set_state(hass, hk_driver): assert call_toggle_mute assert call_toggle_mute[0].data[ATTR_ENTITY_ID] == entity_id assert call_toggle_mute[0].data[ATTR_MEDIA_VOLUME_MUTED] is True + assert len(events) == 7 + assert events[-1].data[ATTR_VALUE] is None await hass.async_add_job(acc.chars[FEATURE_TOGGLE_MUTE] .client_update_value, False) @@ -113,3 +127,5 @@ async def test_media_player_set_state(hass, hk_driver): assert call_toggle_mute assert call_toggle_mute[1].data[ATTR_ENTITY_ID] == entity_id assert call_toggle_mute[1].data[ATTR_MEDIA_VOLUME_MUTED] is False + assert len(events) == 8 + assert events[-1].data[ATTR_VALUE] is None diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index 3ddce0f36eb..3753a1aa433 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -2,6 +2,7 @@ import pytest from homeassistant.components.alarm_control_panel import DOMAIN +from homeassistant.components.homekit.const import ATTR_VALUE from homeassistant.components.homekit.type_security_systems import \ SecuritySystem from homeassistant.const import ( @@ -12,7 +13,7 @@ from homeassistant.const import ( from tests.common import async_mock_service -async def test_switch_set_state(hass, hk_driver): +async def test_switch_set_state(hass, hk_driver, events): """Test if accessory and HA are updated accordingly.""" code = '1234' config = {ATTR_CODE: code} @@ -72,6 +73,8 @@ async def test_switch_set_state(hass, hk_driver): assert call_arm_home[0].data[ATTR_ENTITY_ID] == entity_id assert call_arm_home[0].data[ATTR_CODE] == code assert acc.char_target_state.value == 0 + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] is None await hass.async_add_job(acc.char_target_state.client_update_value, 1) await hass.async_block_till_done() @@ -79,6 +82,8 @@ async def test_switch_set_state(hass, hk_driver): assert call_arm_away[0].data[ATTR_ENTITY_ID] == entity_id assert call_arm_away[0].data[ATTR_CODE] == code assert acc.char_target_state.value == 1 + assert len(events) == 2 + assert events[-1].data[ATTR_VALUE] is None await hass.async_add_job(acc.char_target_state.client_update_value, 2) await hass.async_block_till_done() @@ -86,6 +91,8 @@ async def test_switch_set_state(hass, hk_driver): assert call_arm_night[0].data[ATTR_ENTITY_ID] == entity_id assert call_arm_night[0].data[ATTR_CODE] == code assert acc.char_target_state.value == 2 + assert len(events) == 3 + assert events[-1].data[ATTR_VALUE] is None await hass.async_add_job(acc.char_target_state.client_update_value, 3) await hass.async_block_till_done() @@ -93,10 +100,12 @@ async def test_switch_set_state(hass, hk_driver): assert call_disarm[0].data[ATTR_ENTITY_ID] == entity_id assert call_disarm[0].data[ATTR_CODE] == code assert acc.char_target_state.value == 3 + assert len(events) == 4 + assert events[-1].data[ATTR_VALUE] is None @pytest.mark.parametrize('config', [{}, {ATTR_CODE: None}]) -async def test_no_alarm_code(hass, hk_driver, config): +async def test_no_alarm_code(hass, hk_driver, config, events): """Test accessory if security_system doesn't require an alarm_code.""" entity_id = 'alarm_control_panel.test' @@ -114,3 +123,5 @@ async def test_no_alarm_code(hass, hk_driver, config): assert call_arm_home[0].data[ATTR_ENTITY_ID] == entity_id assert ATTR_CODE not in call_arm_home[0].data assert acc.char_target_state.value == 0 + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] is None diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index bc44a93884a..d170647d492 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -2,7 +2,7 @@ import pytest from homeassistant.components.homekit.const import ( - TYPE_FAUCET, TYPE_SHOWER, TYPE_SPRINKLER, TYPE_VALVE) + ATTR_VALUE, TYPE_FAUCET, TYPE_SHOWER, TYPE_SPRINKLER, TYPE_VALVE) from homeassistant.components.homekit.type_switches import ( Outlet, Switch, Valve) from homeassistant.const import ATTR_ENTITY_ID, CONF_TYPE, STATE_OFF, STATE_ON @@ -11,7 +11,7 @@ from homeassistant.core import split_entity_id from tests.common import async_mock_service -async def test_outlet_set_state(hass, hk_driver): +async def test_outlet_set_state(hass, hk_driver, events): """Test if Outlet accessory and HA are updated accordingly.""" entity_id = 'switch.outlet_test' @@ -43,11 +43,15 @@ async def test_outlet_set_state(hass, hk_driver): await hass.async_block_till_done() assert call_turn_on assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] is None await hass.async_add_job(acc.char_on.client_update_value, False) await hass.async_block_till_done() assert call_turn_off assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id + assert len(events) == 2 + assert events[-1].data[ATTR_VALUE] is None @pytest.mark.parametrize('entity_id', [ @@ -57,7 +61,7 @@ async def test_outlet_set_state(hass, hk_driver): 'script.test', 'switch.test', ]) -async def test_switch_set_state(hass, hk_driver, entity_id): +async def test_switch_set_state(hass, hk_driver, entity_id, events): """Test if accessory and HA are updated accordingly.""" domain = split_entity_id(entity_id)[0] @@ -88,14 +92,18 @@ async def test_switch_set_state(hass, hk_driver, entity_id): await hass.async_block_till_done() assert call_turn_on assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] is None await hass.async_add_job(acc.char_on.client_update_value, False) await hass.async_block_till_done() assert call_turn_off assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id + assert len(events) == 2 + assert events[-1].data[ATTR_VALUE] is None -async def test_valve_set_state(hass, hk_driver): +async def test_valve_set_state(hass, hk_driver, events): """Test if Valve accessory and HA are updated accordingly.""" entity_id = 'switch.valve_test' @@ -154,9 +162,13 @@ async def test_valve_set_state(hass, hk_driver): assert acc.char_in_use.value is True assert call_turn_on assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] is None await hass.async_add_job(acc.char_active.client_update_value, False) await hass.async_block_till_done() assert acc.char_in_use.value is False assert call_turn_off assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id + assert len(events) == 2 + assert events[-1].data[ATTR_VALUE] is None diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 687a9e9513c..e3187b6cf02 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -10,7 +10,7 @@ from homeassistant.components.climate import ( ATTR_OPERATION_LIST, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN, STATE_AUTO, STATE_COOL, STATE_HEAT) from homeassistant.components.homekit.const import ( - PROP_MAX_VALUE, PROP_MIN_VALUE) + ATTR_VALUE, PROP_MAX_VALUE, PROP_MIN_VALUE) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_TEMPERATURE_UNIT, STATE_OFF, TEMP_FAHRENHEIT) @@ -31,7 +31,7 @@ def cls(): patcher.stop() -async def test_default_thermostat(hass, hk_driver, cls): +async def test_default_thermostat(hass, hk_driver, cls, events): """Test if accessory and HA are updated accordingly.""" entity_id = 'climate.test' @@ -157,6 +157,9 @@ async def test_default_thermostat(hass, hk_driver, cls): assert call_set_temperature[0].data[ATTR_ENTITY_ID] == entity_id assert call_set_temperature[0].data[ATTR_TEMPERATURE] == 19.0 assert acc.char_target_temp.value == 19.0 + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] == "target {}°C".format( + acc.char_target_temp.value) await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 1) await hass.async_block_till_done() @@ -164,9 +167,11 @@ async def test_default_thermostat(hass, hk_driver, cls): assert call_set_operation_mode[0].data[ATTR_ENTITY_ID] == entity_id assert call_set_operation_mode[0].data[ATTR_OPERATION_MODE] == STATE_HEAT assert acc.char_target_heat_cool.value == 1 + assert len(events) == 2 + assert events[-1].data[ATTR_VALUE] == STATE_HEAT -async def test_auto_thermostat(hass, hk_driver, cls): +async def test_auto_thermostat(hass, hk_driver, cls, events): """Test if accessory and HA are updated accordingly.""" entity_id = 'climate.test' @@ -238,6 +243,9 @@ async def test_auto_thermostat(hass, hk_driver, cls): assert call_set_temperature[0].data[ATTR_ENTITY_ID] == entity_id assert call_set_temperature[0].data[ATTR_TARGET_TEMP_LOW] == 20.0 assert acc.char_heating_thresh_temp.value == 20.0 + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] == "heating threshold {}°C".format( + acc.char_heating_thresh_temp.value) await hass.async_add_job( acc.char_cooling_thresh_temp.client_update_value, 25.0) @@ -246,9 +254,12 @@ async def test_auto_thermostat(hass, hk_driver, cls): assert call_set_temperature[1].data[ATTR_ENTITY_ID] == entity_id assert call_set_temperature[1].data[ATTR_TARGET_TEMP_HIGH] == 25.0 assert acc.char_cooling_thresh_temp.value == 25.0 + assert len(events) == 2 + assert events[-1].data[ATTR_VALUE] == "cooling threshold {}°C".format( + acc.char_cooling_thresh_temp.value) -async def test_power_state(hass, hk_driver, cls): +async def test_power_state(hass, hk_driver, cls, events): """Test if accessory and HA are updated accordingly.""" entity_id = 'climate.test' @@ -297,15 +308,19 @@ async def test_power_state(hass, hk_driver, cls): assert call_set_operation_mode[0].data[ATTR_ENTITY_ID] == entity_id assert call_set_operation_mode[0].data[ATTR_OPERATION_MODE] == STATE_HEAT assert acc.char_target_heat_cool.value == 1 + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] == STATE_HEAT await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 0) await hass.async_block_till_done() assert call_turn_off assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id assert acc.char_target_heat_cool.value == 0 + assert len(events) == 2 + assert events[-1].data[ATTR_VALUE] is None -async def test_thermostat_fahrenheit(hass, hk_driver, cls): +async def test_thermostat_fahrenheit(hass, hk_driver, cls, events): """Test if accessory and HA are updated accordingly.""" entity_id = 'climate.test' @@ -341,6 +356,8 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls): assert call_set_temperature[0].data[ATTR_ENTITY_ID] == entity_id assert call_set_temperature[0].data[ATTR_TARGET_TEMP_HIGH] == 73.4 assert call_set_temperature[0].data[ATTR_TARGET_TEMP_LOW] == 68 + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] == "cooling threshold 73.4°F" await hass.async_add_job( acc.char_heating_thresh_temp.client_update_value, 22) @@ -349,12 +366,16 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls): assert call_set_temperature[1].data[ATTR_ENTITY_ID] == entity_id assert call_set_temperature[1].data[ATTR_TARGET_TEMP_HIGH] == 73.4 assert call_set_temperature[1].data[ATTR_TARGET_TEMP_LOW] == 71.6 + assert len(events) == 2 + assert events[-1].data[ATTR_VALUE] == "heating threshold 71.6°F" await hass.async_add_job(acc.char_target_temp.client_update_value, 24.0) await hass.async_block_till_done() assert call_set_temperature[2] assert call_set_temperature[2].data[ATTR_ENTITY_ID] == entity_id assert call_set_temperature[2].data[ATTR_TEMPERATURE] == 75.2 + assert len(events) == 3 + assert events[-1].data[ATTR_VALUE] == "target 75.2°F" async def test_get_temperature_range(hass, hk_driver, cls): diff --git a/tests/components/test_logbook.py b/tests/components/test_logbook.py index 3bb3ae57c68..1100a16b381 100644 --- a/tests/components/test_logbook.py +++ b/tests/components/test_logbook.py @@ -7,11 +7,15 @@ import unittest from homeassistant.components import sun import homeassistant.core as ha from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_SERVICE, EVENT_STATE_CHANGED, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, ATTR_HIDDEN, STATE_NOT_HOME, STATE_ON, STATE_OFF) import homeassistant.util.dt as dt_util from homeassistant.components import logbook, recorder from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME +from homeassistant.components.homekit.const import ( + ATTR_DISPLAY_NAME, ATTR_VALUE, DOMAIN as DOMAIN_HOMEKIT, + EVENT_HOMEKIT_CHANGED) from homeassistant.setup import setup_component, async_setup_component from tests.common import ( @@ -684,3 +688,31 @@ async def test_humanify_alexa_event(hass): assert event3['message'] == \ 'send command Alexa.PowerController/TurnOn for light.non_existing' assert event3['entity_id'] == 'light.non_existing' + + +async def test_humanify_homekit_changed_event(hass): + """Test humanifying HomeKit changed event.""" + event1, event2 = list(logbook.humanify(hass, [ + ha.Event(EVENT_HOMEKIT_CHANGED, { + ATTR_ENTITY_ID: 'lock.front_door', + ATTR_DISPLAY_NAME: 'Front Door', + ATTR_SERVICE: 'lock', + }), + ha.Event(EVENT_HOMEKIT_CHANGED, { + ATTR_ENTITY_ID: 'cover.window', + ATTR_DISPLAY_NAME: 'Window', + ATTR_SERVICE: 'set_cover_position', + ATTR_VALUE: 75, + }), + ])) + + assert event1['name'] == 'HomeKit' + assert event1['domain'] == DOMAIN_HOMEKIT + assert event1['message'] == 'send command lock for Front Door' + assert event1['entity_id'] == 'lock.front_door' + + assert event2['name'] == 'HomeKit' + assert event1['domain'] == DOMAIN_HOMEKIT + assert event2['message'] == \ + 'send command set_cover_position to 75 for Window' + assert event2['entity_id'] == 'cover.window' From c6d9ceca63d569cb139a2fe2437c5ad08dd053ee Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Tue, 16 Oct 2018 14:45:39 +0300 Subject: [PATCH 180/265] Bump hdate version to 0.6.5 (#17510) * Bump hdate version to 0.6.4 * Bump to 0.6.5 * Change test so we check when passing tzinfo object --- homeassistant/components/sensor/jewish_calendar.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/sensor/test_jewish_calendar.py | 3 ++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/jewish_calendar.py b/homeassistant/components/sensor/jewish_calendar.py index ad024547d34..e1225b8f25d 100644 --- a/homeassistant/components/sensor/jewish_calendar.py +++ b/homeassistant/components/sensor/jewish_calendar.py @@ -15,7 +15,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util -REQUIREMENTS = ['hdate==0.6.3'] +REQUIREMENTS = ['hdate==0.6.5'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 3c2d1df2545..6f8bb03d5a9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -448,7 +448,7 @@ hangups==0.4.5 hbmqtt==0.9.4 # homeassistant.components.sensor.jewish_calendar -hdate==0.6.3 +hdate==0.6.5 # homeassistant.components.climate.heatmiser heatmiserV3==0.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9dd81e1a6c9..b3fe5ebfeed 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -91,7 +91,7 @@ hangups==0.4.5 hbmqtt==0.9.4 # homeassistant.components.sensor.jewish_calendar -hdate==0.6.3 +hdate==0.6.5 # homeassistant.components.binary_sensor.workday holidays==0.9.7 diff --git a/tests/components/sensor/test_jewish_calendar.py b/tests/components/sensor/test_jewish_calendar.py index 4357de26554..ba3a11d862b 100644 --- a/tests/components/sensor/test_jewish_calendar.py +++ b/tests/components/sensor/test_jewish_calendar.py @@ -5,6 +5,7 @@ from datetime import datetime as dt from unittest.mock import patch from homeassistant.util.async_ import run_coroutine_threadsafe +from homeassistant.util.dt import get_time_zone from homeassistant.setup import setup_component from homeassistant.components.sensor.jewish_calendar import JewishCalSensor from tests.common import get_test_home_assistant @@ -138,7 +139,7 @@ class TestJewishCalenderSensor(unittest.TestCase): sensor = JewishCalSensor( name='test', language='hebrew', sensor_type='first_stars', latitude=40.7128, longitude=-74.0060, - timezone="America/New_York", diaspora=False) + timezone=get_time_zone("America/New_York"), diaspora=False) with patch('homeassistant.util.dt.now', return_value=test_time): run_coroutine_threadsafe( sensor.async_update(), self.hass.loop).result() From 9c52a3ce223c06f2d29b0e61c59d660dd180e5b8 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Tue, 16 Oct 2018 08:58:25 -0400 Subject: [PATCH 181/265] Z-Wave Device Registry Support (#17291) * Add device_registry support for sensor and switch domains * Add device_registry support for light * Add device registry to binary_sensor, climate, cover * Add device registry to zwave fan * Fix test for config entry loading * lint * revert erroneous modification * Revert device_registry.py change --- .../components/binary_sensor/zwave.py | 22 +++++++++++-- homeassistant/components/climate/zwave.py | 21 +++++++++++-- homeassistant/components/cover/zwave.py | 22 +++++++++++-- homeassistant/components/fan/zwave.py | 19 +++++++++++- homeassistant/components/light/zwave.py | 19 +++++++++++- homeassistant/components/sensor/zwave.py | 19 +++++++++++- homeassistant/components/switch/zwave.py | 23 ++++++++++++-- homeassistant/components/zwave/__init__.py | 30 +++++++++++++++--- homeassistant/components/zwave/node_entity.py | 14 ++++++++- tests/components/zwave/test_init.py | 31 ++++++++++--------- 10 files changed, 189 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/binary_sensor/zwave.py b/homeassistant/components/binary_sensor/zwave.py index 3bb3a3c79c5..ca07986976d 100644 --- a/homeassistant/components/binary_sensor/zwave.py +++ b/homeassistant/components/binary_sensor/zwave.py @@ -7,10 +7,11 @@ https://home-assistant.io/components/binary_sensor.zwave/ import logging import datetime import homeassistant.util.dt as dt_util +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import track_point_in_time from homeassistant.components import zwave -from homeassistant.components.zwave import ( # noqa pylint: disable=unused-import - async_setup_platform, workaround) +from homeassistant.components.zwave import workaround from homeassistant.components.binary_sensor import ( DOMAIN, BinarySensorDevice) @@ -19,6 +20,23 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = [] +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Old method of setting up Z-Wave binary sensors.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Z-Wave binary sensors from Config Entry.""" + @callback + def async_add_binary_sensor(binary_sensor): + """Add Z-Wave binary sensor.""" + async_add_entities([binary_sensor]) + + async_dispatcher_connect(hass, 'zwave_new_binary_sensor', + async_add_binary_sensor) + + def get_device(values, **kwargs): """Create Z-Wave entity device.""" device_mapping = workaround.get_device_mapping(values.primary) diff --git a/homeassistant/components/climate/zwave.py b/homeassistant/components/climate/zwave.py index 77b5e111686..561af9c9f57 100644 --- a/homeassistant/components/climate/zwave.py +++ b/homeassistant/components/climate/zwave.py @@ -6,14 +6,15 @@ https://home-assistant.io/components/climate.zwave/ """ # Because we do not compile openzwave on CI import logging +from homeassistant.core import callback from homeassistant.components.climate import ( DOMAIN, ClimateDevice, STATE_AUTO, STATE_COOL, STATE_HEAT, SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE) -from homeassistant.components.zwave import ( # noqa pylint: disable=unused-import - ZWaveDeviceEntity, async_setup_platform) +from homeassistant.components.zwave import ZWaveDeviceEntity from homeassistant.const import ( STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) +from homeassistant.helpers.dispatcher import async_dispatcher_connect _LOGGER = logging.getLogger(__name__) @@ -42,6 +43,22 @@ STATE_MAPPINGS = { } +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Old method of setting up Z-Wave climate devices.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Z-Wave Climate device from Config Entry.""" + @callback + def async_add_climate(climate): + """Add Z-Wave Climate Device.""" + async_add_entities([climate]) + + async_dispatcher_connect(hass, 'zwave_new_climate', async_add_climate) + + def get_device(hass, values, **kwargs): """Create Z-Wave entity device.""" temp_unit = hass.config.units.temperature_unit diff --git a/homeassistant/components/cover/zwave.py b/homeassistant/components/cover/zwave.py index ade44faeab5..835305449e8 100644 --- a/homeassistant/components/cover/zwave.py +++ b/homeassistant/components/cover/zwave.py @@ -5,18 +5,36 @@ For more details about this platform, please refer to the documentation https://home-assistant.io/components/cover.zwave/ """ import logging +from homeassistant.core import callback from homeassistant.components.cover import ( DOMAIN, SUPPORT_OPEN, SUPPORT_CLOSE, ATTR_POSITION) from homeassistant.components import zwave -from homeassistant.components.zwave import ( # noqa pylint: disable=unused-import - ZWaveDeviceEntity, async_setup_platform, workaround) +from homeassistant.components.zwave import ( + ZWaveDeviceEntity, workaround) from homeassistant.components.cover import CoverDevice +from homeassistant.helpers.dispatcher import async_dispatcher_connect _LOGGER = logging.getLogger(__name__) SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Old method of setting up Z-Wave covers.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Z-Wave Cover from Config Entry.""" + @callback + def async_add_cover(cover): + """Add Z-Wave Cover.""" + async_add_entities([cover]) + + async_dispatcher_connect(hass, 'zwave_new_cover', async_add_cover) + + def get_device(hass, values, node_config, **kwargs): """Create Z-Wave entity device.""" invert_buttons = node_config.get(zwave.CONF_INVERT_OPENCLOSE_BUTTONS) diff --git a/homeassistant/components/fan/zwave.py b/homeassistant/components/fan/zwave.py index 645cb033e13..4b4204aa454 100644 --- a/homeassistant/components/fan/zwave.py +++ b/homeassistant/components/fan/zwave.py @@ -7,11 +7,12 @@ https://home-assistant.io/components/fan.zwave/ import logging import math +from homeassistant.core import callback from homeassistant.components.fan import ( DOMAIN, FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, SUPPORT_SET_SPEED) from homeassistant.components import zwave -from homeassistant.components.zwave import async_setup_platform # noqa pylint: disable=unused-import +from homeassistant.helpers.dispatcher import async_dispatcher_connect _LOGGER = logging.getLogger(__name__) @@ -35,6 +36,22 @@ SPEED_TO_VALUE = { } +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Old method of setting up Z-Wave fans.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Z-Wave Fan from Config Entry.""" + @callback + def async_add_fan(fan): + """Add Z-Wave Fan.""" + async_add_entities([fan]) + + async_dispatcher_connect(hass, 'zwave_new_fan', async_add_fan) + + def get_device(values, **kwargs): """Create Z-Wave entity device.""" return ZwaveFan(values) diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py index 1e768eb127a..09f3709f216 100644 --- a/homeassistant/components/light/zwave.py +++ b/homeassistant/components/light/zwave.py @@ -7,13 +7,14 @@ https://home-assistant.io/components/light.zwave/ import logging from threading import Timer +from homeassistant.core import callback from homeassistant.components.light import ( ATTR_WHITE_VALUE, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, DOMAIN, Light) from homeassistant.components import zwave -from homeassistant.components.zwave import async_setup_platform # noqa pylint: disable=unused-import from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -43,6 +44,22 @@ TEMP_WARM_HASS = (TEMP_COLOR_MAX - TEMP_COLOR_MIN) / 3 * 2 + TEMP_COLOR_MIN TEMP_COLD_HASS = (TEMP_COLOR_MAX - TEMP_COLOR_MIN) / 3 + TEMP_COLOR_MIN +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Old method of setting up Z-Wave lights.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Z-Wave Light from Config Entry.""" + @callback + def async_add_light(light): + """Add Z-Wave Light.""" + async_add_entities([light]) + + async_dispatcher_connect(hass, 'zwave_new_light', async_add_light) + + def get_device(node, values, node_config, **kwargs): """Create Z-Wave entity device.""" refresh = node_config.get(zwave.CONF_REFRESH_VALUE) diff --git a/homeassistant/components/sensor/zwave.py b/homeassistant/components/sensor/zwave.py index c6356efe157..ce25b61146b 100644 --- a/homeassistant/components/sensor/zwave.py +++ b/homeassistant/components/sensor/zwave.py @@ -5,14 +5,31 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.zwave/ """ import logging +from homeassistant.core import callback from homeassistant.components.sensor import DOMAIN from homeassistant.components import zwave from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT -from homeassistant.components.zwave import async_setup_platform # noqa pylint: disable=unused-import +from homeassistant.helpers.dispatcher import async_dispatcher_connect _LOGGER = logging.getLogger(__name__) +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Old method of setting up Z-Wave sensors.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Z-Wave Sensor from Config Entry.""" + @callback + def async_add_sensor(sensor): + """Add Z-Wave Sensor.""" + async_add_entities([sensor]) + + async_dispatcher_connect(hass, 'zwave_new_sensor', async_add_sensor) + + def get_device(node, values, **kwargs): """Create Z-Wave entity device.""" # Generic Device mappings diff --git a/homeassistant/components/switch/zwave.py b/homeassistant/components/switch/zwave.py index 31f942bd3af..54a2a729d04 100644 --- a/homeassistant/components/switch/zwave.py +++ b/homeassistant/components/switch/zwave.py @@ -6,13 +6,30 @@ https://home-assistant.io/components/switch.zwave/ """ import logging import time +from homeassistant.core import callback from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.components import zwave -from homeassistant.components.zwave import workaround, async_setup_platform # noqa pylint: disable=unused-import +from homeassistant.helpers.dispatcher import async_dispatcher_connect _LOGGER = logging.getLogger(__name__) +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Old method of setting up Z-Wave switches.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Z-Wave Switch from Config Entry.""" + @callback + def async_add_switch(switch): + """Add Z-Wave Switch.""" + async_add_entities([switch]) + + async_dispatcher_connect(hass, 'zwave_new_switch', async_add_switch) + + def get_device(values, **kwargs): """Create zwave entity device.""" return ZwaveSwitch(values) @@ -25,8 +42,8 @@ class ZwaveSwitch(zwave.ZWaveDeviceEntity, SwitchDevice): """Initialize the Z-Wave switch device.""" zwave.ZWaveDeviceEntity.__init__(self, values, DOMAIN) self.refresh_on_update = ( - workaround.get_device_mapping(values.primary) == - workaround.WORKAROUND_REFRESH_NODE_ON_UPDATE) + zwave.workaround.get_device_mapping(values.primary) == + zwave.workaround.WORKAROUND_REFRESH_NODE_ON_UPDATE) self.last_update = time.perf_counter() self._state = self.values.primary.data diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index b0ea5201812..74678cda0fc 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -66,6 +66,9 @@ DEFAULT_CONF_INVERT_OPENCLOSE_BUTTONS = False DEFAULT_CONF_REFRESH_VALUE = False DEFAULT_CONF_REFRESH_DELAY = 5 +SUPPORTED_PLATFORMS = ['binary_sensor', 'climate', 'fan', + 'light', 'sensor', 'switch'] + RENAME_NODE_SCHEMA = vol.Schema({ vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), vol.Required(const.ATTR_NAME): cv.string, @@ -224,7 +227,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info[const.DISCOVERY_DEVICE], None) if device is None: return False - async_add_entities([device]) return True @@ -777,6 +779,10 @@ async def async_setup_entry(hass, config_entry): hass.services.async_register(DOMAIN, const.SERVICE_START_NETWORK, start_zwave) + for entry_component in SUPPORTED_PLATFORMS: + hass.async_create_task(hass.config_entries.async_forward_entry_setup( + config_entry, entry_component)) + return True @@ -928,9 +934,13 @@ class ZWaveDeviceEntityValues(): async def discover_device(component, device, dict_id): """Put device in a dictionary and call discovery on it.""" self._hass.data[DATA_DEVICES][dict_id] = device - await discovery.async_load_platform( - self._hass, component, DOMAIN, - {const.DISCOVERY_DEVICE: dict_id}, self._zwave_config) + if component in SUPPORTED_PLATFORMS: + async_dispatcher_send( + self._hass, 'zwave_new_{}'.format(component), device) + else: + await discovery.async_load_platform( + self._hass, component, DOMAIN, + {const.DISCOVERY_DEVICE: dict_id}, self._zwave_config) if device.unique_id: self._hass.add_job(discover_device, component, device, dict_id) @@ -1010,6 +1020,18 @@ class ZWaveDeviceEntity(ZWaveBaseEntity): """Return a unique ID.""" return self._unique_id + @property + def device_info(self): + """Return device information.""" + return { + 'identifiers': { + (DOMAIN, self.node_id) + }, + 'manufacturer': self.node.manufacturer_name, + 'model': self.node.product_name, + 'name': node_name(self.node), + } + @property def name(self): """Return the name of the device.""" diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py index 94de03686d3..2339b8aba36 100644 --- a/homeassistant/components/zwave/node_entity.py +++ b/homeassistant/components/zwave/node_entity.py @@ -8,7 +8,7 @@ from homeassistant.helpers.entity import Entity from .const import ( ATTR_NODE_ID, COMMAND_CLASS_WAKE_UP, ATTR_SCENE_ID, ATTR_SCENE_DATA, ATTR_BASIC_LEVEL, EVENT_NODE_EVENT, EVENT_SCENE_ACTIVATED, - COMMAND_CLASS_CENTRAL_SCENE) + COMMAND_CLASS_CENTRAL_SCENE, DOMAIN) from .util import node_name, is_node_parsed _LOGGER = logging.getLogger(__name__) @@ -110,6 +110,18 @@ class ZWaveNodeEntity(ZWaveBaseEntity): """Return unique ID of Z-wave node.""" return self._unique_id + @property + def device_info(self): + """Return device information.""" + return { + 'identifiers': { + (DOMAIN, self.node_id) + }, + 'manufacturer': self.node.manufacturer_name, + 'model': self.node.product_name, + 'name': node_name(self.node) + } + def network_node_changed(self, node=None, value=None, args=None): """Handle a changed node on the network.""" if node and node.node_id != self.node_id: diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index ef330b48f72..b06dfb683b7 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -703,21 +703,24 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): const.COMMAND_CLASS_SWITCH_BINARY], }}} - values = zwave.ZWaveDeviceEntityValues( - hass=self.hass, - schema=self.mock_schema, - primary_value=self.primary, - zwave_config=self.zwave_config, - device_config=self.device_config, - registry=self.registry - ) - values._check_entity_ready() - self.hass.block_till_done() + with patch.object(zwave, 'async_dispatcher_send') as \ + mock_dispatch_send: - assert discovery.async_load_platform.called - assert len(discovery.async_load_platform.mock_calls) == 1 - args = discovery.async_load_platform.mock_calls[0][1] - assert args[1] == 'binary_sensor' + values = zwave.ZWaveDeviceEntityValues( + hass=self.hass, + schema=self.mock_schema, + primary_value=self.primary, + zwave_config=self.zwave_config, + device_config=self.device_config, + registry=self.registry + ) + values._check_entity_ready() + self.hass.block_till_done() + + assert mock_dispatch_send.called + assert len(mock_dispatch_send.mock_calls) == 1 + args = mock_dispatch_send.mock_calls[0][1] + assert args[1] == 'zwave_new_binary_sensor' @patch.object(zwave, 'get_platform') @patch.object(zwave, 'discovery') From 764ea06795ab46c9f1b85a63d206436b9e1a34fc Mon Sep 17 00:00:00 2001 From: Ben Lebherz Date: Tue, 16 Oct 2018 16:41:38 +0200 Subject: [PATCH 182/265] Fix unhandled exception which creates many useless logs (#17508) * Fix unhandled exception which creates many useless logs * recover old component logic, sorry * remove inline conditional --- homeassistant/components/media_player/horizon.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_player/horizon.py b/homeassistant/components/media_player/horizon.py index 04471c69b9c..058796ea46d 100644 --- a/homeassistant/components/media_player/horizon.py +++ b/homeassistant/components/media_player/horizon.py @@ -92,9 +92,12 @@ class HorizonDevice(MediaPlayerDevice): @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) def update(self): """Update State using the media server running on the Horizon.""" - if self._client.is_powered_on(): - self._state = STATE_PLAYING - else: + try: + if self._client.is_powered_on(): + self._state = STATE_PLAYING + else: + self._state = STATE_OFF + except OSError: self._state = STATE_OFF def turn_on(self): From 6235aae196b97cf41311f5c4eb3c929529678596 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Tue, 16 Oct 2018 20:08:04 +0200 Subject: [PATCH 183/265] Update mill library (#17520) --- homeassistant/components/climate/mill.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/climate/mill.py b/homeassistant/components/climate/mill.py index 5185cf115e2..763e239689b 100644 --- a/homeassistant/components/climate/mill.py +++ b/homeassistant/components/climate/mill.py @@ -17,7 +17,7 @@ from homeassistant.const import ( from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -REQUIREMENTS = ['millheater==0.1.1'] +REQUIREMENTS = ['millheater==0.1.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 6f8bb03d5a9..12c26ddf1a8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -606,7 +606,7 @@ mficlient==0.3.0 miflora==0.4.0 # homeassistant.components.climate.mill -millheater==0.1.1 +millheater==0.1.2 # homeassistant.components.sensor.mitemp_bt mitemp_bt==0.0.1 From aa176312a5be87b7963900784a896161108a21fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Tue, 16 Oct 2018 20:08:34 +0200 Subject: [PATCH 184/265] Update switchmate library (#17519) --- homeassistant/components/switch/switchmate.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/switchmate.py b/homeassistant/components/switch/switchmate.py index 4955d72c5e3..2ec77a38267 100644 --- a/homeassistant/components/switch/switchmate.py +++ b/homeassistant/components/switch/switchmate.py @@ -13,7 +13,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA from homeassistant.const import CONF_NAME, CONF_MAC -REQUIREMENTS = ['pySwitchmate==0.4.1'] +REQUIREMENTS = ['pySwitchmate==0.4.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 12c26ddf1a8..65f7197f9de 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -779,7 +779,7 @@ pyMetno==0.3.0 pyRFXtrx==0.23 # homeassistant.components.switch.switchmate -pySwitchmate==0.4.1 +pySwitchmate==0.4.2 # homeassistant.components.tibber pyTibber==0.7.2 From 6b3e4ca7bd167295e7984d2c00b2c4c5e748781a Mon Sep 17 00:00:00 2001 From: Marcel Hoppe Date: Tue, 16 Oct 2018 20:09:34 +0200 Subject: [PATCH 185/265] update hangups to 0.4.6 and fix Issue #16593 hangouts reconnects. (#17518) --- homeassistant/components/hangouts/__init__.py | 2 +- homeassistant/components/hangouts/hangouts_bot.py | 13 ++++++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/hangouts/__init__.py b/homeassistant/components/hangouts/__init__.py index 8480ae09549..5d8a167d2d9 100644 --- a/homeassistant/components/hangouts/__init__.py +++ b/homeassistant/components/hangouts/__init__.py @@ -27,7 +27,7 @@ from .const import ( # We need an import from .config_flow, without it .config_flow is never loaded. from .config_flow import HangoutsFlowHandler # noqa: F401 -REQUIREMENTS = ['hangups==0.4.5'] +REQUIREMENTS = ['hangups==0.4.6'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hangouts/hangouts_bot.py b/homeassistant/components/hangouts/hangouts_bot.py index bceedb1acfa..ed041a30ce6 100644 --- a/homeassistant/components/hangouts/hangouts_bot.py +++ b/homeassistant/components/hangouts/hangouts_bot.py @@ -191,16 +191,19 @@ class HangoutsBot: self._connected = True dispatcher.async_dispatcher_send(self.hass, EVENT_HANGOUTS_CONNECTED) - def _on_disconnect(self): + async def _on_disconnect(self): """Handle disconnecting.""" - _LOGGER.debug('Connection lost!') - self._connected = False - dispatcher.async_dispatcher_send(self.hass, - EVENT_HANGOUTS_DISCONNECTED) + if self._connected: + _LOGGER.debug('Connection lost! Reconnect...') + await self.async_connect() + else: + dispatcher.async_dispatcher_send(self.hass, + EVENT_HANGOUTS_DISCONNECTED) async def async_disconnect(self): """Disconnect the client if it is connected.""" if self._connected: + self._connected = False await self._client.disconnect() async def async_handle_hass_stop(self, _): diff --git a/requirements_all.txt b/requirements_all.txt index 65f7197f9de..7820ed6ba86 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -442,7 +442,7 @@ ha-philipsjs==0.0.5 habitipy==0.2.0 # homeassistant.components.hangouts -hangups==0.4.5 +hangups==0.4.6 # homeassistant.components.mqtt.server hbmqtt==0.9.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b3fe5ebfeed..bf70cf82104 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -85,7 +85,7 @@ georss_client==0.3 ha-ffmpeg==1.9 # homeassistant.components.hangouts -hangups==0.4.5 +hangups==0.4.6 # homeassistant.components.mqtt.server hbmqtt==0.9.4 From 83db673bd085a216efd4d0cbfcd8d5d36cd4cf39 Mon Sep 17 00:00:00 2001 From: Brant Knudson Date: Tue, 16 Oct 2018 16:27:57 -0500 Subject: [PATCH 186/265] Add unique_id to Vera entities (#17450) I believe this adds registry support. The UI allows me to change the entity ID now. For example, a light bulb called "BasementHallLight" in the Vera has an initial Entity ID like light.basementhalllight_108, where 108 is the unique ID that the Vera assigned the device when I added it to the z-wave network. Now I can use the UI to change the Entity ID to light.basementhalllight and I can still turn it on and off. --- homeassistant/components/vera.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/vera.py b/homeassistant/components/vera.py index fb8af0d8855..127cd008a3a 100644 --- a/homeassistant/components/vera.py +++ b/homeassistant/components/vera.py @@ -195,3 +195,11 @@ class VeraDevice(Entity): attr['Vera Device Id'] = self.vera_device.vera_device_id return attr + + @property + def unique_id(self) -> str: + """Return a unique ID. + + The Vera assigns a unique and immutable ID number to each device. + """ + return str(self.vera_device.vera_device_id) From 11004bcf34b836c2f65a93f08828396e084b5a69 Mon Sep 17 00:00:00 2001 From: Nate Clark Date: Tue, 16 Oct 2018 18:39:32 -0400 Subject: [PATCH 187/265] Manual IP & port configuration for Konnected devices (#17120) * add capability for manually specifying IP and port of Konnected device(s) * add config options for blink and discovery settings * import konnected only in functions where needed * updates from code review feedback * convert manual_discovery to async * code review updates; use correct sync vs async --- homeassistant/components/konnected.py | 75 +++++++++++++++++++++------ requirements_all.txt | 2 +- 2 files changed, 61 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/konnected.py b/homeassistant/components/konnected.py index 388fa41f36f..21e2fbba4c7 100644 --- a/homeassistant/components/konnected.py +++ b/homeassistant/components/konnected.py @@ -4,9 +4,11 @@ Support for Konnected devices. For more details about this component, please refer to the documentation at https://home-assistant.io/components/konnected/ """ -import logging +import asyncio import hmac import json +import logging + import voluptuous as vol from aiohttp.hdrs import AUTHORIZATION @@ -16,17 +18,18 @@ from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA from homeassistant.components.discovery import SERVICE_KONNECTED from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( - HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_UNAUTHORIZED, - CONF_DEVICES, CONF_BINARY_SENSORS, CONF_SWITCHES, CONF_HOST, CONF_PORT, - CONF_ID, CONF_NAME, CONF_TYPE, CONF_PIN, CONF_ZONE, CONF_ACCESS_TOKEN, - ATTR_ENTITY_ID, ATTR_STATE, STATE_ON) -from homeassistant.helpers.dispatcher import async_dispatcher_send + EVENT_HOMEASSISTANT_START, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, + HTTP_UNAUTHORIZED, CONF_DEVICES, CONF_BINARY_SENSORS, CONF_SWITCHES, + CONF_HOST, CONF_PORT, CONF_ID, CONF_NAME, CONF_TYPE, CONF_PIN, CONF_ZONE, + CONF_ACCESS_TOKEN, ATTR_ENTITY_ID, ATTR_STATE, STATE_ON) +from homeassistant.helpers.dispatcher import ( + async_dispatcher_send, dispatcher_send) from homeassistant.helpers import discovery from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['konnected==0.1.2'] +REQUIREMENTS = ['konnected==0.1.4'] DOMAIN = 'konnected' @@ -36,6 +39,8 @@ CONF_MOMENTARY = 'momentary' CONF_PAUSE = 'pause' CONF_REPEAT = 'repeat' CONF_INVERSE = 'inverse' +CONF_BLINK = 'blink' +CONF_DISCOVERY = 'discovery' STATE_LOW = 'low' STATE_HIGH = 'high' @@ -49,7 +54,7 @@ _BINARY_SENSOR_SCHEMA = vol.All( vol.Exclusive(CONF_ZONE, 's_pin'): vol.Any(*ZONE_TO_PIN), vol.Required(CONF_TYPE): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_INVERSE): cv.boolean, + vol.Optional(CONF_INVERSE, default=False): cv.boolean, }), cv.has_at_least_one_key(CONF_PIN, CONF_ZONE) ) @@ -81,6 +86,10 @@ CONFIG_SCHEMA = vol.Schema( cv.ensure_list, [_BINARY_SENSOR_SCHEMA]), vol.Optional(CONF_SWITCHES): vol.All( cv.ensure_list, [_SWITCH_SCHEMA]), + vol.Optional(CONF_HOST): cv.string, + vol.Optional(CONF_PORT): cv.port, + vol.Optional(CONF_BLINK, default=True): cv.boolean, + vol.Optional(CONF_DISCOVERY, default=True): cv.boolean, }], }), }, @@ -96,6 +105,8 @@ SIGNAL_SENSOR_UPDATE = 'konnected.{}.update' async def async_setup(hass, config): """Set up the Konnected platform.""" + import konnected + cfg = config.get(DOMAIN) if cfg is None: cfg = {} @@ -107,10 +118,8 @@ async def async_setup(hass, config): CONF_API_HOST: cfg.get(CONF_API_HOST) } - def device_discovered(service, info): - """Call when a Konnected device has been discovered.""" - host = info.get(CONF_HOST) - port = info.get(CONF_PORT) + def setup_device(host, port): + """Set up a Konnected device at `host` listening on `port`.""" discovered = DiscoveredDevice(hass, host, port) if discovered.is_configured: discovered.setup() @@ -119,6 +128,33 @@ async def async_setup(hass, config): " but not specified in configuration.yaml", discovered.device_id) + def device_discovered(service, info): + """Call when a Konnected device has been discovered.""" + host = info.get(CONF_HOST) + port = info.get(CONF_PORT) + setup_device(host, port) + + async def manual_discovery(event): + """Init devices on the network with manually assigned addresses.""" + specified = [dev for dev in cfg.get(CONF_DEVICES) if + dev.get(CONF_HOST) and dev.get(CONF_PORT)] + + while specified: + for dev in specified: + _LOGGER.debug("Discovering Konnected device %s at %s:%s", + dev.get(CONF_ID), + dev.get(CONF_HOST), + dev.get(CONF_PORT)) + try: + await hass.async_add_executor_job(setup_device, + dev.get(CONF_HOST), + dev.get(CONF_PORT)) + specified.remove(dev) + except konnected.Client.ClientError as err: + _LOGGER.error(err) + await asyncio.sleep(10) # try again in 10 seconds + + # Initialize devices specified in the configuration on boot for device in cfg.get(CONF_DEVICES): ConfiguredDevice(hass, device).save_data() @@ -128,6 +164,7 @@ async def async_setup(hass, config): device_discovered) hass.http.register_view(KonnectedView(access_token)) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, manual_discovery) return True @@ -188,6 +225,8 @@ class ConfiguredDevice: device_data = { CONF_BINARY_SENSORS: sensors, CONF_SWITCHES: actuators, + CONF_BLINK: self.config.get(CONF_BLINK), + CONF_DISCOVERY: self.config.get(CONF_DISCOVERY) } if CONF_DEVICES not in self.hass.data[DOMAIN]: @@ -271,7 +310,7 @@ class DiscoveredDevice: if sensor_config.get(CONF_INVERSE): state = not state - async_dispatcher_send( + dispatcher_send( self.hass, SIGNAL_SENSOR_UPDATE.format(entity_id), state) @@ -306,13 +345,19 @@ class DiscoveredDevice: if (desired_sensor_configuration != current_sensor_configuration) or \ (current_actuator_config != desired_actuator_config) or \ - (current_api_endpoint != desired_api_endpoint): + (current_api_endpoint != desired_api_endpoint) or \ + (self.status.get(CONF_BLINK) != + self.stored_configuration.get(CONF_BLINK)) or \ + (self.status.get(CONF_DISCOVERY) != + self.stored_configuration.get(CONF_DISCOVERY)): _LOGGER.info('pushing settings to device %s', self.device_id) self.client.put_settings( desired_sensor_configuration, desired_actuator_config, self.hass.data[DOMAIN].get(CONF_ACCESS_TOKEN), - desired_api_endpoint + desired_api_endpoint, + blink=self.stored_configuration.get(CONF_BLINK), + discovery=self.stored_configuration.get(CONF_DISCOVERY) ) diff --git a/requirements_all.txt b/requirements_all.txt index 7820ed6ba86..aa66a533bf0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -531,7 +531,7 @@ keyrings.alt==3.1 kiwiki-client==0.1.1 # homeassistant.components.konnected -konnected==0.1.2 +konnected==0.1.4 # homeassistant.components.eufy lakeside==0.10 From 5088e7ee499d7bccaeb34a063aae30a0204a404b Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Wed, 17 Oct 2018 02:38:03 -0400 Subject: [PATCH 188/265] Blink update - fixes #17316 (#17538) * Updgrae blinkpy to 0.10.0 - Remove status sensor (API endpoint unreliable for this) - Wifi strength reports in wifi bars rather than dBm (result of new API endpoint) - Added unique ids based on serial number * Update requirements --- .../components/alarm_control_panel/blink.py | 12 +++++++++--- homeassistant/components/binary_sensor/blink.py | 1 + homeassistant/components/blink/__init__.py | 6 ++---- homeassistant/components/camera/blink.py | 8 +++++++- homeassistant/components/sensor/blink.py | 6 ++++++ requirements_all.txt | 2 +- 6 files changed, 26 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/blink.py b/homeassistant/components/alarm_control_panel/blink.py index 850ac52fda4..728b5967db1 100644 --- a/homeassistant/components/alarm_control_panel/blink.py +++ b/homeassistant/components/alarm_control_panel/blink.py @@ -43,6 +43,11 @@ class BlinkSyncModule(AlarmControlPanel): self._name = name self._state = None + @property + def unique_id(self): + """Return the unique id for the sync module.""" + return self.sync.serial + @property def icon(self): """Return icon.""" @@ -61,9 +66,10 @@ class BlinkSyncModule(AlarmControlPanel): @property def device_state_attributes(self): """Return the state attributes.""" - return { - ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION, - } + attr = self.sync.attributes + attr['network_info'] = self.data.networks + attr[ATTR_ATTRIBUTION] = DEFAULT_ATTRIBUTION + return attr def update(self): """Update the state of the device.""" diff --git a/homeassistant/components/binary_sensor/blink.py b/homeassistant/components/binary_sensor/blink.py index 6519d09a29a..46751ce5394 100644 --- a/homeassistant/components/binary_sensor/blink.py +++ b/homeassistant/components/binary_sensor/blink.py @@ -36,6 +36,7 @@ class BlinkBinarySensor(BinarySensorDevice): self._icon = icon self._camera = data.sync.cameras[camera] self._state = None + self._unique_id = "{}-{}".format(self._camera.serial, self._type) @property def name(self): diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py index 1d84b5be113..abdbc1a2e92 100644 --- a/homeassistant/components/blink/__init__.py +++ b/homeassistant/components/blink/__init__.py @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_BINARY_SENSORS, CONF_SENSORS, CONF_FILENAME, CONF_MONITORED_CONDITIONS, TEMP_FAHRENHEIT) -REQUIREMENTS = ['blinkpy==0.9.0'] +REQUIREMENTS = ['blinkpy==0.10.0'] _LOGGER = logging.getLogger(__name__) @@ -36,7 +36,6 @@ TYPE_MOTION_DETECTED = 'motion_detected' TYPE_TEMPERATURE = 'temperature' TYPE_BATTERY = 'battery' TYPE_WIFI_STRENGTH = 'wifi_strength' -TYPE_STATUS = 'status' SERVICE_REFRESH = 'blink_update' SERVICE_TRIGGER = 'trigger_camera' @@ -50,8 +49,7 @@ BINARY_SENSORS = { SENSORS = { TYPE_TEMPERATURE: ['Temperature', TEMP_FAHRENHEIT, 'mdi:thermometer'], TYPE_BATTERY: ['Battery', '%', 'mdi:battery-80'], - TYPE_WIFI_STRENGTH: ['Wifi Signal', 'dBm', 'mdi:wifi-strength-2'], - TYPE_STATUS: ['Status', '', 'mdi:bell'] + TYPE_WIFI_STRENGTH: ['Wifi Signal', 'bars', 'mdi:wifi-strength-2'], } BINARY_SENSOR_SCHEMA = vol.Schema({ diff --git a/homeassistant/components/camera/blink.py b/homeassistant/components/camera/blink.py index 5a728e92ce3..510c2ab2563 100644 --- a/homeassistant/components/camera/blink.py +++ b/homeassistant/components/camera/blink.py @@ -38,6 +38,7 @@ class BlinkCamera(Camera): self.data = data self._name = "{} {}".format(BLINK_DATA, name) self._camera = camera + self._unique_id = "{}-camera".format(camera.serial) self.response = None self.current_image = None self.last_image = None @@ -48,6 +49,11 @@ class BlinkCamera(Camera): """Return the camera name.""" return self._name + @property + def unique_id(self): + """Return the unique camera id.""" + return self._unique_id + @property def device_state_attributes(self): """Return the camera attributes.""" @@ -64,7 +70,7 @@ class BlinkCamera(Camera): @property def motion_detection_enabled(self): """Return the state of the camera.""" - return self._camera.armed + return self._camera.motion_enabled @property def brand(self): diff --git a/homeassistant/components/sensor/blink.py b/homeassistant/components/sensor/blink.py index 885bb939edf..804f83de4fd 100644 --- a/homeassistant/components/sensor/blink.py +++ b/homeassistant/components/sensor/blink.py @@ -43,12 +43,18 @@ class BlinkSensor(Entity): self._state = None self._unit_of_measurement = units self._icon = icon + self._unique_id = "{}-{}".format(self._camera.serial, self._type) @property def name(self): """Return the name of the camera.""" return self._name + @property + def unique_id(self): + """Return the unique id for the camera sensor.""" + return self._unique_id + @property def icon(self): """Return the icon of the sensor.""" diff --git a/requirements_all.txt b/requirements_all.txt index aa66a533bf0..aaab9407864 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -185,7 +185,7 @@ bellows==0.7.0 bimmer_connected==0.5.3 # homeassistant.components.blink -blinkpy==0.9.0 +blinkpy==0.10.0 # homeassistant.components.light.blinksticklight blinkstick==1.1.8 From b7b4224429889ead7fb5d983b9b4d24ddaf37e2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 17 Oct 2018 10:00:15 +0300 Subject: [PATCH 189/265] Huawei LTE sensor improvements (#17533) * Sensor value formatting improvements * Make default names more consistent with other sensors * Improve unique id formatting --- homeassistant/components/sensor/huawei_lte.py | 57 ++++++++++++++----- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/sensor/huawei_lte.py b/homeassistant/components/sensor/huawei_lte.py index 092f9e777d1..5ff5f9b38ae 100644 --- a/homeassistant/components/sensor/huawei_lte.py +++ b/homeassistant/components/sensor/huawei_lte.py @@ -24,7 +24,7 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['huawei_lte'] -DEFAULT_NAME_TEMPLATE = 'Huawei {}: {}' +DEFAULT_NAME_TEMPLATE = 'Huawei {} {}' DEFAULT_SENSORS = [ "device_information.WanIPAddress", @@ -46,6 +46,26 @@ SENSOR_META = { name="WAN IPv6 address", icon="mdi:ip", ), + "device_signal.band": dict( + name="Band", + ), + "device_signal.cell_id": dict( + name="Cell ID", + ), + "device_signal.lac": dict( + name="LAC", + ), + "device_signal.mode": dict( + name="Mode", + formatter=lambda x: ({ + '0': '2G', + '2': '3G', + '7': '4G', + }.get(x, 'Unknown'), None), + ), + "device_signal.pci": dict( + name="PCI", + ), "device_signal.rsrq": dict( name="RSRQ", # http://www.lte-anbieter.info/technik/rsrq.php @@ -102,6 +122,22 @@ def setup_platform( add_entities(sensors, True) +def format_default(value): + """Format value.""" + unit = None + if value is not None: + # Clean up value and infer unit, e.g. -71dBm, 15 dB + match = re.match( + r"(?P.+?)\s*(?P[a-zA-Z]+)\s*$", str(value)) + if match: + try: + value = float(match.group("value")) + unit = match.group("unit") + except ValueError: + pass + return value, unit + + @attr.s class HuaweiLteSensor(Entity): """Huawei LTE sensor entity.""" @@ -117,8 +153,8 @@ class HuaweiLteSensor(Entity): def unique_id(self) -> str: """Return unique ID for sensor.""" return "{}_{}".format( - self.path, self.data["device_information.SerialNumber"], + ".".join(self.path), ) @property @@ -150,23 +186,14 @@ class HuaweiLteSensor(Entity): """Update state.""" self.data.update() - unit = None try: value = self.data[self.path] except KeyError: _LOGGER.warning("%s not in data", self.path) value = None - if value is not None: - # Clean up value and infer unit, e.g. -71dBm, 15 dB - match = re.match( - r"(?P.+?)\s*(?P[a-zA-Z]+)\s*$", str(value)) - if match: - try: - value = float(match.group("value")) - unit = match.group("unit") - except ValueError: - pass + formatter = self.meta.get("formatter") + if not callable(formatter): + formatter = format_default - self._state = value - self._unit = unit + self._state, self._unit = formatter(value) From 15f4ed74ac306cf0524aa896e59fe668ccd5cb63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 17 Oct 2018 10:58:41 +0300 Subject: [PATCH 190/265] Tweak sensors comments in default config (#17526) Makes it more clear that there should be only one sensors section, and that the weather prediction comment applies only to the yr platform. --- homeassistant/config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index 98857d8a83d..8f0690a02c0 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -105,8 +105,9 @@ map: # Track the sun sun: -# Weather prediction +# Sensors sensor: + # Weather prediction - platform: yr # Text to speech From 326787ef1a3c1547febb94e6b0c1ad20e2f0a0da Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 17 Oct 2018 10:45:01 +0200 Subject: [PATCH 191/265] Add another 3 days leeway to give time for payment processing times (#17542) --- homeassistant/components/cloud/__init__.py | 4 ++-- tests/components/cloud/test_init.py | 18 ++++++++++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 217b39aff62..54a221565b4 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -5,7 +5,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/cloud/ """ import asyncio -from datetime import datetime +from datetime import datetime, timedelta import json import logging import os @@ -162,7 +162,7 @@ class Cloud: @property def subscription_expired(self): """Return a boolean if the subscription has expired.""" - return dt_util.utcnow() > self.expiration_date + return dt_util.utcnow() > self.expiration_date + timedelta(days=3) @property def expiration_date(self): diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index 1fdbda496a9..8695830eae9 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -142,14 +142,28 @@ def test_write_user_info(): @asyncio.coroutine def test_subscription_expired(hass): - """Test subscription being expired.""" + """Test subscription being expired after 3 days of expiration.""" cl = cloud.Cloud(hass, cloud.MODE_DEV, None, None) token_val = { 'custom:sub-exp': '2017-11-13' } with patch.object(cl, '_decode_claims', return_value=token_val), \ patch('homeassistant.util.dt.utcnow', - return_value=utcnow().replace(year=2018)): + return_value=utcnow().replace(year=2017, month=11, day=13)): + assert not cl.subscription_expired + + with patch.object(cl, '_decode_claims', return_value=token_val), \ + patch('homeassistant.util.dt.utcnow', + return_value=utcnow().replace( + year=2017, month=11, day=15, hour=23, minute=59, + second=59)): + assert not cl.subscription_expired + + with patch.object(cl, '_decode_claims', return_value=token_val), \ + patch('homeassistant.util.dt.utcnow', + return_value=utcnow().replace( + year=2017, month=11, day=16, hour=0, minute=0, + second=0)): assert cl.subscription_expired From 1e4463957d9da7c335020010907c88f9aa11500a Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 17 Oct 2018 10:50:13 +0200 Subject: [PATCH 192/265] Scan all network interfaces for LIFX bulbs (#17530) --- homeassistant/components/lifx/__init__.py | 59 ++++------------------- homeassistant/components/light/lifx.py | 35 ++++++++------ requirements_all.txt | 2 +- 3 files changed, 32 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/lifx/__init__.py b/homeassistant/components/lifx/__init__.py index 85e249eb934..1ca6c00b23a 100644 --- a/homeassistant/components/lifx/__init__.py +++ b/homeassistant/components/lifx/__init__.py @@ -1,8 +1,4 @@ """Component to embed LIFX.""" -import asyncio -import socket - -import async_timeout import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -12,17 +8,20 @@ from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN DOMAIN = 'lifx' -REQUIREMENTS = ['aiolifx==0.6.3'] +REQUIREMENTS = ['aiolifx==0.6.5'] CONF_SERVER = 'server' CONF_BROADCAST = 'broadcast' +INTERFACE_SCHEMA = vol.Schema({ + vol.Optional(CONF_SERVER): cv.string, + vol.Optional(CONF_BROADCAST): cv.string, +}) + CONFIG_SCHEMA = vol.Schema({ DOMAIN: { - LIGHT_DOMAIN: { - vol.Optional(CONF_SERVER): cv.string, - vol.Optional(CONF_BROADCAST): cv.string, - } + LIGHT_DOMAIN: + vol.Schema(vol.All(cv.ensure_list, [INTERFACE_SCHEMA])), } }, extra=vol.ALLOW_EXTRA) @@ -51,47 +50,9 @@ async def _async_has_devices(hass): """Return if there are devices that can be discovered.""" import aiolifx - manager = DiscoveryManager() - lifx_discovery = aiolifx.LifxDiscovery(hass.loop, manager) - coro = hass.loop.create_datagram_endpoint( - lambda: lifx_discovery, - family=socket.AF_INET) - hass.async_create_task(coro) - - has_devices = await manager.found_devices() - lifx_discovery.cleanup() - - return has_devices + lifx_ip_addresses = await aiolifx.LifxScan(hass.loop).scan() + return len(lifx_ip_addresses) > 0 config_entry_flow.register_discovery_flow( DOMAIN, 'LIFX', _async_has_devices, config_entries.CONN_CLASS_LOCAL_POLL) - - -class DiscoveryManager: - """Temporary LIFX manager for discovering any bulb.""" - - def __init__(self): - """Initialize the manager.""" - self._event = asyncio.Event() - - async def found_devices(self): - """Return whether any device could be discovered.""" - try: - async with async_timeout.timeout(2): - await self._event.wait() - - # Let bulbs recover from the discovery - await asyncio.sleep(1) - - return True - except asyncio.TimeoutError: - return False - - def register(self, bulb): - """Handle aiolifx detected bulb.""" - self._event.set() - - def unregister(self, bulb): - """Handle aiolifx disappearing bulbs.""" - pass diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index 49f2c56826f..f346f88c42b 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.lifx/ """ import asyncio -import socket from datetime import timedelta from functools import partial import logging @@ -145,23 +144,31 @@ async def async_setup_entry(hass, config_entry, async_add_entities): _LOGGER.warning("The lifx platform is known to not work on Windows. " "Consider using the lifx_legacy platform instead") - config = hass.data[LIFX_DOMAIN].get(DOMAIN, {}) + # Priority 1: manual config + interfaces = hass.data[LIFX_DOMAIN].get(DOMAIN) + if not interfaces: + # Priority 2: scanned interfaces + lifx_ip_addresses = await aiolifx().LifxScan(hass.loop).scan() + interfaces = [{CONF_SERVER: ip} for ip in lifx_ip_addresses] + if not interfaces: + # Priority 3: default interface + interfaces = [{}] lifx_manager = LIFXManager(hass, async_add_entities) - broadcast_ip = config.get(CONF_BROADCAST) - kwargs = {'discovery_interval': DISCOVERY_INTERVAL} - if broadcast_ip: - kwargs['broadcast_ip'] = broadcast_ip - lifx_discovery = aiolifx().LifxDiscovery(hass.loop, lifx_manager, **kwargs) + for interface in interfaces: + kwargs = {'discovery_interval': DISCOVERY_INTERVAL} + broadcast_ip = interface.get(CONF_BROADCAST) + if broadcast_ip: + kwargs['broadcast_ip'] = broadcast_ip + lifx_discovery = aiolifx().LifxDiscovery( + hass.loop, lifx_manager, **kwargs) - kwargs = {'family': socket.AF_INET} - local_addr = config.get(CONF_SERVER) - if local_addr is not None: - kwargs['local_addr'] = (local_addr, 0) - coro = hass.loop.create_datagram_endpoint(lambda: lifx_discovery, **kwargs) - - hass.async_create_task(coro) + kwargs = {} + listen_ip = interface.get(CONF_SERVER) + if listen_ip: + kwargs['listen_ip'] = listen_ip + lifx_discovery.start(**kwargs) @callback def cleanup(event): diff --git a/requirements_all.txt b/requirements_all.txt index aaab9407864..43eb37a11f9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -110,7 +110,7 @@ aiohue==1.5.0 aioimaplib==0.7.13 # homeassistant.components.lifx -aiolifx==0.6.3 +aiolifx==0.6.5 # homeassistant.components.light.lifx aiolifx_effects==0.2.1 From e83a9aace41cf30f6fbb26df1978d1c5cc8621a7 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Wed, 17 Oct 2018 10:53:05 +0200 Subject: [PATCH 193/265] Remove unnecessary call (#17514) --- homeassistant/components/media_player/volumio.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/media_player/volumio.py b/homeassistant/components/media_player/volumio.py index 686bfe17a4c..373d3c380fc 100644 --- a/homeassistant/components/media_player/volumio.py +++ b/homeassistant/components/media_player/volumio.py @@ -90,7 +90,6 @@ class Volumio(MediaPlayerDevice): self._url = '{}:{}'.format(host, str(port)) self._name = name self._state = {} - self.async_update() self._lastvol = self._state.get('volume', 0) self._playlists = [] self._currentplaylist = None From a5b9f5040f03cbb0fe207a73bbe9b90c3d61127c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 17 Oct 2018 14:12:41 +0200 Subject: [PATCH 194/265] Update translations --- .../components/auth/.translations/uk.json | 9 +++++ .../components/deconz/.translations/ru.json | 2 +- .../components/ifttt/.translations/cs.json | 11 ++++++ .../components/ifttt/.translations/de.json | 2 +- .../components/ifttt/.translations/ko.json | 2 +- .../components/ifttt/.translations/ro.json | 10 ++++++ .../components/lifx/.translations/cs.json | 15 ++++++++ .../components/lifx/.translations/de.json | 15 ++++++++ .../components/lifx/.translations/hu.json | 10 ++++++ .../components/lifx/.translations/no.json | 15 ++++++++ .../components/lifx/.translations/ro.json | 15 ++++++++ .../components/mqtt/.translations/de.json | 2 +- .../components/mqtt/.translations/uk.json | 23 +++++++++++++ .../components/openuv/.translations/uk.json | 17 ++++++++++ .../sensor/.translations/season.uk.json | 8 +++++ .../simplisafe/.translations/ca.json | 19 +++++++++++ .../simplisafe/.translations/cs.json | 18 ++++++++++ .../simplisafe/.translations/de.json | 19 +++++++++++ .../simplisafe/.translations/hu.json | 11 ++++++ .../simplisafe/.translations/it.json | 19 +++++++++++ .../simplisafe/.translations/ko.json | 19 +++++++++++ .../simplisafe/.translations/lb.json | 19 +++++++++++ .../simplisafe/.translations/no.json | 19 +++++++++++ .../simplisafe/.translations/pl.json | 19 +++++++++++ .../simplisafe/.translations/ro.json | 17 ++++++++++ .../simplisafe/.translations/ru.json | 19 +++++++++++ .../simplisafe/.translations/sl.json | 19 +++++++++++ .../simplisafe/.translations/uk.json | 14 ++++++++ .../simplisafe/.translations/zh-Hans.json | 19 +++++++++++ .../simplisafe/.translations/zh-Hant.json | 19 +++++++++++ .../components/smhi/.translations/cs.json | 19 +++++++++++ .../components/smhi/.translations/de.json | 19 +++++++++++ .../components/smhi/.translations/no.json | 19 +++++++++++ .../components/smhi/.translations/ro.json | 18 ++++++++++ .../components/tradfri/.translations/uk.json | 15 ++++++++ .../components/unifi/.translations/ca.json | 26 ++++++++++++++ .../components/unifi/.translations/en.json | 34 +++++++++---------- .../components/unifi/.translations/hu.json | 12 +++++++ .../components/unifi/.translations/ko.json | 26 ++++++++++++++ .../components/unifi/.translations/lb.json | 26 ++++++++++++++ .../components/unifi/.translations/pl.json | 26 ++++++++++++++ .../components/unifi/.translations/ru.json | 25 ++++++++++++++ .../components/unifi/.translations/sl.json | 26 ++++++++++++++ .../unifi/.translations/zh-Hant.json | 26 ++++++++++++++ .../components/upnp/.translations/cs.json | 13 +++++++ .../components/upnp/.translations/no.json | 6 ++-- .../components/upnp/.translations/ro.json | 27 +++++++++++++++ .../upnp/.translations/zh-Hant.json | 2 +- .../components/zone/.translations/uk.json | 21 ++++++++++++ .../components/zwave/.translations/cs.json | 22 ++++++++++++ .../components/zwave/.translations/de.json | 22 ++++++++++++ .../components/zwave/.translations/hu.json | 5 +++ .../components/zwave/.translations/it.json | 15 ++++++++ .../components/zwave/.translations/ko.json | 2 +- .../components/zwave/.translations/no.json | 22 ++++++++++++ .../components/zwave/.translations/pl.json | 22 ++++++++++++ .../components/zwave/.translations/ro.json | 22 ++++++++++++ script/translations_download | 2 +- 58 files changed, 919 insertions(+), 26 deletions(-) create mode 100644 homeassistant/components/auth/.translations/uk.json create mode 100644 homeassistant/components/ifttt/.translations/cs.json create mode 100644 homeassistant/components/ifttt/.translations/ro.json create mode 100644 homeassistant/components/lifx/.translations/cs.json create mode 100644 homeassistant/components/lifx/.translations/de.json create mode 100644 homeassistant/components/lifx/.translations/hu.json create mode 100644 homeassistant/components/lifx/.translations/no.json create mode 100644 homeassistant/components/lifx/.translations/ro.json create mode 100644 homeassistant/components/mqtt/.translations/uk.json create mode 100644 homeassistant/components/openuv/.translations/uk.json create mode 100644 homeassistant/components/sensor/.translations/season.uk.json create mode 100644 homeassistant/components/simplisafe/.translations/ca.json create mode 100644 homeassistant/components/simplisafe/.translations/cs.json create mode 100644 homeassistant/components/simplisafe/.translations/de.json create mode 100644 homeassistant/components/simplisafe/.translations/hu.json create mode 100644 homeassistant/components/simplisafe/.translations/it.json create mode 100644 homeassistant/components/simplisafe/.translations/ko.json create mode 100644 homeassistant/components/simplisafe/.translations/lb.json create mode 100644 homeassistant/components/simplisafe/.translations/no.json create mode 100644 homeassistant/components/simplisafe/.translations/pl.json create mode 100644 homeassistant/components/simplisafe/.translations/ro.json create mode 100644 homeassistant/components/simplisafe/.translations/ru.json create mode 100644 homeassistant/components/simplisafe/.translations/sl.json create mode 100644 homeassistant/components/simplisafe/.translations/uk.json create mode 100644 homeassistant/components/simplisafe/.translations/zh-Hans.json create mode 100644 homeassistant/components/simplisafe/.translations/zh-Hant.json create mode 100644 homeassistant/components/smhi/.translations/cs.json create mode 100644 homeassistant/components/smhi/.translations/de.json create mode 100644 homeassistant/components/smhi/.translations/no.json create mode 100644 homeassistant/components/smhi/.translations/ro.json create mode 100644 homeassistant/components/tradfri/.translations/uk.json create mode 100644 homeassistant/components/unifi/.translations/ca.json create mode 100644 homeassistant/components/unifi/.translations/hu.json create mode 100644 homeassistant/components/unifi/.translations/ko.json create mode 100644 homeassistant/components/unifi/.translations/lb.json create mode 100644 homeassistant/components/unifi/.translations/pl.json create mode 100644 homeassistant/components/unifi/.translations/ru.json create mode 100644 homeassistant/components/unifi/.translations/sl.json create mode 100644 homeassistant/components/unifi/.translations/zh-Hant.json create mode 100644 homeassistant/components/upnp/.translations/cs.json create mode 100644 homeassistant/components/upnp/.translations/ro.json create mode 100644 homeassistant/components/zone/.translations/uk.json create mode 100644 homeassistant/components/zwave/.translations/cs.json create mode 100644 homeassistant/components/zwave/.translations/de.json create mode 100644 homeassistant/components/zwave/.translations/hu.json create mode 100644 homeassistant/components/zwave/.translations/it.json create mode 100644 homeassistant/components/zwave/.translations/no.json create mode 100644 homeassistant/components/zwave/.translations/pl.json create mode 100644 homeassistant/components/zwave/.translations/ro.json diff --git a/homeassistant/components/auth/.translations/uk.json b/homeassistant/components/auth/.translations/uk.json new file mode 100644 index 00000000000..3d4d9a5b151 --- /dev/null +++ b/homeassistant/components/auth/.translations/uk.json @@ -0,0 +1,9 @@ +{ + "mfa_setup": { + "notify": { + "error": { + "invalid_code": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u043a\u043e\u0434, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index 4cbc9594ead..a9b66314f31 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -28,6 +28,6 @@ "title": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0434\u043b\u044f deCONZ" } }, - "title": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0448\u043b\u044e\u0437 deCONZ" + "title": "deCONZ" } } \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/cs.json b/homeassistant/components/ifttt/.translations/cs.json new file mode 100644 index 00000000000..abbbd9ff890 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "description": "Opravdu chcete nastavit IFTTT?", + "title": "Nastavte applet IFTTT Webhook" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/de.json b/homeassistant/components/ifttt/.translations/de.json index b8fdc819753..a5b66156389 100644 --- a/homeassistant/components/ifttt/.translations/de.json +++ b/homeassistant/components/ifttt/.translations/de.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Nur eine einzige Instanz ist notwendig." }, "create_entry": { - "default": "Um Ereignisse an den Home Assistant zu senden, m\u00fcssen Sie die Aktion \"Eine Webanforderung erstellen\" aus dem [IFTTT Webhook Applet]({applet_url}) ausw\u00e4hlen.\n\nF\u00fcllen Sie folgende Informationen aus: \n- URL: `{webhook_url}`\n- Methode: POST\n- Inhaltstyp: application/json\n\nIn der Dokumentation ({docs_url}) finden Sie Informationen zur Konfiguration der Automation eingehender Daten." + "default": "Um Ereignisse an Home Assistant zu senden, musst du die Aktion \"Eine Webanforderung erstellen\" aus dem [IFTTT Webhook Applet]({applet_url}) ausw\u00e4hlen.\n\nF\u00fclle folgende Informationen aus: \n- URL: `{webhook_url}`\n- Methode: POST\n- Inhaltstyp: application/json\n\nIn der Dokumentation ({docs_url}) findest du Informationen zur Konfiguration der Automation eingehender Daten." }, "step": { "user": { diff --git a/homeassistant/components/ifttt/.translations/ko.json b/homeassistant/components/ifttt/.translations/ko.json index 832123d5065..57ad8037753 100644 --- a/homeassistant/components/ifttt/.translations/ko.json +++ b/homeassistant/components/ifttt/.translations/ko.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." }, "create_entry": { - "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\uae30 \uc704\ud574\uc11c\ub294 [IFTTT Webhook \uc560\ud50c\ub9bf]({applet_url}) \uc5d0\uc11c \"Make a web request\" \ub97c \uc0ac\uc6a9\ud574\uc57c \ud569\ub2c8\ub2e4. \n\n \ub2e4\uc74c\uc758 \uc815\ubcf4\ub97c \uc785\ub825\ud574 \uc8fc\uc138\uc694.\n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\n Home Assistant \ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\ubcf8 \ubb38\uc11c]({docs_url}) \ub97c \ucc38\uc870\ud574 \uc8fc\uc138\uc694." + "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\uae30 \uc704\ud574\uc11c\ub294 [IFTTT Webhook \uc560\ud50c\ub9bf]({applet_url}) \uc5d0\uc11c \"Make a web request\" \ub97c \uc0ac\uc6a9\ud574\uc57c \ud569\ub2c8\ub2e4. \n\n \ub2e4\uc74c\uc758 \uc815\ubcf4\ub97c \uc785\ub825\ud574 \uc8fc\uc138\uc694.\n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\n Home Assistant \ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\uc548\ub0b4]({docs_url})\ub97c \ucc38\uc870\ud574 \uc8fc\uc138\uc694." }, "step": { "user": { diff --git a/homeassistant/components/ifttt/.translations/ro.json b/homeassistant/components/ifttt/.translations/ro.json new file mode 100644 index 00000000000..03c77426671 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/ro.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "description": "Sigur dori\u021bi s\u0103 configura\u021bi IFTTT?" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/cs.json b/homeassistant/components/lifx/.translations/cs.json new file mode 100644 index 00000000000..d83ee576768 --- /dev/null +++ b/homeassistant/components/lifx/.translations/cs.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "V s\u00edti nejsou nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed LIFX.", + "single_instance_allowed": "K dispozici je pouze jedna konfigurace LIFX." + }, + "step": { + "confirm": { + "description": "Chcete nastavit LIFX?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/de.json b/homeassistant/components/lifx/.translations/de.json new file mode 100644 index 00000000000..2553e2d5e86 --- /dev/null +++ b/homeassistant/components/lifx/.translations/de.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Keine LIFX Ger\u00e4te im Netzwerk gefunden.", + "single_instance_allowed": "Nur eine einzige Konfiguration von LIFX ist zul\u00e4ssig." + }, + "step": { + "confirm": { + "description": "M\u00f6chtest du LIFX einrichten?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/hu.json b/homeassistant/components/lifx/.translations/hu.json new file mode 100644 index 00000000000..c78905b09c8 --- /dev/null +++ b/homeassistant/components/lifx/.translations/hu.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/no.json b/homeassistant/components/lifx/.translations/no.json new file mode 100644 index 00000000000..63080a30ff1 --- /dev/null +++ b/homeassistant/components/lifx/.translations/no.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Ingen LIFX-enheter funnet p\u00e5 nettverket.", + "single_instance_allowed": "Kun en enkelt konfigurasjon av LIFX er mulig." + }, + "step": { + "confirm": { + "description": "\u00d8nsker du \u00e5 sette opp LIFX?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/ro.json b/homeassistant/components/lifx/.translations/ro.json new file mode 100644 index 00000000000..12827082104 --- /dev/null +++ b/homeassistant/components/lifx/.translations/ro.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nu exist\u0103 dispozitive LIFX g\u0103site \u00een re\u021bea.", + "single_instance_allowed": "Doar o singur\u0103 configura\u021bie de LIFX este posibil\u0103." + }, + "step": { + "confirm": { + "description": "Dori\u021bi s\u0103 configura\u021bi LIFX?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/de.json b/homeassistant/components/mqtt/.translations/de.json index 1c895136d9d..d95c43cc618 100644 --- a/homeassistant/components/mqtt/.translations/de.json +++ b/homeassistant/components/mqtt/.translations/de.json @@ -22,7 +22,7 @@ "data": { "discovery": "Suche aktivieren" }, - "description": "M\u00f6chten Sie den Home Assistant so konfigurieren, dass er eine Verbindung mit dem MQTT-Broker herstellt, der vom Add-on hass.io {addon} bereitgestellt wird?", + "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass er eine Verbindung mit dem MQTT-Broker herstellt, der vom Add-on hass.io {addon} bereitgestellt wird?", "title": "MQTT Broker per Hass.io add-on" } }, diff --git a/homeassistant/components/mqtt/.translations/uk.json b/homeassistant/components/mqtt/.translations/uk.json new file mode 100644 index 00000000000..e747e774c45 --- /dev/null +++ b/homeassistant/components/mqtt/.translations/uk.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u043c\u043e\u0436\u043b\u0438\u0432\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f \u0434\u043e \u0431\u0440\u043e\u043a\u0435\u0440\u0430." + }, + "step": { + "broker": { + "data": { + "broker": "\u0411\u0440\u043e\u043a\u0435\u0440", + "discovery": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u043f\u043e\u0448\u0443\u043a", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, + "hassio_confirm": { + "data": { + "discovery": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u043f\u043e\u0448\u0443\u043a" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/uk.json b/homeassistant/components/openuv/.translations/uk.json new file mode 100644 index 00000000000..144ae8b8d36 --- /dev/null +++ b/homeassistant/components/openuv/.translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "\u041a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u0438 \u0432\u0436\u0435 \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u043e\u0432\u0430\u043d\u0456" + }, + "step": { + "user": { + "data": { + "elevation": "\u0412\u0438\u0441\u043e\u0442\u0430", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430" + }, + "title": "\u0417\u0430\u043f\u043e\u0432\u043d\u0456\u0442\u044c \u0432\u0430\u0448\u0443 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.uk.json b/homeassistant/components/sensor/.translations/season.uk.json new file mode 100644 index 00000000000..766e59a43da --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.uk.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "\u041e\u0441\u0456\u043d\u044c", + "spring": "\u0412\u0435\u0441\u043d\u0430", + "summer": "\u041b\u0456\u0442\u043e", + "winter": "\u0417\u0438\u043c\u0430" + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/ca.json b/homeassistant/components/simplisafe/.translations/ca.json new file mode 100644 index 00000000000..1662162c439 --- /dev/null +++ b/homeassistant/components/simplisafe/.translations/ca.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Aquest compte ja est\u00e0 registrat", + "invalid_credentials": "Credencials inv\u00e0lides" + }, + "step": { + "user": { + "data": { + "code": "Codi (pel Home Assistant)", + "password": "Contrasenya", + "username": "Correu electr\u00f2nic" + }, + "title": "Introdu\u00efu la vostra informaci\u00f3" + } + }, + "title": "SimpliSafe" + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/cs.json b/homeassistant/components/simplisafe/.translations/cs.json new file mode 100644 index 00000000000..0dd9912de0d --- /dev/null +++ b/homeassistant/components/simplisafe/.translations/cs.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "identifier_exists": "\u00da\u010det je ji\u017e zaregistrov\u00e1n", + "invalid_credentials": "Neplatn\u00e9 p\u0159ihla\u0161ovac\u00ed \u00fadaje" + }, + "step": { + "user": { + "data": { + "code": "K\u00f3d (pro Home Assistant)", + "password": "Heslo", + "username": "E-mailov\u00e1 adresa" + }, + "title": "Vypl\u0148te va\u0161e \u00fadaje" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/de.json b/homeassistant/components/simplisafe/.translations/de.json new file mode 100644 index 00000000000..ee7eaecc852 --- /dev/null +++ b/homeassistant/components/simplisafe/.translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Konto bereits registriert", + "invalid_credentials": "Ung\u00fcltige Anmeldeinformationen" + }, + "step": { + "user": { + "data": { + "code": "Code (f\u00fcr Home Assistant)", + "password": "Passwort", + "username": "E-Mail-Adresse" + }, + "title": "Gebe deine Informationen ein" + } + }, + "title": "SimpliSafe" + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/hu.json b/homeassistant/components/simplisafe/.translations/hu.json new file mode 100644 index 00000000000..ff2c2fc87b5 --- /dev/null +++ b/homeassistant/components/simplisafe/.translations/hu.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/it.json b/homeassistant/components/simplisafe/.translations/it.json new file mode 100644 index 00000000000..134bfae3668 --- /dev/null +++ b/homeassistant/components/simplisafe/.translations/it.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Account gi\u00e0 registrato", + "invalid_credentials": "Credenziali non valide" + }, + "step": { + "user": { + "data": { + "code": "Codice (Home Assistant)", + "password": "Password", + "username": "Indirizzo email" + }, + "title": "Inserisci i tuoi dati" + } + }, + "title": "SimpliSafe" + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/ko.json b/homeassistant/components/simplisafe/.translations/ko.json new file mode 100644 index 00000000000..eca099ed79d --- /dev/null +++ b/homeassistant/components/simplisafe/.translations/ko.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "\uacc4\uc815\uc774 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_credentials": "\uc774\uba54\uc77c \uc8fc\uc18c \ud639\uc740 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "code": "\ucf54\ub4dc (Home Assistant \uc6a9)", + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc774\uba54\uc77c \uc8fc\uc18c" + }, + "title": "\uc0ac\uc6a9\uc790 \uc815\ubcf4\ub97c \uc785\ub825\ud574 \uc8fc\uc138\uc694" + } + }, + "title": "SimpliSafe" + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/lb.json b/homeassistant/components/simplisafe/.translations/lb.json new file mode 100644 index 00000000000..94c451a49db --- /dev/null +++ b/homeassistant/components/simplisafe/.translations/lb.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Konto ass scho registr\u00e9iert", + "invalid_credentials": "Ong\u00eblteg Login Informatioune" + }, + "step": { + "user": { + "data": { + "code": "Code (fir Home Assistant)", + "password": "Passwuert", + "username": "E-Mail Adress" + }, + "title": "F\u00ebllt \u00e4r Informatiounen aus" + } + }, + "title": "SimpliSafe" + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/no.json b/homeassistant/components/simplisafe/.translations/no.json new file mode 100644 index 00000000000..7c28209514e --- /dev/null +++ b/homeassistant/components/simplisafe/.translations/no.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Konto er allerede registrert", + "invalid_credentials": "Ugyldig legitimasjon" + }, + "step": { + "user": { + "data": { + "code": "Kode (for Home Assistant)", + "password": "Passord", + "username": "E-postadresse" + }, + "title": "Fyll ut informasjonen din" + } + }, + "title": "SimpliSafe" + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/pl.json b/homeassistant/components/simplisafe/.translations/pl.json new file mode 100644 index 00000000000..0b83ba8cbed --- /dev/null +++ b/homeassistant/components/simplisafe/.translations/pl.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Konto zosta\u0142o ju\u017c zarejestrowane", + "invalid_credentials": "Nieprawid\u0142owe po\u015bwiadczenia" + }, + "step": { + "user": { + "data": { + "code": "Kod (dla Home Assistant'a)", + "password": "Has\u0142o", + "username": "Adres e-mail" + }, + "title": "Wprowad\u017a swoje dane" + } + }, + "title": "SimpliSafe" + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/ro.json b/homeassistant/components/simplisafe/.translations/ro.json new file mode 100644 index 00000000000..7046b0992b1 --- /dev/null +++ b/homeassistant/components/simplisafe/.translations/ro.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "Contul este deja \u00eenregistrat", + "invalid_credentials": "Credentiale invalide" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Adresa de email" + }, + "title": "Completa\u021bi informa\u021biile dvs." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/ru.json b/homeassistant/components/simplisafe/.translations/ru.json new file mode 100644 index 00000000000..4ddf405e1ed --- /dev/null +++ b/homeassistant/components/simplisafe/.translations/ru.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430", + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435" + }, + "step": { + "user": { + "data": { + "code": "\u041a\u043e\u0434 (\u0434\u043b\u044f Home Assistant)", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" + }, + "title": "\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u0432\u043e\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e" + } + }, + "title": "SimpliSafe" + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/sl.json b/homeassistant/components/simplisafe/.translations/sl.json new file mode 100644 index 00000000000..7fe0adad2df --- /dev/null +++ b/homeassistant/components/simplisafe/.translations/sl.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Ra\u010dun je \u017ee registriran", + "invalid_credentials": "Neveljavne poverilnice" + }, + "step": { + "user": { + "data": { + "code": "Koda (za Home Assistant)", + "password": "Geslo", + "username": "E-po\u0161tni naslov" + }, + "title": "Izpolnite svoje podatke" + } + }, + "title": "SimpliSafe" + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/uk.json b/homeassistant/components/simplisafe/.translations/uk.json new file mode 100644 index 00000000000..4dee0ed5f4d --- /dev/null +++ b/homeassistant/components/simplisafe/.translations/uk.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "code": "\u041a\u043e\u0434 (\u0434\u043b\u044f Home Assistant)", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438" + }, + "title": "\u0417\u0430\u043f\u043e\u0432\u043d\u0456\u0442\u044c \u0432\u0430\u0448\u0443 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/zh-Hans.json b/homeassistant/components/simplisafe/.translations/zh-Hans.json new file mode 100644 index 00000000000..2316f5c7454 --- /dev/null +++ b/homeassistant/components/simplisafe/.translations/zh-Hans.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "\u5e10\u6237\u5df2\u6ce8\u518c", + "invalid_credentials": "\u65e0\u6548\u7684\u8eab\u4efd\u8ba4\u8bc1" + }, + "step": { + "user": { + "data": { + "code": "\u4ee3\u7801\uff08\u7528\u4e8eHome Assistant\uff09", + "password": "\u5bc6\u7801", + "username": "\u7535\u5b50\u90ae\u4ef6\u5730\u5740" + }, + "title": "\u586b\u5199\u60a8\u7684\u4fe1\u606f" + } + }, + "title": "SimpliSafe" + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/zh-Hant.json b/homeassistant/components/simplisafe/.translations/zh-Hant.json new file mode 100644 index 00000000000..bd0b2c6f3d6 --- /dev/null +++ b/homeassistant/components/simplisafe/.translations/zh-Hant.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "\u5e33\u865f\u5df2\u8a3b\u518a", + "invalid_credentials": "\u6191\u8b49\u7121\u6548" + }, + "step": { + "user": { + "data": { + "code": "\u9a57\u8b49\u78bc\uff08Home Assistant \u7528\uff09", + "password": "\u5bc6\u78bc", + "username": "\u96fb\u5b50\u90f5\u4ef6\u5730\u5740" + }, + "title": "\u586b\u5beb\u8cc7\u8a0a" + } + }, + "title": "SimpliSafe" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/cs.json b/homeassistant/components/smhi/.translations/cs.json new file mode 100644 index 00000000000..356603c9cf8 --- /dev/null +++ b/homeassistant/components/smhi/.translations/cs.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "N\u00e1zev ji\u017e existuje", + "wrong_location": "Lokalita pouze pro \u0160v\u00e9dsko" + }, + "step": { + "user": { + "data": { + "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", + "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka", + "name": "N\u00e1zev" + }, + "title": "Lokalita ve \u0160v\u00e9dsku" + } + }, + "title": "\u0160v\u00e9dsk\u00e1 meteorologick\u00e1 slu\u017eba (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/de.json b/homeassistant/components/smhi/.translations/de.json new file mode 100644 index 00000000000..7c41731988c --- /dev/null +++ b/homeassistant/components/smhi/.translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Name existiert bereits", + "wrong_location": "Standort nur in Schweden" + }, + "step": { + "user": { + "data": { + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad", + "name": "Name" + }, + "title": "Standort in Schweden" + } + }, + "title": "Schwedischer Wetterdienst (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/no.json b/homeassistant/components/smhi/.translations/no.json new file mode 100644 index 00000000000..19c90f8ec5c --- /dev/null +++ b/homeassistant/components/smhi/.translations/no.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Navnet eksisterer allerede", + "wrong_location": "Bare plassering i Sverige" + }, + "step": { + "user": { + "data": { + "latitude": "Breddegrad", + "longitude": "Lengdegrad", + "name": "Navn" + }, + "title": "Plassering i Sverige" + } + }, + "title": "Sveriges Meteorologiske og Hydrologiske Institut (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/ro.json b/homeassistant/components/smhi/.translations/ro.json new file mode 100644 index 00000000000..6fe28787655 --- /dev/null +++ b/homeassistant/components/smhi/.translations/ro.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "name_exists": "Numele exist\u0103 deja" + }, + "step": { + "user": { + "data": { + "latitude": "Latitudine", + "longitude": "Longitudine", + "name": "Nume" + }, + "title": "Loca\u021bie \u00een Suedia" + } + }, + "title": "Serviciul meteorologic suedez (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/.translations/uk.json b/homeassistant/components/tradfri/.translations/uk.json new file mode 100644 index 00000000000..a163a4680e3 --- /dev/null +++ b/homeassistant/components/tradfri/.translations/uk.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u043c\u043e\u0436\u043b\u0438\u0432\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f \u0434\u043e \u0448\u043b\u044e\u0437\u0443." + }, + "step": { + "auth": { + "data": { + "security_code": "\u041a\u043e\u0434 \u0431\u0435\u0437\u043f\u0435\u043a\u0438" + }, + "title": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043a\u043e\u0434 \u0431\u0435\u0437\u043f\u0435\u043a\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/ca.json b/homeassistant/components/unifi/.translations/ca.json new file mode 100644 index 00000000000..77d859627dc --- /dev/null +++ b/homeassistant/components/unifi/.translations/ca.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "El lloc del controlador ja est\u00e0 configurat", + "user_privilege": "L'usuari ha de ser administrador" + }, + "error": { + "faulty_credentials": "Credencials d'usuari incorrectes", + "service_unavailable": "Servei no disponible" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "password": "Contrasenya", + "port": "Port", + "site": "ID del lloc", + "username": "Nom d'usuari", + "verify_ssl": "El controlador est\u00e0 utilitzant un certificat adequat" + }, + "title": "Configura el controlador UniFi" + } + }, + "title": "Controlador UniFi" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/en.json b/homeassistant/components/unifi/.translations/en.json index 938ac058d22..3686148fdb6 100644 --- a/homeassistant/components/unifi/.translations/en.json +++ b/homeassistant/components/unifi/.translations/en.json @@ -1,26 +1,26 @@ { "config": { - "title": "UniFi Controller", - "step": { - "user": { - "title": "Set up UniFi Controller", - "data": { - "host": "Host", - "username": "User name", - "password": "Password", - "port": "Port", - "site": "Site ID", - "verify_ssl": "Controller using proper certificate" - } - } + "abort": { + "already_configured": "Controller site is already configured", + "user_privilege": "User needs to be administrator" }, "error": { "faulty_credentials": "Bad user credentials", "service_unavailable": "No service available" }, - "abort": { - "already_configured": "Controller site is already configured", - "user_privilege": "User needs to be administrator" - } + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "port": "Port", + "site": "Site ID", + "username": "User name", + "verify_ssl": "Controller using proper certificate" + }, + "title": "Set up UniFi Controller" + } + }, + "title": "UniFi Controller" } } \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/hu.json b/homeassistant/components/unifi/.translations/hu.json new file mode 100644 index 00000000000..f5827c47353 --- /dev/null +++ b/homeassistant/components/unifi/.translations/hu.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/ko.json b/homeassistant/components/unifi/.translations/ko.json new file mode 100644 index 00000000000..431d6bbf5e6 --- /dev/null +++ b/homeassistant/components/unifi/.translations/ko.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\ucee8\ud2b8\ub864\ub7ec \uc0ac\uc774\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "user_privilege": "\uc0ac\uc6a9\uc790\ub294 \uad00\ub9ac\uc790\uc5ec\uc57c \ud569\ub2c8\ub2e4" + }, + "error": { + "faulty_credentials": "\uc0ac\uc6a9\uc790 \uc790\uaca9\uc99d\uba85\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "service_unavailable": "\uc0ac\uc6a9\ud560 \uc218 \uc788\ub294 \uc11c\ube44\uc2a4\uac00 \uc5c6\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "password": "\ube44\ubc00\ubc88\ud638", + "port": "\ud3ec\ud2b8", + "site": "\uc0ac\uc774\ud2b8 ID", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984", + "verify_ssl": "\uc62c\ubc14\ub978 \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud558\ub294 \ucee8\ud2b8\ub864\ub7ec" + }, + "title": "UniFi \ucee8\ud2b8\ub864\ub7ec \uc124\uc815" + } + }, + "title": "UniFi \ucee8\ud2b8\ub864\ub7ec" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/lb.json b/homeassistant/components/unifi/.translations/lb.json new file mode 100644 index 00000000000..3bef273b83e --- /dev/null +++ b/homeassistant/components/unifi/.translations/lb.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Kontroller Site ass scho konfigur\u00e9iert", + "user_privilege": "Benotzer muss een Administrator sinn" + }, + "error": { + "faulty_credentials": "Ong\u00eblteg Login Informatioune", + "service_unavailable": "Keen Service disponibel" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Passwuert", + "port": "Port", + "site": "Site ID", + "username": "Benotzer", + "verify_ssl": "Kontroller benotzt g\u00ebltegen Zertifikat" + }, + "title": "Unifi Kontroller ariichten" + } + }, + "title": "Unifi Kontroller" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/pl.json b/homeassistant/components/unifi/.translations/pl.json new file mode 100644 index 00000000000..f2f8082ac76 --- /dev/null +++ b/homeassistant/components/unifi/.translations/pl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Witryna kontrolera jest ju\u017c skonfigurowana", + "user_privilege": "U\u017cytkownik musi by\u0107 administratorem" + }, + "error": { + "faulty_credentials": "B\u0142\u0119dne dane uwierzytelniaj\u0105ce", + "service_unavailable": "Brak dost\u0119pnych us\u0142ug" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Has\u0142o", + "port": "Port", + "site": "Identyfikator witryny", + "username": "Nazwa u\u017cytkownika", + "verify_ssl": "Kontroler u\u017cywa prawid\u0142owego certyfikatu" + }, + "title": "Skonfiguruj kontroler UniFi" + } + }, + "title": "Kontroler UniFi" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/ru.json b/homeassistant/components/unifi/.translations/ru.json new file mode 100644 index 00000000000..908c1c5d0c5 --- /dev/null +++ b/homeassistant/components/unifi/.translations/ru.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "user_privilege": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u043e\u043c" + }, + "error": { + "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", + "service_unavailable": "\u0421\u043b\u0443\u0436\u0431\u0430 \u043d\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "site": "ID \u0441\u0430\u0439\u0442\u0430", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 UniFi Controller" + } + }, + "title": "UniFi Controller" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/sl.json b/homeassistant/components/unifi/.translations/sl.json new file mode 100644 index 00000000000..7543542abbf --- /dev/null +++ b/homeassistant/components/unifi/.translations/sl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Nadzornik je \u017ee konfiguriran", + "user_privilege": "Uporabnik mora biti skrbnik" + }, + "error": { + "faulty_credentials": "Napa\u010dni uporabni\u0161ki podatki", + "service_unavailable": "Nobena storitev ni na voljo" + }, + "step": { + "user": { + "data": { + "host": "Gostitelj", + "password": "Geslo", + "port": "Vrata", + "site": "Mesto ID", + "username": "Uporabni\u0161ko ime", + "verify_ssl": "Kontroler uporablja ustrezen certifikat" + }, + "title": "Nastavi UniFi Controller" + } + }, + "title": "UniFi Krmilnik" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/zh-Hant.json b/homeassistant/components/unifi/.translations/zh-Hant.json new file mode 100644 index 00000000000..e506c582cb7 --- /dev/null +++ b/homeassistant/components/unifi/.translations/zh-Hant.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u63a7\u5236\u5668\u4f4d\u5740\u5df2\u7d93\u8a2d\u5b9a", + "user_privilege": "\u4f7f\u7528\u8005\u5fc5\u9808\u70ba\u7ba1\u7406\u54e1\u8eab\u4efd" + }, + "error": { + "faulty_credentials": "\u4f7f\u7528\u8005\u6191\u8b49\u7121\u6548", + "service_unavailable": "\u7121\u670d\u52d9\u53ef\u7528" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0", + "site": "\u4f4d\u5740 ID", + "username": "\u4f7f\u7528\u8005\u540d\u7a31", + "verify_ssl": "\u63a7\u5236\u5668\u4f7f\u7528\u9a57\u8b49" + }, + "title": "\u8a2d\u5b9a UniFi \u63a7\u5236\u5668" + } + }, + "title": "UniFi \u63a7\u5236\u5668" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/cs.json b/homeassistant/components/upnp/.translations/cs.json new file mode 100644 index 00000000000..24a725d1af6 --- /dev/null +++ b/homeassistant/components/upnp/.translations/cs.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "igd": "UPnP/IGD" + }, + "title": "Mo\u017enosti konfigurace pro UPnP/IGD" + } + }, + "title": "UPnP/IGD" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/no.json b/homeassistant/components/upnp/.translations/no.json index fbb1b4afc75..a0c4c23f9c4 100644 --- a/homeassistant/components/upnp/.translations/no.json +++ b/homeassistant/components/upnp/.translations/no.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "UPnP / IGD er allerede konfigurert", - "no_devices_discovered": "Ingen UPnP / IGDs oppdaget" + "no_devices_discovered": "Ingen UPnP / IGDs oppdaget", + "no_sensors_or_port_mapping": "Aktiver minst sensorer eller port mapping" }, "error": { "few": "f\u00e5", @@ -18,12 +19,13 @@ }, "user": { "data": { + "enable_port_mapping": "Aktiver port mapping for Home Assistant", "enable_sensors": "Legg til trafikk sensorer", "igd": "UPnP / IGD" }, "title": "Konfigurasjonsalternativer for UPnP / IGD" } }, - "title": "UPnP / IGD" + "title": "UPnP/IGD" } } \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/ro.json b/homeassistant/components/upnp/.translations/ro.json new file mode 100644 index 00000000000..bb584da05dc --- /dev/null +++ b/homeassistant/components/upnp/.translations/ro.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP/IGD este deja configurat", + "no_devices_discovered": "Nu au fost descoperite UPnP/IGD-uri" + }, + "error": { + "few": "", + "one": "Unul", + "other": "" + }, + "step": { + "init": { + "title": "UPnP/IGD" + }, + "user": { + "data": { + "enable_port_mapping": "Activa\u021bi maparea porturilor pentru Home Assistant", + "enable_sensors": "Ad\u0103uga\u021bi senzori de trafic", + "igd": "UPnP/IGD" + }, + "title": "Op\u021biuni de configurare pentru UPnP/IGD" + } + }, + "title": "UPnP/IGD" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/zh-Hant.json b/homeassistant/components/upnp/.translations/zh-Hant.json index ca8171265ae..22db0f26482 100644 --- a/homeassistant/components/upnp/.translations/zh-Hant.json +++ b/homeassistant/components/upnp/.translations/zh-Hant.json @@ -12,7 +12,7 @@ "user": { "data": { "enable_port_mapping": "\u958b\u555f Home Assistant \u901a\u8a0a\u57e0\u8f49\u767c", - "enable_sensors": "\u65b0\u589e\u4ea4\u901a\u611f\u61c9\u5668", + "enable_sensors": "\u65b0\u589e\u6d41\u91cf\u611f\u61c9\u5668", "igd": "UPnP/IGD" }, "title": "UPnP/IGD \u8a2d\u5b9a\u9078\u9805" diff --git a/homeassistant/components/zone/.translations/uk.json b/homeassistant/components/zone/.translations/uk.json new file mode 100644 index 00000000000..ce082d34a1c --- /dev/null +++ b/homeassistant/components/zone/.translations/uk.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "\u0406\u043c'\u044f \u0432\u0436\u0435 \u0456\u0441\u043d\u0443\u0454" + }, + "step": { + "init": { + "data": { + "icon": "\u0406\u043a\u043e\u043d\u043a\u0430", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430", + "passive": "\u041f\u0430\u0441\u0438\u0432\u043d\u0438\u0439", + "radius": "\u0420\u0430\u0434\u0456\u0443\u0441" + }, + "title": "\u0412\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0456\u0432 \u0437\u043e\u043d\u0438" + } + }, + "title": "\u0417\u043e\u043d\u0430" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/cs.json b/homeassistant/components/zwave/.translations/cs.json new file mode 100644 index 00000000000..a44fb8ad34b --- /dev/null +++ b/homeassistant/components/zwave/.translations/cs.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave je ji\u017e nakonfigurov\u00e1no", + "one_instance_only": "Komponenta podporuje pouze jednu instanci Z-Wave" + }, + "error": { + "option_error": "Z-Wave ov\u011b\u0159en\u00ed se nezda\u0159ilo. Je cesta k USB za\u0159\u00edzen\u00ed spr\u00e1vn\u011b?" + }, + "step": { + "user": { + "data": { + "network_key": "S\u00ed\u0165ov\u00fd kl\u00ed\u010d (ponechte pr\u00e1zdn\u00e9 pro automatick\u00e9 generov\u00e1n\u00ed)", + "usb_path": "Cesta k USB" + }, + "description": "Viz https://www.home-assistant.io/docs/z-wave/installation/ pro informace o konfigura\u010dn\u00edch prom\u011bnn\u00fdch", + "title": "Nastavit Z-Wave" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/de.json b/homeassistant/components/zwave/.translations/de.json new file mode 100644 index 00000000000..f2438f1561f --- /dev/null +++ b/homeassistant/components/zwave/.translations/de.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave ist bereits konfiguriert", + "one_instance_only": "Komponente unterst\u00fctzt nur eine Z-Wave-Instanz" + }, + "error": { + "option_error": "Z-Wave-Validierung fehlgeschlagen. Ist der Pfad zum USB-Stick korrekt?" + }, + "step": { + "user": { + "data": { + "network_key": "Netzwerkschl\u00fcssel (leer lassen, um automatisch zu generieren)", + "usb_path": "USB-Pfad" + }, + "description": "Informationen zu den Konfigurationsvariablen findest du unter https://www.home-assistant.io/docs/z-wave/installation/", + "title": "Z-Wave einrichten" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/hu.json b/homeassistant/components/zwave/.translations/hu.json new file mode 100644 index 00000000000..16c25cb7cab --- /dev/null +++ b/homeassistant/components/zwave/.translations/hu.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/it.json b/homeassistant/components/zwave/.translations/it.json new file mode 100644 index 00000000000..86a61307814 --- /dev/null +++ b/homeassistant/components/zwave/.translations/it.json @@ -0,0 +1,15 @@ +{ + "config": { + "step": { + "user": { + "data": { + "network_key": "Chiave di rete (lascia vuoto per generare automaticamente)", + "usb_path": "Percorso USB" + }, + "description": "Vai su https://www.home-assistant.io/docs/z-wave/installation/ per le informazioni sulle variabili di configurazione", + "title": "Imposta Z-Wave" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/ko.json b/homeassistant/components/zwave/.translations/ko.json index 43103de3d51..d57f758ce25 100644 --- a/homeassistant/components/zwave/.translations/ko.json +++ b/homeassistant/components/zwave/.translations/ko.json @@ -13,7 +13,7 @@ "network_key": "\ub124\ud2b8\uc6cc\ud06c \ud0a4 (\uacf5\ub780\uc73c\ub85c \ube44\uc6cc\ub450\uba74 \uc790\ub3d9 \uc0dd\uc131\ud569\ub2c8\ub2e4)", "usb_path": "USB \uacbd\ub85c" }, - "description": "\uad6c\uc131 \ubcc0\uc218\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 https://www.home-assistant.io/docs/z-wave/installation/ \uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694", + "description": "\uad6c\uc131 \ubcc0\uc218\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4](https://www.home-assistant.io/docs/z-wave/installation/)\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694", "title": "Z-Wave \uc124\uc815" } }, diff --git a/homeassistant/components/zwave/.translations/no.json b/homeassistant/components/zwave/.translations/no.json new file mode 100644 index 00000000000..f70eaa48260 --- /dev/null +++ b/homeassistant/components/zwave/.translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave er allerede konfigurert", + "one_instance_only": "Komponenten st\u00f8tter kun en enkelt Z-Wave-forekomst" + }, + "error": { + "option_error": "Z-Wave-validering mislyktes. Er banen til USB dongel riktig?" + }, + "step": { + "user": { + "data": { + "network_key": "Nettverksn\u00f8kkel (la v\u00e6re tom for automatisk generering)", + "usb_path": "USB bane" + }, + "description": "Se [www.home-assistant.io/docs/z-wave/installation/](https://www.home-assistant.io/docs/z-wave/installation/) for informasjon om konfigurasjon variablene", + "title": "Sett opp Z-Wave" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/pl.json b/homeassistant/components/zwave/.translations/pl.json new file mode 100644 index 00000000000..a96010a74a8 --- /dev/null +++ b/homeassistant/components/zwave/.translations/pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave jest ju\u017c skonfigurowany", + "one_instance_only": "Komponent obs\u0142uguje tylko jedn\u0105 instancj\u0119 Z-Wave" + }, + "error": { + "option_error": "Walidacja Z-Wave nie powiod\u0142a si\u0119. Czy \u015bcie\u017cka do kontrolera Z-Wave USB jest prawid\u0142owa?" + }, + "step": { + "user": { + "data": { + "network_key": "Klucz sieciowy (pozostaw pusty by generowa\u0107 automatycznie)", + "usb_path": "\u015acie\u017cka do kontrolera Z-Wave USB" + }, + "description": "Zobacz https://www.home-assistant.io/docs/z-wave/installation/, aby uzyska\u0107 informacje na temat zmiennych konfiguracyjnych", + "title": "Konfiguracja Z-Wave" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/ro.json b/homeassistant/components/zwave/.translations/ro.json new file mode 100644 index 00000000000..6920f56cdb1 --- /dev/null +++ b/homeassistant/components/zwave/.translations/ro.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave este deja configurat", + "one_instance_only": "Componenta accept\u0103 numai o instan\u021b\u0103 Z-Wave" + }, + "error": { + "option_error": "Validarea Z-Wave a e\u0219uat. Este corect\u0103 calea c\u0103tre stick-ul USB?" + }, + "step": { + "user": { + "data": { + "network_key": "Cheie de re\u021bea (l\u0103sa\u021bi necompletat pentru a genera automat)", + "usb_path": "Cale USB" + }, + "description": "Vede\u021bi https://www.home-assistant.io/docs/z-wave/installation/ pentru informa\u021bii despre variabilele de configurare", + "title": "Configura\u021bi Z-Wave" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/script/translations_download b/script/translations_download index 15b6a681056..9363bc425ae 100755 --- a/script/translations_download +++ b/script/translations_download @@ -28,7 +28,7 @@ mkdir -p ${LOCAL_DIR} docker run \ -v ${LOCAL_DIR}:/opt/dest/locale \ - lokalise/lokalise-cli@sha256:ddf5677f58551261008342df5849731c88bcdc152ab645b133b21819aede8218 lokalise \ + lokalise/lokalise-cli@sha256:b8329d20280263cad04f65b843e54b9e8e6909a348a678eac959550b5ef5c75f lokalise \ --token ${LOKALISE_TOKEN} \ export ${PROJECT_ID} \ --export_empty skip \ From 95c43d634b30ee67b7e73220255ee8efa6f9807f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 17 Oct 2018 14:16:49 +0200 Subject: [PATCH 195/265] Bump frontend to 20181017.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index aa9406d9c62..ce553da31a4 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20181014.0'] +REQUIREMENTS = ['home-assistant-frontend==20181017.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 43eb37a11f9..a48c53fa5b4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -466,7 +466,7 @@ hole==0.3.0 holidays==0.9.7 # homeassistant.components.frontend -home-assistant-frontend==20181014.0 +home-assistant-frontend==20181017.0 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bf70cf82104..5d462b3b0d5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -97,7 +97,7 @@ hdate==0.6.5 holidays==0.9.7 # homeassistant.components.frontend -home-assistant-frontend==20181014.0 +home-assistant-frontend==20181017.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From 33860bf23cedbe53ff10cc23e12f754577eaf04b Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 17 Oct 2018 16:31:06 +0200 Subject: [PATCH 196/265] Adding id to lovelace cards in ui-lovelace.yaml (#17498) * ID is added to cards without ID in ui-lovelace.yaml when loaded * Hound * Remove ui-lovelace.yaml * Nicer get * Update tests * If YAML dump fails, config not gone * Add tests * Woof! * Remove nosetests * Address comments * Woof... * Delete test.yaml * update rights to saved file * fix * line break --- homeassistant/components/lovelace/__init__.py | 80 ++++++++- requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/lovelace/test_init.py | 166 +++++++++++++++++- 5 files changed, 245 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index a24c8eb9e91..e3f4522580b 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -1,19 +1,95 @@ """Lovelace UI.""" +import logging +import uuid +import os +from os import O_WRONLY, O_CREAT, O_TRUNC +from collections import OrderedDict +from typing import Union, List, Dict import voluptuous as vol from homeassistant.components import websocket_api -from homeassistant.util.yaml import load_yaml from homeassistant.exceptions import HomeAssistantError +_LOGGER = logging.getLogger(__name__) DOMAIN = 'lovelace' +REQUIREMENTS = ['ruamel.yaml==0.15.72'] OLD_WS_TYPE_GET_LOVELACE_UI = 'frontend/lovelace_config' WS_TYPE_GET_LOVELACE_UI = 'lovelace/config' + SCHEMA_GET_LOVELACE_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): vol.Any(WS_TYPE_GET_LOVELACE_UI, OLD_WS_TYPE_GET_LOVELACE_UI), }) +JSON_TYPE = Union[List, Dict, str] # pylint: disable=invalid-name + + +class WriteError(HomeAssistantError): + """Error writing the data.""" + + +def save_yaml(fname: str, data: JSON_TYPE): + """Save a YAML file.""" + from ruamel.yaml import YAML + from ruamel.yaml.error import YAMLError + yaml = YAML(typ='rt') + yaml.indent(sequence=4, offset=2) + tmp_fname = fname + "__TEMP__" + try: + with open(os.open(tmp_fname, O_WRONLY | O_CREAT | O_TRUNC, 0o644), + 'w', encoding='utf-8') as temp_file: + yaml.dump(data, temp_file) + os.replace(tmp_fname, fname) + except YAMLError as exc: + _LOGGER.error(str(exc)) + raise HomeAssistantError(exc) + except OSError as exc: + _LOGGER.exception('Saving YAML file failed: %s', fname) + raise WriteError(exc) + finally: + if os.path.exists(tmp_fname): + try: + os.remove(tmp_fname) + except OSError as exc: + # If we are cleaning up then something else went wrong, so + # we should suppress likely follow-on errors in the cleanup + _LOGGER.error("YAML replacement cleanup failed: %s", exc) + + +def load_yaml(fname: str) -> JSON_TYPE: + """Load a YAML file.""" + from ruamel.yaml import YAML + from ruamel.yaml.error import YAMLError + yaml = YAML(typ='rt') + try: + with open(fname, encoding='utf-8') as conf_file: + # If configuration file is empty YAML returns None + # We convert that to an empty dict + return yaml.load(conf_file) or OrderedDict() + except YAMLError as exc: + _LOGGER.error("YAML error: %s", exc) + raise HomeAssistantError(exc) + except UnicodeDecodeError as exc: + _LOGGER.error("Unable to read file %s: %s", fname, exc) + raise HomeAssistantError(exc) + + +def load_config(fname: str) -> JSON_TYPE: + """Load a YAML file and adds id to card if not present.""" + config = load_yaml(fname) + # Check if all cards have an ID or else add one + updated = False + for view in config.get('views', []): + for card in view.get('cards', []): + if 'id' not in card: + updated = True + card['id'] = uuid.uuid4().hex + card.move_to_end('id', last=False) + if updated: + save_yaml(fname, config) + return config + async def async_setup(hass, config): """Set up the Lovelace commands.""" @@ -35,7 +111,7 @@ async def websocket_lovelace_config(hass, connection, msg): error = None try: config = await hass.async_add_executor_job( - load_yaml, hass.config.path('ui-lovelace.yaml')) + load_config, hass.config.path('ui-lovelace.yaml')) message = websocket_api.result_message( msg['id'], config ) diff --git a/requirements_all.txt b/requirements_all.txt index a48c53fa5b4..6dfc1ee5713 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1308,6 +1308,9 @@ roombapy==1.3.1 # homeassistant.components.switch.rpi_rf # rpi-rf==0.9.6 +# homeassistant.components.lovelace +ruamel.yaml==0.15.72 + # homeassistant.components.media_player.russound_rnet russound==0.1.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5d462b3b0d5..746b9a39d02 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -215,6 +215,9 @@ rflink==0.0.37 # homeassistant.components.ring ring_doorbell==0.2.1 +# homeassistant.components.lovelace +ruamel.yaml==0.15.72 + # homeassistant.components.media_player.yamaha rxv==0.5.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 47d11dff582..491531ee12b 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -111,6 +111,7 @@ TEST_REQUIREMENTS = ( 'wakeonlan', 'vultr', 'YesssSMS', + 'ruamel.yaml', ) IGNORE_PACKAGES = ( diff --git a/tests/components/lovelace/test_init.py b/tests/components/lovelace/test_init.py index 0fde6de902c..5e4cf2d8037 100644 --- a/tests/components/lovelace/test_init.py +++ b/tests/components/lovelace/test_init.py @@ -1,9 +1,163 @@ """Test the Lovelace initialization.""" +import os +import unittest from unittest.mock import patch +from tempfile import mkdtemp +from ruamel.yaml import YAML from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.components.lovelace import (load_yaml, + save_yaml, load_config) + +TEST_YAML_A = """\ +title: My Awesome Home +# Include external resources +resources: + - url: /local/my-custom-card.js + type: js + - url: /local/my-webfont.css + type: css + +# Exclude entities from "Unused entities" view +excluded_entities: + - weblink.router +views: + # View tab title. + - title: Example + # Optional unique id for direct access /lovelace/${id} + id: example + # Optional background (overwrites the global background). + background: radial-gradient(crimson, skyblue) + # Each view can have a different theme applied. + theme: dark-mode + # The cards to show on this view. + cards: + # The filter card will filter entities for their state + - type: entity-filter + entities: + - device_tracker.paulus + - device_tracker.anne_there + state_filter: + - 'home' + card: + type: glance + title: People that are home + + # The picture entity card will represent an entity with a picture + - type: picture-entity + image: https://www.home-assistant.io/images/default-social.png + entity: light.bed_light + + # Specify a tab icon if you want the view tab to be an icon. + - icon: mdi:home-assistant + # Title of the view. Will be used as the tooltip for tab icon + title: Second view + cards: + # Entities card will take a list of entities and show their state. + - type: entities + # Title of the entities card + title: Example + # The entities here will be shown in the same order as specified. + # Each entry is an entity ID or a map with extra options. + entities: + - light.kitchen + - switch.ac + - entity: light.living_room + # Override the name to use + name: LR Lights + + # The markdown card will render markdown text. + - type: markdown + title: Lovelace + content: > + Welcome to your **Lovelace UI**. +""" + +TEST_YAML_B = """\ +title: Home +views: + - title: Dashboard + icon: mdi:home + cards: + - id: testid + type: vertical-stack + cards: + - type: picture-entity + entity: group.sample + name: Sample + image: /local/images/sample.jpg + tap_action: toggle +""" + +# Test data that can not be loaded as YAML +TEST_BAD_YAML = """\ +title: Home +views: + - title: Dashboard + icon: mdi:home + cards: + - id: testid + type: vertical-stack +""" + + +class TestYAML(unittest.TestCase): + """Test lovelace.yaml save and load.""" + + def setUp(self): + """Set up for tests.""" + self.tmp_dir = mkdtemp() + self.yaml = YAML(typ='rt') + + def tearDown(self): + """Clean up after tests.""" + for fname in os.listdir(self.tmp_dir): + os.remove(os.path.join(self.tmp_dir, fname)) + os.rmdir(self.tmp_dir) + + def _path_for(self, leaf_name): + return os.path.join(self.tmp_dir, leaf_name+".yaml") + + def test_save_and_load(self): + """Test saving and loading back.""" + fname = self._path_for("test1") + save_yaml(fname, self.yaml.load(TEST_YAML_A)) + data = load_yaml(fname) + self.assertEqual(data, self.yaml.load(TEST_YAML_A)) + + def test_overwrite_and_reload(self): + """Test that we can overwrite an existing file and read back.""" + fname = self._path_for("test3") + save_yaml(fname, self.yaml.load(TEST_YAML_A)) + save_yaml(fname, self.yaml.load(TEST_YAML_B)) + data = load_yaml(fname) + self.assertEqual(data, self.yaml.load(TEST_YAML_B)) + + def test_load_bad_data(self): + """Test error from trying to load unserialisable data.""" + fname = self._path_for("test5") + with open(fname, "w") as fh: + fh.write(TEST_BAD_YAML) + with self.assertRaises(HomeAssistantError): + load_yaml(fname) + + def test_add_id(self): + """Test if id is added.""" + fname = self._path_for("test6") + with patch('homeassistant.components.lovelace.load_yaml', + return_value=self.yaml.load(TEST_YAML_A)): + data = load_config(fname) + assert 'id' in data['views'][0]['cards'][0] + + def test_id_not_changed(self): + """Test if id is not changed if already exists.""" + fname = self._path_for("test7") + with patch('homeassistant.components.lovelace.load_yaml', + return_value=self.yaml.load(TEST_YAML_B)): + data = load_config(fname) + self.assertEqual(data, self.yaml.load(TEST_YAML_B)) async def test_deprecated_lovelace_ui(hass, hass_ws_client): @@ -11,7 +165,7 @@ async def test_deprecated_lovelace_ui(hass, hass_ws_client): await async_setup_component(hass, 'lovelace') client = await hass_ws_client(hass) - with patch('homeassistant.components.lovelace.load_yaml', + with patch('homeassistant.components.lovelace.load_config', return_value={'hello': 'world'}): await client.send_json({ 'id': 5, @@ -30,7 +184,7 @@ async def test_deprecated_lovelace_ui_not_found(hass, hass_ws_client): await async_setup_component(hass, 'lovelace') client = await hass_ws_client(hass) - with patch('homeassistant.components.lovelace.load_yaml', + with patch('homeassistant.components.lovelace.load_config', side_effect=FileNotFoundError): await client.send_json({ 'id': 5, @@ -49,7 +203,7 @@ async def test_deprecated_lovelace_ui_load_err(hass, hass_ws_client): await async_setup_component(hass, 'lovelace') client = await hass_ws_client(hass) - with patch('homeassistant.components.lovelace.load_yaml', + with patch('homeassistant.components.lovelace.load_config', side_effect=HomeAssistantError): await client.send_json({ 'id': 5, @@ -68,7 +222,7 @@ async def test_lovelace_ui(hass, hass_ws_client): await async_setup_component(hass, 'lovelace') client = await hass_ws_client(hass) - with patch('homeassistant.components.lovelace.load_yaml', + with patch('homeassistant.components.lovelace.load_config', return_value={'hello': 'world'}): await client.send_json({ 'id': 5, @@ -87,7 +241,7 @@ async def test_lovelace_ui_not_found(hass, hass_ws_client): await async_setup_component(hass, 'lovelace') client = await hass_ws_client(hass) - with patch('homeassistant.components.lovelace.load_yaml', + with patch('homeassistant.components.lovelace.load_config', side_effect=FileNotFoundError): await client.send_json({ 'id': 5, @@ -106,7 +260,7 @@ async def test_lovelace_ui_load_err(hass, hass_ws_client): await async_setup_component(hass, 'lovelace') client = await hass_ws_client(hass) - with patch('homeassistant.components.lovelace.load_yaml', + with patch('homeassistant.components.lovelace.load_config', side_effect=HomeAssistantError): await client.send_json({ 'id': 5, From daf9d28565080dd083261fcfc0b397de1d7de75f Mon Sep 17 00:00:00 2001 From: ehendrix23 Date: Wed, 17 Oct 2018 10:42:39 -0600 Subject: [PATCH 197/265] Fix mold_indicator errors at startup (#17346) * Initial changes to resolve issue 16733 Added logic to ensure that if the state is unknown during startup that the error about being unable to parse the value is not logged. Further, also ensured that if an attribute is set to None it does not try to convert the None value to Fahrenheit as that will cause an error. * Cleaned up and added few comments Cleaned up some lines based on flake8 and pylint. Added some comment lines on the items added. * Changed to async and using async_added_to_hass Changed sensor to use async. Registering state tracking for sensors and initial setup is now done upon the home assistant start event. * Updated test and small fix Updated test to handle unavailable state of sensor and return of None for attributes when data is unavailable. Ensured that atributes are set to None when state is unavailable due to incorrect data. * Fixed some flake8 issues in test Fixed some flake8 issues in test_moldindicator.py. * Updates based on review Updates based on review from MartinHjelmare * Added sensor entity_id to logger errors Added sensor entity_id to logger error messages Update test to use constant STATE_UNKNOWN instead of fixed string. --- .../components/sensor/mold_indicator.py | 185 +++++++++++++----- tests/components/sensor/test_moldindicator.py | 123 +++++++++++- 2 files changed, 249 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/sensor/mold_indicator.py b/homeassistant/components/sensor/mold_indicator.py index e5794ab1314..2a250f0e63d 100644 --- a/homeassistant/components/sensor/mold_indicator.py +++ b/homeassistant/components/sensor/mold_indicator.py @@ -9,12 +9,15 @@ import math import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant import util -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import track_state_change +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.core import callback from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_NAME) + ATTR_UNIT_OF_MEASUREMENT, EVENT_HOMEASSISTANT_START, STATE_UNKNOWN, + TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_NAME) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_state_change + import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -41,7 +44,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up MoldIndicator sensor.""" name = config.get(CONF_NAME, DEFAULT_NAME) indoor_temp_sensor = config.get(CONF_INDOOR_TEMP) @@ -49,16 +53,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): indoor_humidity_sensor = config.get(CONF_INDOOR_HUMIDITY) calib_factor = config.get(CONF_CALIBRATION_FACTOR) - add_entities([MoldIndicator( - hass, name, indoor_temp_sensor, outdoor_temp_sensor, - indoor_humidity_sensor, calib_factor)], True) + async_add_entities([MoldIndicator( + name, hass.config.units.is_metric, indoor_temp_sensor, + outdoor_temp_sensor, indoor_humidity_sensor, calib_factor)], False) class MoldIndicator(Entity): """Represents a MoldIndication sensor.""" - def __init__(self, hass, name, indoor_temp_sensor, outdoor_temp_sensor, - indoor_humidity_sensor, calib_factor): + def __init__(self, name, is_metric, indoor_temp_sensor, + outdoor_temp_sensor, indoor_humidity_sensor, calib_factor): """Initialize the sensor.""" self._state = None self._name = name @@ -66,7 +70,11 @@ class MoldIndicator(Entity): self._indoor_humidity_sensor = indoor_humidity_sensor self._outdoor_temp_sensor = outdoor_temp_sensor self._calib_factor = calib_factor - self._is_metric = hass.config.units.is_metric + self._is_metric = is_metric + self._available = False + self._entities = set([self._indoor_temp_sensor, + self._indoor_humidity_sensor, + self._outdoor_temp_sensor]) self._dewpoint = None self._indoor_temp = None @@ -74,34 +82,85 @@ class MoldIndicator(Entity): self._indoor_hum = None self._crit_temp = None - track_state_change(hass, indoor_temp_sensor, self._sensor_changed) - track_state_change(hass, outdoor_temp_sensor, self._sensor_changed) - track_state_change(hass, indoor_humidity_sensor, self._sensor_changed) + async def async_added_to_hass(self): + """Register callbacks.""" + @callback + def mold_indicator_sensors_state_listener(entity, old_state, + new_state): + """Handle for state changes for dependent sensors.""" + _LOGGER.debug("Sensor state change for %s that had old state %s " + "and new state %s", entity, old_state, new_state) - # Read initial state - indoor_temp = hass.states.get(indoor_temp_sensor) - outdoor_temp = hass.states.get(outdoor_temp_sensor) - indoor_hum = hass.states.get(indoor_humidity_sensor) + if self._update_sensor(entity, old_state, new_state): + self.async_schedule_update_ha_state(True) - if indoor_temp: - self._indoor_temp = MoldIndicator._update_temp_sensor(indoor_temp) + @callback + def mold_indicator_startup(event): + """Add listeners and get 1st state.""" + _LOGGER.debug("Startup for %s", self.entity_id) - if outdoor_temp: - self._outdoor_temp = MoldIndicator._update_temp_sensor( - outdoor_temp) + async_track_state_change(self.hass, self._entities, + mold_indicator_sensors_state_listener) - if indoor_hum: - self._indoor_hum = MoldIndicator._update_hum_sensor(indoor_hum) + # Read initial state + indoor_temp = self.hass.states.get(self._indoor_temp_sensor) + outdoor_temp = self.hass.states.get(self._outdoor_temp_sensor) + indoor_hum = self.hass.states.get(self._indoor_humidity_sensor) + + schedule_update = self._update_sensor(self._indoor_temp_sensor, + None, indoor_temp) + + schedule_update = False if not self._update_sensor( + self._outdoor_temp_sensor, None, outdoor_temp) else\ + schedule_update + + schedule_update = False if not self._update_sensor( + self._indoor_humidity_sensor, None, indoor_hum) else\ + schedule_update + + if schedule_update: + self.async_schedule_update_ha_state(True) + + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, mold_indicator_startup) + + def _update_sensor(self, entity, old_state, new_state): + """Update information based on new sensor states.""" + _LOGGER.debug("Sensor update for %s", entity) + if new_state is None: + return False + + # If old_state is not set and new state is unknown then it means + # that the sensor just started up + if old_state is None and new_state.state == STATE_UNKNOWN: + return False + + if entity == self._indoor_temp_sensor: + self._indoor_temp = MoldIndicator._update_temp_sensor(new_state) + elif entity == self._outdoor_temp_sensor: + self._outdoor_temp = MoldIndicator._update_temp_sensor(new_state) + elif entity == self._indoor_humidity_sensor: + self._indoor_hum = MoldIndicator._update_hum_sensor(new_state) + + return True @staticmethod def _update_temp_sensor(state): """Parse temperature sensor value.""" + _LOGGER.debug("Updating temp sensor with value %s", state.state) + + # Return an error if the sensor change its state to Unknown. + if state.state == STATE_UNKNOWN: + _LOGGER.error("Unable to parse temperature sensor %s with state:" + " %s", state.entity_id, state.state) + return None + unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) temp = util.convert(state.state, float) if temp is None: - _LOGGER.error('Unable to parse sensor temperature: %s', - state.state) + _LOGGER.error("Unable to parse temperature sensor %s with state:" + " %s", state.entity_id, state.state) return None # convert to celsius if necessary @@ -109,56 +168,62 @@ class MoldIndicator(Entity): return util.temperature.fahrenheit_to_celsius(temp) if unit == TEMP_CELSIUS: return temp - _LOGGER.error("Temp sensor has unsupported unit: %s (allowed: %s, " - "%s)", unit, TEMP_CELSIUS, TEMP_FAHRENHEIT) + _LOGGER.error("Temp sensor %s has unsupported unit: %s (allowed: %s, " + "%s)", state.entity_id, unit, TEMP_CELSIUS, + TEMP_FAHRENHEIT) return None @staticmethod def _update_hum_sensor(state): """Parse humidity sensor value.""" + _LOGGER.debug("Updating humidity sensor with value %s", state.state) + + # Return an error if the sensor change its state to Unknown. + if state.state == STATE_UNKNOWN: + _LOGGER.error('Unable to parse humidity sensor %s, state: %s', + state.entity_id, state.state) + return None + unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) hum = util.convert(state.state, float) if hum is None: - _LOGGER.error('Unable to parse sensor humidity: %s', - state.state) + _LOGGER.error("Unable to parse humidity sensor %s, state: %s", + state.entity_id, state.state) return None if unit != '%': - _LOGGER.error("Humidity sensor has unsupported unit: %s %s", - unit, " (allowed: %)") + _LOGGER.error("Humidity sensor %s has unsupported unit: %s %s", + state.entity_id, unit, " (allowed: %)") + return None if hum > 100 or hum < 0: - _LOGGER.error("Humidity sensor out of range: %s %s", hum, - " (allowed: 0-100%)") + _LOGGER.error("Humidity sensor %s is out of range: %s %s", + state.entity_id, hum, "(allowed: 0-100%)") + return None return hum - def update(self): + async def async_update(self): """Calculate latest state.""" + _LOGGER.debug("Update state for %s", self.entity_id) # check all sensors if None in (self._indoor_temp, self._indoor_hum, self._outdoor_temp): + self._available = False + self._dewpoint = None + self._crit_temp = None return # re-calculate dewpoint and mold indicator self._calc_dewpoint() self._calc_moldindicator() - - def _sensor_changed(self, entity_id, old_state, new_state): - """Handle sensor state changes.""" - if new_state is None: - return - - if entity_id == self._indoor_temp_sensor: - self._indoor_temp = MoldIndicator._update_temp_sensor(new_state) - elif entity_id == self._outdoor_temp_sensor: - self._outdoor_temp = MoldIndicator._update_temp_sensor(new_state) - elif entity_id == self._indoor_humidity_sensor: - self._indoor_hum = MoldIndicator._update_hum_sensor(new_state) - - self.update() - self.schedule_update_ha_state() + if self._state is None: + self._available = False + self._dewpoint = None + self._crit_temp = None + else: + self._available = True def _calc_dewpoint(self): """Calculate the dewpoint for the indoor air.""" @@ -183,6 +248,8 @@ class MoldIndicator(Entity): " calibration-factor: %s", self._dewpoint, self._calib_factor) self._state = None + self._available = False + self._crit_temp = None return # first calculate the approximate temperature at the calibration point @@ -232,6 +299,11 @@ class MoldIndicator(Entity): """Return the state of the entity.""" return self._state + @property + def available(self): + """Return the availability of this sensor.""" + return self._available + @property def device_state_attributes(self): """Return the state attributes.""" @@ -240,9 +312,16 @@ class MoldIndicator(Entity): ATTR_DEWPOINT: self._dewpoint, ATTR_CRITICAL_TEMP: self._crit_temp, } + + dewpoint = util.temperature.celsius_to_fahrenheit(self._dewpoint) \ + if self._dewpoint is not None else None + + crit_temp = util.temperature.celsius_to_fahrenheit(self._crit_temp) \ + if self._crit_temp is not None else None + return { ATTR_DEWPOINT: - util.temperature.celsius_to_fahrenheit(self._dewpoint), + dewpoint, ATTR_CRITICAL_TEMP: - util.temperature.celsius_to_fahrenheit(self._crit_temp), + crit_temp, } diff --git a/tests/components/sensor/test_moldindicator.py b/tests/components/sensor/test_moldindicator.py index 4f1b40bf9ef..7b2480f1298 100644 --- a/tests/components/sensor/test_moldindicator.py +++ b/tests/components/sensor/test_moldindicator.py @@ -5,8 +5,8 @@ from homeassistant.setup import setup_component import homeassistant.components.sensor as sensor from homeassistant.components.sensor.mold_indicator import (ATTR_DEWPOINT, ATTR_CRITICAL_TEMP) -from homeassistant.const import (ATTR_UNIT_OF_MEASUREMENT, - TEMP_CELSIUS) +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CELSIUS) from tests.common import get_test_home_assistant @@ -44,7 +44,7 @@ class TestSensorMoldIndicator(unittest.TestCase): assert moldind assert '%' == moldind.attributes.get('unit_of_measurement') - def test_invalidhum(self): + def test_invalidcalib(self): """Test invalid sensor values.""" self.hass.states.set('test.indoortemp', '10', {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) @@ -53,6 +53,32 @@ class TestSensorMoldIndicator(unittest.TestCase): self.hass.states.set('test.indoorhumidity', '0', {ATTR_UNIT_OF_MEASUREMENT: '%'}) + self.assertTrue(setup_component(self.hass, sensor.DOMAIN, { + 'sensor': { + 'platform': 'mold_indicator', + 'indoor_temp_sensor': 'test.indoortemp', + 'outdoor_temp_sensor': 'test.outdoortemp', + 'indoor_humidity_sensor': 'test.indoorhumidity', + 'calibration_factor': 0 + } + })) + self.hass.start() + self.hass.block_till_done() + moldind = self.hass.states.get('sensor.mold_indicator') + assert moldind + assert moldind.state == 'unavailable' + assert moldind.attributes.get(ATTR_DEWPOINT) is None + assert moldind.attributes.get(ATTR_CRITICAL_TEMP) is None + + def test_invalidhum(self): + """Test invalid sensor values.""" + self.hass.states.set('test.indoortemp', '10', + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + self.hass.states.set('test.outdoortemp', '10', + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + self.hass.states.set('test.indoorhumidity', '-1', + {ATTR_UNIT_OF_MEASUREMENT: '%'}) + self.assertTrue(setup_component(self.hass, sensor.DOMAIN, { 'sensor': { 'platform': 'mold_indicator', @@ -62,9 +88,32 @@ class TestSensorMoldIndicator(unittest.TestCase): 'calibration_factor': 2.0 } })) + + self.hass.start() + self.hass.block_till_done() moldind = self.hass.states.get('sensor.mold_indicator') assert moldind - assert moldind.state == '0' + assert moldind.state == 'unavailable' + assert moldind.attributes.get(ATTR_DEWPOINT) is None + assert moldind.attributes.get(ATTR_CRITICAL_TEMP) is None + + self.hass.states.set('test.indoorhumidity', 'A', + {ATTR_UNIT_OF_MEASUREMENT: '%'}) + self.hass.block_till_done() + moldind = self.hass.states.get('sensor.mold_indicator') + assert moldind + assert moldind.state == 'unavailable' + assert moldind.attributes.get(ATTR_DEWPOINT) is None + assert moldind.attributes.get(ATTR_CRITICAL_TEMP) is None + + self.hass.states.set('test.indoorhumidity', '10', + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + self.hass.block_till_done() + moldind = self.hass.states.get('sensor.mold_indicator') + assert moldind + assert moldind.state == 'unavailable' + assert moldind.attributes.get(ATTR_DEWPOINT) is None + assert moldind.attributes.get(ATTR_CRITICAL_TEMP) is None def test_calculation(self): """Test the mold indicator internal calculations.""" @@ -77,7 +126,8 @@ class TestSensorMoldIndicator(unittest.TestCase): 'calibration_factor': 2.0 } })) - + self.hass.start() + self.hass.block_till_done() moldind = self.hass.states.get('sensor.mold_indicator') assert moldind @@ -98,6 +148,66 @@ class TestSensorMoldIndicator(unittest.TestCase): assert state assert state == '68' + def test_unknown_sensor(self): + """Test the sensor_changed function.""" + self.assertTrue(setup_component(self.hass, sensor.DOMAIN, { + 'sensor': { + 'platform': 'mold_indicator', + 'indoor_temp_sensor': 'test.indoortemp', + 'outdoor_temp_sensor': 'test.outdoortemp', + 'indoor_humidity_sensor': 'test.indoorhumidity', + 'calibration_factor': 2.0 + } + })) + self.hass.start() + + self.hass.states.set('test.indoortemp', STATE_UNKNOWN, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + self.hass.block_till_done() + moldind = self.hass.states.get('sensor.mold_indicator') + assert moldind + assert moldind.state == 'unavailable' + assert moldind.attributes.get(ATTR_DEWPOINT) is None + assert moldind.attributes.get(ATTR_CRITICAL_TEMP) is None + + self.hass.states.set('test.indoortemp', '30', + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + self.hass.states.set('test.outdoortemp', STATE_UNKNOWN, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + self.hass.block_till_done() + moldind = self.hass.states.get('sensor.mold_indicator') + assert moldind + assert moldind.state == 'unavailable' + assert moldind.attributes.get(ATTR_DEWPOINT) is None + assert moldind.attributes.get(ATTR_CRITICAL_TEMP) is None + + self.hass.states.set('test.outdoortemp', '25', + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + self.hass.states.set('test.indoorhumidity', STATE_UNKNOWN, + {ATTR_UNIT_OF_MEASUREMENT: '%'}) + self.hass.block_till_done() + moldind = self.hass.states.get('sensor.mold_indicator') + assert moldind + assert moldind.state == 'unavailable' + assert moldind.attributes.get(ATTR_DEWPOINT) is None + assert moldind.attributes.get(ATTR_CRITICAL_TEMP) is None + + self.hass.states.set('test.indoorhumidity', '20', + {ATTR_UNIT_OF_MEASUREMENT: '%'}) + self.hass.block_till_done() + moldind = self.hass.states.get('sensor.mold_indicator') + assert moldind + assert moldind.state == '23' + + dewpoint = moldind.attributes.get(ATTR_DEWPOINT) + assert dewpoint + assert dewpoint > 4.58 + assert dewpoint < 4.59 + + esttemp = moldind.attributes.get(ATTR_CRITICAL_TEMP) + assert esttemp + assert esttemp == 27.5 + def test_sensor_changed(self): """Test the sensor_changed function.""" self.assertTrue(setup_component(self.hass, sensor.DOMAIN, { @@ -109,6 +219,7 @@ class TestSensorMoldIndicator(unittest.TestCase): 'calibration_factor': 2.0 } })) + self.hass.start() self.hass.states.set('test.indoortemp', '30', {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) @@ -121,6 +232,6 @@ class TestSensorMoldIndicator(unittest.TestCase): assert self.hass.states.get('sensor.mold_indicator').state == '57' self.hass.states.set('test.indoorhumidity', '20', - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + {ATTR_UNIT_OF_MEASUREMENT: '%'}) self.hass.block_till_done() assert self.hass.states.get('sensor.mold_indicator').state == '23' From 7106d9e9d43b31d67e3d9c16edcb8d9e955e04c1 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Wed, 17 Oct 2018 22:56:21 +0200 Subject: [PATCH 198/265] Upgrade async_upnp_client to 0.12.6 (#17560) --- homeassistant/components/media_player/dlna_dmr.py | 2 +- homeassistant/components/upnp/__init__.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_player/dlna_dmr.py b/homeassistant/components/media_player/dlna_dmr.py index 25a729aed6d..b787ed689c8 100644 --- a/homeassistant/components/media_player/dlna_dmr.py +++ b/homeassistant/components/media_player/dlna_dmr.py @@ -25,7 +25,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.util import get_local_ip -REQUIREMENTS = ['async-upnp-client==0.12.5'] +REQUIREMENTS = ['async-upnp-client==0.12.6'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index bf9f8a4746d..c496caba948 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -30,7 +30,7 @@ from .config_flow import ensure_domain_data from .device import Device -REQUIREMENTS = ['async-upnp-client==0.12.5'] +REQUIREMENTS = ['async-upnp-client==0.12.6'] DEPENDENCIES = ['http'] NOTIFICATION_ID = 'upnp_notification' diff --git a/requirements_all.txt b/requirements_all.txt index 6dfc1ee5713..da03d8163a2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -153,7 +153,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.upnp # homeassistant.components.media_player.dlna_dmr -async-upnp-client==0.12.5 +async-upnp-client==0.12.6 # homeassistant.components.light.avion # avion==0.7 From 50a66abd80f85bdc9ea2a21dbf078f60aa9476b9 Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Wed, 17 Oct 2018 22:56:54 +0200 Subject: [PATCH 199/265] Updated package to fix #16960 (#17555) --- homeassistant/components/media_player/webostv.py | 2 +- homeassistant/components/notify/webostv.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py index 0a5b9fe509b..946e0517435 100644 --- a/homeassistant/components/media_player/webostv.py +++ b/homeassistant/components/media_player/webostv.py @@ -24,7 +24,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.helpers.script import Script -REQUIREMENTS = ['pylgtv==0.1.7', 'websockets==6.0'] +REQUIREMENTS = ['pylgtv==0.1.9', 'websockets==6.0'] _CONFIGURING = {} # type: Dict[str, str] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/notify/webostv.py b/homeassistant/components/notify/webostv.py index 78c43c5f0ad..92762b03aea 100644 --- a/homeassistant/components/notify/webostv.py +++ b/homeassistant/components/notify/webostv.py @@ -13,7 +13,7 @@ from homeassistant.components.notify import ( ATTR_DATA, BaseNotificationService, PLATFORM_SCHEMA) from homeassistant.const import (CONF_FILENAME, CONF_HOST, CONF_ICON) -REQUIREMENTS = ['pylgtv==0.1.7'] +REQUIREMENTS = ['pylgtv==0.1.9'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index da03d8163a2..f68bf195f7e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -960,7 +960,7 @@ pylgnetcast-homeassistant==0.2.0.dev0 # homeassistant.components.media_player.webostv # homeassistant.components.notify.webostv -pylgtv==0.1.7 +pylgtv==0.1.9 # homeassistant.components.sensor.linky pylinky==0.1.6 From b50c93ccb77cca90bbc997937875fb247dfc454e Mon Sep 17 00:00:00 2001 From: Jorim Tielemans Date: Wed, 17 Oct 2018 23:09:05 +0200 Subject: [PATCH 200/265] Validate ports as a port (#17549) * Validate ports as port Better than just a positive integer since it limits the range from 1 to 65535. * Validate port for Axis * Validate port for Xiaomi Home Camera * Validate port for Modbus * Validate port for Yamaha MusicCast Receivers * Update zhong_hong.py Validate port as a port, the gateway address as positive_int Also moved the default values to their variable * Validate port for the Asterisk Voicemail interface * Fix lint * Validate port for Xiaomi Cameras --- homeassistant/components/asterisk_mbox.py | 2 +- homeassistant/components/axis.py | 3 ++- homeassistant/components/camera/xiaomi.py | 2 +- homeassistant/components/camera/yi.py | 2 +- homeassistant/components/climate/zhong_hong.py | 16 +++++++++------- .../components/media_player/yamaha_musiccast.py | 2 +- homeassistant/components/modbus.py | 2 +- homeassistant/components/upnp/__init__.py | 4 ++-- 8 files changed, 18 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/asterisk_mbox.py b/homeassistant/components/asterisk_mbox.py index 0907e48b256..406774d5fad 100644 --- a/homeassistant/components/asterisk_mbox.py +++ b/homeassistant/components/asterisk_mbox.py @@ -31,7 +31,7 @@ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_PORT): int, + vol.Required(CONF_PORT): cv.port, }), }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/axis.py b/homeassistant/components/axis.py index 71894364f91..63fce8a74ee 100644 --- a/homeassistant/components/axis.py +++ b/homeassistant/components/axis.py @@ -38,6 +38,7 @@ AXIS_INCLUDE = EVENT_TYPES + PLATFORMS AXIS_DEFAULT_HOST = '192.168.0.90' AXIS_DEFAULT_USERNAME = 'root' AXIS_DEFAULT_PASSWORD = 'pass' +DEFAULT_PORT = 80 DEVICE_SCHEMA = vol.Schema({ vol.Required(CONF_INCLUDE): @@ -47,7 +48,7 @@ DEVICE_SCHEMA = vol.Schema({ vol.Optional(CONF_USERNAME, default=AXIS_DEFAULT_USERNAME): cv.string, vol.Optional(CONF_PASSWORD, default=AXIS_DEFAULT_PASSWORD): cv.string, vol.Optional(CONF_TRIGGER_TIME, default=0): cv.positive_int, - vol.Optional(CONF_PORT, default=80): cv.positive_int, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(ATTR_LOCATION, default=''): cv.string, }) diff --git a/homeassistant/components/camera/xiaomi.py b/homeassistant/components/camera/xiaomi.py index da36299a209..3d6b51cf229 100644 --- a/homeassistant/components/camera/xiaomi.py +++ b/homeassistant/components/camera/xiaomi.py @@ -35,7 +35,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_MODEL): vol.Any(MODEL_YI, MODEL_XIAOFANG), - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string, vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/camera/yi.py b/homeassistant/components/camera/yi.py index eb26c1cc887..f3800ee0648 100644 --- a/homeassistant/components/camera/yi.py +++ b/homeassistant/components/camera/yi.py @@ -32,7 +32,7 @@ CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_NAME): cv.string, vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string, vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/climate/zhong_hong.py b/homeassistant/components/climate/zhong_hong.py index 46d590a9412..b564e9d1fa4 100644 --- a/homeassistant/components/climate/zhong_hong.py +++ b/homeassistant/components/climate/zhong_hong.py @@ -18,21 +18,23 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import (async_dispatcher_connect, async_dispatcher_send) +REQUIREMENTS = ['zhong_hong_hvac==1.0.9'] + _LOGGER = logging.getLogger(__name__) CONF_GATEWAY_ADDRRESS = 'gateway_address' -REQUIREMENTS = ['zhong_hong_hvac==1.0.9'] +DEFAULT_PORT = 9999 +DEFAULT_GATEWAY_ADDRRESS = 1 + SIGNAL_DEVICE_ADDED = 'zhong_hong_device_added' SIGNAL_ZHONG_HONG_HUB_START = 'zhong_hong_hub_start' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): - cv.string, - vol.Optional(CONF_PORT, default=9999): - vol.Coerce(int), - vol.Optional(CONF_GATEWAY_ADDRRESS, default=1): - vol.Coerce(int), + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_GATEWAY_ADDRRESS, default=DEFAULT_GATEWAY_ADDRRESS): + cv.positive_int, }) diff --git a/homeassistant/components/media_player/yamaha_musiccast.py b/homeassistant/components/media_player/yamaha_musiccast.py index bf21a3f5028..535a2ad01ca 100644 --- a/homeassistant/components/media_player/yamaha_musiccast.py +++ b/homeassistant/components/media_player/yamaha_musiccast.py @@ -39,7 +39,7 @@ DEFAULT_INTERVAL = 480 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.positive_int, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(INTERVAL_SECONDS, default=DEFAULT_INTERVAL): cv.positive_int, }) diff --git a/homeassistant/components/modbus.py b/homeassistant/components/modbus.py index f484cb31a6c..c8d71af71b4 100644 --- a/homeassistant/components/modbus.py +++ b/homeassistant/components/modbus.py @@ -37,7 +37,7 @@ SERIAL_SCHEMA = { ETHERNET_SCHEMA = { vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PORT): cv.positive_int, + vol.Required(CONF_PORT): cv.port, vol.Required(CONF_TYPE): vol.Any('tcp', 'udp', 'rtuovertcp'), vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout, } diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index c496caba948..5e6549b71b7 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -43,8 +43,8 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_LOCAL_IP): vol.All(ip_address, cv.string), vol.Optional(CONF_PORTS): vol.Schema({ - vol.Any(CONF_HASS, cv.positive_int): - vol.Any(CONF_HASS, cv.positive_int) + vol.Any(CONF_HASS, cv.port): + vol.Any(CONF_HASS, cv.port) }) }), }, extra=vol.ALLOW_EXTRA) From 3e5233d115d5e4fb30fe33543df7ae816e1ae117 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Thu, 18 Oct 2018 08:12:09 +0200 Subject: [PATCH 201/265] danielhiversen as mill codeowner (#17571) --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/CODEOWNERS b/CODEOWNERS index 40a33c66b78..c49af4864a9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -54,6 +54,7 @@ homeassistant/components/binary_sensor/threshold.py @fabaff homeassistant/components/camera/yi.py @bachya homeassistant/components/climate/ephember.py @ttroy50 homeassistant/components/climate/eq3btsmart.py @rytilahti +homeassistant/components/climate/mill.py @danielhiversen homeassistant/components/climate/sensibo.py @andrey-git homeassistant/components/cover/group.py @cdce8p homeassistant/components/cover/template.py @PhracturedBlue From 45878c6df03d2199a3b56842ee9f66b843b3bd0d Mon Sep 17 00:00:00 2001 From: Brian Gianforcaro Date: Thu, 18 Oct 2018 00:17:55 -0700 Subject: [PATCH 202/265] Upgrade twilio package to version 6.19.1 (#17395) (#17424) - Bump twilio requirement to latest 6.19.1 version - The generic response type is gone in the latest versions of the twilio package. It appears we were generating an empty response just to get the empty xml body. TwilML is the new base class all responses inherit from. So I've switched the code over to using and empty TwilML object instead. - The exception type was moved to a different location. --- homeassistant/components/notify/twilio_call.py | 2 +- homeassistant/components/twilio.py | 6 +++--- requirements_all.txt | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/notify/twilio_call.py b/homeassistant/components/notify/twilio_call.py index b517808d2ce..538a4fd9512 100644 --- a/homeassistant/components/notify/twilio_call.py +++ b/homeassistant/components/notify/twilio_call.py @@ -42,7 +42,7 @@ class TwilioCallNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Call to specified target users.""" - from twilio import TwilioRestException + from twilio.base.exceptions import TwilioRestException targets = kwargs.get(ATTR_TARGET) diff --git a/homeassistant/components/twilio.py b/homeassistant/components/twilio.py index 9f32a44ce7e..9f9767e4675 100644 --- a/homeassistant/components/twilio.py +++ b/homeassistant/components/twilio.py @@ -10,7 +10,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.core import callback from homeassistant.components.http import HomeAssistantView -REQUIREMENTS = ['twilio==5.7.0'] +REQUIREMENTS = ['twilio==6.19.1'] DOMAIN = 'twilio' @@ -51,8 +51,8 @@ class TwilioReceiveDataView(HomeAssistantView): @callback def post(self, request): # pylint: disable=no-self-use """Handle Twilio data post.""" - from twilio.twiml import Response + from twilio.twiml import TwiML hass = request.app['hass'] data = yield from request.post() hass.bus.async_fire(RECEIVED_DATA, dict(data)) - return Response().toxml() + return TwiML().to_xml() diff --git a/requirements_all.txt b/requirements_all.txt index f68bf195f7e..4e9248dc931 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1485,7 +1485,7 @@ transmissionrpc==0.11 tuyapy==0.1.3 # homeassistant.components.twilio -twilio==5.7.0 +twilio==6.19.1 # homeassistant.components.sensor.uber uber_rides==0.6.0 From e2a1e21c8d14bdf10951b03347a6fc4971a1da39 Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Thu, 18 Oct 2018 01:39:33 -0700 Subject: [PATCH 203/265] Add support for LG soundbars (#17570) * Add LG soundbar support We can autodiscover these, so for now let's skip any local configuration. Currently we expose volume, source and equaliser preset - we can expose the other volume controls and options as well if necessary, but I don't know whether it's worth it. * Add discovery of LG devices This is a generic discovery type that doesn't obviously contain enough information to identify whether we're talking to a speaker system or any other kind of device - on the other hand I haven't been able to find any other LG devices that respond like this, so we can cross that bridge when we get to it. * Lint --- .coveragerc | 1 + homeassistant/components/discovery.py | 3 +- .../components/media_player/lg_soundbar.py | 196 ++++++++++++++++++ requirements_all.txt | 5 +- 4 files changed, 203 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/media_player/lg_soundbar.py diff --git a/.coveragerc b/.coveragerc index 8a52a625341..42939e71704 100644 --- a/.coveragerc +++ b/.coveragerc @@ -569,6 +569,7 @@ omit = homeassistant/components/media_player/itunes.py homeassistant/components/media_player/kodi.py homeassistant/components/media_player/lg_netcast.py + homeassistant/components/media_player/lg_soundbar.py homeassistant/components/media_player/liveboxplaytv.py homeassistant/components/media_player/mediaroom.py homeassistant/components/media_player/mpchc.py diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 36f41e15a47..d7bb966a2d3 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.discovery import async_load_platform, async_discover import homeassistant.util.dt as dt_util -REQUIREMENTS = ['netdisco==2.1.0'] +REQUIREMENTS = ['netdisco==2.2.0'] DOMAIN = 'discovery' @@ -86,6 +86,7 @@ SERVICE_HANDLERS = { 'songpal': ('media_player', 'songpal'), 'kodi': ('media_player', 'kodi'), 'volumio': ('media_player', 'volumio'), + 'lg_smart_device': ('media_player', 'lg_soundbar'), 'nanoleaf_aurora': ('light', 'nanoleaf_aurora'), 'freebox': ('device_tracker', 'freebox'), } diff --git a/homeassistant/components/media_player/lg_soundbar.py b/homeassistant/components/media_player/lg_soundbar.py new file mode 100644 index 00000000000..38b27bd074a --- /dev/null +++ b/homeassistant/components/media_player/lg_soundbar.py @@ -0,0 +1,196 @@ +""" +Support for LG soundbars. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.lg_soundbar/ +""" +import logging + +from homeassistant.components.media_player import ( + SUPPORT_SELECT_SOURCE, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + SUPPORT_SELECT_SOUND_MODE, MediaPlayerDevice) + +from homeassistant.const import STATE_ON + +REQUIREMENTS = ['temescal==0.1'] + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_LG = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | SUPPORT_SELECT_SOURCE \ + | SUPPORT_SELECT_SOUND_MODE + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the LG platform.""" + if discovery_info is not None: + add_entities([LGDevice(discovery_info)], True) + + +class LGDevice(MediaPlayerDevice): + """Representation of an LG soundbar device.""" + + def __init__(self, discovery_info): + """Initialize the LG speakers.""" + import temescal + + host = discovery_info.get('host') + port = discovery_info.get('port') + + self._name = "" + self._volume = 0 + self._volume_min = 0 + self._volume_max = 0 + self._function = -1 + self._functions = [] + self._equaliser = -1 + self._equalisers = [] + self._mute = 0 + self._rear_volume = 0 + self._rear_volume_min = 0 + self._rear_volume_max = 0 + self._woofer_volume = 0 + self._woofer_volume_min = 0 + self._woofer_volume_max = 0 + self._bass = 0 + self._treble = 0 + + self._device = temescal.temescal(host, port=port, + callback=self.handle_event) + self.update() + + def handle_event(self, response): + """Handle responses from the speakers.""" + data = response['data'] + if response['msg'] == "EQ_VIEW_INFO": + if 'i_bass' in data: + self._bass = data['i_bass'] + if 'i_treble' in data: + self._treble = data['i_treble'] + if 'ai_eq_list' in data: + self._equalisers = data['ai_eq_list'] + if 'i_curr_eq' in data: + self._equaliser = data['i_curr_eq'] + elif response['msg'] == "SPK_LIST_VIEW_INFO": + if 'i_vol' in data: + self._volume = data['i_vol'] + if 's_user_name' in data: + self._name = data['s_user_name'] + if 'i_vol_min' in data: + self._volume_min = data['i_vol_min'] + if 'i_vol_max' in data: + self._volume_max = data['i_vol_max'] + if 'b_mute' in data: + self._mute = data['b_mute'] + if 'i_curr_func' in data: + self._function = data['i_curr_func'] + elif response['msg'] == "FUNC_VIEW_INFO": + if 'i_curr_func' in data: + self._function = data['i_curr_func'] + if 'ai_func_list' in data: + self._functions = data['ai_func_list'] + elif response['msg'] == "SETTING_VIEW_INFO": + if 'i_rear_min' in data: + self._rear_volume_min = data['i_rear_min'] + if 'i_rear_max' in data: + self._rear_volume_max = data['i_rear_max'] + if 'i_rear_level' in data: + self._rear_volume = data['i_rear_level'] + if 'i_woofer_min' in data: + self._woofer_volume_min = data['i_woofer_min'] + if 'i_woofer_max' in data: + self._woofer_volume_max = data['i_woofer_max'] + if 'i_woofer_level' in data: + self._woofer_volume = data['i_woofer_level'] + if 'i_curr_eq' in data: + self._equaliser = data['i_curr_eq'] + if 's_user_name' in data: + self._name = data['s_user_name'] + self.schedule_update_ha_state() + + def update(self): + """Trigger updates from the device.""" + self._device.get_eq() + self._device.get_info() + self._device.get_func() + self._device.get_settings() + self._device.get_product_info() + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def volume_level(self): + """Volume level of the media player (0..1).""" + if self._volume_max != 0: + return self._volume/self._volume_max + return 0 + + @property + def is_volume_muted(self): + """Boolean if volume is currently muted.""" + return self._mute + + @property + def state(self): + """Return the state of the device.""" + return STATE_ON + + @property + def sound_mode(self): + """Return the current sound mode.""" + import temescal + if self._equaliser == -1: + return "" + return temescal.equalisers[self._equaliser] + + @property + def sound_mode_list(self): + """Return the available sound modes.""" + import temescal + modes = [] + for equaliser in self._equalisers: + modes.append(temescal.equalisers[equaliser]) + return sorted(modes) + + @property + def source(self): + """Return the current input source.""" + import temescal + if self._function == -1: + return "" + return temescal.functions[self._function] + + @property + def source_list(self): + """List of available input sources.""" + import temescal + sources = [] + for function in self._functions: + sources.append(temescal.functions[function]) + return sorted(sources) + + @property + def supported_features(self): + """Flag media player features that are supported.""" + return SUPPORT_LG + + def set_volume_level(self, volume): + """Set volume level, range 0..1.""" + volume = volume * self._volume_max + self._device.set_volume(int(volume)) + + def mute_volume(self, mute): + """Mute (true) or unmute (false) media player.""" + self._device.set_mute(mute) + + def select_source(self, source): + """Select input source.""" + import temescal + self._device.set_func(temescal.functions.index(source)) + + def select_sound_mode(self, sound_mode): + """Set Sound Mode for Receiver..""" + import temescal + self._device.set_eq(temescal.equalisers.index(sound_mode)) diff --git a/requirements_all.txt b/requirements_all.txt index 4e9248dc931..2cd1f29dd49 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -640,7 +640,7 @@ ndms2_client==0.0.4 netdata==0.1.2 # homeassistant.components.discovery -netdisco==2.1.0 +netdisco==2.2.0 # homeassistant.components.sensor.neurio_energy neurio==0.3.1 @@ -1450,6 +1450,9 @@ tellcore-py==1.1.2 # homeassistant.components.tellduslive tellduslive==0.10.4 +# homeassistant.components.media_player.lg_soundbar +temescal==0.1 + # homeassistant.components.sensor.temper temperusb==1.5.3 From 20fa6c5383826bbccab4a3b8d6bb902c5dcbee8a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 18 Oct 2018 11:06:32 +0200 Subject: [PATCH 204/265] Update snapcast to 2.0.9 (#17573) --- homeassistant/components/media_player/snapcast.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/snapcast.py b/homeassistant/components/media_player/snapcast.py index fca440df783..cfe2f997295 100644 --- a/homeassistant/components/media_player/snapcast.py +++ b/homeassistant/components/media_player/snapcast.py @@ -17,7 +17,7 @@ from homeassistant.const import ( STATE_PLAYING, STATE_UNKNOWN) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['snapcast==2.0.8'] +REQUIREMENTS = ['snapcast==2.0.9'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 2cd1f29dd49..f7fc640607b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1386,7 +1386,7 @@ smappy==0.2.16 smhi-pkg==1.0.4 # homeassistant.components.media_player.snapcast -snapcast==2.0.8 +snapcast==2.0.9 # homeassistant.components.sensor.socialblade socialbladeclient==0.2 From 20bd14defb8cbde36efbedc9be4837bfecaf3b4f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 18 Oct 2018 13:52:38 +0200 Subject: [PATCH 205/265] Bump frontend to 20181018.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index ce553da31a4..df25803b4e0 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20181017.0'] +REQUIREMENTS = ['home-assistant-frontend==20181018.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index f7fc640607b..c6889a00016 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -466,7 +466,7 @@ hole==0.3.0 holidays==0.9.7 # homeassistant.components.frontend -home-assistant-frontend==20181017.0 +home-assistant-frontend==20181018.0 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 746b9a39d02..fa39c6aa8a5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -97,7 +97,7 @@ hdate==0.6.5 holidays==0.9.7 # homeassistant.components.frontend -home-assistant-frontend==20181017.0 +home-assistant-frontend==20181018.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From 7eb6e49df77bccc158b0845c812a2df4a4ca7b6c Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Fri, 19 Oct 2018 00:25:48 +1100 Subject: [PATCH 206/265] Fixing race condition in geo location platforms (#17581) * fixed race condition where obsolete entities kept listening for dispatcher signals * making tests python 3.5 compatible --- .../geo_location/geo_json_events.py | 8 +- .../nsw_rural_fire_service_feed.py | 8 +- .../geo_location/test_geo_json_events.py | 88 +++++++++++++++++++ 3 files changed, 100 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/geo_location/geo_json_events.py b/homeassistant/components/geo_location/geo_json_events.py index 7c3f228a4c9..00ac85e6b27 100644 --- a/homeassistant/components/geo_location/geo_json_events.py +++ b/homeassistant/components/geo_location/geo_json_events.py @@ -152,19 +152,23 @@ class GeoJsonLocationEvent(GeoLocationEvent): self._distance = None self._latitude = None self._longitude = None + self._remove_signal_delete = None + self._remove_signal_update = None async def async_added_to_hass(self): """Call when entity is added to hass.""" - async_dispatcher_connect( + self._remove_signal_delete = async_dispatcher_connect( self.hass, SIGNAL_DELETE_ENTITY.format(self._external_id), self._delete_callback) - async_dispatcher_connect( + self._remove_signal_update = async_dispatcher_connect( self.hass, SIGNAL_UPDATE_ENTITY.format(self._external_id), self._update_callback) @callback def _delete_callback(self): """Remove this entity.""" + self._remove_signal_delete() + self._remove_signal_update() self.hass.async_create_task(self.async_remove()) @callback diff --git a/homeassistant/components/geo_location/nsw_rural_fire_service_feed.py b/homeassistant/components/geo_location/nsw_rural_fire_service_feed.py index d3b13abe704..79e0445f494 100644 --- a/homeassistant/components/geo_location/nsw_rural_fire_service_feed.py +++ b/homeassistant/components/geo_location/nsw_rural_fire_service_feed.py @@ -183,19 +183,23 @@ class NswRuralFireServiceLocationEvent(GeoLocationEvent): self._fire = None self._size = None self._responsible_agency = None + self._remove_signal_delete = None + self._remove_signal_update = None async def async_added_to_hass(self): """Call when entity is added to hass.""" - async_dispatcher_connect( + self._remove_signal_delete = async_dispatcher_connect( self.hass, SIGNAL_DELETE_ENTITY.format(self._external_id), self._delete_callback) - async_dispatcher_connect( + self._remove_signal_update = async_dispatcher_connect( self.hass, SIGNAL_UPDATE_ENTITY.format(self._external_id), self._update_callback) @callback def _delete_callback(self): """Remove this entity.""" + self._remove_signal_delete() + self._remove_signal_update() self.hass.async_create_task(self.async_remove()) @callback diff --git a/tests/components/geo_location/test_geo_json_events.py b/tests/components/geo_location/test_geo_json_events.py index dbaf71a6509..00fc9f8c996 100644 --- a/tests/components/geo_location/test_geo_json_events.py +++ b/tests/components/geo_location/test_geo_json_events.py @@ -3,6 +3,7 @@ import unittest from unittest import mock from unittest.mock import patch, MagicMock +import homeassistant from homeassistant.components import geo_location from homeassistant.components.geo_location import ATTR_SOURCE from homeassistant.components.geo_location.geo_json_events import \ @@ -138,3 +139,90 @@ class TestGeoJsonPlatform(unittest.TestCase): all_states = self.hass.states.all() assert len(all_states) == 0 + + @mock.patch('geojson_client.generic_feed.GenericFeed') + def test_setup_race_condition(self, mock_feed): + """Test a particular race condition experienced.""" + # 1. Feed returns 1 entry -> Feed manager creates 1 entity. + # 2. Feed returns error -> Feed manager removes 1 entity. + # However, this stayed on and kept listening for dispatcher signals. + # 3. Feed returns 1 entry -> Feed manager creates 1 entity. + # 4. Feed returns 1 entry -> Feed manager updates 1 entity. + # Internally, the previous entity is updating itself, too. + # 5. Feed returns error -> Feed manager removes 1 entity. + # There are now 2 entities trying to remove themselves from HA, but + # the second attempt fails of course. + + # Set up some mock feed entries for this test. + mock_entry_1 = self._generate_mock_feed_entry('1234', 'Title 1', 15.5, + (-31.0, 150.0)) + mock_feed.return_value.update.return_value = 'OK', [mock_entry_1] + + utcnow = dt_util.utcnow() + # Patching 'utcnow' to gain more control over the timed update. + with patch('homeassistant.util.dt.utcnow', return_value=utcnow): + with assert_setup_component(1, geo_location.DOMAIN): + self.assertTrue(setup_component(self.hass, geo_location.DOMAIN, + CONFIG)) + + # This gives us the ability to assert the '_delete_callback' + # has been called while still executing it. + original_delete_callback = homeassistant.components\ + .geo_location.geo_json_events.GeoJsonLocationEvent\ + ._delete_callback + + def mock_delete_callback(entity): + original_delete_callback(entity) + + with patch('homeassistant.components.geo_location' + '.geo_json_events.GeoJsonLocationEvent' + '._delete_callback', + side_effect=mock_delete_callback, + autospec=True) as mocked_delete_callback: + + # Artificially trigger update. + self.hass.bus.fire(EVENT_HOMEASSISTANT_START) + # Collect events. + self.hass.block_till_done() + + all_states = self.hass.states.all() + assert len(all_states) == 1 + + # Simulate an update - empty data, removes all entities + mock_feed.return_value.update.return_value = 'ERROR', None + fire_time_changed(self.hass, utcnow + SCAN_INTERVAL) + self.hass.block_till_done() + + assert mocked_delete_callback.call_count == 1 + all_states = self.hass.states.all() + assert len(all_states) == 0 + + # Simulate an update - 1 entry + mock_feed.return_value.update.return_value = 'OK', [ + mock_entry_1] + fire_time_changed(self.hass, utcnow + 2 * SCAN_INTERVAL) + self.hass.block_till_done() + + all_states = self.hass.states.all() + assert len(all_states) == 1 + + # Simulate an update - 1 entry + mock_feed.return_value.update.return_value = 'OK', [ + mock_entry_1] + fire_time_changed(self.hass, utcnow + 3 * SCAN_INTERVAL) + self.hass.block_till_done() + + all_states = self.hass.states.all() + assert len(all_states) == 1 + + # Reset mocked method for the next test. + mocked_delete_callback.reset_mock() + + # Simulate an update - empty data, removes all entities + mock_feed.return_value.update.return_value = 'ERROR', None + fire_time_changed(self.hass, utcnow + 4 * SCAN_INTERVAL) + self.hass.block_till_done() + + assert mocked_delete_callback.call_count == 1 + all_states = self.hass.states.all() + assert len(all_states) == 0 From 6239a523ccf9308a348e0ec96979278bdc5d4ce1 Mon Sep 17 00:00:00 2001 From: Nikolay Vasilchuk Date: Thu, 18 Oct 2018 21:28:43 +0300 Subject: [PATCH 207/265] Fix: Xiaomi Plug state is set twice (#17482) * Xiaomi Plug UI fix #17422 * Review --- homeassistant/components/switch/xiaomi_aqara.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/switch/xiaomi_aqara.py b/homeassistant/components/switch/xiaomi_aqara.py index 2d2ba244ba0..d5502c0b6fa 100644 --- a/homeassistant/components/switch/xiaomi_aqara.py +++ b/homeassistant/components/switch/xiaomi_aqara.py @@ -137,7 +137,7 @@ class XiaomiGenericSwitch(XiaomiDevice, SwitchDevice): self._load_power = round(float(data[LOAD_POWER]), 2) value = data.get(self._data_key) - if value is None: + if value not in ['on', 'off']: return False state = value == 'on' From 222ba96b3e3d99735051f783898c15bed2cb5e9f Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Thu, 18 Oct 2018 15:30:46 -0400 Subject: [PATCH 208/265] Bump blinkpy version to 0.10.1 (#17595) --- homeassistant/components/blink/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py index abdbc1a2e92..66cfe3990a3 100644 --- a/homeassistant/components/blink/__init__.py +++ b/homeassistant/components/blink/__init__.py @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_BINARY_SENSORS, CONF_SENSORS, CONF_FILENAME, CONF_MONITORED_CONDITIONS, TEMP_FAHRENHEIT) -REQUIREMENTS = ['blinkpy==0.10.0'] +REQUIREMENTS = ['blinkpy==0.10.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index c6889a00016..59e62006795 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -185,7 +185,7 @@ bellows==0.7.0 bimmer_connected==0.5.3 # homeassistant.components.blink -blinkpy==0.10.0 +blinkpy==0.10.1 # homeassistant.components.light.blinksticklight blinkstick==1.1.8 From cf3a97ff3c452c4b5f73cfa46b04a4d682577ecd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 18 Oct 2018 22:31:52 +0300 Subject: [PATCH 209/265] Upgrade pytest to 3.9.1 (#17598) --- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index 9f36d8f42ca..492708cd904 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,5 +13,5 @@ pytest-aiohttp==0.3.0 pytest-cov==2.5.1 pytest-sugar==0.9.1 pytest-timeout==1.3.2 -pytest==3.8.2 +pytest==3.9.1 requests_mock==1.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fa39c6aa8a5..b50381560e6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -14,7 +14,7 @@ pytest-aiohttp==0.3.0 pytest-cov==2.5.1 pytest-sugar==0.9.1 pytest-timeout==1.3.2 -pytest==3.8.2 +pytest==3.9.1 requests_mock==1.5.2 From 10c13781954b86e04eea9dc63d7a9fa4a97c5d72 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Thu, 18 Oct 2018 22:28:40 +0200 Subject: [PATCH 210/265] Add binary_sensor support to RFlink (#17146) * Add binary_sensor support to RFlink * Add support for aliases * Fix review comments * Refactor, add tests * Review comments * Review comments * Review comments * Review comments --- .../components/binary_sensor/rflink.py | 105 +++++++++++ homeassistant/components/rflink.py | 1 - homeassistant/components/sensor/rflink.py | 2 +- tests/components/binary_sensor/test_rflink.py | 178 ++++++++++++++++++ 4 files changed, 284 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/binary_sensor/rflink.py create mode 100644 tests/components/binary_sensor/test_rflink.py diff --git a/homeassistant/components/binary_sensor/rflink.py b/homeassistant/components/binary_sensor/rflink.py new file mode 100644 index 00000000000..73b912d62da --- /dev/null +++ b/homeassistant/components/binary_sensor/rflink.py @@ -0,0 +1,105 @@ +""" +Support for Rflink binary sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.rflink/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice) +from homeassistant.components.rflink import ( + CONF_ALIASES, CONF_DEVICES, RflinkDevice) +from homeassistant.const import ( + CONF_FORCE_UPDATE, CONF_NAME, CONF_DEVICE_CLASS) +import homeassistant.helpers.config_validation as cv +import homeassistant.helpers.event as evt + +CONF_OFF_DELAY = 'off_delay' +DEFAULT_FORCE_UPDATE = False + +DEPENDENCIES = ['rflink'] + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_DEVICES, default={}): { + cv.string: vol.Schema({ + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): + cv.boolean, + vol.Optional(CONF_OFF_DELAY): cv.positive_int, + vol.Optional(CONF_ALIASES, default=[]): + vol.All(cv.ensure_list, [cv.string]), + }) + }, +}, extra=vol.ALLOW_EXTRA) + + +def devices_from_config(domain_config): + """Parse configuration and add Rflink sensor devices.""" + devices = [] + for device_id, config in domain_config[CONF_DEVICES].items(): + device = RflinkBinarySensor(device_id, **config) + devices.append(device) + + return devices + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the Rflink platform.""" + async_add_entities(devices_from_config(config)) + + +class RflinkBinarySensor(RflinkDevice, BinarySensorDevice): + """Representation of an Rflink binary sensor.""" + + def __init__(self, device_id, device_class=None, + force_update=None, off_delay=None, + **kwargs): + """Handle sensor specific args and super init.""" + self._state = None + self._device_class = device_class + self._force_update = force_update + self._off_delay = off_delay + self._delay_listener = None + super().__init__(device_id, **kwargs) + + def _handle_event(self, event): + """Domain specific event handler.""" + command = event['command'] + if command == 'on': + self._state = True + elif command == 'off': + self._state = False + + if (self._state and self._off_delay is not None): + def off_delay_listener(now): + """Switch device off after a delay.""" + self._delay_listener = None + self._state = False + self.async_schedule_update_ha_state() + + if self._delay_listener is not None: + self._delay_listener() + self._delay_listener = evt.async_call_later( + self.hass, self._off_delay, off_delay_listener) + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self._state + + @property + def device_class(self): + """Return the class of this sensor.""" + return self._device_class + + @property + def force_update(self): + """Force update.""" + return self._force_update diff --git a/homeassistant/components/rflink.py b/homeassistant/components/rflink.py index b75a14968cd..3bb3bb7044b 100644 --- a/homeassistant/components/rflink.py +++ b/homeassistant/components/rflink.py @@ -120,7 +120,6 @@ async def async_setup(hass, config): } hass.data[DATA_ENTITY_GROUP_LOOKUP] = { EVENT_KEY_COMMAND: defaultdict(list), - EVENT_KEY_SENSOR: defaultdict(list), } # Allow platform to specify function to register new unknown devices diff --git a/homeassistant/components/sensor/rflink.py b/homeassistant/components/sensor/rflink.py index a401eebeec6..4065e0a439f 100644 --- a/homeassistant/components/sensor/rflink.py +++ b/homeassistant/components/sensor/rflink.py @@ -2,7 +2,7 @@ Support for Rflink sensors. For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.rflink/ +https://home-assistant.io/components/sensor.rflink/ """ import logging diff --git a/tests/components/binary_sensor/test_rflink.py b/tests/components/binary_sensor/test_rflink.py new file mode 100644 index 00000000000..94f4208d5b8 --- /dev/null +++ b/tests/components/binary_sensor/test_rflink.py @@ -0,0 +1,178 @@ +""" +Test for RFlink sensor components. + +Test setup of rflink sensor component/platform. Verify manual and +automatic sensor creation. +""" +from datetime import timedelta +from unittest.mock import patch + +from ..test_rflink import mock_rflink +from homeassistant.components.rflink import ( + CONF_RECONNECT_INTERVAL) + +import homeassistant.core as ha +from homeassistant.const import ( + EVENT_STATE_CHANGED, STATE_ON, STATE_OFF, STATE_UNAVAILABLE) +import homeassistant.util.dt as dt_util + +from tests.common import async_fire_time_changed + +DOMAIN = 'binary_sensor' + +CONFIG = { + 'rflink': { + 'port': '/dev/ttyABC0', + 'ignore_devices': ['ignore_wildcard_*', 'ignore_sensor'], + }, + DOMAIN: { + 'platform': 'rflink', + 'devices': { + 'test': { + 'name': 'test', + 'device_class': 'door', + }, + 'test2': { + 'name': 'test2', + 'device_class': 'motion', + 'off_delay': 30, + 'force_update': True, + }, + }, + }, +} + + +async def test_default_setup(hass, monkeypatch): + """Test all basic functionality of the rflink sensor component.""" + # setup mocking rflink module + event_callback, create, _, disconnect_callback = await mock_rflink( + hass, CONFIG, DOMAIN, monkeypatch) + + # make sure arguments are passed + assert create.call_args_list[0][1]['ignore'] + + # test default state of sensor loaded from config + config_sensor = hass.states.get('binary_sensor.test') + assert config_sensor + assert config_sensor.state == STATE_OFF + assert config_sensor.attributes['device_class'] == 'door' + + # test event for config sensor + event_callback({ + 'id': 'test', + 'command': 'on', + }) + await hass.async_block_till_done() + + assert hass.states.get('binary_sensor.test').state == STATE_ON + + # test event for config sensor + event_callback({ + 'id': 'test', + 'command': 'off', + }) + await hass.async_block_till_done() + + assert hass.states.get('binary_sensor.test').state == STATE_OFF + + +async def test_entity_availability(hass, monkeypatch): + """If Rflink device is disconnected, entities should become unavailable.""" + # Make sure Rflink mock does not 'recover' to quickly from the + # disconnect or else the unavailability cannot be measured + config = CONFIG + failures = [True, True] + config[CONF_RECONNECT_INTERVAL] = 60 + + # Create platform and entities + event_callback, create, _, disconnect_callback = await mock_rflink( + hass, config, DOMAIN, monkeypatch, failures=failures) + + # Entities are available by default + assert hass.states.get('binary_sensor.test').state == STATE_OFF + + # Mock a disconnect of the Rflink device + disconnect_callback() + + # Wait for dispatch events to propagate + await hass.async_block_till_done() + + # Entity should be unavailable + assert hass.states.get('binary_sensor.test').state == STATE_UNAVAILABLE + + # Reconnect the Rflink device + disconnect_callback() + + # Wait for dispatch events to propagate + await hass.async_block_till_done() + + # Entities should be available again + assert hass.states.get('binary_sensor.test').state == STATE_OFF + + +async def test_off_delay(hass, monkeypatch): + """Test off_delay option.""" + # setup mocking rflink module + event_callback, create, _, disconnect_callback = await mock_rflink( + hass, CONFIG, DOMAIN, monkeypatch) + + # make sure arguments are passed + assert create.call_args_list[0][1]['ignore'] + + events = [] + + on_event = { + 'id': 'test2', + 'command': 'on', + } + + @ha.callback + def callback(event): + """Verify event got called.""" + events.append(event) + + hass.bus.async_listen(EVENT_STATE_CHANGED, callback) + + now = dt_util.utcnow() + # fake time and turn on sensor + future = now + timedelta(seconds=0) + with patch(('homeassistant.helpers.event.' + 'dt_util.utcnow'), return_value=future): + async_fire_time_changed(hass, future) + event_callback(on_event) + await hass.async_block_till_done() + state = hass.states.get('binary_sensor.test2') + assert state.state == STATE_ON + assert len(events) == 1 + + # fake time and turn on sensor again + future = now + timedelta(seconds=15) + with patch(('homeassistant.helpers.event.' + 'dt_util.utcnow'), return_value=future): + async_fire_time_changed(hass, future) + event_callback(on_event) + await hass.async_block_till_done() + state = hass.states.get('binary_sensor.test2') + assert state.state == STATE_ON + assert len(events) == 2 + + # fake time and verify sensor still on (de-bounce) + future = now + timedelta(seconds=35) + with patch(('homeassistant.helpers.event.' + 'dt_util.utcnow'), return_value=future): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + state = hass.states.get('binary_sensor.test2') + assert state.state == STATE_ON + assert len(events) == 2 + + # fake time and verify sensor is off + future = now + timedelta(seconds=45) + with patch(('homeassistant.helpers.event.' + 'dt_util.utcnow'), return_value=future): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + state = hass.states.get('binary_sensor.test2') + assert state.state == STATE_OFF + assert len(events) == 3 From cc4d29d42a6e536847126168e6999df3d46f3d96 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Thu, 18 Oct 2018 23:04:22 +0200 Subject: [PATCH 211/265] Fix flux switch update interval (#17458) --- homeassistant/components/switch/flux.py | 8 +- tests/components/switch/test_flux.py | 428 ++++++++++++------------ 2 files changed, 225 insertions(+), 211 deletions(-) diff --git a/homeassistant/components/switch/flux.py b/homeassistant/components/switch/flux.py index 05e0497155a..00388822be1 100644 --- a/homeassistant/components/switch/flux.py +++ b/homeassistant/components/switch/flux.py @@ -19,7 +19,7 @@ from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.const import ( ATTR_ENTITY_ID, CONF_NAME, CONF_PLATFORM, CONF_LIGHTS, CONF_MODE, SERVICE_TURN_ON) -from homeassistant.helpers.event import track_time_change +from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.sun import get_astral_event_date from homeassistant.util import slugify from homeassistant.util.color import ( @@ -180,8 +180,10 @@ class FluxSwitch(SwitchDevice): # Make initial update self.flux_update() - self.unsub_tracker = track_time_change( - self.hass, self.flux_update, second=[0, self._interval]) + self.unsub_tracker = track_time_interval( + self.hass, + self.flux_update, + datetime.timedelta(seconds=self._interval)) self.schedule_update_ha_state() diff --git a/tests/components/switch/test_flux.py b/tests/components/switch/test_flux.py index 69e65e24659..cb5207adb3e 100644 --- a/tests/components/switch/test_flux.py +++ b/tests/components/switch/test_flux.py @@ -183,22 +183,23 @@ class TestSwitchFlux(unittest.TestCase): return sunrise_time return sunset_time - with patch('homeassistant.util.dt.now', return_value=test_time): - with patch('homeassistant.helpers.sun.get_astral_event_date', - side_effect=event_date): - assert setup_component(self.hass, switch.DOMAIN, { - switch.DOMAIN: { - 'platform': 'flux', - 'name': 'flux', - 'lights': [dev1.entity_id] - } - }) - turn_on_calls = mock_service( - self.hass, light.DOMAIN, SERVICE_TURN_ON) - common.turn_on(self.hass, 'switch.flux') - self.hass.block_till_done() - fire_time_changed(self.hass, test_time) - self.hass.block_till_done() + with patch('homeassistant.components.switch.flux.dt_now', + return_value=test_time), \ + patch('homeassistant.helpers.sun.get_astral_event_date', + side_effect=event_date): + assert setup_component(self.hass, switch.DOMAIN, { + switch.DOMAIN: { + 'platform': 'flux', + 'name': 'flux', + 'lights': [dev1.entity_id] + } + }) + turn_on_calls = mock_service( + self.hass, light.DOMAIN, SERVICE_TURN_ON) + common.turn_on(self.hass, 'switch.flux') + self.hass.block_till_done() + fire_time_changed(self.hass, test_time) + self.hass.block_till_done() call = turn_on_calls[-1] self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 173) self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.439, 0.37]) @@ -229,23 +230,24 @@ class TestSwitchFlux(unittest.TestCase): return sunrise_time return sunset_time - with patch('homeassistant.util.dt.now', return_value=test_time): - with patch('homeassistant.helpers.sun.get_astral_event_date', - side_effect=event_date): - assert setup_component(self.hass, switch.DOMAIN, { - switch.DOMAIN: { - 'platform': 'flux', - 'name': 'flux', - 'lights': [dev1.entity_id], - 'stop_time': '22:00' - } - }) - turn_on_calls = mock_service( - self.hass, light.DOMAIN, SERVICE_TURN_ON) - common.turn_on(self.hass, 'switch.flux') - self.hass.block_till_done() - fire_time_changed(self.hass, test_time) - self.hass.block_till_done() + with patch('homeassistant.components.switch.flux.dt_now', + return_value=test_time), \ + patch('homeassistant.helpers.sun.get_astral_event_date', + side_effect=event_date): + assert setup_component(self.hass, switch.DOMAIN, { + switch.DOMAIN: { + 'platform': 'flux', + 'name': 'flux', + 'lights': [dev1.entity_id], + 'stop_time': '22:00' + } + }) + turn_on_calls = mock_service( + self.hass, light.DOMAIN, SERVICE_TURN_ON) + common.turn_on(self.hass, 'switch.flux') + self.hass.block_till_done() + fire_time_changed(self.hass, test_time) + self.hass.block_till_done() call = turn_on_calls[-1] self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 146) self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.506, 0.385]) @@ -322,24 +324,25 @@ class TestSwitchFlux(unittest.TestCase): return sunrise_time return sunset_time - with patch('homeassistant.util.dt.now', return_value=test_time): - with patch('homeassistant.helpers.sun.get_astral_event_date', - side_effect=event_date): - assert setup_component(self.hass, switch.DOMAIN, { - switch.DOMAIN: { - 'platform': 'flux', - 'name': 'flux', - 'lights': [dev1.entity_id], - 'start_time': '6:00', - 'stop_time': '23:30' - } - }) - turn_on_calls = mock_service( - self.hass, light.DOMAIN, SERVICE_TURN_ON) - common.turn_on(self.hass, 'switch.flux') - self.hass.block_till_done() - fire_time_changed(self.hass, test_time) - self.hass.block_till_done() + with patch('homeassistant.components.switch.flux.dt_now', + return_value=test_time), \ + patch('homeassistant.helpers.sun.get_astral_event_date', + side_effect=event_date): + assert setup_component(self.hass, switch.DOMAIN, { + switch.DOMAIN: { + 'platform': 'flux', + 'name': 'flux', + 'lights': [dev1.entity_id], + 'start_time': '6:00', + 'stop_time': '23:30' + } + }) + turn_on_calls = mock_service( + self.hass, light.DOMAIN, SERVICE_TURN_ON) + common.turn_on(self.hass, 'switch.flux') + self.hass.block_till_done() + fire_time_changed(self.hass, test_time) + self.hass.block_till_done() call = turn_on_calls[-1] self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 147) self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.504, 0.385]) @@ -372,23 +375,24 @@ class TestSwitchFlux(unittest.TestCase): return sunrise_time return sunset_time - with patch('homeassistant.util.dt.now', return_value=test_time): - with patch('homeassistant.helpers.sun.get_astral_event_date', - side_effect=event_date): - assert setup_component(self.hass, switch.DOMAIN, { - switch.DOMAIN: { - 'platform': 'flux', - 'name': 'flux', - 'lights': [dev1.entity_id], - 'stop_time': '01:00' - } - }) - turn_on_calls = mock_service( - self.hass, light.DOMAIN, SERVICE_TURN_ON) - common.turn_on(self.hass, 'switch.flux') - self.hass.block_till_done() - fire_time_changed(self.hass, test_time) - self.hass.block_till_done() + with patch('homeassistant.components.switch.flux.dt_now', + return_value=test_time), \ + patch('homeassistant.helpers.sun.get_astral_event_date', + side_effect=event_date): + assert setup_component(self.hass, switch.DOMAIN, { + switch.DOMAIN: { + 'platform': 'flux', + 'name': 'flux', + 'lights': [dev1.entity_id], + 'stop_time': '01:00' + } + }) + turn_on_calls = mock_service( + self.hass, light.DOMAIN, SERVICE_TURN_ON) + common.turn_on(self.hass, 'switch.flux') + self.hass.block_till_done() + fire_time_changed(self.hass, test_time) + self.hass.block_till_done() call = turn_on_calls[-1] self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 112) self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.606, 0.379]) @@ -423,23 +427,24 @@ class TestSwitchFlux(unittest.TestCase): return sunrise_time return sunset_time - with patch('homeassistant.util.dt.now', return_value=test_time): - with patch('homeassistant.helpers.sun.get_astral_event_date', - side_effect=event_date): - assert setup_component(self.hass, switch.DOMAIN, { - switch.DOMAIN: { - 'platform': 'flux', - 'name': 'flux', - 'lights': [dev1.entity_id], - 'stop_time': '01:00' - } - }) - turn_on_calls = mock_service( - self.hass, light.DOMAIN, SERVICE_TURN_ON) - common.turn_on(self.hass, 'switch.flux') - self.hass.block_till_done() - fire_time_changed(self.hass, test_time) - self.hass.block_till_done() + with patch('homeassistant.components.switch.flux.dt_now', + return_value=test_time), \ + patch('homeassistant.helpers.sun.get_astral_event_date', + side_effect=event_date): + assert setup_component(self.hass, switch.DOMAIN, { + switch.DOMAIN: { + 'platform': 'flux', + 'name': 'flux', + 'lights': [dev1.entity_id], + 'stop_time': '01:00' + } + }) + turn_on_calls = mock_service( + self.hass, light.DOMAIN, SERVICE_TURN_ON) + common.turn_on(self.hass, 'switch.flux') + self.hass.block_till_done() + fire_time_changed(self.hass, test_time) + self.hass.block_till_done() call = turn_on_calls[-1] self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 173) self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.439, 0.37]) @@ -523,23 +528,24 @@ class TestSwitchFlux(unittest.TestCase): return sunrise_time return sunset_time - with patch('homeassistant.util.dt.now', return_value=test_time): - with patch('homeassistant.helpers.sun.get_astral_event_date', - side_effect=event_date): - assert setup_component(self.hass, switch.DOMAIN, { - switch.DOMAIN: { - 'platform': 'flux', - 'name': 'flux', - 'lights': [dev1.entity_id], - 'stop_time': '01:00' - } - }) - turn_on_calls = mock_service( - self.hass, light.DOMAIN, SERVICE_TURN_ON) - common.turn_on(self.hass, 'switch.flux') - self.hass.block_till_done() - fire_time_changed(self.hass, test_time) - self.hass.block_till_done() + with patch('homeassistant.components.switch.flux.dt_now', + return_value=test_time), \ + patch('homeassistant.helpers.sun.get_astral_event_date', + side_effect=event_date): + assert setup_component(self.hass, switch.DOMAIN, { + switch.DOMAIN: { + 'platform': 'flux', + 'name': 'flux', + 'lights': [dev1.entity_id], + 'stop_time': '01:00' + } + }) + turn_on_calls = mock_service( + self.hass, light.DOMAIN, SERVICE_TURN_ON) + common.turn_on(self.hass, 'switch.flux') + self.hass.block_till_done() + fire_time_changed(self.hass, test_time) + self.hass.block_till_done() call = turn_on_calls[-1] self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 114) self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.601, 0.382]) @@ -573,23 +579,24 @@ class TestSwitchFlux(unittest.TestCase): return sunrise_time return sunset_time - with patch('homeassistant.util.dt.now', return_value=test_time): - with patch('homeassistant.helpers.sun.get_astral_event_date', - side_effect=event_date): - assert setup_component(self.hass, switch.DOMAIN, { - switch.DOMAIN: { - 'platform': 'flux', - 'name': 'flux', - 'lights': [dev1.entity_id], - 'stop_time': '01:00' - } - }) - turn_on_calls = mock_service( - self.hass, light.DOMAIN, SERVICE_TURN_ON) - common.turn_on(self.hass, 'switch.flux') - self.hass.block_till_done() - fire_time_changed(self.hass, test_time) - self.hass.block_till_done() + with patch('homeassistant.components.switch.flux.dt_now', + return_value=test_time), \ + patch('homeassistant.helpers.sun.get_astral_event_date', + side_effect=event_date): + assert setup_component(self.hass, switch.DOMAIN, { + switch.DOMAIN: { + 'platform': 'flux', + 'name': 'flux', + 'lights': [dev1.entity_id], + 'stop_time': '01:00' + } + }) + turn_on_calls = mock_service( + self.hass, light.DOMAIN, SERVICE_TURN_ON) + common.turn_on(self.hass, 'switch.flux') + self.hass.block_till_done() + fire_time_changed(self.hass, test_time) + self.hass.block_till_done() call = turn_on_calls[-1] self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 112) self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.606, 0.379]) @@ -620,25 +627,26 @@ class TestSwitchFlux(unittest.TestCase): return sunrise_time return sunset_time - with patch('homeassistant.util.dt.now', return_value=test_time): - with patch('homeassistant.helpers.sun.get_astral_event_date', - side_effect=event_date): - assert setup_component(self.hass, switch.DOMAIN, { - switch.DOMAIN: { - 'platform': 'flux', - 'name': 'flux', - 'lights': [dev1.entity_id], - 'start_colortemp': '1000', - 'stop_colortemp': '6000', - 'stop_time': '22:00' - } - }) - turn_on_calls = mock_service( - self.hass, light.DOMAIN, SERVICE_TURN_ON) - common.turn_on(self.hass, 'switch.flux') - self.hass.block_till_done() - fire_time_changed(self.hass, test_time) - self.hass.block_till_done() + with patch('homeassistant.components.switch.flux.dt_now', + return_value=test_time), \ + patch('homeassistant.helpers.sun.get_astral_event_date', + side_effect=event_date): + assert setup_component(self.hass, switch.DOMAIN, { + switch.DOMAIN: { + 'platform': 'flux', + 'name': 'flux', + 'lights': [dev1.entity_id], + 'start_colortemp': '1000', + 'stop_colortemp': '6000', + 'stop_time': '22:00' + } + }) + turn_on_calls = mock_service( + self.hass, light.DOMAIN, SERVICE_TURN_ON) + common.turn_on(self.hass, 'switch.flux') + self.hass.block_till_done() + fire_time_changed(self.hass, test_time) + self.hass.block_till_done() call = turn_on_calls[-1] self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 159) self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.469, 0.378]) @@ -669,24 +677,25 @@ class TestSwitchFlux(unittest.TestCase): return sunrise_time return sunset_time - with patch('homeassistant.util.dt.now', return_value=test_time): - with patch('homeassistant.helpers.sun.get_astral_event_date', - side_effect=event_date): - assert setup_component(self.hass, switch.DOMAIN, { - switch.DOMAIN: { - 'platform': 'flux', - 'name': 'flux', - 'lights': [dev1.entity_id], - 'brightness': 255, - 'stop_time': '22:00' - } - }) - turn_on_calls = mock_service( - self.hass, light.DOMAIN, SERVICE_TURN_ON) - common.turn_on(self.hass, 'switch.flux') - self.hass.block_till_done() - fire_time_changed(self.hass, test_time) - self.hass.block_till_done() + with patch('homeassistant.components.switch.flux.dt_now', + return_value=test_time), \ + patch('homeassistant.helpers.sun.get_astral_event_date', + side_effect=event_date): + assert setup_component(self.hass, switch.DOMAIN, { + switch.DOMAIN: { + 'platform': 'flux', + 'name': 'flux', + 'lights': [dev1.entity_id], + 'brightness': 255, + 'stop_time': '22:00' + } + }) + turn_on_calls = mock_service( + self.hass, light.DOMAIN, SERVICE_TURN_ON) + common.turn_on(self.hass, 'switch.flux') + self.hass.block_till_done() + fire_time_changed(self.hass, test_time) + self.hass.block_till_done() call = turn_on_calls[-1] self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 255) self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.506, 0.385]) @@ -731,24 +740,25 @@ class TestSwitchFlux(unittest.TestCase): print('sunset {}'.format(sunset_time)) return sunset_time - with patch('homeassistant.util.dt.now', return_value=test_time): - with patch('homeassistant.helpers.sun.get_astral_event_date', - side_effect=event_date): - assert setup_component(self.hass, switch.DOMAIN, { - switch.DOMAIN: { - 'platform': 'flux', - 'name': 'flux', - 'lights': [dev1.entity_id, - dev2.entity_id, - dev3.entity_id] - } - }) - turn_on_calls = mock_service( - self.hass, light.DOMAIN, SERVICE_TURN_ON) - common.turn_on(self.hass, 'switch.flux') - self.hass.block_till_done() - fire_time_changed(self.hass, test_time) - self.hass.block_till_done() + with patch('homeassistant.components.switch.flux.dt_now', + return_value=test_time), \ + patch('homeassistant.helpers.sun.get_astral_event_date', + side_effect=event_date): + assert setup_component(self.hass, switch.DOMAIN, { + switch.DOMAIN: { + 'platform': 'flux', + 'name': 'flux', + 'lights': [dev1.entity_id, + dev2.entity_id, + dev3.entity_id] + } + }) + turn_on_calls = mock_service( + self.hass, light.DOMAIN, SERVICE_TURN_ON) + common.turn_on(self.hass, 'switch.flux') + self.hass.block_till_done() + fire_time_changed(self.hass, test_time) + self.hass.block_till_done() call = turn_on_calls[-1] self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 163) self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.46, 0.376]) @@ -783,23 +793,24 @@ class TestSwitchFlux(unittest.TestCase): return sunrise_time return sunset_time - with patch('homeassistant.util.dt.now', return_value=test_time): - with patch('homeassistant.helpers.sun.get_astral_event_date', - side_effect=event_date): - assert setup_component(self.hass, switch.DOMAIN, { - switch.DOMAIN: { - 'platform': 'flux', - 'name': 'flux', - 'lights': [dev1.entity_id], - 'mode': 'mired' - } - }) - turn_on_calls = mock_service( - self.hass, light.DOMAIN, SERVICE_TURN_ON) - common.turn_on(self.hass, 'switch.flux') - self.hass.block_till_done() - fire_time_changed(self.hass, test_time) - self.hass.block_till_done() + with patch('homeassistant.components.switch.flux.dt_now', + return_value=test_time), \ + patch('homeassistant.helpers.sun.get_astral_event_date', + side_effect=event_date): + assert setup_component(self.hass, switch.DOMAIN, { + switch.DOMAIN: { + 'platform': 'flux', + 'name': 'flux', + 'lights': [dev1.entity_id], + 'mode': 'mired' + } + }) + turn_on_calls = mock_service( + self.hass, light.DOMAIN, SERVICE_TURN_ON) + common.turn_on(self.hass, 'switch.flux') + self.hass.block_till_done() + fire_time_changed(self.hass, test_time) + self.hass.block_till_done() call = turn_on_calls[-1] self.assertEqual(call.data[light.ATTR_COLOR_TEMP], 269) @@ -827,23 +838,24 @@ class TestSwitchFlux(unittest.TestCase): return sunrise_time return sunset_time - with patch('homeassistant.util.dt.now', return_value=test_time): - with patch('homeassistant.helpers.sun.get_astral_event_date', - side_effect=event_date): - assert setup_component(self.hass, switch.DOMAIN, { - switch.DOMAIN: { - 'platform': 'flux', - 'name': 'flux', - 'lights': [dev1.entity_id], - 'mode': 'rgb' - } - }) - turn_on_calls = mock_service( - self.hass, light.DOMAIN, SERVICE_TURN_ON) - common.turn_on(self.hass, 'switch.flux') - self.hass.block_till_done() - fire_time_changed(self.hass, test_time) - self.hass.block_till_done() + with patch('homeassistant.components.switch.flux.dt_now', + return_value=test_time), \ + patch('homeassistant.helpers.sun.get_astral_event_date', + side_effect=event_date): + assert setup_component(self.hass, switch.DOMAIN, { + switch.DOMAIN: { + 'platform': 'flux', + 'name': 'flux', + 'lights': [dev1.entity_id], + 'mode': 'rgb' + } + }) + turn_on_calls = mock_service( + self.hass, light.DOMAIN, SERVICE_TURN_ON) + common.turn_on(self.hass, 'switch.flux') + self.hass.block_till_done() + fire_time_changed(self.hass, test_time) + self.hass.block_till_done() call = turn_on_calls[-1] rgb = (255, 198, 152) rounded_call = tuple(map(round, call.data[light.ATTR_RGB_COLOR])) From 7baffed7b7e09e3126984ab282e0ecdcd6ec225d Mon Sep 17 00:00:00 2001 From: mvn23 Date: Fri, 19 Oct 2018 07:31:19 +0200 Subject: [PATCH 212/265] Add sensor support to opentherm_gw (#17314) * Add OpenTherm Gateway sensor platform. * Add OTGW_ variables to list of supported sensors. * Order imports. * Add OpenTherm Gateway binary sensor support. * Revert "Add OpenTherm Gateway binary sensor support." This reverts commit 115acaa912f4f9ff47408a04f4a770bebd071f26. * Import COMP_SENSOR from sensor component rather than defining it. * Update opentherm_gw sensor platform docs url. * Update dependency to v0.2b1 Old version had incorrect variable names for some of the sensors * Update requirements_all.txt * Address review findings. * Update .coveragerc --- .coveragerc | 2 +- homeassistant/components/opentherm_gw.py | 94 +++++++- .../components/sensor/opentherm_gw.py | 211 ++++++++++++++++++ requirements_all.txt | 2 +- 4 files changed, 304 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/sensor/opentherm_gw.py diff --git a/.coveragerc b/.coveragerc index 42939e71704..04299609bbb 100644 --- a/.coveragerc +++ b/.coveragerc @@ -249,7 +249,7 @@ omit = homeassistant/components/*/opencv.py homeassistant/components/opentherm_gw.py - homeassistant/components/climate/opentherm_gw.py + homeassistant/components/*/opentherm_gw.py homeassistant/components/openuv/__init__.py homeassistant/components/*/openuv.py diff --git a/homeassistant/components/opentherm_gw.py b/homeassistant/components/opentherm_gw.py index 7bc2bbeaa8a..9379e2c2b31 100644 --- a/homeassistant/components/opentherm_gw.py +++ b/homeassistant/components/opentherm_gw.py @@ -8,8 +8,10 @@ import logging import voluptuous as vol -from homeassistant.const import (CONF_DEVICE, CONF_NAME, PRECISION_HALVES, - PRECISION_TENTHS, PRECISION_WHOLE) +from homeassistant.components.sensor import DOMAIN as COMP_SENSOR +from homeassistant.const import (CONF_DEVICE, CONF_MONITORED_VARIABLES, + CONF_NAME, PRECISION_HALVES, PRECISION_TENTHS, + PRECISION_WHOLE) from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -38,10 +40,12 @@ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_DEVICE): cv.string, vol.Optional(CONF_CLIMATE, default={}): CLIMATE_SCHEMA, + vol.Optional(CONF_MONITORED_VARIABLES, default=[]): vol.All( + cv.ensure_list, [cv.string]), }), }, extra=vol.ALLOW_EXTRA) -REQUIREMENTS = ['pyotgw==0.1b0'] +REQUIREMENTS = ['pyotgw==0.2b1'] _LOGGER = logging.getLogger(__name__) @@ -59,6 +63,8 @@ async def async_setup(hass, config): hass, conf[CONF_DEVICE], gateway)) hass.async_create_task(async_load_platform( hass, 'climate', DOMAIN, conf.get(CONF_CLIMATE))) + hass.async_create_task(setup_monitored_vars( + hass, conf.get(CONF_MONITORED_VARIABLES))) return True @@ -72,3 +78,85 @@ async def connect_and_subscribe(hass, device_path, gateway): _LOGGER.debug("Received report: %s", status) async_dispatcher_send(hass, SIGNAL_OPENTHERM_GW_UPDATE, status) gateway.subscribe(handle_report) + + +async def setup_monitored_vars(hass, monitored_vars): + """Set up requested sensors.""" + gw_vars = hass.data[DATA_OPENTHERM_GW][DATA_GW_VARS] + # Use dict to prepare for binary sensor support. + sensor_type_map = { + COMP_SENSOR: [ + gw_vars.DATA_CONTROL_SETPOINT, + gw_vars.DATA_MASTER_MEMBERID, + gw_vars.DATA_SLAVE_MEMBERID, + gw_vars.DATA_SLAVE_OEM_FAULT, + gw_vars.DATA_COOLING_CONTROL, + gw_vars.DATA_CONTROL_SETPOINT_2, + gw_vars.DATA_ROOM_SETPOINT_OVRD, + gw_vars.DATA_SLAVE_MAX_RELATIVE_MOD, + gw_vars.DATA_SLAVE_MAX_CAPACITY, + gw_vars.DATA_SLAVE_MIN_MOD_LEVEL, + gw_vars.DATA_ROOM_SETPOINT, + gw_vars.DATA_REL_MOD_LEVEL, + gw_vars.DATA_CH_WATER_PRESS, + gw_vars.DATA_DHW_FLOW_RATE, + gw_vars.DATA_ROOM_SETPOINT_2, + gw_vars.DATA_ROOM_TEMP, + gw_vars.DATA_CH_WATER_TEMP, + gw_vars.DATA_DHW_TEMP, + gw_vars.DATA_OUTSIDE_TEMP, + gw_vars.DATA_RETURN_WATER_TEMP, + gw_vars.DATA_SOLAR_STORAGE_TEMP, + gw_vars.DATA_SOLAR_COLL_TEMP, + gw_vars.DATA_CH_WATER_TEMP_2, + gw_vars.DATA_DHW_TEMP_2, + gw_vars.DATA_EXHAUST_TEMP, + gw_vars.DATA_SLAVE_DHW_MAX_SETP, + gw_vars.DATA_SLAVE_DHW_MIN_SETP, + gw_vars.DATA_SLAVE_CH_MAX_SETP, + gw_vars.DATA_SLAVE_CH_MIN_SETP, + gw_vars.DATA_DHW_SETPOINT, + gw_vars.DATA_MAX_CH_SETPOINT, + gw_vars.DATA_OEM_DIAG, + gw_vars.DATA_TOTAL_BURNER_STARTS, + gw_vars.DATA_CH_PUMP_STARTS, + gw_vars.DATA_DHW_PUMP_STARTS, + gw_vars.DATA_DHW_BURNER_STARTS, + gw_vars.DATA_TOTAL_BURNER_HOURS, + gw_vars.DATA_CH_PUMP_HOURS, + gw_vars.DATA_DHW_PUMP_HOURS, + gw_vars.DATA_DHW_BURNER_HOURS, + gw_vars.DATA_MASTER_OT_VERSION, + gw_vars.DATA_SLAVE_OT_VERSION, + gw_vars.DATA_MASTER_PRODUCT_TYPE, + gw_vars.DATA_MASTER_PRODUCT_VERSION, + gw_vars.DATA_SLAVE_PRODUCT_TYPE, + gw_vars.DATA_SLAVE_PRODUCT_VERSION, + gw_vars.OTGW_MODE, + gw_vars.OTGW_DHW_OVRD, + gw_vars.OTGW_ABOUT, + gw_vars.OTGW_BUILD, + gw_vars.OTGW_CLOCKMHZ, + gw_vars.OTGW_LED_A, + gw_vars.OTGW_LED_B, + gw_vars.OTGW_LED_C, + gw_vars.OTGW_LED_D, + gw_vars.OTGW_LED_E, + gw_vars.OTGW_LED_F, + gw_vars.OTGW_GPIO_A, + gw_vars.OTGW_GPIO_B, + gw_vars.OTGW_SB_TEMP, + gw_vars.OTGW_SETP_OVRD_MODE, + gw_vars.OTGW_SMART_PWR, + gw_vars.OTGW_THRM_DETECT, + gw_vars.OTGW_VREF, + ] + } + sensors = [] + for var in monitored_vars: + if var in sensor_type_map[COMP_SENSOR]: + sensors.append(var) + else: + _LOGGER.error("Monitored variable not supported: %s", var) + if sensors: + await async_load_platform(hass, COMP_SENSOR, DOMAIN, sensors) diff --git a/homeassistant/components/sensor/opentherm_gw.py b/homeassistant/components/sensor/opentherm_gw.py new file mode 100644 index 00000000000..9ae557654ce --- /dev/null +++ b/homeassistant/components/sensor/opentherm_gw.py @@ -0,0 +1,211 @@ +""" +Support for OpenTherm Gateway sensors. + +For more details about this platform, please refer to the documentation at +http://home-assistant.io/components/sensor.opentherm_gw/ +""" +import logging + +from homeassistant.components.opentherm_gw import ( + DATA_GW_VARS, DATA_OPENTHERM_GW, SIGNAL_OPENTHERM_GW_UPDATE) +from homeassistant.components.sensor import ENTITY_ID_FORMAT +from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity, async_generate_entity_id + +UNIT_BAR = 'bar' +UNIT_HOUR = 'h' +UNIT_KW = 'kW' +UNIT_L_MIN = 'L/min' +UNIT_PERCENT = '%' + +DEPENDENCIES = ['opentherm_gw'] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the OpenTherm Gateway sensors.""" + if discovery_info is None: + return + gw_vars = hass.data[DATA_OPENTHERM_GW][DATA_GW_VARS] + sensor_info = { + # [device_class, unit, friendly_name] + gw_vars.DATA_CONTROL_SETPOINT: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Control Setpoint"], + gw_vars.DATA_MASTER_MEMBERID: [None, None, "Thermostat Member ID"], + gw_vars.DATA_SLAVE_MEMBERID: [None, None, "Boiler Member ID"], + gw_vars.DATA_SLAVE_OEM_FAULT: [None, None, "Boiler OEM Fault Code"], + gw_vars.DATA_COOLING_CONTROL: [ + None, UNIT_PERCENT, "Cooling Control Signal"], + gw_vars.DATA_CONTROL_SETPOINT_2: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Control Setpoint 2"], + gw_vars.DATA_ROOM_SETPOINT_OVRD: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Room Setpoint Override"], + gw_vars.DATA_SLAVE_MAX_RELATIVE_MOD: [ + None, UNIT_PERCENT, "Boiler Maximum Relative Modulation"], + gw_vars.DATA_SLAVE_MAX_CAPACITY: [ + None, UNIT_KW, "Boiler Maximum Capacity"], + gw_vars.DATA_SLAVE_MIN_MOD_LEVEL: [ + None, UNIT_PERCENT, "Boiler Minimum Modulation Level"], + gw_vars.DATA_ROOM_SETPOINT: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Room Setpoint"], + gw_vars.DATA_REL_MOD_LEVEL: [ + None, UNIT_PERCENT, "Relative Modulation Level"], + gw_vars.DATA_CH_WATER_PRESS: [ + None, UNIT_BAR, "Central Heating Water Pressure"], + gw_vars.DATA_DHW_FLOW_RATE: [None, UNIT_L_MIN, "Hot Water Flow Rate"], + gw_vars.DATA_ROOM_SETPOINT_2: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Room Setpoint 2"], + gw_vars.DATA_ROOM_TEMP: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Room Temperature"], + gw_vars.DATA_CH_WATER_TEMP: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, + "Central Heating Water Temperature"], + gw_vars.DATA_DHW_TEMP: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Hot Water Temperature"], + gw_vars.DATA_OUTSIDE_TEMP: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Outside Temperature"], + gw_vars.DATA_RETURN_WATER_TEMP: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, + "Return Water Temperature"], + gw_vars.DATA_SOLAR_STORAGE_TEMP: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, + "Solar Storage Temperature"], + gw_vars.DATA_SOLAR_COLL_TEMP: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, + "Solar Collector Temperature"], + gw_vars.DATA_CH_WATER_TEMP_2: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, + "Central Heating 2 Water Temperature"], + gw_vars.DATA_DHW_TEMP_2: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Hot Water 2 Temperature"], + gw_vars.DATA_EXHAUST_TEMP: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Exhaust Temperature"], + gw_vars.DATA_SLAVE_DHW_MAX_SETP: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, + "Hot Water Maximum Setpoint"], + gw_vars.DATA_SLAVE_DHW_MIN_SETP: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, + "Hot Water Minimum Setpoint"], + gw_vars.DATA_SLAVE_CH_MAX_SETP: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, + "Boiler Maximum Central Heating Setpoint"], + gw_vars.DATA_SLAVE_CH_MIN_SETP: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, + "Boiler Minimum Central Heating Setpoint"], + gw_vars.DATA_DHW_SETPOINT: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Hot Water Setpoint"], + gw_vars.DATA_MAX_CH_SETPOINT: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, + "Maximum Central Heating Setpoint"], + gw_vars.DATA_OEM_DIAG: [None, None, "OEM Diagnostic Code"], + gw_vars.DATA_TOTAL_BURNER_STARTS: [ + None, None, "Total Burner Starts"], + gw_vars.DATA_CH_PUMP_STARTS: [ + None, None, "Central Heating Pump Starts"], + gw_vars.DATA_DHW_PUMP_STARTS: [None, None, "Hot Water Pump Starts"], + gw_vars.DATA_DHW_BURNER_STARTS: [ + None, None, "Hot Water Burner Starts"], + gw_vars.DATA_TOTAL_BURNER_HOURS: [ + None, UNIT_HOUR, "Total Burner Hours"], + gw_vars.DATA_CH_PUMP_HOURS: [ + None, UNIT_HOUR, "Central Heating Pump Hours"], + gw_vars.DATA_DHW_PUMP_HOURS: [None, UNIT_HOUR, "Hot Water Pump Hours"], + gw_vars.DATA_DHW_BURNER_HOURS: [ + None, UNIT_HOUR, "Hot Water Burner Hours"], + gw_vars.DATA_MASTER_OT_VERSION: [ + None, None, "Thermostat OpenTherm Version"], + gw_vars.DATA_SLAVE_OT_VERSION: [ + None, None, "Boiler OpenTherm Version"], + gw_vars.DATA_MASTER_PRODUCT_TYPE: [ + None, None, "Thermostat Product Type"], + gw_vars.DATA_MASTER_PRODUCT_VERSION: [ + None, None, "Thermostat Product Version"], + gw_vars.DATA_SLAVE_PRODUCT_TYPE: [None, None, "Boiler Product Type"], + gw_vars.DATA_SLAVE_PRODUCT_VERSION: [ + None, None, "Boiler Product Version"], + gw_vars.OTGW_MODE: [None, None, "Gateway/Monitor Mode"], + gw_vars.OTGW_DHW_OVRD: [None, None, "Gateway Hot Water Override Mode"], + gw_vars.OTGW_ABOUT: [None, None, "Gateway Firmware Version"], + gw_vars.OTGW_BUILD: [None, None, "Gateway Firmware Build"], + gw_vars.OTGW_CLOCKMHZ: [None, None, "Gateway Clock Speed"], + gw_vars.OTGW_LED_A: [None, None, "Gateway LED A Mode"], + gw_vars.OTGW_LED_B: [None, None, "Gateway LED B Mode"], + gw_vars.OTGW_LED_C: [None, None, "Gateway LED C Mode"], + gw_vars.OTGW_LED_D: [None, None, "Gateway LED D Mode"], + gw_vars.OTGW_LED_E: [None, None, "Gateway LED E Mode"], + gw_vars.OTGW_LED_F: [None, None, "Gateway LED F Mode"], + gw_vars.OTGW_GPIO_A: [None, None, "Gateway GPIO A Mode"], + gw_vars.OTGW_GPIO_B: [None, None, "Gateway GPIO B Mode"], + gw_vars.OTGW_SB_TEMP: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, + "Gateway Setback Temperature"], + gw_vars.OTGW_SETP_OVRD_MODE: [ + None, None, "Gateway Room Setpoint Override Mode"], + gw_vars.OTGW_SMART_PWR: [None, None, "Gateway Smart Power Mode"], + gw_vars.OTGW_THRM_DETECT: [None, None, "Gateway Thermostat Detection"], + gw_vars.OTGW_VREF: [None, None, "Gateway Reference Voltage Setting"], + } + sensors = [] + for var in discovery_info: + device_class = sensor_info[var][0] + unit = sensor_info[var][1] + friendly_name = sensor_info[var][2] + entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, var, hass=hass) + sensors.append( + OpenThermSensor(entity_id, var, device_class, unit, friendly_name)) + async_add_entities(sensors) + + +class OpenThermSensor(Entity): + """Representation of an OpenTherm Gateway sensor.""" + + def __init__(self, entity_id, var, device_class, unit, friendly_name): + """Initialize the sensor.""" + self.entity_id = entity_id + self._var = var + self._value = None + self._device_class = device_class + self._unit = unit + self._friendly_name = friendly_name + + async def async_added_to_hass(self): + """Subscribe to updates from the component.""" + _LOGGER.debug("Added OpenTherm Gateway sensor %s", self._friendly_name) + async_dispatcher_connect(self.hass, SIGNAL_OPENTHERM_GW_UPDATE, + self.receive_report) + + async def receive_report(self, status): + """Handle status updates from the component.""" + value = status.get(self._var) + if isinstance(value, float): + value = '{:2.1f}'.format(value) + self._value = value + self.async_schedule_update_ha_state() + + @property + def name(self): + """Return the friendly name of the sensor.""" + return self._friendly_name + + @property + def device_class(self): + """Return the device class.""" + return self._device_class + + @property + def state(self): + """Return the state of the device.""" + return self._value + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit + + @property + def should_poll(self): + """Return False because entity pushes its state.""" + return False diff --git a/requirements_all.txt b/requirements_all.txt index 59e62006795..c79ad40059b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1033,7 +1033,7 @@ pyoppleio==1.0.5 pyota==2.0.5 # homeassistant.components.opentherm_gw -pyotgw==0.1b0 +pyotgw==0.2b1 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp From 88ec73ed8f0ca95df3cf940be8df63e949649156 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 19 Oct 2018 02:10:41 -0500 Subject: [PATCH 213/265] Add missing await for coroutine (#17609) --- homeassistant/components/camera/ffmpeg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/camera/ffmpeg.py b/homeassistant/components/camera/ffmpeg.py index c458188695a..dfbcc4d70bc 100644 --- a/homeassistant/components/camera/ffmpeg.py +++ b/homeassistant/components/camera/ffmpeg.py @@ -32,7 +32,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up a FFmpeg camera.""" - if not hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_INPUT)): + if not await hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_INPUT)): return async_add_entities([FFmpegCamera(hass, config)]) From 90183bd6820d4af795198c480b24fc63d6caa00f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Lov=C3=A9n?= Date: Fri, 19 Oct 2018 09:17:19 +0200 Subject: [PATCH 214/265] Tuya light icon fix (#17605) * Fix for tuya light icons going too bright * Make sure other values aren't strings either --- homeassistant/components/light/tuya.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/light/tuya.py b/homeassistant/components/light/tuya.py index 0dc2cacc1e0..0a1468a6a51 100644 --- a/homeassistant/components/light/tuya.py +++ b/homeassistant/components/light/tuya.py @@ -40,17 +40,17 @@ class TuyaLight(TuyaDevice, Light): @property def brightness(self): """Return the brightness of the light.""" - return self.tuya.brightness() + return int(self.tuya.brightness()) @property def hs_color(self): """Return the hs_color of the light.""" - return self.tuya.hs_color() + return tuple(map(int, self.tuya.hs_color())) @property def color_temp(self): """Return the color_temp of the light.""" - color_temp = self.tuya.color_temp() + color_temp = int(self.tuya.color_temp()) if color_temp is None: return None return colorutil.color_temperature_kelvin_to_mired(color_temp) From 8bf58e1df5f190c53a6d923dfce6cc748c97be66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Lov=C3=A9n?= Date: Fri, 19 Oct 2018 09:29:48 +0200 Subject: [PATCH 215/265] Revert "De-syncing binary_sensor.ping (#17056)" (#17606) This reverts commit 11d5671ee0a4dbf98c5e9f58a2dec85bdb35f4d0. --- .../components/binary_sensor/ping.py | 42 ++++++++----------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/binary_sensor/ping.py b/homeassistant/components/binary_sensor/ping.py index f12957e6129..4c597dd63e1 100644 --- a/homeassistant/components/binary_sensor/ping.py +++ b/homeassistant/components/binary_sensor/ping.py @@ -4,19 +4,18 @@ Tracks the latency of a host by sending ICMP echo requests (ping). For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.ping/ """ -import asyncio -from datetime import timedelta import logging -import re import subprocess +import re import sys +from datetime import timedelta import voluptuous as vol -from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, BinarySensorDevice) -from homeassistant.const import CONF_HOST, CONF_NAME import homeassistant.helpers.config_validation as cv +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA) +from homeassistant.const import CONF_NAME, CONF_HOST _LOGGER = logging.getLogger(__name__) @@ -49,14 +48,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Ping Binary sensor.""" name = config.get(CONF_NAME) host = config.get(CONF_HOST) count = config.get(CONF_PING_COUNT) - async_add_entities([PingBinarySensor(name, PingData(host, count))], True) + add_entities([PingBinarySensor(name, PingData(host, count))], True) class PingBinarySensor(BinarySensorDevice): @@ -93,9 +91,9 @@ class PingBinarySensor(BinarySensorDevice): ATTR_ROUND_TRIP_TIME_MIN: self.ping.data['min'], } - async def async_update(self): + def update(self): """Get the latest data.""" - await self.ping.update() + self.ping.update() class PingData: @@ -116,13 +114,12 @@ class PingData: 'ping', '-n', '-q', '-c', str(self._count), '-W1', self._ip_address] - async def ping(self): + def ping(self): """Send ICMP echo request and return details if success.""" - pinger = await asyncio.create_subprocess_shell( - ' '.join(self._ping_cmd), stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + pinger = subprocess.Popen( + self._ping_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) try: - out = await pinger.communicate() + out = pinger.communicate() _LOGGER.debug("Output is %s", str(out)) if sys.platform == 'win32': match = WIN32_PING_MATCHER.search(str(out).split('\n')[-1]) @@ -131,8 +128,7 @@ class PingData: 'min': rtt_min, 'avg': rtt_avg, 'max': rtt_max, - 'mdev': '', - } + 'mdev': ''} if 'max/' not in str(out): match = PING_MATCHER_BUSYBOX.search(str(out).split('\n')[-1]) rtt_min, rtt_avg, rtt_max = match.groups() @@ -140,20 +136,18 @@ class PingData: 'min': rtt_min, 'avg': rtt_avg, 'max': rtt_max, - 'mdev': '', - } + 'mdev': ''} match = PING_MATCHER.search(str(out).split('\n')[-1]) rtt_min, rtt_avg, rtt_max, rtt_mdev = match.groups() return { 'min': rtt_min, 'avg': rtt_avg, 'max': rtt_max, - 'mdev': rtt_mdev, - } + 'mdev': rtt_mdev} except (subprocess.CalledProcessError, AttributeError): return False - async def update(self): + def update(self): """Retrieve the latest details from the host.""" - self.data = await self.ping() + self.data = self.ping() self.available = bool(self.data) From f504e5ef6183d71ee555551c89fcae066f1a549d Mon Sep 17 00:00:00 2001 From: ehendrix23 Date: Fri, 19 Oct 2018 01:37:02 -0600 Subject: [PATCH 216/265] Add doorsense sensor for August 3rd Gen Smart Lock Pro (#17299) * Add doorsense sensor for August 3rd Gen Smart Lock Pro Add a binary sensor to August for the August 3rd Gen Smart Lock Pro doorsense sensor. This is a re-do from PR 17116 https://github.com/home-assistant/home-assistant/pull/17116 that I closed due to rebase issue on my end. * Changed to use snjoetw provided code Going through the py-august I found that snjoetw had provided updated versions for the august component (august.py and binary_sensor/august.py) to include DoorSense sensor. Changed what I did to to what snjoetw provided instead as he split it into 2 classes; much cleaner I think. I modified his coding with: Fixes that were done to the August component and not part of the coding snjoetw provided. Added the debug logging improvement I had done in the code. Note, fix I committed earlier for lock atribute (lock/august.py) is thus still the same. * Reverted change from add_device to add_entities Missed an item when merging snjoetw's code with current. Fixed. * Updated call from add_devices to add_entities as well Updated the call from add_devices to add_entities. * Fixed permissions on files Fixed permissions on components/august.py and binary_snesor/august.py * Changed if/else to if/continue Changed logic so that if the door sensor state is unknown during initalization the debug log is written and then continue the loop instead of using if/else logic. * Added available property for Door Sensor Added the available property for the Door Sensor and setting it to False if a status unknown is received. * Updated setting self._available Changed line for setting self._available to what Martin provided. Much more efficient to read. :-) --- homeassistant/components/august.py | 40 +++++++- .../components/binary_sensor/august.py | 92 +++++++++++++++++-- homeassistant/components/lock/august.py | 7 ++ 3 files changed, 130 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/august.py b/homeassistant/components/august.py index 5f268a95f5d..850d972c373 100644 --- a/homeassistant/components/august.py +++ b/homeassistant/components/august.py @@ -4,7 +4,6 @@ Support for August devices. For more details about this component, please refer to the documentation at https://home-assistant.io/components/august/ """ - import logging from datetime import timedelta @@ -124,6 +123,7 @@ def setup_august(hass, config, api, authenticator): return True if state == AuthenticationState.BAD_PASSWORD: + _LOGGER.error("Invalid password provided") return False if state == AuthenticationState.REQUIRES_VALIDATION: request_configuration(hass, config, api, authenticator) @@ -165,6 +165,7 @@ class AugustData: self._doorbell_detail_by_id = {} self._lock_status_by_id = {} self._lock_detail_by_id = {} + self._door_state_by_id = {} self._activities_by_id = {} @property @@ -184,6 +185,7 @@ class AugustData: def get_device_activities(self, device_id, *activity_types): """Return a list of activities.""" + _LOGGER.debug("Getting device activities") self._update_device_activities() activities = self._activities_by_id.get(device_id, []) @@ -199,6 +201,7 @@ class AugustData: @Throttle(MIN_TIME_BETWEEN_UPDATES) def _update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT): """Update data object with latest from August API.""" + _LOGGER.debug("Updating device activities") for house_id in self.house_ids: activities = self._api.get_house_activities(self._access_token, house_id, @@ -218,14 +221,21 @@ class AugustData: def _update_doorbells(self): detail_by_id = {} + _LOGGER.debug("Start retrieving doorbell details") for doorbell in self._doorbells: + _LOGGER.debug("Updating status for %s", + doorbell.device_name) detail_by_id[doorbell.device_id] = self._api.get_doorbell_detail( self._access_token, doorbell.device_id) + _LOGGER.debug("Completed retrieving doorbell details") self._doorbell_detail_by_id = detail_by_id def get_lock_status(self, lock_id): - """Return lock status.""" + """Return status if the door is locked or unlocked. + + This is status for the lock itself. + """ self._update_locks() return self._lock_status_by_id.get(lock_id) @@ -234,17 +244,43 @@ class AugustData: self._update_locks() return self._lock_detail_by_id.get(lock_id) + def get_door_state(self, lock_id): + """Return status if the door is open or closed. + + This is the status from the door sensor. + """ + self._update_doors() + return self._door_state_by_id.get(lock_id) + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def _update_doors(self): + state_by_id = {} + + _LOGGER.debug("Start retrieving door status") + for lock in self._locks: + _LOGGER.debug("Updating status for %s", + lock.device_name) + state_by_id[lock.device_id] = self._api.get_lock_door_status( + self._access_token, lock.device_id) + + _LOGGER.debug("Completed retrieving door status") + self._door_state_by_id = state_by_id + @Throttle(MIN_TIME_BETWEEN_UPDATES) def _update_locks(self): status_by_id = {} detail_by_id = {} + _LOGGER.debug("Start retrieving locks status") for lock in self._locks: + _LOGGER.debug("Updating status for %s", + lock.device_name) status_by_id[lock.device_id] = self._api.get_lock_status( self._access_token, lock.device_id) detail_by_id[lock.device_id] = self._api.get_lock_detail( self._access_token, lock.device_id) + _LOGGER.debug("Completed retrieving locks status") self._lock_status_by_id = status_by_id self._lock_detail_by_id = detail_by_id diff --git a/homeassistant/components/binary_sensor/august.py b/homeassistant/components/binary_sensor/august.py index 7f5da390906..55b31a6da5f 100644 --- a/homeassistant/components/binary_sensor/august.py +++ b/homeassistant/components/binary_sensor/august.py @@ -4,16 +4,26 @@ Support for August binary sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.august/ """ +import logging from datetime import timedelta, datetime from homeassistant.components.august import DATA_AUGUST from homeassistant.components.binary_sensor import (BinarySensorDevice) +_LOGGER = logging.getLogger(__name__) + DEPENDENCIES = ['august'] SCAN_INTERVAL = timedelta(seconds=5) +def _retrieve_door_state(data, lock): + """Get the latest state of the DoorSense sensor.""" + from august.lock import LockDoorStatus + doorstate = data.get_door_state(lock.device_id) + return doorstate == LockDoorStatus.OPEN + + def _retrieve_online_state(data, doorbell): """Get the latest state of the sensor.""" detail = data.get_doorbell_detail(doorbell.device_id) @@ -46,7 +56,11 @@ def _activity_time_based_state(data, doorbell, activity_types): # Sensor types: Name, device_class, state_provider -SENSOR_TYPES = { +SENSOR_TYPES_DOOR = { + 'door_open': ['Open', 'door', _retrieve_door_state], +} + +SENSOR_TYPES_DOORBELL = { 'doorbell_ding': ['Ding', 'occupancy', _retrieve_ding_state], 'doorbell_motion': ['Motion', 'motion', _retrieve_motion_state], 'doorbell_online': ['Online', 'connectivity', _retrieve_online_state], @@ -58,14 +72,78 @@ def setup_platform(hass, config, add_entities, discovery_info=None): data = hass.data[DATA_AUGUST] devices = [] + from august.lock import LockDoorStatus + for door in data.locks: + for sensor_type in SENSOR_TYPES_DOOR: + state_provider = SENSOR_TYPES_DOOR[sensor_type][2] + if state_provider(data, door) is LockDoorStatus.UNKNOWN: + _LOGGER.debug( + "Not adding sensor class %s for lock %s ", + SENSOR_TYPES_DOOR[sensor_type][1], door.device_name + ) + continue + + _LOGGER.debug( + "Adding sensor class %s for %s", + SENSOR_TYPES_DOOR[sensor_type][1], door.device_name + ) + devices.append(AugustDoorBinarySensor(data, sensor_type, door)) + for doorbell in data.doorbells: - for sensor_type in SENSOR_TYPES: - devices.append(AugustBinarySensor(data, sensor_type, doorbell)) + for sensor_type in SENSOR_TYPES_DOORBELL: + _LOGGER.debug("Adding doorbell sensor class %s for %s", + SENSOR_TYPES_DOORBELL[sensor_type][1], + doorbell.device_name) + devices.append( + AugustDoorbellBinarySensor(data, sensor_type, + doorbell) + ) add_entities(devices, True) -class AugustBinarySensor(BinarySensorDevice): +class AugustDoorBinarySensor(BinarySensorDevice): + """Representation of an August Door binary sensor.""" + + def __init__(self, data, sensor_type, door): + """Initialize the sensor.""" + self._data = data + self._sensor_type = sensor_type + self._door = door + self._state = None + self._available = False + + @property + def available(self): + """Return the availability of this sensor.""" + return self._available + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self._state + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return SENSOR_TYPES_DOOR[self._sensor_type][1] + + @property + def name(self): + """Return the name of the binary sensor.""" + return "{} {}".format(self._door.device_name, + SENSOR_TYPES_DOOR[self._sensor_type][0]) + + def update(self): + """Get the latest state of the sensor.""" + state_provider = SENSOR_TYPES_DOOR[self._sensor_type][2] + self._state = state_provider(self._data, self._door) + + from august.lock import LockDoorStatus + self._available = self._state != LockDoorStatus.UNKNOWN + + +class AugustDoorbellBinarySensor(BinarySensorDevice): """Representation of an August binary sensor.""" def __init__(self, data, sensor_type, doorbell): @@ -83,15 +161,15 @@ class AugustBinarySensor(BinarySensorDevice): @property def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" - return SENSOR_TYPES[self._sensor_type][1] + return SENSOR_TYPES_DOORBELL[self._sensor_type][1] @property def name(self): """Return the name of the binary sensor.""" return "{} {}".format(self._doorbell.device_name, - SENSOR_TYPES[self._sensor_type][0]) + SENSOR_TYPES_DOORBELL[self._sensor_type][0]) def update(self): """Get the latest state of the sensor.""" - state_provider = SENSOR_TYPES[self._sensor_type][2] + state_provider = SENSOR_TYPES_DOORBELL[self._sensor_type][2] self._state = state_provider(self._data, self._doorbell) diff --git a/homeassistant/components/lock/august.py b/homeassistant/components/lock/august.py index 7aec3c78690..e8949255ee9 100644 --- a/homeassistant/components/lock/august.py +++ b/homeassistant/components/lock/august.py @@ -4,12 +4,15 @@ Support for August lock. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/lock.august/ """ +import logging from datetime import timedelta from homeassistant.components.august import DATA_AUGUST from homeassistant.components.lock import LockDevice from homeassistant.const import ATTR_BATTERY_LEVEL +_LOGGER = logging.getLogger(__name__) + DEPENDENCIES = ['august'] SCAN_INTERVAL = timedelta(seconds=5) @@ -21,6 +24,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): devices = [] for lock in data.locks: + _LOGGER.debug("Adding lock for %s", lock.device_name) devices.append(AugustLock(data, lock)) add_entities(devices, True) @@ -77,6 +81,9 @@ class AugustLock(LockDevice): @property def device_state_attributes(self): """Return the device specific state attributes.""" + if self._lock_detail is None: + return None + return { ATTR_BATTERY_LEVEL: self._lock_detail.battery_level, } From df2c3cddedf1fee3c15a67f0d7dacf583423536e Mon Sep 17 00:00:00 2001 From: Nick Horvath Date: Fri, 19 Oct 2018 11:48:46 -0400 Subject: [PATCH 217/265] Bump thermoworks version to fix conflict from upstream pyrebase sseclient (#17620) --- homeassistant/components/sensor/thermoworks_smoke.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/thermoworks_smoke.py b/homeassistant/components/sensor/thermoworks_smoke.py index 56ed138e221..e81a3974176 100644 --- a/homeassistant/components/sensor/thermoworks_smoke.py +++ b/homeassistant/components/sensor/thermoworks_smoke.py @@ -17,7 +17,7 @@ from homeassistant.const import TEMP_FAHRENHEIT, CONF_EMAIL, CONF_PASSWORD,\ CONF_MONITORED_CONDITIONS, CONF_EXCLUDE, ATTR_BATTERY_LEVEL from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['thermoworks_smoke==0.1.6', 'stringcase==1.2.0'] +REQUIREMENTS = ['thermoworks_smoke==0.1.7', 'stringcase==1.2.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index c79ad40059b..c50c240795c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1460,7 +1460,7 @@ temperusb==1.5.3 teslajsonpy==0.0.23 # homeassistant.components.sensor.thermoworks_smoke -thermoworks_smoke==0.1.6 +thermoworks_smoke==0.1.7 # homeassistant.components.thingspeak thingspeak==0.4.1 From f7bc44955c8c7adef44f7c50a690ba97974e517b Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Fri, 19 Oct 2018 18:10:04 +0200 Subject: [PATCH 218/265] Upgrade async_upnp_client to 0.12.7 (#17601) --- homeassistant/components/media_player/dlna_dmr.py | 2 +- homeassistant/components/upnp/__init__.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_player/dlna_dmr.py b/homeassistant/components/media_player/dlna_dmr.py index b787ed689c8..bf3fce97650 100644 --- a/homeassistant/components/media_player/dlna_dmr.py +++ b/homeassistant/components/media_player/dlna_dmr.py @@ -25,7 +25,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.util import get_local_ip -REQUIREMENTS = ['async-upnp-client==0.12.6'] +REQUIREMENTS = ['async-upnp-client==0.12.7'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 5e6549b71b7..07c8d5f748e 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -30,7 +30,7 @@ from .config_flow import ensure_domain_data from .device import Device -REQUIREMENTS = ['async-upnp-client==0.12.6'] +REQUIREMENTS = ['async-upnp-client==0.12.7'] DEPENDENCIES = ['http'] NOTIFICATION_ID = 'upnp_notification' diff --git a/requirements_all.txt b/requirements_all.txt index c50c240795c..a59dcd2cff0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -153,7 +153,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.upnp # homeassistant.components.media_player.dlna_dmr -async-upnp-client==0.12.6 +async-upnp-client==0.12.7 # homeassistant.components.light.avion # avion==0.7 From e343f5521c630dfab7939a6ee1ca8b55a1f4b943 Mon Sep 17 00:00:00 2001 From: Nick Touran Date: Fri, 19 Oct 2018 09:11:47 -0700 Subject: [PATCH 219/265] Upgrade gstreamer-player to 1.1.2 (#17568) * Upgrade gstreamer-player to 1.1.2 * Updated requirements for gstreamer-player properly. --- homeassistant/components/media_player/gstreamer.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/gstreamer.py b/homeassistant/components/media_player/gstreamer.py index e520fcb1033..fa8545bae03 100644 --- a/homeassistant/components/media_player/gstreamer.py +++ b/homeassistant/components/media_player/gstreamer.py @@ -14,7 +14,7 @@ from homeassistant.components.media_player import ( from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP, STATE_IDLE import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['gstreamer-player==1.1.0'] +REQUIREMENTS = ['gstreamer-player==1.1.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index a59dcd2cff0..6c20f961228 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -430,7 +430,7 @@ gps3==0.33.3 greenwavereality==0.5.1 # homeassistant.components.media_player.gstreamer -gstreamer-player==1.1.0 +gstreamer-player==1.1.2 # homeassistant.components.ffmpeg ha-ffmpeg==1.9 From ff33cbd22ffe5592c138fcd7bd9294bf9b783dc4 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Fri, 19 Oct 2018 21:04:05 +0200 Subject: [PATCH 220/265] Add water_heater support to HomeKit (#17614) * Homekit add support for water_heater * Added tests --- homeassistant/components/homekit/__init__.py | 3 + homeassistant/components/homekit/const.py | 4 + .../components/homekit/type_thermostats.py | 139 +++++++++++-- .../homekit/test_get_accessories.py | 1 + .../homekit/test_type_thermostats.py | 186 +++++++++++++++--- 5 files changed, 281 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 5d7733584be..59c227fb0b8 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -173,6 +173,9 @@ def get_accessory(hass, driver, state, aid, config): elif state.domain in ('automation', 'input_boolean', 'remote', 'script'): a_type = 'Switch' + elif state.domain == 'water_heater': + a_type = 'WaterHeater' + if a_type is None: return None diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 6c143f7f0da..d35d38c6455 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -146,3 +146,7 @@ DEVICE_CLASS_WINDOW = 'window' # #### Thresholds #### THRESHOLD_CO = 25 THRESHOLD_CO2 = 1000 + +# #### Default values #### +DEFAULT_MIN_TEMP_WATER_HEATER = 40 # °C +DEFAULT_MAX_TEMP_WATER_HEATER = 60 # °C diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index a7344995021..2513ee50981 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -6,13 +6,18 @@ from pyhap.const import CATEGORY_THERMOSTAT from homeassistant.components.climate import ( ATTR_CURRENT_TEMPERATURE, ATTR_MAX_TEMP, ATTR_MIN_TEMP, ATTR_OPERATION_LIST, ATTR_OPERATION_MODE, - ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, - DOMAIN, SERVICE_SET_TEMPERATURE, SERVICE_SET_OPERATION_MODE, STATE_AUTO, - STATE_COOL, STATE_HEAT, SUPPORT_ON_OFF, SUPPORT_TARGET_TEMPERATURE_HIGH, - SUPPORT_TARGET_TEMPERATURE_LOW) + ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN as DOMAIN_CLIMATE, + SERVICE_SET_OPERATION_MODE as SERVICE_SET_OPERATION_MODE_THERMOSTAT, + SERVICE_SET_TEMPERATURE as SERVICE_SET_TEMPERATURE_THERMOSTAT, + STATE_AUTO, STATE_COOL, STATE_HEAT, SUPPORT_ON_OFF, + SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) +from homeassistant.components.water_heater import ( + DOMAIN as DOMAIN_WATER_HEATER, + SERVICE_SET_TEMPERATURE as SERVICE_SET_TEMPERATURE_WATER_HEATER) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_OFF, SERVICE_TURN_ON, + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, + SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) from . import TYPES @@ -21,7 +26,9 @@ from .const import ( CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_CURRENT_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE, CHAR_TARGET_HEATING_COOLING, CHAR_HEATING_THRESHOLD_TEMPERATURE, CHAR_TARGET_TEMPERATURE, - CHAR_TEMP_DISPLAY_UNITS, PROP_MAX_VALUE, PROP_MIN_VALUE, SERV_THERMOSTAT) + CHAR_TEMP_DISPLAY_UNITS, + DEFAULT_MAX_TEMP_WATER_HEATER, DEFAULT_MIN_TEMP_WATER_HEATER, + PROP_MAX_VALUE, PROP_MIN_VALUE, SERV_THERMOSTAT) from .util import temperature_to_homekit, temperature_to_states _LOGGER = logging.getLogger(__name__) @@ -114,7 +121,7 @@ class Thermostat(HomeAccessory): return min_temp, max_temp def set_heat_cool(self, value): - """Move operation mode to value if call came from HomeKit.""" + """Change operation mode to value if call came from HomeKit.""" if value in HC_HOMEKIT_TO_HASS: _LOGGER.debug('%s: Set heat-cool to %d', self.entity_id, value) self.heat_cool_flag_target_state = True @@ -122,13 +129,14 @@ class Thermostat(HomeAccessory): if self.support_power_state is True: params = {ATTR_ENTITY_ID: self.entity_id} if hass_value == STATE_OFF: - self.call_service(DOMAIN, SERVICE_TURN_OFF, params) + self.call_service(DOMAIN_CLIMATE, SERVICE_TURN_OFF, params) return - self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params) + self.call_service(DOMAIN_CLIMATE, SERVICE_TURN_ON, params) params = {ATTR_ENTITY_ID: self.entity_id, ATTR_OPERATION_MODE: hass_value} self.call_service( - DOMAIN, SERVICE_SET_OPERATION_MODE, params, hass_value) + DOMAIN_CLIMATE, SERVICE_SET_OPERATION_MODE_THERMOSTAT, + params, hass_value) @debounce def set_cooling_threshold(self, value): @@ -142,9 +150,9 @@ class Thermostat(HomeAccessory): ATTR_ENTITY_ID: self.entity_id, ATTR_TARGET_TEMP_HIGH: temperature, ATTR_TARGET_TEMP_LOW: temperature_to_states(low, self._unit)} - self.call_service(DOMAIN, SERVICE_SET_TEMPERATURE, params, - "cooling threshold {}{}".format(temperature, - self._unit)) + self.call_service( + DOMAIN_CLIMATE, SERVICE_SET_TEMPERATURE_THERMOSTAT, + params, 'cooling threshold {}{}'.format(temperature, self._unit)) @debounce def set_heating_threshold(self, value): @@ -158,9 +166,9 @@ class Thermostat(HomeAccessory): ATTR_ENTITY_ID: self.entity_id, ATTR_TARGET_TEMP_HIGH: temperature_to_states(high, self._unit), ATTR_TARGET_TEMP_LOW: temperature} - self.call_service(DOMAIN, SERVICE_SET_TEMPERATURE, params, - "heating threshold {}{}".format(temperature, - self._unit)) + self.call_service( + DOMAIN_CLIMATE, SERVICE_SET_TEMPERATURE_THERMOSTAT, + params, 'heating threshold {}{}'.format(temperature, self._unit)) @debounce def set_target_temperature(self, value): @@ -172,12 +180,12 @@ class Thermostat(HomeAccessory): params = { ATTR_ENTITY_ID: self.entity_id, ATTR_TEMPERATURE: temperature} - self.call_service(DOMAIN, SERVICE_SET_TEMPERATURE, params, - "target {}{}".format(temperature, - self._unit)) + self.call_service( + DOMAIN_CLIMATE, SERVICE_SET_TEMPERATURE_THERMOSTAT, + params, 'target {}{}'.format(temperature, self._unit)) def update_state(self, new_state): - """Update security state after state changed.""" + """Update thermostat state after state changed.""" # Update current temperature current_temp = new_state.attributes.get(ATTR_CURRENT_TEMPERATURE) if isinstance(current_temp, (int, float)): @@ -268,3 +276,92 @@ class Thermostat(HomeAccessory): self.char_current_heat_cool.set_value( HC_HASS_TO_HOMEKIT[current_operation_mode]) + + +@TYPES.register('WaterHeater') +class WaterHeater(HomeAccessory): + """Generate a WaterHeater accessory for a water_heater.""" + + def __init__(self, *args): + """Initialize a WaterHeater accessory object.""" + super().__init__(*args, category=CATEGORY_THERMOSTAT) + self._unit = self.hass.config.units.temperature_unit + self.flag_heat_cool = False + self.flag_temperature = False + min_temp, max_temp = self.get_temperature_range() + + serv_thermostat = self.add_preload_service(SERV_THERMOSTAT) + + self.char_current_heat_cool = serv_thermostat.configure_char( + CHAR_CURRENT_HEATING_COOLING, value=1) + self.char_target_heat_cool = serv_thermostat.configure_char( + CHAR_TARGET_HEATING_COOLING, value=1, + setter_callback=self.set_heat_cool) + + self.char_current_temp = serv_thermostat.configure_char( + CHAR_CURRENT_TEMPERATURE, value=50.0) + self.char_target_temp = serv_thermostat.configure_char( + CHAR_TARGET_TEMPERATURE, value=50.0, + properties={PROP_MIN_VALUE: min_temp, + PROP_MAX_VALUE: max_temp}, + setter_callback=self.set_target_temperature) + + self.char_display_units = serv_thermostat.configure_char( + CHAR_TEMP_DISPLAY_UNITS, value=0) + + def get_temperature_range(self): + """Return min and max temperature range.""" + max_temp = self.hass.states.get(self.entity_id) \ + .attributes.get(ATTR_MAX_TEMP) + max_temp = temperature_to_homekit(max_temp, self._unit) if max_temp \ + else DEFAULT_MAX_TEMP_WATER_HEATER + + min_temp = self.hass.states.get(self.entity_id) \ + .attributes.get(ATTR_MIN_TEMP) + min_temp = temperature_to_homekit(min_temp, self._unit) if min_temp \ + else DEFAULT_MIN_TEMP_WATER_HEATER + + return min_temp, max_temp + + def set_heat_cool(self, value): + """Change operation mode to value if call came from HomeKit.""" + _LOGGER.debug('%s: Set heat-cool to %d', self.entity_id, value) + self.flag_heat_cool = True + hass_value = HC_HOMEKIT_TO_HASS[value] + if hass_value != STATE_HEAT: + self.char_target_heat_cool.set_value(1) # Heat + + @debounce + def set_target_temperature(self, value): + """Set target temperature to value if call came from HomeKit.""" + _LOGGER.debug('%s: Set target temperature to %.2f°C', + self.entity_id, value) + self.flag_temperature = True + temperature = temperature_to_states(value, self._unit) + params = { + ATTR_ENTITY_ID: self.entity_id, + ATTR_TEMPERATURE: temperature} + self.call_service( + DOMAIN_WATER_HEATER, SERVICE_SET_TEMPERATURE_WATER_HEATER, + params, 'target {}{}'.format(temperature, self._unit)) + + def update_state(self, new_state): + """Update water_heater state after state change.""" + # Update current and target temperature + temperature = new_state.attributes.get(ATTR_TEMPERATURE) + if isinstance(temperature, (int, float)): + temperature = temperature_to_homekit(temperature, self._unit) + self.char_current_temp.set_value(temperature) + if not self.flag_temperature: + self.char_target_temp.set_value(temperature) + self.flag_temperature = False + + # Update display units + if self._unit and self._unit in UNIT_HASS_TO_HOMEKIT: + self.char_display_units.set_value(UNIT_HASS_TO_HOMEKIT[self._unit]) + + # Update target operation mode + operation_mode = new_state.attributes.get(ATTR_OPERATION_MODE) + if operation_mode and not self.flag_heat_cool: + self.char_target_heat_cool.set_value(1) # Heat + self.flag_heat_cool = False diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index d5552cce82c..7d303c38e93 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -69,6 +69,7 @@ def test_customize_options(config, name): ('Thermostat', 'climate.test', 'auto', {ATTR_SUPPORTED_FEATURES: climate.SUPPORT_TARGET_TEMPERATURE_LOW | climate.SUPPORT_TARGET_TEMPERATURE_HIGH}, {}), + ('WaterHeater', 'water_heater.test', 'auto', {}, {}), ]) def test_types(type_name, entity_id, state, attrs, config): """Test if types are associated correctly.""" diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index e3187b6cf02..73b27ca851c 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -5,15 +5,18 @@ from unittest.mock import patch import pytest from homeassistant.components.climate import ( - ATTR_CURRENT_TEMPERATURE, ATTR_MAX_TEMP, ATTR_MIN_TEMP, ATTR_TEMPERATURE, + ATTR_CURRENT_TEMPERATURE, ATTR_MAX_TEMP, ATTR_MIN_TEMP, ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE, - ATTR_OPERATION_LIST, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN, - STATE_AUTO, STATE_COOL, STATE_HEAT) + ATTR_OPERATION_LIST, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, + DOMAIN as DOMAIN_CLIMATE, STATE_AUTO, STATE_COOL, STATE_HEAT) from homeassistant.components.homekit.const import ( - ATTR_VALUE, PROP_MAX_VALUE, PROP_MIN_VALUE) + ATTR_VALUE, DEFAULT_MAX_TEMP_WATER_HEATER, DEFAULT_MIN_TEMP_WATER_HEATER, + PROP_MAX_VALUE, PROP_MIN_VALUE) +from homeassistant.components.water_heater import ( + DOMAIN as DOMAIN_WATER_HEATER) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_TEMPERATURE_UNIT, STATE_OFF, - TEMP_FAHRENHEIT) + ATTR_ENTITY_ID, ATTR_TEMPERATURE, ATTR_SUPPORTED_FEATURES, + CONF_TEMPERATURE_UNIT, STATE_OFF, TEMP_FAHRENHEIT) from tests.common import async_mock_service from tests.components.homekit.common import patch_debounce @@ -25,13 +28,14 @@ def cls(): patcher = patch_debounce() patcher.start() _import = __import__('homeassistant.components.homekit.type_thermostats', - fromlist=['Thermostat']) - patcher_tuple = namedtuple('Cls', ['thermostat']) - yield patcher_tuple(thermostat=_import.Thermostat) + fromlist=['Thermostat', 'WaterHeater']) + patcher_tuple = namedtuple('Cls', ['thermostat', 'water_heater']) + yield patcher_tuple(thermostat=_import.Thermostat, + water_heater=_import.WaterHeater) patcher.stop() -async def test_default_thermostat(hass, hk_driver, cls, events): +async def test_thermostat(hass, hk_driver, cls, events): """Test if accessory and HA are updated accordingly.""" entity_id = 'climate.test' @@ -147,8 +151,9 @@ async def test_default_thermostat(hass, hk_driver, cls, events): assert acc.char_display_units.value == 0 # Set from HomeKit - call_set_temperature = async_mock_service(hass, DOMAIN, 'set_temperature') - call_set_operation_mode = async_mock_service(hass, DOMAIN, + call_set_temperature = async_mock_service(hass, DOMAIN_CLIMATE, + 'set_temperature') + call_set_operation_mode = async_mock_service(hass, DOMAIN_CLIMATE, 'set_operation_mode') await hass.async_add_job(acc.char_target_temp.client_update_value, 19.0) @@ -158,8 +163,7 @@ async def test_default_thermostat(hass, hk_driver, cls, events): assert call_set_temperature[0].data[ATTR_TEMPERATURE] == 19.0 assert acc.char_target_temp.value == 19.0 assert len(events) == 1 - assert events[-1].data[ATTR_VALUE] == "target {}°C".format( - acc.char_target_temp.value) + assert events[-1].data[ATTR_VALUE] == 'target 19.0°C' await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 1) await hass.async_block_till_done() @@ -171,7 +175,7 @@ async def test_default_thermostat(hass, hk_driver, cls, events): assert events[-1].data[ATTR_VALUE] == STATE_HEAT -async def test_auto_thermostat(hass, hk_driver, cls, events): +async def test_thermostat_auto(hass, hk_driver, cls, events): """Test if accessory and HA are updated accordingly.""" entity_id = 'climate.test' @@ -234,7 +238,8 @@ async def test_auto_thermostat(hass, hk_driver, cls, events): assert acc.char_display_units.value == 0 # Set from HomeKit - call_set_temperature = async_mock_service(hass, DOMAIN, 'set_temperature') + call_set_temperature = async_mock_service(hass, DOMAIN_CLIMATE, + 'set_temperature') await hass.async_add_job( acc.char_heating_thresh_temp.client_update_value, 20.0) @@ -244,8 +249,7 @@ async def test_auto_thermostat(hass, hk_driver, cls, events): assert call_set_temperature[0].data[ATTR_TARGET_TEMP_LOW] == 20.0 assert acc.char_heating_thresh_temp.value == 20.0 assert len(events) == 1 - assert events[-1].data[ATTR_VALUE] == "heating threshold {}°C".format( - acc.char_heating_thresh_temp.value) + assert events[-1].data[ATTR_VALUE] == 'heating threshold 20.0°C' await hass.async_add_job( acc.char_cooling_thresh_temp.client_update_value, 25.0) @@ -255,11 +259,10 @@ async def test_auto_thermostat(hass, hk_driver, cls, events): assert call_set_temperature[1].data[ATTR_TARGET_TEMP_HIGH] == 25.0 assert acc.char_cooling_thresh_temp.value == 25.0 assert len(events) == 2 - assert events[-1].data[ATTR_VALUE] == "cooling threshold {}°C".format( - acc.char_cooling_thresh_temp.value) + assert events[-1].data[ATTR_VALUE] == 'cooling threshold 25.0°C' -async def test_power_state(hass, hk_driver, cls, events): +async def test_thermostat_power_state(hass, hk_driver, cls, events): """Test if accessory and HA are updated accordingly.""" entity_id = 'climate.test' @@ -295,9 +298,9 @@ async def test_power_state(hass, hk_driver, cls, events): assert acc.char_target_heat_cool.value == 0 # Set from HomeKit - call_turn_on = async_mock_service(hass, DOMAIN, 'turn_on') - call_turn_off = async_mock_service(hass, DOMAIN, 'turn_off') - call_set_operation_mode = async_mock_service(hass, DOMAIN, + call_turn_on = async_mock_service(hass, DOMAIN_CLIMATE, 'turn_on') + call_turn_off = async_mock_service(hass, DOMAIN_CLIMATE, 'turn_off') + call_set_operation_mode = async_mock_service(hass, DOMAIN_CLIMATE, 'set_operation_mode') await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 1) @@ -308,7 +311,7 @@ async def test_power_state(hass, hk_driver, cls, events): assert call_set_operation_mode[0].data[ATTR_ENTITY_ID] == entity_id assert call_set_operation_mode[0].data[ATTR_OPERATION_MODE] == STATE_HEAT assert acc.char_target_heat_cool.value == 1 - assert len(events) == 1 + assert len(events) == 2 assert events[-1].data[ATTR_VALUE] == STATE_HEAT await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 0) @@ -316,7 +319,7 @@ async def test_power_state(hass, hk_driver, cls, events): assert call_turn_off assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id assert acc.char_target_heat_cool.value == 0 - assert len(events) == 2 + assert len(events) == 3 assert events[-1].data[ATTR_VALUE] is None @@ -347,7 +350,8 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls, events): assert acc.char_display_units.value == 1 # Set from HomeKit - call_set_temperature = async_mock_service(hass, DOMAIN, 'set_temperature') + call_set_temperature = async_mock_service(hass, DOMAIN_CLIMATE, + 'set_temperature') await hass.async_add_job( acc.char_cooling_thresh_temp.client_update_value, 23) @@ -357,7 +361,7 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls, events): assert call_set_temperature[0].data[ATTR_TARGET_TEMP_HIGH] == 73.4 assert call_set_temperature[0].data[ATTR_TARGET_TEMP_LOW] == 68 assert len(events) == 1 - assert events[-1].data[ATTR_VALUE] == "cooling threshold 73.4°F" + assert events[-1].data[ATTR_VALUE] == 'cooling threshold 73.4°F' await hass.async_add_job( acc.char_heating_thresh_temp.client_update_value, 22) @@ -367,7 +371,7 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls, events): assert call_set_temperature[1].data[ATTR_TARGET_TEMP_HIGH] == 73.4 assert call_set_temperature[1].data[ATTR_TARGET_TEMP_LOW] == 71.6 assert len(events) == 2 - assert events[-1].data[ATTR_VALUE] == "heating threshold 71.6°F" + assert events[-1].data[ATTR_VALUE] == 'heating threshold 71.6°F' await hass.async_add_job(acc.char_target_temp.client_update_value, 24.0) await hass.async_block_till_done() @@ -375,10 +379,10 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls, events): assert call_set_temperature[2].data[ATTR_ENTITY_ID] == entity_id assert call_set_temperature[2].data[ATTR_TEMPERATURE] == 75.2 assert len(events) == 3 - assert events[-1].data[ATTR_VALUE] == "target 75.2°F" + assert events[-1].data[ATTR_VALUE] == 'target 75.2°F' -async def test_get_temperature_range(hass, hk_driver, cls): +async def test_thermostat_get_temperature_range(hass, hk_driver, cls): """Test if temperature range is evaluated correctly.""" entity_id = 'climate.test' @@ -396,3 +400,123 @@ async def test_get_temperature_range(hass, hk_driver, cls): {ATTR_MIN_TEMP: 60, ATTR_MAX_TEMP: 70}) await hass.async_block_till_done() assert acc.get_temperature_range() == (15.6, 21.1) + + +async def test_water_heater(hass, hk_driver, cls, events): + """Test if accessory and HA are updated accordingly.""" + entity_id = 'water_heater.test' + + hass.states.async_set(entity_id, STATE_HEAT) + await hass.async_block_till_done() + acc = cls.water_heater(hass, hk_driver, 'WaterHeater', entity_id, 2, None) + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + + assert acc.aid == 2 + assert acc.category == 9 # Thermostat + + assert acc.char_current_heat_cool.value == 1 # Heat + assert acc.char_target_heat_cool.value == 1 # Heat + assert acc.char_current_temp.value == 50.0 + assert acc.char_target_temp.value == 50.0 + assert acc.char_display_units.value == 0 + + assert acc.char_target_temp.properties[PROP_MAX_VALUE] == \ + DEFAULT_MAX_TEMP_WATER_HEATER + assert acc.char_target_temp.properties[PROP_MIN_VALUE] == \ + DEFAULT_MIN_TEMP_WATER_HEATER + + hass.states.async_set(entity_id, STATE_HEAT, + {ATTR_OPERATION_MODE: STATE_HEAT, + ATTR_TEMPERATURE: 56.0}) + await hass.async_block_till_done() + assert acc.char_target_temp.value == 56.0 + assert acc.char_current_temp.value == 56.0 + assert acc.char_target_heat_cool.value == 1 + assert acc.char_current_heat_cool.value == 1 + assert acc.char_display_units.value == 0 + + hass.states.async_set(entity_id, STATE_AUTO, + {ATTR_OPERATION_MODE: STATE_AUTO}) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == 1 + assert acc.char_current_heat_cool.value == 1 + + # Set from HomeKit + call_set_temperature = async_mock_service(hass, DOMAIN_WATER_HEATER, + 'set_temperature') + + await hass.async_add_job(acc.char_target_temp.client_update_value, 52.0) + await hass.async_block_till_done() + assert call_set_temperature + assert call_set_temperature[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_temperature[0].data[ATTR_TEMPERATURE] == 52.0 + assert acc.char_target_temp.value == 52.0 + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] == 'target 52.0°C' + + await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 0) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == 1 + + await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 2) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == 1 + + await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 3) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == 1 + + +async def test_water_heater_fahrenheit(hass, hk_driver, cls, events): + """Test if accessory and HA are update accordingly.""" + entity_id = 'water_heater.test' + + hass.states.async_set(entity_id, STATE_HEAT) + await hass.async_block_till_done() + with patch.object(hass.config.units, CONF_TEMPERATURE_UNIT, + new=TEMP_FAHRENHEIT): + acc = cls.water_heater(hass, hk_driver, 'WaterHeater', + entity_id, 2, None) + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + + hass.states.async_set(entity_id, STATE_HEAT, + {ATTR_TEMPERATURE: 131}) + await hass.async_block_till_done() + assert acc.char_target_temp.value == 55.0 + assert acc.char_current_temp.value == 55.0 + assert acc.char_display_units.value == 1 + + # Set from HomeKit + call_set_temperature = async_mock_service(hass, DOMAIN_WATER_HEATER, + 'set_temperature') + + await hass.async_add_job(acc.char_target_temp.client_update_value, 60) + await hass.async_block_till_done() + assert call_set_temperature + assert call_set_temperature[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_temperature[0].data[ATTR_TEMPERATURE] == 140.0 + assert acc.char_target_temp.value == 60.0 + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] == 'target 140.0°F' + + +async def test_water_heater_get_temperature_range(hass, hk_driver, cls): + """Test if temperature range is evaluated correctly.""" + entity_id = 'water_heater.test' + + hass.states.async_set(entity_id, STATE_HEAT) + await hass.async_block_till_done() + acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None) + + hass.states.async_set(entity_id, STATE_HEAT, + {ATTR_MIN_TEMP: 20, ATTR_MAX_TEMP: 25}) + await hass.async_block_till_done() + assert acc.get_temperature_range() == (20, 25) + + acc._unit = TEMP_FAHRENHEIT + hass.states.async_set(entity_id, STATE_OFF, + {ATTR_MIN_TEMP: 60, ATTR_MAX_TEMP: 70}) + await hass.async_block_till_done() + assert acc.get_temperature_range() == (15.6, 21.1) From 3655fefec2b69708fc5b803b851e6f2ee01d34d6 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Fri, 19 Oct 2018 17:41:04 -0400 Subject: [PATCH 221/265] Add Elk-M1 sensor platform (#17342) * Initial sensor version. * Add Elk-M1 sensor services. Initial version of services.yaml. * Fix lint errors. * Fix PR comments. * fix PR comment * Fix PR comment * Fix PR comments * fix --- homeassistant/components/elkm1/__init__.py | 23 +- homeassistant/components/elkm1/services.yaml | 12 + homeassistant/components/sensor/elkm1.py | 227 +++++++++++++++++++ 3 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/elkm1/services.yaml create mode 100644 homeassistant/components/sensor/elkm1.py diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index da322232bdf..5c379c7438b 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -35,7 +35,13 @@ CONF_ENABLED = 'enabled' _LOGGER = logging.getLogger(__name__) -SUPPORTED_DOMAINS = ['alarm_control_panel', 'light', 'scene', 'switch'] +SUPPORTED_DOMAINS = ['alarm_control_panel', 'light', 'scene', 'sensor', + 'switch'] + +SPEAK_SERVICE_SCHEMA = vol.Schema({ + vol.Required('number'): + vol.All(vol.Coerce(int), vol.Range(min=0, max=999)) +}) def _host_validator(config): @@ -135,6 +141,8 @@ async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: 'password': conf[CONF_PASSWORD]}) elk.connect() + _create_elk_services(hass, elk) + hass.data[DOMAIN] = {'elk': elk, 'config': config, 'keypads': {}} for component in SUPPORTED_DOMAINS: hass.async_create_task( @@ -143,6 +151,19 @@ async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: return True +def _create_elk_services(hass, elk): + def _speak_word_service(service): + elk.panel.speak_word(service.data.get('number')) + + def _speak_phrase_service(service): + elk.panel.speak_phrase(service.data.get('number')) + + hass.services.async_register( + DOMAIN, 'speak_word', _speak_word_service, SPEAK_SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, 'speak_phrase', _speak_phrase_service, SPEAK_SERVICE_SCHEMA) + + def create_elk_entities(hass, elk_elements, element_type, class_, entities): """Create the ElkM1 devices of a particular class.""" elk_data = hass.data[DOMAIN] diff --git a/homeassistant/components/elkm1/services.yaml b/homeassistant/components/elkm1/services.yaml new file mode 100644 index 00000000000..40571656963 --- /dev/null +++ b/homeassistant/components/elkm1/services.yaml @@ -0,0 +1,12 @@ +speak_word: + description: Speak a word. See list of words in ElkM1 ASCII Protocol documentation. + fields: + number: + description: Word number to speak. + example: 142 +speak_phrase: + description: Speak a phrase. See list of phrases in ElkM1 ASCII Protocol documentation. + fields: + number: + description: Phrase number to speak. + example: 42 diff --git a/homeassistant/components/sensor/elkm1.py b/homeassistant/components/sensor/elkm1.py new file mode 100644 index 00000000000..288f968b2f7 --- /dev/null +++ b/homeassistant/components/sensor/elkm1.py @@ -0,0 +1,227 @@ +""" +Support for control of ElkM1 sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.elkm1/ +""" +from homeassistant.components.elkm1 import ( + DOMAIN as ELK_DOMAIN, create_elk_entities, ElkEntity) + +DEPENDENCIES = [ELK_DOMAIN] + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Create the Elk-M1 sensor platform.""" + if discovery_info is None: + return + + elk = hass.data[ELK_DOMAIN]['elk'] + entities = create_elk_entities( + hass, elk.counters, 'counter', ElkCounter, []) + entities = create_elk_entities( + hass, elk.keypads, 'keypad', ElkKeypad, entities) + entities = create_elk_entities( + hass, [elk.panel], 'panel', ElkPanel, entities) + entities = create_elk_entities( + hass, elk.settings, 'setting', ElkSetting, entities) + entities = create_elk_entities( + hass, elk.zones, 'zone', ElkZone, entities) + async_add_entities(entities, True) + + +def temperature_to_state(temperature, undefined_temperature): + """Convert temperature to a state.""" + return temperature if temperature > undefined_temperature else None + + +class ElkSensor(ElkEntity): + """Base representation of Elk-M1 sensor.""" + + def __init__(self, element, elk, elk_data): + """Initialize the base of all Elk sensors.""" + super().__init__(element, elk, elk_data) + self._state = None + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + +class ElkCounter(ElkSensor): + """Representation of an Elk-M1 Counter.""" + + @property + def icon(self): + """Icon to use in the frontend.""" + return 'mdi:numeric' + + def _element_changed(self, element, changeset): + self._state = self._element.value + + +class ElkKeypad(ElkSensor): + """Representation of an Elk-M1 Keypad.""" + + @property + def temperature_unit(self): + """Return the temperature unit.""" + return self._temperature_unit + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._temperature_unit + + @property + def icon(self): + """Icon to use in the frontend.""" + return 'mdi:thermometer-lines' + + @property + def device_state_attributes(self): + """Attributes of the sensor.""" + from elkm1_lib.util import username + + attrs = self.initial_attrs() + attrs['area'] = self._element.area + 1 + attrs['temperature'] = self._element.temperature + attrs['last_user_time'] = self._element.last_user_time.isoformat() + attrs['last_user'] = self._element.last_user + 1 + attrs['code'] = self._element.code + attrs['last_user_name'] = username(self._elk, self._element.last_user) + return attrs + + def _element_changed(self, element, changeset): + self._state = temperature_to_state(self._element.temperature, -40) + + async def async_added_to_hass(self): + """Register callback for ElkM1 changes and update entity state.""" + await super().async_added_to_hass() + self.hass.data[ELK_DOMAIN]['keypads'][ + self._element.index] = self.entity_id + + +class ElkPanel(ElkSensor): + """Representation of an Elk-M1 Panel.""" + + @property + def icon(self): + """Icon to use in the frontend.""" + return "mdi:home" + + @property + def device_state_attributes(self): + """Attributes of the sensor.""" + attrs = self.initial_attrs() + attrs['system_trouble_status'] = self._element.system_trouble_status + return attrs + + def _element_changed(self, element, changeset): + if self._elk.is_connected(): + self._state = 'Paused' if self._element.remote_programming_status \ + else 'Connected' + else: + self._state = 'Disconnected' + + +class ElkSetting(ElkSensor): + """Representation of an Elk-M1 Setting.""" + + @property + def icon(self): + """Icon to use in the frontend.""" + return 'mdi:numeric' + + def _element_changed(self, element, changeset): + self._state = self._element.value + + @property + def device_state_attributes(self): + """Attributes of the sensor.""" + from elkm1_lib.const import SettingFormat + attrs = self.initial_attrs() + attrs['value_format'] = SettingFormat( + self._element.value_format).name.lower() + return attrs + + +class ElkZone(ElkSensor): + """Representation of an Elk-M1 Zone.""" + + @property + def icon(self): + """Icon to use in the frontend.""" + from elkm1_lib.const import ZoneType + zone_icons = { + ZoneType.FIRE_ALARM.value: 'fire', + ZoneType.FIRE_VERIFIED.value: 'fire', + ZoneType.FIRE_SUPERVISORY.value: 'fire', + ZoneType.KEYFOB.value: 'key', + ZoneType.NON_ALARM.value: 'alarm-off', + ZoneType.MEDICAL_ALARM.value: 'medical-bag', + ZoneType.POLICE_ALARM.value: 'alarm-light', + ZoneType.POLICE_NO_INDICATION.value: 'alarm-light', + ZoneType.KEY_MOMENTARY_ARM_DISARM.value: 'power', + ZoneType.KEY_MOMENTARY_ARM_AWAY.value: 'power', + ZoneType.KEY_MOMENTARY_ARM_STAY.value: 'power', + ZoneType.KEY_MOMENTARY_DISARM.value: 'power', + ZoneType.KEY_ON_OFF.value: 'toggle-switch', + ZoneType.MUTE_AUDIBLES.value: 'volume-mute', + ZoneType.POWER_SUPERVISORY.value: 'power-plug', + ZoneType.TEMPERATURE.value: 'thermometer-lines', + ZoneType.ANALOG_ZONE.value: 'speedometer', + ZoneType.PHONE_KEY.value: 'phone-classic', + ZoneType.INTERCOM_KEY.value: 'deskphone' + } + return 'mdi:{}'.format( + zone_icons.get(self._element.definition, 'alarm-bell')) + + @property + def device_state_attributes(self): + """Attributes of the sensor.""" + from elkm1_lib.const import ( + ZoneLogicalStatus, ZonePhysicalStatus, ZoneType) + + attrs = self.initial_attrs() + attrs['physical_status'] = ZonePhysicalStatus( + self._element.physical_status).name.lower() + attrs['logical_status'] = ZoneLogicalStatus( + self._element.logical_status).name.lower() + attrs['definition'] = ZoneType( + self._element.definition).name.lower() + attrs['area'] = self._element.area + 1 + attrs['bypassed'] = self._element.bypassed + attrs['triggered_alarm'] = self._element.triggered_alarm + return attrs + + @property + def temperature_unit(self): + """Return the temperature unit.""" + from elkm1_lib.const import ZoneType + if self._element.definition == ZoneType.TEMPERATURE.value: + return self._temperature_unit + return None + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + from elkm1_lib.const import ZoneType + if self._element.definition == ZoneType.TEMPERATURE.value: + return self._temperature_unit + if self._element.definition == ZoneType.ANALOG_ZONE.value: + return 'V' + return None + + def _element_changed(self, element, changeset): + from elkm1_lib.const import ZoneLogicalStatus, ZoneType + from elkm1_lib.util import pretty_const + + if self._element.definition == ZoneType.TEMPERATURE.value: + self._state = temperature_to_state(self._element.temperature, -60) + elif self._element.definition == ZoneType.ANALOG_ZONE.value: + self._state = self._element.voltage + else: + self._state = pretty_const(ZoneLogicalStatus( + self._element.logical_status).name) From a9a8cbbd100b4ca5c7f90210fb37da37bc634923 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Sat, 20 Oct 2018 00:14:05 +0200 Subject: [PATCH 222/265] Homekit component cleanup (#17627) * hass.async_add_executor_job * Fix accessories.run -> async_track_state_change * Fixed media_player test * Flags are now local vars * consistent use of " and ' --- homeassistant/components/homekit/__init__.py | 2 +- .../components/homekit/accessories.py | 15 +++- .../components/homekit/type_covers.py | 22 ++--- homeassistant/components/homekit/type_fans.py | 19 +++-- .../components/homekit/type_lights.py | 6 +- .../components/homekit/type_locks.py | 10 +-- .../homekit/type_security_systems.py | 8 +- .../components/homekit/type_sensors.py | 1 - .../components/homekit/type_switches.py | 24 +++--- .../components/homekit/type_thermostats.py | 82 +++++++++---------- homeassistant/components/homekit/util.py | 2 +- tests/components/homekit/conftest.py | 4 +- tests/components/homekit/test_type_lights.py | 9 +- .../homekit/test_type_media_players.py | 22 ++--- .../homekit/test_type_thermostats.py | 8 +- 15 files changed, 120 insertions(+), 114 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 59c227fb0b8..1c30de918e3 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -78,7 +78,7 @@ async def async_setup(hass, config): homekit = HomeKit(hass, name, port, ip_address, entity_filter, entity_config) - await hass.async_add_job(homekit.setup) + await hass.async_add_executor_job(homekit.setup) if auto_start: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, homekit.start) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 2c41885e311..5baed0294b8 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -35,7 +35,7 @@ def debounce(func): """Handle call_later callback.""" debounce_params = self.debounce.pop(func.__name__, None) if debounce_params: - self.hass.async_add_job(func, self, *debounce_params[1:]) + self.hass.async_add_executor_job(func, self, *debounce_params[1:]) @wraps(func) def wrapper(self, *args): @@ -94,8 +94,15 @@ class HomeAccessory(Accessory): Run inside the HAP-python event loop. """ + self.hass.add_job(self.run_handler) + + async def run_handler(self): + """Handle accessory driver started event. + + Run inside the Home Assistant event loop. + """ state = self.hass.states.get(self.entity_id) - self.hass.add_job(self.update_state_callback, None, None, state) + self.hass.async_add_job(self.update_state_callback, None, None, state) async_track_state_change( self.hass, self.entity_id, self.update_state_callback) @@ -107,8 +114,8 @@ class HomeAccessory(Accessory): if new_state is None: return if self._support_battery_level: - self.hass.async_add_job(self.update_battery, new_state) - self.hass.async_add_job(self.update_state, new_state) + self.hass.async_add_executor_job(self.update_battery, new_state) + self.hass.async_add_executor_job(self.update_state, new_state) def update_battery(self, new_state): """Update battery service if available. diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 787e0e52b1d..840800f730b 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -31,7 +31,7 @@ class GarageDoorOpener(HomeAccessory): def __init__(self, *args): """Initialize a GarageDoorOpener accessory object.""" super().__init__(*args, category=CATEGORY_GARAGE_DOOR_OPENER) - self.flag_target_state = False + self._flag_state = False serv_garage_door = self.add_preload_service(SERV_GARAGE_DOOR_OPENER) self.char_current_state = serv_garage_door.configure_char( @@ -42,7 +42,7 @@ class GarageDoorOpener(HomeAccessory): def set_state(self, value): """Change garage state if call came from HomeKit.""" _LOGGER.debug('%s: Set state to %d', self.entity_id, value) - self.flag_target_state = True + self._flag_state = True params = {ATTR_ENTITY_ID: self.entity_id} if value == 0: @@ -58,9 +58,9 @@ class GarageDoorOpener(HomeAccessory): if hass_state in (STATE_OPEN, STATE_CLOSED): current_state = 0 if hass_state == STATE_OPEN else 1 self.char_current_state.set_value(current_state) - if not self.flag_target_state: + if not self._flag_state: self.char_target_state.set_value(current_state) - self.flag_target_state = False + self._flag_state = False @TYPES.register('WindowCovering') @@ -73,7 +73,7 @@ class WindowCovering(HomeAccessory): def __init__(self, *args): """Initialize a WindowCovering accessory object.""" super().__init__(*args, category=CATEGORY_WINDOW_COVERING) - self.homekit_target = None + self._homekit_target = None serv_cover = self.add_preload_service(SERV_WINDOW_COVERING) self.char_current_position = serv_cover.configure_char( @@ -85,7 +85,7 @@ class WindowCovering(HomeAccessory): def move_cover(self, value): """Move cover to value if call came from HomeKit.""" _LOGGER.debug('%s: Set position to %d', self.entity_id, value) - self.homekit_target = value + self._homekit_target = value params = {ATTR_ENTITY_ID: self.entity_id, ATTR_POSITION: value} self.call_service(DOMAIN, SERVICE_SET_COVER_POSITION, params, value) @@ -95,10 +95,10 @@ class WindowCovering(HomeAccessory): current_position = new_state.attributes.get(ATTR_CURRENT_POSITION) if isinstance(current_position, int): self.char_current_position.set_value(current_position) - if self.homekit_target is None or \ - abs(current_position - self.homekit_target) < 6: + if self._homekit_target is None or \ + abs(current_position - self._homekit_target) < 6: self.char_target_position.set_value(current_position) - self.homekit_target = None + self._homekit_target = None @TYPES.register('WindowCoveringBasic') @@ -114,7 +114,7 @@ class WindowCoveringBasic(HomeAccessory): super().__init__(*args, category=CATEGORY_WINDOW_COVERING) features = self.hass.states.get(self.entity_id) \ .attributes.get(ATTR_SUPPORTED_FEATURES) - self.supports_stop = features & SUPPORT_STOP + self._supports_stop = features & SUPPORT_STOP serv_cover = self.add_preload_service(SERV_WINDOW_COVERING) self.char_current_position = serv_cover.configure_char( @@ -129,7 +129,7 @@ class WindowCoveringBasic(HomeAccessory): """Move cover to value if call came from HomeKit.""" _LOGGER.debug('%s: Set position to %d', self.entity_id, value) - if self.supports_stop: + if self._supports_stop: if value > 70: service, position = (SERVICE_OPEN_COVER, 100) elif value < 30: diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index 49eb525aa51..5a860ed21c8 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -34,24 +34,27 @@ class Fan(HomeAccessory): CHAR_SWING_MODE: False} self._state = 0 - self.chars = [] + chars = [] features = self.hass.states.get(self.entity_id) \ .attributes.get(ATTR_SUPPORTED_FEATURES) if features & SUPPORT_DIRECTION: - self.chars.append(CHAR_ROTATION_DIRECTION) + chars.append(CHAR_ROTATION_DIRECTION) if features & SUPPORT_OSCILLATE: - self.chars.append(CHAR_SWING_MODE) + chars.append(CHAR_SWING_MODE) - serv_fan = self.add_preload_service(SERV_FANV2, self.chars) + serv_fan = self.add_preload_service(SERV_FANV2, chars) self.char_active = serv_fan.configure_char( CHAR_ACTIVE, value=0, setter_callback=self.set_state) - if CHAR_ROTATION_DIRECTION in self.chars: + self.char_direction = None + self.char_swing = None + + if CHAR_ROTATION_DIRECTION in chars: self.char_direction = serv_fan.configure_char( CHAR_ROTATION_DIRECTION, value=0, setter_callback=self.set_direction) - if CHAR_SWING_MODE in self.chars: + if CHAR_SWING_MODE in chars: self.char_swing = serv_fan.configure_char( CHAR_SWING_MODE, value=0, setter_callback=self.set_oscillating) @@ -92,7 +95,7 @@ class Fan(HomeAccessory): self._flag[CHAR_ACTIVE] = False # Handle Direction - if CHAR_ROTATION_DIRECTION in self.chars: + if self.char_direction is not None: direction = new_state.attributes.get(ATTR_DIRECTION) if not self._flag[CHAR_ROTATION_DIRECTION] and \ direction in (DIRECTION_FORWARD, DIRECTION_REVERSE): @@ -102,7 +105,7 @@ class Fan(HomeAccessory): self._flag[CHAR_ROTATION_DIRECTION] = False # Handle Oscillating - if CHAR_SWING_MODE in self.chars: + if self.char_swing is not None: oscillating = new_state.attributes.get(ATTR_OSCILLATING) if not self._flag[CHAR_SWING_MODE] and \ oscillating in (True, False): diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index b8125e24eba..a9007ace35b 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -95,7 +95,7 @@ class Light(HomeAccessory): return params = {ATTR_ENTITY_ID: self.entity_id, ATTR_BRIGHTNESS_PCT: value} self.call_service(DOMAIN, SERVICE_TURN_ON, params, - "brightness at {}%".format(value)) + 'brightness at {}%'.format(value)) def set_color_temperature(self, value): """Set color temperature if call came from HomeKit.""" @@ -103,7 +103,7 @@ class Light(HomeAccessory): self._flag[CHAR_COLOR_TEMPERATURE] = True params = {ATTR_ENTITY_ID: self.entity_id, ATTR_COLOR_TEMP: value} self.call_service(DOMAIN, SERVICE_TURN_ON, params, - "color temperature at {}".format(value)) + 'color temperature at {}'.format(value)) def set_saturation(self, value): """Set saturation if call came from HomeKit.""" @@ -129,7 +129,7 @@ class Light(HomeAccessory): CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True}) params = {ATTR_ENTITY_ID: self.entity_id, ATTR_HS_COLOR: color} self.call_service(DOMAIN, SERVICE_TURN_ON, params, - "set color at {}".format(color)) + 'set color at {}'.format(color)) def update_state(self, new_state): """Update light after state change.""" diff --git a/homeassistant/components/homekit/type_locks.py b/homeassistant/components/homekit/type_locks.py index a3d9c3b6fac..fb211617ecf 100644 --- a/homeassistant/components/homekit/type_locks.py +++ b/homeassistant/components/homekit/type_locks.py @@ -33,7 +33,7 @@ class Lock(HomeAccessory): """Initialize a Lock accessory object.""" super().__init__(*args, category=CATEGORY_DOOR_LOCK) self._code = self.config.get(ATTR_CODE) - self.flag_target_state = False + self._flag_state = False serv_lock_mechanism = self.add_preload_service(SERV_LOCK) self.char_current_state = serv_lock_mechanism.configure_char( @@ -45,8 +45,8 @@ class Lock(HomeAccessory): def set_state(self, value): """Set lock state to value if call came from HomeKit.""" - _LOGGER.debug("%s: Set state to %d", self.entity_id, value) - self.flag_target_state = True + _LOGGER.debug('%s: Set state to %d', self.entity_id, value) + self._flag_state = True hass_value = HOMEKIT_TO_HASS.get(value) service = STATE_TO_SERVICE[hass_value] @@ -67,6 +67,6 @@ class Lock(HomeAccessory): # LockTargetState only supports locked and unlocked if hass_state in (STATE_LOCKED, STATE_UNLOCKED): - if not self.flag_target_state: + if not self._flag_state: self.char_target_state.set_value(current_lock_state) - self.flag_target_state = False + self._flag_state = False diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index 206da3e2889..e210217df2f 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -39,7 +39,7 @@ class SecuritySystem(HomeAccessory): """Initialize a SecuritySystem accessory object.""" super().__init__(*args, category=CATEGORY_ALARM_SYSTEM) self._alarm_code = self.config.get(ATTR_CODE) - self.flag_target_state = False + self._flag_state = False serv_alarm = self.add_preload_service(SERV_SECURITY_SYSTEM) self.char_current_state = serv_alarm.configure_char( @@ -52,7 +52,7 @@ class SecuritySystem(HomeAccessory): """Move security state to value if call came from HomeKit.""" _LOGGER.debug('%s: Set security state to %d', self.entity_id, value) - self.flag_target_state = True + self._flag_state = True hass_value = HOMEKIT_TO_HASS[value] service = STATE_TO_SERVICE[hass_value] @@ -71,7 +71,7 @@ class SecuritySystem(HomeAccessory): self.entity_id, hass_state, current_security_state) # SecuritySystemTargetState does not support triggered - if not self.flag_target_state and \ + if not self._flag_state and \ hass_state != STATE_ALARM_TRIGGERED: self.char_target_state.set_value(current_security_state) - self.flag_target_state = False + self._flag_state = False diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index d2101b1e6f9..09da361ddb8 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -58,7 +58,6 @@ class TemperatureSensor(HomeAccessory): serv_temp = self.add_preload_service(SERV_TEMPERATURE_SENSOR) self.char_temp = serv_temp.configure_char( CHAR_CURRENT_TEMPERATURE, value=0, properties=PROP_CELSIUS) - self.unit = None def update_state(self, new_state): """Update temperature after state changed.""" diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 1090205ff56..59ae17b5d9d 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -36,7 +36,7 @@ class Outlet(HomeAccessory): def __init__(self, *args): """Initialize an Outlet accessory object.""" super().__init__(*args, category=CATEGORY_OUTLET) - self.flag_target_state = False + self._flag_state = False serv_outlet = self.add_preload_service(SERV_OUTLET) self.char_on = serv_outlet.configure_char( @@ -48,7 +48,7 @@ class Outlet(HomeAccessory): """Move switch state to value if call came from HomeKit.""" _LOGGER.debug('%s: Set switch state to %s', self.entity_id, value) - self.flag_target_state = True + self._flag_state = True params = {ATTR_ENTITY_ID: self.entity_id} service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF self.call_service(DOMAIN, service, params) @@ -56,11 +56,11 @@ class Outlet(HomeAccessory): def update_state(self, new_state): """Update switch state after state changed.""" current_state = (new_state.state == STATE_ON) - if not self.flag_target_state: + if not self._flag_state: _LOGGER.debug('%s: Set current state to %s', self.entity_id, current_state) self.char_on.set_value(current_state) - self.flag_target_state = False + self._flag_state = False @TYPES.register('Switch') @@ -71,7 +71,7 @@ class Switch(HomeAccessory): """Initialize a Switch accessory object.""" super().__init__(*args, category=CATEGORY_SWITCH) self._domain = split_entity_id(self.entity_id)[0] - self.flag_target_state = False + self._flag_state = False serv_switch = self.add_preload_service(SERV_SWITCH) self.char_on = serv_switch.configure_char( @@ -81,7 +81,7 @@ class Switch(HomeAccessory): """Move switch state to value if call came from HomeKit.""" _LOGGER.debug('%s: Set switch state to %s', self.entity_id, value) - self.flag_target_state = True + self._flag_state = True params = {ATTR_ENTITY_ID: self.entity_id} service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF self.call_service(self._domain, service, params) @@ -89,11 +89,11 @@ class Switch(HomeAccessory): def update_state(self, new_state): """Update switch state after state changed.""" current_state = (new_state.state == STATE_ON) - if not self.flag_target_state: + if not self._flag_state: _LOGGER.debug('%s: Set current state to %s', self.entity_id, current_state) self.char_on.set_value(current_state) - self.flag_target_state = False + self._flag_state = False @TYPES.register('Valve') @@ -103,7 +103,7 @@ class Valve(HomeAccessory): def __init__(self, *args): """Initialize a Valve accessory object.""" super().__init__(*args) - self.flag_target_state = False + self._flag_state = False valve_type = self.config[CONF_TYPE] self.category = VALVE_TYPE[valve_type][0] @@ -119,7 +119,7 @@ class Valve(HomeAccessory): """Move value state to value if call came from HomeKit.""" _LOGGER.debug('%s: Set switch state to %s', self.entity_id, value) - self.flag_target_state = True + self._flag_state = True self.char_in_use.set_value(value) params = {ATTR_ENTITY_ID: self.entity_id} service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF @@ -128,9 +128,9 @@ class Valve(HomeAccessory): def update_state(self, new_state): """Update switch state after state changed.""" current_state = (new_state.state == STATE_ON) - if not self.flag_target_state: + if not self._flag_state: _LOGGER.debug('%s: Set current state to %s', self.entity_id, current_state) self.char_active.set_value(current_state) self.char_in_use.set_value(current_state) - self.flag_target_state = False + self._flag_state = False diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 2513ee50981..49da6db6125 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -51,11 +51,11 @@ class Thermostat(HomeAccessory): """Initialize a Thermostat accessory object.""" super().__init__(*args, category=CATEGORY_THERMOSTAT) self._unit = self.hass.config.units.temperature_unit + self._flag_heat_cool = False + self._flag_temperature = False + self._flag_coolingthresh = False + self._flag_heatingthresh = False self.support_power_state = False - self.heat_cool_flag_target_state = False - self.temperature_flag_target_state = False - self.coolingthresh_flag_target_state = False - self.heatingthresh_flag_target_state = False min_temp, max_temp = self.get_temperature_range() # Add additional characteristics if auto mode is supported @@ -122,28 +122,27 @@ class Thermostat(HomeAccessory): def set_heat_cool(self, value): """Change operation mode to value if call came from HomeKit.""" - if value in HC_HOMEKIT_TO_HASS: - _LOGGER.debug('%s: Set heat-cool to %d', self.entity_id, value) - self.heat_cool_flag_target_state = True - hass_value = HC_HOMEKIT_TO_HASS[value] - if self.support_power_state is True: - params = {ATTR_ENTITY_ID: self.entity_id} - if hass_value == STATE_OFF: - self.call_service(DOMAIN_CLIMATE, SERVICE_TURN_OFF, params) - return - self.call_service(DOMAIN_CLIMATE, SERVICE_TURN_ON, params) - params = {ATTR_ENTITY_ID: self.entity_id, - ATTR_OPERATION_MODE: hass_value} - self.call_service( - DOMAIN_CLIMATE, SERVICE_SET_OPERATION_MODE_THERMOSTAT, - params, hass_value) + _LOGGER.debug('%s: Set heat-cool to %d', self.entity_id, value) + self._flag_heat_cool = True + hass_value = HC_HOMEKIT_TO_HASS[value] + if self.support_power_state is True: + params = {ATTR_ENTITY_ID: self.entity_id} + if hass_value == STATE_OFF: + self.call_service(DOMAIN_CLIMATE, SERVICE_TURN_OFF, params) + return + self.call_service(DOMAIN_CLIMATE, SERVICE_TURN_ON, params) + params = {ATTR_ENTITY_ID: self.entity_id, + ATTR_OPERATION_MODE: hass_value} + self.call_service( + DOMAIN_CLIMATE, SERVICE_SET_OPERATION_MODE_THERMOSTAT, + params, hass_value) @debounce def set_cooling_threshold(self, value): """Set cooling threshold temp to value if call came from HomeKit.""" _LOGGER.debug('%s: Set cooling threshold temperature to %.2f°C', self.entity_id, value) - self.coolingthresh_flag_target_state = True + self._flag_coolingthresh = True low = self.char_heating_thresh_temp.value temperature = temperature_to_states(value, self._unit) params = { @@ -159,7 +158,7 @@ class Thermostat(HomeAccessory): """Set heating threshold temp to value if call came from HomeKit.""" _LOGGER.debug('%s: Set heating threshold temperature to %.2f°C', self.entity_id, value) - self.heatingthresh_flag_target_state = True + self._flag_heatingthresh = True high = self.char_cooling_thresh_temp.value temperature = temperature_to_states(value, self._unit) params = { @@ -175,14 +174,14 @@ class Thermostat(HomeAccessory): """Set target temperature to value if call came from HomeKit.""" _LOGGER.debug('%s: Set target temperature to %.2f°C', self.entity_id, value) - self.temperature_flag_target_state = True + self._flag_temperature = True temperature = temperature_to_states(value, self._unit) params = { ATTR_ENTITY_ID: self.entity_id, ATTR_TEMPERATURE: temperature} self.call_service( DOMAIN_CLIMATE, SERVICE_SET_TEMPERATURE_THERMOSTAT, - params, 'target {}{}'.format(temperature, self._unit)) + params, '{}{}'.format(temperature, self._unit)) def update_state(self, new_state): """Update thermostat state after state changed.""" @@ -196,9 +195,9 @@ class Thermostat(HomeAccessory): target_temp = new_state.attributes.get(ATTR_TEMPERATURE) if isinstance(target_temp, (int, float)): target_temp = temperature_to_homekit(target_temp, self._unit) - if not self.temperature_flag_target_state: + if not self._flag_temperature: self.char_target_temp.set_value(target_temp) - self.temperature_flag_target_state = False + self._flag_temperature = False # Update cooling threshold temperature if characteristic exists if self.char_cooling_thresh_temp: @@ -206,9 +205,9 @@ class Thermostat(HomeAccessory): if isinstance(cooling_thresh, (int, float)): cooling_thresh = temperature_to_homekit(cooling_thresh, self._unit) - if not self.coolingthresh_flag_target_state: + if not self._flag_coolingthresh: self.char_cooling_thresh_temp.set_value(cooling_thresh) - self.coolingthresh_flag_target_state = False + self._flag_coolingthresh = False # Update heating threshold temperature if characteristic exists if self.char_heating_thresh_temp: @@ -216,9 +215,9 @@ class Thermostat(HomeAccessory): if isinstance(heating_thresh, (int, float)): heating_thresh = temperature_to_homekit(heating_thresh, self._unit) - if not self.heatingthresh_flag_target_state: + if not self._flag_heatingthresh: self.char_heating_thresh_temp.set_value(heating_thresh) - self.heatingthresh_flag_target_state = False + self._flag_heatingthresh = False # Update display units if self._unit and self._unit in UNIT_HASS_TO_HOMEKIT: @@ -227,13 +226,12 @@ class Thermostat(HomeAccessory): # Update target operation mode operation_mode = new_state.attributes.get(ATTR_OPERATION_MODE) if self.support_power_state is True and new_state.state == STATE_OFF: - self.char_target_heat_cool.set_value( - HC_HASS_TO_HOMEKIT[STATE_OFF]) + self.char_target_heat_cool.set_value(0) # Off elif operation_mode and operation_mode in HC_HASS_TO_HOMEKIT: - if not self.heat_cool_flag_target_state: + if not self._flag_heat_cool: self.char_target_heat_cool.set_value( HC_HASS_TO_HOMEKIT[operation_mode]) - self.heat_cool_flag_target_state = False + self._flag_heat_cool = False # Set current operation mode based on temperatures and target mode if self.support_power_state is True and new_state.state == STATE_OFF: @@ -286,8 +284,8 @@ class WaterHeater(HomeAccessory): """Initialize a WaterHeater accessory object.""" super().__init__(*args, category=CATEGORY_THERMOSTAT) self._unit = self.hass.config.units.temperature_unit - self.flag_heat_cool = False - self.flag_temperature = False + self._flag_heat_cool = False + self._flag_temperature = False min_temp, max_temp = self.get_temperature_range() serv_thermostat = self.add_preload_service(SERV_THERMOSTAT) @@ -326,7 +324,7 @@ class WaterHeater(HomeAccessory): def set_heat_cool(self, value): """Change operation mode to value if call came from HomeKit.""" _LOGGER.debug('%s: Set heat-cool to %d', self.entity_id, value) - self.flag_heat_cool = True + self._flag_heat_cool = True hass_value = HC_HOMEKIT_TO_HASS[value] if hass_value != STATE_HEAT: self.char_target_heat_cool.set_value(1) # Heat @@ -336,14 +334,14 @@ class WaterHeater(HomeAccessory): """Set target temperature to value if call came from HomeKit.""" _LOGGER.debug('%s: Set target temperature to %.2f°C', self.entity_id, value) - self.flag_temperature = True + self._flag_temperature = True temperature = temperature_to_states(value, self._unit) params = { ATTR_ENTITY_ID: self.entity_id, ATTR_TEMPERATURE: temperature} self.call_service( DOMAIN_WATER_HEATER, SERVICE_SET_TEMPERATURE_WATER_HEATER, - params, 'target {}{}'.format(temperature, self._unit)) + params, '{}{}'.format(temperature, self._unit)) def update_state(self, new_state): """Update water_heater state after state change.""" @@ -352,9 +350,9 @@ class WaterHeater(HomeAccessory): if isinstance(temperature, (int, float)): temperature = temperature_to_homekit(temperature, self._unit) self.char_current_temp.set_value(temperature) - if not self.flag_temperature: + if not self._flag_temperature: self.char_target_temp.set_value(temperature) - self.flag_temperature = False + self._flag_temperature = False # Update display units if self._unit and self._unit in UNIT_HASS_TO_HOMEKIT: @@ -362,6 +360,6 @@ class WaterHeater(HomeAccessory): # Update target operation mode operation_mode = new_state.attributes.get(ATTR_OPERATION_MODE) - if operation_mode and not self.flag_heat_cool: + if operation_mode and not self._flag_heat_cool: self.char_target_heat_cool.set_value(1) # Heat - self.flag_heat_cool = False + self._flag_heat_cool = False diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 4dd7396cf8d..43ae4df3b50 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -104,7 +104,7 @@ def validate_media_player_features(state, feature_list): error_list.append(feature) if error_list: - _LOGGER.error("%s does not support features: %s", + _LOGGER.error('%s does not support features: %s', state.entity_id, error_list) return False return True diff --git a/tests/components/homekit/conftest.py b/tests/components/homekit/conftest.py index 326845cf74f..95829435d0e 100644 --- a/tests/components/homekit/conftest.py +++ b/tests/components/homekit/conftest.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest from homeassistant.components.homekit.const import EVENT_HOMEKIT_CHANGED -from homeassistant.core import callback +from homeassistant.core import callback as ha_callback from pyhap.accessory_driver import AccessoryDriver @@ -25,5 +25,5 @@ def events(hass): events = [] hass.bus.async_listen( EVENT_HOMEKIT_CHANGED, - callback(lambda e: events.append(e))) + ha_callback(lambda e: events.append(e))) yield events diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 540d9a73f48..c540952017b 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -80,7 +80,6 @@ async def test_light_basic(hass, hk_driver, cls, events): async def test_light_brightness(hass, hk_driver, cls, events): """Test light with brightness.""" entity_id = 'light.demo' - event_value = "brightness at " hass.states.async_set(entity_id, STATE_ON, { ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS: 255}) @@ -108,7 +107,7 @@ async def test_light_brightness(hass, hk_driver, cls, events): assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id assert call_turn_on[0].data[ATTR_BRIGHTNESS_PCT] == 20 assert len(events) == 1 - assert events[-1].data[ATTR_VALUE] == "{}20%".format(event_value) + assert events[-1].data[ATTR_VALUE] == 'brightness at 20%' await hass.async_add_job(acc.char_on.client_update_value, 1) await hass.async_add_job(acc.char_brightness.client_update_value, 40) @@ -117,7 +116,7 @@ async def test_light_brightness(hass, hk_driver, cls, events): assert call_turn_on[1].data[ATTR_ENTITY_ID] == entity_id assert call_turn_on[1].data[ATTR_BRIGHTNESS_PCT] == 40 assert len(events) == 2 - assert events[-1].data[ATTR_VALUE] == "{}40%".format(event_value) + assert events[-1].data[ATTR_VALUE] == 'brightness at 40%' await hass.async_add_job(acc.char_on.client_update_value, 1) await hass.async_add_job(acc.char_brightness.client_update_value, 0) @@ -154,7 +153,7 @@ async def test_light_color_temperature(hass, hk_driver, cls, events): assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id assert call_turn_on[0].data[ATTR_COLOR_TEMP] == 250 assert len(events) == 1 - assert events[-1].data[ATTR_VALUE] == "color temperature at 250" + assert events[-1].data[ATTR_VALUE] == 'color temperature at 250' async def test_light_rgb_color(hass, hk_driver, cls, events): @@ -185,4 +184,4 @@ async def test_light_rgb_color(hass, hk_driver, cls, events): assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id assert call_turn_on[0].data[ATTR_HS_COLOR] == (145, 75) assert len(events) == 1 - assert events[-1].data[ATTR_VALUE] == "set color at (145, 75)" + assert events[-1].data[ATTR_VALUE] == 'set color at (145, 75)' diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py index 299570a6923..6b23b3cc58e 100644 --- a/tests/components/homekit/test_type_media_players.py +++ b/tests/components/homekit/test_type_media_players.py @@ -29,32 +29,32 @@ async def test_media_player_set_state(hass, hk_driver, events): assert acc.aid == 2 assert acc.category == 8 # Switch - assert acc.chars[FEATURE_ON_OFF].value == 0 - assert acc.chars[FEATURE_PLAY_PAUSE].value == 0 - assert acc.chars[FEATURE_PLAY_STOP].value == 0 - assert acc.chars[FEATURE_TOGGLE_MUTE].value == 0 + assert acc.chars[FEATURE_ON_OFF].value is False + assert acc.chars[FEATURE_PLAY_PAUSE].value is False + assert acc.chars[FEATURE_PLAY_STOP].value is False + assert acc.chars[FEATURE_TOGGLE_MUTE].value is False hass.states.async_set(entity_id, STATE_ON, {ATTR_MEDIA_VOLUME_MUTED: True}) await hass.async_block_till_done() - assert acc.chars[FEATURE_ON_OFF].value == 1 - assert acc.chars[FEATURE_TOGGLE_MUTE].value == 1 + assert acc.chars[FEATURE_ON_OFF].value is True + assert acc.chars[FEATURE_TOGGLE_MUTE].value is True hass.states.async_set(entity_id, STATE_OFF) await hass.async_block_till_done() - assert acc.chars[FEATURE_ON_OFF].value == 0 + assert acc.chars[FEATURE_ON_OFF].value is False hass.states.async_set(entity_id, STATE_PLAYING) await hass.async_block_till_done() - assert acc.chars[FEATURE_PLAY_PAUSE].value == 1 - assert acc.chars[FEATURE_PLAY_STOP].value == 1 + assert acc.chars[FEATURE_PLAY_PAUSE].value is True + assert acc.chars[FEATURE_PLAY_STOP].value is True hass.states.async_set(entity_id, STATE_PAUSED) await hass.async_block_till_done() - assert acc.chars[FEATURE_PLAY_PAUSE].value == 0 + assert acc.chars[FEATURE_PLAY_PAUSE].value is False hass.states.async_set(entity_id, STATE_IDLE) await hass.async_block_till_done() - assert acc.chars[FEATURE_PLAY_STOP].value == 0 + assert acc.chars[FEATURE_PLAY_STOP].value is False # Set from HomeKit call_turn_on = async_mock_service(hass, DOMAIN, 'turn_on') diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 73b27ca851c..795cb5db7d2 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -163,7 +163,7 @@ async def test_thermostat(hass, hk_driver, cls, events): assert call_set_temperature[0].data[ATTR_TEMPERATURE] == 19.0 assert acc.char_target_temp.value == 19.0 assert len(events) == 1 - assert events[-1].data[ATTR_VALUE] == 'target 19.0°C' + assert events[-1].data[ATTR_VALUE] == '19.0°C' await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 1) await hass.async_block_till_done() @@ -379,7 +379,7 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls, events): assert call_set_temperature[2].data[ATTR_ENTITY_ID] == entity_id assert call_set_temperature[2].data[ATTR_TEMPERATURE] == 75.2 assert len(events) == 3 - assert events[-1].data[ATTR_VALUE] == 'target 75.2°F' + assert events[-1].data[ATTR_VALUE] == '75.2°F' async def test_thermostat_get_temperature_range(hass, hk_driver, cls): @@ -453,7 +453,7 @@ async def test_water_heater(hass, hk_driver, cls, events): assert call_set_temperature[0].data[ATTR_TEMPERATURE] == 52.0 assert acc.char_target_temp.value == 52.0 assert len(events) == 1 - assert events[-1].data[ATTR_VALUE] == 'target 52.0°C' + assert events[-1].data[ATTR_VALUE] == '52.0°C' await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 0) await hass.async_block_till_done() @@ -499,7 +499,7 @@ async def test_water_heater_fahrenheit(hass, hk_driver, cls, events): assert call_set_temperature[0].data[ATTR_TEMPERATURE] == 140.0 assert acc.char_target_temp.value == 60.0 assert len(events) == 1 - assert events[-1].data[ATTR_VALUE] == 'target 140.0°F' + assert events[-1].data[ATTR_VALUE] == '140.0°F' async def test_water_heater_get_temperature_range(hass, hk_driver, cls): From ab826b8fe2aa98518e6b8a5d45b10ff763280407 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Sat, 20 Oct 2018 05:28:30 -0700 Subject: [PATCH 223/265] Use cached robot serial for Neato update (#17633) * Use cached robot serial for neato update * Hound --- homeassistant/components/vacuum/neato.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/vacuum/neato.py b/homeassistant/components/vacuum/neato.py index 53f83a1de1d..83a4ad7c58d 100644 --- a/homeassistant/components/vacuum/neato.py +++ b/homeassistant/components/vacuum/neato.py @@ -108,27 +108,28 @@ class NeatoConnectedVacuum(StateVacuumDevice): self._clean_state = STATE_ERROR self._status_state = ERRORS.get(self._state['error']) - if not self._mapdata.get(self.robot.serial, {}).get('maps', []): + if not self._mapdata.get(self._robot_serial, {}).get('maps', []): return self.clean_time_start = ( - (self._mapdata[self.robot.serial]['maps'][0]['start_at'] + (self._mapdata[self._robot_serial]['maps'][0]['start_at'] .strip('Z')) .replace('T', ' ')) self.clean_time_stop = ( - (self._mapdata[self.robot.serial]['maps'][0]['end_at'].strip('Z')) + (self._mapdata[self._robot_serial]['maps'][0]['end_at'].strip('Z')) .replace('T', ' ')) self.clean_area = ( - self._mapdata[self.robot.serial]['maps'][0]['cleaned_area']) + self._mapdata[self._robot_serial]['maps'][0]['cleaned_area']) self.clean_suspension_charge_count = ( - self._mapdata[self.robot.serial]['maps'][0] + self._mapdata[self._robot_serial]['maps'][0] ['suspended_cleaning_charging_count']) self.clean_suspension_time = ( - self._mapdata[self.robot.serial]['maps'][0] + self._mapdata[self._robot_serial]['maps'][0] ['time_in_suspended_cleaning']) self.clean_battery_start = ( - self._mapdata[self.robot.serial]['maps'][0]['run_charge_at_start']) + self._mapdata[self._robot_serial]['maps'][0]['run_charge_at_start'] + ) self.clean_battery_end = ( - self._mapdata[self.robot.serial]['maps'][0]['run_charge_at_end']) + self._mapdata[self._robot_serial]['maps'][0]['run_charge_at_end']) self._battery_level = self._state['details']['charge'] From 26b7c2de7e20ec71860eef6d777f719b7c240a10 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 20 Oct 2018 15:13:23 +0200 Subject: [PATCH 224/265] deCONZ - Add support for Xiaomi window covers (#17337) Add support for Xiaomi window covers --- homeassistant/components/cover/deconz.py | 52 +++++++++++++++++++++--- homeassistant/components/deconz/const.py | 4 +- tests/components/cover/test_deconz.py | 12 +++++- 3 files changed, 59 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/cover/deconz.py b/homeassistant/components/cover/deconz.py index 9fe65596336..89b29aa10a5 100644 --- a/homeassistant/components/cover/deconz.py +++ b/homeassistant/components/cover/deconz.py @@ -5,10 +5,10 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/cover.deconz/ """ from homeassistant.components.deconz.const import ( - COVER_TYPES, DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB, - DECONZ_DOMAIN) + COVER_TYPES, DAMPERS, DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, + DATA_DECONZ_UNSUB, DECONZ_DOMAIN, WINDOW_COVERS) from homeassistant.components.cover import ( - ATTR_POSITION, CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN, + ATTR_POSITION, CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_STOP, SUPPORT_SET_POSITION) from homeassistant.core import callback from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE @@ -16,6 +16,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect DEPENDENCIES = ['deconz'] +ZIGBEE_SPEC = ['lumi.curtain'] + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -34,7 +36,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities = [] for light in lights: if light.type in COVER_TYPES: - entities.append(DeconzCover(light)) + if light.modelid in ZIGBEE_SPEC: + entities.append(DeconzCoverZigbeeSpec(light)) + else: + entities.append(DeconzCover(light)) async_add_entities(entities, True) hass.data[DATA_DECONZ_UNSUB].append( @@ -49,7 +54,10 @@ class DeconzCover(CoverDevice): def __init__(self, cover): """Set up cover and add update callback to get data from websocket.""" self._cover = cover - self._features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION + self._features = SUPPORT_OPEN + self._features |= SUPPORT_CLOSE + self._features |= SUPPORT_STOP + self._features |= SUPPORT_SET_POSITION async def async_added_to_hass(self): """Subscribe to covers events.""" @@ -91,7 +99,11 @@ class DeconzCover(CoverDevice): @property def device_class(self): """Return the class of the cover.""" - return 'damper' + if self._cover.type in DAMPERS: + return 'damper' + if self._cover.type in WINDOW_COVERS: + return 'window' + return None @property def supported_features(self): @@ -127,6 +139,11 @@ class DeconzCover(CoverDevice): data = {ATTR_POSITION: 0} await self.async_set_cover_position(**data) + async def async_stop_cover(self, **kwargs): + """Stop cover.""" + data = {'bri_inc': 0} + await self._cover.async_set_state(data) + @property def device_info(self): """Return a device description for device registry.""" @@ -144,3 +161,26 @@ class DeconzCover(CoverDevice): 'sw_version': self._cover.swversion, 'via_hub': (DECONZ_DOMAIN, bridgeid), } + + +class DeconzCoverZigbeeSpec(DeconzCover): + """Zigbee spec is the inverse of how deCONZ normally reports attributes.""" + + @property + def current_cover_position(self): + """Return the current position of the cover.""" + return 100 - int(self._cover.brightness / 255 * 100) + + @property + def is_closed(self): + """Return if the cover is closed.""" + return self._cover.state + + async def async_set_cover_position(self, **kwargs): + """Move the cover to a specific position.""" + position = kwargs[ATTR_POSITION] + data = {'on': False} + if position < 100: + data['on'] = True + data['bri'] = 255 - int(position / 100 * 255) + await self._cover.async_set_state(data) diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index 617d231f92e..5462b5b61b9 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -16,7 +16,9 @@ CONF_ALLOW_DECONZ_GROUPS = 'allow_deconz_groups' ATTR_DARK = 'dark' ATTR_ON = 'on' -COVER_TYPES = ["Level controllable output"] +DAMPERS = ["Level controllable output"] +WINDOW_COVERS = ["Window covering device"] +COVER_TYPES = DAMPERS + WINDOW_COVERS POWER_PLUGS = ["On/Off plug-in unit", "Smart plug"] SIRENS = ["Warning device"] diff --git a/tests/components/cover/test_deconz.py b/tests/components/cover/test_deconz.py index 60de9cffdc1..e9c630823bd 100644 --- a/tests/components/cover/test_deconz.py +++ b/tests/components/cover/test_deconz.py @@ -13,7 +13,15 @@ SUPPORTED_COVERS = { "id": "Cover 1 id", "name": "Cover 1 name", "type": "Level controllable output", - "state": {} + "state": {}, + "modelid": "Not zigbee spec" + }, + "2": { + "id": "Cover 2 id", + "name": "Cover 2 name", + "type": "Window covering device", + "state": {}, + "modelid": "lumi.curtain" } } @@ -62,7 +70,7 @@ async def test_cover(hass): await setup_bridge(hass, {"lights": SUPPORTED_COVERS}) assert "cover.cover_1_name" in hass.data[deconz.DATA_DECONZ_ID] assert len(SUPPORTED_COVERS) == len(COVER_TYPES) - assert len(hass.states.async_all()) == 2 + assert len(hass.states.async_all()) == 3 async def test_add_new_cover(hass): From e980d1b9fee95c6688f15d24f94ec90699b277c6 Mon Sep 17 00:00:00 2001 From: Julien Debaru Date: Sat, 20 Oct 2018 15:30:10 +0200 Subject: [PATCH 225/265] Fix linky sensor login error (#17110) * Fix linky sensor login error * Make platform fail-safe Adding following enhancements: * Make sure the platform loads correctly by making the first API request in setup_platform. * Close the session after each API call. * Use timeout parameter everywhere. * Fix Hound CI error: line too long. * Update pylinky library * Remove LinkyClient from update() --- homeassistant/components/sensor/linky.py | 5 +++-- requirements_all.txt | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/linky.py b/homeassistant/components/sensor/linky.py index 83a6d793085..316da010ae4 100644 --- a/homeassistant/components/sensor/linky.py +++ b/homeassistant/components/sensor/linky.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pylinky==0.1.6'] +REQUIREMENTS = ['pylinky==0.1.8'] _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(minutes=10) @@ -37,11 +37,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): from pylinky.client import LinkyClient, PyLinkyError client = LinkyClient(username, password, None, timeout) - try: client.fetch_data() except PyLinkyError as exp: _LOGGER.error(exp) + client.close_session() return devices = [LinkySensor('Linky', client)] @@ -80,6 +80,7 @@ class LinkySensor(Entity): self._client.fetch_data() except PyLinkyError as exp: _LOGGER.error(exp) + self._client.close_session() return _LOGGER.debug(json.dumps(self._client.get_data(), indent=2)) diff --git a/requirements_all.txt b/requirements_all.txt index 6c20f961228..bf8011cbb49 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -963,7 +963,7 @@ pylgnetcast-homeassistant==0.2.0.dev0 pylgtv==0.1.9 # homeassistant.components.sensor.linky -pylinky==0.1.6 +pylinky==0.1.8 # homeassistant.components.litejet pylitejet==0.1 From 2e973c75729b6aae2a9364a65cfd22bba6ed2918 Mon Sep 17 00:00:00 2001 From: Bob Clough Date: Sat, 20 Oct 2018 17:37:25 +0100 Subject: [PATCH 226/265] Fix mqtt light brightness slider (#17075) * Enable brightness slider for RGB If we are using RGB with no brightness topic, the brighness slider should still be visible, as we can scale the RGB amount to give us the brightness. * Output RGB scaled by brightness If we are outputting to an RGB device, but do not have a dedicated brightness topic set, when the brightness slider is changed, we should output the current colour's HS, with the V coming from the brightness slider. * Brightness from RGB when we're not using a brightness topic When we aren't using a brightness topic, set the brightness slider based on the received value from an RGB -> HSV conversion. * Test for new brightness state scaled by RGB This adds a test to make sure the brightness stored in the state is being computed correctly from the RGB value when a dedicated brightness topic is not set. * Changes from review Fixes formatting of supported features flags, and checks HS colour hasn't been set when operating in RGB-only mode * Set optimistic brightness correctly in rgb mode When we're using rgb mode to set the brightness, we want to set optimistic brightness if: we are running in optimistic mode OR the brightness state topic isn't set and we have a brightness command topic OR the rgb state topic isn't set and we don't have a brightness command topic * Add test for turn_on in RGB brightness mode --- homeassistant/components/light/mqtt.py | 34 ++++++++++++- tests/components/light/test_mqtt.py | 68 ++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/mqtt.py b/homeassistant/components/light/mqtt.py index 1c01278197d..92030c8617a 100644 --- a/homeassistant/components/light/mqtt.py +++ b/homeassistant/components/light/mqtt.py @@ -211,7 +211,11 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, Light): self._optimistic_rgb = \ optimistic or topic[CONF_RGB_STATE_TOPIC] is None self._optimistic_brightness = ( - optimistic or topic[CONF_BRIGHTNESS_STATE_TOPIC] is None) + optimistic or + (topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None and + topic[CONF_BRIGHTNESS_STATE_TOPIC] is None) or + (topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is None and + topic[CONF_RGB_STATE_TOPIC] is None)) self._optimistic_color_temp = ( optimistic or topic[CONF_COLOR_TEMP_STATE_TOPIC] is None) self._optimistic_effect = ( @@ -233,7 +237,8 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, Light): self._white_value = None self._supported_features = 0 self._supported_features |= ( - topic[CONF_RGB_COMMAND_TOPIC] is not None and SUPPORT_COLOR) + topic[CONF_RGB_COMMAND_TOPIC] is not None and + (SUPPORT_COLOR | SUPPORT_BRIGHTNESS)) self._supported_features |= ( topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None and SUPPORT_BRIGHTNESS) @@ -325,6 +330,10 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, Light): rgb = [int(val) for val in payload.split(',')] self._hs = color_util.color_RGB_to_hs(*rgb) + if self._topic[CONF_BRIGHTNESS_STATE_TOPIC] is None: + percent_bright = \ + float(color_util.color_RGB_to_hsv(*rgb)[2]) / 100.0 + self._brightness = int(percent_bright * 255) self.async_schedule_update_ha_state() if self._topic[CONF_RGB_STATE_TOPIC] is not None: @@ -616,6 +625,27 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, Light): if self._optimistic_brightness: self._brightness = kwargs[ATTR_BRIGHTNESS] should_update = True + elif ATTR_BRIGHTNESS in kwargs and ATTR_HS_COLOR not in kwargs and\ + self._topic[CONF_RGB_COMMAND_TOPIC] is not None: + rgb = color_util.color_hsv_to_RGB( + self._hs[0], self._hs[1], kwargs[ATTR_BRIGHTNESS] / 255 * 100) + tpl = self._templates[CONF_RGB_COMMAND_TEMPLATE] + if tpl: + rgb_color_str = tpl.async_render({ + 'red': rgb[0], + 'green': rgb[1], + 'blue': rgb[2], + }) + else: + rgb_color_str = '{},{},{}'.format(*rgb) + + mqtt.async_publish( + self.hass, self._topic[CONF_RGB_COMMAND_TOPIC], + rgb_color_str, self._qos, self._retain) + + if self._optimistic_brightness: + self._brightness = kwargs[ATTR_BRIGHTNESS] + should_update = True if ATTR_COLOR_TEMP in kwargs and \ self._topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None: diff --git a/tests/components/light/test_mqtt.py b/tests/components/light/test_mqtt.py index 2b23be101c7..5a768820e18 100644 --- a/tests/components/light/test_mqtt.py +++ b/tests/components/light/test_mqtt.py @@ -393,6 +393,41 @@ class TestLightMQTT(unittest.TestCase): self.assertEqual(255, light_state.attributes['brightness']) + def test_brightness_from_rgb_controlling_scale(self): + """Test the brightness controlling scale.""" + with assert_setup_component(1, light.DOMAIN): + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test_scale_rgb/status', + 'command_topic': 'test_scale_rgb/set', + 'rgb_state_topic': 'test_scale_rgb/rgb/status', + 'rgb_command_topic': 'test_scale_rgb/rgb/set', + 'qos': 0, + 'payload_on': 'on', + 'payload_off': 'off' + } + }) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + self.assertIsNone(state.attributes.get('brightness')) + self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) + + fire_mqtt_message(self.hass, 'test_scale_rgb/status', 'on') + fire_mqtt_message(self.hass, 'test_scale_rgb/rgb/status', '255,0,0') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(255, state.attributes.get('brightness')) + + fire_mqtt_message(self.hass, 'test_scale_rgb/rgb/status', '127,0,0') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(127, state.attributes.get('brightness')) + def test_white_value_controlling_scale(self): """Test the white_value controlling scale.""" with assert_setup_component(1, light.DOMAIN): @@ -894,6 +929,39 @@ class TestLightMQTT(unittest.TestCase): mock.call('test_light/bright', 50, 0, False) ], any_order=True) + def test_on_command_rgb(self): + """Test on command in RGB brightness mode.""" + config = {light.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'command_topic': 'test_light/set', + 'rgb_command_topic': "test_light/rgb", + }} + + with assert_setup_component(1, light.DOMAIN): + assert setup_component(self.hass, light.DOMAIN, config) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + + common.turn_on(self.hass, 'light.test', brightness=127) + self.hass.block_till_done() + + # Should get the following MQTT messages. + # test_light/rgb: '127,127,127' + # test_light/set: 'ON' + self.mock_publish.async_publish.assert_has_calls([ + mock.call('test_light/rgb', '127,127,127', 0, False), + mock.call('test_light/set', 'ON', 0, False), + ], any_order=True) + self.mock_publish.async_publish.reset_mock() + + common.turn_off(self.hass, 'light.test') + self.hass.block_till_done() + + self.mock_publish.async_publish.assert_called_once_with( + 'test_light/set', 'OFF', 0, False) + def test_default_availability_payload(self): """Test availability by default payload with defined topic.""" self.assertTrue(setup_component(self.hass, light.DOMAIN, { From 237ac080763ff124edeb0e180efcbc93dc330bf6 Mon Sep 17 00:00:00 2001 From: mvn23 Date: Sat, 20 Oct 2018 18:51:01 +0200 Subject: [PATCH 227/265] Add opentherm_gw binary sensor support (#17625) * Add OpenTherm Gateway binary sensor support. * opentherm_gw binary_sensor platform does not need polling. --- .../components/binary_sensor/opentherm_gw.py | 145 ++++++++++++++++++ homeassistant/components/opentherm_gw.py | 49 +++++- 2 files changed, 191 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/binary_sensor/opentherm_gw.py diff --git a/homeassistant/components/binary_sensor/opentherm_gw.py b/homeassistant/components/binary_sensor/opentherm_gw.py new file mode 100644 index 00000000000..8c5ff8c44d1 --- /dev/null +++ b/homeassistant/components/binary_sensor/opentherm_gw.py @@ -0,0 +1,145 @@ +""" +Support for OpenTherm Gateway binary sensors. + +For more details about this platform, please refer to the documentation at +http://home-assistant.io/components/binary_sensor.opentherm_gw/ +""" +import logging + +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, ENTITY_ID_FORMAT) +from homeassistant.components.opentherm_gw import ( + DATA_GW_VARS, DATA_OPENTHERM_GW, SIGNAL_OPENTHERM_GW_UPDATE) +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import async_generate_entity_id + +DEVICE_CLASS_COLD = 'cold' +DEVICE_CLASS_HEAT = 'heat' +DEVICE_CLASS_PROBLEM = 'problem' + +DEPENDENCIES = ['opentherm_gw'] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the OpenTherm Gateway binary sensors.""" + if discovery_info is None: + return + gw_vars = hass.data[DATA_OPENTHERM_GW][DATA_GW_VARS] + sensor_info = { + # [device_class, friendly_name] + gw_vars.DATA_MASTER_CH_ENABLED: [ + None, "Thermostat Central Heating Enabled"], + gw_vars.DATA_MASTER_DHW_ENABLED: [ + None, "Thermostat Hot Water Enabled"], + gw_vars.DATA_MASTER_COOLING_ENABLED: [ + None, "Thermostat Cooling Enabled"], + gw_vars.DATA_MASTER_OTC_ENABLED: [ + None, "Thermostat Outside Temperature Correction Enabled"], + gw_vars.DATA_MASTER_CH2_ENABLED: [ + None, "Thermostat Central Heating 2 Enabled"], + gw_vars.DATA_SLAVE_FAULT_IND: [ + DEVICE_CLASS_PROBLEM, "Boiler Fault Indication"], + gw_vars.DATA_SLAVE_CH_ACTIVE: [ + DEVICE_CLASS_HEAT, "Boiler Central Heating Status"], + gw_vars.DATA_SLAVE_DHW_ACTIVE: [ + DEVICE_CLASS_HEAT, "Boiler Hot Water Status"], + gw_vars.DATA_SLAVE_FLAME_ON: [ + DEVICE_CLASS_HEAT, "Boiler Flame Status"], + gw_vars.DATA_SLAVE_COOLING_ACTIVE: [ + DEVICE_CLASS_COLD, "Boiler Cooling Status"], + gw_vars.DATA_SLAVE_CH2_ACTIVE: [ + DEVICE_CLASS_HEAT, "Boiler Central Heating 2 Status"], + gw_vars.DATA_SLAVE_DIAG_IND: [ + DEVICE_CLASS_PROBLEM, "Boiler Diagnostics Indication"], + gw_vars.DATA_SLAVE_DHW_PRESENT: [None, "Boiler Hot Water Present"], + gw_vars.DATA_SLAVE_CONTROL_TYPE: [None, "Boiler Control Type"], + gw_vars.DATA_SLAVE_COOLING_SUPPORTED: [None, "Boiler Cooling Support"], + gw_vars.DATA_SLAVE_DHW_CONFIG: [ + None, "Boiler Hot Water Configuration"], + gw_vars.DATA_SLAVE_MASTER_LOW_OFF_PUMP: [ + None, "Boiler Pump Commands Support"], + gw_vars.DATA_SLAVE_CH2_PRESENT: [ + None, "Boiler Central Heating 2 Present"], + gw_vars.DATA_SLAVE_SERVICE_REQ: [ + DEVICE_CLASS_PROBLEM, "Boiler Service Required"], + gw_vars.DATA_SLAVE_REMOTE_RESET: [None, "Boiler Remote Reset Support"], + gw_vars.DATA_SLAVE_LOW_WATER_PRESS: [ + DEVICE_CLASS_PROBLEM, "Boiler Low Water Pressure"], + gw_vars.DATA_SLAVE_GAS_FAULT: [ + DEVICE_CLASS_PROBLEM, "Boiler Gas Fault"], + gw_vars.DATA_SLAVE_AIR_PRESS_FAULT: [ + DEVICE_CLASS_PROBLEM, "Boiler Air Pressure Fault"], + gw_vars.DATA_SLAVE_WATER_OVERTEMP: [ + DEVICE_CLASS_PROBLEM, "Boiler Water Overtemperature"], + gw_vars.DATA_REMOTE_TRANSFER_DHW: [ + None, "Remote Hot Water Setpoint Transfer Support"], + gw_vars.DATA_REMOTE_TRANSFER_MAX_CH: [ + None, "Remote Maximum Central Heating Setpoint Write Support"], + gw_vars.DATA_REMOTE_RW_DHW: [ + None, "Remote Hot Water Setpoint Write Support"], + gw_vars.DATA_REMOTE_RW_MAX_CH: [ + None, "Remote Central Heating Setpoint Write Support"], + gw_vars.DATA_ROVRD_MAN_PRIO: [ + None, "Remote Override Manual Change Priority"], + gw_vars.DATA_ROVRD_AUTO_PRIO: [ + None, "Remote Override Program Change Priority"], + gw_vars.OTGW_GPIO_A_STATE: [None, "Gateway GPIO A State"], + gw_vars.OTGW_GPIO_B_STATE: [None, "Gateway GPIO B State"], + gw_vars.OTGW_IGNORE_TRANSITIONS: [None, "Gateway Ignore Transitions"], + gw_vars.OTGW_OVRD_HB: [None, "Gateway Override High Byte"], + } + sensors = [] + for var in discovery_info: + device_class = sensor_info[var][0] + friendly_name = sensor_info[var][1] + entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, var, hass=hass) + sensors.append(OpenThermBinarySensor(entity_id, var, device_class, + friendly_name)) + async_add_entities(sensors) + + +class OpenThermBinarySensor(BinarySensorDevice): + """Represent an OpenTherm Gateway binary sensor.""" + + def __init__(self, entity_id, var, device_class, friendly_name): + """Initialize the binary sensor.""" + self.entity_id = entity_id + self._var = var + self._state = None + self._device_class = device_class + self._friendly_name = friendly_name + + async def async_added_to_hass(self): + """Subscribe to updates from the component.""" + _LOGGER.debug( + "Added OpenTherm Gateway binary sensor %s", self._friendly_name) + async_dispatcher_connect(self.hass, SIGNAL_OPENTHERM_GW_UPDATE, + self.receive_report) + + async def receive_report(self, status): + """Handle status updates from the component.""" + self._state = bool(status.get(self._var)) + self.async_schedule_update_ha_state() + + @property + def name(self): + """Return the friendly name.""" + return self._friendly_name + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self._state + + @property + def device_class(self): + """Return the class of this device.""" + return self._device_class + + @property + def should_poll(self): + """Return False because entity pushes its state.""" + return False diff --git a/homeassistant/components/opentherm_gw.py b/homeassistant/components/opentherm_gw.py index 9379e2c2b31..08807a2d2a6 100644 --- a/homeassistant/components/opentherm_gw.py +++ b/homeassistant/components/opentherm_gw.py @@ -8,6 +8,7 @@ import logging import voluptuous as vol +from homeassistant.components.binary_sensor import DOMAIN as COMP_BINARY_SENSOR from homeassistant.components.sensor import DOMAIN as COMP_SENSOR from homeassistant.const import (CONF_DEVICE, CONF_MONITORED_VARIABLES, CONF_NAME, PRECISION_HALVES, PRECISION_TENTHS, @@ -55,6 +56,7 @@ async def async_setup(hass, config): import pyotgw conf = config[DOMAIN] gateway = pyotgw.pyotgw() + monitored_vars = conf.get(CONF_MONITORED_VARIABLES) hass.data[DATA_OPENTHERM_GW] = { DATA_DEVICE: gateway, DATA_GW_VARS: pyotgw.vars, @@ -63,8 +65,8 @@ async def async_setup(hass, config): hass, conf[CONF_DEVICE], gateway)) hass.async_create_task(async_load_platform( hass, 'climate', DOMAIN, conf.get(CONF_CLIMATE))) - hass.async_create_task(setup_monitored_vars( - hass, conf.get(CONF_MONITORED_VARIABLES))) + if monitored_vars: + hass.async_create_task(setup_monitored_vars(hass, monitored_vars)) return True @@ -83,8 +85,43 @@ async def connect_and_subscribe(hass, device_path, gateway): async def setup_monitored_vars(hass, monitored_vars): """Set up requested sensors.""" gw_vars = hass.data[DATA_OPENTHERM_GW][DATA_GW_VARS] - # Use dict to prepare for binary sensor support. sensor_type_map = { + COMP_BINARY_SENSOR: [ + gw_vars.DATA_MASTER_CH_ENABLED, + gw_vars.DATA_MASTER_DHW_ENABLED, + gw_vars.DATA_MASTER_COOLING_ENABLED, + gw_vars.DATA_MASTER_OTC_ENABLED, + gw_vars.DATA_MASTER_CH2_ENABLED, + gw_vars.DATA_SLAVE_FAULT_IND, + gw_vars.DATA_SLAVE_CH_ACTIVE, + gw_vars.DATA_SLAVE_DHW_ACTIVE, + gw_vars.DATA_SLAVE_FLAME_ON, + gw_vars.DATA_SLAVE_COOLING_ACTIVE, + gw_vars.DATA_SLAVE_CH2_ACTIVE, + gw_vars.DATA_SLAVE_DIAG_IND, + gw_vars.DATA_SLAVE_DHW_PRESENT, + gw_vars.DATA_SLAVE_CONTROL_TYPE, + gw_vars.DATA_SLAVE_COOLING_SUPPORTED, + gw_vars.DATA_SLAVE_DHW_CONFIG, + gw_vars.DATA_SLAVE_MASTER_LOW_OFF_PUMP, + gw_vars.DATA_SLAVE_CH2_PRESENT, + gw_vars.DATA_SLAVE_SERVICE_REQ, + gw_vars.DATA_SLAVE_REMOTE_RESET, + gw_vars.DATA_SLAVE_LOW_WATER_PRESS, + gw_vars.DATA_SLAVE_GAS_FAULT, + gw_vars.DATA_SLAVE_AIR_PRESS_FAULT, + gw_vars.DATA_SLAVE_WATER_OVERTEMP, + gw_vars.DATA_REMOTE_TRANSFER_DHW, + gw_vars.DATA_REMOTE_TRANSFER_MAX_CH, + gw_vars.DATA_REMOTE_RW_DHW, + gw_vars.DATA_REMOTE_RW_MAX_CH, + gw_vars.DATA_ROVRD_MAN_PRIO, + gw_vars.DATA_ROVRD_AUTO_PRIO, + gw_vars.OTGW_GPIO_A_STATE, + gw_vars.OTGW_GPIO_B_STATE, + gw_vars.OTGW_IGNORE_TRANSITIONS, + gw_vars.OTGW_OVRD_HB, + ], COMP_SENSOR: [ gw_vars.DATA_CONTROL_SETPOINT, gw_vars.DATA_MASTER_MEMBERID, @@ -152,11 +189,17 @@ async def setup_monitored_vars(hass, monitored_vars): gw_vars.OTGW_VREF, ] } + binary_sensors = [] sensors = [] for var in monitored_vars: if var in sensor_type_map[COMP_SENSOR]: sensors.append(var) + elif var in sensor_type_map[COMP_BINARY_SENSOR]: + binary_sensors.append(var) else: _LOGGER.error("Monitored variable not supported: %s", var) + if binary_sensors: + hass.async_create_task(async_load_platform( + hass, COMP_BINARY_SENSOR, DOMAIN, binary_sensors)) if sensors: await async_load_platform(hass, COMP_SENSOR, DOMAIN, sensors) From 85dbf1eed3367de07c6e4694e2333a24f994dae1 Mon Sep 17 00:00:00 2001 From: Raymon de Looff Date: Sat, 20 Oct 2018 22:07:47 +0200 Subject: [PATCH 228/265] Upgrade dsmr_parser to 0.12 (#17634) --- homeassistant/components/sensor/dsmr.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/dsmr.py b/homeassistant/components/sensor/dsmr.py index d54959813f8..e3cf704d432 100644 --- a/homeassistant/components/sensor/dsmr.py +++ b/homeassistant/components/sensor/dsmr.py @@ -20,7 +20,7 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['dsmr_parser==0.11'] +REQUIREMENTS = ['dsmr_parser==0.12'] CONF_DSMR_VERSION = 'dsmr_version' CONF_RECONNECT_INTERVAL = 'reconnect_interval' diff --git a/requirements_all.txt b/requirements_all.txt index bf8011cbb49..8c6848a4519 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -310,7 +310,7 @@ dlipower==0.7.165 dovado==0.4.1 # homeassistant.components.sensor.dsmr -dsmr_parser==0.11 +dsmr_parser==0.12 # homeassistant.components.dweet # homeassistant.components.sensor.dweet diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b50381560e6..911403245ed 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -56,7 +56,7 @@ coinmarketcap==5.0.3 defusedxml==0.5.0 # homeassistant.components.sensor.dsmr -dsmr_parser==0.11 +dsmr_parser==0.12 # homeassistant.components.sensor.season ephem==3.7.6.0 From c03b13713077b0e264c4b452bef635ab0876c9b1 Mon Sep 17 00:00:00 2001 From: guillaume1410 Date: Sun, 21 Oct 2018 02:08:35 -0400 Subject: [PATCH 229/265] Removing ryobi gdo (#17637) --- homeassistant/components/cover/ryobi_gdo.py | 103 -------------------- requirements_all.txt | 3 - 2 files changed, 106 deletions(-) delete mode 100644 homeassistant/components/cover/ryobi_gdo.py diff --git a/homeassistant/components/cover/ryobi_gdo.py b/homeassistant/components/cover/ryobi_gdo.py deleted file mode 100644 index fec91f843fd..00000000000 --- a/homeassistant/components/cover/ryobi_gdo.py +++ /dev/null @@ -1,103 +0,0 @@ -""" -Ryobi platform for the cover component. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/cover.ryobi_gdo/ -""" -import logging -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv - -from homeassistant.components.cover import ( - CoverDevice, PLATFORM_SCHEMA, SUPPORT_OPEN, SUPPORT_CLOSE) -from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, STATE_UNKNOWN, STATE_CLOSED) - -REQUIREMENTS = ['py_ryobi_gdo==0.0.10'] - -_LOGGER = logging.getLogger(__name__) - -CONF_DEVICE_ID = 'device_id' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, -}) - -SUPPORTED_FEATURES = (SUPPORT_OPEN | SUPPORT_CLOSE) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Ryobi covers.""" - from py_ryobi_gdo import RyobiGDO as ryobi_door - covers = [] - - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - devices = config.get(CONF_DEVICE_ID) - - for device_id in devices: - my_door = ryobi_door(username, password, device_id) - _LOGGER.debug("Getting the API key") - if my_door.get_api_key() is False: - _LOGGER.error("Wrong credentials, no API key retrieved") - return - _LOGGER.debug("Checking if the device ID is present") - if my_door.check_device_id() is False: - _LOGGER.error("%s not in your device list", device_id) - return - _LOGGER.debug("Adding device %s to covers", device_id) - covers.append(RyobiCover(hass, my_door)) - if covers: - _LOGGER.debug("Adding covers") - add_entities(covers, True) - - -class RyobiCover(CoverDevice): - """Representation of a ryobi cover.""" - - def __init__(self, hass, ryobi_door): - """Initialize the cover.""" - self.ryobi_door = ryobi_door - self._name = 'ryobi_gdo_{}'.format(ryobi_door.get_device_id()) - self._door_state = None - - @property - def name(self): - """Return the name of the cover.""" - return self._name - - @property - def is_closed(self): - """Return if the cover is closed.""" - if self._door_state == STATE_UNKNOWN: - return False - return self._door_state == STATE_CLOSED - - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return 'garage' - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORTED_FEATURES - - def close_cover(self, **kwargs): - """Close the cover.""" - _LOGGER.debug("Closing garage door") - self.ryobi_door.close_device() - - def open_cover(self, **kwargs): - """Open the cover.""" - _LOGGER.debug("Opening garage door") - self.ryobi_door.open_device() - - def update(self): - """Update status from the door.""" - _LOGGER.debug("Updating RyobiGDO status") - self.ryobi_door.update() - self._door_state = self.ryobi_door.get_door_status() diff --git a/requirements_all.txt b/requirements_all.txt index 8c6848a4519..aeff66892c2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -790,9 +790,6 @@ pyW215==0.6.0 # homeassistant.components.sensor.noaa_tides # py_noaa==0.3.0 -# homeassistant.components.cover.ryobi_gdo -py_ryobi_gdo==0.0.10 - # homeassistant.components.ads pyads==2.2.6 From 7e3d0f070064b8f1b13ea6d4d3aaa69fc61feea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sun, 21 Oct 2018 11:21:09 +0200 Subject: [PATCH 230/265] Remove ryobi from .coveragerc (#17647) --- .coveragerc | 1 - 1 file changed, 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index 04299609bbb..4b65b28597e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -458,7 +458,6 @@ omit = homeassistant/components/cover/myq.py homeassistant/components/cover/opengarage.py homeassistant/components/cover/rpi_gpio.py - homeassistant/components/cover/ryobi_gdo.py homeassistant/components/cover/scsgate.py homeassistant/components/device_tracker/actiontec.py homeassistant/components/device_tracker/aruba.py From bdfd473aaa22052a786d31d459038fc68e8e6255 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 21 Oct 2018 12:16:24 +0200 Subject: [PATCH 231/265] Reconnect if sub info comes in that is valid again (#17651) --- homeassistant/components/cloud/__init__.py | 2 +- homeassistant/components/cloud/auth_api.py | 18 +++++ homeassistant/components/cloud/http_api.py | 25 +++++-- tests/components/cloud/test_http_api.py | 86 +++++++++++++++++++--- tests/components/cloud/test_init.py | 4 +- 5 files changed, 117 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 54a221565b4..3bfc5909b0b 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -162,7 +162,7 @@ class Cloud: @property def subscription_expired(self): """Return a boolean if the subscription has expired.""" - return dt_util.utcnow() > self.expiration_date + timedelta(days=3) + return dt_util.utcnow() > self.expiration_date + timedelta(days=7) @property def expiration_date(self): diff --git a/homeassistant/components/cloud/auth_api.py b/homeassistant/components/cloud/auth_api.py index dcf7567482a..042b90bf9cb 100644 --- a/homeassistant/components/cloud/auth_api.py +++ b/homeassistant/components/cloud/auth_api.py @@ -113,6 +113,24 @@ def check_token(cloud): raise _map_aws_exception(err) +def renew_access_token(cloud): + """Renew access token.""" + from botocore.exceptions import ClientError + + cognito = _cognito( + cloud, + access_token=cloud.access_token, + refresh_token=cloud.refresh_token) + + try: + cognito.renew_access_token() + cloud.id_token = cognito.id_token + cloud.access_token = cognito.access_token + cloud.write_user_info() + except ClientError as err: + raise _map_aws_exception(err) + + def _authenticate(cloud, email, password): """Log in and return an authenticated Cognito instance.""" from botocore.exceptions import ClientError diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 720ca00cf52..0df4a39406e 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -14,7 +14,7 @@ from homeassistant.components import websocket_api from . import auth_api from .const import DOMAIN, REQUEST_TIMEOUT -from .iot import STATE_DISCONNECTED +from .iot import STATE_DISCONNECTED, STATE_CONNECTED _LOGGER = logging.getLogger(__name__) @@ -249,13 +249,28 @@ async def websocket_subscription(hass, connection, msg): with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): response = await cloud.fetch_subscription_info() - if response.status == 200: - connection.send_message(websocket_api.result_message( - msg['id'], await response.json())) - else: + if response.status != 200: connection.send_message(websocket_api.error_message( msg['id'], 'request_failed', 'Failed to request subscription')) + data = await response.json() + + # Check if a user is subscribed but local info is outdated + # In that case, let's refresh and reconnect + if data.get('provider') and cloud.iot.state != STATE_CONNECTED: + _LOGGER.debug( + "Found disconnected account with valid subscriotion, connecting") + await hass.async_add_executor_job( + auth_api.renew_access_token, cloud) + + # Cancel reconnect in progress + if cloud.iot.state != STATE_DISCONNECTED: + await cloud.iot.disconnect() + + hass.async_create_task(cloud.iot.connect()) + + connection.send_message(websocket_api.result_message(msg['id'], data)) + @websocket_api.async_response async def websocket_update_prefs(hass, connection, msg): diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 5d4b356b9b2..e27760bd6ed 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -7,6 +7,7 @@ from jose import jwt from homeassistant.components.cloud import ( DOMAIN, auth_api, iot, STORAGE_ENABLE_GOOGLE, STORAGE_ENABLE_ALEXA) +from homeassistant.util import dt as dt_util from tests.common import mock_coro @@ -352,24 +353,89 @@ async def test_websocket_status_not_logged_in(hass, hass_ws_client): } -async def test_websocket_subscription(hass, hass_ws_client, aioclient_mock, - mock_auth): - """Test querying the status.""" - aioclient_mock.get(SUBSCRIPTION_INFO_URL, json={'return': 'value'}) +async def test_websocket_subscription_reconnect( + hass, hass_ws_client, aioclient_mock, mock_auth): + """Test querying the status and connecting because valid account.""" + aioclient_mock.get(SUBSCRIPTION_INFO_URL, json={'provider': 'stripe'}) + hass.data[DOMAIN].id_token = jwt.encode({ + 'email': 'hello@home-assistant.io', + 'custom:sub-exp': dt_util.utcnow().date().isoformat() + }, 'test') + client = await hass_ws_client(hass) + + with patch( + 'homeassistant.components.cloud.auth_api.renew_access_token' + ) as mock_renew, patch( + 'homeassistant.components.cloud.iot.CloudIoT.connect' + ) as mock_connect: + await client.send_json({ + 'id': 5, + 'type': 'cloud/subscription' + }) + response = await client.receive_json() + + assert response['result'] == { + 'provider': 'stripe' + } + assert len(mock_renew.mock_calls) == 1 + assert len(mock_connect.mock_calls) == 1 + + +async def test_websocket_subscription_no_reconnect_if_connected( + hass, hass_ws_client, aioclient_mock, mock_auth): + """Test querying the status and not reconnecting because still expired.""" + aioclient_mock.get(SUBSCRIPTION_INFO_URL, json={'provider': 'stripe'}) + hass.data[DOMAIN].iot.state = iot.STATE_CONNECTED + hass.data[DOMAIN].id_token = jwt.encode({ + 'email': 'hello@home-assistant.io', + 'custom:sub-exp': dt_util.utcnow().date().isoformat() + }, 'test') + client = await hass_ws_client(hass) + + with patch( + 'homeassistant.components.cloud.auth_api.renew_access_token' + ) as mock_renew, patch( + 'homeassistant.components.cloud.iot.CloudIoT.connect' + ) as mock_connect: + await client.send_json({ + 'id': 5, + 'type': 'cloud/subscription' + }) + response = await client.receive_json() + + assert response['result'] == { + 'provider': 'stripe' + } + assert len(mock_renew.mock_calls) == 0 + assert len(mock_connect.mock_calls) == 0 + + +async def test_websocket_subscription_no_reconnect_if_expired( + hass, hass_ws_client, aioclient_mock, mock_auth): + """Test querying the status and not reconnecting because still expired.""" + aioclient_mock.get(SUBSCRIPTION_INFO_URL, json={'provider': 'stripe'}) hass.data[DOMAIN].id_token = jwt.encode({ 'email': 'hello@home-assistant.io', 'custom:sub-exp': '2018-01-03' }, 'test') client = await hass_ws_client(hass) - await client.send_json({ - 'id': 5, - 'type': 'cloud/subscription' - }) - response = await client.receive_json() + + with patch( + 'homeassistant.components.cloud.auth_api.renew_access_token' + ) as mock_renew, patch( + 'homeassistant.components.cloud.iot.CloudIoT.connect' + ) as mock_connect: + await client.send_json({ + 'id': 5, + 'type': 'cloud/subscription' + }) + response = await client.receive_json() assert response['result'] == { - 'return': 'value' + 'provider': 'stripe' } + assert len(mock_renew.mock_calls) == 1 + assert len(mock_connect.mock_calls) == 1 async def test_websocket_subscription_fail(hass, hass_ws_client, diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index 8695830eae9..61518f0f0e8 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -155,14 +155,14 @@ def test_subscription_expired(hass): with patch.object(cl, '_decode_claims', return_value=token_val), \ patch('homeassistant.util.dt.utcnow', return_value=utcnow().replace( - year=2017, month=11, day=15, hour=23, minute=59, + year=2017, month=11, day=19, hour=23, minute=59, second=59)): assert not cl.subscription_expired with patch.object(cl, '_decode_claims', return_value=token_val), \ patch('homeassistant.util.dt.utcnow', return_value=utcnow().replace( - year=2017, month=11, day=16, hour=0, minute=0, + year=2017, month=11, day=20, hour=0, minute=0, second=0)): assert cl.subscription_expired From 9982867d667b698794ccb51531280bc4d5dbd7c4 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Sun, 21 Oct 2018 13:05:02 +0200 Subject: [PATCH 232/265] Very minor cleanup of RFLink components (#17649) --- homeassistant/components/light/rflink.py | 13 +++++++------ homeassistant/components/sensor/rflink.py | 5 ++++- homeassistant/components/switch/rflink.py | 7 ++++--- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/light/rflink.py b/homeassistant/components/light/rflink.py index 885239a51c3..3b60280c582 100644 --- a/homeassistant/components/light/rflink.py +++ b/homeassistant/components/light/rflink.py @@ -6,16 +6,18 @@ https://home-assistant.io/components/light.rflink/ """ import logging +import voluptuous as vol + from homeassistant.components.light import ( ATTR_BRIGHTNESS, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, Light) from homeassistant.components.rflink import ( CONF_ALIASES, CONF_ALIASSES, CONF_AUTOMATIC_ADD, CONF_DEVICE_DEFAULTS, CONF_DEVICES, CONF_FIRE_EVENT, CONF_GROUP, CONF_GROUP_ALIASES, - CONF_GROUP_ALIASSES, CONF_IGNORE_DEVICES, CONF_NOGROUP_ALIASES, - CONF_NOGROUP_ALIASSES, CONF_SIGNAL_REPETITIONS, DATA_DEVICE_REGISTER, - DEVICE_DEFAULTS_SCHEMA, - EVENT_KEY_COMMAND, EVENT_KEY_ID, SwitchableRflinkDevice, cv, - remove_deprecated, vol) + CONF_GROUP_ALIASSES, CONF_NOGROUP_ALIASES, CONF_NOGROUP_ALIASSES, + CONF_SIGNAL_REPETITIONS, DATA_DEVICE_REGISTER, DEVICE_DEFAULTS_SCHEMA, + EVENT_KEY_COMMAND, EVENT_KEY_ID, SwitchableRflinkDevice, + remove_deprecated) +import homeassistant.helpers.config_validation as cv from homeassistant.const import (CONF_NAME, CONF_TYPE) DEPENDENCIES = ['rflink'] @@ -28,7 +30,6 @@ TYPE_HYBRID = 'hybrid' TYPE_TOGGLE = 'toggle' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_IGNORE_DEVICES): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_DEVICE_DEFAULTS, default=DEVICE_DEFAULTS_SCHEMA({})): DEVICE_DEFAULTS_SCHEMA, vol.Optional(CONF_AUTOMATIC_ADD, default=True): cv.boolean, diff --git a/homeassistant/components/sensor/rflink.py b/homeassistant/components/sensor/rflink.py index 4065e0a439f..f3ec776fda8 100644 --- a/homeassistant/components/sensor/rflink.py +++ b/homeassistant/components/sensor/rflink.py @@ -6,13 +6,16 @@ https://home-assistant.io/components/sensor.rflink/ """ import logging +import voluptuous as vol + from homeassistant.components.rflink import ( CONF_ALIASES, CONF_ALIASSES, CONF_AUTOMATIC_ADD, CONF_DEVICES, DATA_DEVICE_REGISTER, DATA_ENTITY_LOOKUP, EVENT_KEY_ID, - EVENT_KEY_SENSOR, EVENT_KEY_UNIT, RflinkDevice, cv, remove_deprecated, vol, + EVENT_KEY_SENSOR, EVENT_KEY_UNIT, RflinkDevice, remove_deprecated, SIGNAL_AVAILABILITY, SIGNAL_HANDLE_EVENT, TMP_ENTITY) from homeassistant.components.sensor import ( PLATFORM_SCHEMA) +import homeassistant.helpers.config_validation as cv from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, CONF_UNIT_OF_MEASUREMENT) from homeassistant.helpers.dispatcher import (async_dispatcher_connect) diff --git a/homeassistant/components/switch/rflink.py b/homeassistant/components/switch/rflink.py index 1f217b1c39c..51bf5543584 100644 --- a/homeassistant/components/switch/rflink.py +++ b/homeassistant/components/switch/rflink.py @@ -6,15 +6,16 @@ https://home-assistant.io/components/switch.rflink/ """ import logging +import voluptuous as vol + from homeassistant.components.rflink import ( CONF_ALIASES, CONF_ALIASSES, CONF_DEVICE_DEFAULTS, CONF_DEVICES, CONF_FIRE_EVENT, CONF_GROUP, CONF_GROUP_ALIASES, CONF_GROUP_ALIASSES, CONF_NOGROUP_ALIASES, CONF_NOGROUP_ALIASSES, CONF_SIGNAL_REPETITIONS, - DEVICE_DEFAULTS_SCHEMA, SwitchableRflinkDevice, cv, - remove_deprecated, vol) + DEVICE_DEFAULTS_SCHEMA, SwitchableRflinkDevice, remove_deprecated) from homeassistant.components.switch import ( PLATFORM_SCHEMA, SwitchDevice) - +import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_NAME DEPENDENCIES = ['rflink'] From ef93d48d50bb25b0405cb8adaf044b3ac7268888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sun, 21 Oct 2018 13:41:27 +0200 Subject: [PATCH 233/265] available to switchmate (#17640) --- homeassistant/components/switch/switchmate.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/switch/switchmate.py b/homeassistant/components/switch/switchmate.py index 2ec77a38267..7f00964cd20 100644 --- a/homeassistant/components/switch/switchmate.py +++ b/homeassistant/components/switch/switchmate.py @@ -52,6 +52,11 @@ class Switchmate(SwitchDevice): """Return a unique, HASS-friendly identifier for this entity.""" return self._mac.replace(':', '') + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._device.available + @property def name(self) -> str: """Return the name of the switch.""" From cf2468702402bf7e550600a7021efb969a6a2104 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 21 Oct 2018 14:13:30 +0200 Subject: [PATCH 234/265] Upgrade async_timeout to 3.0.1 (#17655) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9217e3b3961..fa0d675f4b1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ aiohttp==3.4.4 astral==1.6.1 -async_timeout==3.0.0 +async_timeout==3.0.1 attrs==18.2.0 bcrypt==3.1.4 certifi>=2018.04.16 diff --git a/requirements_all.txt b/requirements_all.txt index aeff66892c2..00f263ca04e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1,7 +1,7 @@ # Home Assistant core aiohttp==3.4.4 astral==1.6.1 -async_timeout==3.0.0 +async_timeout==3.0.1 attrs==18.2.0 bcrypt==3.1.4 certifi>=2018.04.16 diff --git a/setup.py b/setup.py index 727badb1d94..90f2e8357fd 100755 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ PACKAGES = find_packages(exclude=['tests', 'tests.*']) REQUIRES = [ 'aiohttp==3.4.4', 'astral==1.6.1', - 'async_timeout==3.0.0', + 'async_timeout==3.0.1', 'attrs==18.2.0', 'bcrypt==3.1.4', 'certifi>=2018.04.16', From 731753b604ad5121b803572f632ef4ac71fe5b89 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 21 Oct 2018 15:07:44 +0200 Subject: [PATCH 235/265] Upgrade holidays to 0.9.8 (#17656) --- homeassistant/components/binary_sensor/workday.py | 9 +++++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/binary_sensor/workday.py b/homeassistant/components/binary_sensor/workday.py index 82b5e66629a..fc8207f83b7 100644 --- a/homeassistant/components/binary_sensor/workday.py +++ b/homeassistant/components/binary_sensor/workday.py @@ -14,15 +14,16 @@ from homeassistant.const import CONF_NAME, WEEKDAYS from homeassistant.components.binary_sensor import BinarySensorDevice import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['holidays==0.9.7'] +REQUIREMENTS = ['holidays==0.9.8'] _LOGGER = logging.getLogger(__name__) # List of all countries currently supported by holidays # There seems to be no way to get the list out at runtime ALL_COUNTRIES = [ - 'Argentina', 'AR', 'Australia', 'AU', 'Austria', 'AT', 'Belarus', 'BY' - 'Belgium', 'BE', 'Canada', 'CA', 'Colombia', 'CO', 'Czech', 'CZ', + 'Argentina', 'AR', 'Australia', 'AU', 'Austria', 'AT', + 'Brazil', 'BR', 'Belarus', 'BY', 'Belgium', 'BE', + 'Canada', 'CA', 'Colombia', 'CO', 'Croatia', 'HR', 'Czech', 'CZ', 'Denmark', 'DK', 'England', 'EuropeanCentralBank', 'ECB', 'TAR', 'Finland', 'FI', 'France', 'FRA', 'Germany', 'DE', 'Hungary', 'HU', 'India', 'IND', 'Ireland', 'Isle of Man', 'Italy', 'IT', 'Japan', 'JP', @@ -30,7 +31,7 @@ ALL_COUNTRIES = [ 'Northern Ireland', 'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT', 'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI', 'Slovakia', 'SK', 'South Africa', 'ZA', 'Spain', 'ES', 'Sweden', 'SE', 'Switzerland', 'CH', - 'UnitedKingdom', 'UK', 'UnitedStates', 'US', 'Wales', + 'Ukraine', 'UA', 'UnitedKingdom', 'UK', 'UnitedStates', 'US', 'Wales', ] ALLOWED_DAYS = WEEKDAYS + ['holiday'] diff --git a/requirements_all.txt b/requirements_all.txt index 00f263ca04e..bfee5976a65 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -463,7 +463,7 @@ hipnotify==1.0.8 hole==0.3.0 # homeassistant.components.binary_sensor.workday -holidays==0.9.7 +holidays==0.9.8 # homeassistant.components.frontend home-assistant-frontend==20181018.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 911403245ed..302db00f706 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -94,7 +94,7 @@ hbmqtt==0.9.4 hdate==0.6.5 # homeassistant.components.binary_sensor.workday -holidays==0.9.7 +holidays==0.9.8 # homeassistant.components.frontend home-assistant-frontend==20181018.0 From b6d3a199ce25740f72cac686f7efa8be17aa7a95 Mon Sep 17 00:00:00 2001 From: Oscar Tin Lai Date: Mon, 22 Oct 2018 02:35:07 +1100 Subject: [PATCH 236/265] Add support for Dyson Hot+Cool Fan as a climate device (#14598) * Added support for dyson hot+cool fan as climate device * Removed decimal place in kelvin units conversion Minor edits to be consistent with Dyson's internal conversion of temperature from kelvin to celsius. It does not include decimal place to convert between kelvin and celsius. * made changes according to comments * Refactored target temp logics, fixed enum issues * changed name of component to entity * removed temperature conversion for min/max property * changed back to 644 permission * added extra tests for almost-all coverage * changed assert method to avoid lack of certain method in py35 * added test_setup_component * shorten line length * fixed mock spec and added checking of message listener is called * added doc string and debug msg * shorten line length * removed pending target temp --- homeassistant/components/climate/dyson.py | 176 +++++++++++ homeassistant/components/dyson.py | 1 + tests/components/climate/test_dyson.py | 358 ++++++++++++++++++++++ tests/components/test_dyson.py | 4 +- 4 files changed, 537 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/climate/dyson.py create mode 100644 tests/components/climate/test_dyson.py diff --git a/homeassistant/components/climate/dyson.py b/homeassistant/components/climate/dyson.py new file mode 100644 index 00000000000..0b09ec7f0b4 --- /dev/null +++ b/homeassistant/components/climate/dyson.py @@ -0,0 +1,176 @@ +""" +Support for Dyson Pure Hot+Cool link fan. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/climate.dyson/ +""" +import logging + +from homeassistant.components.dyson import DYSON_DEVICES +from homeassistant.components.climate import ( + ClimateDevice, STATE_HEAT, STATE_COOL, STATE_IDLE, + SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE) +from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE + +_LOGGER = logging.getLogger(__name__) + +STATE_DIFFUSE = "Diffuse Mode" +STATE_FOCUS = "Focus Mode" +FAN_LIST = [STATE_FOCUS, STATE_DIFFUSE] +OPERATION_LIST = [STATE_HEAT, STATE_COOL] + +SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE + | SUPPORT_OPERATION_MODE) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Dyson fan components.""" + if discovery_info is None: + return + + from libpurecoollink.dyson_pure_hotcool_link import DysonPureHotCoolLink + # Get Dyson Devices from parent component. + add_devices( + [DysonPureHotCoolLinkDevice(device) + for device in hass.data[DYSON_DEVICES] + if isinstance(device, DysonPureHotCoolLink)] + ) + + +class DysonPureHotCoolLinkDevice(ClimateDevice): + """Representation of a Dyson climate fan.""" + + def __init__(self, device): + """Initialize the fan.""" + self._device = device + self._current_temp = None + + async def async_added_to_hass(self): + """Call when entity is added to hass.""" + self.hass.async_add_job(self._device.add_message_listener, + self.on_message) + + def on_message(self, message): + """Call when new messages received from the climate.""" + from libpurecoollink.dyson_pure_state import DysonPureHotCoolState + + if isinstance(message, DysonPureHotCoolState): + _LOGGER.debug("Message received for climate device %s : %s", + self.name, message) + self.schedule_update_ha_state() + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS + + @property + def name(self): + """Return the display name of this climate.""" + return self._device.name + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def current_temperature(self): + """Return the current temperature.""" + if self._device.environmental_state: + temperature_kelvin = self._device.environmental_state.temperature + if temperature_kelvin != 0: + self._current_temp = float("{0:.1f}".format( + temperature_kelvin - 273)) + return self._current_temp + + @property + def target_temperature(self): + """Return the target temperature.""" + heat_target = int(self._device.state.heat_target) / 10 + return int(heat_target - 273) + + @property + def current_humidity(self): + """Return the current humidity.""" + if self._device.environmental_state: + if self._device.environmental_state.humidity == 0: + return None + return self._device.environmental_state.humidity + return None + + @property + def current_operation(self): + """Return current operation ie. heat, cool, idle.""" + from libpurecoollink.const import HeatMode, HeatState + if self._device.state.heat_mode == HeatMode.HEAT_ON.value: + if self._device.state.heat_state == HeatState.HEAT_STATE_ON.value: + return STATE_HEAT + return STATE_IDLE + return STATE_COOL + + @property + def operation_list(self): + """Return the list of available operation modes.""" + return OPERATION_LIST + + @property + def current_fan_mode(self): + """Return the fan setting.""" + from libpurecoollink.const import FocusMode + if self._device.state.focus_mode == FocusMode.FOCUS_ON.value: + return STATE_FOCUS + return STATE_DIFFUSE + + @property + def fan_list(self): + """Return the list of available fan modes.""" + return FAN_LIST + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + target_temp = kwargs.get(ATTR_TEMPERATURE) + if target_temp is None: + return + target_temp = int(target_temp) + _LOGGER.debug("Set %s temperature %s", self.name, target_temp) + # Limit the target temperature into acceptable range. + target_temp = min(self.max_temp, target_temp) + target_temp = max(self.min_temp, target_temp) + from libpurecoollink.const import HeatTarget, HeatMode + self._device.set_configuration( + heat_target=HeatTarget.celsius(target_temp), + heat_mode=HeatMode.HEAT_ON) + + def set_fan_mode(self, fan_mode): + """Set new fan mode.""" + _LOGGER.debug("Set %s focus mode %s", self.name, fan_mode) + from libpurecoollink.const import FocusMode + if fan_mode == STATE_FOCUS: + self._device.set_configuration(focus_mode=FocusMode.FOCUS_ON) + elif fan_mode == STATE_DIFFUSE: + self._device.set_configuration(focus_mode=FocusMode.FOCUS_OFF) + + def set_operation_mode(self, operation_mode): + """Set operation mode.""" + _LOGGER.debug("Set %s heat mode %s", self.name, operation_mode) + from libpurecoollink.const import HeatMode + if operation_mode == STATE_HEAT: + self._device.set_configuration(heat_mode=HeatMode.HEAT_ON) + elif operation_mode == STATE_COOL: + self._device.set_configuration(heat_mode=HeatMode.HEAT_OFF) + + @property + def min_temp(self): + """Return the minimum temperature.""" + return 1 + + @property + def max_temp(self): + """Return the maximum temperature.""" + return 37 diff --git a/homeassistant/components/dyson.py b/homeassistant/components/dyson.py index 3989c0bbe3e..791f990d9ad 100644 --- a/homeassistant/components/dyson.py +++ b/homeassistant/components/dyson.py @@ -102,5 +102,6 @@ def setup(hass, config): discovery.load_platform(hass, "sensor", DOMAIN, {}, config) discovery.load_platform(hass, "fan", DOMAIN, {}, config) discovery.load_platform(hass, "vacuum", DOMAIN, {}, config) + discovery.load_platform(hass, "climate", DOMAIN, {}, config) return True diff --git a/tests/components/climate/test_dyson.py b/tests/components/climate/test_dyson.py new file mode 100644 index 00000000000..6e8b63d64c4 --- /dev/null +++ b/tests/components/climate/test_dyson.py @@ -0,0 +1,358 @@ +"""Test the Dyson fan component.""" +import unittest +from unittest import mock + +from libpurecoollink.const import (FocusMode, HeatMode, HeatState, HeatTarget, + TiltState) +from libpurecoollink.dyson_pure_state import DysonPureHotCoolState +from libpurecoollink.dyson_pure_hotcool_link import DysonPureHotCoolLink +from homeassistant.components.climate import dyson +from homeassistant.components import dyson as dyson_parent +from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE +from homeassistant.setup import setup_component +from tests.common import get_test_home_assistant + + +class MockDysonState(DysonPureHotCoolState): + """Mock Dyson state.""" + + def __init__(self): + """Create new Mock Dyson State.""" + pass + + +def _get_device_with_no_state(): + """Return a device with no state.""" + device = mock.Mock(spec=DysonPureHotCoolLink) + device.name = "Device_name" + device.state = None + device.environmental_state = None + return device + + +def _get_device_off(): + """Return a device with state off.""" + device = mock.Mock(spec=DysonPureHotCoolLink) + device.name = "Device_name" + device.state = mock.Mock() + device.environmental_state = mock.Mock() + return device + + +def _get_device_focus(): + """Return a device with fan state of focus mode.""" + device = mock.Mock(spec=DysonPureHotCoolLink) + device.name = "Device_name" + device.state.focus_mode = FocusMode.FOCUS_ON.value + return device + + +def _get_device_diffuse(): + """Return a device with fan state of diffuse mode.""" + device = mock.Mock(spec=DysonPureHotCoolLink) + device.name = "Device_name" + device.state.focus_mode = FocusMode.FOCUS_OFF.value + return device + + +def _get_device_cool(): + """Return a device with state of cooling.""" + device = mock.Mock(spec=DysonPureHotCoolLink) + device.name = "Device_name" + device.state.tilt = TiltState.TILT_FALSE.value + device.state.focus_mode = FocusMode.FOCUS_OFF.value + device.state.heat_target = HeatTarget.celsius(12) + device.state.heat_mode = HeatMode.HEAT_OFF.value + device.state.heat_state = HeatState.HEAT_STATE_OFF.value + device.environmental_state.temperature = 288 + device.environmental_state.humidity = 53 + return device + + +def _get_device_heat_off(): + """Return a device with state of heat reached target.""" + device = mock.Mock(spec=DysonPureHotCoolLink) + device.name = "Device_name" + device.state = mock.Mock() + device.state.tilt = TiltState.TILT_FALSE.value + device.state.focus_mode = FocusMode.FOCUS_ON.value + device.state.heat_target = HeatTarget.celsius(20) + device.state.heat_mode = HeatMode.HEAT_ON.value + device.state.heat_state = HeatState.HEAT_STATE_OFF.value + device.environmental_state.temperature = 293 + device.environmental_state.humidity = 53 + return device + + +def _get_device_heat_on(): + """Return a device with state of heating.""" + device = mock.Mock(spec=DysonPureHotCoolLink) + device.name = "Device_name" + device.state = mock.Mock() + device.state.tilt = TiltState.TILT_FALSE.value + device.state.focus_mode = FocusMode.FOCUS_ON.value + device.state.heat_target = HeatTarget.celsius(23) + device.state.heat_mode = HeatMode.HEAT_ON.value + device.state.heat_state = HeatState.HEAT_STATE_ON.value + device.environmental_state.temperature = 289 + device.environmental_state.humidity = 53 + return device + + +class DysonTest(unittest.TestCase): + """Dyson Climate component test class.""" + + def setUp(self): # pylint: disable=invalid-name + """Set up things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def tearDown(self): # pylint: disable=invalid-name + """Stop everything that was started.""" + self.hass.stop() + + @mock.patch('libpurecoollink.dyson.DysonAccount.devices', + return_value=[_get_device_heat_on(), _get_device_cool()]) + @mock.patch('libpurecoollink.dyson.DysonAccount.login', return_value=True) + def test_setup_component_with_parent_discovery(self, mocked_login, + mocked_devices): + """Test setup_component using discovery.""" + setup_component(self.hass, dyson_parent.DOMAIN, { + dyson_parent.DOMAIN: { + dyson_parent.CONF_USERNAME: "email", + dyson_parent.CONF_PASSWORD: "password", + dyson_parent.CONF_LANGUAGE: "US", + } + }) + self.assertEqual(len(self.hass.data[dyson.DYSON_DEVICES]), 2) + self.hass.block_till_done() + for m in mocked_devices.return_value: + assert m.add_message_listener.called + + def test_setup_component_without_devices(self): + """Test setup component with no devices.""" + self.hass.data[dyson.DYSON_DEVICES] = [] + add_devices = mock.MagicMock() + dyson.setup_platform(self.hass, None, add_devices) + add_devices.assert_not_called() + + def test_setup_component_with_devices(self): + """Test setup component with valid devices.""" + devices = [ + _get_device_with_no_state(), + _get_device_off(), + _get_device_heat_on() + ] + self.hass.data[dyson.DYSON_DEVICES] = devices + add_devices = mock.MagicMock() + dyson.setup_platform(self.hass, None, add_devices, discovery_info={}) + self.assertTrue(add_devices.called) + + def test_setup_component_with_invalid_devices(self): + """Test setup component with invalid devices.""" + devices = [ + None, + "foo_bar" + ] + self.hass.data[dyson.DYSON_DEVICES] = devices + add_devices = mock.MagicMock() + dyson.setup_platform(self.hass, None, add_devices, discovery_info={}) + add_devices.assert_called_with([]) + + def test_setup_component(self): + """Test setup component with devices.""" + device_fan = _get_device_heat_on() + device_non_fan = _get_device_off() + + def _add_device(devices): + assert len(devices) == 1 + assert devices[0].name == "Device_name" + + self.hass.data[dyson.DYSON_DEVICES] = [device_fan, device_non_fan] + dyson.setup_platform(self.hass, None, _add_device) + + def test_dyson_set_temperature(self): + """Test set climate temperature.""" + device = _get_device_heat_on() + device.temp_unit = TEMP_CELSIUS + entity = dyson.DysonPureHotCoolLinkDevice(device) + self.assertFalse(entity.should_poll) + + # Without target temp. + kwargs = {} + entity.set_temperature(**kwargs) + set_config = device.set_configuration + set_config.assert_not_called() + + kwargs = {ATTR_TEMPERATURE: 23} + entity.set_temperature(**kwargs) + set_config = device.set_configuration + set_config.assert_called_with( + heat_mode=HeatMode.HEAT_ON, + heat_target=HeatTarget.celsius(23)) + + # Should clip the target temperature between 1 and 37 inclusive. + kwargs = {ATTR_TEMPERATURE: 50} + entity.set_temperature(**kwargs) + set_config = device.set_configuration + set_config.assert_called_with( + heat_mode=HeatMode.HEAT_ON, + heat_target=HeatTarget.celsius(37)) + + kwargs = {ATTR_TEMPERATURE: -5} + entity.set_temperature(**kwargs) + set_config = device.set_configuration + set_config.assert_called_with( + heat_mode=HeatMode.HEAT_ON, + heat_target=HeatTarget.celsius(1)) + + def test_dyson_set_temperature_when_cooling_mode(self): + """Test set climate temperature when heating is off.""" + device = _get_device_cool() + device.temp_unit = TEMP_CELSIUS + entity = dyson.DysonPureHotCoolLinkDevice(device) + entity.schedule_update_ha_state = mock.Mock() + + kwargs = {ATTR_TEMPERATURE: 23} + entity.set_temperature(**kwargs) + set_config = device.set_configuration + set_config.assert_called_with( + heat_mode=HeatMode.HEAT_ON, + heat_target=HeatTarget.celsius(23)) + + def test_dyson_set_fan_mode(self): + """Test set fan mode.""" + device = _get_device_heat_on() + entity = dyson.DysonPureHotCoolLinkDevice(device) + self.assertFalse(entity.should_poll) + + entity.set_fan_mode(dyson.STATE_FOCUS) + set_config = device.set_configuration + set_config.assert_called_with(focus_mode=FocusMode.FOCUS_ON) + + entity.set_fan_mode(dyson.STATE_DIFFUSE) + set_config = device.set_configuration + set_config.assert_called_with(focus_mode=FocusMode.FOCUS_OFF) + + def test_dyson_fan_list(self): + """Test get fan list.""" + device = _get_device_heat_on() + entity = dyson.DysonPureHotCoolLinkDevice(device) + self.assertEqual(len(entity.fan_list), 2) + self.assertTrue(dyson.STATE_FOCUS in entity.fan_list) + self.assertTrue(dyson.STATE_DIFFUSE in entity.fan_list) + + def test_dyson_fan_mode_focus(self): + """Test fan focus mode.""" + device = _get_device_focus() + entity = dyson.DysonPureHotCoolLinkDevice(device) + self.assertEqual(entity.current_fan_mode, dyson.STATE_FOCUS) + + def test_dyson_fan_mode_diffuse(self): + """Test fan diffuse mode.""" + device = _get_device_diffuse() + entity = dyson.DysonPureHotCoolLinkDevice(device) + self.assertEqual(entity.current_fan_mode, dyson.STATE_DIFFUSE) + + def test_dyson_set_operation_mode(self): + """Test set operation mode.""" + device = _get_device_heat_on() + entity = dyson.DysonPureHotCoolLinkDevice(device) + self.assertFalse(entity.should_poll) + + entity.set_operation_mode(dyson.STATE_HEAT) + set_config = device.set_configuration + set_config.assert_called_with(heat_mode=HeatMode.HEAT_ON) + + entity.set_operation_mode(dyson.STATE_COOL) + set_config = device.set_configuration + set_config.assert_called_with(heat_mode=HeatMode.HEAT_OFF) + + def test_dyson_operation_list(self): + """Test get operation list.""" + device = _get_device_heat_on() + entity = dyson.DysonPureHotCoolLinkDevice(device) + self.assertEqual(len(entity.operation_list), 2) + self.assertTrue(dyson.STATE_HEAT in entity.operation_list) + self.assertTrue(dyson.STATE_COOL in entity.operation_list) + + def test_dyson_heat_off(self): + """Test turn off heat.""" + device = _get_device_heat_off() + entity = dyson.DysonPureHotCoolLinkDevice(device) + entity.set_operation_mode(dyson.STATE_COOL) + set_config = device.set_configuration + set_config.assert_called_with(heat_mode=HeatMode.HEAT_OFF) + + def test_dyson_heat_on(self): + """Test turn on heat.""" + device = _get_device_heat_on() + entity = dyson.DysonPureHotCoolLinkDevice(device) + entity.set_operation_mode(dyson.STATE_HEAT) + set_config = device.set_configuration + set_config.assert_called_with(heat_mode=HeatMode.HEAT_ON) + + def test_dyson_heat_value_on(self): + """Test get heat value on.""" + device = _get_device_heat_on() + entity = dyson.DysonPureHotCoolLinkDevice(device) + self.assertEqual(entity.current_operation, dyson.STATE_HEAT) + + def test_dyson_heat_value_off(self): + """Test get heat value off.""" + device = _get_device_cool() + entity = dyson.DysonPureHotCoolLinkDevice(device) + self.assertEqual(entity.current_operation, dyson.STATE_COOL) + + def test_dyson_heat_value_idle(self): + """Test get heat value idle.""" + device = _get_device_heat_off() + entity = dyson.DysonPureHotCoolLinkDevice(device) + self.assertEqual(entity.current_operation, dyson.STATE_IDLE) + + def test_on_message(self): + """Test when message is received.""" + device = _get_device_heat_on() + entity = dyson.DysonPureHotCoolLinkDevice(device) + entity.schedule_update_ha_state = mock.Mock() + entity.on_message(MockDysonState()) + entity.schedule_update_ha_state.assert_called_with() + + def test_general_properties(self): + """Test properties of entity.""" + device = _get_device_with_no_state() + entity = dyson.DysonPureHotCoolLinkDevice(device) + self.assertEqual(entity.should_poll, False) + self.assertEqual(entity.supported_features, dyson.SUPPORT_FLAGS) + self.assertEqual(entity.temperature_unit, TEMP_CELSIUS) + + def test_property_current_humidity(self): + """Test properties of current humidity.""" + device = _get_device_heat_on() + entity = dyson.DysonPureHotCoolLinkDevice(device) + self.assertEqual(entity.current_humidity, 53) + + def test_property_current_humidity_with_invalid_env_state(self): + """Test properties of current humidity with invalid env state.""" + device = _get_device_off() + device.environmental_state.humidity = 0 + entity = dyson.DysonPureHotCoolLinkDevice(device) + self.assertEqual(entity.current_humidity, None) + + def test_property_current_humidity_without_env_state(self): + """Test properties of current humidity without env state.""" + device = _get_device_with_no_state() + entity = dyson.DysonPureHotCoolLinkDevice(device) + self.assertEqual(entity.current_humidity, None) + + def test_property_current_temperature(self): + """Test properties of current temperature.""" + device = _get_device_heat_on() + entity = dyson.DysonPureHotCoolLinkDevice(device) + # Result should be in celsius, hence then subtraction of 273. + self.assertEqual(entity.current_temperature, 289 - 273) + + def test_property_target_temperature(self): + """Test properties of target temperature.""" + device = _get_device_heat_on() + entity = dyson.DysonPureHotCoolLinkDevice(device) + self.assertEqual(entity.target_temperature, 23) diff --git a/tests/components/test_dyson.py b/tests/components/test_dyson.py index 19c39754eb2..0352551aec9 100644 --- a/tests/components/test_dyson.py +++ b/tests/components/test_dyson.py @@ -87,7 +87,7 @@ class DysonTest(unittest.TestCase): self.assertEqual(mocked_login.call_count, 1) self.assertEqual(mocked_devices.call_count, 1) self.assertEqual(len(self.hass.data[dyson.DYSON_DEVICES]), 1) - self.assertEqual(mocked_discovery.call_count, 3) + self.assertEqual(mocked_discovery.call_count, 4) @mock.patch('libpurecoollink.dyson.DysonAccount.devices', return_value=[_get_dyson_account_device_not_available()]) @@ -172,7 +172,7 @@ class DysonTest(unittest.TestCase): self.assertEqual(mocked_login.call_count, 1) self.assertEqual(mocked_devices.call_count, 1) self.assertEqual(len(self.hass.data[dyson.DYSON_DEVICES]), 1) - self.assertEqual(mocked_discovery.call_count, 3) + self.assertEqual(mocked_discovery.call_count, 4) @mock.patch('libpurecoollink.dyson.DysonAccount.devices', return_value=[_get_dyson_account_device_not_available()]) From 2d980f2a9240fa029fcc121bcbe419e3c4bdcb26 Mon Sep 17 00:00:00 2001 From: Mathieu Velten Date: Sun, 21 Oct 2018 19:54:01 +0200 Subject: [PATCH 237/265] Update pynetgear to 0.5.0 (#17652) --- homeassistant/components/device_tracker/netgear.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/netgear.py b/homeassistant/components/device_tracker/netgear.py index 2e1b96dffad..12d026a35cd 100644 --- a/homeassistant/components/device_tracker/netgear.py +++ b/homeassistant/components/device_tracker/netgear.py @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT, CONF_SSL, CONF_DEVICES, CONF_EXCLUDE) -REQUIREMENTS = ['pynetgear==0.4.2'] +REQUIREMENTS = ['pynetgear==0.5.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index bfee5976a65..607cabeebbe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1005,7 +1005,7 @@ pymysensors==0.17.0 pynello==1.5.1 # homeassistant.components.device_tracker.netgear -pynetgear==0.4.2 +pynetgear==0.5.0 # homeassistant.components.switch.netio pynetio==0.1.9.1 From 95371fe4a6316724e5e96284e5dff7c8c3bb5b6a Mon Sep 17 00:00:00 2001 From: Luke Fritz Date: Sun, 21 Oct 2018 12:54:51 -0500 Subject: [PATCH 238/265] Bump pyarlo==0.2.2 (#17673) * Bump pyarlo to 0.2.2, fixes #17427 * Increase log level for refresh message to clear up logs --- homeassistant/components/arlo.py | 4 ++-- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/arlo.py b/homeassistant/components/arlo.py index 015e1e0d1fc..f7d9f012f65 100644 --- a/homeassistant/components/arlo.py +++ b/homeassistant/components/arlo.py @@ -16,7 +16,7 @@ from homeassistant.const import ( from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.dispatcher import dispatcher_send -REQUIREMENTS = ['pyarlo==0.2.0'] +REQUIREMENTS = ['pyarlo==0.2.2'] _LOGGER = logging.getLogger(__name__) @@ -81,7 +81,7 @@ def setup(hass, config): def hub_refresh(event_time): """Call ArloHub to refresh information.""" - _LOGGER.info("Updating Arlo Hub component") + _LOGGER.debug("Updating Arlo Hub component") hass.data[DATA_ARLO].update(update_cameras=True, update_base_station=True) dispatcher_send(hass, SIGNAL_UPDATE_ARLO) diff --git a/requirements_all.txt b/requirements_all.txt index 607cabeebbe..82ba4de9139 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -800,7 +800,7 @@ pyairvisual==2.0.1 pyalarmdotcom==0.3.2 # homeassistant.components.arlo -pyarlo==0.2.0 +pyarlo==0.2.2 # homeassistant.components.netatmo pyatmo==1.2 From b2faa67ab7c52f9492de9286ef468dfb6f896c81 Mon Sep 17 00:00:00 2001 From: Richard Patel Date: Sun, 21 Oct 2018 20:12:51 +0200 Subject: [PATCH 239/265] Add new rtorrent sensor (#17421) * New rtorrent sensor * Fix lint issue * Fix another lint issue * Fix pylint issue * how many python linters do you guys use * Cleanup code * python linting * newline --- .coveragerc | 1 + homeassistant/components/sensor/rtorrent.py | 127 ++++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 homeassistant/components/sensor/rtorrent.py diff --git a/.coveragerc b/.coveragerc index 4b65b28597e..0049349cfff 100644 --- a/.coveragerc +++ b/.coveragerc @@ -757,6 +757,7 @@ omit = homeassistant/components/sensor/radarr.py homeassistant/components/sensor/rainbird.py homeassistant/components/sensor/ripple.py + homeassistant/components/sensor/rtorrent.py homeassistant/components/sensor/scrape.py homeassistant/components/sensor/sense.py homeassistant/components/sensor/sensehat.py diff --git a/homeassistant/components/sensor/rtorrent.py b/homeassistant/components/sensor/rtorrent.py new file mode 100644 index 00000000000..f71b9c6dbdb --- /dev/null +++ b/homeassistant/components/sensor/rtorrent.py @@ -0,0 +1,127 @@ +"""Support for monitoring the rtorrent BitTorrent client API.""" +import logging +import xmlrpc.client + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_URL, CONF_NAME, + CONF_MONITORED_VARIABLES, STATE_IDLE) +from homeassistant.helpers.entity import Entity +import homeassistant.helpers.config_validation as cv +from homeassistant.exceptions import PlatformNotReady + +_LOGGER = logging.getLogger(__name__) + +SENSOR_TYPE_CURRENT_STATUS = 'current_status' +SENSOR_TYPE_DOWNLOAD_SPEED = 'download_speed' +SENSOR_TYPE_UPLOAD_SPEED = 'upload_speed' + +DEFAULT_NAME = 'rtorrent' +SENSOR_TYPES = { + SENSOR_TYPE_CURRENT_STATUS: ['Status', None], + SENSOR_TYPE_DOWNLOAD_SPEED: ['Down Speed', 'kB/s'], + SENSOR_TYPE_UPLOAD_SPEED: ['Up Speed', 'kB/s'], +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_URL): cv.url, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_MONITORED_VARIABLES, default=[]): vol.All( + cv.ensure_list, [vol.In(SENSOR_TYPES)]), +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the rtorrent sensors.""" + url = config[CONF_URL] + name = config[CONF_NAME] + + try: + rtorrent = xmlrpc.client.ServerProxy(url) + except (xmlrpc.client.ProtocolError, ConnectionRefusedError): + _LOGGER.error("Connection to rtorrent daemon failed") + raise PlatformNotReady + dev = [] + for variable in config[CONF_MONITORED_VARIABLES]: + dev.append(RTorrentSensor(variable, rtorrent, name)) + + add_entities(dev) + + +def format_speed(speed): + """Return a bytes/s measurement as a human readable string.""" + kb_spd = float(speed) / 1024 + return round(kb_spd, 2 if kb_spd < 0.1 else 1) + + +class RTorrentSensor(Entity): + """Representation of an rtorrent sensor.""" + + def __init__(self, sensor_type, rtorrent_client, client_name): + """Initialize the sensor.""" + self._name = SENSOR_TYPES[sensor_type][0] + self.client = rtorrent_client + self.type = sensor_type + self.client_name = client_name + self._state = None + self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self.data = None + self._available = False + + @property + def name(self): + """Return the name of the sensor.""" + return '{} {}'.format(self.client_name, self._name) + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def available(self): + """Return true if device is available.""" + return self._available + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self._unit_of_measurement + + def update(self): + """Get the latest data from rtorrent and updates the state.""" + multicall = xmlrpc.client.MultiCall(self.client) + multicall.throttle.global_up.rate() + multicall.throttle.global_down.rate() + + try: + self.data = multicall() + self._available = True + except (xmlrpc.client.ProtocolError, ConnectionRefusedError): + _LOGGER.error("Connection to rtorrent lost") + self._available = False + return + + upload = self.data[0] + download = self.data[1] + + if self.type == SENSOR_TYPE_CURRENT_STATUS: + if self.data: + if upload > 0 and download > 0: + self._state = 'Up/Down' + elif upload > 0 and download == 0: + self._state = 'Seeding' + elif upload == 0 and download > 0: + self._state = 'Downloading' + else: + self._state = STATE_IDLE + else: + self._state = None + + if self.data: + if self.type == SENSOR_TYPE_DOWNLOAD_SPEED: + self._state = format_speed(download) + elif self.type == SENSOR_TYPE_UPLOAD_SPEED: + self._state = format_speed(upload) From 8f529b20d7b98b1dbed4f11789f9f07453a6a463 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 21 Oct 2018 20:34:12 +0200 Subject: [PATCH 240/265] Bump frontend to 20181021.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index df25803b4e0..36bb3507dda 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20181018.0'] +REQUIREMENTS = ['home-assistant-frontend==20181021.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 82ba4de9139..84b8b2d57ce 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -466,7 +466,7 @@ hole==0.3.0 holidays==0.9.8 # homeassistant.components.frontend -home-assistant-frontend==20181018.0 +home-assistant-frontend==20181021.0 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 302db00f706..1568fd95607 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -97,7 +97,7 @@ hdate==0.6.5 holidays==0.9.8 # homeassistant.components.frontend -home-assistant-frontend==20181018.0 +home-assistant-frontend==20181021.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From 355005114b3315694e3a9654249d4a1998d76fcd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 21 Oct 2018 20:34:28 +0200 Subject: [PATCH 241/265] Update translations --- .../components/auth/.translations/ro.json | 34 +++++++++++++++++++ .../components/cast/.translations/ro.json | 15 ++++++++ .../components/hangouts/.translations/hu.json | 2 ++ .../components/hangouts/.translations/ro.json | 28 +++++++++++++++ .../homematicip_cloud/.translations/ro.json | 19 +++++++++++ .../components/hue/.translations/ro.json | 2 ++ .../components/ifttt/.translations/es.json | 11 ++++++ .../components/ifttt/.translations/hu.json | 3 ++ .../components/ifttt/.translations/pt.json | 18 ++++++++++ .../components/ifttt/.translations/ro.json | 4 +++ .../components/ios/.translations/ro.json | 14 ++++++++ .../components/lifx/.translations/es.json | 15 ++++++++ .../components/lifx/.translations/pt.json | 7 ++++ .../components/mqtt/.translations/es.json | 4 +++ .../components/mqtt/.translations/hu.json | 1 + .../components/mqtt/.translations/pt.json | 7 ++++ .../components/mqtt/.translations/ro.json | 31 +++++++++++++++++ .../components/nest/.translations/hu.json | 4 ++- .../components/nest/.translations/ro.json | 13 +++++++ .../components/openuv/.translations/ro.json | 20 +++++++++++ .../sensor/.translations/moon.ro.json | 6 ++++ .../simplisafe/.translations/es.json | 19 +++++++++++ .../simplisafe/.translations/hu.json | 9 +++-- .../simplisafe/.translations/pt.json | 19 +++++++++++ .../simplisafe/.translations/ro.json | 4 ++- .../components/smhi/.translations/es.json | 19 +++++++++++ .../components/smhi/.translations/hu.json | 12 +++++++ .../components/smhi/.translations/pt.json | 13 +++++++ .../components/smhi/.translations/ro.json | 3 +- .../components/sonos/.translations/ro.json | 15 ++++++++ .../components/tradfri/.translations/ro.json | 23 +++++++++++++ .../components/unifi/.translations/es.json | 26 ++++++++++++++ .../components/unifi/.translations/hu.json | 10 +++++- .../components/unifi/.translations/pt.json | 26 ++++++++++++++ .../components/unifi/.translations/ro.json | 24 +++++++++++++ .../components/upnp/.translations/es.json | 23 +++++++++++++ .../components/upnp/.translations/hu.json | 16 +++++++++ .../components/upnp/.translations/pt.json | 27 +++++++++++++++ .../components/zwave/.translations/es.json | 21 ++++++++++++ .../components/zwave/.translations/hu.json | 11 ++++++ .../components/zwave/.translations/pt.json | 22 ++++++++++++ 41 files changed, 594 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/auth/.translations/ro.json create mode 100644 homeassistant/components/cast/.translations/ro.json create mode 100644 homeassistant/components/hangouts/.translations/ro.json create mode 100644 homeassistant/components/homematicip_cloud/.translations/ro.json create mode 100644 homeassistant/components/ifttt/.translations/es.json create mode 100644 homeassistant/components/ifttt/.translations/pt.json create mode 100644 homeassistant/components/ios/.translations/ro.json create mode 100644 homeassistant/components/lifx/.translations/es.json create mode 100644 homeassistant/components/lifx/.translations/pt.json create mode 100644 homeassistant/components/mqtt/.translations/ro.json create mode 100644 homeassistant/components/nest/.translations/ro.json create mode 100644 homeassistant/components/openuv/.translations/ro.json create mode 100644 homeassistant/components/sensor/.translations/moon.ro.json create mode 100644 homeassistant/components/simplisafe/.translations/es.json create mode 100644 homeassistant/components/simplisafe/.translations/pt.json create mode 100644 homeassistant/components/smhi/.translations/es.json create mode 100644 homeassistant/components/smhi/.translations/hu.json create mode 100644 homeassistant/components/smhi/.translations/pt.json create mode 100644 homeassistant/components/sonos/.translations/ro.json create mode 100644 homeassistant/components/tradfri/.translations/ro.json create mode 100644 homeassistant/components/unifi/.translations/es.json create mode 100644 homeassistant/components/unifi/.translations/pt.json create mode 100644 homeassistant/components/unifi/.translations/ro.json create mode 100644 homeassistant/components/upnp/.translations/es.json create mode 100644 homeassistant/components/upnp/.translations/hu.json create mode 100644 homeassistant/components/upnp/.translations/pt.json create mode 100644 homeassistant/components/zwave/.translations/es.json create mode 100644 homeassistant/components/zwave/.translations/pt.json diff --git a/homeassistant/components/auth/.translations/ro.json b/homeassistant/components/auth/.translations/ro.json new file mode 100644 index 00000000000..19f9ec10c73 --- /dev/null +++ b/homeassistant/components/auth/.translations/ro.json @@ -0,0 +1,34 @@ +{ + "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Nu sunt disponibile servicii de notificare." + }, + "error": { + "invalid_code": "Cod invalid, va rugam incercati din nou." + }, + "step": { + "init": { + "description": "Selecta\u021bi unul dintre serviciile de notificare:", + "title": "Configura\u021bi o parol\u0103 unic\u0103 livrat\u0103 de o component\u0103 de notificare" + }, + "setup": { + "description": "O parol\u0103 unic\u0103 a fost trimis\u0103 prin **notify.{notify_service}**. Introduce\u021bi parola mai jos:", + "title": "Verifica\u021bi configurarea" + } + }, + "title": "Notifica\u021bi o parol\u0103 unic\u0103" + }, + "totp": { + "error": { + "invalid_code": "Cod invalid, va rugam incercati din nou. Dac\u0103 primi\u021bi aceast\u0103 eroare \u00een mod consecvent, asigura\u021bi-v\u0103 c\u0103 ceasul sistemului dvs. Home Assistant este corect." + }, + "step": { + "init": { + "title": "Configura\u021bi autentificarea cu doi factori utiliz\u00e2nd TOTP" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/ro.json b/homeassistant/components/cast/.translations/ro.json new file mode 100644 index 00000000000..8a1d19c0ecf --- /dev/null +++ b/homeassistant/components/cast/.translations/ro.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nu s-au g\u0103sit dispozitive Google Cast \u00een re\u021bea.", + "single_instance_allowed": "Este necesar\u0103 o singur\u0103 configura\u021bie a serviciului Google Cast." + }, + "step": { + "confirm": { + "description": "Dori\u021bi s\u0103 configura\u021bi Google Cast?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/.translations/hu.json b/homeassistant/components/hangouts/.translations/hu.json index 2631843c784..f6e46e25985 100644 --- a/homeassistant/components/hangouts/.translations/hu.json +++ b/homeassistant/components/hangouts/.translations/hu.json @@ -14,6 +14,7 @@ "data": { "2fa": "2FA Pin" }, + "description": "\u00dcres", "title": "K\u00e9tfaktoros Hiteles\u00edt\u00e9s" }, "user": { @@ -21,6 +22,7 @@ "email": "E-Mail C\u00edm", "password": "Jelsz\u00f3" }, + "description": "\u00dcres", "title": "Google Hangouts Bejelentkez\u00e9s" } }, diff --git a/homeassistant/components/hangouts/.translations/ro.json b/homeassistant/components/hangouts/.translations/ro.json new file mode 100644 index 00000000000..d1c3ed767ce --- /dev/null +++ b/homeassistant/components/hangouts/.translations/ro.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Google Hangouts este deja configurat", + "unknown": "Sa produs o eroare necunoscut\u0103." + }, + "error": { + "invalid_2fa_method": "Metoda 2FA invalid\u0103 (Verifica\u021bi pe telefon).", + "invalid_login": "Conectare invalid\u0103, \u00eencerca\u021bi din nou." + }, + "step": { + "2fa": { + "data": { + "2fa": "2FA Pin" + } + }, + "user": { + "data": { + "email": "Adresa de email", + "password": "Parol\u0103" + }, + "description": "Gol", + "title": "Conectare Google Hangouts" + } + }, + "title": "Google Hangouts" + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/ro.json b/homeassistant/components/homematicip_cloud/.translations/ro.json new file mode 100644 index 00000000000..a5399e7e68c --- /dev/null +++ b/homeassistant/components/homematicip_cloud/.translations/ro.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Punctul de acces este deja configurat", + "unknown": "Sa produs o eroare necunoscut\u0103." + }, + "error": { + "invalid_pin": "Cod PIN invalid, \u00eencerca\u021bi din nou.", + "press_the_button": "V\u0103 rug\u0103m s\u0103 ap\u0103sa\u021bi butonul albastru." + }, + "step": { + "init": { + "data": { + "pin": "Cod PIN (op\u021bional)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/ro.json b/homeassistant/components/hue/.translations/ro.json index 69cee1198d3..a2ecf8964b6 100644 --- a/homeassistant/components/hue/.translations/ro.json +++ b/homeassistant/components/hue/.translations/ro.json @@ -2,6 +2,8 @@ "config": { "abort": { "all_configured": "Toate pun\u021bile Philips Hue sunt deja configurate", + "already_configured": "Gateway-ul este deja configurat", + "cannot_connect": "Nu se poate conecta la gateway.", "discover_timeout": "Imposibil de descoperit podurile Hue" }, "error": { diff --git a/homeassistant/components/ifttt/.translations/es.json b/homeassistant/components/ifttt/.translations/es.json new file mode 100644 index 00000000000..13240ccefb1 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/es.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "description": "\u00bfEst\u00e1s seguro de que quieres configurar IFTTT?", + "title": "Configurar el applet de webhook IFTTT" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/hu.json b/homeassistant/components/ifttt/.translations/hu.json index a131f848d45..6ecf654ff47 100644 --- a/homeassistant/components/ifttt/.translations/hu.json +++ b/homeassistant/components/ifttt/.translations/hu.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "not_internet_accessible": "A Home Assistant-nek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l az IFTTT \u00fczenetek fogad\u00e1s\u00e1hoz." + }, "step": { "user": { "description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani az IFTTT-t?", diff --git a/homeassistant/components/ifttt/.translations/pt.json b/homeassistant/components/ifttt/.translations/pt.json new file mode 100644 index 00000000000..34c6496d7b1 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "A sua inst\u00e2ncia Home Assistent precisa de ser acess\u00edvel a partir da internet para receber mensagens IFTTT.", + "one_instance_allowed": "Apenas uma \u00fanica inst\u00e2ncia \u00e9 necess\u00e1ria." + }, + "create_entry": { + "default": "Para enviar eventos para o Home Assistente, precisa de utilizar a a\u00e7\u00e3o \"Make a web request\" no [IFTTT Webhook applet]({applet_url}).\n\nPreencha com a seguinte informa\u00e7\u00e3o:\n\n- URL: `{webhook_url}`\n- Method: POST \n- Content Type: application/json \n\nConsulte [a documenta\u00e7\u00e3o]({docs_url}) sobre como configurar automa\u00e7\u00f5es para lidar com dados de entrada." + }, + "step": { + "user": { + "description": "Tem certeza de que deseja configurar o IFTTT?", + "title": "Configurar o IFTTT Webhook Applet" + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/ro.json b/homeassistant/components/ifttt/.translations/ro.json index 03c77426671..dd7ae5f72cb 100644 --- a/homeassistant/components/ifttt/.translations/ro.json +++ b/homeassistant/components/ifttt/.translations/ro.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "not_internet_accessible": "Instan\u021ba Home Assistant trebuie s\u0103 fie accesibil\u0103 de pe internet pentru a primi mesaje IFTTT.", + "one_instance_allowed": "Este necesar\u0103 o singur\u0103 instan\u021b\u0103." + }, "step": { "user": { "description": "Sigur dori\u021bi s\u0103 configura\u021bi IFTTT?" diff --git a/homeassistant/components/ios/.translations/ro.json b/homeassistant/components/ios/.translations/ro.json new file mode 100644 index 00000000000..5a83b5cd732 --- /dev/null +++ b/homeassistant/components/ios/.translations/ro.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Este necesar\u0103 numai o singur\u0103 configurare a aplica\u021biei Home Assistant iOS." + }, + "step": { + "confirm": { + "description": "Dori\u021bi s\u0103 configura\u021bi componenta Home Assistant iOS?", + "title": "Home Assistant iOS" + } + }, + "title": "Home Assistant iOS" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/es.json b/homeassistant/components/lifx/.translations/es.json new file mode 100644 index 00000000000..f897c673432 --- /dev/null +++ b/homeassistant/components/lifx/.translations/es.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No se encontraron dispositivos LIFX en la red.", + "single_instance_allowed": "S\u00f3lo es posible una \u00fanica configuraci\u00f3n de LIFX." + }, + "step": { + "confirm": { + "description": "\u00bfQuieres configurar LIFX?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/pt.json b/homeassistant/components/lifx/.translations/pt.json new file mode 100644 index 00000000000..5d7fdf356ef --- /dev/null +++ b/homeassistant/components/lifx/.translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nenhum dispositivo LIFX encontrado na rede." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/es.json b/homeassistant/components/mqtt/.translations/es.json index e9e869ae966..182cce86057 100644 --- a/homeassistant/components/mqtt/.translations/es.json +++ b/homeassistant/components/mqtt/.translations/es.json @@ -2,6 +2,10 @@ "config": { "step": { "hassio_confirm": { + "data": { + "discovery": "Habilitar descubrimiento" + }, + "description": "\u00bfDesea configurar Home Assistant para conectarse al agente MQTT provisto por el complemento hass.io {addon} ?", "title": "MQTT Broker a trav\u00e9s del complemento Hass.io" } } diff --git a/homeassistant/components/mqtt/.translations/hu.json b/homeassistant/components/mqtt/.translations/hu.json index ba08d36d581..f08c601633e 100644 --- a/homeassistant/components/mqtt/.translations/hu.json +++ b/homeassistant/components/mqtt/.translations/hu.json @@ -10,6 +10,7 @@ "broker": { "data": { "broker": "Br\u00f3ker", + "discovery": "Felfedez\u00e9s enged\u00e9lyez\u00e9se", "password": "Jelsz\u00f3", "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" diff --git a/homeassistant/components/mqtt/.translations/pt.json b/homeassistant/components/mqtt/.translations/pt.json index 1b8c3946b7c..3b36345994d 100644 --- a/homeassistant/components/mqtt/.translations/pt.json +++ b/homeassistant/components/mqtt/.translations/pt.json @@ -17,6 +17,13 @@ }, "description": "Por favor, insira os detalhes de liga\u00e7\u00e3o ao seu broker MQTT.", "title": "" + }, + "hassio_confirm": { + "data": { + "discovery": "Ativar descoberta" + }, + "description": "Deseja configurar o Home Assistant para se ligar ao broker MQTT fornecido pelo add-on hass.io {addon}?", + "title": "MQTT Broker atrav\u00e9s do add-on Hass.io" } }, "title": "" diff --git a/homeassistant/components/mqtt/.translations/ro.json b/homeassistant/components/mqtt/.translations/ro.json new file mode 100644 index 00000000000..bcd150e3063 --- /dev/null +++ b/homeassistant/components/mqtt/.translations/ro.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Este permis\u0103 numai o singur\u0103 configura\u021bie de MQTT." + }, + "error": { + "cannot_connect": "Imposibil de conectat la broker." + }, + "step": { + "broker": { + "data": { + "broker": "Broker", + "discovery": "Activa\u021bi descoperirea", + "password": "Parol\u0103", + "port": "Port", + "username": "Nume de utilizator" + }, + "description": "Introduce\u021bi informa\u021biile de conectare ale brokerului dvs. MQTT.", + "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "Activa\u021bi descoperirea" + }, + "description": "Dori\u021bi s\u0103 configura\u021bi Home Assistant pentru a v\u0103 conecta la brokerul MQTT furnizat de addon-ul {addon} ?", + "title": "MQTT Broker, prin intermediul Hass.io add-on" + } + }, + "title": "MQTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/hu.json b/homeassistant/components/nest/.translations/hu.json index 142747a016f..aa99b46e576 100644 --- a/homeassistant/components/nest/.translations/hu.json +++ b/homeassistant/components/nest/.translations/hu.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_setup": "Csak egy Nest-fi\u00f3kot konfigur\u00e1lhat.", "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n." }, "error": { @@ -18,7 +19,8 @@ "link": { "data": { "code": "PIN-k\u00f3d" - } + }, + "title": "Nest fi\u00f3k \u00f6sszekapcsol\u00e1sa" } }, "title": "Nest" diff --git a/homeassistant/components/nest/.translations/ro.json b/homeassistant/components/nest/.translations/ro.json new file mode 100644 index 00000000000..f315cf549fb --- /dev/null +++ b/homeassistant/components/nest/.translations/ro.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "link": { + "data": { + "code": "Cod PIN" + }, + "title": "Leg\u0103tur\u0103 cont Nest" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/ro.json b/homeassistant/components/openuv/.translations/ro.json new file mode 100644 index 00000000000..976221188d3 --- /dev/null +++ b/homeassistant/components/openuv/.translations/ro.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "identifier_exists": "Coordonatele deja \u00eenregistrate", + "invalid_api_key": "Cheie API invalid\u0103" + }, + "step": { + "user": { + "data": { + "api_key": "Cheie API OpenUV", + "elevation": "Altitudine", + "latitude": "Latitudine", + "longitude": "Longitudine" + }, + "title": "Completa\u021bi informa\u021biile dvs." + } + }, + "title": "OpenUV" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.ro.json b/homeassistant/components/sensor/.translations/moon.ro.json new file mode 100644 index 00000000000..6f64e497c74 --- /dev/null +++ b/homeassistant/components/sensor/.translations/moon.ro.json @@ -0,0 +1,6 @@ +{ + "state": { + "full_moon": "Lun\u0103 plin\u0103", + "new_moon": "Lun\u0103 nou\u0103" + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/es.json b/homeassistant/components/simplisafe/.translations/es.json new file mode 100644 index 00000000000..12d0f63356f --- /dev/null +++ b/homeassistant/components/simplisafe/.translations/es.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Cuenta ya registrada", + "invalid_credentials": "Credenciales no v\u00e1lidas" + }, + "step": { + "user": { + "data": { + "code": "C\u00f3digo (para Home Assistant)", + "password": "Contrase\u00f1a", + "username": "Direcci\u00f3n de correo electr\u00f3nico" + }, + "title": "Rellene sus datos" + } + }, + "title": "SimpliSafe" + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/hu.json b/homeassistant/components/simplisafe/.translations/hu.json index ff2c2fc87b5..103bf4e18d0 100644 --- a/homeassistant/components/simplisafe/.translations/hu.json +++ b/homeassistant/components/simplisafe/.translations/hu.json @@ -1,10 +1,15 @@ { "config": { + "error": { + "invalid_credentials": "\u00c9rv\u00e9nytelen hiteles\u00edt\u0151 adatok" + }, "step": { "user": { "data": { - "password": "Jelsz\u00f3" - } + "password": "Jelsz\u00f3", + "username": "Email c\u00edm" + }, + "title": "T\u00f6ltsd ki az adataid" } } } diff --git a/homeassistant/components/simplisafe/.translations/pt.json b/homeassistant/components/simplisafe/.translations/pt.json new file mode 100644 index 00000000000..47929161976 --- /dev/null +++ b/homeassistant/components/simplisafe/.translations/pt.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Conta j\u00e1 registada", + "invalid_credentials": "Credenciais inv\u00e1lidas" + }, + "step": { + "user": { + "data": { + "code": "C\u00f3digo (para Home Assistant)", + "password": "Palavra-passe", + "username": "Endere\u00e7o de e-mail" + }, + "title": "Preencha as suas informa\u00e7\u00f5es" + } + }, + "title": "SimpliSafe" + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/ro.json b/homeassistant/components/simplisafe/.translations/ro.json index 7046b0992b1..b7e281a2bc2 100644 --- a/homeassistant/components/simplisafe/.translations/ro.json +++ b/homeassistant/components/simplisafe/.translations/ro.json @@ -7,11 +7,13 @@ "step": { "user": { "data": { + "code": "Cod (pentru Home Assistant)", "password": "Parola", "username": "Adresa de email" }, "title": "Completa\u021bi informa\u021biile dvs." } - } + }, + "title": "SimpliSafe" } } \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/es.json b/homeassistant/components/smhi/.translations/es.json new file mode 100644 index 00000000000..627c534f6dd --- /dev/null +++ b/homeassistant/components/smhi/.translations/es.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Nombre ya existe", + "wrong_location": "Ubicaci\u00f3n Suecia solamente" + }, + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nombre" + }, + "title": "Ubicaci\u00f3n en Suecia" + } + }, + "title": "Servicio meteorol\u00f3gico sueco (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/hu.json b/homeassistant/components/smhi/.translations/hu.json new file mode 100644 index 00000000000..740fc1a8179 --- /dev/null +++ b/homeassistant/components/smhi/.translations/hu.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/pt.json b/homeassistant/components/smhi/.translations/pt.json new file mode 100644 index 00000000000..a5c71885906 --- /dev/null +++ b/homeassistant/components/smhi/.translations/pt.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nome" + }, + "title": "Localiza\u00e7\u00e3o na Su\u00e9cia" + } + }, + "title": "Servi\u00e7o meteorol\u00f3gico sueco (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/ro.json b/homeassistant/components/smhi/.translations/ro.json index 6fe28787655..6249e49d2d7 100644 --- a/homeassistant/components/smhi/.translations/ro.json +++ b/homeassistant/components/smhi/.translations/ro.json @@ -1,7 +1,8 @@ { "config": { "error": { - "name_exists": "Numele exist\u0103 deja" + "name_exists": "Numele exist\u0103 deja", + "wrong_location": "Loca\u021bia numai \u00een Suedia" }, "step": { "user": { diff --git a/homeassistant/components/sonos/.translations/ro.json b/homeassistant/components/sonos/.translations/ro.json new file mode 100644 index 00000000000..e442ab9504e --- /dev/null +++ b/homeassistant/components/sonos/.translations/ro.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nu exist\u0103 dispozitive Sonos g\u0103site \u00een re\u021bea.", + "single_instance_allowed": "Este necesar\u0103 o singur\u0103 configurare a Sonos." + }, + "step": { + "confirm": { + "description": "Dori\u021bi s\u0103 configura\u021bi Sonos?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/.translations/ro.json b/homeassistant/components/tradfri/.translations/ro.json new file mode 100644 index 00000000000..cea0e6d938f --- /dev/null +++ b/homeassistant/components/tradfri/.translations/ro.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Bridge-ul este deja configurat" + }, + "error": { + "cannot_connect": "Nu se poate conecta la gateway.", + "invalid_key": "Nu s-a \u00eenregistrat cu cheia furnizat\u0103. Dac\u0103 acest lucru se \u00eent\u00e2mpl\u0103 \u00een continuare, \u00eencerca\u021bi s\u0103 reporni\u021bi gateway-ul.", + "timeout": "Timeout la validarea codului." + }, + "step": { + "auth": { + "data": { + "host": "Gazd\u0103", + "security_code": "Cod de securitate" + }, + "description": "Pute\u021bi g\u0103si codul de securitate pe spatele gateway-ului.", + "title": "Introduce\u021bi codul de securitate" + } + }, + "title": "IKEA TR\u00c5DFRI" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/es.json b/homeassistant/components/unifi/.translations/es.json new file mode 100644 index 00000000000..4f570fe1386 --- /dev/null +++ b/homeassistant/components/unifi/.translations/es.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "El sitio del controlador ya est\u00e1 configurado", + "user_privilege": "El usuario debe ser administrador" + }, + "error": { + "faulty_credentials": "Credenciales de usuario incorrectas", + "service_unavailable": "Servicio No disponible" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Contrase\u00f1a", + "port": "Puerto", + "site": "ID del sitio", + "username": "Nombre de usuario", + "verify_ssl": "Controlador usando el certificado adecuado" + }, + "title": "Configurar el controlador UniFi" + } + }, + "title": "Controlador UniFi" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/hu.json b/homeassistant/components/unifi/.translations/hu.json index f5827c47353..06104c6ed6c 100644 --- a/homeassistant/components/unifi/.translations/hu.json +++ b/homeassistant/components/unifi/.translations/hu.json @@ -1,10 +1,18 @@ { "config": { + "abort": { + "user_privilege": "A felhaszn\u00e1l\u00f3nak rendszergazd\u00e1nak kell lennie" + }, + "error": { + "faulty_credentials": "Rossz felhaszn\u00e1l\u00f3i hiteles\u00edt\u0151 adatok", + "service_unavailable": "Nincs el\u00e9rhet\u0151 szolg\u00e1ltat\u00e1s" + }, "step": { "user": { "data": { "password": "Jelsz\u00f3", - "port": "Port" + "port": "Port", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" } } } diff --git a/homeassistant/components/unifi/.translations/pt.json b/homeassistant/components/unifi/.translations/pt.json new file mode 100644 index 00000000000..6730a3d258e --- /dev/null +++ b/homeassistant/components/unifi/.translations/pt.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "O site do controlador j\u00e1 se encontra configurado", + "user_privilege": "Utilizador tem que ser administrador" + }, + "error": { + "faulty_credentials": "Credenciais do utilizador erradas", + "service_unavailable": "Nenhum servi\u00e7o dispon\u00edvel" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "password": "Palavra-passe", + "port": "Porto", + "site": "Site ID", + "username": "Nome do utilizador", + "verify_ssl": "Controlador com certificados adequados" + }, + "title": "Configurar o controlador UniFi" + } + }, + "title": "Controlador UniFi" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/ro.json b/homeassistant/components/unifi/.translations/ro.json new file mode 100644 index 00000000000..99b1ac57e0b --- /dev/null +++ b/homeassistant/components/unifi/.translations/ro.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "user_privilege": "Utilizatorul trebuie s\u0103 fie administrator" + }, + "error": { + "faulty_credentials": "Credentiale utilizator invalide", + "service_unavailable": "Nici un serviciu disponibil" + }, + "step": { + "user": { + "data": { + "host": "Gazd\u0103", + "password": "Parol\u0103", + "port": "Port", + "username": "Nume de utilizator", + "verify_ssl": "Controler utiliz\u00e2nd certificatul adecvat" + }, + "title": "Configura\u021bi un controler UniFi" + } + }, + "title": "Controler UniFi" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/es.json b/homeassistant/components/upnp/.translations/es.json new file mode 100644 index 00000000000..e4cabf4cd50 --- /dev/null +++ b/homeassistant/components/upnp/.translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP / IGD ya est\u00e1 configurado", + "no_devices_discovered": "No se descubrieron UPnP / IGDs", + "no_sensors_or_port_mapping": "Habilitar al menos sensores o mapeo de puertos" + }, + "step": { + "init": { + "title": "UPnP / IGD" + }, + "user": { + "data": { + "enable_port_mapping": "Habilitar la asignaci\u00f3n de puertos para Home Assistant", + "enable_sensors": "A\u00f1adir sensores de tr\u00e1fico", + "igd": "UPnP / IGD" + }, + "title": "Opciones de configuraci\u00f3n para UPnP/IGD" + } + }, + "title": "UPnP / IGD" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/hu.json b/homeassistant/components/upnp/.translations/hu.json new file mode 100644 index 00000000000..a2bf78a7f3e --- /dev/null +++ b/homeassistant/components/upnp/.translations/hu.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "init": { + "title": "UPnP/IGD" + }, + "user": { + "data": { + "igd": "UPnP/IGD" + }, + "title": "Az UPnP/IGD be\u00e1ll\u00edt\u00e1si lehet\u0151s\u00e9gei" + } + }, + "title": "UPnP/IGD" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/pt.json b/homeassistant/components/upnp/.translations/pt.json new file mode 100644 index 00000000000..5e9b516d1c2 --- /dev/null +++ b/homeassistant/components/upnp/.translations/pt.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP/IGD j\u00e1 est\u00e1 configurado", + "no_devices_discovered": "Nenhum UPnP/IGDs descoberto", + "no_sensors_or_port_mapping": "Ative pelo menos os sensores ou o mapeamento de porta" + }, + "error": { + "one": "um", + "other": "v\u00e1rios" + }, + "step": { + "init": { + "title": "" + }, + "user": { + "data": { + "enable_port_mapping": "Ativar o mapeamento de porta para o Home Assistant", + "enable_sensors": "Adicionar sensores de tr\u00e1fego", + "igd": "" + }, + "title": "Op\u00e7\u00f5es de configura\u00e7\u00e3o para o UPnP/IGD" + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/es.json b/homeassistant/components/zwave/.translations/es.json new file mode 100644 index 00000000000..8c287d9a539 --- /dev/null +++ b/homeassistant/components/zwave/.translations/es.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "one_instance_only": "El componente solo admite una instancia de Z-Wave" + }, + "error": { + "option_error": "Z-Wave error de validaci\u00f3n. \u00bfLa ruta de acceso a la memoria USB escorrecta?" + }, + "step": { + "user": { + "data": { + "network_key": "Clave de red (d\u00e9jelo en blanco para generar autom\u00e1ticamente)", + "usb_path": "Ruta USB" + }, + "description": "Consulte https://www.home-assistant.io/docs/z-wave/installation/ para obtener informaci\u00f3n sobre las variables de configuraci\u00f3n", + "title": "Configurar Z-Wave" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/hu.json b/homeassistant/components/zwave/.translations/hu.json index 16c25cb7cab..e2acc5f9115 100644 --- a/homeassistant/components/zwave/.translations/hu.json +++ b/homeassistant/components/zwave/.translations/hu.json @@ -1,5 +1,16 @@ { "config": { + "abort": { + "already_configured": "A Z-Wave m\u00e1r konfigur\u00e1lva van" + }, + "step": { + "user": { + "data": { + "network_key": "H\u00e1l\u00f3zati kulcs (hagyja \u00fcresen az automatikus gener\u00e1l\u00e1shoz)", + "usb_path": "USB el\u00e9r\u00e9si \u00fat" + } + } + }, "title": "Z-Wave" } } \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/pt.json b/homeassistant/components/zwave/.translations/pt.json new file mode 100644 index 00000000000..6962f077498 --- /dev/null +++ b/homeassistant/components/zwave/.translations/pt.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "O Z-Wave j\u00e1 est\u00e1 configurado", + "one_instance_only": "Componente suporta apenas uma inst\u00e2ncia Z-Wave" + }, + "error": { + "option_error": "A valida\u00e7\u00e3o Z-Wave falhou. O caminho para o stick USB est\u00e1 correto?" + }, + "step": { + "user": { + "data": { + "network_key": "Network Key (deixe em branco para auto-gera\u00e7\u00e3o)", + "usb_path": "Endere\u00e7o USB" + }, + "description": "Consulte https://www.home-assistant.io/docs/z-wave/installation/ para obter informa\u00e7\u00f5es sobre as vari\u00e1veis de configura\u00e7\u00e3o", + "title": "Configurar o Z-Wave" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file From ebaf7f8c00db2bd5de793cbd62a4e8c9070d362c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 21 Oct 2018 20:34:50 +0200 Subject: [PATCH 242/265] Bumped version to 0.81.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4dcc171d35c..d44a5139885 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 81 -PATCH_VERSION = '0.dev0' +PATCH_VERSION = '0b0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 02f55b039c45d11881651a8a802b2f090ece3c71 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 23 Oct 2018 14:03:38 +0200 Subject: [PATCH 243/265] Update frontend to 20181023.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 36bb3507dda..55aa0700bef 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20181021.0'] +REQUIREMENTS = ['home-assistant-frontend==20181023.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 84b8b2d57ce..90c0dd8c44c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -466,7 +466,7 @@ hole==0.3.0 holidays==0.9.8 # homeassistant.components.frontend -home-assistant-frontend==20181021.0 +home-assistant-frontend==20181023.0 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1568fd95607..e8fc3517121 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -97,7 +97,7 @@ hdate==0.6.5 holidays==0.9.8 # homeassistant.components.frontend -home-assistant-frontend==20181021.0 +home-assistant-frontend==20181023.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From 86ecd7555a4cbac2aead03aa1148f30c4095f872 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 23 Oct 2018 14:04:25 +0200 Subject: [PATCH 244/265] Update translations --- .../components/ifttt/.translations/tr.json | 5 ++++ .../components/mailgun/.translations/en.json | 18 +++++++++++++++ .../components/mailgun/.translations/lb.json | 18 +++++++++++++++ .../components/mqtt/.translations/tr.json | 11 +++++++++ .../simplisafe/.translations/nl.json | 19 +++++++++++++++ .../simplisafe/.translations/tr.json | 12 ++++++++++ .../components/smhi/.translations/tr.json | 16 +++++++++++++ .../components/unifi/.translations/nl.json | 23 +++++++++++++++++++ .../components/unifi/.translations/tr.json | 12 ++++++++++ .../components/upnp/.translations/tr.json | 11 +++++++++ .../components/zwave/.translations/tr.json | 11 +++++++++ 11 files changed, 156 insertions(+) create mode 100644 homeassistant/components/ifttt/.translations/tr.json create mode 100644 homeassistant/components/mailgun/.translations/en.json create mode 100644 homeassistant/components/mailgun/.translations/lb.json create mode 100644 homeassistant/components/mqtt/.translations/tr.json create mode 100644 homeassistant/components/simplisafe/.translations/nl.json create mode 100644 homeassistant/components/simplisafe/.translations/tr.json create mode 100644 homeassistant/components/smhi/.translations/tr.json create mode 100644 homeassistant/components/unifi/.translations/nl.json create mode 100644 homeassistant/components/unifi/.translations/tr.json create mode 100644 homeassistant/components/upnp/.translations/tr.json create mode 100644 homeassistant/components/zwave/.translations/tr.json diff --git a/homeassistant/components/ifttt/.translations/tr.json b/homeassistant/components/ifttt/.translations/tr.json new file mode 100644 index 00000000000..80188b637f9 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/tr.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "IFTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/en.json b/homeassistant/components/mailgun/.translations/en.json new file mode 100644 index 00000000000..3abb8aba726 --- /dev/null +++ b/homeassistant/components/mailgun/.translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive Mailgun messages.", + "one_instance_allowed": "Only a single instance is necessary." + }, + "create_entry": { + "default": "To send events to Home Assistant, you will need to setup [Webhooks with Mailgun]({mailgun_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data." + }, + "step": { + "user": { + "description": "Are you sure you want to set up Mailgun?", + "title": "Set up the Mailgun Webhook" + } + }, + "title": "Mailgun" + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/lb.json b/homeassistant/components/mailgun/.translations/lb.json new file mode 100644 index 00000000000..f84225444d9 --- /dev/null +++ b/homeassistant/components/mailgun/.translations/lb.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "\u00c4r Home Assistant Instanz muss iwwert Internet accessibel si fir Mailgun Noriichten z'empf\u00e4nken.", + "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg." + }, + "create_entry": { + "default": "Fir Evenementer un Home Assistant ze sch\u00e9cken, mussen [Webhooks mat Mailgun]({mailgun_url}) ageriicht ginn.\n\nF\u00ebllt folgend Informatiounen aus:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\nLiest [Dokumentatioun]({docs_url}) w\u00e9i een Automatiounen ariicht welch eingehend Donn\u00e9\u00eb trait\u00e9ieren." + }, + "step": { + "user": { + "description": "S\u00e9cher fir Mailgun anzeriichten?", + "title": "Mailgun Webhook ariichten" + } + }, + "title": "Mailgun" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/tr.json b/homeassistant/components/mqtt/.translations/tr.json new file mode 100644 index 00000000000..1b73b94d5a4 --- /dev/null +++ b/homeassistant/components/mqtt/.translations/tr.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "hassio_confirm": { + "data": { + "discovery": "Ke\u015ffetmeyi etkinle\u015ftir" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/nl.json b/homeassistant/components/simplisafe/.translations/nl.json new file mode 100644 index 00000000000..c84593c0b23 --- /dev/null +++ b/homeassistant/components/simplisafe/.translations/nl.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Account bestaat al", + "invalid_credentials": "Ongeldige gebruikersgegevens" + }, + "step": { + "user": { + "data": { + "code": "Code (voor Home Assistant)", + "password": "Wachtwoord", + "username": "E-mailadres" + }, + "title": "Vul uw gegevens in" + } + }, + "title": "SimpliSafe" + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/tr.json b/homeassistant/components/simplisafe/.translations/tr.json new file mode 100644 index 00000000000..ec84b1b7c1c --- /dev/null +++ b/homeassistant/components/simplisafe/.translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Parola", + "username": "E-posta adresi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/tr.json b/homeassistant/components/smhi/.translations/tr.json new file mode 100644 index 00000000000..bb50f1e2a8d --- /dev/null +++ b/homeassistant/components/smhi/.translations/tr.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "name_exists": "Bu ad zaten var", + "wrong_location": "Konum sadece \u0130sve\u00e7" + }, + "step": { + "user": { + "data": { + "latitude": "Enlem", + "longitude": "Boylam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/nl.json b/homeassistant/components/unifi/.translations/nl.json new file mode 100644 index 00000000000..8e87dc4b2a6 --- /dev/null +++ b/homeassistant/components/unifi/.translations/nl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "user_privilege": "Gebruiker moet beheerder zijn" + }, + "error": { + "faulty_credentials": "Foutieve gebruikersgegevens", + "service_unavailable": "Geen service beschikbaar" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Wachtwoord", + "port": "Poort", + "username": "Gebruikersnaam" + }, + "title": "Stel de UniFi-controller in" + } + }, + "title": "UniFi-controller" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/tr.json b/homeassistant/components/unifi/.translations/tr.json new file mode 100644 index 00000000000..667a5e676fb --- /dev/null +++ b/homeassistant/components/unifi/.translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/tr.json b/homeassistant/components/upnp/.translations/tr.json new file mode 100644 index 00000000000..91503c17a07 --- /dev/null +++ b/homeassistant/components/upnp/.translations/tr.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "enable_sensors": "Trafik sens\u00f6rleri ekleyin" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/tr.json b/homeassistant/components/zwave/.translations/tr.json new file mode 100644 index 00000000000..c9762784d52 --- /dev/null +++ b/homeassistant/components/zwave/.translations/tr.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "network_key": "A\u011f Anajtar\u0131 (otomatik \u00fcretilmesi i\u00e7in bo\u015f b\u0131rak\u0131n\u0131z)" + } + } + } + } +} \ No newline at end of file From 23316a8344bad8fb4aed2e34d3983c4c786f2d9f Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Tue, 23 Oct 2018 05:01:01 +1100 Subject: [PATCH 245/265] Geo location trigger added (#16967) * zone trigger supports entity id pattern * fixed lint error * fixed test code * initial version of new geo_location trigger * revert to original * simplified code and added tests * refactored geo_location trigger to be based on a source defined by the entity * amended test cases * small refactorings --- .../components/automation/geo_location.py | 74 +++++ homeassistant/const.py | 1 + .../automation/test_geo_location.py | 271 ++++++++++++++++++ 3 files changed, 346 insertions(+) create mode 100644 homeassistant/components/automation/geo_location.py create mode 100644 tests/components/automation/test_geo_location.py diff --git a/homeassistant/components/automation/geo_location.py b/homeassistant/components/automation/geo_location.py new file mode 100644 index 00000000000..b2c9a9c093a --- /dev/null +++ b/homeassistant/components/automation/geo_location.py @@ -0,0 +1,74 @@ +""" +Offer geo location automation rules. + +For more details about this automation trigger, please refer to the +documentation at +https://home-assistant.io/docs/automation/trigger/#geo-location-trigger +""" +import voluptuous as vol + +from homeassistant.components.geo_location import DOMAIN +from homeassistant.core import callback +from homeassistant.const import ( + CONF_EVENT, CONF_PLATFORM, CONF_SOURCE, CONF_ZONE, EVENT_STATE_CHANGED) +from homeassistant.helpers import ( + condition, config_validation as cv) +from homeassistant.helpers.config_validation import entity_domain + +EVENT_ENTER = 'enter' +EVENT_LEAVE = 'leave' +DEFAULT_EVENT = EVENT_ENTER + +TRIGGER_SCHEMA = vol.Schema({ + vol.Required(CONF_PLATFORM): 'geo_location', + vol.Required(CONF_SOURCE): cv.string, + vol.Required(CONF_ZONE): entity_domain('zone'), + vol.Required(CONF_EVENT, default=DEFAULT_EVENT): + vol.Any(EVENT_ENTER, EVENT_LEAVE), +}) + + +def source_match(state, source): + """Check if the state matches the provided source.""" + return state and state.attributes.get('source') == source + + +async def async_trigger(hass, config, action): + """Listen for state changes based on configuration.""" + source = config.get(CONF_SOURCE).lower() + zone_entity_id = config.get(CONF_ZONE) + trigger_event = config.get(CONF_EVENT) + + @callback + def state_change_listener(event): + """Handle specific state changes.""" + # Skip if the event is not a geo_location entity. + if not event.data.get('entity_id').startswith(DOMAIN): + return + # Skip if the event's source does not match the trigger's source. + from_state = event.data.get('old_state') + to_state = event.data.get('new_state') + if not source_match(from_state, source) \ + and not source_match(to_state, source): + return + + zone_state = hass.states.get(zone_entity_id) + from_match = condition.zone(hass, zone_state, from_state) + to_match = condition.zone(hass, zone_state, to_state) + + # pylint: disable=too-many-boolean-expressions + if trigger_event == EVENT_ENTER and not from_match and to_match or \ + trigger_event == EVENT_LEAVE and from_match and not to_match: + hass.async_run_job(action({ + 'trigger': { + 'platform': 'geo_location', + 'source': source, + 'entity_id': event.data.get('entity_id'), + 'from_state': from_state, + 'to_state': to_state, + 'zone': zone_state, + 'event': trigger_event, + }, + }, context=event.context)) + + return hass.bus.async_listen(EVENT_STATE_CHANGED, state_change_listener) diff --git a/homeassistant/const.py b/homeassistant/const.py index d44a5139885..ae4a2b052bc 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -129,6 +129,7 @@ CONF_SENSOR_TYPE = 'sensor_type' CONF_SENSORS = 'sensors' CONF_SHOW_ON_MAP = 'show_on_map' CONF_SLAVE = 'slave' +CONF_SOURCE = 'source' CONF_SSL = 'ssl' CONF_STATE = 'state' CONF_STATE_TEMPLATE = 'state_template' diff --git a/tests/components/automation/test_geo_location.py b/tests/components/automation/test_geo_location.py new file mode 100644 index 00000000000..130cdeef99c --- /dev/null +++ b/tests/components/automation/test_geo_location.py @@ -0,0 +1,271 @@ +"""The tests for the geo location trigger.""" +import unittest + +from homeassistant.components import automation, zone +from homeassistant.core import callback, Context +from homeassistant.setup import setup_component + +from tests.common import get_test_home_assistant, mock_component +from tests.components.automation import common + + +class TestAutomationGeoLocation(unittest.TestCase): + """Test the geo location trigger.""" + + def setUp(self): + """Set up things to be run when tests are started.""" + self.hass = get_test_home_assistant() + mock_component(self.hass, 'group') + assert setup_component(self.hass, zone.DOMAIN, { + 'zone': { + 'name': 'test', + 'latitude': 32.880837, + 'longitude': -117.237561, + 'radius': 250, + } + }) + + self.calls = [] + + @callback + def record_call(service): + """Record calls.""" + self.calls.append(service) + + self.hass.services.register('test', 'automation', record_call) + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + def test_if_fires_on_zone_enter(self): + """Test for firing on zone enter.""" + context = Context() + self.hass.states.set('geo_location.entity', 'hello', { + 'latitude': 32.881011, + 'longitude': -117.234758, + 'source': 'test_source' + }) + self.hass.block_till_done() + + assert setup_component(self.hass, automation.DOMAIN, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'geo_location', + 'source': 'test_source', + 'zone': 'zone.test', + 'event': 'enter', + }, + 'action': { + 'service': 'test.automation', + 'data_template': { + 'some': '{{ trigger.%s }}' % '}} - {{ trigger.'.join(( + 'platform', 'entity_id', + 'from_state.state', 'to_state.state', + 'zone.name')) + }, + + } + } + }) + + self.hass.states.set('geo_location.entity', 'hello', { + 'latitude': 32.880586, + 'longitude': -117.237564 + }, context=context) + self.hass.block_till_done() + + self.assertEqual(1, len(self.calls)) + assert self.calls[0].context is context + self.assertEqual( + 'geo_location - geo_location.entity - hello - hello - test', + self.calls[0].data['some']) + + # Set out of zone again so we can trigger call + self.hass.states.set('geo_location.entity', 'hello', { + 'latitude': 32.881011, + 'longitude': -117.234758 + }) + self.hass.block_till_done() + + common.turn_off(self.hass) + self.hass.block_till_done() + + self.hass.states.set('geo_location.entity', 'hello', { + 'latitude': 32.880586, + 'longitude': -117.237564 + }) + self.hass.block_till_done() + + self.assertEqual(1, len(self.calls)) + + def test_if_not_fires_for_enter_on_zone_leave(self): + """Test for not firing on zone leave.""" + self.hass.states.set('geo_location.entity', 'hello', { + 'latitude': 32.880586, + 'longitude': -117.237564, + 'source': 'test_source' + }) + self.hass.block_till_done() + + assert setup_component(self.hass, automation.DOMAIN, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'geo_location', + 'source': 'test_source', + 'zone': 'zone.test', + 'event': 'enter', + }, + 'action': { + 'service': 'test.automation', + } + } + }) + + self.hass.states.set('geo_location.entity', 'hello', { + 'latitude': 32.881011, + 'longitude': -117.234758 + }) + self.hass.block_till_done() + + self.assertEqual(0, len(self.calls)) + + def test_if_fires_on_zone_leave(self): + """Test for firing on zone leave.""" + self.hass.states.set('geo_location.entity', 'hello', { + 'latitude': 32.880586, + 'longitude': -117.237564, + 'source': 'test_source' + }) + self.hass.block_till_done() + + assert setup_component(self.hass, automation.DOMAIN, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'geo_location', + 'source': 'test_source', + 'zone': 'zone.test', + 'event': 'leave', + }, + 'action': { + 'service': 'test.automation', + } + } + }) + + self.hass.states.set('geo_location.entity', 'hello', { + 'latitude': 32.881011, + 'longitude': -117.234758, + 'source': 'test_source' + }) + self.hass.block_till_done() + + self.assertEqual(1, len(self.calls)) + + def test_if_not_fires_for_leave_on_zone_enter(self): + """Test for not firing on zone enter.""" + self.hass.states.set('geo_location.entity', 'hello', { + 'latitude': 32.881011, + 'longitude': -117.234758, + 'source': 'test_source' + }) + self.hass.block_till_done() + + assert setup_component(self.hass, automation.DOMAIN, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'geo_location', + 'source': 'test_source', + 'zone': 'zone.test', + 'event': 'leave', + }, + 'action': { + 'service': 'test.automation', + } + } + }) + + self.hass.states.set('geo_location.entity', 'hello', { + 'latitude': 32.880586, + 'longitude': -117.237564 + }) + self.hass.block_till_done() + + self.assertEqual(0, len(self.calls)) + + def test_if_fires_on_zone_appear(self): + """Test for firing if entity appears in zone.""" + assert setup_component(self.hass, automation.DOMAIN, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'geo_location', + 'source': 'test_source', + 'zone': 'zone.test', + 'event': 'enter', + }, + 'action': { + 'service': 'test.automation', + 'data_template': { + 'some': '{{ trigger.%s }}' % '}} - {{ trigger.'.join(( + 'platform', 'entity_id', + 'from_state.state', 'to_state.state', + 'zone.name')) + }, + + } + } + }) + + # Entity appears in zone without previously existing outside the zone. + context = Context() + self.hass.states.set('geo_location.entity', 'hello', { + 'latitude': 32.880586, + 'longitude': -117.237564, + 'source': 'test_source' + }, context=context) + self.hass.block_till_done() + + self.assertEqual(1, len(self.calls)) + assert self.calls[0].context is context + self.assertEqual( + 'geo_location - geo_location.entity - - hello - test', + self.calls[0].data['some']) + + def test_if_fires_on_zone_disappear(self): + """Test for firing if entity disappears from zone.""" + self.hass.states.set('geo_location.entity', 'hello', { + 'latitude': 32.880586, + 'longitude': -117.237564, + 'source': 'test_source' + }) + self.hass.block_till_done() + + assert setup_component(self.hass, automation.DOMAIN, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'geo_location', + 'source': 'test_source', + 'zone': 'zone.test', + 'event': 'leave', + }, + 'action': { + 'service': 'test.automation', + 'data_template': { + 'some': '{{ trigger.%s }}' % '}} - {{ trigger.'.join(( + 'platform', 'entity_id', + 'from_state.state', 'to_state.state', + 'zone.name')) + }, + + } + } + }) + + # Entity disappears from zone without new coordinates outside the zone. + self.hass.states.async_remove('geo_location.entity') + self.hass.block_till_done() + + self.assertEqual(1, len(self.calls)) + self.assertEqual( + 'geo_location - geo_location.entity - hello - - test', + self.calls[0].data['some']) From b5323cd89408cd79eb3a5dd3c7e2463b74ef1278 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 22 Oct 2018 14:45:13 +0200 Subject: [PATCH 246/265] Add lovelace websocket get and set card (#17600) * Add ws get, set card * lint+fix test * Add test for set * Added more tests, catch unsupported yaml constructors Like !include will now give an error in the frontend. * lint --- homeassistant/components/lovelace/__init__.py | 187 +++++++++++++++++- tests/components/lovelace/test_init.py | 174 +++++++++++++++- 2 files changed, 348 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index e3f4522580b..2c28b52ec6e 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -2,9 +2,10 @@ import logging import uuid import os -from os import O_WRONLY, O_CREAT, O_TRUNC +from os import O_CREAT, O_TRUNC, O_WRONLY from collections import OrderedDict -from typing import Union, List, Dict +from typing import Dict, List, Union + import voluptuous as vol from homeassistant.components import websocket_api @@ -14,21 +15,45 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = 'lovelace' REQUIREMENTS = ['ruamel.yaml==0.15.72'] +LOVELACE_CONFIG_FILE = 'ui-lovelace.yaml' +JSON_TYPE = Union[List, Dict, str] # pylint: disable=invalid-name + OLD_WS_TYPE_GET_LOVELACE_UI = 'frontend/lovelace_config' WS_TYPE_GET_LOVELACE_UI = 'lovelace/config' +WS_TYPE_GET_CARD = 'lovelace/config/card/get' +WS_TYPE_SET_CARD = 'lovelace/config/card/set' SCHEMA_GET_LOVELACE_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): vol.Any(WS_TYPE_GET_LOVELACE_UI, OLD_WS_TYPE_GET_LOVELACE_UI), }) -JSON_TYPE = Union[List, Dict, str] # pylint: disable=invalid-name +SCHEMA_GET_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_GET_CARD, + vol.Required('card_id'): str, + vol.Optional('format', default='yaml'): str, +}) + +SCHEMA_SET_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_SET_CARD, + vol.Required('card_id'): str, + vol.Required('card_config'): vol.Any(str, Dict), + vol.Optional('format', default='yaml'): str, +}) class WriteError(HomeAssistantError): """Error writing the data.""" +class CardNotFoundError(HomeAssistantError): + """Card not found in data.""" + + +class UnsupportedYamlError(HomeAssistantError): + """Unsupported YAML.""" + + def save_yaml(fname: str, data: JSON_TYPE): """Save a YAML file.""" from ruamel.yaml import YAML @@ -45,7 +70,7 @@ def save_yaml(fname: str, data: JSON_TYPE): _LOGGER.error(str(exc)) raise HomeAssistantError(exc) except OSError as exc: - _LOGGER.exception('Saving YAML file failed: %s', fname) + _LOGGER.exception('Saving YAML file %s failed: %s', fname, exc) raise WriteError(exc) finally: if os.path.exists(tmp_fname): @@ -57,18 +82,29 @@ def save_yaml(fname: str, data: JSON_TYPE): _LOGGER.error("YAML replacement cleanup failed: %s", exc) +def _yaml_unsupported(loader, node): + raise UnsupportedYamlError( + 'Unsupported YAML, you can not use {} in ui-lovelace.yaml' + .format(node.tag)) + + def load_yaml(fname: str) -> JSON_TYPE: """Load a YAML file.""" from ruamel.yaml import YAML + from ruamel.yaml.constructor import RoundTripConstructor from ruamel.yaml.error import YAMLError + + RoundTripConstructor.add_constructor(None, _yaml_unsupported) + yaml = YAML(typ='rt') + try: with open(fname, encoding='utf-8') as conf_file: # If configuration file is empty YAML returns None # We convert that to an empty dict return yaml.load(conf_file) or OrderedDict() except YAMLError as exc: - _LOGGER.error("YAML error: %s", exc) + _LOGGER.error("YAML error in %s: %s", fname, exc) raise HomeAssistantError(exc) except UnicodeDecodeError as exc: _LOGGER.error("Unable to read file %s: %s", fname, exc) @@ -76,21 +112,86 @@ def load_yaml(fname: str) -> JSON_TYPE: def load_config(fname: str) -> JSON_TYPE: - """Load a YAML file and adds id to card if not present.""" + """Load a YAML file and adds id to views and cards if not present.""" config = load_yaml(fname) - # Check if all cards have an ID or else add one + # Check if all views and cards have an id or else add one updated = False + index = 0 for view in config.get('views', []): + if 'id' not in view: + updated = True + view.insert(0, 'id', index, + comment="Automatically created id") for card in view.get('cards', []): if 'id' not in card: updated = True - card['id'] = uuid.uuid4().hex - card.move_to_end('id', last=False) + card.insert(0, 'id', uuid.uuid4().hex, + comment="Automatically created id") + index += 1 if updated: save_yaml(fname, config) return config +def object_to_yaml(data: JSON_TYPE) -> str: + """Create yaml string from object.""" + from ruamel.yaml import YAML + from ruamel.yaml.error import YAMLError + from ruamel.yaml.compat import StringIO + yaml = YAML(typ='rt') + yaml.indent(sequence=4, offset=2) + stream = StringIO() + try: + yaml.dump(data, stream) + return stream.getvalue() + except YAMLError as exc: + _LOGGER.error("YAML error: %s", exc) + raise HomeAssistantError(exc) + + +def yaml_to_object(data: str) -> JSON_TYPE: + """Create object from yaml string.""" + from ruamel.yaml import YAML + from ruamel.yaml.error import YAMLError + yaml = YAML(typ='rt') + try: + return yaml.load(data) + except YAMLError as exc: + _LOGGER.error("YAML error: %s", exc) + raise HomeAssistantError(exc) + + +def get_card(fname: str, card_id: str, data_format: str) -> JSON_TYPE: + """Load a specific card config for id.""" + config = load_yaml(fname) + for view in config.get('views', []): + for card in view.get('cards', []): + if card.get('id') == card_id: + if data_format == 'yaml': + return object_to_yaml(card) + return card + + raise CardNotFoundError( + "Card with ID: {} was not found in {}.".format(card_id, fname)) + + +def set_card(fname: str, card_id: str, card_config: str, data_format: str)\ + -> bool: + """Save a specific card config for id.""" + config = load_yaml(fname) + for view in config.get('views', []): + for card in view.get('cards', []): + if card.get('id') == card_id: + if data_format == 'yaml': + card_config = yaml_to_object(card_config) + card.update(card_config) + save_yaml(fname, config) + return True + + raise CardNotFoundError( + "Card with ID: {} was not found in {}.".format(card_id, fname)) + + async def async_setup(hass, config): """Set up the Lovelace commands.""" # Backwards compat. Added in 0.80. Remove after 0.85 @@ -102,6 +203,14 @@ async def async_setup(hass, config): WS_TYPE_GET_LOVELACE_UI, websocket_lovelace_config, SCHEMA_GET_LOVELACE_UI) + hass.components.websocket_api.async_register_command( + WS_TYPE_GET_CARD, websocket_lovelace_get_card, + SCHEMA_GET_CARD) + + hass.components.websocket_api.async_register_command( + WS_TYPE_SET_CARD, websocket_lovelace_set_card, + SCHEMA_SET_CARD) + return True @@ -111,13 +220,15 @@ async def websocket_lovelace_config(hass, connection, msg): error = None try: config = await hass.async_add_executor_job( - load_config, hass.config.path('ui-lovelace.yaml')) + load_config, hass.config.path(LOVELACE_CONFIG_FILE)) message = websocket_api.result_message( msg['id'], config ) except FileNotFoundError: error = ('file_not_found', 'Could not find ui-lovelace.yaml in your config dir.') + except UnsupportedYamlError as err: + error = 'unsupported_error', str(err) except HomeAssistantError as err: error = 'load_error', str(err) @@ -125,3 +236,59 @@ async def websocket_lovelace_config(hass, connection, msg): message = websocket_api.error_message(msg['id'], *error) connection.send_message(message) + + +@websocket_api.async_response +async def websocket_lovelace_get_card(hass, connection, msg): + """Send lovelace card config over websocket config.""" + error = None + try: + card = await hass.async_add_executor_job( + get_card, hass.config.path(LOVELACE_CONFIG_FILE), msg['card_id'], + msg.get('format', 'yaml')) + message = websocket_api.result_message( + msg['id'], card + ) + except FileNotFoundError: + error = ('file_not_found', + 'Could not find ui-lovelace.yaml in your config dir.') + except UnsupportedYamlError as err: + error = 'unsupported_error', str(err) + except CardNotFoundError: + error = ('card_not_found', + 'Could not find card in ui-lovelace.yaml.') + except HomeAssistantError as err: + error = 'load_error', str(err) + + if error is not None: + message = websocket_api.error_message(msg['id'], *error) + + connection.send_message(message) + + +@websocket_api.async_response +async def websocket_lovelace_set_card(hass, connection, msg): + """Receive lovelace card config over websocket and save.""" + error = None + try: + result = await hass.async_add_executor_job( + set_card, hass.config.path(LOVELACE_CONFIG_FILE), + msg['card_id'], msg['card_config'], msg.get('format', 'yaml')) + message = websocket_api.result_message( + msg['id'], result + ) + except FileNotFoundError: + error = ('file_not_found', + 'Could not find ui-lovelace.yaml in your config dir.') + except UnsupportedYamlError as err: + error = 'unsupported_error', str(err) + except CardNotFoundError: + error = ('card_not_found', + 'Could not find card in ui-lovelace.yaml.') + except HomeAssistantError as err: + error = 'save_error', str(err) + + if error is not None: + message = websocket_api.error_message(msg['id'], *error) + + connection.send_message(message) diff --git a/tests/components/lovelace/test_init.py b/tests/components/lovelace/test_init.py index 5e4cf2d8037..c637267cc7e 100644 --- a/tests/components/lovelace/test_init.py +++ b/tests/components/lovelace/test_init.py @@ -9,7 +9,8 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.components.lovelace import (load_yaml, - save_yaml, load_config) + save_yaml, load_config, + UnsupportedYamlError) TEST_YAML_A = """\ title: My Awesome Home @@ -55,6 +56,8 @@ views: # Title of the view. Will be used as the tooltip for tab icon title: Second view cards: + - id: test + type: entities # Entities card will take a list of entities and show their state. - type: entities # Title of the entities card @@ -79,6 +82,7 @@ TEST_YAML_B = """\ title: Home views: - title: Dashboard + id: dashboard icon: mdi:home cards: - id: testid @@ -102,6 +106,15 @@ views: type: vertical-stack """ +# Test unsupported YAML +TEST_UNSUP_YAML = """\ +title: Home +views: + - title: Dashboard + icon: mdi:home + cards: !include cards.yaml +""" + class TestYAML(unittest.TestCase): """Test lovelace.yaml save and load.""" @@ -147,9 +160,11 @@ class TestYAML(unittest.TestCase): """Test if id is added.""" fname = self._path_for("test6") with patch('homeassistant.components.lovelace.load_yaml', - return_value=self.yaml.load(TEST_YAML_A)): + return_value=self.yaml.load(TEST_YAML_A)), \ + patch('homeassistant.components.lovelace.save_yaml'): data = load_config(fname) assert 'id' in data['views'][0]['cards'][0] + assert 'id' in data['views'][1] def test_id_not_changed(self): """Test if id is not changed if already exists.""" @@ -256,7 +271,7 @@ async def test_lovelace_ui_not_found(hass, hass_ws_client): async def test_lovelace_ui_load_err(hass, hass_ws_client): - """Test lovelace_ui command cannot find file.""" + """Test lovelace_ui command load error.""" await async_setup_component(hass, 'lovelace') client = await hass_ws_client(hass) @@ -272,3 +287,156 @@ async def test_lovelace_ui_load_err(hass, hass_ws_client): assert msg['type'] == TYPE_RESULT assert msg['success'] is False assert msg['error']['code'] == 'load_error' + + +async def test_lovelace_ui_load_json_err(hass, hass_ws_client): + """Test lovelace_ui command load error.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + + with patch('homeassistant.components.lovelace.load_config', + side_effect=UnsupportedYamlError): + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == TYPE_RESULT + assert msg['success'] is False + assert msg['error']['code'] == 'unsupported_error' + + +async def test_lovelace_get_card(hass, hass_ws_client): + """Test get_card command.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + yaml = YAML(typ='rt') + + with patch('homeassistant.components.lovelace.load_yaml', + return_value=yaml.load(TEST_YAML_A)): + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config/card/get', + 'card_id': 'test', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == TYPE_RESULT + assert msg['success'] + assert msg['result'] == 'id: test\ntype: entities\n' + + +async def test_lovelace_get_card_not_found(hass, hass_ws_client): + """Test get_card command cannot find card.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + yaml = YAML(typ='rt') + + with patch('homeassistant.components.lovelace.load_yaml', + return_value=yaml.load(TEST_YAML_A)): + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config/card/get', + 'card_id': 'not_found', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == TYPE_RESULT + assert msg['success'] is False + assert msg['error']['code'] == 'card_not_found' + + +async def test_lovelace_get_card_bad_yaml(hass, hass_ws_client): + """Test get_card command bad yaml.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + + with patch('homeassistant.components.lovelace.load_yaml', + side_effect=HomeAssistantError): + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config/card/get', + 'card_id': 'testid', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == TYPE_RESULT + assert msg['success'] is False + assert msg['error']['code'] == 'load_error' + + +async def test_lovelace_set_card(hass, hass_ws_client): + """Test set_card command.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + yaml = YAML(typ='rt') + + with patch('homeassistant.components.lovelace.load_yaml', + return_value=yaml.load(TEST_YAML_A)), \ + patch('homeassistant.components.lovelace.save_yaml') \ + as save_yaml_mock: + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config/card/set', + 'card_id': 'test', + 'card_config': 'id: test\ntype: glance\n', + }) + msg = await client.receive_json() + + result = save_yaml_mock.call_args_list[0][0][1] + assert result.mlget(['views', 1, 'cards', 0, 'type'], + list_ok=True) == 'glance' + assert msg['id'] == 5 + assert msg['type'] == TYPE_RESULT + assert msg['success'] + + +async def test_lovelace_set_card_not_found(hass, hass_ws_client): + """Test set_card command cannot find card.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + yaml = YAML(typ='rt') + + with patch('homeassistant.components.lovelace.load_yaml', + return_value=yaml.load(TEST_YAML_A)): + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config/card/set', + 'card_id': 'not_found', + 'card_config': 'id: test\ntype: glance\n', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == TYPE_RESULT + assert msg['success'] is False + assert msg['error']['code'] == 'card_not_found' + + +async def test_lovelace_set_card_bad_yaml(hass, hass_ws_client): + """Test set_card command bad yaml.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + yaml = YAML(typ='rt') + + with patch('homeassistant.components.lovelace.load_yaml', + return_value=yaml.load(TEST_YAML_A)), \ + patch('homeassistant.components.lovelace.yaml_to_object', + side_effect=HomeAssistantError): + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config/card/set', + 'card_id': 'test', + 'card_config': 'id: test\ntype: glance\n', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == TYPE_RESULT + assert msg['success'] is False + assert msg['error']['code'] == 'save_error' From fe8dec27a38cfeeb00129a555e1a4e850b1e4251 Mon Sep 17 00:00:00 2001 From: Nicko van Someren Date: Tue, 23 Oct 2018 04:54:03 -0400 Subject: [PATCH 247/265] Fixed issue #16903 re exception with multiple simultanious writes (#17636) Reworked tests/components/emulated_hue/test_init.py to not be dependent on the specific internal implementation of util/jsonn.py --- homeassistant/util/json.py | 14 ++- tests/components/emulated_hue/test_init.py | 122 ++++++++++----------- 2 files changed, 69 insertions(+), 67 deletions(-) diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index 0a2a2a1edf3..b002c8e3147 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -4,7 +4,7 @@ from typing import Union, List, Dict import json import os -from os import O_WRONLY, O_CREAT, O_TRUNC +import tempfile from homeassistant.exceptions import HomeAssistantError @@ -46,13 +46,17 @@ def save_json(filename: str, data: Union[List, Dict], Returns True on success. """ - tmp_filename = filename + "__TEMP__" + tmp_filename = "" + tmp_path = os.path.split(filename)[0] try: json_data = json.dumps(data, sort_keys=True, indent=4) - mode = 0o600 if private else 0o644 - with open(os.open(tmp_filename, O_WRONLY | O_CREAT | O_TRUNC, mode), - 'w', encoding='utf-8') as fdesc: + # Modern versions of Python tempfile create this file with mode 0o600 + with tempfile.NamedTemporaryFile(mode="w", encoding='utf-8', + dir=tmp_path, delete=False) as fdesc: fdesc.write(json_data) + tmp_filename = fdesc.name + if not private: + os.chmod(tmp_filename, 0o644) os.replace(tmp_filename, filename) except TypeError as error: _LOGGER.exception('Failed to serialize to JSON: %s', diff --git a/tests/components/emulated_hue/test_init.py b/tests/components/emulated_hue/test_init.py index 9b0a5cd9052..3de8e969140 100644 --- a/tests/components/emulated_hue/test_init.py +++ b/tests/components/emulated_hue/test_init.py @@ -1,7 +1,5 @@ """Test the Emulated Hue component.""" -import json - -from unittest.mock import patch, Mock, mock_open, MagicMock +from unittest.mock import patch, Mock, MagicMock from homeassistant.components.emulated_hue import Config @@ -14,30 +12,30 @@ def test_config_google_home_entity_id_to_number(): 'type': 'google_home' }) - mop = mock_open(read_data=json.dumps({'1': 'light.test2'})) - handle = mop() + with patch('homeassistant.components.emulated_hue.load_json', + return_value={'1': 'light.test2'}) as json_loader: + with patch('homeassistant.components.emulated_hue' + '.save_json') as json_saver: + number = conf.entity_id_to_number('light.test') + assert number == '2' - with patch('homeassistant.util.json.open', mop, create=True): - with patch('homeassistant.util.json.os.open', return_value=0): - with patch('homeassistant.util.json.os.replace'): - number = conf.entity_id_to_number('light.test') - assert number == '2' - assert handle.write.call_count == 1 - assert json.loads(handle.write.mock_calls[0][1][0]) == { - '1': 'light.test2', - '2': 'light.test', - } + assert json_saver.mock_calls[0][1][1] == { + '1': 'light.test2', '2': 'light.test' + } - number = conf.entity_id_to_number('light.test') - assert number == '2' - assert handle.write.call_count == 1 + assert json_saver.call_count == 1 + assert json_loader.call_count == 1 - number = conf.entity_id_to_number('light.test2') - assert number == '1' - assert handle.write.call_count == 1 + number = conf.entity_id_to_number('light.test') + assert number == '2' + assert json_saver.call_count == 1 - entity_id = conf.number_to_entity_id('1') - assert entity_id == 'light.test2' + number = conf.entity_id_to_number('light.test2') + assert number == '1' + assert json_saver.call_count == 1 + + entity_id = conf.number_to_entity_id('1') + assert entity_id == 'light.test2' def test_config_google_home_entity_id_to_number_altered(): @@ -48,30 +46,30 @@ def test_config_google_home_entity_id_to_number_altered(): 'type': 'google_home' }) - mop = mock_open(read_data=json.dumps({'21': 'light.test2'})) - handle = mop() + with patch('homeassistant.components.emulated_hue.load_json', + return_value={'21': 'light.test2'}) as json_loader: + with patch('homeassistant.components.emulated_hue' + '.save_json') as json_saver: + number = conf.entity_id_to_number('light.test') + assert number == '22' + assert json_saver.call_count == 1 + assert json_loader.call_count == 1 - with patch('homeassistant.util.json.open', mop, create=True): - with patch('homeassistant.util.json.os.open', return_value=0): - with patch('homeassistant.util.json.os.replace'): - number = conf.entity_id_to_number('light.test') - assert number == '22' - assert handle.write.call_count == 1 - assert json.loads(handle.write.mock_calls[0][1][0]) == { - '21': 'light.test2', - '22': 'light.test', - } + assert json_saver.mock_calls[0][1][1] == { + '21': 'light.test2', + '22': 'light.test', + } - number = conf.entity_id_to_number('light.test') - assert number == '22' - assert handle.write.call_count == 1 + number = conf.entity_id_to_number('light.test') + assert number == '22' + assert json_saver.call_count == 1 - number = conf.entity_id_to_number('light.test2') - assert number == '21' - assert handle.write.call_count == 1 + number = conf.entity_id_to_number('light.test2') + assert number == '21' + assert json_saver.call_count == 1 - entity_id = conf.number_to_entity_id('21') - assert entity_id == 'light.test2' + entity_id = conf.number_to_entity_id('21') + assert entity_id == 'light.test2' def test_config_google_home_entity_id_to_number_empty(): @@ -82,29 +80,29 @@ def test_config_google_home_entity_id_to_number_empty(): 'type': 'google_home' }) - mop = mock_open(read_data='') - handle = mop() + with patch('homeassistant.components.emulated_hue.load_json', + return_value={}) as json_loader: + with patch('homeassistant.components.emulated_hue' + '.save_json') as json_saver: + number = conf.entity_id_to_number('light.test') + assert number == '1' + assert json_saver.call_count == 1 + assert json_loader.call_count == 1 - with patch('homeassistant.util.json.open', mop, create=True): - with patch('homeassistant.util.json.os.open', return_value=0): - with patch('homeassistant.util.json.os.replace'): - number = conf.entity_id_to_number('light.test') - assert number == '1' - assert handle.write.call_count == 1 - assert json.loads(handle.write.mock_calls[0][1][0]) == { - '1': 'light.test', - } + assert json_saver.mock_calls[0][1][1] == { + '1': 'light.test', + } - number = conf.entity_id_to_number('light.test') - assert number == '1' - assert handle.write.call_count == 1 + number = conf.entity_id_to_number('light.test') + assert number == '1' + assert json_saver.call_count == 1 - number = conf.entity_id_to_number('light.test2') - assert number == '2' - assert handle.write.call_count == 2 + number = conf.entity_id_to_number('light.test2') + assert number == '2' + assert json_saver.call_count == 2 - entity_id = conf.number_to_entity_id('2') - assert entity_id == 'light.test2' + entity_id = conf.number_to_entity_id('2') + assert entity_id == 'light.test2' def test_config_alexa_entity_id_to_number(): From 87133a0e77fce953a6a293117bb8f1b825bd2f5b Mon Sep 17 00:00:00 2001 From: Matt Snyder Date: Mon, 22 Oct 2018 00:04:47 -0500 Subject: [PATCH 248/265] Update flux library version (#17677) --- homeassistant/components/light/flux_led.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index f389d34cd5d..cab6957c265 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -18,7 +18,7 @@ from homeassistant.components.light import ( import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util -REQUIREMENTS = ['flux_led==0.21'] +REQUIREMENTS = ['flux_led==0.22'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 90c0dd8c44c..591f9e6c71c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -376,7 +376,7 @@ fitbit==0.3.0 fixerio==1.0.0a0 # homeassistant.components.light.flux_led -flux_led==0.21 +flux_led==0.22 # homeassistant.components.sensor.foobot foobot_async==0.3.1 From 2d9a9649538e62beca78f4141f77930b316620d4 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Tue, 23 Oct 2018 07:11:55 +0200 Subject: [PATCH 249/265] Update limitlessled to 1.1.3 (#17703) --- homeassistant/components/light/limitlessled.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/limitlessled.py b/homeassistant/components/light/limitlessled.py index a5aeabba84d..2e2971cfdc2 100644 --- a/homeassistant/components/light/limitlessled.py +++ b/homeassistant/components/light/limitlessled.py @@ -20,7 +20,7 @@ from homeassistant.util.color import ( color_temperature_mired_to_kelvin, color_hs_to_RGB) from homeassistant.helpers.restore_state import async_get_last_state -REQUIREMENTS = ['limitlessled==1.1.2'] +REQUIREMENTS = ['limitlessled==1.1.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 591f9e6c71c..f24fb306de7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -559,7 +559,7 @@ liffylights==0.9.4 lightify==1.0.6.1 # homeassistant.components.light.limitlessled -limitlessled==1.1.2 +limitlessled==1.1.3 # homeassistant.components.linode linode-api==4.1.9b1 From 011cc624b65efb7f3bdf596c7334507a2c2f1413 Mon Sep 17 00:00:00 2001 From: Jaxom Nutt <40261038+JaxomCS@users.noreply.github.com> Date: Tue, 23 Oct 2018 16:28:49 +0800 Subject: [PATCH 250/265] Bug fix for clicksend (#17713) * Bug fix Current version causes 500 error since it is sending an array of from numbers to ClickSend. Changing the from number to 'hass' identifies all messages as coming from Home Assistant making them more recognisable and removes the bug. * Amendment Changed it to use 'hass' as the default instead of defaulting to the recipient which is the array. Would have worked if users set their own name but users who were using the default were experiencing the issue. * Added DEFAULT_SENDER variable --- homeassistant/components/notify/clicksend.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notify/clicksend.py b/homeassistant/components/notify/clicksend.py index c028da2c579..5506d6ed6d0 100644 --- a/homeassistant/components/notify/clicksend.py +++ b/homeassistant/components/notify/clicksend.py @@ -22,6 +22,8 @@ _LOGGER = logging.getLogger(__name__) BASE_API_URL = 'https://rest.clicksend.com/v3' +DEFAULT_SENDER = 'hass' + HEADERS = {CONTENT_TYPE: CONTENT_TYPE_JSON} @@ -29,7 +31,7 @@ def validate_sender(config): """Set the optional sender name if sender name is not provided.""" if CONF_SENDER in config: return config - config[CONF_SENDER] = config[CONF_RECIPIENT] + config[CONF_SENDER] = DEFAULT_SENDER return config @@ -61,7 +63,7 @@ class ClicksendNotificationService(BaseNotificationService): self.username = config.get(CONF_USERNAME) self.api_key = config.get(CONF_API_KEY) self.recipients = config.get(CONF_RECIPIENT) - self.sender = config.get(CONF_SENDER, CONF_RECIPIENT) + self.sender = config.get(CONF_SENDER) def send_message(self, message="", **kwargs): """Send a message to a user.""" From 4750656f1a0efde8a80d03a4f8a2303cac1935a8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 23 Oct 2018 14:09:47 +0200 Subject: [PATCH 251/265] Bumped version to 0.81.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index ae4a2b052bc..5bcfba2caff 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 81 -PATCH_VERSION = '0b0' +PATCH_VERSION = '0b1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 1788eaf037820262a698bdfa2efa45c3d1839d68 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 24 Oct 2018 22:15:21 +0200 Subject: [PATCH 252/265] Update frontend to 20181024.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 55aa0700bef..c155bcf81e3 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20181023.0'] +REQUIREMENTS = ['home-assistant-frontend==20181024.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index f24fb306de7..4f390c992fa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -466,7 +466,7 @@ hole==0.3.0 holidays==0.9.8 # homeassistant.components.frontend -home-assistant-frontend==20181023.0 +home-assistant-frontend==20181024.0 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e8fc3517121..7e93231c82e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -97,7 +97,7 @@ hdate==0.6.5 holidays==0.9.8 # homeassistant.components.frontend -home-assistant-frontend==20181023.0 +home-assistant-frontend==20181024.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From edc1cbdc32a4881c1e72a4bf631016ae17cc9b87 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Wed, 24 Oct 2018 05:15:59 -0400 Subject: [PATCH 253/265] Elk-M1 climate (#17679) * Initial climate for Elk-M1. * Tidy * fix hound error * fix hound error --- homeassistant/components/climate/elkm1.py | 193 +++++++++++++++++++++ homeassistant/components/elkm1/__init__.py | 4 +- 2 files changed, 195 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/climate/elkm1.py diff --git a/homeassistant/components/climate/elkm1.py b/homeassistant/components/climate/elkm1.py new file mode 100644 index 00000000000..6bd33b382dc --- /dev/null +++ b/homeassistant/components/climate/elkm1.py @@ -0,0 +1,193 @@ +""" +Support for control of Elk-M1 connected thermostats. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/climate.elkm1/ +""" +from homeassistant.components.climate import ( + ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, PRECISION_WHOLE, STATE_AUTO, + STATE_COOL, STATE_FAN_ONLY, STATE_HEAT, STATE_IDLE, SUPPORT_AUX_HEAT, + SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE_HIGH, + SUPPORT_TARGET_TEMPERATURE_LOW, ClimateDevice) +from homeassistant.components.elkm1 import ( + DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities) +from homeassistant.const import STATE_ON + +DEPENDENCIES = [ELK_DOMAIN] + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Create the Elk-M1 thermostat platform.""" + if discovery_info is None: + return + + elk = hass.data[ELK_DOMAIN]['elk'] + async_add_entities(create_elk_entities( + hass, elk.thermostats, 'thermostat', ElkThermostat, []), True) + + +class ElkThermostat(ElkEntity, ClimateDevice): + """Representation of an Elk-M1 Thermostat.""" + + def __init__(self, element, elk, elk_data): + """Initialize climate entity.""" + super().__init__(element, elk, elk_data) + self._state = None + + @property + def supported_features(self): + """Return the list of supported features.""" + return (SUPPORT_OPERATION_MODE | SUPPORT_FAN_MODE | SUPPORT_AUX_HEAT + | SUPPORT_TARGET_TEMPERATURE_HIGH + | SUPPORT_TARGET_TEMPERATURE_LOW) + + @property + def temperature_unit(self): + """Return the temperature unit.""" + return self._temperature_unit + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._element.current_temp + + @property + def target_temperature(self): + """Return the temperature we are trying to reach.""" + from elkm1_lib.const import ThermostatMode + if (self._element.mode == ThermostatMode.HEAT.value) or ( + self._element.mode == ThermostatMode.EMERGENCY_HEAT.value): + return self._element.heat_setpoint + if self._element.mode == ThermostatMode.COOL.value: + return self._element.cool_setpoint + return None + + @property + def target_temperature_high(self): + """Return the high target temperature.""" + return self._element.cool_setpoint + + @property + def target_temperature_low(self): + """Return the low target temperature.""" + return self._element.heat_setpoint + + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + return 1 + + @property + def current_humidity(self): + """Return the current humidity.""" + return self._element.humidity + + @property + def current_operation(self): + """Return current operation ie. heat, cool, idle.""" + return self._state + + @property + def operation_list(self): + """Return the list of available operation modes.""" + return [STATE_IDLE, STATE_HEAT, STATE_COOL, STATE_AUTO, STATE_FAN_ONLY] + + @property + def precision(self): + """Return the precision of the system.""" + return PRECISION_WHOLE + + @property + def is_aux_heat_on(self): + """Return if aux heater is on.""" + from elkm1_lib.const import ThermostatMode + return self._element.mode == ThermostatMode.EMERGENCY_HEAT.value + + @property + def min_temp(self): + """Return the minimum temperature supported.""" + return 1 + + @property + def max_temp(self): + """Return the maximum temperature supported.""" + return 99 + + @property + def current_fan_mode(self): + """Return the fan setting.""" + from elkm1_lib.const import ThermostatFan + if self._element.fan == ThermostatFan.AUTO.value: + return STATE_AUTO + if self._element.fan == ThermostatFan.ON.value: + return STATE_ON + return None + + def _elk_set(self, mode, fan): + from elkm1_lib.const import ThermostatSetting + if mode is not None: + self._element.set(ThermostatSetting.MODE.value, mode) + if fan is not None: + self._element.set(ThermostatSetting.FAN.value, fan) + + async def async_set_operation_mode(self, operation_mode): + """Set thermostat operation mode.""" + from elkm1_lib.const import ThermostatFan, ThermostatMode + settings = { + STATE_IDLE: (ThermostatMode.OFF.value, ThermostatFan.AUTO.value), + STATE_HEAT: (ThermostatMode.HEAT.value, None), + STATE_COOL: (ThermostatMode.COOL.value, None), + STATE_AUTO: (ThermostatMode.AUTO.value, None), + STATE_FAN_ONLY: (ThermostatMode.OFF.value, ThermostatFan.ON.value) + } + self._elk_set(settings[operation_mode][0], settings[operation_mode][1]) + + async def async_turn_aux_heat_on(self): + """Turn auxiliary heater on.""" + from elkm1_lib.const import ThermostatMode + self._elk_set(ThermostatMode.EMERGENCY_HEAT.value, None) + + async def async_turn_aux_heat_off(self): + """Turn auxiliary heater off.""" + from elkm1_lib.const import ThermostatMode + self._elk_set(ThermostatMode.HEAT.value, None) + + @property + def fan_list(self): + """Return the list of available fan modes.""" + return [STATE_AUTO, STATE_ON] + + async def async_set_fan_mode(self, fan_mode): + """Set new target fan mode.""" + from elkm1_lib.const import ThermostatFan + if fan_mode == STATE_AUTO: + self._elk_set(None, ThermostatFan.AUTO.value) + elif fan_mode == STATE_ON: + self._elk_set(None, ThermostatFan.ON.value) + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + from elkm1_lib.const import ThermostatSetting + low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW) + high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) + if low_temp is not None: + self._element.set( + ThermostatSetting.HEAT_SETPOINT.value, round(low_temp)) + if high_temp is not None: + self._element.set( + ThermostatSetting.COOL_SETPOINT.value, round(high_temp)) + + def _element_changed(self, element, changeset): + from elkm1_lib.const import ThermostatFan, ThermostatMode + mode_to_state = { + ThermostatMode.OFF.value: STATE_IDLE, + ThermostatMode.COOL.value: STATE_COOL, + ThermostatMode.HEAT.value: STATE_HEAT, + ThermostatMode.EMERGENCY_HEAT.value: STATE_HEAT, + ThermostatMode.AUTO.value: STATE_AUTO, + } + self._state = mode_to_state.get(self._element.mode) + if self._state == STATE_IDLE and \ + self._element.fan == ThermostatFan.ON.value: + self._state = STATE_FAN_ONLY diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 5c379c7438b..76594e16736 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -35,8 +35,8 @@ CONF_ENABLED = 'enabled' _LOGGER = logging.getLogger(__name__) -SUPPORTED_DOMAINS = ['alarm_control_panel', 'light', 'scene', 'sensor', - 'switch'] +SUPPORTED_DOMAINS = ['alarm_control_panel', 'climate', 'light', 'scene', + 'sensor', 'switch'] SPEAK_SERVICE_SCHEMA = vol.Schema({ vol.Required('number'): From 8de0824688e5f9b7a3952384afe5d7147e7f4a07 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Wed, 24 Oct 2018 05:53:45 -0400 Subject: [PATCH 254/265] Add cover to supported platforms (#17725) --- homeassistant/components/zwave/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 74678cda0fc..35703d64974 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -66,7 +66,7 @@ DEFAULT_CONF_INVERT_OPENCLOSE_BUTTONS = False DEFAULT_CONF_REFRESH_VALUE = False DEFAULT_CONF_REFRESH_DELAY = 5 -SUPPORTED_PLATFORMS = ['binary_sensor', 'climate', 'fan', +SUPPORTED_PLATFORMS = ['binary_sensor', 'climate', 'cover', 'fan', 'light', 'sensor', 'switch'] RENAME_NODE_SCHEMA = vol.Schema({ From 295a004326350440acc09e6df4a175acf84f4f6b Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 23 Oct 2018 21:48:35 +0200 Subject: [PATCH 255/265] Lovelace ws: add card (#17730) * Change set to update * Add 'add card' * Woof. --- homeassistant/components/lovelace/__init__.py | 138 ++++++++++++++---- tests/components/lovelace/test_init.py | 71 +++++++-- 2 files changed, 168 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index 2c28b52ec6e..141f3c98334 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -18,10 +18,15 @@ REQUIREMENTS = ['ruamel.yaml==0.15.72'] LOVELACE_CONFIG_FILE = 'ui-lovelace.yaml' JSON_TYPE = Union[List, Dict, str] # pylint: disable=invalid-name +FORMAT_YAML = 'yaml' +FORMAT_JSON = 'json' + OLD_WS_TYPE_GET_LOVELACE_UI = 'frontend/lovelace_config' WS_TYPE_GET_LOVELACE_UI = 'lovelace/config' + WS_TYPE_GET_CARD = 'lovelace/config/card/get' -WS_TYPE_SET_CARD = 'lovelace/config/card/set' +WS_TYPE_UPDATE_CARD = 'lovelace/config/card/update' +WS_TYPE_ADD_CARD = 'lovelace/config/card/add' SCHEMA_GET_LOVELACE_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): vol.Any(WS_TYPE_GET_LOVELACE_UI, @@ -31,14 +36,25 @@ SCHEMA_GET_LOVELACE_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ SCHEMA_GET_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): WS_TYPE_GET_CARD, vol.Required('card_id'): str, - vol.Optional('format', default='yaml'): str, + vol.Optional('format', default=FORMAT_YAML): vol.Any(FORMAT_JSON, + FORMAT_YAML), }) -SCHEMA_SET_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_SET_CARD, +SCHEMA_UPDATE_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_UPDATE_CARD, vol.Required('card_id'): str, vol.Required('card_config'): vol.Any(str, Dict), - vol.Optional('format', default='yaml'): str, + vol.Optional('format', default=FORMAT_YAML): vol.Any(FORMAT_JSON, + FORMAT_YAML), +}) + +SCHEMA_ADD_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_ADD_CARD, + vol.Required('view_id'): str, + vol.Required('card_config'): vol.Any(str, Dict), + vol.Optional('position'): int, + vol.Optional('format', default=FORMAT_YAML): vol.Any(FORMAT_JSON, + FORMAT_YAML), }) @@ -50,6 +66,10 @@ class CardNotFoundError(HomeAssistantError): """Card not found in data.""" +class ViewNotFoundError(HomeAssistantError): + """View not found in data.""" + + class UnsupportedYamlError(HomeAssistantError): """Unsupported YAML.""" @@ -161,37 +181,61 @@ def yaml_to_object(data: str) -> JSON_TYPE: raise HomeAssistantError(exc) -def get_card(fname: str, card_id: str, data_format: str) -> JSON_TYPE: +def get_card(fname: str, card_id: str, data_format: str = FORMAT_YAML)\ + -> JSON_TYPE: """Load a specific card config for id.""" config = load_yaml(fname) for view in config.get('views', []): for card in view.get('cards', []): - if card.get('id') == card_id: - if data_format == 'yaml': - return object_to_yaml(card) - return card + if card.get('id') != card_id: + continue + if data_format == FORMAT_YAML: + return object_to_yaml(card) + return card raise CardNotFoundError( "Card with ID: {} was not found in {}.".format(card_id, fname)) -def set_card(fname: str, card_id: str, card_config: str, data_format: str)\ - -> bool: +def update_card(fname: str, card_id: str, card_config: str, + data_format: str = FORMAT_YAML): """Save a specific card config for id.""" config = load_yaml(fname) for view in config.get('views', []): for card in view.get('cards', []): - if card.get('id') == card_id: - if data_format == 'yaml': - card_config = yaml_to_object(card_config) - card.update(card_config) - save_yaml(fname, config) - return True + if card.get('id') != card_id: + continue + if data_format == FORMAT_YAML: + card_config = yaml_to_object(card_config) + card.update(card_config) + save_yaml(fname, config) + return raise CardNotFoundError( "Card with ID: {} was not found in {}.".format(card_id, fname)) +def add_card(fname: str, view_id: str, card_config: str, + position: int = None, data_format: str = FORMAT_YAML): + """Add a card to a view.""" + config = load_yaml(fname) + for view in config.get('views', []): + if view.get('id') != view_id: + continue + cards = view.get('cards', []) + if data_format == FORMAT_YAML: + card_config = yaml_to_object(card_config) + if position is None: + cards.append(card_config) + else: + cards.insert(position, card_config) + save_yaml(fname, config) + return + + raise ViewNotFoundError( + "View with ID: {} was not found in {}.".format(view_id, fname)) + + async def async_setup(hass, config): """Set up the Lovelace commands.""" # Backwards compat. Added in 0.80. Remove after 0.85 @@ -208,8 +252,12 @@ async def async_setup(hass, config): SCHEMA_GET_CARD) hass.components.websocket_api.async_register_command( - WS_TYPE_SET_CARD, websocket_lovelace_set_card, - SCHEMA_SET_CARD) + WS_TYPE_UPDATE_CARD, websocket_lovelace_update_card, + SCHEMA_UPDATE_CARD) + + hass.components.websocket_api.async_register_command( + WS_TYPE_ADD_CARD, websocket_lovelace_add_card, + SCHEMA_ADD_CARD) return True @@ -245,7 +293,7 @@ async def websocket_lovelace_get_card(hass, connection, msg): try: card = await hass.async_add_executor_job( get_card, hass.config.path(LOVELACE_CONFIG_FILE), msg['card_id'], - msg.get('format', 'yaml')) + msg.get('format', FORMAT_YAML)) message = websocket_api.result_message( msg['id'], card ) @@ -254,9 +302,8 @@ async def websocket_lovelace_get_card(hass, connection, msg): 'Could not find ui-lovelace.yaml in your config dir.') except UnsupportedYamlError as err: error = 'unsupported_error', str(err) - except CardNotFoundError: - error = ('card_not_found', - 'Could not find card in ui-lovelace.yaml.') + except CardNotFoundError as err: + error = 'card_not_found', str(err) except HomeAssistantError as err: error = 'load_error', str(err) @@ -267,24 +314,51 @@ async def websocket_lovelace_get_card(hass, connection, msg): @websocket_api.async_response -async def websocket_lovelace_set_card(hass, connection, msg): +async def websocket_lovelace_update_card(hass, connection, msg): """Receive lovelace card config over websocket and save.""" error = None try: - result = await hass.async_add_executor_job( - set_card, hass.config.path(LOVELACE_CONFIG_FILE), - msg['card_id'], msg['card_config'], msg.get('format', 'yaml')) + await hass.async_add_executor_job( + update_card, hass.config.path(LOVELACE_CONFIG_FILE), + msg['card_id'], msg['card_config'], msg.get('format', FORMAT_YAML)) message = websocket_api.result_message( - msg['id'], result + msg['id'], True ) except FileNotFoundError: error = ('file_not_found', 'Could not find ui-lovelace.yaml in your config dir.') except UnsupportedYamlError as err: error = 'unsupported_error', str(err) - except CardNotFoundError: - error = ('card_not_found', - 'Could not find card in ui-lovelace.yaml.') + except CardNotFoundError as err: + error = 'card_not_found', str(err) + except HomeAssistantError as err: + error = 'save_error', str(err) + + if error is not None: + message = websocket_api.error_message(msg['id'], *error) + + connection.send_message(message) + + +@websocket_api.async_response +async def websocket_lovelace_add_card(hass, connection, msg): + """Add new card to view over websocket and save.""" + error = None + try: + await hass.async_add_executor_job( + add_card, hass.config.path(LOVELACE_CONFIG_FILE), + msg['view_id'], msg['card_config'], msg.get('position'), + msg.get('format', FORMAT_YAML)) + message = websocket_api.result_message( + msg['id'], True + ) + except FileNotFoundError: + error = ('file_not_found', + 'Could not find ui-lovelace.yaml in your config dir.') + except UnsupportedYamlError as err: + error = 'unsupported_error', str(err) + except ViewNotFoundError as err: + error = 'view_not_found', str(err) except HomeAssistantError as err: error = 'save_error', str(err) diff --git a/tests/components/lovelace/test_init.py b/tests/components/lovelace/test_init.py index c637267cc7e..1ce0f9ff602 100644 --- a/tests/components/lovelace/test_init.py +++ b/tests/components/lovelace/test_init.py @@ -370,8 +370,8 @@ async def test_lovelace_get_card_bad_yaml(hass, hass_ws_client): assert msg['error']['code'] == 'load_error' -async def test_lovelace_set_card(hass, hass_ws_client): - """Test set_card command.""" +async def test_lovelace_update_card(hass, hass_ws_client): + """Test update_card command.""" await async_setup_component(hass, 'lovelace') client = await hass_ws_client(hass) yaml = YAML(typ='rt') @@ -382,7 +382,7 @@ async def test_lovelace_set_card(hass, hass_ws_client): as save_yaml_mock: await client.send_json({ 'id': 5, - 'type': 'lovelace/config/card/set', + 'type': 'lovelace/config/card/update', 'card_id': 'test', 'card_config': 'id: test\ntype: glance\n', }) @@ -396,8 +396,8 @@ async def test_lovelace_set_card(hass, hass_ws_client): assert msg['success'] -async def test_lovelace_set_card_not_found(hass, hass_ws_client): - """Test set_card command cannot find card.""" +async def test_lovelace_update_card_not_found(hass, hass_ws_client): + """Test update_card command cannot find card.""" await async_setup_component(hass, 'lovelace') client = await hass_ws_client(hass) yaml = YAML(typ='rt') @@ -406,7 +406,7 @@ async def test_lovelace_set_card_not_found(hass, hass_ws_client): return_value=yaml.load(TEST_YAML_A)): await client.send_json({ 'id': 5, - 'type': 'lovelace/config/card/set', + 'type': 'lovelace/config/card/update', 'card_id': 'not_found', 'card_config': 'id: test\ntype: glance\n', }) @@ -418,8 +418,8 @@ async def test_lovelace_set_card_not_found(hass, hass_ws_client): assert msg['error']['code'] == 'card_not_found' -async def test_lovelace_set_card_bad_yaml(hass, hass_ws_client): - """Test set_card command bad yaml.""" +async def test_lovelace_update_card_bad_yaml(hass, hass_ws_client): + """Test update_card command bad yaml.""" await async_setup_component(hass, 'lovelace') client = await hass_ws_client(hass) yaml = YAML(typ='rt') @@ -430,7 +430,7 @@ async def test_lovelace_set_card_bad_yaml(hass, hass_ws_client): side_effect=HomeAssistantError): await client.send_json({ 'id': 5, - 'type': 'lovelace/config/card/set', + 'type': 'lovelace/config/card/update', 'card_id': 'test', 'card_config': 'id: test\ntype: glance\n', }) @@ -440,3 +440,56 @@ async def test_lovelace_set_card_bad_yaml(hass, hass_ws_client): assert msg['type'] == TYPE_RESULT assert msg['success'] is False assert msg['error']['code'] == 'save_error' + + +async def test_lovelace_add_card(hass, hass_ws_client): + """Test add_card command.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + yaml = YAML(typ='rt') + + with patch('homeassistant.components.lovelace.load_yaml', + return_value=yaml.load(TEST_YAML_A)), \ + patch('homeassistant.components.lovelace.save_yaml') \ + as save_yaml_mock: + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config/card/add', + 'view_id': 'example', + 'card_config': 'id: test\ntype: added\n', + }) + msg = await client.receive_json() + + result = save_yaml_mock.call_args_list[0][0][1] + assert result.mlget(['views', 0, 'cards', 2, 'type'], + list_ok=True) == 'added' + assert msg['id'] == 5 + assert msg['type'] == TYPE_RESULT + assert msg['success'] + + +async def test_lovelace_add_card_position(hass, hass_ws_client): + """Test add_card command.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + yaml = YAML(typ='rt') + + with patch('homeassistant.components.lovelace.load_yaml', + return_value=yaml.load(TEST_YAML_A)), \ + patch('homeassistant.components.lovelace.save_yaml') \ + as save_yaml_mock: + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config/card/add', + 'view_id': 'example', + 'position': 0, + 'card_config': 'id: test\ntype: added\n', + }) + msg = await client.receive_json() + + result = save_yaml_mock.call_args_list[0][0][1] + assert result.mlget(['views', 0, 'cards', 0, 'type'], + list_ok=True) == 'added' + assert msg['id'] == 5 + assert msg['type'] == TYPE_RESULT + assert msg['success'] From 3d841681d70e367ee0e444c6846435c18afd608c Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 24 Oct 2018 14:14:01 +0200 Subject: [PATCH 256/265] Remove day (fixes #17741) (#17743) --- homeassistant/components/sensor/fastdotcom.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/fastdotcom.py b/homeassistant/components/sensor/fastdotcom.py index c6a56701f7c..761dc7c6a00 100644 --- a/homeassistant/components/sensor/fastdotcom.py +++ b/homeassistant/components/sensor/fastdotcom.py @@ -22,7 +22,6 @@ _LOGGER = logging.getLogger(__name__) CONF_SECOND = 'second' CONF_MINUTE = 'minute' CONF_HOUR = 'hour' -CONF_DAY = 'day' CONF_MANUAL = 'manual' ICON = 'mdi:speedometer' @@ -34,8 +33,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(0, 59))]), vol.Optional(CONF_HOUR): vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(0, 23))]), - vol.Optional(CONF_DAY): - vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(1, 31))]), vol.Optional(CONF_MANUAL, default=False): cv.boolean, }) @@ -109,8 +106,7 @@ class SpeedtestData: if not config.get(CONF_MANUAL): track_time_change( hass, self.update, second=config.get(CONF_SECOND), - minute=config.get(CONF_MINUTE), hour=config.get(CONF_HOUR), - day=config.get(CONF_DAY)) + minute=config.get(CONF_MINUTE), hour=config.get(CONF_HOUR)) def update(self, now): """Get the latest data from fast.com.""" From a3ec37834bfd8f2a1ecdca4666c147dc69856dca Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 24 Oct 2018 22:15:57 +0200 Subject: [PATCH 257/265] Bumped version to 0.81.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5bcfba2caff..edcaa88cecb 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 81 -PATCH_VERSION = '0b1' +PATCH_VERSION = '0b2' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From ad903f991729900c9c5f33319581381def9a4b5f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 26 Oct 2018 10:17:40 +0200 Subject: [PATCH 258/265] Bump frontend to 20181026.0 --- homeassistant/components/frontend/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index c155bcf81e3..0e92595ae78 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20181024.0'] +REQUIREMENTS = ['home-assistant-frontend==20181026.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', From a4773dc3e440823e0d2c1260c5979f9616d71f6e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 26 Oct 2018 10:18:10 +0200 Subject: [PATCH 259/265] Update translations --- .../components/cast/.translations/pt.json | 4 ++-- .../components/deconz/.translations/pt.json | 2 +- .../components/hangouts/.translations/pt.json | 6 ++--- .../components/hue/.translations/pt.json | 2 +- .../components/ifttt/.translations/ko.json | 2 +- .../components/ifttt/.translations/pl.json | 6 ++--- .../components/ifttt/.translations/pt.json | 2 +- .../components/ios/.translations/pt.json | 4 ++-- .../components/lifx/.translations/pt.json | 12 ++++++++-- .../components/mailgun/.translations/ca.json | 18 +++++++++++++++ .../components/mailgun/.translations/ko.json | 18 +++++++++++++++ .../components/mailgun/.translations/nl.json | 15 +++++++++++++ .../components/mailgun/.translations/pl.json | 18 +++++++++++++++ .../components/mailgun/.translations/pt.json | 18 +++++++++++++++ .../components/mailgun/.translations/ru.json | 18 +++++++++++++++ .../components/mailgun/.translations/sl.json | 18 +++++++++++++++ .../mailgun/.translations/zh-Hant.json | 18 +++++++++++++++ .../components/mqtt/.translations/pt.json | 6 ++--- .../components/nest/.translations/ko.json | 4 ++-- .../components/nest/.translations/pt.json | 2 +- .../components/openuv/.translations/pt.json | 2 +- .../components/smhi/.translations/fr.json | 16 ++++++++++++++ .../components/smhi/.translations/pt.json | 6 +++++ .../components/tradfri/.translations/pt.json | 2 +- .../components/twilio/.translations/ca.json | 18 +++++++++++++++ .../components/twilio/.translations/en.json | 18 +++++++++++++++ .../components/twilio/.translations/ko.json | 18 +++++++++++++++ .../components/twilio/.translations/nl.json | 14 ++++++++++++ .../components/twilio/.translations/no.json | 5 +++++ .../components/twilio/.translations/pl.json | 18 +++++++++++++++ .../components/twilio/.translations/ru.json | 18 +++++++++++++++ .../components/twilio/.translations/sl.json | 18 +++++++++++++++ .../twilio/.translations/zh-Hant.json | 18 +++++++++++++++ .../components/unifi/.translations/fr.json | 22 +++++++++++++++++++ .../components/unifi/.translations/nl.json | 5 ++++- .../components/unifi/.translations/no.json | 13 +++++++++++ .../components/unifi/.translations/pl.json | 2 +- .../components/upnp/.translations/pt.json | 6 ++--- .../components/zwave/.translations/fr.json | 21 ++++++++++++++++++ .../components/zwave/.translations/ko.json | 2 +- 40 files changed, 405 insertions(+), 30 deletions(-) create mode 100644 homeassistant/components/mailgun/.translations/ca.json create mode 100644 homeassistant/components/mailgun/.translations/ko.json create mode 100644 homeassistant/components/mailgun/.translations/nl.json create mode 100644 homeassistant/components/mailgun/.translations/pl.json create mode 100644 homeassistant/components/mailgun/.translations/pt.json create mode 100644 homeassistant/components/mailgun/.translations/ru.json create mode 100644 homeassistant/components/mailgun/.translations/sl.json create mode 100644 homeassistant/components/mailgun/.translations/zh-Hant.json create mode 100644 homeassistant/components/smhi/.translations/fr.json create mode 100644 homeassistant/components/twilio/.translations/ca.json create mode 100644 homeassistant/components/twilio/.translations/en.json create mode 100644 homeassistant/components/twilio/.translations/ko.json create mode 100644 homeassistant/components/twilio/.translations/nl.json create mode 100644 homeassistant/components/twilio/.translations/no.json create mode 100644 homeassistant/components/twilio/.translations/pl.json create mode 100644 homeassistant/components/twilio/.translations/ru.json create mode 100644 homeassistant/components/twilio/.translations/sl.json create mode 100644 homeassistant/components/twilio/.translations/zh-Hant.json create mode 100644 homeassistant/components/unifi/.translations/fr.json create mode 100644 homeassistant/components/unifi/.translations/no.json create mode 100644 homeassistant/components/zwave/.translations/fr.json diff --git a/homeassistant/components/cast/.translations/pt.json b/homeassistant/components/cast/.translations/pt.json index a6d28538396..85d1b14484d 100644 --- a/homeassistant/components/cast/.translations/pt.json +++ b/homeassistant/components/cast/.translations/pt.json @@ -7,9 +7,9 @@ "step": { "confirm": { "description": "Deseja configurar o Google Cast?", - "title": "" + "title": "Google Cast" } }, - "title": "" + "title": "Google Cast" } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/pt.json b/homeassistant/components/deconz/.translations/pt.json index 1f7b8209089..eef2d5ce946 100644 --- a/homeassistant/components/deconz/.translations/pt.json +++ b/homeassistant/components/deconz/.translations/pt.json @@ -28,6 +28,6 @@ "title": "Op\u00e7\u00f5es extra de configura\u00e7\u00e3o para deCONZ" } }, - "title": "deCONZ" + "title": "Gateway Zigbee deCONZ" } } \ No newline at end of file diff --git a/homeassistant/components/hangouts/.translations/pt.json b/homeassistant/components/hangouts/.translations/pt.json index 64c960a121a..a16c60128c1 100644 --- a/homeassistant/components/hangouts/.translations/pt.json +++ b/homeassistant/components/hangouts/.translations/pt.json @@ -5,7 +5,7 @@ "unknown": "Ocorreu um erro desconhecido." }, "error": { - "invalid_2fa": "Autoriza\u00e7\u00e3o por 2 factores inv\u00e1lida, por favor, tente novamente.", + "invalid_2fa": "Autentica\u00e7\u00e3o por 2 fatores inv\u00e1lida, por favor, tente novamente.", "invalid_2fa_method": "M\u00e9todo 2FA inv\u00e1lido (verificar no telefone).", "invalid_login": "Login inv\u00e1lido, por favor, tente novamente." }, @@ -15,7 +15,7 @@ "2fa": "Pin 2FA" }, "description": "Vazio", - "title": "" + "title": "Autentica\u00e7\u00e3o de 2 Fatores" }, "user": { "data": { @@ -26,6 +26,6 @@ "title": "Login Google Hangouts" } }, - "title": "" + "title": "Google Hangouts" } } \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/pt.json b/homeassistant/components/hue/.translations/pt.json index f7988d82d8c..d52540b0921 100644 --- a/homeassistant/components/hue/.translations/pt.json +++ b/homeassistant/components/hue/.translations/pt.json @@ -24,6 +24,6 @@ "title": "Link Hub" } }, - "title": "" + "title": "Philips Hue" } } \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/ko.json b/homeassistant/components/ifttt/.translations/ko.json index 57ad8037753..2f033e4f4ee 100644 --- a/homeassistant/components/ifttt/.translations/ko.json +++ b/homeassistant/components/ifttt/.translations/ko.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." }, "create_entry": { - "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\uae30 \uc704\ud574\uc11c\ub294 [IFTTT Webhook \uc560\ud50c\ub9bf]({applet_url}) \uc5d0\uc11c \"Make a web request\" \ub97c \uc0ac\uc6a9\ud574\uc57c \ud569\ub2c8\ub2e4. \n\n \ub2e4\uc74c\uc758 \uc815\ubcf4\ub97c \uc785\ub825\ud574 \uc8fc\uc138\uc694.\n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\n Home Assistant \ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\uc548\ub0b4]({docs_url})\ub97c \ucc38\uc870\ud574 \uc8fc\uc138\uc694." + "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\uae30 \uc704\ud574\uc11c\ub294 [IFTTT Webhook \uc560\ud50c\ub9bf]({applet_url}) \uc5d0\uc11c \"Make a web request\" \ub97c \uc0ac\uc6a9\ud574\uc57c \ud569\ub2c8\ub2e4. \n\n \ub2e4\uc74c\uc758 \uc815\ubcf4\ub97c \uc785\ub825\ud574 \uc8fc\uc138\uc694.\n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\n Home Assistant \ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574 \uc8fc\uc138\uc694." }, "step": { "user": { diff --git a/homeassistant/components/ifttt/.translations/pl.json b/homeassistant/components/ifttt/.translations/pl.json index 3c3c2182503..7064364ebe6 100644 --- a/homeassistant/components/ifttt/.translations/pl.json +++ b/homeassistant/components/ifttt/.translations/pl.json @@ -5,12 +5,12 @@ "one_instance_allowed": "Wymagana jest tylko jedna instancja." }, "create_entry": { - "default": "Aby wys\u0142a\u0107 zdarzenia do Home Assistant'a, b\u0119dziesz musia\u0142 u\u017cy\u0107 akcji \"Make a web request\" z [IFTTT Webhook apletu]({applet_url}). \n\n Podaj nast\u0119puj\u0105ce informacje:\n\n - URL: `{webhook_url}`\n - Metoda: POST\n - Typ zawarto\u015bci: application/json\n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}) na temat konfiguracji automatyzacji by obs\u0142u\u017cy\u0107 przychodz\u0105ce dane." + "default": "Aby wys\u0142a\u0107 zdarzenia do Home Assistant'a, b\u0119dziesz musia\u0142 u\u017cy\u0107 akcji \"Make a web request\" z [IFTTT Webhook apletu]({applet_url}). \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}`\n - Metoda: POST\n - Typ zawarto\u015bci: application/json\n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}) na temat konfiguracji automatyzacji by obs\u0142u\u017cy\u0107 przychodz\u0105ce dane." }, "step": { "user": { - "description": "Jeste\u015b pewny, \u017ce chcesz skonfigurowa\u0107 IFTTT?", - "title": "Konfigurowanie apletu Webhook IFTTT" + "description": "Czy chcesz skonfigurowa\u0107 IFTTT?", + "title": "Konfiguracja apletu Webhook IFTTT" } }, "title": "IFTTT" diff --git a/homeassistant/components/ifttt/.translations/pt.json b/homeassistant/components/ifttt/.translations/pt.json index 34c6496d7b1..08b7aee6a08 100644 --- a/homeassistant/components/ifttt/.translations/pt.json +++ b/homeassistant/components/ifttt/.translations/pt.json @@ -13,6 +13,6 @@ "title": "Configurar o IFTTT Webhook Applet" } }, - "title": "" + "title": "IFTTT" } } \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/pt.json b/homeassistant/components/ios/.translations/pt.json index 6752606d9f5..d38b9abb70b 100644 --- a/homeassistant/components/ios/.translations/pt.json +++ b/homeassistant/components/ios/.translations/pt.json @@ -6,9 +6,9 @@ "step": { "confirm": { "description": "Deseja configurar o componente iOS do Home Assistant?", - "title": "" + "title": "Home Assistant iOS" } }, - "title": "" + "title": "Home Assistant iOS" } } \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/pt.json b/homeassistant/components/lifx/.translations/pt.json index 5d7fdf356ef..d5c93c33993 100644 --- a/homeassistant/components/lifx/.translations/pt.json +++ b/homeassistant/components/lifx/.translations/pt.json @@ -1,7 +1,15 @@ { "config": { "abort": { - "no_devices_found": "Nenhum dispositivo LIFX encontrado na rede." - } + "no_devices_found": "Nenhum dispositivo LIFX encontrado na rede.", + "single_instance_allowed": "Apenas uma configura\u00e7\u00e3o do LIFX \u00e9 permitida." + }, + "step": { + "confirm": { + "description": "Deseja configurar o LIFX?", + "title": "LIFX" + } + }, + "title": "LIFX" } } \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/ca.json b/homeassistant/components/mailgun/.translations/ca.json new file mode 100644 index 00000000000..d644b9b8c73 --- /dev/null +++ b/homeassistant/components/mailgun/.translations/ca.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "La vostra inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per rebre missatges de Mailgun.", + "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia." + }, + "create_entry": { + "default": "Per enviar esdeveniments a Home Assistant, haureu de configurar [Webhooks amb Mailgun] ({mailgun_url}). \n\n Ompliu la informaci\u00f3 seg\u00fcent: \n\n - URL: `{webhook_url}` \n - M\u00e8tode: POST \n - Tipus de contingut: application/x-www-form-urlencoded\n\nConsulteu [la documentaci\u00f3]({docs_url}) sobre com configurar els automatismes per gestionar les dades entrants." + }, + "step": { + "user": { + "description": "Esteu segur que voleu configurar Mailgun?", + "title": "Configureu el Webhook de Mailgun" + } + }, + "title": "Mailgun" + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/ko.json b/homeassistant/components/mailgun/.translations/ko.json new file mode 100644 index 00000000000..0dd8cbdb47d --- /dev/null +++ b/homeassistant/components/mailgun/.translations/ko.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Mailgun \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud560 \uc218 \uc788\uc5b4\uc57c\ud569\ub2c8\ub2e4.", + "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." + }, + "create_entry": { + "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 [Mailgun Webhook]({mailgun_url}) \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574 \uc8fc\uc138\uc694. \n\n - URL: `{webhook_url}`\n - Method: POST\n - Content Type: application/x-www-form-urlencoded\n \n Home Assistant \ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574 \uc8fc\uc138\uc694." + }, + "step": { + "user": { + "description": "Mailgun \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Mailgun Webhook \uc124\uc815" + } + }, + "title": "Mailgun" + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/nl.json b/homeassistant/components/mailgun/.translations/nl.json new file mode 100644 index 00000000000..d71c311b7f8 --- /dev/null +++ b/homeassistant/components/mailgun/.translations/nl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Uw Home Assistant instantie moet toegankelijk zijn vanaf het internet om Mailgun-berichten te ontvangen.", + "one_instance_allowed": "Slechts \u00e9\u00e9n enkele instantie is nodig." + }, + "step": { + "user": { + "description": "Weet u zeker dat u Mailgun wilt instellen?", + "title": "Stel de Mailgun Webhook in" + } + }, + "title": "Mailgun" + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/pl.json b/homeassistant/components/mailgun/.translations/pl.json new file mode 100644 index 00000000000..ba89efab0c2 --- /dev/null +++ b/homeassistant/components/mailgun/.translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty Mailgun.", + "one_instance_allowed": "Wymagana jest tylko jedna instancja." + }, + "create_entry": { + "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant'a, musisz skonfigurowa\u0107 [Mailgun Webhook]({mailgun_url}). \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n - Typ zawarto\u015bci: application/x-www-form-urlencoded \n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}) na temat konfiguracji automatyzacji by obs\u0142u\u017cy\u0107 przychodz\u0105ce dane." + }, + "step": { + "user": { + "description": "Czy chcesz skonfigurowa\u0107 Mailgun?", + "title": "Konfiguracja Mailgun Webhook" + } + }, + "title": "Mailgun" + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/pt.json b/homeassistant/components/mailgun/.translations/pt.json new file mode 100644 index 00000000000..963d3322d84 --- /dev/null +++ b/homeassistant/components/mailgun/.translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "A sua inst\u00e2ncia Home Assistent precisa de ser acess\u00edvel a partir da internet para receber mensagens Mailgun.", + "one_instance_allowed": "Apenas uma \u00fanica inst\u00e2ncia \u00e9 necess\u00e1ria." + }, + "create_entry": { + "default": "Para enviar eventos para o Home Assistant, voc\u00ea precisar\u00e1 configurar [Webhooks with Mailgun] ({mailgun_url}). \n\n Preencha as seguintes informa\u00e7\u00f5es: \n\n - URL: `{webhook_url}`\n - M\u00e9todo: POST \n - Tipo de Conte\u00fado: application/x-www-form-urlencoded \n\n Veja [a documenta\u00e7\u00e3o] ({docs_url}) sobre como configurar automa\u00e7\u00f5es para manipular dados de entrada." + }, + "step": { + "user": { + "description": "Tem certeza de que deseja configurar o Mailgun?", + "title": "Configurar o Mailgun Webhook" + } + }, + "title": "Mailgun" + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/ru.json b/homeassistant/components/mailgun/.translations/ru.json new file mode 100644 index 00000000000..62007a95809 --- /dev/null +++ b/homeassistant/components/mailgun/.translations/ru.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 Mailgun.", + "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "create_entry": { + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c [Webhooks \u0434\u043b\u044f Mailgun]({mailgun_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445." + }, + "step": { + "user": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Mailgun?", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Mailgun Webhook" + } + }, + "title": "Mailgun" + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/sl.json b/homeassistant/components/mailgun/.translations/sl.json new file mode 100644 index 00000000000..12dad4d8c7e --- /dev/null +++ b/homeassistant/components/mailgun/.translations/sl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": " \u010ce \u017eelite prejemati sporo\u010dila Mailgun, mora biti Home Assistent dostopen prek interneta.", + "one_instance_allowed": "Potrebna je samo ena instanca." + }, + "create_entry": { + "default": "Za po\u0161iljanje dogodkov Home Assistentu boste morali nastaviti [Webhooks z Mailgun]({mailgun_url}).\n\nIzpolnite naslednje informacije:\n\n- URL: `{webhook_url}`\n- Metoda: POST\n- Vrsta vsebine: application/x-www-form-urlencoded\n\nGlej [dokumentacijo]({docs_url}) o tem, kako nastavite automations za obravnavo dohodnih podatkov." + }, + "step": { + "user": { + "description": "Ali ste prepri\u010dani, da \u017eelite nastaviti Mailgun?", + "title": "Nastavite Mailgun Webhook" + } + }, + "title": "Mailgun" + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/zh-Hant.json b/homeassistant/components/mailgun/.translations/zh-Hant.json new file mode 100644 index 00000000000..4b9ab3a7abb --- /dev/null +++ b/homeassistant/components/mailgun/.translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Mailgun \u8a0a\u606f\u3002", + "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" + }, + "create_entry": { + "default": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u8a2d\u5b9a [Webhooks with Mailgun]({mailgun_url}) \u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\n\u95dc\u65bc\u5982\u4f55\u50b3\u5165\u8cc7\u6599\u81ea\u52d5\u5316\u8a2d\u5b9a\uff0c\u8acb\u53c3\u95b1[\u6587\u4ef6]({docs_url})\u4ee5\u9032\u884c\u4e86\u89e3\u3002" + }, + "step": { + "user": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Mailgun\uff1f", + "title": "\u8a2d\u5b9a Mailgun Webhook" + } + }, + "title": "Mailgun" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/pt.json b/homeassistant/components/mqtt/.translations/pt.json index 3b36345994d..21b9cbdf755 100644 --- a/homeassistant/components/mqtt/.translations/pt.json +++ b/homeassistant/components/mqtt/.translations/pt.json @@ -9,14 +9,14 @@ "step": { "broker": { "data": { - "broker": "", + "broker": "Broker", "discovery": "Ativar descoberta", "password": "Palavra-passe", "port": "Porto", "username": "Utilizador" }, "description": "Por favor, insira os detalhes de liga\u00e7\u00e3o ao seu broker MQTT.", - "title": "" + "title": "MQTT" }, "hassio_confirm": { "data": { @@ -26,6 +26,6 @@ "title": "MQTT Broker atrav\u00e9s do add-on Hass.io" } }, - "title": "" + "title": "MQTT" } } \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/ko.json b/homeassistant/components/nest/.translations/ko.json index 0caa70aeff2..a53a26bca5a 100644 --- a/homeassistant/components/nest/.translations/ko.json +++ b/homeassistant/components/nest/.translations/ko.json @@ -4,7 +4,7 @@ "already_setup": "\ud558\ub098\uc758 Nest \uacc4\uc815\ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", "authorize_url_fail": "\uc778\uc99d url \uc0dd\uc131\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "no_flows": "Nest \ub97c \uc778\uc99d\ud558\uae30 \uc804\uc5d0 Nest \ub97c \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/components/nest/)\ub97c \uc77d\uc5b4\ubcf4\uc138\uc694." + "no_flows": "Nest \ub97c \uc778\uc99d\ud558\uae30 \uc804\uc5d0 Nest \ub97c \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/components/nest/) \ub97c \uc77d\uc5b4\ubcf4\uc138\uc694." }, "error": { "internal_error": "\ucf54\ub4dc \uc720\ud6a8\uc131 \uac80\uc0ac\uc5d0 \ub0b4\ubd80 \uc624\ub958 \ubc1c\uc0dd", @@ -24,7 +24,7 @@ "data": { "code": "\ud540 \ucf54\ub4dc" }, - "description": "Nest \uacc4\uc815\uc744 \uc5f0\uacb0\ud558\ub824\uba74, [\uacc4\uc815 \uc5f0\uacb0 \uc2b9\uc778]({url})\uc744 \ud574\uc8fc\uc138\uc694.\n\n\uc2b9\uc778 \ud6c4, \uc544\ub798\uc758 \ud540 \ucf54\ub4dc\ub97c \ubcf5\uc0ac\ud558\uc5ec \ubd99\uc5ec\ub123\uc73c\uc138\uc694.", + "description": "Nest \uacc4\uc815\uc744 \uc5f0\uacb0\ud558\ub824\uba74, [\uacc4\uc815 \uc5f0\uacb0 \uc2b9\uc778]({url}) \uc744 \ud574\uc8fc\uc138\uc694.\n\n\uc2b9\uc778 \ud6c4, \uc544\ub798\uc758 \ud540 \ucf54\ub4dc\ub97c \ubcf5\uc0ac\ud558\uc5ec \ubd99\uc5ec\ub123\uc73c\uc138\uc694.", "title": "Nest \uacc4\uc815 \uc5f0\uacb0" } }, diff --git a/homeassistant/components/nest/.translations/pt.json b/homeassistant/components/nest/.translations/pt.json index 40743fe3ddb..5ea970d9fb3 100644 --- a/homeassistant/components/nest/.translations/pt.json +++ b/homeassistant/components/nest/.translations/pt.json @@ -28,6 +28,6 @@ "title": "Associar conta Nest" } }, - "title": "" + "title": "Nest" } } \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/pt.json b/homeassistant/components/openuv/.translations/pt.json index 36f875efc00..48283a74106 100644 --- a/homeassistant/components/openuv/.translations/pt.json +++ b/homeassistant/components/openuv/.translations/pt.json @@ -15,6 +15,6 @@ "title": "Preencha com as suas informa\u00e7\u00f5es" } }, - "title": "" + "title": "OpenUV" } } \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/fr.json b/homeassistant/components/smhi/.translations/fr.json new file mode 100644 index 00000000000..d1378f183d5 --- /dev/null +++ b/homeassistant/components/smhi/.translations/fr.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "name_exists": "Ce nom est d\u00e9j\u00e0 utilis\u00e9" + }, + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nom" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/pt.json b/homeassistant/components/smhi/.translations/pt.json index a5c71885906..e814ffd5046 100644 --- a/homeassistant/components/smhi/.translations/pt.json +++ b/homeassistant/components/smhi/.translations/pt.json @@ -1,8 +1,14 @@ { "config": { + "error": { + "name_exists": "Nome j\u00e1 existe", + "wrong_location": "Localiza\u00e7\u00e3o apenas na Su\u00e9cia" + }, "step": { "user": { "data": { + "latitude": "Latitude", + "longitude": "Longitude", "name": "Nome" }, "title": "Localiza\u00e7\u00e3o na Su\u00e9cia" diff --git a/homeassistant/components/tradfri/.translations/pt.json b/homeassistant/components/tradfri/.translations/pt.json index 05d3cbb57fe..e89cb6ac620 100644 --- a/homeassistant/components/tradfri/.translations/pt.json +++ b/homeassistant/components/tradfri/.translations/pt.json @@ -18,6 +18,6 @@ "title": "Introduzir c\u00f3digo de seguran\u00e7a" } }, - "title": "" + "title": "IKEA TR\u00c5DFRI" } } \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/ca.json b/homeassistant/components/twilio/.translations/ca.json new file mode 100644 index 00000000000..3179f420ede --- /dev/null +++ b/homeassistant/components/twilio/.translations/ca.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "La vostra inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per rebre missatges de Twilio.", + "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia." + }, + "create_entry": { + "default": "Per enviar esdeveniments a Home Assistant, haureu de configurar [Webhooks amb Twilio] ({twilio_url}). \n\n Ompliu la informaci\u00f3 seg\u00fcent: \n\n - URL: `{webhook_url}` \n - M\u00e8tode: POST \n - Tipus de contingut: application/x-www-form-urlencoded\n\nConsulteu [la documentaci\u00f3]({docs_url}) sobre com configurar els automatismes per gestionar les dades entrants." + }, + "step": { + "user": { + "description": "Esteu segur que voleu configurar Twilio?", + "title": "Configureu el Webhook de Twilio" + } + }, + "title": "Twilio" + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/en.json b/homeassistant/components/twilio/.translations/en.json new file mode 100644 index 00000000000..3ee0421469c --- /dev/null +++ b/homeassistant/components/twilio/.translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive Twilio messages.", + "one_instance_allowed": "Only a single instance is necessary." + }, + "create_entry": { + "default": "To send events to Home Assistant, you will need to setup [Webhooks with Twilio]({twilio_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data." + }, + "step": { + "user": { + "description": "Are you sure you want to set up Twilio?", + "title": "Set up the Twilio Webhook" + } + }, + "title": "Twilio" + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/ko.json b/homeassistant/components/twilio/.translations/ko.json new file mode 100644 index 00000000000..028919bff90 --- /dev/null +++ b/homeassistant/components/twilio/.translations/ko.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Twilio \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud560 \uc218 \uc788\uc5b4\uc57c\ud569\ub2c8\ub2e4.", + "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." + }, + "create_entry": { + "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 [Twilio Webhook]({twilio_url}) \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574 \uc8fc\uc138\uc694. \n\n - URL: `{webhook_url}`\n - Method: POST\n - Content Type: application/x-www-form-urlencoded\n \n Home Assistant \ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574 \uc8fc\uc138\uc694." + }, + "step": { + "user": { + "description": "Twilio \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Twilio Webhook \uc124\uc815" + } + }, + "title": "Twilio" + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/nl.json b/homeassistant/components/twilio/.translations/nl.json new file mode 100644 index 00000000000..a053bf372a5 --- /dev/null +++ b/homeassistant/components/twilio/.translations/nl.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Slechts \u00e9\u00e9n exemplaar is nodig." + }, + "step": { + "user": { + "description": "Weet u zeker dat u Twilio wilt instellen?", + "title": "Stel de Twilio Webhook in" + } + }, + "title": "Twilio" + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/no.json b/homeassistant/components/twilio/.translations/no.json new file mode 100644 index 00000000000..86e5d9051b3 --- /dev/null +++ b/homeassistant/components/twilio/.translations/no.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Twilio" + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/pl.json b/homeassistant/components/twilio/.translations/pl.json new file mode 100644 index 00000000000..19c835c4b8c --- /dev/null +++ b/homeassistant/components/twilio/.translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty Twilio.", + "one_instance_allowed": "Wymagana jest tylko jedna instancja." + }, + "create_entry": { + "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant'a, musisz skonfigurowa\u0107 [Twilio Webhook]({twilio_url}). \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n - Typ zawarto\u015bci: application/x-www-form-urlencoded \n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}) na temat konfiguracji automatyzacji by obs\u0142u\u017cy\u0107 przychodz\u0105ce dane." + }, + "step": { + "user": { + "description": "Czy chcesz skonfigurowa\u0107 Twilio?", + "title": "Konfiguracja Twilio Webhook" + } + }, + "title": "Twilio" + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/ru.json b/homeassistant/components/twilio/.translations/ru.json new file mode 100644 index 00000000000..e758a47064e --- /dev/null +++ b/homeassistant/components/twilio/.translations/ru.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 Twilio.", + "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "create_entry": { + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c [Webhooks \u0434\u043b\u044f Twilio]({twilio_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445." + }, + "step": { + "user": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Twilio?", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Twilio Webhook" + } + }, + "title": "Twilio" + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/sl.json b/homeassistant/components/twilio/.translations/sl.json new file mode 100644 index 00000000000..0321cb05452 --- /dev/null +++ b/homeassistant/components/twilio/.translations/sl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": " \u010ce \u017eelite prejemati sporo\u010dila Twilio, mora biti Home Assistent dostopen prek interneta.", + "one_instance_allowed": "Potrebna je samo ena instanca." + }, + "create_entry": { + "default": "Za po\u0161iljanje dogodkov Home Assistent-u, boste morali nastaviti [Webhooks z Twilio]({twilio_url}).\n\nIzpolnite naslednje informacije:\n\n- URL: `{webhook_url}`\n- Metoda: POST\n- Vrsta vsebine: application/x-www-form-urlencoded\n\nGlej [dokumentacijo]({docs_url}) o tem, kako nastavite avtomatizacijo za obravnavo dohodnih podatkov." + }, + "step": { + "user": { + "description": "Ali ste prepri\u010dani, da \u017eelite nastaviti Twilio?", + "title": "Nastavite Twilio Webhook" + } + }, + "title": "Twilio" + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/zh-Hant.json b/homeassistant/components/twilio/.translations/zh-Hant.json new file mode 100644 index 00000000000..2e85ef7b2de --- /dev/null +++ b/homeassistant/components/twilio/.translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Twilio \u8a0a\u606f\u3002", + "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" + }, + "create_entry": { + "default": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u8a2d\u5b9a [Webhooks with Twilio]({twilio_url})\u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\n\u95dc\u65bc\u5982\u4f55\u50b3\u5165\u8cc7\u6599\u81ea\u52d5\u5316\u8a2d\u5b9a\uff0c\u8acb\u53c3\u95b1[\u6587\u4ef6]({docs_url})\u4ee5\u9032\u884c\u4e86\u89e3\u3002" + }, + "step": { + "user": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Twilio\uff1f", + "title": "\u8a2d\u5b9a Twilio Webhook" + } + }, + "title": "Twilio" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/fr.json b/homeassistant/components/unifi/.translations/fr.json new file mode 100644 index 00000000000..68e90811a3e --- /dev/null +++ b/homeassistant/components/unifi/.translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "user_privilege": "L'utilisateur doit \u00eatre administrateur" + }, + "error": { + "faulty_credentials": "Mauvaises informations d'identification de l'utilisateur", + "service_unavailable": "Aucun service disponible" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "password": "Mot de passe", + "port": "Port", + "site": "ID du site", + "username": "Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/nl.json b/homeassistant/components/unifi/.translations/nl.json index 8e87dc4b2a6..7a1eea546a2 100644 --- a/homeassistant/components/unifi/.translations/nl.json +++ b/homeassistant/components/unifi/.translations/nl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Controller site is al geconfigureerd", "user_privilege": "Gebruiker moet beheerder zijn" }, "error": { @@ -13,7 +14,9 @@ "host": "Host", "password": "Wachtwoord", "port": "Poort", - "username": "Gebruikersnaam" + "site": "Site ID", + "username": "Gebruikersnaam", + "verify_ssl": "Controller gebruik van het juiste certificaat" }, "title": "Stel de UniFi-controller in" } diff --git a/homeassistant/components/unifi/.translations/no.json b/homeassistant/components/unifi/.translations/no.json new file mode 100644 index 00000000000..7e9251dc026 --- /dev/null +++ b/homeassistant/components/unifi/.translations/no.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Passord", + "port": "Port", + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/pl.json b/homeassistant/components/unifi/.translations/pl.json index f2f8082ac76..5382adcbf7d 100644 --- a/homeassistant/components/unifi/.translations/pl.json +++ b/homeassistant/components/unifi/.translations/pl.json @@ -18,7 +18,7 @@ "username": "Nazwa u\u017cytkownika", "verify_ssl": "Kontroler u\u017cywa prawid\u0142owego certyfikatu" }, - "title": "Skonfiguruj kontroler UniFi" + "title": "Konfiguracja kontrolera UniFi" } }, "title": "Kontroler UniFi" diff --git a/homeassistant/components/upnp/.translations/pt.json b/homeassistant/components/upnp/.translations/pt.json index 5e9b516d1c2..899a5def479 100644 --- a/homeassistant/components/upnp/.translations/pt.json +++ b/homeassistant/components/upnp/.translations/pt.json @@ -11,17 +11,17 @@ }, "step": { "init": { - "title": "" + "title": "UPnP/IGD" }, "user": { "data": { "enable_port_mapping": "Ativar o mapeamento de porta para o Home Assistant", "enable_sensors": "Adicionar sensores de tr\u00e1fego", - "igd": "" + "igd": "UPnP/IGD" }, "title": "Op\u00e7\u00f5es de configura\u00e7\u00e3o para o UPnP/IGD" } }, - "title": "" + "title": "UPnP/IGD" } } \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/fr.json b/homeassistant/components/zwave/.translations/fr.json new file mode 100644 index 00000000000..c667965bebc --- /dev/null +++ b/homeassistant/components/zwave/.translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave est d\u00e9j\u00e0 configur\u00e9", + "one_instance_only": "Le composant ne prend en charge qu'une seule instance Z-Wave" + }, + "error": { + "option_error": "La validation Z-Wave a \u00e9chou\u00e9. Le chemin d'acc\u00e8s \u00e0 la cl\u00e9 USB est-il correct?" + }, + "step": { + "user": { + "data": { + "network_key": "Cl\u00e9 r\u00e9seau (laisser vide pour g\u00e9n\u00e9rer automatiquement)", + "usb_path": "Chemin USB" + }, + "title": "Configurer Z-Wave" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/ko.json b/homeassistant/components/zwave/.translations/ko.json index d57f758ce25..e288019de0c 100644 --- a/homeassistant/components/zwave/.translations/ko.json +++ b/homeassistant/components/zwave/.translations/ko.json @@ -13,7 +13,7 @@ "network_key": "\ub124\ud2b8\uc6cc\ud06c \ud0a4 (\uacf5\ub780\uc73c\ub85c \ube44\uc6cc\ub450\uba74 \uc790\ub3d9 \uc0dd\uc131\ud569\ub2c8\ub2e4)", "usb_path": "USB \uacbd\ub85c" }, - "description": "\uad6c\uc131 \ubcc0\uc218\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4](https://www.home-assistant.io/docs/z-wave/installation/)\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694", + "description": "\uad6c\uc131 \ubcc0\uc218\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4](https://www.home-assistant.io/docs/z-wave/installation/) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694", "title": "Z-Wave \uc124\uc815" } }, From af03390c4f2b76f361f5bfdd73f043861171398e Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 24 Oct 2018 21:53:18 -0600 Subject: [PATCH 260/265] Fixed an incorrect reference in the entity registry (#17775) --- homeassistant/helpers/entity_registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 4a5daa182fa..5adf748dc58 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -185,7 +185,7 @@ class EntityRegistry: for listener_ref in new.update_listeners: listener = listener_ref() if listener is None: - to_remove.append(listener) + to_remove.append(listener_ref) else: try: listener.async_registry_updated(old, new) From bc67115df38a2a5ee6bac8659651f85aab8bc2af Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Thu, 25 Oct 2018 09:45:56 +0200 Subject: [PATCH 261/265] Update HAP-python to 2.3.0 (#17778) * Update HAP-python to 2.3.0 * Fix tests --- homeassistant/components/homekit/__init__.py | 2 +- homeassistant/components/homekit/type_switches.py | 8 +++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homekit/test_type_locks.py | 1 + tests/components/homekit/test_type_media_players.py | 1 + 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 1c30de918e3..d4d8fe0216c 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -29,7 +29,7 @@ from .const import ( from .util import ( show_setup_message, validate_entity_config, validate_media_player_features) -REQUIREMENTS = ['HAP-python==2.2.2'] +REQUIREMENTS = ['HAP-python==2.3.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 59ae17b5d9d..839abe5a580 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -1,7 +1,9 @@ """Class to hold all switch accessories.""" import logging -from pyhap.const import CATEGORY_OUTLET, CATEGORY_SWITCH +from pyhap.const import ( + CATEGORY_FAUCET, CATEGORY_OUTLET, CATEGORY_SHOWER_HEAD, + CATEGORY_SPRINKLER, CATEGORY_SWITCH) from homeassistant.components.switch import DOMAIN from homeassistant.const import ( @@ -17,10 +19,6 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -CATEGORY_SPRINKLER = 28 -CATEGORY_FAUCET = 29 -CATEGORY_SHOWER_HEAD = 30 - VALVE_TYPE = { TYPE_FAUCET: (CATEGORY_FAUCET, 3), TYPE_SHOWER: (CATEGORY_SHOWER_HEAD, 2), diff --git a/requirements_all.txt b/requirements_all.txt index 4f390c992fa..489b4fe9287 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -34,7 +34,7 @@ Adafruit-SHT31==1.0.2 DoorBirdPy==0.1.3 # homeassistant.components.homekit -HAP-python==2.2.2 +HAP-python==2.3.0 # homeassistant.components.notify.mastodon Mastodon.py==1.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7e93231c82e..c735d47d551 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -19,7 +19,7 @@ requests_mock==1.5.2 # homeassistant.components.homekit -HAP-python==2.2.2 +HAP-python==2.3.0 # homeassistant.components.sensor.rmvtransport PyRMVtransport==0.1.3 diff --git a/tests/components/homekit/test_type_locks.py b/tests/components/homekit/test_type_locks.py index e7e52c65559..8132099bd3e 100644 --- a/tests/components/homekit/test_type_locks.py +++ b/tests/components/homekit/test_type_locks.py @@ -82,6 +82,7 @@ async def test_no_code(hass, hk_driver, config, events): # Set from HomeKit call_lock = async_mock_service(hass, DOMAIN, 'lock') + acc.char_target_state.value = 0 await hass.async_add_job(acc.char_target_state.client_update_value, 1) await hass.async_block_till_done() assert call_lock diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py index 6b23b3cc58e..745e4c162bc 100644 --- a/tests/components/homekit/test_type_media_players.py +++ b/tests/components/homekit/test_type_media_players.py @@ -64,6 +64,7 @@ async def test_media_player_set_state(hass, hk_driver, events): call_media_stop = async_mock_service(hass, DOMAIN, 'media_stop') call_toggle_mute = async_mock_service(hass, DOMAIN, 'volume_mute') + acc.chars[FEATURE_ON_OFF].value = False await hass.async_add_job(acc.chars[FEATURE_ON_OFF] .client_update_value, True) await hass.async_block_till_done() From c4b2c2bfcfe7bba97640ac1b3091a7d5243cf570 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomas=20Hellstr=C3=B6m?= Date: Thu, 25 Oct 2018 10:16:09 +0200 Subject: [PATCH 262/265] SMHI weather component not showing correct values in current forecast (#17783) * fixes not showing current forecast correctly * Update config_flow.py * Update smhi.py * Update requirements_all.txt * Update requirements_test_all.txt --- homeassistant/components/smhi/__init__.py | 2 +- homeassistant/components/smhi/config_flow.py | 2 -- homeassistant/components/weather/smhi.py | 1 - requirements_all.txt | 4 +--- requirements_test_all.txt | 4 +--- 5 files changed, 3 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/smhi/__init__.py b/homeassistant/components/smhi/__init__.py index d0e4b6ef487..2421addfd0c 100644 --- a/homeassistant/components/smhi/__init__.py +++ b/homeassistant/components/smhi/__init__.py @@ -12,7 +12,7 @@ from homeassistant.core import Config, HomeAssistant from .config_flow import smhi_locations # noqa: F401 from .const import DOMAIN # noqa: F401 -REQUIREMENTS = ['smhi-pkg==1.0.4'] +REQUIREMENTS = ['smhi-pkg==1.0.5'] DEFAULT_NAME = 'smhi' diff --git a/homeassistant/components/smhi/config_flow.py b/homeassistant/components/smhi/config_flow.py index 3c9875ab797..e461c6d195d 100644 --- a/homeassistant/components/smhi/config_flow.py +++ b/homeassistant/components/smhi/config_flow.py @@ -20,8 +20,6 @@ from homeassistant.util import slugify from .const import DOMAIN, HOME_LOCATION_NAME -REQUIREMENTS = ['smhi-pkg==1.0.4'] - @callback def smhi_locations(hass: HomeAssistant): diff --git a/homeassistant/components/weather/smhi.py b/homeassistant/components/weather/smhi.py index c24d3f8f091..3bbaab3f8ec 100644 --- a/homeassistant/components/weather/smhi.py +++ b/homeassistant/components/weather/smhi.py @@ -30,7 +30,6 @@ from homeassistant.components.smhi.const import ( ENTITY_ID_SENSOR_FORMAT, ATTR_SMHI_CLOUDINESS) DEPENDENCIES = ['smhi'] -REQUIREMENTS = ['smhi-pkg==1.0.4'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 489b4fe9287..304e2f51122 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1378,9 +1378,7 @@ smappy==0.2.16 # smbus-cffi==0.5.1 # homeassistant.components.smhi -# homeassistant.components.smhi.config_flow -# homeassistant.components.weather.smhi -smhi-pkg==1.0.4 +smhi-pkg==1.0.5 # homeassistant.components.media_player.snapcast snapcast==2.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c735d47d551..cdb8f48fbd0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -228,9 +228,7 @@ simplisafe-python==3.1.12 sleepyq==0.6 # homeassistant.components.smhi -# homeassistant.components.smhi.config_flow -# homeassistant.components.weather.smhi -smhi-pkg==1.0.4 +smhi-pkg==1.0.5 # homeassistant.components.climate.honeywell somecomfort==0.5.2 From 0c7b0bdb442849c3839a2c3b915dc3790c1e032e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 25 Oct 2018 19:57:36 +0200 Subject: [PATCH 263/265] Fix unloading an entry can leave states around (#17786) * Add test that tests unloading on remove * Add more test things * Untangle entity remove code from entity platform * Don't add default implementation of async_will_remove * Keep entity weakref alive --- homeassistant/helpers/entity.py | 10 ++-- homeassistant/helpers/entity_platform.py | 19 ++---- tests/test_config_entries.py | 75 ++++++++++++++++++++---- 3 files changed, 76 insertions(+), 28 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 987bdeae6ca..687ed0b6f8b 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -363,14 +363,16 @@ class Entity: async def async_remove(self): """Remove entity from Home Assistant.""" + will_remove = getattr(self, 'async_will_remove_from_hass', None) + + if will_remove: + await will_remove() # pylint: disable=not-callable + if self._on_remove is not None: while self._on_remove: self._on_remove.pop()() - if self.platform is not None: - await self.platform.async_remove_entity(self.entity_id) - else: - self.hass.states.async_remove(self.entity_id) + self.hass.states.async_remove(self.entity_id) @callback def async_registry_updated(self, old, new): diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 3ab45577236..5fd580a33f0 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -345,8 +345,10 @@ class EntityPlatform: raise HomeAssistantError( msg) - self.entities[entity.entity_id] = entity - component_entities.add(entity.entity_id) + entity_id = entity.entity_id + self.entities[entity_id] = entity + component_entities.add(entity_id) + entity.async_on_remove(lambda: self.entities.pop(entity_id)) if hasattr(entity, 'async_added_to_hass'): await entity.async_added_to_hass() @@ -365,7 +367,7 @@ class EntityPlatform: if not self.entities: return - tasks = [self._async_remove_entity(entity_id) + tasks = [self.async_remove_entity(entity_id) for entity_id in self.entities] await asyncio.wait(tasks, loop=self.hass.loop) @@ -376,7 +378,7 @@ class EntityPlatform: async def async_remove_entity(self, entity_id): """Remove entity id from platform.""" - await self._async_remove_entity(entity_id) + await self.entities[entity_id].async_remove() # Clean up polling job if no longer needed if (self._async_unsub_polling is not None and @@ -385,15 +387,6 @@ class EntityPlatform: self._async_unsub_polling() self._async_unsub_polling = None - async def _async_remove_entity(self, entity_id): - """Remove entity id from platform.""" - entity = self.entities.pop(entity_id) - - if hasattr(entity, 'async_will_remove_from_hass'): - await entity.async_will_remove_from_hass() - - self.hass.states.async_remove(entity_id) - async def _update_entity_states(self, now): """Update the states of all the polling entities. diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 340118502b1..59777e2e6bb 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -11,7 +11,8 @@ from homeassistant.setup import async_setup_component from homeassistant.util import dt from tests.common import ( - MockModule, mock_coro, MockConfigEntry, async_fire_time_changed) + MockModule, mock_coro, MockConfigEntry, async_fire_time_changed, + MockPlatform, MockEntity) @pytest.fixture @@ -40,35 +41,87 @@ def test_call_setup_entry(hass): assert len(mock_setup_entry.mock_calls) == 1 -@asyncio.coroutine -def test_remove_entry(hass, manager): +async def test_remove_entry(hass, manager): """Test that we can remove an entry.""" - mock_unload_entry = MagicMock(return_value=mock_coro(True)) + async def mock_setup_entry(hass, entry): + """Mock setting up entry.""" + hass.loop.create_task(hass.config_entries.async_forward_entry_setup( + entry, 'light')) + return True + async def mock_unload_entry(hass, entry): + """Mock unloading an entry.""" + result = await hass.config_entries.async_forward_entry_unload( + entry, 'light') + assert result + return result + + entity = MockEntity( + unique_id='1234', + name='Test Entity', + ) + + async def mock_setup_entry_platform(hass, entry, async_add_entities): + """Mock setting up platform.""" + async_add_entities([entity]) + + loader.set_component(hass, 'test', MockModule( + 'test', + async_setup_entry=mock_setup_entry, + async_unload_entry=mock_unload_entry + )) loader.set_component( - hass, 'test', - MockModule('comp', async_unload_entry=mock_unload_entry)) + hass, 'light.test', + MockPlatform(async_setup_entry=mock_setup_entry_platform)) MockConfigEntry(domain='test', entry_id='test1').add_to_manager(manager) - MockConfigEntry( + entry = MockConfigEntry( domain='test', entry_id='test2', - state=config_entries.ENTRY_STATE_LOADED - ).add_to_manager(manager) + ) + entry.add_to_manager(manager) MockConfigEntry(domain='test', entry_id='test3').add_to_manager(manager) + # Check all config entries exist assert [item.entry_id for item in manager.async_entries()] == \ ['test1', 'test2', 'test3'] - result = yield from manager.async_remove('test2') + # Setup entry + await entry.async_setup(hass) + await hass.async_block_till_done() + # Check entity state got added + assert hass.states.get('light.test_entity') is not None + # Group all_lights, light.test_entity + assert len(hass.states.async_all()) == 2 + + # Check entity got added to entity registry + ent_reg = await hass.helpers.entity_registry.async_get_registry() + assert len(ent_reg.entities) == 1 + entity_entry = list(ent_reg.entities.values())[0] + assert entity_entry.config_entry_id == entry.entry_id + + # Remove entry + result = await manager.async_remove('test2') + await hass.async_block_till_done() + + # Check that unload went well and so no need to restart assert result == { 'require_restart': False } + + # Check that config entry was removed. assert [item.entry_id for item in manager.async_entries()] == \ ['test1', 'test3'] - assert len(mock_unload_entry.mock_calls) == 1 + # Check that entity state has been removed + assert hass.states.get('light.test_entity') is None + # Just Group all_lights + assert len(hass.states.async_all()) == 1 + + # Check that entity registry entry no longer references config_entry_id + entity_entry = list(ent_reg.entities.values())[0] + assert entity_entry.config_entry_id is None @asyncio.coroutine From 121a59abe0fc37ab217429c99594fecc5455e491 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 26 Oct 2018 10:22:26 +0200 Subject: [PATCH 264/265] Bumped version to 0.81.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index edcaa88cecb..5882bd62314 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 81 -PATCH_VERSION = '0b2' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From dcf8aba1507a60b438a25b4e05da32a5b0557941 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 26 Oct 2018 12:55:44 +0200 Subject: [PATCH 265/265] frontend bump --- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_all.txt b/requirements_all.txt index 304e2f51122..ecb09679d42 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -466,7 +466,7 @@ hole==0.3.0 holidays==0.9.8 # homeassistant.components.frontend -home-assistant-frontend==20181024.0 +home-assistant-frontend==20181026.0 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cdb8f48fbd0..7da93c5e0bc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -97,7 +97,7 @@ hdate==0.6.5 holidays==0.9.8 # homeassistant.components.frontend -home-assistant-frontend==20181024.0 +home-assistant-frontend==20181026.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8