diff --git a/.coveragerc b/.coveragerc index f1ff7715580..8e5b61136c0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -10,484 +10,33 @@ omit = homeassistant/helpers/signal.py # omit pieces of code that rely on external devices being present - homeassistant/components/abode.py - homeassistant/components/*/abode.py - - homeassistant/components/ads/__init__.py - homeassistant/components/*/ads.py - - homeassistant/components/alarmdecoder.py - homeassistant/components/*/alarmdecoder.py - - homeassistant/components/ambient_station/__init__.py - homeassistant/components/ambient_station/sensor.py - - homeassistant/components/amcrest.py - homeassistant/components/*/amcrest.py - - homeassistant/components/apcupsd.py - homeassistant/components/*/apcupsd.py - - homeassistant/components/apple_tv.py - homeassistant/components/*/apple_tv.py - - homeassistant/components/aqualogic.py - homeassistant/components/*/aqualogic.py - - homeassistant/components/arduino.py - homeassistant/components/*/arduino.py - - homeassistant/components/bmw_connected_drive/*.py - homeassistant/components/*/bmw_connected_drive.py - - homeassistant/components/android_ip_webcam.py - homeassistant/components/*/android_ip_webcam.py - - homeassistant/components/arlo.py - homeassistant/components/*/arlo.py - - homeassistant/components/asterisk_mbox.py - homeassistant/components/*/asterisk_mbox.py - homeassistant/components/*/asterisk_cdr.py - - homeassistant/components/august.py - homeassistant/components/*/august.py - - homeassistant/components/axis.py - homeassistant/components/*/axis.py - - homeassistant/components/bbb_gpio.py - homeassistant/components/*/bbb_gpio.py - - homeassistant/components/blink/* - homeassistant/components/*/blink.py - - homeassistant/components/bloomsky.py - homeassistant/components/*/bloomsky.py - - homeassistant/components/coinbase.py - homeassistant/components/sensor/coinbase.py - - homeassistant/components/cast/* - homeassistant/components/*/cast.py - - homeassistant/components/cloudflare.py - - homeassistant/components/comfoconnect.py - homeassistant/components/*/comfoconnect.py - - homeassistant/components/daikin/__init__.py - homeassistant/components/daikin/const.py - homeassistant/components/*/daikin.py - - homeassistant/components/digital_ocean.py - homeassistant/components/*/digital_ocean.py - - homeassistant/components/danfoss_air/* - - homeassistant/components/dominos.py - - homeassistant/components/doorbird.py - homeassistant/components/*/doorbird.py - - homeassistant/components/dovado/* - - homeassistant/components/dweet.py - homeassistant/components/*/dweet.py - - homeassistant/components/eight_sleep.py - homeassistant/components/*/eight_sleep.py - - homeassistant/components/ecoal_boiler.py - homeassistant/components/*/ecoal_boiler.py - - homeassistant/components/ecobee.py - homeassistant/components/*/ecobee.py - - homeassistant/components/edp_redy.py - homeassistant/components/*/edp_redy.py - - homeassistant/components/egardia.py - homeassistant/components/*/egardia.py - - homeassistant/components/elkm1/* - homeassistant/components/*/elkm1.py - - homeassistant/components/enocean.py - homeassistant/components/*/enocean.py - - homeassistant/components/envisalink/__init__.py - homeassistant/components/*/envisalink.py - - homeassistant/components/evohome.py - homeassistant/components/*/evohome.py - - homeassistant/components/freebox.py - homeassistant/components/*/freebox.py - - homeassistant/components/fritzbox.py - homeassistant/components/*/fritzbox.py - - homeassistant/components/ecovacs.py - homeassistant/components/*/ecovacs.py - - homeassistant/components/esphome/__init__.py - homeassistant/components/esphome/binary_sensor.py - homeassistant/components/esphome/cover.py - homeassistant/components/esphome/fan.py - homeassistant/components/esphome/light.py - homeassistant/components/esphome/sensor.py - homeassistant/components/esphome/switch.py - - homeassistant/components/eufy.py - homeassistant/components/*/eufy.py - - homeassistant/components/fibaro/__init__.py - homeassistant/components/*/fibaro.py - - homeassistant/components/gc100.py - homeassistant/components/*/gc100.py - - homeassistant/components/google.py - homeassistant/components/*/google.py - - homeassistant/components/greeneye_monitor.py - homeassistant/components/sensor/greeneye_monitor.py - - homeassistant/components/habitica/* - homeassistant/components/*/habitica.py - - homeassistant/components/hangouts/__init__.py - homeassistant/components/hangouts/const.py - homeassistant/components/hangouts/hangouts_bot.py - homeassistant/components/hangouts/hangups_utils.py - homeassistant/components/hangouts/intents.py - homeassistant/components/*/hangouts.py - - homeassistant/components/hdmi_cec.py - homeassistant/components/*/hdmi_cec.py - - homeassistant/components/hive.py - homeassistant/components/*/hive.py - - homeassistant/components/hlk_sw16.py - homeassistant/components/*/hlk_sw16.py - - homeassistant/components/homekit_controller/* - - homeassistant/components/homematic/__init__.py - homeassistant/components/*/homematic.py - - homeassistant/components/homematicip_cloud/hap.py - homeassistant/components/homematicip_cloud/device.py - homeassistant/components/*/homematicip_cloud.py - - homeassistant/components/homeworks.py - homeassistant/components/*/homeworks.py - - homeassistant/components/huawei_lte.py - homeassistant/components/*/huawei_lte.py - - homeassistant/components/hydrawise.py - homeassistant/components/*/hydrawise.py - - homeassistant/components/ihc/* - homeassistant/components/*/ihc.py - - homeassistant/components/insteon/* - homeassistant/components/*/insteon.py - - homeassistant/components/insteon_local.py - - homeassistant/components/insteon_plm.py - - homeassistant/components/ios.py - homeassistant/components/*/ios.py - - homeassistant/components/iota.py - homeassistant/components/*/iota.py - - homeassistant/components/isy994.py - homeassistant/components/*/isy994.py - - homeassistant/components/joaoapps_join.py - homeassistant/components/*/joaoapps_join.py - - homeassistant/components/juicenet.py - homeassistant/components/*/juicenet.py - - homeassistant/components/kira.py - homeassistant/components/*/kira.py - - homeassistant/components/knx.py - homeassistant/components/*/knx.py - - homeassistant/components/konnected.py - homeassistant/components/*/konnected.py - - homeassistant/components/lametric.py - homeassistant/components/*/lametric.py - - homeassistant/components/lcn.py - homeassistant/components/*/lcn.py - - homeassistant/components/linode.py - homeassistant/components/*/linode.py - - homeassistant/components/lightwave.py - homeassistant/components/*/lightwave.py - - homeassistant/components/logi_circle.py - homeassistant/components/*/logi_circle.py - - homeassistant/components/lupusec.py - homeassistant/components/*/lupusec.py - - homeassistant/components/lutron.py - homeassistant/components/*/lutron.py - - homeassistant/components/lutron_caseta.py - homeassistant/components/*/lutron_caseta.py - - homeassistant/components/mailgun/notify.py - - homeassistant/components/matrix.py - homeassistant/components/*/matrix.py - - homeassistant/components/maxcube.py - homeassistant/components/*/maxcube.py - - homeassistant/components/mochad.py - homeassistant/components/*/mochad.py - - homeassistant/components/modbus.py - homeassistant/components/*/modbus.py - - homeassistant/components/mychevy.py - homeassistant/components/*/mychevy.py - - homeassistant/components/mysensors/* - homeassistant/components/*/mysensors.py - - homeassistant/components/neato.py - homeassistant/components/*/neato.py - - homeassistant/components/nest/__init__.py - homeassistant/components/*/nest.py - - homeassistant/components/netatmo.py - homeassistant/components/*/netatmo.py - - homeassistant/components/netgear_lte.py - homeassistant/components/*/netgear_lte.py - - homeassistant/components/octoprint.py - homeassistant/components/*/octoprint.py - - homeassistant/components/opencv.py - homeassistant/components/*/opencv.py - - homeassistant/components/opentherm_gw/* - homeassistant/components/*/opentherm_gw.py - - homeassistant/components/openuv/__init__.py - homeassistant/components/openuv/binary_sensor.py - homeassistant/components/openuv/sensor.py - - homeassistant/components/plum_lightpad.py - homeassistant/components/*/plum_lightpad.py - - homeassistant/components/pilight.py - homeassistant/components/*/pilight.py - - homeassistant/components/point/__init__.py - homeassistant/components/point/const.py - homeassistant/components/*/point.py - - homeassistant/components/switch/qwikswitch.py - homeassistant/components/light/qwikswitch.py - - homeassistant/components/rachio.py - homeassistant/components/*/rachio.py - - homeassistant/components/raincloud.py - homeassistant/components/*/raincloud.py - - homeassistant/components/rainmachine/__init__.py - homeassistant/components/rainmachine/binary_sensor.py - homeassistant/components/rainmachine/sensor.py - homeassistant/components/rainmachine/switch.py - - homeassistant/components/raspihats.py - homeassistant/components/*/raspihats.py - - homeassistant/components/*/raspyrfm.py - - homeassistant/components/rfxtrx.py - homeassistant/components/*/rfxtrx.py - - homeassistant/components/roku.py - homeassistant/components/*/roku.py - - homeassistant/components/rpi_gpio.py - homeassistant/components/*/rpi_gpio.py - - homeassistant/components/rpi_pfio.py - homeassistant/components/*/rpi_pfio.py - - homeassistant/components/sabnzbd.py - homeassistant/components/*/sabnzbd.py - - homeassistant/components/satel_integra.py - homeassistant/components/*/satel_integra.py - - homeassistant/components/scsgate.py - homeassistant/components/*/scsgate.py - - homeassistant/components/sense.py - homeassistant/components/*/sense.py - - homeassistant/components/simplisafe/__init__.py - homeassistant/components/simplisafe/alarm_control_panel.py - - homeassistant/components/sisyphus.py - homeassistant/components/*/sisyphus.py - - homeassistant/components/skybell.py - homeassistant/components/*/skybell.py - - homeassistant/components/smappee.py - homeassistant/components/*/smappee.py - - homeassistant/components/sonos/__init__.py - homeassistant/components/*/sonos.py - - homeassistant/components/tado.py - homeassistant/components/*/tado.py - - homeassistant/components/tahoma.py - homeassistant/components/*/tahoma.py - - homeassistant/components/tellduslive/__init__.py - homeassistant/components/tellduslive/entry.py - homeassistant/components/*/tellduslive.py - - homeassistant/components/tellstick.py - homeassistant/components/*/tellstick.py - - homeassistant/components/tesla.py - homeassistant/components/*/tesla.py - - homeassistant/components/thethingsnetwork.py - homeassistant/components/*/thethingsnetwork.py - - homeassistant/components/*/thinkingcleaner.py - - homeassistant/components/tibber/* - homeassistant/components/*/tibber.py - - homeassistant/components/toon.py - homeassistant/components/*/toon.py - - homeassistant/components/tplink_lte.py - homeassistant/components/*/tplink_lte.py - - homeassistant/components/tradfri.py - homeassistant/components/*/tradfri.py - - homeassistant/components/transmission.py - homeassistant/components/*/transmission.py - - homeassistant/components/notify/twilio_sms.py - homeassistant/components/notify/twilio_call.py - - homeassistant/components/upcloud.py - homeassistant/components/*/upcloud.py - - homeassistant/components/usps.py - homeassistant/components/*/usps.py - - homeassistant/components/velbus.py - homeassistant/components/*/velbus.py - - homeassistant/components/velux.py - homeassistant/components/*/velux.py - - homeassistant/components/vera.py - homeassistant/components/*/vera.py - - homeassistant/components/verisure.py - homeassistant/components/*/verisure.py - - homeassistant/components/volvooncall.py - homeassistant/components/*/volvooncall.py - - homeassistant/components/waterfurnace.py - homeassistant/components/*/waterfurnace.py - - homeassistant/components/*/webostv.py - - homeassistant/components/w800rf32.py - homeassistant/components/*/w800rf32.py - - homeassistant/components/wemo.py - homeassistant/components/*/wemo.py - - homeassistant/components/wink/* - homeassistant/components/*/wink.py - - homeassistant/components/wirelesstag.py - homeassistant/components/*/wirelesstag.py - - homeassistant/components/xiaomi_aqara.py - homeassistant/components/*/xiaomi_aqara.py - - homeassistant/components/*/xiaomi_miio.py - - homeassistant/components/zabbix.py - homeassistant/components/*/zabbix.py - - homeassistant/components/zha/__init__.py - homeassistant/components/zha/binary_sensor.py - homeassistant/components/zha/const.py - homeassistant/components/zha/event.py - homeassistant/components/zha/fan.py - homeassistant/components/zha/light.py - homeassistant/components/zha/sensor.py - homeassistant/components/zha/switch.py - homeassistant/components/zha/api.py - homeassistant/components/zha/entity.py - homeassistant/components/zha/device_entity.py - homeassistant/components/zha/core/helpers.py - homeassistant/components/zha/core/const.py - homeassistant/components/zha/core/device.py - homeassistant/components/zha/core/listeners.py - homeassistant/components/zha/core/gateway.py - homeassistant/components/*/zha.py - - homeassistant/components/zigbee.py - homeassistant/components/*/zigbee.py - - homeassistant/components/zoneminder/* - - homeassistant/components/tuya.py - homeassistant/components/*/tuya.py - - homeassistant/components/spider.py - homeassistant/components/*/spider.py - - homeassistant/components/air_quality/opensensemap.py + homeassistant/components/abode/* + homeassistant/components/ads/* homeassistant/components/air_quality/nilu.py + homeassistant/components/air_quality/norway_air.py + homeassistant/components/air_quality/opensensemap.py homeassistant/components/alarm_control_panel/alarmdotcom.py homeassistant/components/alarm_control_panel/canary.py homeassistant/components/alarm_control_panel/concord232.py homeassistant/components/alarm_control_panel/ialarm.py - 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/totalconnect.py homeassistant/components/alarm_control_panel/yale_smart_alarm.py - homeassistant/components/apiai.py + homeassistant/components/alarmdecoder/* + homeassistant/components/ambient_station/* + homeassistant/components/amcrest/* + homeassistant/components/android_ip_webcam/* + homeassistant/components/apcupsd/* + homeassistant/components/apiai/* + homeassistant/components/apple_tv/* + homeassistant/components/aqualogic/* + homeassistant/components/arduino/* + homeassistant/components/arlo/* + homeassistant/components/asterisk_mbox/* + homeassistant/components/august/* + homeassistant/components/axis/* + homeassistant/components/bbb_gpio/* homeassistant/components/binary_sensor/arest.py homeassistant/components/binary_sensor/concord232.py homeassistant/components/binary_sensor/flic.py @@ -498,7 +47,10 @@ omit = homeassistant/components/binary_sensor/rest.py homeassistant/components/binary_sensor/tapsaff.py homeassistant/components/binary_sensor/uptimerobot.py - homeassistant/components/browser.py + homeassistant/components/blink/* + homeassistant/components/bloomsky/* + homeassistant/components/bmw_connected_drive/* + homeassistant/components/browser/* homeassistant/components/calendar/caldav.py homeassistant/components/calendar/todoist.py homeassistant/components/camera/bloomsky.py @@ -515,6 +67,8 @@ omit = homeassistant/components/camera/xeoma.py homeassistant/components/camera/xiaomi.py homeassistant/components/camera/yi.py + homeassistant/components/cast/* + homeassistant/components/climate/coolmaster.py homeassistant/components/climate/ephember.py homeassistant/components/climate/eq3btsmart.py homeassistant/components/climate/flexit.py @@ -530,6 +84,9 @@ omit = homeassistant/components/climate/touchline.py homeassistant/components/climate/venstar.py homeassistant/components/climate/zhong_hong.py + homeassistant/components/cloudflare/* + homeassistant/components/coinbase/* + homeassistant/components/comfoconnect/* homeassistant/components/cover/aladdin_connect.py homeassistant/components/cover/brunt.py homeassistant/components/cover/garadget.py @@ -540,6 +97,8 @@ omit = homeassistant/components/cover/opengarage.py homeassistant/components/cover/rpi_gpio.py homeassistant/components/cover/scsgate.py + homeassistant/components/daikin/* + homeassistant/components/danfoss_air/* homeassistant/components/device_tracker/actiontec.py homeassistant/components/device_tracker/aruba.py homeassistant/components/device_tracker/asuswrt.py @@ -553,7 +112,6 @@ omit = homeassistant/components/device_tracker/ddwrt.py homeassistant/components/device_tracker/fritz.py homeassistant/components/device_tracker/google_maps.py - homeassistant/components/device_tracker/googlehome.py homeassistant/components/device_tracker/hitron_coda.py homeassistant/components/device_tracker/huawei_router.py homeassistant/components/device_tracker/icloud.py @@ -579,23 +137,84 @@ omit = homeassistant/components/device_tracker/traccar.py homeassistant/components/device_tracker/trackr.py homeassistant/components/device_tracker/ubus.py - homeassistant/components/downloader.py - homeassistant/components/emoncms_history.py + homeassistant/components/digital_ocean/* + homeassistant/components/dominos/* + homeassistant/components/doorbird/* + homeassistant/components/dovado/* + homeassistant/components/downloader/* + homeassistant/components/dweet/* + homeassistant/components/ebusd/* + homeassistant/components/ecoal_boiler/* + homeassistant/components/ecobee/* + homeassistant/components/ecovacs/* + homeassistant/components/edp_redy/* + homeassistant/components/egardia/* + homeassistant/components/eight_sleep/* + homeassistant/components/elkm1/* + homeassistant/components/emoncms_history/* homeassistant/components/emulated_hue/upnp.py - homeassistant/components/fan/mqtt.py + homeassistant/components/enocean/* + homeassistant/components/envisalink/* + homeassistant/components/esphome/__init__.py + homeassistant/components/esphome/binary_sensor.py + homeassistant/components/esphome/cover.py + homeassistant/components/esphome/fan.py + homeassistant/components/esphome/light.py + homeassistant/components/esphome/sensor.py + homeassistant/components/esphome/switch.py + homeassistant/components/eufy/* + homeassistant/components/evohome/* homeassistant/components/fan/wemo.py - homeassistant/components/folder_watcher.py - homeassistant/components/foursquare.py - homeassistant/components/goalfeed.py - homeassistant/components/idteck_prox.py - homeassistant/components/ifttt.py + homeassistant/components/fastdotcom/* + homeassistant/components/fibaro/* + homeassistant/components/folder_watcher/* + homeassistant/components/foursquare/* + homeassistant/components/freebox/* + homeassistant/components/fritzbox/* + homeassistant/components/gc100/* + homeassistant/components/goalfeed/* + homeassistant/components/google/* + homeassistant/components/googlehome/* + homeassistant/components/greeneye_monitor/* + homeassistant/components/habitica/* + homeassistant/components/hangouts/__init__.py + homeassistant/components/hangouts/* + homeassistant/components/hangouts/const.py + homeassistant/components/hangouts/hangouts_bot.py + homeassistant/components/hangouts/hangups_utils.py + homeassistant/components/hdmi_cec/* + homeassistant/components/hive/* + homeassistant/components/hlk_sw16/* + homeassistant/components/homekit_controller/* + homeassistant/components/homematic/* + homeassistant/components/homematicip_cloud/* + homeassistant/components/homeworks/* + homeassistant/components/huawei_lte/* + homeassistant/components/hydrawise/* + homeassistant/components/idteck_prox/* + homeassistant/components/ifttt/* + homeassistant/components/ihc/* homeassistant/components/image_processing/dlib_face_detect.py homeassistant/components/image_processing/dlib_face_identify.py + homeassistant/components/image_processing/qrcode.py homeassistant/components/image_processing/seven_segments.py homeassistant/components/image_processing/tensorflow.py - homeassistant/components/image_processing/qrcode.py - homeassistant/components/keyboard_remote.py - homeassistant/components/keyboard.py + homeassistant/components/insteon_local/* + homeassistant/components/insteon_plm/* + homeassistant/components/insteon/* + homeassistant/components/ios/* + homeassistant/components/iota/* + homeassistant/components/isy994/* + homeassistant/components/joaoapps_join/* + homeassistant/components/juicenet/* + homeassistant/components/keyboard_remote/* + homeassistant/components/keyboard/* + homeassistant/components/kira/* + homeassistant/components/knx/* + homeassistant/components/konnected/* + homeassistant/components/lametric/* + homeassistant/components/lcn/* + homeassistant/components/lifx/* homeassistant/components/light/avion.py homeassistant/components/light/blinksticklight.py homeassistant/components/light/blinkt.py @@ -609,7 +228,6 @@ omit = homeassistant/components/light/hyperion.py homeassistant/components/light/iglo.py homeassistant/components/light/lifx_legacy.py - homeassistant/components/light/lifx.py homeassistant/components/light/limitlessled.py homeassistant/components/light/lw12wifi.py homeassistant/components/light/mystrom.py @@ -627,14 +245,25 @@ omit = homeassistant/components/light/yeelight.py homeassistant/components/light/yeelightsunflower.py homeassistant/components/light/zengge.py - homeassistant/components/lirc.py + homeassistant/components/lightwave/* + homeassistant/components/linode/* + homeassistant/components/lirc/* homeassistant/components/lock/kiwi.py homeassistant/components/lock/lockitron.py homeassistant/components/lock/nello.py homeassistant/components/lock/nuki.py homeassistant/components/lock/sesame.py - homeassistant/components/map.py - homeassistant/components/media_extractor.py + homeassistant/components/logi_circle/* + homeassistant/components/luftdaten/* + homeassistant/components/lupusec/* + homeassistant/components/lutron_caseta/* + homeassistant/components/lutron/* + homeassistant/components/mailbox/asterisk_cdr.py + homeassistant/components/mailgun/notify.py + homeassistant/components/map/* + homeassistant/components/matrix/* + homeassistant/components/maxcube/* + homeassistant/components/media_extractor/* homeassistant/components/media_player/anthemav.py homeassistant/components/media_player/aquostv.py homeassistant/components/media_player/bluesound.py @@ -688,14 +317,22 @@ omit = homeassistant/components/media_player/yamaha_musiccast.py homeassistant/components/media_player/yamaha.py homeassistant/components/media_player/ziggo_mediabox_xl.py - homeassistant/components/mycroft.py + homeassistant/components/mochad/* + homeassistant/components/modbus/* + homeassistant/components/mychevy/* + homeassistant/components/mycroft/* + homeassistant/components/mysensors/* + homeassistant/components/neato/* + homeassistant/components/nest/* + homeassistant/components/netatmo/* + homeassistant/components/netgear_lte/* homeassistant/components/notify/aws_lambda.py homeassistant/components/notify/aws_sns.py homeassistant/components/notify/aws_sqs.py homeassistant/components/notify/ciscospark.py homeassistant/components/notify/clickatell.py - homeassistant/components/notify/clicksend.py homeassistant/components/notify/clicksend_tts.py + homeassistant/components/notify/clicksend.py homeassistant/components/notify/discord.py homeassistant/components/notify/flock.py homeassistant/components/notify/free_mobile.py @@ -726,17 +363,45 @@ omit = homeassistant/components/notify/syslog.py homeassistant/components/notify/telegram.py homeassistant/components/notify/telstra.py + homeassistant/components/notify/twilio_call.py + homeassistant/components/notify/twilio_sms.py homeassistant/components/notify/twitter.py homeassistant/components/notify/xmpp.py - homeassistant/components/nuimo_controller.py - homeassistant/components/prometheus.py - homeassistant/components/rainbird.py + homeassistant/components/nuimo_controller/* + homeassistant/components/octoprint/* + homeassistant/components/opencv/* + homeassistant/components/opentherm_gw/* + homeassistant/components/openuv/__init__.py + homeassistant/components/openuv/binary_sensor.py + homeassistant/components/openuv/sensor.py + homeassistant/components/pilight/* + homeassistant/components/plum_lightpad/* + homeassistant/components/point/* + homeassistant/components/prometheus/* + homeassistant/components/qwikswitch/* + homeassistant/components/rachio/* + homeassistant/components/rainbird/* + homeassistant/components/raincloud/* + homeassistant/components/rainmachine/__init__.py + homeassistant/components/rainmachine/binary_sensor.py + homeassistant/components/rainmachine/sensor.py + homeassistant/components/rainmachine/switch.py + homeassistant/components/raspihats/* + homeassistant/components/raspyrfm/* homeassistant/components/remember_the_milk/__init__.py homeassistant/components/remote/harmony.py homeassistant/components/remote/itach.py - homeassistant/components/route53.py + homeassistant/components/rfxtrx/* + homeassistant/components/roku/* + homeassistant/components/route53/* + homeassistant/components/rpi_gpio/* + homeassistant/components/rpi_pfio/* + homeassistant/components/sabnzbd/* + homeassistant/components/satel_integra/* homeassistant/components/scene/hunterdouglas_powerview.py homeassistant/components/scene/lifx_cloud.py + homeassistant/components/scsgate/* + homeassistant/components/sense/* homeassistant/components/sensor/aftership.py homeassistant/components/sensor/airvisual.py homeassistant/components/sensor/alpha_vantage.py @@ -754,6 +419,7 @@ omit = homeassistant/components/sensor/buienradar.py homeassistant/components/sensor/cert_expiry.py homeassistant/components/sensor/citybikes.py + homeassistant/components/sensor/coinbase.py homeassistant/components/sensor/comed_hourly_pricing.py homeassistant/components/sensor/cpuspeed.py homeassistant/components/sensor/crimereports.py @@ -776,7 +442,6 @@ omit = homeassistant/components/sensor/enphase_envoy.py homeassistant/components/sensor/envirophat.py homeassistant/components/sensor/etherscan.py - homeassistant/components/sensor/fastdotcom.py homeassistant/components/sensor/fedex.py homeassistant/components/sensor/filesize.py homeassistant/components/sensor/fints.py @@ -789,17 +454,18 @@ omit = homeassistant/components/sensor/fritzbox_netmonitor.py homeassistant/components/sensor/gearbest.py homeassistant/components/sensor/geizhals.py + homeassistant/components/sensor/github.py homeassistant/components/sensor/gitlab_ci.py homeassistant/components/sensor/gitter.py homeassistant/components/sensor/glances.py homeassistant/components/sensor/google_travel_time.py homeassistant/components/sensor/gpsd.py + homeassistant/components/sensor/greeneye_monitor.py homeassistant/components/sensor/gtfs.py homeassistant/components/sensor/gtt.py homeassistant/components/sensor/haveibeenpwned.py homeassistant/components/sensor/hp_ilo.py homeassistant/components/sensor/htu21d.py - homeassistant/components/sensor/upnp.py homeassistant/components/sensor/iliad_italy.py homeassistant/components/sensor/imap_email_content.py homeassistant/components/sensor/imap.py @@ -814,7 +480,6 @@ omit = homeassistant/components/sensor/linux_battery.py homeassistant/components/sensor/london_underground.py homeassistant/components/sensor/loopenergy.py - homeassistant/components/sensor/luftdaten.py homeassistant/components/sensor/lyft.py homeassistant/components/sensor/magicseaweed.py homeassistant/components/sensor/meteo_france.py @@ -827,8 +492,8 @@ omit = homeassistant/components/sensor/mvglive.py homeassistant/components/sensor/nederlandse_spoorwegen.py homeassistant/components/sensor/netatmo_public.py - homeassistant/components/sensor/netdata.py homeassistant/components/sensor/netdata_public.py + homeassistant/components/sensor/netdata.py homeassistant/components/sensor/neurio_energy.py homeassistant/components/sensor/nmbs.py homeassistant/components/sensor/noaa_tides.py @@ -856,6 +521,7 @@ omit = homeassistant/components/sensor/radarr.py homeassistant/components/sensor/rainbird.py homeassistant/components/sensor/recollect_waste.py + homeassistant/components/sensor/rejseplanen.py homeassistant/components/sensor/ripple.py homeassistant/components/sensor/rova.py homeassistant/components/sensor/rtorrent.py @@ -865,8 +531,8 @@ omit = homeassistant/components/sensor/serial_pm.py homeassistant/components/sensor/serial.py homeassistant/components/sensor/seventeentrack.py - homeassistant/components/sensor/sht31.py homeassistant/components/sensor/shodan.py + homeassistant/components/sensor/sht31.py homeassistant/components/sensor/sigfox.py homeassistant/components/sensor/simulated.py homeassistant/components/sensor/skybeacon.py @@ -876,8 +542,8 @@ omit = homeassistant/components/sensor/socialblade.py homeassistant/components/sensor/solaredge.py homeassistant/components/sensor/sonarr.py - homeassistant/components/sensor/speedtest.py homeassistant/components/sensor/spotcrime.py + homeassistant/components/sensor/srp_energy.py homeassistant/components/sensor/starlingbank.py homeassistant/components/sensor/steam_online.py homeassistant/components/sensor/supervisord.py @@ -885,7 +551,6 @@ omit = homeassistant/components/sensor/swiss_public_transport.py homeassistant/components/sensor/syncthru.py homeassistant/components/sensor/synologydsm.py - homeassistant/components/sensor/srp_energy.py homeassistant/components/sensor/systemmonitor.py homeassistant/components/sensor/sytadin.py homeassistant/components/sensor/tank_utility.py @@ -912,8 +577,16 @@ omit = homeassistant/components/sensor/xbox_live.py homeassistant/components/sensor/zamg.py homeassistant/components/sensor/zestimate.py - homeassistant/components/shiftr.py - homeassistant/components/spc.py + homeassistant/components/shiftr/* + homeassistant/components/simplisafe/__init__.py + homeassistant/components/simplisafe/alarm_control_panel.py + homeassistant/components/sisyphus/* + homeassistant/components/skybell/* + homeassistant/components/smappee/* + homeassistant/components/sonos/* + homeassistant/components/spc/* + homeassistant/components/speedtestdotnet/* + homeassistant/components/spider/* homeassistant/components/switch/acer_projector.py homeassistant/components/switch/anel_pwrctrl.py homeassistant/components/switch/arest.py @@ -932,8 +605,8 @@ omit = homeassistant/components/switch/pencom.py homeassistant/components/switch/pulseaudio_loopback.py homeassistant/components/switch/rainbird.py - homeassistant/components/switch/rest.py homeassistant/components/switch/recswitch.py + homeassistant/components/switch/rest.py homeassistant/components/switch/rpi_rf.py homeassistant/components/switch/snmp.py homeassistant/components/switch/switchbot.py @@ -941,16 +614,38 @@ omit = homeassistant/components/switch/telnet.py homeassistant/components/switch/tplink.py homeassistant/components/switch/vesync.py + homeassistant/components/tado/* + homeassistant/components/tahoma/* homeassistant/components/telegram_bot/* - homeassistant/components/thingspeak.py + homeassistant/components/tellduslive/* + homeassistant/components/tellstick/* + homeassistant/components/tesla/* + homeassistant/components/thethingsnetwork/* + homeassistant/components/thingspeak/* + homeassistant/components/thinkingcleaner/* + homeassistant/components/tibber/* + homeassistant/components/toon/* + homeassistant/components/tplink_lte/* + homeassistant/components/tradfri/* + homeassistant/components/transmission/* homeassistant/components/tts/amazon_polly.py homeassistant/components/tts/baidu.py homeassistant/components/tts/microsoft.py homeassistant/components/tts/picotts.py - homeassistant/components/vacuum/mqtt.py + homeassistant/components/tuya/* + homeassistant/components/upcloud/* + homeassistant/components/upnp/* + homeassistant/components/usps/* homeassistant/components/vacuum/roomba.py + homeassistant/components/velbus/* + homeassistant/components/velux/* + homeassistant/components/vera/* + homeassistant/components/verisure/* + homeassistant/components/volvooncall/* + homeassistant/components/w800rf32/* homeassistant/components/water_heater/econet.py - homeassistant/components/watson_iot.py + homeassistant/components/waterfurnace/* + homeassistant/components/watson_iot/* homeassistant/components/weather/bom.py homeassistant/components/weather/buienradar.py homeassistant/components/weather/darksky.py @@ -958,7 +653,29 @@ omit = homeassistant/components/weather/metoffice.py homeassistant/components/weather/openweathermap.py homeassistant/components/weather/zamg.py - homeassistant/components/zeroconf.py + homeassistant/components/webostv/* + homeassistant/components/wemo/* + homeassistant/components/wink/* + homeassistant/components/wirelesstag/* + homeassistant/components/xiaomi_aqara/* + homeassistant/components/xiaomi_miio/* + homeassistant/components/xs1/* + homeassistant/components/zabbix/* + homeassistant/components/zeroconf/* + homeassistant/components/zha/__init__.py + homeassistant/components/zha/api.py + homeassistant/components/zha/const.py + homeassistant/components/zha/core/channels/* + homeassistant/components/zha/core/const.py + homeassistant/components/zha/core/device.py + homeassistant/components/zha/core/gateway.py + homeassistant/components/zha/core/helpers.py + homeassistant/components/zha/device_entity.py + homeassistant/components/zha/entity.py + homeassistant/components/zha/light.py + homeassistant/components/zha/sensor.py + homeassistant/components/zigbee/* + homeassistant/components/zoneminder/* homeassistant/components/zwave/util.py [report] @@ -972,4 +689,4 @@ exclude_lines = # Don't complain if tests don't hit defensive assertion code: raise AssertionError - raise NotImplementedError + raise NotImplementedError \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 3bc284627fc..53cc6960fc3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -27,5 +27,5 @@ If the code communicates with devices, web services, or third-party tools: If the code does not interact with devices: - [ ] Tests have been added to verify that the new code works. -[ex-requir]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard.py#L14 -[ex-import]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard.py#L54 +[ex-requir]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard/__init__.py#L14 +[ex-import]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard/__init__.py#L23 diff --git a/CODEOWNERS b/CODEOWNERS index 98eaca90076..64263598121 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -7,34 +7,34 @@ 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/api/* @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/configurator/* @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/history/* @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/introduction/* @home-assistant/core +homeassistant/components/logger/* @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/panel_custom/* @home-assistant/core +homeassistant/components/panel_iframe/* @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/script/* @home-assistant/core +homeassistant/components/shell_command/* @home-assistant/core +homeassistant/components/sun/* @home-assistant/core +homeassistant/components/updater/* @home-assistant/core homeassistant/components/weblink/* @home-assistant/core -homeassistant/components/websocket_api.py @home-assistant/core +homeassistant/components/websocket_api/* @home-assistant/core homeassistant/components/zone/* @home-assistant/core # Home Assistant Developer Teams @@ -53,6 +53,7 @@ homeassistant/components/binary_sensor/hikvision.py @mezz64 homeassistant/components/binary_sensor/threshold.py @fabaff homeassistant/components/binary_sensor/uptimerobot.py @ludeeus homeassistant/components/camera/yi.py @bachya +homeassistant/components/climate/coolmaster.py @OnFreund homeassistant/components/climate/ephember.py @ttroy50 homeassistant/components/climate/eq3btsmart.py @rytilahti homeassistant/components/climate/mill.py @danielhiversen @@ -62,14 +63,13 @@ homeassistant/components/cover/group.py @cdce8p homeassistant/components/cover/template.py @PhracturedBlue homeassistant/components/device_tracker/asuswrt.py @kennedyshead homeassistant/components/device_tracker/automatic.py @armills -homeassistant/components/device_tracker/googlehome.py @ludeeus 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/device_tracker/traccar.py @ludeeus homeassistant/components/device_tracker/bt_smarthub.py @jxwolstenholme -homeassistant/components/history_graph.py @andrey-git -homeassistant/components/influx.py @fabaff +homeassistant/components/history_graph/* @andrey-git +homeassistant/components/influx/* @fabaff homeassistant/components/light/lifx_legacy.py @amelchio homeassistant/components/light/tplink.py @rytilahti homeassistant/components/light/yeelight.py @rytilahti @@ -85,7 +85,7 @@ 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/no_ip/* @fabaff homeassistant/components/notify/file.py @fabaff homeassistant/components/notify/flock.py @fabaff homeassistant/components/notify/instapush.py @fabaff @@ -94,7 +94,7 @@ homeassistant/components/notify/smtp.py @fabaff homeassistant/components/notify/syslog.py @fabaff homeassistant/components/notify/xmpp.py @fabaff homeassistant/components/notify/yessssms.py @flowolf -homeassistant/components/plant.py @ChristianKuehnel +homeassistant/components/plant/* @ChristianKuehnel homeassistant/components/remote/harmony.py @ehendrix23 homeassistant/components/scene/lifx_cloud.py @amelchio homeassistant/components/sensor/airvisual.py @bachya @@ -139,8 +139,8 @@ 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/shiftr/* @fabaff +homeassistant/components/spaceapi/* @fabaff homeassistant/components/switch/switchbot.py @danielhiversen homeassistant/components/switch/switchmate.py @danielhiversen homeassistant/components/switch/tplink.py @rytilahti @@ -150,11 +150,11 @@ 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 +homeassistant/components/xiaomi_aqara/* @danielhiversen @syssi # A homeassistant/components/ambient_station/* @bachya -homeassistant/components/arduino.py @fabaff +homeassistant/components/arduino/* @fabaff homeassistant/components/*/arduino.py @fabaff homeassistant/components/*/arest.py @fabaff homeassistant/components/*/axis.py @kane610 @@ -162,60 +162,67 @@ 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/* @ChristianKuehnel homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel homeassistant/components/*/broadlink.py @danielhiversen # C -homeassistant/components/cloudflare.py @ludeeus +homeassistant/components/cloudflare/* @ludeeus homeassistant/components/counter/* @fabaff # D -homeassistant/components/daikin.py @fredrike @rofrantz +homeassistant/components/daikin/* @fredrike @rofrantz homeassistant/components/*/daikin.py @fredrike @rofrantz homeassistant/components/*/deconz.py @kane610 -homeassistant/components/digital_ocean.py @fabaff +homeassistant/components/digital_ocean/* @fabaff homeassistant/components/*/digital_ocean.py @fabaff -homeassistant/components/dweet.py @fabaff +homeassistant/components/dweet/* @fabaff homeassistant/components/*/dweet.py @fabaff # E -homeassistant/components/ecovacs.py @OverloadUT +homeassistant/components/ecovacs/* @OverloadUT homeassistant/components/*/ecovacs.py @OverloadUT homeassistant/components/*/edp_redy.py @abmantis -homeassistant/components/edp_redy.py @abmantis -homeassistant/components/eight_sleep.py @mezz64 +homeassistant/components/edp_redy/* @abmantis +homeassistant/components/eight_sleep/* @mezz64 homeassistant/components/*/eight_sleep.py @mezz64 homeassistant/components/esphome/*.py @OttoWinter +# G +homeassistant/components/googlehome/* @ludeeus +homeassistant/components/*/googlehome.py @ludeeus + # H -homeassistant/components/hive.py @Rendili @KJonline +homeassistant/components/hive/* @Rendili @KJonline homeassistant/components/*/hive.py @Rendili @KJonline homeassistant/components/homekit/* @cdce8p -homeassistant/components/huawei_lte.py @scop +homeassistant/components/huawei_lte/* @scop homeassistant/components/*/huawei_lte.py @scop +# I +homeassistant/components/ipma/* @dgomes + # K -homeassistant/components/knx.py @Julius2342 +homeassistant/components/knx/* @Julius2342 homeassistant/components/*/knx.py @Julius2342 -homeassistant/components/konnected.py @heythisisnate +homeassistant/components/konnected/* @heythisisnate homeassistant/components/*/konnected.py @heythisisnate # L -homeassistant/components/lifx.py @amelchio +homeassistant/components/lifx/* @amelchio homeassistant/components/*/lifx.py @amelchio homeassistant/components/luftdaten/* @fabaff homeassistant/components/*/luftdaten.py @fabaff # M -homeassistant/components/matrix.py @tinloaf +homeassistant/components/matrix/* @tinloaf homeassistant/components/*/matrix.py @tinloaf -homeassistant/components/melissa.py @kennedyshead +homeassistant/components/melissa/* @kennedyshead homeassistant/components/*/melissa.py @kennedyshead homeassistant/components/*/mystrom.py @fabaff # N -homeassistant/components/ness_alarm.py @nickw444 +homeassistant/components/ness_alarm/* @nickw444 homeassistant/components/*/ness_alarm.py @nickw444 # O @@ -226,7 +233,7 @@ homeassistant/components/point/* @fredrike homeassistant/components/*/point.py @fredrike # Q -homeassistant/components/qwikswitch.py @kellerza +homeassistant/components/qwikswitch/* @kellerza homeassistant/components/*/qwikswitch.py @kellerza # R @@ -237,15 +244,16 @@ homeassistant/components/*/rfxtrx.py @danielhiversen # S homeassistant/components/simplisafe/* @bachya homeassistant/components/smartthings/* @andrewsayre +homeassistant/components/spider/* @peternijssen # T -homeassistant/components/tahoma.py @philklei +homeassistant/components/tahoma/* @philklei homeassistant/components/*/tahoma.py @philklei homeassistant/components/tellduslive/*.py @fredrike homeassistant/components/*/tellduslive.py @fredrike -homeassistant/components/tesla.py @zabuldon +homeassistant/components/tesla/* @zabuldon homeassistant/components/*/tesla.py @zabuldon -homeassistant/components/thethingsnetwork.py @fabaff +homeassistant/components/thethingsnetwork/* @fabaff homeassistant/components/*/thethingsnetwork.py @fabaff homeassistant/components/tibber/* @danielhiversen homeassistant/components/*/tibber.py @danielhiversen @@ -253,17 +261,17 @@ homeassistant/components/tradfri/* @ggravlingen homeassistant/components/*/tradfri.py @ggravlingen # U -homeassistant/components/unifi.py @kane610 +homeassistant/components/unifi/* @kane610 homeassistant/components/switch/unifi.py @kane610 -homeassistant/components/upcloud.py @scop +homeassistant/components/upcloud/* @scop homeassistant/components/*/upcloud.py @scop # V -homeassistant/components/velux.py @Julius2342 +homeassistant/components/velux/* @Julius2342 homeassistant/components/*/velux.py @Julius2342 # W -homeassistant/components/wemo.py @sqldiablo +homeassistant/components/wemo/* @sqldiablo homeassistant/components/*/wemo.py @sqldiablo # X diff --git a/Dockerfile b/Dockerfile index 0dcd0f666c7..aa9415fd1e0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ # When updating this file, please also update virtualization/Docker/Dockerfile.dev # This way, the development image and the production image are kept in sync. -FROM python:3.6 +FROM python:3.7 LABEL maintainer="Paulus Schoutsen " # Uncomment any of the following lines to disable the installation. @@ -27,7 +27,7 @@ COPY requirements_all.txt requirements_all.txt # Uninstall enum34 because some dependencies install it but breaks Python 3.4+. # See PR #8103 for more info. RUN pip3 install --no-cache-dir -r requirements_all.txt && \ - pip3 install --no-cache-dir mysqlclient psycopg2 uvloop cchardet cython tensorflow + pip3 install --no-cache-dir mysqlclient psycopg2 uvloop==0.11.3 cchardet cython tensorflow # Copy source COPY . . diff --git a/docs/source/api/helpers.rst b/docs/source/api/helpers.rst index af186fb1341..28f4059d60d 100644 --- a/docs/source/api/helpers.rst +++ b/docs/source/api/helpers.rst @@ -4,6 +4,23 @@ homeassistant.helpers package Submodules ---------- +homeassistant.helpers.aiohttp_client module +------------------------------------------- + +.. automodule:: homeassistant.helpers.aiohttp_client + :members: + :undoc-members: + :show-inheritance: + + +homeassistant.helpers.area_registry module +------------------------------------------ + +.. automodule:: homeassistant.helpers.area_registry + :members: + :undoc-members: + :show-inheritance: + homeassistant.helpers.condition module -------------------------------------- @@ -12,6 +29,14 @@ homeassistant.helpers.condition module :undoc-members: :show-inheritance: +homeassistant.helpers.config_entry_flow module +---------------------------------------------- + +.. automodule:: homeassistant.helpers.config_entry_flow + :members: + :undoc-members: + :show-inheritance: + homeassistant.helpers.config_validation module ---------------------------------------------- @@ -20,6 +45,30 @@ homeassistant.helpers.config_validation module :undoc-members: :show-inheritance: +homeassistant.helpers.data_entry_flow module +-------------------------------------------- + +.. automodule:: homeassistant.helpers.data_entry_flow + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.deprecation module +---------------------------------------- + +.. automodule:: homeassistant.helpers.depracation + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.device_registry module +-------------------------------------------- + +.. automodule:: homeassistant.helpers.device_registry + :members: + :undoc-members: + :show-inheritance: + homeassistant.helpers.discovery module -------------------------------------- @@ -28,6 +77,14 @@ homeassistant.helpers.discovery module :undoc-members: :show-inheritance: +homeassistant.helpers.dispatcher module +--------------------------------------- + +.. automodule:: homeassistant.helpers.dispatcher + :members: + :undoc-members: + :show-inheritance: + homeassistant.helpers.entity module ----------------------------------- @@ -44,6 +101,38 @@ homeassistant.helpers.entity_component module :undoc-members: :show-inheritance: +homeassistant.helpers.entity_platform module +-------------------------------------------- + +.. automodule:: homeassistant.helpers.entity_platform + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.entity_registry module +-------------------------------------------- + +.. automodule:: homeassistant.helpers.entity_registry + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.entity_values module +------------------------------------------ + +.. automodule:: homeassistant.helpers.entity_values + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.entityfilter module +----------------------------------------- + +.. automodule:: homeassistant.helpers.entityfilter + :members: + :undoc-members: + :show-inheritance: + homeassistant.helpers.event module ---------------------------------- @@ -52,10 +141,26 @@ homeassistant.helpers.event module :undoc-members: :show-inheritance: -homeassistant.helpers.event_decorators module ---------------------------------------------- +homeassistant.helpers.icon module +--------------------------------- -.. automodule:: homeassistant.helpers.event_decorators +.. automodule:: homeassistant.helpers.icon + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.intent module +----------------------------------- + +.. automodule:: homeassistant.helpers.intent + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.json module +--------------------------------- + +.. automodule:: homeassistant.helpers.json :members: :undoc-members: :show-inheritance: @@ -68,6 +173,22 @@ homeassistant.helpers.location module :undoc-members: :show-inheritance: +homeassistant.helpers.logging module +------------------------------------ + +.. automodule:: homeassistant.helpers.logging + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.restore_state module +------------------------------------------ + +.. automodule:: homeassistant.helpers.restore_state + :members: + :undoc-members: + :show-inheritance: + homeassistant.helpers.script module ----------------------------------- @@ -84,6 +205,14 @@ homeassistant.helpers.service module :undoc-members: :show-inheritance: +homeassistant.helpers.signal module +----------------------------------- + +.. automodule:: homeassistant.helpers.signal + :members: + :undoc-members: + :show-inheritance: + homeassistant.helpers.state module ---------------------------------- @@ -92,6 +221,38 @@ homeassistant.helpers.state module :undoc-members: :show-inheritance: +homeassistant.helpers.storage module +------------------------------------ + +.. automodule:: homeassistant.helpers.storage + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.sun module +-------------------------------- + +.. automodule:: homeassistant.helpers.sun + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.system_info module +---------------------------------------- + +.. automodule:: homeassistant.helpers.system_info + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.temperature module +---------------------------------------- + +.. automodule:: homeassistant.helpers.temperature + :members: + :undoc-members: + :show-inheritance: + homeassistant.helpers.template module ------------------------------------- @@ -100,6 +261,14 @@ homeassistant.helpers.template module :undoc-members: :show-inheritance: +homeassistant.helpers.translation module +----------------------------------------- + +.. automodule:: homeassistant.helpers.translation + :members: + :undoc-members: + :show-inheritance: + homeassistant.helpers.typing module ----------------------------------- diff --git a/homeassistant/auth/providers/command_line.py b/homeassistant/auth/providers/command_line.py new file mode 100644 index 00000000000..9cec34c1340 --- /dev/null +++ b/homeassistant/auth/providers/command_line.py @@ -0,0 +1,164 @@ +"""Auth provider that validates credentials via an external command.""" + +from typing import Any, Dict, Optional, cast + +import asyncio.subprocess +import collections +import logging +import os + +import voluptuous as vol + +from homeassistant.exceptions import HomeAssistantError + +from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow +from ..models import Credentials, UserMeta + + +CONF_COMMAND = "command" +CONF_ARGS = "args" +CONF_META = "meta" + +CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({ + vol.Required(CONF_COMMAND): vol.All( + str, + os.path.normpath, + msg="must be an absolute path" + ), + vol.Optional(CONF_ARGS, default=None): vol.Any(vol.DefaultTo(list), [str]), + vol.Optional(CONF_META, default=False): bool, +}, extra=vol.PREVENT_EXTRA) + +_LOGGER = logging.getLogger(__name__) + + +class InvalidAuthError(HomeAssistantError): + """Raised when authentication with given credentials fails.""" + + +@AUTH_PROVIDERS.register("command_line") +class CommandLineAuthProvider(AuthProvider): + """Auth provider validating credentials by calling a command.""" + + DEFAULT_TITLE = "Command Line Authentication" + + # which keys to accept from a program's stdout + ALLOWED_META_KEYS = ("name",) + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Extend parent's __init__. + + Adds self._user_meta dictionary to hold the user-specific + attributes provided by external programs. + """ + super().__init__(*args, **kwargs) + self._user_meta = {} # type: Dict[str, Dict[str, Any]] + + async def async_login_flow(self, context: Optional[dict]) -> LoginFlow: + """Return a flow to login.""" + return CommandLineLoginFlow(self) + + async def async_validate_login(self, username: str, password: str) -> None: + """Validate a username and password.""" + env = { + "username": username, + "password": password, + } + try: + # pylint: disable=no-member + process = await asyncio.subprocess.create_subprocess_exec( + self.config[CONF_COMMAND], *self.config[CONF_ARGS], + env=env, + stdout=asyncio.subprocess.PIPE + if self.config[CONF_META] else None, + ) + stdout, _ = (await process.communicate()) + except OSError as err: + # happens when command doesn't exist or permission is denied + _LOGGER.error("Error while authenticating %r: %s", + username, err) + raise InvalidAuthError + + if process.returncode != 0: + _LOGGER.error("User %r failed to authenticate, command exited " + "with code %d.", + username, process.returncode) + raise InvalidAuthError + + if self.config[CONF_META]: + meta = {} # type: Dict[str, str] + for _line in stdout.splitlines(): + try: + line = _line.decode().lstrip() + if line.startswith("#"): + continue + key, value = line.split("=", 1) + except ValueError: + # malformed line + continue + key = key.strip() + value = value.strip() + if key in self.ALLOWED_META_KEYS: + meta[key] = value + self._user_meta[username] = meta + + async def async_get_or_create_credentials( + self, flow_result: Dict[str, str] + ) -> Credentials: + """Get credentials based on the flow result.""" + username = flow_result["username"] + for credential in await self.async_credentials(): + if credential.data["username"] == username: + return credential + + # Create new credentials. + return self.async_create_credentials({ + "username": username, + }) + + async def async_user_meta_for_credentials( + self, credentials: Credentials + ) -> UserMeta: + """Return extra user metadata for credentials. + + Currently, only name is supported. + """ + meta = self._user_meta.get(credentials.data["username"], {}) + return UserMeta( + name=meta.get("name"), + is_active=True, + ) + + +class CommandLineLoginFlow(LoginFlow): + """Handler for the login flow.""" + + async def async_step_init( + self, user_input: Optional[Dict[str, str]] = None + ) -> Dict[str, Any]: + """Handle the step of the form.""" + errors = {} + + if user_input is not None: + user_input["username"] = user_input["username"].strip() + try: + await cast(CommandLineAuthProvider, self._auth_provider) \ + .async_validate_login( + user_input["username"], user_input["password"] + ) + except InvalidAuthError: + errors["base"] = "invalid_auth" + + if not errors: + user_input.pop("password") + return await self.async_finish(user_input) + + schema = collections.OrderedDict() # type: Dict[str, type] + schema["username"] = str + schema["password"] = str + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema(schema), + errors=errors, + ) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 5dd62005609..a018d540033 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -10,7 +10,8 @@ from typing import Any, Optional, Dict import voluptuous as vol from homeassistant import ( - core, config as conf_util, config_entries, components as core_components) + core, config as conf_util, config_entries, components as core_components, + loader) from homeassistant.components import persistent_notification from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE from homeassistant.setup import async_setup_component @@ -124,6 +125,15 @@ async def async_from_config_dict(config: Dict[str, Any], if key != core.DOMAIN) components.update(hass.config_entries.async_domains()) + # Resolve all dependencies of all components. + for component in list(components): + try: + components.update(loader.component_dependencies(hass, component)) + except loader.LoaderError: + # Ignore it, or we'll break startup + # It will be properly handled during setup. + pass + # setup components res = await core_components.async_setup(hass, config) if not res: @@ -182,6 +192,23 @@ async def async_from_config_dict(config: Dict[str, Any], '\n\n'.join(msg), "Config Warning", "config_warning" ) + # TEMP: warn users of invalid extra keys + # Remove after 0.92 + if cv.INVALID_EXTRA_KEYS_FOUND: + msg = [] + msg.append( + "Your configuration contains extra keys " + "that the platform does not support (but were silently " + "accepted before 0.88). Please find and remove the following." + "This will become a breaking change." + ) + msg.append('\n'.join('- {}'.format(it) + for it in cv.INVALID_EXTRA_KEYS_FOUND)) + + hass.components.persistent_notification.async_create( + '\n\n'.join(msg), "Config Warning", "config_warning" + ) + return hass diff --git a/homeassistant/components/abode.py b/homeassistant/components/abode/__init__.py similarity index 98% rename from homeassistant/components/abode.py rename to homeassistant/components/abode/__init__.py index 8a1a39a726f..71a1dcdd590 100644 --- a/homeassistant/components/abode.py +++ b/homeassistant/components/abode/__init__.py @@ -1,9 +1,4 @@ -""" -This component provides basic support for Abode Home Security system. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/abode/ -""" +"""Support for Abode Home Security system.""" import logging from functools import partial from requests.exceptions import HTTPError, ConnectTimeout diff --git a/homeassistant/components/alarm_control_panel/abode.py b/homeassistant/components/abode/alarm_control_panel.py similarity index 86% rename from homeassistant/components/alarm_control_panel/abode.py rename to homeassistant/components/abode/alarm_control_panel.py index 6d4e28243ea..ec5038a7a84 100644 --- a/homeassistant/components/alarm_control_panel/abode.py +++ b/homeassistant/components/abode/alarm_control_panel.py @@ -1,9 +1,4 @@ -""" -This component provides HA alarm_control_panel support for Abode System. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/alarm_control_panel.abode/ -""" +"""Support for Abode Security System alarm control panels.""" import logging import homeassistant.components.alarm_control_panel as alarm @@ -57,11 +52,6 @@ class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanel): state = None return state - @property - def code_format(self): - """Return one or more digits/characters.""" - return alarm.FORMAT_NUMBER - def alarm_disarm(self, code=None): """Send disarm command.""" self._device.set_standby() diff --git a/homeassistant/components/binary_sensor/abode.py b/homeassistant/components/abode/binary_sensor.py similarity index 90% rename from homeassistant/components/binary_sensor/abode.py rename to homeassistant/components/abode/binary_sensor.py index a821abf445b..47baef1d7e5 100644 --- a/homeassistant/components/binary_sensor/abode.py +++ b/homeassistant/components/abode/binary_sensor.py @@ -1,20 +1,14 @@ -""" -This component provides HA binary_sensor support for Abode Security System. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.abode/ -""" +"""Support for Abode Security System binary sensors.""" import logging from homeassistant.components.abode import (AbodeDevice, AbodeAutomation, DOMAIN as ABODE_DOMAIN) from homeassistant.components.binary_sensor import BinarySensorDevice +_LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['abode'] -_LOGGER = logging.getLogger(__name__) - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up a sensor for an Abode device.""" diff --git a/homeassistant/components/camera/abode.py b/homeassistant/components/abode/camera.py similarity index 92% rename from homeassistant/components/camera/abode.py rename to homeassistant/components/abode/camera.py index 39681760d4d..99613d07c47 100644 --- a/homeassistant/components/camera/abode.py +++ b/homeassistant/components/abode/camera.py @@ -1,9 +1,4 @@ -""" -This component provides HA camera support for Abode Security System. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/camera.abode/ -""" +"""Support for Abode Security System cameras.""" import logging from datetime import timedelta @@ -13,7 +8,6 @@ from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN from homeassistant.components.camera import Camera from homeassistant.util import Throttle - DEPENDENCIES = ['abode'] MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90) diff --git a/homeassistant/components/cover/abode.py b/homeassistant/components/abode/cover.py similarity index 84% rename from homeassistant/components/cover/abode.py rename to homeassistant/components/abode/cover.py index 3ba3fb118f3..03d6219ebce 100644 --- a/homeassistant/components/cover/abode.py +++ b/homeassistant/components/abode/cover.py @@ -1,15 +1,9 @@ -""" -This component provides HA cover support for Abode Security System. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/cover.abode/ -""" +"""Support for Abode Security System covers.""" import logging from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN from homeassistant.components.cover import CoverDevice - DEPENDENCIES = ['abode'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/light/abode.py b/homeassistant/components/abode/light.py similarity index 94% rename from homeassistant/components/light/abode.py rename to homeassistant/components/abode/light.py index 397d61f3073..aabf5fbccdc 100644 --- a/homeassistant/components/light/abode.py +++ b/homeassistant/components/abode/light.py @@ -1,9 +1,4 @@ -""" -This component provides HA light support for Abode Security System. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.abode/ -""" +"""Support for Abode Security System lights.""" import logging from math import ceil from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN diff --git a/homeassistant/components/lock/abode.py b/homeassistant/components/abode/lock.py similarity index 83% rename from homeassistant/components/lock/abode.py rename to homeassistant/components/abode/lock.py index a8777ccb503..ce6634268e9 100644 --- a/homeassistant/components/lock/abode.py +++ b/homeassistant/components/abode/lock.py @@ -1,15 +1,9 @@ -""" -This component provides HA lock support for Abode Security System. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/lock.abode/ -""" +"""Support for Abode Security System locks.""" import logging from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN from homeassistant.components.lock import LockDevice - DEPENDENCIES = ['abode'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/abode.py b/homeassistant/components/abode/sensor.py similarity index 92% rename from homeassistant/components/sensor/abode.py rename to homeassistant/components/abode/sensor.py index 4695a5f0471..fa6cb9323bf 100644 --- a/homeassistant/components/sensor/abode.py +++ b/homeassistant/components/abode/sensor.py @@ -1,9 +1,4 @@ -""" -Support for Abode Security System sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.abode/ -""" +"""Support for Abode Security System sensors.""" import logging from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN diff --git a/homeassistant/components/abode/services.yaml b/homeassistant/components/abode/services.yaml new file mode 100644 index 00000000000..ad0bb076d90 --- /dev/null +++ b/homeassistant/components/abode/services.yaml @@ -0,0 +1,13 @@ +capture_image: + description: Request a new image capture from a camera device. + fields: + entity_id: {description: Entity id of the camera to request an image., example: camera.downstairs_motion_camera} +change_setting: + description: Change an Abode system setting. + fields: + setting: {description: Setting to change., example: beeper_mute} + value: {description: Value of the setting., example: '1'} +trigger_quick_action: + description: Trigger an Abode quick action. + fields: + entity_id: {description: Entity id of the quick action to trigger., example: binary_sensor.home_quick_action} diff --git a/homeassistant/components/switch/abode.py b/homeassistant/components/abode/switch.py similarity index 91% rename from homeassistant/components/switch/abode.py rename to homeassistant/components/abode/switch.py index e3f993e5413..d5303a27cd2 100644 --- a/homeassistant/components/switch/abode.py +++ b/homeassistant/components/abode/switch.py @@ -1,20 +1,14 @@ -""" -This component provides HA switch support for Abode Security System. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.abode/ -""" +"""Support for Abode Security System switches.""" import logging from homeassistant.components.abode import (AbodeDevice, AbodeAutomation, DOMAIN as ABODE_DOMAIN) from homeassistant.components.switch import SwitchDevice +_LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['abode'] -_LOGGER = logging.getLogger(__name__) - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Abode switch devices.""" diff --git a/homeassistant/components/ads/__init__.py b/homeassistant/components/ads/__init__.py index 360236790f8..cfd0f37caa0 100644 --- a/homeassistant/components/ads/__init__.py +++ b/homeassistant/components/ads/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Automation Device Specification (ADS). - -For more details about this component, please refer to the documentation. -https://home-assistant.io/components/ads/ -""" +"""Support for Automation Device Specification (ADS).""" import threading import struct import logging @@ -14,7 +9,7 @@ from homeassistant.const import CONF_DEVICE, CONF_PORT, CONF_IP_ADDRESS, \ EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyads==2.2.6'] +REQUIREMENTS = ['pyads==3.0.7'] _LOGGER = logging.getLogger(__name__) @@ -78,9 +73,10 @@ def setup(hass, config): try: ads = AdsHub(client) - except pyads.pyads.ADSError: + except pyads.ADSError: _LOGGER.error( - "Could not connect to ADS host (netid=%s, port=%s)", net_id, port) + "Could not connect to ADS host (netid=%s, ip=%s, port=%s)", + net_id, ip_address, port) return False hass.data[DATA_ADS] = ads @@ -173,7 +169,7 @@ class AdsHub: self._notification_items[hnotify] = NotificationItem( hnotify, huser, name, plc_datatype, callback) - def _device_notification_callback(self, addr, notification, huser): + def _device_notification_callback(self, notification, name): """Handle device notifications.""" contents = notification.contents diff --git a/homeassistant/components/binary_sensor/ads.py b/homeassistant/components/ads/binary_sensor.py similarity index 92% rename from homeassistant/components/binary_sensor/ads.py rename to homeassistant/components/ads/binary_sensor.py index 1ee56cac9d3..6771e99cd77 100644 --- a/homeassistant/components/binary_sensor/ads.py +++ b/homeassistant/components/ads/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for ADS binary sensors. - -For more details about this platform, please refer to the documentation. -https://home-assistant.io/components/binary_sensor.ads/ -""" +"""Support for ADS binary sensors.""" import logging import voluptuous as vol @@ -44,6 +39,7 @@ class AdsBinarySensor(BinarySensorDevice): def __init__(self, ads_hub, name, ads_var, device_class): """Initialize ADS binary sensor.""" self._name = name + self._unique_id = ads_var self._state = False self._device_class = device_class or 'moving' self._ads_hub = ads_hub @@ -66,6 +62,11 @@ class AdsBinarySensor(BinarySensorDevice): """Return the default name of the binary sensor.""" return self._name + @property + def unique_id(self): + """Return an unique identifier for this entity.""" + return self._unique_id + @property def device_class(self): """Return the device class.""" diff --git a/homeassistant/components/light/ads.py b/homeassistant/components/ads/light.py similarity index 94% rename from homeassistant/components/light/ads.py rename to homeassistant/components/ads/light.py index 10df4c0bf72..e5299821e39 100644 --- a/homeassistant/components/light/ads.py +++ b/homeassistant/components/ads/light.py @@ -1,10 +1,4 @@ -""" -Support for ADS light sources. - -For more details about this platform, please refer to the documentation. -https://home-assistant.io/components/light.ads/ - -""" +"""Support for ADS light sources.""" import logging import voluptuous as vol from homeassistant.components.light import Light, ATTR_BRIGHTNESS, \ @@ -46,6 +40,7 @@ class AdsLight(Light): self._on_state = False self._brightness = None self._name = name + self._unique_id = ads_var_enable self.ads_var_enable = ads_var_enable self.ads_var_brightness = ads_var_brightness @@ -79,6 +74,11 @@ class AdsLight(Light): """Return the name of the device if any.""" return self._name + @property + def unique_id(self): + """Return an unique identifier for this entity.""" + return self._unique_id + @property def brightness(self): """Return the brightness of the light (0..255).""" diff --git a/homeassistant/components/sensor/ads.py b/homeassistant/components/ads/sensor.py similarity index 93% rename from homeassistant/components/sensor/ads.py rename to homeassistant/components/ads/sensor.py index 24515357f5e..2972f50d804 100644 --- a/homeassistant/components/sensor/ads.py +++ b/homeassistant/components/ads/sensor.py @@ -1,9 +1,4 @@ -""" -Support for ADS sensors. - -For more details about this platform, please refer to the documentation. -https://home-assistant.io/components/sensor.ads/ -""" +"""Support for ADS sensors.""" import logging import voluptuous as vol @@ -55,6 +50,7 @@ class AdsSensor(Entity): """Initialize AdsSensor entity.""" self._ads_hub = ads_hub self._name = name + self._unique_id = ads_var self._value = None self._unit_of_measurement = unit_of_measurement self.ads_var = ads_var @@ -84,6 +80,11 @@ class AdsSensor(Entity): """Return the name of the entity.""" return self._name + @property + def unique_id(self): + """Return an unique identifier for this entity.""" + return self._unique_id + @property def state(self): """Return the state of the device.""" diff --git a/homeassistant/components/switch/ads.py b/homeassistant/components/ads/switch.py similarity index 87% rename from homeassistant/components/switch/ads.py rename to homeassistant/components/ads/switch.py index ecd1e7edc31..e3aee023f21 100644 --- a/homeassistant/components/switch/ads.py +++ b/homeassistant/components/ads/switch.py @@ -1,9 +1,4 @@ -""" -Support for ADS switch platform. - -For more details about this platform, please refer to the documentation. -https://home-assistant.io/components/switch.ads/ -""" +"""Support for ADS switch platform.""" import logging import voluptuous as vol @@ -37,20 +32,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class AdsSwitch(ToggleEntity): - """Representation of an Ads switch device.""" + """Representation of an ADS switch device.""" def __init__(self, ads_hub, name, ads_var): """Initialize the AdsSwitch entity.""" self._ads_hub = ads_hub self._on_state = False self._name = name + self._unique_id = ads_var self.ads_var = ads_var async def async_added_to_hass(self): """Register device notification.""" def update(name, value): """Handle device notification.""" - _LOGGER.debug('Variable %s changed its value to %d', name, value) + _LOGGER.debug("Variable %s changed its value to %d", name, value) self._on_state = value self.schedule_update_ha_state() @@ -68,6 +64,11 @@ class AdsSwitch(ToggleEntity): """Return the name of the entity.""" return self._name + @property + def unique_id(self): + """Return an unique identifier for this entity.""" + return self._unique_id + @property def should_poll(self): """Return False because entity pushes its state to HA.""" diff --git a/homeassistant/components/air_quality/__init__.py b/homeassistant/components/air_quality/__init__.py index 5f770e84b37..66af51efcb1 100644 --- a/homeassistant/components/air_quality/__init__.py +++ b/homeassistant/components/air_quality/__init__.py @@ -8,7 +8,8 @@ from datetime import timedelta import logging from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa +from homeassistant.helpers.config_validation import ( # noqa + PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/air_quality/norway_air.py b/homeassistant/components/air_quality/norway_air.py new file mode 100644 index 00000000000..372f3ec079d --- /dev/null +++ b/homeassistant/components/air_quality/norway_air.py @@ -0,0 +1,138 @@ +""" +Sensor for checking the air quality forecast around Norway. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/air_quality.norway_air/ +""" +import logging + +from datetime import timedelta +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.air_quality import ( + PLATFORM_SCHEMA, AirQualityEntity) +from homeassistant.const import (CONF_LATITUDE, CONF_LONGITUDE, + CONF_NAME) +from homeassistant.helpers.aiohttp_client import async_get_clientsession + + +REQUIREMENTS = ['pyMetno==0.4.5'] + +_LOGGER = logging.getLogger(__name__) + +ATTRIBUTION = "Air quality from " \ + "https://luftkvalitet.miljostatus.no/, " \ + "delivered by the Norwegian Meteorological Institute." +# https://api.met.no/license_data.html + +CONF_FORECAST = 'forecast' + +DEFAULT_FORECAST = 0 +DEFAULT_NAME = 'Air quality Norway' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_FORECAST, default=DEFAULT_FORECAST): vol.Coerce(int), + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + +SCAN_INTERVAL = timedelta(minutes=5) + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the air_quality norway sensor.""" + forecast = config.get(CONF_FORECAST) + latitude = config.get(CONF_LATITUDE, hass.config.latitude) + longitude = config.get(CONF_LONGITUDE, hass.config.longitude) + name = config.get(CONF_NAME) + + if None in (latitude, longitude): + _LOGGER.error("Latitude or longitude not set in Home Assistant config") + return + + coordinates = { + 'lat': str(latitude), + 'lon': str(longitude), + } + + async_add_entities([AirSensor(name, coordinates, + forecast, async_get_clientsession(hass), + )], + True) + + +def round_state(func): + """Round state.""" + def _decorator(self): + res = func(self) + if isinstance(res, float): + return round(res, 2) + return res + return _decorator + + +class AirSensor(AirQualityEntity): + """Representation of an Yr.no sensor.""" + + def __init__(self, name, coordinates, forecast, session): + """Initialize the sensor.""" + import metno + self._name = name + self._api = metno.AirQualityData(coordinates, forecast, session) + + @property + def attribution(self) -> str: + """Return the attribution.""" + return ATTRIBUTION + + @property + def device_state_attributes(self) -> dict: + """Return other details about the sensor state.""" + return {'level': self._api.data.get('level')} + + @property + def name(self) -> str: + """Return the name of the sensor.""" + return self._name + + @property + @round_state + def air_quality_index(self): + """Return the Air Quality Index (AQI).""" + return self._api.data.get('aqi') + + @property + @round_state + def nitrogen_dioxide(self): + """Return the NO2 (nitrogen dioxide) level.""" + return self._api.data.get('no2_concentration') + + @property + @round_state + def ozone(self): + """Return the O3 (ozone) level.""" + return self._api.data.get('o3_concentration') + + @property + @round_state + def particulate_matter_2_5(self): + """Return the particulate matter 2.5 level.""" + return self._api.data.get('pm25_concentration') + + @property + @round_state + def particulate_matter_10(self): + """Return the particulate matter 10 level.""" + return self._api.data.get('pm10_concentration') + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self._api.units.get('pm25_concentration') + + async def async_update(self) -> None: + """Update the sensor.""" + await self._api.update() diff --git a/homeassistant/components/air_quality/opensensemap.py b/homeassistant/components/air_quality/opensensemap.py index fe3cca4876e..d77c0c9bfe2 100644 --- a/homeassistant/components/air_quality/opensensemap.py +++ b/homeassistant/components/air_quality/opensensemap.py @@ -2,7 +2,7 @@ Support for openSenseMap Air Quality data. For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/air_quality/opensensemap/ +https://home-assistant.io/components/air_quality.opensensemap/ """ from datetime import timedelta import logging diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index e02e074189c..86bb3e73bda 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -14,7 +14,7 @@ from homeassistant.const import ( SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS) from homeassistant.helpers.config_validation import ( # noqa - PLATFORM_SCHEMA_BASE, PLATFORM_SCHEMA_2 as PLATFORM_SCHEMA) + PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent diff --git a/homeassistant/components/alarmdecoder.py b/homeassistant/components/alarmdecoder/__init__.py similarity index 97% rename from homeassistant/components/alarmdecoder.py rename to homeassistant/components/alarmdecoder/__init__.py index 92eab728210..1f74d72809b 100644 --- a/homeassistant/components/alarmdecoder.py +++ b/homeassistant/components/alarmdecoder/__init__.py @@ -1,9 +1,4 @@ -""" -Support for AlarmDecoder devices. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/alarmdecoder/ -""" +"""Support for AlarmDecoder devices.""" import logging from datetime import timedelta diff --git a/homeassistant/components/alarm_control_panel/alarmdecoder.py b/homeassistant/components/alarmdecoder/alarm_control_panel.py similarity index 95% rename from homeassistant/components/alarm_control_panel/alarmdecoder.py rename to homeassistant/components/alarmdecoder/alarm_control_panel.py index 16e82280433..986907622b1 100644 --- a/homeassistant/components/alarm_control_panel/alarmdecoder.py +++ b/homeassistant/components/alarmdecoder/alarm_control_panel.py @@ -1,9 +1,4 @@ -""" -Support for AlarmDecoder-based alarm control panels (Honeywell/DSC). - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/alarm_control_panel.alarmdecoder/ -""" +"""Support for AlarmDecoder-based alarm control panels (Honeywell/DSC).""" import logging import voluptuous as vol diff --git a/homeassistant/components/binary_sensor/alarmdecoder.py b/homeassistant/components/alarmdecoder/binary_sensor.py similarity index 95% rename from homeassistant/components/binary_sensor/alarmdecoder.py rename to homeassistant/components/alarmdecoder/binary_sensor.py index d8fddeaa540..c5af6ea79cb 100644 --- a/homeassistant/components/binary_sensor/alarmdecoder.py +++ b/homeassistant/components/alarmdecoder/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for AlarmDecoder zone states- represented as binary sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.alarmdecoder/ -""" +"""Support for AlarmDecoder zone states- represented as binary sensors.""" import logging from homeassistant.components.binary_sensor import BinarySensorDevice diff --git a/homeassistant/components/sensor/alarmdecoder.py b/homeassistant/components/alarmdecoder/sensor.py similarity index 88% rename from homeassistant/components/sensor/alarmdecoder.py rename to homeassistant/components/alarmdecoder/sensor.py index 546d09299dc..b2f697ea83f 100644 --- a/homeassistant/components/sensor/alarmdecoder.py +++ b/homeassistant/components/alarmdecoder/sensor.py @@ -1,9 +1,4 @@ -""" -Support for AlarmDecoder Sensors (Shows Panel Display). - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.alarmdecoder/ -""" +"""Support for AlarmDecoder sensors (Shows Panel Display).""" import logging from homeassistant.helpers.entity import Entity diff --git a/homeassistant/components/alert.py b/homeassistant/components/alert/__init__.py similarity index 98% rename from homeassistant/components/alert.py rename to homeassistant/components/alert/__init__.py index 579a19c1b52..f92fd6b187b 100644 --- a/homeassistant/components/alert.py +++ b/homeassistant/components/alert/__init__.py @@ -1,9 +1,4 @@ -""" -Support for repeating alerts when conditions are met. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/alert/ -""" +"""Support for repeating alerts when conditions are met.""" import asyncio import logging from datetime import datetime, timedelta diff --git a/homeassistant/components/alert/services.yaml b/homeassistant/components/alert/services.yaml new file mode 100644 index 00000000000..1cdd1f02e7e --- /dev/null +++ b/homeassistant/components/alert/services.yaml @@ -0,0 +1,12 @@ +toggle: + description: Toggle alert's notifications. + fields: + entity_id: {description: Name of the alert to toggle., example: alert.garage_door_open} +turn_off: + description: Silence alert's notifications. + fields: + entity_id: {description: Name of the alert to silence., example: alert.garage_door_open} +turn_on: + description: Reset alert's notifications. + fields: + entity_id: {description: Name of the alert to reset., example: alert.garage_door_open} diff --git a/homeassistant/components/alexa/__init__.py b/homeassistant/components/alexa/__init__.py index 8491268dfd6..062d698d512 100644 --- a/homeassistant/components/alexa/__init__.py +++ b/homeassistant/components/alexa/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Alexa skill service end point. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/alexa/ -""" +"""Support for Alexa skill service end point.""" import logging import voluptuous as vol @@ -57,7 +52,7 @@ CONFIG_SCHEMA = vol.Schema({ async def async_setup(hass, config): - """Activate Alexa component.""" + """Activate the Alexa component.""" config = config.get(DOMAIN, {}) flash_briefings_config = config.get(CONF_FLASH_BRIEFINGS) diff --git a/homeassistant/components/alexa/auth.py b/homeassistant/components/alexa/auth.py index 978cb611895..6918ec1e54f 100644 --- a/homeassistant/components/alexa/auth.py +++ b/homeassistant/components/alexa/auth.py @@ -1,5 +1,4 @@ """Support for Alexa skill auth.""" - import asyncio import json import logging diff --git a/homeassistant/components/alexa/flash_briefings.py b/homeassistant/components/alexa/flash_briefings.py index 02f47b05617..537f04b20be 100644 --- a/homeassistant/components/alexa/flash_briefings.py +++ b/homeassistant/components/alexa/flash_briefings.py @@ -1,9 +1,4 @@ -""" -Support for Alexa skill service end point. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/alexa/ -""" +"""Support for Alexa skill service end point.""" import copy from datetime import datetime import logging diff --git a/homeassistant/components/alexa/intent.py b/homeassistant/components/alexa/intent.py index 85cb4f105cd..b30a7238b3e 100644 --- a/homeassistant/components/alexa/intent.py +++ b/homeassistant/components/alexa/intent.py @@ -1,9 +1,4 @@ -""" -Support for Alexa skill service end point. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/alexa/ -""" +"""Support for Alexa skill service end point.""" import enum import logging diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 7240912883a..4e2383bb43d 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -1,10 +1,4 @@ -"""Support for alexa Smart Home Skill API. - -API documentation: -https://developer.amazon.com/docs/smarthome/understand-the-smart-home-skill-api.html -https://developer.amazon.com/docs/device-apis/message-guide.html -""" - +"""Support for alexa Smart Home Skill API.""" import asyncio from collections import OrderedDict from datetime import datetime @@ -27,9 +21,9 @@ from homeassistant.const import ( CONF_NAME, SERVICE_LOCK, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP, SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON, - SERVICE_UNLOCK, SERVICE_VOLUME_SET, STATE_LOCKED, STATE_ON, - STATE_UNAVAILABLE, STATE_UNLOCKED, TEMP_CELSIUS, TEMP_FAHRENHEIT, - MATCH_ALL) + SERVICE_UNLOCK, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_UP, SERVICE_VOLUME_SET, + SERVICE_VOLUME_MUTE, STATE_LOCKED, STATE_ON, STATE_UNAVAILABLE, + STATE_UNLOCKED, TEMP_CELSIUS, TEMP_FAHRENHEIT, MATCH_ALL) import homeassistant.core as ha import homeassistant.util.color as color_util from homeassistant.util.decorator import Registry @@ -63,10 +57,11 @@ API_THERMOSTAT_MODES = OrderedDict([ (climate.STATE_COOL, 'COOL'), (climate.STATE_AUTO, 'AUTO'), (climate.STATE_ECO, 'ECO'), + (climate.STATE_MANUAL, 'AUTO'), (climate.STATE_OFF, 'OFF'), (climate.STATE_IDLE, 'OFF'), (climate.STATE_FAN_ONLY, 'OFF'), - (climate.STATE_DRY, 'OFF') + (climate.STATE_DRY, 'OFF'), ]) SMART_HOME_HTTP_ENDPOINT = '/api/alexa/smart_home' @@ -883,7 +878,7 @@ class _LockCapabilities(_AlexaEntity): _AlexaEndpointHealth(self.hass, self.entity)] -@ENTITY_ADAPTERS.register(media_player.DOMAIN) +@ENTITY_ADAPTERS.register(media_player.const.DOMAIN) class _MediaPlayerCapabilities(_AlexaEntity): def default_display_categories(self): return [_DisplayCategory.TV] @@ -893,19 +888,19 @@ class _MediaPlayerCapabilities(_AlexaEntity): yield _AlexaEndpointHealth(self.hass, self.entity) supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - if supported & media_player.SUPPORT_VOLUME_SET: + if supported & media_player.const.SUPPORT_VOLUME_SET: yield _AlexaSpeaker(self.entity) - step_volume_features = (media_player.SUPPORT_VOLUME_MUTE | - media_player.SUPPORT_VOLUME_STEP) + step_volume_features = (media_player.const.SUPPORT_VOLUME_MUTE | + media_player.const.SUPPORT_VOLUME_STEP) if supported & step_volume_features: yield _AlexaStepSpeaker(self.entity) - playback_features = (media_player.SUPPORT_PLAY | - media_player.SUPPORT_PAUSE | - media_player.SUPPORT_STOP | - media_player.SUPPORT_NEXT_TRACK | - media_player.SUPPORT_PREVIOUS_TRACK) + playback_features = (media_player.const.SUPPORT_PLAY | + media_player.const.SUPPORT_PAUSE | + media_player.const.SUPPORT_STOP | + media_player.const.SUPPORT_NEXT_TRACK | + media_player.const.SUPPORT_PREVIOUS_TRACK) if supported & playback_features: yield _AlexaPlaybackController(self.entity) @@ -1792,7 +1787,7 @@ async def async_api_set_volume(hass, config, directive, context): data = { ATTR_ENTITY_ID: entity.entity_id, - media_player.ATTR_MEDIA_VOLUME_LEVEL: volume, + media_player.const.ATTR_MEDIA_VOLUME_LEVEL: volume, } await hass.services.async_call( @@ -1809,7 +1804,8 @@ async def async_api_select_input(hass, config, directive, context): entity = directive.entity # attempt to map the ALL UPPERCASE payload name to a source - source_list = entity.attributes[media_player.ATTR_INPUT_SOURCE_LIST] or [] + source_list = entity.attributes[ + media_player.const.ATTR_INPUT_SOURCE_LIST] or [] for source in source_list: # response will always be space separated, so format the source in the # most likely way to find a match @@ -1824,7 +1820,7 @@ async def async_api_select_input(hass, config, directive, context): data = { ATTR_ENTITY_ID: entity.entity_id, - media_player.ATTR_INPUT_SOURCE: media_input, + media_player.const.ATTR_INPUT_SOURCE: media_input, } await hass.services.async_call( @@ -1840,7 +1836,8 @@ async def async_api_adjust_volume(hass, config, directive, context): volume_delta = int(directive.payload['volume']) entity = directive.entity - current_level = entity.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL) + current_level = entity.attributes.get( + media_player.const.ATTR_MEDIA_VOLUME_LEVEL) # read current state try: @@ -1852,11 +1849,11 @@ async def async_api_adjust_volume(hass, config, directive, context): data = { ATTR_ENTITY_ID: entity.entity_id, - media_player.ATTR_MEDIA_VOLUME_LEVEL: volume, + media_player.const.ATTR_MEDIA_VOLUME_LEVEL: volume, } await hass.services.async_call( - entity.domain, media_player.SERVICE_VOLUME_SET, + entity.domain, SERVICE_VOLUME_SET, data, blocking=False, context=context) return directive.response() @@ -1878,11 +1875,11 @@ async def async_api_adjust_volume_step(hass, config, directive, context): if volume_step > 0: await hass.services.async_call( - entity.domain, media_player.SERVICE_VOLUME_UP, + entity.domain, SERVICE_VOLUME_UP, data, blocking=False, context=context) elif volume_step < 0: await hass.services.async_call( - entity.domain, media_player.SERVICE_VOLUME_DOWN, + entity.domain, SERVICE_VOLUME_DOWN, data, blocking=False, context=context) return directive.response() @@ -1897,11 +1894,11 @@ async def async_api_set_mute(hass, config, directive, context): data = { ATTR_ENTITY_ID: entity.entity_id, - media_player.ATTR_MEDIA_VOLUME_MUTED: mute, + media_player.const.ATTR_MEDIA_VOLUME_MUTED: mute, } await hass.services.async_call( - entity.domain, media_player.SERVICE_VOLUME_MUTE, + entity.domain, SERVICE_VOLUME_MUTE, data, blocking=False, context=context) return directive.response() diff --git a/homeassistant/components/ambient_station/.translations/da.json b/homeassistant/components/ambient_station/.translations/da.json new file mode 100644 index 00000000000..ac3d86a995b --- /dev/null +++ b/homeassistant/components/ambient_station/.translations/da.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Applikationsn\u00f8gle og/eller API n\u00f8gle er allerede registreret", + "invalid_key": "Ugyldig API n\u00f8gle og/eller applikationsn\u00f8gle", + "no_devices": "Ingen enheder fundet i konto" + }, + "step": { + "user": { + "data": { + "api_key": "API n\u00f8gle", + "app_key": "Applikationsn\u00f8gle" + }, + "title": "Udfyld dine oplysninger" + } + }, + "title": "Ambient PWS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/fr.json b/homeassistant/components/ambient_station/.translations/fr.json new file mode 100644 index 00000000000..ede25d0bd4b --- /dev/null +++ b/homeassistant/components/ambient_station/.translations/fr.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "identifier_exists": "Cl\u00e9 d'application et / ou cl\u00e9 API d\u00e9j\u00e0 enregistr\u00e9e", + "invalid_key": "Cl\u00e9 d'API et / ou cl\u00e9 d'application non valide", + "no_devices": "Aucun appareil trouv\u00e9 dans le compte" + }, + "step": { + "user": { + "data": { + "api_key": "Cl\u00e9 d'API", + "app_key": "Cl\u00e9 d'application" + }, + "title": "Veuillez saisir vos informations" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/ko.json b/homeassistant/components/ambient_station/.translations/ko.json index 51a09514159..c316741d36b 100644 --- a/homeassistant/components/ambient_station/.translations/ko.json +++ b/homeassistant/components/ambient_station/.translations/ko.json @@ -11,7 +11,7 @@ "api_key": "API \ud0a4", "app_key": "Application \ud0a4" }, - "title": "\uc0ac\uc6a9\uc790 \uc815\ubcf4\ub97c \uc785\ub825\ud574 \uc8fc\uc138\uc694" + "title": "\uc0ac\uc6a9\uc790 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694" } }, "title": "Ambient PWS" diff --git a/homeassistant/components/ambient_station/.translations/nl.json b/homeassistant/components/ambient_station/.translations/nl.json new file mode 100644 index 00000000000..a070128eefe --- /dev/null +++ b/homeassistant/components/ambient_station/.translations/nl.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Applicatiesleutel en/of API-sleutel al geregistreerd", + "invalid_key": "Ongeldige API-sleutel en/of applicatiesleutel", + "no_devices": "Geen apparaten gevonden in account" + }, + "step": { + "user": { + "data": { + "api_key": "API-sleutel", + "app_key": "Applicatiesleutel" + }, + "title": "Vul uw gegevens in" + } + }, + "title": "Ambient PWS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/no.json b/homeassistant/components/ambient_station/.translations/no.json new file mode 100644 index 00000000000..0b9d377718b --- /dev/null +++ b/homeassistant/components/ambient_station/.translations/no.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Programn\u00f8kkel og/eller API-n\u00f8kkel er allerede registrert", + "invalid_key": "Ugyldig API-n\u00f8kkel og/eller programn\u00f8kkel", + "no_devices": "Ingen enheter funnet i kontoen" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "app_key": "Applikasjonsn\u00f8kkel" + }, + "title": "Fyll ut informasjonen din" + } + }, + "title": "Ambient PWS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/pl.json b/homeassistant/components/ambient_station/.translations/pl.json new file mode 100644 index 00000000000..2140b4e29fe --- /dev/null +++ b/homeassistant/components/ambient_station/.translations/pl.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Klucz aplikacji i/lub klucz API ju\u017c jest zarejestrowany", + "invalid_key": "Nieprawid\u0142owy klucz API i/lub klucz aplikacji", + "no_devices": "Nie znaleziono urz\u0105dze\u0144 na koncie" + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API", + "app_key": "Klucz aplikacji" + }, + "title": "Wprowad\u017a swoje dane" + } + }, + "title": "Ambient PWS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/pt.json b/homeassistant/components/ambient_station/.translations/pt.json new file mode 100644 index 00000000000..01078bbddfe --- /dev/null +++ b/homeassistant/components/ambient_station/.translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "Chave de API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 0991336f42a..4464992e5fa 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Ambient Weather Station Service. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/ambient_station/ -""" +"""Support for Ambient Weather Station Service.""" import logging import voluptuous as vol @@ -12,62 +7,216 @@ from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_NAME, ATTR_LOCATION, CONF_API_KEY, CONF_MONITORED_CONDITIONS, EVENT_HOMEASSISTANT_STOP) +from homeassistant.core import callback 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.dispatcher import ( + async_dispatcher_connect, async_dispatcher_send) +from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_call_later from .config_flow import configured_instances from .const import ( - ATTR_LAST_DATA, CONF_APP_KEY, DATA_CLIENT, DOMAIN, TOPIC_UPDATE) + ATTR_LAST_DATA, CONF_APP_KEY, DATA_CLIENT, DOMAIN, TOPIC_UPDATE, + TYPE_BINARY_SENSOR, TYPE_SENSOR) + +REQUIREMENTS = ['aioambient==0.1.2'] -REQUIREMENTS = ['aioambient==0.1.0'] _LOGGER = logging.getLogger(__name__) +DATA_CONFIG = 'config' + DEFAULT_SOCKET_MIN_RETRY = 15 +TYPE_24HOURRAININ = '24hourrainin' +TYPE_BAROMABSIN = 'baromabsin' +TYPE_BAROMRELIN = 'baromrelin' +TYPE_BATT1 = 'batt1' +TYPE_BATT10 = 'batt10' +TYPE_BATT2 = 'batt2' +TYPE_BATT3 = 'batt3' +TYPE_BATT4 = 'batt4' +TYPE_BATT5 = 'batt5' +TYPE_BATT6 = 'batt6' +TYPE_BATT7 = 'batt7' +TYPE_BATT8 = 'batt8' +TYPE_BATT9 = 'batt9' +TYPE_BATTOUT = 'battout' +TYPE_CO2 = 'co2' +TYPE_DAILYRAININ = 'dailyrainin' +TYPE_DEWPOINT = 'dewPoint' +TYPE_EVENTRAININ = 'eventrainin' +TYPE_FEELSLIKE = 'feelsLike' +TYPE_HOURLYRAININ = 'hourlyrainin' +TYPE_HUMIDITY = 'humidity' +TYPE_HUMIDITY1 = 'humidity1' +TYPE_HUMIDITY10 = 'humidity10' +TYPE_HUMIDITY2 = 'humidity2' +TYPE_HUMIDITY3 = 'humidity3' +TYPE_HUMIDITY4 = 'humidity4' +TYPE_HUMIDITY5 = 'humidity5' +TYPE_HUMIDITY6 = 'humidity6' +TYPE_HUMIDITY7 = 'humidity7' +TYPE_HUMIDITY8 = 'humidity8' +TYPE_HUMIDITY9 = 'humidity9' +TYPE_HUMIDITYIN = 'humidityin' +TYPE_LASTRAIN = 'lastRain' +TYPE_MAXDAILYGUST = 'maxdailygust' +TYPE_MONTHLYRAININ = 'monthlyrainin' +TYPE_RELAY1 = 'relay1' +TYPE_RELAY10 = 'relay10' +TYPE_RELAY2 = 'relay2' +TYPE_RELAY3 = 'relay3' +TYPE_RELAY4 = 'relay4' +TYPE_RELAY5 = 'relay5' +TYPE_RELAY6 = 'relay6' +TYPE_RELAY7 = 'relay7' +TYPE_RELAY8 = 'relay8' +TYPE_RELAY9 = 'relay9' +TYPE_SOILHUM1 = 'soilhum1' +TYPE_SOILHUM10 = 'soilhum10' +TYPE_SOILHUM2 = 'soilhum2' +TYPE_SOILHUM3 = 'soilhum3' +TYPE_SOILHUM4 = 'soilhum4' +TYPE_SOILHUM5 = 'soilhum5' +TYPE_SOILHUM6 = 'soilhum6' +TYPE_SOILHUM7 = 'soilhum7' +TYPE_SOILHUM8 = 'soilhum8' +TYPE_SOILHUM9 = 'soilhum9' +TYPE_SOILTEMP1F = 'soiltemp1f' +TYPE_SOILTEMP10F = 'soiltemp10f' +TYPE_SOILTEMP2F = 'soiltemp2f' +TYPE_SOILTEMP3F = 'soiltemp3f' +TYPE_SOILTEMP4F = 'soiltemp4f' +TYPE_SOILTEMP5F = 'soiltemp5f' +TYPE_SOILTEMP6F = 'soiltemp6f' +TYPE_SOILTEMP7F = 'soiltemp7f' +TYPE_SOILTEMP8F = 'soiltemp8f' +TYPE_SOILTEMP9F = 'soiltemp9f' +TYPE_SOLARRADIATION = 'solarradiation' +TYPE_TEMP10F = 'temp10f' +TYPE_TEMP1F = 'temp1f' +TYPE_TEMP2F = 'temp2f' +TYPE_TEMP3F = 'temp3f' +TYPE_TEMP4F = 'temp4f' +TYPE_TEMP5F = 'temp5f' +TYPE_TEMP6F = 'temp6f' +TYPE_TEMP7F = 'temp7f' +TYPE_TEMP8F = 'temp8f' +TYPE_TEMP9F = 'temp9f' +TYPE_TEMPF = 'tempf' +TYPE_TEMPINF = 'tempinf' +TYPE_TOTALRAININ = 'totalrainin' +TYPE_UV = 'uv' +TYPE_WEEKLYRAININ = 'weeklyrainin' +TYPE_WINDDIR = 'winddir' +TYPE_WINDDIR_AVG10M = 'winddir_avg10m' +TYPE_WINDDIR_AVG2M = 'winddir_avg2m' +TYPE_WINDGUSTDIR = 'windgustdir' +TYPE_WINDGUSTMPH = 'windgustmph' +TYPE_WINDSPDMPH_AVG10M = 'windspdmph_avg10m' +TYPE_WINDSPDMPH_AVG2M = 'windspdmph_avg2m' +TYPE_WINDSPEEDMPH = 'windspeedmph' +TYPE_YEARLYRAININ = 'yearlyrainin' SENSOR_TYPES = { - '24hourrainin': ('24 Hr Rain', 'in'), - 'baromabsin': ('Abs Pressure', 'inHg'), - 'baromrelin': ('Rel Pressure', 'inHg'), - 'battout': ('Battery', ''), - 'co2': ('co2', 'ppm'), - 'dailyrainin': ('Daily Rain', 'in'), - 'dewPoint': ('Dew Point', '°F'), - 'eventrainin': ('Event Rain', 'in'), - 'feelsLike': ('Feels Like', '°F'), - 'hourlyrainin': ('Hourly Rain Rate', 'in/hr'), - 'humidity': ('Humidity', '%'), - 'humidityin': ('Humidity In', '%'), - 'lastRain': ('Last Rain', ''), - 'maxdailygust': ('Max Gust', 'mph'), - 'monthlyrainin': ('Monthly Rain', 'in'), - 'solarradiation': ('Solar Rad', 'W/m^2'), - 'tempf': ('Temp', '°F'), - 'tempinf': ('Inside Temp', '°F'), - 'totalrainin': ('Lifetime Rain', 'in'), - 'uv': ('uv', 'Index'), - 'weeklyrainin': ('Weekly Rain', 'in'), - 'winddir': ('Wind Dir', '°'), - 'winddir_avg10m': ('Wind Dir Avg 10m', '°'), - 'winddir_avg2m': ('Wind Dir Avg 2m', 'mph'), - 'windgustdir': ('Gust Dir', '°'), - 'windgustmph': ('Wind Gust', 'mph'), - 'windspdmph_avg10m': ('Wind Avg 10m', 'mph'), - 'windspdmph_avg2m': ('Wind Avg 2m', 'mph'), - 'windspeedmph': ('Wind Speed', 'mph'), - 'yearlyrainin': ('Yearly Rain', 'in'), + TYPE_24HOURRAININ: ('24 Hr Rain', 'in', TYPE_SENSOR, None), + TYPE_BAROMABSIN: ('Abs Pressure', 'inHg', TYPE_SENSOR, None), + TYPE_BAROMRELIN: ('Rel Pressure', 'inHg', TYPE_SENSOR, None), + TYPE_BATT10: ('Battery 10', None, TYPE_BINARY_SENSOR, 'battery'), + TYPE_BATT1: ('Battery 1', None, TYPE_BINARY_SENSOR, 'battery'), + TYPE_BATT2: ('Battery 2', None, TYPE_BINARY_SENSOR, 'battery'), + TYPE_BATT3: ('Battery 3', None, TYPE_BINARY_SENSOR, 'battery'), + TYPE_BATT4: ('Battery 4', None, TYPE_BINARY_SENSOR, 'battery'), + TYPE_BATT5: ('Battery 5', None, TYPE_BINARY_SENSOR, 'battery'), + TYPE_BATT6: ('Battery 6', None, TYPE_BINARY_SENSOR, 'battery'), + TYPE_BATT7: ('Battery 7', None, TYPE_BINARY_SENSOR, 'battery'), + TYPE_BATT8: ('Battery 8', None, TYPE_BINARY_SENSOR, 'battery'), + TYPE_BATT9: ('Battery 9', None, TYPE_BINARY_SENSOR, 'battery'), + TYPE_BATTOUT: ('Battery', None, TYPE_BINARY_SENSOR, 'battery'), + TYPE_CO2: ('co2', 'ppm', TYPE_SENSOR, None), + TYPE_DAILYRAININ: ('Daily Rain', 'in', TYPE_SENSOR, None), + TYPE_DEWPOINT: ('Dew Point', '°F', TYPE_SENSOR, None), + TYPE_EVENTRAININ: ('Event Rain', 'in', TYPE_SENSOR, None), + TYPE_FEELSLIKE: ('Feels Like', '°F', TYPE_SENSOR, None), + TYPE_HOURLYRAININ: ('Hourly Rain Rate', 'in/hr', TYPE_SENSOR, None), + TYPE_HUMIDITY10: ('Humidity 10', '%', TYPE_SENSOR, None), + TYPE_HUMIDITY1: ('Humidity 1', '%', TYPE_SENSOR, None), + TYPE_HUMIDITY2: ('Humidity 2', '%', TYPE_SENSOR, None), + TYPE_HUMIDITY3: ('Humidity 3', '%', TYPE_SENSOR, None), + TYPE_HUMIDITY4: ('Humidity 4', '%', TYPE_SENSOR, None), + TYPE_HUMIDITY5: ('Humidity 5', '%', TYPE_SENSOR, None), + TYPE_HUMIDITY6: ('Humidity 6', '%', TYPE_SENSOR, None), + TYPE_HUMIDITY7: ('Humidity 7', '%', TYPE_SENSOR, None), + TYPE_HUMIDITY8: ('Humidity 8', '%', TYPE_SENSOR, None), + TYPE_HUMIDITY9: ('Humidity 9', '%', TYPE_SENSOR, None), + TYPE_HUMIDITY: ('Humidity', '%', TYPE_SENSOR, None), + TYPE_HUMIDITYIN: ('Humidity In', '%', TYPE_SENSOR, None), + TYPE_LASTRAIN: ('Last Rain', None, TYPE_SENSOR, None), + TYPE_MAXDAILYGUST: ('Max Gust', 'mph', TYPE_SENSOR, None), + TYPE_MONTHLYRAININ: ('Monthly Rain', 'in', TYPE_SENSOR, None), + TYPE_RELAY10: ('Relay 10', None, TYPE_BINARY_SENSOR, 'connectivity'), + TYPE_RELAY1: ('Relay 1', None, TYPE_BINARY_SENSOR, 'connectivity'), + TYPE_RELAY2: ('Relay 2', None, TYPE_BINARY_SENSOR, 'connectivity'), + TYPE_RELAY3: ('Relay 3', None, TYPE_BINARY_SENSOR, 'connectivity'), + TYPE_RELAY4: ('Relay 4', None, TYPE_BINARY_SENSOR, 'connectivity'), + TYPE_RELAY5: ('Relay 5', None, TYPE_BINARY_SENSOR, 'connectivity'), + TYPE_RELAY6: ('Relay 6', None, TYPE_BINARY_SENSOR, 'connectivity'), + TYPE_RELAY7: ('Relay 7', None, TYPE_BINARY_SENSOR, 'connectivity'), + TYPE_RELAY8: ('Relay 8', None, TYPE_BINARY_SENSOR, 'connectivity'), + TYPE_RELAY9: ('Relay 9', None, TYPE_BINARY_SENSOR, 'connectivity'), + TYPE_SOILHUM10: ('Soil Humidity 10', '%', TYPE_SENSOR, None), + TYPE_SOILHUM1: ('Soil Humidity 1', '%', TYPE_SENSOR, None), + TYPE_SOILHUM2: ('Soil Humidity 2', '%', TYPE_SENSOR, None), + TYPE_SOILHUM3: ('Soil Humidity 3', '%', TYPE_SENSOR, None), + TYPE_SOILHUM4: ('Soil Humidity 4', '%', TYPE_SENSOR, None), + TYPE_SOILHUM5: ('Soil Humidity 5', '%', TYPE_SENSOR, None), + TYPE_SOILHUM6: ('Soil Humidity 6', '%', TYPE_SENSOR, None), + TYPE_SOILHUM7: ('Soil Humidity 7', '%', TYPE_SENSOR, None), + TYPE_SOILHUM8: ('Soil Humidity 8', '%', TYPE_SENSOR, None), + TYPE_SOILHUM9: ('Soil Humidity 9', '%', TYPE_SENSOR, None), + TYPE_SOILTEMP10F: ('Soil Temp 10', '°F', TYPE_SENSOR, None), + TYPE_SOILTEMP1F: ('Soil Temp 1', '°F', TYPE_SENSOR, None), + TYPE_SOILTEMP2F: ('Soil Temp 2', '°F', TYPE_SENSOR, None), + TYPE_SOILTEMP3F: ('Soil Temp 3', '°F', TYPE_SENSOR, None), + TYPE_SOILTEMP4F: ('Soil Temp 4', '°F', TYPE_SENSOR, None), + TYPE_SOILTEMP5F: ('Soil Temp 5', '°F', TYPE_SENSOR, None), + TYPE_SOILTEMP6F: ('Soil Temp 6', '°F', TYPE_SENSOR, None), + TYPE_SOILTEMP7F: ('Soil Temp 7', '°F', TYPE_SENSOR, None), + TYPE_SOILTEMP8F: ('Soil Temp 8', '°F', TYPE_SENSOR, None), + TYPE_SOILTEMP9F: ('Soil Temp 9', '°F', TYPE_SENSOR, None), + TYPE_SOLARRADIATION: ('Solar Rad', 'W/m^2', TYPE_SENSOR, None), + TYPE_TEMP10F: ('Temp 10', '°F', TYPE_SENSOR, None), + TYPE_TEMP1F: ('Temp 1', '°F', TYPE_SENSOR, None), + TYPE_TEMP2F: ('Temp 2', '°F', TYPE_SENSOR, None), + TYPE_TEMP3F: ('Temp 3', '°F', TYPE_SENSOR, None), + TYPE_TEMP4F: ('Temp 4', '°F', TYPE_SENSOR, None), + TYPE_TEMP5F: ('Temp 5', '°F', TYPE_SENSOR, None), + TYPE_TEMP6F: ('Temp 6', '°F', TYPE_SENSOR, None), + TYPE_TEMP7F: ('Temp 7', '°F', TYPE_SENSOR, None), + TYPE_TEMP8F: ('Temp 8', '°F', TYPE_SENSOR, None), + TYPE_TEMP9F: ('Temp 9', '°F', TYPE_SENSOR, None), + TYPE_TEMPF: ('Temp', '°F', TYPE_SENSOR, None), + TYPE_TEMPINF: ('Inside Temp', '°F', TYPE_SENSOR, None), + TYPE_TOTALRAININ: ('Lifetime Rain', 'in', TYPE_SENSOR, None), + TYPE_UV: ('uv', 'Index', TYPE_SENSOR, None), + TYPE_WEEKLYRAININ: ('Weekly Rain', 'in', TYPE_SENSOR, None), + TYPE_WINDDIR: ('Wind Dir', '°', TYPE_SENSOR, None), + TYPE_WINDDIR_AVG10M: ('Wind Dir Avg 10m', '°', TYPE_SENSOR, None), + TYPE_WINDDIR_AVG2M: ('Wind Dir Avg 2m', 'mph', TYPE_SENSOR, None), + TYPE_WINDGUSTDIR: ('Gust Dir', '°', TYPE_SENSOR, None), + TYPE_WINDGUSTMPH: ('Wind Gust', 'mph', TYPE_SENSOR, None), + TYPE_WINDSPDMPH_AVG10M: ('Wind Avg 10m', 'mph', TYPE_SENSOR, None), + TYPE_WINDSPDMPH_AVG2M: ('Wind Avg 2m', 'mph', TYPE_SENSOR, None), + TYPE_WINDSPEEDMPH: ('Wind Speed', 'mph', TYPE_SENSOR, None), + TYPE_YEARLYRAININ: ('Yearly Rain', 'in', TYPE_SENSOR, None), } CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ - vol.Required(CONF_APP_KEY): - cv.string, - vol.Required(CONF_API_KEY): - cv.string, - vol.Optional( - CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): + vol.Required(CONF_APP_KEY): cv.string, + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_MONITORED_CONDITIONS): vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), }) }, extra=vol.ALLOW_EXTRA) @@ -83,12 +232,20 @@ async def async_setup(hass, config): conf = config[DOMAIN] + # Store config for use during entry setup: + hass.data[DOMAIN][DATA_CONFIG] = conf + if conf[CONF_APP_KEY] in configured_instances(hass): return True hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={'source': SOURCE_IMPORT}, data=conf)) + DOMAIN, + context={'source': SOURCE_IMPORT}, + data={ + CONF_API_KEY: conf[CONF_API_KEY], + CONF_APP_KEY: conf[CONF_APP_KEY] + })) return True @@ -96,22 +253,20 @@ async def async_setup(hass, config): async def async_setup_entry(hass, config_entry): """Set up the Ambient PWS as config entry.""" from aioambient import Client - from aioambient.errors import WebsocketConnectionError + from aioambient.errors import WebsocketError session = aiohttp_client.async_get_clientsession(hass) try: ambient = AmbientStation( - hass, - config_entry, + hass, config_entry, Client( config_entry.data[CONF_API_KEY], config_entry.data[CONF_APP_KEY], session), - config_entry.data.get( - CONF_MONITORED_CONDITIONS, list(SENSOR_TYPES))) + hass.data[DOMAIN][DATA_CONFIG].get(CONF_MONITORED_CONDITIONS, [])) hass.loop.create_task(ambient.ws_connect()) hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = ambient - except WebsocketConnectionError as err: + except WebsocketError as err: _LOGGER.error('Config entry failed: %s', err) raise ConfigEntryNotReady @@ -126,8 +281,9 @@ async def async_unload_entry(hass, config_entry): ambient = hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id) hass.async_create_task(ambient.ws_disconnect()) - await hass.config_entries.async_forward_entry_unload( - config_entry, 'sensor') + for component in ('binary_sensor', 'sensor'): + await hass.config_entries.async_forward_entry_unload( + config_entry, component) return True @@ -172,15 +328,27 @@ class AmbientStation: _LOGGER.debug('New station subscription: %s', data) + # If the user hasn't specified monitored conditions, use only + # those that their station supports (and which are defined + # here): + if not self.monitored_conditions: + self.monitored_conditions = [ + k for k in station['lastData'].keys() + if k in SENSOR_TYPES + ] + self.stations[station['macAddress']] = { ATTR_LAST_DATA: station['lastData'], - ATTR_LOCATION: station['info']['location'], - ATTR_NAME: station['info']['name'], + ATTR_LOCATION: station.get('info', {}).get('location'), + ATTR_NAME: + station.get('info', {}).get( + 'name', station['macAddress']), } + for component in ('binary_sensor', 'sensor'): self._hass.async_create_task( self._hass.config_entries.async_forward_entry_setup( - self._config_entry, 'sensor')) + self._config_entry, component)) self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY @@ -194,8 +362,7 @@ class AmbientStation: except WebsocketError as err: _LOGGER.error("Error with the websocket connection: %s", err) - self._ws_reconnect_delay = min( - 2 * self._ws_reconnect_delay, 480) + self._ws_reconnect_delay = min(2 * self._ws_reconnect_delay, 480) async_call_later( self._hass, self._ws_reconnect_delay, self.ws_connect) @@ -203,3 +370,60 @@ class AmbientStation: async def ws_disconnect(self): """Disconnect from the websocket.""" await self.client.websocket.disconnect() + + +class AmbientWeatherEntity(Entity): + """Define a base Ambient PWS entity.""" + + def __init__( + self, ambient, mac_address, station_name, sensor_type, + sensor_name): + """Initialize the sensor.""" + self._ambient = ambient + self._async_unsub_dispatcher_connect = None + self._mac_address = mac_address + self._sensor_name = sensor_name + self._sensor_type = sensor_type + self._state = None + self._station_name = station_name + + @property + def device_info(self): + """Return device registry information for this entity.""" + return { + 'identifiers': { + (DOMAIN, self._mac_address) + }, + 'name': self._station_name, + 'manufacturer': 'Ambient Weather', + } + + @property + def name(self): + """Return the name of the sensor.""" + return '{0}_{1}'.format(self._station_name, self._sensor_name) + + @property + def should_poll(self): + """Disable polling.""" + return False + + @property + def unique_id(self): + """Return a unique, unchanging string that represents this sensor.""" + return '{0}_{1}'.format(self._mac_address, self._sensor_name) + + 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): + """Disconnect dispatcher listener when removed.""" + if self._async_unsub_dispatcher_connect: + self._async_unsub_dispatcher_connect() diff --git a/homeassistant/components/ambient_station/binary_sensor.py b/homeassistant/components/ambient_station/binary_sensor.py new file mode 100644 index 00000000000..2defa032809 --- /dev/null +++ b/homeassistant/components/ambient_station/binary_sensor.py @@ -0,0 +1,72 @@ +"""Support for Ambient Weather Station binary sensors.""" +import logging + +from homeassistant.components.ambient_station import ( + SENSOR_TYPES, TYPE_BATT1, TYPE_BATT10, TYPE_BATT2, TYPE_BATT3, TYPE_BATT4, + TYPE_BATT5, TYPE_BATT6, TYPE_BATT7, TYPE_BATT8, TYPE_BATT9, TYPE_BATTOUT, + AmbientWeatherEntity) +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.const import ATTR_NAME + +from .const import ATTR_LAST_DATA, DATA_CLIENT, DOMAIN, TYPE_BINARY_SENSOR + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['ambient_station'] + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Set up Ambient PWS binary sensors based on the old way.""" + pass + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Ambient PWS binary sensors based on a config entry.""" + ambient = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] + + binary_sensor_list = [] + for mac_address, station in ambient.stations.items(): + for condition in ambient.monitored_conditions: + name, _, kind, device_class = SENSOR_TYPES[condition] + if kind == TYPE_BINARY_SENSOR: + binary_sensor_list.append( + AmbientWeatherBinarySensor( + ambient, mac_address, station[ATTR_NAME], condition, + name, device_class)) + + async_add_entities(binary_sensor_list, True) + + +class AmbientWeatherBinarySensor(AmbientWeatherEntity, BinarySensorDevice): + """Define an Ambient binary sensor.""" + + def __init__( + self, ambient, mac_address, station_name, sensor_type, sensor_name, + device_class): + """Initialize the sensor.""" + super().__init__( + ambient, mac_address, station_name, sensor_type, sensor_name) + + self._device_class = device_class + + @property + def device_class(self): + """Return the device class.""" + return self._device_class + + @property + def is_on(self): + """Return the status of the sensor.""" + if self._sensor_type in (TYPE_BATT1, TYPE_BATT10, TYPE_BATT2, + TYPE_BATT3, TYPE_BATT4, TYPE_BATT5, + TYPE_BATT6, TYPE_BATT7, TYPE_BATT8, + TYPE_BATT9, TYPE_BATTOUT): + return self._state == 0 + + return self._state == 1 + + async def async_update(self): + """Fetch new state data for the entity.""" + self._state = self._ambient.stations[ + self._mac_address][ATTR_LAST_DATA].get(self._sensor_type) diff --git a/homeassistant/components/ambient_station/config_flow.py b/homeassistant/components/ambient_station/config_flow.py index 56e747ce5e0..f01bfd8f791 100644 --- a/homeassistant/components/ambient_station/config_flow.py +++ b/homeassistant/components/ambient_station/config_flow.py @@ -1,5 +1,4 @@ """Config flow to configure the Ambient PWS component.""" - import voluptuous as vol from homeassistant import config_entries diff --git a/homeassistant/components/ambient_station/const.py b/homeassistant/components/ambient_station/const.py index 75606a1c699..27ec7afefaa 100644 --- a/homeassistant/components/ambient_station/const.py +++ b/homeassistant/components/ambient_station/const.py @@ -8,3 +8,6 @@ CONF_APP_KEY = 'app_key' DATA_CLIENT = 'data_client' TOPIC_UPDATE = 'update' + +TYPE_BINARY_SENSOR = 'binary_sensor' +TYPE_SENSOR = 'sensor' diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index 9e0833e3441..fa3222bf0e4 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -1,71 +1,52 @@ -""" -Support for Ambient Weather Station Service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.ambient_station/ -""" +"""Support for Ambient Weather Station sensors.""" import logging -from homeassistant.components.ambient_station import SENSOR_TYPES -from homeassistant.helpers.entity import Entity +from homeassistant.components.ambient_station import ( + SENSOR_TYPES, AmbientWeatherEntity) from homeassistant.const import ATTR_NAME -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import ATTR_LAST_DATA, DATA_CLIENT, DOMAIN, TOPIC_UPDATE +from .const import ATTR_LAST_DATA, DATA_CLIENT, DOMAIN, TYPE_SENSOR + +_LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['ambient_station'] -_LOGGER = logging.getLogger(__name__) async def async_setup_platform( hass, config, async_add_entities, discovery_info=None): - """Set up an Ambient PWS sensor based on existing config.""" + """Set up Ambient PWS sensors based on existing config.""" pass async def async_setup_entry(hass, entry, async_add_entities): - """Set up an Ambient PWS sensor based on a config entry.""" + """Set up Ambient PWS sensors based on a config entry.""" ambient = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] sensor_list = [] for mac_address, station in ambient.stations.items(): for condition in ambient.monitored_conditions: - name, unit = SENSOR_TYPES[condition] - sensor_list.append( - AmbientWeatherSensor( - ambient, mac_address, station[ATTR_NAME], condition, name, - unit)) + name, unit, kind, _ = SENSOR_TYPES[condition] + if kind == TYPE_SENSOR: + sensor_list.append( + AmbientWeatherSensor( + ambient, mac_address, station[ATTR_NAME], condition, + name, unit)) async_add_entities(sensor_list, True) -class AmbientWeatherSensor(Entity): +class AmbientWeatherSensor(AmbientWeatherEntity): """Define an Ambient sensor.""" def __init__( self, ambient, mac_address, station_name, sensor_type, sensor_name, unit): """Initialize the sensor.""" - self._ambient = ambient - self._async_unsub_dispatcher_connect = None - self._mac_address = mac_address - self._sensor_name = sensor_name - self._sensor_type = sensor_type - self._state = None - self._station_name = station_name + super().__init__( + ambient, mac_address, station_name, sensor_type, sensor_name) + self._unit = unit - @property - def name(self): - """Return the name of the sensor.""" - return '{0}_{1}'.format(self._station_name, self._sensor_name) - - @property - def should_poll(self): - """Disable polling.""" - return False - @property def state(self): """Return the state of the sensor.""" @@ -76,26 +57,6 @@ class AmbientWeatherSensor(Entity): """Return the unit of measurement.""" return self._unit - @property - def unique_id(self): - """Return a unique, unchanging string that represents this sensor.""" - return '{0}_{1}'.format(self._mac_address, self._sensor_name) - - 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): - """Disconnect dispatcher listener when removed.""" - if self._async_unsub_dispatcher_connect: - self._async_unsub_dispatcher_connect() - async def async_update(self): """Fetch new state data for the sensor.""" self._state = self._ambient.stations[ diff --git a/homeassistant/components/amcrest.py b/homeassistant/components/amcrest/__init__.py similarity index 96% rename from homeassistant/components/amcrest.py rename to homeassistant/components/amcrest/__init__.py index bcd0c38c3bd..49f11570b21 100644 --- a/homeassistant/components/amcrest.py +++ b/homeassistant/components/amcrest/__init__.py @@ -1,9 +1,4 @@ -""" -This component provides basic support for Amcrest IP cameras. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/amcrest/ -""" +"""Support for Amcrest IP cameras.""" import logging from datetime import timedelta diff --git a/homeassistant/components/camera/amcrest.py b/homeassistant/components/amcrest/camera.py similarity index 93% rename from homeassistant/components/camera/amcrest.py rename to homeassistant/components/amcrest/camera.py index 3b3368c2f5c..7c943b89734 100644 --- a/homeassistant/components/camera/amcrest.py +++ b/homeassistant/components/amcrest/camera.py @@ -1,9 +1,4 @@ -""" -This component provides basic support for Amcrest IP cameras. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/camera.amcrest/ -""" +"""Support for Amcrest IP cameras.""" import logging from homeassistant.components.amcrest import ( diff --git a/homeassistant/components/sensor/amcrest.py b/homeassistant/components/amcrest/sensor.py similarity index 87% rename from homeassistant/components/sensor/amcrest.py rename to homeassistant/components/amcrest/sensor.py index 22e13d05e20..4869dfffa6e 100644 --- a/homeassistant/components/sensor/amcrest.py +++ b/homeassistant/components/amcrest/sensor.py @@ -1,9 +1,4 @@ -""" -This component provides HA sensor support for Amcrest IP cameras. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.amcrest/ -""" +"""Suppoort for Amcrest IP camera sensors.""" from datetime import timedelta import logging @@ -18,8 +13,8 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=10) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up a sensor for an Amcrest IP Camera.""" if discovery_info is None: return @@ -45,8 +40,8 @@ class AmcrestSensor(Entity): self._attrs = {} self._camera = camera self._sensor_type = sensor_type - self._name = '{0}_{1}'.format(name, - SENSORS.get(self._sensor_type)[0]) + self._name = '{0}_{1}'.format( + name, SENSORS.get(self._sensor_type)[0]) self._icon = 'mdi:{}'.format(SENSORS.get(self._sensor_type)[2]) self._state = None diff --git a/homeassistant/components/switch/amcrest.py b/homeassistant/components/amcrest/switch.py similarity index 89% rename from homeassistant/components/switch/amcrest.py rename to homeassistant/components/amcrest/switch.py index 4eb20308850..3c1f03f0145 100644 --- a/homeassistant/components/switch/amcrest.py +++ b/homeassistant/components/amcrest/switch.py @@ -1,9 +1,4 @@ -""" -Support for toggling Amcrest IP camera settings. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.amcrest/ -""" +"""Support for toggling Amcrest IP camera settings.""" import logging from homeassistant.components.amcrest import DATA_AMCREST, SWITCHES @@ -16,8 +11,8 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['amcrest'] -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the IP Amcrest camera switch platform.""" if discovery_info is None: return diff --git a/homeassistant/components/android_ip_webcam.py b/homeassistant/components/android_ip_webcam/__init__.py similarity index 97% rename from homeassistant/components/android_ip_webcam.py rename to homeassistant/components/android_ip_webcam/__init__.py index 1cf46174371..c5424b3d0fa 100644 --- a/homeassistant/components/android_ip_webcam.py +++ b/homeassistant/components/android_ip_webcam/__init__.py @@ -1,9 +1,4 @@ -""" -Support for IP Webcam, an Android app that acts as a full-featured webcam. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/android_ip_webcam/ -""" +"""Support for Android IP Webcam.""" import asyncio import logging from datetime import timedelta diff --git a/homeassistant/components/binary_sensor/android_ip_webcam.py b/homeassistant/components/android_ip_webcam/binary_sensor.py similarity index 83% rename from homeassistant/components/binary_sensor/android_ip_webcam.py rename to homeassistant/components/android_ip_webcam/binary_sensor.py index 085bafd3ae3..e33e22f3778 100644 --- a/homeassistant/components/binary_sensor/android_ip_webcam.py +++ b/homeassistant/components/android_ip_webcam/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for IP Webcam binary sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.android_ip_webcam/ -""" +"""Support for Android IP Webcam binary sensors.""" from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.android_ip_webcam import ( KEY_MAP, DATA_IP_WEBCAM, AndroidIPCamEntity, CONF_HOST, CONF_NAME) @@ -11,8 +6,8 @@ from homeassistant.components.android_ip_webcam import ( DEPENDENCIES = ['android_ip_webcam'] -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the IP Webcam binary sensors.""" if discovery_info is None: return diff --git a/homeassistant/components/sensor/android_ip_webcam.py b/homeassistant/components/android_ip_webcam/sensor.py similarity index 87% rename from homeassistant/components/sensor/android_ip_webcam.py rename to homeassistant/components/android_ip_webcam/sensor.py index 0f795f85dcd..e98ce7951b8 100644 --- a/homeassistant/components/sensor/android_ip_webcam.py +++ b/homeassistant/components/android_ip_webcam/sensor.py @@ -1,10 +1,4 @@ -""" -Support for IP Webcam sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.android_ip_webcam/ -""" - +"""Support for Android IP Webcam sensors.""" from homeassistant.components.android_ip_webcam import ( KEY_MAP, ICON_MAP, DATA_IP_WEBCAM, AndroidIPCamEntity, CONF_HOST, CONF_NAME, CONF_SENSORS) @@ -13,8 +7,8 @@ from homeassistant.helpers.icon import icon_for_battery_level DEPENDENCIES = ['android_ip_webcam'] -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the IP Webcam Sensor.""" if discovery_info is None: return diff --git a/homeassistant/components/switch/android_ip_webcam.py b/homeassistant/components/android_ip_webcam/switch.py similarity index 90% rename from homeassistant/components/switch/android_ip_webcam.py rename to homeassistant/components/android_ip_webcam/switch.py index f770b9d5ebf..73a94acbcdd 100644 --- a/homeassistant/components/switch/android_ip_webcam.py +++ b/homeassistant/components/android_ip_webcam/switch.py @@ -1,10 +1,4 @@ -""" -Support for IP Webcam settings. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.android_ip_webcam/ -""" - +"""Support for Android IP Webcam settings.""" from homeassistant.components.switch import SwitchDevice from homeassistant.components.android_ip_webcam import ( KEY_MAP, ICON_MAP, DATA_IP_WEBCAM, AndroidIPCamEntity, CONF_HOST, @@ -13,8 +7,8 @@ from homeassistant.components.android_ip_webcam import ( DEPENDENCIES = ['android_ip_webcam'] -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the IP Webcam switch platform.""" if discovery_info is None: return diff --git a/homeassistant/components/apcupsd.py b/homeassistant/components/apcupsd/__init__.py similarity index 91% rename from homeassistant/components/apcupsd.py rename to homeassistant/components/apcupsd/__init__.py index 79b88378169..aab6f6dda01 100644 --- a/homeassistant/components/apcupsd.py +++ b/homeassistant/components/apcupsd/__init__.py @@ -1,9 +1,4 @@ -""" -Support for status output of APCUPSd via its Network Information Server (NIS). - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/apcupsd/ -""" +"""Support for APCUPSd via its Network Information Server (NIS).""" import logging from datetime import timedelta diff --git a/homeassistant/components/binary_sensor/apcupsd.py b/homeassistant/components/apcupsd/binary_sensor.py similarity index 87% rename from homeassistant/components/binary_sensor/apcupsd.py rename to homeassistant/components/apcupsd/binary_sensor.py index f876b8cc34b..445dab9b074 100644 --- a/homeassistant/components/binary_sensor/apcupsd.py +++ b/homeassistant/components/apcupsd/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for tracking the online status of a UPS. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.apcupsd/ -""" +"""Support for tracking the online status of a UPS.""" import voluptuous as vol from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/sensor/apcupsd.py b/homeassistant/components/apcupsd/sensor.py similarity index 97% rename from homeassistant/components/sensor/apcupsd.py rename to homeassistant/components/apcupsd/sensor.py index 90c1f2e6795..4ebe0ac8aaf 100644 --- a/homeassistant/components/sensor/apcupsd.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -1,9 +1,4 @@ -""" -Provides a sensor to track various status aspects of a UPS. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.apcupsd/ -""" +"""Support for APCUPSd sensors.""" import logging import voluptuous as vol diff --git a/homeassistant/components/api.py b/homeassistant/components/api/__init__.py similarity index 98% rename from homeassistant/components/api.py rename to homeassistant/components/api/__init__.py index 961350bfa89..7639ac621fe 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api/__init__.py @@ -1,9 +1,4 @@ -""" -Rest API for Home Assistant. - -For more details about the RESTful API, please refer to the documentation at -https://developers.home-assistant.io/docs/en/external_api_rest.html -""" +"""Rest API for Home Assistant.""" import asyncio import json import logging diff --git a/homeassistant/components/apple_tv.py b/homeassistant/components/apple_tv/__init__.py similarity index 98% rename from homeassistant/components/apple_tv.py rename to homeassistant/components/apple_tv/__init__.py index 73cabdfbae6..b265dc533eb 100644 --- a/homeassistant/components/apple_tv.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Apple TV. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/apple_tv/ -""" +"""Support for Apple TV.""" import asyncio import logging from typing import Sequence, TypeVar, Union diff --git a/homeassistant/components/media_player/apple_tv.py b/homeassistant/components/apple_tv/media_player.py similarity index 97% rename from homeassistant/components/media_player/apple_tv.py rename to homeassistant/components/apple_tv/media_player.py index bff8834639d..03ac5bd2549 100644 --- a/homeassistant/components/media_player/apple_tv.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -1,18 +1,13 @@ -""" -Support for Apple TV. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/media_player.apple_tv/ -""" +"""Support for Apple TV media player.""" import logging from homeassistant.components.apple_tv import ( ATTR_ATV, ATTR_POWER, DATA_APPLE_TV, DATA_ENTITIES) -from homeassistant.components.media_player import ( +from homeassistant.components.media_player import MediaPlayerDevice +from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, - SUPPORT_SEEK, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - MediaPlayerDevice) + SUPPORT_SEEK, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON) from homeassistant.const import ( CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, STATE_STANDBY) diff --git a/homeassistant/components/remote/apple_tv.py b/homeassistant/components/apple_tv/remote.py similarity index 88% rename from homeassistant/components/remote/apple_tv.py rename to homeassistant/components/apple_tv/remote.py index 72696143bfe..2d80ded6861 100644 --- a/homeassistant/components/remote/apple_tv.py +++ b/homeassistant/components/apple_tv/remote.py @@ -1,21 +1,14 @@ -""" -Remote control support for Apple TV. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/remote.apple_tv/ -""" - +"""Remote control support for Apple TV.""" from homeassistant.components.apple_tv import ( ATTR_ATV, ATTR_POWER, DATA_APPLE_TV) from homeassistant.components import remote from homeassistant.const import (CONF_NAME, CONF_HOST) - DEPENDENCIES = ['apple_tv'] -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the Apple TV remote platform.""" if not discovery_info: return diff --git a/homeassistant/components/apple_tv/services.yaml b/homeassistant/components/apple_tv/services.yaml new file mode 100644 index 00000000000..01e26a5630b --- /dev/null +++ b/homeassistant/components/apple_tv/services.yaml @@ -0,0 +1,5 @@ +apple_tv_authenticate: + description: Start AirPlay device authentication. + fields: + entity_id: {description: Name(s) of entities to authenticate with., example: media_player.apple_tv} +apple_tv_scan: {description: Scan for Apple TV devices.} diff --git a/homeassistant/components/aqualogic.py b/homeassistant/components/aqualogic/__init__.py similarity index 79% rename from homeassistant/components/aqualogic.py rename to homeassistant/components/aqualogic/__init__.py index abb61d42ca3..a4f83b573f7 100644 --- a/homeassistant/components/aqualogic.py +++ b/homeassistant/components/aqualogic/__init__.py @@ -1,9 +1,4 @@ -""" -Support for AquaLogic component. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/aqualogic/ -""" +"""Support for AquaLogic devices.""" from datetime import timedelta import logging import time @@ -20,15 +15,15 @@ REQUIREMENTS = ["aqualogic==1.0"] _LOGGER = logging.getLogger(__name__) -DOMAIN = "aqualogic" -UPDATE_TOPIC = DOMAIN + "_update" -CONF_UNIT = "unit" +DOMAIN = 'aqualogic' +UPDATE_TOPIC = DOMAIN + '_update' +CONF_UNIT = 'unit' RECONNECT_INTERVAL = timedelta(seconds=10) CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PORT): cv.port + vol.Required(CONF_PORT): cv.port, }), }, extra=vol.ALLOW_EXTRA) @@ -39,10 +34,8 @@ def setup(hass, config): port = config[DOMAIN][CONF_PORT] processor = AquaLogicProcessor(hass, host, port) hass.data[DOMAIN] = processor - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, - processor.start_listen) - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, - processor.shutdown) + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, processor.start_listen) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, processor.shutdown) _LOGGER.debug("AquaLogicProcessor %s:%i initialized", host, port) return True @@ -85,8 +78,7 @@ class AquaLogicProcessor(threading.Thread): if self._shutdown: return - _LOGGER.error("Connection to %s:%d lost", - self._host, self._port) + _LOGGER.error("Connection to %s:%d lost", self._host, self._port) time.sleep(RECONNECT_INTERVAL.seconds) @property diff --git a/homeassistant/components/sensor/aqualogic.py b/homeassistant/components/aqualogic/sensor.py similarity index 92% rename from homeassistant/components/sensor/aqualogic.py rename to homeassistant/components/aqualogic/sensor.py index f10fd05b83f..9e061ba91bf 100644 --- a/homeassistant/components/sensor/aqualogic.py +++ b/homeassistant/components/aqualogic/sensor.py @@ -1,9 +1,4 @@ -""" -Support for AquaLogic sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.aqualogic/ -""" +"""Support for AquaLogic sensors.""" import logging import voluptuous as vol @@ -46,8 +41,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the sensor platform.""" sensors = [] diff --git a/homeassistant/components/switch/aqualogic.py b/homeassistant/components/aqualogic/switch.py similarity index 91% rename from homeassistant/components/switch/aqualogic.py rename to homeassistant/components/aqualogic/switch.py index 48c4702aca0..ee040fa1ba5 100644 --- a/homeassistant/components/switch/aqualogic.py +++ b/homeassistant/components/aqualogic/switch.py @@ -1,9 +1,4 @@ -""" -Support for AquaLogic switches. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.aqualogic/ -""" +"""Support for AquaLogic switches.""" import logging import voluptuous as vol @@ -37,8 +32,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the switch platform.""" switches = [] diff --git a/homeassistant/components/arduino.py b/homeassistant/components/arduino/__init__.py similarity index 94% rename from homeassistant/components/arduino.py rename to homeassistant/components/arduino/__init__.py index 785f8c57f94..351122e74f0 100644 --- a/homeassistant/components/arduino.py +++ b/homeassistant/components/arduino/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Arduino boards running with the Firmata firmware. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/arduino/ -""" +"""Support for Arduino boards running with the Firmata firmware.""" import logging import voluptuous as vol diff --git a/homeassistant/components/sensor/arduino.py b/homeassistant/components/arduino/sensor.py similarity index 88% rename from homeassistant/components/sensor/arduino.py rename to homeassistant/components/arduino/sensor.py index f46eebce1b2..ff758ea5847 100644 --- a/homeassistant/components/sensor/arduino.py +++ b/homeassistant/components/arduino/sensor.py @@ -1,11 +1,4 @@ -""" -Support for getting information from Arduino pins. - -Only analog pins are supported. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.arduino/ -""" +"""Support for getting information from Arduino pins.""" import logging import voluptuous as vol diff --git a/homeassistant/components/switch/arduino.py b/homeassistant/components/arduino/switch.py similarity index 91% rename from homeassistant/components/switch/arduino.py rename to homeassistant/components/arduino/switch.py index ee8f0e878a3..947c5188766 100644 --- a/homeassistant/components/switch/arduino.py +++ b/homeassistant/components/arduino/switch.py @@ -1,11 +1,4 @@ -""" -Support for switching Arduino pins on and off. - -So far only digital pins are supported. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.arduino/ -""" +"""Support for switching Arduino pins on and off.""" import logging import voluptuous as vol diff --git a/homeassistant/components/arlo.py b/homeassistant/components/arlo/__init__.py similarity index 93% rename from homeassistant/components/arlo.py rename to homeassistant/components/arlo/__init__.py index aebd57098b5..7e81836e522 100644 --- a/homeassistant/components/arlo.py +++ b/homeassistant/components/arlo/__init__.py @@ -1,9 +1,4 @@ -""" -This component provides support for Netgear Arlo IP cameras. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/arlo/ -""" +"""Support for Netgear Arlo IP cameras.""" import logging from datetime import timedelta diff --git a/homeassistant/components/alarm_control_panel/arlo.py b/homeassistant/components/arlo/alarm_control_panel.py similarity index 95% rename from homeassistant/components/alarm_control_panel/arlo.py rename to homeassistant/components/arlo/alarm_control_panel.py index 66f11fab83f..8c21a448a23 100644 --- a/homeassistant/components/alarm_control_panel/arlo.py +++ b/homeassistant/components/arlo/alarm_control_panel.py @@ -1,9 +1,4 @@ -""" -Support for Arlo Alarm Control Panels. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/alarm_control_panel.arlo/ -""" +"""Support for Arlo Alarm Control Panels.""" import logging import voluptuous as vol diff --git a/homeassistant/components/camera/arlo.py b/homeassistant/components/arlo/camera.py similarity index 96% rename from homeassistant/components/camera/arlo.py rename to homeassistant/components/arlo/camera.py index 7857995b4af..6f20ecdadcd 100644 --- a/homeassistant/components/camera/arlo.py +++ b/homeassistant/components/arlo/camera.py @@ -1,9 +1,4 @@ -""" -Support for Netgear Arlo IP cameras. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/camera.arlo/ -""" +"""Support for Netgear Arlo IP cameras.""" import logging import voluptuous as vol diff --git a/homeassistant/components/sensor/arlo.py b/homeassistant/components/arlo/sensor.py similarity index 97% rename from homeassistant/components/sensor/arlo.py rename to homeassistant/components/arlo/sensor.py index be940cc4f51..3ad7b70a947 100644 --- a/homeassistant/components/sensor/arlo.py +++ b/homeassistant/components/arlo/sensor.py @@ -1,9 +1,4 @@ -""" -This component provides HA sensor for Netgear Arlo IP cameras. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.arlo/ -""" +"""Sensor support for Netgear Arlo IP cameras.""" import logging import voluptuous as vol diff --git a/homeassistant/components/asterisk_mbox.py b/homeassistant/components/asterisk_mbox/__init__.py similarity index 88% rename from homeassistant/components/asterisk_mbox.py rename to homeassistant/components/asterisk_mbox/__init__.py index 406774d5fad..d8d3b194cd7 100644 --- a/homeassistant/components/asterisk_mbox.py +++ b/homeassistant/components/asterisk_mbox/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Asterisk Voicemail interface. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/asterisk_mbox/ -""" +"""Support for Asterisk Voicemail interface.""" import logging import voluptuous as vol @@ -78,9 +73,8 @@ class AsteriskData: @callback def handle_data(self, command, msg): """Handle changes to the mailbox.""" - from asterisk_mbox.commands import (CMD_MESSAGE_LIST, - CMD_MESSAGE_CDR_AVAILABLE, - CMD_MESSAGE_CDR) + from asterisk_mbox.commands import ( + CMD_MESSAGE_LIST, CMD_MESSAGE_CDR_AVAILABLE, CMD_MESSAGE_CDR) if command == CMD_MESSAGE_LIST: _LOGGER.debug("AsteriskVM sent updated message list: Len %d", @@ -89,8 +83,8 @@ class AsteriskData: self.messages = sorted( msg, key=lambda item: item['info']['origtime'], reverse=True) if not isinstance(old_messages, list): - async_dispatcher_send(self.hass, SIGNAL_DISCOVER_PLATFORM, - DOMAIN) + async_dispatcher_send( + self.hass, SIGNAL_DISCOVER_PLATFORM, DOMAIN) async_dispatcher_send(self.hass, SIGNAL_MESSAGE_UPDATE, self.messages) elif command == CMD_MESSAGE_CDR: diff --git a/homeassistant/components/mailbox/asterisk_mbox.py b/homeassistant/components/asterisk_mbox/mailbox.py similarity index 92% rename from homeassistant/components/mailbox/asterisk_mbox.py rename to homeassistant/components/asterisk_mbox/mailbox.py index 087018084f2..9da4bd1a21a 100644 --- a/homeassistant/components/mailbox/asterisk_mbox.py +++ b/homeassistant/components/asterisk_mbox/mailbox.py @@ -1,9 +1,4 @@ -""" -Asterisk Voicemail interface. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/mailbox.asteriskvm/ -""" +"""Support for the Asterisk Voicemail interface.""" import logging from homeassistant.components.asterisk_mbox import DOMAIN as ASTERISK_DOMAIN diff --git a/homeassistant/components/asuswrt.py b/homeassistant/components/asuswrt/__init__.py similarity index 93% rename from homeassistant/components/asuswrt.py rename to homeassistant/components/asuswrt/__init__.py index 0069b3c0d73..3fc0e9d6476 100644 --- a/homeassistant/components/asuswrt.py +++ b/homeassistant/components/asuswrt/__init__.py @@ -1,9 +1,4 @@ -""" -Support for ASUSWRT devices. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/asuswrt/ -""" +"""Support for ASUSWRT devices.""" import logging import voluptuous as vol @@ -18,15 +13,16 @@ REQUIREMENTS = ['aioasuswrt==1.1.20'] _LOGGER = logging.getLogger(__name__) +CONF_PUB_KEY = 'pub_key' +CONF_REQUIRE_IP = 'require_ip' +CONF_SENSORS = 'sensors' +CONF_SSH_KEY = 'ssh_key' + DOMAIN = "asuswrt" DATA_ASUSWRT = DOMAIN - -CONF_PUB_KEY = 'pub_key' -CONF_SSH_KEY = 'ssh_key' -CONF_REQUIRE_IP = 'require_ip' DEFAULT_SSH_PORT = 22 + SECRET_GROUP = 'Password or SSH Key' -CONF_SENSORS = 'sensors' SENSOR_TYPES = ['upload_speed', 'download_speed', 'download', 'upload'] CONFIG_SCHEMA = vol.Schema({ diff --git a/homeassistant/components/august.py b/homeassistant/components/august/__init__.py similarity index 98% rename from homeassistant/components/august.py rename to homeassistant/components/august/__init__.py index 2073f680e00..8e749dca46e 100644 --- a/homeassistant/components/august.py +++ b/homeassistant/components/august/__init__.py @@ -1,9 +1,4 @@ -""" -Support for August devices. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/august/ -""" +"""Support for August devices.""" import logging from datetime import timedelta diff --git a/homeassistant/components/binary_sensor/august.py b/homeassistant/components/august/binary_sensor.py similarity index 89% rename from homeassistant/components/binary_sensor/august.py rename to homeassistant/components/august/binary_sensor.py index 4116a791b01..1ad2d80cea8 100644 --- a/homeassistant/components/binary_sensor/august.py +++ b/homeassistant/components/august/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for August binary sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.august/ -""" +"""Support for August binary sensors.""" import logging from datetime import timedelta, datetime @@ -144,6 +139,13 @@ class AugustDoorBinarySensor(BinarySensorDevice): from august.lock import LockDoorStatus self._state = self._state == LockDoorStatus.OPEN + @property + def unique_id(self) -> str: + """Get the unique of the door open binary sensor.""" + return '{:s}_{:s}'.format(self._door.device_id, + SENSOR_TYPES_DOOR[self._sensor_type][0] + .lower()) + class AugustDoorbellBinarySensor(BinarySensorDevice): """Representation of an August binary sensor.""" @@ -181,4 +183,11 @@ class AugustDoorbellBinarySensor(BinarySensorDevice): """Get the latest state of the sensor.""" state_provider = SENSOR_TYPES_DOORBELL[self._sensor_type][2] self._state = state_provider(self._data, self._doorbell) - self._available = self._state is not None + self._available = self._doorbell.is_online + + @property + def unique_id(self) -> str: + """Get the unique id of the doorbell sensor.""" + return '{:s}_{:s}'.format(self._doorbell.device_id, + SENSOR_TYPES_DOORBELL[self._sensor_type][0] + .lower()) diff --git a/homeassistant/components/camera/august.py b/homeassistant/components/august/camera.py similarity index 91% rename from homeassistant/components/camera/august.py rename to homeassistant/components/august/camera.py index dcce5e13588..7420bb04067 100644 --- a/homeassistant/components/camera/august.py +++ b/homeassistant/components/august/camera.py @@ -1,9 +1,4 @@ -""" -Support for August camera. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/camera.august/ -""" +"""Support for August camera.""" from datetime import timedelta import requests @@ -74,3 +69,8 @@ class AugustCamera(Camera): timeout=self._timeout).content return self._image_content + + @property + def unique_id(self) -> str: + """Get the unique id of the camera.""" + return '{:s}_camera'.format(self._doorbell.device_id) diff --git a/homeassistant/components/lock/august.py b/homeassistant/components/august/lock.py similarity index 93% rename from homeassistant/components/lock/august.py rename to homeassistant/components/august/lock.py index ce6792ceb39..ff355bbf87b 100644 --- a/homeassistant/components/lock/august.py +++ b/homeassistant/components/august/lock.py @@ -1,9 +1,4 @@ -""" -Support for August lock. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/lock.august/ -""" +"""Support for August lock.""" import logging from datetime import timedelta @@ -95,3 +90,8 @@ class AugustLock(LockDevice): return { ATTR_BATTERY_LEVEL: self._lock_detail.battery_level, } + + @property + def unique_id(self) -> str: + """Get the unique id of the lock.""" + return '{:s}_lock'.format(self._lock.device_id) diff --git a/homeassistant/components/auth/.translations/da.json b/homeassistant/components/auth/.translations/da.json new file mode 100644 index 00000000000..f461f376d16 --- /dev/null +++ b/homeassistant/components/auth/.translations/da.json @@ -0,0 +1,35 @@ +{ + "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Ingen underretningstjenester til r\u00e5dighed." + }, + "error": { + "invalid_code": "Ugyldig kode, pr\u00f8v venligst igen." + }, + "step": { + "init": { + "description": "V\u00e6lg venligst en af meddelelsestjenesterne:", + "title": "Ops\u00e6t engangsadgangskode, der er leveret af besked komponenten" + }, + "setup": { + "description": "En engangsadgangskode er blevet sendt via **notify.{notify_service}**. Indtast den venligst nedenunder:", + "title": "Bekr\u00e6ft ops\u00e6tningen" + } + }, + "title": "Advis\u00e9r engangskodeord" + }, + "totp": { + "error": { + "invalid_code": "Ugyldig kode, pr\u00f8v venligst igen. Hvis du konsekvent f\u00e5r denne fejl skal du s\u00f8rge for at uret p\u00e5 dit Home Assistant system er g\u00e5r n\u00f8jagtigt." + }, + "step": { + "init": { + "description": "Hvis du vil aktivere tofaktorautentificering ved hj\u00e6lp af tidsbaserede engangskoder skal du scanne QR-koden med din autentificeringsapp. Hvis du ikke har en anbefaler vi enten [Google Authenticator] (https://support.google.com/accounts/answer/1066447) eller [Authy] (https://authy.com/). \n\n {qr_code} \n \nN\u00e5r du har scannet koden skal du indtaste den sekscifrede kode fra din app for at bekr\u00e6fte ops\u00e6tningen. Hvis du har problemer med at scanne QR-koden skal du lave en manuel ops\u00e6tning med kode **`{code}`**.", + "title": "Konfigurer to-faktors godkendelse ved hj\u00e6lp af TOTP" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/.translations/ko.json b/homeassistant/components/auth/.translations/ko.json index 7efc50d534c..c5278c63f79 100644 --- a/homeassistant/components/auth/.translations/ko.json +++ b/homeassistant/components/auth/.translations/ko.json @@ -13,7 +13,7 @@ "title": "\uc54c\ub9bc \uad6c\uc131\uc694\uc18c\uac00 \uc81c\uacf5\ud558\ub294 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638 \uc124\uc815" }, "setup": { - "description": "**notify.{notify_service}** \uc5d0\uc11c \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \ubcf4\ub0c8\uc2b5\ub2c8\ub2e4. \uc544\ub798\uc758 \uacf5\ub780\uc5d0 \uc785\ub825\ud574 \uc8fc\uc138\uc694:", + "description": "**notify.{notify_service}** \uc5d0\uc11c \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \ubcf4\ub0c8\uc2b5\ub2c8\ub2e4. \uc544\ub798\uc758 \uacf5\ub780\uc5d0 \uc785\ub825\ud574\uc8fc\uc138\uc694:", "title": "\uc124\uc815 \ud655\uc778" } }, @@ -25,7 +25,7 @@ }, "step": { "init": { - "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574 \uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.", + "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574\uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.", "title": "TOTP \ub97c \uc0ac\uc6a9\ud558\uc5ec 2\ub2e8\uacc4 \uc778\uc99d \uad6c\uc131" } }, diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 836901cde30..35cf695f1e3 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -1,9 +1,4 @@ -""" -Allow to set up simple automation rules via the config file. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/automation/ -""" +"""Allow to set up simple automation rules via the config file.""" import asyncio from functools import partial import importlib diff --git a/homeassistant/components/automation/event.py b/homeassistant/components/automation/event.py index ec47479eac8..6cc7e3dae7d 100644 --- a/homeassistant/components/automation/event.py +++ b/homeassistant/components/automation/event.py @@ -1,9 +1,4 @@ -""" -Offer event listening automation rules. - -For more details about this automation rule, please refer to the documentation -at https://home-assistant.io/docs/automation/trigger/#event-trigger -""" +"""Offer event listening automation rules.""" import logging import voluptuous as vol diff --git a/homeassistant/components/automation/geo_location.py b/homeassistant/components/automation/geo_location.py index 33ef00da380..8f838ea6d6b 100644 --- a/homeassistant/components/automation/geo_location.py +++ b/homeassistant/components/automation/geo_location.py @@ -1,10 +1,4 @@ -""" -Offer geolocation automation rules. - -For more details about this automation trigger, please refer to the -documentation at -https://home-assistant.io/docs/automation/trigger/#geolocation-trigger -""" +"""Offer geolocation automation rules.""" import voluptuous as vol from homeassistant.components.geo_location import DOMAIN diff --git a/homeassistant/components/automation/homeassistant.py b/homeassistant/components/automation/homeassistant.py index 6d7a44291c9..1b022316676 100644 --- a/homeassistant/components/automation/homeassistant.py +++ b/homeassistant/components/automation/homeassistant.py @@ -1,9 +1,4 @@ -""" -Offer Home Assistant core automation rules. - -For more details about this automation rule, please refer to the documentation -at https://home-assistant.io/components/automation/#homeassistant-trigger -""" +"""Offer Home Assistant core automation rules.""" import logging import voluptuous as vol diff --git a/homeassistant/components/automation/litejet.py b/homeassistant/components/automation/litejet.py index 70e01174078..20c689d74cf 100644 --- a/homeassistant/components/automation/litejet.py +++ b/homeassistant/components/automation/litejet.py @@ -1,9 +1,4 @@ -""" -Trigger an automation when a LiteJet switch is released. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/automation.litejet/ -""" +"""Trigger an automation when a LiteJet switch is released.""" import logging import voluptuous as vol diff --git a/homeassistant/components/automation/mqtt.py b/homeassistant/components/automation/mqtt.py index 1fa0d540610..5f52da745ee 100644 --- a/homeassistant/components/automation/mqtt.py +++ b/homeassistant/components/automation/mqtt.py @@ -1,9 +1,4 @@ -""" -Offer MQTT listening automation rules. - -For more details about this automation rule, please refer to the documentation -at https://home-assistant.io/docs/automation/trigger/#mqtt-trigger -""" +"""Offer MQTT listening automation rules.""" import json import voluptuous as vol diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index aa51e631026..bf45abb88f0 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -1,9 +1,4 @@ -""" -Offer numeric state listening automation rules. - -For more details about this automation rule, please refer to the documentation -at https://home-assistant.io/docs/automation/trigger/#numeric-state-trigger -""" +"""Offer numeric state listening automation rules.""" import logging import voluptuous as vol diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index 4e47026d8d1..f4d7f69c07a 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -1,9 +1,4 @@ -""" -Offer state listening automation rules. - -For more details about this automation rule, please refer to the documentation -at https://home-assistant.io/docs/automation/trigger/#state-trigger -""" +"""Offer state listening automation rules.""" import voluptuous as vol from homeassistant.core import callback diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index 509195689a1..07fbf716e1c 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -1,9 +1,4 @@ -""" -Offer sun based automation rules. - -For more details about this automation rule, please refer to the documentation -at https://home-assistant.io/docs/automation/trigger/#sun-trigger -""" +"""Offer sun based automation rules.""" from datetime import timedelta import logging diff --git a/homeassistant/components/automation/template.py b/homeassistant/components/automation/template.py index 347b3f94e7d..ed86d52584f 100644 --- a/homeassistant/components/automation/template.py +++ b/homeassistant/components/automation/template.py @@ -1,9 +1,4 @@ -""" -Offer template automation rules. - -For more details about this automation rule, please refer to the documentation -at https://home-assistant.io/docs/automation/trigger/#template-trigger -""" +"""Offer template automation rules.""" import logging import voluptuous as vol @@ -13,7 +8,6 @@ from homeassistant.const import CONF_VALUE_TEMPLATE, CONF_PLATFORM from homeassistant.helpers.event import async_track_template import homeassistant.helpers.config_validation as cv - _LOGGER = logging.getLogger(__name__) TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema({ diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index d57e190490f..ce6d6eb4446 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -1,9 +1,4 @@ -""" -Offer time listening automation rules. - -For more details about this automation rule, please refer to the documentation -at https://home-assistant.io/docs/automation/trigger/#time-trigger -""" +"""Offer time listening automation rules.""" import logging import voluptuous as vol diff --git a/homeassistant/components/automation/time_pattern.py b/homeassistant/components/automation/time_pattern.py index 8b6e907f7b8..da8bc9f8629 100644 --- a/homeassistant/components/automation/time_pattern.py +++ b/homeassistant/components/automation/time_pattern.py @@ -1,9 +1,4 @@ -""" -Offer time listening automation rules. - -For more details about this automation rule, please refer to the documentation -at https://home-assistant.io/docs/automation/trigger/#time-trigger -""" +"""Offer time listening automation rules.""" import logging import voluptuous as vol diff --git a/homeassistant/components/automation/webhook.py b/homeassistant/components/automation/webhook.py index f4afc8a601a..f65b5cf885c 100644 --- a/homeassistant/components/automation/webhook.py +++ b/homeassistant/components/automation/webhook.py @@ -1,9 +1,4 @@ -""" -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 -""" +"""Offer webhook triggered automation rules.""" from functools import partial import logging diff --git a/homeassistant/components/automation/zone.py b/homeassistant/components/automation/zone.py index 0c3c0941a9e..e2d79eede8d 100644 --- a/homeassistant/components/automation/zone.py +++ b/homeassistant/components/automation/zone.py @@ -1,9 +1,4 @@ -""" -Offer zone automation rules. - -For more details about this automation rule, please refer to the documentation -at https://home-assistant.io/docs/automation/trigger/#zone-trigger -""" +"""Offer zone automation rules.""" import voluptuous as vol from homeassistant.core import callback diff --git a/homeassistant/components/axis/__init__.py b/homeassistant/components/axis/__init__.py index fd2e603445c..df723272a7a 100644 --- a/homeassistant/components/axis/__init__.py +++ b/homeassistant/components/axis/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Axis devices. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/axis/ -""" +"""Support for Axis devices.""" import logging import voluptuous as vol diff --git a/homeassistant/components/binary_sensor/axis.py b/homeassistant/components/axis/binary_sensor.py similarity index 94% rename from homeassistant/components/binary_sensor/axis.py rename to homeassistant/components/axis/binary_sensor.py index 671bbc730d0..11014dc4bc9 100644 --- a/homeassistant/components/binary_sensor/axis.py +++ b/homeassistant/components/axis/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for Axis binary sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.axis/ -""" +"""Support for Axis binary sensors.""" from datetime import timedelta import logging diff --git a/homeassistant/components/camera/axis.py b/homeassistant/components/axis/camera.py similarity index 92% rename from homeassistant/components/camera/axis.py rename to homeassistant/components/axis/camera.py index 9846ae85fb2..b9e969efec1 100644 --- a/homeassistant/components/camera/axis.py +++ b/homeassistant/components/axis/camera.py @@ -1,9 +1,4 @@ -""" -Support for Axis camera streaming. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/camera.axis/ -""" +"""Support for Axis camera streaming.""" import logging from homeassistant.components.camera.mjpeg import ( diff --git a/homeassistant/components/bbb_gpio.py b/homeassistant/components/bbb_gpio/__init__.py similarity index 90% rename from homeassistant/components/bbb_gpio.py rename to homeassistant/components/bbb_gpio/__init__.py index e3f327f1d5c..7749af8f335 100644 --- a/homeassistant/components/bbb_gpio.py +++ b/homeassistant/components/bbb_gpio/__init__.py @@ -1,9 +1,4 @@ -""" -Support for controlling GPIO pins of a Beaglebone Black. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/bbb_gpio/ -""" +"""Support for controlling GPIO pins of a Beaglebone Black.""" import logging from homeassistant.const import ( diff --git a/homeassistant/components/binary_sensor/bbb_gpio.py b/homeassistant/components/bbb_gpio/binary_sensor.py similarity index 92% rename from homeassistant/components/binary_sensor/bbb_gpio.py rename to homeassistant/components/bbb_gpio/binary_sensor.py index 8968b680369..1ee371dcc2a 100644 --- a/homeassistant/components/binary_sensor/bbb_gpio.py +++ b/homeassistant/components/bbb_gpio/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for binary sensor using Beaglebone Black GPIO. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.bbb_gpio/ -""" +"""Support for binary sensor using Beaglebone Black GPIO.""" import logging import voluptuous as vol diff --git a/homeassistant/components/switch/bbb_gpio.py b/homeassistant/components/bbb_gpio/switch.py similarity index 89% rename from homeassistant/components/switch/bbb_gpio.py rename to homeassistant/components/bbb_gpio/switch.py index 9e120beb442..3ad46fd61ae 100644 --- a/homeassistant/components/switch/bbb_gpio.py +++ b/homeassistant/components/bbb_gpio/switch.py @@ -1,9 +1,4 @@ -""" -Allows to configure a switch using BeagleBone Black GPIO. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/switch.bbb_gpio/ -""" +"""Allows to configure a switch using BeagleBone Black GPIO.""" import logging import voluptuous as vol @@ -29,8 +24,7 @@ PIN_SCHEMA = vol.Schema({ }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_PINS, default={}): - vol.Schema({cv.string: PIN_SCHEMA}), + vol.Required(CONF_PINS, default={}): vol.Schema({cv.string: PIN_SCHEMA}), }) diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index 7b2da21ff6a..9972e4dca3b 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -13,7 +13,8 @@ import voluptuous as vol from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import Entity from homeassistant.const import (STATE_ON, STATE_OFF) -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa +from homeassistant.helpers.config_validation import ( # noqa + PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) DOMAIN = 'binary_sensor' SCAN_INTERVAL = timedelta(seconds=30) diff --git a/homeassistant/components/binary_sensor/bayesian.py b/homeassistant/components/binary_sensor/bayesian.py index f7802f0f29d..97889ea7497 100644 --- a/homeassistant/components/binary_sensor/bayesian.py +++ b/homeassistant/components/binary_sensor/bayesian.py @@ -4,29 +4,27 @@ Use Bayesian Inference to trigger a binary sensor. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.bayesian/ """ -import logging from collections import OrderedDict import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.binary_sensor import ( - BinarySensorDevice, PLATFORM_SCHEMA) + PLATFORM_SCHEMA, BinarySensorDevice) from homeassistant.const import ( CONF_ABOVE, CONF_BELOW, CONF_DEVICE_CLASS, CONF_ENTITY_ID, CONF_NAME, - CONF_PLATFORM, CONF_STATE, STATE_UNKNOWN) + CONF_PLATFORM, CONF_STATE, CONF_VALUE_TEMPLATE, STATE_UNKNOWN) from homeassistant.core import callback from homeassistant.helpers import condition +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_state_change -_LOGGER = logging.getLogger(__name__) - ATTR_OBSERVATIONS = 'observations' ATTR_PROBABILITY = 'probability' ATTR_PROBABILITY_THRESHOLD = 'probability_threshold' CONF_OBSERVATIONS = 'observations' CONF_PRIOR = 'prior' +CONF_TEMPLATE = "template" CONF_PROBABILITY_THRESHOLD = 'probability_threshold' CONF_P_GIVEN_F = 'prob_given_false' CONF_P_GIVEN_T = 'prob_given_true' @@ -52,12 +50,20 @@ STATE_SCHEMA = vol.Schema({ vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float) }, required=True) +TEMPLATE_SCHEMA = vol.Schema({ + CONF_PLATFORM: CONF_TEMPLATE, + vol.Required(CONF_VALUE_TEMPLATE): cv.template, + vol.Required(CONF_P_GIVEN_T): vol.Coerce(float), + vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float) +}, required=True) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_DEVICE_CLASS): cv.string, vol.Required(CONF_OBSERVATIONS): vol.Schema(vol.All(cv.ensure_list, - [vol.Any(NUMERIC_STATE_SCHEMA, STATE_SCHEMA)])), + [vol.Any(NUMERIC_STATE_SCHEMA, STATE_SCHEMA, + TEMPLATE_SCHEMA)])), vol.Required(CONF_PRIOR): vol.Coerce(float), vol.Optional(CONF_PROBABILITY_THRESHOLD, default=DEFAULT_PROBABILITY_THRESHOLD): vol.Coerce(float), @@ -68,7 +74,6 @@ def update_probability(prior, prob_true, prob_false): """Update probability using Bayes' rule.""" numerator = prob_true * prior denominator = numerator + prob_false * (1 - prior) - probability = numerator / denominator return probability @@ -104,17 +109,27 @@ class BayesianBinarySensor(BinarySensorDevice): self.current_obs = OrderedDict({}) - to_observe = set(obs['entity_id'] for obs in self._observations) - + to_observe = set() + for obs in self._observations: + if 'entity_id' in obs: + to_observe.update(set([obs.get('entity_id')])) + if 'value_template' in obs: + to_observe.update( + set(obs.get(CONF_VALUE_TEMPLATE).extract_entities())) self.entity_obs = dict.fromkeys(to_observe, []) for ind, obs in enumerate(self._observations): obs['id'] = ind - self.entity_obs[obs['entity_id']].append(obs) + if 'entity_id' in obs: + self.entity_obs[obs['entity_id']].append(obs) + if 'value_template' in obs: + for ent in obs.get(CONF_VALUE_TEMPLATE).extract_entities(): + self.entity_obs[ent].append(obs) self.watchers = { 'numeric_state': self._process_numeric_state, - 'state': self._process_state + 'state': self._process_state, + 'template': self._process_template } async def async_added_to_hass(self): @@ -141,9 +156,8 @@ class BayesianBinarySensor(BinarySensorDevice): self.hass.async_add_job(self.async_update_ha_state, True) - entities = [obs['entity_id'] for obs in self._observations] async_track_state_change( - self.hass, entities, async_threshold_sensor_state_listener) + self.hass, self.entity_obs, async_threshold_sensor_state_listener) def _update_current_obs(self, entity_observation, should_trigger): """Update current observation.""" @@ -182,6 +196,14 @@ class BayesianBinarySensor(BinarySensorDevice): self._update_current_obs(entity_observation, should_trigger) + def _process_template(self, entity_observation): + """Add entity to current_obs if template is true.""" + template = entity_observation.get(CONF_VALUE_TEMPLATE) + template.hass = self.hass + should_trigger = condition.async_template( + self.hass, template, entity_observation) + self._update_current_obs(entity_observation, should_trigger) + @property def name(self): """Return the name of the sensor.""" diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py index 82815d11a6e..8e95f967396 100644 --- a/homeassistant/components/blink/__init__.py +++ b/homeassistant/components/blink/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Blink Home Camera System. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/blink/ -""" +"""Support for Blink Home Camera System.""" import logging from datetime import timedelta import voluptuous as vol @@ -29,7 +24,7 @@ DEFAULT_BRAND = 'Blink' DEFAULT_ATTRIBUTION = "Data provided by immedia-semi.com" SIGNAL_UPDATE_BLINK = "blink_update" -DEFAULT_SCAN_INTERVAL = timedelta(seconds=60) +DEFAULT_SCAN_INTERVAL = timedelta(seconds=300) TYPE_CAMERA_ARMED = 'motion_enabled' TYPE_MOTION_DETECTED = 'motion_detected' diff --git a/homeassistant/components/alarm_control_panel/blink.py b/homeassistant/components/blink/alarm_control_panel.py similarity index 92% rename from homeassistant/components/alarm_control_panel/blink.py rename to homeassistant/components/blink/alarm_control_panel.py index 77267fd7516..b9bf4a5250f 100644 --- a/homeassistant/components/alarm_control_panel/blink.py +++ b/homeassistant/components/blink/alarm_control_panel.py @@ -1,9 +1,4 @@ -""" -Support for Blink Alarm Control Panel. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/alarm_control_panel.blink/ -""" +"""Support for Blink Alarm Control Panel.""" import logging from homeassistant.components.alarm_control_panel import AlarmControlPanel diff --git a/homeassistant/components/binary_sensor/blink.py b/homeassistant/components/blink/binary_sensor.py similarity index 89% rename from homeassistant/components/binary_sensor/blink.py rename to homeassistant/components/blink/binary_sensor.py index cd558f03684..fe0b95b1517 100644 --- a/homeassistant/components/binary_sensor/blink.py +++ b/homeassistant/components/blink/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for Blink system camera control. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.blink. -""" +"""Support for Blink system camera control.""" from homeassistant.components.blink import BLINK_DATA, BINARY_SENSORS from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.const import CONF_MONITORED_CONDITIONS diff --git a/homeassistant/components/camera/blink.py b/homeassistant/components/blink/camera.py similarity index 92% rename from homeassistant/components/camera/blink.py rename to homeassistant/components/blink/camera.py index e9047914456..2e5c024d6e5 100644 --- a/homeassistant/components/camera/blink.py +++ b/homeassistant/components/blink/camera.py @@ -1,9 +1,4 @@ -""" -Support for Blink system camera. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/camera.blink/ -""" +"""Support for Blink system camera.""" import logging from homeassistant.components.blink import BLINK_DATA, DEFAULT_BRAND diff --git a/homeassistant/components/sensor/blink.py b/homeassistant/components/blink/sensor.py similarity index 92% rename from homeassistant/components/sensor/blink.py rename to homeassistant/components/blink/sensor.py index a1a07cb2a73..c1fdf9252dd 100644 --- a/homeassistant/components/sensor/blink.py +++ b/homeassistant/components/blink/sensor.py @@ -1,9 +1,4 @@ -""" -Support for Blink system camera sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.blink/ -""" +"""Support for Blink system camera sensors.""" import logging from homeassistant.components.blink import BLINK_DATA, SENSORS diff --git a/homeassistant/components/bloomsky.py b/homeassistant/components/bloomsky/__init__.py similarity index 92% rename from homeassistant/components/bloomsky.py rename to homeassistant/components/bloomsky/__init__.py index 55fb15c686d..a42eb34004b 100644 --- a/homeassistant/components/bloomsky.py +++ b/homeassistant/components/bloomsky/__init__.py @@ -1,9 +1,4 @@ -""" -Support for BloomSky weather station. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/bloomsky/ -""" +"""Support for BloomSky weather station.""" from datetime import timedelta import logging diff --git a/homeassistant/components/binary_sensor/bloomsky.py b/homeassistant/components/bloomsky/binary_sensor.py similarity index 91% rename from homeassistant/components/binary_sensor/bloomsky.py rename to homeassistant/components/bloomsky/binary_sensor.py index 971941f4dd6..c8763524de7 100644 --- a/homeassistant/components/binary_sensor/bloomsky.py +++ b/homeassistant/components/bloomsky/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support the binary sensors of a BloomSky weather station. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.bloomsky/ -""" +"""Support the binary sensors of a BloomSky weather station.""" import logging import voluptuous as vol diff --git a/homeassistant/components/camera/bloomsky.py b/homeassistant/components/bloomsky/camera.py similarity index 91% rename from homeassistant/components/camera/bloomsky.py rename to homeassistant/components/bloomsky/camera.py index 1c9266ca3a7..5cb2e1adfe1 100644 --- a/homeassistant/components/camera/bloomsky.py +++ b/homeassistant/components/bloomsky/camera.py @@ -1,9 +1,4 @@ -""" -Support for a camera of a BloomSky weather station. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/camera.bloomsky/ -""" +"""Support for a camera of a BloomSky weather station.""" import logging import requests diff --git a/homeassistant/components/sensor/bloomsky.py b/homeassistant/components/bloomsky/sensor.py similarity index 93% rename from homeassistant/components/sensor/bloomsky.py rename to homeassistant/components/bloomsky/sensor.py index 02cd456107f..7e6847f0e7e 100644 --- a/homeassistant/components/sensor/bloomsky.py +++ b/homeassistant/components/bloomsky/sensor.py @@ -1,9 +1,4 @@ -""" -Support the sensor of a BloomSky weather station. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/sensor.bloomsky/ -""" +"""Support the sensor of a BloomSky weather station.""" import logging import voluptuous as vol diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 40f2b91045a..e1ac30120d2 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -1,9 +1,4 @@ -""" -Reads vehicle status from BMW connected drive portal. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/bmw_connected_drive/ -""" +"""Reads vehicle status from BMW connected drive portal.""" import datetime import logging @@ -87,8 +82,8 @@ def setup_account(account_config: dict, hass, name: str) \ read_only = account_config[CONF_READ_ONLY] _LOGGER.debug('Adding new account %s', name) - cd_account = BMWConnectedDriveAccount(username, password, region, name, - read_only) + cd_account = BMWConnectedDriveAccount( + username, password, region, name, read_only) def execute_service(call): """Execute a service for a vehicle. @@ -99,7 +94,7 @@ def setup_account(account_config: dict, hass, name: str) \ vin = call.data[ATTR_VIN] vehicle = cd_account.account.get_vehicle(vin) if not vehicle: - _LOGGER.error('Could not find a vehicle for VIN "%s"!', vin) + _LOGGER.error("Could not find a vehicle for VIN %s", vin) return function_name = _SERVICE_MAP[call.service] function_call = getattr(vehicle.remote_services, function_name) @@ -108,9 +103,7 @@ def setup_account(account_config: dict, hass, name: str) \ # register the remote services for service in _SERVICE_MAP: hass.services.register( - DOMAIN, service, - execute_service, - schema=SERVICE_SCHEMA) + DOMAIN, service, execute_service, schema=SERVICE_SCHEMA) # update every UPDATE_INTERVAL minutes, starting now # this should even out the load on the servers @@ -144,15 +137,15 @@ class BMWConnectedDriveAccount: Notify all listeners about the update. """ - _LOGGER.debug('Updating vehicle state for account %s, ' - 'notifying %d listeners', - self.name, len(self._update_listeners)) + _LOGGER.debug( + "Updating vehicle state for account %s, notifying %d listeners", + self.name, len(self._update_listeners)) try: self.account.update_vehicle_states() for listener in self._update_listeners: listener() except IOError as exception: - _LOGGER.error('Error updating the vehicle state.') + _LOGGER.error("Error updating the vehicle state") _LOGGER.exception(exception) def add_update_listener(self, listener): diff --git a/homeassistant/components/binary_sensor/bmw_connected_drive.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py similarity index 93% rename from homeassistant/components/binary_sensor/bmw_connected_drive.py rename to homeassistant/components/bmw_connected_drive/binary_sensor.py index f8855b2e28b..1843f647df8 100644 --- a/homeassistant/components/binary_sensor/bmw_connected_drive.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Reads vehicle status from BMW connected drive portal. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.bmw_connected_drive/ -""" +"""Reads vehicle status from BMW connected drive portal.""" import logging from homeassistant.components.binary_sensor import BinarySensorDevice @@ -42,14 +37,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if vehicle.has_hv_battery: _LOGGER.debug('BMW with a high voltage battery') for key, value in sorted(SENSOR_TYPES_ELEC.items()): - device = BMWConnectedDriveSensor(account, vehicle, key, - value[0], value[1]) + device = BMWConnectedDriveSensor( + account, vehicle, key, value[0], value[1]) devices.append(device) elif vehicle.has_internal_combustion_engine: _LOGGER.debug('BMW with an internal combustion engine') for key, value in sorted(SENSOR_TYPES.items()): - device = BMWConnectedDriveSensor(account, vehicle, key, - value[0], value[1]) + device = BMWConnectedDriveSensor( + account, vehicle, key, value[0], value[1]) devices.append(device) add_entities(devices, True) diff --git a/homeassistant/components/device_tracker/bmw_connected_drive.py b/homeassistant/components/bmw_connected_drive/device_tracker.py similarity index 88% rename from homeassistant/components/device_tracker/bmw_connected_drive.py rename to homeassistant/components/bmw_connected_drive/device_tracker.py index 02a12653180..21121b069af 100644 --- a/homeassistant/components/device_tracker/bmw_connected_drive.py +++ b/homeassistant/components/bmw_connected_drive/device_tracker.py @@ -1,8 +1,4 @@ -"""Device tracker for BMW Connected Drive vehicles. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/device_tracker.bmw_connected_drive/ -""" +"""Device tracker for BMW Connected Drive vehicles.""" import logging from homeassistant.components.bmw_connected_drive import DOMAIN \ diff --git a/homeassistant/components/lock/bmw_connected_drive.py b/homeassistant/components/bmw_connected_drive/lock.py similarity index 95% rename from homeassistant/components/lock/bmw_connected_drive.py rename to homeassistant/components/bmw_connected_drive/lock.py index b13665610d8..8a5eddaa86a 100644 --- a/homeassistant/components/lock/bmw_connected_drive.py +++ b/homeassistant/components/bmw_connected_drive/lock.py @@ -1,9 +1,4 @@ -""" -Support for BMW cars with BMW ConnectedDrive. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/lock.bmw_connected_drive/ -""" +"""Support for BMW car locks with BMW ConnectedDrive.""" import logging from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN diff --git a/homeassistant/components/sensor/bmw_connected_drive.py b/homeassistant/components/bmw_connected_drive/sensor.py similarity index 84% rename from homeassistant/components/sensor/bmw_connected_drive.py rename to homeassistant/components/bmw_connected_drive/sensor.py index a7ee5724d19..a01142c53ed 100644 --- a/homeassistant/components/sensor/bmw_connected_drive.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -1,9 +1,4 @@ -""" -Reads vehicle status from BMW connected drive portal. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.bmw_connected_drive/ -""" +"""Support for reading vehicle status from BMW connected drive portal.""" import logging from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN @@ -25,7 +20,7 @@ ATTR_TO_HA_METRIC = { '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] + 'charging_status': ['mdi:battery-charging', None], } ATTR_TO_HA_IMPERIAL = { @@ -36,7 +31,7 @@ ATTR_TO_HA_IMPERIAL = { '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] + 'charging_status': ['mdi:battery-charging', None], } @@ -54,12 +49,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for account in accounts: for vehicle in account.account.vehicles: for attribute_name in vehicle.drive_train_attributes: - device = BMWConnectedDriveSensor(account, vehicle, - attribute_name, - attribute_info) + device = BMWConnectedDriveSensor( + account, vehicle, attribute_name, attribute_info) devices.append(device) - device = BMWConnectedDriveSensor(account, vehicle, 'mileage', - attribute_info) + device = BMWConnectedDriveSensor( + account, vehicle, 'mileage', attribute_info) devices.append(device) add_entities(devices, True) @@ -140,13 +134,13 @@ class BMWConnectedDriveSensor(Entity): 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) + 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) + 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/components/browser.py b/homeassistant/components/browser/__init__.py similarity index 69% rename from homeassistant/components/browser.py rename to homeassistant/components/browser/__init__.py index 041a0f9cdc6..1c002f21f5f 100644 --- a/homeassistant/components/browser.py +++ b/homeassistant/components/browser/__init__.py @@ -1,17 +1,13 @@ -""" -Provides functionality to launch a web browser on the host machine. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/browser/ -""" +"""Support for launching a web browser on the host machine.""" import voluptuous as vol -DOMAIN = "browser" -SERVICE_BROWSE_URL = "browse_url" - ATTR_URL = 'url' ATTR_URL_DEFAULT = 'https://www.google.com' +DOMAIN = 'browser' + +SERVICE_BROWSE_URL = 'browse_url' + SERVICE_BROWSE_URL_SCHEMA = vol.Schema({ # pylint: disable=no-value-for-parameter vol.Required(ATTR_URL, default=ATTR_URL_DEFAULT): vol.Url(), diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 9d105fb02d0..aa9e3153fe5 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Google Calendar event device sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/calendar/ -""" +"""Support for Google Calendar event device sensors.""" import logging from datetime import timedelta import re @@ -13,7 +8,8 @@ from aiohttp import web from homeassistant.components.google import ( CONF_OFFSET, CONF_DEVICE_ID, CONF_NAME) from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa +from homeassistant.helpers.config_validation import ( # noqa + PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) from homeassistant.helpers.config_validation import time_period_str from homeassistant.helpers.entity import Entity, generate_entity_id from homeassistant.helpers.entity_component import EntityComponent diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 653d0315ad4..474f9594610 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -25,7 +25,8 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import bind_hass from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa +from homeassistant.helpers.config_validation import ( # noqa + PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED from homeassistant.components import websocket_api import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/canary.py b/homeassistant/components/canary/__init__.py similarity index 96% rename from homeassistant/components/canary.py rename to homeassistant/components/canary/__init__.py index 04c33d83f3d..e53c7e22d2d 100644 --- a/homeassistant/components/canary.py +++ b/homeassistant/components/canary/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Canary. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/canary/ -""" +"""Support for Canary devices.""" import logging from datetime import timedelta diff --git a/homeassistant/components/cast/.translations/uk.json b/homeassistant/components/cast/.translations/uk.json new file mode 100644 index 00000000000..783defdca25 --- /dev/null +++ b/homeassistant/components/cast/.translations/uk.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "confirm": { + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Google Cast?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py index 53f5e704019..1b3da200540 100644 --- a/homeassistant/components/cast/__init__.py +++ b/homeassistant/components/cast/__init__.py @@ -2,9 +2,9 @@ from homeassistant import config_entries from homeassistant.helpers import config_entry_flow +REQUIREMENTS = ['pychromecast==2.5.2'] DOMAIN = 'cast' -REQUIREMENTS = ['pychromecast==2.1.0'] async def async_setup(hass, config): diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/cast/media_player.py similarity index 78% rename from homeassistant/components/media_player/cast.py rename to homeassistant/components/cast/media_player.py index 20a44c0e910..432290482f1 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/cast/media_player.py @@ -1,9 +1,4 @@ -""" -Provide functionality to interact with Cast devices on the network. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/media_player.cast/ -""" +"""Provide functionality to interact with Cast devices on the network.""" import asyncio import logging import threading @@ -14,10 +9,12 @@ import voluptuous as vol from homeassistant.components.cast import DOMAIN as CAST_DOMAIN from homeassistant.components.media_player import ( - MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, PLATFORM_SCHEMA, + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice) + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET) from homeassistant.const import ( CONF_HOST, EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING) @@ -55,6 +52,10 @@ ADDED_CAST_DEVICES_KEY = 'cast_added_cast_devices' # Chromecast or receive it through configuration SIGNAL_CAST_DISCOVERED = 'cast_discovered' +# Dispatcher signal fired with a ChromecastInfo every time a Chromecast is +# removed +SIGNAL_CAST_REMOVED = 'cast_removed' + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_IGNORE_CEC, default=[]): @@ -71,6 +72,7 @@ class ChromecastInfo: host = attr.ib(type=str) port = attr.ib(type=int) + service = attr.ib(type=Optional[str], default=None) uuid = attr.ib(type=Optional[str], converter=attr.converters.optional(str), default=None) # always convert UUID to string if not None manufacturer = attr.ib(type=str, default='') @@ -103,13 +105,15 @@ def _fill_out_missing_chromecast_info(info: ChromecastInfo) -> ChromecastInfo: # Fill out missing information via HTTP dial. from pychromecast import dial - http_device_status = dial.get_device_status(info.host) + http_device_status = dial.get_device_status( + info.host, services=[info.service], + zconf=ChromeCastZeroconf.get_zeroconf()) if http_device_status is None: # HTTP dial didn't give us any new information. return info return ChromecastInfo( - host=info.host, port=info.port, + service=info.service, host=info.host, port=info.port, uuid=(info.uuid or http_device_status.uuid), friendly_name=(info.friendly_name or http_device_status.friendly_name), manufacturer=(info.manufacturer or http_device_status.manufacturer), @@ -120,7 +124,6 @@ def _fill_out_missing_chromecast_info(info: ChromecastInfo) -> ChromecastInfo: def _discover_chromecast(hass: HomeAssistantType, info: ChromecastInfo): if info in hass.data[KNOWN_CHROMECAST_INFO_KEY]: _LOGGER.debug("Discovered previous chromecast %s", info) - return # Either discovered completely new chromecast or a "moved" one. info = _fill_out_missing_chromecast_info(info) @@ -136,6 +139,29 @@ def _discover_chromecast(hass: HomeAssistantType, info: ChromecastInfo): dispatcher_send(hass, SIGNAL_CAST_DISCOVERED, info) +def _remove_chromecast(hass: HomeAssistantType, info: ChromecastInfo): + # Removed chromecast + _LOGGER.debug("Removed chromecast %s", info) + + dispatcher_send(hass, SIGNAL_CAST_REMOVED, info) + + +class ChromeCastZeroconf: + """Class to hold a zeroconf instance.""" + + __zconf = None + + @classmethod + def set_zeroconf(cls, zconf): + """Set zeroconf.""" + cls.__zconf = zconf + + @classmethod + def get_zeroconf(cls): + """Get zeroconf.""" + return cls.__zconf + + def _setup_internal_discovery(hass: HomeAssistantType) -> None: """Set up the pychromecast internal discovery.""" if INTERNAL_DISCOVERY_RUNNING_KEY not in hass.data: @@ -147,10 +173,22 @@ def _setup_internal_discovery(hass: HomeAssistantType) -> None: import pychromecast - def internal_callback(name): + def internal_add_callback(name): """Handle zeroconf discovery of a new chromecast.""" mdns = listener.services[name] _discover_chromecast(hass, ChromecastInfo( + service=name, + host=mdns[0], + port=mdns[1], + uuid=mdns[2], + model_name=mdns[3], + friendly_name=mdns[4], + )) + + def internal_remove_callback(name, mdns): + """Handle zeroconf discovery of a removed chromecast.""" + _remove_chromecast(hass, ChromecastInfo( + service=name, host=mdns[0], port=mdns[1], uuid=mdns[2], @@ -159,7 +197,9 @@ def _setup_internal_discovery(hass: HomeAssistantType) -> None: )) _LOGGER.debug("Starting internal pychromecast discovery.") - listener, browser = pychromecast.start_discovery(internal_callback) + listener, browser = pychromecast.start_discovery(internal_add_callback, + internal_remove_callback) + ChromeCastZeroconf.set_zeroconf(browser.zc) def stop_discovery(event): """Stop discovery of new chromecasts.""" @@ -323,13 +363,20 @@ class CastDevice(MediaPlayerDevice): def __init__(self, cast_info): """Initialize the cast device.""" + import pychromecast # noqa: pylint: disable=unused-import self._cast_info = cast_info # type: ChromecastInfo + self.services = None + if cast_info.service: + self.services = set() + self.services.add(cast_info.service) self._chromecast = None # type: Optional[pychromecast.Chromecast] self.cast_status = None self.media_status = None self.media_status_received = None self._available = False # type: bool self._status_listener = None # type: Optional[CastStatusListener] + self._add_remove_handler = None + self._del_remove_handler = None async def async_added_to_hass(self): """Create chromecast object when added to hass.""" @@ -342,15 +389,36 @@ class CastDevice(MediaPlayerDevice): if self._cast_info.uuid != discover.uuid: # Discovered is not our device. return + if self.services is None: + _LOGGER.warning( + "[%s %s (%s:%s)] Received update for manually added Cast", + self.entity_id, self._cast_info.friendly_name, + self._cast_info.host, self._cast_info.port) + return _LOGGER.debug("Discovered chromecast with same UUID: %s", discover) self.hass.async_create_task(self.async_set_cast_info(discover)) + def async_cast_removed(discover: ChromecastInfo): + """Handle removal of Chromecast.""" + if self._cast_info.uuid is None: + # We can't handle empty UUIDs + return + if self._cast_info.uuid != discover.uuid: + # Removed is not our device. + return + _LOGGER.debug("Removed chromecast with same UUID: %s", discover) + self.hass.async_create_task(self.async_del_cast_info(discover)) + async def async_stop(event): """Disconnect socket on Home Assistant stop.""" await self._async_disconnect() - async_dispatcher_connect(self.hass, SIGNAL_CAST_DISCOVERED, - async_cast_discovered) + self._add_remove_handler = async_dispatcher_connect( + self.hass, SIGNAL_CAST_DISCOVERED, + async_cast_discovered) + self._del_remove_handler = async_dispatcher_connect( + self.hass, SIGNAL_CAST_REMOVED, + async_cast_removed) self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop) self.hass.async_create_task(self.async_set_cast_info(self._cast_info)) @@ -361,27 +429,52 @@ class CastDevice(MediaPlayerDevice): # Remove the entity from the added casts so that it can dynamically # be re-added again. self.hass.data[ADDED_CAST_DEVICES_KEY].remove(self._cast_info.uuid) + if self._add_remove_handler: + self._add_remove_handler() + if self._del_remove_handler: + self._del_remove_handler() async def async_set_cast_info(self, cast_info): """Set the cast information and set up the chromecast object.""" import pychromecast - old_cast_info = self._cast_info self._cast_info = cast_info + if self.services is not None: + if cast_info.service not in self.services: + _LOGGER.debug("[%s %s (%s:%s)] Got new service: %s (%s)", + self.entity_id, self._cast_info.friendly_name, + self._cast_info.host, self._cast_info.port, + cast_info.service, self.services) + + self.services.add(cast_info.service) + if self._chromecast is not None: - if old_cast_info.host_port == cast_info.host_port: - _LOGGER.debug("No connection related update: %s", - cast_info.host_port) - return - await self._async_disconnect() + # Only setup the chromecast once, added elements to services + # will automatically be picked up. + return # pylint: disable=protected-access - _LOGGER.debug("Connecting to cast device %s", cast_info) - chromecast = await self.hass.async_add_job( - pychromecast._get_chromecast_from_host, ( - cast_info.host, cast_info.port, cast_info.uuid, - cast_info.model_name, cast_info.friendly_name - )) + if self.services is None: + _LOGGER.debug( + "[%s %s (%s:%s)] Connecting to cast device by host %s", + self.entity_id, self._cast_info.friendly_name, + self._cast_info.host, self._cast_info.port, cast_info) + chromecast = await self.hass.async_add_job( + pychromecast._get_chromecast_from_host, ( + cast_info.host, cast_info.port, cast_info.uuid, + cast_info.model_name, cast_info.friendly_name + )) + else: + _LOGGER.debug( + "[%s %s (%s:%s)] Connecting to cast device by service %s", + self.entity_id, self._cast_info.friendly_name, + self._cast_info.host, self._cast_info.port, self.services) + chromecast = await self.hass.async_add_job( + pychromecast._get_chromecast_from_service, ( + self.services, ChromeCastZeroconf.get_zeroconf(), + cast_info.uuid, cast_info.model_name, + cast_info.friendly_name + )) self._chromecast = chromecast self._status_listener = CastStatusListener(self, chromecast) # Initialise connection status as connected because we can only @@ -391,15 +484,27 @@ class CastDevice(MediaPlayerDevice): self._available = True self.cast_status = chromecast.status self.media_status = chromecast.media_controller.status - _LOGGER.debug("Connection successful!") + _LOGGER.debug("[%s %s (%s:%s)] Connection successful!", + self.entity_id, self._cast_info.friendly_name, + self._cast_info.host, self._cast_info.port) self.async_schedule_update_ha_state() + async def async_del_cast_info(self, cast_info): + """Remove the service.""" + self.services.discard(cast_info.service) + _LOGGER.debug("[%s %s (%s:%s)] Remove service: %s (%s)", + self.entity_id, self._cast_info.friendly_name, + self._cast_info.host, self._cast_info.port, + cast_info.service, self.services) + async def _async_disconnect(self): """Disconnect Chromecast object if it is set.""" if self._chromecast is None: # Can't disconnect if not connected. return - _LOGGER.debug("Disconnecting from chromecast socket.") + _LOGGER.debug("[%s %s (%s:%s)] Disconnecting from chromecast socket.", + self.entity_id, self._cast_info.friendly_name, + self._cast_info.host, self._cast_info.port) self._available = False self.async_schedule_update_ha_state() @@ -436,8 +541,11 @@ class CastDevice(MediaPlayerDevice): from pychromecast.socket_client import CONNECTION_STATUS_CONNECTED, \ CONNECTION_STATUS_DISCONNECTED - _LOGGER.debug("Received cast device connection status: %s", - connection_status.status) + _LOGGER.debug( + "[%s %s (%s:%s)] Received cast device connection status: %s", + self.entity_id, self._cast_info.friendly_name, + self._cast_info.host, self._cast_info.port, + connection_status.status) if connection_status.status == CONNECTION_STATUS_DISCONNECTED: self._available = False self._invalidate() @@ -449,8 +557,11 @@ class CastDevice(MediaPlayerDevice): # Connection status callbacks happen often when disconnected. # Only update state when availability changed to put less pressure # on state machine. - _LOGGER.debug("Cast device availability changed: %s", - connection_status.status) + _LOGGER.debug( + "[%s %s (%s:%s)] Cast device availability changed: %s", + self.entity_id, self._cast_info.friendly_name, + self._cast_info.host, self._cast_info.port, + connection_status.status) self._available = new_available self.schedule_update_ha_state() diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 6d7f9432e39..e1d3093995c 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -14,32 +14,54 @@ 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 +from homeassistant.helpers.config_validation import ( # noqa + PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) 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) +from .const import ( + ATTR_AUX_HEAT, + ATTR_AWAY_MODE, + ATTR_CURRENT_HUMIDITY, + ATTR_CURRENT_TEMPERATURE, + ATTR_FAN_LIST, + ATTR_FAN_MODE, + ATTR_HOLD_MODE, + ATTR_HUMIDITY, + ATTR_MAX_HUMIDITY, + ATTR_MAX_TEMP, + ATTR_MIN_HUMIDITY, + ATTR_MIN_TEMP, + ATTR_OPERATION_LIST, + ATTR_OPERATION_MODE, + ATTR_SWING_LIST, + ATTR_SWING_MODE, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + ATTR_TARGET_TEMP_STEP, + DOMAIN, + SERVICE_SET_AUX_HEAT, + SERVICE_SET_AWAY_MODE, + SERVICE_SET_FAN_MODE, + SERVICE_SET_HOLD_MODE, + SERVICE_SET_HUMIDITY, + SERVICE_SET_OPERATION_MODE, + SERVICE_SET_SWING_MODE, + SERVICE_SET_TEMPERATURE, +) +from .reproduce_state import async_reproduce_states # noqa + DEFAULT_MIN_TEMP = 7 DEFAULT_MAX_TEMP = 35 DEFAULT_MIN_HUMITIDY = 30 DEFAULT_MAX_HUMIDITY = 99 -DOMAIN = 'climate' - ENTITY_ID_FORMAT = DOMAIN + '.{}' SCAN_INTERVAL = timedelta(seconds=60) -SERVICE_SET_AWAY_MODE = 'set_away_mode' -SERVICE_SET_AUX_HEAT = 'set_aux_heat' -SERVICE_SET_TEMPERATURE = 'set_temperature' -SERVICE_SET_FAN_MODE = 'set_fan_mode' -SERVICE_SET_HOLD_MODE = 'set_hold_mode' -SERVICE_SET_OPERATION_MODE = 'set_operation_mode' -SERVICE_SET_SWING_MODE = 'set_swing_mode' -SERVICE_SET_HUMIDITY = 'set_humidity' - STATE_HEAT = 'heat' STATE_COOL = 'cool' STATE_IDLE = 'idle' @@ -63,26 +85,6 @@ SUPPORT_AWAY_MODE = 1024 SUPPORT_AUX_HEAT = 2048 SUPPORT_ON_OFF = 4096 -ATTR_CURRENT_TEMPERATURE = 'current_temperature' -ATTR_MAX_TEMP = 'max_temp' -ATTR_MIN_TEMP = 'min_temp' -ATTR_TARGET_TEMP_HIGH = 'target_temp_high' -ATTR_TARGET_TEMP_LOW = 'target_temp_low' -ATTR_TARGET_TEMP_STEP = 'target_temp_step' -ATTR_AWAY_MODE = 'away_mode' -ATTR_AUX_HEAT = 'aux_heat' -ATTR_FAN_MODE = 'fan_mode' -ATTR_FAN_LIST = 'fan_list' -ATTR_CURRENT_HUMIDITY = 'current_humidity' -ATTR_HUMIDITY = 'humidity' -ATTR_MAX_HUMIDITY = 'max_humidity' -ATTR_MIN_HUMIDITY = 'min_humidity' -ATTR_HOLD_MODE = 'hold_mode' -ATTR_OPERATION_MODE = 'operation_mode' -ATTR_OPERATION_LIST = 'operation_list' -ATTR_SWING_MODE = 'swing_mode' -ATTR_SWING_LIST = 'swing_list' - CONVERTIBLE_ATTRIBUTE = [ ATTR_TEMPERATURE, ATTR_TARGET_TEMP_LOW, diff --git a/homeassistant/components/climate/const.py b/homeassistant/components/climate/const.py new file mode 100644 index 00000000000..2f84ee27bbd --- /dev/null +++ b/homeassistant/components/climate/const.py @@ -0,0 +1,32 @@ +"""Proides the constants needed for component.""" + +ATTR_AUX_HEAT = 'aux_heat' +ATTR_AWAY_MODE = 'away_mode' +ATTR_CURRENT_HUMIDITY = 'current_humidity' +ATTR_CURRENT_TEMPERATURE = 'current_temperature' +ATTR_FAN_LIST = 'fan_list' +ATTR_FAN_MODE = 'fan_mode' +ATTR_HOLD_MODE = 'hold_mode' +ATTR_HUMIDITY = 'humidity' +ATTR_MAX_HUMIDITY = 'max_humidity' +ATTR_MAX_TEMP = 'max_temp' +ATTR_MIN_HUMIDITY = 'min_humidity' +ATTR_MIN_TEMP = 'min_temp' +ATTR_OPERATION_LIST = 'operation_list' +ATTR_OPERATION_MODE = 'operation_mode' +ATTR_SWING_LIST = 'swing_list' +ATTR_SWING_MODE = 'swing_mode' +ATTR_TARGET_TEMP_HIGH = 'target_temp_high' +ATTR_TARGET_TEMP_LOW = 'target_temp_low' +ATTR_TARGET_TEMP_STEP = 'target_temp_step' + +DOMAIN = 'climate' + +SERVICE_SET_AUX_HEAT = 'set_aux_heat' +SERVICE_SET_AWAY_MODE = 'set_away_mode' +SERVICE_SET_FAN_MODE = 'set_fan_mode' +SERVICE_SET_HOLD_MODE = 'set_hold_mode' +SERVICE_SET_HUMIDITY = 'set_humidity' +SERVICE_SET_OPERATION_MODE = 'set_operation_mode' +SERVICE_SET_SWING_MODE = 'set_swing_mode' +SERVICE_SET_TEMPERATURE = 'set_temperature' diff --git a/homeassistant/components/climate/coolmaster.py b/homeassistant/components/climate/coolmaster.py new file mode 100644 index 00000000000..32c77b93eea --- /dev/null +++ b/homeassistant/components/climate/coolmaster.py @@ -0,0 +1,188 @@ +""" +CoolMasterNet platform that offers control of CoolMasteNet Climate Devices. + +For more details about this platform, please refer to the documentation +https://www.home-assistant.io/components/climate.coolmaster/ +""" + +import logging + +import voluptuous as vol + +from homeassistant.components.climate import ( + PLATFORM_SCHEMA, STATE_AUTO, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, + STATE_HEAT, SUPPORT_FAN_MODE, SUPPORT_ON_OFF, SUPPORT_OPERATION_MODE, + SUPPORT_TARGET_TEMPERATURE, ClimateDevice) +from homeassistant.const import ( + ATTR_TEMPERATURE, CONF_HOST, CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['pycoolmasternet==0.0.4'] + +SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE | + SUPPORT_OPERATION_MODE | SUPPORT_ON_OFF) + +DEFAULT_PORT = 10102 + +AVAILABLE_MODES = [STATE_HEAT, STATE_COOL, STATE_AUTO, STATE_DRY, + STATE_FAN_ONLY] + +CM_TO_HA_STATE = { + 'heat': STATE_HEAT, + 'cool': STATE_COOL, + 'auto': STATE_AUTO, + 'dry': STATE_DRY, + 'fan': STATE_FAN_ONLY, +} + +HA_STATE_TO_CM = {value: key for key, value in CM_TO_HA_STATE.items()} + +FAN_MODES = ['low', 'med', 'high', 'auto'] + +CONF_SUPPORTED_MODES = 'supported_modes' +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_SUPPORTED_MODES, default=AVAILABLE_MODES): + vol.All(cv.ensure_list, [vol.In(AVAILABLE_MODES)]), +}) + +_LOGGER = logging.getLogger(__name__) + + +def _build_entity(device, supported_modes): + _LOGGER.debug("Found device %s", device.uid) + return CoolmasterClimate(device, supported_modes) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the CoolMasterNet climate platform.""" + from pycoolmasternet import CoolMasterNet + + supported_modes = config.get(CONF_SUPPORTED_MODES) + host = config[CONF_HOST] + port = config[CONF_PORT] + cool = CoolMasterNet(host, port=port) + devices = cool.devices() + + all_devices = [_build_entity(device, supported_modes) + for device in devices] + + add_entities(all_devices, True) + + +class CoolmasterClimate(ClimateDevice): + """Representation of a coolmaster climate device.""" + + def __init__(self, device, supported_modes): + """Initialize the climate device.""" + self._device = device + self._uid = device.uid + self._operation_list = supported_modes + self._target_temperature = None + self._current_temperature = None + self._current_fan_mode = None + self._current_operation = None + self._on = None + self._unit = None + + def update(self): + """Pull state from CoolMasterNet.""" + status = self._device.status + self._target_temperature = status['thermostat'] + self._current_temperature = status['temperature'] + self._current_fan_mode = status['fan_speed'] + self._on = status['is_on'] + + device_mode = status['mode'] + self._current_operation = CM_TO_HA_STATE[device_mode] + + if status['unit'] == 'celsius': + self._unit = TEMP_CELSIUS + else: + self._unit = TEMP_FAHRENHEIT + + @property + def unique_id(self): + """Return unique ID for this device.""" + return self._uid + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS + + @property + def name(self): + """Return the name of the climate device.""" + return self.unique_id + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return self._unit + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._current_temperature + + @property + def target_temperature(self): + """Return the temperature we are trying 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_on(self): + """Return true if the device is on.""" + return self._on + + @property + def current_fan_mode(self): + """Return the fan setting.""" + return self._current_fan_mode + + @property + def fan_list(self): + """Return the list of available fan modes.""" + return FAN_MODES + + def set_temperature(self, **kwargs): + """Set new target temperatures.""" + temp = kwargs.get(ATTR_TEMPERATURE) + if temp is not None: + _LOGGER.debug("Setting temp of %s to %s", self.unique_id, + str(temp)) + self._device.set_thermostat(str(temp)) + + def set_fan_mode(self, fan_mode): + """Set new fan mode.""" + _LOGGER.debug("Setting fan mode of %s to %s", self.unique_id, + fan_mode) + self._device.set_fan_speed(fan_mode) + + def set_operation_mode(self, operation_mode): + """Set new operation mode.""" + _LOGGER.debug("Setting operation mode of %s to %s", self.unique_id, + operation_mode) + self._device.set_mode(HA_STATE_TO_CM[operation_mode]) + + def turn_on(self): + """Turn on.""" + _LOGGER.debug("Turning %s on", self.unique_id) + self._device.turn_on() + + def turn_off(self): + """Turn off.""" + _LOGGER.debug("Turning %s off", self.unique_id) + self._device.turn_off() diff --git a/homeassistant/components/climate/demo.py b/homeassistant/components/climate/demo.py index bc0b9bd52ee..14c22cefbe9 100644 --- a/homeassistant/components/climate/demo.py +++ b/homeassistant/components/climate/demo.py @@ -186,22 +186,22 @@ class DemoClimate(ClimateDevice): self.schedule_update_ha_state() def set_humidity(self, humidity): - """Set new target temperature.""" + """Set new humidity level.""" self._target_humidity = humidity self.schedule_update_ha_state() def set_swing_mode(self, swing_mode): - """Set new target temperature.""" + """Set new swing mode.""" self._current_swing_mode = swing_mode self.schedule_update_ha_state() def set_fan_mode(self, fan_mode): - """Set new target temperature.""" + """Set new fan mode.""" self._current_fan_mode = fan_mode self.schedule_update_ha_state() def set_operation_mode(self, operation_mode): - """Set new target temperature.""" + """Set new operation mode.""" self._current_operation = operation_mode self.schedule_update_ha_state() diff --git a/homeassistant/components/climate/flexit.py b/homeassistant/components/climate/flexit.py index de74d2facb5..e0453b8bf90 100644 --- a/homeassistant/components/climate/flexit.py +++ b/homeassistant/components/climate/flexit.py @@ -20,13 +20,15 @@ from homeassistant.const import ( from homeassistant.components.climate import ( ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE) -from homeassistant.components import modbus +from homeassistant.components.modbus import ( + CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['pyflexit==0.3'] DEPENDENCIES = ['modbus'] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, vol.Required(CONF_SLAVE): vol.All(int, vol.Range(min=0, max=32)), vol.Optional(CONF_NAME, default=DEVICE_DEFAULT_NAME): cv.string }) @@ -40,15 +42,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Flexit Platform.""" modbus_slave = config.get(CONF_SLAVE, None) name = config.get(CONF_NAME, None) - add_entities([Flexit(modbus_slave, name)], True) + hub = hass.data[MODBUS_DOMAIN][config.get(CONF_HUB)] + add_entities([Flexit(hub, modbus_slave, name)], True) class Flexit(ClimateDevice): """Representation of a Flexit AC unit.""" - def __init__(self, modbus_slave, name): + def __init__(self, hub, modbus_slave, name): """Initialize the unit.""" from pyflexit import pyflexit + self._hub = hub self._name = name self._slave = modbus_slave self._target_temperature = None @@ -64,7 +68,7 @@ class Flexit(ClimateDevice): self._heating = None self._cooling = None self._alarm = False - self.unit = pyflexit.pyflexit(modbus.HUB, modbus_slave) + self.unit = pyflexit.pyflexit(hub, modbus_slave) @property def supported_features(self): diff --git a/homeassistant/components/climate/reproduce_state.py b/homeassistant/components/climate/reproduce_state.py new file mode 100644 index 00000000000..3259e4084cf --- /dev/null +++ b/homeassistant/components/climate/reproduce_state.py @@ -0,0 +1,91 @@ +"""Module that groups code required to handle state restore for component.""" +import asyncio +from typing import Iterable, Optional + +from homeassistant.const import ( + ATTR_TEMPERATURE, SERVICE_TURN_OFF, + SERVICE_TURN_ON, STATE_OFF, STATE_ON) +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.loader import bind_hass + +from .const import ( + ATTR_AUX_HEAT, + ATTR_AWAY_MODE, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + ATTR_HOLD_MODE, + ATTR_OPERATION_MODE, + ATTR_SWING_MODE, + ATTR_HUMIDITY, + SERVICE_SET_AWAY_MODE, + SERVICE_SET_AUX_HEAT, + SERVICE_SET_TEMPERATURE, + SERVICE_SET_HOLD_MODE, + SERVICE_SET_OPERATION_MODE, + SERVICE_SET_SWING_MODE, + SERVICE_SET_HUMIDITY, + DOMAIN, +) + + +async def _async_reproduce_states(hass: HomeAssistantType, + state: State, + context: Optional[Context] = None) -> None: + """Reproduce component states.""" + async def call_service(service: str, keys: Iterable): + """Call service with set of attributes given.""" + data = {} + data['entity_id'] = state.entity_id + for key in keys: + if key in state.attributes: + data[key] = state.attributes[key] + + await hass.services.async_call( + DOMAIN, service, data, + blocking=True, context=context) + + if state.state == STATE_ON: + await call_service(SERVICE_TURN_ON, []) + elif state.state == STATE_OFF: + await call_service(SERVICE_TURN_OFF, []) + + if ATTR_AUX_HEAT in state.attributes: + await call_service(SERVICE_SET_AUX_HEAT, [ATTR_AUX_HEAT]) + + if ATTR_AWAY_MODE in state.attributes: + await call_service(SERVICE_SET_AWAY_MODE, [ATTR_AWAY_MODE]) + + if (ATTR_TEMPERATURE in state.attributes) or \ + (ATTR_TARGET_TEMP_HIGH in state.attributes) or \ + (ATTR_TARGET_TEMP_LOW in state.attributes): + await call_service(SERVICE_SET_TEMPERATURE, + [ATTR_TEMPERATURE, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW]) + + if ATTR_HOLD_MODE in state.attributes: + await call_service(SERVICE_SET_HOLD_MODE, + [ATTR_HOLD_MODE]) + + if ATTR_OPERATION_MODE in state.attributes: + await call_service(SERVICE_SET_OPERATION_MODE, + [ATTR_OPERATION_MODE]) + + if ATTR_SWING_MODE in state.attributes: + await call_service(SERVICE_SET_SWING_MODE, + [ATTR_SWING_MODE]) + + if ATTR_HUMIDITY in state.attributes: + await call_service(SERVICE_SET_HUMIDITY, + [ATTR_HUMIDITY]) + + +@bind_hass +async def async_reproduce_states(hass: HomeAssistantType, + states: Iterable[State], + context: Optional[Context] = None) -> None: + """Reproduce component states.""" + await asyncio.gather(*[ + _async_reproduce_states(hass, state, context) + for state in states]) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 98e649e1742..c427657c76d 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -1,9 +1,4 @@ -""" -Component to integrate the Home Assistant cloud. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/cloud/ -""" +"""Component to integrate the Home Assistant cloud.""" from datetime import datetime, timedelta import json import logging diff --git a/homeassistant/components/cloudflare.py b/homeassistant/components/cloudflare/__init__.py similarity index 91% rename from homeassistant/components/cloudflare.py rename to homeassistant/components/cloudflare/__init__.py index ae400ca6385..363e7c5eeb1 100644 --- a/homeassistant/components/cloudflare.py +++ b/homeassistant/components/cloudflare/__init__.py @@ -1,9 +1,4 @@ -""" -Update the IP addresses of your Cloudflare DNS records. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/cloudflare/ -""" +"""Update the IP addresses of your Cloudflare DNS records.""" from datetime import timedelta import logging diff --git a/homeassistant/components/coinbase.py b/homeassistant/components/coinbase/__init__.py similarity index 95% rename from homeassistant/components/coinbase.py rename to homeassistant/components/coinbase/__init__.py index 98c321b9f5a..40d04eadb3a 100644 --- a/homeassistant/components/coinbase.py +++ b/homeassistant/components/coinbase/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Coinbase. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/coinbase/ -""" +"""Support for Coinbase.""" from datetime import timedelta import logging diff --git a/homeassistant/components/comfoconnect.py b/homeassistant/components/comfoconnect/__init__.py similarity index 95% rename from homeassistant/components/comfoconnect.py rename to homeassistant/components/comfoconnect/__init__.py index 69d88274f29..64ebec18545 100644 --- a/homeassistant/components/comfoconnect.py +++ b/homeassistant/components/comfoconnect/__init__.py @@ -1,9 +1,4 @@ -""" -Support to control a Zehnder ComfoAir Q350/450/600 ventilation unit. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/comfoconnect/ -""" +"""Support to control a Zehnder ComfoAir Q350/450/600 ventilation unit.""" import logging import voluptuous as vol diff --git a/homeassistant/components/fan/comfoconnect.py b/homeassistant/components/comfoconnect/fan.py similarity index 94% rename from homeassistant/components/fan/comfoconnect.py rename to homeassistant/components/comfoconnect/fan.py index 0e0ac8c80b6..a396dd276a5 100644 --- a/homeassistant/components/fan/comfoconnect.py +++ b/homeassistant/components/comfoconnect/fan.py @@ -1,9 +1,4 @@ -""" -Platform to control a Zehnder ComfoAir Q350/450/600 ventilation unit. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/fan.comfoconnect/ -""" +"""Platform to control a Zehnder ComfoAir Q350/450/600 ventilation unit.""" import logging from homeassistant.components.comfoconnect import ( diff --git a/homeassistant/components/sensor/comfoconnect.py b/homeassistant/components/comfoconnect/sensor.py similarity index 94% rename from homeassistant/components/sensor/comfoconnect.py rename to homeassistant/components/comfoconnect/sensor.py index 9ae6a4de091..ac5de866cfb 100644 --- a/homeassistant/components/sensor/comfoconnect.py +++ b/homeassistant/components/comfoconnect/sensor.py @@ -1,9 +1,4 @@ -""" -Platform to control a Zehnder ComfoAir Q350/450/600 ventilation unit. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/sensor.comfoconnect/ -""" +"""Platform to control a Zehnder ComfoAir Q350/450/600 ventilation unit.""" import logging from homeassistant.components.comfoconnect import ( diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index 7836cba6cf9..47ac9d3a4b2 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -7,7 +7,6 @@ from homeassistant.const import CONF_ID, SERVICE_RELOAD from homeassistant.components.automation import DOMAIN, PLATFORM_SCHEMA import homeassistant.helpers.config_validation as cv - CONFIG_PATH = 'automations.yaml' diff --git a/homeassistant/components/config/customize.py b/homeassistant/components/config/customize.py index b7a8c9c070a..ec9d9d0ff27 100644 --- a/homeassistant/components/config/customize.py +++ b/homeassistant/components/config/customize.py @@ -1,5 +1,4 @@ """Provide configuration end points for Customize.""" - from homeassistant.components.config import EditKeyBasedConfigView from homeassistant.components import SERVICE_RELOAD_CORE_CONFIG from homeassistant.config import DATA_CUSTOMIZE diff --git a/homeassistant/components/config/script.py b/homeassistant/components/config/script.py index 3adc6f14233..640e267d066 100644 --- a/homeassistant/components/config/script.py +++ b/homeassistant/components/config/script.py @@ -1,5 +1,4 @@ """Provide configuration end points for scripts.""" - from homeassistant.components.config import EditKeyBasedConfigView from homeassistant.components.script import DOMAIN, SCRIPT_ENTRY_SCHEMA from homeassistant.const import SERVICE_RELOAD diff --git a/homeassistant/components/config/zwave.py b/homeassistant/components/config/zwave.py index 57123ee12de..f29dde4594c 100644 --- a/homeassistant/components/config/zwave.py +++ b/homeassistant/components/config/zwave.py @@ -1,13 +1,14 @@ """Provide configuration end points for Z-Wave.""" +from collections import deque import logging -from collections import deque from aiohttp.web import Response -import homeassistant.core as ha -from homeassistant.const import HTTP_NOT_FOUND, HTTP_OK -from homeassistant.components.http import HomeAssistantView + from homeassistant.components.config import EditKeyBasedConfigView -from homeassistant.components.zwave import const, DEVICE_CONFIG_SCHEMA_ENTRY +from homeassistant.components.http import HomeAssistantView +from homeassistant.components.zwave import DEVICE_CONFIG_SCHEMA_ENTRY, const +from homeassistant.const import HTTP_NOT_FOUND, HTTP_OK +import homeassistant.core as ha import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/configurator.py b/homeassistant/components/configurator/__init__.py similarity index 100% rename from homeassistant/components/configurator.py rename to homeassistant/components/configurator/__init__.py diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index d8d386f5ca0..7082cb36726 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -1,9 +1,4 @@ -""" -Support for functionality to have conversations with Home Assistant. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/conversation/ -""" +"""Support for functionality to have conversations with Home Assistant.""" import logging import re diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index aeef2818f63..ab7ada618fe 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -1,9 +1,4 @@ -""" -Component to count within automations. - -For more details about this component, please refer to the documentation -at https://home-assistant.io/components/counter/ -""" +"""Component to count within automations.""" import logging import voluptuous as vol diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index b5b2a91b097..bd003f1ad67 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -13,7 +13,8 @@ import voluptuous as vol from homeassistant.loader import bind_hass from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import Entity -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa +from homeassistant.helpers.config_validation import ( # noqa + PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) import homeassistant.helpers.config_validation as cv from homeassistant.components import group from homeassistant.helpers import intent diff --git a/homeassistant/components/daikin/.translations/da.json b/homeassistant/components/daikin/.translations/da.json new file mode 100644 index 00000000000..856bb1445c7 --- /dev/null +++ b/homeassistant/components/daikin/.translations/da.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Enheden er allerede konfigureret", + "device_fail": "Uventet fejl ved oprettelse af enhed.", + "device_timeout": "Timeout ved tilslutning til enheden." + }, + "step": { + "user": { + "data": { + "host": "V\u00e6rt" + }, + "description": "Indtast IP-adresse p\u00e5 dit Daikin AC.", + "title": "Konfigurer Daikin AC" + } + }, + "title": "Daikin AC" + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index f16d6a87d55..ce4a58162c7 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -1,9 +1,4 @@ -""" -Platform for the Daikin AC. - -For more details about this component, please refer to the documentation -https://home-assistant.io/components/daikin/ -""" +"""Platform for the Daikin AC.""" import asyncio from datetime import timedelta import logging diff --git a/homeassistant/components/climate/daikin.py b/homeassistant/components/daikin/climate.py similarity index 98% rename from homeassistant/components/climate/daikin.py rename to homeassistant/components/daikin/climate.py index 45589c12864..d97b506e273 100644 --- a/homeassistant/components/climate/daikin.py +++ b/homeassistant/components/daikin/climate.py @@ -1,9 +1,4 @@ -""" -Support for the Daikin HVAC. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/climate.daikin/ -""" +"""Support for the Daikin HVAC.""" import logging import re @@ -22,7 +17,6 @@ from homeassistant.const import ( ATTR_TEMPERATURE, CONF_HOST, CONF_NAME, TEMP_CELSIUS) import homeassistant.helpers.config_validation as cv - _LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ diff --git a/homeassistant/components/sensor/daikin.py b/homeassistant/components/daikin/sensor.py similarity index 95% rename from homeassistant/components/sensor/daikin.py rename to homeassistant/components/daikin/sensor.py index f887ccf64a0..6065a182274 100644 --- a/homeassistant/components/sensor/daikin.py +++ b/homeassistant/components/daikin/sensor.py @@ -1,9 +1,4 @@ -""" -Support for Daikin AC Sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.daikin/ -""" +"""Support for Daikin AC sensors.""" import logging from homeassistant.components.daikin import DOMAIN as DAIKIN_DOMAIN diff --git a/homeassistant/components/danfoss_air/__init__.py b/homeassistant/components/danfoss_air/__init__.py index 80c36b6f0c6..d6123a25f23 100644 --- a/homeassistant/components/danfoss_air/__init__.py +++ b/homeassistant/components/danfoss_air/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Danfoss Air HRV. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/danfoss_air/ -""" +"""Support for Danfoss Air HRV.""" from datetime import timedelta import logging diff --git a/homeassistant/components/datadog.py b/homeassistant/components/datadog/__init__.py similarity index 94% rename from homeassistant/components/datadog.py rename to homeassistant/components/datadog/__init__.py index 58503d7187b..3b519514d17 100644 --- a/homeassistant/components/datadog.py +++ b/homeassistant/components/datadog/__init__.py @@ -1,9 +1,4 @@ -""" -A component which allows you to send data to Datadog. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/datadog/ -""" +"""Support for sending data to Datadog.""" import logging import voluptuous as vol diff --git a/homeassistant/components/deconz/.translations/da.json b/homeassistant/components/deconz/.translations/da.json index 7f9aad83160..e4e5f098a4d 100644 --- a/homeassistant/components/deconz/.translations/da.json +++ b/homeassistant/components/deconz/.translations/da.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Bridge er allerede konfigureret", "no_bridges": "Ingen deConz bridge fundet", "one_instance_only": "Komponenten underst\u00f8tter kun \u00e9n deCONZ forekomst" }, @@ -11,8 +12,13 @@ "init": { "data": { "host": "V\u00e6rt", - "port": "Port (standardv\u00e6rdi: '80')" - } + "port": "Port" + }, + "title": "Definer deCONZ gateway" + }, + "link": { + "description": "L\u00e5s din deCONZ-gateway op for at registrere dig med Home Assistant. \n\n 1. G\u00e5 til deCONZ settings -> Gateway -> Advanced\n 2. Tryk p\u00e5 knappen \"Authenticate app\"", + "title": "Link med deCONZ" }, "options": { "data": { @@ -21,6 +27,7 @@ }, "title": "Ekstra konfiguration valgmuligheder for deCONZ" } - } + }, + "title": "deCONZ Zigbee gateway" } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ko.json b/homeassistant/components/deconz/.translations/ko.json index a501951540b..6a527ab0a0b 100644 --- a/homeassistant/components/deconz/.translations/ko.json +++ b/homeassistant/components/deconz/.translations/ko.json @@ -12,12 +12,12 @@ "init": { "data": { "host": "\ud638\uc2a4\ud2b8", - "port": "\ud3ec\ud2b8 (\uae30\ubcf8\uac12: '80')" + "port": "\ud3ec\ud2b8" }, "title": "deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774 \uc815\uc758" }, "link": { - "description": "deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \uc5b8\ub77d\ud558\uc5ec Home Assistant \uc5d0 \uc5f0\uacb0\ud558\uae30\n\n1. deCONZ \uc2dc\uc2a4\ud15c \uc124\uc815\uc73c\ub85c \uc774\ub3d9\ud558\uc138\uc694\n2. \"Unlock Gateway\" \ubc84\ud2bc\uc744 \ub204\ub974\uc138\uc694 ", + "description": "deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \uc5b8\ub77d\ud558\uc5ec Home Assistant \uc5d0 \uc5f0\uacb0\ud558\uae30.\n\n1. deCONZ \uc2dc\uc2a4\ud15c \uc124\uc815\uc73c\ub85c \uc774\ub3d9\ud558\uc138\uc694\n2. \"Authenticate app\" \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694", "title": "deCONZ\uc640 \uc5f0\uacb0" }, "options": { diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index 3ff60254a6a..c92f1562157 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -17,7 +17,7 @@ "title": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0448\u043b\u044e\u0437 deCONZ" }, "link": { - "description": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0439\u0442\u0435 \u0448\u043b\u044e\u0437 deCONZ \u0434\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0432 Home Assistant:\n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u044b deCONZ\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u00ab\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0448\u043b\u044e\u0437\u00bb", + "description": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0439\u0442\u0435 \u0448\u043b\u044e\u0437 deCONZ \u0434\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0432 Home Assistant:\n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u044b deCONZ -> Gateway -> Advanced\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u00abAuthenticate app\u00bb", "title": "\u0421\u0432\u044f\u0437\u044c \u0441 deCONZ" }, "options": { diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 4d3e2cbc6a9..8015324be13 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -1,9 +1,4 @@ -""" -Support for deCONZ devices. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/deconz/ -""" +"""Support for deCONZ devices.""" import voluptuous as vol from homeassistant import config_entries diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 286b310c1a9..77d01c5c40b 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for deCONZ binary sensor. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.deconz/ -""" +"""Support for deCONZ binary sensors.""" from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.const import ATTR_BATTERY_LEVEL from homeassistant.core import callback @@ -16,8 +11,8 @@ from .deconz_device import DeconzDevice DEPENDENCIES = ['deconz'] -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ binary sensors.""" pass diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index f7bc71a2398..8f90f303fca 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -1,5 +1,4 @@ """Config flow to configure deCONZ component.""" - import voluptuous as vol from homeassistant import config_entries diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index 99bdd20a295..48f06a894bb 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -1,9 +1,4 @@ -""" -Support for deCONZ covers. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/cover.deconz/ -""" +"""Support for deCONZ covers.""" from homeassistant.components.cover import ( ATTR_POSITION, CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_STOP, SUPPORT_SET_POSITION) @@ -18,8 +13,8 @@ DEPENDENCIES = ['deconz'] ZIGBEE_SPEC = ['lumi.curtain'] -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Unsupported way of setting up deCONZ covers.""" pass diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 8d33e011b94..fe9fc4b7752 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -1,5 +1,5 @@ """Representation of a deCONZ gateway.""" -from homeassistant import config_entries +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.const import CONF_EVENT, CONF_ID from homeassistant.core import EventOrigin, callback from homeassistant.helpers import aiohttp_client @@ -8,7 +8,7 @@ from homeassistant.helpers.dispatcher import ( from homeassistant.util import slugify from .const import ( - _LOGGER, DECONZ_REACHABLE, CONF_ALLOW_CLIP_SENSOR, SUPPORTED_PLATFORMS) + DECONZ_REACHABLE, CONF_ALLOW_CLIP_SENSOR, SUPPORTED_PLATFORMS) class DeconzGateway: @@ -20,7 +20,6 @@ class DeconzGateway: self.config_entry = config_entry self.available = True self.api = None - self._cancel_retry_setup = None self.deconz_ids = {} self.events = [] @@ -35,22 +34,8 @@ class DeconzGateway: self.async_connection_status_callback ) - if self.api is False: - retry_delay = 2 ** (tries + 1) - _LOGGER.error( - "Error connecting to deCONZ gateway. 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 + if not self.api: + raise ConfigEntryNotReady for component in SUPPORTED_PLATFORMS: hass.async_create_task( @@ -107,12 +92,6 @@ class DeconzGateway: 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 - self.api.close() for component in SUPPORTED_PLATFORMS: diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index f7c777b8100..50e22c84d6f 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -1,9 +1,4 @@ -""" -Support for deCONZ light. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/light.deconz/ -""" +"""Support for deCONZ lights.""" from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR, ATTR_TRANSITION, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT, @@ -21,8 +16,8 @@ from .deconz_device import DeconzDevice DEPENDENCIES = ['deconz'] -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ lights and group.""" pass diff --git a/homeassistant/components/deconz/scene.py b/homeassistant/components/deconz/scene.py index 05845a02288..d3a6df810ba 100644 --- a/homeassistant/components/deconz/scene.py +++ b/homeassistant/components/deconz/scene.py @@ -1,9 +1,4 @@ -""" -Support for deCONZ scenes. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/scene.deconz/ -""" +"""Support for deCONZ scenes.""" from homeassistant.components.deconz import DOMAIN as DECONZ_DOMAIN from homeassistant.components.scene import Scene from homeassistant.core import callback @@ -12,8 +7,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect DEPENDENCIES = ['deconz'] -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ scenes.""" pass diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 1913e3d5087..3083f0c6732 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -1,9 +1,4 @@ -""" -Support for deCONZ sensor. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/sensor.deconz/ -""" +"""Support for deCONZ sensors.""" from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY) from homeassistant.core import callback @@ -21,8 +16,8 @@ ATTR_DAYLIGHT = 'daylight' ATTR_EVENT_ID = 'event_id' -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ sensors.""" pass diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index 9ab7b56c0ca..1bf7235713a 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -11,7 +11,7 @@ }, "link": { "title": "Link with deCONZ", - "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button" + "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ Settings -> Gateway -> Advanced\n2. Press \"Authenticate app\" button" }, "options": { "title": "Extra configuration options for deCONZ", diff --git a/homeassistant/components/deconz/switch.py b/homeassistant/components/deconz/switch.py index 64d93389670..c48c7205e01 100644 --- a/homeassistant/components/deconz/switch.py +++ b/homeassistant/components/deconz/switch.py @@ -1,9 +1,4 @@ -""" -Support for deCONZ switches. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.deconz/ -""" +"""Support for deCONZ switches.""" from homeassistant.components.switch import SwitchDevice from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -11,12 +6,11 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import DOMAIN as DECONZ_DOMAIN, POWER_PLUGS, SIRENS from .deconz_device import DeconzDevice - DEPENDENCIES = ['deconz'] -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ switches.""" pass diff --git a/homeassistant/components/default_config/__init__.py b/homeassistant/components/default_config/__init__.py new file mode 100644 index 00000000000..d56cf9a4ee8 --- /dev/null +++ b/homeassistant/components/default_config/__init__.py @@ -0,0 +1,24 @@ +"""Component providing default configuration for new users.""" + +DOMAIN = 'default_config' +DEPENDENCIES = ( + 'automation', + 'cloud', + 'config', + 'conversation', + 'discovery', + 'frontend', + 'history', + 'logbook', + 'map', + 'person', + 'script', + 'sun', + 'system_health', + 'updater', +) + + +async def async_setup(hass, config): + """Initialize default configuration.""" + return True diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo/__init__.py similarity index 97% rename from homeassistant/components/demo.py rename to homeassistant/components/demo/__init__.py index 2b9854fbcc7..2699ade0b1d 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo/__init__.py @@ -1,9 +1,4 @@ -""" -Set up the demo environment that mimics interaction with devices. - -For more details about this component, please refer to the documentation -https://home-assistant.io/components/demo/ -""" +"""Set up the demo environment that mimics interaction with devices.""" import asyncio import time diff --git a/homeassistant/components/remote/demo.py b/homeassistant/components/demo/remote.py similarity index 100% rename from homeassistant/components/remote/demo.py rename to homeassistant/components/demo/remote.py diff --git a/homeassistant/components/device_sun_light_trigger.py b/homeassistant/components/device_sun_light_trigger/__init__.py similarity index 100% rename from homeassistant/components/device_sun_light_trigger.py rename to homeassistant/components/device_sun_light_trigger/__init__.py diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 202883713c7..af33453c9d5 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -22,10 +22,10 @@ from homeassistant.components.zone.zone import async_active_zone from homeassistant.config import load_yaml_config_file, async_log_exception from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, discovery +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType -import homeassistant.helpers.config_validation as cv from homeassistant import util from homeassistant.util.async_ import run_coroutine_threadsafe import homeassistant.util.dt as dt_util @@ -96,6 +96,7 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NEW_DEVICE_DEFAULTS, default={}): NEW_DEVICE_DEFAULTS_SCHEMA }) +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE.extend(PLATFORM_SCHEMA.schema) SERVICE_SEE_PAYLOAD_SCHEMA = vol.Schema(vol.All( cv.has_at_least_one_key(ATTR_MAC, ATTR_DEV_ID), { ATTR_MAC: cv.string, diff --git a/homeassistant/components/device_tracker/bbox.py b/homeassistant/components/device_tracker/bbox.py index 297e98e548a..f59c922577b 100644 --- a/homeassistant/components/device_tracker/bbox.py +++ b/homeassistant/components/device_tracker/bbox.py @@ -45,6 +45,8 @@ class BboxDeviceScanner(DeviceScanner): def __init__(self, config): """Get host from config.""" + from typing import List # noqa: pylint: disable=unused-import + self.host = config[CONF_HOST] """Initialize the scanner.""" diff --git a/homeassistant/components/device_tracker/googlehome.py b/homeassistant/components/device_tracker/googlehome.py deleted file mode 100644 index daa36d1d2c7..00000000000 --- a/homeassistant/components/device_tracker/googlehome.py +++ /dev/null @@ -1,95 +0,0 @@ -""" -Support for Google Home bluetooth tacker. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/device_tracker.googlehome/ -""" -import logging - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.components.device_tracker import ( - DOMAIN, PLATFORM_SCHEMA, DeviceScanner) -from homeassistant.const import CONF_HOST - -REQUIREMENTS = ['ghlocalapi==0.3.5'] - -_LOGGER = logging.getLogger(__name__) - -CONF_RSSI_THRESHOLD = 'rssi_threshold' - -PLATFORM_SCHEMA = vol.All( - PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_RSSI_THRESHOLD, default=-70): vol.Coerce(int), - })) - - -async def async_get_scanner(hass, config): - """Validate the configuration and return an Google Home scanner.""" - scanner = GoogleHomeDeviceScanner(hass, config[DOMAIN]) - await scanner.async_connect() - return scanner if scanner.success_init else None - - -class GoogleHomeDeviceScanner(DeviceScanner): - """This class queries a Google Home unit.""" - - def __init__(self, hass, config): - """Initialize the scanner.""" - from ghlocalapi.device_info import DeviceInfo - from ghlocalapi.bluetooth import Bluetooth - - self.last_results = {} - - self.success_init = False - self._host = config[CONF_HOST] - self.rssi_threshold = config[CONF_RSSI_THRESHOLD] - - session = async_get_clientsession(hass) - self.deviceinfo = DeviceInfo(hass.loop, session, self._host) - self.scanner = Bluetooth(hass.loop, session, self._host) - - async def async_connect(self): - """Initialize connection to Google Home.""" - await self.deviceinfo.get_device_info() - data = self.deviceinfo.device_info - self.success_init = data is not None - - async def async_scan_devices(self): - """Scan for new devices and return a list with found device IDs.""" - await self.async_update_info() - return list(self.last_results.keys()) - - async def async_get_device_name(self, device): - """Return the name of the given device or None if we don't know.""" - if device not in self.last_results: - return None - return '{}_{}'.format(self._host, - self.last_results[device]['btle_mac_address']) - - async def get_extra_attributes(self, device): - """Return the extra attributes of the device.""" - return self.last_results[device] - - async def async_update_info(self): - """Ensure the information from Google Home is up to date.""" - _LOGGER.debug('Checking Devices...') - await self.scanner.scan_for_devices() - await self.scanner.get_scan_result() - ghname = self.deviceinfo.device_info['name'] - devices = {} - for device in self.scanner.devices: - if device['rssi'] > self.rssi_threshold: - uuid = '{}_{}'.format(self._host, device['mac_address']) - devices[uuid] = {} - devices[uuid]['rssi'] = device['rssi'] - devices[uuid]['btle_mac_address'] = device['mac_address'] - devices[uuid]['ghname'] = ghname - devices[uuid]['source_type'] = 'bluetooth' - if device['name']: - devices[uuid]['btle_name'] = device['name'] - await self.scanner.clear_scan_result() - self.last_results = devices diff --git a/homeassistant/components/device_tracker/mikrotik.py b/homeassistant/components/device_tracker/mikrotik.py index cddcd1f26ee..c4635f8dc43 100644 --- a/homeassistant/components/device_tracker/mikrotik.py +++ b/homeassistant/components/device_tracker/mikrotik.py @@ -174,7 +174,7 @@ class MikrotikScanner(DeviceScanner): else: devices_tracker = 'ip' - _LOGGER.info( + _LOGGER.debug( "Loading %s devices from Mikrotik (%s) ...", devices_tracker, self.host) diff --git a/homeassistant/components/device_tracker/synology_srm.py b/homeassistant/components/device_tracker/synology_srm.py index cc931b797d4..5c7ac9a5d00 100644 --- a/homeassistant/components/device_tracker/synology_srm.py +++ b/homeassistant/components/device_tracker/synology_srm.py @@ -13,7 +13,7 @@ from homeassistant.const import ( CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_SSL, CONF_VERIFY_SSL) -REQUIREMENTS = ['synology-srm==0.0.3'] +REQUIREMENTS = ['synology-srm==0.0.4'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/dialogflow/.translations/da.json b/homeassistant/components/dialogflow/.translations/da.json new file mode 100644 index 00000000000..2fb203450a5 --- /dev/null +++ b/homeassistant/components/dialogflow/.translations/da.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Dit Home Assistant system skal v\u00e6re tilg\u00e6ngeligt fra internettet for at modtage Dialogflow meddelelser.", + "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning" + }, + "create_entry": { + "default": "For at sende begivenheder til Home Assistant skal du konfigurere [Webhook integration med Dialogflow]({dialogflow_url}).\n\n Udfyld f\u00f8lgende oplysninger: \n\n - URL: `{webhook_url}`\n - Metode: POST\n - Indholdstype: application/json\n\n Se [dokumentationen]({docs_url}) for yderligere oplysninger." + }, + "step": { + "user": { + "description": "Er du sikker p\u00e5 at du vil konfigurere Dialogflow?", + "title": "Konfigurer Dialogflow Webhook" + } + }, + "title": "Dialogflow" + } +} \ No newline at end of file diff --git a/homeassistant/components/dialogflow/.translations/ko.json b/homeassistant/components/dialogflow/.translations/ko.json index cf53f81bdb8..33c465bf0e7 100644 --- a/homeassistant/components/dialogflow/.translations/ko.json +++ b/homeassistant/components/dialogflow/.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\ub824\uba74 [Dialogflow Webhook]({dialogflow_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/json\n \n\uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574 \uc8fc\uc138\uc694." + "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 [Dialogflow Webhook]({dialogflow_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/json\n \n\uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "step": { "user": { diff --git a/homeassistant/components/dialogflow/__init__.py b/homeassistant/components/dialogflow/__init__.py index 3f3fbe7c14e..210aebe80d5 100644 --- a/homeassistant/components/dialogflow/__init__.py +++ b/homeassistant/components/dialogflow/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Dialogflow webhook. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/dialogflow/ -""" +"""Support for Dialogflow webhook.""" import logging import voluptuous as vol @@ -12,6 +7,7 @@ from aiohttp import web from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import intent, template, config_entry_flow + _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['webhook'] @@ -29,7 +25,7 @@ class DialogFlowError(HomeAssistantError): async def async_setup(hass, config): - """Set up Dialogflow component.""" + """Set up the Dialogflow component.""" return True @@ -45,9 +41,7 @@ async def handle_webhook(hass, webhook_id, request): except DialogFlowError as err: _LOGGER.warning(str(err)) - return web.json_response( - dialogflow_error_response(message, str(err)) - ) + return web.json_response(dialogflow_error_response(message, str(err))) except intent.UnknownIntent as err: _LOGGER.warning(str(err)) diff --git a/homeassistant/components/digital_ocean.py b/homeassistant/components/digital_ocean/__init__.py similarity index 93% rename from homeassistant/components/digital_ocean.py rename to homeassistant/components/digital_ocean/__init__.py index c0c9d95586c..d061dad6726 100644 --- a/homeassistant/components/digital_ocean.py +++ b/homeassistant/components/digital_ocean/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Digital Ocean. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/digital_ocean/ -""" +"""Support for Digital Ocean.""" import logging from datetime import timedelta diff --git a/homeassistant/components/binary_sensor/digital_ocean.py b/homeassistant/components/digital_ocean/binary_sensor.py similarity index 93% rename from homeassistant/components/binary_sensor/digital_ocean.py rename to homeassistant/components/digital_ocean/binary_sensor.py index 0f604c525e0..255f43b67ba 100644 --- a/homeassistant/components/binary_sensor/digital_ocean.py +++ b/homeassistant/components/digital_ocean/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for monitoring the state of Digital Ocean droplets. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.digital_ocean/ -""" +"""Support for monitoring the state of Digital Ocean droplets.""" import logging import voluptuous as vol diff --git a/homeassistant/components/switch/digital_ocean.py b/homeassistant/components/digital_ocean/switch.py similarity index 93% rename from homeassistant/components/switch/digital_ocean.py rename to homeassistant/components/digital_ocean/switch.py index c17df81abba..a10c961b8e4 100644 --- a/homeassistant/components/switch/digital_ocean.py +++ b/homeassistant/components/digital_ocean/switch.py @@ -1,9 +1,4 @@ -""" -Support for interacting with Digital Ocean droplets. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/switch.digital_ocean/ -""" +"""Support for interacting with Digital Ocean droplets.""" import logging import voluptuous as vol diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery/__init__.py similarity index 95% rename from homeassistant/components/discovery.py rename to homeassistant/components/discovery/__init__.py index d8198ba3033..85e3164d08b 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery/__init__.py @@ -26,28 +26,28 @@ REQUIREMENTS = ['netdisco==2.3.0'] DOMAIN = 'discovery' SCAN_INTERVAL = timedelta(seconds=300) -SERVICE_NETGEAR = 'netgear_router' -SERVICE_WEMO = 'belkin_wemo' -SERVICE_HASS_IOS_APP = 'hass_ios' -SERVICE_IKEA_TRADFRI = 'ikea_tradfri' -SERVICE_HASSIO = 'hassio' -SERVICE_AXIS = 'axis' SERVICE_APPLE_TV = 'apple_tv' -SERVICE_WINK = 'wink' -SERVICE_XIAOMI_GW = 'xiaomi_gw' -SERVICE_TELLDUSLIVE = 'tellstick' -SERVICE_HUE = 'philips_hue' -SERVICE_KONNECTED = 'konnected' -SERVICE_DECONZ = 'deconz' +SERVICE_AXIS = 'axis' SERVICE_DAIKIN = 'daikin' +SERVICE_DECONZ = 'deconz' +SERVICE_DLNA_DMR = 'dlna_dmr' +SERVICE_FREEBOX = 'freebox' +SERVICE_HASS_IOS_APP = 'hass_ios' +SERVICE_HASSIO = 'hassio' +SERVICE_HOMEKIT = 'homekit' +SERVICE_HUE = 'philips_hue' +SERVICE_IGD = 'igd' +SERVICE_IKEA_TRADFRI = 'ikea_tradfri' +SERVICE_KONNECTED = 'konnected' +SERVICE_NETGEAR = 'netgear_router' +SERVICE_OCTOPRINT = 'octoprint' +SERVICE_ROKU = 'roku' SERVICE_SABNZBD = 'sabnzbd' SERVICE_SAMSUNG_PRINTER = 'samsung_printer' -SERVICE_HOMEKIT = 'homekit' -SERVICE_OCTOPRINT = 'octoprint' -SERVICE_FREEBOX = 'freebox' -SERVICE_IGD = 'igd' -SERVICE_DLNA_DMR = 'dlna_dmr' -SERVICE_ROKU = 'roku' +SERVICE_TELLDUSLIVE = 'tellstick' +SERVICE_WEMO = 'belkin_wemo' +SERVICE_WINK = 'wink' +SERVICE_XIAOMI_GW = 'xiaomi_gw' CONFIG_ENTRY_HANDLERS = { SERVICE_DAIKIN: 'daikin', @@ -105,7 +105,7 @@ CONF_IGNORE = 'ignore' CONF_ENABLE = 'enable' CONFIG_SCHEMA = vol.Schema({ - vol.Required(DOMAIN): vol.Schema({ + vol.Optional(DOMAIN): vol.Schema({ vol.Optional(CONF_IGNORE, default=[]): vol.All(cv.ensure_list, [ vol.In(list(CONFIG_ENTRY_HANDLERS) + list(SERVICE_HANDLERS))]), @@ -126,11 +126,15 @@ async def async_setup(hass, config): # Disable zeroconf logging, it spams logging.getLogger('zeroconf').setLevel(logging.CRITICAL) - # Platforms ignore by config - ignored_platforms = config[DOMAIN][CONF_IGNORE] + if DOMAIN in config: + # Platforms ignore by config + ignored_platforms = config[DOMAIN][CONF_IGNORE] - # Optional platforms enabled by config - enabled_platforms = config[DOMAIN][CONF_ENABLE] + # Optional platforms enabled by config + enabled_platforms = config[DOMAIN][CONF_ENABLE] + else: + ignored_platforms = [] + enabled_platforms = [] async def new_service_found(service, info): """Handle a new service if one is found.""" diff --git a/homeassistant/components/dominos.py b/homeassistant/components/dominos/__init__.py similarity index 96% rename from homeassistant/components/dominos.py rename to homeassistant/components/dominos/__init__.py index 2c9f763aaa8..1c8966f3b4b 100644 --- a/homeassistant/components/dominos.py +++ b/homeassistant/components/dominos/__init__.py @@ -1,24 +1,18 @@ -""" -Support for Dominos Pizza ordering. - -The Dominos Pizza component creates a service which can be invoked to order -from their menu - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/dominos/. -""" -import logging +"""Support for Dominos Pizza ordering.""" from datetime import timedelta +import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components import http from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.util import Throttle +REQUIREMENTS = ['pizzapi==0.0.3'] + _LOGGER = logging.getLogger(__name__) # The domain of your component. Should be equal to the name of your component. @@ -40,8 +34,6 @@ ATTR_ORDER_CODES = 'codes' MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) MIN_TIME_BETWEEN_STORE_UPDATES = timedelta(minutes=3330) -REQUIREMENTS = ['pizzapi==0.0.3'] - DEPENDENCIES = ['http'] _ORDERS_SCHEMA = vol.Schema({ diff --git a/homeassistant/components/doorbird.py b/homeassistant/components/doorbird/__init__.py similarity index 97% rename from homeassistant/components/doorbird.py rename to homeassistant/components/doorbird/__init__.py index 28747bbe8be..d477836425d 100644 --- a/homeassistant/components/doorbird.py +++ b/homeassistant/components/doorbird/__init__.py @@ -1,19 +1,15 @@ -""" -Support for DoorBird device. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/doorbird/ -""" +"""Support for DoorBird devices.""" import logging - from urllib.error import HTTPError + import voluptuous as vol from homeassistant.components.http import HomeAssistantView -from homeassistant.const import CONF_HOST, CONF_USERNAME, \ - CONF_PASSWORD, CONF_NAME, CONF_DEVICES, CONF_MONITORED_CONDITIONS +from homeassistant.const import ( + CONF_DEVICES, CONF_HOST, CONF_MONITORED_CONDITIONS, CONF_NAME, + CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME) import homeassistant.helpers.config_validation as cv -from homeassistant.util import slugify, dt as dt_util +from homeassistant.util import dt as dt_util, slugify REQUIREMENTS = ['doorbirdpy==2.0.6'] @@ -28,7 +24,6 @@ CONF_DOORBELL_EVENTS = 'doorbell_events' CONF_DOORBELL_NUMS = 'doorbell_numbers' CONF_RELAY_NUMS = 'relay_numbers' CONF_MOTION_EVENTS = 'motion_events' -CONF_TOKEN = 'token' SENSOR_TYPES = { 'doorbell': { diff --git a/homeassistant/components/camera/doorbird.py b/homeassistant/components/doorbird/camera.py similarity index 93% rename from homeassistant/components/camera/doorbird.py rename to homeassistant/components/doorbird/camera.py index 8982e6d0847..e201837aaf6 100644 --- a/homeassistant/components/camera/doorbird.py +++ b/homeassistant/components/doorbird/camera.py @@ -1,9 +1,4 @@ -""" -Support for viewing the camera feed from a DoorBird video doorbell. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/camera.doorbird/ -""" +"""Support for viewing the camera feed from a DoorBird video doorbell.""" import asyncio import datetime import logging diff --git a/homeassistant/components/switch/doorbird.py b/homeassistant/components/doorbird/switch.py similarity index 100% rename from homeassistant/components/switch/doorbird.py rename to homeassistant/components/doorbird/switch.py diff --git a/homeassistant/components/dovado/__init__.py b/homeassistant/components/dovado/__init__.py index 7a50ac815b1..df2eed3011a 100644 --- a/homeassistant/components/dovado/__init__.py +++ b/homeassistant/components/dovado/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Dovado router. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/dovado/ -""" +"""Support for Dovado router.""" import logging from datetime import timedelta @@ -37,17 +32,14 @@ def setup(hass, config): hass.data[DOMAIN] = DovadoData( dovado.Dovado( - config[CONF_USERNAME], - config[CONF_PASSWORD], - config.get(CONF_HOST), - config.get(CONF_PORT) - ) + config[CONF_USERNAME], config[CONF_PASSWORD], + config.get(CONF_HOST), config.get(CONF_PORT)) ) return True class DovadoData: - """Maintains a connection to the router.""" + """Maintain a connection to the router.""" def __init__(self, client): """Set up a new Dovado connection.""" diff --git a/homeassistant/components/dovado/notify.py b/homeassistant/components/dovado/notify.py index 00036378a78..ea6ba2b455f 100644 --- a/homeassistant/components/dovado/notify.py +++ b/homeassistant/components/dovado/notify.py @@ -1,9 +1,4 @@ -""" -Support for SMS notifications from the Dovado router. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/notify.dovado/ -""" +"""Support for SMS notifications from the Dovado router.""" import logging from homeassistant.components.dovado import DOMAIN as DOVADO_DOMAIN diff --git a/homeassistant/components/dovado/sensor.py b/homeassistant/components/dovado/sensor.py index b89275b1795..eb0016ed298 100644 --- a/homeassistant/components/dovado/sensor.py +++ b/homeassistant/components/dovado/sensor.py @@ -1,9 +1,4 @@ -""" -Support for sensors from the Dovado router. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.dovado/ -""" +"""Support for sensors from the Dovado router.""" import logging import re from datetime import timedelta @@ -31,20 +26,16 @@ SENSOR_SMS_UNREAD = 'sms' SENSORS = { SENSOR_NETWORK: ('signal strength', 'Network', None, 'mdi:access-point-network'), - SENSOR_SIGNAL: ('signal strength', 'Signal Strength', '%', - 'mdi:signal'), + SENSOR_SIGNAL: ('signal strength', 'Signal Strength', '%', 'mdi:signal'), SENSOR_SMS_UNREAD: ('sms unread', 'SMS unread', '', 'mdi:message-text-outline'), - SENSOR_UPLOAD: ('traffic modem tx', 'Sent', 'GB', - 'mdi:cloud-upload'), + SENSOR_UPLOAD: ('traffic modem tx', 'Sent', 'GB', 'mdi:cloud-upload'), SENSOR_DOWNLOAD: ('traffic modem rx', 'Received', 'GB', 'mdi:cloud-download'), } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_SENSORS): vol.All( - cv.ensure_list, [vol.In(SENSORS)] - ), + vol.Required(CONF_SENSORS): vol.All(cv.ensure_list, [vol.In(SENSORS)]), }) @@ -69,6 +60,7 @@ class DovadoSensor(Entity): self._state = self._compute_state() def _compute_state(self): + """Compute the state of the sensor.""" state = self._data.state.get(SENSORS[self._sensor][0]) if self._sensor == SENSOR_NETWORK: match = re.search(r"\((.+)\)", state) diff --git a/homeassistant/components/downloader.py b/homeassistant/components/downloader/__init__.py similarity index 96% rename from homeassistant/components/downloader.py rename to homeassistant/components/downloader/__init__.py index 0d57740a83d..5af367ef92d 100644 --- a/homeassistant/components/downloader.py +++ b/homeassistant/components/downloader/__init__.py @@ -1,9 +1,4 @@ -""" -Support for functionality to download files. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/downloader/ -""" +"""Support for functionality to download files.""" import logging import os import re diff --git a/homeassistant/components/duckdns.py b/homeassistant/components/duckdns/__init__.py similarity index 93% rename from homeassistant/components/duckdns.py rename to homeassistant/components/duckdns/__init__.py index 3420bbed1bc..9899a0af98e 100644 --- a/homeassistant/components/duckdns.py +++ b/homeassistant/components/duckdns/__init__.py @@ -1,9 +1,4 @@ -""" -Integrate with DuckDNS. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/duckdns/ -""" +"""Integrate with DuckDNS.""" from datetime import timedelta import logging diff --git a/homeassistant/components/dweet.py b/homeassistant/components/dweet/__init__.py similarity index 90% rename from homeassistant/components/dweet.py rename to homeassistant/components/dweet/__init__.py index d5f94bb2c7b..f8e5b181163 100644 --- a/homeassistant/components/dweet.py +++ b/homeassistant/components/dweet/__init__.py @@ -1,9 +1,4 @@ -""" -A component which allows you to send data to Dweet.io. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/dweet/ -""" +"""Support for sending data to Dweet.io.""" import logging from datetime import timedelta diff --git a/homeassistant/components/sensor/dweet.py b/homeassistant/components/dweet/sensor.py similarity index 91% rename from homeassistant/components/sensor/dweet.py rename to homeassistant/components/dweet/sensor.py index 5f85164f35d..d1a64201e6d 100644 --- a/homeassistant/components/sensor/dweet.py +++ b/homeassistant/components/dweet/sensor.py @@ -1,9 +1,4 @@ -""" -Support for showing values from Dweet.io. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.dweet/ -""" +"""Support for showing values from Dweet.io.""" import json import logging from datetime import timedelta @@ -13,15 +8,13 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, CONF_VALUE_TEMPLATE, CONF_UNIT_OF_MEASUREMENT) + CONF_NAME, CONF_VALUE_TEMPLATE, CONF_UNIT_OF_MEASUREMENT, CONF_DEVICE) from homeassistant.helpers.entity import Entity REQUIREMENTS = ['dweepy==0.3.0'] _LOGGER = logging.getLogger(__name__) -CONF_DEVICE = 'device' - DEFAULT_NAME = 'Dweet.io Sensor' SCAN_INTERVAL = timedelta(minutes=1) @@ -49,11 +42,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): content = json.dumps(dweepy.get_latest_dweet_for(device)[0]['content']) except dweepy.DweepyError: _LOGGER.error("Device/thing %s could not be found", device) - return False + return if value_template.render_with_possible_json_value(content) == '': _LOGGER.error("%s was not found", value_template) - return False + return dweet = DweetData(device) diff --git a/homeassistant/components/dyson.py b/homeassistant/components/dyson/__init__.py similarity index 90% rename from homeassistant/components/dyson.py rename to homeassistant/components/dyson/__init__.py index 791f990d9ad..c2e56436bd8 100644 --- a/homeassistant/components/dyson.py +++ b/homeassistant/components/dyson/__init__.py @@ -1,29 +1,25 @@ -"""Parent component for Dyson Pure Cool Link devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/dyson/ -""" - +"""Support for Dyson Pure Cool Link devices.""" import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv +from homeassistant.const import ( + CONF_DEVICES, CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME) from homeassistant.helpers import discovery -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT, \ - CONF_DEVICES +import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['libpurecoollink==0.4.2'] _LOGGER = logging.getLogger(__name__) -CONF_LANGUAGE = "language" -CONF_RETRY = "retry" +CONF_LANGUAGE = 'language' +CONF_RETRY = 'retry' DEFAULT_TIMEOUT = 5 DEFAULT_RETRY = 10 +DYSON_DEVICES = 'dyson_devices' -DOMAIN = "dyson" +DOMAIN = 'dyson' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -37,8 +33,6 @@ CONFIG_SCHEMA = vol.Schema({ }) }, extra=vol.ALLOW_EXTRA) -DYSON_DEVICES = "dyson_devices" - def setup(hass, config): """Set up the Dyson parent component.""" diff --git a/homeassistant/components/ebusd/.translations/ca.json b/homeassistant/components/ebusd/.translations/ca.json new file mode 100644 index 00000000000..88b76539deb --- /dev/null +++ b/homeassistant/components/ebusd/.translations/ca.json @@ -0,0 +1,6 @@ +{ + "state": { + "day": "Dia", + "night": "Nit" + } +} \ No newline at end of file diff --git a/homeassistant/components/ebusd/.translations/da.json b/homeassistant/components/ebusd/.translations/da.json new file mode 100644 index 00000000000..00b499e2a39 --- /dev/null +++ b/homeassistant/components/ebusd/.translations/da.json @@ -0,0 +1,6 @@ +{ + "state": { + "day": "Dag", + "night": "Nat" + } +} \ No newline at end of file diff --git a/homeassistant/components/ebusd/.translations/de.json b/homeassistant/components/ebusd/.translations/de.json new file mode 100644 index 00000000000..347c6e6eeb5 --- /dev/null +++ b/homeassistant/components/ebusd/.translations/de.json @@ -0,0 +1,6 @@ +{ + "state": { + "day": "Tag", + "night": "Nacht" + } +} \ No newline at end of file diff --git a/homeassistant/components/ebusd/.translations/ebusd.en.json b/homeassistant/components/ebusd/.translations/ebusd.en.json new file mode 100644 index 00000000000..16ab79fc582 --- /dev/null +++ b/homeassistant/components/ebusd/.translations/ebusd.en.json @@ -0,0 +1,7 @@ +{ + "state": { + "day": "Day", + "night": "Night", + "auto": "Automatic" + } +} \ No newline at end of file diff --git a/homeassistant/components/ebusd/.translations/ebusd.it.json b/homeassistant/components/ebusd/.translations/ebusd.it.json new file mode 100644 index 00000000000..d0b95daaafa --- /dev/null +++ b/homeassistant/components/ebusd/.translations/ebusd.it.json @@ -0,0 +1,7 @@ +{ + "state": { + "day": "Giorno", + "night": "Notte", + "auto": "Automatico" + } +} \ No newline at end of file diff --git a/homeassistant/components/ebusd/.translations/en.json b/homeassistant/components/ebusd/.translations/en.json new file mode 100644 index 00000000000..abe5fff8fec --- /dev/null +++ b/homeassistant/components/ebusd/.translations/en.json @@ -0,0 +1,6 @@ +{ + "state": { + "day": "Day", + "night": "Night" + } +} \ No newline at end of file diff --git a/homeassistant/components/ebusd/.translations/it.json b/homeassistant/components/ebusd/.translations/it.json new file mode 100644 index 00000000000..dd70cfd2c6e --- /dev/null +++ b/homeassistant/components/ebusd/.translations/it.json @@ -0,0 +1,6 @@ +{ + "state": { + "day": "Giorno", + "night": "Notte" + } +} \ No newline at end of file diff --git a/homeassistant/components/ebusd/.translations/ko.json b/homeassistant/components/ebusd/.translations/ko.json new file mode 100644 index 00000000000..5a302af79e1 --- /dev/null +++ b/homeassistant/components/ebusd/.translations/ko.json @@ -0,0 +1,6 @@ +{ + "state": { + "day": "\uc8fc\uac04", + "night": "\uc57c\uac04" + } +} \ No newline at end of file diff --git a/homeassistant/components/ebusd/.translations/lb.json b/homeassistant/components/ebusd/.translations/lb.json new file mode 100644 index 00000000000..624744de470 --- /dev/null +++ b/homeassistant/components/ebusd/.translations/lb.json @@ -0,0 +1,6 @@ +{ + "state": { + "day": "Dag", + "night": "Nuecht" + } +} \ No newline at end of file diff --git a/homeassistant/components/ebusd/.translations/pl.json b/homeassistant/components/ebusd/.translations/pl.json new file mode 100644 index 00000000000..0c926a0335c --- /dev/null +++ b/homeassistant/components/ebusd/.translations/pl.json @@ -0,0 +1,6 @@ +{ + "state": { + "day": "Dzie\u0144", + "night": "Noc" + } +} \ No newline at end of file diff --git a/homeassistant/components/ebusd/.translations/ru.json b/homeassistant/components/ebusd/.translations/ru.json new file mode 100644 index 00000000000..7b013a4d7bc --- /dev/null +++ b/homeassistant/components/ebusd/.translations/ru.json @@ -0,0 +1,6 @@ +{ + "state": { + "day": "\u0414\u0435\u043d\u044c", + "night": "\u041d\u043e\u0447\u044c" + } +} \ No newline at end of file diff --git a/homeassistant/components/ebusd/.translations/zh-Hant.json b/homeassistant/components/ebusd/.translations/zh-Hant.json new file mode 100644 index 00000000000..1d2851acb6b --- /dev/null +++ b/homeassistant/components/ebusd/.translations/zh-Hant.json @@ -0,0 +1,6 @@ +{ + "state": { + "day": "\u767d\u5929", + "night": "\u591c\u665a" + } +} \ No newline at end of file diff --git a/homeassistant/components/ebusd/__init__.py b/homeassistant/components/ebusd/__init__.py new file mode 100644 index 00000000000..bc1b3aa9595 --- /dev/null +++ b/homeassistant/components/ebusd/__init__.py @@ -0,0 +1,112 @@ +"""Support for Ebusd daemon for communication with eBUS heating systems.""" +from datetime import timedelta +import logging +import socket + +import voluptuous as vol + +from homeassistant.const import ( + CONF_NAME, CONF_HOST, CONF_PORT, CONF_MONITORED_CONDITIONS) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform +from homeassistant.util import Throttle + +from .const import (DOMAIN, SENSOR_TYPES) + +REQUIREMENTS = ['ebusdpy==0.0.16'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'ebusd' +DEFAULT_PORT = 8888 +CONF_CIRCUIT = 'circuit' +CACHE_TTL = 900 +SERVICE_EBUSD_WRITE = 'ebusd_write' + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=15) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_CIRCUIT): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_MONITORED_CONDITIONS, default=[]): vol.All( + cv.ensure_list, [vol.In(SENSOR_TYPES['700'])]) + }) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the eBusd component.""" + conf = config[DOMAIN] + name = conf[CONF_NAME] + circuit = conf[CONF_CIRCUIT] + monitored_conditions = conf.get(CONF_MONITORED_CONDITIONS) + server_address = ( + conf.get(CONF_HOST), conf.get(CONF_PORT)) + + try: + _LOGGER.debug("Ebusd component setup started") + import ebusdpy + ebusdpy.init(server_address) + hass.data[DOMAIN] = EbusdData(server_address, circuit) + + sensor_config = { + CONF_MONITORED_CONDITIONS: monitored_conditions, + 'client_name': name, + 'sensor_types': SENSOR_TYPES[circuit] + } + load_platform(hass, 'sensor', DOMAIN, sensor_config, config) + + hass.services.register( + DOMAIN, SERVICE_EBUSD_WRITE, hass.data[DOMAIN].write) + + _LOGGER.debug("Ebusd component setup completed") + return True + except (socket.timeout, socket.error): + return False + + +class EbusdData: + """Get the latest data from Ebusd.""" + + def __init__(self, address, circuit): + """Initialize the data object.""" + self._circuit = circuit + self._address = address + self.value = {} + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self, name, stype): + """Call the Ebusd API to update the data.""" + import ebusdpy + + try: + _LOGGER.debug("Opening socket to ebusd %s", name) + command_result = ebusdpy.read( + self._address, self._circuit, name, stype, CACHE_TTL) + if command_result is not None: + if 'ERR:' in command_result: + _LOGGER.warning(command_result) + else: + self.value[name] = command_result + except RuntimeError as err: + _LOGGER.error(err) + raise RuntimeError(err) + + def write(self, call): + """Call write methon on ebusd.""" + import ebusdpy + name = call.data.get('name') + value = call.data.get('value') + + try: + _LOGGER.debug("Opening socket to ebusd %s", name) + command_result = ebusdpy.write( + self._address, self._circuit, name, value) + if command_result is not None: + if 'done' not in command_result: + _LOGGER.warning('Write command failed: %s', name) + except RuntimeError as err: + _LOGGER.error(err) diff --git a/homeassistant/components/ebusd/const.py b/homeassistant/components/ebusd/const.py new file mode 100644 index 00000000000..c36981c5278 --- /dev/null +++ b/homeassistant/components/ebusd/const.py @@ -0,0 +1,100 @@ +"""Constants for ebus component.""" +DOMAIN = 'ebusd' + +# SensorTypes: +# 0='decimal', 1='time-schedule', 2='switch', 3='string', 4='value;status' + +SENSOR_TYPES = { + '700': { + 'ActualFlowTemperatureDesired': + ['Hc1ActualFlowTempDesired', '°C', 'mdi:thermometer', 0], + 'MaxFlowTemperatureDesired': + ['Hc1MaxFlowTempDesired', '°C', 'mdi:thermometer', 0], + 'MinFlowTemperatureDesired': + ['Hc1MinFlowTempDesired', '°C', 'mdi:thermometer', 0], + 'PumpStatus': + ['Hc1PumpStatus', None, 'mdi:toggle-switch', 2], + 'HCSummerTemperatureLimit': + ['Hc1SummerTempLimit', '°C', 'mdi:weather-sunny', 0], + 'HolidayTemperature': + ['HolidayTemp', '°C', 'mdi:thermometer', 0], + 'HWTemperatureDesired': + ['HwcTempDesired', '°C', 'mdi:thermometer', 0], + 'HWTimerMonday': + ['hwcTimer.Monday', None, 'mdi:timer', 1], + 'HWTimerTuesday': + ['hwcTimer.Tuesday', None, 'mdi:timer', 1], + 'HWTimerWednesday': + ['hwcTimer.Wednesday', None, 'mdi:timer', 1], + 'HWTimerThursday': + ['hwcTimer.Thursday', None, 'mdi:timer', 1], + 'HWTimerFriday': + ['hwcTimer.Friday', None, 'mdi:timer', 1], + 'HWTimerSaturday': + ['hwcTimer.Saturday', None, 'mdi:timer', 1], + 'HWTimerSunday': + ['hwcTimer.Sunday', None, 'mdi:timer', 1], + 'WaterPressure': + ['WaterPressure', 'bar', 'mdi:water-pump', 0], + 'Zone1RoomZoneMapping': + ['z1RoomZoneMapping', None, 'mdi:label', 0], + 'Zone1NightTemperature': + ['z1NightTemp', '°C', 'mdi:weather-night', 0], + 'Zone1DayTemperature': + ['z1DayTemp', '°C', 'mdi:weather-sunny', 0], + 'Zone1HolidayTemperature': + ['z1HolidayTemp', '°C', 'mdi:thermometer', 0], + 'Zone1RoomTemperature': + ['z1RoomTemp', '°C', 'mdi:thermometer', 0], + 'Zone1ActualRoomTemperatureDesired': + ['z1ActualRoomTempDesired', '°C', 'mdi:thermometer', 0], + 'Zone1TimerMonday': + ['z1Timer.Monday', None, 'mdi:timer', 1], + 'Zone1TimerTuesday': + ['z1Timer.Tuesday', None, 'mdi:timer', 1], + 'Zone1TimerWednesday': + ['z1Timer.Wednesday', None, 'mdi:timer', 1], + 'Zone1TimerThursday': + ['z1Timer.Thursday', None, 'mdi:timer', 1], + 'Zone1TimerFriday': + ['z1Timer.Friday', None, 'mdi:timer', 1], + 'Zone1TimerSaturday': + ['z1Timer.Saturday', None, 'mdi:timer', 1], + 'Zone1TimerSunday': + ['z1Timer.Sunday', None, 'mdi:timer', 1], + 'Zone1OperativeMode': + ['z1OpMode', None, 'mdi:math-compass', 3], + 'ContinuosHeating': + ['ContinuosHeating', '°C', 'mdi:weather-snowy', 0], + 'PowerEnergyConsumptionLastMonth': + ['PrEnergySumHcLastMonth', 'kWh', 'mdi:flash', 0], + 'PowerEnergyConsumptionThisMonth': + ['PrEnergySumHcThisMonth', 'kWh', 'mdi:flash', 0] + }, + 'ehp': { + 'HWTemperature': + ['HwcTemp', '°C', 'mdi:thermometer', 4], + 'OutsideTemp': + ['OutsideTemp', '°C', 'mdi:thermometer', 4] + }, + 'bai': { + 'ReturnTemperature': + ['ReturnTemp', '°C', 'mdi:thermometer', 4], + 'CentralHeatingPump': + ['WP', None, 'mdi:toggle-switch', 2], + 'HeatingSwitch': + ['HeatingSwitch', None, 'mdi:toggle-switch', 2], + 'FlowTemperature': + ['FlowTemp', '°C', 'mdi:thermometer', 4], + 'Flame': + ['Flame', None, 'mdi:toggle-switch', 2], + 'PowerEnergyConsumptionHeatingCircuit': + ['PrEnergySumHc1', 'kWh', 'mdi:flash', 0], + 'PowerEnergyConsumptionHotWaterCircuit': + ['PrEnergySumHwc1', 'kWh', 'mdi:flash', 0], + 'RoomThermostat': + ['DCRoomthermostat', None, 'mdi:toggle-switch', 2], + 'HeatingPartLoad': + ['PartloadHcKW', 'kWh', 'mdi:flash', 0] + } +} diff --git a/homeassistant/components/ebusd/sensor.py b/homeassistant/components/ebusd/sensor.py new file mode 100644 index 00000000000..942ba107509 --- /dev/null +++ b/homeassistant/components/ebusd/sensor.py @@ -0,0 +1,99 @@ +"""Support for Ebusd sensors.""" +import logging +import datetime + +from homeassistant.helpers.entity import Entity + +from .const import DOMAIN + +DEPENDENCIES = ['ebusd'] + +TIME_FRAME1_BEGIN = 'time_frame1_begin' +TIME_FRAME1_END = 'time_frame1_end' +TIME_FRAME2_BEGIN = 'time_frame2_begin' +TIME_FRAME2_END = 'time_frame2_end' +TIME_FRAME3_BEGIN = 'time_frame3_begin' +TIME_FRAME3_END = 'time_frame3_end' + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Ebus sensor.""" + ebusd_api = hass.data[DOMAIN] + monitored_conditions = discovery_info['monitored_conditions'] + name = discovery_info['client_name'] + + dev = [] + for condition in monitored_conditions: + dev.append(EbusdSensor( + ebusd_api, discovery_info['sensor_types'][condition], name)) + + add_entities(dev, True) + + +class EbusdSensor(Entity): + """Ebusd component sensor methods definition.""" + + def __init__(self, data, sensor, name): + """Initialize the sensor.""" + self._state = None + self._client_name = name + self._name, self._unit_of_measurement, self._icon, self._type = sensor + self.data = data + + @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 device_state_attributes(self): + """Return the device state attributes.""" + if self._type == 1 and self._state is not None: + schedule = { + TIME_FRAME1_BEGIN: None, + TIME_FRAME1_END: None, + TIME_FRAME2_BEGIN: None, + TIME_FRAME2_END: None, + TIME_FRAME3_BEGIN: None, + TIME_FRAME3_END: None + } + time_frame = self._state.split(';') + for index, item in enumerate(sorted(schedule.items())): + if index < len(time_frame): + parsed = datetime.datetime.strptime( + time_frame[index], '%H:%M') + parsed = parsed.replace( + datetime.datetime.now().year, + datetime.datetime.now().month, + datetime.datetime.now().day) + schedule[item[0]] = parsed.isoformat() + return schedule + return None + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self._icon + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit_of_measurement + + def update(self): + """Fetch new state data for the sensor.""" + try: + self.data.update(self._name, self._type) + if self._name not in self.data.value: + return + + self._state = self.data.value[self._name] + except RuntimeError: + _LOGGER.debug("EbusdData.update exception") diff --git a/homeassistant/components/ebusd/services.yaml b/homeassistant/components/ebusd/services.yaml new file mode 100644 index 00000000000..0f64533f7f1 --- /dev/null +++ b/homeassistant/components/ebusd/services.yaml @@ -0,0 +1,6 @@ +write: + description: Call ebusd write command. + fields: + call: + description: Property name and value to set + example: '{"name": "Hc1MaxFlowTempDesired", "value": 21}' \ No newline at end of file diff --git a/homeassistant/components/ebusd/strings.json b/homeassistant/components/ebusd/strings.json new file mode 100644 index 00000000000..ee62df8ddad --- /dev/null +++ b/homeassistant/components/ebusd/strings.json @@ -0,0 +1,6 @@ +{ + "state": { + "day": "Day", + "night": "Night" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecoal_boiler.py b/homeassistant/components/ecoal_boiler/__init__.py similarity index 88% rename from homeassistant/components/ecoal_boiler.py rename to homeassistant/components/ecoal_boiler/__init__.py index bd08024e64a..6ab9fc3181c 100644 --- a/homeassistant/components/ecoal_boiler.py +++ b/homeassistant/components/ecoal_boiler/__init__.py @@ -1,9 +1,4 @@ -""" -Component to control ecoal/esterownik.pl coal/wood boiler controller. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/ecoal_boiler/ -""" +"""Support to control ecoal/esterownik.pl coal/wood boiler controller.""" import logging import voluptuous as vol @@ -61,12 +56,10 @@ SENSOR_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_USERNAME, - default=DEFAULT_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD, - default=DEFAULT_PASSWORD): cv.string, - vol.Optional(CONF_SWITCHES, default={}): SWITCH_SCHEMA, + vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, + vol.Optional(CONF_SWITCHES, default={}): SWITCH_SCHEMA, + vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, }) }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/sensor/ecoal_boiler.py b/homeassistant/components/ecoal_boiler/sensor.py similarity index 89% rename from homeassistant/components/sensor/ecoal_boiler.py rename to homeassistant/components/ecoal_boiler/sensor.py index de81d16470c..47ed2d6ebdf 100644 --- a/homeassistant/components/sensor/ecoal_boiler.py +++ b/homeassistant/components/ecoal_boiler/sensor.py @@ -1,9 +1,4 @@ -""" -Allows reading temperatures from ecoal/esterownik.pl controller. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.ecoal_boiler/ -""" +"""Allows reading temperatures from ecoal/esterownik.pl controller.""" import logging from homeassistant.components.ecoal_boiler import ( diff --git a/homeassistant/components/switch/ecoal_boiler.py b/homeassistant/components/ecoal_boiler/switch.py similarity index 92% rename from homeassistant/components/switch/ecoal_boiler.py rename to homeassistant/components/ecoal_boiler/switch.py index d8d6c98bb8b..f113125194a 100644 --- a/homeassistant/components/switch/ecoal_boiler.py +++ b/homeassistant/components/ecoal_boiler/switch.py @@ -1,9 +1,4 @@ -""" -Allows to configuration ecoal (esterownik.pl) pumps as switches. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.ecoal_boiler/ -""" +"""Allows to configuration ecoal (esterownik.pl) pumps as switches.""" import logging from typing import Optional diff --git a/homeassistant/components/ecobee.py b/homeassistant/components/ecobee/__init__.py similarity index 94% rename from homeassistant/components/ecobee.py rename to homeassistant/components/ecobee/__init__.py index 3829c2caebd..167132a5f41 100644 --- a/homeassistant/components/ecobee.py +++ b/homeassistant/components/ecobee/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Ecobee. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/ecobee/ -""" +"""Support for Ecobee devices.""" import logging import os from datetime import timedelta @@ -34,7 +29,7 @@ NETWORK = None CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Optional(CONF_API_KEY): cv.string, - vol.Optional(CONF_HOLD_TEMP, default=False): cv.boolean + vol.Optional(CONF_HOLD_TEMP, default=False): cv.boolean, }) }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/binary_sensor/ecobee.py b/homeassistant/components/ecobee/binary_sensor.py similarity index 89% rename from homeassistant/components/binary_sensor/ecobee.py rename to homeassistant/components/ecobee/binary_sensor.py index 37f25476bd0..ca8e551bf5e 100644 --- a/homeassistant/components/binary_sensor/ecobee.py +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for Ecobee sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.ecobee/ -""" +"""Support for Ecobee binary sensors.""" from homeassistant.components import ecobee from homeassistant.components.binary_sensor import BinarySensorDevice @@ -33,7 +28,7 @@ class EcobeeBinarySensor(BinarySensorDevice): """Representation of an Ecobee sensor.""" def __init__(self, sensor_name, sensor_index): - """Initialize the sensor.""" + """Initialize the Ecobee sensor.""" self._name = sensor_name + ' Occupancy' self.sensor_name = sensor_name self.index = sensor_index diff --git a/homeassistant/components/climate/ecobee.py b/homeassistant/components/ecobee/climate.py similarity index 98% rename from homeassistant/components/climate/ecobee.py rename to homeassistant/components/ecobee/climate.py index 46fc5c29752..aa6440894e1 100644 --- a/homeassistant/components/climate/ecobee.py +++ b/homeassistant/components/ecobee/climate.py @@ -1,9 +1,4 @@ -""" -Platform for Ecobee Thermostats. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/climate.ecobee/ -""" +"""Support for Ecobee Thermostats.""" import logging import voluptuous as vol diff --git a/homeassistant/components/notify/ecobee.py b/homeassistant/components/ecobee/notify.py similarity index 85% rename from homeassistant/components/notify/ecobee.py rename to homeassistant/components/ecobee/notify.py index 31e4c4751c8..9824d20b85e 100644 --- a/homeassistant/components/notify/ecobee.py +++ b/homeassistant/components/ecobee/notify.py @@ -1,9 +1,4 @@ -""" -Support for ecobee Send Message service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/notify.ecobee/ -""" +"""Support for Ecobee Send Message service.""" import logging import voluptuous as vol diff --git a/homeassistant/components/sensor/ecobee.py b/homeassistant/components/ecobee/sensor.py similarity index 94% rename from homeassistant/components/sensor/ecobee.py rename to homeassistant/components/ecobee/sensor.py index ae22401a618..1f9fd5cbde8 100644 --- a/homeassistant/components/sensor/ecobee.py +++ b/homeassistant/components/ecobee/sensor.py @@ -1,9 +1,4 @@ -""" -Support for Ecobee sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.ecobee/ -""" +"""Support for Ecobee sensors.""" from homeassistant.components import ecobee from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, TEMP_FAHRENHEIT) diff --git a/homeassistant/components/weather/ecobee.py b/homeassistant/components/ecobee/weather.py similarity index 95% rename from homeassistant/components/weather/ecobee.py rename to homeassistant/components/ecobee/weather.py index 7382e5c1815..2ba5f362b7d 100644 --- a/homeassistant/components/weather/ecobee.py +++ b/homeassistant/components/ecobee/weather.py @@ -1,9 +1,4 @@ -""" -Support for displaying weather info from Ecobee API. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/weather.ecobee/ -""" +"""Support for displaying weather info from Ecobee API.""" from datetime import datetime from homeassistant.components import ecobee @@ -23,7 +18,7 @@ MISSING_DATA = -5002 def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Ecobee weather component.""" + """Set up the Ecobee weather platform.""" if discovery_info is None: return dev = list() diff --git a/homeassistant/components/ecovacs.py b/homeassistant/components/ecovacs/__init__.py similarity index 90% rename from homeassistant/components/ecovacs.py rename to homeassistant/components/ecovacs/__init__.py index 8cbe95ee685..124cae3ca47 100644 --- a/homeassistant/components/ecovacs.py +++ b/homeassistant/components/ecovacs/__init__.py @@ -1,19 +1,14 @@ -"""Parent component for Ecovacs Deebot vacuums. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/ecovacs/ -""" - +"""Support for Ecovacs Deebot vacuums.""" import logging import random import string import voluptuous as vol -import homeassistant.helpers.config_validation as cv +from homeassistant.const import ( + CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers import discovery -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, \ - EVENT_HOMEASSISTANT_STOP +import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['sucks==0.9.3'] diff --git a/homeassistant/components/vacuum/ecovacs.py b/homeassistant/components/ecovacs/vacuum.py similarity index 97% rename from homeassistant/components/vacuum/ecovacs.py rename to homeassistant/components/ecovacs/vacuum.py index 7f05554c496..9d2af730315 100644 --- a/homeassistant/components/vacuum/ecovacs.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -1,9 +1,4 @@ -""" -Support for Ecovacs Ecovacs Vaccums. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/vacuum.ecovacs/ -""" +"""Support for Ecovacs Ecovacs Vaccums.""" import logging from homeassistant.components.vacuum import ( diff --git a/homeassistant/components/edp_redy.py b/homeassistant/components/edp_redy/__init__.py similarity index 91% rename from homeassistant/components/edp_redy.py rename to homeassistant/components/edp_redy/__init__.py index 10780103613..9b8bfaa437a 100644 --- a/homeassistant/components/edp_redy.py +++ b/homeassistant/components/edp_redy/__init__.py @@ -1,19 +1,13 @@ -""" -Support for EDP re:dy. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/edp_redy/ -""" - -import logging +"""Support for EDP re:dy.""" from datetime import timedelta +import logging import voluptuous as vol -from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, - EVENT_HOMEASSISTANT_START) +from homeassistant.const import ( + CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_START) from homeassistant.core import callback -from homeassistant.helpers import discovery, dispatcher, aiohttp_client +from homeassistant.helpers import aiohttp_client, discovery, dispatcher import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_time diff --git a/homeassistant/components/sensor/edp_redy.py b/homeassistant/components/edp_redy/sensor.py similarity index 96% rename from homeassistant/components/sensor/edp_redy.py rename to homeassistant/components/edp_redy/sensor.py index 0f259ec673a..926a073832c 100644 --- a/homeassistant/components/sensor/edp_redy.py +++ b/homeassistant/components/edp_redy/sensor.py @@ -13,8 +13,8 @@ DEPENDENCIES = ['edp_redy'] ATTR_ACTIVE_POWER = 'active_power' -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Perform the setup for re:dy devices.""" from edp_redy.session import ACTIVE_POWER_ID diff --git a/homeassistant/components/switch/edp_redy.py b/homeassistant/components/edp_redy/switch.py similarity index 96% rename from homeassistant/components/switch/edp_redy.py rename to homeassistant/components/edp_redy/switch.py index 1576361da33..ad4ce8fe728 100644 --- a/homeassistant/components/switch/edp_redy.py +++ b/homeassistant/components/edp_redy/switch.py @@ -12,8 +12,8 @@ DEPENDENCIES = ['edp_redy'] ATTR_ACTIVE_POWER = 'active_power' -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Perform the setup for re:dy devices.""" session = hass.data[EDP_REDY] devices = [] diff --git a/homeassistant/components/egardia.py b/homeassistant/components/egardia/__init__.py similarity index 88% rename from homeassistant/components/egardia.py rename to homeassistant/components/egardia/__init__.py index 3547f4fc76e..fe613824c95 100644 --- a/homeassistant/components/egardia.py +++ b/homeassistant/components/egardia/__init__.py @@ -1,28 +1,24 @@ -""" -Interfaces with Egardia/Woonveilig alarm control panel. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/egardia/ -""" +"""Interfaces with Egardia/Woonveilig alarm control panel.""" import logging import requests import voluptuous as vol +from homeassistant.const import ( + CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_USERNAME, + EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.const import ( - CONF_PORT, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_NAME, - EVENT_HOMEASSISTANT_STOP) REQUIREMENTS = ['pythonegardia==1.0.39'] _LOGGER = logging.getLogger(__name__) +ATTR_DISCOVER_DEVICES = 'egardia_sensor' + CONF_REPORT_SERVER_CODES = 'report_server_codes' CONF_REPORT_SERVER_ENABLED = 'report_server_enabled' CONF_REPORT_SERVER_PORT = 'report_server_port' -REPORT_SERVER_CODES_IGNORE = 'ignore' CONF_VERSION = 'version' DEFAULT_NAME = 'Egardia' @@ -31,21 +27,24 @@ DEFAULT_REPORT_SERVER_ENABLED = False DEFAULT_REPORT_SERVER_PORT = 52010 DEFAULT_VERSION = 'GATE-01' DOMAIN = 'egardia' -EGARDIA_SERVER = 'egardia_server' + EGARDIA_DEVICE = 'egardiadevice' EGARDIA_NAME = 'egardianame' -EGARDIA_REPORT_SERVER_ENABLED = 'egardia_rs_enabled' EGARDIA_REPORT_SERVER_CODES = 'egardia_rs_codes' +EGARDIA_REPORT_SERVER_ENABLED = 'egardia_rs_enabled' +EGARDIA_SERVER = 'egardia_server' + NOTIFICATION_ID = 'egardia_notification' NOTIFICATION_TITLE = 'Egardia' -ATTR_DISCOVER_DEVICES = 'egardia_sensor' + +REPORT_SERVER_CODES_IGNORE = 'ignore' SERVER_CODE_SCHEMA = vol.Schema({ vol.Optional('arm'): vol.All(cv.ensure_list_csv, [cv.string]), vol.Optional('disarm'): vol.All(cv.ensure_list_csv, [cv.string]), vol.Optional('armhome'): vol.All(cv.ensure_list_csv, [cv.string]), vol.Optional('triggered'): vol.All(cv.ensure_list_csv, [cv.string]), - vol.Optional('ignore'): vol.All(cv.ensure_list_csv, [cv.string]) + vol.Optional('ignore'): vol.All(cv.ensure_list_csv, [cv.string]), }) CONFIG_SCHEMA = vol.Schema({ @@ -82,10 +81,10 @@ def setup(hass, config): host, port, username, password, '', version) except requests.exceptions.RequestException: _LOGGER.error("An error occurred accessing your Egardia device. " - "Please check config.") + "Please check configuration") return False except egardiadevice.UnauthorizedError: - _LOGGER.error("Unable to authorize. Wrong password or username.") + _LOGGER.error("Unable to authorize. Wrong password or username") return False # Set up the egardia server if enabled if rs_enabled: @@ -101,21 +100,21 @@ def setup(hass, config): server.start() def handle_stop_event(event): - """Handle HA stop event.""" + """Handle Home Assistant stop event.""" server.stop() # listen to home assistant stop event hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, handle_stop_event) except IOError: - _LOGGER.error("Binding error occurred while starting " - "EgardiaServer.") + _LOGGER.error( + "Binding error occurred while starting EgardiaServer") return False discovery.load_platform(hass, 'alarm_control_panel', DOMAIN, discovered=conf, hass_config=config) - # get the sensors from the device and add those + # Get the sensors from the device and add those sensors = device.getsensors() discovery.load_platform(hass, 'binary_sensor', DOMAIN, {ATTR_DISCOVER_DEVICES: sensors}, config) diff --git a/homeassistant/components/alarm_control_panel/egardia.py b/homeassistant/components/egardia/alarm_control_panel.py similarity index 88% rename from homeassistant/components/alarm_control_panel/egardia.py rename to homeassistant/components/egardia/alarm_control_panel.py index dfd60c4abde..e202a46f9f1 100644 --- a/homeassistant/components/alarm_control_panel/egardia.py +++ b/homeassistant/components/egardia/alarm_control_panel.py @@ -1,23 +1,17 @@ -""" -Interfaces with Egardia/Woonveilig alarm control panel. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/alarm_control_panel.egardia/ -""" +"""Interfaces with Egardia/Woonveilig alarm control panel.""" import logging import requests import homeassistant.components.alarm_control_panel as alarm -from homeassistant.const import ( - STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED, - STATE_ALARM_ARMED_NIGHT) from homeassistant.components.egardia import ( - EGARDIA_DEVICE, EGARDIA_SERVER, - REPORT_SERVER_CODES_IGNORE, CONF_REPORT_SERVER_CODES, - CONF_REPORT_SERVER_ENABLED, CONF_REPORT_SERVER_PORT - ) + CONF_REPORT_SERVER_CODES, CONF_REPORT_SERVER_ENABLED, + CONF_REPORT_SERVER_PORT, EGARDIA_DEVICE, EGARDIA_SERVER, + REPORT_SERVER_CODES_IGNORE) +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED) + DEPENDENCIES = ['egardia'] _LOGGER = logging.getLogger(__name__) @@ -34,7 +28,7 @@ STATES = { def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Egardia platform.""" + """Set up the Egardia Alarm Control Panael platform.""" if discovery_info is None: return device = EgardiaAlarm( @@ -43,7 +37,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): discovery_info[CONF_REPORT_SERVER_ENABLED], discovery_info.get(CONF_REPORT_SERVER_CODES), discovery_info[CONF_REPORT_SERVER_PORT]) - # add egardia alarm device + add_entities([device], True) diff --git a/homeassistant/components/binary_sensor/egardia.py b/homeassistant/components/egardia/binary_sensor.py similarity index 80% rename from homeassistant/components/binary_sensor/egardia.py rename to homeassistant/components/egardia/binary_sensor.py index 56d7dda17ba..74a048a86c0 100644 --- a/homeassistant/components/binary_sensor/egardia.py +++ b/homeassistant/components/egardia/binary_sensor.py @@ -1,20 +1,20 @@ -""" -Interfaces with Egardia/Woonveilig alarm control panel. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.egardia/ -""" +"""Interfaces with Egardia/Woonveilig alarm control panel.""" import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.const import STATE_ON, STATE_OFF from homeassistant.components.egardia import ( - EGARDIA_DEVICE, ATTR_DISCOVER_DEVICES) + ATTR_DISCOVER_DEVICES, EGARDIA_DEVICE) +from homeassistant.const import STATE_OFF, STATE_ON + _LOGGER = logging.getLogger(__name__) + DEPENDENCIES = ['egardia'] -EGARDIA_TYPE_TO_DEVICE_CLASS = {'IR Sensor': 'motion', - 'Door Contact': 'opening', - 'IR': 'motion'} + +EGARDIA_TYPE_TO_DEVICE_CLASS = { + 'IR Sensor': 'motion', + 'Door Contact': 'opening', + 'IR': 'motion', +} async def async_setup_platform(hass, config, async_add_entities, @@ -25,7 +25,7 @@ async def async_setup_platform(hass, config, async_add_entities, return disc_info = discovery_info[ATTR_DISCOVER_DEVICES] - # multiple devices here! + async_add_entities( ( EgardiaBinarySensor( diff --git a/homeassistant/components/eight_sleep.py b/homeassistant/components/eight_sleep/__init__.py similarity index 96% rename from homeassistant/components/eight_sleep.py rename to homeassistant/components/eight_sleep/__init__.py index 8c36830817e..ca6c8a5a5c6 100644 --- a/homeassistant/components/eight_sleep.py +++ b/homeassistant/components/eight_sleep/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Eight smart mattress covers and mattresses. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/eight_sleep/ -""" +"""Support for Eight smart mattress covers and mattresses.""" import logging from datetime import timedelta @@ -21,7 +16,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow -REQUIREMENTS = ['pyeight==0.1.0'] +REQUIREMENTS = ['pyeight==0.1.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/binary_sensor/eight_sleep.py b/homeassistant/components/eight_sleep/binary_sensor.py similarity index 90% rename from homeassistant/components/binary_sensor/eight_sleep.py rename to homeassistant/components/eight_sleep/binary_sensor.py index 34d3a7a13ca..2a9cb19a327 100644 --- a/homeassistant/components/binary_sensor/eight_sleep.py +++ b/homeassistant/components/eight_sleep/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for Eight Sleep binary sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.eight_sleep/ -""" +"""Support for Eight Sleep binary sensors.""" import logging from homeassistant.components.binary_sensor import BinarySensorDevice diff --git a/homeassistant/components/sensor/eight_sleep.py b/homeassistant/components/eight_sleep/sensor.py similarity index 98% rename from homeassistant/components/sensor/eight_sleep.py rename to homeassistant/components/eight_sleep/sensor.py index 0fc793d31ca..2bb03c8d4f2 100644 --- a/homeassistant/components/sensor/eight_sleep.py +++ b/homeassistant/components/eight_sleep/sensor.py @@ -1,9 +1,4 @@ -""" -Support for Eight Sleep sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.eight_sleep/ -""" +"""Support for Eight Sleep sensors.""" import logging from homeassistant.components.eight_sleep import ( diff --git a/homeassistant/components/eight_sleep/services.yaml b/homeassistant/components/eight_sleep/services.yaml new file mode 100644 index 00000000000..db7690730dd --- /dev/null +++ b/homeassistant/components/eight_sleep/services.yaml @@ -0,0 +1,6 @@ +heat_set: + description: Set heating level for eight sleep. + fields: + duration: {description: Duration to heat at the target level in seconds., example: 3600} + entity_id: {description: Entity id of the bed state to adjust., example: sensor.eight_left_bed_state} + target: {description: Target heating level from 0-100., example: 35} diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 8ac3cec6411..a0c08bf5429 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -1,10 +1,4 @@ -""" -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/ -""" - +"""Support the ElkM1 Gold and ElkM1 EZ8 alarm/integration panels.""" import logging import re @@ -18,20 +12,20 @@ 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.13'] +DOMAIN = 'elkm1' + CONF_AREA = 'area' CONF_COUNTER = 'counter' +CONF_ENABLED = 'enabled' CONF_KEYPAD = 'keypad' CONF_OUTPUT = 'output' +CONF_PLC = 'plc' CONF_SETTING = 'setting' CONF_TASK = 'task' CONF_THERMOSTAT = 'thermostat' -CONF_PLC = 'plc' CONF_ZONE = 'zone' -CONF_ENABLED = 'enabled' _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/alarm_control_panel/elkm1.py b/homeassistant/components/elkm1/alarm_control_panel.py similarity index 97% rename from homeassistant/components/alarm_control_panel/elkm1.py rename to homeassistant/components/elkm1/alarm_control_panel.py index c6405f953fd..63b38c1d321 100644 --- a/homeassistant/components/alarm_control_panel/elkm1.py +++ b/homeassistant/components/elkm1/alarm_control_panel.py @@ -1,10 +1,4 @@ -""" -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/ -""" - +"""Each ElkM1 area will be created as a separate alarm_control_panel.""" import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm from homeassistant.const import ( diff --git a/homeassistant/components/climate/elkm1.py b/homeassistant/components/elkm1/climate.py similarity index 97% rename from homeassistant/components/climate/elkm1.py rename to homeassistant/components/elkm1/climate.py index 6bd33b382dc..467d542ee6d 100644 --- a/homeassistant/components/climate/elkm1.py +++ b/homeassistant/components/elkm1/climate.py @@ -1,9 +1,4 @@ -""" -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/ -""" +"""Support for control of Elk-M1 connected thermostats.""" 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, diff --git a/homeassistant/components/light/elkm1.py b/homeassistant/components/elkm1/light.py similarity index 79% rename from homeassistant/components/light/elkm1.py rename to homeassistant/components/elkm1/light.py index 707aedbb161..3a282595d58 100644 --- a/homeassistant/components/light/elkm1.py +++ b/homeassistant/components/elkm1/light.py @@ -1,10 +1,4 @@ -""" -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/ -""" - +"""Support for control of ElkM1 lighting (X10, UPB, etc).""" from homeassistant.components.light import ( ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) from homeassistant.components.elkm1 import ( @@ -13,8 +7,8 @@ from homeassistant.components.elkm1 import ( DEPENDENCIES = [ELK_DOMAIN] -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +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 @@ -24,10 +18,10 @@ async def async_setup_platform(hass, config, async_add_entities, class ElkLight(ElkEntity, Light): - """Elk lighting device.""" + """Representation of an Elk lighting device.""" def __init__(self, element, elk, elk_data): - """Initialize light.""" + """Initialize the Elk light.""" super().__init__(element, elk, elk_data) self._brightness = self._element.status diff --git a/homeassistant/components/scene/elkm1.py b/homeassistant/components/elkm1/scene.py similarity index 66% rename from homeassistant/components/scene/elkm1.py rename to homeassistant/components/elkm1/scene.py index 47dd17a56ae..c8583b1d8bf 100644 --- a/homeassistant/components/scene/elkm1.py +++ b/homeassistant/components/elkm1/scene.py @@ -1,11 +1,4 @@ -""" -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/ -""" - - +"""Support for control of ElkM1 tasks ("macros").""" from homeassistant.components.elkm1 import ( DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities) from homeassistant.components.scene import Scene @@ -13,8 +6,8 @@ from homeassistant.components.scene import Scene DEPENDENCIES = [ELK_DOMAIN] -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +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 diff --git a/homeassistant/components/sensor/elkm1.py b/homeassistant/components/elkm1/sensor.py similarity index 96% rename from homeassistant/components/sensor/elkm1.py rename to homeassistant/components/elkm1/sensor.py index 3fd57b190a6..63da6ea5376 100644 --- a/homeassistant/components/sensor/elkm1.py +++ b/homeassistant/components/elkm1/sensor.py @@ -1,17 +1,12 @@ -""" -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/ -""" +"""Support for control of ElkM1 sensors.""" 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): +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 diff --git a/homeassistant/components/switch/elkm1.py b/homeassistant/components/elkm1/switch.py similarity index 74% rename from homeassistant/components/switch/elkm1.py rename to homeassistant/components/elkm1/switch.py index a838e1b948e..7badd6ee5dc 100644 --- a/homeassistant/components/switch/elkm1.py +++ b/homeassistant/components/elkm1/switch.py @@ -1,11 +1,4 @@ -""" -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/ -""" - - +"""Support for control of ElkM1 outputs (relays).""" from homeassistant.components.elkm1 import ( DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities) from homeassistant.components.switch import SwitchDevice @@ -13,8 +6,8 @@ from homeassistant.components.switch import SwitchDevice DEPENDENCIES = [ELK_DOMAIN] -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +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 diff --git a/homeassistant/components/emoncms_history.py b/homeassistant/components/emoncms_history/__init__.py similarity index 93% rename from homeassistant/components/emoncms_history.py rename to homeassistant/components/emoncms_history/__init__.py index 6a92ab64044..45fb358cecc 100644 --- a/homeassistant/components/emoncms_history.py +++ b/homeassistant/components/emoncms_history/__init__.py @@ -1,9 +1,4 @@ -""" -A component which allows you to send data to Emoncms. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/emoncms_history/ -""" +"""Support for sending data to Emoncms.""" import logging from datetime import timedelta diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index 07ecb9d265a..c8ed263a2dc 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -1,9 +1,4 @@ -""" -Support for local control of entities by emulating the Phillips Hue bridge. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/emulated_hue/ -""" +"""Support for local control of entities by emulating a Phillips Hue bridge.""" import logging from aiohttp import web @@ -31,18 +26,18 @@ _LOGGER = logging.getLogger(__name__) NUMBERS_FILE = 'emulated_hue_ids.json' -CONF_HOST_IP = 'host_ip' -CONF_LISTEN_PORT = 'listen_port' CONF_ADVERTISE_IP = 'advertise_ip' CONF_ADVERTISE_PORT = 'advertise_port' -CONF_UPNP_BIND_MULTICAST = 'upnp_bind_multicast' -CONF_OFF_MAPS_TO_ON_DOMAINS = 'off_maps_to_on_domains' +CONF_ENTITIES = 'entities' +CONF_ENTITY_HIDDEN = 'hidden' +CONF_ENTITY_NAME = 'name' CONF_EXPOSE_BY_DEFAULT = 'expose_by_default' CONF_EXPOSED_DOMAINS = 'exposed_domains' +CONF_HOST_IP = 'host_ip' +CONF_LISTEN_PORT = 'listen_port' +CONF_OFF_MAPS_TO_ON_DOMAINS = 'off_maps_to_on_domains' CONF_TYPE = 'type' -CONF_ENTITIES = 'entities' -CONF_ENTITY_NAME = 'name' -CONF_ENTITY_HIDDEN = 'hidden' +CONF_UPNP_BIND_MULTICAST = 'upnp_bind_multicast' TYPE_ALEXA = 'alexa' TYPE_GOOGLE = 'google_home' diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 815e28b4fa4..95b3c470d9e 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -1,4 +1,4 @@ -"""Provides a Hue API to control Home Assistant.""" +"""Support for a Hue API to control Home Assistant.""" import logging from aiohttp import web @@ -12,18 +12,27 @@ from homeassistant.const import ( from homeassistant.components.light import ( ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS ) -from homeassistant.components.media_player import ( +from homeassistant.components.media_player.const import ( ATTR_MEDIA_VOLUME_LEVEL, SUPPORT_VOLUME_SET, ) from homeassistant.components.fan import ( ATTR_SPEED, SUPPORT_SET_SPEED, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH ) + +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, ATTR_POSITION, SERVICE_SET_COVER_POSITION, + SUPPORT_SET_POSITION +) + +from homeassistant.components import ( + cover, fan, media_player, light, script, scene +) + 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__) HUE_API_STATE_ON = 'on' @@ -239,13 +248,13 @@ class HueOneLightChangeView(HomeAssistantView): # Make sure the entity actually supports brightness entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - if entity.domain == "light": + if entity.domain == light.DOMAIN: if entity_features & SUPPORT_BRIGHTNESS: if brightness is not None: data[ATTR_BRIGHTNESS] = brightness # If the requested entity is a script add some variables - elif entity.domain == "script": + elif entity.domain == script.DOMAIN: data['variables'] = { 'requested_state': STATE_ON if result else STATE_OFF } @@ -254,7 +263,7 @@ class HueOneLightChangeView(HomeAssistantView): data['variables']['requested_level'] = brightness # If the requested entity is a media player, convert to volume - elif entity.domain == "media_player": + elif entity.domain == media_player.DOMAIN: if entity_features & SUPPORT_VOLUME_SET: if brightness is not None: turn_on_needed = True @@ -264,15 +273,21 @@ class HueOneLightChangeView(HomeAssistantView): data[ATTR_MEDIA_VOLUME_LEVEL] = brightness / 100.0 # If the requested entity is a cover, convert to open_cover/close_cover - elif entity.domain == "cover": + elif entity.domain == cover.DOMAIN: domain = entity.domain if service == SERVICE_TURN_ON: service = SERVICE_OPEN_COVER else: service = SERVICE_CLOSE_COVER + if entity_features & SUPPORT_SET_POSITION: + if brightness is not None: + domain = entity.domain + service = SERVICE_SET_COVER_POSITION + data[ATTR_POSITION] = brightness + # If the requested entity is a fan, convert to speed - elif entity.domain == "fan": + elif entity.domain == fan.DOMAIN: if entity_features & SUPPORT_SET_SPEED: if brightness is not None: domain = entity.domain @@ -344,19 +359,19 @@ def parse_hue_api_put_light_body(request_json, entity): # Make sure the entity actually supports brightness entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - if entity.domain == "light": + if entity.domain == light.DOMAIN: if entity_features & SUPPORT_BRIGHTNESS: report_brightness = True result = (brightness > 0) - elif entity.domain == "scene": + elif entity.domain == scene.DOMAIN: brightness = None report_brightness = False result = True - elif (entity.domain == "script" or - entity.domain == "media_player" or - entity.domain == "fan"): + elif entity.domain in [ + script.DOMAIN, media_player.DOMAIN, + fan.DOMAIN, cover.DOMAIN]: # Convert 0-255 to 0-100 level = brightness / 255 * 100 brightness = round(level) @@ -378,16 +393,16 @@ def get_entity_state(config, entity): # Make sure the entity actually supports brightness entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - if entity.domain == "light": + if entity.domain == light.DOMAIN: if entity_features & SUPPORT_BRIGHTNESS: pass - elif entity.domain == "media_player": + elif entity.domain == media_player.DOMAIN: level = entity.attributes.get( ATTR_MEDIA_VOLUME_LEVEL, 1.0 if final_state else 0.0) # Convert 0.0-1.0 to 0-255 final_brightness = round(min(1.0, level) * 255) - elif entity.domain == "fan": + elif entity.domain == fan.DOMAIN: speed = entity.attributes.get(ATTR_SPEED, 0) # Convert 0.0-1.0 to 0-255 final_brightness = 0 @@ -397,6 +412,9 @@ def get_entity_state(config, entity): final_brightness = 170 elif speed == SPEED_HIGH: final_brightness = 255 + elif entity.domain == cover.DOMAIN: + level = entity.attributes.get(ATTR_CURRENT_POSITION, 0) + final_brightness = round(level / 100 * 255) else: final_state, final_brightness = cached_state # Make sure brightness is valid diff --git a/homeassistant/components/emulated_hue/upnp.py b/homeassistant/components/emulated_hue/upnp.py index 548b6f3d771..a163d4b2e91 100644 --- a/homeassistant/components/emulated_hue/upnp.py +++ b/homeassistant/components/emulated_hue/upnp.py @@ -1,4 +1,4 @@ -"""Provides a UPNP discovery method that mimics Hue hubs.""" +"""Support UPNP discovery method that mimics Hue hubs.""" import threading import socket import logging diff --git a/homeassistant/components/emulated_roku/.translations/da.json b/homeassistant/components/emulated_roku/.translations/da.json new file mode 100644 index 00000000000..0479dee437d --- /dev/null +++ b/homeassistant/components/emulated_roku/.translations/da.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "name_exists": "Navnet findes allerede" + }, + "step": { + "user": { + "data": { + "advertise_ip": "Adviserings IP", + "advertise_port": "Adviserings port", + "host_ip": "V\u00e6rt IP", + "listen_port": "Lytte port", + "name": "Navn", + "upnp_bind_multicast": "Bind multicast (sand/falsk)" + }, + "title": "Angiv server konfiguration" + } + }, + "title": "EmulatedRoku" + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/.translations/fr.json b/homeassistant/components/emulated_roku/.translations/fr.json new file mode 100644 index 00000000000..5da2d437a35 --- /dev/null +++ b/homeassistant/components/emulated_roku/.translations/fr.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "D\u00e9finir la configuration du serveur" + } + }, + "title": "EmulatedRoku" + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/.translations/nl.json b/homeassistant/components/emulated_roku/.translations/nl.json new file mode 100644 index 00000000000..fe26cda31e2 --- /dev/null +++ b/homeassistant/components/emulated_roku/.translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "name_exists": "Naam bestaat al" + }, + "step": { + "user": { + "data": { + "advertise_ip": "Adverteer IP", + "advertise_port": "Adverterenpoort", + "host_ip": "Host IP", + "listen_port": "Luisterpoort", + "name": "Naam", + "upnp_bind_multicast": "Bind multicast (waar/niet waar)" + }, + "title": "Serverconfiguratie defini\u00ebren" + } + }, + "title": "EmulatedRoku" + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/.translations/pt.json b/homeassistant/components/emulated_roku/.translations/pt.json new file mode 100644 index 00000000000..286cd58dd89 --- /dev/null +++ b/homeassistant/components/emulated_roku/.translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/.translations/ru.json b/homeassistant/components/emulated_roku/.translations/ru.json index 611b5647233..c7b85c19592 100644 --- a/homeassistant/components/emulated_roku/.translations/ru.json +++ b/homeassistant/components/emulated_roku/.translations/ru.json @@ -6,9 +6,12 @@ "step": { "user": { "data": { + "advertise_ip": "\u041e\u0431\u044a\u044f\u0432\u043b\u044f\u0442\u044c IP", + "advertise_port": "\u041e\u0431\u044a\u044f\u0432\u043b\u044f\u0442\u044c \u043f\u043e\u0440\u0442", "host_ip": "\u0425\u043e\u0441\u0442", "listen_port": "\u041f\u043e\u0440\u0442", - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "upnp_bind_multicast": "\u041f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c multicast (True/False)" }, "title": "EmulatedRoku" } diff --git a/homeassistant/components/emulated_roku/__init__.py b/homeassistant/components/emulated_roku/__init__.py index 4dec1d5602a..ef87e14ec43 100644 --- a/homeassistant/components/emulated_roku/__init__.py +++ b/homeassistant/components/emulated_roku/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Roku API emulation. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/emulated_roku/ -""" +"""Support for Roku API emulation.""" import voluptuous as vol from homeassistant import config_entries, util diff --git a/homeassistant/components/emulated_roku/config_flow.py b/homeassistant/components/emulated_roku/config_flow.py index f2d56f84681..d08ad09f1c0 100644 --- a/homeassistant/components/emulated_roku/config_flow.py +++ b/homeassistant/components/emulated_roku/config_flow.py @@ -1,5 +1,4 @@ """Config flow to configure emulated_roku component.""" - import voluptuous as vol from homeassistant import config_entries diff --git a/homeassistant/components/emulated_roku/const.py b/homeassistant/components/emulated_roku/const.py index f4a034e31ac..25ea3adaa84 100644 --- a/homeassistant/components/emulated_roku/const.py +++ b/homeassistant/components/emulated_roku/const.py @@ -1,5 +1,4 @@ """Constants for the emulated_roku component.""" - DOMAIN = 'emulated_roku' CONF_SERVERS = 'servers' diff --git a/homeassistant/components/enocean.py b/homeassistant/components/enocean/__init__.py similarity index 96% rename from homeassistant/components/enocean.py rename to homeassistant/components/enocean/__init__.py index 75e456f62bd..8b3c27025cd 100644 --- a/homeassistant/components/enocean.py +++ b/homeassistant/components/enocean/__init__.py @@ -1,9 +1,4 @@ -""" -EnOcean Component. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/EnOcean/ -""" +"""Support for EnOcean devices.""" import logging import voluptuous as vol diff --git a/homeassistant/components/binary_sensor/enocean.py b/homeassistant/components/enocean/binary_sensor.py similarity index 93% rename from homeassistant/components/binary_sensor/enocean.py rename to homeassistant/components/enocean/binary_sensor.py index c883897c2ea..1fde8c79e40 100644 --- a/homeassistant/components/binary_sensor/enocean.py +++ b/homeassistant/components/enocean/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for EnOcean binary sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.enocean/ -""" +"""Support for EnOcean binary sensors.""" import logging import voluptuous as vol diff --git a/homeassistant/components/light/enocean.py b/homeassistant/components/enocean/light.py similarity index 94% rename from homeassistant/components/light/enocean.py rename to homeassistant/components/enocean/light.py index ebe2c409796..f574f89f951 100644 --- a/homeassistant/components/light/enocean.py +++ b/homeassistant/components/enocean/light.py @@ -1,9 +1,4 @@ -""" -Support for EnOcean light sources. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.enocean/ -""" +"""Support for EnOcean light sources.""" import logging import math diff --git a/homeassistant/components/sensor/enocean.py b/homeassistant/components/enocean/sensor.py similarity index 91% rename from homeassistant/components/sensor/enocean.py rename to homeassistant/components/enocean/sensor.py index 02e6812d5d5..d2e88ed3825 100644 --- a/homeassistant/components/sensor/enocean.py +++ b/homeassistant/components/enocean/sensor.py @@ -1,9 +1,4 @@ -""" -Support for EnOcean sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.enocean/ -""" +"""Support for EnOcean sensors.""" import logging import voluptuous as vol diff --git a/homeassistant/components/switch/enocean.py b/homeassistant/components/enocean/switch.py similarity index 93% rename from homeassistant/components/switch/enocean.py rename to homeassistant/components/enocean/switch.py index ab979604f50..4dfbafd36b1 100644 --- a/homeassistant/components/switch/enocean.py +++ b/homeassistant/components/enocean/switch.py @@ -1,9 +1,4 @@ -""" -Support for EnOcean switches. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.enocean/ -""" +"""Support for EnOcean switches.""" import logging import voluptuous as vol diff --git a/homeassistant/components/envisalink/__init__.py b/homeassistant/components/envisalink/__init__.py index 8b89b307db9..b7590341f78 100644 --- a/homeassistant/components/envisalink/__init__.py +++ b/homeassistant/components/envisalink/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Envisalink devices. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/envisalink/ -""" +"""Support for Envisalink devices.""" import asyncio import logging diff --git a/homeassistant/components/alarm_control_panel/envisalink.py b/homeassistant/components/envisalink/alarm_control_panel.py similarity index 90% rename from homeassistant/components/alarm_control_panel/envisalink.py rename to homeassistant/components/envisalink/alarm_control_panel.py index 9b772d9bdf0..a4cc5864fc4 100644 --- a/homeassistant/components/alarm_control_panel/envisalink.py +++ b/homeassistant/components/envisalink/alarm_control_panel.py @@ -1,9 +1,4 @@ -""" -Support for Envisalink-based alarm control panels (Honeywell/DSC). - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/alarm_control_panel.envisalink/ -""" +"""Support for Envisalink-based alarm control panels (Honeywell/DSC).""" import logging import voluptuous as vol @@ -31,8 +26,8 @@ ALARM_KEYPRESS_SCHEMA = vol.Schema({ }) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Perform the setup for Envisalink alarm panels.""" configured_partitions = discovery_info['partitions'] code = discovery_info[CONF_CODE] @@ -42,14 +37,9 @@ async def async_setup_platform(hass, config, async_add_entities, for part_num in configured_partitions: device_config_data = PARTITION_SCHEMA(configured_partitions[part_num]) device = EnvisalinkAlarm( - hass, - part_num, - device_config_data[CONF_PARTITIONNAME], - code, - panic_type, - hass.data[DATA_EVL].alarm_state['partition'][part_num], - hass.data[DATA_EVL] - ) + hass, part_num, device_config_data[CONF_PARTITIONNAME], code, + panic_type, hass.data[DATA_EVL].alarm_state['partition'][part_num], + hass.data[DATA_EVL]) devices.append(device) async_add_entities(devices) diff --git a/homeassistant/components/binary_sensor/envisalink.py b/homeassistant/components/envisalink/binary_sensor.py similarity index 94% rename from homeassistant/components/binary_sensor/envisalink.py rename to homeassistant/components/envisalink/binary_sensor.py index 276ace8dd51..26b54e16cc8 100644 --- a/homeassistant/components/binary_sensor/envisalink.py +++ b/homeassistant/components/envisalink/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for Envisalink zone states- represented as binary sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.envisalink/ -""" +"""Support for Envisalink zone states- represented as binary sensors.""" import logging import datetime diff --git a/homeassistant/components/sensor/envisalink.py b/homeassistant/components/envisalink/sensor.py similarity index 84% rename from homeassistant/components/sensor/envisalink.py rename to homeassistant/components/envisalink/sensor.py index aed2b056d2f..cc6a8b87232 100644 --- a/homeassistant/components/sensor/envisalink.py +++ b/homeassistant/components/envisalink/sensor.py @@ -1,9 +1,4 @@ -""" -Support for Envisalink sensors (shows panel info). - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.envisalink/ -""" +"""Support for Envisalink sensors (shows panel info).""" import logging from homeassistant.core import callback @@ -18,8 +13,8 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['envisalink'] -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Perform the setup for Envisalink sensor devices.""" configured_partitions = discovery_info['partitions'] @@ -27,11 +22,10 @@ async def async_setup_platform(hass, config, async_add_entities, for part_num in configured_partitions: device_config_data = PARTITION_SCHEMA(configured_partitions[part_num]) device = EnvisalinkSensor( - hass, - device_config_data[CONF_PARTITIONNAME], - part_num, + hass, device_config_data[CONF_PARTITIONNAME], part_num, hass.data[DATA_EVL].alarm_state['partition'][part_num], hass.data[DATA_EVL]) + devices.append(device) async_add_entities(devices) diff --git a/homeassistant/components/esphome/.translations/da.json b/homeassistant/components/esphome/.translations/da.json new file mode 100644 index 00000000000..20224ec0d15 --- /dev/null +++ b/homeassistant/components/esphome/.translations/da.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "ESP er allerede konfigureret" + }, + "error": { + "connection_error": "Kan ikke oprette forbindelse til ESP. S\u00f8rg for, at din YAML-fil indeholder en 'api:' linje.", + "invalid_password": "Ugyldig adgangskode!", + "resolve_error": "Kan ikke finde adressen p\u00e5 ESP. Hvis denne fejl forts\u00e6tter skal du angive en statisk IP-adresse: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + }, + "step": { + "authenticate": { + "data": { + "password": "Adgangskode" + }, + "description": "Indtast venligst den adgangskode, du har angivet i din konfiguration.", + "title": "Indtast adgangskode" + }, + "user": { + "data": { + "host": "V\u00e6rt", + "port": "Port" + }, + "description": "Angiv forbindelsesindstillinger for din [ESPHome](https://esphomelib.com/) node.", + "title": "ESPHome" + } + }, + "title": "ESPHome" + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/.translations/fr.json b/homeassistant/components/esphome/.translations/fr.json index a021f1fe9f4..cebe6848f1b 100644 --- a/homeassistant/components/esphome/.translations/fr.json +++ b/homeassistant/components/esphome/.translations/fr.json @@ -8,14 +8,18 @@ "data": { "password": "Mot de passe" }, + "description": "Veuillez saisir le mot de passe que vous avez d\u00e9fini dans votre configuration.", "title": "Entrer votre mot de passe" }, "user": { "data": { "host": "H\u00f4te", "port": "Port" - } + }, + "description": "Veuillez saisir les param\u00e8tres de connexion de votre n\u0153ud [ESPHome] (https://esphomelib.com/).", + "title": "ESPHome" } - } + }, + "title": "ESPHome" } } \ No newline at end of file diff --git a/homeassistant/components/esphome/.translations/pt.json b/homeassistant/components/esphome/.translations/pt.json index 70e21e14666..ea1e25c3024 100644 --- a/homeassistant/components/esphome/.translations/pt.json +++ b/homeassistant/components/esphome/.translations/pt.json @@ -22,9 +22,9 @@ "port": "Porta" }, "description": "Por favor, insira as configura\u00e7\u00f5es de liga\u00e7\u00e3o ao seu n\u00f3 [ESPHome] (https://esphomelib.com/).", - "title": "" + "title": "ESPHome" } }, - "title": "" + "title": "ESPHome" } } \ No newline at end of file diff --git a/homeassistant/components/esphome/.translations/uk.json b/homeassistant/components/esphome/.translations/uk.json new file mode 100644 index 00000000000..94dafeb3c2e --- /dev/null +++ b/homeassistant/components/esphome/.translations/uk.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "ESP \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e" + }, + "error": { + "connection_error": "\u041d\u0435 \u0432\u0434\u0430\u0454\u0442\u044c\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f \u0434\u043e ESP. \u041f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u0444\u0430\u0439\u043b YAML \u043c\u0456\u0441\u0442\u0438\u0442\u044c \u0440\u044f\u0434\u043e\u043a \"api:\".", + "invalid_password": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u043f\u0430\u0440\u043e\u043b\u044c!", + "resolve_error": "\u041d\u0435\u043c\u043e\u0436\u043b\u0438\u0432\u043e \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0430\u0434\u0440\u0435\u0441\u0443 ESP. \u042f\u043a\u0449\u043e \u0446\u044f \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043d\u0435 \u0437\u043d\u0438\u043a\u0430\u0454, \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0456\u0442\u044c \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u0443 IP-\u0430\u0434\u0440\u0435\u0441\u0443: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + }, + "step": { + "authenticate": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c, \u044f\u043a\u0438\u0439 \u0432\u0438 \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u043b\u0438 \u0443 \u0441\u0432\u043e\u0457\u0439 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u0457.", + "title": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0432\u0430\u0448\u043e\u0433\u043e \u0432\u0443\u0437\u043b\u0430 [ESPHome] (https://esphomelib.com/)." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 1ff2c10c828..004162341b1 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -30,9 +30,11 @@ if TYPE_CHECKING: from aioesphomeapi import APIClient, EntityInfo, EntityState, DeviceInfo, \ ServiceCall -DOMAIN = 'esphome' -REQUIREMENTS = ['aioesphomeapi==1.4.2'] +REQUIREMENTS = ['aioesphomeapi==1.5.0'] +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'esphome' DISPATCHER_UPDATE_ENTITY = 'esphome_{entry_id}_update_{component_key}_{key}' DISPATCHER_REMOVE_ENTITY = 'esphome_{entry_id}_remove_{component_key}_{key}' @@ -53,8 +55,6 @@ HA_COMPONENTS = [ 'switch', ] -_LOGGER = logging.getLogger(__name__) - # No config schema - only configuration entry CONFIG_SCHEMA = vol.Schema({}, extra=vol.ALLOW_EXTRA) @@ -325,6 +325,7 @@ async def _setup_auto_reconnect_logic(hass: HomeAssistantType, # In the future another API will be set up so that the ESP can # notify HA of connectivity directly, but for new we'll use a # really short reconnect interval. + tries = min(tries, 10) # prevent OverflowError wait_time = int(round(min(1.8**tries, 60.0))) _LOGGER.info("Trying to reconnect in %s seconds", wait_time) await asyncio.sleep(wait_time) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 1f71d8d66b5..e509455c12e 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -55,11 +55,10 @@ class EsphomeFlowHandler(config_entries.ConfigFlow): async def async_step_discovery(self, user_input: ConfigType): """Handle discovery.""" - # mDNS hostname has additional '.' at end - hostname = user_input['hostname'][:-1] - hosts = (hostname, user_input['host']) + address = user_input['properties'].get( + 'address', user_input['hostname'][:-1]) for entry in self._async_current_entries(): - if entry.data['host'] in hosts: + if entry.data['host'] == address: return self.async_abort( reason='already_configured' ) @@ -67,7 +66,7 @@ class EsphomeFlowHandler(config_entries.ConfigFlow): # Prefer .local addresses (mDNS is available after all, otherwise # we wouldn't have received the discovery message) return await self.async_step_user(user_input={ - 'host': hostname, + 'host': address, 'port': user_input['port'], }) diff --git a/homeassistant/components/eufy.py b/homeassistant/components/eufy/__init__.py similarity index 81% rename from homeassistant/components/eufy.py rename to homeassistant/components/eufy/__init__.py index c1166f8cf7b..d5a0938bf66 100644 --- a/homeassistant/components/eufy.py +++ b/homeassistant/components/eufy/__init__.py @@ -1,20 +1,14 @@ -""" -Support for Eufy devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/eufy/ -""" +"""Support for Eufy devices.""" import logging import voluptuous as vol -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_ADDRESS, \ - CONF_DEVICES, CONF_USERNAME, CONF_PASSWORD, CONF_TYPE, CONF_NAME +from homeassistant.const import ( + CONF_ACCESS_TOKEN, CONF_ADDRESS, CONF_DEVICES, CONF_NAME, CONF_PASSWORD, + CONF_TYPE, CONF_USERNAME) from homeassistant.helpers import discovery - import homeassistant.helpers.config_validation as cv - REQUIREMENTS = ['lakeside==0.11'] _LOGGER = logging.getLogger(__name__) @@ -30,8 +24,8 @@ DEVICE_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ - vol.Optional(CONF_DEVICES, default=[]): vol.All(cv.ensure_list, - [DEVICE_SCHEMA]), + vol.Optional(CONF_DEVICES, default=[]): + vol.All(cv.ensure_list, [DEVICE_SCHEMA]), vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string, vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string, }), diff --git a/homeassistant/components/light/eufy.py b/homeassistant/components/eufy/light.py similarity index 97% rename from homeassistant/components/light/eufy.py rename to homeassistant/components/eufy/light.py index 7a44a58cd81..62bc058f155 100644 --- a/homeassistant/components/light/eufy.py +++ b/homeassistant/components/eufy/light.py @@ -1,9 +1,4 @@ -""" -Support for Eufy lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.eufy/ -""" +"""Support for Eufy lights.""" import logging from homeassistant.components.light import ( diff --git a/homeassistant/components/switch/eufy.py b/homeassistant/components/eufy/switch.py similarity index 91% rename from homeassistant/components/switch/eufy.py rename to homeassistant/components/eufy/switch.py index a226797c0f8..96d68194107 100644 --- a/homeassistant/components/switch/eufy.py +++ b/homeassistant/components/eufy/switch.py @@ -1,9 +1,4 @@ -""" -Support for Eufy switches. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.eufy/ -""" +"""Support for Eufy switches.""" import logging from homeassistant.components.switch import SwitchDevice diff --git a/homeassistant/components/evohome.py b/homeassistant/components/evohome/__init__.py similarity index 87% rename from homeassistant/components/evohome.py rename to homeassistant/components/evohome/__init__.py index 40ba5b9b70f..52bb77516e6 100644 --- a/homeassistant/components/evohome.py +++ b/homeassistant/components/evohome/__init__.py @@ -1,19 +1,10 @@ -"""Support for (EMEA/EU-based) Honeywell evohome systems. - -Support for a temperature control system (TCS, controller) with 0+ heating -zones (e.g. TRVs, relays) and, optionally, a DHW controller. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/evohome/ -""" - +"""Support for (EMEA/EU-based) Honeywell evohome systems.""" # Glossary: # TCS - temperature control system (a.k.a. Controller, Parent), which can # have up to 13 Children: # 0-12 Heating zones (a.k.a. Zone), and # 0-1 DHW controller, (a.k.a. Boiler) # The TCS & Zones are implemented as Climate devices, Boiler as a WaterHeater - from datetime import timedelta import logging @@ -46,8 +37,7 @@ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_LOCATION_IDX, default=0): - cv.positive_int, + vol.Optional(CONF_LOCATION_IDX, default=0): cv.positive_int, vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL_DEFAULT): vol.All(cv.time_period, vol.Range(min=SCAN_INTERVAL_MINIMUM)), }), @@ -109,9 +99,9 @@ def setup(hass, hass_config): ) else: - raise # we dont expect/handle any other HTTPErrors + raise # We don't expect/handle any other HTTPErrors - return False # unable to continue + return False finally: # Redact username, password as no longer needed evo_data['params'][CONF_USERNAME] = 'REDACTED' @@ -136,11 +126,8 @@ def setup(hass, hass_config): except IndexError: _LOGGER.warning( "setup(): Parameter '%s'=%s, is outside its range (0-%s)", - CONF_LOCATION_IDX, - loc_idx, - len(client.installation_info) - 1 - ) - return False # unable to continue + CONF_LOCATION_IDX, loc_idx, len(client.installation_info) - 1) + return False if _LOGGER.isEnabledFor(logging.DEBUG): tmp_loc = dict(evo_data['config']) @@ -154,7 +141,7 @@ def setup(hass, hass_config): @callback def _first_update(event): - # When HA has started, the hub knows to retreive it's first update + """When HA has started, the hub knows to retrieve it's first update.""" pkt = {'sender': 'setup()', 'signal': 'refresh', 'to': EVO_PARENT} async_dispatcher_send(hass, DISPATCHER_EVOHOME, pkt) diff --git a/homeassistant/components/climate/evohome.py b/homeassistant/components/evohome/climate.py similarity index 91% rename from homeassistant/components/climate/evohome.py rename to homeassistant/components/evohome/climate.py index fd58e6c01e8..ef82a3dc81c 100644 --- a/homeassistant/components/climate/evohome.py +++ b/homeassistant/components/evohome/climate.py @@ -1,12 +1,4 @@ -"""Support for Climate devices of (EMEA/EU-based) Honeywell evohome systems. - -Support for a temperature control system (TCS, controller) with 0+ heating -zones (e.g. TRVs, relays). - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/climate.evohome/ -""" - +"""Support for Climate devices of (EMEA/EU-based) Honeywell evohome systems.""" from datetime import datetime, timedelta import logging @@ -40,7 +32,7 @@ from homeassistant.helpers.dispatcher import ( _LOGGER = logging.getLogger(__name__) -# the Controller's opmode/state and the zone's (inherited) state +# The Controller's opmode/state and the zone's (inherited) state EVO_RESET = 'AutoWithReset' EVO_AUTO = 'Auto' EVO_AUTOECO = 'AutoWithEco' @@ -49,12 +41,12 @@ EVO_DAYOFF = 'DayOff' EVO_CUSTOM = 'Custom' EVO_HEATOFF = 'HeatingOff' -# these are for Zones' opmode, and state +# These are for Zones' opmode, and state EVO_FOLLOW = 'FollowSchedule' EVO_TEMPOVER = 'TemporaryOverride' EVO_PERMOVER = 'PermanentOverride' -# for the Controller. NB: evohome treats Away mode as a mode in/of itself, +# For the Controller. NB: evohome treats Away mode as a mode in/of itself, # where HA considers it to 'override' the exising operating mode TCS_STATE_TO_HA = { EVO_RESET: STATE_AUTO, @@ -100,16 +92,12 @@ async def async_setup_platform(hass, hass_config, async_add_entities, # evohomeclient has exposed no means of accessing non-default location # (i.e. loc_idx > 0) other than using a protected member, such as below - tcs_obj_ref = client.locations[loc_idx]._gateways[0]._control_systems[0] # noqa E501; pylint: disable=protected-access + tcs_obj_ref = client.locations[loc_idx]._gateways[0]._control_systems[0] # noqa E501; pylint: disable=protected-access _LOGGER.debug( - "setup_platform(): Found Controller, id=%s [%s], " - "name=%s (location_idx=%s)", - tcs_obj_ref.systemId, - tcs_obj_ref.modelType, - tcs_obj_ref.location.name, - loc_idx - ) + "Found Controller, id=%s [%s], name=%s (location_idx=%s)", + tcs_obj_ref.systemId, tcs_obj_ref.modelType, tcs_obj_ref.location.name, + loc_idx) controller = EvoController(evo_data, client, tcs_obj_ref) zones = [] @@ -117,12 +105,8 @@ async def async_setup_platform(hass, hass_config, async_add_entities, for zone_idx in tcs_obj_ref.zones: zone_obj_ref = tcs_obj_ref.zones[zone_idx] _LOGGER.debug( - "setup_platform(): Found Zone, id=%s [%s], " - "name=%s", - zone_obj_ref.zoneId, - zone_obj_ref.zone_type, - zone_obj_ref.name - ) + "Found Zone, id=%s [%s], name=%s", + zone_obj_ref.zoneId, zone_obj_ref.zone_type, zone_obj_ref.name) zones.append(EvoZone(evo_data, client, zone_obj_ref)) entities = [controller] + zones @@ -164,12 +148,9 @@ class EvoClimateDevice(ClimateDevice): _LOGGER.warning( "API rate limit has been exceeded. Suspending polling for %s " - "seconds, and increasing '%s' from %s to %s seconds.", - new_interval * 3, - CONF_SCAN_INTERVAL, - old_interval, - new_interval, - ) + "seconds, and increasing '%s' from %s to %s seconds", + new_interval * 3, CONF_SCAN_INTERVAL, old_interval, + new_interval) self._timers['statusUpdated'] = datetime.now() + new_interval * 3 @@ -316,7 +297,7 @@ class EvoZone(EvoClimateDevice): try: self._obj.set_temperature(temperature, until) except HTTPError as err: - self._handle_exception("HTTPError", str(err)) # noqa: E501; pylint: disable=no-member + self._handle_exception("HTTPError", str(err)) # noqa: E501; pylint: disable=no-member def set_temperature(self, **kwargs): """Set new target temperature, indefinitely.""" @@ -366,7 +347,7 @@ class EvoZone(EvoClimateDevice): try: self._obj.cancel_temp_override(self._obj) except HTTPError as err: - self._handle_exception("HTTPError", str(err)) # noqa: E501; pylint: disable=no-member + self._handle_exception("HTTPError", str(err)) # noqa: E501; pylint: disable=no-member elif operation_mode == EVO_TEMPOVER: _LOGGER.error( @@ -526,7 +507,7 @@ class EvoController(EvoClimateDevice): def _set_operation_mode(self, operation_mode): try: - self._obj._set_status(operation_mode) # noqa: E501; pylint: disable=protected-access + self._obj._set_status(operation_mode) # noqa: E501; pylint: disable=protected-access except HTTPError as err: self._handle_requests_exceptions(err) @@ -558,7 +539,7 @@ class EvoController(EvoClimateDevice): if not expired: return - # Retreive the latest state data via the client api + # Retrieve the latest state data via the client API loc_idx = self._params[CONF_LOCATION_IDX] try: @@ -570,10 +551,7 @@ class EvoController(EvoClimateDevice): self._timers['statusUpdated'] = datetime.now() self._available = True - _LOGGER.debug( - "_update_state_data(): self._status = %s", - self._status - ) + _LOGGER.debug("Status = %s", self._status) # inform the child devices that state data has been updated pkt = {'sender': 'controller', 'signal': 'refresh', 'to': EVO_CHILD} diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 3525b95c007..50d6802c4d2 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -16,7 +16,8 @@ from homeassistant.const import (SERVICE_TURN_ON, SERVICE_TOGGLE, from homeassistant.loader import bind_hass from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa +from homeassistant.helpers.config_validation import ( # noqa + PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/fastdotcom/__init__.py b/homeassistant/components/fastdotcom/__init__.py new file mode 100644 index 00000000000..2e092e527c5 --- /dev/null +++ b/homeassistant/components/fastdotcom/__init__.py @@ -0,0 +1,80 @@ +"""Support for testing internet speed via Fast.com.""" +import logging +from datetime import timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_UPDATE_INTERVAL, CONF_SCAN_INTERVAL, \ + CONF_UPDATE_INTERVAL_INVALIDATION_VERSION +from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.event import async_track_time_interval + +REQUIREMENTS = ['fastdotcom==0.0.3'] + +DOMAIN = 'fastdotcom' +DATA_UPDATED = '{}_data_updated'.format(DOMAIN) + +_LOGGER = logging.getLogger(__name__) + +CONF_MANUAL = 'manual' + +DEFAULT_INTERVAL = timedelta(hours=1) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All( + vol.Schema({ + vol.Optional(CONF_UPDATE_INTERVAL): + vol.All(cv.time_period, cv.positive_timedelta), + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_INTERVAL): + vol.All(cv.time_period, cv.positive_timedelta), + vol.Optional(CONF_MANUAL, default=False): cv.boolean, + }), + cv.deprecated( + CONF_UPDATE_INTERVAL, + replacement_key=CONF_SCAN_INTERVAL, + invalidation_version=CONF_UPDATE_INTERVAL_INVALIDATION_VERSION, + default=DEFAULT_INTERVAL + ) + ) +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the Fast.com component.""" + conf = config[DOMAIN] + data = hass.data[DOMAIN] = SpeedtestData(hass) + + if not conf[CONF_MANUAL]: + async_track_time_interval( + hass, data.update, conf[CONF_SCAN_INTERVAL] + ) + + def update(call=None): + """Service call to manually update the data.""" + data.update() + + hass.services.async_register(DOMAIN, 'speedtest', update) + + hass.async_create_task( + async_load_platform(hass, 'sensor', DOMAIN, {}, config) + ) + + return True + + +class SpeedtestData: + """Get the latest data from fast.com.""" + + def __init__(self, hass): + """Initialize the data object.""" + self.data = None + self._hass = hass + + def update(self, now=None): + """Get the latest data from fast.com.""" + from fastdotcom import fast_com + _LOGGER.debug("Executing fast.com speedtest") + self.data = {'download': fast_com()} + dispatcher_send(self._hass, DATA_UPDATED) diff --git a/homeassistant/components/fastdotcom/sensor.py b/homeassistant/components/fastdotcom/sensor.py new file mode 100644 index 00000000000..0f17179f918 --- /dev/null +++ b/homeassistant/components/fastdotcom/sensor.py @@ -0,0 +1,80 @@ +"""Support for Fast.com internet speed testing sensor.""" +import logging + +from homeassistant.components.fastdotcom import DOMAIN as FASTDOTCOM_DOMAIN, \ + DATA_UPDATED +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.restore_state import RestoreEntity + +DEPENDENCIES = ['fastdotcom'] + +_LOGGER = logging.getLogger(__name__) + +ICON = 'mdi:speedometer' + +UNIT_OF_MEASUREMENT = 'Mbit/s' + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the Fast.com sensor.""" + async_add_entities([SpeedtestSensor(hass.data[FASTDOTCOM_DOMAIN])]) + + +class SpeedtestSensor(RestoreEntity): + """Implementation of a FAst.com sensor.""" + + def __init__(self, speedtest_data): + """Initialize the sensor.""" + self._name = 'Fast.com Download' + self.speedtest_client = speedtest_data + 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 device.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return UNIT_OF_MEASUREMENT + + @property + def icon(self): + """Return icon.""" + return ICON + + @property + def should_poll(self): + """Return the polling requirement for this sensor.""" + return False + + async def async_added_to_hass(self): + """Handle entity which will be added.""" + await super().async_added_to_hass() + state = await self.async_get_last_state() + if not state: + return + self._state = state.state + + async_dispatcher_connect( + self.hass, DATA_UPDATED, self._schedule_immediate_update + ) + + def update(self): + """Get the latest data and update the states.""" + data = self.speedtest_client.data + if data is None: + return + self._state = data['download'] + + @callback + def _schedule_immediate_update(self): + self.async_schedule_update_ha_state(True) diff --git a/homeassistant/components/fastdotcom/services.yaml b/homeassistant/components/fastdotcom/services.yaml new file mode 100644 index 00000000000..fe6cb1ac12d --- /dev/null +++ b/homeassistant/components/fastdotcom/services.yaml @@ -0,0 +1,2 @@ +speedtest: + description: Immediately take a speedest with Fast.com \ No newline at end of file diff --git a/homeassistant/components/feedreader.py b/homeassistant/components/feedreader/__init__.py similarity index 97% rename from homeassistant/components/feedreader.py rename to homeassistant/components/feedreader/__init__.py index 7882cdc5a15..86744bfd39c 100644 --- a/homeassistant/components/feedreader.py +++ b/homeassistant/components/feedreader/__init__.py @@ -1,9 +1,4 @@ -""" -Support for RSS/Atom feeds. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/feedreader/ -""" +"""Support for RSS/Atom feeds.""" from datetime import datetime, timedelta from logging import getLogger from os.path import exists @@ -16,7 +11,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_START, CONF_SCAN_INTERVAL from homeassistant.helpers.event import track_time_interval import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['feedparser==5.2.1'] +REQUIREMENTS = ['feedparser-homeassistant==5.2.2.dev1'] _LOGGER = getLogger(__name__) diff --git a/homeassistant/components/ffmpeg.py b/homeassistant/components/ffmpeg/__init__.py similarity index 97% rename from homeassistant/components/ffmpeg.py rename to homeassistant/components/ffmpeg/__init__.py index 3184b5a5d54..7b7e3a81294 100644 --- a/homeassistant/components/ffmpeg.py +++ b/homeassistant/components/ffmpeg/__init__.py @@ -1,9 +1,4 @@ -""" -Component that will help set the FFmpeg component. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/ffmpeg/ -""" +"""Support for FFmpeg.""" import logging import re diff --git a/homeassistant/components/ffmpeg/services.yaml b/homeassistant/components/ffmpeg/services.yaml new file mode 100644 index 00000000000..05c9c4fb180 --- /dev/null +++ b/homeassistant/components/ffmpeg/services.yaml @@ -0,0 +1,15 @@ +restart: + description: Send a restart command to a ffmpeg based sensor. + fields: + entity_id: {description: Name(s) of entities that will restart. Platform dependent., + example: binary_sensor.ffmpeg_noise} +start: + description: Send a start command to a ffmpeg based sensor. + fields: + entity_id: {description: Name(s) of entities that will start. Platform dependent., + example: binary_sensor.ffmpeg_noise} +stop: + description: Send a stop command to a ffmpeg based sensor. + fields: + entity_id: {description: Name(s) of entities that will stop. Platform dependent., + example: binary_sensor.ffmpeg_noise} diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index 715cc036265..9a6ccccb5e3 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -1,10 +1,4 @@ -""" -Support for the Fibaro devices. - -For more details about this platform, please refer to the documentation. -https://home-assistant.io/components/fibaro/ -""" - +"""Support for the Fibaro devices.""" import logging from collections import defaultdict from typing import Optional @@ -22,20 +16,30 @@ from homeassistant.util import convert, slugify REQUIREMENTS = ['fiblary3==0.1.7'] _LOGGER = logging.getLogger(__name__) -DOMAIN = 'fibaro' -FIBARO_DEVICES = 'fibaro_devices' -FIBARO_CONTROLLERS = 'fibaro_controllers' -ATTR_CURRENT_POWER_W = "current_power_w" -ATTR_CURRENT_ENERGY_KWH = "current_energy_kwh" -CONF_PLUGINS = "plugins" -CONF_GATEWAYS = 'gateways' -CONF_DIMMING = "dimming" -CONF_COLOR = "color" -CONF_RESET_COLOR = "reset_color" -CONF_DEVICE_CONFIG = "device_config" -FIBARO_COMPONENTS = ['binary_sensor', 'cover', 'light', - 'scene', 'sensor', 'switch'] +ATTR_CURRENT_ENERGY_KWH = 'current_energy_kwh' +ATTR_CURRENT_POWER_W = 'current_power_w' + +CONF_COLOR = 'color' +CONF_DEVICE_CONFIG = 'device_config' +CONF_DIMMING = 'dimming' +CONF_GATEWAYS = 'gateways' +CONF_PLUGINS = 'plugins' +CONF_RESET_COLOR = 'reset_color' + +DOMAIN = 'fibaro' + +FIBARO_CONTROLLERS = 'fibaro_controllers' +FIBARO_DEVICES = 'fibaro_devices' + +FIBARO_COMPONENTS = [ + 'binary_sensor', + 'cover', + 'light', + 'scene', + 'sensor', + 'switch', +] FIBARO_TYPEMAP = { 'com.fibaro.multilevelSensor': "sensor", @@ -78,8 +82,7 @@ GATEWAY_CONFIG = vol.Schema({ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ - vol.Required(CONF_GATEWAYS): - vol.All(cv.ensure_list, [GATEWAY_CONFIG]) + vol.Required(CONF_GATEWAYS): vol.All(cv.ensure_list, [GATEWAY_CONFIG]), }) }, extra=vol.ALLOW_EXTRA) @@ -91,20 +94,19 @@ class FibaroController(): """Initialize the Fibaro controller.""" from fiblary3.client.v4.client import Client as FibaroClient - self._client = FibaroClient(config[CONF_URL], - config[CONF_USERNAME], - config[CONF_PASSWORD]) + self._client = FibaroClient( + config[CONF_URL], config[CONF_USERNAME], config[CONF_PASSWORD]) self._scene_map = None # Whether to import devices from plugins self._import_plugins = config[CONF_PLUGINS] self._device_config = config[CONF_DEVICE_CONFIG] - self._room_map = None # Mapping roomId to room object - self._device_map = None # Mapping deviceId to device object - self.fibaro_devices = None # List of devices by type - self._callbacks = {} # Update value callbacks by deviceId - self._state_handler = None # Fiblary's StateHandler object + self._room_map = None # Mapping roomId to room object + self._device_map = None # Mapping deviceId to device object + self.fibaro_devices = None # List of devices by type + self._callbacks = {} # Update value callbacks by deviceId + self._state_handler = None # Fiblary's StateHandler object self._excluded_devices = config[CONF_EXCLUDE] - self.hub_serial = None # Unique serial number of the hub + self.hub_serial = None # Unique serial number of the hub def connect(self): """Start the communication with the Fibaro controller.""" @@ -118,7 +120,7 @@ class FibaroController(): return False if login is None or login.status is False: _LOGGER.error("Invalid login for Fibaro HC. " - "Please check username and password.") + "Please check username and password") return False self._room_map = {room.id: room for room in self._client.rooms.list()} diff --git a/homeassistant/components/binary_sensor/fibaro.py b/homeassistant/components/fibaro/binary_sensor.py similarity index 93% rename from homeassistant/components/binary_sensor/fibaro.py rename to homeassistant/components/fibaro/binary_sensor.py index 1934580c58e..2c2d9c30a79 100644 --- a/homeassistant/components/binary_sensor/fibaro.py +++ b/homeassistant/components/fibaro/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for Fibaro binary sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.fibaro/ -""" +"""Support for Fibaro binary sensors.""" import logging from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/cover/fibaro.py b/homeassistant/components/fibaro/cover.py similarity index 92% rename from homeassistant/components/cover/fibaro.py rename to homeassistant/components/fibaro/cover.py index d47dbb20315..aa34fcc36a9 100644 --- a/homeassistant/components/cover/fibaro.py +++ b/homeassistant/components/fibaro/cover.py @@ -1,9 +1,4 @@ -""" -Support for Fibaro cover - curtains, rollershutters etc. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/cover.fibaro/ -""" +"""Support for Fibaro cover - curtains, rollershutters etc.""" import logging from homeassistant.components.cover import ( diff --git a/homeassistant/components/light/fibaro.py b/homeassistant/components/fibaro/light.py similarity index 97% rename from homeassistant/components/light/fibaro.py rename to homeassistant/components/fibaro/light.py index 9b3b3850f39..5ee3e83b95f 100644 --- a/homeassistant/components/light/fibaro.py +++ b/homeassistant/components/fibaro/light.py @@ -1,10 +1,4 @@ -""" -Support for Fibaro lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.fibaro/ -""" - +"""Support for Fibaro lights.""" import logging import asyncio from functools import partial diff --git a/homeassistant/components/scene/fibaro.py b/homeassistant/components/fibaro/scene.py similarity index 81% rename from homeassistant/components/scene/fibaro.py rename to homeassistant/components/fibaro/scene.py index a0bd4e7ff40..620f095b733 100644 --- a/homeassistant/components/scene/fibaro.py +++ b/homeassistant/components/fibaro/scene.py @@ -1,9 +1,4 @@ -""" -Support for Fibaro scenes. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/scene.fibaro/ -""" +"""Support for Fibaro scenes.""" import logging from homeassistant.components.scene import ( diff --git a/homeassistant/components/sensor/fibaro.py b/homeassistant/components/fibaro/sensor.py similarity index 94% rename from homeassistant/components/sensor/fibaro.py rename to homeassistant/components/fibaro/sensor.py index e437ef8710d..01452d8b394 100644 --- a/homeassistant/components/sensor/fibaro.py +++ b/homeassistant/components/fibaro/sensor.py @@ -1,9 +1,4 @@ -""" -Support for Fibaro sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.fibaro/ -""" +"""Support for Fibaro sensors.""" import logging from homeassistant.const import ( diff --git a/homeassistant/components/switch/fibaro.py b/homeassistant/components/fibaro/switch.py similarity index 91% rename from homeassistant/components/switch/fibaro.py rename to homeassistant/components/fibaro/switch.py index 8b59aabec72..04b8aba1cf4 100644 --- a/homeassistant/components/switch/fibaro.py +++ b/homeassistant/components/fibaro/switch.py @@ -1,9 +1,4 @@ -""" -Support for Fibaro switches. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.fibaro/ -""" +"""Support for Fibaro switches.""" import logging from homeassistant.util import convert diff --git a/homeassistant/components/folder_watcher.py b/homeassistant/components/folder_watcher/__init__.py similarity index 94% rename from homeassistant/components/folder_watcher.py rename to homeassistant/components/folder_watcher/__init__.py index 098b34ac948..babfbd9e9aa 100644 --- a/homeassistant/components/folder_watcher.py +++ b/homeassistant/components/folder_watcher/__init__.py @@ -1,17 +1,15 @@ -""" -Component for monitoring activity on a folder. - -For more details about this platform, refer to the documentation at -https://home-assistant.io/components/folder_watcher/ -""" -import os +"""Component for monitoring activity on a folder.""" import logging +import os + import voluptuous as vol + from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['watchdog==0.8.3'] + _LOGGER = logging.getLogger(__name__) CONF_FOLDER = 'folder' diff --git a/homeassistant/components/foursquare.py b/homeassistant/components/foursquare/__init__.py similarity index 94% rename from homeassistant/components/foursquare.py rename to homeassistant/components/foursquare/__init__.py index a4a7395adc4..0c5a48049ec 100644 --- a/homeassistant/components/foursquare.py +++ b/homeassistant/components/foursquare/__init__.py @@ -1,9 +1,4 @@ -""" -Support for the Foursquare (Swarm) API. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/foursquare/ -""" +"""Support for the Foursquare (Swarm) API.""" import logging import requests diff --git a/homeassistant/components/foursquare/services.yaml b/homeassistant/components/foursquare/services.yaml new file mode 100644 index 00000000000..3d15a9583f6 --- /dev/null +++ b/homeassistant/components/foursquare/services.yaml @@ -0,0 +1,29 @@ +checkin: + description: Check a user into a Foursquare venue. + fields: + alt: {description: 'Altitude of the user''s location, in meters. [Optional]', + example: 0} + altAcc: {description: 'Vertical accuracy of the user''s location, in meters.', + example: 1} + broadcast: {description: 'Who to broadcast this check-in to. Accepts a comma-delimited + list of values: private (off the grid) or public (share with friends), facebook + share on facebook, twitter share on twitter, followers share with followers + (celebrity mode users only), If no valid value is found, the default is public. + [Optional]', example: 'public,twitter'} + eventId: {description: 'The event the user is checking in to. [Optional]', example: UHR8THISVNT} + ll: {description: 'Latitude and longitude of the user''s location. Only specify + this field if you have a GPS or other device reported location for the user + at the time of check-in. [Optional]', example: '33.7,44.2'} + llAcc: {description: 'Accuracy of the user''s latitude and longitude, in meters. + [Optional]', example: 1} + mentions: {description: 'Mentions in your check-in. This parameter is a semicolon-delimited + list of mentions. A single mention is of the form "start,end,userid", where + start is the index of the first character in the shout representing the mention, + end is the index of the first character in the shout after the mention, and + userid is the userid of the user being mentioned. If userid is prefixed with + "fbu-", this indicates a Facebook userid that is being mention. Character + indices in shouts are 0-based. [Optional]', example: '5,10,HZXXY3Y;15,20,GZYYZ3Z;25,30,fbu-GZXY13Y'} + shout: {description: 'A message about your check-in. The maximum length of this + field is 140 characters. [Optional]', example: There are crayons! Crayons!} + venueId: {description: 'The Foursquare venue where the user is checking in. [Required]', + example: IHR8THISVNU} diff --git a/homeassistant/components/freebox.py b/homeassistant/components/freebox/__init__.py similarity index 91% rename from homeassistant/components/freebox.py rename to homeassistant/components/freebox/__init__.py index 99efbae0984..41e60d884ce 100644 --- a/homeassistant/components/freebox.py +++ b/homeassistant/components/freebox/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Freebox devices (Freebox v6 and Freebox mini 4K). - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/freebox/ -""" +"""Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" import logging import socket @@ -26,7 +21,7 @@ FREEBOX_CONFIG_FILE = 'freebox.conf' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PORT): cv.port + vol.Required(CONF_PORT): cv.port, }) }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/device_tracker/freebox.py b/homeassistant/components/freebox/device_tracker.py similarity index 91% rename from homeassistant/components/device_tracker/freebox.py rename to homeassistant/components/freebox/device_tracker.py index f4e1ce5bd8a..fb94f7f56f5 100644 --- a/homeassistant/components/device_tracker/freebox.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -1,9 +1,4 @@ -""" -Support for Freebox devices (Freebox v6 and Freebox mini 4K). - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/device_tracker.freebox/ -""" +"""Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" from collections import namedtuple import logging diff --git a/homeassistant/components/sensor/freebox.py b/homeassistant/components/freebox/sensor.py similarity index 86% rename from homeassistant/components/sensor/freebox.py rename to homeassistant/components/freebox/sensor.py index 2f8ccbc745d..49e68dc2c41 100644 --- a/homeassistant/components/sensor/freebox.py +++ b/homeassistant/components/freebox/sensor.py @@ -1,9 +1,4 @@ -""" -Support for Freebox devices (Freebox v6 and Freebox mini 4K). - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/sensor.freebox/ -""" +"""Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" import logging from homeassistant.components.freebox import DATA_FREEBOX @@ -18,10 +13,7 @@ async def async_setup_platform( hass, config, async_add_entities, discovery_info=None): """Set up the sensors.""" fbx = hass.data[DATA_FREEBOX] - async_add_entities([ - FbxRXSensor(fbx), - FbxTXSensor(fbx) - ], True) + async_add_entities([FbxRXSensor(fbx), FbxTXSensor(fbx)], True) class FbxSensor(Entity): diff --git a/homeassistant/components/freedns.py b/homeassistant/components/freedns/__init__.py similarity index 70% rename from homeassistant/components/freedns.py rename to homeassistant/components/freedns/__init__.py index ec38bb59cc7..edb3a57c28c 100644 --- a/homeassistant/components/freedns.py +++ b/homeassistant/components/freedns/__init__.py @@ -13,7 +13,8 @@ import async_timeout import voluptuous as vol from homeassistant.const import (CONF_URL, CONF_ACCESS_TOKEN, - CONF_UPDATE_INTERVAL) + CONF_UPDATE_INTERVAL, CONF_SCAN_INTERVAL, + CONF_UPDATE_INTERVAL_INVALIDATION_VERSION) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -26,21 +27,31 @@ TIMEOUT = 10 UPDATE_URL = 'https://freedns.afraid.org/dynamic/update.php' CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Exclusive(CONF_URL, DOMAIN): cv.string, - vol.Exclusive(CONF_ACCESS_TOKEN, DOMAIN): cv.string, - vol.Optional(CONF_UPDATE_INTERVAL, default=DEFAULT_INTERVAL): vol.All( - cv.time_period, cv.positive_timedelta), - - }) + DOMAIN: vol.All( + vol.Schema({ + vol.Exclusive(CONF_URL, DOMAIN): cv.string, + vol.Exclusive(CONF_ACCESS_TOKEN, DOMAIN): cv.string, + vol.Optional(CONF_UPDATE_INTERVAL): + vol.All(cv.time_period, cv.positive_timedelta), + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_INTERVAL): + vol.All(cv.time_period, cv.positive_timedelta), + }), + cv.deprecated( + CONF_UPDATE_INTERVAL, + replacement_key=CONF_SCAN_INTERVAL, + invalidation_version=CONF_UPDATE_INTERVAL_INVALIDATION_VERSION, + default=DEFAULT_INTERVAL + ) + ) }, extra=vol.ALLOW_EXTRA) async def async_setup(hass, config): """Initialize the FreeDNS component.""" - url = config[DOMAIN].get(CONF_URL) - auth_token = config[DOMAIN].get(CONF_ACCESS_TOKEN) - update_interval = config[DOMAIN].get(CONF_UPDATE_INTERVAL) + conf = config[DOMAIN] + url = conf.get(CONF_URL) + auth_token = conf.get(CONF_ACCESS_TOKEN) + update_interval = conf[CONF_SCAN_INTERVAL] session = hass.helpers.aiohttp_client.async_get_clientsession() diff --git a/homeassistant/components/fritzbox.py b/homeassistant/components/fritzbox/__init__.py similarity index 92% rename from homeassistant/components/fritzbox.py rename to homeassistant/components/fritzbox/__init__.py index ad3c7bc1929..81ba019acbc 100644 --- a/homeassistant/components/fritzbox.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -1,9 +1,4 @@ -""" -Support for AVM Fritz!Box smarthome devices. - -For more details about this component, please refer to the documentation at -http://home-assistant.io/components/fritzbox/ -""" +"""Support for AVM Fritz!Box smarthome devices.""" import logging import voluptuous as vol diff --git a/homeassistant/components/binary_sensor/fritzbox.py b/homeassistant/components/fritzbox/binary_sensor.py similarity index 89% rename from homeassistant/components/binary_sensor/fritzbox.py rename to homeassistant/components/fritzbox/binary_sensor.py index ab58e6e84bc..c68c79f1e77 100644 --- a/homeassistant/components/binary_sensor/fritzbox.py +++ b/homeassistant/components/fritzbox/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for Fritzbox binary sensors. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.fritzbox/ -""" +"""Support for Fritzbox binary sensors.""" import logging import requests diff --git a/homeassistant/components/climate/fritzbox.py b/homeassistant/components/fritzbox/climate.py similarity index 96% rename from homeassistant/components/climate/fritzbox.py rename to homeassistant/components/fritzbox/climate.py index f2d13ee92f6..64d99ebf133 100644 --- a/homeassistant/components/climate/fritzbox.py +++ b/homeassistant/components/fritzbox/climate.py @@ -1,9 +1,4 @@ -""" -Support for AVM Fritz!Box smarthome thermostate devices. - -For more details about this component, please refer to the documentation at -http://home-assistant.io/components/climate.fritzbox/ -""" +"""Support for AVM Fritz!Box smarthome thermostate devices.""" import logging import requests @@ -19,6 +14,7 @@ from homeassistant.components.climate import ( SUPPORT_TARGET_TEMPERATURE) from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, PRECISION_HALVES, TEMP_CELSIUS) + DEPENDENCIES = ['fritzbox'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/fritzbox.py b/homeassistant/components/fritzbox/sensor.py similarity index 91% rename from homeassistant/components/sensor/fritzbox.py rename to homeassistant/components/fritzbox/sensor.py index 66c515c2bfd..a1736fb9857 100644 --- a/homeassistant/components/sensor/fritzbox.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -1,9 +1,4 @@ -""" -Support for AVM Fritz!Box smarthome temperature sensor only devices. - -For more details about this component, please refer to the documentation at -http://home-assistant.io/components/sensor.fritzbox/ -""" +"""Support for AVM Fritz!Box smarthome temperature sensor only devices.""" import logging import requests diff --git a/homeassistant/components/switch/fritzbox.py b/homeassistant/components/fritzbox/switch.py similarity index 94% rename from homeassistant/components/switch/fritzbox.py rename to homeassistant/components/fritzbox/switch.py index 55fa8a04796..be9212793ab 100644 --- a/homeassistant/components/switch/fritzbox.py +++ b/homeassistant/components/fritzbox/switch.py @@ -1,9 +1,4 @@ -""" -Support for AVM Fritz!Box smarthome switch devices. - -For more details about this component, please refer to the documentation at -http://home-assistant.io/components/switch.fritzbox/ -""" +"""Support for AVM Fritz!Box smarthome switch devices.""" import logging import requests diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 46652b4d7b0..caf6bbccb5c 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -1,9 +1,4 @@ -""" -Handle the frontend for Home Assistant. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/frontend/ -""" +"""Handle the frontend for Home Assistant.""" import asyncio import json import logging @@ -24,7 +19,9 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20190203.0'] +from .storage import async_setup_frontend_storage + +REQUIREMENTS = ['home-assistant-frontend==20190220.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', @@ -195,6 +192,7 @@ def add_manifest_json_key(key, val): async def async_setup(hass, config): """Set up the serving of the frontend.""" + await async_setup_frontend_storage(hass) hass.components.websocket_api.async_register_command( WS_TYPE_GET_PANELS, websocket_get_panels, SCHEMA_GET_PANELS) hass.components.websocket_api.async_register_command( diff --git a/homeassistant/components/frontend/storage.py b/homeassistant/components/frontend/storage.py new file mode 100644 index 00000000000..f01abc79e8e --- /dev/null +++ b/homeassistant/components/frontend/storage.py @@ -0,0 +1,79 @@ +"""API for persistent storage for the frontend.""" +from functools import wraps +import voluptuous as vol + +from homeassistant.components import websocket_api + +DATA_STORAGE = 'frontend_storage' +STORAGE_VERSION_USER_DATA = 1 +STORAGE_KEY_USER_DATA = 'frontend.user_data_{}' + + +async def async_setup_frontend_storage(hass): + """Set up frontend storage.""" + hass.data[DATA_STORAGE] = {} + hass.components.websocket_api.async_register_command( + websocket_set_user_data + ) + hass.components.websocket_api.async_register_command( + websocket_get_user_data + ) + + +def with_store(orig_func): + """Decorate function to provide data.""" + @wraps(orig_func) + async def with_store_func(hass, connection, msg): + """Provide user specific data and store to function.""" + store = hass.helpers.storage.Store( + STORAGE_VERSION_USER_DATA, + STORAGE_KEY_USER_DATA.format(connection.user.id) + ) + data = hass.data[DATA_STORAGE] + user_id = connection.user.id + if user_id not in data: + data[user_id] = await store.async_load() or {} + + await orig_func( + hass, connection, msg, + store, + data[user_id], + ) + return with_store_func + + +@websocket_api.websocket_command({ + vol.Required('type'): 'frontend/set_user_data', + vol.Required('key'): str, + vol.Required('value'): vol.Any(bool, str, int, float, dict, list, None), +}) +@websocket_api.async_response +@with_store +async def websocket_set_user_data(hass, connection, msg, store, data): + """Handle set global data command. + + Async friendly. + """ + data[msg['key']] = msg['value'] + await store.async_save(data) + connection.send_message(websocket_api.result_message( + msg['id'], + )) + + +@websocket_api.websocket_command({ + vol.Required('type'): 'frontend/get_user_data', + vol.Optional('key'): str, +}) +@websocket_api.async_response +@with_store +async def websocket_get_user_data(hass, connection, msg, store, data): + """Handle get global data command. + + Async friendly. + """ + connection.send_message(websocket_api.result_message( + msg['id'], { + 'value': data.get(msg['key']) if 'key' in msg else data + } + )) diff --git a/homeassistant/components/gc100.py b/homeassistant/components/gc100/__init__.py similarity index 90% rename from homeassistant/components/gc100.py rename to homeassistant/components/gc100/__init__.py index 0d4b19da030..36e9c61b1ba 100644 --- a/homeassistant/components/gc100.py +++ b/homeassistant/components/gc100/__init__.py @@ -1,9 +1,4 @@ -""" -Support for controlling Global Cache gc100. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/gc100/ -""" +"""Support for controlling Global Cache gc100.""" import logging import voluptuous as vol diff --git a/homeassistant/components/binary_sensor/gc100.py b/homeassistant/components/gc100/binary_sensor.py similarity index 100% rename from homeassistant/components/binary_sensor/gc100.py rename to homeassistant/components/gc100/binary_sensor.py diff --git a/homeassistant/components/switch/gc100.py b/homeassistant/components/gc100/switch.py similarity index 100% rename from homeassistant/components/switch/gc100.py rename to homeassistant/components/gc100/switch.py diff --git a/homeassistant/components/geo_location/__init__.py b/homeassistant/components/geo_location/__init__.py index 4597a56c61a..9095ce617aa 100644 --- a/homeassistant/components/geo_location/__init__.py +++ b/homeassistant/components/geo_location/__init__.py @@ -9,7 +9,8 @@ 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.config_validation import ( # noqa + PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent diff --git a/homeassistant/components/geofency/.translations/da.json b/homeassistant/components/geofency/.translations/da.json new file mode 100644 index 00000000000..1390dfb504a --- /dev/null +++ b/homeassistant/components/geofency/.translations/da.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Dit Home Assistant system skal v\u00e6re tilg\u00e6ngeligt fra internettet for at modtage Geofency meddelelser.", + "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning" + }, + "create_entry": { + "default": "For at sende begivenheder til Home Assistant skal du konfigurere webhook funktionen i Geofency.\n\n Udfyld f\u00f8lgende oplysninger: \n\n - URL: `{webhook_url}`\n - Metode: POST\n \n Se [dokumentationen]({docs_url}) for yderligere oplysninger." + }, + "step": { + "user": { + "description": "Er du sikker p\u00e5 at du vil konfigurere Geofency Webhook?", + "title": "Ops\u00e6tning af Geofency Webhook" + } + }, + "title": "Geofency Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/.translations/fr.json b/homeassistant/components/geofency/.translations/fr.json new file mode 100644 index 00000000000..142f40754b9 --- /dev/null +++ b/homeassistant/components/geofency/.translations/fr.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Votre instance Home Assistant doit \u00eatre accessible \u00e0 partir d'Internet pour recevoir les messages Geofency.", + "one_instance_allowed": "Une seule instance est n\u00e9cessaire." + }, + "create_entry": { + "default": "Pour envoyer des \u00e9v\u00e9nements \u00e0 Home Assistant, vous devez configurer la fonctionnalit\u00e9 Webhook dans Geofency. \n\n Remplissez les informations suivantes: \n\n - URL: ` {webhook_url} ` \n - M\u00e9thode: POST \n\n Voir [la documentation] ( {docs_url} ) pour plus de d\u00e9tails." + }, + "step": { + "user": { + "description": "\u00cates-vous s\u00fbr de vouloir configurer le Webhook Geofency ?", + "title": "Configurer le Webhook Geofency" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/.translations/ko.json b/homeassistant/components/geofency/.translations/ko.json index 8a857acbdc6..db60ec18fe1 100644 --- a/homeassistant/components/geofency/.translations/ko.json +++ b/homeassistant/components/geofency/.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\ub824\uba74 Geofency \uc5d0\uc11c Webhook \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 \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574 \uc8fc\uc138\uc694." + "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Geofency \uc5d0\uc11c Webhook \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 \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "step": { "user": { diff --git a/homeassistant/components/geofency/.translations/nl.json b/homeassistant/components/geofency/.translations/nl.json new file mode 100644 index 00000000000..04aec33b5d6 --- /dev/null +++ b/homeassistant/components/geofency/.translations/nl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Uw Home Assistant instantie moet toegankelijk zijn vanaf het internet om Geofency-berichten te ontvangen.", + "one_instance_allowed": "Slechts \u00e9\u00e9n instantie is nodig." + }, + "create_entry": { + "default": "Om locaties naar Home Assistant te sturen, moet u de Webhook-functie instellen in Geofency.\n\n Vul de volgende info in: \n\n - URL: `{webhook_url}` \n - Methode: POST \n\n Zie [de documentatie]({docs_url}) voor meer informatie." + }, + "step": { + "user": { + "description": "Weet u zeker dat u de Geofency Webhook wilt instellen?", + "title": "Geofency Webhook instellen" + } + }, + "title": "Geofency Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/.translations/ru.json b/homeassistant/components/geofency/.translations/ru.json index 34290b35f42..2460e28393a 100644 --- a/homeassistant/components/geofency/.translations/ru.json +++ b/homeassistant/components/geofency/.translations/ru.json @@ -9,10 +9,10 @@ }, "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 Geofency Webhook?", - "title": "Geofency Webhook" + "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 Geofency?", + "title": "Geofency" } }, - "title": "Geofency Webhook" + "title": "Geofency" } } \ No newline at end of file diff --git a/homeassistant/components/goalfeed.py b/homeassistant/components/goalfeed/__init__.py similarity index 100% rename from homeassistant/components/goalfeed.py rename to homeassistant/components/goalfeed/__init__.py diff --git a/homeassistant/components/google.py b/homeassistant/components/google/__init__.py similarity index 100% rename from homeassistant/components/google.py rename to homeassistant/components/google/__init__.py diff --git a/homeassistant/components/calendar/google.py b/homeassistant/components/google/calendar.py similarity index 100% rename from homeassistant/components/calendar/google.py rename to homeassistant/components/google/calendar.py diff --git a/homeassistant/components/tts/google.py b/homeassistant/components/google/tts.py similarity index 100% rename from homeassistant/components/tts/google.py rename to homeassistant/components/google/tts.py diff --git a/homeassistant/components/google_domains.py b/homeassistant/components/google_domains/__init__.py similarity index 100% rename from homeassistant/components/google_domains.py rename to homeassistant/components/google_domains/__init__.py diff --git a/homeassistant/components/google_pubsub/__init__.py b/homeassistant/components/google_pubsub/__init__.py new file mode 100644 index 00000000000..af8bb60f8b1 --- /dev/null +++ b/homeassistant/components/google_pubsub/__init__.py @@ -0,0 +1,99 @@ +""" +Support for Google Cloud Pub/Sub. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/google_pubsub/ +""" +import datetime +import json +import logging +import os +from typing import Any, Dict + +import voluptuous as vol + +from homeassistant.const import ( + EVENT_STATE_CHANGED, STATE_UNAVAILABLE, STATE_UNKNOWN) +from homeassistant.core import Event, HomeAssistant +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entityfilter import FILTER_SCHEMA + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['google-cloud-pubsub==0.39.1'] + +DOMAIN = 'google_pubsub' + +CONF_PROJECT_ID = 'project_id' +CONF_TOPIC_NAME = 'topic_name' +CONF_SERVICE_PRINCIPAL = 'credentials_json' +CONF_FILTER = 'filter' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_PROJECT_ID): cv.string, + vol.Required(CONF_TOPIC_NAME): cv.string, + vol.Required(CONF_SERVICE_PRINCIPAL): cv.string, + vol.Required(CONF_FILTER): FILTER_SCHEMA + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): + """Activate Google Pub/Sub component.""" + from google.cloud import pubsub_v1 + + config = yaml_config[DOMAIN] + project_id = config[CONF_PROJECT_ID] + topic_name = config[CONF_TOPIC_NAME] + service_principal_path = os.path.join(hass.config.config_dir, + config[CONF_SERVICE_PRINCIPAL]) + + if not os.path.isfile(service_principal_path): + _LOGGER.error("Path to credentials file cannot be found") + return False + + entities_filter = config[CONF_FILTER] + + publisher = (pubsub_v1 + .PublisherClient + .from_service_account_json(service_principal_path) + ) + + topic_path = publisher.topic_path(project_id, # pylint: disable=E1101 + topic_name) + + encoder = DateTimeJSONEncoder() + + def send_to_pubsub(event: Event): + """Send states to Pub/Sub.""" + state = event.data.get('new_state') + if (state is None + or state.state in (STATE_UNKNOWN, '', STATE_UNAVAILABLE) + or not entities_filter(state.entity_id)): + return + + as_dict = state.as_dict() + data = json.dumps( + obj=as_dict, + default=encoder.encode + ).encode('utf-8') + + publisher.publish(topic_path, data=data) + + hass.bus.listen(EVENT_STATE_CHANGED, send_to_pubsub) + + return True + + +class DateTimeJSONEncoder(json.JSONEncoder): + """Encode python objects. + + Additionally add encoding for datetime objects as isoformat. + """ + + def default(self, o): # pylint: disable=E0202 + """Implement encoding logic.""" + if isinstance(o, datetime.datetime): + return o.isoformat() + return super().default(o) diff --git a/homeassistant/components/googlehome/__init__.py b/homeassistant/components/googlehome/__init__.py new file mode 100644 index 00000000000..f2d5ad09350 --- /dev/null +++ b/homeassistant/components/googlehome/__init__.py @@ -0,0 +1,111 @@ +""" +Support Google Home units. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/googlehome/ +""" +import logging + +import asyncio +import voluptuous as vol +from homeassistant.const import CONF_DEVICES, CONF_HOST +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['googledevices==1.0.2'] + +DOMAIN = 'googlehome' +CLIENT = 'googlehome_client' + +NAME = 'GoogleHome' + +CONF_DEVICE_TYPES = 'device_types' +CONF_RSSI_THRESHOLD = 'rssi_threshold' +CONF_TRACK_ALARMS = 'track_alarms' + +DEVICE_TYPES = [1, 2, 3] +DEFAULT_RSSI_THRESHOLD = -70 + +DEVICE_CONFIG = vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_DEVICE_TYPES, + default=DEVICE_TYPES): vol.All(cv.ensure_list, + [vol.In(DEVICE_TYPES)]), + vol.Optional(CONF_RSSI_THRESHOLD, + default=DEFAULT_RSSI_THRESHOLD): vol.Coerce(int), + vol.Optional(CONF_TRACK_ALARMS, default=False): cv.boolean, +}) + + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [DEVICE_CONFIG]), + }), +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the Google Home component.""" + hass.data[DOMAIN] = {} + hass.data[CLIENT] = GoogleHomeClient(hass) + + for device in config[DOMAIN][CONF_DEVICES]: + hass.data[DOMAIN][device['host']] = {} + hass.async_create_task( + discovery.async_load_platform( + hass, 'device_tracker', DOMAIN, device, config)) + + if device[CONF_TRACK_ALARMS]: + hass.async_create_task( + discovery.async_load_platform( + hass, 'sensor', DOMAIN, device, config)) + + return True + + +class GoogleHomeClient: + """Handle all communication with the Google Home unit.""" + + def __init__(self, hass): + """Initialize the Google Home Client.""" + self.hass = hass + self._connected = None + + async def update_info(self, host): + """Update data from Google Home.""" + from googledevices.api.connect import Cast + _LOGGER.debug("Updating Google Home info for %s", host) + session = async_get_clientsession(self.hass) + + device_info = await Cast(host, self.hass.loop, session).info() + device_info_data = await device_info.get_device_info() + self._connected = bool(device_info_data) + + self.hass.data[DOMAIN][host]['info'] = device_info_data + + async def update_bluetooth(self, host): + """Update bluetooth from Google Home.""" + from googledevices.api.connect import Cast + _LOGGER.debug("Updating Google Home bluetooth for %s", host) + session = async_get_clientsession(self.hass) + + bluetooth = await Cast(host, self.hass.loop, session).bluetooth() + await bluetooth.scan_for_devices() + await asyncio.sleep(5) + bluetooth_data = await bluetooth.get_scan_result() + + self.hass.data[DOMAIN][host]['bluetooth'] = bluetooth_data + + async def update_alarms(self, host): + """Update alarms from Google Home.""" + from googledevices.api.connect import Cast + _LOGGER.debug("Updating Google Home bluetooth for %s", host) + session = async_get_clientsession(self.hass) + + assistant = await Cast(host, self.hass.loop, session).assistant() + alarms_data = await assistant.get_alarms() + + self.hass.data[DOMAIN][host]['alarms'] = alarms_data diff --git a/homeassistant/components/googlehome/device_tracker.py b/homeassistant/components/googlehome/device_tracker.py new file mode 100644 index 00000000000..c4b490ab316 --- /dev/null +++ b/homeassistant/components/googlehome/device_tracker.py @@ -0,0 +1,86 @@ +""" +Support for Google Home bluetooth tacker. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.googlehome/ +""" +import logging +from datetime import timedelta + +from homeassistant.components.device_tracker import DeviceScanner +from homeassistant.components.googlehome import ( + CLIENT, DOMAIN as GOOGLEHOME_DOMAIN, NAME) +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util import slugify + +DEPENDENCIES = ['googlehome'] + +DEFAULT_SCAN_INTERVAL = timedelta(seconds=10) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_scanner(hass, config, async_see, discovery_info=None): + """Validate the configuration and return a Google Home scanner.""" + if discovery_info is None: + _LOGGER.warning( + "To use this you need to configure the 'googlehome' component") + return False + scanner = GoogleHomeDeviceScanner(hass, hass.data[CLIENT], + discovery_info, async_see) + return await scanner.async_init() + + +class GoogleHomeDeviceScanner(DeviceScanner): + """This class queries a Google Home unit.""" + + def __init__(self, hass, client, config, async_see): + """Initialize the scanner.""" + self.async_see = async_see + self.hass = hass + self.rssi = config['rssi_threshold'] + self.device_types = config['device_types'] + self.host = config['host'] + self.client = client + + async def async_init(self): + """Further initialize connection to Google Home.""" + await self.client.update_info(self.host) + data = self.hass.data[GOOGLEHOME_DOMAIN][self.host] + info = data.get('info', {}) + connected = bool(info) + if connected: + await self.async_update() + async_track_time_interval(self.hass, + self.async_update, + DEFAULT_SCAN_INTERVAL) + return connected + + async def async_update(self, now=None): + """Ensure the information from Google Home is up to date.""" + _LOGGER.debug('Checking Devices on %s', self.host) + await self.client.update_bluetooth(self.host) + data = self.hass.data[GOOGLEHOME_DOMAIN][self.host] + info = data.get('info') + bluetooth = data.get('bluetooth') + if info is None or bluetooth is None: + return + google_home_name = info.get('name', NAME) + + for device in bluetooth: + if (device['device_type'] not in + self.device_types or device['rssi'] < self.rssi): + continue + + name = "{} {}".format(self.host, device['mac_address']) + + attributes = {} + attributes['btle_mac_address'] = device['mac_address'] + attributes['ghname'] = google_home_name + attributes['rssi'] = device['rssi'] + attributes['source_type'] = 'bluetooth' + if device['name']: + attributes['name'] = device['name'] + + await self.async_see(dev_id=slugify(name), + attributes=attributes) diff --git a/homeassistant/components/googlehome/sensor.py b/homeassistant/components/googlehome/sensor.py new file mode 100644 index 00000000000..90b9cda80bb --- /dev/null +++ b/homeassistant/components/googlehome/sensor.py @@ -0,0 +1,103 @@ +""" +Support for Google Home alarm sensor. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.googlehome/ +""" +import logging +from datetime import timedelta + +from homeassistant.components.googlehome import ( + CLIENT, DOMAIN as GOOGLEHOME_DOMAIN, NAME) +from homeassistant.const import DEVICE_CLASS_TIMESTAMP +from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt_util + + +DEPENDENCIES = ['googlehome'] + +SCAN_INTERVAL = timedelta(seconds=10) + +_LOGGER = logging.getLogger(__name__) + +ICON = 'mdi:alarm' + +SENSOR_TYPES = { + 'timer': "Timer", + 'alarm': "Alarm", +} + + +async def async_setup_platform(hass, config, + async_add_entities, discovery_info=None): + """Set up the googlehome sensor platform.""" + if discovery_info is None: + _LOGGER.warning( + "To use this you need to configure the 'googlehome' component") + return + + await hass.data[CLIENT].update_info(discovery_info['host']) + data = hass.data[GOOGLEHOME_DOMAIN][discovery_info['host']] + info = data.get('info', {}) + + devices = [] + for condition in SENSOR_TYPES: + device = GoogleHomeAlarm(hass.data[CLIENT], condition, + discovery_info, info.get('name', NAME)) + devices.append(device) + + async_add_entities(devices, True) + + +class GoogleHomeAlarm(Entity): + """Representation of a GoogleHomeAlarm.""" + + def __init__(self, client, condition, config, name): + """Initialize the GoogleHomeAlarm sensor.""" + self._host = config['host'] + self._client = client + self._condition = condition + self._name = None + self._state = None + self._available = True + self._name = "{} {}".format(name, SENSOR_TYPES[self._condition]) + + async def async_update(self): + """Update the data.""" + await self._client.update_alarms(self._host) + data = self.hass.data[GOOGLEHOME_DOMAIN][self._host] + + alarms = data.get('alarms')[self._condition] + if not alarms: + self._available = False + return + self._available = True + time_date = dt_util.utc_from_timestamp(min(element['fire_time'] + for element in alarms) + / 1000) + self._state = time_date.isoformat() + + @property + def state(self): + """Return the state.""" + return self._state + + @property + def name(self): + """Return the name.""" + return self._name + + @property + def device_class(self): + """Return the device class.""" + return DEVICE_CLASS_TIMESTAMP + + @property + def available(self): + """Return the availability state.""" + return self._available + + @property + def icon(self): + """Return the icon.""" + return ICON diff --git a/homeassistant/components/gpslogger/.translations/da.json b/homeassistant/components/gpslogger/.translations/da.json new file mode 100644 index 00000000000..6d5c2185718 --- /dev/null +++ b/homeassistant/components/gpslogger/.translations/da.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Dit Home Assistant system skal v\u00e6re tilg\u00e6ngeligt fra internettet for at modtage GPSLogger meddelelser.", + "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning" + }, + "create_entry": { + "default": "For at sende begivenheder til Home Assistant skal du konfigurere webhook funktionen i GPSLogger.\n\n Udfyld f\u00f8lgende oplysninger: \n\n - URL: `{webhook_url}`\n - Metode: POST\n \n Se [dokumentationen]({docs_url}) for yderligere oplysninger." + }, + "step": { + "user": { + "description": "Er du sikker p\u00e5 at du vil konfigurere GPSLogger Webhook?", + "title": "Konfigurer GPSLogger Webhook" + } + }, + "title": "GPSLogger Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/.translations/fr.json b/homeassistant/components/gpslogger/.translations/fr.json new file mode 100644 index 00000000000..ae2b2177712 --- /dev/null +++ b/homeassistant/components/gpslogger/.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 GPSLogger.", + "one_instance_allowed": "Une seule instance est n\u00e9cessaire." + }, + "create_entry": { + "default": "Pour envoyer des \u00e9v\u00e9nements \u00e0 Home Assistant, vous devez configurer la fonction Webhook dans GPSLogger. \n\n Remplissez les informations suivantes: \n\n - URL: ` {webhook_url} ` \n - M\u00e9thode: POST \n\n Voir [la documentation] ( {docs_url} ) pour plus de d\u00e9tails." + }, + "step": { + "user": { + "description": "\u00cates-vous s\u00fbr de vouloir configurer le Webhook GPSLogger ?", + "title": "Configurer le Webhook GPSLogger" + } + }, + "title": "Webhook GPSLogger" + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/.translations/ko.json b/homeassistant/components/gpslogger/.translations/ko.json index a65e51d7cae..2c8881034ff 100644 --- a/homeassistant/components/gpslogger/.translations/ko.json +++ b/homeassistant/components/gpslogger/.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\ub824\uba74 GPSLogger \uc5d0\uc11c Webhook \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 \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574 \uc8fc\uc138\uc694." + "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 GPSLogger \uc5d0\uc11c Webhook \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 \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "step": { "user": { diff --git a/homeassistant/components/gpslogger/.translations/nl.json b/homeassistant/components/gpslogger/.translations/nl.json new file mode 100644 index 00000000000..d0dece65a0f --- /dev/null +++ b/homeassistant/components/gpslogger/.translations/nl.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "GPSLogger Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/.translations/ru.json b/homeassistant/components/gpslogger/.translations/ru.json index 34b7e907288..ac9c1c2d43e 100644 --- a/homeassistant/components/gpslogger/.translations/ru.json +++ b/homeassistant/components/gpslogger/.translations/ru.json @@ -1,7 +1,8 @@ { "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 GPSLogger." + "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 GPSLogger.", + "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 GPSLogger.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\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\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 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." diff --git a/homeassistant/components/graphite.py b/homeassistant/components/graphite/__init__.py similarity index 100% rename from homeassistant/components/graphite.py rename to homeassistant/components/graphite/__init__.py diff --git a/homeassistant/components/greeneye_monitor.py b/homeassistant/components/greeneye_monitor/__init__.py similarity index 100% rename from homeassistant/components/greeneye_monitor.py rename to homeassistant/components/greeneye_monitor/__init__.py diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index b6dcd65fc2c..d1cd88a8438 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -23,6 +23,8 @@ from homeassistant.helpers.event import async_track_state_change import homeassistant.helpers.config_validation as cv from homeassistant.util.async_ import run_coroutine_threadsafe +from .reproduce_state import async_reproduce_states # noqa + DOMAIN = 'group' ENTITY_ID_FORMAT = DOMAIN + '.{}' diff --git a/homeassistant/components/group/reproduce_state.py b/homeassistant/components/group/reproduce_state.py new file mode 100644 index 00000000000..1cf1793e6f6 --- /dev/null +++ b/homeassistant/components/group/reproduce_state.py @@ -0,0 +1,28 @@ +"""Module that groups code required to handle state restore for component.""" +from typing import Iterable, Optional + +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.loader import bind_hass + + +@bind_hass +async def async_reproduce_states(hass: HomeAssistantType, + states: Iterable[State], + context: Optional[Context] = None) -> None: + """Reproduce component states.""" + from . import get_entity_ids + from homeassistant.helpers.state import async_reproduce_state + states_copy = [] + for state in states: + members = get_entity_ids(hass, state.entity_id) + for member in members: + states_copy.append( + State(member, + state.state, + state.attributes, + last_changed=state.last_changed, + last_updated=state.last_updated, + context=state.context)) + await async_reproduce_state(hass, states_copy, blocking=True, + context=context) diff --git a/homeassistant/components/sensor/habitica.py b/homeassistant/components/habitica/sensor.py similarity index 100% rename from homeassistant/components/sensor/habitica.py rename to homeassistant/components/habitica/sensor.py diff --git a/homeassistant/components/hangouts/.translations/da.json b/homeassistant/components/hangouts/.translations/da.json new file mode 100644 index 00000000000..079b57722e2 --- /dev/null +++ b/homeassistant/components/hangouts/.translations/da.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Google Hangouts er allerede konfigureret", + "unknown": "Ukendt fejl opstod" + }, + "error": { + "invalid_2fa": "Ugyldig 2-faktor godkendelse, pr\u00f8v venligst igen.", + "invalid_2fa_method": "Ugyldig 2FA-metode (Bekr\u00e6ft p\u00e5 telefon).", + "invalid_login": "Ugyldig login, pr\u00f8v venligst igen." + }, + "step": { + "2fa": { + "data": { + "2fa": "2FA pin" + }, + "title": "To-faktor autentificering" + }, + "user": { + "data": { + "email": "Email adresse", + "password": "Adgangskode" + }, + "title": "Google Hangouts login" + } + }, + "title": "Google Hangouts" + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/.translations/ko.json b/homeassistant/components/hangouts/.translations/ko.json index af0e76829e5..b1bcf5725be 100644 --- a/homeassistant/components/hangouts/.translations/ko.json +++ b/homeassistant/components/hangouts/.translations/ko.json @@ -5,7 +5,7 @@ "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { - "invalid_2fa": "2\ub2e8\uacc4 \uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574 \uc8fc\uc138\uc694.", + "invalid_2fa": "2\ub2e8\uacc4 \uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", "invalid_2fa_method": "2\ub2e8\uacc4 \uc778\uc99d \ubc29\ubc95\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. (\uc804\ud654\uae30\uc5d0\uc11c \ud655\uc778)", "invalid_login": "\uc798\ubabb\ub41c \ub85c\uadf8\uc778\uc785\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." }, diff --git a/homeassistant/components/notify/hangouts.py b/homeassistant/components/hangouts/notify.py similarity index 100% rename from homeassistant/components/notify/hangouts.py rename to homeassistant/components/hangouts/notify.py diff --git a/homeassistant/components/harmony/__init__.py b/homeassistant/components/harmony/__init__.py new file mode 100644 index 00000000000..25a33929c1a --- /dev/null +++ b/homeassistant/components/harmony/__init__.py @@ -0,0 +1,5 @@ +"""The harmony component. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/harmony/ +""" diff --git a/homeassistant/components/remote/harmony.py b/homeassistant/components/harmony/remote.py similarity index 99% rename from homeassistant/components/remote/harmony.py rename to homeassistant/components/harmony/remote.py index a5e4f5a8528..489fe9144f2 100644 --- a/homeassistant/components/remote/harmony.py +++ b/homeassistant/components/harmony/remote.py @@ -22,7 +22,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import PlatformNotReady from homeassistant.util import slugify -REQUIREMENTS = ['aioharmony==0.1.5'] +REQUIREMENTS = ['aioharmony==0.1.8'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hassio/services.yaml b/homeassistant/components/hassio/services.yaml new file mode 100644 index 00000000000..33574c5dd71 --- /dev/null +++ b/homeassistant/components/hassio/services.yaml @@ -0,0 +1,37 @@ +addon_install: + description: Install a HassIO docker addon. + fields: + addon: {description: Name of addon., example: smb_config} + version: {description: Optional or it will be use the latest version., example: '0.2'} +addon_start: + description: Start a HassIO docker addon. + fields: + addon: {description: Name of addon., example: smb_config} +addon_stop: + description: Stop a HassIO docker addon. + fields: + addon: {description: Name of addon., example: smb_config} +addon_uninstall: + description: Uninstall a HassIO docker addon. + fields: + addon: {description: Name of addon., example: smb_config} +addon_update: + description: Update a HassIO docker addon. + fields: + addon: {description: Name of addon., example: smb_config} + version: {description: Optional or it will be use the latest version., example: '0.2'} +homeassistant_update: + description: Update HomeAssistant docker image. + fields: + version: {description: Optional or it will be use the latest version., example: 0.40.1} +host_reboot: {description: Reboot host computer.} +host_shutdown: {description: Poweroff host computer.} +host_update: + description: Update host computer. + fields: + version: {description: Optional or it will be use the latest version., example: '0.3'} +supervisor_reload: {description: Reload HassIO supervisor addons/updates/configs.} +supervisor_update: + description: Update HassIO supervisor. + fields: + version: {description: Optional or it will be use the latest version., example: '0.3'} diff --git a/homeassistant/components/hdmi_cec.py b/homeassistant/components/hdmi_cec/__init__.py similarity index 100% rename from homeassistant/components/hdmi_cec.py rename to homeassistant/components/hdmi_cec/__init__.py diff --git a/homeassistant/components/media_player/hdmi_cec.py b/homeassistant/components/hdmi_cec/media_player.py similarity index 97% rename from homeassistant/components/media_player/hdmi_cec.py rename to homeassistant/components/hdmi_cec/media_player.py index d69d8a74ce6..6e691cad94f 100644 --- a/homeassistant/components/media_player/hdmi_cec.py +++ b/homeassistant/components/hdmi_cec/media_player.py @@ -7,10 +7,11 @@ https://home-assistant.io/components/hdmi_cec/ import logging from homeassistant.components.hdmi_cec import ATTR_NEW, CecDevice -from homeassistant.components.media_player import ( +from homeassistant.components.media_player import MediaPlayerDevice +from homeassistant.components.media_player.const import ( DOMAIN, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, MediaPlayerDevice) + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP) from homeassistant.const import ( STATE_IDLE, STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING) diff --git a/homeassistant/components/hdmi_cec/services.yaml b/homeassistant/components/hdmi_cec/services.yaml new file mode 100644 index 00000000000..bb0f5f932ae --- /dev/null +++ b/homeassistant/components/hdmi_cec/services.yaml @@ -0,0 +1,32 @@ +power_on: {description: Power on all devices which supports it.} +select_device: + description: Select HDMI device. + fields: + device: {description: 'Address of device to select. Can be entity_id, physical + address or alias from confuguration.', example: '"switch.hdmi_1" or "1.1.0.0" + or "01:10"'} +send_command: + description: Sends CEC command into HDMI CEC capable adapter. + fields: + att: + description: Optional parameters. + example: [0, 2] + cmd: {description: 'Command itself. Could be decimal number or string with hexadeximal + notation: "0x10".', example: 144 or "0x90"} + dst: {description: 'Destination for command. Could be decimal number or string + with hexadeximal notation: "0x10".', example: 5 or "0x5"} + raw: {description: 'Raw CEC command in format "00:00:00:00" where first two digits + are source and destination, second byte is command and optional other bytes + are command parameters. If raw command specified, other params are ignored.', + example: '"10:36"'} + src: {desctiption: 'Source of command. Could be decimal number or string with + hexadeximal notation: "0x10".', example: 12 or "0xc"} +standby: {description: Standby all devices which supports it.} +update: {description: Update devices state from network.} +volume: + description: Increase or decrease volume of system. + fields: + down: {description: Decreases volume x levels., example: 3} + mute: {description: 'Mutes audio system. Value should be on, off or toggle.', + example: toggle} + up: {description: Increases volume x levels., example: 3} diff --git a/homeassistant/components/switch/hdmi_cec.py b/homeassistant/components/hdmi_cec/switch.py similarity index 100% rename from homeassistant/components/switch/hdmi_cec.py rename to homeassistant/components/hdmi_cec/switch.py diff --git a/homeassistant/components/history.py b/homeassistant/components/history/__init__.py similarity index 100% rename from homeassistant/components/history.py rename to homeassistant/components/history/__init__.py diff --git a/homeassistant/components/history_graph.py b/homeassistant/components/history_graph/__init__.py similarity index 100% rename from homeassistant/components/history_graph.py rename to homeassistant/components/history_graph/__init__.py diff --git a/homeassistant/components/hive.py b/homeassistant/components/hive/__init__.py similarity index 100% rename from homeassistant/components/hive.py rename to homeassistant/components/hive/__init__.py diff --git a/homeassistant/components/binary_sensor/hive.py b/homeassistant/components/hive/binary_sensor.py similarity index 100% rename from homeassistant/components/binary_sensor/hive.py rename to homeassistant/components/hive/binary_sensor.py diff --git a/homeassistant/components/climate/hive.py b/homeassistant/components/hive/climate.py similarity index 100% rename from homeassistant/components/climate/hive.py rename to homeassistant/components/hive/climate.py diff --git a/homeassistant/components/light/hive.py b/homeassistant/components/hive/light.py similarity index 100% rename from homeassistant/components/light/hive.py rename to homeassistant/components/hive/light.py diff --git a/homeassistant/components/sensor/hive.py b/homeassistant/components/hive/sensor.py similarity index 100% rename from homeassistant/components/sensor/hive.py rename to homeassistant/components/hive/sensor.py diff --git a/homeassistant/components/switch/hive.py b/homeassistant/components/hive/switch.py similarity index 100% rename from homeassistant/components/switch/hive.py rename to homeassistant/components/hive/switch.py diff --git a/homeassistant/components/hlk_sw16.py b/homeassistant/components/hlk_sw16/__init__.py similarity index 100% rename from homeassistant/components/hlk_sw16.py rename to homeassistant/components/hlk_sw16/__init__.py diff --git a/homeassistant/components/switch/hlk_sw16.py b/homeassistant/components/hlk_sw16/switch.py similarity index 100% rename from homeassistant/components/switch/hlk_sw16.py rename to homeassistant/components/hlk_sw16/switch.py diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 5baed0294b8..ca1b560e336 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -124,6 +124,8 @@ class HomeAccessory(Accessory): """ battery_level = convert_to_float( new_state.attributes.get(ATTR_BATTERY_LEVEL)) + if battery_level is None: + return self._char_battery.set_value(battery_level) self._char_low_battery.set_value(battery_level < 20) _LOGGER.debug('%s: Updated battery level to %d', self.entity_id, diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index d0e3d52b363..1b2a4dbf05d 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -113,6 +113,7 @@ CHAR_OUTLET_IN_USE = 'OutletInUse' CHAR_ON = 'On' CHAR_POSITION_STATE = 'PositionState' CHAR_ROTATION_DIRECTION = 'RotationDirection' +CHAR_ROTATION_SPEED = 'RotationSpeed' CHAR_SATURATION = 'Saturation' CHAR_SERIAL_NUMBER = 'SerialNumber' CHAR_SMOKE_DETECTED = 'SmokeDetected' diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index 2b4e55c4c8d..dcc93b7cf9e 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -4,17 +4,20 @@ import logging from pyhap.const import CATEGORY_FAN from homeassistant.components.fan import ( - ATTR_DIRECTION, ATTR_OSCILLATING, DIRECTION_FORWARD, DIRECTION_REVERSE, - DOMAIN, SERVICE_OSCILLATE, SERVICE_SET_DIRECTION, SUPPORT_DIRECTION, - SUPPORT_OSCILLATE) + ATTR_DIRECTION, ATTR_OSCILLATING, ATTR_SPEED, ATTR_SPEED_LIST, + DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN, SERVICE_OSCILLATE, + SERVICE_SET_DIRECTION, SERVICE_SET_SPEED, SUPPORT_DIRECTION, + SUPPORT_OSCILLATE, SUPPORT_SET_SPEED) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_OFF, - SERVICE_TURN_ON, STATE_OFF, STATE_ON) + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_OFF, STATE_ON) from . import TYPES -from .accessories import HomeAccessory +from .accessories import debounce, HomeAccessory from .const import ( - CHAR_ACTIVE, CHAR_ROTATION_DIRECTION, CHAR_SWING_MODE, SERV_FANV2) + CHAR_ACTIVE, CHAR_ROTATION_DIRECTION, CHAR_ROTATION_SPEED, CHAR_SWING_MODE, + SERV_FANV2) +from .util import HomeKitSpeedMapping _LOGGER = logging.getLogger(__name__) @@ -41,12 +44,18 @@ class Fan(HomeAccessory): chars.append(CHAR_ROTATION_DIRECTION) if features & SUPPORT_OSCILLATE: chars.append(CHAR_SWING_MODE) + if features & SUPPORT_SET_SPEED: + speed_list = self.hass.states.get(self.entity_id) \ + .attributes.get(ATTR_SPEED_LIST) + self.speed_mapping = HomeKitSpeedMapping(speed_list) + chars.append(CHAR_ROTATION_SPEED) 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) self.char_direction = None + self.char_speed = None self.char_swing = None if CHAR_ROTATION_DIRECTION in chars: @@ -54,6 +63,10 @@ class Fan(HomeAccessory): CHAR_ROTATION_DIRECTION, value=0, setter_callback=self.set_direction) + if CHAR_ROTATION_SPEED in chars: + self.char_speed = serv_fan.configure_char( + CHAR_ROTATION_SPEED, value=0, setter_callback=self.set_speed) + if CHAR_SWING_MODE in chars: self.char_swing = serv_fan.configure_char( CHAR_SWING_MODE, value=0, setter_callback=self.set_oscillating) @@ -83,6 +96,15 @@ class Fan(HomeAccessory): ATTR_OSCILLATING: oscillating} self.call_service(DOMAIN, SERVICE_OSCILLATE, params, oscillating) + @debounce + def set_speed(self, value): + """Set state if call came from HomeKit.""" + _LOGGER.debug('%s: Set speed to %d', self.entity_id, value) + speed = self.speed_mapping.speed_to_states(value) + params = {ATTR_ENTITY_ID: self.entity_id, + ATTR_SPEED: speed} + self.call_service(DOMAIN, SERVICE_SET_SPEED, params, speed) + def update_state(self, new_state): """Update fan after state change.""" # Handle State @@ -104,6 +126,14 @@ class Fan(HomeAccessory): self.char_direction.set_value(hk_direction) self._flag[CHAR_ROTATION_DIRECTION] = False + # Handle Speed + if self.char_speed is not None: + speed = new_state.attributes.get(ATTR_SPEED) + hk_speed_value = self.speed_mapping.speed_to_homekit(speed) + if hk_speed_value is not None and \ + self.char_speed.value != hk_speed_value: + self.char_speed.set_value(hk_speed_value) + # Handle Oscillating if self.char_swing is not None: oscillating = new_state.attributes.get(ATTR_OSCILLATING) diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 10fdc07e7b4..f1327f8b527 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -1,17 +1,19 @@ """Collection of useful functions for the HomeKit component.""" +from collections import namedtuple, OrderedDict import logging import voluptuous as vol -from homeassistant.components import media_player -from homeassistant.core import split_entity_id +from homeassistant.components import fan, media_player from homeassistant.const import ( ATTR_CODE, ATTR_SUPPORTED_FEATURES, CONF_NAME, CONF_TYPE, TEMP_CELSIUS) +from homeassistant.core import split_entity_id import homeassistant.helpers.config_validation as cv import homeassistant.util.temperature as temp_util + from .const import ( - CONF_FEATURE, CONF_FEATURE_LIST, HOMEKIT_NOTIFY_ID, FEATURE_ON_OFF, - FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, TYPE_FAUCET, + CONF_FEATURE, CONF_FEATURE_LIST, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, + FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, HOMEKIT_NOTIFY_ID, TYPE_FAUCET, TYPE_OUTLET, TYPE_SHOWER, TYPE_SPRINKLER, TYPE_SWITCH, TYPE_VALVE) _LOGGER = logging.getLogger(__name__) @@ -61,7 +63,7 @@ def validate_entity_config(values): if domain in ('alarm_control_panel', 'lock'): config = CODE_SCHEMA(config) - elif domain == media_player.DOMAIN: + elif domain == media_player.const.DOMAIN: config = FEATURE_SCHEMA(config) feature_list = {} for feature in config[CONF_FEATURE_LIST]: @@ -88,14 +90,16 @@ def validate_media_player_features(state, feature_list): features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) supported_modes = [] - if features & (media_player.SUPPORT_TURN_ON | - media_player.SUPPORT_TURN_OFF): + if features & (media_player.const.SUPPORT_TURN_ON | + media_player.const.SUPPORT_TURN_OFF): supported_modes.append(FEATURE_ON_OFF) - if features & (media_player.SUPPORT_PLAY | media_player.SUPPORT_PAUSE): + if features & (media_player.const.SUPPORT_PLAY | + media_player.const.SUPPORT_PAUSE): supported_modes.append(FEATURE_PLAY_PAUSE) - if features & (media_player.SUPPORT_PLAY | media_player.SUPPORT_STOP): + if features & (media_player.const.SUPPORT_PLAY | + media_player.const.SUPPORT_STOP): supported_modes.append(FEATURE_PLAY_STOP) - if features & media_player.SUPPORT_VOLUME_MUTE: + if features & media_player.const.SUPPORT_VOLUME_MUTE: supported_modes.append(FEATURE_TOGGLE_MUTE) error_list = [] @@ -110,6 +114,50 @@ def validate_media_player_features(state, feature_list): return True +SpeedRange = namedtuple('SpeedRange', ('start', 'target')) +SpeedRange.__doc__ += """ Maps Home Assistant speed \ +values to percentage based HomeKit speeds. +start: Start of the range (inclusive). +target: Percentage to use to determine HomeKit percentages \ +from HomeAssistant speed. +""" + + +class HomeKitSpeedMapping: + """Supports conversion between Home Assistant and HomeKit fan speeds.""" + + def __init__(self, speed_list): + """Initialize a new SpeedMapping object.""" + if speed_list[0] != fan.SPEED_OFF: + _LOGGER.warning("%s does not contain the speed setting " + "%s as its first element. " + "Assuming that %s is equivalent to 'off'.", + speed_list, fan.SPEED_OFF, speed_list[0]) + self.speed_ranges = OrderedDict() + list_size = len(speed_list) + for index, speed in enumerate(speed_list): + # By dividing by list_size -1 the following + # desired attributes hold true: + # * index = 0 => 0%, equal to "off" + # * index = len(speed_list) - 1 => 100 % + # * all other indices are equally distributed + target = index * 100 / (list_size - 1) + start = index * 100 / list_size + self.speed_ranges[speed] = SpeedRange(start, target) + + def speed_to_homekit(self, speed): + """Map Home Assistant speed state to HomeKit speed.""" + speed_range = self.speed_ranges[speed] + return speed_range.target + + def speed_to_states(self, speed): + """Map HomeKit speed to Home Assistant speed state.""" + for state, speed_range in reversed(self.speed_ranges.items()): + if speed_range.start <= speed: + return state + return list(self.speed_ranges.keys())[0] + + def show_setup_message(hass, pincode): """Display persistent notification with setup information.""" pin = pincode.decode() diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 9a496d914fc..3439a23adb3 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -90,7 +90,7 @@ HM_DEVICE_TYPES = { 'IPShutterContact', 'HMWIOSwitch', 'MaxShutterContact', 'Rain', 'WiredSensor', 'PresenceIP', 'IPWeatherSensor', 'IPPassageSensor', 'SmartwareMotion', 'IPWeatherSensorPlus', 'MotionIPV2', 'WaterIP', - 'IPMultiIO'], + 'IPMultiIO', 'TiltIP'], DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt'], DISCOVER_LOCKS: ['KeyMatic'] } diff --git a/homeassistant/components/binary_sensor/homematic.py b/homeassistant/components/homematic/binary_sensor.py similarity index 100% rename from homeassistant/components/binary_sensor/homematic.py rename to homeassistant/components/homematic/binary_sensor.py diff --git a/homeassistant/components/climate/homematic.py b/homeassistant/components/homematic/climate.py similarity index 100% rename from homeassistant/components/climate/homematic.py rename to homeassistant/components/homematic/climate.py diff --git a/homeassistant/components/cover/homematic.py b/homeassistant/components/homematic/cover.py similarity index 100% rename from homeassistant/components/cover/homematic.py rename to homeassistant/components/homematic/cover.py diff --git a/homeassistant/components/light/homematic.py b/homeassistant/components/homematic/light.py similarity index 100% rename from homeassistant/components/light/homematic.py rename to homeassistant/components/homematic/light.py diff --git a/homeassistant/components/lock/homematic.py b/homeassistant/components/homematic/lock.py similarity index 100% rename from homeassistant/components/lock/homematic.py rename to homeassistant/components/homematic/lock.py diff --git a/homeassistant/components/notify/homematic.py b/homeassistant/components/homematic/notify.py similarity index 100% rename from homeassistant/components/notify/homematic.py rename to homeassistant/components/homematic/notify.py diff --git a/homeassistant/components/sensor/homematic.py b/homeassistant/components/homematic/sensor.py similarity index 100% rename from homeassistant/components/sensor/homematic.py rename to homeassistant/components/homematic/sensor.py diff --git a/homeassistant/components/switch/homematic.py b/homeassistant/components/homematic/switch.py similarity index 100% rename from homeassistant/components/switch/homematic.py rename to homeassistant/components/homematic/switch.py diff --git a/homeassistant/components/homematicip_cloud/.translations/da.json b/homeassistant/components/homematicip_cloud/.translations/da.json index 7473b4a7b86..4b8371fc748 100644 --- a/homeassistant/components/homematicip_cloud/.translations/da.json +++ b/homeassistant/components/homematicip_cloud/.translations/da.json @@ -1,14 +1,30 @@ { "config": { + "abort": { + "already_configured": "Access point er allerede konfigureret", + "connection_aborted": "Kunne ikke oprette forbindelse til HMIP-serveren", + "unknown": "Ukendt fejl opstod" + }, "error": { - "invalid_pin": "Ugyldig PIN, pr\u00f8v igen." + "invalid_pin": "Ugyldig PIN, pr\u00f8v igen.", + "press_the_button": "Tryk venligst p\u00e5 den bl\u00e5 knap.", + "register_failed": "Fejl ved registrering, pr\u00f8v venligst igen.", + "timeout_button": "Tryk p\u00e5 bl\u00e5 knap timeout, pr\u00f8v venligst igen." }, "step": { "init": { "data": { + "hapid": "Access point ID (SGTIN)", + "name": "Navn (valgfrit, bruges som pr\u00e6fiks til navnet for alle enheder)", "pin": "Pin kode (valgfri)" - } + }, + "title": "V\u00e6lg HomematicIP Access point" + }, + "link": { + "description": "Tryk p\u00e5 den bl\u00e5 knap p\u00e5 adgangspunktet og send knappen for at registrere HomematicIP med Home Assistant.\n\n ![Placering af knap p\u00e5 bridge](/static/images/config_flows/config_homematicip_cloud.png)", + "title": "Link adgangspunkt" } - } + }, + "title": "HomematicIP Cloud" } } \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index b0ea1a3b348..f048a50d1d0 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -19,7 +19,7 @@ from .const import ( from .device import HomematicipGenericDevice # noqa: F401 from .hap import HomematicipAuth, HomematicipHAP # noqa: F401 -REQUIREMENTS = ['homematicip==0.10.4'] +REQUIREMENTS = ['homematicip==0.10.5'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/alarm_control_panel/homematicip_cloud.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py similarity index 100% rename from homeassistant/components/alarm_control_panel/homematicip_cloud.py rename to homeassistant/components/homematicip_cloud/alarm_control_panel.py diff --git a/homeassistant/components/binary_sensor/homematicip_cloud.py b/homeassistant/components/homematicip_cloud/binary_sensor.py similarity index 100% rename from homeassistant/components/binary_sensor/homematicip_cloud.py rename to homeassistant/components/homematicip_cloud/binary_sensor.py diff --git a/homeassistant/components/climate/homematicip_cloud.py b/homeassistant/components/homematicip_cloud/climate.py similarity index 100% rename from homeassistant/components/climate/homematicip_cloud.py rename to homeassistant/components/homematicip_cloud/climate.py diff --git a/homeassistant/components/cover/homematicip_cloud.py b/homeassistant/components/homematicip_cloud/cover.py similarity index 86% rename from homeassistant/components/cover/homematicip_cloud.py rename to homeassistant/components/homematicip_cloud/cover.py index 27f26805e81..80fc8f7b430 100644 --- a/homeassistant/components/cover/homematicip_cloud.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -15,6 +15,9 @@ DEPENDENCIES = ['homematicip_cloud'] _LOGGER = logging.getLogger(__name__) +HMIP_COVER_OPEN = 0 +HMIP_COVER_CLOSED = 1 + async def async_setup_platform( hass, config, async_add_entities, discovery_info=None): @@ -47,23 +50,24 @@ class HomematicipCoverShutter(HomematicipGenericDevice, CoverDevice): async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" position = kwargs[ATTR_POSITION] - level = position / 100.0 + # HmIP cover is closed:1 -> open:0 + level = 1 - position / 100.0 await self._device.set_shutter_level(level) @property def is_closed(self): """Return if the cover is closed.""" if self._device.shutterLevel is not None: - return self._device.shutterLevel == 0 + return self._device.shutterLevel == HMIP_COVER_CLOSED return None async def async_open_cover(self, **kwargs): """Open the cover.""" - await self._device.set_shutter_level(1) + await self._device.set_shutter_level(HMIP_COVER_OPEN) async def async_close_cover(self, **kwargs): """Close the cover.""" - await self._device.set_shutter_level(0) + await self._device.set_shutter_level(HMIP_COVER_CLOSED) async def async_stop_cover(self, **kwargs): """Stop the device if in motion.""" diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 37507a1fca8..9af6669652d 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -2,7 +2,7 @@ import asyncio import logging -from homeassistant import config_entries +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -84,7 +84,6 @@ class HomematicipHAP: self._retry_task = None self._tries = 0 self._accesspoint_connected = True - self._retry_setup = None async def async_setup(self, tries=0): """Initialize connection.""" @@ -96,20 +95,7 @@ class HomematicipHAP: self.config_entry.data.get(HMIPC_NAME) ) except HmipcConnectionError: - retry_delay = 2 ** min(tries, 8) - _LOGGER.error("Error connecting to HomematicIP with HAP %s. " - "Retrying in %d seconds", - self.config_entry.data.get(HMIPC_HAPID), retry_delay) - - async def retry_setup(_now): - """Retry setup.""" - if await self.async_setup(tries + 1): - self.config_entry.state = config_entries.ENTRY_STATE_LOADED - - self._retry_setup = self.hass.helpers.event.async_call_later( - retry_delay, retry_setup) - - return False + raise ConfigEntryNotReady _LOGGER.info("Connected to HomematicIP with HAP %s", self.config_entry.data.get(HMIPC_HAPID)) @@ -209,8 +195,6 @@ class HomematicipHAP: async def async_reset(self): """Close the websocket connection.""" self._ws_close_requested = True - if self._retry_setup is not None: - self._retry_setup.cancel() if self._retry_task is not None: self._retry_task.cancel() await self.home.disable_events() diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py new file mode 100644 index 00000000000..5d604d2c665 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/light.py @@ -0,0 +1,257 @@ +""" +Support for HomematicIP Cloud lights. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.homematicip_cloud/ +""" +import logging + +from homeassistant.components.homematicip_cloud import ( + DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice) +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_COLOR_NAME, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, Light) + +DEPENDENCIES = ['homematicip_cloud'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_ENERGY_COUNTER = 'energy_counter_kwh' +ATTR_POWER_CONSUMPTION = 'power_consumption' +ATTR_PROFILE_MODE = 'profile_mode' + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Old way of setting up HomematicIP Cloud lights.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the HomematicIP Cloud lights from a config entry.""" + from homematicip.aio.device import AsyncBrandSwitchMeasuring, AsyncDimmer,\ + AsyncPluggableDimmer, AsyncBrandDimmer, AsyncFullFlushDimmer,\ + AsyncBrandSwitchNotificationLight + + home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home + devices = [] + for device in home.devices: + if isinstance(device, AsyncBrandSwitchMeasuring): + devices.append(HomematicipLightMeasuring(home, device)) + elif isinstance(device, AsyncBrandSwitchNotificationLight): + devices.append(HomematicipLight(home, device)) + devices.append(HomematicipNotificationLight( + home, device, device.topLightChannelIndex)) + devices.append(HomematicipNotificationLight( + home, device, device.bottomLightChannelIndex)) + elif isinstance(device, + (AsyncDimmer, AsyncPluggableDimmer, + AsyncBrandDimmer, AsyncFullFlushDimmer)): + devices.append(HomematicipDimmer(home, device)) + + if devices: + async_add_entities(devices) + + +class HomematicipLight(HomematicipGenericDevice, Light): + """Representation of a HomematicIP Cloud light device.""" + + def __init__(self, home, device): + """Initialize the light device.""" + super().__init__(home, device) + + @property + def is_on(self): + """Return true if device is on.""" + return self._device.on + + async def async_turn_on(self, **kwargs): + """Turn the device on.""" + await self._device.turn_on() + + async def async_turn_off(self, **kwargs): + """Turn the device off.""" + await self._device.turn_off() + + +class HomematicipLightMeasuring(HomematicipLight): + """Representation of a HomematicIP Cloud measuring light device.""" + + @property + def device_state_attributes(self): + """Return the state attributes of the generic device.""" + attr = super().device_state_attributes + if self._device.currentPowerConsumption > 0.05: + attr.update({ + ATTR_POWER_CONSUMPTION: + round(self._device.currentPowerConsumption, 2) + }) + attr.update({ + ATTR_ENERGY_COUNTER: round(self._device.energyCounter, 2) + }) + return attr + + +class HomematicipDimmer(HomematicipGenericDevice, Light): + """Representation of HomematicIP Cloud dimmer light device.""" + + def __init__(self, home, device): + """Initialize the dimmer light device.""" + super().__init__(home, device) + + @property + def is_on(self): + """Return true if device is on.""" + return self._device.dimLevel != 0 + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + return int(self._device.dimLevel*255) + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS + + async def async_turn_on(self, **kwargs): + """Turn the light on.""" + if ATTR_BRIGHTNESS in kwargs: + await self._device.set_dim_level(kwargs[ATTR_BRIGHTNESS]/255.0) + else: + await self._device.set_dim_level(1) + + async def async_turn_off(self, **kwargs): + """Turn the light off.""" + await self._device.set_dim_level(0) + + +class HomematicipNotificationLight(HomematicipGenericDevice, Light): + """Representation of HomematicIP Cloud dimmer light device.""" + + def __init__(self, home, device, channel_index): + """Initialize the dimmer light device.""" + self._channel_index = channel_index + if self._channel_index == 2: + super().__init__(home, device, 'Top') + else: + super().__init__(home, device, 'Bottom') + + from homematicip.base.enums import RGBColorState + self._color_switcher = { + RGBColorState.WHITE: [0.0, 0.0], + RGBColorState.RED: [0.0, 100.0], + RGBColorState.YELLOW: [60.0, 100.0], + RGBColorState.GREEN: [120.0, 100.0], + RGBColorState.TURQUOISE: [180.0, 100.0], + RGBColorState.BLUE: [240.0, 100.0], + RGBColorState.PURPLE: [300.0, 100.0] + } + + @property + def _channel(self): + return self._device.functionalChannels[self._channel_index] + + @property + def is_on(self): + """Return true if device is on.""" + return self._channel.dimLevel > 0.0 + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + return int(self._channel.dimLevel * 255) + + @property + def hs_color(self): + """Return the hue and saturation color value [float, float].""" + simple_rgb_color = self._channel.simpleRGBColorState + return self._color_switcher.get(simple_rgb_color, [0.0, 0.0]) + + @property + def device_state_attributes(self): + """Return the state attributes of the generic device.""" + attr = super().device_state_attributes + if self.is_on: + attr.update({ + ATTR_COLOR_NAME: + self._channel.simpleRGBColorState + }) + return attr + + @property + def name(self): + """Return the name of the generic device.""" + return "{} {}".format(super().name, 'Notification') + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return "{}_{}_{}".format(self.__class__.__name__, + self.post, + self._device.id) + + async def async_turn_on(self, **kwargs): + """Turn the light on.""" + # Use hs_color from kwargs, + # if not applicable use current hs_color. + hs_color = kwargs.get(ATTR_HS_COLOR, self.hs_color) + simple_rgb_color = _convert_color(hs_color) + + # Use brightness from kwargs, + # if not applicable use current brightness. + brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness) + + # If no kwargs, use default value. + if not kwargs: + brightness = 255 + + # Minimum brightness is 10, otherwise the led is disabled + brightness = max(10, brightness) + dim_level = brightness / 255.0 + + await self._device.set_rgb_dim_level( + self._channel_index, + simple_rgb_color, + dim_level) + + async def async_turn_off(self, **kwargs): + """Turn the light off.""" + simple_rgb_color = self._channel.simpleRGBColorState + await self._device.set_rgb_dim_level( + self._channel_index, + simple_rgb_color, 0.0) + + +def _convert_color(color): + """ + Convert the given color to the reduced RGBColorState color. + + RGBColorStat contains only 8 colors including white and black, + so a conversion is required. + """ + from homematicip.base.enums import RGBColorState + + if color is None: + return RGBColorState.WHITE + + hue = int(color[0]) + saturation = int(color[1]) + if saturation < 5: + return RGBColorState.WHITE + if 30 < hue <= 90: + return RGBColorState.YELLOW + if 90 < hue <= 160: + return RGBColorState.GREEN + if 150 < hue <= 210: + return RGBColorState.TURQUOISE + if 210 < hue <= 270: + return RGBColorState.BLUE + if 270 < hue <= 330: + return RGBColorState.PURPLE + return RGBColorState.RED diff --git a/homeassistant/components/sensor/homematicip_cloud.py b/homeassistant/components/homematicip_cloud/sensor.py similarity index 79% rename from homeassistant/components/sensor/homematicip_cloud.py rename to homeassistant/components/homematicip_cloud/sensor.py index 77feda20be1..911c00e45bc 100644 --- a/homeassistant/components/sensor/homematicip_cloud.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -7,11 +7,10 @@ https://home-assistant.io/components/sensor.homematicip_cloud/ import logging from homeassistant.components.homematicip_cloud import ( - HMIPC_HAPID, HomematicipGenericDevice) -from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN + DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice) from homeassistant.const import ( - DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, - TEMP_CELSIUS) + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS) _LOGGER = logging.getLogger(__name__) @@ -36,7 +35,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): AsyncHeatingThermostat, AsyncTemperatureHumiditySensorWithoutDisplay, AsyncTemperatureHumiditySensorDisplay, AsyncMotionDetectorIndoor, AsyncTemperatureHumiditySensorOutdoor, - AsyncMotionDetectorPushButton) + AsyncMotionDetectorPushButton, AsyncLightSensor, + AsyncPlugableSwitchMeasuring, AsyncBrandSwitchMeasuring, + AsyncFullFlushSwitchMeasuring) home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [HomematicipAccesspointStatus(home)] @@ -51,6 +52,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if isinstance(device, (AsyncMotionDetectorIndoor, AsyncMotionDetectorPushButton)): devices.append(HomematicipIlluminanceSensor(home, device)) + if isinstance(device, AsyncLightSensor): + devices.append(HomematicipLightSensor(home, device)) + if isinstance(device, (AsyncPlugableSwitchMeasuring, + AsyncBrandSwitchMeasuring, + AsyncFullFlushSwitchMeasuring)): + devices.append(HomematicipPowerSensor(home, device)) if devices: async_add_entities(devices) @@ -184,3 +191,30 @@ class HomematicipIlluminanceSensor(HomematicipGenericDevice): def unit_of_measurement(self): """Return the unit this state is expressed in.""" return 'lx' + + +class HomematicipLightSensor(HomematicipIlluminanceSensor): + """Represenation of a HomematicIP Illuminance device.""" + + @property + def state(self): + """Return the state.""" + return self._device.averageIllumination + + +class HomematicipPowerSensor(HomematicipGenericDevice): + """Represenation of a HomematicIP power measuring device.""" + + def __init__(self, home, device): + """Initialize the device.""" + super().__init__(home, device, 'Power') + + @property + def state(self): + """Represenation of the HomematicIP power comsumption value.""" + return self._device.currentPowerConsumption + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return 'W' diff --git a/homeassistant/components/switch/homematicip_cloud.py b/homeassistant/components/homematicip_cloud/switch.py similarity index 100% rename from homeassistant/components/switch/homematicip_cloud.py rename to homeassistant/components/homematicip_cloud/switch.py diff --git a/homeassistant/components/homeworks.py b/homeassistant/components/homeworks/__init__.py similarity index 100% rename from homeassistant/components/homeworks.py rename to homeassistant/components/homeworks/__init__.py diff --git a/homeassistant/components/light/homeworks.py b/homeassistant/components/homeworks/light.py similarity index 100% rename from homeassistant/components/light/homeworks.py rename to homeassistant/components/homeworks/light.py diff --git a/homeassistant/components/huawei_lte.py b/homeassistant/components/huawei_lte/__init__.py similarity index 100% rename from homeassistant/components/huawei_lte.py rename to homeassistant/components/huawei_lte/__init__.py diff --git a/homeassistant/components/device_tracker/huawei_lte.py b/homeassistant/components/huawei_lte/device_tracker.py similarity index 100% rename from homeassistant/components/device_tracker/huawei_lte.py rename to homeassistant/components/huawei_lte/device_tracker.py diff --git a/homeassistant/components/huawei_lte/notify.py b/homeassistant/components/huawei_lte/notify.py new file mode 100644 index 00000000000..a406a7ec2d8 --- /dev/null +++ b/homeassistant/components/huawei_lte/notify.py @@ -0,0 +1,60 @@ +"""Huawei LTE router platform for notify component. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.huawei_lte/ +""" + +import logging + +import voluptuous as vol +import attr + +from homeassistant.components.notify import ( + BaseNotificationService, ATTR_TARGET, PLATFORM_SCHEMA) +from homeassistant.const import CONF_RECIPIENT, CONF_URL +import homeassistant.helpers.config_validation as cv + +from ..huawei_lte import DATA_KEY + + +DEPENDENCIES = ['huawei_lte'] + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_URL): cv.url, + vol.Required(CONF_RECIPIENT): vol.All(cv.ensure_list, [cv.string]), +}) + + +async def async_get_service(hass, config, discovery_info=None): + """Get the notification service.""" + return HuaweiLteSmsNotificationService(hass, config) + + +@attr.s +class HuaweiLteSmsNotificationService(BaseNotificationService): + """Huawei LTE router SMS notification service.""" + + hass = attr.ib() + config = attr.ib() + + def send_message(self, message="", **kwargs): + """Send message to target numbers.""" + from huawei_lte_api.exceptions import ResponseErrorException + + targets = kwargs.get(ATTR_TARGET, self.config.get(CONF_RECIPIENT)) + if not targets or not message: + return + + data = self.hass.data[DATA_KEY].get_data(self.config) + if not data: + _LOGGER.error("Router not available") + return + + try: + resp = data.client.sms.send_sms( + phone_numbers=targets, message=message) + _LOGGER.debug("Sent to %s: %s", targets, resp) + except ResponseErrorException as ex: + _LOGGER.error("Could not send to %s: %s", targets, ex) diff --git a/homeassistant/components/sensor/huawei_lte.py b/homeassistant/components/huawei_lte/sensor.py similarity index 100% rename from homeassistant/components/sensor/huawei_lte.py rename to homeassistant/components/huawei_lte/sensor.py diff --git a/homeassistant/components/hue/.translations/da.json b/homeassistant/components/hue/.translations/da.json index 19e60b073d3..08bad3e91ea 100644 --- a/homeassistant/components/hue/.translations/da.json +++ b/homeassistant/components/hue/.translations/da.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/hue/__init__.py b/homeassistant/components/hue/__init__.py index 7618e702d04..0871d961a93 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -28,6 +28,8 @@ CONF_BRIDGES = "bridges" CONF_ALLOW_UNREACHABLE = 'allow_unreachable' DEFAULT_ALLOW_UNREACHABLE = False +DATA_CONFIGS = 'hue_configs' + PHUE_CONFIG_FILE = 'phue.conf' CONF_ALLOW_HUE_GROUPS = "allow_hue_groups" @@ -59,6 +61,7 @@ async def async_setup(hass, config): conf = {} hass.data[DOMAIN] = {} + hass.data[DATA_CONFIGS] = {} configured = configured_hosts(hass) # User has configured bridges @@ -71,7 +74,7 @@ async def async_setup(hass, config): host = bridge_conf[CONF_HOST] # Store config in hass.data so the config entry can find it - hass.data[DOMAIN][host] = bridge_conf + hass.data[DATA_CONFIGS][host] = bridge_conf # If configured, the bridge will be set up during config entry phase if host in configured: @@ -96,7 +99,7 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Set up a bridge from a config entry.""" host = entry.data['host'] - config = hass.data[DOMAIN].get(host) + config = hass.data[DATA_CONFIGS].get(host) if config is None: allow_unreachable = DEFAULT_ALLOW_UNREACHABLE @@ -106,11 +109,11 @@ async def async_setup_entry(hass, entry): allow_groups = config[CONF_ALLOW_HUE_GROUPS] bridge = HueBridge(hass, entry, allow_unreachable, allow_groups) - hass.data[DOMAIN][host] = bridge if not await bridge.async_setup(): return False + hass.data[DOMAIN][host] = bridge config = bridge.api.config device_registry = await dr.async_get_registry(hass) device_registry.async_get_or_create( diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 93241622f0b..6e3d818db68 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -5,6 +5,7 @@ import async_timeout import voluptuous as vol from homeassistant import config_entries +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv from .const import DOMAIN, LOGGER @@ -30,7 +31,6 @@ class HueBridge: self.allow_groups = allow_groups self.available = True self.api = None - self._cancel_retry_setup = None @property def host(self): @@ -59,20 +59,7 @@ class HueBridge: return False except CannotConnect: - retry_delay = 2 ** (tries + 1) - LOGGER.error("Error connecting to the Hue bridge at %s. Retrying " - "in %d seconds", host, 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 + raise ConfigEntryNotReady except Exception: # pylint: disable=broad-except LOGGER.exception('Unknown error connecting with Hue bridge at %s', @@ -97,13 +84,6 @@ class HueBridge: # The bridge can be in 3 states: # - Setup was successful, self.api is not None # - Authentication was wrong, self.api is None, not retrying setup. - # - Host was down. self.api is None, we're retrying setup - - # 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: diff --git a/homeassistant/components/hydrawise.py b/homeassistant/components/hydrawise/__init__.py similarity index 96% rename from homeassistant/components/hydrawise.py rename to homeassistant/components/hydrawise/__init__.py index 5a045a083b3..800d19d7efe 100644 --- a/homeassistant/components/hydrawise.py +++ b/homeassistant/components/hydrawise/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Hydrawise cloud. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/hydrawise/ -""" +"""Support for Hydrawise cloud.""" from datetime import timedelta import logging diff --git a/homeassistant/components/binary_sensor/hydrawise.py b/homeassistant/components/hydrawise/binary_sensor.py similarity index 93% rename from homeassistant/components/binary_sensor/hydrawise.py rename to homeassistant/components/hydrawise/binary_sensor.py index 38b660c506f..bfe7cbd5531 100644 --- a/homeassistant/components/binary_sensor/hydrawise.py +++ b/homeassistant/components/hydrawise/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for Hydrawise sprinkler. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.hydrawise/ -""" +"""Support for Hydrawise sprinkler binary sensors.""" import logging import voluptuous as vol diff --git a/homeassistant/components/sensor/hydrawise.py b/homeassistant/components/hydrawise/sensor.py similarity index 93% rename from homeassistant/components/sensor/hydrawise.py rename to homeassistant/components/hydrawise/sensor.py index bff99ddc501..575686b92cd 100644 --- a/homeassistant/components/sensor/hydrawise.py +++ b/homeassistant/components/hydrawise/sensor.py @@ -1,9 +1,4 @@ -""" -Support for Hydrawise sprinkler. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.hydrawise/ -""" +"""Support for Hydrawise sprinkler sensors.""" import logging import voluptuous as vol diff --git a/homeassistant/components/switch/hydrawise.py b/homeassistant/components/hydrawise/switch.py similarity index 90% rename from homeassistant/components/switch/hydrawise.py rename to homeassistant/components/hydrawise/switch.py index 6b73333431d..a6a8b9c54cf 100644 --- a/homeassistant/components/switch/hydrawise.py +++ b/homeassistant/components/hydrawise/switch.py @@ -1,9 +1,4 @@ -""" -Support for Hydrawise cloud. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.hydrawise/ -""" +"""Support for Hydrawise cloud switches.""" import logging import voluptuous as vol @@ -36,12 +31,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensors = [] for sensor_type in config.get(CONF_MONITORED_CONDITIONS): - # create a switch for each zone + # Create a switch for each zone for zone in hydrawise.relays: sensors.append( - HydrawiseSwitch(default_watering_timer, - zone, - sensor_type)) + HydrawiseSwitch(default_watering_timer, zone, sensor_type)) add_entities(sensors, True) diff --git a/homeassistant/components/idteck_prox.py b/homeassistant/components/idteck_prox/__init__.py similarity index 91% rename from homeassistant/components/idteck_prox.py rename to homeassistant/components/idteck_prox/__init__.py index 90c07c487b6..8ec6f49b95d 100644 --- a/homeassistant/components/idteck_prox.py +++ b/homeassistant/components/idteck_prox/__init__.py @@ -1,8 +1,4 @@ -"""Component for interfacing RFK101 proximity card readers. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/idteck_prox/ -""" +"""Component for interfacing RFK101 proximity card readers.""" import logging import voluptuous as vol diff --git a/homeassistant/components/ifttt/.translations/da.json b/homeassistant/components/ifttt/.translations/da.json new file mode 100644 index 00000000000..25c502ed05e --- /dev/null +++ b/homeassistant/components/ifttt/.translations/da.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Dit Home Assistant system skal v\u00e6re tilg\u00e6ngeligt fra internettet for at modtage IFTTT meddelelser.", + "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning" + }, + "create_entry": { + "default": "For at sende begivenheder til Home Assistant skal du bruge handlingen \"Foretag en web foresp\u00f8rgsel\" fra [IFTTT Webhook applet] ({applet_url}).\n\n Udfyld f\u00f8lgende oplysninger: \n\n - URL: `{webhook_url}`\n - Metode: POST\n - Indholdstype: application/json\n\n Se [dokumentationen] ({docs_url}) om hvordan du konfigurerer automatiseringer til at h\u00e5ndtere indg\u00e5ende data." + }, + "step": { + "user": { + "description": "Er du sikker p\u00e5 at du vil oprette IFTTT?", + "title": "Konfigurer IFTTT Webhook Applet" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/ko.json b/homeassistant/components/ifttt/.translations/ko.json index bb54f7ef6cb..75bdd0d99c8 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\nHome 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\nHome 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/__init__.py b/homeassistant/components/ifttt/__init__.py index 209bbcef607..7dee93b2260 100644 --- a/homeassistant/components/ifttt/__init__.py +++ b/homeassistant/components/ifttt/__init__.py @@ -1,9 +1,4 @@ -""" -Support to trigger Maker IFTTT recipes. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/ifttt/ -""" +"""Support to trigger Maker IFTTT recipes.""" import json import logging diff --git a/homeassistant/components/alarm_control_panel/ifttt.py b/homeassistant/components/ifttt/alarm_control_panel.py similarity index 96% rename from homeassistant/components/alarm_control_panel/ifttt.py rename to homeassistant/components/ifttt/alarm_control_panel.py index fe9c96a0083..bbb9a02c8a1 100644 --- a/homeassistant/components/alarm_control_panel/ifttt.py +++ b/homeassistant/components/ifttt/alarm_control_panel.py @@ -1,9 +1,4 @@ -""" -Interfaces with alarm control panels that have to be controlled through IFTTT. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/alarm_control_panel.ifttt/ -""" +"""Support for alarm control panels that can be controlled through IFTTT.""" import logging import re diff --git a/homeassistant/components/binary_sensor/ihc.py b/homeassistant/components/ihc/binary_sensor.py similarity index 100% rename from homeassistant/components/binary_sensor/ihc.py rename to homeassistant/components/ihc/binary_sensor.py diff --git a/homeassistant/components/light/ihc.py b/homeassistant/components/ihc/light.py similarity index 100% rename from homeassistant/components/light/ihc.py rename to homeassistant/components/ihc/light.py diff --git a/homeassistant/components/sensor/ihc.py b/homeassistant/components/ihc/sensor.py similarity index 100% rename from homeassistant/components/sensor/ihc.py rename to homeassistant/components/ihc/sensor.py diff --git a/homeassistant/components/switch/ihc.py b/homeassistant/components/ihc/switch.py similarity index 100% rename from homeassistant/components/switch/ihc.py rename to homeassistant/components/ihc/switch.py diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index b2cbb2b2391..f854384bb03 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -60,6 +60,7 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ vol.Optional(CONF_CONFIDENCE, default=DEFAULT_CONFIDENCE): vol.All(vol.Coerce(float), vol.Range(min=0, max=100)), }) +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE.extend(PLATFORM_SCHEMA.schema) SERVICE_SCAN_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, diff --git a/homeassistant/components/influxdb.py b/homeassistant/components/influxdb/__init__.py similarity index 98% rename from homeassistant/components/influxdb.py rename to homeassistant/components/influxdb/__init__.py index 2b9a5d9e193..b421960b51f 100644 --- a/homeassistant/components/influxdb.py +++ b/homeassistant/components/influxdb/__init__.py @@ -1,9 +1,4 @@ -""" -A component which allows you to send data to an Influx database. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/influxdb/ -""" +"""Support for sending data to an Influx database.""" import logging import re import queue diff --git a/homeassistant/components/input_boolean.py b/homeassistant/components/input_boolean/__init__.py similarity index 94% rename from homeassistant/components/input_boolean.py rename to homeassistant/components/input_boolean/__init__.py index 896de61130c..246af2613a7 100644 --- a/homeassistant/components/input_boolean.py +++ b/homeassistant/components/input_boolean/__init__.py @@ -1,9 +1,4 @@ -""" -Component to keep track of user controlled booleans for within automation. - -For more details about this component, please refer to the documentation -at https://home-assistant.io/components/input_boolean/ -""" +"""Support to keep track of user controlled booleans for within automation.""" import logging import voluptuous as vol diff --git a/homeassistant/components/input_boolean/services.yaml b/homeassistant/components/input_boolean/services.yaml new file mode 100644 index 00000000000..e49d46c9b86 --- /dev/null +++ b/homeassistant/components/input_boolean/services.yaml @@ -0,0 +1,12 @@ +toggle: + description: Toggles an input boolean. + fields: + entity_id: {description: Entity id of the input boolean to toggle., example: input_boolean.notify_alerts} +turn_off: + description: Turns off an input boolean + fields: + entity_id: {description: Entity id of the input boolean to turn off., example: input_boolean.notify_alerts} +turn_on: + description: Turns on an input boolean. + fields: + entity_id: {description: Entity id of the input boolean to turn on., example: input_boolean.notify_alerts} diff --git a/homeassistant/components/input_datetime.py b/homeassistant/components/input_datetime/__init__.py similarity index 96% rename from homeassistant/components/input_datetime.py rename to homeassistant/components/input_datetime/__init__.py index 63dcc364c9c..34faffd2028 100644 --- a/homeassistant/components/input_datetime.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -1,9 +1,4 @@ -""" -Component to offer a way to select a date and / or a time. - -For more details about this component, please refer to the documentation -at https://home-assistant.io/components/input_datetime/ -""" +"""Support to select a date and/or a time.""" import logging import datetime diff --git a/homeassistant/components/input_number.py b/homeassistant/components/input_number/__init__.py similarity index 96% rename from homeassistant/components/input_number.py rename to homeassistant/components/input_number/__init__.py index 8cfa7abaf20..d9d3ac8bbc0 100644 --- a/homeassistant/components/input_number.py +++ b/homeassistant/components/input_number/__init__.py @@ -1,9 +1,4 @@ -""" -Component to offer a way to set a numeric value from a slider or text box. - -For more details about this component, please refer to the documentation -at https://home-assistant.io/components/input_number/ -""" +"""Support to set a numeric value from a slider or text box.""" import logging import voluptuous as vol diff --git a/homeassistant/components/input_number/services.yaml b/homeassistant/components/input_number/services.yaml new file mode 100644 index 00000000000..650abc056a9 --- /dev/null +++ b/homeassistant/components/input_number/services.yaml @@ -0,0 +1,16 @@ +decrement: + description: Decrement the value of an input number entity by its stepping. + fields: + entity_id: {description: Entity id of the input number the should be decremented., + example: input_number.threshold} +increment: + description: Increment the value of an input number entity by its stepping. + fields: + entity_id: {description: Entity id of the input number the should be incremented., + example: input_number.threshold} +set_value: + description: Set the value of an input number entity. + fields: + entity_id: {description: Entity id of the input number to set the new value., + example: input_number.threshold} + value: {description: The target value the entity should be set to., example: 42} diff --git a/homeassistant/components/input_select.py b/homeassistant/components/input_select/__init__.py similarity index 96% rename from homeassistant/components/input_select.py rename to homeassistant/components/input_select/__init__.py index fc858e75397..fd3e4335c33 100644 --- a/homeassistant/components/input_select.py +++ b/homeassistant/components/input_select/__init__.py @@ -1,9 +1,4 @@ -""" -Component to offer a way to select an option from a list. - -For more details about this component, please refer to the documentation -at https://home-assistant.io/components/input_select/ -""" +"""Support to select an option from a list.""" import logging import voluptuous as vol diff --git a/homeassistant/components/input_select/services.yaml b/homeassistant/components/input_select/services.yaml new file mode 100644 index 00000000000..8084e56b731 --- /dev/null +++ b/homeassistant/components/input_select/services.yaml @@ -0,0 +1,22 @@ +select_next: + description: Select the next options of an input select entity. + fields: + entity_id: {description: Entity id of the input select to select the next value + for., example: input_select.my_select} +select_option: + description: Select an option of an input select entity. + fields: + entity_id: {description: Entity id of the input select to select the value., example: input_select.my_select} + option: {description: Option to be selected., example: '"Item A"'} +select_previous: + description: Select the previous options of an input select entity. + fields: + entity_id: {description: Entity id of the input select to select the previous + value for., example: input_select.my_select} +set_options: + description: Set the options of an input select entity. + fields: + entity_id: {description: Entity id of the input select to set the new options + for., example: input_select.my_select} + options: {description: Options for the input select entity., example: '["Item + A", "Item B", "Item C"]'} diff --git a/homeassistant/components/input_text.py b/homeassistant/components/input_text/__init__.py similarity index 96% rename from homeassistant/components/input_text.py rename to homeassistant/components/input_text/__init__.py index 580337a3af3..48a467b54a2 100644 --- a/homeassistant/components/input_text.py +++ b/homeassistant/components/input_text/__init__.py @@ -1,9 +1,4 @@ -""" -Component to offer a way to enter a value into a text box. - -For more details about this component, please refer to the documentation -at https://home-assistant.io/components/input_text/ -""" +"""Support to enter a value into a text box.""" import logging import voluptuous as vol diff --git a/homeassistant/components/input_text/services.yaml b/homeassistant/components/input_text/services.yaml new file mode 100644 index 00000000000..219eecf2fd6 --- /dev/null +++ b/homeassistant/components/input_text/services.yaml @@ -0,0 +1,6 @@ +set_value: + description: Set the value of an input text entity. + fields: + entity_id: {description: Entity id of the input text to set the new value., example: input_text.text1} + value: {description: The target value the entity should be set to., example: This + is an example text} diff --git a/homeassistant/components/binary_sensor/insteon.py b/homeassistant/components/insteon/binary_sensor.py similarity index 100% rename from homeassistant/components/binary_sensor/insteon.py rename to homeassistant/components/insteon/binary_sensor.py diff --git a/homeassistant/components/cover/insteon.py b/homeassistant/components/insteon/cover.py similarity index 100% rename from homeassistant/components/cover/insteon.py rename to homeassistant/components/insteon/cover.py diff --git a/homeassistant/components/fan/insteon.py b/homeassistant/components/insteon/fan.py similarity index 100% rename from homeassistant/components/fan/insteon.py rename to homeassistant/components/insteon/fan.py diff --git a/homeassistant/components/light/insteon.py b/homeassistant/components/insteon/light.py similarity index 100% rename from homeassistant/components/light/insteon.py rename to homeassistant/components/insteon/light.py diff --git a/homeassistant/components/sensor/insteon.py b/homeassistant/components/insteon/sensor.py similarity index 100% rename from homeassistant/components/sensor/insteon.py rename to homeassistant/components/insteon/sensor.py diff --git a/homeassistant/components/switch/insteon.py b/homeassistant/components/insteon/switch.py similarity index 100% rename from homeassistant/components/switch/insteon.py rename to homeassistant/components/insteon/switch.py diff --git a/homeassistant/components/insteon_local.py b/homeassistant/components/insteon_local/__init__.py similarity index 81% rename from homeassistant/components/insteon_local.py rename to homeassistant/components/insteon_local/__init__.py index 003714d0f94..f73c46746f0 100644 --- a/homeassistant/components/insteon_local.py +++ b/homeassistant/components/insteon_local/__init__.py @@ -1,9 +1,4 @@ -""" -Local support for Insteon. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/insteon_local/ -""" +"""Local support for Insteon.""" import logging _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/insteon_plm.py b/homeassistant/components/insteon_plm/__init__.py similarity index 80% rename from homeassistant/components/insteon_plm.py rename to homeassistant/components/insteon_plm/__init__.py index b3011e9d7bd..5ff492b6f6c 100644 --- a/homeassistant/components/insteon_plm.py +++ b/homeassistant/components/insteon_plm/__init__.py @@ -1,9 +1,4 @@ -""" -Support for INSTEON PowerLinc Modem. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/insteon_plm/ -""" +"""Support for INSTEON PowerLinc Modem.""" import logging _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/intent_script.py b/homeassistant/components/intent_script/__init__.py similarity index 100% rename from homeassistant/components/intent_script.py rename to homeassistant/components/intent_script/__init__.py diff --git a/homeassistant/components/introduction.py b/homeassistant/components/introduction/__init__.py similarity index 89% rename from homeassistant/components/introduction.py rename to homeassistant/components/introduction/__init__.py index 17de7fcd6ca..8a2d72ebbdd 100644 --- a/homeassistant/components/introduction.py +++ b/homeassistant/components/introduction/__init__.py @@ -1,9 +1,4 @@ -""" -Component that will help guide the user taking its first steps. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/introduction/ -""" +"""Component that will help guide the user taking its first steps.""" import logging import voluptuous as vol diff --git a/homeassistant/components/ios/.translations/da.json b/homeassistant/components/ios/.translations/da.json new file mode 100644 index 00000000000..4a900097b14 --- /dev/null +++ b/homeassistant/components/ios/.translations/da.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning af Home Assistant iOS" + }, + "step": { + "confirm": { + "description": "Er du sikker p\u00e5 at du vil konfigurere Home Assistant iOS?", + "title": "Home Assistant iOS" + } + }, + "title": "Home Assistant iOS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/ru.json b/homeassistant/components/ios/.translations/ru.json index fdcc964a0e6..282715ebb3b 100644 --- a/homeassistant/components/ios/.translations/ru.json +++ b/homeassistant/components/ios/.translations/ru.json @@ -5,7 +5,7 @@ }, "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 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 Home Assistant iOS?", + "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 Home Assistant iOS?", "title": "Home Assistant iOS" } }, diff --git a/homeassistant/components/notify/ios.py b/homeassistant/components/ios/notify.py similarity index 100% rename from homeassistant/components/notify/ios.py rename to homeassistant/components/ios/notify.py diff --git a/homeassistant/components/sensor/ios.py b/homeassistant/components/ios/sensor.py similarity index 100% rename from homeassistant/components/sensor/ios.py rename to homeassistant/components/ios/sensor.py diff --git a/homeassistant/components/iota.py b/homeassistant/components/iota/__init__.py similarity index 92% rename from homeassistant/components/iota.py rename to homeassistant/components/iota/__init__.py index 717213da9ac..e28de61aad0 100644 --- a/homeassistant/components/iota.py +++ b/homeassistant/components/iota/__init__.py @@ -1,9 +1,4 @@ -""" -Support for IOTA wallets. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/iota/ -""" +"""Support for IOTA wallets.""" import logging from datetime import timedelta diff --git a/homeassistant/components/sensor/iota.py b/homeassistant/components/iota/sensor.py similarity index 92% rename from homeassistant/components/sensor/iota.py rename to homeassistant/components/iota/sensor.py index 961cd119d78..5cd5db6169b 100644 --- a/homeassistant/components/sensor/iota.py +++ b/homeassistant/components/iota/sensor.py @@ -1,9 +1,4 @@ -""" -Support for IOTA wallets. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/iota -""" +"""Support for IOTA wallet sensors.""" import logging from datetime import timedelta @@ -26,12 +21,10 @@ SCAN_INTERVAL = timedelta(minutes=3) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the IOTA sensor.""" - # Add sensors for wallet balance iota_config = discovery_info sensors = [IotaBalanceSensor(wallet, iota_config) for wallet in iota_config[CONF_WALLETS]] - # Add sensor for node information sensors.append(IotaNodeSensor(iota_config=iota_config)) add_entities(sensors) diff --git a/homeassistant/components/ipma/.translations/ca.json b/homeassistant/components/ipma/.translations/ca.json new file mode 100644 index 00000000000..29dbaa4f58d --- /dev/null +++ b/homeassistant/components/ipma/.translations/ca.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "El nom ja existeix" + }, + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nom" + }, + "description": "Instituto Portugu\u00eas do Mar e Atmosfera", + "title": "Ubicaci\u00f3" + } + }, + "title": "Servei meteorol\u00f2gic portugu\u00e8s (IPMA)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/.translations/da.json b/homeassistant/components/ipma/.translations/da.json new file mode 100644 index 00000000000..080c41429ba --- /dev/null +++ b/homeassistant/components/ipma/.translations/da.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Navnet findes allerede" + }, + "step": { + "user": { + "data": { + "latitude": "Breddegrad", + "longitude": "L\u00e6ngdegrad", + "name": "Navn" + }, + "description": "Instituto Portugu\u00eas do Mar e Atmosfera", + "title": "Beliggenhed" + } + }, + "title": "Portugisisk vejrservice (IPMA)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/.translations/de.json b/homeassistant/components/ipma/.translations/de.json new file mode 100644 index 00000000000..9e717b77843 --- /dev/null +++ b/homeassistant/components/ipma/.translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Name existiert bereits" + }, + "step": { + "user": { + "data": { + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad", + "name": "Name" + }, + "description": "Instituto Portugu\u00eas do Mar e Atmosfera", + "title": "Standort" + } + }, + "title": "Portugiesischer Wetterdienst (IPMA)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/.translations/en.json b/homeassistant/components/ipma/.translations/en.json new file mode 100644 index 00000000000..15459b91f2a --- /dev/null +++ b/homeassistant/components/ipma/.translations/en.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Name already exists" + }, + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Name" + }, + "description": "Instituto Portugu\u00eas do Mar e Atmosfera", + "title": "Location" + } + }, + "title": "Portuguese weather service (IPMA)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/.translations/ko.json b/homeassistant/components/ipma/.translations/ko.json new file mode 100644 index 00000000000..828733c9195 --- /dev/null +++ b/homeassistant/components/ipma/.translations/ko.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "\uc774\ub984\uc774 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "latitude": "\uc704\ub3c4", + "longitude": "\uacbd\ub3c4", + "name": "\uc774\ub984" + }, + "description": "\ud3ec\ub974\ud22c\uac08 \ud574\uc591 \ubc0f \ub300\uae30 \uc5f0\uad6c\uc18c (Instituto Portugu\u00eas do Mar e Atmosfera)", + "title": "\uc704\uce58" + } + }, + "title": "\ud3ec\ub974\ud22c\uac08 \uae30\uc0c1 \uc11c\ube44\uc2a4 (IPMA)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/.translations/lb.json b/homeassistant/components/ipma/.translations/lb.json new file mode 100644 index 00000000000..c9eb3a01941 --- /dev/null +++ b/homeassistant/components/ipma/.translations/lb.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Numm g\u00ebtt et schonn" + }, + "step": { + "user": { + "data": { + "latitude": "Breedegrad", + "longitude": "L\u00e4ngegrad", + "name": "Numm" + }, + "description": "Instituto Portugu\u00eas do Mar e Atmosfera", + "title": "Uertschaft" + } + }, + "title": "Portugisesche Wieder D\u00e9ngscht (IPMA)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/.translations/nl.json b/homeassistant/components/ipma/.translations/nl.json new file mode 100644 index 00000000000..bc10eb3573e --- /dev/null +++ b/homeassistant/components/ipma/.translations/nl.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Naam bestaat al" + }, + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Naam" + }, + "description": "Instituto Portugu\u00eas do Mar e Atmosfera", + "title": "Locatie" + } + }, + "title": "Portugese weerservice (IPMA)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/.translations/pl.json b/homeassistant/components/ipma/.translations/pl.json new file mode 100644 index 00000000000..735f5a4a126 --- /dev/null +++ b/homeassistant/components/ipma/.translations/pl.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Nazwa ju\u017c istnieje" + }, + "step": { + "user": { + "data": { + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna", + "name": "Nazwa" + }, + "description": "Portugalski Instytut Morza i Atmosfery", + "title": "Lokalizacja" + } + }, + "title": "Portugalski serwis pogodowy (IPMA)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/.translations/pt.json b/homeassistant/components/ipma/.translations/pt.json new file mode 100644 index 00000000000..2ddeb9a4b33 --- /dev/null +++ b/homeassistant/components/ipma/.translations/pt.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Nome j\u00e1 existente" + }, + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nome" + }, + "description": "Instituto Portugu\u00eas do Mar e Atmosfera", + "title": "Localiza\u00e7\u00e3o" + } + }, + "title": "Servi\u00e7o Meteorol\u00f3gico Portugu\u00eas (IPMA)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/.translations/ru.json b/homeassistant/components/ipma/.translations/ru.json new file mode 100644 index 00000000000..f49852d5c0c --- /dev/null +++ b/homeassistant/components/ipma/.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" + }, + "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" + }, + "description": "\u041f\u043e\u0440\u0442\u0443\u0433\u0430\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0441\u0442\u0438\u0442\u0443\u0442 \u043c\u043e\u0440\u044f \u0438 \u0430\u0442\u043c\u043e\u0441\u0444\u0435\u0440\u044b", + "title": "\u041c\u0435\u0441\u0442\u043e\u043d\u0430\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435" + } + }, + "title": "\u041c\u0435\u0442\u0435\u043e\u0440\u043e\u043b\u043e\u0433\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u041f\u043e\u0440\u0442\u0443\u0433\u0430\u043b\u0438\u0438 (IPMA)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/.translations/zh-Hant.json b/homeassistant/components/ipma/.translations/zh-Hant.json new file mode 100644 index 00000000000..25c832e51c6 --- /dev/null +++ b/homeassistant/components/ipma/.translations/zh-Hant.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "\u8a72\u540d\u7a31\u5df2\u5b58\u5728" + }, + "step": { + "user": { + "data": { + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6", + "name": "\u540d\u7a31" + }, + "description": "Instituto Portugu\u00eas do Mar e Atmosfera", + "title": "\u5ea7\u6a19" + } + }, + "title": "\u8461\u8404\u7259\u6c23\u8c61\u670d\u52d9\uff08IPMA\uff09" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/__init__.py b/homeassistant/components/ipma/__init__.py new file mode 100644 index 00000000000..87f62371b55 --- /dev/null +++ b/homeassistant/components/ipma/__init__.py @@ -0,0 +1,31 @@ +""" +Component for the Portuguese weather service - IPMA. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/ipma/ +""" +from homeassistant.core import Config, HomeAssistant +from .config_flow import IpmaFlowHandler # noqa +from .const import DOMAIN # noqa + +DEFAULT_NAME = 'ipma' + + +async def async_setup(hass: HomeAssistant, config: Config) -> bool: + """Set up configured IPMA.""" + # No support for component configuration + return True + + +async def async_setup_entry(hass, config_entry): + """Set up IPMA station 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, config_entry): + """Unload a config entry.""" + await hass.config_entries.async_forward_entry_unload( + config_entry, 'weather') + return True diff --git a/homeassistant/components/ipma/config_flow.py b/homeassistant/components/ipma/config_flow.py new file mode 100644 index 00000000000..bb42a00742e --- /dev/null +++ b/homeassistant/components/ipma/config_flow.py @@ -0,0 +1,53 @@ +"""Config flow to configure IPMA component.""" +import voluptuous as vol + +from homeassistant import config_entries, data_entry_flow +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +import homeassistant.helpers.config_validation as cv + +from .const import DOMAIN, HOME_LOCATION_NAME + + +@config_entries.HANDLERS.register(DOMAIN) +class IpmaFlowHandler(data_entry_flow.FlowHandler): + """Config flow for IPMA component.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self): + """Init IpmaFlowHandler.""" + 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: + if user_input[CONF_NAME] not in\ + self.hass.config_entries.async_entries(DOMAIN): + return self.async_create_entry( + title=user_input[CONF_NAME], + data=user_input, + ) + + self._errors[CONF_NAME] = 'name_exists' + + # default location is set hass configuration + return await self._show_config_form( + name=HOME_LOCATION_NAME, + latitude=self.hass.config.latitude, + longitude=self.hass.config.longitude) + + async def _show_config_form(self, name=None, latitude=None, + longitude=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, + ) diff --git a/homeassistant/components/ipma/const.py b/homeassistant/components/ipma/const.py new file mode 100644 index 00000000000..dbb19294541 --- /dev/null +++ b/homeassistant/components/ipma/const.py @@ -0,0 +1,14 @@ +"""Constants in ipma component.""" +import logging + +from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN + +DOMAIN = 'ipma' + +HOME_LOCATION_NAME = 'Home' + +ENTITY_ID_SENSOR_FORMAT = WEATHER_DOMAIN + ".ipma_{}" +ENTITY_ID_SENSOR_FORMAT_HOME = ENTITY_ID_SENSOR_FORMAT.format( + HOME_LOCATION_NAME) + +_LOGGER = logging.getLogger('homeassistant.components.ipma') diff --git a/homeassistant/components/ipma/strings.json b/homeassistant/components/ipma/strings.json new file mode 100644 index 00000000000..f22d1b62fe4 --- /dev/null +++ b/homeassistant/components/ipma/strings.json @@ -0,0 +1,19 @@ +{ + "config": { + "title": "Portuguese weather service (IPMA)", + "step": { + "user": { + "title": "Location", + "description": "Instituto Português do Mar e Atmosfera", + "data": { + "name": "Name", + "latitude": "Latitude", + "longitude": "Longitude" + } + } + }, + "error": { + "name_exists": "Name already exists" + } + } +} diff --git a/homeassistant/components/weather/ipma.py b/homeassistant/components/ipma/weather.py similarity index 82% rename from homeassistant/components/weather/ipma.py rename to homeassistant/components/ipma/weather.py index fda0fef4f25..ec9b6fec2e8 100644 --- a/homeassistant/components/weather/ipma.py +++ b/homeassistant/components/ipma/weather.py @@ -20,7 +20,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers import config_validation as cv from homeassistant.util import Throttle -REQUIREMENTS = ['pyipma==1.1.6'] +REQUIREMENTS = ['pyipma==1.2.1'] _LOGGER = logging.getLogger(__name__) @@ -56,7 +56,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the ipma platform.""" + """Set up the ipma platform. + + Deprecated. + """ + _LOGGER.warning('Loading IPMA via platform config is deprecated') + latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) @@ -64,6 +69,23 @@ async def async_setup_platform(hass, config, async_add_entities, _LOGGER.error("Latitude or longitude not set in Home Assistant config") return + station = await async_get_station(hass, latitude, longitude) + + async_add_entities([IPMAWeather(station, config)], True) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add a weather entity from a config_entry.""" + latitude = config_entry.data[CONF_LATITUDE] + longitude = config_entry.data[CONF_LONGITUDE] + + station = await async_get_station(hass, latitude, longitude) + + async_add_entities([IPMAWeather(station, config_entry.data)], True) + + +async def async_get_station(hass, latitude, longitude): + """Retrieve weather station, station name to be used as the entity name.""" from pyipma import Station websession = async_get_clientsession(hass) @@ -74,7 +96,7 @@ async def async_setup_platform(hass, config, async_add_entities, _LOGGER.debug("Initializing for coordinates %s, %s -> station %s", latitude, longitude, station.local) - async_add_entities([IPMAWeather(station, config)], True) + return station class IPMAWeather(WeatherEntity): @@ -103,6 +125,11 @@ class IPMAWeather(WeatherEntity): self._forecast = await self._station.forecast() self._description = self._forecast[0].description + @property + def unique_id(self) -> str: + """Return a unique id.""" + return '{}, {}'.format(self._station.latitude, self._station.longitude) + @property def attribution(self): """Return the attribution.""" @@ -125,26 +152,41 @@ class IPMAWeather(WeatherEntity): @property def temperature(self): """Return the current temperature.""" + if not self._condition: + return None + return self._condition.temperature @property def pressure(self): """Return the current pressure.""" + if not self._condition: + return None + return self._condition.pressure @property def humidity(self): """Return the name of the sensor.""" + if not self._condition: + return None + return self._condition.humidity @property def wind_speed(self): """Return the current windspeed.""" + if not self._condition: + return None + return self._condition.windspeed @property def wind_bearing(self): """Return the current wind bearing (degrees).""" + if not self._condition: + return None + return self._condition.winddirection @property diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994/__init__.py similarity index 99% rename from homeassistant/components/isy994.py rename to homeassistant/components/isy994/__init__.py index a9916ed54fe..2b5f8fcb13f 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994/__init__.py @@ -265,7 +265,7 @@ def _is_sensor_a_binary_sensor(hass: HomeAssistant, node) -> bool: def _categorize_nodes(hass: HomeAssistant, nodes, ignore_identifier: str, - sensor_identifier: str)-> None: + sensor_identifier: str) -> None: """Sort the nodes to their proper domains.""" for (path, node) in nodes: ignored = ignore_identifier in path or ignore_identifier in node.name diff --git a/homeassistant/components/binary_sensor/isy994.py b/homeassistant/components/isy994/binary_sensor.py similarity index 100% rename from homeassistant/components/binary_sensor/isy994.py rename to homeassistant/components/isy994/binary_sensor.py diff --git a/homeassistant/components/cover/isy994.py b/homeassistant/components/isy994/cover.py similarity index 100% rename from homeassistant/components/cover/isy994.py rename to homeassistant/components/isy994/cover.py diff --git a/homeassistant/components/fan/isy994.py b/homeassistant/components/isy994/fan.py similarity index 100% rename from homeassistant/components/fan/isy994.py rename to homeassistant/components/isy994/fan.py diff --git a/homeassistant/components/light/isy994.py b/homeassistant/components/isy994/light.py similarity index 100% rename from homeassistant/components/light/isy994.py rename to homeassistant/components/isy994/light.py diff --git a/homeassistant/components/lock/isy994.py b/homeassistant/components/isy994/lock.py similarity index 100% rename from homeassistant/components/lock/isy994.py rename to homeassistant/components/isy994/lock.py diff --git a/homeassistant/components/sensor/isy994.py b/homeassistant/components/isy994/sensor.py similarity index 100% rename from homeassistant/components/sensor/isy994.py rename to homeassistant/components/isy994/sensor.py diff --git a/homeassistant/components/switch/isy994.py b/homeassistant/components/isy994/switch.py similarity index 100% rename from homeassistant/components/switch/isy994.py rename to homeassistant/components/isy994/switch.py diff --git a/homeassistant/components/itach/__init__.py b/homeassistant/components/itach/__init__.py new file mode 100644 index 00000000000..de43b41fdb7 --- /dev/null +++ b/homeassistant/components/itach/__init__.py @@ -0,0 +1 @@ +"""Support for itach devices.""" diff --git a/homeassistant/components/remote/itach.py b/homeassistant/components/itach/remote.py similarity index 93% rename from homeassistant/components/remote/itach.py rename to homeassistant/components/itach/remote.py index e7f23dfcd13..beb773838fb 100644 --- a/homeassistant/components/remote/itach.py +++ b/homeassistant/components/itach/remote.py @@ -1,10 +1,4 @@ -""" -Support for iTach IR Devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/remote.itach/ -""" - +"""Support for iTach IR devices.""" import logging import voluptuous as vol @@ -38,7 +32,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_CONNADDR): vol.Coerce(int), vol.Required(CONF_COMMANDS): vol.All(cv.ensure_list, [{ vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_DATA): cv.string + vol.Required(CONF_DATA): cv.string, }]) }]) }) diff --git a/homeassistant/components/joaoapps_join.py b/homeassistant/components/joaoapps_join/__init__.py similarity index 95% rename from homeassistant/components/joaoapps_join.py rename to homeassistant/components/joaoapps_join/__init__.py index b5bcb1e1a8a..adc856bdd3a 100644 --- a/homeassistant/components/joaoapps_join.py +++ b/homeassistant/components/joaoapps_join/__init__.py @@ -1,9 +1,4 @@ -""" -Component for Joaoapps Join services. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/join/ -""" +"""Support for Joaoapps Join services.""" import logging import voluptuous as vol @@ -16,6 +11,7 @@ REQUIREMENTS = ['python-join-api==0.0.2'] _LOGGER = logging.getLogger(__name__) DOMAIN = 'joaoapps_join' + CONF_DEVICE_ID = 'device_id' CONF_DEVICE_IDS = 'device_ids' CONF_DEVICE_NAMES = 'device_names' @@ -26,7 +22,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_DEVICE_ID): cv.string, vol.Optional(CONF_DEVICE_IDS): cv.string, vol.Optional(CONF_DEVICE_NAMES): cv.string, - vol.Optional(CONF_NAME): cv.string + vol.Optional(CONF_NAME): cv.string, }]) }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/notify/joaoapps_join.py b/homeassistant/components/joaoapps_join/notify.py similarity index 93% rename from homeassistant/components/notify/joaoapps_join.py rename to homeassistant/components/joaoapps_join/notify.py index a75ff9cd165..c586147d632 100644 --- a/homeassistant/components/notify/joaoapps_join.py +++ b/homeassistant/components/joaoapps_join/notify.py @@ -1,9 +1,4 @@ -""" -Join platform for notify component. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/notify.join/ -""" +"""Support for Join notifications.""" import logging import voluptuous as vol from homeassistant.components.notify import ( diff --git a/homeassistant/components/juicenet.py b/homeassistant/components/juicenet/__init__.py similarity index 88% rename from homeassistant/components/juicenet.py rename to homeassistant/components/juicenet/__init__.py index 55567d45879..f62331d1502 100644 --- a/homeassistant/components/juicenet.py +++ b/homeassistant/components/juicenet/__init__.py @@ -1,10 +1,4 @@ -""" -Support for Juicenet cloud. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/juicenet -""" - +"""Support for Juicenet cloud.""" import logging import voluptuous as vol @@ -22,7 +16,7 @@ DOMAIN = 'juicenet' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ - vol.Required(CONF_ACCESS_TOKEN): cv.string + vol.Required(CONF_ACCESS_TOKEN): cv.string, }) }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/sensor/juicenet.py b/homeassistant/components/juicenet/sensor.py similarity index 94% rename from homeassistant/components/sensor/juicenet.py rename to homeassistant/components/juicenet/sensor.py index 18725394a1f..e3786627075 100644 --- a/homeassistant/components/sensor/juicenet.py +++ b/homeassistant/components/juicenet/sensor.py @@ -1,19 +1,14 @@ -""" -Support for monitoring juicenet/juicepoint/juicebox based EVSE sensors. - -For more details about this platform, please refer to the documentation at -at https://home-assistant.io/components/sensor.juicenet/ -""" - +"""Support for monitoring juicenet/juicepoint/juicebox based EVSE sensors.""" import logging from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.entity import Entity from homeassistant.components.juicenet import JuicenetDevice, DOMAIN -DEPENDENCIES = ['juicenet'] _LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['juicenet'] + SENSOR_TYPES = { 'status': ['Charging Status', None], 'temperature': ['Temperature', TEMP_CELSIUS], diff --git a/homeassistant/components/keyboard.py b/homeassistant/components/keyboard/__init__.py similarity index 90% rename from homeassistant/components/keyboard.py rename to homeassistant/components/keyboard/__init__.py index 16253ba271a..44accca2f56 100644 --- a/homeassistant/components/keyboard.py +++ b/homeassistant/components/keyboard/__init__.py @@ -1,9 +1,4 @@ -""" -Provides functionality to emulate keyboard presses on host machine. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/keyboard/ -""" +"""Support to emulate keyboard presses on host machine.""" import voluptuous as vol from homeassistant.const import ( diff --git a/homeassistant/components/keyboard_remote.py b/homeassistant/components/keyboard_remote/__init__.py similarity index 97% rename from homeassistant/components/keyboard_remote.py rename to homeassistant/components/keyboard_remote/__init__.py index e02c2ee5475..e786fe458a8 100644 --- a/homeassistant/components/keyboard_remote.py +++ b/homeassistant/components/keyboard_remote/__init__.py @@ -1,9 +1,4 @@ -""" -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/ -""" +"""Receive signals from a keyboard and use it as a remote control.""" # pylint: disable=import-error import threading import logging diff --git a/homeassistant/components/kira.py b/homeassistant/components/kira/__init__.py similarity index 94% rename from homeassistant/components/kira.py rename to homeassistant/components/kira/__init__.py index 3a5ee25f05e..d60d8e0cfeb 100644 --- a/homeassistant/components/kira.py +++ b/homeassistant/components/kira/__init__.py @@ -1,9 +1,4 @@ -""" -KIRA interface to receive UDP packets from an IR-IP bridge. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/kira/ -""" +"""KIRA interface to receive UDP packets from an IR-IP bridge.""" import logging import os @@ -13,7 +8,7 @@ import yaml from homeassistant.const import ( CONF_DEVICE, CONF_HOST, CONF_NAME, CONF_PORT, CONF_SENSORS, CONF_TYPE, - EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN) + EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN, CONF_CODE) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv @@ -26,7 +21,6 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_HOST = "0.0.0.0" DEFAULT_PORT = 65432 -CONF_CODE = "code" CONF_REPEAT = "repeat" CONF_REMOTES = "remotes" CONF_SENSOR = "sensor" diff --git a/homeassistant/components/remote/kira.py b/homeassistant/components/kira/remote.py similarity index 89% rename from homeassistant/components/remote/kira.py rename to homeassistant/components/kira/remote.py index 24fc54ee78c..8ddf0858e16 100644 --- a/homeassistant/components/remote/kira.py +++ b/homeassistant/components/kira/remote.py @@ -1,9 +1,4 @@ -""" -Support for Keene Electronics IR-IP devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/remote.kira/ -""" +"""Support for Keene Electronics IR-IP devices.""" import functools as ft import logging @@ -15,7 +10,7 @@ DOMAIN = 'kira' _LOGGER = logging.getLogger(__name__) -CONF_REMOTE = "remote" +CONF_REMOTE = 'remote' def setup_platform(hass, config, add_entities, discovery_info=None): diff --git a/homeassistant/components/sensor/kira.py b/homeassistant/components/kira/sensor.py similarity index 90% rename from homeassistant/components/sensor/kira.py rename to homeassistant/components/kira/sensor.py index ced17281cc8..8885ebcbe24 100644 --- a/homeassistant/components/sensor/kira.py +++ b/homeassistant/components/kira/sensor.py @@ -1,9 +1,4 @@ -""" -KIRA interface to receive UDP packets from an IR-IP bridge. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.kira/ -""" +"""KIRA interface to receive UDP packets from an IR-IP bridge.""" import logging from homeassistant.const import CONF_DEVICE, CONF_NAME, STATE_UNKNOWN diff --git a/homeassistant/components/knx.py b/homeassistant/components/knx/__init__.py similarity index 98% rename from homeassistant/components/knx.py rename to homeassistant/components/knx/__init__.py index 366ec4405cd..fdaba5e5709 100644 --- a/homeassistant/components/knx.py +++ b/homeassistant/components/knx/__init__.py @@ -1,10 +1,4 @@ -""" -Connects to KNX platform. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/knx/ -""" - +"""Support KNX devices.""" import logging import voluptuous as vol @@ -17,7 +11,9 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.script import Script -REQUIREMENTS = ['xknx==0.9.3'] +REQUIREMENTS = ['xknx==0.9.4'] + +_LOGGER = logging.getLogger(__name__) DOMAIN = "knx" DATA_KNX = "data_knx" @@ -39,8 +35,6 @@ SERVICE_KNX_ATTR_PAYLOAD = "payload" ATTR_DISCOVER_DEVICES = 'devices' -_LOGGER = logging.getLogger(__name__) - TUNNELING_SCHEMA = vol.Schema({ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_KNX_LOCAL_IP): cv.string, diff --git a/homeassistant/components/binary_sensor/knx.py b/homeassistant/components/knx/binary_sensor.py similarity index 95% rename from homeassistant/components/binary_sensor/knx.py rename to homeassistant/components/knx/binary_sensor.py index a89d5d1c945..ca7037fe81d 100644 --- a/homeassistant/components/binary_sensor/knx.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -1,10 +1,4 @@ -""" -Support for KNX/IP binary sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.knx/ -""" - +"""Support for KNX/IP binary sensors.""" import voluptuous as vol from homeassistant.components.binary_sensor import ( @@ -35,7 +29,7 @@ DEPENDENCIES = ['knx'] AUTOMATION_SCHEMA = vol.Schema({ vol.Optional(CONF_HOOK, default=CONF_DEFAULT_HOOK): cv.string, vol.Optional(CONF_COUNTER, default=CONF_DEFAULT_COUNTER): cv.port, - vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA + vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA, }) AUTOMATIONS_SCHEMA = vol.All( diff --git a/homeassistant/components/climate/knx.py b/homeassistant/components/knx/climate.py similarity index 98% rename from homeassistant/components/climate/knx.py rename to homeassistant/components/knx/climate.py index 77d995af2a1..82eaa52ae5a 100644 --- a/homeassistant/components/climate/knx.py +++ b/homeassistant/components/knx/climate.py @@ -1,10 +1,4 @@ -""" -Support for KNX/IP climate devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/climate.knx/ -""" - +"""Support for KNX/IP climate devices.""" import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.climate import ( diff --git a/homeassistant/components/cover/knx.py b/homeassistant/components/knx/cover.py similarity index 96% rename from homeassistant/components/cover/knx.py rename to homeassistant/components/knx/cover.py index 4173db5f450..9423983f9f7 100644 --- a/homeassistant/components/cover/knx.py +++ b/homeassistant/components/knx/cover.py @@ -1,10 +1,4 @@ -""" -Support for KNX/IP covers. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/cover.knx/ -""" - +"""Support for KNX/IP covers.""" import voluptuous as vol from homeassistant.components.cover import ( @@ -49,8 +43,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up cover(s) for KNX platform.""" if discovery_info is not None: async_add_entities_discovery(hass, discovery_info, async_add_entities) diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py new file mode 100644 index 00000000000..f2a6f15e08b --- /dev/null +++ b/homeassistant/components/knx/light.py @@ -0,0 +1,291 @@ +"""Support for KNX/IP lights.""" +from enum import Enum + +import voluptuous as vol + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, PLATFORM_SCHEMA, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, + Light) +from homeassistant.const import CONF_NAME +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util + +from homeassistant.components.knx import ATTR_DISCOVER_DEVICES, DATA_KNX + + +CONF_ADDRESS = 'address' +CONF_STATE_ADDRESS = 'state_address' +CONF_BRIGHTNESS_ADDRESS = 'brightness_address' +CONF_BRIGHTNESS_STATE_ADDRESS = 'brightness_state_address' +CONF_COLOR_ADDRESS = 'color_address' +CONF_COLOR_STATE_ADDRESS = 'color_state_address' +CONF_COLOR_TEMP_ADDRESS = 'color_temperature_address' +CONF_COLOR_TEMP_STATE_ADDRESS = 'color_temperature_state_address' +CONF_COLOR_TEMP_MODE = 'color_temperature_mode' +CONF_MIN_KELVIN = 'min_kelvin' +CONF_MAX_KELVIN = 'max_kelvin' + +DEFAULT_NAME = 'KNX Light' +DEFAULT_COLOR = [255, 255, 255] +DEFAULT_BRIGHTNESS = 255 +DEFAULT_COLOR_TEMP_MODE = 'absolute' +DEFAULT_MIN_KELVIN = 2700 # 370 mireds +DEFAULT_MAX_KELVIN = 6000 # 166 mireds +DEPENDENCIES = ['knx'] + + +class ColorTempModes(Enum): + """Color temperature modes for config validation.""" + + absolute = "DPT-7.600" + relative = "DPT-5.001" + + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_ADDRESS): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_STATE_ADDRESS): cv.string, + vol.Optional(CONF_BRIGHTNESS_ADDRESS): cv.string, + vol.Optional(CONF_BRIGHTNESS_STATE_ADDRESS): cv.string, + vol.Optional(CONF_COLOR_ADDRESS): cv.string, + vol.Optional(CONF_COLOR_STATE_ADDRESS): cv.string, + vol.Optional(CONF_COLOR_TEMP_ADDRESS): cv.string, + vol.Optional(CONF_COLOR_TEMP_STATE_ADDRESS): cv.string, + vol.Optional(CONF_COLOR_TEMP_MODE, default=DEFAULT_COLOR_TEMP_MODE): + cv.enum(ColorTempModes), + vol.Optional(CONF_MIN_KELVIN, default=DEFAULT_MIN_KELVIN): + vol.All(vol.Coerce(int), vol.Range(min=1)), + vol.Optional(CONF_MAX_KELVIN, default=DEFAULT_MAX_KELVIN): + vol.All(vol.Coerce(int), vol.Range(min=1)), +}) + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up lights for KNX platform.""" + if discovery_info is not None: + async_add_entities_discovery(hass, discovery_info, async_add_entities) + else: + async_add_entities_config(hass, config, async_add_entities) + + +@callback +def async_add_entities_discovery(hass, discovery_info, async_add_entities): + """Set up lights for KNX platform configured via xknx.yaml.""" + entities = [] + for device_name in discovery_info[ATTR_DISCOVER_DEVICES]: + device = hass.data[DATA_KNX].xknx.devices[device_name] + entities.append(KNXLight(device)) + async_add_entities(entities) + + +@callback +def async_add_entities_config(hass, config, async_add_entities): + """Set up light for KNX platform configured within platform.""" + import xknx + + group_address_tunable_white = None + group_address_tunable_white_state = None + group_address_color_temp = None + group_address_color_temp_state = None + if config[CONF_COLOR_TEMP_MODE] == ColorTempModes.absolute: + group_address_color_temp = config.get(CONF_COLOR_TEMP_ADDRESS) + group_address_color_temp_state = \ + config.get(CONF_COLOR_TEMP_STATE_ADDRESS) + elif config[CONF_COLOR_TEMP_MODE] == ColorTempModes.relative: + group_address_tunable_white = config.get(CONF_COLOR_TEMP_ADDRESS) + group_address_tunable_white_state = \ + config.get(CONF_COLOR_TEMP_STATE_ADDRESS) + + light = xknx.devices.Light( + hass.data[DATA_KNX].xknx, + name=config[CONF_NAME], + group_address_switch=config[CONF_ADDRESS], + group_address_switch_state=config.get(CONF_STATE_ADDRESS), + group_address_brightness=config.get(CONF_BRIGHTNESS_ADDRESS), + group_address_brightness_state=config.get( + CONF_BRIGHTNESS_STATE_ADDRESS), + group_address_color=config.get(CONF_COLOR_ADDRESS), + group_address_color_state=config.get(CONF_COLOR_STATE_ADDRESS), + group_address_tunable_white=group_address_tunable_white, + group_address_tunable_white_state=group_address_tunable_white_state, + group_address_color_temperature=group_address_color_temp, + group_address_color_temperature_state=group_address_color_temp_state, + min_kelvin=config[CONF_MIN_KELVIN], + max_kelvin=config[CONF_MAX_KELVIN]) + hass.data[DATA_KNX].xknx.devices.add(light) + async_add_entities([KNXLight(light)]) + + +class KNXLight(Light): + """Representation of a KNX light.""" + + def __init__(self, device): + """Initialize of KNX light.""" + self.device = device + + self._min_kelvin = device.min_kelvin + self._max_kelvin = device.max_kelvin + self._min_mireds = \ + color_util.color_temperature_kelvin_to_mired(self._max_kelvin) + self._max_mireds = \ + color_util.color_temperature_kelvin_to_mired(self._min_kelvin) + + @callback + def async_register_callbacks(self): + """Register callbacks to update hass after device was changed.""" + async def after_update_callback(device): + """Call after device was updated.""" + 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.""" + return self.device.name + + @property + def available(self): + """Return True if entity is available.""" + return self.hass.data[DATA_KNX].connected + + @property + def should_poll(self): + """No polling needed within KNX.""" + return False + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + if self.device.supports_color: + if self.device.current_color is None: + return None + return max(self.device.current_color) + if self.device.supports_brightness: + return self.device.current_brightness + return None + + @property + def hs_color(self): + """Return the HS color value.""" + if self.device.supports_color: + rgb = self.device.current_color + if rgb is None: + return None + return color_util.color_RGB_to_hs(*rgb) + return None + + @property + def color_temp(self): + """Return the color temperature in mireds.""" + if self.device.supports_color_temperature: + kelvin = self.device.current_color_temperature + if kelvin is not None: + return color_util.color_temperature_kelvin_to_mired(kelvin) + if self.device.supports_tunable_white: + relative_ct = self.device.current_tunable_white + if relative_ct is not None: + # as KNX devices typically use Kelvin we use it as base for + # calculating ct from percent + return color_util.color_temperature_kelvin_to_mired( + self._min_kelvin + ( + (relative_ct / 255) * + (self._max_kelvin - self._min_kelvin))) + return None + + @property + def min_mireds(self): + """Return the coldest color temp this light supports in mireds.""" + return self._min_mireds + + @property + def max_mireds(self): + """Return the warmest color temp this light supports in mireds.""" + return self._max_mireds + + @property + def effect_list(self): + """Return the list of supported effects.""" + return None + + @property + def effect(self): + """Return the current effect.""" + return None + + @property + def is_on(self): + """Return true if light is on.""" + return self.device.state + + @property + def supported_features(self): + """Flag supported features.""" + flags = 0 + if self.device.supports_brightness: + flags |= SUPPORT_BRIGHTNESS + if self.device.supports_color: + flags |= SUPPORT_COLOR | SUPPORT_BRIGHTNESS + if self.device.supports_color_temperature or \ + self.device.supports_tunable_white: + flags |= SUPPORT_COLOR_TEMP + return flags + + async def async_turn_on(self, **kwargs): + """Turn the light on.""" + brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness) + hs_color = kwargs.get(ATTR_HS_COLOR, self.hs_color) + mireds = kwargs.get(ATTR_COLOR_TEMP, self.color_temp) + + update_brightness = ATTR_BRIGHTNESS in kwargs + update_color = ATTR_HS_COLOR in kwargs + update_color_temp = ATTR_COLOR_TEMP in kwargs + + # always only go one path for turning on (avoid conflicting changes + # and weird effects) + if self.device.supports_brightness and \ + (update_brightness and not update_color): + # if we don't need to update the color, try updating brightness + # directly if supported; don't do it if color also has to be + # changed, as RGB color implicitly sets the brightness as well + await self.device.set_brightness(brightness) + elif self.device.supports_color and \ + (update_brightness or update_color): + # change RGB color (includes brightness) + # if brightness or hs_color was not yet set use the default value + # to calculate RGB from as a fallback + if brightness is None: + brightness = DEFAULT_BRIGHTNESS + if hs_color is None: + hs_color = DEFAULT_COLOR + await self.device.set_color( + color_util.color_hsv_to_RGB(*hs_color, brightness * 100 / 255)) + elif self.device.supports_color_temperature and \ + update_color_temp: + # change color temperature without ON telegram + kelvin = int(color_util.color_temperature_mired_to_kelvin(mireds)) + if kelvin > self._max_kelvin: + kelvin = self._max_kelvin + elif kelvin < self._min_kelvin: + kelvin = self._min_kelvin + await self.device.set_color_temperature(kelvin) + elif self.device.supports_tunable_white and \ + update_color_temp: + # calculate relative_ct from Kelvin to fit typical KNX devices + kelvin = int(color_util.color_temperature_mired_to_kelvin(mireds)) + relative_ct = int(255 * (kelvin - self._min_kelvin) / + (self._max_kelvin - self._min_kelvin)) + await self.device.set_tunable_white(relative_ct) + else: + # no color/brightness change requested, so just turn it on + await self.device.set_on() + + async def async_turn_off(self, **kwargs): + """Turn the light off.""" + await self.device.set_off() diff --git a/homeassistant/components/notify/knx.py b/homeassistant/components/knx/notify.py similarity index 94% rename from homeassistant/components/notify/knx.py rename to homeassistant/components/knx/notify.py index 750e3945569..2488114aa41 100644 --- a/homeassistant/components/notify/knx.py +++ b/homeassistant/components/knx/notify.py @@ -1,10 +1,4 @@ -""" -KNX/IP notification service. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/notify.knx/ -""" - +"""Support for KNX/IP notification services.""" import voluptuous as vol from homeassistant.components.knx import DATA_KNX, ATTR_DISCOVER_DEVICES @@ -16,6 +10,7 @@ import homeassistant.helpers.config_validation as cv CONF_ADDRESS = 'address' DEFAULT_NAME = 'KNX Notify' + DEPENDENCIES = ['knx'] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ diff --git a/homeassistant/components/scene/knx.py b/homeassistant/components/knx/scene.py similarity index 88% rename from homeassistant/components/scene/knx.py rename to homeassistant/components/knx/scene.py index cd333ba79b4..008e81508b9 100644 --- a/homeassistant/components/scene/knx.py +++ b/homeassistant/components/knx/scene.py @@ -1,9 +1,4 @@ -""" -Support for KNX scenes. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/scene.knx/ -""" +"""Support for KNX scenes.""" import voluptuous as vol from homeassistant.components.knx import ATTR_DISCOVER_DEVICES, DATA_KNX @@ -26,8 +21,8 @@ PLATFORM_SCHEMA = vol.Schema({ }) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the scenes for KNX platform.""" if discovery_info is not None: async_add_entities_discovery(hass, discovery_info, async_add_entities) diff --git a/homeassistant/components/sensor/knx.py b/homeassistant/components/knx/sensor.py similarity index 95% rename from homeassistant/components/sensor/knx.py rename to homeassistant/components/knx/sensor.py index c096e15192d..6a2d8144b1e 100644 --- a/homeassistant/components/sensor/knx.py +++ b/homeassistant/components/knx/sensor.py @@ -1,10 +1,4 @@ -""" -Support for KNX/IP sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.knx/ -""" - +"""Support for KNX/IP sensors.""" import voluptuous as vol from homeassistant.components.knx import ATTR_DISCOVER_DEVICES, DATA_KNX diff --git a/homeassistant/components/knx/services.yaml b/homeassistant/components/knx/services.yaml new file mode 100644 index 00000000000..79b11c129af --- /dev/null +++ b/homeassistant/components/knx/services.yaml @@ -0,0 +1,5 @@ +group_write: + description: Turn a light on. + fields: + address: {description: Group address(es) to write to., example: 1/1/0} + data: {description: KNX data to send., example: 1} diff --git a/homeassistant/components/switch/knx.py b/homeassistant/components/knx/switch.py similarity index 95% rename from homeassistant/components/switch/knx.py rename to homeassistant/components/knx/switch.py index 26b9f77028d..305234e1eec 100644 --- a/homeassistant/components/switch/knx.py +++ b/homeassistant/components/knx/switch.py @@ -1,10 +1,4 @@ -""" -Support for KNX/IP switches. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.knx/ -""" - +"""Support for KNX/IP switches.""" import voluptuous as vol from homeassistant.components.knx import ATTR_DISCOVER_DEVICES, DATA_KNX diff --git a/homeassistant/components/konnected.py b/homeassistant/components/konnected/__init__.py similarity index 99% rename from homeassistant/components/konnected.py rename to homeassistant/components/konnected/__init__.py index 0cba8552346..e3f9a46743d 100644 --- a/homeassistant/components/konnected.py +++ b/homeassistant/components/konnected/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Konnected devices. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/konnected/ -""" +"""Support for Konnected devices.""" import asyncio import hmac import json diff --git a/homeassistant/components/binary_sensor/konnected.py b/homeassistant/components/konnected/binary_sensor.py similarity index 83% rename from homeassistant/components/binary_sensor/konnected.py rename to homeassistant/components/konnected/binary_sensor.py index e91d3f6136a..cb15e44e798 100644 --- a/homeassistant/components/binary_sensor/konnected.py +++ b/homeassistant/components/konnected/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for wired binary sensors attached to a Konnected device. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.konnected/ -""" +"""Support for wired binary sensors attached to a Konnected device.""" import logging from homeassistant.components.binary_sensor import BinarySensorDevice @@ -20,8 +15,8 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['konnected'] -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up binary sensors attached to a Konnected device.""" if discovery_info is None: return @@ -38,7 +33,7 @@ class KonnectedBinarySensor(BinarySensorDevice): """Representation of a Konnected binary sensor.""" def __init__(self, device_id, pin_num, data): - """Initialize the binary sensor.""" + """Initialize the Konnected binary sensor.""" self._data = data self._device_id = device_id self._pin_num = pin_num @@ -46,7 +41,7 @@ class KonnectedBinarySensor(BinarySensorDevice): self._device_class = self._data.get(CONF_TYPE) self._name = self._data.get(CONF_NAME, 'Konnected {} Zone {}'.format( device_id, PIN_TO_ZONE[pin_num])) - _LOGGER.debug('Created new Konnected sensor: %s', self._name) + _LOGGER.debug("Created new Konnected sensor: %s", self._name) @property def name(self): diff --git a/homeassistant/components/switch/konnected.py b/homeassistant/components/konnected/switch.py similarity index 88% rename from homeassistant/components/switch/konnected.py rename to homeassistant/components/konnected/switch.py index 84016dac28d..897933e6d80 100644 --- a/homeassistant/components/switch/konnected.py +++ b/homeassistant/components/konnected/switch.py @@ -1,10 +1,4 @@ -""" -Support for wired switches attached to a Konnected device. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.konnected/ -""" - +"""Support for wired switches attached to a Konnected device.""" import logging from homeassistant.components.konnected import ( @@ -19,8 +13,8 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['konnected'] -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set switches attached to a Konnected device.""" if discovery_info is None: return @@ -37,7 +31,7 @@ class KonnectedSwitch(ToggleEntity): """Representation of a Konnected switch.""" def __init__(self, device_id, pin_num, data): - """Initialize the switch.""" + """Initialize the Konnected switch.""" self._data = data self._device_id = device_id self._pin_num = pin_num @@ -49,7 +43,7 @@ class KonnectedSwitch(ToggleEntity): self._name = self._data.get( 'name', 'Konnected {} Actuator {}'.format( device_id, PIN_TO_ZONE[pin_num])) - _LOGGER.debug('Created new switch: %s', self._name) + _LOGGER.debug("Created new switch: %s", self._name) @property def name(self): diff --git a/homeassistant/components/lametric.py b/homeassistant/components/lametric/__init__.py similarity index 85% rename from homeassistant/components/lametric.py rename to homeassistant/components/lametric/__init__.py index 96ea3781566..0c3c8b08dd7 100644 --- a/homeassistant/components/lametric.py +++ b/homeassistant/components/lametric/__init__.py @@ -1,12 +1,4 @@ -""" -Support for LaMetric time. - -This is the base platform to support LaMetric components: -Notify, Light, Mediaplayer - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/lametric/ -""" +"""Support for LaMetric time.""" import logging import voluptuous as vol diff --git a/homeassistant/components/notify/lametric.py b/homeassistant/components/lametric/notify.py similarity index 84% rename from homeassistant/components/notify/lametric.py rename to homeassistant/components/lametric/notify.py index 61bc5172af0..e5e6a5bd522 100644 --- a/homeassistant/components/notify/lametric.py +++ b/homeassistant/components/lametric/notify.py @@ -1,9 +1,4 @@ -""" -Notifier for LaMetric time. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/notify.lametric/ -""" +"""Support for LaMetric notifications.""" import logging from requests.exceptions import ConnectionError as RequestsConnectionError @@ -17,33 +12,32 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.lametric import DOMAIN as LAMETRIC_DOMAIN REQUIREMENTS = ['lmnotify==0.0.4'] -DEPENDENCIES = ['lametric'] _LOGGER = logging.getLogger(__name__) -CONF_LIFETIME = "lifetime" -CONF_CYCLES = "cycles" -CONF_PRIORITY = "priority" +AVAILABLE_PRIORITIES = ['info', 'warning', 'critical'] -AVAILABLE_PRIORITIES = ["info", "warning", "critical"] +CONF_CYCLES = 'cycles' +CONF_LIFETIME = 'lifetime' +CONF_PRIORITY = 'priority' + +DEPENDENCIES = ['lametric'] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_ICON, default="i555"): cv.string, + vol.Optional(CONF_ICON, default='i555'): cv.string, vol.Optional(CONF_LIFETIME, default=10): cv.positive_int, vol.Optional(CONF_CYCLES, default=1): cv.positive_int, - vol.Optional(CONF_PRIORITY, default="warning"): - vol.In(AVAILABLE_PRIORITIES) + vol.Optional(CONF_PRIORITY, default='warning'): + vol.In(AVAILABLE_PRIORITIES), }) def get_service(hass, config, discovery_info=None): """Get the LaMetric notification service.""" hlmn = hass.data.get(LAMETRIC_DOMAIN) - return LaMetricNotificationService(hlmn, - config[CONF_ICON], - config[CONF_LIFETIME] * 1000, - config[CONF_CYCLES], - config[CONF_PRIORITY]) + return LaMetricNotificationService( + hlmn, config[CONF_ICON], config[CONF_LIFETIME] * 1000, + config[CONF_CYCLES], config[CONF_PRIORITY]) class LaMetricNotificationService(BaseNotificationService): diff --git a/homeassistant/components/lcn.py b/homeassistant/components/lcn/__init__.py similarity index 97% rename from homeassistant/components/lcn.py rename to homeassistant/components/lcn/__init__.py index 8efdcc99794..941160b6397 100644 --- a/homeassistant/components/lcn.py +++ b/homeassistant/components/lcn/__init__.py @@ -1,10 +1,4 @@ -""" -Connects to LCN platform. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/lcn/ -""" - +"""Support for LCN devices.""" import logging import re diff --git a/homeassistant/components/light/lcn.py b/homeassistant/components/lcn/light.py similarity index 95% rename from homeassistant/components/light/lcn.py rename to homeassistant/components/lcn/light.py index b9457b7b7d9..2b7f4ed4074 100644 --- a/homeassistant/components/light/lcn.py +++ b/homeassistant/components/lcn/light.py @@ -1,10 +1,4 @@ -""" -Support for LCN lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.lcn/ -""" - +"""Support for LCN lights.""" from homeassistant.components.lcn import ( CONF_CONNECTIONS, CONF_DIMMABLE, CONF_OUTPUT, CONF_TRANSITION, DATA_LCN, OUTPUT_PORTS, LcnDevice, get_connection) @@ -16,8 +10,8 @@ from homeassistant.const import CONF_ADDRESS DEPENDENCIES = ['lcn'] -async def async_setup_platform(hass, hass_config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, hass_config, async_add_entities, discovery_info=None): """Set up the LCN light platform.""" if discovery_info is None: return diff --git a/homeassistant/components/switch/lcn.py b/homeassistant/components/lcn/switch.py similarity index 96% rename from homeassistant/components/switch/lcn.py rename to homeassistant/components/lcn/switch.py index 468afe178b5..60eda2ea779 100755 --- a/homeassistant/components/switch/lcn.py +++ b/homeassistant/components/lcn/switch.py @@ -1,10 +1,4 @@ -""" -Support for LCN switches. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.lcn/ -""" - +"""Support for LCN switches.""" from homeassistant.components.lcn import ( CONF_CONNECTIONS, CONF_OUTPUT, DATA_LCN, OUTPUT_PORTS, LcnDevice, get_connection) diff --git a/homeassistant/components/lifx/.translations/da.json b/homeassistant/components/lifx/.translations/da.json new file mode 100644 index 00000000000..ffd8e20ce42 --- /dev/null +++ b/homeassistant/components/lifx/.translations/da.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Ingen LIFX enheder kunne findes p\u00e5 netv\u00e6rket.", + "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning af LIFX." + }, + "step": { + "confirm": { + "description": "Konfigurer 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 index a2ae6266a8d..82802bab4af 100644 --- a/homeassistant/components/lifx/__init__.py +++ b/homeassistant/components/lifx/__init__.py @@ -1,4 +1,4 @@ -"""Component to embed LIFX.""" +"""Support for LIFX.""" import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -7,7 +7,6 @@ from homeassistant.const import CONF_PORT from homeassistant.helpers import config_entry_flow from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN - DOMAIN = 'lifx' REQUIREMENTS = ['aiolifx==0.6.7'] diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/lifx/light.py similarity index 99% rename from homeassistant/components/light/lifx.py rename to homeassistant/components/lifx/light.py index f0cd7b7a7fe..c0b6158f186 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/lifx/light.py @@ -1,9 +1,4 @@ -""" -Support for the LIFX platform that implements lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.lifx/ -""" +"""Support for LIFX lights.""" import asyncio from datetime import timedelta from functools import partial diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index a16d1aaf87e..816f93b5881 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -20,7 +20,8 @@ from homeassistant.const import ( STATE_ON) from homeassistant.exceptions import UnknownUser, Unauthorized import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa +from homeassistant.helpers.config_validation import ( # noqa + PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers import intent diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index cab6957c265..5ecf3f55e10 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -23,6 +23,10 @@ REQUIREMENTS = ['flux_led==0.22'] _LOGGER = logging.getLogger(__name__) CONF_AUTOMATIC_ADD = 'automatic_add' +CONF_CUSTOM_EFFECT = 'custom_effect' +CONF_COLORS = 'colors' +CONF_SPEED_PCT = 'speed_pct' +CONF_TRANSITION = 'transition' ATTR_MODE = 'mode' DOMAIN = 'flux_led' @@ -57,6 +61,7 @@ EFFECT_CYAN_STROBE = 'cyan_strobe' EFFECT_PURPLE_STROBE = 'purple_strobe' EFFECT_WHITE_STROBE = 'white_strobe' EFFECT_COLORJUMP = 'colorjump' +EFFECT_CUSTOM = 'custom' EFFECT_MAP = { EFFECT_COLORLOOP: 0x25, @@ -73,17 +78,32 @@ EFFECT_MAP = { EFFECT_COLORSTROBE: 0x30, EFFECT_RED_STROBE: 0x31, EFFECT_GREEN_STROBE: 0x32, - EFFECT_BLUE_STROBE: 0x33, + EFFECT_BLUE_STROBE: 0x33, EFFECT_YELLOW_STROBE: 0x34, EFFECT_CYAN_STROBE: 0x35, EFFECT_PURPLE_STROBE: 0x36, EFFECT_WHITE_STROBE: 0x37, EFFECT_COLORJUMP: 0x38 } +EFFECT_CUSTOM_CODE = 0x60 -FLUX_EFFECT_LIST = [ - EFFECT_RANDOM, - ] + list(EFFECT_MAP) +TRANSITION_GRADUAL = 'gradual' +TRANSITION_JUMP = 'jump' +TRANSITION_STROBE = 'strobe' + +FLUX_EFFECT_LIST = sorted(list(EFFECT_MAP)) + [EFFECT_RANDOM] + +CUSTOM_EFFECT_SCHEMA = vol.Schema({ + vol.Required(CONF_COLORS): + vol.All(cv.ensure_list, vol.Length(min=1, max=16), + [vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)), + vol.Coerce(tuple))]), + vol.Optional(CONF_SPEED_PCT, default=50): + vol.All(vol.Range(min=0, max=100), vol.Coerce(int)), + vol.Optional(CONF_TRANSITION, default=TRANSITION_GRADUAL): + vol.All(cv.string, vol.In( + [TRANSITION_GRADUAL, TRANSITION_JUMP, TRANSITION_STROBE])), +}) DEVICE_SCHEMA = vol.Schema({ vol.Optional(CONF_NAME): cv.string, @@ -91,6 +111,7 @@ DEVICE_SCHEMA = vol.Schema({ vol.All(cv.string, vol.In([MODE_RGBW, MODE_RGB, MODE_WHITE])), vol.Optional(CONF_PROTOCOL): vol.All(cv.string, vol.In(['ledenet'])), + vol.Optional(CONF_CUSTOM_EFFECT): CUSTOM_EFFECT_SCHEMA, }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -111,6 +132,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device['ipaddr'] = ipaddr device[CONF_PROTOCOL] = device_config.get(CONF_PROTOCOL) device[ATTR_MODE] = device_config[ATTR_MODE] + device[CONF_CUSTOM_EFFECT] = device_config.get(CONF_CUSTOM_EFFECT) light = FluxLight(device) lights.append(light) light_ips.append(ipaddr) @@ -144,6 +166,7 @@ class FluxLight(Light): self._ipaddr = device['ipaddr'] self._protocol = device[CONF_PROTOCOL] self._mode = device[ATTR_MODE] + self._custom_effect = device[CONF_CUSTOM_EFFECT] self._bulb = None self._error_reported = False @@ -214,8 +237,25 @@ class FluxLight(Light): @property def effect_list(self): """Return the list of supported effects.""" + if self._custom_effect: + return FLUX_EFFECT_LIST + [EFFECT_CUSTOM] + return FLUX_EFFECT_LIST + @property + def effect(self): + """Return the current effect.""" + current_mode = self._bulb.raw_state[3] + + if current_mode == EFFECT_CUSTOM_CODE: + return EFFECT_CUSTOM + + for effect, code in EFFECT_MAP.items(): + if current_mode == code: + return effect + + return None + def turn_on(self, **kwargs): """Turn the specified or all lights on.""" if not self.is_on: @@ -244,6 +284,14 @@ class FluxLight(Light): random.randint(0, 255)) return + if effect == EFFECT_CUSTOM: + if self._custom_effect: + self._bulb.setCustomPattern( + self._custom_effect[CONF_COLORS], + self._custom_effect[CONF_SPEED_PCT], + self._custom_effect[CONF_TRANSITION]) + return + # Effect selection if effect in EFFECT_MAP: self._bulb.setPresetPattern(EFFECT_MAP[effect], 50) diff --git a/homeassistant/components/light/homematicip_cloud.py b/homeassistant/components/light/homematicip_cloud.py deleted file mode 100644 index 2837edbd5b7..00000000000 --- a/homeassistant/components/light/homematicip_cloud.py +++ /dev/null @@ -1,119 +0,0 @@ -""" -Support for HomematicIP Cloud lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.homematicip_cloud/ -""" -import logging - -from homeassistant.components.homematicip_cloud import ( - HMIPC_HAPID, HomematicipGenericDevice) -from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) - -DEPENDENCIES = ['homematicip_cloud'] - -_LOGGER = logging.getLogger(__name__) - -ATTR_ENERGY_COUNTER = 'energy_counter_kwh' -ATTR_POWER_CONSUMPTION = 'power_consumption' -ATTR_PROFILE_MODE = 'profile_mode' - - -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): - """Old way of setting up HomematicIP Cloud lights.""" - pass - - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up the HomematicIP Cloud lights from a config entry.""" - from homematicip.aio.device import AsyncBrandSwitchMeasuring, AsyncDimmer,\ - AsyncPluggableDimmer, AsyncBrandDimmer, AsyncFullFlushDimmer - - home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home - devices = [] - for device in home.devices: - if isinstance(device, AsyncBrandSwitchMeasuring): - devices.append(HomematicipLightMeasuring(home, device)) - elif isinstance(device, - (AsyncDimmer, AsyncPluggableDimmer, - AsyncBrandDimmer, AsyncFullFlushDimmer)): - devices.append(HomematicipDimmer(home, device)) - - if devices: - async_add_entities(devices) - - -class HomematicipLight(HomematicipGenericDevice, Light): - """Representation of a HomematicIP Cloud light device.""" - - def __init__(self, home, device): - """Initialize the light device.""" - super().__init__(home, device) - - @property - def is_on(self): - """Return true if device is on.""" - return self._device.on - - async def async_turn_on(self, **kwargs): - """Turn the device on.""" - await self._device.turn_on() - - async def async_turn_off(self, **kwargs): - """Turn the device off.""" - await self._device.turn_off() - - -class HomematicipLightMeasuring(HomematicipLight): - """Representation of a HomematicIP Cloud measuring light device.""" - - @property - def device_state_attributes(self): - """Return the state attributes of the generic device.""" - attr = super().device_state_attributes - if self._device.currentPowerConsumption > 0.05: - attr.update({ - ATTR_POWER_CONSUMPTION: - round(self._device.currentPowerConsumption, 2) - }) - attr.update({ - ATTR_ENERGY_COUNTER: round(self._device.energyCounter, 2) - }) - return attr - - -class HomematicipDimmer(HomematicipGenericDevice, Light): - """Representation of HomematicIP Cloud dimmer light device.""" - - def __init__(self, home, device): - """Initialize the dimmer light device.""" - super().__init__(home, device) - - @property - def is_on(self): - """Return true if device is on.""" - return self._device.dimLevel != 0 - - @property - def brightness(self): - """Return the brightness of this light between 0..255.""" - return int(self._device.dimLevel*255) - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_BRIGHTNESS - - async def async_turn_on(self, **kwargs): - """Turn the light on.""" - if ATTR_BRIGHTNESS in kwargs: - await self._device.set_dim_level(kwargs[ATTR_BRIGHTNESS]/255.0) - else: - await self._device.set_dim_level(1) - - async def async_turn_off(self, **kwargs): - """Turn the light off.""" - await self._device.set_dim_level(0) diff --git a/homeassistant/components/light/hyperion.py b/homeassistant/components/light/hyperion.py index ebe209c745e..16be7d45825 100644 --- a/homeassistant/components/light/hyperion.py +++ b/homeassistant/components/light/hyperion.py @@ -177,6 +177,11 @@ class Hyperion(Light): def turn_off(self, **kwargs): """Disconnect all remotes.""" self.json_request({'command': 'clearall'}) + self.json_request({ + 'command': 'color', + 'priority': self._priority, + 'color': [0, 0, 0] + }) def update(self): """Get the lights status.""" diff --git a/homeassistant/components/light/knx.py b/homeassistant/components/light/knx.py deleted file mode 100644 index a1423cc6682..00000000000 --- a/homeassistant/components/light/knx.py +++ /dev/null @@ -1,174 +0,0 @@ -""" -Support for KNX/IP lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.knx/ -""" - -import voluptuous as vol - -from homeassistant.components.knx import ATTR_DISCOVER_DEVICES, DATA_KNX -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_HS_COLOR, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, Light) -from homeassistant.const import CONF_NAME -from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv -import homeassistant.util.color as color_util - -CONF_ADDRESS = 'address' -CONF_STATE_ADDRESS = 'state_address' -CONF_BRIGHTNESS_ADDRESS = 'brightness_address' -CONF_BRIGHTNESS_STATE_ADDRESS = 'brightness_state_address' -CONF_COLOR_ADDRESS = 'color_address' -CONF_COLOR_STATE_ADDRESS = 'color_state_address' - -DEFAULT_NAME = 'KNX Light' -DEPENDENCIES = ['knx'] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_ADDRESS): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_STATE_ADDRESS): cv.string, - vol.Optional(CONF_BRIGHTNESS_ADDRESS): cv.string, - vol.Optional(CONF_BRIGHTNESS_STATE_ADDRESS): cv.string, - vol.Optional(CONF_COLOR_ADDRESS): cv.string, - vol.Optional(CONF_COLOR_STATE_ADDRESS): cv.string, -}) - - -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up lights for KNX platform.""" - if discovery_info is not None: - async_add_entities_discovery(hass, discovery_info, async_add_entities) - else: - async_add_entities_config(hass, config, async_add_entities) - - -@callback -def async_add_entities_discovery(hass, discovery_info, async_add_entities): - """Set up lights for KNX platform configured via xknx.yaml.""" - entities = [] - for device_name in discovery_info[ATTR_DISCOVER_DEVICES]: - device = hass.data[DATA_KNX].xknx.devices[device_name] - entities.append(KNXLight(device)) - async_add_entities(entities) - - -@callback -def async_add_entities_config(hass, config, async_add_entities): - """Set up light for KNX platform configured within platform.""" - import xknx - light = xknx.devices.Light( - hass.data[DATA_KNX].xknx, - name=config.get(CONF_NAME), - group_address_switch=config.get(CONF_ADDRESS), - group_address_switch_state=config.get(CONF_STATE_ADDRESS), - group_address_brightness=config.get(CONF_BRIGHTNESS_ADDRESS), - group_address_brightness_state=config.get( - CONF_BRIGHTNESS_STATE_ADDRESS), - 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(light)]) - - -class KNXLight(Light): - """Representation of a KNX light.""" - - def __init__(self, device): - """Initialize of KNX light.""" - self.device = device - - @callback - def async_register_callbacks(self): - """Register callbacks to update hass after device was changed.""" - async def after_update_callback(device): - """Call after device was updated.""" - 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.""" - return self.device.name - - @property - def available(self): - """Return True if entity is available.""" - return self.hass.data[DATA_KNX].connected - - @property - def should_poll(self): - """No polling needed within KNX.""" - return False - - @property - def brightness(self): - """Return the brightness of this light between 0..255.""" - return self.device.current_brightness \ - if self.device.supports_brightness else \ - None - - @property - def hs_color(self): - """Return the HS color value.""" - if self.device.supports_color: - return color_util.color_RGB_to_hs(*self.device.current_color) - return None - - @property - def color_temp(self): - """Return the CT color temperature.""" - return None - - @property - def white_value(self): - """Return the white value of this light between 0..255.""" - return None - - @property - def effect_list(self): - """Return the list of supported effects.""" - return None - - @property - def effect(self): - """Return the current effect.""" - return None - - @property - def is_on(self): - """Return true if light is on.""" - return self.device.state - - @property - def supported_features(self): - """Flag supported features.""" - flags = 0 - if self.device.supports_brightness: - flags |= SUPPORT_BRIGHTNESS - if self.device.supports_color: - flags |= SUPPORT_COLOR - return flags - - async def async_turn_on(self, **kwargs): - """Turn the light on.""" - if ATTR_BRIGHTNESS in kwargs: - if self.device.supports_brightness: - await self.device.set_brightness(int(kwargs[ATTR_BRIGHTNESS])) - elif ATTR_HS_COLOR in kwargs: - if self.device.supports_color: - await self.device.set_color(color_util.color_hs_to_RGB( - *kwargs[ATTR_HS_COLOR])) - else: - await self.device.set_on() - - async def async_turn_off(self, **kwargs): - """Turn the light off.""" - await self.device.set_off() diff --git a/homeassistant/components/light/rflink.py b/homeassistant/components/light/rflink.py index ef389bb84f9..726433b4f70 100644 --- a/homeassistant/components/light/rflink.py +++ b/homeassistant/components/light/rflink.py @@ -185,6 +185,14 @@ class DimmableRflinkLight(SwitchableRflinkDevice, Light): """Return the brightness of this light between 0..255.""" return self._brightness + @property + def device_state_attributes(self): + """Return the device state attributes.""" + attr = {} + if self._brightness is not None: + attr[ATTR_BRIGHTNESS] = self._brightness + return attr + @property def supported_features(self): """Flag supported features.""" @@ -239,6 +247,14 @@ class HybridRflinkLight(SwitchableRflinkDevice, Light): """Return the brightness of this light between 0..255.""" return self._brightness + @property + def device_state_attributes(self): + """Return the device state attributes.""" + attr = {} + if self._brightness is not None: + attr[ATTR_BRIGHTNESS] = self._brightness + return attr + @property def supported_features(self): """Flag supported features.""" diff --git a/homeassistant/components/light/tplink.py b/homeassistant/components/light/tplink.py index b0f4c6d1a3c..bd1621a0b35 100644 --- a/homeassistant/components/light/tplink.py +++ b/homeassistant/components/light/tplink.py @@ -19,7 +19,7 @@ from homeassistant.util.color import \ from homeassistant.util.color import ( color_temperature_kelvin_to_mired as kelvin_to_mired) -REQUIREMENTS = ['pyHS100==0.3.3'] +REQUIREMENTS = ['pyHS100==0.3.4'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/lightwave.py b/homeassistant/components/lightwave/__init__.py similarity index 88% rename from homeassistant/components/lightwave.py rename to homeassistant/components/lightwave/__init__.py index e1aa1664eba..a9e5dcf9823 100644 --- a/homeassistant/components/lightwave.py +++ b/homeassistant/components/lightwave/__init__.py @@ -1,9 +1,4 @@ -""" -Support for device connected via Lightwave WiFi-link hub. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/lightwave/ -""" +"""Support for device connected via Lightwave WiFi-link hub.""" import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.const import (CONF_HOST, CONF_LIGHTS, CONF_NAME, @@ -11,7 +6,9 @@ from homeassistant.const import (CONF_HOST, CONF_LIGHTS, CONF_NAME, from homeassistant.helpers.discovery import async_load_platform REQUIREMENTS = ['lightwave==0.15'] + LIGHTWAVE_LINK = 'lightwave_link' + DOMAIN = 'lightwave' diff --git a/homeassistant/components/light/lightwave.py b/homeassistant/components/lightwave/light.py similarity index 88% rename from homeassistant/components/light/lightwave.py rename to homeassistant/components/lightwave/light.py index 50c664d9046..1dfbac37c88 100644 --- a/homeassistant/components/light/lightwave.py +++ b/homeassistant/components/lightwave/light.py @@ -1,9 +1,4 @@ -""" -Implements LightwaveRF lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.lightwave/ -""" +"""Support for LightwaveRF lights.""" from homeassistant.components.light import ( ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) from homeassistant.components.lightwave import LIGHTWAVE_LINK @@ -14,8 +9,8 @@ DEPENDENCIES = ['lightwave'] MAX_BRIGHTNESS = 255 -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Find and return LightWave lights.""" if not discovery_info: return diff --git a/homeassistant/components/switch/lightwave.py b/homeassistant/components/lightwave/switch.py similarity index 84% rename from homeassistant/components/switch/lightwave.py rename to homeassistant/components/lightwave/switch.py index b612cd8dec7..d6c00b7fddb 100644 --- a/homeassistant/components/switch/lightwave.py +++ b/homeassistant/components/lightwave/switch.py @@ -1,9 +1,4 @@ -""" -Implements LightwaveRF switches. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.lightwave/ -""" +"""Support for LightwaveRF switches.""" from homeassistant.components.lightwave import LIGHTWAVE_LINK from homeassistant.components.switch import SwitchDevice from homeassistant.const import CONF_NAME @@ -11,8 +6,8 @@ from homeassistant.const import CONF_NAME DEPENDENCIES = ['lightwave'] -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Find and return LightWave switches.""" if not discovery_info: return diff --git a/homeassistant/components/linode.py b/homeassistant/components/linode/__init__.py similarity index 93% rename from homeassistant/components/linode.py rename to homeassistant/components/linode/__init__.py index c98ef16c7ed..8bbd98c0acf 100644 --- a/homeassistant/components/linode.py +++ b/homeassistant/components/linode/__init__.py @@ -1,17 +1,12 @@ -""" -Support for Linode. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/linode/ -""" -import logging +"""Support for Linode.""" from datetime import timedelta +import logging import voluptuous as vol from homeassistant.const import CONF_ACCESS_TOKEN -from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle REQUIREMENTS = ['linode-api==4.1.9b1'] diff --git a/homeassistant/components/binary_sensor/linode.py b/homeassistant/components/linode/binary_sensor.py similarity index 86% rename from homeassistant/components/binary_sensor/linode.py rename to homeassistant/components/linode/binary_sensor.py index 24abc3dd8be..a05681497de 100644 --- a/homeassistant/components/binary_sensor/linode.py +++ b/homeassistant/components/linode/binary_sensor.py @@ -1,20 +1,15 @@ -""" -Support for monitoring the state of Linode Nodes. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.linode/ -""" +"""Support for monitoring the state of Linode Nodes.""" import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.binary_sensor import ( - BinarySensorDevice, PLATFORM_SCHEMA) + PLATFORM_SCHEMA, BinarySensorDevice) from homeassistant.components.linode import ( - CONF_NODES, ATTR_CREATED, ATTR_NODE_ID, ATTR_NODE_NAME, - ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY, - ATTR_REGION, ATTR_VCPUS, DATA_LINODE) + ATTR_CREATED, ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY, + ATTR_NODE_ID, ATTR_NODE_NAME, ATTR_REGION, ATTR_VCPUS, CONF_NODES, + DATA_LINODE) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/linode.py b/homeassistant/components/linode/switch.py similarity index 86% rename from homeassistant/components/switch/linode.py rename to homeassistant/components/linode/switch.py index 47bba280e1c..0cab2f4d0f2 100644 --- a/homeassistant/components/switch/linode.py +++ b/homeassistant/components/linode/switch.py @@ -1,19 +1,14 @@ -""" -Support for interacting with Linode nodes. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/switch.linode/ -""" +"""Support for interacting with Linode nodes.""" import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) from homeassistant.components.linode import ( - CONF_NODES, ATTR_CREATED, ATTR_NODE_ID, ATTR_NODE_NAME, - ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY, - ATTR_REGION, ATTR_VCPUS, DATA_LINODE) + ATTR_CREATED, ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY, + ATTR_NODE_ID, ATTR_NODE_NAME, ATTR_REGION, ATTR_VCPUS, CONF_NODES, + DATA_LINODE) +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/lirc.py b/homeassistant/components/lirc/__init__.py similarity index 91% rename from homeassistant/components/lirc.py rename to homeassistant/components/lirc/__init__.py index d7ec49e0096..0f00eda2007 100644 --- a/homeassistant/components/lirc.py +++ b/homeassistant/components/lirc/__init__.py @@ -1,10 +1,5 @@ -""" -LIRC interface to receive signals from an infrared remote control. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/lirc/ -""" -# pylint: disable=no-member +"""Support for LIRC devices.""" +# pylint: disable=no-member, import-error import threading import time import logging diff --git a/homeassistant/components/litejet.py b/homeassistant/components/litejet/__init__.py similarity index 86% rename from homeassistant/components/litejet.py rename to homeassistant/components/litejet/__init__.py index e7c8452b27b..b4e8e45fa0b 100644 --- a/homeassistant/components/litejet.py +++ b/homeassistant/components/litejet/__init__.py @@ -1,8 +1,4 @@ -"""Allows the LiteJet lighting system to be controlled by Home Assistant. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/litejet/ -""" +"""Support for the LiteJet lighting system.""" import logging import voluptuous as vol @@ -24,7 +20,7 @@ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_PORT): cv.string, vol.Optional(CONF_EXCLUDE_NAMES): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_INCLUDE_SWITCHES, default=False): cv.boolean + vol.Optional(CONF_INCLUDE_SWITCHES, default=False): cv.boolean, }) }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/locative/.translations/da.json b/homeassistant/components/locative/.translations/da.json new file mode 100644 index 00000000000..8211d52fa5d --- /dev/null +++ b/homeassistant/components/locative/.translations/da.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Dit Home Assistant system skal v\u00e6re tilg\u00e6ngeligt fra internettet for at modtage Geofency meddelelser.", + "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning" + }, + "create_entry": { + "default": "For at sende lokationer til Home Assistant skal du konfigurere webhook funktionen i Locative applicationen.\n\n Udfyld f\u00f8lgende oplysninger: \n\n - URL: `{webhook_url}`\n - Metode: POST\n \n Se [dokumentationen]({docs_url}) for yderligere oplysninger." + }, + "step": { + "user": { + "description": "Er du sikker p\u00e5 at du vil konfigurere Locative Webhook?", + "title": "Konfigurer Locative Webhook" + } + }, + "title": "Locative Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/locative/.translations/fr.json b/homeassistant/components/locative/.translations/fr.json new file mode 100644 index 00000000000..81950c49b4c --- /dev/null +++ b/homeassistant/components/locative/.translations/fr.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Votre instance Home Assistant doit \u00eatre accessible \u00e0 partir d'Internet pour recevoir les messages Geofency.", + "one_instance_allowed": "Une seule instance est n\u00e9cessaire." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/locative/.translations/ko.json b/homeassistant/components/locative/.translations/ko.json index a57b27cdd75..92e6775ea27 100644 --- a/homeassistant/components/locative/.translations/ko.json +++ b/homeassistant/components/locative/.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\ub824\uba74 Locative \uc571\uc5d0\uc11c Webhook \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 \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574 \uc8fc\uc138\uc694." + "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Locative \uc571\uc5d0\uc11c Webhook \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 \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "step": { "user": { diff --git a/homeassistant/components/locative/.translations/nl.json b/homeassistant/components/locative/.translations/nl.json new file mode 100644 index 00000000000..237d21c46ee --- /dev/null +++ b/homeassistant/components/locative/.translations/nl.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Slechts \u00e9\u00e9n instantie is nodig." + }, + "create_entry": { + "default": "Om locaties naar Home Assistant te sturen, moet u de Webhook-functie instellen in de Locative app. \n\n Vul de volgende info in: \n\n - URL: `{webhook_url}` \n - Methode: POST \n\n Zie [de documentatie]({docs_url}) voor meer informatie." + }, + "step": { + "user": { + "description": "Weet u zeker dat u de Locative Webhook wilt instellen?", + "title": "Stel de Locative Webhook in" + } + }, + "title": "Locative Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/locative/.translations/ru.json b/homeassistant/components/locative/.translations/ru.json index d8b8d55a608..ff07393da04 100644 --- a/homeassistant/components/locative/.translations/ru.json +++ b/homeassistant/components/locative/.translations/ru.json @@ -1,7 +1,8 @@ { "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 Locative." + "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 Locative.", + "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 Locative.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\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\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 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." diff --git a/homeassistant/components/locative/.translations/zh-Hans.json b/homeassistant/components/locative/.translations/zh-Hans.json index d98793d96e5..96626a57c5b 100644 --- a/homeassistant/components/locative/.translations/zh-Hans.json +++ b/homeassistant/components/locative/.translations/zh-Hans.json @@ -2,7 +2,7 @@ "config": { "abort": { "not_internet_accessible": "\u60a8\u7684Home Assistant\u5b9e\u4f8b\u9700\u8981\u53ef\u4ee5\u4eceInternet\u8bbf\u95ee\u4ee5\u63a5\u6536\u6765\u81eaGeofency\u7684\u6d88\u606f\u3002", - "one_instance_allowed": "\u53ea\u9700\u8981\u4e00\u4e2a\u5b9e\u4f8b\u3002" + "one_instance_allowed": "\u53ea\u6709\u4e00\u4e2a\u5b9e\u4f8b\u662f\u5fc5\u9700\u7684\u3002" }, "step": { "user": { diff --git a/homeassistant/components/locative/__init__.py b/homeassistant/components/locative/__init__.py index 5a27fbaec63..e6a5b56ecda 100644 --- a/homeassistant/components/locative/__init__.py +++ b/homeassistant/components/locative/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Locative. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/locative/ -""" +"""Support for Locative.""" import logging from typing import Dict diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index 750977fac87..71c838679fb 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -13,7 +13,8 @@ import voluptuous as vol from homeassistant.loader import bind_hass from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import Entity -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa +from homeassistant.helpers.config_validation import ( # noqa + PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) import homeassistant.helpers.config_validation as cv from homeassistant.const import ( ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, @@ -84,6 +85,11 @@ async def async_setup_entry(hass, 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 LockDevice(Entity): """Representation of a lock.""" diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook/__init__.py similarity index 98% rename from homeassistant/components/logbook.py rename to homeassistant/components/logbook/__init__.py index 0c6608e3572..74a90f0f5f0 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook/__init__.py @@ -1,9 +1,4 @@ -""" -Event parser and human readable log generator. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/logbook/ -""" +"""Event parser and human readable log generator.""" from datetime import timedelta from itertools import groupby import logging @@ -47,12 +42,12 @@ CONFIG_SCHEMA = vol.Schema({ CONF_EXCLUDE: vol.Schema({ vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids, vol.Optional(CONF_DOMAINS, default=[]): - vol.All(cv.ensure_list, [cv.string]) + vol.All(cv.ensure_list, [cv.string]), }), CONF_INCLUDE: vol.Schema({ vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids, vol.Optional(CONF_DOMAINS, default=[]): - vol.All(cv.ensure_list, [cv.string]) + vol.All(cv.ensure_list, [cv.string]), }) }), }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/logentries.py b/homeassistant/components/logentries/__init__.py similarity index 71% rename from homeassistant/components/logentries.py rename to homeassistant/components/logentries/__init__.py index 6dc76d8d932..383fa000514 100644 --- a/homeassistant/components/logentries.py +++ b/homeassistant/components/logentries/__init__.py @@ -1,9 +1,4 @@ -""" -Support for sending data to Logentries webhook endpoint. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/logentries/ -""" +"""Support for sending data to Logentries webhook endpoint.""" import json import logging import requests @@ -42,19 +37,17 @@ def setup(hass, config): _state = state_helper.state_as_number(state) except ValueError: _state = state.state - json_body = [ - { - 'domain': state.domain, - 'entity_id': state.object_id, - 'attributes': dict(state.attributes), - 'time': str(event.time_fired), - 'value': _state, - } - ] + json_body = [{ + 'domain': state.domain, + 'entity_id': state.object_id, + 'attributes': dict(state.attributes), + 'time': str(event.time_fired), + 'value': _state, + }] try: payload = { - "host": le_wh, - "event": json_body + 'host': le_wh, + 'event': json_body } requests.post(le_wh, data=json.dumps(payload), timeout=10) except requests.exceptions.RequestException as error: diff --git a/homeassistant/components/logger.py b/homeassistant/components/logger/__init__.py similarity index 94% rename from homeassistant/components/logger.py rename to homeassistant/components/logger/__init__.py index 21ae7595ab8..2bfc6656945 100644 --- a/homeassistant/components/logger.py +++ b/homeassistant/components/logger/__init__.py @@ -1,9 +1,4 @@ -""" -Component that will help set the level of logging for components. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/logger/ -""" +"""Support for settting the level of logging for components.""" import logging from collections import OrderedDict diff --git a/homeassistant/components/logger/services.yaml b/homeassistant/components/logger/services.yaml new file mode 100644 index 00000000000..4d1ba649d36 --- /dev/null +++ b/homeassistant/components/logger/services.yaml @@ -0,0 +1,6 @@ +set_default_level: + description: Set the default log level for components. + fields: + level: {description: 'Default severity level. Possible values are notset, debug, + info, warn, warning, error, fatal, critical', example: debug} +set_level: {description: Set log level for components.} diff --git a/homeassistant/components/logi_circle.py b/homeassistant/components/logi_circle/__init__.py similarity index 93% rename from homeassistant/components/logi_circle.py rename to homeassistant/components/logi_circle/__init__.py index c0a7f4c2621..50500f47e42 100644 --- a/homeassistant/components/logi_circle.py +++ b/homeassistant/components/logi_circle/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Logi Circle cameras. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/logi_circle/ -""" +"""Support for Logi Circle devices.""" import logging import asyncio diff --git a/homeassistant/components/camera/logi_circle.py b/homeassistant/components/logi_circle/camera.py similarity index 97% rename from homeassistant/components/camera/logi_circle.py rename to homeassistant/components/logi_circle/camera.py index 1dae58ad0f7..51bd7c124a3 100644 --- a/homeassistant/components/camera/logi_circle.py +++ b/homeassistant/components/logi_circle/camera.py @@ -1,9 +1,4 @@ -""" -This component provides support to the Logi Circle camera. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/camera.logi_circle/ -""" +"""Support to the Logi Circle cameras.""" import logging import asyncio from datetime import timedelta diff --git a/homeassistant/components/sensor/logi_circle.py b/homeassistant/components/logi_circle/sensor.py similarity index 86% rename from homeassistant/components/sensor/logi_circle.py rename to homeassistant/components/logi_circle/sensor.py index 104de68ce03..74c2039c120 100644 --- a/homeassistant/components/sensor/logi_circle.py +++ b/homeassistant/components/logi_circle/sensor.py @@ -1,9 +1,4 @@ -""" -This component provides HA sensor support for Logi Circle cameras. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.logi_circle/ -""" +"""Support for Logi Circle sensors.""" import logging import voluptuous as vol @@ -26,26 +21,13 @@ _LOGGER = logging.getLogger(__name__) # Sensor types: Name, unit of measure, icon per sensor key. SENSOR_TYPES = { - 'battery_level': [ - 'Battery', '%', 'battery-50'], - - 'last_activity_time': [ - 'Last Activity', None, 'history'], - - 'privacy_mode': [ - 'Privacy Mode', None, 'eye'], - - 'signal_strength_category': [ - 'WiFi Signal Category', None, 'wifi'], - - 'signal_strength_percentage': [ - 'WiFi Signal Strength', '%', 'wifi'], - - 'speaker_volume': [ - 'Volume', '%', 'volume-high'], - - 'streaming_mode': [ - 'Streaming Mode', None, 'camera'], + 'battery_level': ['Battery', '%', 'battery-50'], + 'last_activity_time': ['Last Activity', None, 'history'], + 'privacy_mode': ['Privacy Mode', None, 'eye'], + 'signal_strength_category': ['WiFi Signal Category', None, 'wifi'], + 'signal_strength_percentage': ['WiFi Signal Strength', '%', 'wifi'], + 'speaker_volume': ['Volume', '%', 'volume-high'], + 'streaming_mode': ['Streaming Mode', None, 'camera'], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index b4cb2b18dca..03b1cf06d68 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -1,9 +1,4 @@ -""" -Support for the Lovelace UI. - -For more details about this component, please refer to the documentation -at https://www.home-assistant.io/lovelace/ -""" +"""Support for the Lovelace UI.""" from functools import wraps import logging import os diff --git a/homeassistant/components/luftdaten/.translations/da.json b/homeassistant/components/luftdaten/.translations/da.json new file mode 100644 index 00000000000..d43fc1128ae --- /dev/null +++ b/homeassistant/components/luftdaten/.translations/da.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "communication_error": "Kan ikke oprette forbindelse til Luftdaten API", + "invalid_sensor": "Sensor ikke tilg\u00e6ngelig eller ugyldig", + "sensor_exists": "Sensor er allerede registreret" + }, + "step": { + "user": { + "data": { + "show_on_map": "Vis p\u00e5 kort", + "station_id": "Luftdaten Sensor ID" + }, + "title": "Definer Luftdaten" + } + }, + "title": "Luftdaten" + } +} \ No newline at end of file diff --git a/homeassistant/components/luftdaten/.translations/pt.json b/homeassistant/components/luftdaten/.translations/pt.json index 0402f352c5c..9ed3611da27 100644 --- a/homeassistant/components/luftdaten/.translations/pt.json +++ b/homeassistant/components/luftdaten/.translations/pt.json @@ -14,6 +14,6 @@ "title": "Definir Luftdaten" } }, - "title": "" + "title": "Luftdaten" } } \ No newline at end of file diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index 45d75b90f7f..125cefb9026 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Luftdaten stations. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/luftdaten/ -""" +"""Support for Luftdaten stations.""" import logging import voluptuous as vol diff --git a/homeassistant/components/sensor/luftdaten.py b/homeassistant/components/luftdaten/sensor.py similarity index 95% rename from homeassistant/components/sensor/luftdaten.py rename to homeassistant/components/luftdaten/sensor.py index d0792883457..398ec30a3f5 100644 --- a/homeassistant/components/sensor/luftdaten.py +++ b/homeassistant/components/luftdaten/sensor.py @@ -1,9 +1,4 @@ -""" -Support for Luftdaten sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.luftdaten/ -""" +"""Support for Luftdaten sensors.""" import logging from homeassistant.components.luftdaten import ( diff --git a/homeassistant/components/lupusec.py b/homeassistant/components/lupusec/__init__.py similarity index 90% rename from homeassistant/components/lupusec.py rename to homeassistant/components/lupusec/__init__.py index 94cb3abc4a2..8a5f098f741 100644 --- a/homeassistant/components/lupusec.py +++ b/homeassistant/components/lupusec/__init__.py @@ -1,10 +1,4 @@ -""" -This component provides basic support for Lupusec Home Security system. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/lupusec -""" - +"""Support for Lupusec Home Security system.""" import logging import voluptuous as vol @@ -14,6 +8,7 @@ from homeassistant.helpers import discovery from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, CONF_NAME, CONF_IP_ADDRESS) from homeassistant.helpers.entity import Entity + _LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['lupupy==0.0.17'] @@ -28,7 +23,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_IP_ADDRESS): cv.string, - vol.Optional(CONF_NAME): cv.string + vol.Optional(CONF_NAME): cv.string, }), }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/alarm_control_panel/lupusec.py b/homeassistant/components/lupusec/alarm_control_panel.py similarity index 89% rename from homeassistant/components/alarm_control_panel/lupusec.py rename to homeassistant/components/lupusec/alarm_control_panel.py index 21eefc238a0..de62e5bfac2 100644 --- a/homeassistant/components/alarm_control_panel/lupusec.py +++ b/homeassistant/components/lupusec/alarm_control_panel.py @@ -1,10 +1,4 @@ -""" -This component provides HA alarm_control_panel support for Lupusec System. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/alarm_control_panel.lupusec/ -""" - +"""Support for Lupusec System alarm control panels.""" from datetime import timedelta from homeassistant.components.alarm_control_panel import AlarmControlPanel diff --git a/homeassistant/components/binary_sensor/lupusec.py b/homeassistant/components/lupusec/binary_sensor.py similarity index 85% rename from homeassistant/components/binary_sensor/lupusec.py rename to homeassistant/components/lupusec/binary_sensor.py index df8210df026..8a5e103db0d 100644 --- a/homeassistant/components/binary_sensor/lupusec.py +++ b/homeassistant/components/lupusec/binary_sensor.py @@ -1,9 +1,4 @@ -""" -This component provides HA binary_sensor support for Lupusec Security System. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.lupusec/ -""" +"""Support for Lupusec Security System binary sensors.""" import logging from datetime import timedelta diff --git a/homeassistant/components/switch/lupusec.py b/homeassistant/components/lupusec/switch.py similarity index 84% rename from homeassistant/components/switch/lupusec.py rename to homeassistant/components/lupusec/switch.py index 35744160f24..8a30d65fec3 100644 --- a/homeassistant/components/switch/lupusec.py +++ b/homeassistant/components/lupusec/switch.py @@ -1,9 +1,4 @@ -""" -This component provides HA switch support for Lupusec Security System. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.lupusec/ -""" +"""Support for Lupusec Security System switches.""" import logging from datetime import timedelta diff --git a/homeassistant/components/lutron.py b/homeassistant/components/lutron/__init__.py similarity index 96% rename from homeassistant/components/lutron.py rename to homeassistant/components/lutron/__init__.py index 435039ce4bd..e4ebec4cc5a 100644 --- a/homeassistant/components/lutron.py +++ b/homeassistant/components/lutron/__init__.py @@ -1,9 +1,4 @@ -""" -Component for interacting with a Lutron RadioRA 2 system. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/lutron/ -""" +"""Component for interacting with a Lutron RadioRA 2 system.""" import logging import voluptuous as vol diff --git a/homeassistant/components/cover/lutron.py b/homeassistant/components/lutron/cover.py similarity index 92% rename from homeassistant/components/cover/lutron.py rename to homeassistant/components/lutron/cover.py index 7ea7abf882d..cc7a57a5522 100644 --- a/homeassistant/components/cover/lutron.py +++ b/homeassistant/components/lutron/cover.py @@ -1,9 +1,4 @@ -""" -Support for Lutron shades. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/cover.lutron/ -""" +"""Support for Lutron shades.""" import logging from homeassistant.components.cover import ( diff --git a/homeassistant/components/light/lutron.py b/homeassistant/components/lutron/light.py similarity index 94% rename from homeassistant/components/light/lutron.py rename to homeassistant/components/lutron/light.py index 359ef0114c5..c0b3b991147 100644 --- a/homeassistant/components/light/lutron.py +++ b/homeassistant/components/lutron/light.py @@ -1,9 +1,4 @@ -""" -Support for Lutron lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.lutron/ -""" +"""Support for Lutron lights.""" import logging from homeassistant.components.light import ( diff --git a/homeassistant/components/scene/lutron.py b/homeassistant/components/lutron/scene.py similarity index 68% rename from homeassistant/components/scene/lutron.py rename to homeassistant/components/lutron/scene.py index bdb8bc344fe..f9002f2a839 100644 --- a/homeassistant/components/scene/lutron.py +++ b/homeassistant/components/lutron/scene.py @@ -1,9 +1,4 @@ -""" -Support for Lutron scenes. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/scene.lutron/ -""" +"""Support for Lutron scenes.""" import logging from homeassistant.components.lutron import ( @@ -30,12 +25,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class LutronScene(LutronDevice, Scene): """Representation of a Lutron Scene.""" - def __init__(self, - area_name, - keypad_name, - lutron_device, - lutron_led, - controller): + def __init__( + self, area_name, keypad_name, lutron_device, lutron_led, + controller): """Initialize the scene/button.""" super().__init__(area_name, lutron_device, controller) self._keypad_name = keypad_name @@ -48,6 +40,5 @@ class LutronScene(LutronDevice, Scene): @property def name(self): """Return the name of the device.""" - return "{} {}: {}".format(self._area_name, - self._keypad_name, - self._lutron_device.name) + return "{} {}: {}".format( + self._area_name, self._keypad_name, self._lutron_device.name) diff --git a/homeassistant/components/switch/lutron.py b/homeassistant/components/lutron/switch.py similarity index 87% rename from homeassistant/components/switch/lutron.py rename to homeassistant/components/lutron/switch.py index 4146ba5a43b..bfdb06be33c 100644 --- a/homeassistant/components/switch/lutron.py +++ b/homeassistant/components/lutron/switch.py @@ -1,9 +1,4 @@ -""" -Support for Lutron switches. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.lutron/ -""" +"""Support for Lutron switches.""" import logging from homeassistant.components.switch import SwitchDevice diff --git a/homeassistant/components/lutron_caseta.py b/homeassistant/components/lutron_caseta/__init__.py similarity index 82% rename from homeassistant/components/lutron_caseta.py rename to homeassistant/components/lutron_caseta/__init__.py index eb4010e43a1..61c005f60b2 100644 --- a/homeassistant/components/lutron_caseta.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -1,9 +1,4 @@ -""" -Component for interacting with a Lutron Caseta system. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/lutron_caseta/ -""" +"""Component for interacting with a Lutron Caseta system.""" import logging import voluptuous as vol @@ -30,7 +25,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_KEYFILE): cv.string, vol.Required(CONF_CERTFILE): cv.string, - vol.Required(CONF_CA_CERTS): cv.string + vol.Required(CONF_CA_CERTS): cv.string, }) }, extra=vol.ALLOW_EXTRA) @@ -47,15 +42,14 @@ async def async_setup(hass, base_config): keyfile = hass.config.path(config[CONF_KEYFILE]) certfile = hass.config.path(config[CONF_CERTFILE]) ca_certs = hass.config.path(config[CONF_CA_CERTS]) - bridge = Smartbridge.create_tls(hostname=config[CONF_HOST], - keyfile=keyfile, - certfile=certfile, - ca_certs=ca_certs) + bridge = Smartbridge.create_tls( + hostname=config[CONF_HOST], keyfile=keyfile, certfile=certfile, + ca_certs=ca_certs) hass.data[LUTRON_CASETA_SMARTBRIDGE] = bridge await bridge.connect() if not hass.data[LUTRON_CASETA_SMARTBRIDGE].is_connected(): - _LOGGER.error("Unable to connect to Lutron smartbridge at %s", - config[CONF_HOST]) + _LOGGER.error( + "Unable to connect to Lutron smartbridge at %s", config[CONF_HOST]) return False _LOGGER.info("Connected to Lutron smartbridge at %s", config[CONF_HOST]) diff --git a/homeassistant/components/cover/lutron_caseta.py b/homeassistant/components/lutron_caseta/cover.py similarity index 86% rename from homeassistant/components/cover/lutron_caseta.py rename to homeassistant/components/lutron_caseta/cover.py index 37b7c1be42c..5e09dcc3c85 100644 --- a/homeassistant/components/cover/lutron_caseta.py +++ b/homeassistant/components/lutron_caseta/cover.py @@ -1,9 +1,4 @@ -""" -Support for Lutron Caseta shades. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/cover.lutron_caseta/ -""" +"""Support for Lutron Caseta shades.""" import logging from homeassistant.components.cover import ( @@ -17,8 +12,8 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['lutron_caseta'] -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the Lutron Caseta shades as a cover device.""" devs = [] bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE] diff --git a/homeassistant/components/light/lutron_caseta.py b/homeassistant/components/lutron_caseta/light.py similarity index 83% rename from homeassistant/components/light/lutron_caseta.py rename to homeassistant/components/lutron_caseta/light.py index 21360e71c42..3bab781f3b6 100644 --- a/homeassistant/components/light/lutron_caseta.py +++ b/homeassistant/components/lutron_caseta/light.py @@ -1,14 +1,9 @@ -""" -Support for Lutron Caseta lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.lutron_caseta/ -""" +"""Support for Lutron Caseta lights.""" import logging from homeassistant.components.light import ( ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light, DOMAIN) -from homeassistant.components.light.lutron import ( +from homeassistant.components.lutron.light import ( to_hass_level, to_lutron_level) from homeassistant.components.lutron_caseta import ( LUTRON_CASETA_SMARTBRIDGE, LutronCasetaDevice) @@ -18,8 +13,8 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['lutron_caseta'] -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the Lutron Caseta lights.""" devs = [] bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE] diff --git a/homeassistant/components/scene/lutron_caseta.py b/homeassistant/components/lutron_caseta/scene.py similarity index 71% rename from homeassistant/components/scene/lutron_caseta.py rename to homeassistant/components/lutron_caseta/scene.py index 0ef974e2778..c6ca7bad3ac 100644 --- a/homeassistant/components/scene/lutron_caseta.py +++ b/homeassistant/components/lutron_caseta/scene.py @@ -1,9 +1,4 @@ -""" -Support for Lutron Caseta scenes. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/scene.lutron_caseta/ -""" +"""Support for Lutron Caseta scenes.""" import logging from homeassistant.components.lutron_caseta import LUTRON_CASETA_SMARTBRIDGE @@ -14,8 +9,8 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['lutron_caseta'] -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the Lutron Caseta lights.""" devs = [] bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE] @@ -32,8 +27,8 @@ class LutronCasetaScene(Scene): def __init__(self, scene, bridge): """Initialize the Lutron Caseta scene.""" - self._scene_name = scene["name"] - self._scene_id = scene["scene_id"] + self._scene_name = scene['name'] + self._scene_id = scene['scene_id'] self._bridge = bridge @property diff --git a/homeassistant/components/switch/lutron_caseta.py b/homeassistant/components/lutron_caseta/switch.py similarity index 81% rename from homeassistant/components/switch/lutron_caseta.py rename to homeassistant/components/lutron_caseta/switch.py index f983050cffa..0ef0595187b 100644 --- a/homeassistant/components/switch/lutron_caseta.py +++ b/homeassistant/components/lutron_caseta/switch.py @@ -1,9 +1,4 @@ -""" -Support for Lutron Caseta switches. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sitch.lutron_caseta/ -""" +"""Support for Lutron Caseta switches.""" import logging from homeassistant.components.lutron_caseta import ( @@ -15,8 +10,8 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['lutron_caseta'] -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up Lutron switch.""" devs = [] bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE] diff --git a/homeassistant/components/mailbox/__init__.py b/homeassistant/components/mailbox/__init__.py index 2ed12b23164..1907a1e9e97 100644 --- a/homeassistant/components/mailbox/__init__.py +++ b/homeassistant/components/mailbox/__init__.py @@ -1,9 +1,4 @@ -""" -Provides functionality for mailboxes. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/mailbox/ -""" +"""Support for Voice mailboxes.""" import asyncio from contextlib import suppress from datetime import timedelta diff --git a/homeassistant/components/mailbox/asterisk_cdr.py b/homeassistant/components/mailbox/asterisk_cdr.py index ae0939c3da5..db5d4e8d6ee 100644 --- a/homeassistant/components/mailbox/asterisk_cdr.py +++ b/homeassistant/components/mailbox/asterisk_cdr.py @@ -1,9 +1,4 @@ -""" -Asterisk CDR interface. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/mailbox.asterisk_cdr/ -""" +"""Support for the Asterisk CDR interface.""" import logging import hashlib import datetime @@ -14,9 +9,11 @@ from homeassistant.components.asterisk_mbox import DOMAIN as ASTERISK_DOMAIN from homeassistant.components.mailbox import Mailbox from homeassistant.helpers.dispatcher import async_dispatcher_connect -DEPENDENCIES = ['asterisk_mbox'] _LOGGER = logging.getLogger(__name__) -MAILBOX_NAME = "asterisk_cdr" + +DEPENDENCIES = ['asterisk_mbox'] + +MAILBOX_NAME = 'asterisk_cdr' async def async_get_handler(hass, config, discovery_info=None): diff --git a/homeassistant/components/mailbox/demo.py b/homeassistant/components/mailbox/demo.py index 2aabde42b36..885988adb6b 100644 --- a/homeassistant/components/mailbox/demo.py +++ b/homeassistant/components/mailbox/demo.py @@ -1,9 +1,4 @@ -""" -Asterisk Voicemail interface. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/mailbox.asteriskvm/ -""" +"""Support for a demo mailbox.""" from hashlib import sha1 import logging import os @@ -14,7 +9,7 @@ from homeassistant.util import dt _LOGGER = logging.getLogger(__name__) -MAILBOX_NAME = "DemoMailbox" +MAILBOX_NAME = 'DemoMailbox' async def async_get_handler(hass, config, discovery_info=None): diff --git a/homeassistant/components/mailgun/.translations/da.json b/homeassistant/components/mailgun/.translations/da.json new file mode 100644 index 00000000000..0e25974031d --- /dev/null +++ b/homeassistant/components/mailgun/.translations/da.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Dit Home Assistant system skal v\u00e6re tilg\u00e6ngeligt fra internettet for at modtage Mailgun meddelelser.", + "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning." + }, + "create_entry": { + "default": "For at sende begivenheder til Home Assistant skal du konfigurere [Webhooks med Mailgun]({mailgun_url}).\n\n Udfyld f\u00f8lgende oplysninger: \n\n - URL: `{webhook_url}`\n - Metode: POST\n - Indholdstype: application/json\n\n Se [dokumentationen] ({docs_url}) om hvordan du konfigurerer automatiseringer til at h\u00e5ndtere indg\u00e5ende data." + }, + "step": { + "user": { + "description": "Er du sikker p\u00e5 at du vil konfigurere Mailgun?", + "title": "Konfigurer Mailgun Webhook" + } + }, + "title": "Mailgun" + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/fr.json b/homeassistant/components/mailgun/.translations/fr.json new file mode 100644 index 00000000000..905715de727 --- /dev/null +++ b/homeassistant/components/mailgun/.translations/fr.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Votre instance Home Assistant doit \u00eatre accessible \u00e0 partir d'Internet pour recevoir les messages Mailgun.", + "one_instance_allowed": "Une seule instance est n\u00e9cessaire." + }, + "create_entry": { + "default": "Pour envoyer des \u00e9v\u00e9nements \u00e0 Home Assistant, vous devez configurer [Webhooks avec Mailgun] ( {mailgun_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 Mailgun?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/ko.json b/homeassistant/components/mailgun/.translations/ko.json index 95897a25f15..ae973bdc93d 100644 --- a/homeassistant/components/mailgun/.translations/ko.json +++ b/homeassistant/components/mailgun/.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\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 \nHome 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\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/json\n \nHome 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/mailgun/__init__.py b/homeassistant/components/mailgun/__init__.py index 7fa08bb0f22..3903bd14e25 100644 --- a/homeassistant/components/mailgun/__init__.py +++ b/homeassistant/components/mailgun/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Mailgun. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/mailgun/ -""" +"""Support for Mailgun.""" import hashlib import hmac import json @@ -15,12 +10,15 @@ import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_API_KEY, CONF_DOMAIN, CONF_WEBHOOK_ID from homeassistant.helpers import config_entry_flow -DOMAIN = 'mailgun' _LOGGER = logging.getLogger(__name__) -DEPENDENCIES = ['webhook'] -MESSAGE_RECEIVED = '{}_message_received'.format(DOMAIN) + CONF_SANDBOX = 'sandbox' + DEFAULT_SANDBOX = False +DEPENDENCIES = ['webhook'] +DOMAIN = 'mailgun' + +MESSAGE_RECEIVED = '{}_message_received'.format(DOMAIN) CONFIG_SCHEMA = vol.Schema({ vol.Optional(DOMAIN): vol.Schema({ diff --git a/homeassistant/components/mailgun/notify.py b/homeassistant/components/mailgun/notify.py index d4052678d36..05137254fcc 100644 --- a/homeassistant/components/mailgun/notify.py +++ b/homeassistant/components/mailgun/notify.py @@ -1,9 +1,4 @@ -""" -Support for the Mailgun mail notification service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/notify.mailgun/ -""" +"""Support for the Mailgun mail notifications.""" import logging import voluptuous as vol @@ -16,10 +11,11 @@ from homeassistant.components.notify import ( from homeassistant.const import ( CONF_API_KEY, CONF_DOMAIN, CONF_RECIPIENT, CONF_SENDER) +REQUIREMENTS = ['pymailgunner==1.4'] + _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['mailgun'] -REQUIREMENTS = ['pymailgunner==1.4'] # Images to attach to notification ATTR_IMAGES = 'images' @@ -30,7 +26,7 @@ DEFAULT_SANDBOX = False # pylint: disable=no-value-for-parameter PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_RECIPIENT): vol.Email(), - vol.Optional(CONF_SENDER): vol.Email() + vol.Optional(CONF_SENDER): vol.Email(), }) diff --git a/homeassistant/components/map.py b/homeassistant/components/map/__init__.py similarity index 55% rename from homeassistant/components/map.py rename to homeassistant/components/map/__init__.py index d30a7568452..df8ac49a6d5 100644 --- a/homeassistant/components/map.py +++ b/homeassistant/components/map/__init__.py @@ -1,9 +1,4 @@ -""" -Provides a map panel for showing device locations. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/map/ -""" +"""Support for showing device locations.""" DOMAIN = 'map' diff --git a/homeassistant/components/matrix.py b/homeassistant/components/matrix/__init__.py similarity index 94% rename from homeassistant/components/matrix.py rename to homeassistant/components/matrix/__init__.py index 5f6c30aaeba..4b3c1bf4d76 100644 --- a/homeassistant/components/matrix.py +++ b/homeassistant/components/matrix/__init__.py @@ -1,9 +1,4 @@ -""" -The matrix bot component. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/matrix/ -""" +"""The matrix bot component.""" import logging import os from functools import partial @@ -41,8 +36,8 @@ COMMAND_SCHEMA = vol.All( vol.Exclusive(CONF_WORD, 'trigger'): cv.string, vol.Exclusive(CONF_EXPRESSION, 'trigger'): cv.is_regex, vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_ROOMS, default=[]): vol.All(cv.ensure_list, - [cv.string]), + vol.Optional(CONF_ROOMS, default=[]): + vol.All(cv.ensure_list, [cv.string]), }), # Make sure it's either a word or an expression command cv.has_at_least_one_key(CONF_WORD, CONF_EXPRESSION) @@ -54,8 +49,8 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, vol.Required(CONF_USERNAME): cv.matches_regex("@[^:]*:.*"), vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_ROOMS, default=[]): vol.All(cv.ensure_list, - [cv.string]), + vol.Optional(CONF_ROOMS, default=[]): + vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_COMMANDS, default=[]): [COMMAND_SCHEMA] }) }, extra=vol.ALLOW_EXTRA) @@ -76,13 +71,9 @@ def setup(hass, config): try: bot = MatrixBot( - hass, - os.path.join(hass.config.path(), SESSION_FILE), - config[CONF_HOMESERVER], - config[CONF_VERIFY_SSL], - config[CONF_USERNAME], - config[CONF_PASSWORD], - config[CONF_ROOMS], + hass, os.path.join(hass.config.path(), SESSION_FILE), + config[CONF_HOMESERVER], config[CONF_VERIFY_SSL], + config[CONF_USERNAME], config[CONF_PASSWORD], config[CONF_ROOMS], config[CONF_COMMANDS]) hass.data[DOMAIN] = bot except MatrixRequestError as exception: diff --git a/homeassistant/components/notify/matrix.py b/homeassistant/components/matrix/notify.py similarity index 88% rename from homeassistant/components/notify/matrix.py rename to homeassistant/components/matrix/notify.py index fc29ad91dc9..f1f53268c2b 100644 --- a/homeassistant/components/notify/matrix.py +++ b/homeassistant/components/matrix/notify.py @@ -1,9 +1,4 @@ -""" -Matrix notification service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/notify.matrix/ -""" +"""Support for Matrix notifications.""" import logging import voluptuous as vol diff --git a/homeassistant/components/maxcube.py b/homeassistant/components/maxcube/__init__.py similarity index 93% rename from homeassistant/components/maxcube.py rename to homeassistant/components/maxcube/__init__.py index 9980d554232..c398ccbde4f 100644 --- a/homeassistant/components/maxcube.py +++ b/homeassistant/components/maxcube/__init__.py @@ -1,9 +1,4 @@ -""" -Platform for the MAX! Cube LAN Gateway. - -For more details about this component, please refer to the documentation -https://home-assistant.io/components/maxcube/ -""" +"""Support for the MAX! Cube LAN Gateway.""" import logging import time from socket import timeout @@ -38,7 +33,7 @@ CONFIG_GATEWAY = vol.Schema({ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_GATEWAYS, default={}): - vol.All(cv.ensure_list, [CONFIG_GATEWAY]) + vol.All(cv.ensure_list, [CONFIG_GATEWAY]), }), }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/binary_sensor/maxcube.py b/homeassistant/components/maxcube/binary_sensor.py similarity index 91% rename from homeassistant/components/binary_sensor/maxcube.py rename to homeassistant/components/maxcube/binary_sensor.py index 850a416acc5..8d5ab84f6d3 100644 --- a/homeassistant/components/binary_sensor/maxcube.py +++ b/homeassistant/components/maxcube/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for MAX! Window Shutter via MAX! Cube. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/maxcube/ -""" +"""Support for MAX! binary sensors via MAX! Cube.""" import logging from homeassistant.components.binary_sensor import BinarySensorDevice diff --git a/homeassistant/components/climate/maxcube.py b/homeassistant/components/maxcube/climate.py similarity index 97% rename from homeassistant/components/climate/maxcube.py rename to homeassistant/components/maxcube/climate.py index 328cdabde62..f5c4533123f 100644 --- a/homeassistant/components/climate/maxcube.py +++ b/homeassistant/components/maxcube/climate.py @@ -1,9 +1,4 @@ -""" -Support for MAX! Thermostats via MAX! Cube. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/maxcube/ -""" +"""Support for MAX! Thermostats via MAX! Cube.""" import socket import logging diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor/__init__.py similarity index 92% rename from homeassistant/components/media_extractor.py rename to homeassistant/components/media_extractor/__init__.py index 333f62a9aa7..efc3e8bddc8 100644 --- a/homeassistant/components/media_extractor.py +++ b/homeassistant/components/media_extractor/__init__.py @@ -1,20 +1,18 @@ -""" -Decorator service for the media_player.play_media service. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/media_extractor/ -""" +"""Decorator service for the media_player.play_media service.""" import logging import voluptuous as vol from homeassistant.components.media_player import ( - ATTR_ENTITY_ID, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, - DOMAIN as MEDIA_PLAYER_DOMAIN, MEDIA_PLAYER_PLAY_MEDIA_SCHEMA, - SERVICE_PLAY_MEDIA) + MEDIA_PLAYER_PLAY_MEDIA_SCHEMA) +from homeassistant.components.media_player.const import ( + ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, + DOMAIN as MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA) +from homeassistant.const import ( + ATTR_ENTITY_ID) from homeassistant.helpers import config_validation as cv -REQUIREMENTS = ['youtube_dl==2019.01.24'] +REQUIREMENTS = ['youtube_dl==2019.02.08'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index b526d1659ba..ad29a645765 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -30,15 +30,63 @@ from homeassistant.const import ( STATE_OFF, STATE_PLAYING) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa +from homeassistant.helpers.config_validation import ( # noqa + PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.loader import bind_hass +from .const import ( + ATTR_APP_ID, + ATTR_APP_NAME, + ATTR_INPUT_SOURCE, + ATTR_INPUT_SOURCE_LIST, + ATTR_MEDIA_ALBUM_ARTIST, + ATTR_MEDIA_ALBUM_NAME, + ATTR_MEDIA_ARTIST, + ATTR_MEDIA_CHANNEL, + ATTR_MEDIA_CONTENT_ID, + ATTR_MEDIA_CONTENT_TYPE, + ATTR_MEDIA_DURATION, + ATTR_MEDIA_ENQUEUE, + ATTR_MEDIA_EPISODE, + ATTR_MEDIA_PLAYLIST, + ATTR_MEDIA_POSITION, + ATTR_MEDIA_POSITION_UPDATED_AT, + ATTR_MEDIA_SEASON, + ATTR_MEDIA_SEEK_POSITION, + ATTR_MEDIA_SERIES_TITLE, + ATTR_MEDIA_SHUFFLE, + ATTR_MEDIA_TITLE, + ATTR_MEDIA_TRACK, + ATTR_MEDIA_VOLUME_LEVEL, + ATTR_MEDIA_VOLUME_MUTED, + ATTR_SOUND_MODE, + ATTR_SOUND_MODE_LIST, + DOMAIN, + SERVICE_CLEAR_PLAYLIST, + SERVICE_PLAY_MEDIA, + SERVICE_SELECT_SOUND_MODE, + SERVICE_SELECT_SOURCE, + SUPPORT_PAUSE, + SUPPORT_SEEK, + SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_MUTE, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_NEXT_TRACK, + SUPPORT_PLAY_MEDIA, + SUPPORT_SELECT_SOURCE, + SUPPORT_STOP, + SUPPORT_CLEAR_PLAYLIST, + SUPPORT_PLAY, + SUPPORT_SHUFFLE_SET, + SUPPORT_SELECT_SOUND_MODE, +) +from .reproduce_state import async_reproduce_states # noqa + _LOGGER = logging.getLogger(__name__) _RND = SystemRandom() -DOMAIN = 'media_player' DEPENDENCIES = ['http'] ENTITY_ID_FORMAT = DOMAIN + '.{}' @@ -54,67 +102,8 @@ ENTITY_IMAGE_CACHE = { CACHE_MAXSIZE: 16 } -SERVICE_PLAY_MEDIA = 'play_media' -SERVICE_SELECT_SOURCE = 'select_source' -SERVICE_SELECT_SOUND_MODE = 'select_sound_mode' -SERVICE_CLEAR_PLAYLIST = 'clear_playlist' - -ATTR_MEDIA_VOLUME_LEVEL = 'volume_level' -ATTR_MEDIA_VOLUME_MUTED = 'is_volume_muted' -ATTR_MEDIA_SEEK_POSITION = 'seek_position' -ATTR_MEDIA_CONTENT_ID = 'media_content_id' -ATTR_MEDIA_CONTENT_TYPE = 'media_content_type' -ATTR_MEDIA_DURATION = 'media_duration' -ATTR_MEDIA_POSITION = 'media_position' -ATTR_MEDIA_POSITION_UPDATED_AT = 'media_position_updated_at' -ATTR_MEDIA_TITLE = 'media_title' -ATTR_MEDIA_ARTIST = 'media_artist' -ATTR_MEDIA_ALBUM_NAME = 'media_album_name' -ATTR_MEDIA_ALBUM_ARTIST = 'media_album_artist' -ATTR_MEDIA_TRACK = 'media_track' -ATTR_MEDIA_SERIES_TITLE = 'media_series_title' -ATTR_MEDIA_SEASON = 'media_season' -ATTR_MEDIA_EPISODE = 'media_episode' -ATTR_MEDIA_CHANNEL = 'media_channel' -ATTR_MEDIA_PLAYLIST = 'media_playlist' -ATTR_APP_ID = 'app_id' -ATTR_APP_NAME = 'app_name' -ATTR_INPUT_SOURCE = 'source' -ATTR_INPUT_SOURCE_LIST = 'source_list' -ATTR_SOUND_MODE = 'sound_mode' -ATTR_SOUND_MODE_LIST = 'sound_mode_list' -ATTR_MEDIA_ENQUEUE = 'enqueue' -ATTR_MEDIA_SHUFFLE = 'shuffle' - -MEDIA_TYPE_MUSIC = 'music' -MEDIA_TYPE_TVSHOW = 'tvshow' -MEDIA_TYPE_MOVIE = 'movie' -MEDIA_TYPE_VIDEO = 'video' -MEDIA_TYPE_EPISODE = 'episode' -MEDIA_TYPE_CHANNEL = 'channel' -MEDIA_TYPE_PLAYLIST = 'playlist' -MEDIA_TYPE_URL = 'url' - SCAN_INTERVAL = timedelta(seconds=10) -SUPPORT_PAUSE = 1 -SUPPORT_SEEK = 2 -SUPPORT_VOLUME_SET = 4 -SUPPORT_VOLUME_MUTE = 8 -SUPPORT_PREVIOUS_TRACK = 16 -SUPPORT_NEXT_TRACK = 32 - -SUPPORT_TURN_ON = 128 -SUPPORT_TURN_OFF = 256 -SUPPORT_PLAY_MEDIA = 512 -SUPPORT_VOLUME_STEP = 1024 -SUPPORT_SELECT_SOURCE = 2048 -SUPPORT_STOP = 4096 -SUPPORT_CLEAR_PLAYLIST = 8192 -SUPPORT_PLAY = 16384 -SUPPORT_SHUFFLE_SET = 32768 -SUPPORT_SELECT_SOUND_MODE = 65536 - # Service call validation schemas MEDIA_PLAYER_SCHEMA = vol.Schema({ ATTR_ENTITY_ID: cv.comp_entity_ids, diff --git a/homeassistant/components/media_player/anthemav.py b/homeassistant/components/media_player/anthemav.py index a0bc3d05dcb..d48f90d2bd7 100644 --- a/homeassistant/components/media_player/anthemav.py +++ b/homeassistant/components/media_player/anthemav.py @@ -9,8 +9,10 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice) + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET) from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_STOP, STATE_OFF, STATE_ON) diff --git a/homeassistant/components/media_player/aquostv.py b/homeassistant/components/media_player/aquostv.py index 5c1994e65fc..59723b47522 100644 --- a/homeassistant/components/media_player/aquostv.py +++ b/homeassistant/components/media_player/aquostv.py @@ -9,10 +9,12 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_VOLUME_STEP, MediaPlayerDevice) + SUPPORT_VOLUME_STEP) from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_TIMEOUT, CONF_USERNAME, STATE_OFF, STATE_ON) diff --git a/homeassistant/components/media_player/blackbird.py b/homeassistant/components/media_player/blackbird.py index 2c78bb24bbd..2daa2656e83 100644 --- a/homeassistant/components/media_player/blackbird.py +++ b/homeassistant/components/media_player/blackbird.py @@ -10,8 +10,10 @@ import socket import voluptuous as vol from homeassistant.components.media_player import ( - DOMAIN, MEDIA_PLAYER_SCHEMA, PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE, - SUPPORT_TURN_OFF, SUPPORT_TURN_ON, MediaPlayerDevice) + MediaPlayerDevice, MEDIA_PLAYER_SCHEMA, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + DOMAIN, SUPPORT_SELECT_SOURCE, + SUPPORT_TURN_OFF, SUPPORT_TURN_ON) from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_PORT, CONF_TYPE, STATE_OFF, STATE_ON) diff --git a/homeassistant/components/media_player/bluesound.py b/homeassistant/components/media_player/bluesound.py index 998f559bc8a..c6a8c51ca58 100644 --- a/homeassistant/components/media_player/bluesound.py +++ b/homeassistant/components/media_player/bluesound.py @@ -16,12 +16,13 @@ import async_timeout import voluptuous as vol from homeassistant.components.media_player import ( - ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA, + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, SUPPORT_STOP, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, - MediaPlayerDevice) + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP) from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, CONF_HOSTS, CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_OFF, diff --git a/homeassistant/components/media_player/braviatv.py b/homeassistant/components/media_player/braviatv.py index 04dc013108f..7efb7abd569 100644 --- a/homeassistant/components/media_player/braviatv.py +++ b/homeassistant/components/media_player/braviatv.py @@ -10,10 +10,12 @@ import re import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_VOLUME_STEP, MediaPlayerDevice) + SUPPORT_VOLUME_STEP) from homeassistant.const import CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json, save_json diff --git a/homeassistant/components/media_player/channels.py b/homeassistant/components/media_player/channels.py index 43259c40f65..2f7b169601c 100644 --- a/homeassistant/components/media_player/channels.py +++ b/homeassistant/components/media_player/channels.py @@ -9,11 +9,12 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( DOMAIN, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_EPISODE, MEDIA_TYPE_MOVIE, - MEDIA_TYPE_TVSHOW, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + MEDIA_TYPE_TVSHOW, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, - SUPPORT_SELECT_SOURCE, SUPPORT_STOP, SUPPORT_VOLUME_MUTE, - MediaPlayerDevice) + SUPPORT_SELECT_SOURCE, SUPPORT_STOP, SUPPORT_VOLUME_MUTE) from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_PORT, STATE_IDLE, STATE_PAUSED, STATE_PLAYING) diff --git a/homeassistant/components/media_player/clementine.py b/homeassistant/components/media_player/clementine.py index 2add2bd682a..24df7c24611 100644 --- a/homeassistant/components/media_player/clementine.py +++ b/homeassistant/components/media_player/clementine.py @@ -11,9 +11,11 @@ import time import voluptuous as vol from homeassistant.components.media_player import ( - MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, - SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, MediaPlayerDevice) + SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP) from homeassistant.const import ( CONF_ACCESS_TOKEN, CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_PAUSED, STATE_PLAYING) diff --git a/homeassistant/components/media_player/cmus.py b/homeassistant/components/media_player/cmus.py index 2711ac1ff11..20b292749b4 100644 --- a/homeassistant/components/media_player/cmus.py +++ b/homeassistant/components/media_player/cmus.py @@ -9,10 +9,11 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, - SUPPORT_SEEK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_SET, - MediaPlayerDevice) + SUPPORT_SEEK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_SET) from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, STATE_OFF, STATE_PAUSED, STATE_PLAYING) diff --git a/homeassistant/components/media_player/const.py b/homeassistant/components/media_player/const.py new file mode 100644 index 00000000000..bf7e6b4e0ce --- /dev/null +++ b/homeassistant/components/media_player/const.py @@ -0,0 +1,62 @@ +"""Proides the constants needed for component.""" + +ATTR_APP_ID = 'app_id' +ATTR_APP_NAME = 'app_name' +ATTR_INPUT_SOURCE = 'source' +ATTR_INPUT_SOURCE_LIST = 'source_list' +ATTR_MEDIA_ALBUM_ARTIST = 'media_album_artist' +ATTR_MEDIA_ALBUM_NAME = 'media_album_name' +ATTR_MEDIA_ARTIST = 'media_artist' +ATTR_MEDIA_CHANNEL = 'media_channel' +ATTR_MEDIA_CONTENT_ID = 'media_content_id' +ATTR_MEDIA_CONTENT_TYPE = 'media_content_type' +ATTR_MEDIA_DURATION = 'media_duration' +ATTR_MEDIA_ENQUEUE = 'enqueue' +ATTR_MEDIA_EPISODE = 'media_episode' +ATTR_MEDIA_PLAYLIST = 'media_playlist' +ATTR_MEDIA_POSITION = 'media_position' +ATTR_MEDIA_POSITION_UPDATED_AT = 'media_position_updated_at' +ATTR_MEDIA_SEASON = 'media_season' +ATTR_MEDIA_SEEK_POSITION = 'seek_position' +ATTR_MEDIA_SERIES_TITLE = 'media_series_title' +ATTR_MEDIA_SHUFFLE = 'shuffle' +ATTR_MEDIA_TITLE = 'media_title' +ATTR_MEDIA_TRACK = 'media_track' +ATTR_MEDIA_VOLUME_LEVEL = 'volume_level' +ATTR_MEDIA_VOLUME_MUTED = 'is_volume_muted' +ATTR_SOUND_MODE = 'sound_mode' +ATTR_SOUND_MODE_LIST = 'sound_mode_list' + +DOMAIN = 'media_player' + +MEDIA_TYPE_MUSIC = 'music' +MEDIA_TYPE_TVSHOW = 'tvshow' +MEDIA_TYPE_MOVIE = 'movie' +MEDIA_TYPE_VIDEO = 'video' +MEDIA_TYPE_EPISODE = 'episode' +MEDIA_TYPE_CHANNEL = 'channel' +MEDIA_TYPE_PLAYLIST = 'playlist' +MEDIA_TYPE_URL = 'url' + +SERVICE_CLEAR_PLAYLIST = 'clear_playlist' +SERVICE_PLAY_MEDIA = 'play_media' +SERVICE_SELECT_SOUND_MODE = 'select_sound_mode' +SERVICE_SELECT_SOURCE = 'select_source' + +SUPPORT_PAUSE = 1 +SUPPORT_SEEK = 2 +SUPPORT_VOLUME_SET = 4 +SUPPORT_VOLUME_MUTE = 8 +SUPPORT_PREVIOUS_TRACK = 16 +SUPPORT_NEXT_TRACK = 32 + +SUPPORT_TURN_ON = 128 +SUPPORT_TURN_OFF = 256 +SUPPORT_PLAY_MEDIA = 512 +SUPPORT_VOLUME_STEP = 1024 +SUPPORT_SELECT_SOURCE = 2048 +SUPPORT_STOP = 4096 +SUPPORT_CLEAR_PLAYLIST = 8192 +SUPPORT_PLAY = 16384 +SUPPORT_SHUFFLE_SET = 32768 +SUPPORT_SELECT_SOUND_MODE = 65536 diff --git a/homeassistant/components/media_player/demo.py b/homeassistant/components/media_player/demo.py index 8a88e3bd74e..de455879d3d 100644 --- a/homeassistant/components/media_player/demo.py +++ b/homeassistant/components/media_player/demo.py @@ -6,12 +6,13 @@ https://home-assistant.io/components/demo/ """ import homeassistant.util.dt as dt_util from homeassistant.components.media_player import ( + MediaPlayerDevice) +from homeassistant.components.media_player.const import ( MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, SUPPORT_TURN_OFF, - SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - MediaPlayerDevice) + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET) from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING diff --git a/homeassistant/components/media_player/denon.py b/homeassistant/components/media_player/denon.py index 79b69b551ce..3dc4e550d9b 100644 --- a/homeassistant/components/media_player/denon.py +++ b/homeassistant/components/media_player/denon.py @@ -10,10 +10,11 @@ import telnetlib import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_STOP, - SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - MediaPlayerDevice) + SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET) from homeassistant.const import CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/media_player/denonavr.py b/homeassistant/components/media_player/denonavr.py index c565a161b10..380484add53 100644 --- a/homeassistant/components/media_player/denonavr.py +++ b/homeassistant/components/media_player/denonavr.py @@ -11,17 +11,19 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - MEDIA_TYPE_CHANNEL, MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_CHANNEL, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_VOLUME_STEP, MediaPlayerDevice) + SUPPORT_VOLUME_STEP) from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_TIMEOUT, CONF_ZONE, STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['denonavr==0.7.7'] +REQUIREMENTS = ['denonavr==0.7.8'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_player/directv.py b/homeassistant/components/media_player/directv.py index 707014328c6..9c5a3bf07b8 100644 --- a/homeassistant/components/media_player/directv.py +++ b/homeassistant/components/media_player/directv.py @@ -9,10 +9,11 @@ import requests import voluptuous as vol from homeassistant.components.media_player import ( - MEDIA_TYPE_CHANNEL, MEDIA_TYPE_MOVIE, MEDIA_TYPE_TVSHOW, PLATFORM_SCHEMA, + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_CHANNEL, MEDIA_TYPE_MOVIE, MEDIA_TYPE_TVSHOW, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, - SUPPORT_PREVIOUS_TRACK, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - MediaPlayerDevice) + SUPPORT_PREVIOUS_TRACK, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON) from homeassistant.const import ( CONF_DEVICE, CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_PAUSED, STATE_PLAYING) diff --git a/homeassistant/components/media_player/dlna_dmr.py b/homeassistant/components/media_player/dlna_dmr.py index 802b2b597fc..03015cd5c01 100644 --- a/homeassistant/components/media_player/dlna_dmr.py +++ b/homeassistant/components/media_player/dlna_dmr.py @@ -14,9 +14,11 @@ import aiohttp import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_STOP, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice) + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET) from homeassistant.const import ( CONF_NAME, CONF_URL, EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING) diff --git a/homeassistant/components/media_player/dunehd.py b/homeassistant/components/media_player/dunehd.py index 00c8ff3f4df..796aea86414 100644 --- a/homeassistant/components/media_player/dunehd.py +++ b/homeassistant/components/media_player/dunehd.py @@ -7,9 +7,11 @@ https://home-assistant.io/components/media_player.dunehd/ import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, - SUPPORT_TURN_ON, MediaPlayerDevice) + SUPPORT_TURN_ON) from homeassistant.const import ( CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/media_player/emby.py b/homeassistant/components/media_player/emby.py index dd43d48ee6a..b1259db913d 100644 --- a/homeassistant/components/media_player/emby.py +++ b/homeassistant/components/media_player/emby.py @@ -9,9 +9,11 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, - PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, - SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_STOP, MediaPlayerDevice) + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, + SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_STOP) from homeassistant.const import ( CONF_API_KEY, CONF_HOST, CONF_PORT, CONF_SSL, DEVICE_DEFAULT_NAME, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_OFF, diff --git a/homeassistant/components/media_player/epson.py b/homeassistant/components/media_player/epson.py index bb1618f2351..38c0ffacc32 100644 --- a/homeassistant/components/media_player/epson.py +++ b/homeassistant/components/media_player/epson.py @@ -9,10 +9,11 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - DOMAIN, MEDIA_PLAYER_SCHEMA, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, + MediaPlayerDevice, MEDIA_PLAYER_SCHEMA, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + DOMAIN, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, - SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, - MediaPlayerDevice) + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP) from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_PORT, CONF_SSL, STATE_OFF, STATE_ON) diff --git a/homeassistant/components/media_player/firetv.py b/homeassistant/components/media_player/firetv.py index c04ed96d6e0..58f1913b9f9 100644 --- a/homeassistant/components/media_player/firetv.py +++ b/homeassistant/components/media_player/firetv.py @@ -10,9 +10,11 @@ import threading import voluptuous as vol from homeassistant.components.media_player import ( - MediaPlayerDevice, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_STOP, - SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_SET, ) + SUPPORT_TURN_OFF, SUPPORT_TURN_ON) from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PORT, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, STATE_STANDBY) @@ -25,7 +27,7 @@ _LOGGER = logging.getLogger(__name__) SUPPORT_FIRETV = SUPPORT_PAUSE | \ SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \ SUPPORT_NEXT_TRACK | SUPPORT_SELECT_SOURCE | SUPPORT_STOP | \ - SUPPORT_VOLUME_SET | SUPPORT_PLAY + SUPPORT_PLAY CONF_ADBKEY = 'adbkey' CONF_GET_SOURCE = 'get_source' @@ -171,6 +173,11 @@ class FireTVDevice(MediaPlayerDevice): """Return whether or not the ADB connection is valid.""" return self._available + @property + def app_id(self): + """Return the current app.""" + return self._current_app + @property def source(self): """Return the current app.""" diff --git a/homeassistant/components/media_player/frontier_silicon.py b/homeassistant/components/media_player/frontier_silicon.py index 67c84bd7b1b..ed7041a3b82 100644 --- a/homeassistant/components/media_player/frontier_silicon.py +++ b/homeassistant/components/media_player/frontier_silicon.py @@ -9,11 +9,12 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_SELECT_SOURCE, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, - MediaPlayerDevice) + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP) from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_PORT, STATE_OFF, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN) diff --git a/homeassistant/components/media_player/gpmdp.py b/homeassistant/components/media_player/gpmdp.py index b4ede671d52..c72d14ebb8a 100644 --- a/homeassistant/components/media_player/gpmdp.py +++ b/homeassistant/components/media_player/gpmdp.py @@ -12,9 +12,10 @@ import time import voluptuous as vol from homeassistant.components.media_player import ( - MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, - SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_VOLUME_SET, - MediaPlayerDevice) + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_VOLUME_SET) from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_PAUSED, STATE_PLAYING) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/media_player/gstreamer.py b/homeassistant/components/media_player/gstreamer.py index fa8545bae03..c6571894472 100644 --- a/homeassistant/components/media_player/gstreamer.py +++ b/homeassistant/components/media_player/gstreamer.py @@ -9,8 +9,10 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, - SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_SET, MediaPlayerDevice) + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_SET) from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP, STATE_IDLE import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/media_player/harman_kardon_avr.py b/homeassistant/components/media_player/harman_kardon_avr.py index 46d1dd4d698..334757c086d 100644 --- a/homeassistant/components/media_player/harman_kardon_avr.py +++ b/homeassistant/components/media_player/harman_kardon_avr.py @@ -10,9 +10,10 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.media_player import ( + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, - PLATFORM_SCHEMA, SUPPORT_TURN_ON, SUPPORT_SELECT_SOURCE, - MediaPlayerDevice) + SUPPORT_TURN_ON, SUPPORT_SELECT_SOURCE) from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON) diff --git a/homeassistant/components/media_player/horizon.py b/homeassistant/components/media_player/horizon.py index 058796ea46d..e7cfcfe62b1 100644 --- a/homeassistant/components/media_player/horizon.py +++ b/homeassistant/components/media_player/horizon.py @@ -11,9 +11,11 @@ import voluptuous as vol from homeassistant import util from homeassistant.components.media_player import ( - MEDIA_TYPE_CHANNEL, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, - SUPPORT_TURN_ON, MediaPlayerDevice) + SUPPORT_TURN_ON) from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_PAUSED, STATE_PLAYING) from homeassistant.exceptions import PlatformNotReady diff --git a/homeassistant/components/media_player/itunes.py b/homeassistant/components/media_player/itunes.py index e2ae179676b..f8380032aea 100644 --- a/homeassistant/components/media_player/itunes.py +++ b/homeassistant/components/media_player/itunes.py @@ -10,10 +10,12 @@ import requests import voluptuous as vol from homeassistant.components.media_player import ( - MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, - SUPPORT_VOLUME_SET, MediaPlayerDevice) + SUPPORT_VOLUME_SET) from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PORT, CONF_SSL, STATE_IDLE, STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING) diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index a83287eb617..f8d0cdc5a12 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -16,13 +16,14 @@ import aiohttp import voluptuous as vol from homeassistant.components.media_player import ( - DOMAIN, MEDIA_PLAYER_SCHEMA, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_MOVIE, + MediaPlayerDevice, MEDIA_PLAYER_SCHEMA, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + DOMAIN, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, - PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_SHUFFLE_SET, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, - MediaPlayerDevice) + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP) from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_PROXY_SSL, CONF_TIMEOUT, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, STATE_IDLE, diff --git a/homeassistant/components/media_player/lg_netcast.py b/homeassistant/components/media_player/lg_netcast.py index c2f63c71f89..7c5d9789372 100644 --- a/homeassistant/components/media_player/lg_netcast.py +++ b/homeassistant/components/media_player/lg_netcast.py @@ -12,10 +12,11 @@ import voluptuous as vol from homeassistant import util from homeassistant.components.media_player import ( - MEDIA_TYPE_CHANNEL, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, - SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, - MediaPlayerDevice) + SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP) from homeassistant.const import ( CONF_ACCESS_TOKEN, CONF_HOST, CONF_NAME, STATE_OFF, STATE_PAUSED, STATE_PLAYING) diff --git a/homeassistant/components/media_player/lg_soundbar.py b/homeassistant/components/media_player/lg_soundbar.py index 38b27bd074a..b45baf88bca 100644 --- a/homeassistant/components/media_player/lg_soundbar.py +++ b/homeassistant/components/media_player/lg_soundbar.py @@ -7,8 +7,10 @@ https://home-assistant.io/components/media_player.lg_soundbar/ import logging from homeassistant.components.media_player import ( + MediaPlayerDevice) +from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOURCE, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_SELECT_SOUND_MODE, MediaPlayerDevice) + SUPPORT_SELECT_SOUND_MODE) from homeassistant.const import STATE_ON diff --git a/homeassistant/components/media_player/liveboxplaytv.py b/homeassistant/components/media_player/liveboxplaytv.py index 3f8ea2cfd48..f69c3c67aec 100644 --- a/homeassistant/components/media_player/liveboxplaytv.py +++ b/homeassistant/components/media_player/liveboxplaytv.py @@ -11,10 +11,12 @@ import requests import voluptuous as vol from homeassistant.components.media_player import ( - MEDIA_TYPE_CHANNEL, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, - SUPPORT_VOLUME_STEP, MediaPlayerDevice) + SUPPORT_VOLUME_STEP) from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING) diff --git a/homeassistant/components/media_player/mediaroom.py b/homeassistant/components/media_player/mediaroom.py index 345b58cbbe4..29cc7332936 100644 --- a/homeassistant/components/media_player/mediaroom.py +++ b/homeassistant/components/media_player/mediaroom.py @@ -9,10 +9,12 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - MEDIA_TYPE_CHANNEL, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, - SUPPORT_VOLUME_STEP, MediaPlayerDevice) + SUPPORT_VOLUME_STEP) from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_OPTIMISTIC, CONF_TIMEOUT, EVENT_HOMEASSISTANT_STOP, STATE_OFF, STATE_PAUSED, STATE_PLAYING, diff --git a/homeassistant/components/media_player/monoprice.py b/homeassistant/components/media_player/monoprice.py index 96b8dcbcf26..e98ad47a6e7 100644 --- a/homeassistant/components/media_player/monoprice.py +++ b/homeassistant/components/media_player/monoprice.py @@ -9,9 +9,11 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - DOMAIN, MEDIA_PLAYER_SCHEMA, PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE, + MediaPlayerDevice, MEDIA_PLAYER_SCHEMA, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + DOMAIN, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_VOLUME_STEP, MediaPlayerDevice) + SUPPORT_VOLUME_STEP) from homeassistant.const import ( ATTR_ENTITY_ID, CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/media_player/mpchc.py b/homeassistant/components/media_player/mpchc.py index e6bc1f2699d..61d89c6d0b1 100644 --- a/homeassistant/components/media_player/mpchc.py +++ b/homeassistant/components/media_player/mpchc.py @@ -11,9 +11,11 @@ import requests import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_STOP, SUPPORT_VOLUME_MUTE, - SUPPORT_VOLUME_STEP, MediaPlayerDevice) + SUPPORT_VOLUME_STEP) from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PORT, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING) diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py index 09d0a976b82..9d8015109b2 100644 --- a/homeassistant/components/media_player/mpd.py +++ b/homeassistant/components/media_player/mpd.py @@ -11,12 +11,14 @@ import os import voluptuous as vol from homeassistant.components.media_player import ( - MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, PLATFORM_SCHEMA, + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_VOLUME_STEP, MediaPlayerDevice) + SUPPORT_VOLUME_STEP) from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, STATE_OFF, STATE_PAUSED, STATE_PLAYING) diff --git a/homeassistant/components/media_player/nad.py b/homeassistant/components/media_player/nad.py index 57dca116f6b..127be02dac4 100644 --- a/homeassistant/components/media_player/nad.py +++ b/homeassistant/components/media_player/nad.py @@ -10,9 +10,10 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, - MediaPlayerDevice) + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP) from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON REQUIREMENTS = ['nad_receiver==0.0.11'] diff --git a/homeassistant/components/media_player/onkyo.py b/homeassistant/components/media_player/onkyo.py index 5ff54201b3c..df30c7e0782 100644 --- a/homeassistant/components/media_player/onkyo.py +++ b/homeassistant/components/media_player/onkyo.py @@ -12,9 +12,11 @@ from typing import List # noqa: F401 import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_SELECT_SOURCE, + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_VOLUME_STEP, MediaPlayerDevice, DOMAIN) + SUPPORT_VOLUME_STEP, DOMAIN) from homeassistant.const import ( CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, ATTR_ENTITY_ID) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/media_player/openhome.py b/homeassistant/components/media_player/openhome.py index ab23f8a7f9a..d828284a563 100644 --- a/homeassistant/components/media_player/openhome.py +++ b/homeassistant/components/media_player/openhome.py @@ -7,10 +7,11 @@ https://home-assistant.io/components/media_player.openhome/ import logging from homeassistant.components.media_player import ( + MediaPlayerDevice) +from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, - MediaPlayerDevice) + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP) from homeassistant.const import ( STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING) diff --git a/homeassistant/components/media_player/panasonic_bluray.py b/homeassistant/components/media_player/panasonic_bluray.py index 041efed74bf..36a3160d3b5 100644 --- a/homeassistant/components/media_player/panasonic_bluray.py +++ b/homeassistant/components/media_player/panasonic_bluray.py @@ -10,8 +10,10 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_STOP, - SUPPORT_TURN_OFF, SUPPORT_TURN_ON, MediaPlayerDevice) + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_STOP, + SUPPORT_TURN_OFF, SUPPORT_TURN_ON) from homeassistant.const import ( CONF_HOST, CONF_NAME, STATE_IDLE, STATE_OFF, STATE_PLAYING) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/media_player/panasonic_viera.py b/homeassistant/components/media_player/panasonic_viera.py index bff108d70d7..e5ce22e9524 100644 --- a/homeassistant/components/media_player/panasonic_viera.py +++ b/homeassistant/components/media_player/panasonic_viera.py @@ -9,10 +9,12 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - MEDIA_TYPE_URL, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_URL, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_VOLUME_STEP, MediaPlayerDevice) + SUPPORT_VOLUME_STEP) from homeassistant.const import ( CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/media_player/pandora.py b/homeassistant/components/media_player/pandora.py index 231ea5302ae..ca78f7a4318 100644 --- a/homeassistant/components/media_player/pandora.py +++ b/homeassistant/components/media_player/pandora.py @@ -12,14 +12,14 @@ import shutil import signal from homeassistant import util -from homeassistant.components.media_player import ( - MEDIA_TYPE_MUSIC, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PLAY, - SERVICE_MEDIA_PLAY_PAUSE, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_UP, - SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_SELECT_SOURCE, - SUPPORT_TURN_OFF, SUPPORT_TURN_ON, MediaPlayerDevice) +from homeassistant.components.media_player import MediaPlayerDevice +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, + SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON) from homeassistant.const import ( - EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_OFF, STATE_PAUSED, - STATE_PLAYING) + EVENT_HOMEASSISTANT_STOP, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PLAY, + SERVICE_MEDIA_PLAY_PAUSE, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_UP, + STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING) REQUIREMENTS = ['pexpect==4.6.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_player/philips_js.py b/homeassistant/components/media_player/philips_js.py index 506e5a9e479..4f8a1339781 100644 --- a/homeassistant/components/media_player/philips_js.py +++ b/homeassistant/components/media_player/philips_js.py @@ -10,10 +10,11 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + SUPPORT_NEXT_TRACK, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, - MediaPlayerDevice) + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP) from homeassistant.const import ( CONF_API_VERSION, CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/media_player/pioneer.py b/homeassistant/components/media_player/pioneer.py index 171343d4adb..00fa453100a 100644 --- a/homeassistant/components/media_player/pioneer.py +++ b/homeassistant/components/media_player/pioneer.py @@ -10,9 +10,10 @@ import telnetlib import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_SELECT_SOURCE, - SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - MediaPlayerDevice) + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_SELECT_SOURCE, + SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET) from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PORT, CONF_TIMEOUT, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/media_player/pjlink.py b/homeassistant/components/media_player/pjlink.py index 0609b75f98d..c1b883a0295 100644 --- a/homeassistant/components/media_player/pjlink.py +++ b/homeassistant/components/media_player/pjlink.py @@ -9,8 +9,10 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, MediaPlayerDevice) + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE) from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index 2110c42d371..c67849edee9 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -13,10 +13,11 @@ import voluptuous as vol from homeassistant import util from homeassistant.components.media_player import ( - MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, PLATFORM_SCHEMA, + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, - SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - MediaPlayerDevice) + SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET) from homeassistant.const import ( DEVICE_DEFAULT_NAME, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING) from homeassistant.helpers import config_validation as cv diff --git a/homeassistant/components/media_player/reproduce_state.py b/homeassistant/components/media_player/reproduce_state.py new file mode 100644 index 00000000000..cbe98704615 --- /dev/null +++ b/homeassistant/components/media_player/reproduce_state.py @@ -0,0 +1,87 @@ +"""Module that groups code required to handle state restore for component.""" +import asyncio +from typing import Iterable, Optional + +from homeassistant.const import ( + SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_SEEK, + SERVICE_MEDIA_STOP, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_MUTE, + SERVICE_VOLUME_SET, STATE_IDLE, STATE_OFF, STATE_ON, STATE_PAUSED, + STATE_PLAYING) +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.loader import bind_hass + +from .const import ( + ATTR_MEDIA_VOLUME_LEVEL, + ATTR_MEDIA_VOLUME_MUTED, + ATTR_MEDIA_SEEK_POSITION, + ATTR_INPUT_SOURCE, + ATTR_SOUND_MODE, + ATTR_MEDIA_CONTENT_TYPE, + ATTR_MEDIA_CONTENT_ID, + ATTR_MEDIA_ENQUEUE, + SERVICE_PLAY_MEDIA, + SERVICE_SELECT_SOURCE, + SERVICE_SELECT_SOUND_MODE, + DOMAIN, +) + + +async def _async_reproduce_states(hass: HomeAssistantType, + state: State, + context: Optional[Context] = None) -> None: + """Reproduce component states.""" + async def call_service(service: str, keys: Iterable): + """Call service with set of attributes given.""" + data = {} + data['entity_id'] = state.entity_id + for key in keys: + if key in state.attributes: + data[key] = state.attributes[key] + + await hass.services.async_call( + DOMAIN, service, data, + blocking=True, context=context) + + if state.state == STATE_ON: + await call_service(SERVICE_TURN_ON, []) + elif state.state == STATE_OFF: + await call_service(SERVICE_TURN_OFF, []) + elif state.state == STATE_PLAYING: + await call_service(SERVICE_MEDIA_PLAY, []) + elif state.state == STATE_IDLE: + await call_service(SERVICE_MEDIA_STOP, []) + elif state.state == STATE_PAUSED: + await call_service(SERVICE_MEDIA_PAUSE, []) + + if ATTR_MEDIA_VOLUME_LEVEL in state.attributes: + await call_service(SERVICE_VOLUME_SET, [ATTR_MEDIA_VOLUME_LEVEL]) + + if ATTR_MEDIA_VOLUME_MUTED in state.attributes: + await call_service(SERVICE_VOLUME_MUTE, [ATTR_MEDIA_VOLUME_MUTED]) + + if ATTR_MEDIA_SEEK_POSITION in state.attributes: + await call_service(SERVICE_MEDIA_SEEK, [ATTR_MEDIA_SEEK_POSITION]) + + if ATTR_INPUT_SOURCE in state.attributes: + await call_service(SERVICE_SELECT_SOURCE, [ATTR_INPUT_SOURCE]) + + if ATTR_SOUND_MODE in state.attributes: + await call_service(SERVICE_SELECT_SOUND_MODE, [ATTR_SOUND_MODE]) + + if (ATTR_MEDIA_CONTENT_TYPE in state.attributes) and \ + (ATTR_MEDIA_CONTENT_ID in state.attributes): + await call_service(SERVICE_PLAY_MEDIA, + [ATTR_MEDIA_CONTENT_TYPE, + ATTR_MEDIA_CONTENT_ID, + ATTR_MEDIA_ENQUEUE]) + + +@bind_hass +async def async_reproduce_states(hass: HomeAssistantType, + states: Iterable[State], + context: Optional[Context] = None) -> None: + """Reproduce component states.""" + await asyncio.gather(*[ + _async_reproduce_states(hass, state, context) + for state in states]) diff --git a/homeassistant/components/media_player/russound_rio.py b/homeassistant/components/media_player/russound_rio.py index 19cc2228d32..972594e07e6 100644 --- a/homeassistant/components/media_player/russound_rio.py +++ b/homeassistant/components/media_player/russound_rio.py @@ -9,9 +9,10 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, - SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - MediaPlayerDevice) + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_MUSIC, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET) from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_STOP, STATE_OFF, STATE_ON) diff --git a/homeassistant/components/media_player/russound_rnet.py b/homeassistant/components/media_player/russound_rnet.py index 7f4d04eb634..6d919cdf7a8 100644 --- a/homeassistant/components/media_player/russound_rnet.py +++ b/homeassistant/components/media_player/russound_rnet.py @@ -9,8 +9,10 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice) + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET) from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py index 9def9875ab4..db6bd317c40 100644 --- a/homeassistant/components/media_player/samsungtv.py +++ b/homeassistant/components/media_player/samsungtv.py @@ -12,10 +12,11 @@ import socket import voluptuous as vol from homeassistant.components.media_player import ( - MEDIA_TYPE_CHANNEL, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, - SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, - MediaPlayerDevice) + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP) from homeassistant.const import ( CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT, CONF_TIMEOUT, STATE_OFF, STATE_ON) diff --git a/homeassistant/components/media_player/snapcast.py b/homeassistant/components/media_player/snapcast.py index cfe2f997295..74b17ae5ff1 100644 --- a/homeassistant/components/media_player/snapcast.py +++ b/homeassistant/components/media_player/snapcast.py @@ -10,8 +10,10 @@ import socket import voluptuous as vol from homeassistant.components.media_player import ( - DOMAIN, PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE, SUPPORT_VOLUME_MUTE, - SUPPORT_VOLUME_SET, MediaPlayerDevice) + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + DOMAIN, SUPPORT_SELECT_SOURCE, SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET) from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, CONF_PORT, STATE_IDLE, STATE_OFF, STATE_ON, STATE_PLAYING, STATE_UNKNOWN) diff --git a/homeassistant/components/media_player/songpal.py b/homeassistant/components/media_player/songpal.py index e67578539ad..7665b409d1d 100644 --- a/homeassistant/components/media_player/songpal.py +++ b/homeassistant/components/media_player/songpal.py @@ -11,9 +11,11 @@ from collections import OrderedDict import voluptuous as vol from homeassistant.components.media_player import ( - DOMAIN, PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + DOMAIN, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_VOLUME_STEP, MediaPlayerDevice) + SUPPORT_VOLUME_STEP) from homeassistant.const import ( ATTR_ENTITY_ID, CONF_NAME, STATE_OFF, STATE_ON, EVENT_HOMEASSISTANT_STOP) from homeassistant.exceptions import PlatformNotReady diff --git a/homeassistant/components/media_player/soundtouch.py b/homeassistant/components/media_player/soundtouch.py index 037a9b88fc6..b2045b9b65e 100644 --- a/homeassistant/components/media_player/soundtouch.py +++ b/homeassistant/components/media_player/soundtouch.py @@ -10,10 +10,11 @@ import re import voluptuous as vol from homeassistant.components.media_player import ( - DOMAIN, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + DOMAIN, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, - MediaPlayerDevice) + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP) from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_PAUSED, STATE_PLAYING, STATE_UNAVAILABLE) diff --git a/homeassistant/components/media_player/spotify.py b/homeassistant/components/media_player/spotify.py index 4fbd43f3f16..9965487ded9 100644 --- a/homeassistant/components/media_player/spotify.py +++ b/homeassistant/components/media_player/spotify.py @@ -11,10 +11,11 @@ import voluptuous as vol from homeassistant.components.http import HomeAssistantView from homeassistant.components.media_player import ( - MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, - SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, SUPPORT_VOLUME_SET, - MediaPlayerDevice) + SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, SUPPORT_VOLUME_SET) from homeassistant.const import ( CONF_NAME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING) from homeassistant.core import callback diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index 73b6a070419..5f6fd525a11 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -14,11 +14,13 @@ import async_timeout import voluptuous as vol from homeassistant.components.media_player import ( - ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_PLAYER_SCHEMA, MEDIA_TYPE_MUSIC, - PLATFORM_SCHEMA, SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + MediaPlayerDevice, MEDIA_PLAYER_SCHEMA, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, + SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_SHUFFLE_SET, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice) + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET) from homeassistant.const import ( ATTR_COMMAND, CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING) diff --git a/homeassistant/components/media_player/ue_smart_radio.py b/homeassistant/components/media_player/ue_smart_radio.py index 75f6d92a98c..2261aadc2f6 100644 --- a/homeassistant/components/media_player/ue_smart_radio.py +++ b/homeassistant/components/media_player/ue_smart_radio.py @@ -11,10 +11,11 @@ import requests import voluptuous as vol from homeassistant.components.media_player import ( - MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_STOP, SUPPORT_TURN_OFF, - SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - MediaPlayerDevice) + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET) from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING) diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index 18b953a0372..5730a086731 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -10,6 +10,8 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( ATTR_APP_ID, ATTR_APP_NAME, ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE_LIST, ATTR_MEDIA_ALBUM_ARTIST, ATTR_MEDIA_ALBUM_NAME, ATTR_MEDIA_ARTIST, ATTR_MEDIA_CHANNEL, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, @@ -17,11 +19,11 @@ from homeassistant.components.media_player import ( ATTR_MEDIA_POSITION, ATTR_MEDIA_POSITION_UPDATED_AT, ATTR_MEDIA_SEASON, ATTR_MEDIA_SEEK_POSITION, ATTR_MEDIA_SERIES_TITLE, ATTR_MEDIA_SHUFFLE, ATTR_MEDIA_TITLE, ATTR_MEDIA_TRACK, ATTR_MEDIA_VOLUME_LEVEL, - ATTR_MEDIA_VOLUME_MUTED, DOMAIN, PLATFORM_SCHEMA, SERVICE_CLEAR_PLAYLIST, + ATTR_MEDIA_VOLUME_MUTED, DOMAIN, SERVICE_CLEAR_PLAYLIST, SERVICE_PLAY_MEDIA, SERVICE_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST, SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_VOLUME_STEP, MediaPlayerDevice) + SUPPORT_VOLUME_STEP) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, ATTR_SUPPORTED_FEATURES, CONF_NAME, CONF_STATE, CONF_STATE_TEMPLATE, SERVICE_MEDIA_NEXT_TRACK, @@ -333,8 +335,7 @@ class UniversalMediaPlayer(MediaPlayerDevice): if any([cmd in self._cmds for cmd in [SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN]]): flags |= SUPPORT_VOLUME_STEP - flags &= ~SUPPORT_VOLUME_SET - elif SERVICE_VOLUME_SET in self._cmds: + if SERVICE_VOLUME_SET in self._cmds: flags |= SUPPORT_VOLUME_SET if SERVICE_VOLUME_MUTE in self._cmds and \ diff --git a/homeassistant/components/media_player/vizio.py b/homeassistant/components/media_player/vizio.py index 5aae8661bd8..395f5bb369e 100644 --- a/homeassistant/components/media_player/vizio.py +++ b/homeassistant/components/media_player/vizio.py @@ -11,10 +11,11 @@ import voluptuous as vol from homeassistant import util from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, - MediaPlayerDevice) + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP) from homeassistant.const import ( CONF_ACCESS_TOKEN, CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON) from homeassistant.helpers import config_validation as cv diff --git a/homeassistant/components/media_player/vlc.py b/homeassistant/components/media_player/vlc.py index 5cc4196d4e1..592243938d7 100644 --- a/homeassistant/components/media_player/vlc.py +++ b/homeassistant/components/media_player/vlc.py @@ -9,9 +9,10 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA, SUPPORT_PAUSE, SUPPORT_PLAY, - SUPPORT_PLAY_MEDIA, SUPPORT_STOP, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - MediaPlayerDevice) + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_MUSIC, SUPPORT_PAUSE, SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, SUPPORT_STOP, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET) from homeassistant.const import ( CONF_NAME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/media_player/volumio.py b/homeassistant/components/media_player/volumio.py index bd43e6c3710..a72f34fac1d 100644 --- a/homeassistant/components/media_player/volumio.py +++ b/homeassistant/components/media_player/volumio.py @@ -15,11 +15,12 @@ import aiohttp import voluptuous as vol from homeassistant.components.media_player import ( - MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA, SUPPORT_CLEAR_PLAYLIST, + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_MUSIC, SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_SELECT_SOURCE, SUPPORT_STOP, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, - MediaPlayerDevice) + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP) from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PORT, STATE_IDLE, STATE_PAUSED, STATE_PLAYING) from homeassistant.helpers.aiohttp_client import async_get_clientsession diff --git a/homeassistant/components/media_player/xiaomi_tv.py b/homeassistant/components/media_player/xiaomi_tv.py index 09d36f82db0..e3b25c3c31f 100644 --- a/homeassistant/components/media_player/xiaomi_tv.py +++ b/homeassistant/components/media_player/xiaomi_tv.py @@ -9,8 +9,9 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_STEP, - MediaPlayerDevice) + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_STEP) from homeassistant.const import CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/media_player/yamaha.py b/homeassistant/components/media_player/yamaha.py index 0bb34aee7e1..f652d95e713 100644 --- a/homeassistant/components/media_player/yamaha.py +++ b/homeassistant/components/media_player/yamaha.py @@ -10,18 +10,20 @@ import requests import voluptuous as vol from homeassistant.components.media_player import ( - DOMAIN, MEDIA_PLAYER_SCHEMA, MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA, + MediaPlayerDevice, MEDIA_PLAYER_SCHEMA, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, 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) + SUPPORT_SELECT_SOUND_MODE) from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, STATE_IDLE, STATE_OFF, STATE_ON, STATE_PLAYING) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['rxv==0.5.1'] +REQUIREMENTS = ['rxv==0.6.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_player/yamaha_musiccast.py b/homeassistant/components/media_player/yamaha_musiccast.py index 535a2ad01ca..6aa06b604c5 100644 --- a/homeassistant/components/media_player/yamaha_musiccast.py +++ b/homeassistant/components/media_player/yamaha_musiccast.py @@ -9,10 +9,11 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_STOP, - SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - MediaPlayerDevice) + SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET) from homeassistant.const import ( CONF_HOST, CONF_PORT, STATE_IDLE, STATE_ON, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN) diff --git a/homeassistant/components/media_player/ziggo_mediabox_xl.py b/homeassistant/components/media_player/ziggo_mediabox_xl.py index 57ef69c923e..abad22d89eb 100644 --- a/homeassistant/components/media_player/ziggo_mediabox_xl.py +++ b/homeassistant/components/media_player/ziggo_mediabox_xl.py @@ -10,9 +10,11 @@ import socket import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, - SUPPORT_TURN_ON, MediaPlayerDevice) + SUPPORT_TURN_ON) from homeassistant.const import ( CONF_HOST, CONF_NAME, STATE_OFF, STATE_PAUSED, STATE_PLAYING) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/melissa.py b/homeassistant/components/melissa/__init__.py similarity index 84% rename from homeassistant/components/melissa.py rename to homeassistant/components/melissa/__init__.py index 638d8c55bd5..2037caa11c3 100644 --- a/homeassistant/components/melissa.py +++ b/homeassistant/components/melissa/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Melissa climate. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/melissa/ -""" +"""Support for Melissa climate.""" import logging import voluptuous as vol @@ -16,7 +11,7 @@ REQUIREMENTS = ["py-melissa-climate==2.0.0"] _LOGGER = logging.getLogger(__name__) -DOMAIN = "melissa" +DOMAIN = 'melissa' DATA_MELISSA = 'MELISSA' diff --git a/homeassistant/components/microsoft_face.py b/homeassistant/components/microsoft_face/__init__.py similarity index 97% rename from homeassistant/components/microsoft_face.py rename to homeassistant/components/microsoft_face/__init__.py index 9be2f8eadf5..9b3ee960fb2 100644 --- a/homeassistant/components/microsoft_face.py +++ b/homeassistant/components/microsoft_face/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Microsoft face recognition. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/microsoft_face/ -""" +"""Support for Microsoft face recognition.""" import asyncio import json import logging @@ -45,7 +40,7 @@ SERVICE_TRAIN_GROUP = 'train_group' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_API_KEY): cv.string, - vol.Optional(CONF_AZURE_REGION, default="westus"): cv.string, + vol.Optional(CONF_AZURE_REGION, default='westus'): cv.string, vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, }), }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/microsoft_face/services.yaml b/homeassistant/components/microsoft_face/services.yaml new file mode 100644 index 00000000000..386f7083f92 --- /dev/null +++ b/homeassistant/components/microsoft_face/services.yaml @@ -0,0 +1,28 @@ +create_group: + description: Create a new person group. + fields: + name: {description: Name of the group., example: family} +create_person: + description: Create a new person in the group. + fields: + group: {description: Name of the group, example: family} + name: {description: Name of the person, example: Hans} +delete_group: + description: Delete a new person group. + fields: + name: {description: Name of the group., example: family} +delete_person: + description: Delete a person in the group. + fields: + group: {description: Name of the group., example: family} + name: {description: Name of the person., example: Hans} +face_person: + description: Add a new picture to a person. + fields: + camera_entity: {description: Camera to take a picture., example: camera.door} + group: {description: Name of the group., example: family} + person: {description: Name of the person., example: Hans} +train_group: + description: Train a person group. + fields: + group: {description: Name of the group, example: family} diff --git a/homeassistant/components/mochad.py b/homeassistant/components/mochad/__init__.py similarity index 91% rename from homeassistant/components/mochad.py rename to homeassistant/components/mochad/__init__.py index 7e6738b95f8..e10adf693fe 100644 --- a/homeassistant/components/mochad.py +++ b/homeassistant/components/mochad/__init__.py @@ -1,9 +1,4 @@ -""" -Support for CM15A/CM19A X10 Controller using mochad daemon. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/mochad/ -""" +"""Support for CM15A/CM19A X10 Controller using mochad daemon.""" import logging import threading diff --git a/homeassistant/components/light/mochad.py b/homeassistant/components/mochad/light.py similarity index 91% rename from homeassistant/components/light/mochad.py rename to homeassistant/components/mochad/light.py index 2e68c369ba6..d2e1a567d27 100644 --- a/homeassistant/components/light/mochad.py +++ b/homeassistant/components/mochad/light.py @@ -1,10 +1,4 @@ -""" -Contains functionality to use a X10 dimmer over Mochad. - -For more details about this platform, please refer to the documentation at -https://home.assistant.io/components/light.mochad/ -""" - +"""Support for X10 dimmer over Mochad.""" import logging import voluptuous as vol @@ -16,11 +10,11 @@ from homeassistant.const import ( CONF_NAME, CONF_PLATFORM, CONF_DEVICES, CONF_ADDRESS) from homeassistant.helpers import config_validation as cv -DEPENDENCIES = ['mochad'] _LOGGER = logging.getLogger(__name__) -CONF_BRIGHTNESS_LEVELS = 'brightness_levels' +DEPENDENCIES = ['mochad'] +CONF_BRIGHTNESS_LEVELS = 'brightness_levels' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_PLATFORM): mochad.DOMAIN, @@ -51,11 +45,11 @@ class MochadLight(Light): self._controller = ctrl self._address = dev[CONF_ADDRESS] - self._name = dev.get(CONF_NAME, - 'x10_light_dev_{}'.format(self._address)) + self._name = dev.get( + CONF_NAME, 'x10_light_dev_{}'.format(self._address)) self._comm_type = dev.get(mochad.CONF_COMM_TYPE, 'pl') - self.light = device.Device(ctrl, self._address, - comm_type=self._comm_type) + self.light = device.Device( + ctrl, self._address, comm_type=self._comm_type) self._brightness = 0 self._state = self._get_device_status() self._brightness_levels = dev.get(CONF_BRIGHTNESS_LEVELS) - 1 diff --git a/homeassistant/components/switch/mochad.py b/homeassistant/components/mochad/switch.py similarity index 91% rename from homeassistant/components/switch/mochad.py rename to homeassistant/components/mochad/switch.py index b703d91be34..03fd2db07bf 100644 --- a/homeassistant/components/switch/mochad.py +++ b/homeassistant/components/mochad/switch.py @@ -1,10 +1,4 @@ -""" -Contains functionality to use a X10 switch over Mochad. - -For more details about this platform, please refer to the documentation at -https://home.assistant.io/components/switch.mochad -""" - +"""Support for X10 switch over Mochad.""" import logging import voluptuous as vol @@ -15,9 +9,10 @@ from homeassistant.const import (CONF_NAME, CONF_DEVICES, CONF_PLATFORM, CONF_ADDRESS) from homeassistant.helpers import config_validation as cv -DEPENDENCIES = ['mochad'] _LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['mochad'] + PLATFORM_SCHEMA = vol.Schema({ vol.Required(CONF_PLATFORM): mochad.DOMAIN, @@ -48,8 +43,8 @@ class MochadSwitch(SwitchDevice): self._address = dev[CONF_ADDRESS] self._name = dev.get(CONF_NAME, 'x10_switch_dev_%s' % self._address) self._comm_type = dev.get(mochad.CONF_COMM_TYPE, 'pl') - self.switch = device.Device(ctrl, self._address, - comm_type=self._comm_type) + self.switch = device.Device( + ctrl, self._address, comm_type=self._comm_type) # Init with false to avoid locking HA for long on CM19A (goes from rf # to pl via TM751, but not other way around) if self._comm_type == 'pl': diff --git a/homeassistant/components/modbus.py b/homeassistant/components/modbus/__init__.py similarity index 66% rename from homeassistant/components/modbus.py rename to homeassistant/components/modbus/__init__.py index 40ede019c10..f42423bf9a8 100644 --- a/homeassistant/components/modbus.py +++ b/homeassistant/components/modbus/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Modbus. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/modbus/ -""" +"""Support for Modbus.""" import logging import threading @@ -12,19 +7,27 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - CONF_HOST, CONF_METHOD, CONF_PORT, CONF_TYPE, CONF_TIMEOUT, ATTR_STATE) + CONF_HOST, CONF_METHOD, CONF_NAME, CONF_PORT, CONF_TYPE, CONF_TIMEOUT, + ATTR_STATE) DOMAIN = 'modbus' REQUIREMENTS = ['pymodbus==1.5.2'] +CONF_HUB = 'hub' # Type of network CONF_BAUDRATE = 'baudrate' CONF_BYTESIZE = 'bytesize' CONF_STOPBITS = 'stopbits' CONF_PARITY = 'parity' -SERIAL_SCHEMA = { +DEFAULT_HUB = 'default' + +BASE_SCHEMA = vol.Schema({ + vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string +}) + +SERIAL_SCHEMA = BASE_SCHEMA.extend({ vol.Required(CONF_BAUDRATE): cv.positive_int, vol.Required(CONF_BYTESIZE): vol.Any(5, 6, 7, 8), vol.Required(CONF_METHOD): vol.Any('rtu', 'ascii'), @@ -33,20 +36,18 @@ SERIAL_SCHEMA = { vol.Required(CONF_STOPBITS): vol.Any(1, 2), vol.Required(CONF_TYPE): 'serial', vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout, -} +}) -ETHERNET_SCHEMA = { +ETHERNET_SCHEMA = BASE_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORT): cv.port, vol.Required(CONF_TYPE): vol.Any('tcp', 'udp', 'rtuovertcp'), vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout, -} - +}) CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Any(SERIAL_SCHEMA, ETHERNET_SCHEMA) -}, extra=vol.ALLOW_EXTRA) - + DOMAIN: vol.All(cv.ensure_list, [vol.Any(SERIAL_SCHEMA, ETHERNET_SCHEMA)]) +}, extra=vol.ALLOW_EXTRA,) _LOGGER = logging.getLogger(__name__) @@ -54,71 +55,79 @@ SERVICE_WRITE_REGISTER = 'write_register' SERVICE_WRITE_COIL = 'write_coil' ATTR_ADDRESS = 'address' +ATTR_HUB = 'hub' ATTR_UNIT = 'unit' ATTR_VALUE = 'value' SERVICE_WRITE_REGISTER_SCHEMA = vol.Schema({ + vol.Optional(ATTR_HUB, default=DEFAULT_HUB): cv.string, vol.Required(ATTR_UNIT): cv.positive_int, vol.Required(ATTR_ADDRESS): cv.positive_int, vol.Required(ATTR_VALUE): vol.All(cv.ensure_list, [cv.positive_int]) }) SERVICE_WRITE_COIL_SCHEMA = vol.Schema({ + vol.Optional(ATTR_HUB, default=DEFAULT_HUB): cv.string, vol.Required(ATTR_UNIT): cv.positive_int, vol.Required(ATTR_ADDRESS): cv.positive_int, vol.Required(ATTR_STATE): cv.boolean }) -HUB = None + +def setup_client(client_config): + """Set up pymodbus client.""" + client_type = client_config[CONF_TYPE] + + if client_type == 'serial': + from pymodbus.client.sync import ModbusSerialClient as ModbusClient + return ModbusClient(method=client_config[CONF_METHOD], + port=client_config[CONF_PORT], + baudrate=client_config[CONF_BAUDRATE], + stopbits=client_config[CONF_STOPBITS], + bytesize=client_config[CONF_BYTESIZE], + parity=client_config[CONF_PARITY], + timeout=client_config[CONF_TIMEOUT]) + if client_type == 'rtuovertcp': + from pymodbus.client.sync import ModbusTcpClient as ModbusClient + from pymodbus.transaction import ModbusRtuFramer + return ModbusClient(host=client_config[CONF_HOST], + port=client_config[CONF_PORT], + framer=ModbusRtuFramer, + timeout=client_config[CONF_TIMEOUT]) + if client_type == 'tcp': + from pymodbus.client.sync import ModbusTcpClient as ModbusClient + return ModbusClient(host=client_config[CONF_HOST], + port=client_config[CONF_PORT], + timeout=client_config[CONF_TIMEOUT]) + if client_type == 'udp': + from pymodbus.client.sync import ModbusUdpClient as ModbusClient + return ModbusClient(host=client_config[CONF_HOST], + port=client_config[CONF_PORT], + timeout=client_config[CONF_TIMEOUT]) + assert False def setup(hass, config): """Set up Modbus component.""" # Modbus connection type - client_type = config[DOMAIN][CONF_TYPE] + hass.data[DOMAIN] = hub_collect = {} - # Connect to Modbus network - # pylint: disable=import-error - - if client_type == 'serial': - from pymodbus.client.sync import ModbusSerialClient as ModbusClient - client = ModbusClient(method=config[DOMAIN][CONF_METHOD], - port=config[DOMAIN][CONF_PORT], - baudrate=config[DOMAIN][CONF_BAUDRATE], - stopbits=config[DOMAIN][CONF_STOPBITS], - bytesize=config[DOMAIN][CONF_BYTESIZE], - parity=config[DOMAIN][CONF_PARITY], - timeout=config[DOMAIN][CONF_TIMEOUT]) - elif client_type == 'rtuovertcp': - from pymodbus.client.sync import ModbusTcpClient as ModbusClient - from pymodbus.transaction import ModbusRtuFramer as ModbusFramer - client = ModbusClient(host=config[DOMAIN][CONF_HOST], - port=config[DOMAIN][CONF_PORT], - framer=ModbusFramer, - timeout=config[DOMAIN][CONF_TIMEOUT]) - elif client_type == 'tcp': - from pymodbus.client.sync import ModbusTcpClient as ModbusClient - client = ModbusClient(host=config[DOMAIN][CONF_HOST], - port=config[DOMAIN][CONF_PORT], - timeout=config[DOMAIN][CONF_TIMEOUT]) - elif client_type == 'udp': - from pymodbus.client.sync import ModbusUdpClient as ModbusClient - client = ModbusClient(host=config[DOMAIN][CONF_HOST], - port=config[DOMAIN][CONF_PORT], - timeout=config[DOMAIN][CONF_TIMEOUT]) - else: - return False - - global HUB - HUB = ModbusHub(client) + for client_config in config[DOMAIN]: + client = setup_client(client_config) + name = client_config[CONF_NAME] + hub_collect[name] = ModbusHub(client, name) + _LOGGER.debug('Setting up hub: %s', client_config) def stop_modbus(event): """Stop Modbus service.""" - HUB.close() + for client in hub_collect.values(): + client.close() def start_modbus(event): """Start Modbus service.""" - HUB.connect() + for client in hub_collect.values(): + client.connect() + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus) # Register services for modbus @@ -134,13 +143,14 @@ def setup(hass, config): unit = int(float(service.data.get(ATTR_UNIT))) address = int(float(service.data.get(ATTR_ADDRESS))) value = service.data.get(ATTR_VALUE) + client_name = service.data.get(ATTR_HUB) if isinstance(value, list): - HUB.write_registers( + hub_collect[client_name].write_registers( unit, address, [int(float(i)) for i in value]) else: - HUB.write_register( + hub_collect[client_name].write_register( unit, address, int(float(value))) @@ -150,7 +160,8 @@ def setup(hass, config): unit = service.data.get(ATTR_UNIT) address = service.data.get(ATTR_ADDRESS) state = service.data.get(ATTR_STATE) - HUB.write_coil(unit, address, state) + client_name = service.data.get(ATTR_HUB) + hub_collect[client_name].write_coil(unit, address, state) hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_modbus) @@ -160,10 +171,16 @@ def setup(hass, config): class ModbusHub: """Thread safe wrapper class for pymodbus.""" - def __init__(self, modbus_client): + def __init__(self, modbus_client, name): """Initialize the modbus hub.""" self._client = modbus_client self._lock = threading.Lock() + self._name = name + + @property + def name(self): + """Return the name of this hub.""" + return self._name def close(self): """Disconnect client.""" diff --git a/homeassistant/components/binary_sensor/modbus.py b/homeassistant/components/modbus/binary_sensor.py similarity index 69% rename from homeassistant/components/binary_sensor/modbus.py rename to homeassistant/components/modbus/binary_sensor.py index f9f2597593e..38511ffed7e 100644 --- a/homeassistant/components/binary_sensor/modbus.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -1,13 +1,9 @@ -""" -Support for Modbus Coil sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.modbus/ -""" +"""Support for Modbus Coil sensors.""" import logging import voluptuous as vol -from homeassistant.components import modbus +from homeassistant.components.modbus import ( + CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN) from homeassistant.const import CONF_NAME, CONF_SLAVE from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.helpers import config_validation as cv @@ -21,9 +17,10 @@ CONF_COILS = 'coils' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_COILS): [{ + vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, vol.Required(CONF_COIL): cv.positive_int, vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_SLAVE): cv.positive_int + vol.Optional(CONF_SLAVE): cv.positive_int, }] }) @@ -32,9 +29,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Modbus binary sensors.""" sensors = [] for coil in config.get(CONF_COILS): + hub = hass.data[MODBUS_DOMAIN][coil.get(CONF_HUB)] sensors.append(ModbusCoilSensor( - coil.get(CONF_NAME), - coil.get(CONF_SLAVE), + hub, coil.get(CONF_NAME), coil.get(CONF_SLAVE), coil.get(CONF_COIL))) add_entities(sensors) @@ -42,8 +39,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class ModbusCoilSensor(BinarySensorDevice): """Modbus coil sensor.""" - def __init__(self, name, slave, coil): + def __init__(self, hub, name, slave, coil): """Initialize the modbus coil sensor.""" + self._hub = hub self._name = name self._slave = int(slave) if slave else None self._coil = int(coil) @@ -61,11 +59,9 @@ class ModbusCoilSensor(BinarySensorDevice): def update(self): """Update the state of the sensor.""" - result = modbus.HUB.read_coils(self._slave, self._coil, 1) + result = self._hub.read_coils(self._slave, self._coil, 1) try: self._value = result.bits[0] except AttributeError: - _LOGGER.error( - 'No response from modbus slave %s coil %s', - self._slave, - self._coil) + _LOGGER.error("No response from hub %s, slave %s, coil %s", + self._hub.name, self._slave, self._coil) diff --git a/homeassistant/components/climate/modbus.py b/homeassistant/components/modbus/climate.py similarity index 83% rename from homeassistant/components/climate/modbus.py rename to homeassistant/components/modbus/climate.py index 1c5c03e4502..ed8cbda863f 100644 --- a/homeassistant/components/climate/modbus.py +++ b/homeassistant/components/modbus/climate.py @@ -1,13 +1,4 @@ -""" -Platform for a Generic Modbus Thermostat. - -This uses a setpoint and process -value within the controller, so both the current temperature register and the -target temperature register need to be configured. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/climate.modbus/ -""" +"""Support for Generic Modbus Thermostats.""" import logging import struct @@ -17,10 +8,12 @@ from homeassistant.const import ( CONF_NAME, CONF_SLAVE, ATTR_TEMPERATURE) from homeassistant.components.climate import ( ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE) - -from homeassistant.components import modbus +from homeassistant.components.modbus import ( + CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN) import homeassistant.helpers.config_validation as cv +_LOGGER = logging.getLogger(__name__) + DEPENDENCIES = ['modbus'] # Parameters not defined by homeassistant.const @@ -35,6 +28,7 @@ DATA_TYPE_UINT = 'uint' DATA_TYPE_FLOAT = 'float' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, vol.Required(CONF_NAME): cv.string, vol.Required(CONF_SLAVE): cv.positive_int, vol.Required(CONF_TARGET_TEMP): cv.positive_int, @@ -45,8 +39,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_PRECISION, default=1): cv.positive_int }) -_LOGGER = logging.getLogger(__name__) - SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE @@ -59,18 +51,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None): data_type = config.get(CONF_DATA_TYPE) count = config.get(CONF_COUNT) precision = config.get(CONF_PRECISION) + hub_name = config.get(CONF_HUB) + hub = hass.data[MODBUS_DOMAIN][hub_name] - add_entities([ModbusThermostat(name, modbus_slave, - target_temp_register, current_temp_register, - data_type, count, precision)], True) + add_entities([ModbusThermostat( + hub, name, modbus_slave, target_temp_register, current_temp_register, + data_type, count, precision)], True) class ModbusThermostat(ClimateDevice): """Representation of a Modbus Thermostat.""" - def __init__(self, name, modbus_slave, target_temp_register, + def __init__(self, hub, name, modbus_slave, target_temp_register, current_temp_register, data_type, count, precision): """Initialize the unit.""" + self._hub = hub self._name = name self._slave = modbus_slave self._target_temperature_register = target_temp_register @@ -133,8 +128,8 @@ class ModbusThermostat(ClimateDevice): def read_register(self, register): """Read holding register using the modbus hub slave.""" try: - result = modbus.HUB.read_holding_registers(self._slave, register, - self._count) + result = self._hub.read_holding_registers(self._slave, register, + self._count) except AttributeError as ex: _LOGGER.error(ex) byte_string = b''.join( @@ -145,4 +140,4 @@ class ModbusThermostat(ClimateDevice): def write_register(self, register, value): """Write register using the modbus hub slave.""" - modbus.HUB.write_registers(self._slave, register, [value, 0]) + self._hub.write_registers(self._slave, register, [value, 0]) diff --git a/homeassistant/components/sensor/modbus.py b/homeassistant/components/modbus/sensor.py similarity index 80% rename from homeassistant/components/sensor/modbus.py rename to homeassistant/components/modbus/sensor.py index 833cb0c5a62..6ba8d92d155 100644 --- a/homeassistant/components/sensor/modbus.py +++ b/homeassistant/components/modbus/sensor.py @@ -1,19 +1,15 @@ -""" -Support for Modbus Register sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.modbus/ -""" +"""Support for Modbus Register sensors.""" import logging import struct import voluptuous as vol -from homeassistant.components import modbus +from homeassistant.components.modbus import ( + CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN) from homeassistant.const import ( CONF_NAME, CONF_OFFSET, CONF_UNIT_OF_MEASUREMENT, CONF_SLAVE, CONF_STRUCTURE) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers import config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -40,6 +36,7 @@ DATA_TYPE_CUSTOM = 'custom' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_REGISTERS): [{ + vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, vol.Required(CONF_NAME): cv.string, vol.Required(CONF_REGISTER): cv.positive_int, vol.Optional(CONF_REGISTER_TYPE, default=REGISTER_TYPE_HOLDING): @@ -54,7 +51,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.In([DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT, DATA_TYPE_CUSTOM]), vol.Optional(CONF_STRUCTURE): cv.string, - vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, }] }) @@ -70,11 +67,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): structure = '>i' if register.get(CONF_DATA_TYPE) != DATA_TYPE_CUSTOM: try: - structure = '>{}'.format(data_types[ - register.get(CONF_DATA_TYPE)][register.get(CONF_COUNT)]) + structure = '>{}'.format(data_types[register.get( + CONF_DATA_TYPE)][register.get(CONF_COUNT)]) except KeyError: _LOGGER.error("Unable to detect data type for %s sensor, " - "try a custom type.", register.get(CONF_NAME)) + "try a custom type", register.get(CONF_NAME)) continue else: structure = register.get(CONF_STRUCTURE) @@ -93,7 +90,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): "(%d words)", size, register.get(CONF_COUNT)) continue + hub_name = register.get(CONF_HUB) + hub = hass.data[MODBUS_DOMAIN][hub_name] sensors.append(ModbusRegisterSensor( + hub, register.get(CONF_NAME), register.get(CONF_SLAVE), register.get(CONF_REGISTER), @@ -111,13 +111,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors) -class ModbusRegisterSensor(Entity): +class ModbusRegisterSensor(RestoreEntity): """Modbus register sensor.""" - def __init__(self, name, slave, register, register_type, + def __init__(self, hub, name, slave, register, register_type, unit_of_measurement, count, reverse_order, scale, offset, structure, precision): """Initialize the modbus register sensor.""" + self._hub = hub self._name = name self._slave = int(slave) if slave else None self._register = int(register) @@ -131,6 +132,13 @@ class ModbusRegisterSensor(Entity): self._structure = structure self._value = None + async def async_added_to_hass(self): + """Handle entity which will be added.""" + state = await self.async_get_last_state() + if not state: + return + self._value = state.state + @property def state(self): """Return the state of the sensor.""" @@ -149,12 +157,12 @@ class ModbusRegisterSensor(Entity): def update(self): """Update the state of the sensor.""" if self._register_type == REGISTER_TYPE_INPUT: - result = modbus.HUB.read_input_registers( + result = self._hub.read_input_registers( self._slave, self._register, self._count) else: - result = modbus.HUB.read_holding_registers( + result = self._hub.read_holding_registers( self._slave, self._register, self._count) @@ -165,8 +173,8 @@ class ModbusRegisterSensor(Entity): if self._reverse_order: registers.reverse() except AttributeError: - _LOGGER.error("No response from modbus slave %s, register %s", - self._slave, self._register) + _LOGGER.error("No response from hub %s, slave %s, register %s", + self._hub.name, self._slave, self._register) return byte_string = b''.join( [x.to_bytes(2, byteorder='big') for x in registers] diff --git a/homeassistant/components/modbus/services.yaml b/homeassistant/components/modbus/services.yaml new file mode 100644 index 00000000000..0fd9e5a49e7 --- /dev/null +++ b/homeassistant/components/modbus/services.yaml @@ -0,0 +1,12 @@ +write_coil: + description: Write to a modbus coil. + fields: + address: {description: Address of the register to read., example: 0} + state: {description: State to write., example: false} + unit: {description: Address of the modbus unit., example: 21} +write_register: + description: Write to a modbus holding register. + fields: + address: {description: Address of the holding register to write to., example: 0} + unit: {description: Address of the modbus unit., example: 21} + value: {description: Value to write., example: 0} diff --git a/homeassistant/components/switch/modbus.py b/homeassistant/components/modbus/switch.py similarity index 69% rename from homeassistant/components/switch/modbus.py rename to homeassistant/components/modbus/switch.py index a8c8358f0cf..47ad8e98958 100644 --- a/homeassistant/components/switch/modbus.py +++ b/homeassistant/components/modbus/switch.py @@ -1,20 +1,18 @@ -""" -Support for Modbus switches. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.modbus/ -""" +"""Support for Modbus switches.""" import logging import voluptuous as vol -from homeassistant.components import modbus +from homeassistant.components.modbus import ( + CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN) from homeassistant.const import ( - CONF_NAME, CONF_SLAVE, CONF_COMMAND_ON, CONF_COMMAND_OFF) + CONF_NAME, CONF_SLAVE, CONF_COMMAND_ON, CONF_COMMAND_OFF, STATE_ON) from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers import config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA _LOGGER = logging.getLogger(__name__) + DEPENDENCIES = ['modbus'] CONF_COIL = "coil" @@ -31,6 +29,7 @@ REGISTER_TYPE_HOLDING = 'holding' REGISTER_TYPE_INPUT = 'input' REGISTERS_SCHEMA = vol.Schema({ + vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, vol.Required(CONF_NAME): cv.string, vol.Optional(CONF_SLAVE): cv.positive_int, vol.Required(CONF_REGISTER): cv.positive_int, @@ -46,6 +45,7 @@ REGISTERS_SCHEMA = vol.Schema({ }) COILS_SCHEMA = vol.Schema({ + vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, vol.Required(CONF_COIL): cv.positive_int, vol.Required(CONF_NAME): cv.string, vol.Required(CONF_SLAVE): cv.positive_int, @@ -55,7 +55,7 @@ PLATFORM_SCHEMA = vol.All( cv.has_at_least_one_key(CONF_COILS, CONF_REGISTERS), PLATFORM_SCHEMA.extend({ vol.Optional(CONF_COILS): [COILS_SCHEMA], - vol.Optional(CONF_REGISTERS): [REGISTERS_SCHEMA] + vol.Optional(CONF_REGISTERS): [REGISTERS_SCHEMA], })) @@ -64,13 +64,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): switches = [] if CONF_COILS in config: for coil in config.get(CONF_COILS): + hub_name = coil.get(CONF_HUB) + hub = hass.data[MODBUS_DOMAIN][hub_name] switches.append(ModbusCoilSwitch( - coil.get(CONF_NAME), - coil.get(CONF_SLAVE), + hub, coil.get(CONF_NAME), coil.get(CONF_SLAVE), coil.get(CONF_COIL))) if CONF_REGISTERS in config: for register in config.get(CONF_REGISTERS): + hub_name = register.get(CONF_HUB) + hub = hass.data[MODBUS_DOMAIN][hub_name] + switches.append(ModbusRegisterSwitch( + hub, register.get(CONF_NAME), register.get(CONF_SLAVE), register.get(CONF_REGISTER), @@ -81,19 +86,28 @@ def setup_platform(hass, config, add_entities, discovery_info=None): register.get(CONF_REGISTER_TYPE), register.get(CONF_STATE_ON), register.get(CONF_STATE_OFF))) + add_entities(switches) -class ModbusCoilSwitch(ToggleEntity): +class ModbusCoilSwitch(ToggleEntity, RestoreEntity): """Representation of a Modbus coil switch.""" - def __init__(self, name, slave, coil): + def __init__(self, hub, name, slave, coil): """Initialize the coil switch.""" + self._hub = hub self._name = name self._slave = int(slave) if slave else None self._coil = int(coil) self._is_on = None + async def async_added_to_hass(self): + """Handle entity which will be added.""" + state = await self.async_get_last_state() + if not state: + return + self._is_on = state.state == STATE_ON + @property def is_on(self): """Return true if switch is on.""" @@ -106,32 +120,32 @@ class ModbusCoilSwitch(ToggleEntity): def turn_on(self, **kwargs): """Set switch on.""" - modbus.HUB.write_coil(self._slave, self._coil, True) + self._hub.write_coil(self._slave, self._coil, True) def turn_off(self, **kwargs): """Set switch off.""" - modbus.HUB.write_coil(self._slave, self._coil, False) + self._hub.write_coil(self._slave, self._coil, False) def update(self): """Update the state of the switch.""" - result = modbus.HUB.read_coils(self._slave, self._coil, 1) + result = self._hub.read_coils(self._slave, self._coil, 1) try: self._is_on = bool(result.bits[0]) except AttributeError: _LOGGER.error( - 'No response from modbus slave %s coil %s', - self._slave, - self._coil) + 'No response from hub %s, slave %s, coil %s', + self._hub.name, self._slave, self._coil) class ModbusRegisterSwitch(ModbusCoilSwitch): """Representation of a Modbus register switch.""" # pylint: disable=super-init-not-called - def __init__(self, name, slave, register, command_on, + def __init__(self, hub, name, slave, register, command_on, command_off, verify_state, verify_register, register_type, state_on, state_off): """Initialize the register switch.""" + self._hub = hub self._name = name self._slave = slave self._register = register @@ -156,19 +170,14 @@ class ModbusRegisterSwitch(ModbusCoilSwitch): def turn_on(self, **kwargs): """Set switch on.""" - modbus.HUB.write_register( - self._slave, - self._register, - self._command_on) + self._hub.write_register(self._slave, self._register, self._command_on) if not self._verify_state: self._is_on = True def turn_off(self, **kwargs): """Set switch off.""" - modbus.HUB.write_register( - self._slave, - self._register, - self._command_off) + self._hub.write_register( + self._slave, self._register, self._command_off) if not self._verify_state: self._is_on = False @@ -179,23 +188,18 @@ class ModbusRegisterSwitch(ModbusCoilSwitch): value = 0 if self._register_type == REGISTER_TYPE_INPUT: - result = modbus.HUB.read_input_registers( - self._slave, - self._register, - 1) + result = self._hub.read_input_registers( + self._slave, self._register, 1) else: - result = modbus.HUB.read_holding_registers( - self._slave, - self._register, - 1) + result = self._hub.read_holding_registers( + self._slave, self._register, 1) try: value = int(result.registers[0]) except AttributeError: _LOGGER.error( - 'No response from modbus slave %s register %s', - self._slave, - self._verify_register) + "No response from hub %s, slave %s, register %s", + self._hub.name, self._slave, self._verify_register) if value == self._state_on: self._is_on = True @@ -203,8 +207,6 @@ class ModbusRegisterSwitch(ModbusCoilSwitch): self._is_on = False else: _LOGGER.error( - 'Unexpected response from modbus slave %s ' - 'register %s, got 0x%2x', - self._slave, - self._verify_register, - value) + "Unexpected response from hub %s, slave %s " + "register %s, got 0x%2x", + self._hub.name, self._slave, self._verify_register, value) diff --git a/homeassistant/components/mqtt/.translations/da.json b/homeassistant/components/mqtt/.translations/da.json new file mode 100644 index 00000000000..ebe5696f514 --- /dev/null +++ b/homeassistant/components/mqtt/.translations/da.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning af MQTT" + }, + "error": { + "cannot_connect": "Kunne ikke oprette forbindelse til broker" + }, + "step": { + "broker": { + "data": { + "broker": "Broker", + "discovery": "Aktiv\u00e9r opdagelse", + "password": "Adgangskode", + "port": "Port", + "username": "Brugernavn" + }, + "description": "Indtast venligst forbindelsesindstillinger for din MQTT broker.", + "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "Aktiv\u00e9r opdagelse" + }, + "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til MQTT brokeren, der leveres af hass.io add-on {addon}?", + "title": "MQTT Broker via Hass.io add-on" + } + }, + "title": "MQTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/ru.json b/homeassistant/components/mqtt/.translations/ru.json index 9757716b1bf..663d79f3c14 100644 --- a/homeassistant/components/mqtt/.translations/ru.json +++ b/homeassistant/components/mqtt/.translations/ru.json @@ -22,7 +22,7 @@ "data": { "discovery": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0430\u0432\u0442\u043e\u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432" }, - "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 Home Assistant \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT \u0447\u0435\u0440\u0435\u0437 \u0430\u0434\u0434\u043e\u043d Hass.io {addon}?", + "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 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT \u0447\u0435\u0440\u0435\u0437 \u0430\u0434\u0434\u043e\u043d Hass.io {addon}?", "title": "\u0411\u0440\u043e\u043a\u0435\u0440 MQTT \u0447\u0435\u0440\u0435\u0437 \u0430\u0434\u0434\u043e\u043d Hass.io" } }, diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index ed2a3cd6c52..e430b1fbc9f 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -234,7 +234,7 @@ MQTT_JSON_ATTRS_SCHEMA = vol.Schema({ vol.Optional(CONF_JSON_ATTRS_TOPIC): valid_subscribe_topic, }) -MQTT_BASE_PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA_2.extend(SCHEMA_BASE) +MQTT_BASE_PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(SCHEMA_BASE) # Sensor type platforms subscribe to MQTT events MQTT_RO_PLATFORM_SCHEMA = MQTT_BASE_PLATFORM_SCHEMA.extend({ diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index b3e4d452b5c..8e1b62414b7 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -19,8 +19,8 @@ from homeassistant.components.mqtt.discovery import ( MQTT_DISCOVERY_NEW, clear_discovery_hash) from homeassistant.const import ( CONF_CODE, CONF_DEVICE, CONF_NAME, STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, - STATE_ALARM_TRIGGERED) + STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, + STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -31,7 +31,9 @@ _LOGGER = logging.getLogger(__name__) CONF_PAYLOAD_DISARM = 'payload_disarm' CONF_PAYLOAD_ARM_HOME = 'payload_arm_home' CONF_PAYLOAD_ARM_AWAY = 'payload_arm_away' +CONF_PAYLOAD_ARM_NIGHT = 'payload_arm_night' +DEFAULT_ARM_NIGHT = 'ARM_NIGHT' DEFAULT_ARM_AWAY = 'ARM_AWAY' DEFAULT_ARM_HOME = 'ARM_HOME' DEFAULT_DISARM = 'DISARM' @@ -44,6 +46,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ vol.Required(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_CODE): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PAYLOAD_ARM_NIGHT, default=DEFAULT_ARM_NIGHT): cv.string, vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string, vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string, vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string, @@ -124,7 +127,9 @@ class MqttAlarm(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, def message_received(topic, payload, qos): """Run when new MQTT message has been received.""" if payload not in (STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_AWAY, STATE_ALARM_PENDING, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED): _LOGGER.warning("Received unexpected payload: %s", payload) return @@ -213,6 +218,19 @@ class MqttAlarm(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, self._config.get(CONF_QOS), self._config.get(CONF_RETAIN)) + async def async_alarm_arm_night(self, code=None): + """Send arm night command. + + This method is a coroutine. + """ + if not self._validate_code(code, 'arming night'): + return + mqtt.async_publish( + self.hass, self._config.get(CONF_COMMAND_TOPIC), + self._config.get(CONF_PAYLOAD_ARM_NIGHT), + self._config.get(CONF_QOS), + self._config.get(CONF_RETAIN)) + def _validate_code(self, code, state): """Validate given code.""" conf_code = self._config.get(CONF_CODE) diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index be176a39a25..569d69a9ad8 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -13,7 +13,8 @@ import voluptuous as vol from homeassistant.components import camera, mqtt from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.components.mqtt import ( - ATTR_DISCOVERY_HASH, CONF_UNIQUE_ID, MqttDiscoveryUpdate, subscription) + ATTR_DISCOVERY_HASH, CONF_STATE_TOPIC, CONF_UNIQUE_ID, MqttDiscoveryUpdate, + subscription) from homeassistant.components.mqtt.discovery import ( MQTT_DISCOVERY_NEW, clear_discovery_hash) from homeassistant.const import CONF_NAME @@ -47,7 +48,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def async_discover(discovery_payload): """Discover and add a MQTT camera.""" try: - discovery_hash = discovery_payload[ATTR_DISCOVERY_HASH] + discovery_hash = discovery_payload.pop(ATTR_DISCOVERY_HASH) + # state_topic is implicitly set by MQTT discovery, remove it + discovery_payload.pop(CONF_STATE_TOPIC, None) config = PLATFORM_SCHEMA(discovery_payload) await _async_setup_entity(config, async_add_entities, discovery_hash) @@ -89,6 +92,8 @@ class MqttCamera(MqttDiscoveryUpdate, Camera): async def discovery_update(self, discovery_payload): """Handle updated discovery message.""" + # state_topic is implicitly set by MQTT discovery, remove it + discovery_payload.pop(CONF_STATE_TOPIC, None) config = PLATFORM_SCHEMA(discovery_payload) self._config = config await self._subscribe_topics() @@ -105,7 +110,8 @@ class MqttCamera(MqttDiscoveryUpdate, Camera): self.hass, self._sub_state, {'state_topic': {'topic': self._config.get(CONF_TOPIC), 'msg_callback': message_received, - 'qos': self._qos}}) + 'qos': self._qos, + 'encoding': None}}) async def async_will_remove_from_hass(self): """Unsubscribe when removed.""" diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index db46f11b88e..c028ca5a6f6 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -17,9 +17,9 @@ from homeassistant.components.climate import ( SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, ClimateDevice) from homeassistant.components.fan import SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM from homeassistant.components.mqtt import ( - ATTR_DISCOVERY_HASH, CONF_QOS, CONF_RETAIN, CONF_UNIQUE_ID, - MQTT_BASE_PLATFORM_SCHEMA, MqttAttributes, MqttAvailability, - MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) + ATTR_DISCOVERY_HASH, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, + CONF_UNIQUE_ID, MQTT_BASE_PLATFORM_SCHEMA, MqttAttributes, + MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) from homeassistant.components.mqtt.discovery import ( MQTT_DISCOVERY_NEW, clear_discovery_hash) from homeassistant.const import ( @@ -156,7 +156,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def async_discover(discovery_payload): """Discover and add a MQTT climate device.""" try: - discovery_hash = discovery_payload[ATTR_DISCOVERY_HASH] + discovery_hash = discovery_payload.pop(ATTR_DISCOVERY_HASH) + # state_topic is implicitly set by MQTT discovery, remove it + discovery_payload.pop(CONF_STATE_TOPIC, None) config = PLATFORM_SCHEMA(discovery_payload) await _async_setup_entity(hass, config, async_add_entities, config_entry, discovery_hash) @@ -217,6 +219,8 @@ class MqttClimate(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, async def discovery_update(self, discovery_payload): """Handle updated discovery message.""" + # state_topic is implicitly set by MQTT discovery, remove it + discovery_payload.pop(CONF_STATE_TOPIC, None) config = PLATFORM_SCHEMA(discovery_payload) self._config = config self._setup_from_config(config) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 75fae0e9c15..829be266b09 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -10,8 +10,8 @@ import voluptuous as vol from homeassistant.components import cover, mqtt from homeassistant.components.cover import ( - ATTR_POSITION, ATTR_TILT_POSITION, SUPPORT_CLOSE, SUPPORT_CLOSE_TILT, - SUPPORT_OPEN, SUPPORT_OPEN_TILT, SUPPORT_SET_POSITION, + ATTR_POSITION, ATTR_TILT_POSITION, DEVICE_CLASSES_SCHEMA, SUPPORT_CLOSE, + SUPPORT_CLOSE_TILT, SUPPORT_OPEN, SUPPORT_OPEN_TILT, SUPPORT_SET_POSITION, SUPPORT_SET_TILT_POSITION, SUPPORT_STOP, SUPPORT_STOP_TILT, CoverDevice) from homeassistant.components.mqtt import ( ATTR_DISCOVERY_HASH, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, @@ -20,8 +20,8 @@ from homeassistant.components.mqtt import ( from homeassistant.components.mqtt.discovery import ( MQTT_DISCOVERY_NEW, clear_discovery_hash) from homeassistant.const import ( - CONF_DEVICE, CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE, STATE_CLOSED, - STATE_OPEN, STATE_UNKNOWN) + CONF_DEVICE, CONF_DEVICE_CLASS, CONF_NAME, CONF_OPTIMISTIC, + CONF_VALUE_TEMPLATE, STATE_CLOSED, STATE_OPEN, STATE_UNKNOWN) from homeassistant.core import callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv @@ -120,6 +120,7 @@ PLATFORM_SCHEMA = vol.All(mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ default=DEFAULT_TILT_INVERT_STATE): cv.boolean, vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema).extend( mqtt.MQTT_JSON_ATTRS_SCHEMA.schema), validate_options) @@ -328,6 +329,11 @@ class MqttCover(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, """Return current position of cover tilt.""" return self._tilt_value + @property + def device_class(self): + """Return the class of this sensor.""" + return self._config.get(CONF_DEVICE_CLASS) + @property def supported_features(self): """Flag supported features.""" diff --git a/homeassistant/components/device_tracker/mqtt.py b/homeassistant/components/mqtt/device_tracker.py similarity index 100% rename from homeassistant/components/device_tracker/mqtt.py rename to homeassistant/components/mqtt/device_tracker.py diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 9a2daf388cb..688912070bd 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -11,7 +11,7 @@ import re from homeassistant.components import mqtt from homeassistant.components.mqtt import ATTR_DISCOVERY_HASH, CONF_STATE_TOPIC -from homeassistant.const import CONF_PLATFORM +from homeassistant.const import CONF_DEVICE, CONF_PLATFORM from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import HomeAssistantType @@ -81,6 +81,7 @@ ABBREVIATIONS = { 'cln_tpl': 'cleaning_template', 'cmd_t': 'command_topic', 'curr_temp_t': 'current_temperature_topic', + 'dev': 'device', 'dev_cla': 'device_class', 'dock_t': 'docked_topic', 'dock_tpl': 'docked_template', @@ -104,6 +105,7 @@ ABBREVIATIONS = { 'ic': 'icon', 'init': 'initial', 'json_attr': 'json_attributes', + 'json_attr_t': 'json_attributes_topic', 'max_temp': 'max_temp', 'min_temp': 'min_temp', 'mode_cmd_t': 'mode_command_topic', @@ -172,6 +174,7 @@ ABBREVIATIONS = { 'unit_of_meas': 'unit_of_measurement', 'val_tpl': 'value_template', 'whit_val_cmd_t': 'white_value_command_topic', + 'whit_val_scl': 'white_value_scale', 'whit_val_stat_t': 'white_value_state_topic', 'whit_val_tpl': 'white_value_template', 'xy_cmd_t': 'xy_command_topic', @@ -179,6 +182,15 @@ ABBREVIATIONS = { 'xy_val_tpl': 'xy_value_template', } +DEVICE_ABBREVIATIONS = { + 'cns': 'connections', + 'ids': 'identifiers', + 'name': 'name', + 'mf': 'manufacturer', + 'mdl': 'model', + 'sw': 'sw_version', +} + def clear_discovery_hash(hass, discovery_hash): """Clear entry in ALREADY_DISCOVERED list.""" @@ -216,6 +228,13 @@ async def async_start(hass: HomeAssistantType, discovery_topic, hass_config, key = ABBREVIATIONS.get(key, key) payload[key] = payload.pop(abbreviated_key) + if CONF_DEVICE in payload: + device = payload[CONF_DEVICE] + for key in list(device.keys()): + abbreviated_key = key + key = DEVICE_ABBREVIATIONS.get(key, key) + device[key] = device.pop(abbreviated_key) + base = payload.pop(TOPIC_BASE, None) if base: for key, value in payload.items(): diff --git a/homeassistant/components/mqtt_eventstream.py b/homeassistant/components/mqtt_eventstream/__init__.py similarity index 94% rename from homeassistant/components/mqtt_eventstream.py rename to homeassistant/components/mqtt_eventstream/__init__.py index 2cde7825734..6e545d19fe2 100644 --- a/homeassistant/components/mqtt_eventstream.py +++ b/homeassistant/components/mqtt_eventstream/__init__.py @@ -1,9 +1,4 @@ -""" -Connect two Home Assistant instances via MQTT. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/mqtt_eventstream/ -""" +"""Connect two Home Assistant instances via MQTT.""" import asyncio import json @@ -33,7 +28,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_SUBSCRIBE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_PUBLISH_EVENTSTREAM_RECEIVED, default=False): cv.boolean, - vol.Optional(CONF_IGNORE_EVENT, default=[]): cv.ensure_list + vol.Optional(CONF_IGNORE_EVENT, default=[]): cv.ensure_list, }), }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/mqtt_statestream.py b/homeassistant/components/mqtt_statestream/__init__.py similarity index 91% rename from homeassistant/components/mqtt_statestream.py rename to homeassistant/components/mqtt_statestream/__init__.py index 3a0e5d39ff0..18a70bf75bb 100644 --- a/homeassistant/components/mqtt_statestream.py +++ b/homeassistant/components/mqtt_statestream/__init__.py @@ -1,9 +1,4 @@ -""" -Publish simple item state changes via MQTT. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/mqtt_statestream/ -""" +"""Publish simple item state changes via MQTT.""" import json import voluptuous as vol @@ -20,6 +15,7 @@ import homeassistant.helpers.config_validation as cv CONF_BASE_TOPIC = 'base_topic' CONF_PUBLISH_ATTRIBUTES = 'publish_attributes' CONF_PUBLISH_TIMESTAMPS = 'publish_timestamps' + DEPENDENCIES = ['mqtt'] DOMAIN = 'mqtt_statestream' @@ -28,16 +24,16 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_EXCLUDE, default={}): vol.Schema({ vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids, vol.Optional(CONF_DOMAINS, default=[]): - vol.All(cv.ensure_list, [cv.string]) + vol.All(cv.ensure_list, [cv.string]), }), vol.Optional(CONF_INCLUDE, default={}): vol.Schema({ vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids, vol.Optional(CONF_DOMAINS, default=[]): - vol.All(cv.ensure_list, [cv.string]) + vol.All(cv.ensure_list, [cv.string]), }), vol.Required(CONF_BASE_TOPIC): valid_publish_topic, vol.Optional(CONF_PUBLISH_ATTRIBUTES, default=False): cv.boolean, - vol.Optional(CONF_PUBLISH_TIMESTAMPS, default=False): cv.boolean + vol.Optional(CONF_PUBLISH_TIMESTAMPS, default=False): cv.boolean, }) }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/mychevy.py b/homeassistant/components/mychevy/__init__.py similarity index 95% rename from homeassistant/components/mychevy.py rename to homeassistant/components/mychevy/__init__.py index 209027ad472..e6fd7f19c2a 100644 --- a/homeassistant/components/mychevy.py +++ b/homeassistant/components/mychevy/__init__.py @@ -1,9 +1,4 @@ -""" -MyChevy Component. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/mychevy/ -""" +"""Support for MyChevy.""" from datetime import timedelta import logging import threading @@ -16,7 +11,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers import discovery from homeassistant.util import Throttle -REQUIREMENTS = ["mychevy==1.2.0"] +REQUIREMENTS = ['mychevy==1.2.0'] DOMAIN = 'mychevy' UPDATE_TOPIC = DOMAIN @@ -41,7 +36,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_COUNTRY, default=DEFAULT_COUNTRY): - vol.All(cv.string, vol.In(['us', 'ca'])) + vol.All(cv.string, vol.In(['us', 'ca'])), }), }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/binary_sensor/mychevy.py b/homeassistant/components/mychevy/binary_sensor.py similarity index 82% rename from homeassistant/components/binary_sensor/mychevy.py rename to homeassistant/components/mychevy/binary_sensor.py index c1e3b6f0aac..67f12a14359 100644 --- a/homeassistant/components/binary_sensor/mychevy.py +++ b/homeassistant/components/mychevy/binary_sensor.py @@ -1,8 +1,4 @@ -"""Support for MyChevy sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.mychevy/ -""" +"""Support for MyChevy binary sensors.""" import logging from homeassistant.components.mychevy import ( @@ -20,8 +16,8 @@ SENSORS = [ ] -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the MyChevy sensors.""" if discovery_info is None: return @@ -41,7 +37,6 @@ class EVBinarySensor(BinarySensorDevice): The only real difference between sensors is which units and what attribute from the car object they are returning. All logic can be built with just setting subclass attributes. - """ def __init__(self, connection, config, car_vid): @@ -53,9 +48,8 @@ class EVBinarySensor(BinarySensorDevice): self._is_on = None self._car_vid = car_vid self.entity_id = ENTITY_ID_FORMAT.format( - '{}_{}_{}'.format(MYCHEVY_DOMAIN, - slugify(self._car.name), - slugify(self._name))) + '{}_{}_{}'.format( + MYCHEVY_DOMAIN, slugify(self._car.name), slugify(self._name))) @property def name(self): diff --git a/homeassistant/components/sensor/mychevy.py b/homeassistant/components/mychevy/sensor.py similarity index 93% rename from homeassistant/components/sensor/mychevy.py rename to homeassistant/components/mychevy/sensor.py index b478e2ef3ca..c7d140e0c4c 100644 --- a/homeassistant/components/sensor/mychevy.py +++ b/homeassistant/components/mychevy/sensor.py @@ -1,9 +1,4 @@ -"""Support for MyChevy sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.mychevy/ -""" - +"""Support for MyChevy sensors.""" import logging from homeassistant.components.mychevy import ( @@ -16,6 +11,8 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.util import slugify +_LOGGER = logging.getLogger(__name__) + BATTERY_SENSOR = "batteryLevel" SENSORS = [ @@ -28,8 +25,6 @@ SENSORS = [ ["charging"]) ] -_LOGGER = logging.getLogger(__name__) - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the MyChevy sensors.""" @@ -49,7 +44,7 @@ class MyChevyStatus(Entity): """A string representing the charge mode.""" _name = "MyChevy Status" - _icon = "mdi:car-connected" + _icon = 'mdi:car-connected' def __init__(self): """Initialize sensor with car connection.""" @@ -107,7 +102,6 @@ class EVSensor(Entity): The only real difference between sensors is which units and what attribute from the car object they are returning. All logic can be built with just setting subclass attributes. - """ def __init__(self, connection, config, car_vid): @@ -123,9 +117,8 @@ class EVSensor(Entity): self._car_vid = car_vid self.entity_id = ENTITY_ID_FORMAT.format( - '{}_{}_{}'.format(MYCHEVY_DOMAIN, - slugify(self._car.name), - slugify(self._name))) + '{}_{}_{}'.format( + MYCHEVY_DOMAIN, slugify(self._car.name), slugify(self._name))) async def async_added_to_hass(self): """Register callbacks.""" diff --git a/homeassistant/components/mycroft.py b/homeassistant/components/mycroft/__init__.py similarity index 79% rename from homeassistant/components/mycroft.py rename to homeassistant/components/mycroft/__init__.py index 834572bc551..29f6383f686 100644 --- a/homeassistant/components/mycroft.py +++ b/homeassistant/components/mycroft/__init__.py @@ -1,10 +1,4 @@ -""" -Support for Mycroft AI. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/mycroft -""" - +"""Support for Mycroft AI.""" import logging import voluptuous as vol @@ -17,10 +11,8 @@ REQUIREMENTS = ['mycroftapi==2.0'] _LOGGER = logging.getLogger(__name__) - DOMAIN = 'mycroft' - CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_HOST): cv.string diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index 49f8560c6b3..7ca21ac582a 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -1,9 +1,4 @@ -""" -Connect to a MySensors gateway via pymysensors API. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/mysensors/ -""" +"""Connect to a MySensors gateway via pymysensors API.""" import logging import voluptuous as vol diff --git a/homeassistant/components/binary_sensor/mysensors.py b/homeassistant/components/mysensors/binary_sensor.py similarity index 88% rename from homeassistant/components/binary_sensor/mysensors.py rename to homeassistant/components/mysensors/binary_sensor.py index f0b7832cf25..57e8f1c1ef8 100644 --- a/homeassistant/components/binary_sensor/mysensors.py +++ b/homeassistant/components/mysensors/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for MySensors binary sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.mysensors/ -""" +"""Support for MySensors binary sensors.""" from homeassistant.components import mysensors from homeassistant.components.binary_sensor import ( DEVICE_CLASSES, DOMAIN, BinarySensorDevice) diff --git a/homeassistant/components/climate/mysensors.py b/homeassistant/components/mysensors/climate.py similarity index 97% rename from homeassistant/components/climate/mysensors.py rename to homeassistant/components/mysensors/climate.py index 66c634d8cd9..20d608e1ca5 100644 --- a/homeassistant/components/climate/mysensors.py +++ b/homeassistant/components/mysensors/climate.py @@ -1,9 +1,4 @@ -""" -MySensors platform that offers a Climate (MySensors-HVAC) component. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/climate.mysensors/ -""" +"""MySensors platform that offers a Climate (MySensors-HVAC) component.""" from homeassistant.components import mysensors from homeassistant.components.climate import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DOMAIN, STATE_AUTO, diff --git a/homeassistant/components/cover/mysensors.py b/homeassistant/components/mysensors/cover.py similarity index 94% rename from homeassistant/components/cover/mysensors.py rename to homeassistant/components/mysensors/cover.py index 60ff7aeef1d..01605bb9afe 100644 --- a/homeassistant/components/cover/mysensors.py +++ b/homeassistant/components/mysensors/cover.py @@ -1,9 +1,4 @@ -""" -Support for MySensors covers. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/cover.mysensors/ -""" +"""Support for MySensors covers.""" from homeassistant.components import mysensors from homeassistant.components.cover import ATTR_POSITION, DOMAIN, CoverDevice from homeassistant.const import STATE_OFF, STATE_ON diff --git a/homeassistant/components/device_tracker/mysensors.py b/homeassistant/components/mysensors/device_tracker.py similarity index 90% rename from homeassistant/components/device_tracker/mysensors.py rename to homeassistant/components/mysensors/device_tracker.py index 705dc9968c9..b50286585a4 100644 --- a/homeassistant/components/device_tracker/mysensors.py +++ b/homeassistant/components/mysensors/device_tracker.py @@ -1,9 +1,4 @@ -""" -Support for tracking MySensors devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/device_tracker.mysensors/ -""" +"""Support for tracking MySensors devices.""" from homeassistant.components import mysensors from homeassistant.components.device_tracker import DOMAIN from homeassistant.helpers.dispatcher import async_dispatcher_connect diff --git a/homeassistant/components/light/mysensors.py b/homeassistant/components/mysensors/light.py similarity index 97% rename from homeassistant/components/light/mysensors.py rename to homeassistant/components/mysensors/light.py index 23d602c5d40..56511b73dfe 100644 --- a/homeassistant/components/light/mysensors.py +++ b/homeassistant/components/mysensors/light.py @@ -1,9 +1,4 @@ -""" -Support for MySensors lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.mysensors/ -""" +"""Support for MySensors lights.""" from homeassistant.components import mysensors from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_WHITE_VALUE, DOMAIN, diff --git a/homeassistant/components/notify/mysensors.py b/homeassistant/components/mysensors/notify.py similarity index 90% rename from homeassistant/components/notify/mysensors.py rename to homeassistant/components/mysensors/notify.py index 71ce7fb0b74..ab198bc21bc 100644 --- a/homeassistant/components/notify/mysensors.py +++ b/homeassistant/components/mysensors/notify.py @@ -1,9 +1,4 @@ -""" -MySensors notification service. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/notify.mysensors/ -""" +"""MySensors notification service.""" from homeassistant.components import mysensors from homeassistant.components.notify import ( ATTR_TARGET, DOMAIN, BaseNotificationService) diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/mysensors/sensor.py similarity index 94% rename from homeassistant/components/sensor/mysensors.py rename to homeassistant/components/mysensors/sensor.py index 160d4b4784b..ce6d5da2b4c 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/mysensors/sensor.py @@ -1,9 +1,4 @@ -""" -Support for MySensors sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.mysensors/ -""" +"""Support for MySensors sensors.""" from homeassistant.components import mysensors from homeassistant.components.sensor import DOMAIN from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT diff --git a/homeassistant/components/switch/mysensors.py b/homeassistant/components/mysensors/switch.py similarity index 91% rename from homeassistant/components/switch/mysensors.py rename to homeassistant/components/mysensors/switch.py index 20e50518df8..0ad9be1d508 100644 --- a/homeassistant/components/switch/mysensors.py +++ b/homeassistant/components/mysensors/switch.py @@ -1,9 +1,4 @@ -""" -Support for MySensors switches. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.mysensors/ -""" +"""Support for MySensors switches.""" import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -89,7 +84,7 @@ class MySensorsSwitch(mysensors.device.MySensorsEntity, SwitchDevice): self.gateway.set_child_value( self.node_id, self.child_id, self.value_type, 1) if self.gateway.optimistic: - # optimistically assume that switch has changed state + # Optimistically assume that switch has changed state self._values[self.value_type] = STATE_ON self.async_schedule_update_ha_state() @@ -98,7 +93,7 @@ class MySensorsSwitch(mysensors.device.MySensorsEntity, SwitchDevice): self.gateway.set_child_value( self.node_id, self.child_id, self.value_type, 0) if self.gateway.optimistic: - # optimistically assume that switch has changed state + # Optimistically assume that switch has changed state self._values[self.value_type] = STATE_OFF self.async_schedule_update_ha_state() @@ -127,11 +122,11 @@ class MySensorsIRSwitch(MySensorsSwitch): self.gateway.set_child_value( self.node_id, self.child_id, set_req.V_LIGHT, 1) if self.gateway.optimistic: - # optimistically assume that switch has changed state + # Optimistically assume that switch has changed state self._values[self.value_type] = self._ir_code self._values[set_req.V_LIGHT] = STATE_ON self.async_schedule_update_ha_state() - # turn off switch after switch was turned on + # Turn off switch after switch was turned on await self.async_turn_off() async def async_turn_off(self, **kwargs): @@ -140,7 +135,7 @@ class MySensorsIRSwitch(MySensorsSwitch): self.gateway.set_child_value( self.node_id, self.child_id, set_req.V_LIGHT, 0) if self.gateway.optimistic: - # optimistically assume that switch has changed state + # Optimistically assume that switch has changed state self._values[set_req.V_LIGHT] = STATE_OFF self.async_schedule_update_ha_state() diff --git a/homeassistant/components/mythicbeastsdns.py b/homeassistant/components/mythicbeastsdns/__init__.py similarity index 53% rename from homeassistant/components/mythicbeastsdns.py rename to homeassistant/components/mythicbeastsdns/__init__.py index d73e4619c78..3d0d250557b 100644 --- a/homeassistant/components/mythicbeastsdns.py +++ b/homeassistant/components/mythicbeastsdns/__init__.py @@ -1,18 +1,16 @@ -""" -Integrate with Mythic Beasts Dynamic DNS service. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/mythicbeastsdns/ -""" +"""Support for Mythic Beasts Dynamic DNS service.""" from datetime import timedelta import logging + import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_HOST, CONF_DOMAIN, CONF_PASSWORD, \ - CONF_UPDATE_INTERVAL -from homeassistant.helpers.event import async_track_time_interval +from homeassistant.const import ( + CONF_HOST, CONF_DOMAIN, CONF_PASSWORD, CONF_UPDATE_INTERVAL, + CONF_SCAN_INTERVAL, CONF_UPDATE_INTERVAL_INVALIDATION_VERSION +) from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_track_time_interval REQUIREMENTS = ['mbddns==0.1.2'] @@ -23,13 +21,23 @@ DOMAIN = 'mythicbeastsdns' DEFAULT_INTERVAL = timedelta(minutes=10) CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_DOMAIN): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_UPDATE_INTERVAL, default=DEFAULT_INTERVAL): vol.All( - cv.time_period, cv.positive_timedelta), - }) + DOMAIN: vol.All( + vol.Schema({ + vol.Required(CONF_DOMAIN): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_UPDATE_INTERVAL): + vol.All(cv.time_period, cv.positive_timedelta), + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_INTERVAL): + vol.All(cv.time_period, cv.positive_timedelta), + }), + cv.deprecated( + CONF_UPDATE_INTERVAL, + replacement_key=CONF_SCAN_INTERVAL, + invalidation_version=CONF_UPDATE_INTERVAL_INVALIDATION_VERSION, + default=DEFAULT_INTERVAL + ) + ) }, extra=vol.ALLOW_EXTRA) @@ -40,7 +48,7 @@ async def async_setup(hass, config): domain = config[DOMAIN][CONF_DOMAIN] password = config[DOMAIN][CONF_PASSWORD] host = config[DOMAIN][CONF_HOST] - update_interval = config[DOMAIN][CONF_UPDATE_INTERVAL] + update_interval = config[DOMAIN][CONF_SCAN_INTERVAL] session = async_get_clientsession(hass) diff --git a/homeassistant/components/namecheapdns.py b/homeassistant/components/namecheapdns/__init__.py similarity index 91% rename from homeassistant/components/namecheapdns.py rename to homeassistant/components/namecheapdns/__init__.py index f817544ca77..f86e7d18556 100644 --- a/homeassistant/components/namecheapdns.py +++ b/homeassistant/components/namecheapdns/__init__.py @@ -1,9 +1,4 @@ -""" -Integrate with namecheap DNS services. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/namecheapdns/ -""" +"""Support for namecheap DNS services.""" import logging from datetime import timedelta diff --git a/homeassistant/components/neato.py b/homeassistant/components/neato/__init__.py similarity index 97% rename from homeassistant/components/neato.py rename to homeassistant/components/neato/__init__.py index 31410d1c9b2..2b4af3e1e91 100644 --- a/homeassistant/components/neato.py +++ b/homeassistant/components/neato/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Neato botvac connected vacuum cleaners. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/neato/ -""" +"""Support for Neato botvac connected vacuum cleaners.""" import logging from datetime import timedelta from urllib.error import HTTPError @@ -15,10 +10,10 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import discovery from homeassistant.util import Throttle -_LOGGER = logging.getLogger(__name__) - REQUIREMENTS = ['pybotvac==0.0.13'] +_LOGGER = logging.getLogger(__name__) + DOMAIN = 'neato' NEATO_ROBOTS = 'neato_robots' NEATO_LOGIN = 'neato_login' diff --git a/homeassistant/components/camera/neato.py b/homeassistant/components/neato/camera.py similarity index 91% rename from homeassistant/components/camera/neato.py rename to homeassistant/components/neato/camera.py index 4df423344bb..530aa8fc6f1 100644 --- a/homeassistant/components/camera/neato.py +++ b/homeassistant/components/neato/camera.py @@ -1,9 +1,4 @@ -""" -Camera that loads a picture from Neato. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/camera.neato/ -""" +"""Support for loading picture from Neato.""" import logging from datetime import timedelta diff --git a/homeassistant/components/switch/neato.py b/homeassistant/components/neato/switch.py similarity index 94% rename from homeassistant/components/switch/neato.py rename to homeassistant/components/neato/switch.py index 0b49cb71ba2..fcc72762b8d 100644 --- a/homeassistant/components/switch/neato.py +++ b/homeassistant/components/neato/switch.py @@ -1,9 +1,4 @@ -""" -Support for Neato Connected Vacuums switches. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.neato/ -""" +"""Support for Neato Connected Vacuums switches.""" import logging from datetime import timedelta import requests diff --git a/homeassistant/components/vacuum/neato.py b/homeassistant/components/neato/vacuum.py similarity index 97% rename from homeassistant/components/vacuum/neato.py rename to homeassistant/components/neato/vacuum.py index 9ec9fe688b7..45cfd273aca 100644 --- a/homeassistant/components/vacuum/neato.py +++ b/homeassistant/components/neato/vacuum.py @@ -1,9 +1,4 @@ -""" -Support for Neato Connected Vacuums. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/vacuum.neato/ -""" +"""Support for Neato Connected Vacuums.""" import logging from datetime import timedelta import requests diff --git a/homeassistant/components/ness_alarm.py b/homeassistant/components/ness_alarm/__init__.py similarity index 92% rename from homeassistant/components/ness_alarm.py rename to homeassistant/components/ness_alarm/__init__.py index e97ee903abc..dae244ece3f 100644 --- a/homeassistant/components/ness_alarm.py +++ b/homeassistant/components/ness_alarm/__init__.py @@ -1,16 +1,11 @@ -""" -Support for Ness D8X/D16X devices. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/ness_alarm/ -""" -import logging +"""Support for Ness D8X/D16X devices.""" from collections import namedtuple +import logging import voluptuous as vol from homeassistant.components.binary_sensor import DEVICE_CLASSES -from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ATTR_CODE, ATTR_STATE, EVENT_HOMEASSISTANT_STOP from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -28,9 +23,7 @@ CONF_ZONES = 'zones' CONF_ZONE_NAME = 'name' CONF_ZONE_TYPE = 'type' CONF_ZONE_ID = 'id' -ATTR_CODE = 'code' ATTR_OUTPUT_ID = 'output_id' -ATTR_STATE = 'state' DEFAULT_ZONES = [] SIGNAL_ZONE_CHANGED = 'ness_alarm.zone_changed' diff --git a/homeassistant/components/nest/.translations/da.json b/homeassistant/components/nest/.translations/da.json index 5edf3a00af4..7dfd1c8b250 100644 --- a/homeassistant/components/nest/.translations/da.json +++ b/homeassistant/components/nest/.translations/da.json @@ -1,22 +1,30 @@ { "config": { "abort": { - "already_setup": "Du kan kun konfigurere en enkelt Nest konto." + "already_setup": "Du kan kun konfigurere en enkelt Nest konto.", + "authorize_url_fail": "Ukendt fejl ved generering af en autoriseret url.", + "authorize_url_timeout": "Timeout ved generering af autoriseret url.", + "no_flows": "Du skal konfigurere Nest f\u00f8r du kan autentificere med det. [L\u00e6s venligst vejledningen](https://www.home-assistant.io/components/nest/)." }, "error": { - "invalid_code": "Ugyldig kode" + "internal_error": "Intern fejl ved validering af kode", + "invalid_code": "Ugyldig kode", + "timeout": "Timeout ved validering af kode", + "unknown": "Ukendt fejl ved validering af kode" }, "step": { "init": { "data": { "flow_impl": "Udbyder" }, + "description": "V\u00e6lg hvilken godkendelsesudbyder du vil godkende med Nest.", "title": "Godkendelses udbyder" }, "link": { "data": { "code": "PIN-kode" }, + "description": "For at forbinde din Nest-konto, [godkend din konto]({url}). \n\nEfter godkendelse skal du kopiere pin koden nedenfor.", "title": "Link Nest-konto" } }, diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 7f0fe27df73..fe6a34cf404 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Nest devices. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/nest/ -""" +"""Support for Nest devices.""" import logging import socket from datetime import datetime, timedelta @@ -53,7 +48,7 @@ AWAY_MODE_AWAY = 'away' AWAY_MODE_HOME = 'home' SENSOR_SCHEMA = vol.Schema({ - vol.Optional(CONF_MONITORED_CONDITIONS): vol.All(cv.ensure_list) + vol.Optional(CONF_MONITORED_CONDITIONS): vol.All(cv.ensure_list), }) CONFIG_SCHEMA = vol.Schema({ @@ -62,25 +57,25 @@ CONFIG_SCHEMA = vol.Schema({ vol.Required(CONF_CLIENT_SECRET): cv.string, vol.Optional(CONF_STRUCTURE): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_SENSORS): SENSOR_SCHEMA, - vol.Optional(CONF_BINARY_SENSORS): SENSOR_SCHEMA + vol.Optional(CONF_BINARY_SENSORS): SENSOR_SCHEMA, }) }, extra=vol.ALLOW_EXTRA) SET_AWAY_MODE_SCHEMA = vol.Schema({ vol.Required(ATTR_AWAY_MODE): vol.In([AWAY_MODE_AWAY, AWAY_MODE_HOME]), - vol.Optional(ATTR_STRUCTURE): vol.All(cv.ensure_list, [cv.string]) + vol.Optional(ATTR_STRUCTURE): vol.All(cv.ensure_list, [cv.string]), }) SET_ETA_SCHEMA = vol.Schema({ vol.Required(ATTR_ETA): cv.time_period, vol.Optional(ATTR_TRIP_ID): cv.string, vol.Optional(ATTR_ETA_WINDOW): cv.time_period, - vol.Optional(ATTR_STRUCTURE): vol.All(cv.ensure_list, [cv.string]) + vol.Optional(ATTR_STRUCTURE): vol.All(cv.ensure_list, [cv.string]), }) CANCEL_ETA_SCHEMA = vol.Schema({ vol.Required(ATTR_TRIP_ID): cv.string, - vol.Optional(ATTR_STRUCTURE): vol.All(cv.ensure_list, [cv.string]) + vol.Optional(ATTR_STRUCTURE): vol.All(cv.ensure_list, [cv.string]), }) @@ -90,7 +85,7 @@ def nest_update_event_broker(hass, nest): Runs in its own thread. """ - _LOGGER.debug("listening nest.update_event") + _LOGGER.debug("Listening for nest.update_event") while hass.is_running: nest.update_event.wait() @@ -99,10 +94,10 @@ def nest_update_event_broker(hass, nest): break nest.update_event.clear() - _LOGGER.debug("dispatching nest data update") + _LOGGER.debug("Dispatching nest data update") dispatcher_send(hass, SIGNAL_NEST_UPDATE) - _LOGGER.debug("stop listening nest.update_event") + _LOGGER.debug("Stop listening for nest.update_event") async def async_setup(hass, config): diff --git a/homeassistant/components/binary_sensor/nest.py b/homeassistant/components/nest/binary_sensor.py similarity index 87% rename from homeassistant/components/binary_sensor/nest.py rename to homeassistant/components/nest/binary_sensor.py index 7f7278d9789..1077fdb073e 100644 --- a/homeassistant/components/binary_sensor/nest.py +++ b/homeassistant/components/nest/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for Nest Thermostat Binary Sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.nest/ -""" +"""Support for Nest Thermostat binary sensors.""" from itertools import chain import logging @@ -12,6 +7,8 @@ from homeassistant.components.nest import ( DATA_NEST, DATA_NEST_CONFIG, CONF_BINARY_SENSORS, NestSensorDevice) from homeassistant.const import CONF_MONITORED_CONDITIONS +_LOGGER = logging.getLogger(__name__) + DEPENDENCIES = ['nest'] BINARY_TYPES = {'online': 'connectivity'} @@ -48,10 +45,12 @@ _BINARY_TYPES_DEPRECATED = [ 'hvac_emer_heat_state', ] -_VALID_BINARY_SENSOR_TYPES = {**BINARY_TYPES, **CLIMATE_BINARY_TYPES, - **CAMERA_BINARY_TYPES, **STRUCTURE_BINARY_TYPES} - -_LOGGER = logging.getLogger(__name__) +_VALID_BINARY_SENSOR_TYPES = { + **BINARY_TYPES, + **CLIMATE_BINARY_TYPES, + **CAMERA_BINARY_TYPES, + **STRUCTURE_BINARY_TYPES, +} def setup_platform(hass, config, add_entities, discovery_info=None): @@ -89,9 +88,8 @@ async def async_setup_entry(hass, entry, async_add_entities): sensors += [NestBinarySensor(structure, None, variable) for variable in conditions if variable in STRUCTURE_BINARY_TYPES] - device_chain = chain(nest.thermostats(), - nest.smoke_co_alarms(), - nest.cameras()) + device_chain = chain( + nest.thermostats(), nest.smoke_co_alarms(), nest.cameras()) for structure, device in device_chain: sensors += [NestBinarySensor(structure, device, variable) for variable in conditions @@ -106,9 +104,8 @@ async def async_setup_entry(hass, entry, async_add_entities): for variable in conditions if variable in CAMERA_BINARY_TYPES] for activity_zone in device.activity_zones: - sensors += [NestActivityZoneSensor(structure, - device, - activity_zone)] + sensors += [NestActivityZoneSensor( + structure, device, activity_zone)] return sensors diff --git a/homeassistant/components/camera/nest.py b/homeassistant/components/nest/camera.py similarity index 96% rename from homeassistant/components/camera/nest.py rename to homeassistant/components/nest/camera.py index 158123989c0..8b450e02b46 100644 --- a/homeassistant/components/camera/nest.py +++ b/homeassistant/components/nest/camera.py @@ -1,9 +1,4 @@ -""" -Support for Nest Cameras. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/camera.nest/ -""" +"""Support for Nest Cameras.""" import logging from datetime import timedelta diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/nest/climate.py similarity index 98% rename from homeassistant/components/climate/nest.py rename to homeassistant/components/nest/climate.py index bd6bb2991cc..8746a1959ae 100644 --- a/homeassistant/components/climate/nest.py +++ b/homeassistant/components/nest/climate.py @@ -1,9 +1,4 @@ -""" -Support for Nest thermostats. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/climate.nest/ -""" +"""Support for Nest thermostats.""" import logging import voluptuous as vol diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/nest/sensor.py similarity index 97% rename from homeassistant/components/sensor/nest.py rename to homeassistant/components/nest/sensor.py index 5514203c6ea..10fa83d23e0 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/nest/sensor.py @@ -1,9 +1,4 @@ -""" -Support for Nest Thermostat Sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.nest/ -""" +"""Support for Nest Thermostat sensors.""" import logging from homeassistant.components.climate import ( diff --git a/homeassistant/components/nest/services.yaml b/homeassistant/components/nest/services.yaml index e10e6264643..0015c83342d 100644 --- a/homeassistant/components/nest/services.yaml +++ b/homeassistant/components/nest/services.yaml @@ -1,37 +1,16 @@ -# Describes the format for available Nest services +set_mode: + description: 'Set the home/away mode for a Nest structure. Set to away mode will + also set Estimated Arrival Time if provided. Set ETA will cause the thermostat + to begin warming or cooling the home before the user arrives. After ETA set other + Automation can read ETA sensor as a signal to prepare the home for the user''s + arrival. -set_away_mode: - description: Set the away mode for a Nest structure. + ' fields: - away_mode: - description: New mode to set. Valid modes are "away" or "home". - example: "away" - structure: - description: Name(s) of structure(s) to change. Defaults to all structures if not specified. - example: "Apartment" - -set_eta: - description: Set or update the estimated time of arrival window for a Nest structure. - fields: - eta: - description: Estimated time of arrival from now. - example: "00:10:30" - eta_window: - description: Estimated time of arrival window. Default is 1 minute. - example: "00:05" - trip_id: - description: Unique ID for the trip. Default is auto-generated using a timestamp. - example: "Leave Work" - structure: - description: Name(s) of structure(s) to change. Defaults to all structures if not specified. - example: "Apartment" - -cancel_eta: - description: Cancel an existing estimated time of arrival window for a Nest structure. - fields: - trip_id: - description: Unique ID for the trip. - example: "Leave Work" - structure: - description: Name(s) of structure(s) to change. Defaults to all structures if not specified. - example: "Apartment" + eta: {description: Optional Estimated Arrival Time from now., example: '0:10'} + eta_window: {description: Optional ETA window. Default is 1 minute., example: '0:5'} + home_mode: {description: home or away, example: home} + structure: {description: Optional structure name. Default set all structures managed + by Home Assistant., example: My Home} + trip_id: {description: Optional identity of a trip. Using the same trip_ID will + update the estimation., example: trip_back_home} diff --git a/homeassistant/components/netatmo.py b/homeassistant/components/netatmo/__init__.py similarity index 95% rename from homeassistant/components/netatmo.py rename to homeassistant/components/netatmo/__init__.py index 50bd290797d..495e22aae24 100644 --- a/homeassistant/components/netatmo.py +++ b/homeassistant/components/netatmo/__init__.py @@ -1,9 +1,4 @@ -""" -Support for the Netatmo devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/netatmo/ -""" +"""Support for the Netatmo devices.""" import logging from datetime import timedelta from urllib.error import HTTPError @@ -16,7 +11,7 @@ from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle -REQUIREMENTS = ['pyatmo==1.4'] +REQUIREMENTS = ['pyatmo==1.8'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/binary_sensor/netatmo.py b/homeassistant/components/netatmo/binary_sensor.py similarity index 96% rename from homeassistant/components/binary_sensor/netatmo.py rename to homeassistant/components/netatmo/binary_sensor.py index 2cafacf401c..727ed0a68c7 100644 --- a/homeassistant/components/binary_sensor/netatmo.py +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -1,11 +1,4 @@ -""" -Support for the Netatmo binary sensors. - -The binary sensors based on events seen by the Netatmo cameras. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.netatmo/. -""" +"""Support for the Netatmo binary sensors.""" import logging import voluptuous as vol diff --git a/homeassistant/components/camera/netatmo.py b/homeassistant/components/netatmo/camera.py similarity index 95% rename from homeassistant/components/camera/netatmo.py rename to homeassistant/components/netatmo/camera.py index 93ad2cd055b..a3a5461631d 100644 --- a/homeassistant/components/camera/netatmo.py +++ b/homeassistant/components/netatmo/camera.py @@ -1,9 +1,4 @@ -""" -Support for the Netatmo cameras. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/camera.netatmo/. -""" +"""Support for the Netatmo cameras.""" import logging import requests diff --git a/homeassistant/components/climate/netatmo.py b/homeassistant/components/netatmo/climate.py similarity index 96% rename from homeassistant/components/climate/netatmo.py rename to homeassistant/components/netatmo/climate.py index 8849ada5ccc..2b9bcbebaf2 100644 --- a/homeassistant/components/climate/netatmo.py +++ b/homeassistant/components/netatmo/climate.py @@ -1,9 +1,4 @@ -""" -Support for Netatmo Smart Thermostat. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/climate.netatmo/ -""" +"""Support for Netatmo Smart thermostats.""" import logging from datetime import timedelta import voluptuous as vol diff --git a/homeassistant/components/sensor/netatmo.py b/homeassistant/components/netatmo/sensor.py similarity index 53% rename from homeassistant/components/sensor/netatmo.py rename to homeassistant/components/netatmo/sensor.py index d593d93729b..78a118528b9 100644 --- a/homeassistant/components/sensor/netatmo.py +++ b/homeassistant/components/netatmo/sensor.py @@ -1,9 +1,4 @@ -""" -Support for the NetAtmo Weather Service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.netatmo/ -""" +"""Support for the NetAtmo Weather Service.""" import logging from time import time import threading @@ -55,8 +50,7 @@ SENSOR_TYPES = { } MODULE_SCHEMA = vol.Schema({ - vol.Required(cv.string): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.Required(cv.string): vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -164,146 +158,152 @@ class NetAtmoSensor(Entity): self._state = None return - if self.type == 'temperature': - self._state = round(data['Temperature'], 1) - elif self.type == 'humidity': - self._state = data['Humidity'] - elif self.type == 'rain': - self._state = data['Rain'] - elif self.type == 'sum_rain_1': - self._state = data['sum_rain_1'] - elif self.type == 'sum_rain_24': - self._state = data['sum_rain_24'] - elif self.type == 'noise': - self._state = data['Noise'] - elif self.type == 'co2': - self._state = data['CO2'] - elif self.type == 'pressure': - self._state = round(data['Pressure'], 1) - elif self.type == 'battery_percent': - self._state = data['battery_percent'] - elif self.type == 'battery_lvl': - self._state = data['battery_vp'] - elif (self.type == 'battery_vp' and - self._module_type == MODULE_TYPE_WIND): - if data['battery_vp'] >= 5590: - self._state = "Full" - elif data['battery_vp'] >= 5180: - self._state = "High" - elif data['battery_vp'] >= 4770: - self._state = "Medium" - elif data['battery_vp'] >= 4360: - self._state = "Low" - elif data['battery_vp'] < 4360: - self._state = "Very Low" - elif (self.type == 'battery_vp' and - self._module_type == MODULE_TYPE_RAIN): - if data['battery_vp'] >= 5500: - self._state = "Full" - elif data['battery_vp'] >= 5000: - self._state = "High" - elif data['battery_vp'] >= 4500: - self._state = "Medium" - elif data['battery_vp'] >= 4000: - self._state = "Low" - elif data['battery_vp'] < 4000: - self._state = "Very Low" - elif (self.type == 'battery_vp' and - self._module_type == MODULE_TYPE_INDOOR): - if data['battery_vp'] >= 5640: - self._state = "Full" - elif data['battery_vp'] >= 5280: - self._state = "High" - elif data['battery_vp'] >= 4920: - self._state = "Medium" - elif data['battery_vp'] >= 4560: - self._state = "Low" - elif data['battery_vp'] < 4560: - self._state = "Very Low" - elif (self.type == 'battery_vp' and - self._module_type == MODULE_TYPE_OUTDOOR): - if data['battery_vp'] >= 5500: - self._state = "Full" - elif data['battery_vp'] >= 5000: - self._state = "High" - elif data['battery_vp'] >= 4500: - self._state = "Medium" - elif data['battery_vp'] >= 4000: - self._state = "Low" - elif data['battery_vp'] < 4000: - self._state = "Very Low" - elif self.type == 'min_temp': - self._state = data['min_temp'] - elif self.type == 'max_temp': - self._state = data['max_temp'] - elif self.type == 'windangle_value': - self._state = data['WindAngle'] - elif self.type == 'windangle': - if data['WindAngle'] >= 330: - self._state = "N (%d\xb0)" % data['WindAngle'] - elif data['WindAngle'] >= 300: - self._state = "NW (%d\xb0)" % data['WindAngle'] - elif data['WindAngle'] >= 240: - self._state = "W (%d\xb0)" % data['WindAngle'] - elif data['WindAngle'] >= 210: - self._state = "SW (%d\xb0)" % data['WindAngle'] - elif data['WindAngle'] >= 150: - self._state = "S (%d\xb0)" % data['WindAngle'] - elif data['WindAngle'] >= 120: - self._state = "SE (%d\xb0)" % data['WindAngle'] - elif data['WindAngle'] >= 60: - self._state = "E (%d\xb0)" % data['WindAngle'] - elif data['WindAngle'] >= 30: - self._state = "NE (%d\xb0)" % data['WindAngle'] - elif data['WindAngle'] >= 0: - self._state = "N (%d\xb0)" % data['WindAngle'] - elif self.type == 'windstrength': - self._state = data['WindStrength'] - elif self.type == 'gustangle_value': - self._state = data['GustAngle'] - elif self.type == 'gustangle': - if data['GustAngle'] >= 330: - self._state = "N (%d\xb0)" % data['GustAngle'] - elif data['GustAngle'] >= 300: - self._state = "NW (%d\xb0)" % data['GustAngle'] - elif data['GustAngle'] >= 240: - self._state = "W (%d\xb0)" % data['GustAngle'] - elif data['GustAngle'] >= 210: - self._state = "SW (%d\xb0)" % data['GustAngle'] - elif data['GustAngle'] >= 150: - self._state = "S (%d\xb0)" % data['GustAngle'] - elif data['GustAngle'] >= 120: - self._state = "SE (%d\xb0)" % data['GustAngle'] - elif data['GustAngle'] >= 60: - self._state = "E (%d\xb0)" % data['GustAngle'] - elif data['GustAngle'] >= 30: - self._state = "NE (%d\xb0)" % data['GustAngle'] - elif data['GustAngle'] >= 0: - self._state = "N (%d\xb0)" % data['GustAngle'] - elif self.type == 'guststrength': - self._state = data['GustStrength'] - elif self.type == 'rf_status_lvl': - self._state = data['rf_status'] - elif self.type == 'rf_status': - if data['rf_status'] >= 90: - self._state = "Low" - elif data['rf_status'] >= 76: - self._state = "Medium" - elif data['rf_status'] >= 60: - self._state = "High" - elif data['rf_status'] <= 59: - self._state = "Full" - elif self.type == 'wifi_status_lvl': - self._state = data['wifi_status'] - elif self.type == 'wifi_status': - if data['wifi_status'] >= 86: - self._state = "Low" - elif data['wifi_status'] >= 71: - self._state = "Medium" - elif data['wifi_status'] >= 56: - self._state = "High" - elif data['wifi_status'] <= 55: - self._state = "Full" + try: + if self.type == 'temperature': + self._state = round(data['Temperature'], 1) + elif self.type == 'humidity': + self._state = data['Humidity'] + elif self.type == 'rain': + self._state = data['Rain'] + elif self.type == 'sum_rain_1': + self._state = data['sum_rain_1'] + elif self.type == 'sum_rain_24': + self._state = data['sum_rain_24'] + elif self.type == 'noise': + self._state = data['Noise'] + elif self.type == 'co2': + self._state = data['CO2'] + elif self.type == 'pressure': + self._state = round(data['Pressure'], 1) + elif self.type == 'battery_percent': + self._state = data['battery_percent'] + elif self.type == 'battery_lvl': + self._state = data['battery_vp'] + elif (self.type == 'battery_vp' and + self._module_type == MODULE_TYPE_WIND): + if data['battery_vp'] >= 5590: + self._state = "Full" + elif data['battery_vp'] >= 5180: + self._state = "High" + elif data['battery_vp'] >= 4770: + self._state = "Medium" + elif data['battery_vp'] >= 4360: + self._state = "Low" + elif data['battery_vp'] < 4360: + self._state = "Very Low" + elif (self.type == 'battery_vp' and + self._module_type == MODULE_TYPE_RAIN): + if data['battery_vp'] >= 5500: + self._state = "Full" + elif data['battery_vp'] >= 5000: + self._state = "High" + elif data['battery_vp'] >= 4500: + self._state = "Medium" + elif data['battery_vp'] >= 4000: + self._state = "Low" + elif data['battery_vp'] < 4000: + self._state = "Very Low" + elif (self.type == 'battery_vp' and + self._module_type == MODULE_TYPE_INDOOR): + if data['battery_vp'] >= 5640: + self._state = "Full" + elif data['battery_vp'] >= 5280: + self._state = "High" + elif data['battery_vp'] >= 4920: + self._state = "Medium" + elif data['battery_vp'] >= 4560: + self._state = "Low" + elif data['battery_vp'] < 4560: + self._state = "Very Low" + elif (self.type == 'battery_vp' and + self._module_type == MODULE_TYPE_OUTDOOR): + if data['battery_vp'] >= 5500: + self._state = "Full" + elif data['battery_vp'] >= 5000: + self._state = "High" + elif data['battery_vp'] >= 4500: + self._state = "Medium" + elif data['battery_vp'] >= 4000: + self._state = "Low" + elif data['battery_vp'] < 4000: + self._state = "Very Low" + elif self.type == 'min_temp': + self._state = data['min_temp'] + elif self.type == 'max_temp': + self._state = data['max_temp'] + elif self.type == 'windangle_value': + self._state = data['WindAngle'] + elif self.type == 'windangle': + if data['WindAngle'] >= 330: + self._state = "N (%d\xb0)" % data['WindAngle'] + elif data['WindAngle'] >= 300: + self._state = "NW (%d\xb0)" % data['WindAngle'] + elif data['WindAngle'] >= 240: + self._state = "W (%d\xb0)" % data['WindAngle'] + elif data['WindAngle'] >= 210: + self._state = "SW (%d\xb0)" % data['WindAngle'] + elif data['WindAngle'] >= 150: + self._state = "S (%d\xb0)" % data['WindAngle'] + elif data['WindAngle'] >= 120: + self._state = "SE (%d\xb0)" % data['WindAngle'] + elif data['WindAngle'] >= 60: + self._state = "E (%d\xb0)" % data['WindAngle'] + elif data['WindAngle'] >= 30: + self._state = "NE (%d\xb0)" % data['WindAngle'] + elif data['WindAngle'] >= 0: + self._state = "N (%d\xb0)" % data['WindAngle'] + elif self.type == 'windstrength': + self._state = data['WindStrength'] + elif self.type == 'gustangle_value': + self._state = data['GustAngle'] + elif self.type == 'gustangle': + if data['GustAngle'] >= 330: + self._state = "N (%d\xb0)" % data['GustAngle'] + elif data['GustAngle'] >= 300: + self._state = "NW (%d\xb0)" % data['GustAngle'] + elif data['GustAngle'] >= 240: + self._state = "W (%d\xb0)" % data['GustAngle'] + elif data['GustAngle'] >= 210: + self._state = "SW (%d\xb0)" % data['GustAngle'] + elif data['GustAngle'] >= 150: + self._state = "S (%d\xb0)" % data['GustAngle'] + elif data['GustAngle'] >= 120: + self._state = "SE (%d\xb0)" % data['GustAngle'] + elif data['GustAngle'] >= 60: + self._state = "E (%d\xb0)" % data['GustAngle'] + elif data['GustAngle'] >= 30: + self._state = "NE (%d\xb0)" % data['GustAngle'] + elif data['GustAngle'] >= 0: + self._state = "N (%d\xb0)" % data['GustAngle'] + elif self.type == 'guststrength': + self._state = data['GustStrength'] + elif self.type == 'rf_status_lvl': + self._state = data['rf_status'] + elif self.type == 'rf_status': + if data['rf_status'] >= 90: + self._state = "Low" + elif data['rf_status'] >= 76: + self._state = "Medium" + elif data['rf_status'] >= 60: + self._state = "High" + elif data['rf_status'] <= 59: + self._state = "Full" + elif self.type == 'wifi_status_lvl': + self._state = data['wifi_status'] + elif self.type == 'wifi_status': + if data['wifi_status'] >= 86: + self._state = "Low" + elif data['wifi_status'] >= 71: + self._state = "Medium" + elif data['wifi_status'] >= 56: + self._state = "High" + elif data['wifi_status'] <= 55: + self._state = "Full" + except KeyError: + _LOGGER.error("No %s data found for %s", self.type, + self.module_name) + self._state = None + return class NetAtmoData: @@ -360,10 +360,14 @@ class NetAtmoData: self.data = self.station_data.lastData(exclude=3600) newinterval = 0 - for module in self.data: - if 'When' in self.data[module]: - newinterval = self.data[module]['When'] - break + try: + for module in self.data: + if 'When' in self.data[module]: + newinterval = self.data[module]['When'] + break + except TypeError: + _LOGGER.error("No modules found!") + if newinterval: # Try and estimate when fresh data will be available newinterval += NETATMO_UPDATE_INTERVAL - time() diff --git a/homeassistant/components/netgear_lte.py b/homeassistant/components/netgear_lte/__init__.py similarity index 94% rename from homeassistant/components/netgear_lte.py rename to homeassistant/components/netgear_lte/__init__.py index 7658015ea67..5f8c680b7f0 100644 --- a/homeassistant/components/netgear_lte.py +++ b/homeassistant/components/netgear_lte/__init__.py @@ -1,16 +1,11 @@ -""" -Support for Netgear LTE modems. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/netgear_lte/ -""" +"""Support for Netgear LTE modems.""" import asyncio from datetime import timedelta import logging -import voluptuous as vol -import attr import aiohttp +import attr +import voluptuous as vol from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP) @@ -144,7 +139,7 @@ async def _retry_login(hass, modem_data, password): import eternalegypt _LOGGER.warning( - "Could not connect to %s. Will keep trying.", modem_data.host) + "Could not connect to %s. Will keep trying", modem_data.host) modem_data.connected = False delay = 15 diff --git a/homeassistant/components/notify/netgear_lte.py b/homeassistant/components/netgear_lte/notify.py similarity index 85% rename from homeassistant/components/notify/netgear_lte.py rename to homeassistant/components/netgear_lte/notify.py index 9ba804e193d..20a20b21291 100644 --- a/homeassistant/components/notify/netgear_lte.py +++ b/homeassistant/components/netgear_lte/notify.py @@ -1,22 +1,16 @@ -"""Netgear LTE platform for notify component. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/notify.netgear_lte/ -""" - +"""Suport for Netgear LTE notifications.""" import logging -import voluptuous as vol import attr +import voluptuous as vol from homeassistant.components.notify import ( - BaseNotificationService, ATTR_TARGET, PLATFORM_SCHEMA) + ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService) from homeassistant.const import CONF_HOST import homeassistant.helpers.config_validation as cv from ..netgear_lte import DATA_KEY - DEPENDENCIES = ['netgear_lte'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/netgear_lte.py b/homeassistant/components/netgear_lte/sensor.py similarity index 91% rename from homeassistant/components/sensor/netgear_lte.py rename to homeassistant/components/netgear_lte/sensor.py index 3c17750d6ad..339fa678d61 100644 --- a/homeassistant/components/sensor/netgear_lte.py +++ b/homeassistant/components/netgear_lte/sensor.py @@ -1,17 +1,12 @@ -"""Netgear LTE sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.netgear_lte/ -""" - -import voluptuous as vol +"""Support for Netgear LTE sensors.""" import attr +import voluptuous as vol -from homeassistant.const import CONF_HOST, CONF_SENSORS from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_HOST, CONF_SENSORS from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity from ..netgear_lte import DATA_KEY @@ -23,7 +18,7 @@ SENSOR_USAGE = 'usage' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HOST): cv.string, vol.Required(CONF_SENSORS): vol.All( - cv.ensure_list, [vol.In([SENSOR_SMS, SENSOR_USAGE])]) + cv.ensure_list, [vol.In([SENSOR_SMS, SENSOR_USAGE])]), }) diff --git a/homeassistant/components/no_ip.py b/homeassistant/components/no_ip/__init__.py similarity index 94% rename from homeassistant/components/no_ip.py rename to homeassistant/components/no_ip/__init__.py index beb11ed738f..6a714747484 100644 --- a/homeassistant/components/no_ip.py +++ b/homeassistant/components/no_ip/__init__.py @@ -1,9 +1,4 @@ -""" -Integrate with NO-IP Dynamic DNS service. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/no_ip/ -""" +"""Integrate with NO-IP Dynamic DNS service.""" import asyncio import base64 from datetime import timedelta diff --git a/homeassistant/components/notify/pushover.py b/homeassistant/components/notify/pushover.py index 3ec0b27e7c4..b249ca804b3 100644 --- a/homeassistant/components/notify/pushover.py +++ b/homeassistant/components/notify/pushover.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_TITLE, ATTR_TITLE_DEFAULT, ATTR_TARGET, ATTR_DATA, - BaseNotificationService) + BaseNotificationService, PLATFORM_SCHEMA) from homeassistant.const import CONF_API_KEY import homeassistant.helpers.config_validation as cv @@ -20,7 +20,7 @@ _LOGGER = logging.getLogger(__name__) CONF_USER_KEY = 'user_key' -PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_USER_KEY): cv.string, vol.Required(CONF_API_KEY): cv.string, }) diff --git a/homeassistant/components/notify/xmpp.py b/homeassistant/components/notify/xmpp.py index eac20c62797..462dd007d53 100644 --- a/homeassistant/components/notify/xmpp.py +++ b/homeassistant/components/notify/xmpp.py @@ -21,7 +21,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv import homeassistant.helpers.template as template_helper -REQUIREMENTS = ['slixmpp==1.4.1'] +REQUIREMENTS = ['slixmpp==1.4.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/nuheat.py b/homeassistant/components/nuheat/__init__.py similarity index 84% rename from homeassistant/components/nuheat.py rename to homeassistant/components/nuheat/__init__.py index fb14f119dbd..4ea37339ef3 100644 --- a/homeassistant/components/nuheat.py +++ b/homeassistant/components/nuheat/__init__.py @@ -1,9 +1,4 @@ -""" -Support for NuHeat thermostats. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/nuheat/ -""" +"""Support for NuHeat thermostats.""" import logging import voluptuous as vol @@ -16,7 +11,7 @@ REQUIREMENTS = ["nuheat==0.3.0"] _LOGGER = logging.getLogger(__name__) -DOMAIN = "nuheat" +DOMAIN = 'nuheat' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ diff --git a/homeassistant/components/nuimo_controller.py b/homeassistant/components/nuimo_controller/__init__.py similarity index 96% rename from homeassistant/components/nuimo_controller.py rename to homeassistant/components/nuimo_controller/__init__.py index 0f8fbb39073..70509469d2b 100644 --- a/homeassistant/components/nuimo_controller.py +++ b/homeassistant/components/nuimo_controller/__init__.py @@ -1,9 +1,4 @@ -""" -Component that connects to a Nuimo device over Bluetooth LE. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/nuimo_controller/ -""" +"""Support for Nuimo device over Bluetooth LE.""" import logging import threading import time diff --git a/homeassistant/components/octoprint.py b/homeassistant/components/octoprint/__init__.py similarity index 98% rename from homeassistant/components/octoprint.py rename to homeassistant/components/octoprint/__init__.py index 869f3bd7d6e..35740a7be0d 100644 --- a/homeassistant/components/octoprint.py +++ b/homeassistant/components/octoprint/__init__.py @@ -1,9 +1,4 @@ -""" -Support for monitoring OctoPrint 3D printers. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/octoprint/ -""" +"""Support for monitoring OctoPrint 3D printers.""" import logging import time @@ -23,10 +18,11 @@ from homeassistant.util import slugify as util_slugify _LOGGER = logging.getLogger(__name__) -DOMAIN = 'octoprint' -CONF_NUMBER_OF_TOOLS = 'number_of_tools' CONF_BED = 'bed' +CONF_NUMBER_OF_TOOLS = 'number_of_tools' + DEFAULT_NAME = 'OctoPrint' +DOMAIN = 'octoprint' def has_all_unique_names(value): diff --git a/homeassistant/components/binary_sensor/octoprint.py b/homeassistant/components/octoprint/binary_sensor.py similarity index 92% rename from homeassistant/components/binary_sensor/octoprint.py rename to homeassistant/components/octoprint/binary_sensor.py index 285495c03a0..cb860177796 100644 --- a/homeassistant/components/binary_sensor/octoprint.py +++ b/homeassistant/components/octoprint/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for monitoring OctoPrint binary sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.octoprint/ -""" +"""Support for monitoring OctoPrint binary sensors.""" import logging import requests diff --git a/homeassistant/components/sensor/octoprint.py b/homeassistant/components/octoprint/sensor.py similarity index 95% rename from homeassistant/components/sensor/octoprint.py rename to homeassistant/components/octoprint/sensor.py index 8170b97c4c8..2df307f02ef 100644 --- a/homeassistant/components/sensor/octoprint.py +++ b/homeassistant/components/octoprint/sensor.py @@ -1,9 +1,4 @@ -""" -Support for monitoring OctoPrint sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.octoprint/ -""" +"""Support for monitoring OctoPrint sensors.""" import logging import requests @@ -16,6 +11,7 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['octoprint'] + NOTIFICATION_ID = 'octoprint_notification' NOTIFICATION_TITLE = 'OctoPrint sensor setup error' diff --git a/homeassistant/components/onboarding/__init__.py b/homeassistant/components/onboarding/__init__.py index 25aca9f8afa..6bbe546dcb1 100644 --- a/homeassistant/components/onboarding/__init__.py +++ b/homeassistant/components/onboarding/__init__.py @@ -1,4 +1,4 @@ -"""Component to help onboard new users.""" +"""Support to help onboard new users.""" from homeassistant.core import callback from homeassistant.loader import bind_hass @@ -19,8 +19,8 @@ def async_is_onboarded(hass): async def async_setup(hass, config): """Set up the onboarding component.""" - store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY, - private=True) + store = hass.helpers.storage.Store( + STORAGE_VERSION, STORAGE_KEY, private=True) data = await store.async_load() if data is None: diff --git a/homeassistant/components/onboarding/views.py b/homeassistant/components/onboarding/views.py index 497fa827f08..804589200fa 100644 --- a/homeassistant/components/onboarding/views.py +++ b/homeassistant/components/onboarding/views.py @@ -94,6 +94,10 @@ class UserOnboardingView(_BaseOnboardingView): }) await provider.data.async_save() await hass.auth.async_link_user(user, credentials) + if 'person' in hass.config.components: + await hass.components.person.async_create_person( + data['name'], user_id=user.id + ) await self._async_mark_done(hass) diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index cf74aae7577..7676806cfdf 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -1,9 +1,4 @@ -""" -Support for OpenTherm Gateway devices. - -For more details about this component, please refer to the documentation at -http://home-assistant.io/components/opentherm_gw/ -""" +"""Support for OpenTherm Gateway devices.""" import logging from datetime import datetime, date @@ -20,6 +15,10 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send import homeassistant.helpers.config_validation as cv +REQUIREMENTS = ['pyotgw==0.4b1'] + +_LOGGER = logging.getLogger(__name__) + DOMAIN = 'opentherm_gw' ATTR_MODE = 'mode' @@ -104,10 +103,6 @@ CONFIG_SCHEMA = vol.Schema({ }), }, extra=vol.ALLOW_EXTRA) -REQUIREMENTS = ['pyotgw==0.4b1'] - -_LOGGER = logging.getLogger(__name__) - async def async_setup(hass, config): """Set up the OpenTherm Gateway component.""" diff --git a/homeassistant/components/binary_sensor/opentherm_gw.py b/homeassistant/components/opentherm_gw/binary_sensor.py similarity index 94% rename from homeassistant/components/binary_sensor/opentherm_gw.py rename to homeassistant/components/opentherm_gw/binary_sensor.py index 8c5ff8c44d1..b35998c807b 100644 --- a/homeassistant/components/binary_sensor/opentherm_gw.py +++ b/homeassistant/components/opentherm_gw/binary_sensor.py @@ -1,9 +1,4 @@ -""" -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/ -""" +"""Support for OpenTherm Gateway binary sensors.""" import logging from homeassistant.components.binary_sensor import ( @@ -13,17 +8,17 @@ from homeassistant.components.opentherm_gw import ( from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import async_generate_entity_id +_LOGGER = logging.getLogger(__name__) + 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): +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 diff --git a/homeassistant/components/climate/opentherm_gw.py b/homeassistant/components/opentherm_gw/climate.py similarity index 94% rename from homeassistant/components/climate/opentherm_gw.py rename to homeassistant/components/opentherm_gw/climate.py index 6dc52e6acc7..ff6acc1a884 100644 --- a/homeassistant/components/climate/opentherm_gw.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -1,9 +1,4 @@ -""" -Support for OpenTherm Gateway climate devices. - -For more details about this platform, please refer to the documentation at -http://home-assistant.io/components/climate.opentherm_gw/ -""" +"""Support for OpenTherm Gateway climate devices.""" import logging from homeassistant.components.climate import (ClimateDevice, STATE_IDLE, @@ -17,14 +12,15 @@ from homeassistant.const import (ATTR_TEMPERATURE, CONF_NAME, PRECISION_HALVES, TEMP_CELSIUS) from homeassistant.helpers.dispatcher import async_dispatcher_connect -DEPENDENCIES = ['opentherm_gw'] - -SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE) _LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['opentherm_gw'] -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the opentherm_gw device.""" gateway = OpenThermGateway(hass, discovery_info) async_add_entities([gateway]) diff --git a/homeassistant/components/sensor/opentherm_gw.py b/homeassistant/components/opentherm_gw/sensor.py similarity index 96% rename from homeassistant/components/sensor/opentherm_gw.py rename to homeassistant/components/opentherm_gw/sensor.py index 9ae557654ce..070f847e5e5 100644 --- a/homeassistant/components/sensor/opentherm_gw.py +++ b/homeassistant/components/opentherm_gw/sensor.py @@ -1,9 +1,4 @@ -""" -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/ -""" +"""Support for OpenTherm Gateway sensors.""" import logging from homeassistant.components.opentherm_gw import ( @@ -13,6 +8,8 @@ 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 +_LOGGER = logging.getLogger(__name__) + UNIT_BAR = 'bar' UNIT_HOUR = 'h' UNIT_KW = 'kW' @@ -21,11 +18,9 @@ UNIT_PERCENT = '%' DEPENDENCIES = ['opentherm_gw'] -_LOGGER = logging.getLogger(__name__) - -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +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 @@ -163,7 +158,7 @@ class OpenThermSensor(Entity): """Representation of an OpenTherm Gateway sensor.""" def __init__(self, entity_id, var, device_class, unit, friendly_name): - """Initialize the sensor.""" + """Initialize the OpenTherm Gateway sensor.""" self.entity_id = entity_id self._var = var self._value = None diff --git a/homeassistant/components/openuv/.translations/da.json b/homeassistant/components/openuv/.translations/da.json index 5cda5c6e663..a783c8646e0 100644 --- a/homeassistant/components/openuv/.translations/da.json +++ b/homeassistant/components/openuv/.translations/da.json @@ -1,9 +1,14 @@ { "config": { + "error": { + "identifier_exists": "Koordinater er allerede registreret", + "invalid_api_key": "Ugyldig API n\u00f8gle" + }, "step": { "user": { "data": { "api_key": "OpenUV API N\u00f8gle", + "elevation": "Elevation", "latitude": "Breddegrad", "longitude": "L\u00e6ngdegrad" }, diff --git a/homeassistant/components/openuv/.translations/ko.json b/homeassistant/components/openuv/.translations/ko.json index bb054f0b3a6..5e06be81d31 100644 --- a/homeassistant/components/openuv/.translations/ko.json +++ b/homeassistant/components/openuv/.translations/ko.json @@ -12,7 +12,7 @@ "latitude": "\uc704\ub3c4", "longitude": "\uacbd\ub3c4" }, - "title": "\uc0ac\uc6a9\uc790 \uc815\ubcf4\ub97c \uc785\ub825\ud574 \uc8fc\uc138\uc694" + "title": "\uc0ac\uc6a9\uc790 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694" } }, "title": "OpenUV" diff --git a/homeassistant/components/openuv/.translations/pl.json b/homeassistant/components/openuv/.translations/pl.json index f6c52ffd04e..2c4c47e8da4 100644 --- a/homeassistant/components/openuv/.translations/pl.json +++ b/homeassistant/components/openuv/.translations/pl.json @@ -12,7 +12,7 @@ "latitude": "Szeroko\u015b\u0107 geograficzna", "longitude": "D\u0142ugo\u015b\u0107 geograficzna" }, - "title": "Wpisz swoje informacje" + "title": "Wprowad\u017a swoje dane" } }, "title": "OpenUV" diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index 32c3da0d3e5..52383366c4d 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -1,9 +1,4 @@ -""" -Support for UV data from openuv.io. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/openuv/ -""" +"""Support for UV data from openuv.io.""" import logging import voluptuous as vol @@ -11,8 +6,7 @@ import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_API_KEY, CONF_BINARY_SENSORS, CONF_ELEVATION, - CONF_LATITUDE, CONF_LONGITUDE, CONF_MONITORED_CONDITIONS, - CONF_SENSORS) + CONF_LATITUDE, CONF_LONGITUDE, CONF_MONITORED_CONDITIONS, 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 @@ -22,6 +16,7 @@ from .config_flow import configured_instances from .const import DOMAIN REQUIREMENTS = ['pyopenuv==1.0.4'] + _LOGGER = logging.getLogger(__name__) DATA_OPENUV_CLIENT = 'data_client' diff --git a/homeassistant/components/openuv/binary_sensor.py b/homeassistant/components/openuv/binary_sensor.py index 3e9bb0b0bc3..b790427b228 100644 --- a/homeassistant/components/openuv/binary_sensor.py +++ b/homeassistant/components/openuv/binary_sensor.py @@ -1,26 +1,21 @@ -""" -This platform provides binary sensors for OpenUV data. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.openuv/ -""" +"""Support for OpenUV binary sensors.""" import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.components.openuv import ( BINARY_SENSORS, DATA_OPENUV_CLIENT, DATA_PROTECTION_WINDOW, DOMAIN, TOPIC_UPDATE, TYPE_PROTECTION_WINDOW, OpenUvEntity) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util.dt import as_local, parse_datetime, utcnow -DEPENDENCIES = ['openuv'] _LOGGER = logging.getLogger(__name__) - -ATTR_PROTECTION_WINDOW_STARTING_TIME = 'start_time' -ATTR_PROTECTION_WINDOW_STARTING_UV = 'start_uv' ATTR_PROTECTION_WINDOW_ENDING_TIME = 'end_time' ATTR_PROTECTION_WINDOW_ENDING_UV = 'end_uv' +ATTR_PROTECTION_WINDOW_STARTING_TIME = 'start_time' +ATTR_PROTECTION_WINDOW_STARTING_UV = 'start_uv' + +DEPENDENCIES = ['openuv'] async def async_setup_platform( diff --git a/homeassistant/components/openuv/config_flow.py b/homeassistant/components/openuv/config_flow.py index 0f566e5a9ef..7150a8499d8 100644 --- a/homeassistant/components/openuv/config_flow.py +++ b/homeassistant/components/openuv/config_flow.py @@ -3,9 +3,9 @@ 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) +from homeassistant.core import callback from homeassistant.helpers import aiohttp_client, config_validation as cv from .const import DOMAIN diff --git a/homeassistant/components/openuv/sensor.py b/homeassistant/components/openuv/sensor.py index 63527db42a6..489a100a5e5 100644 --- a/homeassistant/components/openuv/sensor.py +++ b/homeassistant/components/openuv/sensor.py @@ -1,24 +1,20 @@ -""" -This platform provides sensors for OpenUV data. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.openuv/ -""" +"""Support for OpenUV sensors.""" import logging -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.components.openuv import ( DATA_OPENUV_CLIENT, DATA_UV, DOMAIN, SENSORS, TOPIC_UPDATE, TYPE_CURRENT_OZONE_LEVEL, TYPE_CURRENT_UV_INDEX, TYPE_CURRENT_UV_LEVEL, TYPE_MAX_UV_INDEX, TYPE_SAFE_EXPOSURE_TIME_1, TYPE_SAFE_EXPOSURE_TIME_2, TYPE_SAFE_EXPOSURE_TIME_3, TYPE_SAFE_EXPOSURE_TIME_4, TYPE_SAFE_EXPOSURE_TIME_5, TYPE_SAFE_EXPOSURE_TIME_6, OpenUvEntity) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util.dt import as_local, parse_datetime -DEPENDENCIES = ['openuv'] _LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['openuv'] + ATTR_MAX_UV_TIME = 'time' EXPOSURE_TYPE_MAP = { @@ -30,11 +26,11 @@ EXPOSURE_TYPE_MAP = { TYPE_SAFE_EXPOSURE_TIME_6: 'st6' } -UV_LEVEL_EXTREME = "Extreme" -UV_LEVEL_VHIGH = "Very High" -UV_LEVEL_HIGH = "High" -UV_LEVEL_MODERATE = "Moderate" -UV_LEVEL_LOW = "Low" +UV_LEVEL_EXTREME = 'Extreme' +UV_LEVEL_VHIGH = 'Very High' +UV_LEVEL_HIGH = 'High' +UV_LEVEL_MODERATE = 'Moderate' +UV_LEVEL_LOW = 'Low' async def async_setup_platform( diff --git a/homeassistant/components/owntracks/.translations/da.json b/homeassistant/components/owntracks/.translations/da.json new file mode 100644 index 00000000000..7f4053f8ead --- /dev/null +++ b/homeassistant/components/owntracks/.translations/da.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning" + }, + "create_entry": { + "default": "\n\n P\u00e5 Android skal du \u00e5bne [OwnTracks applikationen]({android_url}), g\u00e5 til indstillinger -> forbindelse. Skift f\u00f8lgende indstillinger: \n - Tilstand: Privat HTTP\n - V\u00e6rt: {webhook_url}\n - Identifikation:\n - Brugernavn: ` ` \n - Enheds-id: ` ` \n\n P\u00e5 iOS skal du \u00e5bne [OwnTracks applikationen]({ios_url}), tryk p\u00e5 (i) ikonet \u00f8verst til venstre -> indstillinger. Skift f\u00f8lgende indstillinger: \n - Tilstand: HTTP\n - URL: {webhook_url}\n - Aktiver godkendelse \n - Bruger ID: ` ` \n\n {secret}\n \n Se [dokumentationen]({docs_url}) for at f\u00e5 flere oplysninger." + }, + "step": { + "user": { + "description": "Er du sikker p\u00e5 at du vil konfigurere OwnTracks?", + "title": "Konfigurer OwnTracks" + } + }, + "title": "OwnTracks" + } +} \ No newline at end of file diff --git a/homeassistant/components/owntracks/.translations/ko.json b/homeassistant/components/owntracks/.translations/ko.json index ba264ad4b47..d70ca8b114e 100644 --- a/homeassistant/components/owntracks/.translations/ko.json +++ b/homeassistant/components/owntracks/.translations/ko.json @@ -4,7 +4,7 @@ "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." }, "create_entry": { - "default": "\n\nAndroid \uc778 \uacbd\uc6b0, [OwnTracks \uc571]({android_url}) \uc744 \uc5f4\uace0 preferences -> connection \uc73c\ub85c \uc774\ub3d9\ud558\uc5ec \ub2e4\uc74c\uacfc \uac19\uc774 \uc124\uc815\ud574\uc8fc\uc138\uc694:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\niOS \uc778 \uacbd\uc6b0, [OwnTracks \uc571]({ios_url}) \uc744 \uc5f4\uace0 \uc67c\ucabd \uc0c1\ub2e8\uc758 (i) \uc544\uc774\ucf58\uc744 \ud0ed\ud558\uc5ec \uc124\uc815\uc73c\ub85c \uc774\ub3d9\ud558\uc5ec \ub2e4\uc74c\uacfc \uac19\uc774 \uc124\uc815\ud574\uc8fc\uc138\uc694:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ``\n\n{secret} \n \n\uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574 \uc8fc\uc138\uc694." + "default": "\n\nAndroid \uc778 \uacbd\uc6b0, [OwnTracks \uc571]({android_url}) \uc744 \uc5f4\uace0 preferences -> connection \uc73c\ub85c \uc774\ub3d9\ud558\uc5ec \ub2e4\uc74c\uacfc \uac19\uc774 \uc124\uc815\ud574\uc8fc\uc138\uc694:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\niOS \uc778 \uacbd\uc6b0, [OwnTracks \uc571]({ios_url}) \uc744 \uc5f4\uace0 \uc67c\ucabd \uc0c1\ub2e8\uc758 (i) \uc544\uc774\ucf58\uc744 \ud0ed\ud558\uc5ec \uc124\uc815\uc73c\ub85c \uc774\ub3d9\ud558\uc5ec \ub2e4\uc74c\uacfc \uac19\uc774 \uc124\uc815\ud574\uc8fc\uc138\uc694:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ``\n\n{secret} \n \n\uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "step": { "user": { diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index d0ba27aeddd..cc918dcf674 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -1,4 +1,4 @@ -"""Component for OwnTracks.""" +"""Support for OwnTracks.""" from collections import defaultdict import json import logging @@ -8,16 +8,19 @@ from aiohttp.web import json_response import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import mqtt from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.core import callback -from homeassistant.components import mqtt -from homeassistant.setup import async_when_setup import homeassistant.helpers.config_validation as cv +from homeassistant.setup import async_when_setup from .config_flow import CONF_SECRET -DOMAIN = "owntracks" REQUIREMENTS = ['libnacl==1.6.1'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'owntracks' DEPENDENCIES = ['webhook'] CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy' @@ -47,8 +50,6 @@ CONFIG_SCHEMA = vol.Schema({ } }, extra=vol.ALLOW_EXTRA) -_LOGGER = logging.getLogger(__name__) - async def async_setup(hass, config): """Initialize OwnTracks component.""" diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/owntracks/device_tracker.py similarity index 100% rename from homeassistant/components/device_tracker/owntracks.py rename to homeassistant/components/owntracks/device_tracker.py diff --git a/homeassistant/components/panel_custom.py b/homeassistant/components/panel_custom/__init__.py similarity index 94% rename from homeassistant/components/panel_custom.py rename to homeassistant/components/panel_custom/__init__.py index 740a28a9dec..f6602169eb2 100644 --- a/homeassistant/components/panel_custom.py +++ b/homeassistant/components/panel_custom/__init__.py @@ -1,16 +1,13 @@ -""" -Register a custom front end panel. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/panel_custom/ -""" +"""Register a custom front end panel.""" import logging import os import voluptuous as vol -from homeassistant.loader import bind_hass import homeassistant.helpers.config_validation as cv +from homeassistant.loader import bind_hass + +_LOGGER = logging.getLogger(__name__) DOMAIN = 'panel_custom' DEPENDENCIES = ['frontend'] @@ -58,8 +55,6 @@ CONFIG_SCHEMA = vol.Schema({ })]) }, extra=vol.ALLOW_EXTRA) -_LOGGER = logging.getLogger(__name__) - @bind_hass async def async_register_panel( @@ -154,8 +149,8 @@ async def async_setup(hass, config): kwargs['module_url'] = panel[CONF_MODULE_URL] elif not await hass.async_add_job(os.path.isfile, panel_path): - _LOGGER.error('Unable to find webcomponent for %s: %s', - name, panel_path) + _LOGGER.error( + "Unable to find webcomponent for %s: %s", name, panel_path) continue else: diff --git a/homeassistant/components/panel_iframe.py b/homeassistant/components/panel_iframe/__init__.py similarity index 82% rename from homeassistant/components/panel_iframe.py rename to homeassistant/components/panel_iframe/__init__.py index 030fbbf9324..b82f9fa9789 100644 --- a/homeassistant/components/panel_iframe.py +++ b/homeassistant/components/panel_iframe/__init__.py @@ -1,12 +1,7 @@ -""" -Register an iFrame front end panel. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/panel_iframe/ -""" +"""Register an iFrame front end panel.""" import voluptuous as vol -from homeassistant.const import (CONF_ICON, CONF_URL) +from homeassistant.const import CONF_ICON, CONF_URL import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['frontend'] diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index d38501b9b07..0a648f6eff7 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -1,21 +1,16 @@ -""" -A component which is collecting configuration errors. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/persistent_notification/ -""" -import logging +"""Support for displaying persistent notifications.""" from collections import OrderedDict +import logging from typing import Awaitable import voluptuous as vol from homeassistant.components import websocket_api -from homeassistant.core import callback, HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError -from homeassistant.loader import bind_hass from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.loader import bind_hass from homeassistant.util import slugify import homeassistant.util.dt as dt_util diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py new file mode 100644 index 00000000000..6fb7d42e0ee --- /dev/null +++ b/homeassistant/components/person/__init__.py @@ -0,0 +1,482 @@ +"""Support for tracking people.""" +from collections import OrderedDict +from itertools import chain +import logging +import uuid + +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.components.device_tracker import ( + DOMAIN as DEVICE_TRACKER_DOMAIN) +from homeassistant.const import ( + ATTR_ID, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_ID, CONF_NAME, + EVENT_HOMEASSISTANT_START, STATE_UNKNOWN, STATE_UNAVAILABLE) +from homeassistant.core import callback, Event +from homeassistant.auth import EVENT_USER_REMOVED +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.storage import Store +from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.loader import bind_hass + +_LOGGER = logging.getLogger(__name__) + +ATTR_EDITABLE = 'editable' +ATTR_SOURCE = 'source' +ATTR_USER_ID = 'user_id' + +CONF_DEVICE_TRACKERS = 'device_trackers' +CONF_USER_ID = 'user_id' + +DOMAIN = 'person' + +STORAGE_KEY = DOMAIN +STORAGE_VERSION = 1 +SAVE_DELAY = 10 +# Device tracker states to ignore +IGNORE_STATES = (STATE_UNKNOWN, STATE_UNAVAILABLE) + +PERSON_SCHEMA = vol.Schema({ + vol.Required(CONF_ID): cv.string, + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_USER_ID): cv.string, + vol.Optional(CONF_DEVICE_TRACKERS, default=[]): vol.All( + cv.ensure_list, cv.entities_domain(DEVICE_TRACKER_DOMAIN)), +}) + +CONFIG_SCHEMA = vol.Schema({ + vol.Optional(DOMAIN): vol.Any(vol.All(cv.ensure_list, [PERSON_SCHEMA]), {}) +}, extra=vol.ALLOW_EXTRA) + +_UNDEF = object() + + +@bind_hass +async def async_create_person(hass, name, *, user_id=None, + device_trackers=None): + """Create a new person.""" + await hass.data[DOMAIN].async_create_person( + name=name, + user_id=user_id, + device_trackers=device_trackers, + ) + + +class PersonManager: + """Manage person data.""" + + def __init__(self, hass: HomeAssistantType, component: EntityComponent, + config_persons): + """Initialize person storage.""" + self.hass = hass + self.component = component + self.store = Store(hass, STORAGE_VERSION, STORAGE_KEY) + self.storage_data = None + + config_data = self.config_data = OrderedDict() + for conf in config_persons: + person_id = conf[CONF_ID] + + if person_id in config_data: + _LOGGER.error( + "Found config user with duplicate ID: %s", person_id) + continue + + config_data[person_id] = conf + + @property + def storage_persons(self): + """Iterate over persons stored in storage.""" + return list(self.storage_data.values()) + + @property + def config_persons(self): + """Iterate over persons stored in config.""" + return list(self.config_data.values()) + + async def async_initialize(self): + """Get the person data.""" + raw_storage = await self.store.async_load() + + if raw_storage is None: + raw_storage = { + 'persons': [] + } + + storage_data = self.storage_data = OrderedDict() + + for person in raw_storage['persons']: + storage_data[person[CONF_ID]] = person + + entities = [] + seen_users = set() + + for person_conf in self.config_data.values(): + person_id = person_conf[CONF_ID] + user_id = person_conf.get(CONF_USER_ID) + + if user_id is not None: + if await self.hass.auth.async_get_user(user_id) is None: + _LOGGER.error( + "Invalid user_id detected for person %s", person_id) + continue + + if user_id in seen_users: + _LOGGER.error( + "Duplicate user_id %s detected for person %s", + user_id, person_id) + continue + + seen_users.add(user_id) + + entities.append(Person(person_conf, False)) + + # To make sure IDs don't overlap between config/storage + seen_persons = set(self.config_data) + + for person_conf in storage_data.values(): + person_id = person_conf[CONF_ID] + user_id = person_conf[CONF_USER_ID] + + if person_id in seen_persons: + _LOGGER.error( + "Skipping adding person from storage with same ID as" + " configuration.yaml entry: %s", person_id) + continue + + if user_id is not None and user_id in seen_users: + _LOGGER.error( + "Duplicate user_id %s detected for person %s", + user_id, person_id) + continue + + # To make sure all users have just 1 person linked. + seen_users.add(user_id) + + entities.append(Person(person_conf, True)) + + if entities: + await self.component.async_add_entities(entities) + + self.hass.bus.async_listen(EVENT_USER_REMOVED, self._user_removed) + + async def async_create_person( + self, *, name, device_trackers=None, user_id=None): + """Create a new person.""" + if not name: + raise ValueError("Name is required") + + if user_id is not None: + await self._validate_user_id(user_id) + + person = { + CONF_ID: uuid.uuid4().hex, + CONF_NAME: name, + CONF_USER_ID: user_id, + CONF_DEVICE_TRACKERS: device_trackers, + } + self.storage_data[person[CONF_ID]] = person + self._async_schedule_save() + await self.component.async_add_entities([Person(person, True)]) + return person + + async def async_update_person(self, person_id, *, name=_UNDEF, + device_trackers=_UNDEF, user_id=_UNDEF): + """Update person.""" + current = self.storage_data.get(person_id) + + if current is None: + raise ValueError("Invalid person specified.") + + changes = { + key: value for key, value in ( + (CONF_NAME, name), + (CONF_DEVICE_TRACKERS, device_trackers), + (CONF_USER_ID, user_id) + ) if value is not _UNDEF and current[key] != value + } + + if CONF_USER_ID in changes and user_id is not None: + await self._validate_user_id(user_id) + + self.storage_data[person_id].update(changes) + self._async_schedule_save() + + for entity in self.component.entities: + if entity.unique_id == person_id: + entity.person_updated() + break + + return self.storage_data[person_id] + + async def async_delete_person(self, person_id): + """Delete person.""" + if person_id not in self.storage_data: + raise ValueError("Invalid person specified.") + + self.storage_data.pop(person_id) + self._async_schedule_save() + ent_reg = await self.hass.helpers.entity_registry.async_get_registry() + + for entity in self.component.entities: + if entity.unique_id == person_id: + await entity.async_remove() + ent_reg.async_remove(entity.entity_id) + break + + @callback + def _async_schedule_save(self) -> None: + """Schedule saving the area registry.""" + self.store.async_delay_save(self._data_to_save, SAVE_DELAY) + + @callback + def _data_to_save(self) -> dict: + """Return data of area registry to store in a file.""" + return { + 'persons': list(self.storage_data.values()) + } + + async def _validate_user_id(self, user_id): + """Validate the used user_id.""" + if await self.hass.auth.async_get_user(user_id) is None: + raise ValueError("User does not exist") + + if any(person for person + in chain(self.storage_data.values(), + self.config_data.values()) + if person.get(CONF_USER_ID) == user_id): + raise ValueError("User already taken") + + async def _user_removed(self, event: Event): + """Handle event that a person is removed.""" + user_id = event.data['user_id'] + for person in self.storage_data.values(): + if person[CONF_USER_ID] == user_id: + await self.async_update_person( + person_id=person[CONF_ID], + user_id=None + ) + + +async def async_setup(hass: HomeAssistantType, config: ConfigType): + """Set up the person component.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + conf_persons = config.get(DOMAIN, []) + manager = hass.data[DOMAIN] = PersonManager(hass, component, conf_persons) + await manager.async_initialize() + + websocket_api.async_register_command(hass, ws_list_person) + websocket_api.async_register_command(hass, ws_create_person) + websocket_api.async_register_command(hass, ws_update_person) + websocket_api.async_register_command(hass, ws_delete_person) + + return True + + +class Person(RestoreEntity): + """Represent a tracked person.""" + + def __init__(self, config, editable): + """Set up person.""" + self._config = config + self._editable = editable + self._latitude = None + self._longitude = None + self._source = None + self._state = None + self._unsub_track_device = None + + @property + def name(self): + """Return the name of the entity.""" + return self._config[CONF_NAME] + + @property + def should_poll(self): + """Return True if entity has to be polled for state. + + False if entity pushes its state to HA. + """ + return False + + @property + def state(self): + """Return the state of the person.""" + return self._state + + @property + def state_attributes(self): + """Return the state attributes of the person.""" + data = { + ATTR_EDITABLE: self._editable, + ATTR_ID: self.unique_id, + } + if self._latitude is not None: + 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 + user_id = self._config.get(CONF_USER_ID) + if user_id is not None: + data[ATTR_USER_ID] = user_id + return data + + @property + def unique_id(self): + """Return a unique ID for the person.""" + return self._config[CONF_ID] + + async def async_added_to_hass(self): + """Register device trackers.""" + await super().async_added_to_hass() + state = await self.async_get_last_state() + if state: + self._parse_source_state(state) + + @callback + def person_start_hass(now): + self.person_updated() + + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, person_start_hass) + + @callback + def person_updated(self): + """Handle when the config is updated.""" + if self._unsub_track_device is not None: + self._unsub_track_device() + self._unsub_track_device = None + + trackers = self._config.get(CONF_DEVICE_TRACKERS) + + if trackers: + _LOGGER.debug( + "Subscribe to device trackers for %s", self.entity_id) + + self._unsub_track_device = async_track_state_change( + self.hass, trackers, self._async_handle_tracker_update) + + self._update_state() + + @callback + def _async_handle_tracker_update(self, entity, old_state, new_state): + """Handle the device tracker state changes.""" + self._update_state() + + @callback + def _update_state(self): + """Update the state.""" + latest = None + for entity_id in self._config.get(CONF_DEVICE_TRACKERS, []): + state = self.hass.states.get(entity_id) + + if not state or state.state in IGNORE_STATES: + continue + + if latest is None or state.last_updated > latest.last_updated: + latest = state + + if latest: + self._parse_source_state(latest) + else: + self._state = None + self._source = None + self._latitude = None + self._longitude = None + + self.async_schedule_update_ha_state() + + @callback + def _parse_source_state(self, state): + """Parse source state and set person attributes. + + This is a device tracker state or the restored person state. + """ + self._state = state.state + self._source = state.entity_id + self._latitude = state.attributes.get(ATTR_LATITUDE) + self._longitude = state.attributes.get(ATTR_LONGITUDE) + + +@websocket_api.websocket_command({ + vol.Required('type'): 'person/list', +}) +def ws_list_person(hass: HomeAssistantType, + connection: websocket_api.ActiveConnection, msg): + """List persons.""" + manager = hass.data[DOMAIN] # type: PersonManager + connection.send_result(msg['id'], { + 'storage': manager.storage_persons, + 'config': manager.config_persons, + }) + + +@websocket_api.websocket_command({ + vol.Required('type'): 'person/create', + vol.Required('name'): vol.All(str, vol.Length(min=1)), + vol.Optional('user_id'): vol.Any(str, None), + vol.Optional('device_trackers', default=[]): vol.All( + cv.ensure_list, cv.entities_domain(DEVICE_TRACKER_DOMAIN)), +}) +@websocket_api.require_admin +@websocket_api.async_response +async def ws_create_person(hass: HomeAssistantType, + connection: websocket_api.ActiveConnection, msg): + """Create a person.""" + manager = hass.data[DOMAIN] # type: PersonManager + try: + person = await manager.async_create_person( + name=msg['name'], + user_id=msg.get('user_id'), + device_trackers=msg['device_trackers'] + ) + connection.send_result(msg['id'], person) + except ValueError as err: + connection.send_error( + msg['id'], websocket_api.const.ERR_INVALID_FORMAT, str(err)) + + +@websocket_api.websocket_command({ + vol.Required('type'): 'person/update', + vol.Required('person_id'): str, + vol.Required('name'): vol.All(str, vol.Length(min=1)), + vol.Optional('user_id'): vol.Any(str, None), + vol.Optional(CONF_DEVICE_TRACKERS, default=[]): vol.All( + cv.ensure_list, cv.entities_domain(DEVICE_TRACKER_DOMAIN)), +}) +@websocket_api.require_admin +@websocket_api.async_response +async def ws_update_person(hass: HomeAssistantType, + connection: websocket_api.ActiveConnection, msg): + """Update a person.""" + manager = hass.data[DOMAIN] # type: PersonManager + changes = {} + for key in ('name', 'user_id', 'device_trackers'): + if key in msg: + changes[key] = msg[key] + + try: + person = await manager.async_update_person(msg['person_id'], **changes) + connection.send_result(msg['id'], person) + except ValueError as err: + connection.send_error( + msg['id'], websocket_api.const.ERR_INVALID_FORMAT, str(err)) + + +@websocket_api.websocket_command({ + vol.Required('type'): 'person/delete', + vol.Required('person_id'): str, +}) +@websocket_api.require_admin +@websocket_api.async_response +async def ws_delete_person(hass: HomeAssistantType, + connection: websocket_api.ActiveConnection, + msg): + """Delete a person.""" + manager = hass.data[DOMAIN] # type: PersonManager + await manager.async_delete_person(msg['person_id']) + connection.send_result(msg['id']) diff --git a/homeassistant/components/pilight.py b/homeassistant/components/pilight/__init__.py similarity index 100% rename from homeassistant/components/pilight.py rename to homeassistant/components/pilight/__init__.py diff --git a/homeassistant/components/binary_sensor/pilight.py b/homeassistant/components/pilight/binary_sensor.py similarity index 100% rename from homeassistant/components/binary_sensor/pilight.py rename to homeassistant/components/pilight/binary_sensor.py diff --git a/homeassistant/components/sensor/pilight.py b/homeassistant/components/pilight/sensor.py similarity index 100% rename from homeassistant/components/sensor/pilight.py rename to homeassistant/components/pilight/sensor.py diff --git a/homeassistant/components/switch/pilight.py b/homeassistant/components/pilight/switch.py similarity index 100% rename from homeassistant/components/switch/pilight.py rename to homeassistant/components/pilight/switch.py diff --git a/homeassistant/components/plant.py b/homeassistant/components/plant/__init__.py similarity index 92% rename from homeassistant/components/plant.py rename to homeassistant/components/plant/__init__.py index ed43562d221..27324ad57a3 100644 --- a/homeassistant/components/plant.py +++ b/homeassistant/components/plant/__init__.py @@ -1,24 +1,21 @@ -"""Component to monitor plants. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/plant/ -""" -import logging -from datetime import datetime, timedelta +"""Support for monitoring plants.""" from collections import deque +from datetime import datetime, timedelta +import logging + import voluptuous as vol -from homeassistant.exceptions import HomeAssistantError -from homeassistant.const import ( - STATE_OK, STATE_PROBLEM, STATE_UNKNOWN, TEMP_CELSIUS, ATTR_TEMPERATURE, - CONF_SENSORS, ATTR_UNIT_OF_MEASUREMENT) from homeassistant.components import group +from homeassistant.components.recorder.util import execute, session_scope +from homeassistant.const import ( + ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, CONF_SENSORS, STATE_OK, + STATE_PROBLEM, STATE_UNKNOWN, TEMP_CELSIUS) +from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.core import callback from homeassistant.helpers.event import async_track_state_change -from homeassistant.components.recorder.util import session_scope, execute _LOGGER = logging.getLogger(__name__) @@ -111,8 +108,8 @@ ENABLE_LOAD_HISTORY = False async def async_setup(hass, config): """Set up the Plant component.""" - component = EntityComponent(_LOGGER, DOMAIN, hass, - group_name=GROUP_NAME_ALL_PLANTS) + component = EntityComponent( + _LOGGER, DOMAIN, hass, group_name=GROUP_NAME_ALL_PLANTS) entities = [] for plant_name, plant_config in config[DOMAIN].items(): @@ -204,8 +201,8 @@ class Plant(Entity): self._conductivity = int(float(value)) elif reading == READING_BRIGHTNESS: self._brightness = int(float(value)) - self._brightness_history.add_measurement(self._brightness, - new_state.last_updated) + self._brightness_history.add_measurement( + self._brightness, new_state.last_updated) else: raise HomeAssistantError( "Unknown reading from sensor {}: {}".format(entity_id, value)) @@ -260,8 +257,8 @@ class Plant(Entity): # only use the database if it's configured self.hass.async_add_job(self._load_history_from_db) - async_track_state_change(self.hass, list(self._sensormap), - self.state_changed) + async_track_state_change( + self.hass, list(self._sensormap), self.state_changed) for entity_id in self._sensormap: state = self.hass.states.get(entity_id) @@ -277,11 +274,11 @@ class Plant(Entity): start_date = datetime.now() - timedelta(days=self._conf_check_days) entity_id = self._readingmap.get(READING_BRIGHTNESS) if entity_id is None: - _LOGGER.debug("not reading the history from the database as " - "there is no brightness sensor configured.") + _LOGGER.debug("Not reading the history from the database as " + "there is no brightness sensor configured") return - _LOGGER.debug("initializing values for %s from the database", + _LOGGER.debug("Initializing values for %s from the database", self._name) with session_scope(hass=self.hass) as session: query = session.query(States).filter( @@ -298,7 +295,7 @@ class Plant(Entity): int(state.state), state.last_updated) except ValueError: pass - _LOGGER.debug("initializing from database completed") + _LOGGER.debug("Initializing from database completed") self.async_schedule_update_ha_state() @property @@ -366,7 +363,7 @@ class DailyHistory: elif day > current_day: self._add_day(day, value) else: - _LOGGER.warning('received old measurement, not storing it!') + _LOGGER.warning("Received old measurement, not storing it") self.max = max(self._max_dict.values()) diff --git a/homeassistant/components/plum_lightpad.py b/homeassistant/components/plum_lightpad/__init__.py similarity index 92% rename from homeassistant/components/plum_lightpad.py rename to homeassistant/components/plum_lightpad/__init__.py index 979f257f25f..5b99223d25a 100644 --- a/homeassistant/components/plum_lightpad.py +++ b/homeassistant/components/plum_lightpad/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Plum Lightpad switches. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/plum_lightpad -""" +"""Support for Plum Lightpad devices.""" import asyncio import logging diff --git a/homeassistant/components/light/plum_lightpad.py b/homeassistant/components/plum_lightpad/light.py similarity index 87% rename from homeassistant/components/light/plum_lightpad.py rename to homeassistant/components/plum_lightpad/light.py index fa15c842deb..43cceaa671f 100644 --- a/homeassistant/components/light/plum_lightpad.py +++ b/homeassistant/components/plum_lightpad/light.py @@ -1,9 +1,4 @@ -""" -Support for Plum Lightpad switches. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/light.plum_lightpad/ -""" +"""Support for Plum Lightpad lights.""" from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light) from homeassistant.components.plum_lightpad import PLUM_DATA @@ -12,8 +7,8 @@ import homeassistant.util.color as color_util DEPENDENCIES = ['plum_lightpad'] -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Initialize the Plum Lightpad Light and GlowRing.""" if discovery_info is None: return @@ -35,7 +30,7 @@ async def async_setup_platform(hass, config, async_add_entities, class PlumLight(Light): - """Represenation of a Plum Lightpad dimmer.""" + """Representation of a Plum Lightpad dimmer.""" def __init__(self, load): """Initialize the light.""" @@ -91,7 +86,7 @@ class PlumLight(Light): class GlowRing(Light): - """Represenation of a Plum Lightpad dimmer glow ring.""" + """Representation of a Plum Lightpad dimmer glow ring.""" def __init__(self, lightpad): """Initialize the light.""" @@ -107,8 +102,8 @@ class GlowRing(Light): async def async_added_to_hass(self): """Subscribe to configchange events.""" - self._lightpad.add_event_listener('configchange', - self.configchange_event) + self._lightpad.add_event_listener( + 'configchange', self.configchange_event) def configchange_event(self, event): """Handle Configuration change event.""" @@ -155,7 +150,7 @@ class GlowRing(Light): @property def icon(self): - """Return the crop-portait icon representing the glow ring.""" + """Return the crop-portrait icon representing the glow ring.""" return 'mdi:crop-portrait' @property @@ -181,5 +176,4 @@ class GlowRing(Light): await self._lightpad.set_config( {"glowIntensity": kwargs[ATTR_BRIGHTNESS]}) else: - await self._lightpad.set_config( - {"glowEnabled": False}) + await self._lightpad.set_config({"glowEnabled": False}) diff --git a/homeassistant/components/point/.translations/da.json b/homeassistant/components/point/.translations/da.json new file mode 100644 index 00000000000..109bcbe6c37 --- /dev/null +++ b/homeassistant/components/point/.translations/da.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_setup": "Du kan kun konfigurere en enkelt Point konto.", + "authorize_url_fail": "Ukendt fejl ved generering af en autoriseret url.", + "authorize_url_timeout": "Timeout ved generering af autoriseret url.", + "external_setup": "Point er konfigureret med succes fra et andet flow.", + "no_flows": "Du skal konfigurere Point f\u00f8r du kan godkende med det. [L\u00e6s venligst vejledningen](https://www.home-assistant.io/components/point/)." + }, + "create_entry": { + "default": "Godkendt med Minut mod Point enhed(er)" + }, + "error": { + "follow_link": "F\u00f8lg linket og godkend f\u00f8r du trykker p\u00e5 send", + "no_token": "Ikke godkendt med Minut" + }, + "step": { + "auth": { + "description": "F\u00f8lg linket herunder og Accept\u00e9r adgang til din Minut konto. Vend s\u00e5 tilbage og tryk p\u00e5 Tilf\u00f8j nedenfor. \n\n [Link]({authorization_url})", + "title": "Godkend Point" + }, + "user": { + "data": { + "flow_impl": "Udbyder" + }, + "description": "V\u00e6lg hvilken godkendelsesudbyder du vil godkende med Point.", + "title": "Godkendelses udbyder" + } + }, + "title": "Minut Point" + } +} \ No newline at end of file diff --git a/homeassistant/components/point/.translations/pt.json b/homeassistant/components/point/.translations/pt.json index 6d24d56233c..874f0832b6c 100644 --- a/homeassistant/components/point/.translations/pt.json +++ b/homeassistant/components/point/.translations/pt.json @@ -27,6 +27,6 @@ "title": "Fornecedor de Autentica\u00e7\u00e3o" } }, - "title": "" + "title": "Minut Point" } } \ No newline at end of file diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index 6616d6b24ec..f223ded998f 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Minut Point. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/point/ -""" +"""Support for Minut Point.""" import asyncio import logging @@ -12,7 +7,6 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_TOKEN, CONF_WEBHOOK_ID -from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send) @@ -26,11 +20,12 @@ from .const import ( CONF_WEBHOOK_URL, DOMAIN, EVENT_RECEIVED, POINT_DISCOVERY_NEW, SCAN_INTERVAL, SIGNAL_UPDATE_ENTITY, SIGNAL_WEBHOOK) -REQUIREMENTS = ['pypoint==1.0.6'] -DEPENDENCIES = ['webhook'] +REQUIREMENTS = ['pypoint==1.0.8'] _LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['webhook'] + CONF_CLIENT_ID = 'client_id' CONF_CLIENT_SECRET = 'client_secret' @@ -117,8 +112,11 @@ async def async_setup_webhook(hass: HomeAssistantType, entry: ConfigEntry, entry, data={ **entry.data, }) - session.update_webhook(entry.data[CONF_WEBHOOK_URL], - entry.data[CONF_WEBHOOK_ID], events=['*']) + await hass.async_add_executor_job( + session.update_webhook, + entry.data[CONF_WEBHOOK_URL], + entry.data[CONF_WEBHOOK_ID], + ['*']) hass.components.webhook.async_register( DOMAIN, 'Point', entry.data[CONF_WEBHOOK_ID], handle_webhook) @@ -127,8 +125,8 @@ async def async_setup_webhook(hass: HomeAssistantType, entry: ConfigEntry, async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): """Unload a config entry.""" hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) - client = hass.data[DOMAIN].pop(entry.entry_id) - client.remove_webhook() + session = hass.data[DOMAIN].pop(entry.entry_id) + await hass.async_add_executor_job(session.remove_webhook) if not hass.data[DOMAIN]: hass.data.pop(DOMAIN) @@ -174,7 +172,8 @@ class MinutPointClient(): async def _sync(self): """Update local list of devices.""" - if not self._client.update() and self._is_available: + if not await self._hass.async_add_executor_job( + self._client.update) and self._is_available: self._is_available = False _LOGGER.warning("Device is unavailable") return @@ -237,15 +236,14 @@ class MinutPointEntity(Entity): _LOGGER.debug('Created device %s', self) self._async_unsub_dispatcher_connect = async_dispatcher_connect( self.hass, SIGNAL_UPDATE_ENTITY, self._update_callback) - self._update_callback() + await self._update_callback() async def async_will_remove_from_hass(self): """Disconnect dispatcher listener when removed.""" if self._async_unsub_dispatcher_connect: self._async_unsub_dispatcher_connect() - @callback - def _update_callback(self): + async def _update_callback(self): """Update the value of the sensor.""" pass diff --git a/homeassistant/components/binary_sensor/point.py b/homeassistant/components/point/binary_sensor.py similarity index 91% rename from homeassistant/components/binary_sensor/point.py rename to homeassistant/components/point/binary_sensor.py index 1bd97ce2747..c29ce421682 100644 --- a/homeassistant/components/binary_sensor/point.py +++ b/homeassistant/components/point/binary_sensor.py @@ -1,10 +1,4 @@ -""" -Support for Minut Point. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.point/ -""" - +"""Support for Minut Point binary sensors.""" import logging from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice @@ -56,7 +50,7 @@ class MinutPointBinarySensor(MinutPointEntity, BinarySensorDevice): """The platform class required by Home Assistant.""" def __init__(self, point_client, device_id, device_class): - """Initialize the entity.""" + """Initialize the binary sensor.""" super().__init__(point_client, device_id, device_class) self._async_unsub_hook_dispatcher_connect = None @@ -64,7 +58,7 @@ class MinutPointBinarySensor(MinutPointEntity, BinarySensorDevice): self._is_on = None async def async_added_to_hass(self): - """Call when entity is added to hass.""" + """Call when entity is added to HOme Assistant.""" await super().async_added_to_hass() self._async_unsub_hook_dispatcher_connect = async_dispatcher_connect( self.hass, SIGNAL_WEBHOOK, self._webhook_event) @@ -75,8 +69,7 @@ class MinutPointBinarySensor(MinutPointEntity, BinarySensorDevice): if self._async_unsub_hook_dispatcher_connect: self._async_unsub_hook_dispatcher_connect() - @callback - def _update_callback(self): + async def _update_callback(self): """Update the value of the sensor.""" if not self.is_updated: return @@ -92,7 +85,8 @@ class MinutPointBinarySensor(MinutPointEntity, BinarySensorDevice): if self.device.webhook != webhook: return _type = data.get('event', {}).get('type') - if _type not in self._events: + _device_id = data.get('event', {}).get('device_id') + if _type not in self._events or _device_id != self.device.device_id: return _LOGGER.debug("Recieved webhook: %s", _type) if _type == self._events[0]: diff --git a/homeassistant/components/point/config_flow.py b/homeassistant/components/point/config_flow.py index 8cda30c7171..64583a5ab38 100644 --- a/homeassistant/components/point/config_flow.py +++ b/homeassistant/components/point/config_flow.py @@ -43,7 +43,7 @@ class PointFlowHandler(config_entries.ConfigFlow): """Handle a config flow.""" VERSION = 1 - CONNETION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL def __init__(self): """Initialize flow.""" diff --git a/homeassistant/components/sensor/point.py b/homeassistant/components/point/sensor.py similarity index 84% rename from homeassistant/components/sensor/point.py rename to homeassistant/components/point/sensor.py index 9413cf163d9..90b83ba42e3 100644 --- a/homeassistant/components/sensor/point.py +++ b/homeassistant/components/point/sensor.py @@ -1,9 +1,4 @@ -""" -Support for Minut Point. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.point/ -""" +"""Support for Minut Point sensors.""" import logging from homeassistant.components.point import MinutPointEntity @@ -13,7 +8,6 @@ from homeassistant.components.sensor import DOMAIN from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS) -from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util.dt import parse_datetime @@ -46,16 +40,16 @@ class MinutPointSensor(MinutPointEntity): """The platform class required by Home Assistant.""" def __init__(self, point_client, device_id, device_class): - """Initialize the entity.""" + """Initialize the sensor.""" super().__init__(point_client, device_id, device_class) self._device_prop = SENSOR_TYPES[device_class] - @callback - def _update_callback(self): + async def _update_callback(self): """Update the value of the sensor.""" if self.is_updated: - _LOGGER.debug('Update sensor value for %s', self) - self._value = self.device.sensor(self.device_class) + _LOGGER.debug("Update sensor value for %s", self) + self._value = await self.hass.async_add_executor_job( + self.device.sensor, self.device_class) self._updated = parse_datetime(self.device.last_update) self.async_schedule_update_ha_state() @@ -67,6 +61,8 @@ class MinutPointSensor(MinutPointEntity): @property def state(self): """Return the state of the sensor.""" + if self.value is None: + return None return round(self.value, self._device_prop[1]) @property diff --git a/homeassistant/components/prometheus.py b/homeassistant/components/prometheus/__init__.py similarity index 96% rename from homeassistant/components/prometheus.py rename to homeassistant/components/prometheus/__init__.py index dc868530f88..4508611e51b 100644 --- a/homeassistant/components/prometheus.py +++ b/homeassistant/components/prometheus/__init__.py @@ -1,22 +1,17 @@ -""" -Support for Prometheus metrics export. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/prometheus/ -""" +"""Support for Prometheus metrics export.""" import logging -import voluptuous as vol from aiohttp import web +import voluptuous as vol +from homeassistant import core as hacore from homeassistant.components.climate import ATTR_CURRENT_TEMPERATURE from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( - EVENT_STATE_CHANGED, TEMP_FAHRENHEIT, CONTENT_TYPE_TEXT_PLAIN, - ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT) -from homeassistant import core as hacore -import homeassistant.helpers.config_validation as cv + ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, CONTENT_TYPE_TEXT_PLAIN, + EVENT_STATE_CHANGED, TEMP_FAHRENHEIT) from homeassistant.helpers import entityfilter, state as state_helper +import homeassistant.helpers.config_validation as cv from homeassistant.util.temperature import fahrenheit_to_celsius REQUIREMENTS = ['prometheus_client==0.2.0'] diff --git a/homeassistant/components/proximity.py b/homeassistant/components/proximity/__init__.py similarity index 96% rename from homeassistant/components/proximity.py rename to homeassistant/components/proximity/__init__.py index e8d86d480e5..0a617bcec90 100644 --- a/homeassistant/components/proximity.py +++ b/homeassistant/components/proximity/__init__.py @@ -1,19 +1,11 @@ -""" -Support for tracking the proximity of a device. - -Component to monitor the proximity of devices to a particular zone and the -direction of travel. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/proximity/ -""" +"""Support for tracking the proximity of a device.""" import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - CONF_ZONE, CONF_DEVICES, CONF_UNIT_OF_MEASUREMENT) + CONF_DEVICES, CONF_UNIT_OF_MEASUREMENT, CONF_ZONE) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_state_change from homeassistant.util.distance import convert diff --git a/homeassistant/components/python_script.py b/homeassistant/components/python_script/__init__.py similarity index 99% rename from homeassistant/components/python_script.py rename to homeassistant/components/python_script/__init__.py index 3cfa0696644..3d0952b89fb 100644 --- a/homeassistant/components/python_script.py +++ b/homeassistant/components/python_script/__init__.py @@ -18,7 +18,7 @@ from homeassistant.loader import bind_hass from homeassistant.util import sanitize_filename import homeassistant.util.dt as dt_util -REQUIREMENTS = ['restrictedpython==4.0b7'] +REQUIREMENTS = ['restrictedpython==4.0b8'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/qwikswitch.py b/homeassistant/components/qwikswitch/__init__.py similarity index 100% rename from homeassistant/components/qwikswitch.py rename to homeassistant/components/qwikswitch/__init__.py diff --git a/homeassistant/components/binary_sensor/qwikswitch.py b/homeassistant/components/qwikswitch/binary_sensor.py similarity index 100% rename from homeassistant/components/binary_sensor/qwikswitch.py rename to homeassistant/components/qwikswitch/binary_sensor.py diff --git a/homeassistant/components/light/qwikswitch.py b/homeassistant/components/qwikswitch/light.py similarity index 100% rename from homeassistant/components/light/qwikswitch.py rename to homeassistant/components/qwikswitch/light.py diff --git a/homeassistant/components/sensor/qwikswitch.py b/homeassistant/components/qwikswitch/sensor.py similarity index 100% rename from homeassistant/components/sensor/qwikswitch.py rename to homeassistant/components/qwikswitch/sensor.py diff --git a/homeassistant/components/switch/qwikswitch.py b/homeassistant/components/qwikswitch/switch.py similarity index 100% rename from homeassistant/components/switch/qwikswitch.py rename to homeassistant/components/qwikswitch/switch.py diff --git a/homeassistant/components/rachio.py b/homeassistant/components/rachio/__init__.py similarity index 100% rename from homeassistant/components/rachio.py rename to homeassistant/components/rachio/__init__.py diff --git a/homeassistant/components/binary_sensor/rachio.py b/homeassistant/components/rachio/binary_sensor.py similarity index 100% rename from homeassistant/components/binary_sensor/rachio.py rename to homeassistant/components/rachio/binary_sensor.py diff --git a/homeassistant/components/switch/rachio.py b/homeassistant/components/rachio/switch.py similarity index 100% rename from homeassistant/components/switch/rachio.py rename to homeassistant/components/rachio/switch.py diff --git a/homeassistant/components/rainbird.py b/homeassistant/components/rainbird/__init__.py similarity index 100% rename from homeassistant/components/rainbird.py rename to homeassistant/components/rainbird/__init__.py diff --git a/homeassistant/components/raincloud.py b/homeassistant/components/raincloud/__init__.py similarity index 100% rename from homeassistant/components/raincloud.py rename to homeassistant/components/raincloud/__init__.py diff --git a/homeassistant/components/binary_sensor/raincloud.py b/homeassistant/components/raincloud/binary_sensor.py similarity index 100% rename from homeassistant/components/binary_sensor/raincloud.py rename to homeassistant/components/raincloud/binary_sensor.py diff --git a/homeassistant/components/sensor/raincloud.py b/homeassistant/components/raincloud/sensor.py similarity index 100% rename from homeassistant/components/sensor/raincloud.py rename to homeassistant/components/raincloud/sensor.py diff --git a/homeassistant/components/switch/raincloud.py b/homeassistant/components/raincloud/switch.py similarity index 100% rename from homeassistant/components/switch/raincloud.py rename to homeassistant/components/raincloud/switch.py diff --git a/homeassistant/components/rainmachine/.translations/da.json b/homeassistant/components/rainmachine/.translations/da.json new file mode 100644 index 00000000000..61d29894fe2 --- /dev/null +++ b/homeassistant/components/rainmachine/.translations/da.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Konto er allerede registreret", + "invalid_credentials": "Ugyldige legitimationsoplysninger" + }, + "step": { + "user": { + "data": { + "ip_address": "V\u00e6rtsnavn eller IP-adresse", + "password": "Password", + "port": "Port" + }, + "title": "Udfyld dine oplysninger" + } + }, + "title": "RainMachine" + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/.translations/ko.json b/homeassistant/components/rainmachine/.translations/ko.json index 0885c7e9e66..5ce254c4026 100644 --- a/homeassistant/components/rainmachine/.translations/ko.json +++ b/homeassistant/components/rainmachine/.translations/ko.json @@ -11,7 +11,7 @@ "password": "\ube44\ubc00\ubc88\ud638", "port": "\ud3ec\ud2b8" }, - "title": "\uc0ac\uc6a9\uc790 \uc815\ubcf4\ub97c \uc785\ub825\ud574 \uc8fc\uc138\uc694" + "title": "\uc0ac\uc6a9\uc790 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694" } }, "title": "RainMachine" diff --git a/homeassistant/components/rainmachine/.translations/pt.json b/homeassistant/components/rainmachine/.translations/pt.json index 97b9f84f26c..12e77ed8e46 100644 --- a/homeassistant/components/rainmachine/.translations/pt.json +++ b/homeassistant/components/rainmachine/.translations/pt.json @@ -14,6 +14,6 @@ "title": "Preencha as suas informa\u00e7\u00f5es" } }, - "title": "" + "title": "RainMachine" } } \ No newline at end of file diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 4eb9e390d71..0591af8acfa 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -1,9 +1,4 @@ -""" -Support for RainMachine devices. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/rainmachine/ -""" +"""Support for RainMachine devices.""" import logging from datetime import timedelta diff --git a/homeassistant/components/raspihats.py b/homeassistant/components/raspihats/__init__.py similarity index 98% rename from homeassistant/components/raspihats.py rename to homeassistant/components/raspihats/__init__.py index 5e834cdf7ec..69b03a36769 100644 --- a/homeassistant/components/raspihats.py +++ b/homeassistant/components/raspihats/__init__.py @@ -1,9 +1,4 @@ -""" -Support for controlling raspihats boards. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/raspihats/ -""" +"""Support for controlling raspihats boards.""" import logging import threading import time diff --git a/homeassistant/components/binary_sensor/raspihats.py b/homeassistant/components/raspihats/binary_sensor.py similarity index 93% rename from homeassistant/components/binary_sensor/raspihats.py rename to homeassistant/components/raspihats/binary_sensor.py index feef5396d88..04885402e72 100644 --- a/homeassistant/components/binary_sensor/raspihats.py +++ b/homeassistant/components/raspihats/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Configure a binary_sensor using a digital input from a raspihats board. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.raspihats/ -""" +"""Support for raspihats board binary sensors.""" import logging import voluptuous as vol @@ -34,7 +29,7 @@ _CHANNELS_SCHEMA = vol.Schema([{ _I2C_HATS_SCHEMA = vol.Schema([{ vol.Required(CONF_BOARD): vol.In(I2C_HAT_NAMES), vol.Required(CONF_ADDRESS): vol.Coerce(int), - vol.Required(CONF_CHANNELS): _CHANNELS_SCHEMA + vol.Required(CONF_CHANNELS): _CHANNELS_SCHEMA, }]) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ diff --git a/homeassistant/components/switch/raspihats.py b/homeassistant/components/raspihats/switch.py similarity index 95% rename from homeassistant/components/switch/raspihats.py rename to homeassistant/components/raspihats/switch.py index b422efea2ff..10bb2f748c4 100644 --- a/homeassistant/components/switch/raspihats.py +++ b/homeassistant/components/raspihats/switch.py @@ -1,9 +1,4 @@ -""" -Configure a switch using a digital output from a raspihats board. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.raspihats/ -""" +"""Support for raspihats board switches.""" import logging import voluptuous as vol diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index eb97d197e3e..9b852b4a00a 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -1,12 +1,4 @@ -""" -Support for recording details. - -Component that records all events and state changes. Allows other components -to query this database. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/recorder/ -""" +"""Support for recording details.""" import asyncio from collections import namedtuple import concurrent.futures @@ -33,7 +25,7 @@ from . import migration, purge from .const import DATA_INSTANCE from .util import session_scope -REQUIREMENTS = ['sqlalchemy==1.2.16'] +REQUIREMENTS = ['sqlalchemy==1.2.17'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/remember_the_milk/__init__.py b/homeassistant/components/remember_the_milk/__init__.py index a94e8e95c6f..82619e35a0e 100644 --- a/homeassistant/components/remember_the_milk/__init__.py +++ b/homeassistant/components/remember_the_milk/__init__.py @@ -1,9 +1,4 @@ -""" -Component to interact with Remember The Milk. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/remember_the_milk/ -""" +"""Support to interact with Remember The Milk.""" import json import logging import os diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index 162cb41d92e..b7923596039 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -1,9 +1,4 @@ -""" -Component to interface with universal remote control devices. - -For more details about this component, please refer to the documentation -at https://home-assistant.io/components/remote/ -""" +"""Support to interface with universal remote control devices.""" from datetime import timedelta import functools as ft import logging @@ -18,7 +13,8 @@ from homeassistant.const import ( STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID) from homeassistant.components import group -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa +from homeassistant.helpers.config_validation import ( # noqa + PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/rest_command.py b/homeassistant/components/rest_command/__init__.py similarity index 95% rename from homeassistant/components/rest_command.py rename to homeassistant/components/rest_command/__init__.py index c1ccc73b81c..01c5d837ca9 100644 --- a/homeassistant/components/rest_command.py +++ b/homeassistant/components/rest_command/__init__.py @@ -1,9 +1,4 @@ -""" -Exposes regular rest commands as services. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/rest_command/ -""" +"""Support for exposing regular REST commands as services.""" import asyncio import logging diff --git a/homeassistant/components/rflink.py b/homeassistant/components/rflink/__init__.py similarity index 99% rename from homeassistant/components/rflink.py rename to homeassistant/components/rflink/__init__.py index b91ff251bd1..98e80580fea 100644 --- a/homeassistant/components/rflink.py +++ b/homeassistant/components/rflink/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Rflink components. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/rflink/ -""" +"""Support for Rflink devices.""" import asyncio from collections import defaultdict import logging @@ -345,6 +340,7 @@ class RflinkDevice(Entity): async def async_added_to_hass(self): """Register update callback.""" + await super().async_added_to_hass() # 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][ diff --git a/homeassistant/components/rflink/services.yaml b/homeassistant/components/rflink/services.yaml new file mode 100644 index 00000000000..9269326ece6 --- /dev/null +++ b/homeassistant/components/rflink/services.yaml @@ -0,0 +1,5 @@ +send_command: + description: Send device command through RFLink. + fields: + command: {description: The command to be sent., example: 'on'} + device_id: {description: RFLink device ID., example: newkaku_0000c6c2_1} diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx/__init__.py similarity index 98% rename from homeassistant/components/rfxtrx.py rename to homeassistant/components/rfxtrx/__init__.py index f2c82842bc1..a7b703ef2ab 100644 --- a/homeassistant/components/rfxtrx.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -1,9 +1,4 @@ -""" -Support for RFXtrx components. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/rfxtrx/ -""" +"""Support for RFXtrx devices.""" from collections import OrderedDict import logging diff --git a/homeassistant/components/binary_sensor/rfxtrx.py b/homeassistant/components/rfxtrx/binary_sensor.py similarity index 97% rename from homeassistant/components/binary_sensor/rfxtrx.py rename to homeassistant/components/rfxtrx/binary_sensor.py index 1e88c72e19d..9a49bd02b97 100644 --- a/homeassistant/components/binary_sensor/rfxtrx.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for RFXtrx binary sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.rfxtrx/ -""" +"""Support for RFXtrx binary sensors.""" import logging import voluptuous as vol @@ -35,7 +30,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Any(cv.time_period, cv.positive_timedelta), vol.Optional(CONF_DATA_BITS): cv.positive_int, vol.Optional(CONF_COMMAND_ON): cv.byte, - vol.Optional(CONF_COMMAND_OFF): cv.byte + vol.Optional(CONF_COMMAND_OFF): cv.byte, }) }, vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean, diff --git a/homeassistant/components/cover/rfxtrx.py b/homeassistant/components/rfxtrx/cover.py similarity index 93% rename from homeassistant/components/cover/rfxtrx.py rename to homeassistant/components/rfxtrx/cover.py index d486b601977..5a657923683 100644 --- a/homeassistant/components/cover/rfxtrx.py +++ b/homeassistant/components/rfxtrx/cover.py @@ -1,9 +1,4 @@ -""" -Support for RFXtrx cover components. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/cover.rfxtrx/ -""" +"""Support for RFXtrx covers.""" import voluptuous as vol from homeassistant.components import rfxtrx diff --git a/homeassistant/components/light/rfxtrx.py b/homeassistant/components/rfxtrx/light.py similarity index 93% rename from homeassistant/components/light/rfxtrx.py rename to homeassistant/components/rfxtrx/light.py index 10288773486..d0b75c2f962 100644 --- a/homeassistant/components/light/rfxtrx.py +++ b/homeassistant/components/rfxtrx/light.py @@ -1,9 +1,4 @@ -""" -Support for RFXtrx lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.rfxtrx/ -""" +"""Support for RFXtrx lights.""" import logging import voluptuous as vol diff --git a/homeassistant/components/sensor/rfxtrx.py b/homeassistant/components/rfxtrx/sensor.py similarity index 96% rename from homeassistant/components/sensor/rfxtrx.py rename to homeassistant/components/rfxtrx/sensor.py index c2eff8d7c5d..74c64635563 100644 --- a/homeassistant/components/sensor/rfxtrx.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -1,9 +1,4 @@ -""" -Support for RFXtrx sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.rfxtrx/ -""" +"""Support for RFXtrx sensors.""" import logging import voluptuous as vol diff --git a/homeassistant/components/switch/rfxtrx.py b/homeassistant/components/rfxtrx/switch.py similarity index 92% rename from homeassistant/components/switch/rfxtrx.py rename to homeassistant/components/rfxtrx/switch.py index c0f057da138..141cf2c2c1a 100644 --- a/homeassistant/components/switch/rfxtrx.py +++ b/homeassistant/components/rfxtrx/switch.py @@ -1,9 +1,4 @@ -""" -Support for RFXtrx switches. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.rfxtrx/ -""" +"""Support for RFXtrx switches.""" import logging import voluptuous as vol diff --git a/homeassistant/components/ring.py b/homeassistant/components/ring/__init__.py similarity index 90% rename from homeassistant/components/ring.py rename to homeassistant/components/ring/__init__.py index 2e048caa52f..526388a0918 100644 --- a/homeassistant/components/ring.py +++ b/homeassistant/components/ring/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Ring Doorbell/Chimes. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/ring/ -""" +"""Support for Ring Doorbell/Chimes.""" import logging from requests.exceptions import HTTPError, ConnectTimeout diff --git a/homeassistant/components/roku.py b/homeassistant/components/roku/__init__.py similarity index 94% rename from homeassistant/components/roku.py rename to homeassistant/components/roku/__init__.py index 5ceebb3dee5..89bb1a9acb8 100644 --- a/homeassistant/components/roku.py +++ b/homeassistant/components/roku/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Roku platform. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/roku/ -""" +"""Support for Roku.""" import logging import voluptuous as vol diff --git a/homeassistant/components/media_player/roku.py b/homeassistant/components/roku/media_player.py similarity index 94% rename from homeassistant/components/media_player/roku.py rename to homeassistant/components/roku/media_player.py index 9dc1151064d..3cf27af0674 100644 --- a/homeassistant/components/media_player/roku.py +++ b/homeassistant/components/roku/media_player.py @@ -1,16 +1,12 @@ -""" -Support for the Roku media player. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/media_player.roku/ -""" +"""Support for the Roku media player.""" import logging import requests.exceptions -from homeassistant.components.media_player import ( +from homeassistant.components.media_player import MediaPlayerDevice +from homeassistant.components.media_player.const import ( MEDIA_TYPE_MOVIE, SUPPORT_NEXT_TRACK, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice) + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET) from homeassistant.const import (CONF_HOST, STATE_HOME, STATE_IDLE, STATE_PLAYING) diff --git a/homeassistant/components/remote/roku.py b/homeassistant/components/roku/remote.py similarity index 85% rename from homeassistant/components/remote/roku.py rename to homeassistant/components/roku/remote.py index 86a7105dafe..5529918010c 100644 --- a/homeassistant/components/remote/roku.py +++ b/homeassistant/components/roku/remote.py @@ -1,20 +1,14 @@ -""" -Support for the Roku remote. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/remote.roku/ -""" +"""Support for the Roku remote.""" import requests.exceptions from homeassistant.components import remote from homeassistant.const import (CONF_HOST) - DEPENDENCIES = ['roku'] -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the Roku remote platform.""" if not discovery_info: return diff --git a/homeassistant/components/route53.py b/homeassistant/components/route53/__init__.py similarity index 95% rename from homeassistant/components/route53.py rename to homeassistant/components/route53/__init__.py index 4c35983feed..725dec8b8e5 100644 --- a/homeassistant/components/route53.py +++ b/homeassistant/components/route53/__init__.py @@ -1,9 +1,4 @@ -""" -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/ -""" +"""Update the IP addresses of your Route53 DNS records.""" from datetime import timedelta import logging from typing import List diff --git a/homeassistant/components/rpi_gpio.py b/homeassistant/components/rpi_gpio/__init__.py similarity index 89% rename from homeassistant/components/rpi_gpio.py rename to homeassistant/components/rpi_gpio/__init__.py index 5f52341f1cb..b5bd0796f16 100644 --- a/homeassistant/components/rpi_gpio.py +++ b/homeassistant/components/rpi_gpio/__init__.py @@ -1,9 +1,4 @@ -""" -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/ -""" +"""Support for controlling GPIO pins of a Raspberry Pi.""" import logging from homeassistant.const import ( diff --git a/homeassistant/components/binary_sensor/rpi_gpio.py b/homeassistant/components/rpi_gpio/binary_sensor.py similarity index 93% rename from homeassistant/components/binary_sensor/rpi_gpio.py rename to homeassistant/components/rpi_gpio/binary_sensor.py index 2fe4e0766ed..559ae958404 100644 --- a/homeassistant/components/binary_sensor/rpi_gpio.py +++ b/homeassistant/components/rpi_gpio/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for binary sensor using RPi GPIO. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.rpi_gpio/ -""" +"""Support for binary sensor using RPi GPIO.""" import logging import voluptuous as vol diff --git a/homeassistant/components/cover/rpi_gpio.py b/homeassistant/components/rpi_gpio/cover.py similarity index 92% rename from homeassistant/components/cover/rpi_gpio.py rename to homeassistant/components/rpi_gpio/cover.py index 828f5e8e0fe..403f7ec6867 100644 --- a/homeassistant/components/cover/rpi_gpio.py +++ b/homeassistant/components/rpi_gpio/cover.py @@ -1,12 +1,4 @@ -""" -Support for controlling a Raspberry Pi cover. - -Instructions for building the controller can be found here -https://github.com/andrewshilliday/garage-door-controller - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/cover.rpi_gpio/ -""" +"""Support for controlling a Raspberry Pi cover.""" import logging from time import sleep diff --git a/homeassistant/components/switch/rpi_gpio.py b/homeassistant/components/rpi_gpio/switch.py similarity index 92% rename from homeassistant/components/switch/rpi_gpio.py rename to homeassistant/components/rpi_gpio/switch.py index 3dec1135ec8..bdb79d03eec 100644 --- a/homeassistant/components/switch/rpi_gpio.py +++ b/homeassistant/components/rpi_gpio/switch.py @@ -1,9 +1,4 @@ -""" -Allows to configure a switch using RPi GPIO. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.rpi_gpio/ -""" +"""Allows to configure a switch using RPi GPIO.""" import logging import voluptuous as vol diff --git a/homeassistant/components/rpi_pfio.py b/homeassistant/components/rpi_pfio/__init__.py similarity index 88% rename from homeassistant/components/rpi_pfio.py rename to homeassistant/components/rpi_pfio/__init__.py index 286be87bce9..b096d9fe98a 100644 --- a/homeassistant/components/rpi_pfio.py +++ b/homeassistant/components/rpi_pfio/__init__.py @@ -1,9 +1,4 @@ -""" -Support for controlling the PiFace Digital I/O module on a RPi. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/rpi_pfio/ -""" +"""Support for controlling the PiFace Digital I/O module on a RPi.""" import logging from homeassistant.const import ( diff --git a/homeassistant/components/binary_sensor/rpi_pfio.py b/homeassistant/components/rpi_pfio/binary_sensor.py similarity index 92% rename from homeassistant/components/binary_sensor/rpi_pfio.py rename to homeassistant/components/rpi_pfio/binary_sensor.py index 61d1f8ac285..677ec3bb16f 100644 --- a/homeassistant/components/binary_sensor/rpi_pfio.py +++ b/homeassistant/components/rpi_pfio/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for binary sensor using the PiFace Digital I/O module on a RPi. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.rpi_pfio/ -""" +"""Support for binary sensor using the PiFace Digital I/O module on a RPi.""" import logging import voluptuous as vol diff --git a/homeassistant/components/switch/rpi_pfio.py b/homeassistant/components/rpi_pfio/switch.py similarity index 91% rename from homeassistant/components/switch/rpi_pfio.py rename to homeassistant/components/rpi_pfio/switch.py index 8b0871ca800..fc158bd666f 100644 --- a/homeassistant/components/switch/rpi_pfio.py +++ b/homeassistant/components/rpi_pfio/switch.py @@ -1,9 +1,4 @@ -""" -Allows to configure a switch using the PiFace Digital I/O module on a RPi. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.rpi_pfio/ -""" +"""Support for switches using the PiFace Digital I/O module on a RPi.""" import logging import voluptuous as vol diff --git a/homeassistant/components/rss_feed_template.py b/homeassistant/components/rss_feed_template/__init__.py similarity index 94% rename from homeassistant/components/rss_feed_template.py rename to homeassistant/components/rss_feed_template/__init__.py index 34bee1ec5fc..3c93fe2ac83 100644 --- a/homeassistant/components/rss_feed_template.py +++ b/homeassistant/components/rss_feed_template/__init__.py @@ -1,10 +1,4 @@ -""" -Exports sensor values via RSS feed. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/rss_feed_template/ -""" - +"""Support to export sensor values via RSS feed.""" from html import escape from aiohttp import web diff --git a/homeassistant/components/sabnzbd.py b/homeassistant/components/sabnzbd/__init__.py similarity index 98% rename from homeassistant/components/sabnzbd.py rename to homeassistant/components/sabnzbd/__init__.py index 25fce22c641..d070872f85c 100644 --- a/homeassistant/components/sabnzbd.py +++ b/homeassistant/components/sabnzbd/__init__.py @@ -1,9 +1,4 @@ -""" -Support for monitoring an SABnzbd NZB client. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sabnzbd/ -""" +"""Support for monitoring an SABnzbd NZB client.""" import logging from datetime import timedelta diff --git a/homeassistant/components/sensor/sabnzbd.py b/homeassistant/components/sabnzbd/sensor.py similarity index 83% rename from homeassistant/components/sensor/sabnzbd.py rename to homeassistant/components/sabnzbd/sensor.py index 7e94f9026a8..ca8fc64eea4 100644 --- a/homeassistant/components/sensor/sabnzbd.py +++ b/homeassistant/components/sabnzbd/sensor.py @@ -1,9 +1,4 @@ -""" -Support for monitoring an SABnzbd NZB client. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.sabnzbd/ -""" +"""Support for monitoring an SABnzbd NZB client.""" import logging from homeassistant.components.sabnzbd import DATA_SABNZBD, \ @@ -16,8 +11,8 @@ DEPENDENCIES = ['sabnzbd'] _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the SABnzbd sensors.""" if discovery_info is None: return @@ -44,8 +39,8 @@ class SabnzbdSensor(Entity): async def async_added_to_hass(self): """Call when entity about to be added to hass.""" - async_dispatcher_connect(self.hass, SIGNAL_SABNZBD_UPDATED, - self.update_state) + async_dispatcher_connect( + self.hass, SIGNAL_SABNZBD_UPDATED, self.update_state) @property def name(self): diff --git a/homeassistant/components/satel_integra.py b/homeassistant/components/satel_integra/__init__.py similarity index 96% rename from homeassistant/components/satel_integra.py rename to homeassistant/components/satel_integra/__init__.py index 25295c6f2ce..bff365a079f 100644 --- a/homeassistant/components/satel_integra.py +++ b/homeassistant/components/satel_integra/__init__.py @@ -1,10 +1,4 @@ -""" -Support for Satel Integra devices. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/satel_integra/ -""" - +"""Support for Satel Integra devices.""" import asyncio import logging diff --git a/homeassistant/components/alarm_control_panel/satel_integra.py b/homeassistant/components/satel_integra/alarm_control_panel.py similarity index 87% rename from homeassistant/components/alarm_control_panel/satel_integra.py rename to homeassistant/components/satel_integra/alarm_control_panel.py index b704677800f..360acdb2497 100644 --- a/homeassistant/components/alarm_control_panel/satel_integra.py +++ b/homeassistant/components/satel_integra/alarm_control_panel.py @@ -1,9 +1,4 @@ -""" -Support for Satel Integra alarm, using ETHM module: https://www.satel.pl/en/ . - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/alarm_control_panel.satel_integra/ -""" +"""Support for Satel Integra alarm, using ETHM module.""" import logging import homeassistant.components.alarm_control_panel as alarm @@ -17,8 +12,8 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['satel_integra'] -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up for Satel Integra alarm panels.""" if not discovery_info: return diff --git a/homeassistant/components/binary_sensor/satel_integra.py b/homeassistant/components/satel_integra/binary_sensor.py similarity index 82% rename from homeassistant/components/binary_sensor/satel_integra.py rename to homeassistant/components/satel_integra/binary_sensor.py index b4541cf2c44..34ced628712 100644 --- a/homeassistant/components/binary_sensor/satel_integra.py +++ b/homeassistant/components/satel_integra/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for Satel Integra zone states- represented as binary sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.satel_integra/ -""" +"""Support for Satel Integra zone states- represented as binary sensors.""" import logging from homeassistant.components.binary_sensor import BinarySensorDevice @@ -21,8 +16,8 @@ DEPENDENCIES = ['satel_integra'] _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the Satel Integra binary sensor devices.""" if not discovery_info: return @@ -34,8 +29,8 @@ async def async_setup_platform(hass, config, async_add_entities, for zone_num, device_config_data in configured_zones.items(): zone_type = device_config_data[CONF_ZONE_TYPE] zone_name = device_config_data[CONF_ZONE_NAME] - device = SatelIntegraBinarySensor(zone_num, zone_name, zone_type, - SIGNAL_ZONES_UPDATED) + device = SatelIntegraBinarySensor( + zone_num, zone_name, zone_type, SIGNAL_ZONES_UPDATED) devices.append(device) configured_outputs = discovery_info[CONF_OUTPUTS] @@ -43,8 +38,8 @@ async def async_setup_platform(hass, config, async_add_entities, for zone_num, device_config_data in configured_outputs.items(): zone_type = device_config_data[CONF_ZONE_TYPE] zone_name = device_config_data[CONF_ZONE_NAME] - device = SatelIntegraBinarySensor(zone_num, zone_name, zone_type, - SIGNAL_OUTPUTS_UPDATED) + device = SatelIntegraBinarySensor( + zone_num, zone_name, zone_type, SIGNAL_OUTPUTS_UPDATED) devices.append(device) async_add_entities(devices) diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index b3ab5228875..802512dbf5d 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -1,9 +1,4 @@ -""" -Allow users to set and activate scenes. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/scene/ -""" +"""Allow users to set and activate scenes.""" import asyncio import importlib import logging @@ -25,8 +20,7 @@ STATES = 'states' def _hass_domain_validator(config): """Validate platform in config for homeassistant domain.""" if CONF_PLATFORM not in config: - config = { - CONF_PLATFORM: HASS_DOMAIN, STATES: config} + config = {CONF_PLATFORM: HASS_DOMAIN, STATES: config} return config diff --git a/homeassistant/components/scene/homeassistant.py b/homeassistant/components/scene/homeassistant.py index 5812512ccef..96e24138b4a 100644 --- a/homeassistant/components/scene/homeassistant.py +++ b/homeassistant/components/scene/homeassistant.py @@ -1,9 +1,4 @@ -""" -Allow users to set and activate scenes. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/scene/ -""" +"""Allow users to set and activate scenes.""" from collections import namedtuple import voluptuous as vol diff --git a/homeassistant/components/scene/hunterdouglas_powerview.py b/homeassistant/components/scene/hunterdouglas_powerview.py index 7676deb1a9c..7f0709aa6c1 100644 --- a/homeassistant/components/scene/hunterdouglas_powerview.py +++ b/homeassistant/components/scene/hunterdouglas_powerview.py @@ -1,9 +1,4 @@ -""" -Support for Powerview scenes from a Powerview hub. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/scene.hunterdouglas_powerview/ -""" +"""Support for Powerview scenes from a Powerview hub.""" import logging import voluptuous as vol diff --git a/homeassistant/components/scene/lifx_cloud.py b/homeassistant/components/scene/lifx_cloud.py index c1dda86343d..c877bddbe53 100644 --- a/homeassistant/components/scene/lifx_cloud.py +++ b/homeassistant/components/scene/lifx_cloud.py @@ -1,9 +1,4 @@ -""" -Support for LIFX Cloud scenes. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/scene.lifx_cloud/ -""" +"""Support for LIFX Cloud scenes.""" import asyncio import logging diff --git a/homeassistant/components/scene/litejet.py b/homeassistant/components/scene/litejet.py index e12643fa651..2563c9ceb0c 100644 --- a/homeassistant/components/scene/litejet.py +++ b/homeassistant/components/scene/litejet.py @@ -1,9 +1,4 @@ -""" -Support for LiteJet scenes. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/scene.litejet/ -""" +"""Support for LiteJet scenes.""" import logging from homeassistant.components import litejet diff --git a/homeassistant/components/script.py b/homeassistant/components/script/__init__.py similarity index 94% rename from homeassistant/components/script.py rename to homeassistant/components/script/__init__.py index 15df6907468..fceedb57428 100644 --- a/homeassistant/components/script.py +++ b/homeassistant/components/script/__init__.py @@ -1,12 +1,4 @@ -""" -Support for scripts. - -Scripts are a sequence of actions that can be triggered manually -by the user or automatically based upon automation events, etc. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/script/ -""" +"""Support for scripts.""" import asyncio import logging @@ -124,7 +116,7 @@ async def _async_process_config(hass, config, component): scripts = [] - for object_id, cfg in config[DOMAIN].items(): + for object_id, cfg in config.get(DOMAIN, {}).items(): alias = cfg.get(CONF_ALIAS, object_id) script = ScriptEntity(hass, object_id, alias, cfg[CONF_SEQUENCE]) scripts.append(script) diff --git a/homeassistant/components/scsgate.py b/homeassistant/components/scsgate/__init__.py similarity index 100% rename from homeassistant/components/scsgate.py rename to homeassistant/components/scsgate/__init__.py diff --git a/homeassistant/components/cover/scsgate.py b/homeassistant/components/scsgate/cover.py similarity index 94% rename from homeassistant/components/cover/scsgate.py rename to homeassistant/components/scsgate/cover.py index 2d85c1fe3c3..fc1c16e1ff3 100644 --- a/homeassistant/components/cover/scsgate.py +++ b/homeassistant/components/scsgate/cover.py @@ -1,9 +1,4 @@ -""" -Allow to configure a SCSGate cover. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/cover.scsgate/ -""" +"""Support for SCSGate covers.""" import logging import voluptuous as vol diff --git a/homeassistant/components/light/scsgate.py b/homeassistant/components/scsgate/light.py similarity index 95% rename from homeassistant/components/light/scsgate.py rename to homeassistant/components/scsgate/light.py index c218e194791..87d7e02b383 100644 --- a/homeassistant/components/light/scsgate.py +++ b/homeassistant/components/scsgate/light.py @@ -1,9 +1,4 @@ -""" -Support for SCSGate lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.scsgate/ -""" +"""Support for SCSGate lights.""" import logging import voluptuous as vol diff --git a/homeassistant/components/switch/scsgate.py b/homeassistant/components/scsgate/switch.py similarity index 97% rename from homeassistant/components/switch/scsgate.py rename to homeassistant/components/scsgate/switch.py index 9344aeab7ed..2b2bf2de94f 100644 --- a/homeassistant/components/switch/scsgate.py +++ b/homeassistant/components/scsgate/switch.py @@ -1,9 +1,4 @@ -""" -Support for SCSGate switches. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.scsgate/ -""" +"""Support for SCSGate switches.""" import logging import voluptuous as vol diff --git a/homeassistant/components/sense.py b/homeassistant/components/sense/__init__.py similarity index 88% rename from homeassistant/components/sense.py rename to homeassistant/components/sense/__init__.py index 2fac2820230..11c45991400 100644 --- a/homeassistant/components/sense.py +++ b/homeassistant/components/sense/__init__.py @@ -1,9 +1,4 @@ -""" -Support for monitoring a Sense energy sensor. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sense/ -""" +"""Support for monitoring a Sense energy sensor.""" import logging import voluptuous as vol diff --git a/homeassistant/components/binary_sensor/sense.py b/homeassistant/components/sense/binary_sensor.py similarity index 94% rename from homeassistant/components/binary_sensor/sense.py rename to homeassistant/components/sense/binary_sensor.py index a85a0c889d1..80fb8f2634d 100644 --- a/homeassistant/components/binary_sensor/sense.py +++ b/homeassistant/components/sense/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for monitoring a Sense energy sensor device. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.sense/ -""" +"""Support for monitoring a Sense energy sensor device.""" import logging from homeassistant.components.binary_sensor import BinarySensorDevice diff --git a/homeassistant/components/sensor/sense.py b/homeassistant/components/sense/sensor.py similarity index 95% rename from homeassistant/components/sensor/sense.py rename to homeassistant/components/sense/sensor.py index 769b3a9e148..2995b860e5b 100644 --- a/homeassistant/components/sensor/sense.py +++ b/homeassistant/components/sense/sensor.py @@ -1,9 +1,4 @@ -""" -Support for monitoring a Sense energy sensor. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.sense/ -""" +"""Support for monitoring a Sense energy sensor.""" from datetime import timedelta import logging diff --git a/homeassistant/components/sensor/.translations/moon.da.json b/homeassistant/components/sensor/.translations/moon.da.json index 330df6d0cf7..c2406de68bb 100644 --- a/homeassistant/components/sensor/.translations/moon.da.json +++ b/homeassistant/components/sensor/.translations/moon.da.json @@ -1,5 +1,12 @@ { "state": { - "full_moon": "Fuldm\u00e5ne" + "first_quarter": "F\u00f8rste kvartal", + "full_moon": "Fuldm\u00e5ne", + "last_quarter": "Sidste kvartal", + "new_moon": "Nym\u00e5ne", + "waning_crescent": "Aftagende halvm\u00e5ne", + "waning_gibbous": "Aftagende m\u00e5ne", + "waxing_crescent": "Tiltagende halvm\u00e5ne", + "waxing_gibbous": "Tiltagende m\u00e5ne" } } \ No newline at end of file diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 2800b689dc6..50549f28fd7 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -11,7 +11,8 @@ import logging import voluptuous as vol from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa +from homeassistant.helpers.config_validation import ( # noqa + PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, DEVICE_CLASS_PRESSURE) diff --git a/homeassistant/components/sensor/broadlink.py b/homeassistant/components/sensor/broadlink.py index 50f9f955148..5720201b3f2 100644 --- a/homeassistant/components/sensor/broadlink.py +++ b/homeassistant/components/sensor/broadlink.py @@ -14,7 +14,8 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_HOST, CONF_MAC, CONF_MONITORED_CONDITIONS, CONF_NAME, TEMP_CELSIUS, - CONF_TIMEOUT, CONF_UPDATE_INTERVAL) + CONF_TIMEOUT, CONF_UPDATE_INTERVAL, CONF_SCAN_INTERVAL, + CONF_UPDATE_INTERVAL_INVALIDATION_VERSION) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv @@ -25,6 +26,7 @@ _LOGGER = logging.getLogger(__name__) DEVICE_DEFAULT_NAME = 'Broadlink sensor' DEFAULT_TIMEOUT = 10 +SCAN_INTERVAL = timedelta(seconds=300) SENSOR_TYPES = { 'temperature': ['Temperature', TEMP_CELSIUS], @@ -34,16 +36,24 @@ SENSOR_TYPES = { 'noise': ['Noise', ' '], } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=DEVICE_DEFAULT_NAME): vol.Coerce(str), - vol.Optional(CONF_MONITORED_CONDITIONS, default=[]): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), - vol.Optional(CONF_UPDATE_INTERVAL, default=timedelta(seconds=300)): ( - vol.All(cv.time_period, cv.positive_timedelta)), - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_MAC): cv.string, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int -}) +PLATFORM_SCHEMA = vol.All( + PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEVICE_DEFAULT_NAME): vol.Coerce(str), + vol.Optional(CONF_MONITORED_CONDITIONS, default=[]): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.Optional(CONF_UPDATE_INTERVAL): + vol.All(cv.time_period, cv.positive_timedelta), + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_MAC): cv.string, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int + }), + cv.deprecated( + CONF_UPDATE_INTERVAL, + replacement_key=CONF_SCAN_INTERVAL, + invalidation_version=CONF_UPDATE_INTERVAL_INVALIDATION_VERSION, + default=SCAN_INTERVAL + ) +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -53,7 +63,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): mac_addr = binascii.unhexlify(mac) name = config.get(CONF_NAME) timeout = config.get(CONF_TIMEOUT) - update_interval = config.get(CONF_UPDATE_INTERVAL) + update_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) broadlink_data = BroadlinkData(update_interval, host, mac_addr, timeout) dev = [] for variable in config[CONF_MONITORED_CONDITIONS]: diff --git a/homeassistant/components/sensor/co2signal.py b/homeassistant/components/sensor/co2signal.py index ad46f3b494f..7b4cd67bd70 100644 --- a/homeassistant/components/sensor/co2signal.py +++ b/homeassistant/components/sensor/co2signal.py @@ -15,7 +15,7 @@ from homeassistant.helpers.entity import Entity CONF_COUNTRY_CODE = "country_code" -REQUIREMENTS = ['co2signal==0.4.1'] +REQUIREMENTS = ['co2signal==0.4.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/cpuspeed.py b/homeassistant/components/sensor/cpuspeed.py index e97972dae3b..f69d0b285ba 100644 --- a/homeassistant/components/sensor/cpuspeed.py +++ b/homeassistant/components/sensor/cpuspeed.py @@ -21,6 +21,9 @@ ATTR_BRAND = 'Brand' ATTR_HZ = 'GHz Advertised' ATTR_ARCH = 'arch' +HZ_ACTUAL_RAW = 'hz_actual_raw' +HZ_ADVERTISED_RAW = 'hz_advertised_raw' + DEFAULT_NAME = 'CPU speed' ICON = 'mdi:pulse' @@ -66,12 +69,17 @@ class CpuSpeedSensor(Entity): def device_state_attributes(self): """Return the state attributes.""" if self.info is not None: - return { + attrs = { ATTR_ARCH: self.info['arch'], ATTR_BRAND: self.info['brand'], - ATTR_HZ: round(self.info['hz_advertised_raw'][0]/10**9, 2) } + if HZ_ADVERTISED_RAW in self.info: + attrs[ATTR_HZ] = round( + self.info[HZ_ADVERTISED_RAW][0] / 10 ** 9, 2 + ) + return attrs + @property def icon(self): """Return the icon to use in the frontend, if any.""" @@ -82,4 +90,9 @@ class CpuSpeedSensor(Entity): from cpuinfo import cpuinfo self.info = cpuinfo.get_cpu_info() - self._state = round(float(self.info['hz_actual_raw'][0])/10**9, 2) + if HZ_ACTUAL_RAW in self.info: + self._state = round( + float(self.info[HZ_ACTUAL_RAW][0]) / 10 ** 9, 2 + ) + else: + self._state = None diff --git a/homeassistant/components/sensor/darksky.py b/homeassistant/components/sensor/darksky.py index 28a51bd8ef2..c68bb2cd3a3 100644 --- a/homeassistant/components/sensor/darksky.py +++ b/homeassistant/components/sensor/darksky.py @@ -14,7 +14,8 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, - CONF_MONITORED_CONDITIONS, CONF_NAME, UNIT_UV_INDEX, CONF_UPDATE_INTERVAL) + CONF_MONITORED_CONDITIONS, CONF_NAME, UNIT_UV_INDEX, CONF_UPDATE_INTERVAL, + CONF_SCAN_INTERVAL, CONF_UPDATE_INTERVAL_INVALIDATION_VERSION) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -30,8 +31,8 @@ CONF_LANGUAGE = 'language' CONF_UNITS = 'units' DEFAULT_LANGUAGE = 'en' - DEFAULT_NAME = 'Dark Sky' +SCAN_INTERVAL = timedelta(seconds=300) DEPRECATED_SENSOR_TYPES = { 'apparent_temperature_max', @@ -162,28 +163,44 @@ CONDITION_PICTURES = { # Language Supported Codes LANGUAGE_CODES = [ 'ar', 'az', 'be', 'bg', 'bs', 'ca', 'cs', 'da', 'de', 'el', 'en', 'es', - 'et', 'fi', 'fr', 'hr', 'hu', 'id', 'is', 'it', 'ja', 'ka', 'kw', 'nb', - 'nl', 'pl', 'pt', 'ro', 'ru', 'sk', 'sl', 'sr', 'sv', 'tet', 'tr', 'uk', - 'x-pig-latin', 'zh', 'zh-tw', + 'et', 'fi', 'fr', 'he', 'hr', 'hu', 'id', 'is', 'it', 'ja', 'ka', 'ko', + 'kw', 'lv', 'nb', 'nl', 'pl', 'pt', 'ro', 'ru', 'sk', 'sl', 'sr', 'sv', + 'tet', 'tr', 'uk', 'x-pig-latin', 'zh', 'zh-tw', ] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_MONITORED_CONDITIONS): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), - vol.Required(CONF_API_KEY): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_UNITS): vol.In(['auto', 'si', 'us', 'ca', 'uk', 'uk2']), - vol.Optional(CONF_LANGUAGE, - default=DEFAULT_LANGUAGE): vol.In(LANGUAGE_CODES), - vol.Inclusive(CONF_LATITUDE, 'coordinates', - 'Latitude and longitude must exist together'): cv.latitude, - vol.Inclusive(CONF_LONGITUDE, 'coordinates', - 'Latitude and longitude must exist together'): cv.longitude, - vol.Optional(CONF_UPDATE_INTERVAL, default=timedelta(seconds=300)): ( - vol.All(cv.time_period, cv.positive_timedelta)), - vol.Optional(CONF_FORECAST): - vol.All(cv.ensure_list, [vol.Range(min=0, max=7)]), -}) +ALLOWED_UNITS = ['auto', 'si', 'us', 'ca', 'uk', 'uk2'] + +PLATFORM_SCHEMA = vol.All( + PLATFORM_SCHEMA.extend({ + vol.Required(CONF_MONITORED_CONDITIONS): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_UNITS): vol.In(ALLOWED_UNITS), + vol.Optional(CONF_LANGUAGE, + default=DEFAULT_LANGUAGE): vol.In(LANGUAGE_CODES), + vol.Inclusive( + CONF_LATITUDE, + 'coordinates', + 'Latitude and longitude must exist together' + ): cv.latitude, + vol.Inclusive( + CONF_LONGITUDE, + 'coordinates', + 'Latitude and longitude must exist together' + ): cv.longitude, + vol.Optional(CONF_UPDATE_INTERVAL): + vol.All(cv.time_period, cv.positive_timedelta), + vol.Optional(CONF_FORECAST): + vol.All(cv.ensure_list, [vol.Range(min=0, max=7)]), + }), + cv.deprecated( + CONF_UPDATE_INTERVAL, + replacement_key=CONF_SCAN_INTERVAL, + invalidation_version=CONF_UPDATE_INTERVAL_INVALIDATION_VERSION, + default=SCAN_INTERVAL + ) +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -191,7 +208,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) language = config.get(CONF_LANGUAGE) - interval = config.get(CONF_UPDATE_INTERVAL) + interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) if CONF_UNITS in config: units = config[CONF_UNITS] diff --git a/homeassistant/components/sensor/dsmr.py b/homeassistant/components/sensor/dsmr.py index ed3b409c49d..8b7d78aa038 100644 --- a/homeassistant/components/sensor/dsmr.py +++ b/homeassistant/components/sensor/dsmr.py @@ -24,9 +24,12 @@ REQUIREMENTS = ['dsmr_parser==0.12'] CONF_DSMR_VERSION = 'dsmr_version' CONF_RECONNECT_INTERVAL = 'reconnect_interval' +CONF_PRECISION = 'precision' DEFAULT_DSMR_VERSION = '2.2' DEFAULT_PORT = '/dev/ttyUSB0' +DEFAULT_PRECISION = 3 + DOMAIN = 'dsmr' ICON_GAS = 'mdi:fire' @@ -45,6 +48,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_DSMR_VERSION, default=DEFAULT_DSMR_VERSION): vol.All( cv.string, vol.In(['5', '4', '2.2'])), vol.Optional(CONF_RECONNECT_INTERVAL, default=30): int, + vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): vol.Coerce(int), }) @@ -146,7 +150,7 @@ async def async_setup_platform(hass, config, async_add_entities, ] # Generate device entities - devices = [DSMREntity(name, obis) for name, obis in obis_mapping] + devices = [DSMREntity(name, obis, config) for name, obis in obis_mapping] # Protocol version specific obis if dsmr_version in ('4', '5'): @@ -156,8 +160,8 @@ async def async_setup_platform(hass, config, async_add_entities, # Add gas meter reading and derivative for usage devices += [ - DSMREntity('Gas Consumption', gas_obis), - DerivativeDSMREntity('Hourly Gas Consumption', gas_obis), + DSMREntity('Gas Consumption', gas_obis, config), + DerivativeDSMREntity('Hourly Gas Consumption', gas_obis, config), ] async_add_entities(devices) @@ -224,10 +228,11 @@ async def async_setup_platform(hass, config, async_add_entities, class DSMREntity(Entity): """Entity reading values from DSMR telegram.""" - def __init__(self, name, obis): + def __init__(self, name, obis, config): """Initialize entity.""" self._name = name self._obis = obis + self._config = config self.telegram = {} def get_dsmr_object_attr(self, attribute): @@ -267,6 +272,11 @@ class DSMREntity(Entity): if self._obis == obis.ELECTRICITY_ACTIVE_TARIFF: return self.translate_tariff(value) + try: + value = round(float(value), self._config[CONF_PRECISION]) + except TypeError: + pass + if value is not None: return value diff --git a/homeassistant/components/sensor/fastdotcom.py b/homeassistant/components/sensor/fastdotcom.py deleted file mode 100644 index 8e975c48574..00000000000 --- a/homeassistant/components/sensor/fastdotcom.py +++ /dev/null @@ -1,115 +0,0 @@ -""" -Support for Fast.com internet speed testing sensor. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.fastdotcom/ -""" -import logging - -import voluptuous as vol - -from homeassistant.components.sensor import DOMAIN, PLATFORM_SCHEMA -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import track_time_change -from homeassistant.helpers.restore_state import RestoreEntity -import homeassistant.util.dt as dt_util - -REQUIREMENTS = ['fastdotcom==0.0.3'] - -_LOGGER = logging.getLogger(__name__) - -CONF_SECOND = 'second' -CONF_MINUTE = 'minute' -CONF_HOUR = 'hour' -CONF_MANUAL = 'manual' - -ICON = 'mdi:speedometer' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_SECOND, default=[0]): - vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(0, 59))]), - vol.Optional(CONF_MINUTE, default=[0]): - 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_MANUAL, default=False): cv.boolean, -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Fast.com sensor.""" - data = SpeedtestData(hass, config) - sensor = SpeedtestSensor(data) - add_entities([sensor]) - - def update(call=None): - """Update service for manual updates.""" - data.update(dt_util.now()) - sensor.update() - - hass.services.register(DOMAIN, 'update_fastdotcom', update) - - -class SpeedtestSensor(RestoreEntity): - """Implementation of a FAst.com sensor.""" - - def __init__(self, speedtest_data): - """Initialize the sensor.""" - self._name = 'Fast.com Download' - self.speedtest_client = speedtest_data - self._state = None - self._unit_of_measurement = 'Mbit/s' - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def state(self): - """Return the state of the device.""" - return self._state - - @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 and update the states.""" - data = self.speedtest_client.data - if data is None: - return - - self._state = data['download'] - - async def async_added_to_hass(self): - """Handle entity which will be added.""" - await super().async_added_to_hass() - state = await self.async_get_last_state() - if not state: - return - self._state = state.state - - @property - def icon(self): - """Return icon.""" - return ICON - - -class SpeedtestData: - """Get the latest data from fast.com.""" - - def __init__(self, hass, config): - """Initialize the data object.""" - self.data = None - 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)) - - def update(self, now): - """Get the latest data from fast.com.""" - from fastdotcom import fast_com - _LOGGER.info("Executing fast.com speedtest") - self.data = {'download': fast_com()} diff --git a/homeassistant/components/sensor/fedex.py b/homeassistant/components/sensor/fedex.py index 02938ff837b..54c319e6441 100644 --- a/homeassistant/components/sensor/fedex.py +++ b/homeassistant/components/sensor/fedex.py @@ -12,7 +12,9 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import (CONF_NAME, CONF_USERNAME, CONF_PASSWORD, - ATTR_ATTRIBUTION, CONF_UPDATE_INTERVAL) + ATTR_ATTRIBUTION, CONF_UPDATE_INTERVAL, + CONF_SCAN_INTERVAL, + CONF_UPDATE_INTERVAL_INVALIDATION_VERSION) from homeassistant.helpers.entity import Entity from homeassistant.util import slugify from homeassistant.util import Throttle @@ -31,13 +33,23 @@ ICON = 'mdi:package-variant-closed' STATUS_DELIVERED = 'delivered' -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_UPDATE_INTERVAL, default=timedelta(seconds=1800)): - vol.All(cv.time_period, cv.positive_timedelta), -}) +SCAN_INTERVAL = timedelta(seconds=1800) + +PLATFORM_SCHEMA = vol.All( + PLATFORM_SCHEMA.extend({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_UPDATE_INTERVAL): + vol.All(cv.time_period, cv.positive_timedelta), + }), + cv.deprecated( + CONF_UPDATE_INTERVAL, + replacement_key=CONF_SCAN_INTERVAL, + invalidation_version=CONF_UPDATE_INTERVAL_INVALIDATION_VERSION, + default=SCAN_INTERVAL + ) +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -45,7 +57,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): import fedexdeliverymanager name = config.get(CONF_NAME) - update_interval = config.get(CONF_UPDATE_INTERVAL) + update_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) try: cookie = hass.config.path(COOKIE) diff --git a/homeassistant/components/sensor/github.py b/homeassistant/components/sensor/github.py new file mode 100644 index 00000000000..335dbc668d9 --- /dev/null +++ b/homeassistant/components/sensor/github.py @@ -0,0 +1,215 @@ +""" +Support for GitHub. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.github/ +""" +from datetime import timedelta +import logging +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + ATTR_NAME, CONF_ACCESS_TOKEN, CONF_NAME, CONF_PATH, CONF_URL) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['PyGithub==1.43.5'] + +_LOGGER = logging.getLogger(__name__) + +CONF_REPOS = 'repositories' + +ATTR_LATEST_COMMIT_MESSAGE = 'latest_commit_message' +ATTR_LATEST_COMMIT_SHA = 'latest_commit_sha' +ATTR_LATEST_RELEASE_URL = 'latest_release_url' +ATTR_LATEST_OPEN_ISSUE_URL = 'latest_open_issue_url' +ATTR_OPEN_ISSUES = 'open_issues' +ATTR_LATEST_OPEN_PULL_REQUEST_URL = 'latest_open_pull_request_url' +ATTR_OPEN_PULL_REQUESTS = 'open_pull_requests' +ATTR_PATH = 'path' +ATTR_STARGAZERS = 'stargazers' + +DEFAULT_NAME = 'GitHub' + +SCAN_INTERVAL = timedelta(seconds=300) + +REPO_SCHEMA = vol.Schema({ + vol.Required(CONF_PATH): cv.string, + vol.Optional(CONF_NAME): cv.string +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_ACCESS_TOKEN): cv.string, + vol.Optional(CONF_URL): cv.url, + vol.Required(CONF_REPOS): + vol.All(cv.ensure_list, [REPO_SCHEMA]) +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the GitHub sensor platform.""" + sensors = [] + for repository in config[CONF_REPOS]: + data = GitHubData( + repository=repository, + access_token=config.get(CONF_ACCESS_TOKEN), + server_url=config.get(CONF_URL) + ) + if data.setup_error is True: + _LOGGER.error("Error setting up GitHub platform. %s", + "Check previous errors for details") + return + sensors.append(GitHubSensor(data)) + add_entities(sensors, True) + + +class GitHubSensor(Entity): + """Representation of a GitHub sensor.""" + + def __init__(self, github_data): + """Initialize the GitHub sensor.""" + self._unique_id = github_data.repository_path + self._name = None + self._state = None + self._available = False + self._repository_path = None + self._latest_commit_message = None + self._latest_commit_sha = None + self._latest_release_url = None + self._open_issue_count = None + self._latest_open_issue_url = None + self._pull_request_count = None + self._latest_open_pr_url = None + self._stargazers = None + self._github_data = github_data + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def unique_id(self): + """Return unique ID for the sensor.""" + return self._unique_id + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def available(self): + """Return True if entity is available.""" + return self._available + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_PATH: self._repository_path, + ATTR_NAME: self._name, + ATTR_LATEST_COMMIT_MESSAGE: self._latest_commit_message, + ATTR_LATEST_COMMIT_SHA: self._latest_commit_sha, + ATTR_LATEST_RELEASE_URL: self._latest_release_url, + ATTR_LATEST_OPEN_ISSUE_URL: self._latest_open_issue_url, + ATTR_OPEN_ISSUES: self._open_issue_count, + ATTR_LATEST_OPEN_PULL_REQUEST_URL: self._latest_open_pr_url, + ATTR_OPEN_PULL_REQUESTS: self._pull_request_count, + ATTR_STARGAZERS: self._stargazers + } + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return 'mdi:github-circle' + + def update(self): + """Collect updated data from GitHub API.""" + self._github_data.update() + + self._name = self._github_data.name + self._state = self._github_data.latest_commit_sha + self._repository_path = self._github_data.repository_path + self._available = self._github_data.available + self._latest_commit_message = self._github_data.latest_commit_message + self._latest_commit_sha = self._github_data.latest_commit_sha + self._latest_release_url = self._github_data.latest_release_url + self._open_issue_count = self._github_data.open_issue_count + self._latest_open_issue_url = self._github_data.latest_open_issue_url + self._pull_request_count = self._github_data.pull_request_count + self._latest_open_pr_url = self._github_data.latest_open_pr_url + self._stargazers = self._github_data.stargazers + + +class GitHubData(): + """GitHub Data object.""" + + def __init__(self, repository, access_token=None, server_url=None): + """Set up GitHub.""" + import github + + self._github = github + + self.setup_error = False + + try: + if server_url is not None: + server_url += "/api/v3" + self._github_obj = github.Github( + access_token, base_url=server_url) + else: + self._github_obj = github.Github(access_token) + + self.repository_path = repository[CONF_PATH] + + repo = self._github_obj.get_repo(self.repository_path) + except self._github.GithubException as err: + _LOGGER.error("GitHub error for %s: %s", self.repository_path, err) + self.setup_error = True + return + + self.name = repository.get(CONF_NAME, repo.name) + + self.available = False + self.latest_commit_message = None + self.latest_commit_sha = None + self.latest_release_url = None + self.open_issue_count = None + self.latest_open_issue_url = None + self.pull_request_count = None + self.latest_open_pr_url = None + self.stargazers = None + + def update(self): + """Update GitHub Sensor.""" + try: + repo = self._github_obj.get_repo(self.repository_path) + + self.stargazers = repo.stargazers_count + + open_issues = repo.get_issues(state='open', sort='created') + if open_issues is not None: + self.open_issue_count = open_issues.totalCount + if open_issues.totalCount > 0: + self.latest_open_issue_url = open_issues[0].html_url + + open_pull_requests = repo.get_pulls(state='open', sort='created') + if open_pull_requests is not None: + self.pull_request_count = open_pull_requests.totalCount + if open_pull_requests.totalCount > 0: + self.latest_open_pr_url = open_pull_requests[0].html_url + + latest_commit = repo.get_commits()[0] + self.latest_commit_sha = latest_commit.sha + self.latest_commit_message = latest_commit.commit.message + + releases = repo.get_releases() + if releases and releases.totalCount > 0: + self.latest_release_url = releases[0].html_url + + self.available = True + except self._github.GithubException as err: + _LOGGER.error("GitHub error for %s: %s", self.repository_path, err) + self.available = False diff --git a/homeassistant/components/sensor/gtfs.py b/homeassistant/components/sensor/gtfs.py index 3ccc60457b6..94f21287e39 100644 --- a/homeassistant/components/sensor/gtfs.py +++ b/homeassistant/components/sensor/gtfs.py @@ -29,6 +29,16 @@ DEFAULT_NAME = 'GTFS Sensor' DEFAULT_PATH = 'gtfs' ICON = 'mdi:train' +ICONS = { + 0: 'mdi:tram', + 1: 'mdi:subway', + 2: 'mdi:train', + 3: 'mdi:bus', + 4: 'mdi:ferry', + 5: 'mdi:train-variant', + 6: 'mdi:gondola', + 7: 'mdi:stairs', +} TIME_FORMAT = '%Y-%m-%d %H:%M:%S' @@ -55,22 +65,20 @@ def get_next_departure(sched, start_station_id, end_station_id, offset): sql_query = text(""" SELECT trip.trip_id, trip.route_id, - time(origin_stop_time.departure_time), - time(destination_stop_time.arrival_time), - time(origin_stop_time.arrival_time), - time(origin_stop_time.departure_time), - origin_stop_time.drop_off_type, - origin_stop_time.pickup_type, - origin_stop_time.shape_dist_traveled, - origin_stop_time.stop_headsign, - origin_stop_time.stop_sequence, - time(destination_stop_time.arrival_time), - time(destination_stop_time.departure_time), - destination_stop_time.drop_off_type, - destination_stop_time.pickup_type, - destination_stop_time.shape_dist_traveled, - destination_stop_time.stop_headsign, - destination_stop_time.stop_sequence + time(origin_stop_time.arrival_time) AS origin_arrival_time, + time(origin_stop_time.departure_time) AS origin_depart_time, + origin_stop_time.drop_off_type AS origin_drop_off_type, + origin_stop_time.pickup_type AS origin_pickup_type, + origin_stop_time.shape_dist_traveled AS origin_shape_dist_traveled, + origin_stop_time.stop_headsign AS origin_stop_headsign, + origin_stop_time.stop_sequence AS origin_stop_sequence, + time(destination_stop_time.arrival_time) AS dest_arrival_time, + time(destination_stop_time.departure_time) AS dest_depart_time, + destination_stop_time.drop_off_type AS dest_drop_off_type, + destination_stop_time.pickup_type AS dest_pickup_type, + destination_stop_time.shape_dist_traveled AS dest_dist_traveled, + destination_stop_time.stop_headsign AS dest_stop_headsign, + destination_stop_time.stop_sequence AS dest_stop_sequence FROM trips trip INNER JOIN calendar calendar ON trip.service_id = calendar.service_id @@ -83,13 +91,14 @@ def get_next_departure(sched, start_station_id, end_station_id, offset): INNER JOIN stops end_station ON destination_stop_time.stop_id = end_station.stop_id WHERE calendar.{day_name} = 1 - AND time(origin_stop_time.departure_time) > time(:now_str) + AND origin_depart_time > time(:now_str) AND start_station.stop_id = :origin_station_id AND end_station.stop_id = :end_station_id - AND origin_stop_time.stop_sequence < destination_stop_time.stop_sequence + AND origin_stop_sequence < dest_stop_sequence AND calendar.start_date <= :today AND calendar.end_date >= :today - ORDER BY origin_stop_time.departure_time LIMIT 1; + ORDER BY origin_stop_time.departure_time + LIMIT 1 """.format(day_name=day_name)) result = sched.engine.execute(sql_query, now_str=now_str, origin_station_id=origin_station.id, @@ -102,46 +111,46 @@ def get_next_departure(sched, start_station_id, end_station_id, offset): if item == {}: return None - departure_time_string = '{} {}'.format(today, item[2]) - arrival_time_string = '{} {}'.format(today, item[3]) - departure_time = datetime.datetime.strptime(departure_time_string, - TIME_FORMAT) - arrival_time = datetime.datetime.strptime(arrival_time_string, - TIME_FORMAT) + origin_arrival_time = '{} {}'.format(today, item['origin_arrival_time']) + origin_depart_time = '{} {}'.format(today, item['origin_depart_time']) + dest_arrival_time = '{} {}'.format(today, item['dest_arrival_time']) + dest_depart_time = '{} {}'.format(today, item['dest_depart_time']) - seconds_until = (departure_time-datetime.datetime.now()).total_seconds() + depart_time = datetime.datetime.strptime(origin_depart_time, TIME_FORMAT) + arrival_time = datetime.datetime.strptime(dest_arrival_time, TIME_FORMAT) + + seconds_until = (depart_time - datetime.datetime.now()).total_seconds() minutes_until = int(seconds_until / 60) - route = sched.routes_by_id(item[1])[0] - - origin_stoptime_arrival_time = '{} {}'.format(today, item[4]) - origin_stoptime_departure_time = '{} {}'.format(today, item[5]) - dest_stoptime_arrival_time = '{} {}'.format(today, item[11]) - dest_stoptime_depart_time = '{} {}'.format(today, item[12]) + route = sched.routes_by_id(item['route_id'])[0] origin_stop_time_dict = { - 'Arrival Time': origin_stoptime_arrival_time, - 'Departure Time': origin_stoptime_departure_time, - 'Drop Off Type': item[6], 'Pickup Type': item[7], - 'Shape Dist Traveled': item[8], 'Headsign': item[9], - 'Sequence': item[10] + 'Arrival Time': origin_arrival_time, + 'Departure Time': origin_depart_time, + 'Drop Off Type': item['origin_drop_off_type'], + 'Pickup Type': item['origin_pickup_type'], + 'Shape Dist Traveled': item['origin_shape_dist_traveled'], + 'Headsign': item['origin_stop_headsign'], + 'Sequence': item['origin_stop_sequence'] } destination_stop_time_dict = { - 'Arrival Time': dest_stoptime_arrival_time, - 'Departure Time': dest_stoptime_depart_time, - 'Drop Off Type': item[13], 'Pickup Type': item[14], - 'Shape Dist Traveled': item[15], 'Headsign': item[16], - 'Sequence': item[17] + 'Arrival Time': dest_arrival_time, + 'Departure Time': dest_depart_time, + 'Drop Off Type': item['dest_drop_off_type'], + 'Pickup Type': item['dest_pickup_type'], + 'Shape Dist Traveled': item['dest_dist_traveled'], + 'Headsign': item['dest_stop_headsign'], + 'Sequence': item['dest_stop_sequence'] } return { - 'trip_id': item[0], - 'trip': sched.trips_by_id(item[0])[0], + 'trip_id': item['trip_id'], + 'trip': sched.trips_by_id(item['trip_id'])[0], 'route': route, 'agency': sched.agencies_by_id(route.agency_id)[0], 'origin_station': origin_station, - 'departure_time': departure_time, + 'departure_time': depart_time, 'destination_station': destination_station, 'arrival_time': arrival_time, 'seconds_until_departure': seconds_until, @@ -193,9 +202,10 @@ class GTFSDepartureSensor(Entity): self.destination = destination self._offset = offset self._custom_name = name + self._icon = ICON self._name = '' self._unit_of_measurement = 'min' - self._state = 0 + self._state = None self._attributes = {} self.lock = threading.Lock() self.update() @@ -223,7 +233,7 @@ class GTFSDepartureSensor(Entity): @property def icon(self): """Icon to use in the frontend, if any.""" - return ICON + return self._icon def update(self): """Get the latest data from GTFS and update the states.""" @@ -231,7 +241,7 @@ class GTFSDepartureSensor(Entity): self._departure = get_next_departure( self._pygtfs, self.origin, self.destination, self._offset) if not self._departure: - self._state = 0 + self._state = None self._attributes = {'Info': 'No more departures today'} if self._name == '': self._name = (self._custom_name or DEFAULT_NAME) @@ -257,6 +267,8 @@ class GTFSDepartureSensor(Entity): self._attributes = {} self._attributes['offset'] = self._offset.seconds / 60 + self._icon = ICONS.get(route.route_type, ICON) + def dict_for_table(resource): """Return a dict for the SQLAlchemy resource given.""" return dict((col, getattr(resource, col)) diff --git a/homeassistant/components/sensor/imap.py b/homeassistant/components/sensor/imap.py index b8d363417c2..571d05e78e9 100644 --- a/homeassistant/components/sensor/imap.py +++ b/homeassistant/components/sensor/imap.py @@ -20,7 +20,7 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['aioimaplib==0.7.13'] +REQUIREMENTS = ['aioimaplib==0.7.15'] CONF_SERVER = 'server' CONF_FOLDER = 'folder' diff --git a/homeassistant/components/sensor/integration.py b/homeassistant/components/sensor/integration.py index 9426730be35..9250c8dde05 100644 --- a/homeassistant/components/sensor/integration.py +++ b/homeassistant/components/sensor/integration.py @@ -26,6 +26,12 @@ CONF_ROUND_DIGITS = 'round' CONF_UNIT_PREFIX = 'unit_prefix' CONF_UNIT_TIME = 'unit_time' CONF_UNIT_OF_MEASUREMENT = 'unit' +CONF_METHOD = 'method' + +TRAPEZOIDAL_METHOD = 'trapezoidal' +LEFT_METHOD = 'left' +RIGHT_METHOD = 'right' +INTEGRATION_METHOD = [TRAPEZOIDAL_METHOD, LEFT_METHOD, RIGHT_METHOD] # SI Metric prefixes UNIT_PREFIXES = {None: 1, @@ -49,7 +55,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_ROUND_DIGITS, default=DEFAULT_ROUND): vol.Coerce(int), vol.Optional(CONF_UNIT_PREFIX, default=None): vol.In(UNIT_PREFIXES), vol.Optional(CONF_UNIT_TIME, default='h'): vol.In(UNIT_TIME), - vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_METHOD, default=TRAPEZOIDAL_METHOD): + vol.In(INTEGRATION_METHOD), }) @@ -61,7 +69,8 @@ async def async_setup_platform(hass, config, async_add_entities, config[CONF_ROUND_DIGITS], config[CONF_UNIT_PREFIX], config[CONF_UNIT_TIME], - config.get(CONF_UNIT_OF_MEASUREMENT)) + config.get(CONF_UNIT_OF_MEASUREMENT), + config[CONF_METHOD]) async_add_entities([integral]) @@ -70,11 +79,12 @@ class IntegrationSensor(RestoreEntity): """Representation of an integration sensor.""" def __init__(self, source_entity, name, round_digits, unit_prefix, - unit_time, unit_of_measurement): + unit_time, unit_of_measurement, integration_method): """Initialize the integration sensor.""" self._sensor_source_id = source_entity self._round_digits = round_digits self._state = 0 + self._method = integration_method self._name = name if name is not None\ else '{} integral'.format(source_entity) @@ -117,12 +127,19 @@ class IntegrationSensor(RestoreEntity): try: # integration as the Riemann integral of previous measures. + area = 0 elapsed_time = (new_state.last_updated - old_state.last_updated).total_seconds() - area = (Decimal(new_state.state) - + Decimal(old_state.state))*Decimal(elapsed_time)/2 - integral = area / (self._unit_prefix * self._unit_time) + if self._method == TRAPEZOIDAL_METHOD: + area = (Decimal(new_state.state) + + Decimal(old_state.state))*Decimal(elapsed_time)/2 + elif self._method == LEFT_METHOD: + area = Decimal(old_state.state)*Decimal(elapsed_time) + elif self._method == RIGHT_METHOD: + area = Decimal(new_state.state)*Decimal(elapsed_time) + + integral = area / (self._unit_prefix * self._unit_time) assert isinstance(integral, Decimal) except ValueError as err: _LOGGER.warning("While calculating integration: %s", err) diff --git a/homeassistant/components/sensor/jewish_calendar.py b/homeassistant/components/sensor/jewish_calendar.py index cc226337f02..65aeaf7fba9 100644 --- a/homeassistant/components/sensor/jewish_calendar.py +++ b/homeassistant/components/sensor/jewish_calendar.py @@ -4,7 +4,6 @@ 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 @@ -140,12 +139,6 @@ class JewishCalSensor(Entity): _LOGGER.debug("Now: %s Sunset: %s", now, sunset) - if now > sunset: - today += timedelta(1) - - date = hdate.HDate( - today, diaspora=self.diaspora, hebrew=self._hebrew) - location = hdate.Location(latitude=self.latitude, longitude=self.longitude, timezone=self.timezone, @@ -158,27 +151,43 @@ class JewishCalSensor(Entity): candle_lighting_offset=self.candle_lighting_offset, havdalah_offset=self.havdalah_offset, hebrew=self._hebrew) + date = hdate.HDate( + today, diaspora=self.diaspora, hebrew=self._hebrew) + lagging_date = date + + # Advance Hebrew date if sunset has passed. + # Not all sensors should advance immediately when the Hebrew date + # officially changes (i.e. after sunset), hence lagging_date. + if now > sunset: + date = date.next_day + today_times = make_zmanim(today) + if today_times.havdalah and now > today_times.havdalah: + lagging_date = lagging_date.next_day + + # Terminology note: by convention in py-libhdate library, "upcoming" + # refers to "current" or "upcoming" dates. if self.type == 'date': self._state = date.hebrew_date elif self.type == 'weekly_portion': # Compute the weekly portion based on the upcoming shabbat. - self._state = date.upcoming_shabbat.parasha + self._state = lagging_date.upcoming_shabbat.parasha elif self.type == 'holiday_name': self._state = date.holiday_description elif self.type == 'holyness': self._state = date.holiday_type elif self.type == 'upcoming_shabbat_candle_lighting': - times = make_zmanim(date.upcoming_shabbat.previous_day.gdate) - self._state = times.candle_lighting - elif self.type == 'upcoming_candle_lighting': - times = make_zmanim(date.upcoming_shabbat_or_yom_tov.first_day + times = make_zmanim(lagging_date.upcoming_shabbat .previous_day.gdate) self._state = times.candle_lighting + elif self.type == 'upcoming_candle_lighting': + times = make_zmanim(lagging_date.upcoming_shabbat_or_yom_tov + .first_day.previous_day.gdate) + self._state = times.candle_lighting elif self.type == 'upcoming_shabbat_havdalah': - times = make_zmanim(date.upcoming_shabbat.gdate) + times = make_zmanim(lagging_date.upcoming_shabbat.gdate) self._state = times.havdalah elif self.type == 'upcoming_havdalah': - times = make_zmanim(date.upcoming_shabbat_or_yom_tov + times = make_zmanim(lagging_date.upcoming_shabbat_or_yom_tov .last_day.gdate) self._state = times.havdalah elif self.type == 'issur_melacha_in_effect': diff --git a/homeassistant/components/sensor/miflora.py b/homeassistant/components/sensor/miflora.py index 412b339caf3..91f873f5a2d 100644 --- a/homeassistant/components/sensor/miflora.py +++ b/homeassistant/components/sensor/miflora.py @@ -30,13 +30,13 @@ DEFAULT_NAME = 'Mi Flora' SCAN_INTERVAL = timedelta(seconds=1200) -# Sensor types are defined like: Name, units +# Sensor types are defined like: Name, units, icon SENSOR_TYPES = { - 'temperature': ['Temperature', '°C'], - 'light': ['Light intensity', 'lx'], - 'moisture': ['Moisture', '%'], - 'conductivity': ['Conductivity', 'µS/cm'], - 'battery': ['Battery', '%'], + 'temperature': ['Temperature', '°C', 'mdi:thermometer'], + 'light': ['Light intensity', 'lx', 'mdi:white-balance-sunny'], + 'moisture': ['Moisture', '%', 'mdi:water-percent'], + 'conductivity': ['Conductivity', 'µS/cm', 'mdi:flash-circle'], + 'battery': ['Battery', '%', 'mdi:battery-charging'], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -75,13 +75,14 @@ async def async_setup_platform(hass, config, async_add_entities, for parameter in config[CONF_MONITORED_CONDITIONS]: name = SENSOR_TYPES[parameter][0] unit = SENSOR_TYPES[parameter][1] + icon = SENSOR_TYPES[parameter][2] prefix = config.get(CONF_NAME) if prefix: name = "{} {}".format(prefix, name) devs.append(MiFloraSensor( - poller, parameter, name, unit, force_update, median)) + poller, parameter, name, unit, icon, force_update, median)) async_add_entities(devs) @@ -89,11 +90,13 @@ async def async_setup_platform(hass, config, async_add_entities, class MiFloraSensor(Entity): """Implementing the MiFlora sensor.""" - def __init__(self, poller, parameter, name, unit, force_update, median): + def __init__( + self, poller, parameter, name, unit, icon, force_update, median): """Initialize the sensor.""" self.poller = poller self.parameter = parameter self._unit = unit + self._icon = icon self._name = name self._state = None self.data = [] @@ -126,6 +129,11 @@ class MiFloraSensor(Entity): """Return the units of measurement.""" return self._unit + @property + def icon(self): + """Return the icon of the sensor.""" + return self._icon + @property def force_update(self): """Force update.""" diff --git a/homeassistant/components/sensor/mqtt_room.py b/homeassistant/components/sensor/mqtt_room.py index 39c202ef01c..b52f039281c 100644 --- a/homeassistant/components/sensor/mqtt_room.py +++ b/homeassistant/components/sensor/mqtt_room.py @@ -38,12 +38,11 @@ DEFAULT_TOPIC = 'room_presence' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_DEVICE_ID): cv.string, - vol.Required(CONF_STATE_TOPIC, default=DEFAULT_TOPIC): cv.string, vol.Required(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, vol.Optional(CONF_AWAY_TIMEOUT, default=DEFAULT_AWAY_TIMEOUT): cv.positive_int, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string -}) +}).extend(mqtt.MQTT_RO_PLATFORM_SCHEMA.schema) MQTT_PAYLOAD = vol.Schema(vol.All(json.loads, vol.Schema({ vol.Required(ATTR_ID): cv.string, diff --git a/homeassistant/components/sensor/rejseplanen.py b/homeassistant/components/sensor/rejseplanen.py new file mode 100755 index 00000000000..bade1bd6315 --- /dev/null +++ b/homeassistant/components/sensor/rejseplanen.py @@ -0,0 +1,228 @@ +""" +Support for Rejseplanen information from rejseplanen.dk. + +For more info on the API see: +https://help.rejseplanen.dk/hc/en-us/articles/214174465-Rejseplanen-s-API + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.rejseplanen/ +""" +import logging +from datetime import timedelta, datetime +from operator import itemgetter + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['rjpl==0.3.5'] +_LOGGER = logging.getLogger(__name__) + +ATTR_STOP_ID = 'Stop ID' +ATTR_STOP_NAME = 'Stop' +ATTR_ROUTE = 'Route' +ATTR_TYPE = 'Type' +ATTR_DIRECTION = "Direction" +ATTR_DUE_IN = 'Due in' +ATTR_DUE_AT = 'Due at' +ATTR_NEXT_UP = 'Later departure' + +CONF_ATTRIBUTION = "Data provided by rejseplanen.dk" +CONF_STOP_ID = 'stop_id' +CONF_ROUTE = 'route' +CONF_DIRECTION = 'direction' +CONF_DEPARTURE_TYPE = 'departure_type' + +DEFAULT_NAME = 'Next departure' +ICON = 'mdi:bus' + +SCAN_INTERVAL = timedelta(minutes=1) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_STOP_ID): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_ROUTE, default=[]): + vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_DIRECTION, default=[]): + vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_DEPARTURE_TYPE, default=[]): + vol.All(cv.ensure_list, [vol.In(list(['BUS', 'EXB', 'M', + 'S', 'REG']))]) +}) + + +def due_in_minutes(timestamp): + """Get the time in minutes from a timestamp. + + The timestamp should be in the format day.month.year hour:minute + """ + diff = datetime.strptime( + timestamp, "%d.%m.%y %H:%M") - dt_util.now().replace(tzinfo=None) + + return int(diff.total_seconds() // 60) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Rejseplanen transport sensor.""" + name = config[CONF_NAME] + stop_id = config[CONF_STOP_ID] + route = config.get(CONF_ROUTE) + direction = config[CONF_DIRECTION] + departure_type = config[CONF_DEPARTURE_TYPE] + + data = PublicTransportData(stop_id, route, direction, departure_type) + add_devices([RejseplanenTransportSensor(data, + stop_id, + route, + direction, + name)], + True) + + +class RejseplanenTransportSensor(Entity): + """Implementation of Rejseplanen transport sensor.""" + + def __init__(self, data, stop_id, route, direction, name): + """Initialize the sensor.""" + self.data = data + self._name = name + self._stop_id = stop_id + self._route = route + self._direction = direction + 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: + next_up = None + if len(self._times) > 1: + next_up = ('{} towards ' + '{} in ' + '{} from ' + '{}'.format(self._times[1][ATTR_ROUTE], + self._times[1][ATTR_DIRECTION], + str(self._times[1][ATTR_DUE_IN]), + self._times[1][ATTR_STOP_NAME])) + params = { + ATTR_DUE_IN: str(self._times[0][ATTR_DUE_IN]), + ATTR_DUE_AT: self._times[0][ATTR_DUE_AT], + ATTR_TYPE: self._times[0][ATTR_TYPE], + ATTR_ROUTE: self._times[0][ATTR_ROUTE], + ATTR_DIRECTION: self._times[0][ATTR_DIRECTION], + ATTR_STOP_NAME: self._times[0][ATTR_STOP_NAME], + ATTR_STOP_ID: self._stop_id, + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_NEXT_UP: next_up + } + return {k: v for k, v in params.items() if v} + + @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 rejseplanen.dk and update the states.""" + self.data.update() + self._times = self.data.info + try: + self._state = self._times[0][ATTR_DUE_IN] + except TypeError: + pass + + +class PublicTransportData(): + """The Class for handling the data retrieval.""" + + def __init__(self, stop_id, route, direction, departure_type): + """Initialize the data object.""" + self.stop_id = stop_id + self.route = route + self.direction = direction + self.departure_type = departure_type + self.info = self.empty_result() + + def empty_result(self): + """Object returned when no departures are found.""" + return [{ATTR_DUE_IN: 'n/a', + ATTR_DUE_AT: 'n/a', + ATTR_TYPE: 'n/a', + ATTR_ROUTE: self.route, + ATTR_DIRECTION: 'n/a', + ATTR_STOP_NAME: 'n/a'}] + + def update(self): + """Get the latest data from rejseplanen.""" + import rjpl + self.info = [] + + try: + results = rjpl.departureBoard(int(self.stop_id), timeout=5) + except rjpl.rjplAPIError as error: + _LOGGER.debug("API returned error: %s", error) + self.info = self.empty_result() + return + except (rjpl.rjplConnectionError, rjpl.rjplHTTPError): + _LOGGER.debug("Error occured while connecting to the API") + self.info = self.empty_result() + return + + # Filter result + results = [d for d in results if 'cancelled' not in d] + if self.route: + results = [d for d in results if d['name'] in self.route] + if self.direction: + results = [d for d in results if d['direction'] in self.direction] + if self.departure_type: + results = [d for d in results if d['type'] in self.departure_type] + + for item in results: + route = item.get('name') + + due_at_date = item.get('rtDate') + due_at_time = item.get('rtTime') + + if due_at_date is None: + due_at_date = item.get('date') # Scheduled date + if due_at_time is None: + due_at_time = item.get('time') # Scheduled time + + if (due_at_date is not None and + due_at_time is not None and + route is not None): + due_at = '{} {}'.format(due_at_date, due_at_time) + + departure_data = {ATTR_DUE_IN: due_in_minutes(due_at), + ATTR_DUE_AT: due_at, + ATTR_TYPE: item.get('type'), + ATTR_ROUTE: route, + ATTR_DIRECTION: item.get('direction'), + ATTR_STOP_NAME: item.get('stop')} + self.info.append(departure_data) + + if not self.info: + _LOGGER.debug("No departures with given parameters") + self.info = self.empty_result() + + # Sort the data by time + self.info = sorted(self.info, key=itemgetter(ATTR_DUE_IN)) diff --git a/homeassistant/components/sensor/speedtest.py b/homeassistant/components/sensor/speedtest.py deleted file mode 100644 index f834b51b064..00000000000 --- a/homeassistant/components/sensor/speedtest.py +++ /dev/null @@ -1,171 +0,0 @@ -""" -Support for Speedtest.net. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.speedtest/ -""" -import logging - -import voluptuous as vol - -from homeassistant.components.sensor import DOMAIN, PLATFORM_SCHEMA -from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import track_time_change -from homeassistant.helpers.restore_state import RestoreEntity -import homeassistant.util.dt as dt_util - -REQUIREMENTS = ['speedtest-cli==2.0.2'] - -_LOGGER = logging.getLogger(__name__) - -ATTR_BYTES_RECEIVED = 'bytes_received' -ATTR_BYTES_SENT = 'bytes_sent' -ATTR_SERVER_COUNTRY = 'server_country' -ATTR_SERVER_HOST = 'server_host' -ATTR_SERVER_ID = 'server_id' -ATTR_SERVER_LATENCY = 'latency' -ATTR_SERVER_NAME = 'server_name' - -CONF_ATTRIBUTION = "Data retrieved from Speedtest by Ookla" -CONF_SECOND = 'second' -CONF_MINUTE = 'minute' -CONF_HOUR = 'hour' -CONF_SERVER_ID = 'server_id' -CONF_MANUAL = 'manual' - -ICON = 'mdi:speedometer' - -SENSOR_TYPES = { - 'ping': ['Ping', 'ms'], - 'download': ['Download', 'Mbit/s'], - 'upload': ['Upload', 'Mbit/s'], -} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_MONITORED_CONDITIONS): - vol.All(cv.ensure_list, [vol.In(list(SENSOR_TYPES))]), - 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, - vol.Optional(CONF_MINUTE, default=[0]): - vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(0, 59))]), - vol.Optional(CONF_SECOND, default=[0]): - vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(0, 59))]), - vol.Optional(CONF_SERVER_ID): cv.positive_int, -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Speedtest sensor.""" - data = SpeedtestData(hass, config) - - dev = [] - for sensor in config[CONF_MONITORED_CONDITIONS]: - dev.append(SpeedtestSensor(data, sensor)) - - add_entities(dev) - - def update(call=None): - """Update service for manual updates.""" - data.update(dt_util.now()) - for sensor in dev: - sensor.update() - - hass.services.register(DOMAIN, 'update_speedtest', update) - - -class SpeedtestSensor(RestoreEntity): - """Implementation of a speedtest.net sensor.""" - - def __init__(self, speedtest_data, sensor_type): - """Initialize the sensor.""" - self._name = SENSOR_TYPES[sensor_type][0] - self.speedtest_client = speedtest_data - self.type = sensor_type - self._state = None - self._data = None - self._unit_of_measurement = SENSOR_TYPES[self.type][1] - - @property - def name(self): - """Return the name of the sensor.""" - return '{} {}'.format('Speedtest', self._name) - - @property - def state(self): - """Return the state of the device.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement - - @property - def icon(self): - """Return icon.""" - return ICON - - @property - def device_state_attributes(self): - """Return the state attributes.""" - if self._data is not None: - return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - ATTR_BYTES_RECEIVED: self._data['bytes_received'], - ATTR_BYTES_SENT: self._data['bytes_sent'], - ATTR_SERVER_COUNTRY: self._data['server']['country'], - ATTR_SERVER_ID: self._data['server']['id'], - ATTR_SERVER_LATENCY: self._data['server']['latency'], - ATTR_SERVER_NAME: self._data['server']['name'], - } - - def update(self): - """Get the latest data and update the states.""" - self._data = self.speedtest_client.data - if self._data is None: - return - - if self.type == 'ping': - self._state = self._data['ping'] - elif self.type == 'download': - self._state = round(self._data['download'] / 10**6, 2) - elif self.type == 'upload': - self._state = round(self._data['upload'] / 10**6, 2) - - async def async_added_to_hass(self): - """Handle all entity which are about to be added.""" - await super().async_added_to_hass() - state = await self.async_get_last_state() - if not state: - return - self._state = state.state - - -class SpeedtestData: - """Get the latest data from speedtest.net.""" - - def __init__(self, hass, config): - """Initialize the data object.""" - self.data = None - self._server_id = config.get(CONF_SERVER_ID) - 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)) - - def update(self, now): - """Get the latest data from speedtest.net.""" - import speedtest - _LOGGER.debug("Executing speedtest...") - - servers = [] if self._server_id is None else [self._server_id] - - speed = speedtest.Speedtest() - speed.get_servers(servers) - speed.get_best_server() - speed.download() - speed.upload() - - self.data = speed.results.dict() diff --git a/homeassistant/components/sensor/sql.py b/homeassistant/components/sensor/sql.py index 136e3cac23b..f780158dd4e 100644 --- a/homeassistant/components/sensor/sql.py +++ b/homeassistant/components/sensor/sql.py @@ -20,7 +20,7 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['sqlalchemy==1.2.16'] +REQUIREMENTS = ['sqlalchemy==1.2.17'] CONF_COLUMN_NAME = 'column' CONF_QUERIES = 'queries' diff --git a/homeassistant/components/sensor/ted5000.py b/homeassistant/components/sensor/ted5000.py index a6ea7cbd534..82a1ec8bb68 100644 --- a/homeassistant/components/sensor/ted5000.py +++ b/homeassistant/components/sensor/ted5000.py @@ -114,7 +114,4 @@ class Ted5000Gateway: voltage = int(doc["LiveData"]["Voltage"]["MTU%d" % mtu] ["VoltageNow"]) - if power == 0 or voltage == 0: - continue - else: - self.data[mtu] = {'W': power, 'V': voltage / 10} + self.data[mtu] = {'W': power, 'V': voltage / 10} diff --git a/homeassistant/components/sensor/thermoworks_smoke.py b/homeassistant/components/sensor/thermoworks_smoke.py index e81a3974176..0c6cddd9fcd 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.7', 'stringcase==1.2.0'] +REQUIREMENTS = ['thermoworks_smoke==0.1.8', 'stringcase==1.2.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/ups.py b/homeassistant/components/sensor/ups.py index 44ecdc433c5..e4aab555050 100644 --- a/homeassistant/components/sensor/ups.py +++ b/homeassistant/components/sensor/ups.py @@ -12,7 +12,9 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import (CONF_NAME, CONF_USERNAME, CONF_PASSWORD, - ATTR_ATTRIBUTION, CONF_UPDATE_INTERVAL) + ATTR_ATTRIBUTION, CONF_UPDATE_INTERVAL, + CONF_SCAN_INTERVAL, + CONF_UPDATE_INTERVAL_INVALIDATION_VERSION) from homeassistant.helpers.entity import Entity from homeassistant.util import slugify from homeassistant.util import Throttle @@ -28,13 +30,23 @@ COOKIE = 'upsmychoice_cookies.pickle' ICON = 'mdi:package-variant-closed' STATUS_DELIVERED = 'delivered' -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_UPDATE_INTERVAL, default=timedelta(seconds=1800)): ( - vol.All(cv.time_period, cv.positive_timedelta)), -}) +SCAN_INTERVAL = timedelta(seconds=1800) + +PLATFORM_SCHEMA = vol.All( + PLATFORM_SCHEMA.extend({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_UPDATE_INTERVAL): ( + vol.All(cv.time_period, cv.positive_timedelta)), + }), + cv.deprecated( + CONF_UPDATE_INTERVAL, + replacement_key=CONF_SCAN_INTERVAL, + invalidation_version=CONF_UPDATE_INTERVAL_INVALIDATION_VERSION, + default=SCAN_INTERVAL + ) +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -49,8 +61,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.exception("Could not connect to UPS My Choice") return False - add_entities([UPSSensor(session, config.get(CONF_NAME), - config.get(CONF_UPDATE_INTERVAL))], True) + add_entities([UPSSensor( + session, + config.get(CONF_NAME), + config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) + )], True) class UPSSensor(Entity): diff --git a/homeassistant/components/services.yaml b/homeassistant/components/services.yaml index 8988021a5b6..40c76376531 100644 --- a/homeassistant/components/services.yaml +++ b/homeassistant/components/services.yaml @@ -1,488 +1,5 @@ # Describes the format for available component services -foursquare: - checkin: - description: Check a user into a Foursquare venue. - fields: - venueId: - description: The Foursquare venue where the user is checking in. [Required] - example: IHR8THISVNU - eventId: - description: The event the user is checking in to. [Optional] - example: UHR8THISVNT - shout: - description: A message about your check-in. The maximum length of this field is 140 characters. [Optional] - example: There are crayons! Crayons! - mentions: - description: Mentions in your check-in. This parameter is a semicolon-delimited list of mentions. A single mention is of the form "start,end,userid", where start is the index of the first character in the shout representing the mention, end is the index of the first character in the shout after the mention, and userid is the userid of the user being mentioned. If userid is prefixed with "fbu-", this indicates a Facebook userid that is being mention. Character indices in shouts are 0-based. [Optional] - example: 5,10,HZXXY3Y;15,20,GZYYZ3Z;25,30,fbu-GZXY13Y - broadcast: - description: "Who to broadcast this check-in to. Accepts a comma-delimited list of values: private (off the grid) or public (share with friends), facebook share on facebook, twitter share on twitter, followers share with followers (celebrity mode users only), If no valid value is found, the default is public. [Optional]" - example: public,twitter - ll: - description: Latitude and longitude of the user's location. Only specify this field if you have a GPS or other device reported location for the user at the time of check-in. [Optional] - example: 33.7,44.2 - llAcc: - description: Accuracy of the user's latitude and longitude, in meters. [Optional] - example: 1 - alt: - description: Altitude of the user's location, in meters. [Optional] - example: 0 - altAcc: - description: Vertical accuracy of the user's location, in meters. - example: 1 - -microsoft_face: - create_group: - description: Create a new person group. - fields: - name: - description: Name of the group. - example: 'family' - delete_group: - description: Delete a new person group. - fields: - name: - description: Name of the group. - example: 'family' - train_group: - description: Train a person group. - fields: - group: - description: Name of the group - example: 'family' - create_person: - description: Create a new person in the group. - fields: - name: - description: Name of the person - example: 'Hans' - group: - description: Name of the group - example: 'family' - delete_person: - description: Delete a person in the group. - fields: - name: - description: Name of the person. - example: 'Hans' - group: - description: Name of the group. - example: 'family' - face_person: - description: Add a new picture to a person. - fields: - person: - description: Name of the person. - example: 'Hans' - group: - description: Name of the group. - example: 'family' - camera_entity: - description: Camera to take a picture. - example: camera.door - -verisure: - capture_smartcam: - description: Capture a new image from a smartcam. - fields: - device_serial: - description: The serial number of the smartcam you want to capture an image from. - example: '2DEU AT5Z' - -alert: - turn_off: - description: Silence alert's notifications. - fields: - entity_id: - description: Name of the alert to silence. - example: 'alert.garage_door_open' - turn_on: - description: Reset alert's notifications. - fields: - entity_id: - description: Name of the alert to reset. - example: 'alert.garage_door_open' - toggle: - description: Toggle alert's notifications. - fields: - entity_id: - description: Name of the alert to toggle. - example: 'alert.garage_door_open' - -hdmi_cec: - send_command: - description: Sends CEC command into HDMI CEC capable adapter. - fields: - raw: - description: 'Raw CEC command in format "00:00:00:00" where first two digits are source and destination, second byte is command and optional other bytes are command parameters. If raw command specified, other params are ignored.' - example: '"10:36"' - src: - desctiption: 'Source of command. Could be decimal number or string with hexadeximal notation: "0x10".' - example: '12 or "0xc"' - dst: - description: 'Destination for command. Could be decimal number or string with hexadeximal notation: "0x10".' - example: '5 or "0x5"' - cmd: - description: 'Command itself. Could be decimal number or string with hexadeximal notation: "0x10".' - example: '144 or "0x90"' - att: - description: Optional parameters. - example: [0, 2] - update: - description: Update devices state from network. - volume: - description: Increase or decrease volume of system. - fields: - up: - description: Increases volume x levels. - example: 3 - down: - description: Decreases volume x levels. - example: 3 - mute: - description: Mutes audio system. Value should be on, off or toggle. - example: "toggle" - select_device: - description: Select HDMI device. - fields: - device: - description: Address of device to select. Can be entity_id, physical address or alias from confuguration. - example: '"switch.hdmi_1" or "1.1.0.0" or "01:10"' - power_on: - description: Power on all devices which supports it. - standby: - description: Standby all devices which supports it. - -ffmpeg: - start: - description: Send a start command to a ffmpeg based sensor. - fields: - entity_id: - description: Name(s) of entities that will start. Platform dependent. - example: 'binary_sensor.ffmpeg_noise' - stop: - description: Send a stop command to a ffmpeg based sensor. - fields: - entity_id: - description: Name(s) of entities that will stop. Platform dependent. - example: 'binary_sensor.ffmpeg_noise' - restart: - description: Send a restart command to a ffmpeg based sensor. - fields: - entity_id: - description: Name(s) of entities that will restart. Platform dependent. - example: 'binary_sensor.ffmpeg_noise' - -logger: - set_default_level: - description: Set the default log level for components. - fields: - level: - description: Default severity level. Possible values are notset, debug, info, warn, warning, error, fatal, critical - example: 'debug' - set_level: - description: Set log level for components. - -hassio: - host_reboot: - description: Reboot host computer. - host_shutdown: - description: Poweroff host computer. - host_update: - description: Update host computer. - fields: - version: - description: Optional or it will be use the latest version. - example: '0.3' - supervisor_update: - description: Update HassIO supervisor. - fields: - version: - description: Optional or it will be use the latest version. - example: '0.3' - supervisor_reload: - description: Reload HassIO supervisor addons/updates/configs. - homeassistant_update: - description: Update HomeAssistant docker image. - fields: - version: - description: Optional or it will be use the latest version. - example: '0.40.1' - addon_install: - description: Install a HassIO docker addon. - fields: - addon: - description: Name of addon. - example: 'smb_config' - version: - description: Optional or it will be use the latest version. - example: '0.2' - addon_uninstall: - description: Uninstall a HassIO docker addon. - fields: - addon: - description: Name of addon. - example: 'smb_config' - addon_update: - description: Update a HassIO docker addon. - fields: - addon: - description: Name of addon. - example: 'smb_config' - version: - description: Optional or it will be use the latest version. - example: '0.2' - addon_start: - description: Start a HassIO docker addon. - fields: - addon: - description: Name of addon. - example: 'smb_config' - addon_stop: - description: Stop a HassIO docker addon. - fields: - addon: - description: Name of addon. - example: 'smb_config' - -eight_sleep: - heat_set: - description: Set heating level for eight sleep. - fields: - entity_id: - description: Entity id of the bed state to adjust. - example: 'sensor.eight_left_bed_state' - target: - description: Target heating level from 0-100. - example: 35 - duration: - description: Duration to heat at the target level in seconds. - example: 3600 - -apple_tv: - apple_tv_authenticate: - description: Start AirPlay device authentication. - fields: - entity_id: - description: Name(s) of entities to authenticate with. - example: 'media_player.apple_tv' - apple_tv_scan: - description: Scan for Apple TV devices. - -modbus: - write_register: - description: Write to a modbus holding register. - fields: - unit: - description: Address of the modbus unit. - example: 21 - address: - description: Address of the holding register to write to. - example: 0 - value: - description: Value to write. - example: 0 - write_coil: - description: Write to a modbus coil. - fields: - unit: - description: Address of the modbus unit. - example: 21 - address: - description: Address of the register to read. - example: 0 - state: - description: State to write. - example: false - -wake_on_lan: - send_magic_packet: - description: Send a 'magic packet' to wake up a device with 'Wake-On-LAN' capabilities. - fields: - mac: - description: MAC address of the device to wake up. - example: 'aa:bb:cc:dd:ee:ff' - broadcast_address: - description: Optional broadcast IP where to send the magic packet. - example: '192.168.255.255' - -knx: - group_write: - description: Turn a light on. - fields: - address: - description: Group address(es) to write to. - example: '1/1/0' - data: - description: KNX data to send. - example: 1 - -rflink: - send_command: - description: Send device command through RFLink. - fields: - device_id: - description: RFLink device ID. - example: 'newkaku_0000c6c2_1' - - command: - description: The command to be sent. - example: 'on' - -abode: - change_setting: - description: Change an Abode system setting. - fields: - setting: - description: Setting to change. - example: 'beeper_mute' - value: - description: Value of the setting. - example: '1' - capture_image: - description: Request a new image capture from a camera device. - fields: - entity_id: - description: Entity id of the camera to request an image. - example: 'camera.downstairs_motion_camera' - trigger_quick_action: - description: Trigger an Abode quick action. - fields: - entity_id: - description: Entity id of the quick action to trigger. - example: 'binary_sensor.home_quick_action' - -snips: - say: - description: Send a TTS message to Snips. - fields: - text: - description: Text to say. - example: My name is snips - site_id: - description: Site to use to start session, defaults to default (optional) - example: bedroom - custom_data: - description: custom data that will be included with all messages in this session - example: user=UserName - say_action: - description: Send a TTS message to Snips to listen for a response. - fields: - text: - description: Text to say - example: My name is snips - site_id: - description: Site to use to start session, defaults to default (optional) - example: bedroom - custom_data: - description: custom data that will be included with all messages in this session - example: user=UserName - can_be_enqueued: - description: If True, session waits for an open session to end, if False session is dropped if one is running - example: True - intent_filter: - description: Optional Array of Strings - A list of intents names to restrict the NLU resolution to on the first query. - example: turnOnLights, turnOffLights - feedback_on: - description: Turns feedback sounds on. - fields: - site_id: - description: Site to turn sounds on, defaults to all sites (optional) - example: bedroom - feedback_off: - description: Turns feedback sounds off. - fields: - site_id: - description: Site to turn sounds on, defaults to all sites (optional) - example: bedroom - -input_boolean: - toggle: - description: Toggles an input boolean. - fields: - entity_id: - description: Entity id of the input boolean to toggle. - example: 'input_boolean.notify_alerts' - turn_off: - description: Turns off an input boolean - fields: - entity_id: - description: Entity id of the input boolean to turn off. - example: 'input_boolean.notify_alerts' - turn_on: - description: Turns on an input boolean. - fields: - entity_id: - description: Entity id of the input boolean to turn on. - example: 'input_boolean.notify_alerts' - -input_text: - set_value: - description: Set the value of an input text entity. - fields: - entity_id: - description: Entity id of the input text to set the new value. - example: 'input_text.text1' - value: - description: The target value the entity should be set to. - example: This is an example text - -input_number: - set_value: - description: Set the value of an input number entity. - fields: - entity_id: - description: Entity id of the input number to set the new value. - example: 'input_number.threshold' - value: - description: The target value the entity should be set to. - example: 42 - increment: - description: Increment the value of an input number entity by its stepping. - fields: - entity_id: - description: Entity id of the input number the should be incremented. - example: 'input_number.threshold' - decrement: - description: Decrement the value of an input number entity by its stepping. - fields: - entity_id: - description: Entity id of the input number the should be decremented. - example: 'input_number.threshold' - -input_select: - select_option: - description: Select an option of an input select entity. - fields: - entity_id: - description: Entity id of the input select to select the value. - example: 'input_select.my_select' - option: - description: Option to be selected. - example: '"Item A"' - set_options: - description: Set the options of an input select entity. - fields: - entity_id: - description: Entity id of the input select to set the new options for. - example: 'input_select.my_select' - options: - description: Options for the input select entity. - example: '["Item A", "Item B", "Item C"]' - select_previous: - description: Select the previous options of an input select entity. - fields: - entity_id: - description: Entity id of the input select to select the previous value for. - example: 'input_select.my_select' - select_next: - description: Select the next options of an input select entity. - fields: - entity_id: - description: Entity id of the input select to select the next value for. - example: 'input_select.my_select' - homeassistant: check_config: description: Check the Home Assistant configuration files for errors. Errors will be displayed in the Home Assistant log. @@ -516,77 +33,3 @@ homeassistant: entity_id: description: One or multiple entity_ids to update. Can be a list. example: light.living_room - -xiaomi_aqara: - play_ringtone: - description: Play a specific ringtone. The version of the gateway firmware must be 1.4.1_145 at least. - fields: - gw_mac: - description: MAC address of the Xiaomi Aqara Gateway. - example: 34ce00880088 - ringtone_id: - description: One of the allowed ringtone ids. - example: 8 - ringtone_vol: - description: The volume in percent. - example: 30 - stop_ringtone: - description: Stops a playing ringtone immediately. - fields: - gw_mac: - description: MAC address of the Xiaomi Aqara Gateway. - example: 34ce00880088 - add_device: - description: Enables the join permission of the Xiaomi Aqara Gateway for 30 seconds. A new device can be added afterwards by pressing the pairing button once. - fields: - gw_mac: - description: MAC address of the Xiaomi Aqara Gateway. - example: 34ce00880088 - remove_device: - description: Removes a specific device. The removal is required if a device shall be paired with another gateway. - fields: - gw_mac: - description: MAC address of the Xiaomi Aqara Gateway. - example: 34ce00880088 - device_id: - description: Hardware address of the device to remove. - example: 158d0000000000 - -shopping_list: - add_item: - description: Adds an item to the shopping list. - fields: - name: - description: The name of the item to add. - example: Beer - complete_item: - description: Marks an item as completed in the shopping list. It does not remove the item. - fields: - name: - description: The name of the item to mark as completed. - example: Beer - -nest: - set_mode: - description: > - Set the home/away mode for a Nest structure. - Set to away mode will also set Estimated Arrival Time if provided. - Set ETA will cause the thermostat to begin warming or cooling the home before the user arrives. - After ETA set other Automation can read ETA sensor as a signal to prepare the home for - the user's arrival. - fields: - home_mode: - description: home or away - example: home - structure: - description: Optional structure name. Default set all structures managed by Home Assistant. - example: My Home - eta: - description: Optional Estimated Arrival Time from now. - example: 0:10 - eta_window: - description: Optional ETA window. Default is 1 minute. - example: 0:5 - trip_id: - description: Optional identity of a trip. Using the same trip_ID will update the estimation. - example: trip_back_home diff --git a/homeassistant/components/shell_command.py b/homeassistant/components/shell_command/__init__.py similarity index 94% rename from homeassistant/components/shell_command.py rename to homeassistant/components/shell_command/__init__.py index f9ec8da54e3..e27870e2d86 100644 --- a/homeassistant/components/shell_command.py +++ b/homeassistant/components/shell_command/__init__.py @@ -1,9 +1,4 @@ -""" -Exposes regular shell commands as services. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/shell_command/ -""" +"""Expose regular shell commands as services.""" import asyncio import logging import shlex @@ -15,7 +10,6 @@ from homeassistant.core import ServiceCall from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.typing import ConfigType, HomeAssistantType - DOMAIN = 'shell_command' _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/shiftr.py b/homeassistant/components/shiftr/__init__.py similarity index 92% rename from homeassistant/components/shiftr.py rename to homeassistant/components/shiftr/__init__.py index 17a46be4734..438bc36b1bf 100644 --- a/homeassistant/components/shiftr.py +++ b/homeassistant/components/shiftr/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Shiftr.io. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/shiftr/ -""" +"""Support for Shiftr.io.""" import logging import voluptuous as vol diff --git a/homeassistant/components/shopping_list.py b/homeassistant/components/shopping_list/__init__.py similarity index 99% rename from homeassistant/components/shopping_list.py rename to homeassistant/components/shopping_list/__init__.py index 2ebd80c3de0..1a036f3661a 100644 --- a/homeassistant/components/shopping_list.py +++ b/homeassistant/components/shopping_list/__init__.py @@ -1,4 +1,4 @@ -"""Component to manage a shopping list.""" +"""Support to manage a shopping list.""" import asyncio import logging import uuid diff --git a/homeassistant/components/shopping_list/services.yaml b/homeassistant/components/shopping_list/services.yaml new file mode 100644 index 00000000000..1d667e43fa6 --- /dev/null +++ b/homeassistant/components/shopping_list/services.yaml @@ -0,0 +1,9 @@ +add_item: + description: Adds an item to the shopping list. + fields: + name: {description: The name of the item to add., example: Beer} +complete_item: + description: Marks an item as completed in the shopping list. It does not remove + the item. + fields: + name: {description: The name of the item to mark as completed., example: Beer} diff --git a/homeassistant/components/simplisafe/.translations/da.json b/homeassistant/components/simplisafe/.translations/da.json new file mode 100644 index 00000000000..3ec3d7b456c --- /dev/null +++ b/homeassistant/components/simplisafe/.translations/da.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Konto er allerede registreret", + "invalid_credentials": "Ugyldige legitimationsoplysninger" + }, + "step": { + "user": { + "data": { + "code": "Kode (til Home Assistant)", + "password": "Adgangskode", + "username": "Email adresse" + }, + "title": "Udfyld dine oplysninger" + } + }, + "title": "SimpliSafe" + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/fr.json b/homeassistant/components/simplisafe/.translations/fr.json new file mode 100644 index 00000000000..de05edea8c9 --- /dev/null +++ b/homeassistant/components/simplisafe/.translations/fr.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Compte d\u00e9j\u00e0 enregistr\u00e9", + "invalid_credentials": "Informations d'identification invalides" + }, + "step": { + "user": { + "data": { + "code": "Code (pour Home Assistant)", + "password": "Mot de passe", + "username": "Adresse e-mail" + }, + "title": "Veuillez saisir vos informations" + } + }, + "title": "SimpliSafe" + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/ko.json b/homeassistant/components/simplisafe/.translations/ko.json index 5426c564e03..8fb056e3f93 100644 --- a/homeassistant/components/simplisafe/.translations/ko.json +++ b/homeassistant/components/simplisafe/.translations/ko.json @@ -11,7 +11,7 @@ "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": "\uc0ac\uc6a9\uc790 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694" } }, "title": "SimpliSafe" diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 7f1f8f539eb..fcd9d15839b 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -1,9 +1,4 @@ -""" -Support for SimpliSafe alarm systems. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/simplisafe/ -""" +"""Support for SimpliSafe alarm systems.""" import logging from datetime import timedelta @@ -36,7 +31,7 @@ ACCOUNT_CONFIG_SCHEMA = vol.Schema({ vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_CODE): cv.string, vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): - cv.time_period + cv.time_period, }) CONFIG_SCHEMA = vol.Schema({ diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 626a819b0b9..9fdeea73da8 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -1,9 +1,4 @@ -""" -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/ -""" +"""Support for SimpliSafe alarm control panels.""" import logging import re diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index 0a59dcb3e1d..66e26bd5204 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -1,5 +1,4 @@ """Config flow to configure the SimpliSafe component.""" - from collections import OrderedDict import voluptuous as vol diff --git a/homeassistant/components/sisyphus.py b/homeassistant/components/sisyphus/__init__.py similarity index 92% rename from homeassistant/components/sisyphus.py rename to homeassistant/components/sisyphus/__init__.py index f875e3a91c7..b1bec15d40e 100644 --- a/homeassistant/components/sisyphus.py +++ b/homeassistant/components/sisyphus/__init__.py @@ -1,9 +1,4 @@ -""" -Support for controlling Sisyphus Kinetic Art Tables. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/sisyphus/ -""" +"""Support for controlling Sisyphus Kinetic Art Tables.""" import asyncio import logging diff --git a/homeassistant/components/light/sisyphus.py b/homeassistant/components/sisyphus/light.py similarity index 83% rename from homeassistant/components/light/sisyphus.py rename to homeassistant/components/sisyphus/light.py index 75cc86a0154..c9d20959696 100644 --- a/homeassistant/components/light/sisyphus.py +++ b/homeassistant/components/sisyphus/light.py @@ -1,9 +1,4 @@ -""" -Support for the light on the Sisyphus Kinetic Art Table. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.sisyphus/ -""" +"""Support for the light on the Sisyphus Kinetic Art Table.""" import logging from homeassistant.const import CONF_NAME @@ -26,15 +21,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class SisyphusLight(Light): - """Represents a Sisyphus table as a light.""" + """Representation of a Sisyphus table as a light.""" def __init__(self, name, table): - """ - Constructor. - - :param name: name of the table - :param table: sisyphus-control Table object - """ + """Initialize the Sisyphus table.""" self._name = name self._table = table diff --git a/homeassistant/components/media_player/sisyphus.py b/homeassistant/components/sisyphus/media_player.py similarity index 94% rename from homeassistant/components/media_player/sisyphus.py rename to homeassistant/components/sisyphus/media_player.py index ef6b02514f0..463ac2b6cd1 100644 --- a/homeassistant/components/media_player/sisyphus.py +++ b/homeassistant/components/sisyphus/media_player.py @@ -1,15 +1,11 @@ -""" -Support for track controls on the Sisyphus Kinetic Art Table. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/media_player.sisyphus/ -""" +"""Support for track controls on the Sisyphus Kinetic Art Table.""" import logging -from homeassistant.components.media_player import ( +from homeassistant.components.media_player import MediaPlayerDevice +from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_SHUFFLE_SET, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice) + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET) from homeassistant.components.sisyphus import DATA_SISYPHUS from homeassistant.const import ( CONF_HOST, CONF_NAME, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING) diff --git a/homeassistant/components/skybell.py b/homeassistant/components/skybell/__init__.py similarity index 93% rename from homeassistant/components/skybell.py rename to homeassistant/components/skybell/__init__.py index ee384fd7094..8724f7d3d66 100644 --- a/homeassistant/components/skybell.py +++ b/homeassistant/components/skybell/__init__.py @@ -1,9 +1,4 @@ -""" -Support for the Skybell HD Doorbell. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/skybell/ -""" +"""Support for the Skybell HD Doorbell.""" import logging from requests.exceptions import HTTPError, ConnectTimeout diff --git a/homeassistant/components/binary_sensor/skybell.py b/homeassistant/components/skybell/binary_sensor.py similarity index 93% rename from homeassistant/components/binary_sensor/skybell.py rename to homeassistant/components/skybell/binary_sensor.py index 7d8b3a84a2a..169e1b51a4e 100644 --- a/homeassistant/components/binary_sensor/skybell.py +++ b/homeassistant/components/skybell/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Binary sensor support for the Skybell HD Doorbell. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.skybell/ -""" +"""Binary sensor support for the Skybell HD Doorbell.""" from datetime import timedelta import logging diff --git a/homeassistant/components/camera/skybell.py b/homeassistant/components/skybell/camera.py similarity index 94% rename from homeassistant/components/camera/skybell.py rename to homeassistant/components/skybell/camera.py index 3ad95e40d62..c22489aa654 100644 --- a/homeassistant/components/camera/skybell.py +++ b/homeassistant/components/skybell/camera.py @@ -1,9 +1,4 @@ -""" -Camera support for the Skybell HD Doorbell. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/camera.skybell/ -""" +"""Camera support for the Skybell HD Doorbell.""" from datetime import timedelta import logging diff --git a/homeassistant/components/light/skybell.py b/homeassistant/components/skybell/light.py similarity index 92% rename from homeassistant/components/light/skybell.py rename to homeassistant/components/skybell/light.py index ecb240f2ef3..02be279f609 100644 --- a/homeassistant/components/light/skybell.py +++ b/homeassistant/components/skybell/light.py @@ -1,9 +1,4 @@ -""" -Light/LED support for the Skybell HD Doorbell. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.skybell/ -""" +"""Light/LED support for the Skybell HD Doorbell.""" import logging diff --git a/homeassistant/components/sensor/skybell.py b/homeassistant/components/skybell/sensor.py similarity index 92% rename from homeassistant/components/sensor/skybell.py rename to homeassistant/components/skybell/sensor.py index de8c3a5694d..89841ae74ef 100644 --- a/homeassistant/components/sensor/skybell.py +++ b/homeassistant/components/skybell/sensor.py @@ -1,9 +1,4 @@ -""" -Sensor support for Skybell Doorbells. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.skybell/ -""" +"""Sensor support for Skybell Doorbells.""" from datetime import timedelta import logging diff --git a/homeassistant/components/switch/skybell.py b/homeassistant/components/skybell/switch.py similarity index 86% rename from homeassistant/components/switch/skybell.py rename to homeassistant/components/skybell/switch.py index 9d791aa3df3..32f1d7f9392 100644 --- a/homeassistant/components/switch/skybell.py +++ b/homeassistant/components/skybell/switch.py @@ -1,9 +1,4 @@ -""" -Switch support for the Skybell HD Doorbell. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.skybell/ -""" +"""Switch support for the Skybell HD Doorbell.""" import logging import voluptuous as vol @@ -53,8 +48,8 @@ class SkybellSwitch(SkybellDevice, SwitchDevice): """Initialize a light for a Skybell device.""" super().__init__(device) self._switch_type = switch_type - self._name = "{0} {1}".format(self._device.name, - SWITCH_TYPES[self._switch_type][0]) + self._name = "{0} {1}".format( + self._device.name, SWITCH_TYPES[self._switch_type][0]) @property def name(self): diff --git a/homeassistant/components/sleepiq.py b/homeassistant/components/sleepiq/__init__.py similarity index 94% rename from homeassistant/components/sleepiq.py rename to homeassistant/components/sleepiq/__init__.py index 4d4ecf0160b..7a23c6c4609 100644 --- a/homeassistant/components/sleepiq.py +++ b/homeassistant/components/sleepiq/__init__.py @@ -1,9 +1,4 @@ -""" -Support for SleepIQ from SleepNumber. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sleepiq/ -""" +"""Support for SleepIQ from SleepNumber.""" import logging from datetime import timedelta from requests.exceptions import HTTPError diff --git a/homeassistant/components/smappee.py b/homeassistant/components/smappee/__init__.py similarity index 98% rename from homeassistant/components/smappee.py rename to homeassistant/components/smappee/__init__.py index d8b7a68a506..7a495d7b89a 100644 --- a/homeassistant/components/smappee.py +++ b/homeassistant/components/smappee/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Smappee energy monitor. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/smappee/ -""" +"""Support for Smappee energy monitor.""" import logging from datetime import datetime, timedelta import re diff --git a/homeassistant/components/sensor/smappee.py b/homeassistant/components/smappee/sensor.py similarity index 97% rename from homeassistant/components/sensor/smappee.py rename to homeassistant/components/smappee/sensor.py index 65f21815960..67213ab15bf 100644 --- a/homeassistant/components/sensor/smappee.py +++ b/homeassistant/components/smappee/sensor.py @@ -1,9 +1,4 @@ -""" -Support for monitoring a Smappee energy sensor. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.smappee/ -""" +"""Support for monitoring a Smappee energy sensor.""" import logging from datetime import timedelta diff --git a/homeassistant/components/switch/smappee.py b/homeassistant/components/smappee/switch.py similarity index 79% rename from homeassistant/components/switch/smappee.py rename to homeassistant/components/smappee/switch.py index fc2716b8019..3b9bee081f7 100644 --- a/homeassistant/components/switch/smappee.py +++ b/homeassistant/components/smappee/switch.py @@ -1,9 +1,4 @@ -""" -Support for interacting with Smappee Comport Plugs. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/switch.smappee/ -""" +"""Support for interacting with Smappee Comport Plugs.""" import logging from homeassistant.components.smappee import DATA_SMAPPEE @@ -26,17 +21,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for items in smappee.info[location_id].get('actuators'): if items.get('name') != '': _LOGGER.debug("Remote actuator %s", items) - dev.append(SmappeeSwitch(smappee, - items.get('name'), - location_id, - items.get('id'))) + dev.append(SmappeeSwitch( + smappee, items.get('name'), location_id, + items.get('id'))) elif smappee.is_local_active: for items in smappee.local_devices: _LOGGER.debug("Local actuator %s", items) - dev.append(SmappeeSwitch(smappee, - items.get('value'), - None, - items.get('key'))) + dev.append(SmappeeSwitch( + smappee, items.get('value'), None, items.get('key'))) add_entities(dev) diff --git a/homeassistant/components/smartthings/.translations/da.json b/homeassistant/components/smartthings/.translations/da.json new file mode 100644 index 00000000000..1c571b4e639 --- /dev/null +++ b/homeassistant/components/smartthings/.translations/da.json @@ -0,0 +1,27 @@ +{ + "config": { + "error": { + "app_not_installed": "S\u00f8rg for at du har installeret og autoriseret Home Assistant SmartApp og pr\u00f8v igen.", + "app_setup_error": "SmartApp kunne ikke konfigureres. Pr\u00f8v igen.", + "base_url_not_https": "`base_url` til` http` komponenten skal konfigureres og starte med `https://`.", + "token_already_setup": "Token er allerede konfigureret.", + "token_forbidden": "Adgangstoken er ikke indenfor OAuth", + "token_invalid_format": "Adgangstoken skal v\u00e6re i UID/GUID format", + "token_unauthorized": "Adgangstoken er ugyldigt eller ikke l\u00e6ngere godkendt." + }, + "step": { + "user": { + "data": { + "access_token": "Adgangstoken" + }, + "description": "Indtast venligst en SmartThings [Personal Access Token]({token_url}), som er oprettet if\u00f8lge [instruktionerne]({component_url}).", + "title": "Indtast personlig adgangstoken" + }, + "wait_install": { + "description": "Installer Home Assistant SmartApp mindst et sted og klik p\u00e5 send.", + "title": "Installer SmartApp" + } + }, + "title": "SmartThings" + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/.translations/de.json b/homeassistant/components/smartthings/.translations/de.json new file mode 100644 index 00000000000..f65c338bf03 --- /dev/null +++ b/homeassistant/components/smartthings/.translations/de.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "user": { + "data": { + "access_token": "Zugangstoken" + }, + "description": "Bitte gib einen SmartThings [pers\u00f6nlichen Zugangstoken]({token_url}) ein, welcher gem\u00e4\u00df den [Anweisungen]({component_url}) erstellt wurde.", + "title": "Gib den pers\u00f6nlichen Zugangstoken an" + }, + "wait_install": { + "title": "SmartApp installieren" + } + }, + "title": "SmartThings" + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/.translations/en.json b/homeassistant/components/smartthings/.translations/en.json index f2775b30ae2..e35035b8fa0 100644 --- a/homeassistant/components/smartthings/.translations/en.json +++ b/homeassistant/components/smartthings/.translations/en.json @@ -7,7 +7,8 @@ "token_already_setup": "The token has already been setup.", "token_forbidden": "The token does not have the required OAuth scopes.", "token_invalid_format": "The token must be in the UID/GUID format", - "token_unauthorized": "The token is invalid or no longer authorized." + "token_unauthorized": "The token is invalid or no longer authorized.", + "webhook_error": "SmartThings could not validate the endpoint configured in `base_url`. Please review the component requirements." }, "step": { "user": { diff --git a/homeassistant/components/smartthings/.translations/fr.json b/homeassistant/components/smartthings/.translations/fr.json new file mode 100644 index 00000000000..6503634b92c --- /dev/null +++ b/homeassistant/components/smartthings/.translations/fr.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "app_not_installed": "Assurez-vous d'avoir install\u00e9 et autoris\u00e9 l'application Home Assistant SmartApp, puis r\u00e9essayez.", + "app_setup_error": "Impossible de configurer la SmartApp. Veuillez r\u00e9essayer.", + "base_url_not_https": "Le param\u00e8tre `base_url` du composant` http` doit \u00eatre configur\u00e9 et commencer par `https: //`.", + "token_already_setup": "Le jeton a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9.", + "token_invalid_format": "Le jeton doit \u00eatre au format UID / GUID", + "token_unauthorized": "Le jeton est invalide ou n'est plus autoris\u00e9." + }, + "step": { + "user": { + "data": { + "access_token": "Jeton d'acc\u00e8s" + }, + "title": "Entrer un jeton d'acc\u00e8s personnel" + }, + "wait_install": { + "title": "Installer SmartApp" + } + }, + "title": "SmartThings" + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/.translations/ko.json b/homeassistant/components/smartthings/.translations/ko.json new file mode 100644 index 00000000000..e4131543d50 --- /dev/null +++ b/homeassistant/components/smartthings/.translations/ko.json @@ -0,0 +1,27 @@ +{ + "config": { + "error": { + "app_not_installed": "Home Assistant SmartApp \uc744 \uc124\uce58\ud558\uace0 \uc778\uc99d\ud588\ub294\uc9c0 \ud655\uc778\ud558\uace0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "app_setup_error": "SmartApp \uc744 \uc124\uc815\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "base_url_not_https": "`http` \uad6c\uc131\uc694\uc18c\ub97c \uc704\ud55c `base_url` \uc740 `https://`\ub85c \uc2dc\uc791\ud558\ub3c4\ub85d \uad6c\uc131\ub418\uc5b4\uc57c\ud569\ub2c8\ub2e4.", + "token_already_setup": "\ud1a0\ud070\uc774 \uc774\ubbf8 \uc124\uc815\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "token_forbidden": "\ud1a0\ud070\uc5d0 \ud544\uc694\ud55c OAuth \ubc94\uc704\ubaa9\ub85d\uc774 \uc5c6\uc2b5\ub2c8\ub2e4.", + "token_invalid_format": "\ud1a0\ud070\uc740 UID/GUID \ud615\uc2dd\uc774\uc5b4\uc57c\ud569\ub2c8\ub2e4", + "token_unauthorized": "\ud1a0\ud070\uc774 \uc720\ud6a8\ud558\uc9c0 \uc54a\uac70\ub098 \uc2b9\uc778\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4." + }, + "step": { + "user": { + "data": { + "access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070" + }, + "description": "[\uc548\ub0b4]({component_url}) \uc5d0 \ub530\ub77c \uc0dd\uc131 \ub41c SmartThings [\uac1c\uc778 \uc561\uc138\uc2a4 \ud1a0\ud070]({token_url}) \uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "\uac1c\uc778 \uc561\uc138\uc2a4 \ud1a0\ud070 \uc785\ub825" + }, + "wait_install": { + "description": "\ud558\ub098 \uc774\uc0c1\uc758 \uc704\uce58\uc5d0 Home Assistant SmartApp \uc744 \uc124\uce58\ud558\uace0 submit \uc744 \ud074\ub9ad\ud574\uc8fc\uc138\uc694.", + "title": "SmartApp \uc124\uce58" + } + }, + "title": "SmartThings" + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/.translations/nl.json b/homeassistant/components/smartthings/.translations/nl.json new file mode 100644 index 00000000000..93150b2ae7d --- /dev/null +++ b/homeassistant/components/smartthings/.translations/nl.json @@ -0,0 +1,27 @@ +{ + "config": { + "error": { + "app_not_installed": "Zorg ervoor dat u de Home Assistant SmartApp heeft ge\u00efnstalleerd en geautoriseerd en probeer het opnieuw.", + "app_setup_error": "Instellen van SmartApp mislukt. Probeer het opnieuw.", + "base_url_not_https": "De `base_url` voor het `http` component moet worden geconfigureerd en beginnen met `https://`.", + "token_already_setup": "Het token is al ingesteld.", + "token_forbidden": "Het token heeft niet de vereiste OAuth-scopes.", + "token_invalid_format": "Het token moet de UID/GUID-indeling hebben", + "token_unauthorized": "Het token is ongeldig of niet langer geautoriseerd." + }, + "step": { + "user": { + "data": { + "access_token": "Toegangstoken" + }, + "description": "Voer een SmartThings [Personal Access Token]({token_url}) in die is aangemaakt volgens de [instructies]({component_url}).", + "title": "Persoonlijk toegangstoken invoeren" + }, + "wait_install": { + "description": "Installeer de Home Assistant SmartApp in tenminste \u00e9\u00e9n locatie en klik Verzenden.", + "title": "Installeer SmartApp" + } + }, + "title": "SmartThings" + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/.translations/no.json b/homeassistant/components/smartthings/.translations/no.json new file mode 100644 index 00000000000..4d7df8bd65d --- /dev/null +++ b/homeassistant/components/smartthings/.translations/no.json @@ -0,0 +1,27 @@ +{ + "config": { + "error": { + "app_not_installed": "V\u00e6r sikker p\u00e5 at du har installert og autorisert Home Assistant SmartApp og pr\u00f8v igjen.", + "app_setup_error": "Kan ikke konfigurere SmartApp. Vennligst pr\u00f8v p\u00e5 nytt.", + "base_url_not_https": "`base_url` for `http` komponenten m\u00e5 konfigureres og starte med `https://`.", + "token_already_setup": "Token har allerede blitt satt opp.", + "token_forbidden": "Tollet har ikke de n\u00f8dvendige OAuth m\u00e5lene.", + "token_invalid_format": "Token m\u00e5 v\u00e6re i UID/GUID format", + "token_unauthorized": "Tollet er ugyldig eller ikke lenger autorisert." + }, + "step": { + "user": { + "data": { + "access_token": "Tilgangstoken" + }, + "description": "Vennligst skriv inn en SmartThings [Personlig tilgangstoken]({token_url}) som er opprettet etter [instruksjonene]({component_url}).", + "title": "Oppgi Personlig Tilgangstoken" + }, + "wait_install": { + "description": "Vennligst installer Home Assistant SmartApp p\u00e5 minst ett sted og klikk p\u00e5 send.", + "title": "Installer SmartApp" + } + }, + "title": "SmartThings" + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/.translations/pl.json b/homeassistant/components/smartthings/.translations/pl.json index 379cdf699b7..570f1130383 100644 --- a/homeassistant/components/smartthings/.translations/pl.json +++ b/homeassistant/components/smartthings/.translations/pl.json @@ -1,7 +1,24 @@ { "config": { + "error": { + "app_not_installed": "Upewnij si\u0119, \u017ce zainstalowa\u0142e\u015b i autoryzowa\u0142e\u015b Home Assistant SmartApp i spr\u00f3buj ponownie.", + "app_setup_error": "Nie mo\u017cna skonfigurowa\u0107 SmartApp. Prosz\u0119 spr\u00f3buj ponownie.", + "base_url_not_https": "Parametr `base_url` dla komponentu `http` musi by\u0107 skonfigurowany i rozpoczyna\u0107 si\u0119 od `https://`.", + "token_already_setup": "Token zosta\u0142 ju\u017c skonfigurowany.", + "token_forbidden": "Token nie ma wymaganych zakres\u00f3w OAuth.", + "token_invalid_format": "Token musi by\u0107 w formacie UID/GUID", + "token_unauthorized": "Token jest niewa\u017cny lub nie ma ju\u017c autoryzacji." + }, "step": { + "user": { + "data": { + "access_token": "Token dost\u0119pu" + }, + "description": "Wprowad\u017a [token dost\u0119pu osobistego]({token_url}) SmartThings, kt\u00f3ry zosta\u0142 utworzony zgodnie z [instrukcj\u0105]({component_url}).", + "title": "Wprowad\u017a osobisty token dost\u0119pu" + }, "wait_install": { + "description": "Prosz\u0119 zainstalowa\u0107 Home Assistant SmartApp w co najmniej jednej lokalizacji i klikn\u0105\u0107 przycisk Wy\u015blij.", "title": "Zainstaluj SmartApp" } }, diff --git a/homeassistant/components/smartthings/.translations/pt.json b/homeassistant/components/smartthings/.translations/pt.json new file mode 100644 index 00000000000..d805cfc563d --- /dev/null +++ b/homeassistant/components/smartthings/.translations/pt.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "app_setup_error": "N\u00e3o \u00e9 poss\u00edvel configurar o SmartApp. Por favor, tente novamente.", + "token_already_setup": "O token j\u00e1 foi configurado." + }, + "step": { + "user": { + "data": { + "access_token": "Token de Acesso" + } + }, + "wait_install": { + "title": "Instalar SmartApp" + } + }, + "title": "SmartThings" + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/.translations/ru.json b/homeassistant/components/smartthings/.translations/ru.json new file mode 100644 index 00000000000..334e5d8cb23 --- /dev/null +++ b/homeassistant/components/smartthings/.translations/ru.json @@ -0,0 +1,27 @@ +{ + "config": { + "error": { + "app_not_installed": "\u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0412\u044b \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u043b\u0438 \u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043b\u0438 SmartApp Home Assistant \u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443.", + "app_setup_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c SmartApp. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", + "base_url_not_https": "\u0412 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0435 `http` \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0443\u043a\u0430\u0437\u0430\u043d \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 `base_url`, \u043d\u0430\u0447\u0438\u043d\u0430\u044e\u0449\u0438\u0439\u0441\u044f \u0441 `https://`.", + "token_already_setup": "\u0422\u043e\u043a\u0435\u043d \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d.", + "token_forbidden": "\u0422\u043e\u043a\u0435\u043d \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d \u0434\u043b\u044f OAuth.", + "token_invalid_format": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 UID / GUID", + "token_unauthorized": "\u0422\u043e\u043a\u0435\u043d \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d \u0438\u043b\u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d." + }, + "step": { + "user": { + "data": { + "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430" + }, + "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 [\u043f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430]({token_url}) SmartThings, \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u044b\u0439 \u0432 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u0438 \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({component_url}).", + "title": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430" + }, + "wait_install": { + "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0435 SmartApp Home Assistant \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **\u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c**.", + "title": "SmartThings" + } + }, + "title": "SmartThings" + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index d86524ef62b..3cf38c358bc 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -1,6 +1,6 @@ -"""SmartThings Cloud integration for Home Assistant.""" - +"""Support for SmartThings Cloud.""" import asyncio +import importlib import logging from typing import Iterable @@ -23,7 +23,7 @@ from .const import ( from .smartapp import ( setup_smartapp, setup_smartapp_endpoint, validate_installed_app) -REQUIREMENTS = ['pysmartapp==0.3.0', 'pysmartthings==0.4.2'] +REQUIREMENTS = ['pysmartapp==0.3.0', 'pysmartthings==0.6.2'] DEPENDENCIES = ['webhook'] _LOGGER = logging.getLogger(__name__) @@ -133,12 +133,45 @@ class DeviceBroker: """Create a new instance of the DeviceBroker.""" self._hass = hass self._installed_app_id = installed_app_id + self.assignments = self._assign_capabilities(devices) self.devices = {device.device_id: device for device in devices} self.event_handler_disconnect = None + def _assign_capabilities(self, devices: Iterable): + """Assign platforms to capabilities.""" + assignments = {} + for device in devices: + capabilities = device.capabilities.copy() + slots = {} + for platform_name in SUPPORTED_PLATFORMS: + platform = importlib.import_module( + '.' + platform_name, self.__module__) + assigned = platform.get_capabilities(capabilities) + if not assigned: + continue + # Draw-down capabilities and set slot assignment + for capability in assigned: + if capability not in capabilities: + continue + capabilities.remove(capability) + slots[capability] = platform_name + assignments[device.device_id] = slots + return assignments + + def get_assigned(self, device_id: str, platform: str): + """Get the capabilities assigned to the platform.""" + slots = self.assignments.get(device_id, {}) + return [key for key, value in slots.items() if value == platform] + + def any_assigned(self, device_id: str, platform: str): + """Return True if the platform has any assigned capabilities.""" + slots = self.assignments.get(device_id, {}) + return any(value for value in slots.values() if value == platform) + async def event_handler(self, req, resp, app): """Broker for incoming events.""" from pysmartapp.event import EVENT_TYPE_DEVICE + from pysmartthings import Capability, Attribute # Do not process events received from a different installed app # under the same parent SmartApp (valid use-scenario) @@ -156,7 +189,8 @@ class DeviceBroker: evt.component_id, evt.capability, evt.attribute, evt.value) # Fire events for buttons - if evt.capability == 'button' and evt.attribute == 'button': + if evt.capability == Capability.button and \ + evt.attribute == Attribute.button: data = { 'component_id': evt.component_id, 'device_id': evt.device_id, @@ -166,10 +200,18 @@ class DeviceBroker: } self._hass.bus.async_fire(EVENT_BUTTON, data) _LOGGER.debug("Fired button event: %s", data) + else: + data = { + 'location_id': evt.location_id, + 'device_id': evt.device_id, + 'component_id': evt.component_id, + 'capability': evt.capability, + 'attribute': evt.attribute, + 'value': evt.value, + } + _LOGGER.debug("Push update received: %s", data) updated_devices.add(device.device_id) - _LOGGER.debug("Update received with %s events and updated %s devices", - len(req.events), len(updated_devices)) async_dispatcher_send(self._hass, SIGNAL_SMARTTHINGS_UPDATE, updated_devices) diff --git a/homeassistant/components/smartthings/binary_sensor.py b/homeassistant/components/smartthings/binary_sensor.py index 045944ccfa9..45101601d5f 100644 --- a/homeassistant/components/smartthings/binary_sensor.py +++ b/homeassistant/components/smartthings/binary_sensor.py @@ -1,9 +1,6 @@ -""" -Support for binary sensors through the SmartThings cloud API. +"""Support for binary sensors through the SmartThings cloud API.""" +from typing import Optional, Sequence -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/smartthings.binary_sensor/ -""" from homeassistant.components.binary_sensor import BinarySensorDevice from . import SmartThingsEntity @@ -20,7 +17,7 @@ CAPABILITY_TO_ATTRIB = { 'soundSensor': 'sound', 'tamperAlert': 'tamper', 'valve': 'valve', - 'waterSensor': 'water' + 'waterSensor': 'water', } ATTRIB_TO_CLASS = { 'acceleration': 'moving', @@ -31,7 +28,7 @@ ATTRIB_TO_CLASS = { 'sound': 'sound', 'tamper': 'problem', 'valve': 'opening', - 'water': 'moisture' + 'water': 'moisture', } @@ -46,12 +43,19 @@ async def async_setup_entry(hass, config_entry, async_add_entities): broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] sensors = [] for device in broker.devices.values(): - for capability, attrib in CAPABILITY_TO_ATTRIB.items(): - if capability in device.capabilities: - sensors.append(SmartThingsBinarySensor(device, attrib)) + for capability in broker.get_assigned( + device.device_id, 'binary_sensor'): + attrib = CAPABILITY_TO_ATTRIB[capability] + sensors.append(SmartThingsBinarySensor(device, attrib)) async_add_entities(sensors) +def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]: + """Return all capabilities supported if minimum required are present.""" + return [capability for capability in CAPABILITY_TO_ATTRIB + if capability in capabilities] + + class SmartThingsBinarySensor(SmartThingsEntity, BinarySensorDevice): """Define a SmartThings Binary Sensor.""" diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py new file mode 100644 index 00000000000..ab7334f1316 --- /dev/null +++ b/homeassistant/components/smartthings/climate.py @@ -0,0 +1,225 @@ +"""Support for climate devices through the SmartThings cloud API.""" +import asyncio +from typing import Optional, Sequence + +from homeassistant.components.climate import ( + ATTR_OPERATION_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + STATE_AUTO, STATE_COOL, STATE_ECO, STATE_HEAT, SUPPORT_FAN_MODE, + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW, + ClimateDevice) +from homeassistant.const import ( + ATTR_TEMPERATURE, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) + +from . import SmartThingsEntity +from .const import DATA_BROKERS, DOMAIN + +DEPENDENCIES = ['smartthings'] + +ATTR_OPERATION_STATE = 'operation_state' +MODE_TO_STATE = { + 'auto': STATE_AUTO, + 'cool': STATE_COOL, + 'eco': STATE_ECO, + 'rush hour': STATE_ECO, + 'emergency heat': STATE_HEAT, + 'heat': STATE_HEAT, + 'off': STATE_OFF +} +STATE_TO_MODE = { + STATE_AUTO: 'auto', + STATE_COOL: 'cool', + STATE_ECO: 'eco', + STATE_HEAT: 'heat', + STATE_OFF: 'off' +} +UNIT_MAP = { + 'C': TEMP_CELSIUS, + 'F': TEMP_FAHRENHEIT +} + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add climate entities for a config entry.""" + broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] + async_add_entities( + [SmartThingsThermostat(device) for device in broker.devices.values() + if broker.any_assigned(device.device_id, 'climate')]) + + +def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]: + """Return all capabilities supported if minimum required are present.""" + from pysmartthings import Capability + + supported = [ + Capability.thermostat, + Capability.temperature_measurement, + Capability.thermostat_cooling_setpoint, + Capability.thermostat_heating_setpoint, + Capability.thermostat_mode, + Capability.relative_humidity_measurement, + Capability.thermostat_operating_state, + Capability.thermostat_fan_mode + ] + # Can have this legacy/deprecated capability + if Capability.thermostat in capabilities: + return supported + # Or must have all of these + climate_capabilities = [ + Capability.temperature_measurement, + Capability.thermostat_cooling_setpoint, + Capability.thermostat_heating_setpoint, + Capability.thermostat_mode] + if all(capability in capabilities + for capability in climate_capabilities): + return supported + + return None + + +class SmartThingsThermostat(SmartThingsEntity, ClimateDevice): + """Define a SmartThings climate entities.""" + + def __init__(self, device): + """Init the class.""" + super().__init__(device) + self._supported_features = self._determine_features() + + def _determine_features(self): + from pysmartthings import Capability + + flags = SUPPORT_OPERATION_MODE \ + | SUPPORT_TARGET_TEMPERATURE \ + | SUPPORT_TARGET_TEMPERATURE_LOW \ + | SUPPORT_TARGET_TEMPERATURE_HIGH + if self._device.get_capability( + Capability.thermostat_fan_mode, Capability.thermostat): + flags |= SUPPORT_FAN_MODE + return flags + + async def async_set_fan_mode(self, fan_mode): + """Set new target fan mode.""" + await self._device.set_thermostat_fan_mode(fan_mode, set_status=True) + + # State is set optimistically in the command above, therefore update + # the entity state ahead of receiving the confirming push updates + self.async_schedule_update_ha_state(True) + + async def async_set_operation_mode(self, operation_mode): + """Set new target operation mode.""" + mode = STATE_TO_MODE[operation_mode] + await self._device.set_thermostat_mode(mode, set_status=True) + + # State is set optimistically in the command above, therefore update + # the entity state ahead of receiving the confirming push updates + self.async_schedule_update_ha_state(True) + + async def async_set_temperature(self, **kwargs): + """Set new operation mode and target temperatures.""" + # Operation state + operation_state = kwargs.get(ATTR_OPERATION_MODE) + if operation_state: + mode = STATE_TO_MODE[operation_state] + await self._device.set_thermostat_mode(mode, set_status=True) + + # Heat/cool setpoint + heating_setpoint = None + cooling_setpoint = None + if self.current_operation == STATE_HEAT: + heating_setpoint = kwargs.get(ATTR_TEMPERATURE) + elif self.current_operation == STATE_COOL: + cooling_setpoint = kwargs.get(ATTR_TEMPERATURE) + else: + heating_setpoint = kwargs.get(ATTR_TARGET_TEMP_LOW) + cooling_setpoint = kwargs.get(ATTR_TARGET_TEMP_HIGH) + tasks = [] + if heating_setpoint is not None: + tasks.append(self._device.set_heating_setpoint( + round(heating_setpoint, 3), set_status=True)) + if cooling_setpoint is not None: + tasks.append(self._device.set_cooling_setpoint( + round(cooling_setpoint, 3), set_status=True)) + await asyncio.gather(*tasks) + + # State is set optimistically in the commands above, therefore update + # the entity state ahead of receiving the confirming push updates + self.async_schedule_update_ha_state(True) + + @property + def current_fan_mode(self): + """Return the fan setting.""" + return self._device.status.thermostat_fan_mode + + @property + def current_humidity(self): + """Return the current humidity.""" + return self._device.status.humidity + + @property + def current_operation(self): + """Return current operation ie. heat, cool, idle.""" + return MODE_TO_STATE[self._device.status.thermostat_mode] + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._device.status.temperature + + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + return { + ATTR_OPERATION_STATE: + self._device.status.thermostat_operating_state + } + + @property + def fan_list(self): + """Return the list of available fan modes.""" + return self._device.status.supported_thermostat_fan_modes + + @property + def operation_list(self): + """Return the list of available operation modes.""" + return {MODE_TO_STATE[mode] for mode in + self._device.status.supported_thermostat_modes} + + @property + def supported_features(self): + """Return the supported features.""" + return self._supported_features + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + if self.current_operation == STATE_COOL: + return self._device.status.cooling_setpoint + if self.current_operation == STATE_HEAT: + return self._device.status.heating_setpoint + return None + + @property + def target_temperature_high(self): + """Return the highbound target temperature we try to reach.""" + if self.current_operation == STATE_AUTO: + return self._device.status.cooling_setpoint + return None + + @property + def target_temperature_low(self): + """Return the lowbound target temperature we try to reach.""" + if self.current_operation == STATE_AUTO: + return self._device.status.heating_setpoint + return None + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return UNIT_MAP.get( + self._device.status.attributes['temperature'].unit) diff --git a/homeassistant/components/smartthings/config_flow.py b/homeassistant/components/smartthings/config_flow.py index b280036a615..4663222c3b4 100644 --- a/homeassistant/components/smartthings/config_flow.py +++ b/homeassistant/components/smartthings/config_flow.py @@ -1,7 +1,7 @@ """Config flow to configure SmartThings.""" import logging -from aiohttp.client_exceptions import ClientResponseError +from aiohttp import ClientResponseError import voluptuous as vol from homeassistant import config_entries @@ -50,7 +50,7 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow): async def async_step_user(self, user_input=None): """Get access token and validate it.""" - from pysmartthings import SmartThings + from pysmartthings import APIResponseError, SmartThings errors = {} if not self.hass.config.api.base_url.lower().startswith('https://'): @@ -87,6 +87,14 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow): app = await create_app(self.hass, self.api) setup_smartapp(self.hass, app) self.app_id = app.app_id + except APIResponseError as ex: + if ex.is_target_error(): + errors['base'] = 'webhook_error' + else: + errors['base'] = "app_setup_error" + _LOGGER.exception("API error setting up the SmartApp: %s", + ex.raw_error_response) + return self._show_step_user(errors) except ClientResponseError as ex: if ex.status == 401: errors[CONF_ACCESS_TOKEN] = "token_unauthorized" @@ -94,6 +102,7 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow): errors[CONF_ACCESS_TOKEN] = "token_forbidden" else: errors['base'] = "app_setup_error" + _LOGGER.exception("Unexpected error setting up the SmartApp") return self._show_step_user(errors) except Exception: # pylint:disable=broad-except errors['base'] = "app_setup_error" diff --git a/homeassistant/components/smartthings/const.py b/homeassistant/components/smartthings/const.py index 3d0e5cb95f8..27260b155d1 100644 --- a/homeassistant/components/smartthings/const.py +++ b/homeassistant/components/smartthings/const.py @@ -18,28 +18,16 @@ SIGNAL_SMARTAPP_PREFIX = 'smartthings_smartap_' SETTINGS_INSTANCE_ID = "hassInstanceId" STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 +# Ordered 'specific to least-specific platform' in order for capabilities +# to be drawn-down and represented by the appropriate platform. SUPPORTED_PLATFORMS = [ - 'binary_sensor', + 'climate', 'fan', 'light', - 'switch' -] -SUPPORTED_CAPABILITIES = [ - 'accelerationSensor', - 'button', - 'colorControl', - 'colorTemperature', - 'contactSensor', - 'fanSpeed', - 'filterStatus', - 'motionSensor', - 'presenceSensor', - 'soundSensor', + 'lock', 'switch', - 'switchLevel', - 'tamperAlert', - 'valve', - 'waterSensor' + 'binary_sensor', + 'sensor' ] VAL_UID = "^(?:([0-9a-fA-F]{32})|([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]" \ "{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}))$" diff --git a/homeassistant/components/smartthings/fan.py b/homeassistant/components/smartthings/fan.py index 7862736e60b..e722cd21d65 100644 --- a/homeassistant/components/smartthings/fan.py +++ b/homeassistant/components/smartthings/fan.py @@ -1,9 +1,5 @@ -""" -Support for fans through the SmartThings cloud API. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/smartthings.fan/ -""" +"""Support for fans through the SmartThings cloud API.""" +from typing import Optional, Sequence from homeassistant.components.fan import ( SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, SPEED_OFF, SUPPORT_SET_SPEED, @@ -35,15 +31,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] async_add_entities( [SmartThingsFan(device) for device in broker.devices.values() - if is_fan(device)]) + if broker.any_assigned(device.device_id, 'fan')]) -def is_fan(device): - """Determine if the device should be represented as a fan.""" +def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]: + """Return all capabilities supported if minimum required are present.""" from pysmartthings import Capability + + supported = [Capability.switch, Capability.fan_speed] # Must have switch and fan_speed - return all(capability in device.capabilities - for capability in [Capability.switch, Capability.fan_speed]) + if all(capability in capabilities for capability in supported): + return supported class SmartThingsFan(SmartThingsEntity, FanEntity): diff --git a/homeassistant/components/smartthings/light.py b/homeassistant/components/smartthings/light.py index 8495be62a73..79a5eabc20a 100644 --- a/homeassistant/components/smartthings/light.py +++ b/homeassistant/components/smartthings/light.py @@ -1,10 +1,6 @@ -""" -Support for lights through the SmartThings cloud API. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/smartthings.light/ -""" +"""Support for lights through the SmartThings cloud API.""" import asyncio +from typing import Optional, Sequence from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, @@ -29,29 +25,32 @@ async def async_setup_entry(hass, config_entry, async_add_entities): broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] async_add_entities( [SmartThingsLight(device) for device in broker.devices.values() - if is_light(device)], True) + if broker.any_assigned(device.device_id, 'light')], True) -def is_light(device): - """Determine if the device should be represented as a light.""" +def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]: + """Return all capabilities supported if minimum required are present.""" from pysmartthings import Capability + supported = [ + Capability.switch, + Capability.switch_level, + Capability.color_control, + Capability.color_temperature, + ] # Must be able to be turned on/off. - if Capability.switch not in device.capabilities: - return False - # Not a fan (which might also have switch_level) - if Capability.fan_speed in device.capabilities: - return False + if Capability.switch not in capabilities: + return None # Must have one of these light_capabilities = [ Capability.color_control, Capability.color_temperature, Capability.switch_level ] - if any(capability in device.capabilities + if any(capability in capabilities for capability in light_capabilities): - return True - return False + return supported + return None def convert_scale(value, value_scale, target_scale, round_digits=4): diff --git a/homeassistant/components/smartthings/lock.py b/homeassistant/components/smartthings/lock.py new file mode 100644 index 00000000000..d3f633ed0e4 --- /dev/null +++ b/homeassistant/components/smartthings/lock.py @@ -0,0 +1,73 @@ +"""Support for locks through the SmartThings cloud API.""" +from typing import Optional, Sequence + +from homeassistant.components.lock import LockDevice + +from . import SmartThingsEntity +from .const import DATA_BROKERS, DOMAIN + +DEPENDENCIES = ['smartthings'] + +ST_STATE_LOCKED = 'locked' +ST_LOCK_ATTR_MAP = { + 'method': 'method', + 'codeId': 'code_id', + 'timeout': 'timeout' +} + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Platform uses config entry setup.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add locks for a config entry.""" + broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] + async_add_entities( + [SmartThingsLock(device) for device in broker.devices.values() + if broker.any_assigned(device.device_id, 'lock')]) + + +def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]: + """Return all capabilities supported if minimum required are present.""" + from pysmartthings import Capability + + if Capability.lock in capabilities: + return [Capability.lock] + return None + + +class SmartThingsLock(SmartThingsEntity, LockDevice): + """Define a SmartThings lock.""" + + async def async_lock(self, **kwargs): + """Lock the device.""" + await self._device.lock(set_status=True) + self.async_schedule_update_ha_state() + + async def async_unlock(self, **kwargs): + """Unlock the device.""" + await self._device.unlock(set_status=True) + self.async_schedule_update_ha_state() + + @property + def is_locked(self): + """Return true if lock is locked.""" + return self._device.status.lock == ST_STATE_LOCKED + + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + from pysmartthings import Attribute + state_attrs = {} + status = self._device.status.attributes[Attribute.lock] + if status.value: + state_attrs['lock_state'] = status.value + if isinstance(status.data, dict): + for st_attr, ha_attr in ST_LOCK_ATTR_MAP.items(): + data_val = status.data.get(st_attr) + if data_val is not None: + state_attrs[ha_attr] = data_val + return state_attrs diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py new file mode 100644 index 00000000000..32047c179b4 --- /dev/null +++ b/homeassistant/components/smartthings/sensor.py @@ -0,0 +1,220 @@ +"""Support for sensors through the SmartThings cloud API.""" +from collections import namedtuple +from typing import Optional, Sequence + +from homeassistant.const import ( + DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, MASS_KILOGRAMS, + TEMP_CELSIUS, TEMP_FAHRENHEIT) + +from . import SmartThingsEntity +from .const import DATA_BROKERS, DOMAIN + +DEPENDENCIES = ['smartthings'] + +Map = namedtuple("map", "attribute name default_unit device_class") + +CAPABILITY_TO_SENSORS = { + 'activityLightingMode': [ + Map('lightingMode', "Activity Lighting Mode", None, None)], + 'airConditionerMode': [ + Map('airConditionerMode', "Air Conditioner Mode", None, None)], + 'airQualitySensor': [ + Map('airQuality', "Air Quality", 'CAQI', None)], + 'alarm': [ + Map('alarm', "Alarm", None, None)], + 'audioVolume': [ + Map('volume', "Volume", "%", None)], + 'battery': [ + Map('battery', "Battery", "%", DEVICE_CLASS_BATTERY)], + 'bodyMassIndexMeasurement': [ + Map('bmiMeasurement', "Body Mass Index", "kg/m^2", None)], + 'bodyWeightMeasurement': [ + Map('bodyWeightMeasurement', "Body Weight", MASS_KILOGRAMS, None)], + 'carbonDioxideMeasurement': [ + Map('carbonDioxide', "Carbon Dioxide Measurement", "ppm", None)], + 'carbonMonoxideDetector': [ + Map('carbonMonoxide', "Carbon Monoxide Detector", None, None)], + 'carbonMonoxideMeasurement': [ + Map('carbonMonoxideLevel', "Carbon Monoxide Measurement", "ppm", + None)], + 'dishwasherOperatingState': [ + Map('machineState', "Dishwasher Machine State", None, None), + Map('dishwasherJobState', "Dishwasher Job State", None, None), + Map('completionTime', "Dishwasher Completion Time", None, + DEVICE_CLASS_TIMESTAMP)], + 'doorControl': [ + Map('door', "Door", None, None)], + 'dryerMode': [ + Map('dryerMode', "Dryer Mode", None, None)], + 'dryerOperatingState': [ + Map('machineState', "Dryer Machine State", None, None), + Map('dryerJobState', "Dryer Job State", None, None), + Map('completionTime', "Dryer Completion Time", None, + DEVICE_CLASS_TIMESTAMP)], + 'dustSensor': [ + Map('fineDustLevel', "Fine Dust Level", None, None), + Map('dustLevel', "Dust Level", None, None)], + 'energyMeter': [ + Map('energy', "Energy Meter", 'kWh', None)], + 'equivalentCarbonDioxideMeasurement': [ + Map('equivalentCarbonDioxideMeasurement', + 'Equivalent Carbon Dioxide Measurement', 'ppm', None)], + 'formaldehydeMeasurement': [ + Map('formaldehydeLevel', 'Formaldehyde Measurement', 'ppm', None)], + 'garageDoorControl': [ + Map('door', 'Garage Door', None, None)], + 'illuminanceMeasurement': [ + Map('illuminance', "Illuminance", 'lux', DEVICE_CLASS_ILLUMINANCE)], + 'infraredLevel': [ + Map('infraredLevel', "Infrared Level", '%', None)], + 'lock': [ + Map('lock', "Lock", None, None)], + 'mediaInputSource': [ + Map('inputSource', "Media Input Source", None, None)], + 'mediaPlaybackRepeat': [ + Map('playbackRepeatMode', "Media Playback Repeat", None, None)], + 'mediaPlaybackShuffle': [ + Map('playbackShuffle', "Media Playback Shuffle", None, None)], + 'mediaPlayback': [ + Map('playbackStatus', "Media Playback Status", None, None)], + 'odorSensor': [ + Map('odorLevel', "Odor Sensor", None, None)], + 'ovenMode': [ + Map('ovenMode', "Oven Mode", None, None)], + 'ovenOperatingState': [ + Map('machineState', "Oven Machine State", None, None), + Map('ovenJobState', "Oven Job State", None, None), + Map('completionTime', "Oven Completion Time", None, None)], + 'ovenSetpoint': [ + Map('ovenSetpoint', "Oven Set Point", None, None)], + 'powerMeter': [ + Map('power', "Power Meter", 'W', None)], + 'powerSource': [ + Map('powerSource', "Power Source", None, None)], + 'refrigerationSetpoint': [ + Map('refrigerationSetpoint', "Refrigeration Setpoint", TEMP_CELSIUS, + DEVICE_CLASS_TEMPERATURE)], + 'relativeHumidityMeasurement': [ + Map('humidity', "Relative Humidity Measurement", '%', + DEVICE_CLASS_HUMIDITY)], + 'robotCleanerCleaningMode': [ + Map('robotCleanerCleaningMode', "Robot Cleaner Cleaning Mode", + None, None)], + 'robotCleanerMovement': [ + Map('robotCleanerMovement', "Robot Cleaner Movement", None, None)], + 'robotCleanerTurboMode': [ + Map('robotCleanerTurboMode', "Robot Cleaner Turbo Mode", None, None)], + 'signalStrength': [ + Map('lqi', "LQI Signal Strength", None, None), + Map('rssi', "RSSI Signal Strength", None, None)], + 'smokeDetector': [ + Map('smoke', "Smoke Detector", None, None)], + 'temperatureMeasurement': [ + Map('temperature', "Temperature Measurement", TEMP_CELSIUS, + DEVICE_CLASS_TEMPERATURE)], + 'thermostatCoolingSetpoint': [ + Map('coolingSetpoint', "Thermostat Cooling Setpoint", TEMP_CELSIUS, + DEVICE_CLASS_TEMPERATURE)], + 'thermostatFanMode': [ + Map('thermostatFanMode', "Thermostat Fan Mode", None, None)], + 'thermostatHeatingSetpoint': [ + Map('heatingSetpoint', "Thermostat Heating Setpoint", TEMP_CELSIUS, + DEVICE_CLASS_TEMPERATURE)], + 'thermostatMode': [ + Map('thermostatMode', "Thermostat Mode", None, None)], + 'thermostatOperatingState': [ + Map('thermostatOperatingState', "Thermostat Operating State", + None, None)], + 'thermostatSetpoint': [ + Map('thermostatSetpoint', "Thermostat Setpoint", TEMP_CELSIUS, + DEVICE_CLASS_TEMPERATURE)], + 'tvChannel': [ + Map('tvChannel', "Tv Channel", None, None)], + 'tvocMeasurement': [ + Map('tvocLevel', "Tvoc Measurement", 'ppm', None)], + 'ultravioletIndex': [ + Map('ultravioletIndex', "Ultraviolet Index", None, None)], + 'voltageMeasurement': [ + Map('voltage', "Voltage Measurement", 'V', None)], + 'washerMode': [ + Map('washerMode', "Washer Mode", None, None)], + 'washerOperatingState': [ + Map('machineState', "Washer Machine State", None, None), + Map('washerJobState', "Washer Job State", None, None), + Map('completionTime', "Washer Completion Time", None, + DEVICE_CLASS_TIMESTAMP)], + 'windowShade': [ + Map('windowShade', 'Window Shade', None, None)] +} + +UNITS = { + 'C': TEMP_CELSIUS, + 'F': TEMP_FAHRENHEIT +} + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add binary sensors for a config entry.""" + broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] + sensors = [] + for device in broker.devices.values(): + for capability in broker.get_assigned(device.device_id, 'sensor'): + maps = CAPABILITY_TO_SENSORS[capability] + sensors.extend([ + SmartThingsSensor( + device, m.attribute, m.name, m.default_unit, + m.device_class) + for m in maps]) + async_add_entities(sensors) + + +def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]: + """Return all capabilities supported if minimum required are present.""" + return [capability for capability in CAPABILITY_TO_SENSORS + if capability in capabilities] + + +class SmartThingsSensor(SmartThingsEntity): + """Define a SmartThings Binary Sensor.""" + + def __init__(self, device, attribute: str, name: str, + default_unit: str, device_class: str): + """Init the class.""" + super().__init__(device) + self._attribute = attribute + self._name = name + self._device_class = device_class + self._default_unit = default_unit + + @property + def name(self) -> str: + """Return the name of the binary sensor.""" + return '{} {}'.format(self._device.label, self._name) + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return '{}.{}'.format(self._device.device_id, self._attribute) + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.status.attributes[self._attribute].value + + @property + def device_class(self): + """Return the device class of the sensor.""" + return self._device_class + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + unit = self._device.status.attributes[self._attribute].unit + return UNITS.get(unit, unit) if unit else self._default_unit diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py index 9d9dacf8460..89043d4f76c 100644 --- a/homeassistant/components/smartthings/smartapp.py +++ b/homeassistant/components/smartthings/smartapp.py @@ -22,8 +22,7 @@ from homeassistant.helpers.typing import HomeAssistantType from .const import ( APP_NAME_PREFIX, APP_OAUTH_SCOPES, CONF_APP_ID, CONF_INSTALLED_APP_ID, CONF_INSTANCE_ID, CONF_LOCATION_ID, DATA_BROKERS, DATA_MANAGER, DOMAIN, - SETTINGS_INSTANCE_ID, SIGNAL_SMARTAPP_PREFIX, STORAGE_KEY, STORAGE_VERSION, - SUPPORTED_CAPABILITIES) + SETTINGS_INSTANCE_ID, SIGNAL_SMARTAPP_PREFIX, STORAGE_KEY, STORAGE_VERSION) _LOGGER = logging.getLogger(__name__) @@ -176,6 +175,7 @@ async def setup_smartapp_endpoint(hass: HomeAssistantType): webhook.async_generate_path(config[CONF_WEBHOOK_ID]), dispatcher=dispatcher) manager.connect_install(functools.partial(smartapp_install, hass)) + manager.connect_update(functools.partial(smartapp_update, hass)) manager.connect_uninstall(functools.partial(smartapp_uninstall, hass)) webhook.async_register(hass, DOMAIN, 'SmartApp', @@ -189,6 +189,45 @@ async def setup_smartapp_endpoint(hass: HomeAssistantType): } +async def smartapp_sync_subscriptions( + hass: HomeAssistantType, auth_token: str, location_id: str, + installed_app_id: str, *, skip_delete=False): + """Synchronize subscriptions of an installed up.""" + from pysmartthings import ( + CAPABILITIES, SmartThings, SourceType, Subscription) + + api = SmartThings(async_get_clientsession(hass), auth_token) + devices = await api.devices(location_ids=[location_id]) + + # Build set of capabilities and prune unsupported ones + capabilities = set() + for device in devices: + capabilities.update(device.capabilities) + capabilities.intersection_update(CAPABILITIES) + + # Remove all (except for installs) + if not skip_delete: + await api.delete_subscriptions(installed_app_id) + + # Create for each capability + async def create_subscription(target): + sub = Subscription() + sub.installed_app_id = installed_app_id + sub.location_id = location_id + sub.source_type = SourceType.CAPABILITY + sub.capability = target + try: + await api.create_subscription(sub) + _LOGGER.debug("Created subscription for '%s' under app '%s'", + target, installed_app_id) + except Exception: # pylint:disable=broad-except + _LOGGER.exception("Failed to create subscription for '%s' under " + "app '%s'", target, installed_app_id) + + tasks = [create_subscription(c) for c in capabilities] + await asyncio.gather(*tasks) + + async def smartapp_install(hass: HomeAssistantType, req, resp, app): """ Handle when a SmartApp is installed by the user into a location. @@ -199,30 +238,9 @@ async def smartapp_install(hass: HomeAssistantType, req, resp, app): representing the installation if this is not the first installation under the account. """ - from pysmartthings import SmartThings, Subscription, SourceType - - # This access token is a temporary 'SmartApp token' that expires in 5 min - # and is used to create subscriptions only. - api = SmartThings(async_get_clientsession(hass), req.auth_token) - - async def create_subscription(target): - sub = Subscription() - sub.installed_app_id = req.installed_app_id - sub.location_id = req.location_id - sub.source_type = SourceType.CAPABILITY - sub.capability = target - try: - await api.create_subscription(sub) - _LOGGER.debug("Created subscription for '%s' under app '%s'", - target, req.installed_app_id) - except Exception: # pylint:disable=broad-except - _LOGGER.exception("Failed to create subscription for '%s' under " - "app '%s'", target, req.installed_app_id) - - tasks = [create_subscription(c) for c in SUPPORTED_CAPABILITIES] - await asyncio.gather(*tasks) - _LOGGER.debug("SmartApp '%s' under parent app '%s' was installed", - req.installed_app_id, app.app_id) + await smartapp_sync_subscriptions( + hass, req.auth_token, req.location_id, req.installed_app_id, + skip_delete=True) # The permanent access token is copied from another config flow with the # same parent app_id. If one is not found, that means the user is within @@ -244,6 +262,19 @@ async def smartapp_install(hass: HomeAssistantType, req, resp, app): }) +async def smartapp_update(hass: HomeAssistantType, req, resp, app): + """ + Handle when a SmartApp is updated (reconfigured) by the user. + + Synchronize subscriptions to ensure we're up-to-date. + """ + await smartapp_sync_subscriptions( + hass, req.auth_token, req.location_id, req.installed_app_id) + + _LOGGER.debug("SmartApp '%s' under parent app '%s' was updated", + req.installed_app_id, app.app_id) + + async def smartapp_uninstall(hass: HomeAssistantType, req, resp, app): """ Handle when a SmartApp is removed from a location by the user. diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index 1fb4e878cb4..3578bcd5138 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -21,7 +21,8 @@ "token_already_setup": "The token has already been setup.", "app_setup_error": "Unable to setup the SmartApp. Please try again.", "app_not_installed": "Please ensure you have installed and authorized the Home Assistant SmartApp and try again.", - "base_url_not_https": "The `base_url` for the `http` component must be configured and start with `https://`." + "base_url_not_https": "The `base_url` for the `http` component must be configured and start with `https://`.", + "webhook_error": "SmartThings could not validate the endpoint configured in `base_url`. Please review the component requirements." } } } \ No newline at end of file diff --git a/homeassistant/components/smartthings/switch.py b/homeassistant/components/smartthings/switch.py index 1fccfcd3619..5a1224f4fc2 100644 --- a/homeassistant/components/smartthings/switch.py +++ b/homeassistant/components/smartthings/switch.py @@ -1,9 +1,6 @@ -""" -Support for switches through the SmartThings cloud API. +"""Support for switches through the SmartThings cloud API.""" +from typing import Optional, Sequence -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/smartthings.switch/ -""" from homeassistant.components.switch import SwitchDevice from . import SmartThingsEntity @@ -12,8 +9,8 @@ from .const import DATA_BROKERS, DOMAIN DEPENDENCIES = ['smartthings'] -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Platform uses config entry setup.""" pass @@ -23,28 +20,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] async_add_entities( [SmartThingsSwitch(device) for device in broker.devices.values() - if is_switch(device)]) + if broker.any_assigned(device.device_id, 'switch')]) -def is_switch(device): - """Determine if the device should be represented as a switch.""" +def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]: + """Return all capabilities supported if minimum required are present.""" from pysmartthings import Capability # Must be able to be turned on/off. - if Capability.switch not in device.capabilities: - return False - # Must not have a capability represented by other types. - non_switch_capabilities = [ - Capability.color_control, - Capability.color_temperature, - Capability.fan_speed, - Capability.switch_level - ] - if any(capability in device.capabilities - for capability in non_switch_capabilities): - return False - - return True + if Capability.switch in capabilities: + return [Capability.switch] + return None class SmartThingsSwitch(SmartThingsEntity, SwitchDevice): diff --git a/homeassistant/components/smhi/.translations/da.json b/homeassistant/components/smhi/.translations/da.json new file mode 100644 index 00000000000..b43fef7ec45 --- /dev/null +++ b/homeassistant/components/smhi/.translations/da.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Navnet findes allerede", + "wrong_location": "Placering kun i Sverige" + }, + "step": { + "user": { + "data": { + "latitude": "Breddegrad", + "longitude": "L\u00e6ngdegrad", + "name": "Navn" + }, + "title": "Placering i Sverige" + } + }, + "title": "Svensk vejr service (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/fr.json b/homeassistant/components/smhi/.translations/fr.json index d1378f183d5..aa4589e558d 100644 --- a/homeassistant/components/smhi/.translations/fr.json +++ b/homeassistant/components/smhi/.translations/fr.json @@ -1,7 +1,8 @@ { "config": { "error": { - "name_exists": "Ce nom est d\u00e9j\u00e0 utilis\u00e9" + "name_exists": "Ce nom est d\u00e9j\u00e0 utilis\u00e9", + "wrong_location": "En Su\u00e8de uniquement" }, "step": { "user": { @@ -9,8 +10,10 @@ "latitude": "Latitude", "longitude": "Longitude", "name": "Nom" - } + }, + "title": "Localisation en Su\u00e8de" } - } + }, + "title": "Service m\u00e9t\u00e9orologique su\u00e9dois (SMHI)" } } \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/ru.json b/homeassistant/components/smhi/.translations/ru.json index 012bb74c568..3496d19f5f4 100644 --- a/homeassistant/components/smhi/.translations/ru.json +++ b/homeassistant/components/smhi/.translations/ru.json @@ -14,6 +14,6 @@ "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)" + "title": "\u041c\u0435\u0442\u0435\u043e\u0440\u043e\u043b\u043e\u0433\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0428\u0432\u0435\u0446\u0438\u0438 (SMHI)" } } \ No newline at end of file diff --git a/homeassistant/components/smhi/__init__.py b/homeassistant/components/smhi/__init__.py index 0ca3bac3e35..6af8c14843b 100644 --- a/homeassistant/components/smhi/__init__.py +++ b/homeassistant/components/smhi/__init__.py @@ -1,9 +1,4 @@ -""" -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/ -""" +"""Support for the Swedish weather institute weather service.""" from homeassistant.config_entries import ConfigEntry from homeassistant.core import Config, HomeAssistant @@ -11,7 +6,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.5'] +REQUIREMENTS = ['smhi-pkg==1.0.8'] DEFAULT_NAME = 'smhi' diff --git a/homeassistant/components/weather/smhi.py b/homeassistant/components/smhi/weather.py similarity index 88% rename from homeassistant/components/weather/smhi.py rename to homeassistant/components/smhi/weather.py index 94873b03bd6..75a0c51d010 100644 --- a/homeassistant/components/weather/smhi.py +++ b/homeassistant/components/smhi/weather.py @@ -1,9 +1,4 @@ -""" -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/ -""" +"""Support for the Swedish weather institute weather service.""" import asyncio from datetime import timedelta import logging @@ -22,7 +17,7 @@ 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 Throttle, dt, slugify +from homeassistant.util import Throttle, slugify DEPENDENCIES = ['smhi'] @@ -208,25 +203,24 @@ class SmhiWeather(WeatherEntity): @property def forecast(self) -> List: """Return the forecast.""" - if self._forecasts is None: + if self._forecasts is None or len(self._forecasts) < 2: return None data = [] - for forecast in self._forecasts: + + for forecast in self._forecasts[1:]: 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, - }) + data.append({ + ATTR_FORECAST_TIME: forecast.valid_time.isoformat(), + ATTR_FORECAST_TEMP: forecast.temperature_max, + ATTR_FORECAST_TEMP_LOW: forecast.temperature_min, + ATTR_FORECAST_PRECIPITATION: + round(forecast.total_precipitation), + ATTR_FORECAST_CONDITION: condition, + }) return data diff --git a/homeassistant/components/snips.py b/homeassistant/components/snips/__init__.py similarity index 97% rename from homeassistant/components/snips.py rename to homeassistant/components/snips/__init__.py index 1aebeae59cb..9a5508c8f32 100644 --- a/homeassistant/components/snips.py +++ b/homeassistant/components/snips/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Snips on-device ASR and NLU. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/snips/ -""" +"""Support for Snips on-device ASR and NLU.""" import json import logging from datetime import timedelta diff --git a/homeassistant/components/snips/services.yaml b/homeassistant/components/snips/services.yaml new file mode 100644 index 00000000000..cca39062ce9 --- /dev/null +++ b/homeassistant/components/snips/services.yaml @@ -0,0 +1,31 @@ +feedback_off: + description: Turns feedback sounds off. + fields: + site_id: {description: 'Site to turn sounds on, defaults to all sites (optional)', + example: bedroom} +feedback_on: + description: Turns feedback sounds on. + fields: + site_id: {description: 'Site to turn sounds on, defaults to all sites (optional)', + example: bedroom} +say: + description: Send a TTS message to Snips. + fields: + custom_data: {description: custom data that will be included with all messages + in this session, example: user=UserName} + site_id: {description: 'Site to use to start session, defaults to default (optional)', + example: bedroom} + text: {description: Text to say., example: My name is snips} +say_action: + description: Send a TTS message to Snips to listen for a response. + fields: + can_be_enqueued: {description: 'If True, session waits for an open session to + end, if False session is dropped if one is running', example: true} + custom_data: {description: custom data that will be included with all messages + in this session, example: user=UserName} + intent_filter: {description: Optional Array of Strings - A list of intents names + to restrict the NLU resolution to on the first query., example: 'turnOnLights, + turnOffLights'} + site_id: {description: 'Site to use to start session, defaults to default (optional)', + example: bedroom} + text: {description: Text to say, example: My name is snips} diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index b4f507a60dd..69d5a9bfc33 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -1,4 +1,4 @@ -"""Component to embed Sonos.""" +"""Support to embed Sonos.""" from homeassistant import config_entries from homeassistant.helpers import config_entry_flow diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/sonos/media_player.py similarity index 99% rename from homeassistant/components/media_player/sonos.py rename to homeassistant/components/sonos/media_player.py index b34aabd4c51..2a7eafaf835 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/sonos/media_player.py @@ -1,9 +1,4 @@ -""" -Support to interface with Sonos players. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/media_player.sonos/ -""" +"""Support to interface with Sonos players.""" import datetime import functools as ft import logging @@ -15,11 +10,13 @@ import requests import voluptuous as vol from homeassistant.components.media_player import ( - ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA, + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, SUPPORT_STOP, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice) + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET) from homeassistant.components.sonos import DOMAIN as SONOS_DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TIME, CONF_HOSTS, STATE_IDLE, STATE_OFF, STATE_PAUSED, diff --git a/homeassistant/components/spaceapi.py b/homeassistant/components/spaceapi/__init__.py similarity index 97% rename from homeassistant/components/spaceapi.py rename to homeassistant/components/spaceapi/__init__.py index fa2e5e8e1ea..fb76718f2d5 100644 --- a/homeassistant/components/spaceapi.py +++ b/homeassistant/components/spaceapi/__init__.py @@ -1,9 +1,4 @@ -""" -Support for the SpaceAPI. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/spaceapi/ -""" +"""Support for the SpaceAPI.""" import logging import voluptuous as vol diff --git a/homeassistant/components/spc.py b/homeassistant/components/spc/__init__.py similarity index 91% rename from homeassistant/components/spc.py rename to homeassistant/components/spc/__init__.py index dd1931e27f1..8aafb6f1210 100644 --- a/homeassistant/components/spc.py +++ b/homeassistant/components/spc/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Vanderbilt (formerly Siemens) SPC alarm systems. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/spc/ -""" +"""Support for Vanderbilt (formerly Siemens) SPC alarm systems.""" import logging import voluptuous as vol diff --git a/homeassistant/components/speedtestdotnet/__init__.py b/homeassistant/components/speedtestdotnet/__init__.py new file mode 100644 index 00000000000..4eae738b0d3 --- /dev/null +++ b/homeassistant/components/speedtestdotnet/__init__.py @@ -0,0 +1,99 @@ +"""Support for testing internet speed via Speedtest.net.""" +import logging +from datetime import timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.speedtestdotnet.const import DOMAIN, \ + DATA_UPDATED, SENSOR_TYPES +from homeassistant.const import CONF_MONITORED_CONDITIONS, \ + CONF_UPDATE_INTERVAL, CONF_SCAN_INTERVAL, \ + CONF_UPDATE_INTERVAL_INVALIDATION_VERSION +from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN + +REQUIREMENTS = ['speedtest-cli==2.0.2'] + +_LOGGER = logging.getLogger(__name__) + +CONF_SERVER_ID = 'server_id' +CONF_MANUAL = 'manual' + +DEFAULT_INTERVAL = timedelta(hours=1) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All( + vol.Schema({ + vol.Optional(CONF_SERVER_ID): cv.positive_int, + vol.Optional(CONF_UPDATE_INTERVAL): + vol.All(cv.time_period, cv.positive_timedelta), + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_INTERVAL): + vol.All(cv.time_period, cv.positive_timedelta), + vol.Optional(CONF_MANUAL, default=False): cv.boolean, + vol.Optional( + CONF_MONITORED_CONDITIONS, + default=list(SENSOR_TYPES) + ): vol.All(cv.ensure_list, [vol.In(list(SENSOR_TYPES))]) + }), + cv.deprecated( + CONF_UPDATE_INTERVAL, + replacement_key=CONF_SCAN_INTERVAL, + invalidation_version=CONF_UPDATE_INTERVAL_INVALIDATION_VERSION, + default=DEFAULT_INTERVAL + ) + ) +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the Speedtest.net component.""" + conf = config[DOMAIN] + data = hass.data[DOMAIN] = SpeedtestData(hass, conf.get(CONF_SERVER_ID)) + + if not conf[CONF_MANUAL]: + async_track_time_interval( + hass, data.update, conf[CONF_SCAN_INTERVAL] + ) + + def update(call=None): + """Service call to manually update the data.""" + data.update() + + hass.services.async_register(DOMAIN, 'speedtest', update) + + hass.async_create_task( + async_load_platform( + hass, + SENSOR_DOMAIN, + DOMAIN, + conf[CONF_MONITORED_CONDITIONS], + config + ) + ) + + return True + + +class SpeedtestData: + """Get the latest data from speedtest.net.""" + + def __init__(self, hass, server_id): + """Initialize the data object.""" + self.data = None + self._hass = hass + self._servers = [] if server_id is None else [server_id] + + def update(self, now=None): + """Get the latest data from speedtest.net.""" + import speedtest + _LOGGER.debug("Executing speedtest.net speedtest") + speed = speedtest.Speedtest() + speed.get_servers(self._servers) + speed.get_best_server() + speed.download() + speed.upload() + self.data = speed.results.dict() + dispatcher_send(self._hass, DATA_UPDATED) diff --git a/homeassistant/components/speedtestdotnet/const.py b/homeassistant/components/speedtestdotnet/const.py new file mode 100644 index 00000000000..7f19d796fd0 --- /dev/null +++ b/homeassistant/components/speedtestdotnet/const.py @@ -0,0 +1,10 @@ +"""Consts used by Speedtest.net.""" + +DOMAIN = 'speedtestdotnet' +DATA_UPDATED = '{}_data_updated'.format(DOMAIN) + +SENSOR_TYPES = { + 'ping': ['Ping', 'ms'], + 'download': ['Download', 'Mbit/s'], + 'upload': ['Upload', 'Mbit/s'], +} diff --git a/homeassistant/components/speedtestdotnet/sensor.py b/homeassistant/components/speedtestdotnet/sensor.py new file mode 100644 index 00000000000..4deb6550444 --- /dev/null +++ b/homeassistant/components/speedtestdotnet/sensor.py @@ -0,0 +1,118 @@ +"""Support for Speedtest.net internet speed testing sensor.""" +import logging + +from homeassistant.components.speedtestdotnet.const import \ + DOMAIN as SPEEDTESTDOTNET_DOMAIN, DATA_UPDATED, SENSOR_TYPES +from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.restore_state import RestoreEntity + +DEPENDENCIES = ['speedtestdotnet'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_BYTES_RECEIVED = 'bytes_received' +ATTR_BYTES_SENT = 'bytes_sent' +ATTR_SERVER_COUNTRY = 'server_country' +ATTR_SERVER_HOST = 'server_host' +ATTR_SERVER_ID = 'server_id' +ATTR_SERVER_LATENCY = 'latency' +ATTR_SERVER_NAME = 'server_name' + +ATTRIBUTION = 'Data retrieved from Speedtest.net by Ookla' + +ICON = 'mdi:speedometer' + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info): + """Set up the Speedtest.net sensor.""" + data = hass.data[SPEEDTESTDOTNET_DOMAIN] + async_add_entities( + [SpeedtestSensor(data, sensor) for sensor in discovery_info] + ) + + +class SpeedtestSensor(RestoreEntity): + """Implementation of a speedtest.net sensor.""" + + def __init__(self, speedtest_data, sensor_type): + """Initialize the sensor.""" + self._name = SENSOR_TYPES[sensor_type][0] + self.speedtest_client = speedtest_data + self.type = sensor_type + self._state = None + self._data = None + self._unit_of_measurement = SENSOR_TYPES[self.type][1] + + @property + def name(self): + """Return the name of the sensor.""" + return '{} {}'.format('Speedtest', self._name) + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self._unit_of_measurement + + @property + def icon(self): + """Return icon.""" + return ICON + + @property + def should_poll(self): + """Return the polling requirement for this sensor.""" + return False + + @property + def device_state_attributes(self): + """Return the state attributes.""" + attributes = { + ATTR_ATTRIBUTION: ATTRIBUTION + } + if self._data is not None: + return attributes.update({ + ATTR_BYTES_RECEIVED: self._data['bytes_received'], + ATTR_BYTES_SENT: self._data['bytes_sent'], + ATTR_SERVER_COUNTRY: self._data['server']['country'], + ATTR_SERVER_ID: self._data['server']['id'], + ATTR_SERVER_LATENCY: self._data['server']['latency'], + ATTR_SERVER_NAME: self._data['server']['name'], + }) + return attributes + + async def async_added_to_hass(self): + """Handle entity which will be added.""" + await super().async_added_to_hass() + state = await self.async_get_last_state() + if not state: + return + self._state = state.state + + async_dispatcher_connect( + self.hass, DATA_UPDATED, self._schedule_immediate_update + ) + + def update(self): + """Get the latest data and update the states.""" + self._data = self.speedtest_client.data + if self._data is None: + return + + if self.type == 'ping': + self._state = self._data['ping'] + elif self.type == 'download': + self._state = round(self._data['download'] / 10**6, 2) + elif self.type == 'upload': + self._state = round(self._data['upload'] / 10**6, 2) + + @callback + def _schedule_immediate_update(self): + self.async_schedule_update_ha_state(True) diff --git a/homeassistant/components/speedtestdotnet/services.yaml b/homeassistant/components/speedtestdotnet/services.yaml new file mode 100644 index 00000000000..db813affe76 --- /dev/null +++ b/homeassistant/components/speedtestdotnet/services.yaml @@ -0,0 +1,2 @@ +speedtest: + description: Immediately take a speedest with Speedtest.net \ No newline at end of file diff --git a/homeassistant/components/spider.py b/homeassistant/components/spider/__init__.py similarity index 88% rename from homeassistant/components/spider.py rename to homeassistant/components/spider/__init__.py index a10fa129fe5..b565f183457 100644 --- a/homeassistant/components/spider.py +++ b/homeassistant/components/spider/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Spider Smart devices. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/spider/ -""" +"""Support for Spider Smart devices.""" from datetime import timedelta import logging @@ -14,7 +9,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform -REQUIREMENTS = ['spiderpy==1.2.0'] +REQUIREMENTS = ['spiderpy==1.3.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/climate/spider.py b/homeassistant/components/spider/climate.py similarity index 81% rename from homeassistant/components/climate/spider.py rename to homeassistant/components/spider/climate.py index a9d966bd499..08af44ad1ad 100644 --- a/homeassistant/components/climate/spider.py +++ b/homeassistant/components/spider/climate.py @@ -1,20 +1,26 @@ -""" -Support for Spider thermostats. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/climate.spider/ -""" +"""Support for Spider thermostats.""" import logging from homeassistant.components.climate import ( ATTR_TEMPERATURE, STATE_COOL, STATE_HEAT, STATE_IDLE, - SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, ClimateDevice) + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_FAN_MODE, ClimateDevice) from homeassistant.components.spider import DOMAIN as SPIDER_DOMAIN from homeassistant.const import TEMP_CELSIUS DEPENDENCIES = ['spider'] +FAN_LIST = [ + 'Auto', + 'Low', + 'Medium', + 'High', + 'Boost 10', + 'Boost 20', + 'Boost 30', +] + OPERATION_LIST = [ STATE_HEAT, STATE_COOL, @@ -23,7 +29,7 @@ OPERATION_LIST = [ HA_STATE_TO_SPIDER = { STATE_COOL: 'Cool', STATE_HEAT: 'Heat', - STATE_IDLE: 'Idle' + STATE_IDLE: 'Idle', } SPIDER_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_SPIDER.items()} @@ -55,7 +61,10 @@ class SpiderThermostat(ClimateDevice): supports = SUPPORT_TARGET_TEMPERATURE if self.thermostat.has_operation_mode: - supports = supports | SUPPORT_OPERATION_MODE + supports |= SUPPORT_OPERATION_MODE + + if self.thermostat.has_fan_mode: + supports |= SUPPORT_FAN_MODE return supports @@ -122,6 +131,20 @@ class SpiderThermostat(ClimateDevice): self.thermostat.set_operation_mode( HA_STATE_TO_SPIDER.get(operation_mode)) + @property + def current_fan_mode(self): + """Return the fan setting.""" + return self.thermostat.current_fan_speed + + def set_fan_mode(self, fan_mode): + """Set fan mode.""" + self.thermostat.set_fan_speed(fan_mode) + + @property + def fan_list(self): + """List of available fan modes.""" + return FAN_LIST + def update(self): """Get the latest data.""" self.thermostat = self.api.get_thermostat(self.unique_id) diff --git a/homeassistant/components/switch/spider.py b/homeassistant/components/spider/switch.py similarity index 92% rename from homeassistant/components/switch/spider.py rename to homeassistant/components/spider/switch.py index f4bf74ad010..227e4748515 100644 --- a/homeassistant/components/switch/spider.py +++ b/homeassistant/components/spider/switch.py @@ -1,10 +1,4 @@ -""" -Support for Spider switches. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.spider/ -""" - +"""Support for Spider switches.""" import logging from homeassistant.components.spider import DOMAIN as SPIDER_DOMAIN diff --git a/homeassistant/components/splunk.py b/homeassistant/components/splunk/__init__.py similarity index 93% rename from homeassistant/components/splunk.py rename to homeassistant/components/splunk/__init__.py index 37f56ed3b1c..fed05fe3498 100644 --- a/homeassistant/components/splunk.py +++ b/homeassistant/components/splunk/__init__.py @@ -1,9 +1,4 @@ -""" -Support to send data to an Splunk instance. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/splunk/ -""" +"""Support to send data to an Splunk instance.""" import json import logging diff --git a/homeassistant/components/statsd.py b/homeassistant/components/statsd/__init__.py similarity index 94% rename from homeassistant/components/statsd.py rename to homeassistant/components/statsd/__init__.py index 6b528733601..a8c34d0a843 100644 --- a/homeassistant/components/statsd.py +++ b/homeassistant/components/statsd/__init__.py @@ -1,9 +1,4 @@ -""" -A component which allows you to send data to StatsD. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/statsd/ -""" +"""Support for sending data to StatsD.""" import logging import voluptuous as vol diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun/__init__.py similarity index 96% rename from homeassistant/components/sun.py rename to homeassistant/components/sun/__init__.py index 250c6a2ed2f..92cdcb0a2e4 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun/__init__.py @@ -1,9 +1,4 @@ -""" -Support for functionality to keep track of the sun. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/sun/ -""" +"""Support for functionality to keep track of the sun.""" import logging from datetime import timedelta diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 513ebbcb5ea..d517f635a92 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -12,7 +12,8 @@ import voluptuous as vol from homeassistant.loader import bind_hass from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import ToggleEntity -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa +from homeassistant.helpers.config_validation import ( # noqa + PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) import homeassistant.helpers.config_validation as cv from homeassistant.const import ( STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, diff --git a/homeassistant/components/switch/broadlink.py b/homeassistant/components/switch/broadlink.py index 9c17767f033..2237a0a2977 100644 --- a/homeassistant/components/switch/broadlink.py +++ b/homeassistant/components/switch/broadlink.py @@ -235,7 +235,7 @@ class BroadlinkRMSwitch(SwitchDevice): self._device.send_data(packet) except (socket.timeout, ValueError) as error: if retry < 1: - _LOGGER.error(error) + _LOGGER.error("Error during sending a packet: %s", error) return False if not self._auth(): return False @@ -247,6 +247,8 @@ class BroadlinkRMSwitch(SwitchDevice): auth = self._device.auth() except socket.timeout: auth = False + if retry < 1: + _LOGGER.error("Timeout during authorization") if not auth and retry > 0: return self._auth(retry-1) return auth @@ -268,7 +270,7 @@ class BroadlinkSP1Switch(BroadlinkRMSwitch): self._device.set_power(packet) except (socket.timeout, ValueError) as error: if retry < 1: - _LOGGER.error(error) + _LOGGER.error("Error during sending a packet: %s", error) return False if not self._auth(): return False @@ -308,7 +310,7 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch): load_power = self._device.get_energy() except (socket.timeout, ValueError) as error: if retry < 1: - _LOGGER.error(error) + _LOGGER.error("Error during updating the state: %s", error) return if not self._auth(): return @@ -341,7 +343,7 @@ class BroadlinkMP1Slot(BroadlinkRMSwitch): self._device.set_power(self._slot, packet) except (socket.timeout, ValueError) as error: if retry < 1: - _LOGGER.error(error) + _LOGGER.error("Error during sending a packet: %s", error) return False if not self._auth(): return False @@ -382,7 +384,7 @@ class BroadlinkMP1Switch: states = self._device.check_power() except (socket.timeout, ValueError) as error: if retry < 1: - _LOGGER.error(error) + _LOGGER.error("Error during updating the state: %s", error) return if not self._auth(): return diff --git a/homeassistant/components/switch/switchmate.py b/homeassistant/components/switch/switchmate.py index 23794abeba4..be80ef19169 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.4'] +REQUIREMENTS = ['pySwitchmate==0.4.5'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/tplink.py b/homeassistant/components/switch/tplink.py index 01d5c2cc72c..67c8094a1f2 100644 --- a/homeassistant/components/switch/tplink.py +++ b/homeassistant/components/switch/tplink.py @@ -14,7 +14,7 @@ from homeassistant.components.switch import ( from homeassistant.const import (CONF_HOST, CONF_NAME, ATTR_VOLTAGE) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyHS100==0.3.3'] +REQUIREMENTS = ['pyHS100==0.3.4'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/system_health/__init__.py b/homeassistant/components/system_health/__init__.py index fca433550d7..9a171296ce9 100644 --- a/homeassistant/components/system_health/__init__.py +++ b/homeassistant/components/system_health/__init__.py @@ -1,4 +1,4 @@ -"""System health component.""" +"""Support for System health .""" import asyncio from collections import OrderedDict import logging @@ -7,15 +7,17 @@ from typing import Callable, Dict import async_timeout import voluptuous as vol -from homeassistant.core import callback -from homeassistant.loader import bind_hass -from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.components import websocket_api +from homeassistant.core import callback +from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.loader import bind_hass + +_LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['http'] DOMAIN = 'system_health' + INFO_CALLBACK_TIMEOUT = 5 -_LOGGER = logging.getLogger(__name__) @bind_hass diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 8ab6bd752ef..16786bdeba4 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -1,11 +1,5 @@ -""" -Support for system log. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/system_log/ -""" -from collections import deque -from io import StringIO +"""Support for system log.""" +from collections import OrderedDict import logging import re import traceback @@ -89,11 +83,63 @@ def _figure_out_source(record, call_stack, hass): return record.pathname -def _exception_as_string(exc_info): - buf = StringIO() - if exc_info: - traceback.print_exception(*exc_info, file=buf) - return buf.getvalue() +class LogEntry: + """Store HA log entries.""" + + def __init__(self, record, stack, source): + """Initialize a log entry.""" + self.first_occured = self.timestamp = record.created + self.level = record.levelname + self.message = record.getMessage() + if record.exc_info: + self.exception = ''.join( + traceback.format_exception(*record.exc_info)) + _, _, tb = record.exc_info # pylint: disable=invalid-name + # Last line of traceback contains the root cause of the exception + self.root_cause = str(traceback.extract_tb(tb)[-1]) + else: + self.exception = '' + self.root_cause = None + self.source = source + self.count = 1 + + def hash(self): + """Calculate a key for DedupStore.""" + return frozenset([self.message, self.root_cause]) + + def to_dict(self): + """Convert object into dict to maintain backward compatability.""" + return vars(self) + + +class DedupStore(OrderedDict): + """Data store to hold max amount of deduped entries.""" + + def __init__(self, maxlen=50): + """Initialize a new DedupStore.""" + super().__init__() + self.maxlen = maxlen + + def add_entry(self, entry): + """Add a new entry.""" + key = str(entry.hash()) + + if key in self: + # Update stored entry + self[key].count += 1 + self[key].timestamp = entry.timestamp + + self.move_to_end(key) + else: + self[key] = entry + + if len(self) > self.maxlen: + # Removes the first record which should also be the oldest + self.popitem(last=False) + + def to_list(self): + """Return reversed list of log entries - LIFO.""" + return [value.to_dict() for value in reversed(self.values())] class LogErrorHandler(logging.Handler): @@ -103,18 +149,9 @@ class LogErrorHandler(logging.Handler): """Initialize a new LogErrorHandler.""" super().__init__() self.hass = hass - self.records = deque(maxlen=maxlen) + self.records = DedupStore(maxlen=maxlen) self.fire_event = fire_event - def _create_entry(self, record, call_stack): - return { - 'timestamp': record.created, - 'level': record.levelname, - 'message': record.getMessage(), - 'exception': _exception_as_string(record.exc_info), - 'source': _figure_out_source(record, call_stack, self.hass), - } - def emit(self, record): """Save error and warning logs. @@ -127,10 +164,11 @@ class LogErrorHandler(logging.Handler): if not record.exc_info: stack = [f for f, _, _, _ in traceback.extract_stack()] - entry = self._create_entry(record, stack) - self.records.appendleft(entry) + entry = LogEntry(record, stack, + _figure_out_source(record, stack, self.hass)) + self.records.add_entry(entry) if self.fire_event: - self.hass.bus.fire(EVENT_SYSTEM_LOG, entry) + self.hass.bus.fire(EVENT_SYSTEM_LOG, entry.to_dict()) async def async_setup(hass, config): @@ -186,6 +224,4 @@ class AllErrorsView(HomeAssistantView): async def get(self, request): """Get all errors and warnings.""" - # deque is not serializable (it's just "list-like") so it must be - # converted to a list before it can be serialized to json - return self.json(list(self.handler.records)) + return self.json(self.handler.records.to_list()) diff --git a/homeassistant/components/tado.py b/homeassistant/components/tado/__init__.py similarity index 94% rename from homeassistant/components/tado.py rename to homeassistant/components/tado/__init__.py index 7c045518132..767e29ba0b9 100644 --- a/homeassistant/components/tado.py +++ b/homeassistant/components/tado/__init__.py @@ -1,9 +1,4 @@ -""" -Support for the (unofficial) Tado api. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/tado/ -""" +"""Support for the (unofficial) Tado API.""" import logging import urllib from datetime import timedelta @@ -31,7 +26,7 @@ TADO_COMPONENTS = [ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string + vol.Required(CONF_PASSWORD): cv.string, }) }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/climate/tado.py b/homeassistant/components/tado/climate.py similarity index 98% rename from homeassistant/components/climate/tado.py rename to homeassistant/components/tado/climate.py index 1e52c163624..1812d36b7cd 100644 --- a/homeassistant/components/climate/tado.py +++ b/homeassistant/components/tado/climate.py @@ -1,9 +1,4 @@ -""" -Tado component to create a climate device for each zone. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/climate.tado/ -""" +"""Support for Tado to create a climate device for each zone.""" import logging from homeassistant.const import (PRECISION_TENTHS, TEMP_CELSIUS) diff --git a/homeassistant/components/device_tracker/tado.py b/homeassistant/components/tado/device_tracker.py similarity index 95% rename from homeassistant/components/device_tracker/tado.py rename to homeassistant/components/tado/device_tracker.py index ef816338ce9..7812bbd812b 100644 --- a/homeassistant/components/device_tracker/tado.py +++ b/homeassistant/components/tado/device_tracker.py @@ -1,9 +1,4 @@ -""" -Support for Tado Smart Thermostat. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/device_tracker.tado/ -""" +"""Support for Tado Smart device trackers.""" import logging from datetime import timedelta from collections import namedtuple @@ -29,7 +24,7 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_HOME_ID): cv.string + vol.Optional(CONF_HOME_ID): cv.string, }) diff --git a/homeassistant/components/sensor/tado.py b/homeassistant/components/tado/sensor.py similarity index 97% rename from homeassistant/components/sensor/tado.py rename to homeassistant/components/tado/sensor.py index 46ad6206fff..a1eb918ac5d 100644 --- a/homeassistant/components/sensor/tado.py +++ b/homeassistant/components/tado/sensor.py @@ -1,9 +1,4 @@ -""" -Tado component to create some sensors for each zone. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.tado/ -""" +"""Support for Tado sensors for each zone.""" import logging from homeassistant.components.tado import DATA_TADO diff --git a/homeassistant/components/tahoma.py b/homeassistant/components/tahoma/__init__.py similarity index 96% rename from homeassistant/components/tahoma.py rename to homeassistant/components/tahoma/__init__.py index 5e30b845863..e76cadc7ce3 100644 --- a/homeassistant/components/tahoma.py +++ b/homeassistant/components/tahoma/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Tahoma devices. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/tahoma/ -""" +"""Support for Tahoma devices.""" from collections import defaultdict import logging import voluptuous as vol diff --git a/homeassistant/components/binary_sensor/tahoma.py b/homeassistant/components/tahoma/binary_sensor.py similarity index 94% rename from homeassistant/components/binary_sensor/tahoma.py rename to homeassistant/components/tahoma/binary_sensor.py index 73035a2da0d..69855f7cb57 100644 --- a/homeassistant/components/binary_sensor/tahoma.py +++ b/homeassistant/components/tahoma/binary_sensor.py @@ -1,10 +1,4 @@ -""" -Support for Tahoma binary sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.tahoma/ -""" - +"""Support for Tahoma binary sensors.""" import logging from datetime import timedelta diff --git a/homeassistant/components/cover/tahoma.py b/homeassistant/components/tahoma/cover.py similarity index 97% rename from homeassistant/components/cover/tahoma.py rename to homeassistant/components/tahoma/cover.py index baf32073c44..6dbf9a39807 100644 --- a/homeassistant/components/cover/tahoma.py +++ b/homeassistant/components/tahoma/cover.py @@ -1,9 +1,4 @@ -""" -Support for Tahoma cover - shutters etc. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/cover.tahoma/ -""" +"""Support for Tahoma cover - shutters etc.""" from datetime import timedelta import logging diff --git a/homeassistant/components/scene/tahoma.py b/homeassistant/components/tahoma/scene.py similarity index 88% rename from homeassistant/components/scene/tahoma.py rename to homeassistant/components/tahoma/scene.py index 5846d97c7f9..643cc65aa19 100644 --- a/homeassistant/components/scene/tahoma.py +++ b/homeassistant/components/tahoma/scene.py @@ -1,9 +1,4 @@ -""" -Support for Tahoma scenes. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/scene.tahoma/ -""" +"""Support for Tahoma scenes.""" import logging from homeassistant.components.scene import Scene diff --git a/homeassistant/components/sensor/tahoma.py b/homeassistant/components/tahoma/sensor.py similarity index 95% rename from homeassistant/components/sensor/tahoma.py rename to homeassistant/components/tahoma/sensor.py index 5918bd7c9f8..8a2ea976ba7 100644 --- a/homeassistant/components/sensor/tahoma.py +++ b/homeassistant/components/tahoma/sensor.py @@ -1,10 +1,4 @@ -""" -Support for Tahoma sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.tahoma/ -""" - +"""Support for Tahoma sensors.""" import logging from datetime import timedelta diff --git a/homeassistant/components/switch/tahoma.py b/homeassistant/components/tahoma/switch.py similarity index 90% rename from homeassistant/components/switch/tahoma.py rename to homeassistant/components/tahoma/switch.py index bcac038d43b..779bff9ce0d 100644 --- a/homeassistant/components/switch/tahoma.py +++ b/homeassistant/components/tahoma/switch.py @@ -1,12 +1,4 @@ -""" -Support for Tahoma Switch - those are push buttons for garage door etc. - -Those buttons are implemented as switches that are never on. They only -receive the turn_on action, perform the relay click, and stay in OFF state - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.tahoma/ -""" +"""Support for Tahoma switches.""" import logging from homeassistant.components.switch import SwitchDevice diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 28bc7a1ad0d..18f206541df 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -1,9 +1,4 @@ -""" -Component to send and receive Telegram messages. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/telegram_bot/ -""" +"""Support to send and receive Telegram messages.""" import io from functools import partial import logging diff --git a/homeassistant/components/telegram_bot/broadcast.py b/homeassistant/components/telegram_bot/broadcast.py index 7cfcc272a33..eb52ab496d7 100644 --- a/homeassistant/components/telegram_bot/broadcast.py +++ b/homeassistant/components/telegram_bot/broadcast.py @@ -1,9 +1,4 @@ -""" -Telegram bot implementation to send messages only. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/telegram_bot.broadcast/ -""" +"""Support for Telegram bot to send messages only.""" import logging from homeassistant.components.telegram_bot import ( @@ -17,8 +12,6 @@ PLATFORM_SCHEMA = TELEGRAM_PLATFORM_SCHEMA async def async_setup_platform(hass, config): """Set up the Telegram broadcast platform.""" - # Check the API key works - bot = initialize_bot(config) bot_config = await hass.async_add_job(bot.getMe) diff --git a/homeassistant/components/telegram_bot/polling.py b/homeassistant/components/telegram_bot/polling.py index d1dea051985..5bca4321a5f 100644 --- a/homeassistant/components/telegram_bot/polling.py +++ b/homeassistant/components/telegram_bot/polling.py @@ -1,9 +1,4 @@ -""" -Telegram bot polling implementation. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/telegram_bot.polling/ -""" +"""Support for Telegram bot using polling.""" import logging from homeassistant.components.telegram_bot import ( diff --git a/homeassistant/components/telegram_bot/webhooks.py b/homeassistant/components/telegram_bot/webhooks.py index 5406ba60b13..41a206944e7 100644 --- a/homeassistant/components/telegram_bot/webhooks.py +++ b/homeassistant/components/telegram_bot/webhooks.py @@ -1,9 +1,4 @@ -""" -Allows utilizing telegram webhooks. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/telegram_bot.webhooks/ -""" +"""Support for Telegram bots using webhooks.""" import datetime as dt from ipaddress import ip_network import logging diff --git a/homeassistant/components/tellduslive/.translations/da.json b/homeassistant/components/tellduslive/.translations/da.json new file mode 100644 index 00000000000..717e3ec5ac9 --- /dev/null +++ b/homeassistant/components/tellduslive/.translations/da.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "all_configured": "TelldusLive er allerede konfigureret", + "already_setup": "TelldusLive er allerede konfigureret", + "authorize_url_fail": "Ukendt fejl ved generering af en autoriseret url.", + "authorize_url_timeout": "Timeout ved generering af autoriseret url.", + "unknown": "Ukendt fejl opstod" + }, + "error": { + "auth_error": "Godkendelsesfejl, pr\u00f8v venligst igen" + }, + "step": { + "auth": { + "description": "For at forbinde din TelldusLive-konto:\n 1. Klik p\u00e5 linket herunder\n 2. Log p\u00e5 Telldus Live\n 3. Tillad **{app_name}** (klik **Ja**). \n 4. Vend tilbage hertil og klik **SUBMIT**.\n\n [Forbind TelldusLive konto]({auth_url})", + "title": "Godkendelse mod TelldusLive" + }, + "user": { + "data": { + "host": "V\u00e6rt" + }, + "description": "Tom", + "title": "V\u00e6lg slutpunkt." + } + }, + "title": "Telldus Live" + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/fr.json b/homeassistant/components/tellduslive/.translations/fr.json index 9a3121c5896..2dd1c03022a 100644 --- a/homeassistant/components/tellduslive/.translations/fr.json +++ b/homeassistant/components/tellduslive/.translations/fr.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_setup": "TelldusLive est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "auth_error": "Erreur d'authentification, veuillez r\u00e9essayer." + }, "step": { "user": { "description": "Vide" diff --git a/homeassistant/components/tellduslive/.translations/ko.json b/homeassistant/components/tellduslive/.translations/ko.json index 29f64a87cb3..8a68e303aff 100644 --- a/homeassistant/components/tellduslive/.translations/ko.json +++ b/homeassistant/components/tellduslive/.translations/ko.json @@ -8,7 +8,7 @@ "unknown": "\uc54c \uc218\uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { - "auth_error": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574 \uc8fc\uc138\uc694." + "auth_error": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." }, "step": { "auth": { diff --git a/homeassistant/components/tellduslive/.translations/pt.json b/homeassistant/components/tellduslive/.translations/pt.json index d0d38a42caf..90da12451df 100644 --- a/homeassistant/components/tellduslive/.translations/pt.json +++ b/homeassistant/components/tellduslive/.translations/pt.json @@ -19,6 +19,6 @@ "title": "Escolher endpoint." } }, - "title": "" + "title": "Telldus Live" } } \ No newline at end of file diff --git a/homeassistant/components/tellduslive/__init__.py b/homeassistant/components/tellduslive/__init__.py index 2a57a78ee9e..397e21922d9 100644 --- a/homeassistant/components/tellduslive/__init__.py +++ b/homeassistant/components/tellduslive/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Telldus Live. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/tellduslive/ -""" +"""Support for Telldus Live.""" import asyncio from functools import partial import logging @@ -11,7 +6,8 @@ import logging import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_UPDATE_INTERVAL +from homeassistant.const import CONF_UPDATE_INTERVAL, CONF_SCAN_INTERVAL, \ + CONF_UPDATE_INTERVAL_INVALIDATION_VERSION import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later @@ -28,18 +24,23 @@ REQUIREMENTS = ['tellduslive==0.10.10'] _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All( vol.Schema({ - vol.Optional(CONF_HOST, default=DOMAIN): - cv.string, - vol.Optional(CONF_UPDATE_INTERVAL, default=SCAN_INTERVAL): - (vol.All(cv.time_period, vol.Clamp(min=MIN_UPDATE_INTERVAL))) + vol.Optional(CONF_HOST, default=DOMAIN): cv.string, + vol.Optional(CONF_UPDATE_INTERVAL): + vol.All(cv.time_period, vol.Clamp(min=MIN_UPDATE_INTERVAL)), + vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): + vol.All(cv.time_period, vol.Clamp(min=MIN_UPDATE_INTERVAL)), }), - }, - extra=vol.ALLOW_EXTRA, -) + cv.deprecated( + CONF_UPDATE_INTERVAL, + replacement_key=CONF_SCAN_INTERVAL, + invalidation_version=CONF_UPDATE_INTERVAL_INVALIDATION_VERSION, + default=SCAN_INTERVAL + ) + ) +}, extra=vol.ALLOW_EXTRA) DATA_CONFIG_ENTRY_LOCK = 'tellduslive_config_entry_lock' CONFIG_ENTRY_IS_SETUP = 'telldus_config_entry_is_setup' @@ -108,7 +109,7 @@ async def async_setup(hass, config): context={'source': config_entries.SOURCE_IMPORT}, data={ KEY_HOST: config[DOMAIN].get(CONF_HOST), - KEY_SCAN_INTERVAL: config[DOMAIN].get(CONF_UPDATE_INTERVAL), + KEY_SCAN_INTERVAL: config[DOMAIN][CONF_SCAN_INTERVAL], })) return True diff --git a/homeassistant/components/binary_sensor/tellduslive.py b/homeassistant/components/tellduslive/binary_sensor.py similarity index 76% rename from homeassistant/components/binary_sensor/tellduslive.py rename to homeassistant/components/tellduslive/binary_sensor.py index f6ed85db132..85faeca96d4 100644 --- a/homeassistant/components/binary_sensor/tellduslive.py +++ b/homeassistant/components/tellduslive/binary_sensor.py @@ -1,12 +1,4 @@ -""" -Support for binary sensors using Tellstick Net. - -This platform uses the Telldus Live online service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.tellduslive/ - -""" +"""Support for binary sensors using Tellstick Net.""" import logging from homeassistant.components import binary_sensor, tellduslive @@ -35,8 +27,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_dispatcher_connect( hass, - tellduslive.TELLDUS_DISCOVERY_NEW.format(binary_sensor.DOMAIN, - tellduslive.DOMAIN), + tellduslive.TELLDUS_DISCOVERY_NEW.format( + binary_sensor.DOMAIN, tellduslive.DOMAIN), async_discover_binary_sensor) diff --git a/homeassistant/components/cover/tellduslive.py b/homeassistant/components/tellduslive/cover.py similarity index 87% rename from homeassistant/components/cover/tellduslive.py rename to homeassistant/components/tellduslive/cover.py index 1879c88c83c..5a22311d7f0 100644 --- a/homeassistant/components/cover/tellduslive.py +++ b/homeassistant/components/tellduslive/cover.py @@ -1,11 +1,4 @@ -""" -Support for Tellstick covers using Tellstick Net. - -This platform uses the Telldus Live online service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/cover.tellduslive/ -""" +"""Support for Tellstick covers using Tellstick Net.""" import logging from homeassistant.components import cover, tellduslive diff --git a/homeassistant/components/tellduslive/entry.py b/homeassistant/components/tellduslive/entry.py index d6e56329699..9255f9da645 100644 --- a/homeassistant/components/tellduslive/entry.py +++ b/homeassistant/components/tellduslive/entry.py @@ -1,4 +1,4 @@ -"""Base Entity for all TelldusLiveEntities.""" +"""Base Entity for all TelldusLive entities.""" from datetime import datetime import logging diff --git a/homeassistant/components/light/tellduslive.py b/homeassistant/components/tellduslive/light.py similarity index 90% rename from homeassistant/components/light/tellduslive.py rename to homeassistant/components/tellduslive/light.py index 3f14b34ea78..10eaee1ad8b 100644 --- a/homeassistant/components/light/tellduslive.py +++ b/homeassistant/components/tellduslive/light.py @@ -1,11 +1,4 @@ -""" -Support for Tellstick switches using Tellstick Net. - -This platform uses the Telldus Live online service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.tellduslive/ -""" +"""Support for Tellstick lights using Tellstick Net.""" import logging from homeassistant.components import light, tellduslive diff --git a/homeassistant/components/sensor/tellduslive.py b/homeassistant/components/tellduslive/sensor.py similarity index 96% rename from homeassistant/components/sensor/tellduslive.py rename to homeassistant/components/tellduslive/sensor.py index f024f62109b..48133fd69e6 100644 --- a/homeassistant/components/sensor/tellduslive.py +++ b/homeassistant/components/tellduslive/sensor.py @@ -1,9 +1,4 @@ -""" -Support for Tellstick Net/Telstick Live. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.tellduslive/ -""" +"""Support for Tellstick Net/Telstick Live sensors.""" import logging from homeassistant.components import sensor, tellduslive diff --git a/homeassistant/components/switch/tellduslive.py b/homeassistant/components/tellduslive/switch.py similarity index 86% rename from homeassistant/components/switch/tellduslive.py rename to homeassistant/components/tellduslive/switch.py index 5c04e872623..63d1512698c 100644 --- a/homeassistant/components/switch/tellduslive.py +++ b/homeassistant/components/tellduslive/switch.py @@ -1,12 +1,4 @@ -""" -Support for Tellstick switches using Tellstick Net. - -This platform uses the Telldus Live online service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.tellduslive/ - -""" +"""Support for Tellstick switches using Tellstick Net.""" import logging from homeassistant.components import switch, tellduslive diff --git a/homeassistant/components/tellstick.py b/homeassistant/components/tellstick/__init__.py similarity index 98% rename from homeassistant/components/tellstick.py rename to homeassistant/components/tellstick/__init__.py index 8f1c45d7312..c35d2f79027 100644 --- a/homeassistant/components/tellstick.py +++ b/homeassistant/components/tellstick/__init__.py @@ -1,9 +1,4 @@ -""" -Tellstick Component. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/tellstick/ -""" +"""Support for Tellstick.""" import logging import threading diff --git a/homeassistant/components/cover/tellstick.py b/homeassistant/components/tellstick/cover.py similarity index 91% rename from homeassistant/components/cover/tellstick.py rename to homeassistant/components/tellstick/cover.py index 88608ac42e9..d0c9c031435 100644 --- a/homeassistant/components/cover/tellstick.py +++ b/homeassistant/components/tellstick/cover.py @@ -1,11 +1,4 @@ -""" -Support for Tellstick covers. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/cover.tellstick/ -""" - - +"""Support for Tellstick covers.""" from homeassistant.components.cover import CoverDevice from homeassistant.components.tellstick import ( DEFAULT_SIGNAL_REPETITIONS, ATTR_DISCOVER_DEVICES, ATTR_DISCOVER_CONFIG, diff --git a/homeassistant/components/light/tellstick.py b/homeassistant/components/tellstick/light.py similarity index 94% rename from homeassistant/components/light/tellstick.py rename to homeassistant/components/tellstick/light.py index cf9dd545e99..5deee5e08a6 100644 --- a/homeassistant/components/light/tellstick.py +++ b/homeassistant/components/tellstick/light.py @@ -1,10 +1,4 @@ -""" -Support for Tellstick lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.tellstick/ -""" - +"""Support for Tellstick lights.""" from homeassistant.components.light import ( ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) from homeassistant.components.tellstick import ( diff --git a/homeassistant/components/sensor/tellstick.py b/homeassistant/components/tellstick/sensor.py similarity index 95% rename from homeassistant/components/sensor/tellstick.py rename to homeassistant/components/tellstick/sensor.py index aac97580f2c..c6d281772a5 100644 --- a/homeassistant/components/sensor/tellstick.py +++ b/homeassistant/components/tellstick/sensor.py @@ -1,9 +1,4 @@ -""" -Support for Tellstick sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.tellstick/ -""" +"""Support for Tellstick sensors.""" import logging from collections import namedtuple diff --git a/homeassistant/components/switch/tellstick.py b/homeassistant/components/tellstick/switch.py similarity index 91% rename from homeassistant/components/switch/tellstick.py rename to homeassistant/components/tellstick/switch.py index 51a04e9f5b3..56d563e494c 100644 --- a/homeassistant/components/switch/tellstick.py +++ b/homeassistant/components/tellstick/switch.py @@ -1,9 +1,4 @@ -""" -Support for Tellstick switches. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.tellstick/ -""" +"""Support for Tellstick switches.""" from homeassistant.components.tellstick import ( DEFAULT_SIGNAL_REPETITIONS, ATTR_DISCOVER_DEVICES, ATTR_DISCOVER_CONFIG, DATA_TELLSTICK, TellstickDevice) diff --git a/homeassistant/components/tesla.py b/homeassistant/components/tesla/__init__.py similarity index 96% rename from homeassistant/components/tesla.py rename to homeassistant/components/tesla/__init__.py index 76b5c00d9d4..fc433ae18b1 100644 --- a/homeassistant/components/tesla.py +++ b/homeassistant/components/tesla/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Tesla cars. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/tesla/ -""" +"""Support for Tesla cars.""" from collections import defaultdict import logging diff --git a/homeassistant/components/binary_sensor/tesla.py b/homeassistant/components/tesla/binary_sensor.py similarity index 90% rename from homeassistant/components/binary_sensor/tesla.py rename to homeassistant/components/tesla/binary_sensor.py index f7613d74dfb..2c037140f0a 100644 --- a/homeassistant/components/binary_sensor/tesla.py +++ b/homeassistant/components/tesla/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for Tesla binary sensor. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.tesla/ -""" +"""Support for Tesla binary sensor.""" import logging from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/climate/tesla.py b/homeassistant/components/tesla/climate.py similarity index 95% rename from homeassistant/components/climate/tesla.py rename to homeassistant/components/tesla/climate.py index ef5f2227c11..302c0006bcf 100644 --- a/homeassistant/components/climate/tesla.py +++ b/homeassistant/components/tesla/climate.py @@ -1,9 +1,4 @@ -""" -Support for Tesla HVAC system. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/climate.tesla/ -""" +"""Support for Tesla HVAC system.""" import logging from homeassistant.components.climate import ( diff --git a/homeassistant/components/device_tracker/tesla.py b/homeassistant/components/tesla/device_tracker.py similarity index 90% rename from homeassistant/components/device_tracker/tesla.py rename to homeassistant/components/tesla/device_tracker.py index c08ddb4047b..0aeab5b1c7d 100644 --- a/homeassistant/components/device_tracker/tesla.py +++ b/homeassistant/components/tesla/device_tracker.py @@ -1,9 +1,4 @@ -""" -Support for the Tesla platform. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/device_tracker.tesla/ -""" +"""Support for tracking Tesla cars.""" import logging from homeassistant.components.tesla import DOMAIN as TESLA_DOMAIN diff --git a/homeassistant/components/lock/tesla.py b/homeassistant/components/tesla/lock.py similarity index 91% rename from homeassistant/components/lock/tesla.py rename to homeassistant/components/tesla/lock.py index 2ffb996aec3..34d660ac83c 100644 --- a/homeassistant/components/lock/tesla.py +++ b/homeassistant/components/tesla/lock.py @@ -1,9 +1,4 @@ -""" -Support for Tesla door locks. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/lock.tesla/ -""" +"""Support for Tesla door locks.""" import logging from homeassistant.components.lock import ENTITY_ID_FORMAT, LockDevice diff --git a/homeassistant/components/sensor/tesla.py b/homeassistant/components/tesla/sensor.py similarity index 95% rename from homeassistant/components/sensor/tesla.py rename to homeassistant/components/tesla/sensor.py index 51b7ea2325d..1d4505ed9a4 100644 --- a/homeassistant/components/sensor/tesla.py +++ b/homeassistant/components/tesla/sensor.py @@ -1,9 +1,4 @@ -""" -Sensors for the Tesla sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.tesla/ -""" +"""Support for the Tesla sensors.""" from datetime import timedelta import logging diff --git a/homeassistant/components/switch/tesla.py b/homeassistant/components/tesla/switch.py similarity index 94% rename from homeassistant/components/switch/tesla.py rename to homeassistant/components/tesla/switch.py index 30972b1014b..a1787e9993e 100644 --- a/homeassistant/components/switch/tesla.py +++ b/homeassistant/components/tesla/switch.py @@ -1,9 +1,4 @@ -""" -Support for Tesla charger switch. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.tesla/ -""" +"""Support for Tesla charger switches.""" import logging from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchDevice diff --git a/homeassistant/components/thethingsnetwork.py b/homeassistant/components/thethingsnetwork/__init__.py similarity index 84% rename from homeassistant/components/thethingsnetwork.py rename to homeassistant/components/thethingsnetwork/__init__.py index 61f9843be45..952755d289e 100644 --- a/homeassistant/components/thethingsnetwork.py +++ b/homeassistant/components/thethingsnetwork/__init__.py @@ -1,9 +1,4 @@ -""" -Support for The Things network. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/thethingsnetwork/ -""" +"""Support for The Things network.""" import logging import voluptuous as vol diff --git a/homeassistant/components/sensor/thethingsnetwork.py b/homeassistant/components/thethingsnetwork/sensor.py similarity index 93% rename from homeassistant/components/sensor/thethingsnetwork.py rename to homeassistant/components/thethingsnetwork/sensor.py index 0f1220f9b07..05da90bf7ac 100644 --- a/homeassistant/components/sensor/thethingsnetwork.py +++ b/homeassistant/components/thethingsnetwork/sensor.py @@ -1,9 +1,4 @@ -""" -Support for The Things Network's Data storage integration. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/sensor.thethingsnetwork_data/ -""" +"""Support for The Things Network's Data storage integration.""" import asyncio import logging @@ -38,8 +33,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up The Things Network Data storage sensors.""" ttn = hass.data.get(DATA_TTN) device_id = config.get(CONF_DEVICE_ID) @@ -153,7 +148,7 @@ class TtnDataStorage: return False data = await req.json() - self.data = data[0] + self.data = data[-1] for value in self._values.items(): if value[0] not in self.data.keys(): diff --git a/homeassistant/components/thingspeak.py b/homeassistant/components/thingspeak/__init__.py similarity index 86% rename from homeassistant/components/thingspeak.py rename to homeassistant/components/thingspeak/__init__.py index 9a876a87683..0fa15e7efb4 100644 --- a/homeassistant/components/thingspeak.py +++ b/homeassistant/components/thingspeak/__init__.py @@ -1,9 +1,4 @@ -""" -A component to submit data to thingspeak. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/thingspeak/ -""" +"""Support for submitting data to Thingspeak.""" import logging from requests.exceptions import RequestException @@ -26,8 +21,8 @@ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_ID): int, - vol.Required(CONF_WHITELIST): cv.string - }), + vol.Required(CONF_WHITELIST): cv.string, + }), }, extra=vol.ALLOW_EXTRA) @@ -47,7 +42,7 @@ def setup(hass, config): except RequestException: _LOGGER.error("Error while accessing the ThingSpeak channel. " "Please check that the channel exists and your " - "API key is correct.") + "API key is correct") return False def thingspeak_listener(entity_id, old_state, new_state): diff --git a/homeassistant/components/thinkingcleaner/__init__.py b/homeassistant/components/thinkingcleaner/__init__.py new file mode 100644 index 00000000000..a72cda45fd5 --- /dev/null +++ b/homeassistant/components/thinkingcleaner/__init__.py @@ -0,0 +1 @@ +"""Support for Thinkingcleaner devices.""" diff --git a/homeassistant/components/sensor/thinkingcleaner.py b/homeassistant/components/thinkingcleaner/sensor.py similarity index 95% rename from homeassistant/components/sensor/thinkingcleaner.py rename to homeassistant/components/thinkingcleaner/sensor.py index 17e2f717f5a..f8462435a45 100644 --- a/homeassistant/components/sensor/thinkingcleaner.py +++ b/homeassistant/components/thinkingcleaner/sensor.py @@ -1,9 +1,4 @@ -""" -Support for ThinkingCleaner. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.thinkingcleaner/ -""" +"""Support for ThinkingCleaner sensors.""" import logging from datetime import timedelta diff --git a/homeassistant/components/switch/thinkingcleaner.py b/homeassistant/components/thinkingcleaner/switch.py similarity index 92% rename from homeassistant/components/switch/thinkingcleaner.py rename to homeassistant/components/thinkingcleaner/switch.py index 89586465b43..38a96eb0298 100644 --- a/homeassistant/components/switch/thinkingcleaner.py +++ b/homeassistant/components/thinkingcleaner/switch.py @@ -1,9 +1,4 @@ -""" -Support for ThinkingCleaner. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.thinkingcleaner/ -""" +"""Support for ThinkingCleaner switches.""" import time import logging from datetime import timedelta @@ -45,8 +40,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): dev = [] for device in devices: for type_name in SWITCH_TYPES: - dev.append(ThinkingCleanerSwitch(device, type_name, - update_devices)) + dev.append(ThinkingCleanerSwitch( + device, type_name, update_devices)) add_entities(dev) diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py index c2d1daa584c..ba9ae43f13b 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Tibber. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/tibber/ -""" +"""Support for Tibber.""" import asyncio import logging diff --git a/homeassistant/components/notify/tibber.py b/homeassistant/components/tibber/notify.py similarity index 85% rename from homeassistant/components/notify/tibber.py rename to homeassistant/components/tibber/notify.py index ddbcb3f6c65..6ae22c34209 100644 --- a/homeassistant/components/notify/tibber.py +++ b/homeassistant/components/tibber/notify.py @@ -1,9 +1,4 @@ -""" -Tibber platform for notify component. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/notify.tibber/ -""" +"""Support for Tibber notifications.""" import asyncio import logging @@ -11,7 +6,6 @@ from homeassistant.components.notify import ( ATTR_TITLE, ATTR_TITLE_DEFAULT, BaseNotificationService) from homeassistant.components.tibber import DOMAIN as TIBBER_DOMAIN - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/tibber.py b/homeassistant/components/tibber/sensor.py similarity index 96% rename from homeassistant/components/sensor/tibber.py rename to homeassistant/components/tibber/sensor.py index 1c3ef601633..f3e0c39a1e6 100644 --- a/homeassistant/components/sensor/tibber.py +++ b/homeassistant/components/tibber/sensor.py @@ -1,9 +1,4 @@ -""" -Support for Tibber. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.tibber/ -""" +"""Support for Tibber sensors.""" import asyncio import logging @@ -25,8 +20,8 @@ SCAN_INTERVAL = timedelta(minutes=1) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the Tibber sensor.""" if discovery_info is None: return diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index b898c577bb2..04d9acc06af 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -1,21 +1,15 @@ -""" -Timer component. - -For more details about this component, please refer to the documentation -at https://home-assistant.io/components/timer/ -""" -import logging +"""Support for Timers.""" from datetime import timedelta +import logging import voluptuous as vol -import homeassistant.util.dt as dt_util +from homeassistant.const import ATTR_ENTITY_ID, CONF_ICON, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.const import (ATTR_ENTITY_ID, CONF_ICON, CONF_NAME) from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.restore_state import RestoreEntity - +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -43,11 +37,11 @@ SERVICE_CANCEL = 'cancel' SERVICE_FINISH = 'finish' SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, }) SERVICE_SCHEMA_DURATION = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, vol.Optional(ATTR_DURATION, default=timedelta(DEFAULT_DURATION)): cv.time_period, }) diff --git a/homeassistant/components/toon.py b/homeassistant/components/toon/__init__.py similarity index 96% rename from homeassistant/components/toon.py rename to homeassistant/components/toon/__init__.py index 01f170f0b31..96d8b4e6d15 100644 --- a/homeassistant/components/toon.py +++ b/homeassistant/components/toon/__init__.py @@ -1,9 +1,4 @@ -""" -Toon van Eneco Support. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/toon/ -""" +"""Support for Toon van Eneco devices.""" from datetime import datetime, timedelta import logging diff --git a/homeassistant/components/climate/toon.py b/homeassistant/components/toon/climate.py similarity index 91% rename from homeassistant/components/climate/toon.py rename to homeassistant/components/toon/climate.py index 022a509ce06..3397e3dacc2 100644 --- a/homeassistant/components/climate/toon.py +++ b/homeassistant/components/toon/climate.py @@ -1,12 +1,4 @@ -""" -Toon van Eneco Thermostat Support. - -This provides a component for the rebranded Quby thermostat as provided by -Eneco. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/climate.toon/ -""" +"""Support for Toon van Eneco Thermostats.""" from homeassistant.components.climate import ( ATTR_TEMPERATURE, STATE_COOL, STATE_ECO, STATE_HEAT, STATE_AUTO, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, ClimateDevice) diff --git a/homeassistant/components/sensor/toon.py b/homeassistant/components/toon/sensor.py similarity index 97% rename from homeassistant/components/sensor/toon.py rename to homeassistant/components/toon/sensor.py index fb057603a1a..ebd25e02cde 100644 --- a/homeassistant/components/sensor/toon.py +++ b/homeassistant/components/toon/sensor.py @@ -1,9 +1,4 @@ -""" -Component for the rebranded Quby thermostat as provided by Eneco. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.toon/ -""" +"""Support for rebranded Quby thermostat as provided by Eneco.""" import logging import datetime diff --git a/homeassistant/components/switch/toon.py b/homeassistant/components/toon/switch.py similarity index 91% rename from homeassistant/components/switch/toon.py rename to homeassistant/components/toon/switch.py index 087ca673e85..08ccec588b4 100644 --- a/homeassistant/components/switch/toon.py +++ b/homeassistant/components/toon/switch.py @@ -1,9 +1,4 @@ -""" -Support for Eneco Slimmer stekkers (Smart Plugs). - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.toon/ -""" +"""Support for Eneco Slimmer stekkers (Smart Plugs).""" import logging from homeassistant.components.switch import SwitchDevice diff --git a/homeassistant/components/tplink_lte.py b/homeassistant/components/tplink_lte/__init__.py similarity index 84% rename from homeassistant/components/tplink_lte.py rename to homeassistant/components/tplink_lte/__init__.py index 17288a881aa..d0f6e600a0d 100644 --- a/homeassistant/components/tplink_lte.py +++ b/homeassistant/components/tplink_lte/__init__.py @@ -1,9 +1,4 @@ -""" -Support for TP-Link LTE modems. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/tplink_lte/ -""" +"""Support for TP-Link LTE modems.""" import asyncio import logging @@ -13,7 +8,8 @@ import voluptuous as vol from homeassistant.components.notify import ATTR_TARGET from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP) + CONF_HOST, CONF_NAME, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP, + CONF_RECIPIENT) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.aiohttp_client import async_create_clientsession @@ -25,19 +21,30 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = 'tplink_lte' DATA_KEY = 'tplink_lte' -CONF_NOTIFY = "notify" +CONF_NOTIFY = 'notify' -_NOTIFY_SCHEMA = vol.All(vol.Schema({ - vol.Optional(CONF_NAME): cv.string, - vol.Required(ATTR_TARGET): vol.All(cv.ensure_list, [cv.string]), -})) +# Deprecated in 0.88.0, invalidated in 0.91.0, remove in 0.92.0 +ATTR_TARGET_INVALIDATION_VERSION = '0.91.0' + +_NOTIFY_SCHEMA = vol.All( + vol.Schema({ + vol.Optional(CONF_NAME): cv.string, + vol.Optional(ATTR_TARGET): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_RECIPIENT): vol.All(cv.ensure_list, [cv.string]) + }), + cv.deprecated( + ATTR_TARGET, + replacement_key=CONF_RECIPIENT, + invalidation_version=ATTR_TARGET_INVALIDATION_VERSION + ), + cv.has_at_least_one_key(CONF_RECIPIENT), +) CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NOTIFY): - vol.All(cv.ensure_list, [_NOTIFY_SCHEMA]), + vol.Optional(CONF_NOTIFY): vol.All(cv.ensure_list, [_NOTIFY_SCHEMA]), })]) }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/notify/tplink_lte.py b/homeassistant/components/tplink_lte/notify.py similarity index 84% rename from homeassistant/components/notify/tplink_lte.py rename to homeassistant/components/tplink_lte/notify.py index 9bb80e2591c..519641ed34b 100644 --- a/homeassistant/components/notify/tplink_lte.py +++ b/homeassistant/components/tplink_lte/notify.py @@ -1,15 +1,11 @@ -"""TP-Link LTE platform for notify component. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/notify.tplink_lte/ -""" - +"""Support for TP-Link LTE notifications.""" import logging import attr from homeassistant.components.notify import ( ATTR_TARGET, BaseNotificationService) +from homeassistant.const import CONF_RECIPIENT from ..tplink_lte import DATA_KEY @@ -40,7 +36,7 @@ class TplinkNotifyService(BaseNotificationService): _LOGGER.error("No modem available") return - phone = self.config[ATTR_TARGET] + phone = self.config[CONF_RECIPIENT] targets = kwargs.get(ATTR_TARGET, phone) if targets and message: for target in targets: diff --git a/homeassistant/components/tradfri/.translations/da.json b/homeassistant/components/tradfri/.translations/da.json new file mode 100644 index 00000000000..f0e5acf9d9c --- /dev/null +++ b/homeassistant/components/tradfri/.translations/da.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Bridge er allerede konfigureret" + }, + "error": { + "cannot_connect": "Kan ikke oprette forbindelse til gateway.", + "invalid_key": "Fejl ved registrerering med den leverede n\u00f8gle. Hvis dette sker konsekvent skal du pr\u00f8ve at genstarte gatewayen.", + "timeout": "Timeout ved validering af kode" + }, + "step": { + "auth": { + "data": { + "host": "V\u00e6rt", + "security_code": "Sikkerhedskode" + }, + "description": "Du kan finde sikkerhedskoden p\u00e5 bagsiden af din gateway.", + "title": "Indtast sikkerhedskode" + } + }, + "title": "IKEA TR\u00c5DFRI" + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index ba13b8d511a..b14bc811754 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -1,9 +1,4 @@ -""" -Support for IKEA Tradfri. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/ikea_tradfri/ -""" +"""Support for IKEA Tradfri.""" import logging import voluptuous as vol @@ -20,6 +15,9 @@ from . import config_flow # noqa pylint_disable=unused-import REQUIREMENTS = ['pytradfri[async]==6.0.1'] +_LOGGER = logging.getLogger(__name__) + + DOMAIN = 'tradfri' CONFIG_FILE = '.tradfri_psk.conf' KEY_GATEWAY = 'tradfri_gateway' @@ -35,8 +33,6 @@ CONFIG_SCHEMA = vol.Schema({ }) }, extra=vol.ALLOW_EXTRA) -_LOGGER = logging.getLogger(__name__) - async def async_setup(hass, config): """Set up the Tradfri component.""" diff --git a/homeassistant/components/light/tradfri.py b/homeassistant/components/tradfri/light.py similarity index 98% rename from homeassistant/components/light/tradfri.py rename to homeassistant/components/tradfri/light.py index 50e92f15e3c..e5e27ecbed1 100644 --- a/homeassistant/components/light/tradfri.py +++ b/homeassistant/components/tradfri/light.py @@ -1,9 +1,4 @@ -""" -Support for the IKEA Tradfri platform. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.tradfri/ -""" +"""Support for IKEA Tradfri lights.""" import logging from homeassistant.core import callback diff --git a/homeassistant/components/sensor/tradfri.py b/homeassistant/components/tradfri/sensor.py similarity index 94% rename from homeassistant/components/sensor/tradfri.py rename to homeassistant/components/tradfri/sensor.py index 45167874de2..97c7dc9627d 100644 --- a/homeassistant/components/sensor/tradfri.py +++ b/homeassistant/components/tradfri/sensor.py @@ -1,9 +1,4 @@ -""" -Support for the IKEA Tradfri platform. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.tradfri/ -""" +"""Support for IKEA Tradfri sensors.""" import logging from datetime import timedelta diff --git a/homeassistant/components/switch/tradfri.py b/homeassistant/components/tradfri/switch.py similarity index 96% rename from homeassistant/components/switch/tradfri.py rename to homeassistant/components/tradfri/switch.py index b247858b062..23e6cb20c8f 100644 --- a/homeassistant/components/switch/tradfri.py +++ b/homeassistant/components/tradfri/switch.py @@ -1,9 +1,4 @@ -""" -Support for the IKEA Tradfri platform. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.tradfri/ -""" +"""Support for IKEA Tradfri switches.""" import logging from homeassistant.core import callback diff --git a/homeassistant/components/transmission.py b/homeassistant/components/transmission/__init__.py similarity index 92% rename from homeassistant/components/transmission.py rename to homeassistant/components/transmission/__init__.py index b14881fccca..25e21dc3d8a 100644 --- a/homeassistant/components/transmission.py +++ b/homeassistant/components/transmission/__init__.py @@ -1,31 +1,22 @@ -""" -Component for monitoring the Transmission BitTorrent client API. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/transmission/ -""" +"""Support for the Transmission BitTorrent client API.""" from datetime import timedelta - import logging + import voluptuous as vol from homeassistant.const import ( - CONF_HOST, - CONF_MONITORED_CONDITIONS, - CONF_NAME, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, - CONF_SCAN_INTERVAL -) -from homeassistant.helpers import discovery, config_validation as cv + CONF_HOST, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_PASSWORD, CONF_PORT, + CONF_SCAN_INTERVAL, CONF_USERNAME) +from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import track_time_interval - REQUIREMENTS = ['transmissionrpc==0.11'] + _LOGGER = logging.getLogger(__name__) DOMAIN = 'transmission' +DATA_UPDATED = 'transmission_data_updated' DATA_TRANSMISSION = 'data_transmission' DEFAULT_NAME = 'Transmission' @@ -83,6 +74,8 @@ def setup(hass, config): tm_data = hass.data[DATA_TRANSMISSION] = TransmissionData( hass, config, api) + + tm_data.update() tm_data.init_torrent_list() def refresh(event_time): @@ -94,10 +87,12 @@ def setup(hass, config): sensorconfig = { 'sensors': config[DOMAIN][CONF_MONITORED_CONDITIONS], 'client_name': config[DOMAIN][CONF_NAME]} + discovery.load_platform(hass, 'sensor', DOMAIN, sensorconfig, config) if config[DOMAIN][TURTLE_MODE]: discovery.load_platform(hass, 'switch', DOMAIN, sensorconfig, config) + return True @@ -127,6 +122,8 @@ class TransmissionData: self.check_completed_torrent() self.check_started_torrent() + dispatcher_send(self.hass, DATA_UPDATED) + _LOGGER.debug("Torrent Data updated") self.available = True except TransmissionError: @@ -189,4 +186,7 @@ class TransmissionData: def get_alt_speed_enabled(self): """Get the alternative speed flag.""" + if self.session is None: + return None + return self.session.alt_speed_enabled diff --git a/homeassistant/components/sensor/transmission.py b/homeassistant/components/transmission/sensor.py similarity index 77% rename from homeassistant/components/sensor/transmission.py rename to homeassistant/components/transmission/sensor.py index 84c7d54306e..061ed2c0c64 100644 --- a/homeassistant/components/sensor/transmission.py +++ b/homeassistant/components/transmission/sensor.py @@ -1,18 +1,14 @@ -""" -Support for monitoring the Transmission BitTorrent client API. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.transmission/ -""" +"""Support for monitoring the Transmission BitTorrent client API.""" from datetime import timedelta import logging from homeassistant.components.transmission import ( - DATA_TRANSMISSION, SENSOR_TYPES) + DATA_TRANSMISSION, SENSOR_TYPES, DATA_UPDATED) from homeassistant.const import STATE_IDLE +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle DEPENDENCIES = ['transmission'] @@ -23,7 +19,8 @@ DEFAULT_NAME = 'Transmission' SCAN_INTERVAL = timedelta(seconds=120) -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 Transmission sensors.""" if discovery_info is None: return @@ -35,24 +32,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None): dev = [] for sensor_type in monitored_variables: dev.append(TransmissionSensor( - sensor_type, - transmission_api, - name, - SENSOR_TYPES[sensor_type][0], - SENSOR_TYPES[sensor_type][1])) + sensor_type, transmission_api, name, + SENSOR_TYPES[sensor_type][0], SENSOR_TYPES[sensor_type][1])) - add_entities(dev, True) + async_add_entities(dev, True) class TransmissionSensor(Entity): """Representation of a Transmission sensor.""" def __init__( - self, - sensor_type, - transmission_api, - client_name, - sensor_name, + self, sensor_type, transmission_api, client_name, sensor_name, unit_of_measurement): """Initialize the sensor.""" self._name = sensor_name @@ -73,6 +63,11 @@ class TransmissionSensor(Entity): """Return the state of the sensor.""" return self._state + @property + def should_poll(self): + """Return the polling requirement for this sensor.""" + return False + @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" @@ -83,10 +78,17 @@ class TransmissionSensor(Entity): """Could the device be accessed during the last update call.""" return self._transmission_api.available - @Throttle(SCAN_INTERVAL) + async def async_added_to_hass(self): + """Handle entity which will be added.""" + async_dispatcher_connect( + self.hass, DATA_UPDATED, self._schedule_immediate_update) + + @callback + def _schedule_immediate_update(self): + self.async_schedule_update_ha_state(True) + def update(self): """Get the latest data from Transmission and updates the state.""" - self._transmission_api.update() self._data = self._transmission_api.data if self.type == 'completed_torrents': diff --git a/homeassistant/components/switch/transmission.py b/homeassistant/components/transmission/switch.py similarity index 70% rename from homeassistant/components/switch/transmission.py rename to homeassistant/components/transmission/switch.py index 8e6c0a8cb44..373397eddd6 100644 --- a/homeassistant/components/switch/transmission.py +++ b/homeassistant/components/transmission/switch.py @@ -1,19 +1,13 @@ -""" -Support for setting the Transmission BitTorrent client Turtle Mode. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.transmission/ -""" -from datetime import timedelta - +"""Support for setting the Transmission BitTorrent client Turtle Mode.""" import logging from homeassistant.components.transmission import ( - DATA_TRANSMISSION) + DATA_TRANSMISSION, DATA_UPDATED) from homeassistant.const import ( STATE_OFF, STATE_ON) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import ToggleEntity -from homeassistant.util import Throttle DEPENDENCIES = ['transmission'] @@ -21,10 +15,9 @@ _LOGGING = logging.getLogger(__name__) DEFAULT_NAME = 'Transmission Turtle Mode' -SCAN_INTERVAL = timedelta(seconds=120) - -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 Transmission switch.""" if discovery_info is None: return @@ -33,7 +26,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): transmission_api = hass.data[component_name] name = discovery_info['client_name'] - add_entities([TransmissionSwitch(transmission_api, name)], True) + async_add_entities([TransmissionSwitch(transmission_api, name)], True) class TransmissionSwitch(ToggleEntity): @@ -58,7 +51,7 @@ class TransmissionSwitch(ToggleEntity): @property def should_poll(self): """Poll for status regularly.""" - return True + return False @property def is_on(self): @@ -75,8 +68,21 @@ class TransmissionSwitch(ToggleEntity): _LOGGING.debug("Turning Turtle Mode of Transmission off") self.transmission_client.set_alt_speed_enabled(False) - @Throttle(SCAN_INTERVAL) + async def async_added_to_hass(self): + """Handle entity which will be added.""" + async_dispatcher_connect( + self.hass, DATA_UPDATED, self._schedule_immediate_update + ) + + @callback + def _schedule_immediate_update(self): + self.async_schedule_update_ha_state(True) + def update(self): """Get the latest data from Transmission and updates the state.""" active = self.transmission_client.get_alt_speed_enabled() + + if active is None: + return + self._state = STATE_ON if active else STATE_OFF diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 063ba428d4a..475ed2c6892 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -18,10 +18,10 @@ from aiohttp import web import voluptuous as vol from homeassistant.components.http import HomeAssistantView -from homeassistant.components.media_player import ( +from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, MEDIA_TYPE_MUSIC, SERVICE_PLAY_MEDIA) -from homeassistant.components.media_player import DOMAIN as DOMAIN_MP +from homeassistant.components.media_player.const import DOMAIN as DOMAIN_MP from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError @@ -68,6 +68,7 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ vol.All(vol.Coerce(int), vol.Range(min=60, max=57600)), vol.Optional(CONF_BASE_URL): cv.string, }) +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE.extend(PLATFORM_SCHEMA.schema) SCHEMA_SERVICE_SAY = vol.Schema({ vol.Required(ATTR_MESSAGE): cv.string, diff --git a/homeassistant/components/tuya.py b/homeassistant/components/tuya/__init__.py similarity index 96% rename from homeassistant/components/tuya.py rename to homeassistant/components/tuya/__init__.py index 22a82dec8e2..117424fd55e 100644 --- a/homeassistant/components/tuya.py +++ b/homeassistant/components/tuya/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Tuya Smart devices. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/tuya/ -""" +"""Support for Tuya Smart devices.""" from datetime import timedelta import logging import voluptuous as vol diff --git a/homeassistant/components/climate/tuya.py b/homeassistant/components/tuya/climate.py similarity index 96% rename from homeassistant/components/climate/tuya.py rename to homeassistant/components/tuya/climate.py index 4548867a45e..97ff18ba911 100644 --- a/homeassistant/components/climate/tuya.py +++ b/homeassistant/components/tuya/climate.py @@ -1,10 +1,4 @@ -""" -Support for the Tuya climate devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/climate.tuya/ -""" - +"""Support for the Tuya climate devices.""" from homeassistant.components.climate import ( ATTR_TEMPERATURE, ENTITY_ID_FORMAT, STATE_AUTO, STATE_COOL, STATE_ECO, STATE_FAN_ONLY, STATE_HEAT, SUPPORT_FAN_MODE, SUPPORT_ON_OFF, diff --git a/homeassistant/components/cover/tuya.py b/homeassistant/components/tuya/cover.py similarity index 91% rename from homeassistant/components/cover/tuya.py rename to homeassistant/components/tuya/cover.py index a3a3db972e9..ac2309cbf9e 100644 --- a/homeassistant/components/cover/tuya.py +++ b/homeassistant/components/tuya/cover.py @@ -1,9 +1,4 @@ -""" -Support for Tuya cover. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/cover.tuya/ -""" +"""Support for Tuya covers.""" from homeassistant.components.cover import ( CoverDevice, ENTITY_ID_FORMAT, SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_STOP) from homeassistant.components.tuya import DATA_TUYA, TuyaDevice diff --git a/homeassistant/components/fan/tuya.py b/homeassistant/components/tuya/fan.py similarity index 94% rename from homeassistant/components/fan/tuya.py rename to homeassistant/components/tuya/fan.py index 9cb7cdc3f2c..b6e2cb6950c 100644 --- a/homeassistant/components/fan/tuya.py +++ b/homeassistant/components/tuya/fan.py @@ -1,10 +1,4 @@ -""" -Support for Tuya fans. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/fan.tuya/ -""" - +"""Support for Tuya fans.""" from homeassistant.components.fan import ( ENTITY_ID_FORMAT, FanEntity, SUPPORT_OSCILLATE, SUPPORT_SET_SPEED) from homeassistant.components.tuya import DATA_TUYA, TuyaDevice diff --git a/homeassistant/components/light/tuya.py b/homeassistant/components/tuya/light.py similarity index 95% rename from homeassistant/components/light/tuya.py rename to homeassistant/components/tuya/light.py index 0a1468a6a51..1cf2f811872 100644 --- a/homeassistant/components/light/tuya.py +++ b/homeassistant/components/tuya/light.py @@ -1,9 +1,4 @@ -""" -Support for the Tuya light. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.tuya/ -""" +"""Support for the Tuya lights.""" from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ENTITY_ID_FORMAT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR, Light) diff --git a/homeassistant/components/scene/tuya.py b/homeassistant/components/tuya/scene.py similarity index 85% rename from homeassistant/components/scene/tuya.py rename to homeassistant/components/tuya/scene.py index 2e03e5dba9a..33d207d8545 100644 --- a/homeassistant/components/scene/tuya.py +++ b/homeassistant/components/tuya/scene.py @@ -1,9 +1,4 @@ -""" -Support for the Tuya scene. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/scene.tuya/ -""" +"""Support for the Tuya scenes.""" from homeassistant.components.scene import Scene, DOMAIN from homeassistant.components.tuya import DATA_TUYA, TuyaDevice diff --git a/homeassistant/components/switch/tuya.py b/homeassistant/components/tuya/switch.py similarity index 87% rename from homeassistant/components/switch/tuya.py rename to homeassistant/components/tuya/switch.py index 9fc1f92016e..1e8fab2cc1b 100644 --- a/homeassistant/components/switch/tuya.py +++ b/homeassistant/components/tuya/switch.py @@ -1,9 +1,4 @@ -""" -Support for Tuya switch. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.tuya/ -""" +"""Support for Tuya switches.""" from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchDevice from homeassistant.components.tuya import DATA_TUYA, TuyaDevice diff --git a/homeassistant/components/twilio/.translations/da.json b/homeassistant/components/twilio/.translations/da.json new file mode 100644 index 00000000000..3c1ab7c01b5 --- /dev/null +++ b/homeassistant/components/twilio/.translations/da.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Dit Home Assistant system skal v\u00e6re tilg\u00e6ngeligt fra internettet for at modtage Twilio meddelelser.", + "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning." + }, + "create_entry": { + "default": "For at sende begivenheder til Home Assistant skal du konfigurere [Webhooks med Twilio]({twilio_url}).\n\n Udfyld f\u00f8lgende oplysninger: \n\n - URL: `{webhook_url}`\n - Metode: POST\n - Indholdstype: application/x-www-form-urlencoded\n\n Se [dokumentationen]({docs_url}) om hvordan du konfigurerer automatiseringer til at h\u00e5ndtere indg\u00e5ende data." + }, + "step": { + "user": { + "description": "Er du sikker p\u00e5 at du vil konfigurere Twilio?", + "title": "Konfigurer Twilio Webhook" + } + }, + "title": "Twilio" + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/fr.json b/homeassistant/components/twilio/.translations/fr.json new file mode 100644 index 00000000000..09ca0f63cfd --- /dev/null +++ b/homeassistant/components/twilio/.translations/fr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Votre instance de Home Assistant doit \u00eatre accessible depuis Internet pour recevoir les messages Twilio.", + "one_instance_allowed": "Une seule instance est n\u00e9cessaire." + }, + "create_entry": { + "default": "Pour envoyer des \u00e9v\u00e9nements \u00e0 Home Assistant, vous devez configurer [Webhooks avec Twilio] ( {twilio_url} ). \n\n Remplissez les informations suivantes: \n\n - URL: ` {webhook_url} ` \n - M\u00e9thode: POST \n - Type de contenu: application / x-www-form-urlencoded \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 Twilio?", + "title": "Configurer le Webhook Twilio" + } + }, + "title": "Twilio" + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/ko.json b/homeassistant/components/twilio/.translations/ko.json index 8790c708008..618c91e6a65 100644 --- a/homeassistant/components/twilio/.translations/ko.json +++ b/homeassistant/components/twilio/.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\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 \nHome 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\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 \nHome 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/twilio/__init__.py b/homeassistant/components/twilio/__init__.py index 9fcba4da817..ce8c272165f 100644 --- a/homeassistant/components/twilio/__init__.py +++ b/homeassistant/components/twilio/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Twilio. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/twilio/ -""" +"""Support for Twilio.""" import voluptuous as vol import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/unifi/.translations/da.json b/homeassistant/components/unifi/.translations/da.json new file mode 100644 index 00000000000..4155658d7de --- /dev/null +++ b/homeassistant/components/unifi/.translations/da.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Controller site er allerede konfigureret", + "user_privilege": "Bruger skal v\u00e6re administrator" + }, + "error": { + "faulty_credentials": "Ugyldige legitimationsoplysninger", + "service_unavailable": "Service utilg\u00e6ngelig" + }, + "step": { + "user": { + "data": { + "host": "V\u00e6rt", + "password": "Adgangskode", + "port": "Port", + "site": "Site ID", + "username": "Brugernavn", + "verify_ssl": "Controller bruger korrekt certifikat" + }, + "title": "Konfigurer UniFi Controller" + } + }, + "title": "UniFi Controller" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/fr.json b/homeassistant/components/unifi/.translations/fr.json index 68e90811a3e..767962e37eb 100644 --- a/homeassistant/components/unifi/.translations/fr.json +++ b/homeassistant/components/unifi/.translations/fr.json @@ -14,9 +14,12 @@ "password": "Mot de passe", "port": "Port", "site": "ID du site", - "username": "Nom d'utilisateur" - } + "username": "Nom d'utilisateur", + "verify_ssl": "Contr\u00f4leur utilisant un certificat appropri\u00e9" + }, + "title": "Configurer le contr\u00f4leur UniFi" } - } + }, + "title": "Contr\u00f4leur UniFi" } } \ No newline at end of file diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index 8476aade7d7..7e236789a5c 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -1,10 +1,4 @@ -""" -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/ -""" - +"""Support for devices connected to UniFi POE.""" import voluptuous as vol from homeassistant import config_entries diff --git a/homeassistant/components/unifi/const.py b/homeassistant/components/unifi/const.py index 7250feec799..8fe90823c49 100644 --- a/homeassistant/components/unifi/const.py +++ b/homeassistant/components/unifi/const.py @@ -1,5 +1,4 @@ """Constants for the UniFi component.""" - import logging LOGGER = logging.getLogger('homeassistant.components.unifi') diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 11529cbe171..2b9aa89fef2 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -1,11 +1,10 @@ """UniFi Controller abstraction.""" - import asyncio import async_timeout from aiohttp import CookieJar -from homeassistant import config_entries +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.const import CONF_HOST from homeassistant.helpers import aiohttp_client @@ -23,7 +22,6 @@ class UniFiController: self.available = True self.api = None self.progress = None - self._cancel_retry_setup = None @property def host(self): @@ -48,20 +46,7 @@ class UniFiController: 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 + raise ConfigEntryNotReady except Exception: # pylint: disable=broad-except LOGGER.error( @@ -81,12 +66,6 @@ class UniFiController: 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 diff --git a/homeassistant/components/switch/unifi.py b/homeassistant/components/unifi/switch.py similarity index 96% rename from homeassistant/components/switch/unifi.py rename to homeassistant/components/unifi/switch.py index c1f8a96946f..425b9878f6d 100644 --- a/homeassistant/components/switch/unifi.py +++ b/homeassistant/components/unifi/switch.py @@ -1,10 +1,4 @@ -""" -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/ -""" - +"""Support for devices connected to UniFi POE.""" import asyncio import logging @@ -39,7 +33,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """ controller_id = CONTROLLER_ID.format( host=config_entry.data[CONF_CONTROLLER][CONF_HOST], - site=config_entry.data[CONF_CONTROLLER][CONF_SITE_ID] + site=config_entry.data[CONF_CONTROLLER][CONF_SITE_ID], ) controller = hass.data[unifi.DOMAIN][controller_id] switches = {} diff --git a/homeassistant/components/upcloud.py b/homeassistant/components/upcloud/__init__.py similarity index 94% rename from homeassistant/components/upcloud.py rename to homeassistant/components/upcloud/__init__.py index ca0f554bd39..7981cf948bb 100644 --- a/homeassistant/components/upcloud.py +++ b/homeassistant/components/upcloud/__init__.py @@ -1,9 +1,4 @@ -""" -Support for UpCloud. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/upcloud/ -""" +"""Support for UpCloud.""" import logging from datetime import timedelta @@ -43,12 +38,12 @@ UPCLOUD_PLATFORMS = ['binary_sensor', 'switch'] SCAN_INTERVAL = timedelta(seconds=60) -SIGNAL_UPDATE_UPCLOUD = "upcloud_update" +SIGNAL_UPDATE_UPCLOUD = 'upcloud_update' STATE_MAP = { - "started": STATE_ON, - "stopped": STATE_OFF, - "error": STATE_PROBLEM, + 'error': STATE_PROBLEM, + 'started': STATE_ON, + 'stopped': STATE_OFF, } CONFIG_SCHEMA = vol.Schema({ diff --git a/homeassistant/components/binary_sensor/upcloud.py b/homeassistant/components/upcloud/binary_sensor.py similarity index 82% rename from homeassistant/components/binary_sensor/upcloud.py rename to homeassistant/components/upcloud/binary_sensor.py index c7b8a284dc9..3fd54b349a2 100644 --- a/homeassistant/components/binary_sensor/upcloud.py +++ b/homeassistant/components/upcloud/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for monitoring the state of UpCloud servers. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.upcloud/ -""" +"""Support for monitoring the state of UpCloud servers.""" import logging import voluptuous as vol diff --git a/homeassistant/components/switch/upcloud.py b/homeassistant/components/upcloud/switch.py similarity index 86% rename from homeassistant/components/switch/upcloud.py rename to homeassistant/components/upcloud/switch.py index f2818e59a9b..0b44d787f6f 100644 --- a/homeassistant/components/switch/upcloud.py +++ b/homeassistant/components/upcloud/switch.py @@ -1,9 +1,4 @@ -""" -Support for interacting with UpCloud servers. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/switch.upcloud/ -""" +"""Support for interacting with UpCloud servers.""" import logging import voluptuous as vol diff --git a/homeassistant/components/updater.py b/homeassistant/components/updater/__init__.py similarity index 96% rename from homeassistant/components/updater.py rename to homeassistant/components/updater/__init__.py index daa85a2425e..cb2646ea942 100644 --- a/homeassistant/components/updater.py +++ b/homeassistant/components/updater/__init__.py @@ -1,9 +1,4 @@ -""" -Support to check for available updates. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/updater/ -""" +"""Support to check for available updates.""" import asyncio from datetime import timedelta # pylint: disable=import-error,no-name-in-module @@ -23,7 +18,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -REQUIREMENTS = ['distro==1.3.0'] +REQUIREMENTS = ['distro==1.4.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/upnp/.translations/da.json b/homeassistant/components/upnp/.translations/da.json new file mode 100644 index 00000000000..1d0097c2f1f --- /dev/null +++ b/homeassistant/components/upnp/.translations/da.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP/IGD er allerede konfigureret", + "incomplete_device": "Ignorerer ufuldst\u00e6ndig UPnP-enhed", + "no_devices_discovered": "Ingen UPnP/IGD enheder fundet.", + "no_devices_found": "Ingen UPnP/IGD enheder kunne findes p\u00e5 netv\u00e6rket.", + "no_sensors_or_port_mapping": "Aktiv\u00e9r enten sensorer eller porttilknytning", + "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning af UPnP/IGD." + }, + "error": { + "one": "En", + "other": "Anden" + }, + "step": { + "confirm": { + "description": "Er du sikker p\u00e5 at du vil konfigurere UPnP/IGD?", + "title": "UPnP/IGD" + }, + "init": { + "title": "UPnP/IGD" + }, + "user": { + "data": { + "enable_port_mapping": "Aktiv\u00e9r porttilknytning til Home Assistent", + "enable_sensors": "Tilf\u00f8j trafik sensorer", + "igd": "UPnP/IGD" + }, + "title": "Konfigurationsindstillinger for UPnP/IGD" + } + }, + "title": "UPnP/IGD" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/fr.json b/homeassistant/components/upnp/.translations/fr.json index 3eac9577890..d1ff04d4824 100644 --- a/homeassistant/components/upnp/.translations/fr.json +++ b/homeassistant/components/upnp/.translations/fr.json @@ -3,9 +3,15 @@ "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" + "no_devices_found": "Aucun p\u00e9riph\u00e9rique UPnP / IGD trouv\u00e9 sur le r\u00e9seau.", + "no_sensors_or_port_mapping": "Activer au moins les capteurs ou la cartographie des ports", + "single_instance_allowed": "Une seule configuration UPnP / IGD est n\u00e9cessaire." }, "step": { + "confirm": { + "description": "Voulez-vous configurer UPnP / IGD?", + "title": "UPnP / IGD" + }, "init": { "title": "UPnP / IGD" }, diff --git a/homeassistant/components/upnp/.translations/pt.json b/homeassistant/components/upnp/.translations/pt.json index 5c5693e6a0c..d559a05ff23 100644 --- a/homeassistant/components/upnp/.translations/pt.json +++ b/homeassistant/components/upnp/.translations/pt.json @@ -15,7 +15,7 @@ "step": { "confirm": { "description": "Deseja configurar o UPnP / IGD?", - "title": "" + "title": "UPnP/IGD" }, "init": { "title": "UPnP/IGD" diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 2a1b8c52d79..efa3ee73af8 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -1,9 +1,4 @@ -""" -Will open a port in your router for Home Assistant and provide statistics. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/upnp/ -""" +"""Open ports in your router for Home Assistant and provide statistics.""" from ipaddress import ip_address import voluptuous as vol @@ -28,7 +23,6 @@ from .const import DOMAIN from .const import LOGGER as _LOGGER from .device import Device - REQUIREMENTS = ['async-upnp-client==0.14.4'] NOTIFICATION_ID = 'upnp_notification' @@ -41,8 +35,7 @@ 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.port): - vol.Any(CONF_HASS, cv.port) + vol.Any(CONF_HASS, cv.port): vol.Any(CONF_HASS, cv.port) }) }), }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/sensor/upnp.py b/homeassistant/components/upnp/sensor.py similarity index 100% rename from homeassistant/components/sensor/upnp.py rename to homeassistant/components/upnp/sensor.py diff --git a/homeassistant/components/usps.py b/homeassistant/components/usps/__init__.py similarity index 92% rename from homeassistant/components/usps.py rename to homeassistant/components/usps/__init__.py index 41aa240492b..8a7d7d52255 100644 --- a/homeassistant/components/usps.py +++ b/homeassistant/components/usps/__init__.py @@ -1,9 +1,4 @@ -""" -Support for USPS packages and mail. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/usps/ -""" +"""Support for USPS packages and mail.""" from datetime import timedelta import logging @@ -33,7 +28,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_NAME, default=DOMAIN): cv.string, - vol.Optional(CONF_DRIVER): cv.string + vol.Optional(CONF_DRIVER): cv.string, }), }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/camera/usps.py b/homeassistant/components/usps/camera.py similarity index 92% rename from homeassistant/components/camera/usps.py rename to homeassistant/components/usps/camera.py index d23359d8c57..d4769102d14 100644 --- a/homeassistant/components/camera/usps.py +++ b/homeassistant/components/usps/camera.py @@ -1,9 +1,4 @@ -""" -Support for a camera made up of usps mail images. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/camera.usps/ -""" +"""Support for a camera made up of USPS mail images.""" from datetime import timedelta import logging diff --git a/homeassistant/components/sensor/usps.py b/homeassistant/components/usps/sensor.py similarity index 95% rename from homeassistant/components/sensor/usps.py rename to homeassistant/components/usps/sensor.py index 17fa11fe8d3..1603715861d 100644 --- a/homeassistant/components/sensor/usps.py +++ b/homeassistant/components/usps/sensor.py @@ -1,9 +1,4 @@ -""" -Sensor for USPS packages. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.usps/ -""" +"""Sensor for USPS packages.""" from collections import defaultdict import logging diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py index 8a8e669ba88..3cf1b2fea61 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -1,10 +1,4 @@ -""" -Component to track utility consumption over given periods of time. - -For more details about this component, please refer to the documentation -at https://www.home-assistant.io/components/utility_meter/ -""" - +"""Support for tracking consumption over given periods of time.""" import logging import voluptuous as vol @@ -25,7 +19,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -TARIFF_ICON = "mdi:clock-outline" +TARIFF_ICON = 'mdi:clock-outline' ATTR_TARIFFS = 'tariffs' @@ -57,6 +51,7 @@ async def async_setup(hass, config): """Set up an Utility Meter.""" component = EntityComponent(_LOGGER, DOMAIN, hass) hass.data[DATA_UTILITY] = {} + register_services = False for meter, conf in config.get(DOMAIN).items(): _LOGGER.debug("Setup %s.%s", DOMAIN, meter) @@ -86,21 +81,23 @@ async def async_setup(hass, config): }) hass.async_create_task(discovery.async_load_platform( hass, SENSOR_DOMAIN, DOMAIN, tariff_confs, config)) + register_services = True - component.async_register_entity_service( - SERVICE_RESET, SERVICE_METER_SCHEMA, - 'async_reset_meters' - ) + if register_services: + component.async_register_entity_service( + SERVICE_RESET, SERVICE_METER_SCHEMA, + 'async_reset_meters' + ) - component.async_register_entity_service( - SERVICE_SELECT_TARIFF, SERVICE_SELECT_TARIFF_SCHEMA, - 'async_select_tariff' - ) + component.async_register_entity_service( + SERVICE_SELECT_TARIFF, SERVICE_SELECT_TARIFF_SCHEMA, + 'async_select_tariff' + ) - component.async_register_entity_service( - SERVICE_SELECT_NEXT_TARIFF, SERVICE_METER_SCHEMA, - 'async_next_tariff' - ) + component.async_register_entity_service( + SERVICE_SELECT_NEXT_TARIFF, SERVICE_METER_SCHEMA, + 'async_next_tariff' + ) return True @@ -156,6 +153,7 @@ class TariffSelect(RestoreEntity): async def async_reset_meters(self): """Reset all sensors of this meter.""" + _LOGGER.debug("reset meter %s", self.entity_id) async_dispatcher_send(self.hass, SIGNAL_RESET_METER, self.entity_id) diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index cd86f9c0bd0..a01c53b20e3 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -1,9 +1,4 @@ -""" -Utility meter from sensors providing raw data. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.utility_meter/ -""" +"""Utility meter from sensors providing raw data.""" import logging from decimal import Decimal, DecimalException @@ -40,8 +35,8 @@ PAUSED = 'paused' COLLECTING = 'collecting' -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the utility meter sensor.""" if discovery_info is None: _LOGGER.error("This platform is only available through discovery") @@ -56,12 +51,10 @@ async def async_setup_platform(hass, config, async_add_entities, conf_meter_tariff_entity = hass.data[DATA_UTILITY][meter].get( CONF_TARIFF_ENTITY) - meters.append(UtilityMeterSensor(conf_meter_source, - conf.get(CONF_NAME), - conf_meter_type, - conf_meter_offset, - conf.get(CONF_TARIFF), - conf_meter_tariff_entity)) + meters.append(UtilityMeterSensor( + conf_meter_source, conf.get(CONF_NAME), conf_meter_type, + conf_meter_offset, conf.get(CONF_TARIFF), + conf_meter_tariff_entity)) async_add_entities(meters) @@ -90,9 +83,9 @@ class UtilityMeterSensor(RestoreEntity): @callback def async_reading(self, entity, old_state, new_state): """Handle the sensor state changes.""" - if any([old_state is None, - old_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE], - new_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE]]): + if old_state is None or new_state is None or\ + old_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE] or\ + new_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE]: return if self._unit_of_measurement is None and\ @@ -181,21 +174,22 @@ class UtilityMeterSensor(RestoreEntity): self._last_reset = state.attributes.get(ATTR_LAST_RESET) await self.async_update_ha_state() if state.attributes.get(ATTR_STATUS) == PAUSED: - # Fake cancelation function to init the meter paused + # Fake cancellation function to init the meter paused self._collecting = lambda: None @callback def async_source_tracking(event): """Wait for source to be ready, then start meter.""" if self._tariff_entity is not None: - _LOGGER.debug("track %s", self._tariff_entity) - async_track_state_change(self.hass, self._tariff_entity, - self.async_tariff_change) + _LOGGER.debug("Track %s", self._tariff_entity) + async_track_state_change( + self.hass, self._tariff_entity, self.async_tariff_change) tariff_entity_state = self.hass.states.get(self._tariff_entity) if self._tariff != tariff_entity_state.state: return + _LOGGER.debug("tracking source: %s", self._sensor_source_id) self._collecting = async_track_state_change( self.hass, self._sensor_source_id, self.async_reading) diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 6341c9661ed..3fdc7cb1a51 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -16,7 +16,8 @@ from homeassistant.const import ( SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON, STATE_PAUSED, STATE_IDLE) from homeassistant.loader import bind_hass import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa +from homeassistant.helpers.config_validation import ( # noqa + PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import (ToggleEntity, Entity) from homeassistant.helpers.icon import icon_for_battery_level diff --git a/homeassistant/components/velbus.py b/homeassistant/components/velbus/__init__.py similarity index 94% rename from homeassistant/components/velbus.py rename to homeassistant/components/velbus/__init__.py index 15ca8584a4e..38d8b6c3f1c 100644 --- a/homeassistant/components/velbus.py +++ b/homeassistant/components/velbus/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Velbus platform. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/velbus/ -""" +"""Support for Velbus devices.""" import logging import voluptuous as vol @@ -18,7 +13,6 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = 'velbus' - VELBUS_MESSAGE = 'velbus.message' CONFIG_SCHEMA = vol.Schema({ diff --git a/homeassistant/components/binary_sensor/velbus.py b/homeassistant/components/velbus/binary_sensor.py similarity index 73% rename from homeassistant/components/binary_sensor/velbus.py rename to homeassistant/components/velbus/binary_sensor.py index b123b958560..43ffa232b40 100644 --- a/homeassistant/components/binary_sensor/velbus.py +++ b/homeassistant/components/velbus/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for Velbus Binary Sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.velbus/ -""" +"""Support for Velbus Binary Sensors.""" import logging from homeassistant.components.binary_sensor import BinarySensorDevice @@ -15,8 +10,8 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['velbus'] -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up Velbus binary sensors.""" if discovery_info is None: return diff --git a/homeassistant/components/climate/velbus.py b/homeassistant/components/velbus/climate.py similarity index 92% rename from homeassistant/components/climate/velbus.py rename to homeassistant/components/velbus/climate.py index 0b0205acefb..ae7a2828492 100644 --- a/homeassistant/components/climate/velbus.py +++ b/homeassistant/components/velbus/climate.py @@ -1,9 +1,4 @@ -""" -Support for Velbus thermostat. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/climate.velbus/ -""" +"""Support for Velbus thermostat.""" import logging from homeassistant.components.climate import ( diff --git a/homeassistant/components/cover/velbus.py b/homeassistant/components/velbus/cover.py similarity index 96% rename from homeassistant/components/cover/velbus.py rename to homeassistant/components/velbus/cover.py index 7e5099cecf8..72a5a7af79b 100644 --- a/homeassistant/components/cover/velbus.py +++ b/homeassistant/components/velbus/cover.py @@ -1,9 +1,4 @@ -""" -Support for Velbus covers. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/cover.velbus/ -""" +"""Support for Velbus covers.""" import logging import time diff --git a/homeassistant/components/sensor/velbus.py b/homeassistant/components/velbus/sensor.py similarity index 79% rename from homeassistant/components/sensor/velbus.py rename to homeassistant/components/velbus/sensor.py index 8e9aafd3605..10ad89ab847 100644 --- a/homeassistant/components/sensor/velbus.py +++ b/homeassistant/components/velbus/sensor.py @@ -1,9 +1,4 @@ -""" -Velbus sensors. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/sensor.velbus/ -""" +"""Support for Velbus sensors.""" import logging from homeassistant.components.velbus import ( @@ -14,8 +9,8 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['velbus'] -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the Velbus temp sensor platform.""" if discovery_info is None: return diff --git a/homeassistant/components/switch/velbus.py b/homeassistant/components/velbus/switch.py similarity index 78% rename from homeassistant/components/switch/velbus.py rename to homeassistant/components/velbus/switch.py index 300ff43d676..7104bb0750d 100644 --- a/homeassistant/components/switch/velbus.py +++ b/homeassistant/components/velbus/switch.py @@ -1,9 +1,4 @@ -""" -Support for Velbus switches. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.velbus/ -""" +"""Support for Velbus switches.""" import logging from homeassistant.components.switch import SwitchDevice @@ -15,8 +10,8 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['velbus'] -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the Velbus Switch platform.""" if discovery_info is None: return diff --git a/homeassistant/components/velux.py b/homeassistant/components/velux/__init__.py similarity index 86% rename from homeassistant/components/velux.py rename to homeassistant/components/velux/__init__.py index c3c6c1e2114..1018f72fdbc 100644 --- a/homeassistant/components/velux.py +++ b/homeassistant/components/velux/__init__.py @@ -1,9 +1,4 @@ -""" -Connects to VELUX KLF 200 interface. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/velux/ -""" +"""Support for VELUX KLF 200 devices.""" import logging import voluptuous as vol @@ -14,10 +9,10 @@ from homeassistant.const import (CONF_HOST, CONF_PASSWORD) DOMAIN = "velux" DATA_VELUX = "data_velux" -SUPPORTED_DOMAINS = ['scene'] +SUPPORTED_DOMAINS = ['cover', 'scene'] _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pyvlx==0.1.3'] +REQUIREMENTS = ['pyvlx==0.2.8'] CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -59,3 +54,4 @@ class VeluxModule: async def async_start(self): """Start velux component.""" await self.pyvlx.load_scenes() + await self.pyvlx.load_nodes() diff --git a/homeassistant/components/velux/cover.py b/homeassistant/components/velux/cover.py new file mode 100644 index 00000000000..1c3192961af --- /dev/null +++ b/homeassistant/components/velux/cover.py @@ -0,0 +1,90 @@ +"""Support for Velux covers.""" +from homeassistant.components.cover import ( + ATTR_POSITION, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, + SUPPORT_STOP, CoverDevice) +from homeassistant.components.velux import DATA_VELUX +from homeassistant.core import callback + +DEPENDENCIES = ['velux'] + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Set up cover(s) for Velux platform.""" + entities = [] + for node in hass.data[DATA_VELUX].pyvlx.nodes: + from pyvlx import OpeningDevice + if isinstance(node, OpeningDevice): + entities.append(VeluxCover(node)) + async_add_entities(entities) + + +class VeluxCover(CoverDevice): + """Representation of a Velux cover.""" + + def __init__(self, node): + """Initialize the cover.""" + self.node = node + + @callback + def async_register_callbacks(self): + """Register callbacks to update hass after device was changed.""" + async def after_update_callback(device): + """Call after device was updated.""" + await self.async_update_ha_state() + self.node.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 Velux device.""" + return self.node.name + + @property + def should_poll(self): + """No polling needed within Velux.""" + return False + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_OPEN | SUPPORT_CLOSE | \ + SUPPORT_SET_POSITION | SUPPORT_STOP + + @property + def current_cover_position(self): + """Return the current position of the cover.""" + return 100 - self.node.position.position_percent + + @property + def device_class(self): + """Define this cover as a window.""" + return 'window' + + @property + def is_closed(self): + """Return if the cover is closed.""" + return self.node.position.closed + + async def async_close_cover(self, **kwargs): + """Close the cover.""" + await self.node.close() + + async def async_open_cover(self, **kwargs): + """Open the cover.""" + await self.node.open() + + async def async_set_cover_position(self, **kwargs): + """Move the cover to a specific position.""" + if ATTR_POSITION in kwargs: + position_percent = 100 - kwargs[ATTR_POSITION] + from pyvlx import Position + await self.node.set_position( + Position(position_percent=position_percent)) + + async def async_stop_cover(self, **kwargs): + """Stop the cover.""" + await self.node.stop() diff --git a/homeassistant/components/scene/velux.py b/homeassistant/components/velux/scene.py similarity index 59% rename from homeassistant/components/scene/velux.py rename to homeassistant/components/velux/scene.py index 77ba30158e4..db1e9450daf 100644 --- a/homeassistant/components/scene/velux.py +++ b/homeassistant/components/velux/scene.py @@ -1,20 +1,13 @@ -""" -Support for VELUX scenes. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/scene.velux/ -""" - +"""Support for VELUX scenes.""" from homeassistant.components.scene import Scene from homeassistant.components.velux import _LOGGER, DATA_VELUX - DEPENDENCIES = ['velux'] -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up the scenes for velux platform.""" +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Set up the scenes for Velux platform.""" entities = [] for scene in hass.data[DATA_VELUX].pyvlx.scenes: entities.append(VeluxScene(scene)) @@ -22,11 +15,11 @@ async def async_setup_platform(hass, config, async_add_entities, class VeluxScene(Scene): - """Representation of a velux scene.""" + """Representation of a Velux scene.""" def __init__(self, scene): """Init velux scene.""" - _LOGGER.info("Adding VELUX scene: %s", scene) + _LOGGER.info("Adding Velux scene: %s", scene) self.scene = scene @property diff --git a/homeassistant/components/vera.py b/homeassistant/components/vera/__init__.py similarity index 97% rename from homeassistant/components/vera.py rename to homeassistant/components/vera/__init__.py index 127cd008a3a..3f4c66d238a 100644 --- a/homeassistant/components/vera.py +++ b/homeassistant/components/vera/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Vera devices. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/vera/ -""" +"""Support for Vera devices.""" import logging from collections import defaultdict @@ -43,7 +38,7 @@ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_CONTROLLER): cv.url, vol.Optional(CONF_EXCLUDE, default=[]): VERA_ID_LIST_SCHEMA, - vol.Optional(CONF_LIGHTS, default=[]): VERA_ID_LIST_SCHEMA + vol.Optional(CONF_LIGHTS, default=[]): VERA_ID_LIST_SCHEMA, }), }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/binary_sensor/vera.py b/homeassistant/components/vera/binary_sensor.py similarity index 86% rename from homeassistant/components/binary_sensor/vera.py rename to homeassistant/components/vera/binary_sensor.py index bb1e7331de8..837422dbc7c 100644 --- a/homeassistant/components/binary_sensor/vera.py +++ b/homeassistant/components/vera/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for Vera binary sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.vera/ -""" +"""Support for Vera binary sensors.""" import logging from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/climate/vera.py b/homeassistant/components/vera/climate.py similarity index 96% rename from homeassistant/components/climate/vera.py rename to homeassistant/components/vera/climate.py index 5e016b8666b..7cd3129bc14 100644 --- a/homeassistant/components/climate/vera.py +++ b/homeassistant/components/vera/climate.py @@ -1,9 +1,4 @@ -""" -Support for Vera thermostats. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.vera/ -""" +"""Support for Vera thermostats.""" import logging from homeassistant.util import convert diff --git a/homeassistant/components/cover/vera.py b/homeassistant/components/vera/cover.py similarity index 91% rename from homeassistant/components/cover/vera.py rename to homeassistant/components/vera/cover.py index 279e4a4307d..1168cca8425 100644 --- a/homeassistant/components/cover/vera.py +++ b/homeassistant/components/vera/cover.py @@ -1,9 +1,4 @@ -""" -Support for Vera cover - curtains, rollershutters etc. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/cover.vera/ -""" +"""Support for Vera cover - curtains, rollershutters etc.""" import logging from homeassistant.components.cover import CoverDevice, ENTITY_ID_FORMAT, \ diff --git a/homeassistant/components/light/vera.py b/homeassistant/components/vera/light.py similarity index 94% rename from homeassistant/components/light/vera.py rename to homeassistant/components/vera/light.py index 702236ac748..93e54b915c7 100644 --- a/homeassistant/components/light/vera.py +++ b/homeassistant/components/vera/light.py @@ -1,9 +1,4 @@ -""" -Support for Vera lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.vera/ -""" +"""Support for Vera lights.""" import logging from homeassistant.components.light import ( diff --git a/homeassistant/components/lock/vera.py b/homeassistant/components/vera/lock.py similarity index 90% rename from homeassistant/components/lock/vera.py rename to homeassistant/components/vera/lock.py index 21287b6328e..61d5f0baf28 100644 --- a/homeassistant/components/lock/vera.py +++ b/homeassistant/components/vera/lock.py @@ -1,9 +1,4 @@ -""" -Support for Vera locks. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/lock.vera/ -""" +"""Support for Vera locks.""" import logging from homeassistant.components.lock import ENTITY_ID_FORMAT, LockDevice diff --git a/homeassistant/components/scene/vera.py b/homeassistant/components/vera/scene.py similarity index 89% rename from homeassistant/components/scene/vera.py rename to homeassistant/components/vera/scene.py index 6cae1195f87..0960512f6d1 100644 --- a/homeassistant/components/scene/vera.py +++ b/homeassistant/components/vera/scene.py @@ -1,9 +1,4 @@ -""" -Support for Vera scenes. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/scene.vera/ -""" +"""Support for Vera scenes.""" import logging from homeassistant.util import slugify @@ -11,10 +6,10 @@ from homeassistant.components.scene import Scene from homeassistant.components.vera import ( VERA_CONTROLLER, VERA_SCENES, VERA_ID_FORMAT) -DEPENDENCIES = ['vera'] - _LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['vera'] + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Vera scenes.""" diff --git a/homeassistant/components/sensor/vera.py b/homeassistant/components/vera/sensor.py similarity index 95% rename from homeassistant/components/sensor/vera.py rename to homeassistant/components/vera/sensor.py index c9b5a36afa3..8b68cc9190f 100644 --- a/homeassistant/components/sensor/vera.py +++ b/homeassistant/components/vera/sensor.py @@ -1,9 +1,4 @@ -""" -Support for Vera sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.vera/ -""" +"""Support for Vera sensors.""" import logging from datetime import timedelta @@ -15,10 +10,10 @@ from homeassistant.util import convert from homeassistant.components.vera import ( VERA_CONTROLLER, VERA_DEVICES, VeraDevice) -DEPENDENCIES = ['vera'] - _LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['vera'] + SCAN_INTERVAL = timedelta(seconds=5) diff --git a/homeassistant/components/switch/vera.py b/homeassistant/components/vera/switch.py similarity index 90% rename from homeassistant/components/switch/vera.py rename to homeassistant/components/vera/switch.py index 4742b755944..2f4d18e34e1 100644 --- a/homeassistant/components/switch/vera.py +++ b/homeassistant/components/vera/switch.py @@ -1,9 +1,4 @@ -""" -Support for Vera switches. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.vera/ -""" +"""Support for Vera switches.""" import logging from homeassistant.util import convert @@ -11,10 +6,10 @@ from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchDevice from homeassistant.components.vera import ( VERA_CONTROLLER, VERA_DEVICES, VeraDevice) -DEPENDENCIES = ['vera'] - _LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['vera'] + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Vera switches.""" diff --git a/homeassistant/components/verisure.py b/homeassistant/components/verisure/__init__.py similarity index 97% rename from homeassistant/components/verisure.py rename to homeassistant/components/verisure/__init__.py index 481aa331e41..393a4066002 100644 --- a/homeassistant/components/verisure.py +++ b/homeassistant/components/verisure/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Verisure components. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/verisure/ -""" +"""Support for Verisure devices.""" import logging import threading from datetime import timedelta diff --git a/homeassistant/components/alarm_control_panel/verisure.py b/homeassistant/components/verisure/alarm_control_panel.py similarity index 93% rename from homeassistant/components/alarm_control_panel/verisure.py rename to homeassistant/components/verisure/alarm_control_panel.py index 160f152ef8a..adcdcd668cb 100644 --- a/homeassistant/components/alarm_control_panel/verisure.py +++ b/homeassistant/components/verisure/alarm_control_panel.py @@ -1,9 +1,4 @@ -""" -Interfaces with Verisure alarm control panel. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/alarm_control_panel.verisure/ -""" +"""Support for Verisure alarm control panels.""" import logging from time import sleep diff --git a/homeassistant/components/binary_sensor/verisure.py b/homeassistant/components/verisure/binary_sensor.py similarity index 90% rename from homeassistant/components/binary_sensor/verisure.py rename to homeassistant/components/verisure/binary_sensor.py index e040da959ea..4c9e79724fe 100644 --- a/homeassistant/components/binary_sensor/verisure.py +++ b/homeassistant/components/verisure/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Interfaces with Verisure sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.verisure/ -""" +"""Support for Verisure binary sensors.""" import logging from homeassistant.components.binary_sensor import BinarySensorDevice diff --git a/homeassistant/components/camera/verisure.py b/homeassistant/components/verisure/camera.py similarity index 94% rename from homeassistant/components/camera/verisure.py rename to homeassistant/components/verisure/camera.py index 01e4e82f3bc..7112e535a95 100644 --- a/homeassistant/components/camera/verisure.py +++ b/homeassistant/components/verisure/camera.py @@ -1,9 +1,4 @@ -""" -Camera that loads a picture from a local file. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/camera.verisure/ -""" +"""Support for Verisure cameras.""" import errno import logging import os diff --git a/homeassistant/components/lock/verisure.py b/homeassistant/components/verisure/lock.py similarity index 95% rename from homeassistant/components/lock/verisure.py rename to homeassistant/components/verisure/lock.py index be9a0a24fee..cdd230ea7f7 100644 --- a/homeassistant/components/lock/verisure.py +++ b/homeassistant/components/verisure/lock.py @@ -1,9 +1,4 @@ -""" -Interfaces with Verisure locks. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/verisure/ -""" +"""Support for Verisure locks.""" import logging from time import sleep from time import time @@ -18,7 +13,7 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Verisure platform.""" + """Set up the Verisure lock platform.""" locks = [] if int(hub.config.get(CONF_LOCKS, 1)): hub.update_overview() diff --git a/homeassistant/components/sensor/verisure.py b/homeassistant/components/verisure/sensor.py similarity index 96% rename from homeassistant/components/sensor/verisure.py rename to homeassistant/components/verisure/sensor.py index b6ea75ae8cc..13706d8408f 100644 --- a/homeassistant/components/sensor/verisure.py +++ b/homeassistant/components/verisure/sensor.py @@ -1,9 +1,4 @@ -""" -Interfaces with Verisure sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.verisure/ -""" +"""Support for Verisure sensors.""" import logging from homeassistant.components.verisure import HUB as hub diff --git a/homeassistant/components/verisure/services.yaml b/homeassistant/components/verisure/services.yaml new file mode 100644 index 00000000000..405f0c5d57d --- /dev/null +++ b/homeassistant/components/verisure/services.yaml @@ -0,0 +1,5 @@ +capture_smartcam: + description: Capture a new image from a smartcam. + fields: + device_serial: {description: The serial number of the smartcam you want to capture + an image from., example: 2DEU AT5Z} diff --git a/homeassistant/components/switch/verisure.py b/homeassistant/components/verisure/switch.py similarity index 92% rename from homeassistant/components/switch/verisure.py rename to homeassistant/components/verisure/switch.py index 11ccd82696e..a418eec6bc5 100644 --- a/homeassistant/components/switch/verisure.py +++ b/homeassistant/components/verisure/switch.py @@ -1,9 +1,4 @@ -""" -Support for Verisure Smartplugs. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.verisure/ -""" +"""Support for Verisure Smartplugs.""" import logging from time import time diff --git a/homeassistant/components/volvooncall.py b/homeassistant/components/volvooncall/__init__.py similarity index 84% rename from homeassistant/components/volvooncall.py rename to homeassistant/components/volvooncall/__init__.py index 0d89537b8e8..7e72607c2f3 100644 --- a/homeassistant/components/volvooncall.py +++ b/homeassistant/components/volvooncall/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Volvo On Call. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/volvooncall/ -""" +"""Support for Volvo On Call.""" from datetime import timedelta import logging @@ -11,7 +6,8 @@ import voluptuous as vol from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, CONF_NAME, CONF_RESOURCES, - CONF_UPDATE_INTERVAL) + CONF_UPDATE_INTERVAL, CONF_SCAN_INTERVAL, + CONF_UPDATE_INTERVAL_INVALIDATION_VERSION) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -45,7 +41,7 @@ COMPONENTS = { 'binary_sensor': 'binary_sensor', 'lock': 'lock', 'device_tracker': 'device_tracker', - 'switch': 'switch' + 'switch': 'switch', } RESOURCES = [ @@ -88,20 +84,30 @@ RESOURCES = [ ] CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_UPDATE_INTERVAL, default=DEFAULT_UPDATE_INTERVAL): ( - vol.All(cv.time_period, vol.Clamp(min=MIN_UPDATE_INTERVAL))), - vol.Optional(CONF_NAME, default={}): - cv.schema_with_slug_keys(cv.string), - vol.Optional(CONF_RESOURCES): vol.All( - cv.ensure_list, [vol.In(RESOURCES)]), - vol.Optional(CONF_REGION): cv.string, - vol.Optional(CONF_SERVICE_URL): cv.string, - vol.Optional(CONF_MUTABLE, default=True): cv.boolean, - vol.Optional(CONF_SCANDINAVIAN_MILES, default=False): cv.boolean, - }), + DOMAIN: vol.All( + vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_UPDATE_INTERVAL): + vol.All(cv.time_period, vol.Clamp(min=MIN_UPDATE_INTERVAL)), + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_UPDATE_INTERVAL): + vol.All(cv.time_period, vol.Clamp(min=MIN_UPDATE_INTERVAL)), + vol.Optional(CONF_NAME, default={}): + cv.schema_with_slug_keys(cv.string), + vol.Optional(CONF_RESOURCES): vol.All( + cv.ensure_list, [vol.In(RESOURCES)]), + vol.Optional(CONF_REGION): cv.string, + vol.Optional(CONF_SERVICE_URL): cv.string, + vol.Optional(CONF_MUTABLE, default=True): cv.boolean, + vol.Optional(CONF_SCANDINAVIAN_MILES, default=False): cv.boolean, + }), + cv.deprecated( + CONF_UPDATE_INTERVAL, + replacement_key=CONF_SCAN_INTERVAL, + invalidation_version=CONF_UPDATE_INTERVAL_INVALIDATION_VERSION, + default=DEFAULT_UPDATE_INTERVAL + ) + ) }, extra=vol.ALLOW_EXTRA) @@ -117,7 +123,7 @@ async def async_setup(hass, config): service_url=config[DOMAIN].get(CONF_SERVICE_URL), region=config[DOMAIN].get(CONF_REGION)) - interval = config[DOMAIN].get(CONF_UPDATE_INTERVAL) + interval = config[DOMAIN][CONF_SCAN_INTERVAL] data = hass.data[DATA_KEY] = VolvoData(config) diff --git a/homeassistant/components/binary_sensor/volvooncall.py b/homeassistant/components/volvooncall/binary_sensor.py similarity index 74% rename from homeassistant/components/binary_sensor/volvooncall.py rename to homeassistant/components/volvooncall/binary_sensor.py index e7092ff16d5..7158e4df69b 100644 --- a/homeassistant/components/binary_sensor/volvooncall.py +++ b/homeassistant/components/volvooncall/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for VOC. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.volvooncall/ -""" +"""Support for VOC.""" import logging from homeassistant.components.volvooncall import VolvoEntity, DATA_KEY @@ -13,8 +8,8 @@ from homeassistant.components.binary_sensor import ( _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the Volvo sensors.""" if discovery_info is None: return diff --git a/homeassistant/components/device_tracker/volvooncall.py b/homeassistant/components/volvooncall/device_tracker.py similarity index 86% rename from homeassistant/components/device_tracker/volvooncall.py rename to homeassistant/components/volvooncall/device_tracker.py index 395b539a065..d4838c01505 100644 --- a/homeassistant/components/device_tracker/volvooncall.py +++ b/homeassistant/components/volvooncall/device_tracker.py @@ -1,9 +1,4 @@ -""" -Support for tracking a Volvo. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/device_tracker.volvooncall/ -""" +"""Support for tracking a Volvo.""" import logging from homeassistant.util import slugify diff --git a/homeassistant/components/lock/volvooncall.py b/homeassistant/components/volvooncall/lock.py similarity index 72% rename from homeassistant/components/lock/volvooncall.py rename to homeassistant/components/volvooncall/lock.py index 83301aa3d4e..f281ea64461 100644 --- a/homeassistant/components/lock/volvooncall.py +++ b/homeassistant/components/volvooncall/lock.py @@ -1,9 +1,4 @@ -""" -Support for Volvo On Call locks. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/lock.volvooncall/ -""" +"""Support for Volvo On Call locks.""" import logging from homeassistant.components.lock import LockDevice @@ -12,8 +7,8 @@ from homeassistant.components.volvooncall import VolvoEntity, DATA_KEY _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the Volvo On Call lock.""" if discovery_info is None: return diff --git a/homeassistant/components/sensor/volvooncall.py b/homeassistant/components/volvooncall/sensor.py similarity index 68% rename from homeassistant/components/sensor/volvooncall.py rename to homeassistant/components/volvooncall/sensor.py index 65b996a5bd5..07f16e580bd 100644 --- a/homeassistant/components/sensor/volvooncall.py +++ b/homeassistant/components/volvooncall/sensor.py @@ -1,10 +1,4 @@ -""" -Support for VOC. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.volvooncall/ - -""" +"""Support for Volvo On Call sensors.""" import logging from homeassistant.components.volvooncall import VolvoEntity, DATA_KEY @@ -12,8 +6,8 @@ from homeassistant.components.volvooncall import VolvoEntity, DATA_KEY _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the Volvo sensors.""" if discovery_info is None: return diff --git a/homeassistant/components/switch/volvooncall.py b/homeassistant/components/volvooncall/switch.py similarity index 70% rename from homeassistant/components/switch/volvooncall.py rename to homeassistant/components/volvooncall/switch.py index 81abf7d0e6c..d3985557cff 100644 --- a/homeassistant/components/switch/volvooncall.py +++ b/homeassistant/components/volvooncall/switch.py @@ -1,11 +1,4 @@ -""" -Support for Volvo heater. - -This platform uses the Volvo online service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.volvooncall/ -""" +"""Support for Volvo heater.""" import logging from homeassistant.components.volvooncall import VolvoEntity, DATA_KEY @@ -14,8 +7,8 @@ from homeassistant.helpers.entity import ToggleEntity _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up a Volvo switch.""" if discovery_info is None: return diff --git a/homeassistant/components/vultr.py b/homeassistant/components/vultr/__init__.py similarity index 94% rename from homeassistant/components/vultr.py rename to homeassistant/components/vultr/__init__.py index b28189444ee..9f2efabd412 100644 --- a/homeassistant/components/vultr.py +++ b/homeassistant/components/vultr/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Vultr. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/vultr/ -""" +"""Support for Vultr.""" import logging from datetime import timedelta diff --git a/homeassistant/components/w800rf32.py b/homeassistant/components/w800rf32/__init__.py similarity index 88% rename from homeassistant/components/w800rf32.py rename to homeassistant/components/w800rf32/__init__.py index 4b237272546..d2c0cf6b968 100644 --- a/homeassistant/components/w800rf32.py +++ b/homeassistant/components/w800rf32/__init__.py @@ -1,10 +1,4 @@ -""" -Support for w800rf32 components. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/w800rf32/ - -""" +"""Support for w800rf32 devices.""" import logging import voluptuous as vol @@ -18,15 +12,16 @@ from homeassistant.helpers.dispatcher import (dispatcher_send) REQUIREMENTS = ['pyW800rf32==0.1'] -DOMAIN = 'w800rf32' DATA_W800RF32 = 'data_w800rf32' +DOMAIN = 'w800rf32' + W800RF32_DEVICE = 'w800rf32_{}' _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ - vol.Required(CONF_DEVICE): cv.string + vol.Required(CONF_DEVICE): cv.string, }), }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/binary_sensor/w800rf32.py b/homeassistant/components/w800rf32/binary_sensor.py similarity index 91% rename from homeassistant/components/binary_sensor/w800rf32.py rename to homeassistant/components/w800rf32/binary_sensor.py index 48ac6f41a12..855a5f3ed0a 100644 --- a/homeassistant/components/binary_sensor/w800rf32.py +++ b/homeassistant/components/w800rf32/binary_sensor.py @@ -1,10 +1,4 @@ -""" -Support for w800rf32 binary sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.w800rf32/ - -""" +"""Support for w800rf32 binary sensors.""" import logging import voluptuous as vol @@ -30,14 +24,14 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_OFF_DELAY): - vol.All(cv.time_period, cv.positive_timedelta) + vol.All(cv.time_period, cv.positive_timedelta), }) }, }, extra=vol.ALLOW_EXTRA) -async def async_setup_platform(hass, config, - add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, add_entities, discovery_info=None): """Set up the Binary Sensor platform to w800rf32.""" binary_sensors = [] # device_id --> "c1 or a3" X10 device. entity (type dictionary) diff --git a/homeassistant/components/wake_on_lan.py b/homeassistant/components/wake_on_lan/__init__.py similarity index 87% rename from homeassistant/components/wake_on_lan.py rename to homeassistant/components/wake_on_lan/__init__.py index dba99bf7e3d..e6e12ef0afe 100644 --- a/homeassistant/components/wake_on_lan.py +++ b/homeassistant/components/wake_on_lan/__init__.py @@ -1,9 +1,4 @@ -""" -Component to wake up devices sending Wake-On-LAN magic packets. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/wake_on_lan/ -""" +"""Support for sending Wake-On-LAN magic packets.""" from functools import partial import logging diff --git a/homeassistant/components/wake_on_lan/services.yaml b/homeassistant/components/wake_on_lan/services.yaml new file mode 100644 index 00000000000..e20dd64396f --- /dev/null +++ b/homeassistant/components/wake_on_lan/services.yaml @@ -0,0 +1,6 @@ +send_magic_packet: + description: Send a 'magic packet' to wake up a device with 'Wake-On-LAN' capabilities. + fields: + broadcast_address: {description: Optional broadcast IP where to send the magic + packet., example: 192.168.255.255} + mac: {description: MAC address of the device to wake up., example: 'aa:bb:cc:dd:ee:ff'} diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index fee2846e8d5..6c3cc7405ba 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -1,9 +1,4 @@ -""" -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/ -""" +"""Support for water heater devices.""" from datetime import timedelta import logging import functools as ft @@ -14,7 +9,8 @@ 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 +from homeassistant.helpers.config_validation import ( # noqa + PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) import homeassistant.helpers.config_validation as cv from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_TURN_ON, SERVICE_TURN_OFF, diff --git a/homeassistant/components/water_heater/demo.py b/homeassistant/components/water_heater/demo.py index 89b86c12af4..a0220927f16 100644 --- a/homeassistant/components/water_heater/demo.py +++ b/homeassistant/components/water_heater/demo.py @@ -1,9 +1,4 @@ -""" -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/ -""" +"""Demo platform that offers a fake water_heater device.""" from homeassistant.components.water_heater import ( WaterHeaterDevice, SUPPORT_TARGET_TEMPERATURE, @@ -18,11 +13,10 @@ SUPPORT_FLAGS_HEATER = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_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') - + DemoWaterHeater( + 'Demo Water Heater', 119, TEMP_FAHRENHEIT, False, 'eco'), + DemoWaterHeater( + 'Demo Water Heater Celsius', 45, TEMP_CELSIUS, True, 'eco'), ]) diff --git a/homeassistant/components/water_heater/econet.py b/homeassistant/components/water_heater/econet.py index 6af8ea43fa6..93ae98ed94b 100644 --- a/homeassistant/components/water_heater/econet.py +++ b/homeassistant/components/water_heater/econet.py @@ -1,9 +1,4 @@ -""" -Support for Rheem EcoNet water heaters. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/water_heater.econet/ -""" +"""Support for Rheem EcoNet water heaters.""" import datetime import logging diff --git a/homeassistant/components/waterfurnace.py b/homeassistant/components/waterfurnace/__init__.py similarity index 94% rename from homeassistant/components/waterfurnace.py rename to homeassistant/components/waterfurnace/__init__.py index bbae6170048..38fd44cd1c7 100644 --- a/homeassistant/components/waterfurnace.py +++ b/homeassistant/components/waterfurnace/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Waterfurnace component. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/waterfurnace/ -""" +"""Support for Waterfurnaces.""" from datetime import timedelta import logging import time @@ -18,12 +13,11 @@ from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers import discovery - -REQUIREMENTS = ["waterfurnace==1.1.0"] +REQUIREMENTS = ['waterfurnace==1.1.0'] _LOGGER = logging.getLogger(__name__) -DOMAIN = "waterfurnace" +DOMAIN = 'waterfurnace' UPDATE_TOPIC = DOMAIN + "_update" SCAN_INTERVAL = timedelta(seconds=10) ERROR_INTERVAL = timedelta(seconds=300) @@ -35,7 +29,7 @@ NOTIFICATION_TITLE = 'WaterFurnace website status' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string + vol.Required(CONF_USERNAME): cv.string, }), }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/sensor/waterfurnace.py b/homeassistant/components/waterfurnace/sensor.py similarity index 100% rename from homeassistant/components/sensor/waterfurnace.py rename to homeassistant/components/waterfurnace/sensor.py diff --git a/homeassistant/components/watson_iot.py b/homeassistant/components/watson_iot/__init__.py similarity index 96% rename from homeassistant/components/watson_iot.py rename to homeassistant/components/watson_iot/__init__.py index 889984eb223..e9a907ee6d2 100644 --- a/homeassistant/components/watson_iot.py +++ b/homeassistant/components/watson_iot/__init__.py @@ -1,9 +1,4 @@ -""" -A component which allows you to send data to the IBM Watson IoT Platform. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/watson_iot/ -""" +"""Support for the IBM Watson IoT Platform.""" import logging import queue import threading @@ -44,7 +39,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_INCLUDE, default={}): vol.Schema({ vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids, vol.Optional(CONF_DOMAINS, default=[]): - vol.All(cv.ensure_list, [cv.string]) + vol.All(cv.ensure_list, [cv.string]), }), })), }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index b9cb300ee21..d479725657b 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -10,7 +10,8 @@ import logging from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.temperature import display_temp as show_temp from homeassistant.const import PRECISION_WHOLE, PRECISION_TENTHS, TEMP_CELSIUS -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa +from homeassistant.helpers.config_validation import ( # noqa + PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/weather/met.py b/homeassistant/components/weather/met.py index bab6624e9d0..c905e6b6ce3 100644 --- a/homeassistant/components/weather/met.py +++ b/homeassistant/components/weather/met.py @@ -18,7 +18,7 @@ from homeassistant.helpers.event import (async_track_utc_time_change, async_call_later) import homeassistant.util.dt as dt_util -REQUIREMENTS = ['pyMetno==0.3.0'] +REQUIREMENTS = ['pyMetno==0.4.5'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/webhook.py b/homeassistant/components/webhook/__init__.py similarity index 95% rename from homeassistant/components/webhook.py rename to homeassistant/components/webhook/__init__.py index 9ec6d0298ea..59be3ab1890 100644 --- a/homeassistant/components/webhook.py +++ b/homeassistant/components/webhook/__init__.py @@ -1,8 +1,4 @@ -"""Webhooks for Home Assistant. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/webhook/ -""" +"""Webhooks for Home Assistant.""" import logging from aiohttp.web import Response @@ -14,13 +10,16 @@ from homeassistant.auth.util import generate_secret from homeassistant.components import websocket_api from homeassistant.components.http.view import HomeAssistantView -DOMAIN = 'webhook' -DEPENDENCIES = ['http'] _LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['http'] + +DOMAIN = 'webhook' URL_WEBHOOK_PATH = "/api/webhook/{webhook_id}" + WS_TYPE_LIST = 'webhook/list' + SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): WS_TYPE_LIST, }) diff --git a/homeassistant/components/weblink.py b/homeassistant/components/weblink/__init__.py similarity index 91% rename from homeassistant/components/weblink.py rename to homeassistant/components/weblink/__init__.py index cd87bd838fa..608328c659b 100644 --- a/homeassistant/components/weblink.py +++ b/homeassistant/components/weblink/__init__.py @@ -1,9 +1,4 @@ -""" -Support for links to external web pages. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/weblink/ -""" +"""Support for links to external web pages.""" import logging import voluptuous as vol diff --git a/homeassistant/components/webostv/__init__.py b/homeassistant/components/webostv/__init__.py new file mode 100644 index 00000000000..f0b3c2c5f7e --- /dev/null +++ b/homeassistant/components/webostv/__init__.py @@ -0,0 +1 @@ +"""Support for WebOS TV.""" diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/webostv/media_player.py similarity index 97% rename from homeassistant/components/media_player/webostv.py rename to homeassistant/components/webostv/media_player.py index f80e29d35a0..a6cbfbae99d 100644 --- a/homeassistant/components/media_player/webostv.py +++ b/homeassistant/components/webostv/media_player.py @@ -1,9 +1,4 @@ -""" -Support for interface with an LG webOS Smart TV. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/media_player.webostv/ -""" +"""Support for interface with an LG webOS Smart TV.""" import asyncio from datetime import timedelta import logging @@ -14,10 +9,12 @@ import voluptuous as vol from homeassistant import util from homeassistant.components.media_player import ( - MEDIA_TYPE_CHANNEL, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, MediaPlayerDevice) + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP) from homeassistant.const import ( CONF_CUSTOMIZE, CONF_FILENAME, CONF_HOST, CONF_NAME, CONF_TIMEOUT, STATE_OFF, STATE_PAUSED, STATE_PLAYING) diff --git a/homeassistant/components/notify/webostv.py b/homeassistant/components/webostv/notify.py similarity index 86% rename from homeassistant/components/notify/webostv.py rename to homeassistant/components/webostv/notify.py index 92762b03aea..5887586df65 100644 --- a/homeassistant/components/notify/webostv.py +++ b/homeassistant/components/webostv/notify.py @@ -1,9 +1,4 @@ -""" -LG WebOS TV notification service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/notify.webostv/ -""" +"""Support for LG WebOS TV notification service.""" import logging import voluptuous as vol @@ -22,7 +17,7 @@ WEBOSTV_CONFIG_FILE = 'webostv.conf' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_FILENAME, default=WEBOSTV_CONFIG_FILE): cv.string, - vol.Optional(CONF_ICON): cv.string + vol.Optional(CONF_ICON): cv.string, }) @@ -32,8 +27,8 @@ def get_service(hass, config, discovery_info=None): from pylgtv import PyLGTVPairException path = hass.config.path(config.get(CONF_FILENAME)) - client = WebOsClient(config.get(CONF_HOST), key_file_path=path, - timeout_connect=8) + client = WebOsClient( + config.get(CONF_HOST), key_file_path=path, timeout_connect=8) if not client.is_registered(): try: diff --git a/homeassistant/components/websocket_api/__init__.py b/homeassistant/components/websocket_api/__init__.py index 48c8f27996a..3734f46abb7 100644 --- a/homeassistant/components/websocket_api/__init__.py +++ b/homeassistant/components/websocket_api/__init__.py @@ -1,9 +1,4 @@ -""" -Websocket based API for Home Assistant. - -For more details about this component, please refer to the documentation at -https://developers.home-assistant.io/docs/external_api_websocket.html -""" +"""WebSocket based API for Home Assistant.""" from homeassistant.core import callback from homeassistant.loader import bind_hass diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index ff928b43873..34bb04cb394 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -9,7 +9,6 @@ from homeassistant.helpers.service import async_get_all_descriptions from . import const, decorators, messages - TYPE_CALL_SERVICE = 'call_service' TYPE_EVENT = 'event' TYPE_GET_CONFIG = 'get_config' diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index 60e2caa54ac..041aad3969e 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -31,6 +31,16 @@ class ActiveConnection: return Context() return Context(user_id=user.id) + @callback + def send_result(self, msg_id, result=None): + """Send a result message.""" + self.send_message(messages.result_message(msg_id, result)) + + @callback + def send_error(self, msg_id, code, message): + """Send a error message.""" + self.send_message(messages.error_message(msg_id, code, message)) + @callback def async_handle(self, msg): """Handle a single incoming message.""" diff --git a/homeassistant/components/wemo.py b/homeassistant/components/wemo/__init__.py similarity index 84% rename from homeassistant/components/wemo.py rename to homeassistant/components/wemo/__init__.py index 3ec9b8920c3..709b3ec8672 100644 --- a/homeassistant/components/wemo.py +++ b/homeassistant/components/wemo/__init__.py @@ -1,9 +1,4 @@ -""" -Support for WeMo device discovery. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/wemo/ -""" +"""Support for WeMo device discovery.""" import logging import requests @@ -31,7 +26,7 @@ WEMO_MODEL_DISPATCH = { 'Maker': 'switch', 'Motion': 'binary_sensor', 'Sensor': 'binary_sensor', - 'Socket': 'switch' + 'Socket': 'switch', } SUBSCRIPTION_REGISTRY = None @@ -68,8 +63,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_STATIC, default=[]): vol.Schema([ vol.All(cv.string, coerce_host_port) ]), - vol.Optional(CONF_DISCOVERY, - default=DEFAULT_DISCOVERY): cv.boolean + vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY): cv.boolean, }), }, extra=vol.ALLOW_EXTRA) @@ -115,17 +109,17 @@ def setup(hass, config): # Only register a device once if serial in KNOWN_DEVICES: - _LOGGER.debug('Ignoring known device %s %s', - service, discovery_info) + _LOGGER.debug( + "Ignoring known device %s %s", service, discovery_info) return - _LOGGER.debug('Discovered unique WeMo device: %s', serial) + _LOGGER.debug("Discovered unique WeMo device: %s", serial) KNOWN_DEVICES.append(serial) component = WEMO_MODEL_DISPATCH.get(model_name, 'switch') - discovery.load_platform(hass, component, DOMAIN, - discovery_info, config) + discovery.load_platform( + hass, component, DOMAIN, discovery_info, config) discovery.listen(hass, SERVICE_WEMO, discovery_dispatch) @@ -146,7 +140,7 @@ def setup(hass, config): device = pywemo.discovery.device_from_description(url, None) except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as err: - _LOGGER.error('Unable to access WeMo at %s (%s)', url, err) + _LOGGER.error("Unable to access WeMo at %s (%s)", url, err) continue if not [d[1] for d in devices @@ -162,8 +156,8 @@ def setup(hass, config): device)) for url, device in devices: - _LOGGER.debug('Adding WeMo device at %s:%i', - device.host, device.port) + _LOGGER.debug( + "Adding WeMo device at %s:%i", device.host, device.port) discovery_info = { 'model_name': device.model_name, @@ -176,7 +170,6 @@ def setup(hass, config): _LOGGER.debug("WeMo device discovery has finished") - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, - discover_wemo_devices) + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, discover_wemo_devices) return True diff --git a/homeassistant/components/binary_sensor/wemo.py b/homeassistant/components/wemo/binary_sensor.py similarity index 96% rename from homeassistant/components/binary_sensor/wemo.py rename to homeassistant/components/wemo/binary_sensor.py index e44cbb31e66..d6c1ad721b9 100644 --- a/homeassistant/components/binary_sensor/wemo.py +++ b/homeassistant/components/wemo/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for WeMo sensors. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.wemo/ -""" +"""Support for WeMo binary sensors.""" import asyncio import logging diff --git a/homeassistant/components/fan/wemo.py b/homeassistant/components/wemo/fan.py similarity index 98% rename from homeassistant/components/fan/wemo.py rename to homeassistant/components/wemo/fan.py index fbf72185ac2..29a493bf5bc 100644 --- a/homeassistant/components/fan/wemo.py +++ b/homeassistant/components/wemo/fan.py @@ -1,9 +1,4 @@ -""" -Support for WeMo humidifier. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/fan.wemo/ -""" +"""Support for WeMo humidifier.""" import asyncio import logging from datetime import timedelta diff --git a/homeassistant/components/light/wemo.py b/homeassistant/components/wemo/light.py similarity index 98% rename from homeassistant/components/light/wemo.py rename to homeassistant/components/wemo/light.py index 38044b7a736..e0f729fb165 100644 --- a/homeassistant/components/light/wemo.py +++ b/homeassistant/components/wemo/light.py @@ -1,9 +1,4 @@ -""" -Support for Belkin WeMo lights. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/light.wemo/ -""" +"""Support for Belkin WeMo lights.""" import asyncio import logging from datetime import timedelta diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/wemo/switch.py similarity index 97% rename from homeassistant/components/switch/wemo.py rename to homeassistant/components/wemo/switch.py index 0815f307a9a..0a583e49e96 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/wemo/switch.py @@ -1,9 +1,4 @@ -""" -Support for WeMo switches. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/switch.wemo/ -""" +"""Support for WeMo switches.""" import asyncio import logging from datetime import datetime, timedelta @@ -47,7 +42,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device = discovery.device_from_description(location, mac) except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as err: - _LOGGER.error('Unable to access %s (%s)', location, err) + _LOGGER.error("Unable to access %s (%s)", location, err) raise PlatformNotReady if device: diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index 3cedb0b126b..2b03d7711ac 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Wink hubs. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/wink/ -""" +"""Support for Wink hubs.""" from datetime import timedelta import json import logging @@ -56,7 +51,7 @@ USER_AGENT = "Manufacturer/Home-Assistant{} python/3 Wink/3".format( DEFAULT_CONFIG = { 'client_id': 'CLIENT_ID_HERE', - 'client_secret': 'CLIENT_SECRET_HERE' + 'client_secret': 'CLIENT_SECRET_HERE', } SERVICE_ADD_NEW_DEVICES = 'pull_newly_added_devices_from_wink' @@ -115,42 +110,42 @@ CONFIG_SCHEMA = vol.Schema({ RENAME_DEVICE_SCHEMA = vol.Schema({ vol.Required(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_NAME): cv.string + vol.Required(ATTR_NAME): cv.string, }, extra=vol.ALLOW_EXTRA) DELETE_DEVICE_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_ids + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, }, extra=vol.ALLOW_EXTRA) SET_PAIRING_MODE_SCHEMA = vol.Schema({ vol.Required(ATTR_HUB_NAME): cv.string, vol.Required(ATTR_PAIRING_MODE): cv.string, - vol.Optional(ATTR_KIDDE_RADIO_CODE): cv.string + vol.Optional(ATTR_KIDDE_RADIO_CODE): cv.string, }, extra=vol.ALLOW_EXTRA) SET_VOLUME_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_VOLUME): vol.In(VOLUMES) + vol.Required(ATTR_VOLUME): vol.In(VOLUMES), }) SET_SIREN_TONE_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_TONE): vol.In(TONES) + vol.Required(ATTR_TONE): vol.In(TONES), }) SET_CHIME_MODE_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_TONE): vol.In(CHIME_TONES) + vol.Required(ATTR_TONE): vol.In(CHIME_TONES), }) SET_AUTO_SHUTOFF_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_AUTO_SHUTOFF): vol.In(AUTO_SHUTOFF_TIMES) + vol.Required(ATTR_AUTO_SHUTOFF): vol.In(AUTO_SHUTOFF_TIMES), }) SET_STROBE_ENABLED_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_ENABLED): cv.boolean + vol.Required(ATTR_ENABLED): cv.boolean, }) ENABLED_SIREN_SCHEMA = vol.Schema({ @@ -166,13 +161,13 @@ DIAL_CONFIG_SCHEMA = vol.Schema({ vol.Optional(ATTR_MAX_POSITION): cv.positive_int, vol.Optional(ATTR_ROTATION): vol.In(ROTATIONS), vol.Optional(ATTR_SCALE): vol.In(SCALES), - vol.Optional(ATTR_TICKS): cv.positive_int + vol.Optional(ATTR_TICKS): cv.positive_int, }) DIAL_STATE_SCHEMA = vol.Schema({ vol.Required(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_VALUE): vol.Coerce(int), - vol.Optional(ATTR_LABELS): cv.ensure_list(cv.string) + vol.Optional(ATTR_LABELS): cv.ensure_list(cv.string), }) WINK_COMPONENTS = [ diff --git a/homeassistant/components/alarm_control_panel/wink.py b/homeassistant/components/wink/alarm_control_panel.py similarity index 92% rename from homeassistant/components/alarm_control_panel/wink.py rename to homeassistant/components/wink/alarm_control_panel.py index b2ae3578133..594198ddd12 100644 --- a/homeassistant/components/alarm_control_panel/wink.py +++ b/homeassistant/components/wink/alarm_control_panel.py @@ -1,9 +1,4 @@ -""" -Interfaces with Wink Cameras. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/alarm_control_panel.wink/ -""" +"""Support Wink alarm control panels.""" import logging import homeassistant.components.alarm_control_panel as alarm diff --git a/homeassistant/components/binary_sensor/wink.py b/homeassistant/components/wink/binary_sensor.py similarity index 97% rename from homeassistant/components/binary_sensor/wink.py rename to homeassistant/components/wink/binary_sensor.py index a950289789e..12e1b557a73 100644 --- a/homeassistant/components/binary_sensor/wink.py +++ b/homeassistant/components/wink/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for Wink binary sensors. - -For more details about this platform, please refer to the documentation at -at https://home-assistant.io/components/binary_sensor.wink/ -""" +"""Support for Wink binary sensors.""" import logging from homeassistant.components.binary_sensor import BinarySensorDevice diff --git a/homeassistant/components/climate/wink.py b/homeassistant/components/wink/climate.py similarity index 98% rename from homeassistant/components/climate/wink.py rename to homeassistant/components/wink/climate.py index 7e5230ba3c7..8d946bf03df 100644 --- a/homeassistant/components/climate/wink.py +++ b/homeassistant/components/wink/climate.py @@ -1,9 +1,4 @@ -""" -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/ -""" +"""Support for Wink thermostats and Air Conditioners.""" import logging from homeassistant.components.climate import ( diff --git a/homeassistant/components/cover/wink.py b/homeassistant/components/wink/cover.py similarity index 92% rename from homeassistant/components/cover/wink.py rename to homeassistant/components/wink/cover.py index 3cf9c753e3a..19ff792592f 100644 --- a/homeassistant/components/cover/wink.py +++ b/homeassistant/components/wink/cover.py @@ -1,9 +1,4 @@ -""" -Support for Wink Covers. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/cover.wink/ -""" +"""Support for Wink covers.""" from homeassistant.components.cover import CoverDevice, ATTR_POSITION from homeassistant.components.wink import WinkDevice, DOMAIN diff --git a/homeassistant/components/fan/wink.py b/homeassistant/components/wink/fan.py similarity index 95% rename from homeassistant/components/fan/wink.py rename to homeassistant/components/wink/fan.py index eca985a8d1e..5bc6e03c49b 100644 --- a/homeassistant/components/fan/wink.py +++ b/homeassistant/components/wink/fan.py @@ -1,9 +1,4 @@ -""" -Support for Wink fans. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/fan.wink/ -""" +"""Support for Wink fans.""" import logging from homeassistant.components.fan import ( diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/wink/light.py similarity index 95% rename from homeassistant/components/light/wink.py rename to homeassistant/components/wink/light.py index 96c8f20679e..14a983154f8 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/wink/light.py @@ -1,10 +1,4 @@ -""" -Support for Wink lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.wink/ -""" - +"""Support for Wink lights.""" from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR, Light) diff --git a/homeassistant/components/lock/wink.py b/homeassistant/components/wink/lock.py similarity index 97% rename from homeassistant/components/lock/wink.py rename to homeassistant/components/wink/lock.py index 68cc7a79ae6..0ef4f30dc5a 100644 --- a/homeassistant/components/lock/wink.py +++ b/homeassistant/components/wink/lock.py @@ -1,9 +1,4 @@ -""" -Support for Wink locks. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/lock.wink/ -""" +"""Support for Wink locks.""" import logging import voluptuous as vol diff --git a/homeassistant/components/scene/wink.py b/homeassistant/components/wink/scene.py similarity index 86% rename from homeassistant/components/scene/wink.py rename to homeassistant/components/wink/scene.py index 35db96c3b8b..d05fe58a427 100644 --- a/homeassistant/components/scene/wink.py +++ b/homeassistant/components/wink/scene.py @@ -1,9 +1,4 @@ -""" -Support for Wink scenes. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/scene.wink/ -""" +"""Support for Wink scenes.""" import logging from homeassistant.components.scene import Scene diff --git a/homeassistant/components/sensor/wink.py b/homeassistant/components/wink/sensor.py similarity index 87% rename from homeassistant/components/sensor/wink.py rename to homeassistant/components/wink/sensor.py index 8c2abc0f875..ab61769c94d 100644 --- a/homeassistant/components/sensor/wink.py +++ b/homeassistant/components/wink/sensor.py @@ -1,14 +1,8 @@ -""" -Support for Wink sensors. - -For more details about this platform, please refer to the documentation at -at https://home-assistant.io/components/sensor.wink/ -""" +"""Support for Wink sensors.""" import logging from homeassistant.components.wink import DOMAIN, WinkDevice from homeassistant.const import TEMP_CELSIUS -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -47,7 +41,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.info("Device is not a sensor") -class WinkSensorDevice(WinkDevice, Entity): +class WinkSensorDevice(WinkDevice): """Representation of a Wink sensor.""" def __init__(self, wink, hass): @@ -87,3 +81,14 @@ class WinkSensorDevice(WinkDevice, Entity): def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement + + @property + def device_state_attributes(self): + """Return the state attributes.""" + super_attrs = super().device_state_attributes + try: + super_attrs['egg_times'] = self.wink.eggs() + except AttributeError: + # Ignore error, this sensor isn't an eggminder + pass + return super_attrs diff --git a/homeassistant/components/switch/wink.py b/homeassistant/components/wink/switch.py similarity index 92% rename from homeassistant/components/switch/wink.py rename to homeassistant/components/wink/switch.py index 9dea93488af..cd55026879a 100644 --- a/homeassistant/components/switch/wink.py +++ b/homeassistant/components/wink/switch.py @@ -1,9 +1,4 @@ -""" -Support for Wink switches. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.wink/ -""" +"""Support for Wink switches.""" import logging from homeassistant.components.wink import DOMAIN, WinkDevice diff --git a/homeassistant/components/water_heater/wink.py b/homeassistant/components/wink/water_heater.py similarity index 95% rename from homeassistant/components/water_heater/wink.py rename to homeassistant/components/wink/water_heater.py index a840baf980a..34cd86a50f4 100644 --- a/homeassistant/components/water_heater/wink.py +++ b/homeassistant/components/wink/water_heater.py @@ -1,9 +1,4 @@ -""" -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/ -""" +"""Support for Wink water heaters.""" import logging from homeassistant.components.water_heater import ( diff --git a/homeassistant/components/wirelesstag.py b/homeassistant/components/wirelesstag/__init__.py similarity index 95% rename from homeassistant/components/wirelesstag.py rename to homeassistant/components/wirelesstag/__init__.py index 77b4c48b41b..28c8cb4d515 100644 --- a/homeassistant/components/wirelesstag.py +++ b/homeassistant/components/wirelesstag/__init__.py @@ -1,10 +1,4 @@ -""" -Wireless Sensor Tags platform support. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/wirelesstag/ -""" - +"""Support for Wireless Sensor Tags.""" import logging from requests.exceptions import HTTPError, ConnectTimeout @@ -22,11 +16,11 @@ REQUIREMENTS = ['wirelesstagpy==0.4.0'] _LOGGER = logging.getLogger(__name__) -# strength of signal in dBm +# Strength of signal in dBm ATTR_TAG_SIGNAL_STRENGTH = 'signal_strength' -# indicates if tag is out of range or not +# Indicates if tag is out of range or not ATTR_TAG_OUT_OF_RANGE = 'out_of_range' -# number in percents from max power of tag receiver +# Number in percents from max power of tag receiver ATTR_TAG_POWER_CONSUMPTION = 'power_consumption' @@ -36,11 +30,11 @@ NOTIFICATION_TITLE = "Wireless Sensor Tag Setup" DOMAIN = 'wirelesstag' DEFAULT_ENTITY_NAMESPACE = 'wirelesstag' -# template for signal - first parameter is tag_id, +# Template for signal - first parameter is tag_id, # second, tag manager mac address SIGNAL_TAG_UPDATE = 'wirelesstag.tag_info_updated_{}_{}' -# template for signal - tag_id, sensor type and +# Template for signal - tag_id, sensor type and # tag manager mac address SIGNAL_BINARY_EVENT_UPDATE = 'wirelesstag.binary_event_updated_{}_{}_{}' diff --git a/homeassistant/components/binary_sensor/wirelesstag.py b/homeassistant/components/wirelesstag/binary_sensor.py similarity index 95% rename from homeassistant/components/binary_sensor/wirelesstag.py rename to homeassistant/components/wirelesstag/binary_sensor.py index 190b408abf3..6f2c24a7788 100644 --- a/homeassistant/components/binary_sensor/wirelesstag.py +++ b/homeassistant/components/wirelesstag/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Binary sensor support for Wireless Sensor Tags. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.wirelesstag/ -""" +"""Binary sensor support for Wireless Sensor Tags.""" import logging import voluptuous as vol diff --git a/homeassistant/components/sensor/wirelesstag.py b/homeassistant/components/wirelesstag/sensor.py similarity index 86% rename from homeassistant/components/sensor/wirelesstag.py rename to homeassistant/components/wirelesstag/sensor.py index eb9ce297065..3703e214d83 100644 --- a/homeassistant/components/sensor/wirelesstag.py +++ b/homeassistant/components/wirelesstag/sensor.py @@ -1,10 +1,4 @@ -""" -Sensor support for Wireless Sensor Tags platform. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.wirelesstag/ -""" - +"""Sensor support for Wireless Sensor Tags platform.""" import logging import voluptuous as vol @@ -32,7 +26,7 @@ SENSOR_TYPES = [ SENSOR_TEMPERATURE, SENSOR_HUMIDITY, SENSOR_MOISTURE, - SENSOR_LIGHT + SENSOR_LIGHT, ] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -69,9 +63,9 @@ class WirelessTagSensor(WirelessTagBaseSensor): # sensor.wirelesstag_bedroom_temperature # and not as sensor.bedroom for temperature and # sensor.bedroom_2 for humidity - self._entity_id = '{}.{}_{}_{}'.format('sensor', WIRELESSTAG_DOMAIN, - self.underscored_name, - self._sensor_type) + self._entity_id = '{}.{}_{}_{}'.format( + 'sensor', WIRELESSTAG_DOMAIN, self.underscored_name, + self._sensor_type) async def async_added_to_hass(self): """Register callbacks.""" @@ -118,8 +112,8 @@ class WirelessTagSensor(WirelessTagBaseSensor): @callback def _update_tag_info_callback(self, event): """Handle push notification sent by tag manager.""" - _LOGGER.info("Entity to update state: %s event data: %s", - self, event.data) + _LOGGER.debug( + "Entity to update state: %s event data: %s", self, event.data) new_value = self._sensor.value_from_update_event(event.data) self._state = self.decorate_value(new_value) self.async_schedule_update_ha_state() diff --git a/homeassistant/components/switch/wirelesstag.py b/homeassistant/components/wirelesstag/switch.py similarity index 87% rename from homeassistant/components/switch/wirelesstag.py rename to homeassistant/components/wirelesstag/switch.py index cbe62d107da..913438e9d8c 100644 --- a/homeassistant/components/switch/wirelesstag.py +++ b/homeassistant/components/wirelesstag/switch.py @@ -1,9 +1,4 @@ -""" -Switch implementation for Wireless Sensor Tags (wirelesstag.net) platform. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.wirelesstag/ -""" +"""Switch implementation for Wireless Sensor Tags (wirelesstag.net).""" import logging import voluptuous as vol @@ -64,8 +59,8 @@ class WirelessTagSwitch(WirelessTagBaseSensor, SwitchDevice): super().__init__(api, tag) self._switch_type = switch_type self.sensor_type = SWITCH_TYPES[self._switch_type][1] - self._name = '{} {}'.format(self._tag.name, - SWITCH_TYPES[self._switch_type][0]) + self._name = '{} {}'.format( + self._tag.name, SWITCH_TYPES[self._switch_type][0]) def turn_on(self, **kwargs): """Turn on the switch.""" diff --git a/homeassistant/components/wunderlist/__init__.py b/homeassistant/components/wunderlist/__init__.py index f64d97dfc0d..d67cf089b5e 100644 --- a/homeassistant/components/wunderlist/__init__.py +++ b/homeassistant/components/wunderlist/__init__.py @@ -1,9 +1,4 @@ -""" -Component to interact with Wunderlist. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/wunderlist/ -""" +"""Support to interact with Wunderlist.""" import logging import voluptuous as vol @@ -25,7 +20,7 @@ CONF_STARRED = 'starred' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_ACCESS_TOKEN): cv.string + vol.Required(CONF_ACCESS_TOKEN): cv.string, }) }, extra=vol.ALLOW_EXTRA) @@ -35,7 +30,7 @@ SERVICE_CREATE_TASK = 'create_task' SERVICE_SCHEMA_CREATE_TASK = vol.Schema({ vol.Required(CONF_LIST_NAME): cv.string, vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_STARRED): cv.boolean + vol.Optional(CONF_STARRED): cv.boolean, }) diff --git a/homeassistant/components/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara/__init__.py similarity index 98% rename from homeassistant/components/xiaomi_aqara.py rename to homeassistant/components/xiaomi_aqara/__init__.py index 25e7a72db90..ce943fb2c93 100644 --- a/homeassistant/components/xiaomi_aqara.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Xiaomi Gateways. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/xiaomi_aqara/ -""" +"""Support for Xiaomi Gateways.""" import logging from datetime import timedelta @@ -324,7 +319,7 @@ def _add_gateway_to_schema(xiaomi, schema): # If the user has only 1 gateway, make it the default for services. if len(gateways) == 1: - kwargs['default'] = gateways[0] + kwargs['default'] = gateways[0].sid return schema.extend({ vol.Required(ATTR_GW_MAC, **kwargs): gateway diff --git a/homeassistant/components/binary_sensor/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara/binary_sensor.py similarity index 100% rename from homeassistant/components/binary_sensor/xiaomi_aqara.py rename to homeassistant/components/xiaomi_aqara/binary_sensor.py diff --git a/homeassistant/components/cover/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara/cover.py similarity index 100% rename from homeassistant/components/cover/xiaomi_aqara.py rename to homeassistant/components/xiaomi_aqara/cover.py diff --git a/homeassistant/components/light/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara/light.py similarity index 100% rename from homeassistant/components/light/xiaomi_aqara.py rename to homeassistant/components/xiaomi_aqara/light.py diff --git a/homeassistant/components/lock/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara/lock.py similarity index 90% rename from homeassistant/components/lock/xiaomi_aqara.py rename to homeassistant/components/xiaomi_aqara/lock.py index 15415a73284..f19492664b1 100644 --- a/homeassistant/components/lock/xiaomi_aqara.py +++ b/homeassistant/components/xiaomi_aqara/lock.py @@ -1,9 +1,4 @@ -""" -Support for Xiaomi Aqara Lock. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/lock.xiaomi_aqara/ -""" +"""Support for Xiaomi Aqara locks.""" import logging from homeassistant.components.xiaomi_aqara import (PY_XIAOMI_GATEWAY, XiaomiDevice) @@ -24,8 +19,8 @@ ATTR_VERIFIED_WRONG_TIMES = 'verified_wrong_times' UNLOCK_MAINTAIN_TIME = 5 -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Perform the setup for Xiaomi devices.""" devices = [] diff --git a/homeassistant/components/sensor/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara/sensor.py similarity index 99% rename from homeassistant/components/sensor/xiaomi_aqara.py rename to homeassistant/components/xiaomi_aqara/sensor.py index 1859b7be16a..133814e216e 100644 --- a/homeassistant/components/sensor/xiaomi_aqara.py +++ b/homeassistant/components/xiaomi_aqara/sensor.py @@ -1,4 +1,4 @@ -"""Support for Xiaomi aqara sensors.""" +"""Support for Xiaomi Aqara sensors.""" import logging from homeassistant.components.xiaomi_aqara import (PY_XIAOMI_GATEWAY, diff --git a/homeassistant/components/xiaomi_aqara/services.yaml b/homeassistant/components/xiaomi_aqara/services.yaml new file mode 100644 index 00000000000..0c5b89dc2cb --- /dev/null +++ b/homeassistant/components/xiaomi_aqara/services.yaml @@ -0,0 +1,22 @@ +add_device: + description: Enables the join permission of the Xiaomi Aqara Gateway for 30 seconds. + A new device can be added afterwards by pressing the pairing button once. + fields: + gw_mac: {description: MAC address of the Xiaomi Aqara Gateway., example: 34ce00880088} +play_ringtone: + description: Play a specific ringtone. The version of the gateway firmware must + be 1.4.1_145 at least. + fields: + gw_mac: {description: MAC address of the Xiaomi Aqara Gateway., example: 34ce00880088} + ringtone_id: {description: One of the allowed ringtone ids., example: 8} + ringtone_vol: {description: The volume in percent., example: 30} +remove_device: + description: Removes a specific device. The removal is required if a device shall + be paired with another gateway. + fields: + device_id: {description: Hardware address of the device to remove., example: 158d0000000000} + gw_mac: {description: MAC address of the Xiaomi Aqara Gateway., example: 34ce00880088} +stop_ringtone: + description: Stops a playing ringtone immediately. + fields: + gw_mac: {description: MAC address of the Xiaomi Aqara Gateway., example: 34ce00880088} diff --git a/homeassistant/components/switch/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara/switch.py similarity index 68% rename from homeassistant/components/switch/xiaomi_aqara.py rename to homeassistant/components/xiaomi_aqara/switch.py index 166f51fb091..c3cde8ede6d 100644 --- a/homeassistant/components/switch/xiaomi_aqara.py +++ b/homeassistant/components/xiaomi_aqara/switch.py @@ -1,4 +1,4 @@ -"""Support for Xiaomi aqara binary sensors.""" +"""Support for Xiaomi Aqara binary sensors.""" import logging from homeassistant.components.switch import SwitchDevice @@ -31,39 +31,33 @@ def setup_platform(hass, config, add_entities, discovery_info=None): data_key = 'status' else: data_key = 'channel_0' - devices.append(XiaomiGenericSwitch(device, "Plug", data_key, - True, gateway)) + devices.append(XiaomiGenericSwitch( + device, "Plug", data_key, True, gateway)) elif model in ['ctrl_neutral1', 'ctrl_neutral1.aq1']: - devices.append(XiaomiGenericSwitch(device, 'Wall Switch', - 'channel_0', - False, gateway)) + devices.append(XiaomiGenericSwitch( + device, 'Wall Switch', 'channel_0', False, gateway)) elif model in ['ctrl_ln1', 'ctrl_ln1.aq1']: - devices.append(XiaomiGenericSwitch(device, 'Wall Switch LN', - 'channel_0', - False, gateway)) + devices.append(XiaomiGenericSwitch( + device, 'Wall Switch LN', 'channel_0', False, gateway)) elif model in ['ctrl_neutral2', 'ctrl_neutral2.aq1']: - devices.append(XiaomiGenericSwitch(device, 'Wall Switch Left', - 'channel_0', - False, gateway)) - devices.append(XiaomiGenericSwitch(device, 'Wall Switch Right', - 'channel_1', - False, gateway)) + devices.append(XiaomiGenericSwitch( + device, 'Wall Switch Left', 'channel_0', False, gateway)) + devices.append(XiaomiGenericSwitch( + device, 'Wall Switch Right', 'channel_1', False, gateway)) elif model in ['ctrl_ln2', 'ctrl_ln2.aq1']: - devices.append(XiaomiGenericSwitch(device, - 'Wall Switch LN Left', - 'channel_0', - False, gateway)) - devices.append(XiaomiGenericSwitch(device, - 'Wall Switch LN Right', - 'channel_1', - False, gateway)) + devices.append(XiaomiGenericSwitch( + device, 'Wall Switch LN Left', 'channel_0', False, + gateway)) + devices.append(XiaomiGenericSwitch( + device, 'Wall Switch LN Right', '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', - data_key, True, gateway)) + devices.append(XiaomiGenericSwitch( + device, 'Wall Plug', data_key, True, gateway)) add_entities(devices) @@ -96,9 +90,11 @@ class XiaomiGenericSwitch(XiaomiDevice, SwitchDevice): def device_state_attributes(self): """Return the state attributes.""" if self._supports_power_consumption: - attrs = {ATTR_IN_USE: self._in_use, - ATTR_LOAD_POWER: self._load_power, - ATTR_POWER_CONSUMED: self._power_consumed} + attrs = { + ATTR_IN_USE: self._in_use, + ATTR_LOAD_POWER: self._load_power, + ATTR_POWER_CONSUMED: self._power_consumed, + } else: attrs = {} attrs.update(super().device_state_attributes) diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py new file mode 100644 index 00000000000..9abc871b9b4 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -0,0 +1 @@ +"""Support for Xiaomi Miio.""" diff --git a/homeassistant/components/device_tracker/xiaomi_miio.py b/homeassistant/components/xiaomi_miio/device_tracker.py similarity index 92% rename from homeassistant/components/device_tracker/xiaomi_miio.py rename to homeassistant/components/xiaomi_miio/device_tracker.py index c5c6ebcbc35..1aec3647d61 100644 --- a/homeassistant/components/device_tracker/xiaomi_miio.py +++ b/homeassistant/components/xiaomi_miio/device_tracker.py @@ -1,9 +1,4 @@ -""" -Support for Xiaomi Mi WiFi Repeater 2. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/device_tracker.xiaomi_miio/ -""" +"""Support for Xiaomi Mi WiFi Repeater 2.""" import logging import voluptuous as vol diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/xiaomi_miio/fan.py similarity index 99% rename from homeassistant/components/fan/xiaomi_miio.py rename to homeassistant/components/xiaomi_miio/fan.py index 2e0b1657d23..c4cfa6bbe6b 100644 --- a/homeassistant/components/fan/xiaomi_miio.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -1,9 +1,4 @@ -""" -Support for Xiaomi Mi Air Purifier and Xiaomi Mi Air Humidifier. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/fan.xiaomi_miio/ -""" +"""Support for Xiaomi Mi Air Purifier and Xiaomi Mi Air Humidifier.""" import asyncio from enum import Enum from functools import partial diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/xiaomi_miio/light.py similarity index 99% rename from homeassistant/components/light/xiaomi_miio.py rename to homeassistant/components/xiaomi_miio/light.py index 62433ca9f97..3ae6bfe6cd7 100644 --- a/homeassistant/components/light/xiaomi_miio.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -1,11 +1,4 @@ -""" -Support for Xiaomi Philips Lights. - -LED Ball, Candle, Downlight, Ceiling, Eyecare 2, Bedside & Desklamp Lamp. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/light.xiaomi_miio/ -""" +"""Support for Xiaomi Philips Lights.""" import asyncio import datetime from datetime import timedelta diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/xiaomi_miio/remote.py similarity index 96% rename from homeassistant/components/remote/xiaomi_miio.py rename to homeassistant/components/xiaomi_miio/remote.py index c8ffd043321..1c4ae85a0d8 100644 --- a/homeassistant/components/remote/xiaomi_miio.py +++ b/homeassistant/components/xiaomi_miio/remote.py @@ -1,9 +1,4 @@ -""" -Support for the Xiaomi IR Remote (Chuangmi IR). - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/remote.xiaomi_miio/ -""" +"""Support for the Xiaomi IR Remote (Chuangmi IR).""" import asyncio import logging import time @@ -37,8 +32,7 @@ DEFAULT_SLOT = 1 LEARN_COMMAND_SCHEMA = vol.Schema({ vol.Required(ATTR_ENTITY_ID): vol.All(str), - vol.Optional(CONF_TIMEOUT, default=10): - vol.All(int, vol.Range(min=0)), + vol.Optional(CONF_TIMEOUT, default=10): vol.All(int, vol.Range(min=0)), vol.Optional(CONF_SLOT, default=1): vol.All(int, vol.Range(min=1, max=1000000)), }) diff --git a/homeassistant/components/sensor/xiaomi_miio.py b/homeassistant/components/xiaomi_miio/sensor.py similarity index 96% rename from homeassistant/components/sensor/xiaomi_miio.py rename to homeassistant/components/xiaomi_miio/sensor.py index ef5ed1d5c38..bd5f8642e54 100644 --- a/homeassistant/components/sensor/xiaomi_miio.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -1,9 +1,4 @@ -""" -Support for Xiaomi Mi Air Quality Monitor (PM2.5). - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/sensor.xiaomi_miio/ -""" +"""Support for Xiaomi Mi Air Quality Monitor (PM2.5).""" import logging import voluptuous as vol diff --git a/homeassistant/components/switch/xiaomi_miio.py b/homeassistant/components/xiaomi_miio/switch.py similarity index 98% rename from homeassistant/components/switch/xiaomi_miio.py rename to homeassistant/components/xiaomi_miio/switch.py index 4ead90ca4ec..eb7f09f95e6 100644 --- a/homeassistant/components/switch/xiaomi_miio.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -1,9 +1,4 @@ -""" -Support for Xiaomi Smart WiFi Socket and Smart Power Strip. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/switch.xiaomi_miio/ -""" +"""Support for Xiaomi Smart WiFi Socket and Smart Power Strip.""" import asyncio from functools import partial import logging diff --git a/homeassistant/components/vacuum/xiaomi_miio.py b/homeassistant/components/xiaomi_miio/vacuum.py similarity index 97% rename from homeassistant/components/vacuum/xiaomi_miio.py rename to homeassistant/components/xiaomi_miio/vacuum.py index 943b487857f..82a92dd317c 100644 --- a/homeassistant/components/vacuum/xiaomi_miio.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -1,9 +1,4 @@ -""" -Support for the Xiaomi vacuum cleaner robot. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/vacuum.xiaomi_miio/ -""" +"""Support for the Xiaomi vacuum cleaner robot.""" import asyncio from functools import partial import logging @@ -287,9 +282,8 @@ class MiroboVacuum(StateVacuumDevice): async def async_pause(self): """Pause the cleaning task.""" - if self.state == STATE_CLEANING: - await self._try_command( - "Unable to set start/pause: %s", self._vacuum.pause) + await self._try_command( + "Unable to set start/pause: %s", self._vacuum.pause) async def async_stop(self, **kwargs): """Stop the vacuum cleaner.""" diff --git a/homeassistant/components/xs1/__init__.py b/homeassistant/components/xs1/__init__.py new file mode 100644 index 00000000000..f67eb8fd15a --- /dev/null +++ b/homeassistant/components/xs1/__init__.py @@ -0,0 +1,108 @@ +"""Support for the EZcontrol XS1 gateway.""" +import asyncio +from functools import partial +import logging + +import voluptuous as vol + +from homeassistant.const import ( + CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_SSL, CONF_USERNAME) +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['xs1-api-client==2.3.5'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'xs1' +ACTUATORS = 'actuators' +SENSORS = 'sensors' + +# define configuration parameters +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_PORT, default=80): cv.string, + vol.Optional(CONF_SSL, default=False): cv.boolean, + vol.Optional(CONF_USERNAME): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + +XS1_COMPONENTS = [ + 'climate', + 'sensor', + 'switch', +] + +# Lock used to limit the amount of concurrent update requests +# as the XS1 Gateway can only handle a very +# small amount of concurrent requests +UPDATE_LOCK = asyncio.Lock() + + +def _create_controller_api(host, port, ssl, user, password): + """Create an api instance to use for communication.""" + import xs1_api_client + + try: + return xs1_api_client.XS1( + host=host, port=port, ssl=ssl, user=user, password=password) + except ConnectionError as error: + _LOGGER.error("Failed to create XS1 API client " + "because of a connection error: %s", error) + return None + + +async def async_setup(hass, config): + """Set up XS1 Component.""" + _LOGGER.debug("Initializing XS1") + + host = config[DOMAIN][CONF_HOST] + port = config[DOMAIN][CONF_PORT] + ssl = config[DOMAIN][CONF_SSL] + user = config[DOMAIN].get(CONF_USERNAME) + password = config[DOMAIN].get(CONF_PASSWORD) + + # initialize XS1 API + xs1 = await hass.async_add_executor_job( + partial(_create_controller_api, host, port, ssl, user, password)) + if xs1 is None: + return False + + _LOGGER.debug( + "Establishing connection to XS1 gateway and retrieving data...") + + hass.data[DOMAIN] = {} + + actuators = await hass.async_add_executor_job( + partial(xs1.get_all_actuators, enabled=True)) + sensors = await hass.async_add_executor_job( + partial(xs1.get_all_sensors, enabled=True)) + + hass.data[DOMAIN][ACTUATORS] = actuators + hass.data[DOMAIN][SENSORS] = sensors + + _LOGGER.debug("Loading components for XS1 platform...") + # Load components for supported devices + for component in XS1_COMPONENTS: + hass.async_create_task( + discovery.async_load_platform( + hass, component, DOMAIN, {}, config)) + + return True + + +class XS1DeviceEntity(Entity): + """Representation of a base XS1 device.""" + + def __init__(self, device): + """Initialize the XS1 device.""" + self.device = device + + async def async_update(self): + """Retrieve latest device state.""" + async with UPDATE_LOCK: + await self.hass.async_add_executor_job( + partial(self.device.update)) diff --git a/homeassistant/components/xs1/climate.py b/homeassistant/components/xs1/climate.py new file mode 100644 index 00000000000..92a2c75895c --- /dev/null +++ b/homeassistant/components/xs1/climate.py @@ -0,0 +1,103 @@ +"""Support for XS1 climate devices.""" +from functools import partial +import logging + +from homeassistant.components.climate import ( + ATTR_TEMPERATURE, ClimateDevice, SUPPORT_TARGET_TEMPERATURE) +from homeassistant.components.xs1 import ( + ACTUATORS, DOMAIN as COMPONENT_DOMAIN, SENSORS, XS1DeviceEntity) + +DEPENDENCIES = ['xs1'] +_LOGGER = logging.getLogger(__name__) + +MIN_TEMP = 8 +MAX_TEMP = 25 + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Set up the XS1 thermostat platform.""" + from xs1_api_client.api_constants import ActuatorType + + actuators = hass.data[COMPONENT_DOMAIN][ACTUATORS] + sensors = hass.data[COMPONENT_DOMAIN][SENSORS] + + thermostat_entities = [] + for actuator in actuators: + if actuator.type() == ActuatorType.TEMPERATURE: + # Search for a matching sensor (by name) + actuator_name = actuator.name() + + matching_sensor = None + for sensor in sensors: + if actuator_name in sensor.name(): + matching_sensor = sensor + break + + thermostat_entities.append( + XS1ThermostatEntity(actuator, matching_sensor)) + + async_add_entities(thermostat_entities) + + +class XS1ThermostatEntity(XS1DeviceEntity, ClimateDevice): + """Representation of a XS1 thermostat.""" + + def __init__(self, device, sensor): + """Initialize the actuator.""" + super().__init__(device) + self.sensor = sensor + + @property + def name(self): + """Return the name of the device if any.""" + return self.device.name() + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_TARGET_TEMPERATURE + + @property + def current_temperature(self): + """Return the current temperature.""" + if self.sensor is None: + return None + + return self.sensor.value() + + @property + def temperature_unit(self): + """Return the unit of measurement used by the platform.""" + return self.device.unit() + + @property + def target_temperature(self): + """Return the current target temperature.""" + return self.device.new_value() + + @property + def min_temp(self): + """Return the minimum temperature.""" + return MIN_TEMP + + @property + def max_temp(self): + """Return the maximum temperature.""" + return MAX_TEMP + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + temp = kwargs.get(ATTR_TEMPERATURE) + + self.device.set_value(temp) + + if self.sensor is not None: + self.schedule_update_ha_state() + + async def async_update(self): + """Also update the sensor when available.""" + await super().async_update() + if self.sensor is not None: + await self.hass.async_add_executor_job( + partial(self.sensor.update)) diff --git a/homeassistant/components/xs1/sensor.py b/homeassistant/components/xs1/sensor.py new file mode 100644 index 00000000000..9de91a6b012 --- /dev/null +++ b/homeassistant/components/xs1/sensor.py @@ -0,0 +1,51 @@ +"""Support for XS1 sensors.""" +import logging + +from homeassistant.components.xs1 import ( + ACTUATORS, DOMAIN as COMPONENT_DOMAIN, SENSORS, XS1DeviceEntity) +from homeassistant.helpers.entity import Entity + +DEPENDENCIES = ['xs1'] +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Set up the XS1 sensor platform.""" + from xs1_api_client.api_constants import ActuatorType + + sensors = hass.data[COMPONENT_DOMAIN][SENSORS] + actuators = hass.data[COMPONENT_DOMAIN][ACTUATORS] + + sensor_entities = [] + for sensor in sensors: + belongs_to_climate_actuator = False + for actuator in actuators: + if actuator.type() == ActuatorType.TEMPERATURE and \ + actuator.name() in sensor.name(): + belongs_to_climate_actuator = True + break + + if not belongs_to_climate_actuator: + sensor_entities.append(XS1Sensor(sensor)) + + async_add_entities(sensor_entities) + + +class XS1Sensor(XS1DeviceEntity, Entity): + """Representation of a Sensor.""" + + @property + def name(self): + """Return the name of the sensor.""" + return self.device.name() + + @property + def state(self): + """Return the state of the sensor.""" + return self.device.value() + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self.device.unit() diff --git a/homeassistant/components/xs1/switch.py b/homeassistant/components/xs1/switch.py new file mode 100644 index 00000000000..35568587b19 --- /dev/null +++ b/homeassistant/components/xs1/switch.py @@ -0,0 +1,48 @@ +"""Support for XS1 switches.""" +import logging + +from homeassistant.components.xs1 import ( + ACTUATORS, DOMAIN as COMPONENT_DOMAIN, XS1DeviceEntity) +from homeassistant.helpers.entity import ToggleEntity + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['xs1'] + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Set up the XS1 switch platform.""" + from xs1_api_client.api_constants import ActuatorType + + actuators = hass.data[COMPONENT_DOMAIN][ACTUATORS] + + switch_entities = [] + for actuator in actuators: + if (actuator.type() == ActuatorType.SWITCH) or \ + (actuator.type() == ActuatorType.DIMMER): + switch_entities.append(XS1SwitchEntity(actuator)) + + async_add_entities(switch_entities) + + +class XS1SwitchEntity(XS1DeviceEntity, ToggleEntity): + """Representation of a XS1 switch actuator.""" + + @property + def name(self): + """Return the name of the device if any.""" + return self.device.name() + + @property + def is_on(self): + """Return true if switch is on.""" + return self.device.value() == 100 + + def turn_on(self, **kwargs): + """Turn the device on.""" + self.device.turn_on() + + def turn_off(self, **kwargs): + """Turn the device off.""" + self.device.turn_off() diff --git a/homeassistant/components/zabbix.py b/homeassistant/components/zabbix/__init__.py similarity index 87% rename from homeassistant/components/zabbix.py rename to homeassistant/components/zabbix/__init__.py index ea5a6d85d6b..f33c60b1c39 100644 --- a/homeassistant/components/zabbix.py +++ b/homeassistant/components/zabbix/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Zabbix. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zabbix/ -""" +"""Support for Zabbix.""" import logging from urllib.parse import urljoin @@ -19,17 +14,15 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_SSL = False DEFAULT_PATH = 'zabbix' - DOMAIN = 'zabbix' - CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_PASSWORD): cv.string, vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string }) }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/sensor/zabbix.py b/homeassistant/components/zabbix/sensor.py similarity index 97% rename from homeassistant/components/sensor/zabbix.py rename to homeassistant/components/zabbix/sensor.py index 7a468a9b906..ae2e70ede2c 100644 --- a/homeassistant/components/sensor/zabbix.py +++ b/homeassistant/components/zabbix/sensor.py @@ -1,9 +1,4 @@ -""" -Support for Zabbix Sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.zabbix/ -""" +"""Support for Zabbix sensors.""" import logging import voluptuous as vol diff --git a/homeassistant/components/zeroconf.py b/homeassistant/components/zeroconf/__init__.py similarity index 88% rename from homeassistant/components/zeroconf.py rename to homeassistant/components/zeroconf/__init__.py index 5d6161da904..bf743eaf370 100644 --- a/homeassistant/components/zeroconf.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -1,9 +1,4 @@ -""" -This module exposes Home Assistant via Zeroconf. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zeroconf/ -""" +"""Support for exposing Home Assistant via Zeroconf.""" import logging import socket diff --git a/homeassistant/components/zha/.translations/da.json b/homeassistant/components/zha/.translations/da.json new file mode 100644 index 00000000000..e336c14dcce --- /dev/null +++ b/homeassistant/components/zha/.translations/da.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning af ZHA." + }, + "error": { + "cannot_connect": "Kunne ikke oprette forbindelse til ZHA-enhed." + }, + "step": { + "user": { + "data": { + "radio_type": "Radio type", + "usb_path": "Sti til USB enhed" + }, + "description": "Tom", + "title": "ZHA" + } + }, + "title": "ZHA" + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/pt.json b/homeassistant/components/zha/.translations/pt.json index 0259403fabc..c1de13b5381 100644 --- a/homeassistant/components/zha/.translations/pt.json +++ b/homeassistant/components/zha/.translations/pt.json @@ -13,9 +13,9 @@ "usb_path": "Caminho do Dispositivo USB" }, "description": "Vazio", - "title": "" + "title": "ZHA" } }, - "title": "" + "title": "ZHA" } } \ No newline at end of file diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 4f9b5b04362..6c7e83689ad 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -4,6 +4,7 @@ Support for Zigbee Home Automation devices. For more details about this component, please refer to the documentation at https://home-assistant.io/components/zha/ """ +import asyncio import logging import os import types @@ -17,14 +18,15 @@ from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE # Loading the config flow file will register the flow from . import config_flow # noqa # pylint: disable=unused-import from . import api -from .core.gateway import ZHAGateway -from .const import ( +from .core import ZHAGateway +from .core.const import ( COMPONENTS, CONF_BAUDRATE, CONF_DATABASE, CONF_DEVICE_CONFIG, CONF_RADIO_TYPE, CONF_USB_PATH, DATA_ZHA, DATA_ZHA_BRIDGE_ID, DATA_ZHA_CONFIG, DATA_ZHA_CORE_COMPONENT, DATA_ZHA_DISPATCHERS, DATA_ZHA_RADIO, DEFAULT_BAUDRATE, DEFAULT_DATABASE_NAME, - DEFAULT_RADIO_TYPE, DOMAIN, RadioType, DATA_ZHA_CORE_EVENTS, - ENABLE_QUIRKS) + DEFAULT_RADIO_TYPE, DOMAIN, RadioType, DATA_ZHA_CORE_EVENTS, ENABLE_QUIRKS) +from .core.gateway import establish_device_mappings +from .core.channels.registry import populate_channel_registry REQUIREMENTS = [ 'bellows==0.7.0', @@ -87,9 +89,16 @@ async def async_setup_entry(hass, config_entry): Will automatically load components to support devices found on the network. """ + establish_device_mappings() + populate_channel_registry() + + for component in COMPONENTS: + hass.data[DATA_ZHA][component] = ( + hass.data[DATA_ZHA].get(component, {}) + ) + hass.data[DATA_ZHA] = hass.data.get(DATA_ZHA, {}) hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS] = [] - config = hass.data[DATA_ZHA].get(DATA_ZHA_CONFIG, {}) if config.get(ENABLE_QUIRKS, True): @@ -137,14 +146,34 @@ async def async_setup_entry(hass, config_entry): ClusterPersistingListener ) - application_controller = ControllerApplication(radio, database) zha_gateway = ZHAGateway(hass, config) + + # Patch handle_message until zigpy can provide an event here + def handle_message(sender, is_reply, profile, cluster, + src_ep, dst_ep, tsn, command_id, args): + """Handle message from a device.""" + if not sender.initializing and sender.ieee in zha_gateway.devices and \ + not zha_gateway.devices[sender.ieee].available: + hass.async_create_task( + zha_gateway.async_device_became_available( + sender, is_reply, profile, cluster, src_ep, dst_ep, tsn, + command_id, args + ) + ) + return sender.handle_message( + is_reply, profile, cluster, src_ep, dst_ep, tsn, command_id, args) + + application_controller = ControllerApplication(radio, database) + application_controller.handle_message = handle_message application_controller.add_listener(zha_gateway) await application_controller.startup(auto_form=True) + hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = str(application_controller.ieee) + + init_tasks = [] for device in application_controller.devices.values(): - hass.async_create_task( - zha_gateway.async_device_initialized(device, False)) + init_tasks.append(zha_gateway.async_device_initialized(device, False)) + await asyncio.gather(*init_tasks) device_registry = await \ hass.helpers.device_registry.async_get_registry() @@ -157,8 +186,6 @@ async def async_setup_entry(hass, config_entry): model=radio_description, ) - hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = str(application_controller.ieee) - for component in COMPONENTS: hass.async_create_task( hass.config_entries.async_forward_entry_setup( diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 0312a40967f..0dd6dd78400 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -9,20 +9,17 @@ import logging import voluptuous as vol from homeassistant.components import websocket_api -from homeassistant.const import ATTR_ENTITY_ID import homeassistant.helpers.config_validation as cv -from .device_entity import ZhaDeviceEntity -from .const import ( +from .core.const import ( DOMAIN, ATTR_CLUSTER_ID, ATTR_CLUSTER_TYPE, ATTR_ATTRIBUTE, ATTR_VALUE, ATTR_MANUFACTURER, ATTR_COMMAND, ATTR_COMMAND_TYPE, ATTR_ARGS, IN, OUT, - CLIENT_COMMANDS, SERVER_COMMANDS, SERVER) + CLIENT_COMMANDS, SERVER_COMMANDS, SERVER, NAME, ATTR_ENDPOINT_ID) _LOGGER = logging.getLogger(__name__) TYPE = 'type' CLIENT = 'client' ID = 'id' -NAME = 'name' RESPONSE = 'response' DEVICE_INFO = 'device_info' @@ -34,7 +31,6 @@ SERVICE_PERMIT = 'permit' SERVICE_REMOVE = 'remove' SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE = 'set_zigbee_cluster_attribute' SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND = 'issue_zigbee_cluster_command' -ZIGBEE_CLUSTER_SERVICE = 'zigbee_cluster_service' IEEE_SERVICE = 'ieee_based_service' SERVICE_SCHEMAS = { @@ -45,13 +41,9 @@ SERVICE_SCHEMAS = { IEEE_SERVICE: vol.Schema({ vol.Required(ATTR_IEEE_ADDRESS): cv.string, }), - ZIGBEE_CLUSTER_SERVICE: vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_id, - vol.Required(ATTR_CLUSTER_ID): cv.positive_int, - vol.Optional(ATTR_CLUSTER_TYPE, default=IN): cv.string - }), SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE: vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_IEEE): cv.string, + vol.Required(ATTR_ENDPOINT_ID): cv.positive_int, vol.Required(ATTR_CLUSTER_ID): cv.positive_int, vol.Optional(ATTR_CLUSTER_TYPE, default=IN): cv.string, vol.Required(ATTR_ATTRIBUTE): cv.positive_int, @@ -59,7 +51,8 @@ SERVICE_SCHEMAS = { vol.Optional(ATTR_MANUFACTURER): cv.positive_int, }), SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND: vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_IEEE): cv.string, + vol.Required(ATTR_ENDPOINT_ID): cv.positive_int, vol.Required(ATTR_CLUSTER_ID): cv.positive_int, vol.Optional(ATTR_CLUSTER_TYPE, default=IN): cv.string, vol.Required(ATTR_COMMAND): cv.positive_int, @@ -69,164 +62,56 @@ SERVICE_SCHEMAS = { }), } -WS_RECONFIGURE_NODE = 'zha/nodes/reconfigure' +WS_RECONFIGURE_NODE = 'zha/devices/reconfigure' SCHEMA_WS_RECONFIGURE_NODE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required(TYPE): WS_RECONFIGURE_NODE, vol.Required(ATTR_IEEE): str }) -WS_ENTITIES_BY_IEEE = 'zha/entities' -SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required(TYPE): WS_ENTITIES_BY_IEEE, +WS_DEVICES = 'zha/devices' +SCHEMA_WS_LIST_DEVICES = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required(TYPE): WS_DEVICES, }) -WS_ENTITY_CLUSTERS = 'zha/entities/clusters' +WS_DEVICE_CLUSTERS = 'zha/devices/clusters' SCHEMA_WS_CLUSTERS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required(TYPE): WS_ENTITY_CLUSTERS, - vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(TYPE): WS_DEVICE_CLUSTERS, vol.Required(ATTR_IEEE): str }) -WS_ENTITY_CLUSTER_ATTRIBUTES = 'zha/entities/clusters/attributes' +WS_DEVICE_CLUSTER_ATTRIBUTES = 'zha/devices/clusters/attributes' SCHEMA_WS_CLUSTER_ATTRIBUTES = \ websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required(TYPE): WS_ENTITY_CLUSTER_ATTRIBUTES, - vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(TYPE): WS_DEVICE_CLUSTER_ATTRIBUTES, vol.Required(ATTR_IEEE): str, + vol.Required(ATTR_ENDPOINT_ID): int, vol.Required(ATTR_CLUSTER_ID): int, vol.Required(ATTR_CLUSTER_TYPE): str }) -WS_READ_CLUSTER_ATTRIBUTE = 'zha/entities/clusters/attributes/value' +WS_READ_CLUSTER_ATTRIBUTE = 'zha/devices/clusters/attributes/value' SCHEMA_WS_READ_CLUSTER_ATTRIBUTE = \ websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required(TYPE): WS_READ_CLUSTER_ATTRIBUTE, - vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_IEEE): str, + vol.Required(ATTR_ENDPOINT_ID): int, vol.Required(ATTR_CLUSTER_ID): int, vol.Required(ATTR_CLUSTER_TYPE): str, vol.Required(ATTR_ATTRIBUTE): int, vol.Optional(ATTR_MANUFACTURER): object, }) -WS_ENTITY_CLUSTER_COMMANDS = 'zha/entities/clusters/commands' +WS_DEVICE_CLUSTER_COMMANDS = 'zha/devices/clusters/commands' SCHEMA_WS_CLUSTER_COMMANDS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required(TYPE): WS_ENTITY_CLUSTER_COMMANDS, - vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(TYPE): WS_DEVICE_CLUSTER_COMMANDS, vol.Required(ATTR_IEEE): str, + vol.Required(ATTR_ENDPOINT_ID): int, vol.Required(ATTR_CLUSTER_ID): int, vol.Required(ATTR_CLUSTER_TYPE): str }) -@websocket_api.async_response -async def websocket_entity_cluster_attributes(hass, connection, msg): - """Return a list of cluster attributes.""" - entity_id = msg[ATTR_ENTITY_ID] - cluster_id = msg[ATTR_CLUSTER_ID] - cluster_type = msg[ATTR_CLUSTER_TYPE] - component = hass.data.get(entity_id.split('.')[0]) - entity = component.get_entity(entity_id) - cluster_attributes = [] - if entity is not None: - res = await entity.get_cluster_attributes(cluster_id, cluster_type) - if res is not None: - for attr_id in res: - cluster_attributes.append( - { - ID: attr_id, - NAME: res[attr_id][0] - } - ) - _LOGGER.debug("Requested attributes for: %s %s %s %s", - "{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id), - "{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type), - "{}: [{}]".format(ATTR_ENTITY_ID, entity_id), - "{}: [{}]".format(RESPONSE, cluster_attributes) - ) - - connection.send_message(websocket_api.result_message( - msg[ID], - cluster_attributes - )) - - -@websocket_api.async_response -async def websocket_entity_cluster_commands(hass, connection, msg): - """Return a list of cluster commands.""" - entity_id = msg[ATTR_ENTITY_ID] - cluster_id = msg[ATTR_CLUSTER_ID] - cluster_type = msg[ATTR_CLUSTER_TYPE] - component = hass.data.get(entity_id.split('.')[0]) - entity = component.get_entity(entity_id) - cluster_commands = [] - if entity is not None: - res = await entity.get_cluster_commands(cluster_id, cluster_type) - if res is not None: - for cmd_id in res[CLIENT_COMMANDS]: - cluster_commands.append( - { - TYPE: CLIENT, - ID: cmd_id, - NAME: res[CLIENT_COMMANDS][cmd_id][0] - } - ) - for cmd_id in res[SERVER_COMMANDS]: - cluster_commands.append( - { - TYPE: SERVER, - ID: cmd_id, - NAME: res[SERVER_COMMANDS][cmd_id][0] - } - ) - _LOGGER.debug("Requested commands for: %s %s %s %s", - "{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id), - "{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type), - "{}: [{}]".format(ATTR_ENTITY_ID, entity_id), - "{}: [{}]".format(RESPONSE, cluster_commands) - ) - - connection.send_message(websocket_api.result_message( - msg[ID], - cluster_commands - )) - - -@websocket_api.async_response -async def websocket_read_zigbee_cluster_attributes(hass, connection, msg): - """Read zigbee attribute for cluster on zha entity.""" - entity_id = msg[ATTR_ENTITY_ID] - cluster_id = msg[ATTR_CLUSTER_ID] - cluster_type = msg[ATTR_CLUSTER_TYPE] - attribute = msg[ATTR_ATTRIBUTE] - component = hass.data.get(entity_id.split('.')[0]) - entity = component.get_entity(entity_id) - clusters = await entity.get_clusters() - cluster = clusters[cluster_type][cluster_id] - manufacturer = msg.get(ATTR_MANUFACTURER) or None - success = failure = None - if entity is not None: - success, failure = await cluster.read_attributes( - [attribute], - allow_cache=False, - only_cache=False, - manufacturer=manufacturer - ) - _LOGGER.debug("Read attribute for: %s %s %s %s %s %s %s", - "{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id), - "{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type), - "{}: [{}]".format(ATTR_ENTITY_ID, entity_id), - "{}: [{}]".format(ATTR_ATTRIBUTE, attribute), - "{}: [{}]".format(ATTR_MANUFACTURER, manufacturer), - "{}: [{}]".format(RESPONSE, str(success.get(attribute))), - "{}: [{}]".format('failure', failure) - ) - connection.send_message(websocket_api.result_message( - msg[ID], - str(success.get(attribute)) - )) - - -def async_load_api(hass, application_controller, listener): +def async_load_api(hass, application_controller, zha_gateway): """Set up the web socket API.""" async def permit(service): """Allow devices to join this network.""" @@ -250,17 +135,18 @@ def async_load_api(hass, application_controller, listener): async def set_zigbee_cluster_attributes(service): """Set zigbee attribute for cluster on zha entity.""" - entity_id = service.data.get(ATTR_ENTITY_ID) + ieee = service.data.get(ATTR_IEEE) + endpoint_id = service.data.get(ATTR_ENDPOINT_ID) cluster_id = service.data.get(ATTR_CLUSTER_ID) cluster_type = service.data.get(ATTR_CLUSTER_TYPE) attribute = service.data.get(ATTR_ATTRIBUTE) value = service.data.get(ATTR_VALUE) manufacturer = service.data.get(ATTR_MANUFACTURER) or None - component = hass.data.get(entity_id.split('.')[0]) - entity = component.get_entity(entity_id) + zha_device = zha_gateway.get_device(ieee) response = None - if entity is not None: - response = await entity.write_zigbe_attribute( + if zha_device is not None: + response = await zha_device.write_zigbee_attribute( + endpoint_id, cluster_id, attribute, value, @@ -270,7 +156,7 @@ def async_load_api(hass, application_controller, listener): _LOGGER.debug("Set attribute for: %s %s %s %s %s %s %s", "{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id), "{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type), - "{}: [{}]".format(ATTR_ENTITY_ID, entity_id), + "{}: [{}]".format(ATTR_ENDPOINT_ID, endpoint_id), "{}: [{}]".format(ATTR_ATTRIBUTE, attribute), "{}: [{}]".format(ATTR_VALUE, value), "{}: [{}]".format(ATTR_MANUFACTURER, manufacturer), @@ -285,18 +171,19 @@ def async_load_api(hass, application_controller, listener): async def issue_zigbee_cluster_command(service): """Issue command on zigbee cluster on zha entity.""" - entity_id = service.data.get(ATTR_ENTITY_ID) + ieee = service.data.get(ATTR_IEEE) + endpoint_id = service.data.get(ATTR_ENDPOINT_ID) cluster_id = service.data.get(ATTR_CLUSTER_ID) cluster_type = service.data.get(ATTR_CLUSTER_TYPE) command = service.data.get(ATTR_COMMAND) command_type = service.data.get(ATTR_COMMAND_TYPE) args = service.data.get(ATTR_ARGS) manufacturer = service.data.get(ATTR_MANUFACTURER) or None - component = hass.data.get(entity_id.split('.')[0]) - entity = component.get_entity(entity_id) + zha_device = zha_gateway.get_device(ieee) response = None - if entity is not None: - response = await entity.issue_cluster_command( + if zha_device is not None: + response = await zha_device.issue_cluster_command( + endpoint_id, cluster_id, command, command_type, @@ -307,7 +194,7 @@ def async_load_api(hass, application_controller, listener): _LOGGER.debug("Issue command for: %s %s %s %s %s %s %s %s", "{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id), "{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type), - "{}: [{}]".format(ATTR_ENTITY_ID, entity_id), + "{}: [{}]".format(ATTR_ENDPOINT_ID, endpoint_id), "{}: [{}]".format(ATTR_COMMAND, command), "{}: [{}]".format(ATTR_COMMAND_TYPE, command_type), "{}: [{}]".format(ATTR_ARGS, args), @@ -321,15 +208,36 @@ def async_load_api(hass, application_controller, listener): SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND ]) + @websocket_api.async_response + async def websocket_get_devices(hass, connection, msg): + """Get ZHA devices.""" + devices = [ + { + **device.device_info, + 'entities': [{ + 'entity_id': entity_ref.reference_id, + NAME: entity_ref.device_info[NAME] + } for entity_ref in zha_gateway.device_registry[device.ieee]] + } for device in zha_gateway.devices.values() + ] + + connection.send_message(websocket_api.result_message( + msg[ID], + devices + )) + + hass.components.websocket_api.async_register_command( + WS_DEVICES, websocket_get_devices, + SCHEMA_WS_LIST_DEVICES + ) + @websocket_api.async_response async def websocket_reconfigure_node(hass, connection, msg): """Reconfigure a ZHA nodes entities by its ieee address.""" ieee = msg[ATTR_IEEE] - entities = listener.get_entities_for_ieee(ieee) + device = zha_gateway.get_device(ieee) _LOGGER.debug("Reconfiguring node with ieee_address: %s", ieee) - for entity in entities: - if hasattr(entity, 'async_configure'): - hass.async_create_task(entity.async_configure()) + hass.async_create_task(device.async_configure()) hass.components.websocket_api.async_register_command( WS_RECONFIGURE_NODE, websocket_reconfigure_node, @@ -337,71 +245,163 @@ def async_load_api(hass, application_controller, listener): ) @websocket_api.async_response - async def websocket_entities_by_ieee(hass, connection, msg): - """Return a dict of all zha entities grouped by ieee.""" - entities_by_ieee = {} - for ieee, entities in listener.device_registry.items(): - ieee_string = str(ieee) - entities_by_ieee[ieee_string] = [] - for entity in entities: - if not isinstance(entity, ZhaDeviceEntity): - entities_by_ieee[ieee_string].append({ - ATTR_ENTITY_ID: entity.entity_id, - DEVICE_INFO: entity.device_info + async def websocket_device_clusters(hass, connection, msg): + """Return a list of device clusters.""" + ieee = msg[ATTR_IEEE] + zha_device = zha_gateway.get_device(ieee) + response_clusters = [] + if zha_device is not None: + clusters_by_endpoint = await zha_device.get_clusters() + for ep_id, clusters in clusters_by_endpoint.items(): + for c_id, cluster in clusters[IN].items(): + response_clusters.append({ + TYPE: IN, + ID: c_id, + NAME: cluster.__class__.__name__, + 'endpoint_id': ep_id + }) + for c_id, cluster in clusters[OUT].items(): + response_clusters.append({ + TYPE: OUT, + ID: c_id, + NAME: cluster.__class__.__name__, + 'endpoint_id': ep_id }) - connection.send_message(websocket_api.result_message( - msg[ID], - entities_by_ieee - )) - - hass.components.websocket_api.async_register_command( - WS_ENTITIES_BY_IEEE, websocket_entities_by_ieee, - SCHEMA_WS_LIST - ) - - @websocket_api.async_response - async def websocket_entity_clusters(hass, connection, msg): - """Return a list of entity clusters.""" - entity_id = msg[ATTR_ENTITY_ID] - entities = listener.get_entities_for_ieee(msg[ATTR_IEEE]) - entity = next( - ent for ent in entities if ent.entity_id == entity_id) - entity_clusters = await entity.get_clusters() - clusters = [] - - for cluster_id, cluster in entity_clusters[IN].items(): - clusters.append({ - TYPE: IN, - ID: cluster_id, - NAME: cluster.__class__.__name__ - }) - for cluster_id, cluster in entity_clusters[OUT].items(): - clusters.append({ - TYPE: OUT, - ID: cluster_id, - NAME: cluster.__class__.__name__ - }) connection.send_message(websocket_api.result_message( msg[ID], - clusters + response_clusters )) hass.components.websocket_api.async_register_command( - WS_ENTITY_CLUSTERS, websocket_entity_clusters, + WS_DEVICE_CLUSTERS, websocket_device_clusters, SCHEMA_WS_CLUSTERS ) + @websocket_api.async_response + async def websocket_device_cluster_attributes(hass, connection, msg): + """Return a list of cluster attributes.""" + ieee = msg[ATTR_IEEE] + endpoint_id = msg[ATTR_ENDPOINT_ID] + cluster_id = msg[ATTR_CLUSTER_ID] + cluster_type = msg[ATTR_CLUSTER_TYPE] + cluster_attributes = [] + zha_device = zha_gateway.get_device(ieee) + attributes = None + if zha_device is not None: + attributes = await zha_device.get_cluster_attributes( + endpoint_id, + cluster_id, + cluster_type) + if attributes is not None: + for attr_id in attributes: + cluster_attributes.append( + { + ID: attr_id, + NAME: attributes[attr_id][0] + } + ) + _LOGGER.debug("Requested attributes for: %s %s %s %s", + "{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id), + "{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type), + "{}: [{}]".format(ATTR_ENDPOINT_ID, endpoint_id), + "{}: [{}]".format(RESPONSE, cluster_attributes) + ) + + connection.send_message(websocket_api.result_message( + msg[ID], + cluster_attributes + )) + hass.components.websocket_api.async_register_command( - WS_ENTITY_CLUSTER_ATTRIBUTES, websocket_entity_cluster_attributes, + WS_DEVICE_CLUSTER_ATTRIBUTES, websocket_device_cluster_attributes, SCHEMA_WS_CLUSTER_ATTRIBUTES ) + @websocket_api.async_response + async def websocket_device_cluster_commands(hass, connection, msg): + """Return a list of cluster commands.""" + cluster_id = msg[ATTR_CLUSTER_ID] + cluster_type = msg[ATTR_CLUSTER_TYPE] + ieee = msg[ATTR_IEEE] + endpoint_id = msg[ATTR_ENDPOINT_ID] + zha_device = zha_gateway.get_device(ieee) + cluster_commands = [] + commands = None + if zha_device is not None: + commands = await zha_device.get_cluster_commands( + endpoint_id, + cluster_id, + cluster_type) + + if commands is not None: + for cmd_id in commands[CLIENT_COMMANDS]: + cluster_commands.append( + { + TYPE: CLIENT, + ID: cmd_id, + NAME: commands[CLIENT_COMMANDS][cmd_id][0] + } + ) + for cmd_id in commands[SERVER_COMMANDS]: + cluster_commands.append( + { + TYPE: SERVER, + ID: cmd_id, + NAME: commands[SERVER_COMMANDS][cmd_id][0] + } + ) + _LOGGER.debug("Requested commands for: %s %s %s %s", + "{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id), + "{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type), + "{}: [{}]".format(ATTR_ENDPOINT_ID, endpoint_id), + "{}: [{}]".format(RESPONSE, cluster_commands) + ) + + connection.send_message(websocket_api.result_message( + msg[ID], + cluster_commands + )) + hass.components.websocket_api.async_register_command( - WS_ENTITY_CLUSTER_COMMANDS, websocket_entity_cluster_commands, + WS_DEVICE_CLUSTER_COMMANDS, websocket_device_cluster_commands, SCHEMA_WS_CLUSTER_COMMANDS ) + @websocket_api.async_response + async def websocket_read_zigbee_cluster_attributes(hass, connection, msg): + """Read zigbee attribute for cluster on zha entity.""" + ieee = msg[ATTR_IEEE] + endpoint_id = msg[ATTR_ENDPOINT_ID] + cluster_id = msg[ATTR_CLUSTER_ID] + cluster_type = msg[ATTR_CLUSTER_TYPE] + attribute = msg[ATTR_ATTRIBUTE] + manufacturer = msg.get(ATTR_MANUFACTURER) or None + zha_device = zha_gateway.get_device(ieee) + success = failure = None + if zha_device is not None: + cluster = await zha_device.get_cluster( + endpoint_id, cluster_id, cluster_type=cluster_type) + success, failure = await cluster.read_attributes( + [attribute], + allow_cache=False, + only_cache=False, + manufacturer=manufacturer + ) + _LOGGER.debug("Read attribute for: %s %s %s %s %s %s %s", + "{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id), + "{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type), + "{}: [{}]".format(ATTR_ENDPOINT_ID, endpoint_id), + "{}: [{}]".format(ATTR_ATTRIBUTE, attribute), + "{}: [{}]".format(ATTR_MANUFACTURER, manufacturer), + "{}: [{}]".format(RESPONSE, str(success.get(attribute))), + "{}: [{}]".format('failure', failure) + ) + connection.send_message(websocket_api.result_message( + msg[ID], + str(success.get(attribute)) + )) + hass.components.websocket_api.async_register_command( WS_READ_CLUSTER_ATTRIBUTE, websocket_read_zigbee_cluster_attributes, SCHEMA_WS_READ_CLUSTER_ATTRIBUTE diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index d0f23ff3dd2..a46ffdd305d 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -7,16 +7,13 @@ at https://home-assistant.io/components/binary_sensor.zha/ import logging from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice -from homeassistant.const import STATE_ON from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.restore_state import RestoreEntity -from .core import helpers from .core.const import ( - DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_IMMEDIATE, ZHA_DISCOVERY_NEW) + DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, ON_OFF_CHANNEL, + LEVEL_CHANNEL, ZONE_CHANNEL, SIGNAL_ATTR_UPDATED, SIGNAL_MOVE_LEVEL, + SIGNAL_SET_LEVEL, ATTRIBUTE_CHANNEL, UNKNOWN, OPENING, ZONE, OCCUPANCY, + ATTR_LEVEL, SENSOR_TYPE) from .entity import ZhaEntity -from .core.listeners import ( - OnOffListener, LevelListener -) _LOGGER = logging.getLogger(__name__) @@ -31,7 +28,20 @@ CLASS_MAPPING = { 0x002b: 'gas', 0x002d: 'vibration', } -DEVICE_CLASS_OCCUPANCY = 'occupancy' + + +async def get_ias_device_class(channel): + """Get the HA device class from the channel.""" + zone_type = await channel.get_attribute_value('zone_type') + return CLASS_MAPPING.get(zone_type) + + +DEVICE_CLASS_REGISTRY = { + UNKNOWN: None, + OPENING: OPENING, + ZONE: get_ias_device_class, + OCCUPANCY: OCCUPANCY, +} async def async_setup_platform(hass, config, async_add_entities, @@ -60,249 +70,60 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def _async_setup_entities(hass, config_entry, async_add_entities, discovery_infos): """Set up the ZHA binary sensors.""" - from zigpy.zcl.clusters.general import OnOff - from zigpy.zcl.clusters.measurement import OccupancySensing - from zigpy.zcl.clusters.security import IasZone - entities = [] for discovery_info in discovery_infos: - if IasZone.cluster_id in discovery_info['in_clusters']: - entities.append(await _async_setup_iaszone(discovery_info)) - elif OccupancySensing.cluster_id in discovery_info['in_clusters']: - entities.append( - BinarySensor(DEVICE_CLASS_OCCUPANCY, **discovery_info)) - elif OnOff.cluster_id in discovery_info['out_clusters']: - entities.append(Remote(**discovery_info)) + entities.append(BinarySensor(**discovery_info)) async_add_entities(entities, update_before_add=True) -async def _async_setup_iaszone(discovery_info): - device_class = None - from zigpy.zcl.clusters.security import IasZone - cluster = discovery_info['in_clusters'][IasZone.cluster_id] - - try: - zone_type = await cluster['zone_type'] - device_class = CLASS_MAPPING.get(zone_type, None) - except Exception: # pylint: disable=broad-except - # If we fail to read from the device, use a non-specific class - pass - - return IasZoneSensor(device_class, **discovery_info) - - -class IasZoneSensor(RestoreEntity, ZhaEntity, BinarySensorDevice): - """The IasZoneSensor Binary Sensor.""" - - _domain = DOMAIN - - def __init__(self, device_class, **kwargs): - """Initialize the ZHA binary sensor.""" - super().__init__(**kwargs) - self._device_class = device_class - from zigpy.zcl.clusters.security import IasZone - self._ias_zone_cluster = self._in_clusters[IasZone.cluster_id] - - @property - def is_on(self) -> bool: - """Return True if entity is on.""" - if self._state is None: - return False - return bool(self._state) - - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return self._device_class - - def cluster_command(self, tsn, command_id, args): - """Handle commands received to this cluster.""" - if command_id == 0: - self._state = args[0] & 3 - _LOGGER.debug("Updated alarm state: %s", self._state) - self.async_schedule_update_ha_state() - elif command_id == 1: - _LOGGER.debug("Enroll requested") - res = self._ias_zone_cluster.enroll_response(0, 0) - self.hass.async_add_job(res) - - async def async_added_to_hass(self): - """Run when about to be added to hass.""" - await super().async_added_to_hass() - old_state = await self.async_get_last_state() - if self._state is not None or old_state is None: - return - - _LOGGER.debug("%s restoring old state: %s", self.entity_id, old_state) - if old_state.state == STATE_ON: - self._state = 3 - else: - self._state = 0 - - async def async_configure(self): - """Configure IAS device.""" - await self._ias_zone_cluster.bind() - ieee = self._ias_zone_cluster.endpoint.device.application.ieee - await self._ias_zone_cluster.write_attributes({'cie_addr': ieee}) - _LOGGER.debug("%s: finished configuration", self.entity_id) - - async def async_update(self): - """Retrieve latest state.""" - from zigpy.types.basic import uint16_t - - result = await helpers.safe_read(self._endpoint.ias_zone, - ['zone_status'], - allow_cache=False, - only_cache=(not self._initialized)) - state = result.get('zone_status', self._state) - if isinstance(state, (int, uint16_t)): - self._state = result.get('zone_status', self._state) & 3 - - -class Remote(RestoreEntity, ZhaEntity, BinarySensorDevice): - """ZHA switch/remote controller/button.""" - - _domain = DOMAIN - - def __init__(self, **kwargs): - """Initialize Switch.""" - super().__init__(**kwargs) - self._level = 0 - from zigpy.zcl.clusters import general - self._out_listeners = { - general.OnOff.cluster_id: OnOffListener( - self, - self._out_clusters[general.OnOff.cluster_id] - ) - } - - out_clusters = kwargs.get('out_clusters') - self._zcl_reporting = {} - - if general.LevelControl.cluster_id in out_clusters: - self._out_listeners.update({ - general.LevelControl.cluster_id: LevelListener( - self, - out_clusters[general.LevelControl.cluster_id] - ) - }) - - @property - def is_on(self) -> bool: - """Return true if the binary sensor is on.""" - return self._state - - @property - def device_state_attributes(self): - """Return the device state attributes.""" - self._device_state_attributes.update({ - 'level': self._state and self._level or 0 - }) - return self._device_state_attributes - - @property - def zcl_reporting_config(self): - """Return ZCL attribute reporting configuration.""" - return self._zcl_reporting - - def move_level(self, change): - """Increment the level, setting state if appropriate.""" - if not self._state and change > 0: - self._level = 0 - self._level = min(255, max(0, self._level + change)) - self._state = bool(self._level) - self.async_schedule_update_ha_state() - - def set_level(self, level): - """Set the level, setting state if appropriate.""" - self._level = level - self._state = bool(self._level) - self.async_schedule_update_ha_state() - - def set_state(self, state): - """Set the state.""" - self._state = state - if self._level == 0: - self._level = 255 - self.async_schedule_update_ha_state() - - async def async_configure(self): - """Bind clusters.""" - from zigpy.zcl.clusters import general - await helpers.bind_cluster( - self.entity_id, - self._out_clusters[general.OnOff.cluster_id] - ) - if general.LevelControl.cluster_id in self._out_clusters: - await helpers.bind_cluster( - self.entity_id, - self._out_clusters[general.LevelControl.cluster_id] - ) - - async def async_added_to_hass(self): - """Run when about to be added to hass.""" - await super().async_added_to_hass() - old_state = await self.async_get_last_state() - if self._state is not None or old_state is None: - return - - _LOGGER.debug("%s restoring old state: %s", self.entity_id, old_state) - if 'level' in old_state.attributes: - self._level = old_state.attributes['level'] - self._state = old_state.state == STATE_ON - - async def async_update(self): - """Retrieve latest state.""" - from zigpy.zcl.clusters.general import OnOff - result = await helpers.safe_read( - 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) - - -class BinarySensor(RestoreEntity, ZhaEntity, BinarySensorDevice): - """ZHA switch.""" +class BinarySensor(ZhaEntity, BinarySensorDevice): + """ZHA BinarySensor.""" _domain = DOMAIN _device_class = None - value_attribute = 0 - def __init__(self, device_class, **kwargs): + def __init__(self, **kwargs): """Initialize the ZHA binary sensor.""" super().__init__(**kwargs) - self._device_class = device_class - self._cluster = list(kwargs['in_clusters'].values())[0] + self._device_state_attributes = {} + self._zone_channel = self.cluster_channels.get(ZONE_CHANNEL) + self._on_off_channel = self.cluster_channels.get(ON_OFF_CHANNEL) + self._level_channel = self.cluster_channels.get(LEVEL_CHANNEL) + self._attr_channel = self.cluster_channels.get(ATTRIBUTE_CHANNEL) + self._zha_sensor_type = kwargs[SENSOR_TYPE] + self._level = None - def attribute_updated(self, attribute, value): - """Handle attribute update from device.""" - _LOGGER.debug("Attribute updated: %s %s %s", self, attribute, value) - if attribute == self.value_attribute: - self._state = bool(value) - self.async_schedule_update_ha_state() + async def _determine_device_class(self): + """Determine the device class for this binary sensor.""" + device_class_supplier = DEVICE_CLASS_REGISTRY.get( + self._zha_sensor_type) + if callable(device_class_supplier): + channel = self.cluster_channels.get(self._zha_sensor_type) + if channel is None: + return None + return await device_class_supplier(channel) + return device_class_supplier async def async_added_to_hass(self): """Run when about to be added to hass.""" + self._device_class = await self._determine_device_class() await super().async_added_to_hass() - old_state = await self.async_get_last_state() - if self._state is not None or old_state is None: - return - - _LOGGER.debug("%s restoring old state: %s", self.entity_id, old_state) - self._state = old_state.state == STATE_ON - - @property - def cluster(self): - """Zigbee cluster for this entity.""" - return self._cluster - - @property - def zcl_reporting_config(self): - """ZHA reporting configuration.""" - return {self.cluster: {self.value_attribute: REPORT_CONFIG_IMMEDIATE}} + if self._level_channel: + await self.async_accept_signal( + self._level_channel, SIGNAL_SET_LEVEL, self.set_level) + await self.async_accept_signal( + self._level_channel, SIGNAL_MOVE_LEVEL, self.move_level) + if self._on_off_channel: + await self.async_accept_signal( + self._on_off_channel, SIGNAL_ATTR_UPDATED, + self.async_set_state) + if self._zone_channel: + await self.async_accept_signal( + self._zone_channel, SIGNAL_ATTR_UPDATED, self.async_set_state) + if self._attr_channel: + await self.async_accept_signal( + self._attr_channel, SIGNAL_ATTR_UPDATED, self.async_set_state) @property def is_on(self) -> bool: @@ -315,3 +136,32 @@ class BinarySensor(RestoreEntity, ZhaEntity, BinarySensorDevice): def device_class(self) -> str: """Return device class from component DEVICE_CLASSES.""" return self._device_class + + def async_set_state(self, state): + """Set the state.""" + self._state = bool(state) + self.async_schedule_update_ha_state() + + def move_level(self, change): + """Increment the level, setting state if appropriate.""" + level = self._level or 0 + if not self._state and change > 0: + level = 0 + self._level = min(254, max(0, level + change)) + self._state = bool(self._level) + self.async_schedule_update_ha_state() + + def set_level(self, level): + """Set the level, setting state if appropriate.""" + self._level = level + self._state = bool(level) + self.async_schedule_update_ha_state() + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + if self._level_channel is not None: + self._device_state_attributes.update({ + ATTR_LEVEL: self._state and self._level or 0 + }) + return self._device_state_attributes diff --git a/homeassistant/components/zha/core/__init__.py b/homeassistant/components/zha/core/__init__.py index 47e6ed2b0ee..145b725fc79 100644 --- a/homeassistant/components/zha/core/__init__.py +++ b/homeassistant/components/zha/core/__init__.py @@ -4,3 +4,7 @@ Core module for Zigbee Home Automation. For more details about this component, please refer to the documentation at https://home-assistant.io/components/zha/ """ + +# flake8: noqa +from .device import ZHADevice +from .gateway import ZHAGateway diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py new file mode 100644 index 00000000000..0c0e1ed2173 --- /dev/null +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -0,0 +1,308 @@ +""" +Channels module for Zigbee Home Automation. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/zha/ +""" +import asyncio +from enum import Enum +from functools import wraps +import logging +from random import uniform + +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_send +from ..helpers import ( + bind_configure_reporting, construct_unique_id, + safe_read, get_attr_id_by_name) +from ..const import ( + CLUSTER_REPORT_CONFIGS, REPORT_CONFIG_DEFAULT, SIGNAL_ATTR_UPDATED, + ATTRIBUTE_CHANNEL, EVENT_RELAY_CHANNEL +) + +ZIGBEE_CHANNEL_REGISTRY = {} +_LOGGER = logging.getLogger(__name__) + + +def parse_and_log_command(unique_id, cluster, tsn, command_id, args): + """Parse and log a zigbee cluster command.""" + cmd = cluster.server_commands.get(command_id, [command_id])[0] + _LOGGER.debug( + "%s: received '%s' command with %s args on cluster_id '%s' tsn '%s'", + unique_id, + cmd, + args, + cluster.cluster_id, + tsn + ) + return cmd + + +def decorate_command(channel, command): + """Wrap a cluster command to make it safe.""" + @wraps(command) + async def wrapper(*args, **kwds): + from zigpy.zcl.foundation import Status + from zigpy.exceptions import DeliveryError + try: + result = await command(*args, **kwds) + _LOGGER.debug("%s: executed command: %s %s %s %s", + channel.unique_id, + command.__name__, + "{}: {}".format("with args", args), + "{}: {}".format("with kwargs", kwds), + "{}: {}".format("and result", result)) + if isinstance(result, bool): + return result + return result[1] is Status.SUCCESS + except DeliveryError: + _LOGGER.debug("%s: command failed: %s", channel.unique_id, + command.__name__) + return False + return wrapper + + +class ChannelStatus(Enum): + """Status of a channel.""" + + CREATED = 1 + CONFIGURED = 2 + INITIALIZED = 3 + + +class ZigbeeChannel: + """Base channel for a Zigbee cluster.""" + + def __init__(self, cluster, device): + """Initialize ZigbeeChannel.""" + self.name = 'channel_{}'.format(cluster.cluster_id) + self._cluster = cluster + self._zha_device = device + self._unique_id = construct_unique_id(cluster) + self._report_config = CLUSTER_REPORT_CONFIGS.get( + self._cluster.cluster_id, + [{'attr': 0, 'config': REPORT_CONFIG_DEFAULT}] + ) + self._status = ChannelStatus.CREATED + self._cluster.add_listener(self) + + @property + def unique_id(self): + """Return the unique id for this channel.""" + return self._unique_id + + @property + def cluster(self): + """Return the zigpy cluster for this channel.""" + return self._cluster + + @property + def device(self): + """Return the device this channel is linked to.""" + return self._zha_device + + @property + def status(self): + """Return the status of the channel.""" + return self._status + + def set_report_config(self, report_config): + """Set the reporting configuration.""" + self._report_config = report_config + + async def async_configure(self): + """Set cluster binding and attribute reporting.""" + manufacturer = None + manufacturer_code = self._zha_device.manufacturer_code + if self.cluster.cluster_id >= 0xfc00 and manufacturer_code: + manufacturer = manufacturer_code + + skip_bind = False # bind cluster only for the 1st configured attr + for report_config in self._report_config: + attr = report_config.get('attr') + min_report_interval, max_report_interval, change = \ + report_config.get('config') + await bind_configure_reporting( + self._unique_id, self.cluster, attr, + min_report=min_report_interval, + max_report=max_report_interval, + reportable_change=change, + skip_bind=skip_bind, + manufacturer=manufacturer + ) + skip_bind = True + await asyncio.sleep(uniform(0.1, 0.5)) + _LOGGER.debug( + "%s: finished channel configuration", + self._unique_id + ) + self._status = ChannelStatus.CONFIGURED + + async def async_initialize(self, from_cache): + """Initialize channel.""" + self._status = ChannelStatus.INITIALIZED + + @callback + def cluster_command(self, tsn, command_id, args): + """Handle commands received to this cluster.""" + pass + + @callback + def attribute_updated(self, attrid, value): + """Handle attribute updates on this cluster.""" + pass + + @callback + def zdo_command(self, *args, **kwargs): + """Handle ZDO commands on this cluster.""" + pass + + @callback + def zha_send_event(self, cluster, command, args): + """Relay events to hass.""" + self._zha_device.hass.bus.async_fire( + 'zha_event', + { + 'unique_id': self._unique_id, + 'device_ieee': str(self._zha_device.ieee), + 'command': command, + 'args': args + } + ) + + async def async_update(self): + """Retrieve latest state from cluster.""" + pass + + async def get_attribute_value(self, attribute, from_cache=True): + """Get the value for an attribute.""" + result = await safe_read( + self._cluster, + [attribute], + allow_cache=from_cache, + only_cache=from_cache + ) + return result.get(attribute) + + def __getattr__(self, name): + """Get attribute or a decorated cluster command.""" + if hasattr(self._cluster, name) and callable( + getattr(self._cluster, name)): + command = getattr(self._cluster, name) + command.__name__ = name + return decorate_command( + self, + command + ) + return self.__getattribute__(name) + + +class AttributeListeningChannel(ZigbeeChannel): + """Channel for attribute reports from the cluster.""" + + def __init__(self, cluster, device): + """Initialize AttributeListeningChannel.""" + super().__init__(cluster, device) + self.name = ATTRIBUTE_CHANNEL + attr = self._report_config[0].get('attr') + if isinstance(attr, str): + self._value_attribute = get_attr_id_by_name(self.cluster, attr) + else: + self._value_attribute = attr + + @callback + def attribute_updated(self, attrid, value): + """Handle attribute updates on this cluster.""" + if attrid == self._value_attribute: + async_dispatcher_send( + self._zha_device.hass, + "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), + value + ) + + async def async_initialize(self, from_cache): + """Initialize listener.""" + await self.get_attribute_value( + self._report_config[0].get('attr'), from_cache=from_cache) + await super().async_initialize(from_cache) + + +class ZDOChannel: + """Channel for ZDO events.""" + + def __init__(self, cluster, device): + """Initialize ZDOChannel.""" + self.name = 'zdo' + self._cluster = cluster + self._zha_device = device + self._status = ChannelStatus.CREATED + self._unique_id = "{}_ZDO".format(device.name) + self._cluster.add_listener(self) + + @property + def unique_id(self): + """Return the unique id for this channel.""" + return self._unique_id + + @property + def cluster(self): + """Return the aigpy cluster for this channel.""" + return self._cluster + + @property + def status(self): + """Return the status of the channel.""" + return self._status + + @callback + def device_announce(self, zigpy_device): + """Device announce handler.""" + pass + + @callback + def permit_duration(self, duration): + """Permit handler.""" + pass + + async def async_initialize(self, from_cache): + """Initialize channel.""" + self._status = ChannelStatus.INITIALIZED + + async def async_configure(self): + """Configure channel.""" + self._status = ChannelStatus.CONFIGURED + + +class EventRelayChannel(ZigbeeChannel): + """Event relay that can be attached to zigbee clusters.""" + + def __init__(self, cluster, device): + """Initialize EventRelayChannel.""" + super().__init__(cluster, device) + self.name = EVENT_RELAY_CHANNEL + + @callback + def attribute_updated(self, attrid, value): + """Handle an attribute updated on this cluster.""" + self.zha_send_event( + self._cluster, + SIGNAL_ATTR_UPDATED, + { + 'attribute_id': attrid, + 'attribute_name': self._cluster.attributes.get( + attrid, + ['Unknown'])[0], + 'value': value + } + ) + + @callback + def cluster_command(self, tsn, command_id, args): + """Handle a cluster command received on this cluster.""" + if self._cluster.server_commands is not None and \ + self._cluster.server_commands.get(command_id) is not None: + self.zha_send_event( + self._cluster, + self._cluster.server_commands.get(command_id)[0], + args + ) diff --git a/homeassistant/components/zha/core/channels/closures.py b/homeassistant/components/zha/core/channels/closures.py new file mode 100644 index 00000000000..ba3b6b2e716 --- /dev/null +++ b/homeassistant/components/zha/core/channels/closures.py @@ -0,0 +1,9 @@ +""" +Closures channels module for Zigbee Home Automation. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/zha/ +""" +import logging + +_LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py new file mode 100644 index 00000000000..a29b23d340b --- /dev/null +++ b/homeassistant/components/zha/core/channels/general.py @@ -0,0 +1,202 @@ +""" +General channels module for Zigbee Home Automation. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/zha/ +""" +import logging +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_send +from . import ZigbeeChannel, parse_and_log_command +from ..helpers import get_attr_id_by_name +from ..const import ( + SIGNAL_ATTR_UPDATED, + SIGNAL_MOVE_LEVEL, SIGNAL_SET_LEVEL, SIGNAL_STATE_ATTR, BASIC_CHANNEL, + ON_OFF_CHANNEL, LEVEL_CHANNEL, POWER_CONFIGURATION_CHANNEL +) + +_LOGGER = logging.getLogger(__name__) + + +class OnOffChannel(ZigbeeChannel): + """Channel for the OnOff Zigbee cluster.""" + + ON_OFF = 0 + + def __init__(self, cluster, device): + """Initialize OnOffChannel.""" + super().__init__(cluster, device) + self.name = ON_OFF_CHANNEL + self._state = None + + @callback + def cluster_command(self, tsn, command_id, args): + """Handle commands received to this cluster.""" + cmd = parse_and_log_command( + self.unique_id, + self._cluster, + tsn, + command_id, + args + ) + + if cmd in ('off', 'off_with_effect'): + self.attribute_updated(self.ON_OFF, False) + elif cmd in ('on', 'on_with_recall_global_scene', 'on_with_timed_off'): + self.attribute_updated(self.ON_OFF, True) + elif cmd == 'toggle': + self.attribute_updated(self.ON_OFF, not bool(self._state)) + + @callback + def attribute_updated(self, attrid, value): + """Handle attribute updates on this cluster.""" + if attrid == self.ON_OFF: + async_dispatcher_send( + self._zha_device.hass, + "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), + value + ) + self._state = bool(value) + + async def async_initialize(self, from_cache): + """Initialize channel.""" + self._state = bool( + await self.get_attribute_value(self.ON_OFF, from_cache=from_cache)) + await super().async_initialize(from_cache) + + +class LevelControlChannel(ZigbeeChannel): + """Channel for the LevelControl Zigbee cluster.""" + + CURRENT_LEVEL = 0 + + def __init__(self, cluster, device): + """Initialize LevelControlChannel.""" + super().__init__(cluster, device) + self.name = LEVEL_CHANNEL + + @callback + def cluster_command(self, tsn, command_id, args): + """Handle commands received to this cluster.""" + cmd = parse_and_log_command( + self.unique_id, + self._cluster, + tsn, + command_id, + args + ) + + if cmd in ('move_to_level', 'move_to_level_with_on_off'): + self.dispatch_level_change(SIGNAL_SET_LEVEL, args[0]) + elif cmd in ('move', 'move_with_on_off'): + # We should dim slowly -- for now, just step once + rate = args[1] + if args[0] == 0xff: + rate = 10 # Should read default move rate + self.dispatch_level_change( + SIGNAL_MOVE_LEVEL, -rate if args[0] else rate) + elif cmd in ('step', 'step_with_on_off'): + # Step (technically may change on/off) + self.dispatch_level_change( + SIGNAL_MOVE_LEVEL, -args[1] if args[0] else args[1]) + + @callback + def attribute_updated(self, attrid, value): + """Handle attribute updates on this cluster.""" + _LOGGER.debug("%s: received attribute: %s update with value: %i", + self.unique_id, attrid, value) + if attrid == self.CURRENT_LEVEL: + self.dispatch_level_change(SIGNAL_SET_LEVEL, value) + + def dispatch_level_change(self, command, level): + """Dispatch level change.""" + async_dispatcher_send( + self._zha_device.hass, + "{}_{}".format(self.unique_id, command), + level + ) + + async def async_initialize(self, from_cache): + """Initialize channel.""" + await self.get_attribute_value( + self.CURRENT_LEVEL, from_cache=from_cache) + await super().async_initialize(from_cache) + + +class BasicChannel(ZigbeeChannel): + """Channel to interact with the basic cluster.""" + + BATTERY = 3 + POWER_SOURCES = { + 0: 'Unknown', + 1: 'Mains (single phase)', + 2: 'Mains (3 phase)', + BATTERY: 'Battery', + 4: 'DC source', + 5: 'Emergency mains constantly powered', + 6: 'Emergency mains and transfer switch' + } + + def __init__(self, cluster, device): + """Initialize BasicChannel.""" + super().__init__(cluster, device) + self.name = BASIC_CHANNEL + self._power_source = None + + async def async_configure(self): + """Configure this channel.""" + await super().async_configure() + await self.async_initialize(False) + + async def async_initialize(self, from_cache): + """Initialize channel.""" + self._power_source = await self.get_attribute_value( + 'power_source', from_cache=from_cache) + await super().async_initialize(from_cache) + + def get_power_source(self): + """Get the power source.""" + return self._power_source + + +class PowerConfigurationChannel(ZigbeeChannel): + """Channel for the zigbee power configuration cluster.""" + + def __init__(self, cluster, device): + """Initialize PowerConfigurationChannel.""" + super().__init__(cluster, device) + self.name = POWER_CONFIGURATION_CHANNEL + + @callback + def attribute_updated(self, attrid, value): + """Handle attribute updates on this cluster.""" + attr = self._report_config[1].get('attr') + if isinstance(attr, str): + attr_id = get_attr_id_by_name(self.cluster, attr) + else: + attr_id = attr + if attrid == attr_id: + async_dispatcher_send( + self._zha_device.hass, + "{}_{}".format(self.unique_id, SIGNAL_STATE_ATTR), + 'battery_level', + value + ) + + async def async_initialize(self, from_cache): + """Initialize channel.""" + await self.async_read_state(from_cache) + await super().async_initialize(from_cache) + + async def async_update(self): + """Retrieve latest state.""" + await self.async_read_state(True) + + async def async_read_state(self, from_cache): + """Read data from the cluster.""" + await self.get_attribute_value( + 'battery_size', from_cache=from_cache) + await self.get_attribute_value( + 'battery_percentage_remaining', from_cache=from_cache) + await self.get_attribute_value( + 'battery_voltage', from_cache=from_cache) diff --git a/homeassistant/components/zha/core/channels/homeautomation.py b/homeassistant/components/zha/core/channels/homeautomation.py new file mode 100644 index 00000000000..2518889fcb1 --- /dev/null +++ b/homeassistant/components/zha/core/channels/homeautomation.py @@ -0,0 +1,40 @@ +""" +Home automation channels module for Zigbee Home Automation. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/zha/ +""" +import logging +from homeassistant.helpers.dispatcher import async_dispatcher_send +from . import AttributeListeningChannel +from ..const import SIGNAL_ATTR_UPDATED, ELECTRICAL_MEASUREMENT_CHANNEL + +_LOGGER = logging.getLogger(__name__) + + +class ElectricalMeasurementChannel(AttributeListeningChannel): + """Channel that polls active power level.""" + + def __init__(self, cluster, device): + """Initialize ElectricalMeasurementChannel.""" + super().__init__(cluster, device) + self.name = ELECTRICAL_MEASUREMENT_CHANNEL + + async def async_update(self): + """Retrieve latest state.""" + _LOGGER.debug("%s async_update", self.unique_id) + + # This is a polling channel. Don't allow cache. + result = await self.get_attribute_value( + ELECTRICAL_MEASUREMENT_CHANNEL, from_cache=False) + async_dispatcher_send( + self._zha_device.hass, + "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), + result + ) + + async def async_initialize(self, from_cache): + """Initialize channel.""" + await self.get_attribute_value( + ELECTRICAL_MEASUREMENT_CHANNEL, from_cache=from_cache) + await super().async_initialize(from_cache) diff --git a/homeassistant/components/zha/core/channels/hvac.py b/homeassistant/components/zha/core/channels/hvac.py new file mode 100644 index 00000000000..c62ec66588e --- /dev/null +++ b/homeassistant/components/zha/core/channels/hvac.py @@ -0,0 +1,62 @@ +""" +HVAC channels module for Zigbee Home Automation. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/zha/ +""" +import logging +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_send +from . import ZigbeeChannel +from ..const import FAN_CHANNEL, SIGNAL_ATTR_UPDATED + +_LOGGER = logging.getLogger(__name__) + + +class FanChannel(ZigbeeChannel): + """Fan channel.""" + + _value_attribute = 0 + + def __init__(self, cluster, device): + """Initialize FanChannel.""" + super().__init__(cluster, device) + self.name = FAN_CHANNEL + + async def async_set_speed(self, value) -> None: + """Set the speed of the fan.""" + from zigpy.exceptions import DeliveryError + try: + await self.cluster.write_attributes({'fan_mode': value}) + except DeliveryError as ex: + _LOGGER.error("%s: Could not set speed: %s", self.unique_id, ex) + return + + async def async_update(self): + """Retrieve latest state.""" + result = await self.get_attribute_value('fan_mode', from_cache=True) + + async_dispatcher_send( + self._zha_device.hass, + "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), + result + ) + + @callback + def attribute_updated(self, attrid, value): + """Handle attribute update from fan cluster.""" + attr_name = self.cluster.attributes.get(attrid, [attrid])[0] + _LOGGER.debug("%s: Attribute report '%s'[%s] = %s", + self.unique_id, self.cluster.name, attr_name, value) + if attrid == self._value_attribute: + async_dispatcher_send( + self._zha_device.hass, + "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), + value + ) + + async def async_initialize(self, from_cache): + """Initialize channel.""" + await self.get_attribute_value( + self._value_attribute, from_cache=from_cache) + await super().async_initialize(from_cache) diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py new file mode 100644 index 00000000000..ee88a30e828 --- /dev/null +++ b/homeassistant/components/zha/core/channels/lighting.py @@ -0,0 +1,48 @@ +""" +Lighting channels module for Zigbee Home Automation. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/zha/ +""" +import logging +from . import ZigbeeChannel +from ..const import COLOR_CHANNEL + +_LOGGER = logging.getLogger(__name__) + + +class ColorChannel(ZigbeeChannel): + """Color channel.""" + + CAPABILITIES_COLOR_XY = 0x08 + CAPABILITIES_COLOR_TEMP = 0x10 + UNSUPPORTED_ATTRIBUTE = 0x86 + + def __init__(self, cluster, device): + """Initialize ColorChannel.""" + super().__init__(cluster, device) + self.name = COLOR_CHANNEL + self._color_capabilities = None + + def get_color_capabilities(self): + """Return the color capabilities.""" + return self._color_capabilities + + async def async_initialize(self, from_cache): + """Initialize channel.""" + capabilities = await self.get_attribute_value( + 'color_capabilities', from_cache=from_cache) + + if capabilities is None: + # ZCL Version 4 devices don't support the color_capabilities + # attribute. In this version XY support is mandatory, but we + # need to probe to determine if the device supports color + # temperature. + capabilities = self.CAPABILITIES_COLOR_XY + result = await self.get_attribute_value( + 'color_temperature', from_cache=from_cache) + + if result is not self.UNSUPPORTED_ATTRIBUTE: + capabilities |= self.CAPABILITIES_COLOR_TEMP + self._color_capabilities = capabilities + await super().async_initialize(from_cache) diff --git a/homeassistant/components/zha/core/channels/lightlink.py b/homeassistant/components/zha/core/channels/lightlink.py new file mode 100644 index 00000000000..83fca6e80c2 --- /dev/null +++ b/homeassistant/components/zha/core/channels/lightlink.py @@ -0,0 +1,9 @@ +""" +Lightlink channels module for Zigbee Home Automation. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/zha/ +""" +import logging + +_LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py new file mode 100644 index 00000000000..a0eebd78343 --- /dev/null +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -0,0 +1,9 @@ +""" +Manufacturer specific channels module for Zigbee Home Automation. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/zha/ +""" +import logging + +_LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/core/channels/measurement.py b/homeassistant/components/zha/core/channels/measurement.py new file mode 100644 index 00000000000..51146289e69 --- /dev/null +++ b/homeassistant/components/zha/core/channels/measurement.py @@ -0,0 +1,9 @@ +""" +Measurement channels module for Zigbee Home Automation. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/zha/ +""" +import logging + +_LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/core/channels/protocol.py b/homeassistant/components/zha/core/channels/protocol.py new file mode 100644 index 00000000000..2cae156aec5 --- /dev/null +++ b/homeassistant/components/zha/core/channels/protocol.py @@ -0,0 +1,9 @@ +""" +Protocol channels module for Zigbee Home Automation. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/zha/ +""" +import logging + +_LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/core/channels/registry.py b/homeassistant/components/zha/core/channels/registry.py new file mode 100644 index 00000000000..f0363ac8330 --- /dev/null +++ b/homeassistant/components/zha/core/channels/registry.py @@ -0,0 +1,46 @@ +""" +Channel registry module for Zigbee Home Automation. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/zha/ +""" +from . import ZigbeeChannel +from .general import ( + OnOffChannel, LevelControlChannel, PowerConfigurationChannel, BasicChannel +) +from .homeautomation import ElectricalMeasurementChannel +from .hvac import FanChannel +from .lighting import ColorChannel +from .security import IASZoneChannel + + +ZIGBEE_CHANNEL_REGISTRY = {} + + +def populate_channel_registry(): + """Populate the channel registry.""" + from zigpy import zcl + ZIGBEE_CHANNEL_REGISTRY.update({ + zcl.clusters.general.Alarms.cluster_id: ZigbeeChannel, + zcl.clusters.general.Commissioning.cluster_id: ZigbeeChannel, + zcl.clusters.general.Identify.cluster_id: ZigbeeChannel, + zcl.clusters.general.Groups.cluster_id: ZigbeeChannel, + zcl.clusters.general.Scenes.cluster_id: ZigbeeChannel, + zcl.clusters.general.Partition.cluster_id: ZigbeeChannel, + zcl.clusters.general.Ota.cluster_id: ZigbeeChannel, + zcl.clusters.general.PowerProfile.cluster_id: ZigbeeChannel, + zcl.clusters.general.ApplianceControl.cluster_id: ZigbeeChannel, + zcl.clusters.general.PollControl.cluster_id: ZigbeeChannel, + zcl.clusters.general.GreenPowerProxy.cluster_id: ZigbeeChannel, + zcl.clusters.general.OnOffConfiguration.cluster_id: ZigbeeChannel, + zcl.clusters.general.OnOff.cluster_id: OnOffChannel, + zcl.clusters.general.LevelControl.cluster_id: LevelControlChannel, + zcl.clusters.lighting.Color.cluster_id: ColorChannel, + zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id: + ElectricalMeasurementChannel, + zcl.clusters.general.PowerConfiguration.cluster_id: + PowerConfigurationChannel, + zcl.clusters.general.Basic.cluster_id: BasicChannel, + zcl.clusters.security.IasZone.cluster_id: IASZoneChannel, + zcl.clusters.hvac.Fan.cluster_id: FanChannel, + }) diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py new file mode 100644 index 00000000000..e8c0e71a263 --- /dev/null +++ b/homeassistant/components/zha/core/channels/security.py @@ -0,0 +1,82 @@ +""" +Security channels module for Zigbee Home Automation. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/zha/ +""" +import logging +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_send +from . import ZigbeeChannel +from ..helpers import bind_cluster +from ..const import SIGNAL_ATTR_UPDATED, ZONE_CHANNEL + +_LOGGER = logging.getLogger(__name__) + + +class IASZoneChannel(ZigbeeChannel): + """Channel for the IASZone Zigbee cluster.""" + + def __init__(self, cluster, device): + """Initialize IASZoneChannel.""" + super().__init__(cluster, device) + self.name = ZONE_CHANNEL + + @callback + def cluster_command(self, tsn, command_id, args): + """Handle commands received to this cluster.""" + if command_id == 0: + state = args[0] & 3 + async_dispatcher_send( + self._zha_device.hass, + "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), + state + ) + _LOGGER.debug("Updated alarm state: %s", state) + elif command_id == 1: + _LOGGER.debug("Enroll requested") + res = self._cluster.enroll_response(0, 0) + self._zha_device.hass.async_create_task(res) + + async def async_configure(self): + """Configure IAS device.""" + from zigpy.exceptions import DeliveryError + _LOGGER.debug("%s: started IASZoneChannel configuration", + self._unique_id) + + await bind_cluster(self.unique_id, self._cluster) + ieee = self._cluster.endpoint.device.application.ieee + + try: + res = await self._cluster.write_attributes({'cie_addr': ieee}) + _LOGGER.debug( + "%s: wrote cie_addr: %s to '%s' cluster: %s", + self.unique_id, str(ieee), self._cluster.ep_attribute, + res[0] + ) + except DeliveryError as ex: + _LOGGER.debug( + "%s: Failed to write cie_addr: %s to '%s' cluster: %s", + self.unique_id, str(ieee), self._cluster.ep_attribute, str(ex) + ) + _LOGGER.debug("%s: finished IASZoneChannel configuration", + self._unique_id) + + await self.get_attribute_value('zone_type', from_cache=False) + + @callback + def attribute_updated(self, attrid, value): + """Handle attribute updates on this cluster.""" + if attrid == 2: + value = value & 3 + async_dispatcher_send( + self._zha_device.hass, + "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), + value + ) + + async def async_initialize(self, from_cache): + """Initialize channel.""" + await self.get_attribute_value('zone_status', from_cache=from_cache) + await self.get_attribute_value('zone_state', from_cache=from_cache) + await super().async_initialize(from_cache) diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py new file mode 100644 index 00000000000..d17eae30a96 --- /dev/null +++ b/homeassistant/components/zha/core/channels/smartenergy.py @@ -0,0 +1,9 @@ +""" +Smart energy channels module for Zigbee Home Automation. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/zha/ +""" +import logging + +_LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 3069ebf02db..d1001682c7b 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -55,10 +55,41 @@ IEEE = 'ieee' MODEL = 'model' NAME = 'name' -LISTENER_BATTERY = 'battery' +SENSOR_TYPE = 'sensor_type' +HUMIDITY = 'humidity' +TEMPERATURE = 'temperature' +ILLUMINANCE = 'illuminance' +PRESSURE = 'pressure' +METERING = 'metering' +ELECTRICAL_MEASUREMENT = 'electrical_measurement' +GENERIC = 'generic' +UNKNOWN = 'unknown' +OPENING = 'opening' +ZONE = 'zone' +OCCUPANCY = 'occupancy' + +ATTR_LEVEL = 'level' + +ON_OFF_CHANNEL = 'on_off' +ATTRIBUTE_CHANNEL = 'attribute' +BASIC_CHANNEL = 'basic' +COLOR_CHANNEL = 'color' +FAN_CHANNEL = 'fan' +LEVEL_CHANNEL = ATTR_LEVEL +ZONE_CHANNEL = 'zone' +ELECTRICAL_MEASUREMENT_CHANNEL = 'active_power' +POWER_CONFIGURATION_CHANNEL = 'battery' +EVENT_RELAY_CHANNEL = 'event_relay' SIGNAL_ATTR_UPDATED = 'attribute_updated' +SIGNAL_MOVE_LEVEL = "move_level" +SIGNAL_SET_LEVEL = "set_level" +SIGNAL_STATE_ATTR = "update_state_attribute" SIGNAL_AVAILABLE = 'available' +SIGNAL_REMOVE = 'remove' + +QUIRK_APPLIED = 'quirk_applied' +QUIRK_CLASS = 'quirk_class' class RadioType(enum.Enum): @@ -78,9 +109,11 @@ DISCOVERY_KEY = 'zha_discovery_info' DEVICE_CLASS = {} SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {} SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {} +CLUSTER_REPORT_CONFIGS = {} CUSTOM_CLUSTER_MAPPINGS = {} COMPONENT_CLUSTERS = {} -EVENTABLE_CLUSTERS = [] +EVENT_RELAY_CLUSTERS = [] +NO_SENSOR_CLUSTERS = [] REPORT_CONFIG_MAX_INT = 900 REPORT_CONFIG_MAX_INT_BATTERY_SAVE = 10800 diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index c7dabced24b..12bb397fbc3 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -5,22 +5,32 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/zha/ """ import asyncio +from enum import Enum import logging from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send ) from .const import ( - ATTR_MANUFACTURER, LISTENER_BATTERY, SIGNAL_AVAILABLE, IN, OUT, + ATTR_MANUFACTURER, POWER_CONFIGURATION_CHANNEL, SIGNAL_AVAILABLE, IN, OUT, ATTR_CLUSTER_ID, ATTR_ATTRIBUTE, ATTR_VALUE, ATTR_COMMAND, SERVER, ATTR_COMMAND_TYPE, ATTR_ARGS, CLIENT_COMMANDS, SERVER_COMMANDS, - ATTR_ENDPOINT_ID, IEEE, MODEL, NAME + ATTR_ENDPOINT_ID, IEEE, MODEL, NAME, UNKNOWN, QUIRK_APPLIED, + QUIRK_CLASS, BASIC_CHANNEL ) -from .listeners import EventRelayListener +from .channels import EventRelayChannel +from .channels.general import BasicChannel _LOGGER = logging.getLogger(__name__) +class DeviceStatus(Enum): + """Status of a device.""" + + CREATED = 1 + INITIALIZED = 2 + + class ZHADevice: """ZHA Zigbee device object.""" @@ -30,13 +40,16 @@ class ZHADevice: self._zigpy_device = zigpy_device # Get first non ZDO endpoint id to use to get manufacturer and model endpoint_ids = zigpy_device.endpoints.keys() - ept_id = next(ept_id for ept_id in endpoint_ids if ept_id != 0) - self._manufacturer = zigpy_device.endpoints[ept_id].manufacturer - self._model = zigpy_device.endpoints[ept_id].model + self._manufacturer = UNKNOWN + self._model = UNKNOWN + ept_id = next((ept_id for ept_id in endpoint_ids if ept_id != 0), None) + if ept_id is not None: + self._manufacturer = zigpy_device.endpoints[ept_id].manufacturer + self._model = zigpy_device.endpoints[ept_id].model self._zha_gateway = zha_gateway - self._cluster_listeners = {} - self._relay_listeners = [] - self._all_listeners = [] + self.cluster_channels = {} + self._relay_channels = [] + self._all_channels = [] self._name = "{} {}".format( self.manufacturer, self.model @@ -49,6 +62,14 @@ class ZHADevice: self._available_signal, self.async_initialize ) + from zigpy.quirks import CustomDevice + self.quirk_applied = isinstance(self._zigpy_device, CustomDevice) + self.quirk_class = "{}.{}".format( + self._zigpy_device.__class__.__module__, + self._zigpy_device.__class__.__name__ + ) + self.power_source = None + self.status = DeviceStatus.CREATED @property def name(self): @@ -102,19 +123,9 @@ class ZHADevice: return self._zha_gateway @property - def cluster_listeners(self): - """Return cluster listeners for device.""" - return self._cluster_listeners.values() - - @property - def all_listeners(self): - """Return cluster listeners and relay listeners for device.""" - return self._all_listeners - - @property - def cluster_listener_keys(self): - """Return cluster listeners for device.""" - return self._cluster_listeners.keys() + def all_channels(self): + """Return cluster channels and relay channels for device.""" + return self._all_channels @property def available_signal(self): @@ -150,63 +161,65 @@ class ZHADevice: IEEE: ieee, ATTR_MANUFACTURER: self.manufacturer, MODEL: self.model, - NAME: self.name or ieee + NAME: self.name or ieee, + QUIRK_APPLIED: self.quirk_applied, + QUIRK_CLASS: self.quirk_class } - def add_cluster_listener(self, cluster_listener): - """Add cluster listener to device.""" - # only keep 1 power listener - if cluster_listener.name is LISTENER_BATTERY and \ - LISTENER_BATTERY in self._cluster_listeners: + def add_cluster_channel(self, cluster_channel): + """Add cluster channel to device.""" + # only keep 1 power configuration channel + if cluster_channel.name is POWER_CONFIGURATION_CHANNEL and \ + POWER_CONFIGURATION_CHANNEL in self.cluster_channels: return - self._all_listeners.append(cluster_listener) - if isinstance(cluster_listener, EventRelayListener): - self._relay_listeners.append(cluster_listener) + self._all_channels.append(cluster_channel) + if isinstance(cluster_channel, EventRelayChannel): + self._relay_channels.append(cluster_channel) else: - self._cluster_listeners[cluster_listener.name] = cluster_listener - - def get_cluster_listener(self, name): - """Get cluster listener by name.""" - return self._cluster_listeners.get(name, None) + self.cluster_channels[cluster_channel.name] = cluster_channel async def async_configure(self): """Configure the device.""" _LOGGER.debug('%s: started configuration', self.name) - await self._execute_listener_tasks('async_configure') + await self._execute_channel_tasks('async_configure') _LOGGER.debug('%s: completed configuration', self.name) - async def async_initialize(self, from_cache): - """Initialize listeners.""" + async def async_initialize(self, from_cache=False): + """Initialize channels.""" _LOGGER.debug('%s: started initialization', self.name) - await self._execute_listener_tasks('async_initialize', from_cache) + await self._execute_channel_tasks('async_initialize', from_cache) + self.power_source = self.cluster_channels.get( + BASIC_CHANNEL).get_power_source() + _LOGGER.debug( + '%s: power source: %s', + self.name, + BasicChannel.POWER_SOURCES.get(self.power_source) + ) + self.status = DeviceStatus.INITIALIZED _LOGGER.debug('%s: completed initialization', self.name) - async def async_accept_messages(self): - """Start accepting messages from the zigbee network.""" - await self._execute_listener_tasks('accept_messages') + async def _execute_channel_tasks(self, task_name, *args): + """Gather and execute a set of CHANNEL tasks.""" + channel_tasks = [] + for channel in self.all_channels: + channel_tasks.append( + self._async_create_task(channel, task_name, *args)) + await asyncio.gather(*channel_tasks) - async def _execute_listener_tasks(self, task_name, *args): - """Gather and execute a set of listener tasks.""" - listener_tasks = [] - for listener in self.all_listeners: - listener_tasks.append( - self._async_create_task(listener, task_name, *args)) - await asyncio.gather(*listener_tasks) - - async def _async_create_task(self, listener, func_name, *args): - """Configure a single listener on this device.""" + async def _async_create_task(self, channel, func_name, *args): + """Configure a single channel on this device.""" try: - await getattr(listener, func_name)(*args) - _LOGGER.debug('%s: listener: %s %s stage succeeded', + await getattr(channel, func_name)(*args) + _LOGGER.debug('%s: channel: %s %s stage succeeded', self.name, "{}-{}".format( - listener.name, listener.unique_id), + channel.name, channel.unique_id), func_name) except Exception as ex: # pylint: disable=broad-except _LOGGER.warning( - '%s listener: %s %s stage failed ex: %s', + '%s channel: %s %s stage failed ex: %s', self.name, - "{}-{}".format(listener.name, listener.unique_id), + "{}-{}".format(channel.name, channel.unique_id), func_name, ex ) @@ -226,24 +239,24 @@ class ZHADevice: if ep_id != 0 } - async def get_cluster(self, endpooint_id, cluster_id, cluster_type=IN): + async def get_cluster(self, endpoint_id, cluster_id, cluster_type=IN): """Get zigbee cluster from this entity.""" clusters = await self.get_clusters() - return clusters[endpooint_id][cluster_type][cluster_id] + return clusters[endpoint_id][cluster_type][cluster_id] - async def get_cluster_attributes(self, endpooint_id, cluster_id, + async def get_cluster_attributes(self, endpoint_id, cluster_id, cluster_type=IN): """Get zigbee attributes for specified cluster.""" - cluster = await self.get_cluster(endpooint_id, cluster_id, + cluster = await self.get_cluster(endpoint_id, cluster_id, cluster_type) if cluster is None: return None return cluster.attributes - async def get_cluster_commands(self, endpooint_id, cluster_id, + async def get_cluster_commands(self, endpoint_id, cluster_id, cluster_type=IN): """Get zigbee commands for specified cluster.""" - cluster = await self.get_cluster(endpooint_id, cluster_id, + cluster = await self.get_cluster(endpoint_id, cluster_id, cluster_type) if cluster is None: return None @@ -252,12 +265,12 @@ class ZHADevice: SERVER_COMMANDS: cluster.server_commands, } - async def write_zigbee_attribute(self, endpooint_id, cluster_id, + async def write_zigbee_attribute(self, endpoint_id, cluster_id, attribute, value, cluster_type=IN, manufacturer=None): """Write a value to a zigbee attribute for a cluster in this entity.""" cluster = await self.get_cluster( - endpooint_id, cluster_id, cluster_type) + endpoint_id, cluster_id, cluster_type) if cluster is None: return None @@ -272,7 +285,7 @@ class ZHADevice: value, attribute, cluster_id, - endpooint_id, + endpoint_id, response ) return response @@ -282,17 +295,17 @@ class ZHADevice: '{}: {}'.format(ATTR_VALUE, value), '{}: {}'.format(ATTR_ATTRIBUTE, attribute), '{}: {}'.format(ATTR_CLUSTER_ID, cluster_id), - '{}: {}'.format(ATTR_ENDPOINT_ID, endpooint_id), + '{}: {}'.format(ATTR_ENDPOINT_ID, endpoint_id), exc ) return None - async def issue_cluster_command(self, endpooint_id, cluster_id, command, + async def issue_cluster_command(self, endpoint_id, cluster_id, command, command_type, args, cluster_type=IN, manufacturer=None): """Issue a command against specified zigbee cluster on this entity.""" cluster = await self.get_cluster( - endpooint_id, cluster_id, cluster_type) + endpoint_id, cluster_id, cluster_type) if cluster is None: return None response = None @@ -311,6 +324,6 @@ class ZHADevice: '{}: {}'.format(ATTR_ARGS, args), '{}: {}'.format(ATTR_CLUSTER_ID, cluster_type), '{}: {}'.format(ATTR_MANUFACTURER, manufacturer), - '{}: {}'.format(ATTR_ENDPOINT_ID, endpooint_id) + '{}: {}'.format(ATTR_ENDPOINT_ID, endpoint_id) ) return response diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 479b2f79b26..a50bfeae1be 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -5,7 +5,9 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/zha/ """ +import asyncio import collections +import itertools import logging from homeassistant import const as ha_const from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -13,15 +15,30 @@ from homeassistant.helpers.entity_component import EntityComponent from . import const as zha_const from .const import ( COMPONENTS, CONF_DEVICE_CONFIG, DATA_ZHA, DATA_ZHA_CORE_COMPONENT, DOMAIN, - ZHA_DISCOVERY_NEW, EVENTABLE_CLUSTERS, DATA_ZHA_CORE_EVENTS, DEVICE_CLASS, - SINGLE_INPUT_CLUSTER_DEVICE_CLASS, SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS, - CUSTOM_CLUSTER_MAPPINGS, COMPONENT_CLUSTERS) + ZHA_DISCOVERY_NEW, DEVICE_CLASS, SINGLE_INPUT_CLUSTER_DEVICE_CLASS, + SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS, COMPONENT_CLUSTERS, HUMIDITY, + TEMPERATURE, ILLUMINANCE, PRESSURE, METERING, ELECTRICAL_MEASUREMENT, + GENERIC, SENSOR_TYPE, EVENT_RELAY_CLUSTERS, UNKNOWN, + OPENING, ZONE, OCCUPANCY, CLUSTER_REPORT_CONFIGS, REPORT_CONFIG_IMMEDIATE, + REPORT_CONFIG_ASAP, REPORT_CONFIG_DEFAULT, REPORT_CONFIG_MIN_INT, + REPORT_CONFIG_MAX_INT, REPORT_CONFIG_OP, SIGNAL_REMOVE, NO_SENSOR_CLUSTERS, + POWER_CONFIGURATION_CHANNEL) +from .device import ZHADevice, DeviceStatus from ..device_entity import ZhaDeviceEntity -from ..event import ZhaEvent, ZhaRelayEvent +from .channels import ( + AttributeListeningChannel, EventRelayChannel, ZDOChannel +) +from .channels.general import BasicChannel +from .channels.registry import ZIGBEE_CHANNEL_REGISTRY from .helpers import convert_ieee _LOGGER = logging.getLogger(__name__) +SENSOR_TYPES = {} +BINARY_SENSOR_TYPES = {} +EntityReference = collections.namedtuple( + 'EntityReference', 'reference_id zha_device cluster_channels device_info') + class ZHAGateway: """Gateway that handles events that happen on the ZHA Zigbee network.""" @@ -31,16 +48,9 @@ class ZHAGateway: self._hass = hass self._config = config self._component = EntityComponent(_LOGGER, DOMAIN, hass) + self._devices = {} self._device_registry = collections.defaultdict(list) - self._events = {} - establish_device_mappings() - - for component in COMPONENTS: - hass.data[DATA_ZHA][component] = ( - hass.data[DATA_ZHA].get(component, {}) - ) hass.data[DATA_ZHA][DATA_ZHA_CORE_COMPONENT] = self._component - hass.data[DATA_ZHA][DATA_ZHA_CORE_EVENTS] = self._events def device_joined(self, device): """Handle device joined. @@ -67,197 +77,347 @@ class ZHAGateway: def device_removed(self, device): """Handle device being removed from the network.""" - for device_entity in self._device_registry[device.ieee]: - self._hass.async_create_task(device_entity.async_remove()) - if device.ieee in self._events: - self._events.pop(device.ieee) - - def get_device_entity(self, ieee_str): - """Return ZHADeviceEntity for given ieee.""" - ieee = convert_ieee(ieee_str) - if ieee in self._device_registry: - entities = self._device_registry[ieee] - entity = next( - ent for ent in entities if isinstance(ent, ZhaDeviceEntity)) - return entity - return None - - def get_entities_for_ieee(self, ieee_str): - """Return list of entities for given ieee.""" - ieee = convert_ieee(ieee_str) - if ieee in self._device_registry: - return self._device_registry[ieee] - return [] - - @property - def device_registry(self) -> str: - """Return devices.""" - return self._device_registry - - async def async_device_initialized(self, device, join): - """Handle device joined and basic information discovered (async).""" - import zigpy.profiles - - device_manufacturer = device_model = None - - for endpoint_id, endpoint in device.endpoints.items(): - if endpoint_id == 0: # ZDO - continue - - if endpoint.manufacturer is not None: - device_manufacturer = endpoint.manufacturer - if endpoint.model is not None: - device_model = endpoint.model - - component = None - profile_clusters = ([], []) - device_key = "{}-{}".format(device.ieee, endpoint_id) - node_config = {} - if CONF_DEVICE_CONFIG in self._config: - node_config = self._config[CONF_DEVICE_CONFIG].get( - device_key, {} - ) - - if endpoint.profile_id in zigpy.profiles.PROFILES: - profile = zigpy.profiles.PROFILES[endpoint.profile_id] - if zha_const.DEVICE_CLASS.get(endpoint.profile_id, - {}).get(endpoint.device_type, - None): - profile_clusters = profile.CLUSTERS[endpoint.device_type] - profile_info = zha_const.DEVICE_CLASS[endpoint.profile_id] - component = profile_info[endpoint.device_type] - - if ha_const.CONF_TYPE in node_config: - component = node_config[ha_const.CONF_TYPE] - profile_clusters = zha_const.COMPONENT_CLUSTERS[component] - - if component: - in_clusters = [endpoint.in_clusters[c] - for c in profile_clusters[0] - if c in endpoint.in_clusters] - out_clusters = [endpoint.out_clusters[c] - for c in profile_clusters[1] - if c in endpoint.out_clusters] - discovery_info = { - 'application_listener': self, - 'endpoint': endpoint, - 'in_clusters': {c.cluster_id: c for c in in_clusters}, - 'out_clusters': {c.cluster_id: c for c in out_clusters}, - 'manufacturer': endpoint.manufacturer, - 'model': endpoint.model, - 'new_join': join, - 'unique_id': device_key, - } - - if join: - async_dispatcher_send( - self._hass, - ZHA_DISCOVERY_NEW.format(component), - discovery_info - ) - else: - self._hass.data[DATA_ZHA][component][device_key] = ( - discovery_info - ) - - for cluster in endpoint.in_clusters.values(): - await self._attempt_single_cluster_device( - endpoint, - cluster, - profile_clusters[0], - device_key, - zha_const.SINGLE_INPUT_CLUSTER_DEVICE_CLASS, - 'in_clusters', - join, - ) - - for cluster in endpoint.out_clusters.values(): - await self._attempt_single_cluster_device( - endpoint, - cluster, - profile_clusters[1], - device_key, - zha_const.SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS, - 'out_clusters', - join, - ) - - endpoint_entity = ZhaDeviceEntity( - device, - device_manufacturer, - device_model, - self, - ) - await self._component.async_add_entities([endpoint_entity]) - - def register_entity(self, ieee, entity_obj): - """Record the creation of a hass entity associated with ieee.""" - self._device_registry[ieee].append(entity_obj) - - async def _attempt_single_cluster_device(self, endpoint, cluster, - profile_clusters, device_key, - device_classes, discovery_attr, - is_new_join): - """Try to set up an entity from a "bare" cluster.""" - if cluster.cluster_id in EVENTABLE_CLUSTERS: - if cluster.endpoint.device.ieee not in self._events: - self._events.update({cluster.endpoint.device.ieee: []}) - from zigpy.zcl.clusters.general import OnOff, LevelControl - if discovery_attr == 'out_clusters' and \ - (cluster.cluster_id == OnOff.cluster_id or - cluster.cluster_id == LevelControl.cluster_id): - self._events[cluster.endpoint.device.ieee].append( - ZhaRelayEvent(self._hass, cluster) - ) - else: - self._events[cluster.endpoint.device.ieee].append(ZhaEvent( - self._hass, - cluster - )) - - if cluster.cluster_id in profile_clusters: - return - - 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 - - cluster_key = "{}-{}".format(device_key, cluster.cluster_id) - discovery_info = { - 'application_listener': self, - 'endpoint': endpoint, - 'in_clusters': {}, - 'out_clusters': {}, - 'manufacturer': endpoint.manufacturer, - 'model': endpoint.model, - 'new_join': is_new_join, - 'unique_id': cluster_key, - 'entity_suffix': '_{}'.format(cluster.cluster_id), - } - discovery_info[discovery_attr] = {cluster.cluster_id: cluster} - if sub_component: - discovery_info.update({'sub_component': sub_component}) - - if is_new_join: + device = self._devices.pop(device.ieee, None) + self._device_registry.pop(device.ieee, None) + if device is not None: + self._hass.async_create_task(device.async_unsub_dispatcher()) async_dispatcher_send( self._hass, - ZHA_DISCOVERY_NEW.format(component), - discovery_info + "{}_{}".format(SIGNAL_REMOVE, str(device.ieee)) ) - else: - self._hass.data[DATA_ZHA][component][cluster_key] = discovery_info + + def get_device(self, ieee_str): + """Return ZHADevice for given ieee.""" + ieee = convert_ieee(ieee_str) + return self._devices.get(ieee) + + def get_entity_reference(self, entity_id): + """Return entity reference for given entity_id if found.""" + for entity_reference in itertools.chain.from_iterable( + self.device_registry.values()): + if entity_id == entity_reference.reference_id: + return entity_reference + + @property + def devices(self): + """Return devices.""" + return self._devices + + @property + def device_registry(self): + """Return entities by ieee.""" + return self._device_registry + + def register_entity_reference( + self, ieee, reference_id, zha_device, cluster_channels, + device_info): + """Record the creation of a hass entity associated with ieee.""" + self._device_registry[ieee].append( + EntityReference( + reference_id=reference_id, + zha_device=zha_device, + cluster_channels=cluster_channels, + device_info=device_info + ) + ) + + async def _get_or_create_device(self, zigpy_device): + """Get or create a ZHA device.""" + zha_device = self._devices.get(zigpy_device.ieee) + if zha_device is None: + zha_device = ZHADevice(self._hass, zigpy_device, self) + self._devices[zigpy_device.ieee] = zha_device + return zha_device + + async def async_device_became_available( + self, sender, is_reply, profile, cluster, src_ep, dst_ep, tsn, + command_id, args): + """Handle tasks when a device becomes available.""" + self.async_update_device(sender) + + def async_update_device(self, sender): + """Update device that has just become available.""" + if sender.ieee in self.devices: + device = self.devices[sender.ieee] + # avoid a race condition during new joins + if device.status is DeviceStatus.INITIALIZED: + device.update_available(True) + + async def async_device_initialized(self, device, is_new_join): + """Handle device joined and basic information discovered (async).""" + zha_device = await self._get_or_create_device(device) + discovery_infos = [] + endpoint_tasks = [] + for endpoint_id, endpoint in device.endpoints.items(): + endpoint_tasks.append(self._async_process_endpoint( + endpoint_id, endpoint, discovery_infos, device, zha_device, + is_new_join + )) + await asyncio.gather(*endpoint_tasks) + + await zha_device.async_initialize(from_cache=(not is_new_join)) + + discovery_tasks = [] + for discovery_info in discovery_infos: + discovery_tasks.append(_dispatch_discovery_info( + self._hass, + is_new_join, + discovery_info + )) + await asyncio.gather(*discovery_tasks) + + device_entity = _create_device_entity(zha_device) + await self._component.async_add_entities([device_entity]) + + if is_new_join: + # because it's a new join we can immediately mark the device as + # available and we already loaded fresh state above + zha_device.update_available(True) + elif not zha_device.available and zha_device.power_source is not None\ + and zha_device.power_source != BasicChannel.BATTERY: + # the device is currently marked unavailable and it isn't a battery + # powered device so we should be able to update it now + _LOGGER.debug( + "attempting to request fresh state for %s %s", + zha_device.name, + "with power source: {}".format( + BasicChannel.POWER_SOURCES.get(zha_device.power_source) + ) + ) + await zha_device.async_initialize(from_cache=False) + + async def _async_process_endpoint( + self, endpoint_id, endpoint, discovery_infos, device, zha_device, + is_new_join): + """Process an endpoint on a zigpy device.""" + import zigpy.profiles + + if endpoint_id == 0: # ZDO + await _create_cluster_channel( + endpoint, + zha_device, + is_new_join, + channel_class=ZDOChannel + ) + return + + component = None + profile_clusters = ([], []) + device_key = "{}-{}".format(device.ieee, endpoint_id) + node_config = {} + if CONF_DEVICE_CONFIG in self._config: + node_config = self._config[CONF_DEVICE_CONFIG].get( + device_key, {} + ) + + if endpoint.profile_id in zigpy.profiles.PROFILES: + profile = zigpy.profiles.PROFILES[endpoint.profile_id] + if zha_const.DEVICE_CLASS.get(endpoint.profile_id, + {}).get(endpoint.device_type, + None): + profile_clusters = profile.CLUSTERS[endpoint.device_type] + profile_info = zha_const.DEVICE_CLASS[endpoint.profile_id] + component = profile_info[endpoint.device_type] + + if ha_const.CONF_TYPE in node_config: + component = node_config[ha_const.CONF_TYPE] + profile_clusters = zha_const.COMPONENT_CLUSTERS[component] + + if component and component in COMPONENTS: + profile_match = await _handle_profile_match( + self._hass, endpoint, profile_clusters, zha_device, + component, device_key, is_new_join) + discovery_infos.append(profile_match) + + discovery_infos.extend(await _handle_single_cluster_matches( + self._hass, + endpoint, + zha_device, + profile_clusters, + device_key, + is_new_join + )) + + +async def _create_cluster_channel(cluster, zha_device, is_new_join, + channels=None, channel_class=None): + """Create a cluster channel and attach it to a device.""" + if channel_class is None: + channel_class = ZIGBEE_CHANNEL_REGISTRY.get(cluster.cluster_id, + AttributeListeningChannel) + channel = channel_class(cluster, zha_device) + if is_new_join: + await channel.async_configure() + zha_device.add_cluster_channel(channel) + if channels is not None: + channels.append(channel) + + +async def _dispatch_discovery_info(hass, is_new_join, discovery_info): + """Dispatch or store discovery information.""" + if not discovery_info['channels']: + _LOGGER.warning( + "there are no channels in the discovery info: %s", discovery_info) + return + component = discovery_info['component'] + if is_new_join: + async_dispatcher_send( + hass, + ZHA_DISCOVERY_NEW.format(component), + discovery_info + ) + else: + hass.data[DATA_ZHA][component][discovery_info['unique_id']] = \ + discovery_info + + +async def _handle_profile_match(hass, endpoint, profile_clusters, zha_device, + component, device_key, is_new_join): + """Dispatch a profile match to the appropriate HA component.""" + in_clusters = [endpoint.in_clusters[c] + for c in profile_clusters[0] + if c in endpoint.in_clusters] + out_clusters = [endpoint.out_clusters[c] + for c in profile_clusters[1] + if c in endpoint.out_clusters] + + channels = [] + cluster_tasks = [] + + for cluster in in_clusters: + cluster_tasks.append(_create_cluster_channel( + cluster, zha_device, is_new_join, channels=channels)) + + for cluster in out_clusters: + cluster_tasks.append(_create_cluster_channel( + cluster, zha_device, is_new_join, channels=channels)) + + await asyncio.gather(*cluster_tasks) + + discovery_info = { + 'unique_id': device_key, + 'zha_device': zha_device, + 'channels': channels, + 'component': component + } + + if component == 'binary_sensor': + discovery_info.update({SENSOR_TYPE: UNKNOWN}) + cluster_ids = [] + cluster_ids.extend(profile_clusters[0]) + cluster_ids.extend(profile_clusters[1]) + for cluster_id in cluster_ids: + if cluster_id in BINARY_SENSOR_TYPES: + discovery_info.update({ + SENSOR_TYPE: BINARY_SENSOR_TYPES.get( + cluster_id, UNKNOWN) + }) + break + + return discovery_info + + +async def _handle_single_cluster_matches(hass, endpoint, zha_device, + profile_clusters, device_key, + is_new_join): + """Dispatch single cluster matches to HA components.""" + cluster_matches = [] + cluster_match_tasks = [] + event_channel_tasks = [] + for cluster in endpoint.in_clusters.values(): + # don't let profiles prevent these channels from being created + if cluster.cluster_id in NO_SENSOR_CLUSTERS: + cluster_match_tasks.append(_handle_channel_only_cluster_match( + zha_device, + cluster, + is_new_join, + )) + + if cluster.cluster_id not in profile_clusters[0]: + cluster_match_tasks.append(_handle_single_cluster_match( + hass, + zha_device, + cluster, + device_key, + zha_const.SINGLE_INPUT_CLUSTER_DEVICE_CLASS, + is_new_join, + )) + + for cluster in endpoint.out_clusters.values(): + if cluster.cluster_id not in profile_clusters[1]: + cluster_match_tasks.append(_handle_single_cluster_match( + hass, + zha_device, + cluster, + device_key, + zha_const.SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS, + is_new_join, + )) + + if cluster.cluster_id in EVENT_RELAY_CLUSTERS: + event_channel_tasks.append(_create_cluster_channel( + cluster, + zha_device, + is_new_join, + channel_class=EventRelayChannel + )) + await asyncio.gather(*event_channel_tasks) + cluster_match_results = await asyncio.gather(*cluster_match_tasks) + for cluster_match in cluster_match_results: + if cluster_match is not None: + cluster_matches.append(cluster_match) + return cluster_matches + + +async def _handle_channel_only_cluster_match( + zha_device, cluster, is_new_join): + """Handle a channel only cluster match.""" + await _create_cluster_channel(cluster, zha_device, is_new_join) + + +async def _handle_single_cluster_match(hass, zha_device, cluster, device_key, + device_classes, is_new_join): + """Dispatch a single cluster match to a HA component.""" + component = None # sub_component = None + for cluster_type, candidate_component in device_classes.items(): + if isinstance(cluster, cluster_type): + component = candidate_component + break + + if component is None or component not in COMPONENTS: + return + channels = [] + await _create_cluster_channel(cluster, zha_device, is_new_join, + channels=channels) + + cluster_key = "{}-{}".format(device_key, cluster.cluster_id) + discovery_info = { + 'unique_id': cluster_key, + 'zha_device': zha_device, + 'channels': channels, + 'entity_suffix': '_{}'.format(cluster.cluster_id), + 'component': component + } + + if component == 'sensor': + discovery_info.update({ + SENSOR_TYPE: SENSOR_TYPES.get(cluster.cluster_id, GENERIC) + }) + if component == 'binary_sensor': + discovery_info.update({ + SENSOR_TYPE: BINARY_SENSOR_TYPES.get(cluster.cluster_id, UNKNOWN) + }) + + return discovery_info + + +def _create_device_entity(zha_device): + """Create ZHADeviceEntity.""" + device_entity_channels = [] + if POWER_CONFIGURATION_CHANNEL in zha_device.cluster_channels: + channel = zha_device.cluster_channels.get(POWER_CONFIGURATION_CHANNEL) + device_entity_channels.append(channel) + return ZhaDeviceEntity(zha_device, device_entity_channels) def establish_device_mappings(): @@ -266,19 +426,20 @@ def establish_device_mappings(): These cannot be module level, as importing bellows must be done in a in a function. """ - from zigpy import zcl, quirks + from zigpy import zcl from zigpy.profiles import PROFILES, zha, zll - from ..sensor import RelativeHumiditySensor if zha.PROFILE_ID not in DEVICE_CLASS: DEVICE_CLASS[zha.PROFILE_ID] = {} if zll.PROFILE_ID not in DEVICE_CLASS: DEVICE_CLASS[zll.PROFILE_ID] = {} - EVENTABLE_CLUSTERS.append(zcl.clusters.general.AnalogInput.cluster_id) - EVENTABLE_CLUSTERS.append(zcl.clusters.general.LevelControl.cluster_id) - EVENTABLE_CLUSTERS.append(zcl.clusters.general.MultistateInput.cluster_id) - EVENTABLE_CLUSTERS.append(zcl.clusters.general.OnOff.cluster_id) + EVENT_RELAY_CLUSTERS.append(zcl.clusters.general.LevelControl.cluster_id) + EVENT_RELAY_CLUSTERS.append(zcl.clusters.general.OnOff.cluster_id) + + NO_SENSOR_CLUSTERS.append(zcl.clusters.general.Basic.cluster_id) + NO_SENSOR_CLUSTERS.append( + zcl.clusters.general.PowerConfiguration.cluster_id) DEVICE_CLASS[zha.PROFILE_ID].update({ zha.DeviceType.ON_OFF_SWITCH: 'binary_sensor', @@ -293,6 +454,7 @@ def establish_device_mappings(): zha.DeviceType.DIMMER_SWITCH: 'binary_sensor', zha.DeviceType.COLOR_DIMMER_SWITCH: 'binary_sensor', }) + DEVICE_CLASS[zll.PROFILE_ID].update({ zll.DeviceType.ON_OFF_LIGHT: 'light', zll.DeviceType.ON_OFF_PLUGIN_UNIT: 'switch', @@ -316,19 +478,112 @@ def establish_device_mappings(): zcl.clusters.measurement.IlluminanceMeasurement: 'sensor', zcl.clusters.smartenergy.Metering: 'sensor', zcl.clusters.homeautomation.ElectricalMeasurement: 'sensor', - zcl.clusters.general.PowerConfiguration: 'sensor', zcl.clusters.security.IasZone: 'binary_sensor', zcl.clusters.measurement.OccupancySensing: 'binary_sensor', zcl.clusters.hvac.Fan: 'fan', }) + SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS.update({ zcl.clusters.general.OnOff: 'binary_sensor', }) - # A map of device/cluster to component/sub-component - CUSTOM_CLUSTER_MAPPINGS.update({ - (quirks.smartthings.SmartthingsTemperatureHumiditySensor, 64581): - ('sensor', RelativeHumiditySensor) + SENSOR_TYPES.update({ + zcl.clusters.measurement.RelativeHumidity.cluster_id: HUMIDITY, + zcl.clusters.measurement.TemperatureMeasurement.cluster_id: + TEMPERATURE, + zcl.clusters.measurement.PressureMeasurement.cluster_id: PRESSURE, + zcl.clusters.measurement.IlluminanceMeasurement.cluster_id: + ILLUMINANCE, + zcl.clusters.smartenergy.Metering.cluster_id: METERING, + zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id: + ELECTRICAL_MEASUREMENT, + }) + + BINARY_SENSOR_TYPES.update({ + zcl.clusters.measurement.OccupancySensing.cluster_id: OCCUPANCY, + zcl.clusters.security.IasZone.cluster_id: ZONE, + zcl.clusters.general.OnOff.cluster_id: OPENING + }) + + CLUSTER_REPORT_CONFIGS.update({ + zcl.clusters.general.Alarms.cluster_id: [], + zcl.clusters.general.Basic.cluster_id: [], + zcl.clusters.general.Commissioning.cluster_id: [], + zcl.clusters.general.Identify.cluster_id: [], + zcl.clusters.general.Groups.cluster_id: [], + zcl.clusters.general.Scenes.cluster_id: [], + zcl.clusters.general.Partition.cluster_id: [], + zcl.clusters.general.Ota.cluster_id: [], + zcl.clusters.general.PowerProfile.cluster_id: [], + zcl.clusters.general.ApplianceControl.cluster_id: [], + zcl.clusters.general.PollControl.cluster_id: [], + zcl.clusters.general.GreenPowerProxy.cluster_id: [], + zcl.clusters.general.OnOffConfiguration.cluster_id: [], + zcl.clusters.general.OnOff.cluster_id: [{ + 'attr': 'on_off', + 'config': REPORT_CONFIG_IMMEDIATE + }], + zcl.clusters.general.LevelControl.cluster_id: [{ + 'attr': 'current_level', + 'config': REPORT_CONFIG_ASAP + }], + zcl.clusters.lighting.Color.cluster_id: [{ + 'attr': 'current_x', + 'config': REPORT_CONFIG_DEFAULT + }, { + 'attr': 'current_y', + 'config': REPORT_CONFIG_DEFAULT + }, { + 'attr': 'color_temperature', + 'config': REPORT_CONFIG_DEFAULT + }], + zcl.clusters.measurement.RelativeHumidity.cluster_id: [{ + 'attr': 'measured_value', + 'config': ( + REPORT_CONFIG_MIN_INT, + REPORT_CONFIG_MAX_INT, + 50 + ) + }], + zcl.clusters.measurement.TemperatureMeasurement.cluster_id: [{ + 'attr': 'measured_value', + 'config': ( + REPORT_CONFIG_MIN_INT, + REPORT_CONFIG_MAX_INT, + 50 + ) + }], + zcl.clusters.measurement.PressureMeasurement.cluster_id: [{ + 'attr': 'measured_value', + 'config': REPORT_CONFIG_DEFAULT + }], + zcl.clusters.measurement.IlluminanceMeasurement.cluster_id: [{ + 'attr': 'measured_value', + 'config': REPORT_CONFIG_DEFAULT + }], + zcl.clusters.smartenergy.Metering.cluster_id: [{ + 'attr': 'instantaneous_demand', + 'config': REPORT_CONFIG_DEFAULT + }], + zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id: [{ + 'attr': 'active_power', + 'config': REPORT_CONFIG_DEFAULT + }], + zcl.clusters.general.PowerConfiguration.cluster_id: [{ + 'attr': 'battery_voltage', + 'config': REPORT_CONFIG_DEFAULT + }, { + 'attr': 'battery_percentage_remaining', + 'config': REPORT_CONFIG_DEFAULT + }], + zcl.clusters.measurement.OccupancySensing.cluster_id: [{ + 'attr': 'occupancy', + 'config': REPORT_CONFIG_IMMEDIATE + }], + zcl.clusters.hvac.Fan.cluster_id: [{ + 'attr': 'fan_mode', + 'config': REPORT_CONFIG_OP + }], }) # A map of hass components to all Zigbee clusters it could use diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 6957edc4f3f..643e44ada1b 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -6,7 +6,7 @@ https://home-assistant.io/components/zha/ """ import asyncio import logging - +from concurrent.futures import TimeoutError as Timeout from .const import ( DEFAULT_BAUDRATE, REPORT_CONFIG_MAX_INT, REPORT_CONFIG_MIN_INT, REPORT_CONFIG_RPT_CHANGE, RadioType) @@ -48,7 +48,7 @@ async def bind_cluster(entity_id, cluster): _LOGGER.debug( "%s: bound '%s' cluster: %s", entity_id, cluster_name, res[0] ) - except DeliveryError as ex: + except (DeliveryError, Timeout) as ex: _LOGGER.debug( "%s: Failed to bind '%s' cluster: %s", entity_id, cluster_name, str(ex) @@ -68,7 +68,12 @@ async def configure_reporting(entity_id, cluster, attr, from zigpy.exceptions import DeliveryError attr_name = cluster.attributes.get(attr, [attr])[0] - attr_id = get_attr_id_by_name(cluster, attr_name) + + if isinstance(attr, str): + attr_id = get_attr_id_by_name(cluster, attr_name) + else: + attr_id = attr + cluster_name = cluster.ep_attribute kwargs = {} if manufacturer: @@ -82,7 +87,7 @@ async def configure_reporting(entity_id, cluster, attr, entity_id, attr_name, cluster_name, min_report, max_report, reportable_change, res ) - except DeliveryError as ex: + except (DeliveryError, Timeout) as ex: _LOGGER.debug( "%s: failed to set reporting for '%s' attr on '%s' cluster: %s", entity_id, attr_name, cluster_name, str(ex) diff --git a/homeassistant/components/zha/core/listeners.py b/homeassistant/components/zha/core/listeners.py deleted file mode 100644 index 4f60ea83d6f..00000000000 --- a/homeassistant/components/zha/core/listeners.py +++ /dev/null @@ -1,145 +0,0 @@ -""" -Cluster listeners for Zigbee Home Automation. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ -""" - -import logging - -from homeassistant.core import callback -from .const import SIGNAL_ATTR_UPDATED - -_LOGGER = logging.getLogger(__name__) - - -def parse_and_log_command(entity_id, cluster, tsn, command_id, args): - """Parse and log a zigbee cluster command.""" - cmd = cluster.server_commands.get(command_id, [command_id])[0] - _LOGGER.debug( - "%s: received '%s' command with %s args on cluster_id '%s' tsn '%s'", - entity_id, - cmd, - args, - cluster.cluster_id, - tsn - ) - return cmd - - -class ClusterListener: - """Listener for a Zigbee cluster.""" - - def __init__(self, entity, cluster): - """Initialize ClusterListener.""" - self._entity = entity - self._cluster = cluster - - def cluster_command(self, tsn, command_id, args): - """Handle commands received to this cluster.""" - pass - - def attribute_updated(self, attrid, value): - """Handle attribute updates on this cluster.""" - pass - - def zdo_command(self, *args, **kwargs): - """Handle ZDO commands on this cluster.""" - pass - - def zha_send_event(self, cluster, command, args): - """Relay entity events to hass.""" - pass # don't let entities fire events - - -class OnOffListener(ClusterListener): - """Listener for the OnOff Zigbee cluster.""" - - ON_OFF = 0 - - def cluster_command(self, tsn, command_id, args): - """Handle commands received to this cluster.""" - cmd = parse_and_log_command( - self._entity.entity_id, - self._cluster, - tsn, - command_id, - args - ) - - if cmd in ('off', 'off_with_effect'): - self._entity.set_state(False) - elif cmd in ('on', 'on_with_recall_global_scene', 'on_with_timed_off'): - self._entity.set_state(True) - elif cmd == 'toggle': - self._entity.set_state(not self._entity.is_on) - - def attribute_updated(self, attrid, value): - """Handle attribute updates on this cluster.""" - if attrid == self.ON_OFF: - self._entity.set_state(bool(value)) - - -class LevelListener(ClusterListener): - """Listener for the LevelControl Zigbee cluster.""" - - CURRENT_LEVEL = 0 - - def cluster_command(self, tsn, command_id, args): - """Handle commands received to this cluster.""" - cmd = parse_and_log_command( - self._entity.entity_id, - self._cluster, - tsn, - command_id, - args - ) - - if cmd in ('move_to_level', 'move_to_level_with_on_off'): - self._entity.set_level(args[0]) - elif cmd in ('move', 'move_with_on_off'): - # We should dim slowly -- for now, just step once - rate = args[1] - if args[0] == 0xff: - rate = 10 # Should read default move rate - self._entity.move_level(-rate if args[0] else rate) - elif cmd in ('step', 'step_with_on_off'): - # Step (technically may change on/off) - self._entity.move_level(-args[1] if args[0] else args[1]) - - def attribute_updated(self, attrid, value): - """Handle attribute updates on this cluster.""" - if attrid == self.CURRENT_LEVEL: - self._entity.set_level(value) - - -class EventRelayListener(ClusterListener): - """Event relay that can be attached to zigbee clusters.""" - - name = 'event_relay' - - @callback - def attribute_updated(self, attrid, value): - """Handle an attribute updated on this cluster.""" - self.zha_send_event( - self._cluster, - SIGNAL_ATTR_UPDATED, - { - 'attribute_id': attrid, - 'attribute_name': self._cluster.attributes.get( - attrid, - ['Unknown'])[0], - 'value': value - } - ) - - @callback - def cluster_command(self, tsn, command_id, args): - """Handle a cluster command received on this cluster.""" - if self._cluster.server_commands is not None and \ - self._cluster.server_commands.get(command_id) is not None: - self.zha_send_event( - self._cluster, - self._cluster.server_commands.get(command_id)[0], - args - ) diff --git a/homeassistant/components/zha/device_entity.py b/homeassistant/components/zha/device_entity.py index 2d2a5d76b81..5632c849d59 100644 --- a/homeassistant/components/zha/device_entity.py +++ b/homeassistant/components/zha/device_entity.py @@ -5,78 +5,146 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/zha/ """ +import logging import time -from homeassistant.helpers import entity +from homeassistant.core import callback from homeassistant.util import slugify +from .entity import ZhaEntity +from .const import POWER_CONFIGURATION_CHANNEL, SIGNAL_STATE_ATTR + +_LOGGER = logging.getLogger(__name__) + +BATTERY_SIZES = { + 0: 'No battery', + 1: 'Built in', + 2: 'Other', + 3: 'AA', + 4: 'AAA', + 5: 'C', + 6: 'D', + 7: 'CR2', + 8: 'CR123A', + 9: 'CR2450', + 10: 'CR2032', + 11: 'CR1632', + 255: 'Unknown' +} + +STATE_ONLINE = 'online' +STATE_OFFLINE = 'offline' -class ZhaDeviceEntity(entity.Entity): +class ZhaDeviceEntity(ZhaEntity): """A base class for ZHA devices.""" - def __init__(self, device, manufacturer, model, application_listener, - keepalive_interval=7200, **kwargs): + def __init__(self, zha_device, channels, keepalive_interval=7200, + **kwargs): """Init ZHA endpoint entity.""" - self._device_state_attributes = { - 'nwk': '0x{0:04x}'.format(device.nwk), - 'ieee': str(device.ieee), - 'lqi': device.lqi, - 'rssi': device.rssi, - } - - ieee = device.ieee + ieee = zha_device.ieee ieeetail = ''.join(['%02x' % (o, ) for o in ieee[-4:]]) - if manufacturer is not None and model is not None: - self._unique_id = "{}_{}_{}".format( - slugify(manufacturer), - slugify(model), + unique_id = None + if zha_device.manufacturer is not None and \ + zha_device.model is not None: + unique_id = "{}_{}_{}".format( + slugify(zha_device.manufacturer), + slugify(zha_device.model), ieeetail, ) - self._device_state_attributes['friendly_name'] = "{} {}".format( - manufacturer, - model, - ) else: - self._unique_id = str(ieeetail) + unique_id = str(ieeetail) + + kwargs['component'] = 'zha' + super().__init__(unique_id, zha_device, channels, skip_entity_id=True, + **kwargs) - self._device = device - self._state = 'offline' self._keepalive_interval = keepalive_interval - - application_listener.register_entity(ieee, self) - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return self._unique_id + self._device_state_attributes.update({ + 'nwk': '0x{0:04x}'.format(zha_device.nwk), + 'ieee': str(zha_device.ieee), + 'lqi': zha_device.lqi, + 'rssi': zha_device.rssi, + }) + self._should_poll = True + self._battery_channel = self.cluster_channels.get( + POWER_CONFIGURATION_CHANNEL) @property def state(self) -> str: """Return the state of the entity.""" return self._state + @property + def available(self): + """Return True if device is available.""" + return self._zha_device.available + @property def device_state_attributes(self): """Return device specific state attributes.""" update_time = None - if self._device.last_seen is not None and self._state == 'offline': - time_struct = time.localtime(self._device.last_seen) + device = self._zha_device + if device.last_seen is not None and not self.available: + time_struct = time.localtime(device.last_seen) update_time = time.strftime("%Y-%m-%dT%H:%M:%S", time_struct) self._device_state_attributes['last_seen'] = update_time if ('last_seen' in self._device_state_attributes and - self._state != 'offline'): + self.available): del self._device_state_attributes['last_seen'] - self._device_state_attributes['lqi'] = self._device.lqi - self._device_state_attributes['rssi'] = self._device.rssi + self._device_state_attributes['lqi'] = device.lqi + self._device_state_attributes['rssi'] = device.rssi return self._device_state_attributes + async def async_added_to_hass(self): + """Run when about to be added to hass.""" + await super().async_added_to_hass() + if self._battery_channel: + await self.async_accept_signal( + self._battery_channel, SIGNAL_STATE_ATTR, + self.async_update_state_attribute) + # only do this on add to HA because it is static + await self._async_init_battery_values() + async def async_update(self): """Handle polling.""" - if self._device.last_seen is None: - self._state = 'offline' + if self._zha_device.last_seen is None: + self._zha_device.update_available(False) else: - difference = time.time() - self._device.last_seen + difference = time.time() - self._zha_device.last_seen if difference > self._keepalive_interval: - self._state = 'offline' + self._zha_device.update_available(False) else: - self._state = 'online' + self._zha_device.update_available(True) + if self._battery_channel: + await self.async_get_latest_battery_reading() + + @callback + def async_set_available(self, available): + """Set entity availability.""" + if available: + self._state = STATE_ONLINE + else: + self._state = STATE_OFFLINE + super().async_set_available(available) + + async def _async_init_battery_values(self): + """Get initial battery level and battery info from channel cache.""" + battery_size = await self._battery_channel.get_attribute_value( + 'battery_size') + if battery_size is not None: + self._device_state_attributes['battery_size'] = BATTERY_SIZES.get( + battery_size, 'Unknown') + + battery_quantity = await self._battery_channel.get_attribute_value( + 'battery_quantity') + if battery_quantity is not None: + self._device_state_attributes['battery_quantity'] = \ + battery_quantity + await self.async_get_latest_battery_reading() + + async def async_get_latest_battery_reading(self): + """Get the latest battery reading from channels cache.""" + battery = await self._battery_channel.get_attribute_value( + 'battery_percentage_remaining') + if battery is not None: + self._device_state_attributes['battery_level'] = battery diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index e112e32d592..2f5aed4ca29 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -4,20 +4,18 @@ Entity for Zigbee Home Automation. For more details about this component, please refer to the documentation at https://home-assistant.io/components/zha/ """ -import asyncio -import logging -from random import uniform -from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.core import callback +import logging + from homeassistant.helpers import entity from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import slugify + from .core.const import ( - DATA_ZHA, DATA_ZHA_BRIDGE_ID, DOMAIN, ATTR_CLUSTER_ID, ATTR_ATTRIBUTE, - ATTR_VALUE, ATTR_MANUFACTURER, ATTR_COMMAND, SERVER, ATTR_COMMAND_TYPE, - ATTR_ARGS, IN, OUT, CLIENT_COMMANDS, SERVER_COMMANDS) -from .core.helpers import bind_configure_reporting + DOMAIN, ATTR_MANUFACTURER, DATA_ZHA, DATA_ZHA_BRIDGE_ID, MODEL, NAME, + SIGNAL_REMOVE +) _LOGGER = logging.getLogger(__name__) @@ -29,287 +27,153 @@ class ZhaEntity(entity.Entity): _domain = None # Must be overridden by subclasses - def __init__(self, endpoint, in_clusters, out_clusters, manufacturer, - model, application_listener, unique_id, new_join=False, - **kwargs): + def __init__(self, unique_id, zha_device, channels, + skip_entity_id=False, **kwargs): """Init ZHA entity.""" - self._device_state_attributes = {} - self._name = None - ieee = endpoint.device.ieee - ieeetail = ''.join(['%02x' % (o, ) for o in ieee[-4:]]) - if manufacturer and model is not None: - self.entity_id = "{}.{}_{}_{}_{}{}".format( - self._domain, - slugify(manufacturer), - slugify(model), - ieeetail, - endpoint.endpoint_id, - kwargs.get(ENTITY_SUFFIX, ''), - ) - self._name = "{} {}".format(manufacturer, model) - else: - self.entity_id = "{}.zha_{}_{}{}".format( - self._domain, - ieeetail, - endpoint.endpoint_id, - kwargs.get(ENTITY_SUFFIX, ''), - ) - - self._endpoint = endpoint - self._in_clusters = in_clusters - self._out_clusters = out_clusters - self._new_join = new_join - self._state = None + self._force_update = False + self._should_poll = False self._unique_id = unique_id - - # Normally the entity itself is the listener. Sub-classes may set this - # to a dict of cluster ID -> listener to receive messages for specific - # clusters separately - self._in_listeners = {} - self._out_listeners = {} - - self._initialized = False - self.manufacturer_code = None - application_listener.register_entity(ieee, self) - - async def get_clusters(self): - """Get zigbee clusters from this entity.""" - return { - IN: self._in_clusters, - OUT: self._out_clusters - } - - async def _get_cluster(self, cluster_id, cluster_type=IN): - """Get zigbee cluster from this entity.""" - if cluster_type == IN: - cluster = self._in_clusters[cluster_id] - else: - cluster = self._out_clusters[cluster_id] - if cluster is None: - _LOGGER.warning('in_cluster with id: %s not found on entity: %s', - cluster_id, self.entity_id) - return cluster - - async def get_cluster_attributes(self, cluster_id, cluster_type=IN): - """Get zigbee attributes for specified cluster.""" - cluster = await self._get_cluster(cluster_id, cluster_type) - if cluster is None: - return - return cluster.attributes - - async def write_zigbe_attribute(self, cluster_id, attribute, value, - cluster_type=IN, manufacturer=None): - """Write a value to a zigbee attribute for a cluster in this entity.""" - cluster = await self._get_cluster(cluster_id, cluster_type) - if cluster is None: - return - - from zigpy.exceptions import DeliveryError - try: - response = await cluster.write_attributes( - {attribute: value}, - manufacturer=manufacturer + self._name = None + if zha_device.manufacturer and zha_device.model is not None: + self._name = "{} {}".format( + zha_device.manufacturer, + zha_device.model ) - _LOGGER.debug( - 'set: %s for attr: %s to cluster: %s for entity: %s - res: %s', - value, - attribute, - cluster_id, - self.entity_id, - response - ) - return response - except DeliveryError as exc: - _LOGGER.debug( - 'failed to set attribute: %s %s %s %s %s', - '{}: {}'.format(ATTR_VALUE, value), - '{}: {}'.format(ATTR_ATTRIBUTE, attribute), - '{}: {}'.format(ATTR_CLUSTER_ID, cluster_id), - '{}: {}'.format(ATTR_ENTITY_ID, self.entity_id), - exc - ) - - async def get_cluster_commands(self, cluster_id, cluster_type=IN): - """Get zigbee commands for specified cluster.""" - cluster = await self._get_cluster(cluster_id, cluster_type) - if cluster is None: - return - return { - CLIENT_COMMANDS: cluster.client_commands, - SERVER_COMMANDS: cluster.server_commands, - } - - async def issue_cluster_command(self, cluster_id, command, command_type, - args, cluster_type=IN, - manufacturer=None): - """Issue a command against specified zigbee cluster on this entity.""" - cluster = await self._get_cluster(cluster_id, cluster_type) - if cluster is None: - return - response = None - if command_type == SERVER: - response = await cluster.command(command, *args, - manufacturer=manufacturer, - expect_reply=True) - else: - response = await cluster.client_command(command, *args) - - _LOGGER.debug( - 'Issued cluster command: %s %s %s %s %s %s %s', - '{}: {}'.format(ATTR_CLUSTER_ID, cluster_id), - '{}: {}'.format(ATTR_COMMAND, command), - '{}: {}'.format(ATTR_COMMAND_TYPE, command_type), - '{}: {}'.format(ATTR_ARGS, args), - '{}: {}'.format(ATTR_CLUSTER_ID, cluster_type), - '{}: {}'.format(ATTR_MANUFACTURER, manufacturer), - '{}: {}'.format(ATTR_ENTITY_ID, self.entity_id) - ) - return response - - async def async_added_to_hass(self): - """Handle entity addition to hass. - - It is now safe to update the entity state - """ - for cluster_id, cluster in self._in_clusters.items(): - cluster.add_listener(self._in_listeners.get(cluster_id, self)) - for cluster_id, cluster in self._out_clusters.items(): - cluster.add_listener(self._out_listeners.get(cluster_id, self)) - - self._endpoint.device.zdo.add_listener(self) - - if self._new_join: - self.hass.async_create_task(self.async_configure()) - - self._initialized = True - - async def async_configure(self): - """Set cluster binding and attribute reporting.""" - for cluster_key, attrs in self.zcl_reporting_config.items(): - cluster = self._get_cluster_from_report_config(cluster_key) - if cluster is None: - continue - - manufacturer = None - if cluster.cluster_id >= 0xfc00 and self.manufacturer_code: - manufacturer = self.manufacturer_code - - skip_bind = False # bind cluster only for the 1st configured attr - for attr, details in attrs.items(): - min_report_interval, max_report_interval, change = details - await bind_configure_reporting( - self.entity_id, cluster, attr, - min_report=min_report_interval, - max_report=max_report_interval, - reportable_change=change, - skip_bind=skip_bind, - manufacturer=manufacturer + if not skip_entity_id: + ieee = zha_device.ieee + ieeetail = ''.join(['%02x' % (o, ) for o in ieee[-4:]]) + if zha_device.manufacturer and zha_device.model is not None: + self.entity_id = "{}.{}_{}_{}_{}{}".format( + self._domain, + slugify(zha_device.manufacturer), + slugify(zha_device.model), + ieeetail, + channels[0].cluster.endpoint.endpoint_id, + kwargs.get(ENTITY_SUFFIX, ''), ) - skip_bind = True - await asyncio.sleep(uniform(0.1, 0.5)) - _LOGGER.debug("%s: finished configuration", self.entity_id) - - def _get_cluster_from_report_config(self, cluster_key): - """Parse an entry from zcl_reporting_config dict.""" - from zigpy.zcl import Cluster as Zcl_Cluster - - cluster = None - if isinstance(cluster_key, Zcl_Cluster): - cluster = cluster_key - elif isinstance(cluster_key, str): - cluster = getattr(self._endpoint, cluster_key, None) - elif isinstance(cluster_key, int): - if cluster_key in self._in_clusters: - cluster = self._in_clusters[cluster_key] - elif cluster_key in self._out_clusters: - cluster = self._out_clusters[cluster_key] - elif issubclass(cluster_key, Zcl_Cluster): - cluster_id = cluster_key.cluster_id - if cluster_id in self._in_clusters: - cluster = self._in_clusters[cluster_id] - elif cluster_id in self._out_clusters: - cluster = self._out_clusters[cluster_id] - return cluster + else: + self.entity_id = "{}.zha_{}_{}{}".format( + self._domain, + ieeetail, + channels[0].cluster.endpoint.endpoint_id, + kwargs.get(ENTITY_SUFFIX, ''), + ) + self._state = None + self._device_state_attributes = {} + self._zha_device = zha_device + self.cluster_channels = {} + self._available = False + self._component = kwargs['component'] + self._unsubs = [] + for channel in channels: + self.cluster_channels[channel.name] = channel @property def name(self): """Return Entity's default name.""" return self._name - @property - def zcl_reporting_config(self): - """Return a dict of ZCL attribute reporting configuration. - - { - Cluster_Class: { - attr_id: (min_report_interval, max_report_interval, change), - attr_name: (min_rep_interval, max_rep_interval, change) - } - Cluster_Instance: { - attr_id: (min_report_interval, max_report_interval, change), - attr_name: (min_rep_interval, max_rep_interval, change) - } - cluster_id: { - attr_id: (min_report_interval, max_report_interval, change), - attr_name: (min_rep_interval, max_rep_interval, change) - } - 'cluster_name': { - attr_id: (min_report_interval, max_report_interval, change), - attr_name: (min_rep_interval, max_rep_interval, change) - } - } - """ - return {} - @property def unique_id(self) -> str: """Return a unique ID.""" return self._unique_id + @property + def zha_device(self): + """Return the zha device this entity is attached to.""" + return self._zha_device + @property def device_state_attributes(self): """Return device specific state attributes.""" return self._device_state_attributes + @property + def force_update(self) -> bool: + """Force update this entity.""" + return self._force_update + @property def should_poll(self) -> bool: - """Let ZHA handle polling.""" - return False - - @callback - def attribute_updated(self, attribute, value): - """Handle an attribute updated on this cluster.""" - pass - - @callback - def zdo_command(self, tsn, command_id, args): - """Handle a ZDO command received on this cluster.""" - pass - - @callback - def device_announce(self, device): - """Handle device_announce zdo event.""" - self.async_schedule_update_ha_state(force_refresh=True) - - @callback - def permit_duration(self, permit_duration): - """Handle permit_duration zdo event.""" - pass + """Poll state from device.""" + return self._should_poll @property def device_info(self): """Return a device description for device registry.""" - ieee = str(self._endpoint.device.ieee) + zha_device_info = self._zha_device.device_info + ieee = zha_device_info['ieee'] return { 'connections': {(CONNECTION_ZIGBEE, ieee)}, 'identifiers': {(DOMAIN, ieee)}, - ATTR_MANUFACTURER: self._endpoint.manufacturer, - 'model': self._endpoint.model, - 'name': self.name or ieee, + ATTR_MANUFACTURER: zha_device_info[ATTR_MANUFACTURER], + MODEL: zha_device_info[MODEL], + NAME: zha_device_info[NAME], 'via_hub': (DOMAIN, self.hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID]), } - @callback - def zha_send_event(self, cluster, command, args): - """Relay entity events to hass.""" - pass # don't relay events from entities + @property + def available(self): + """Return entity availability.""" + return self._available + + def async_set_available(self, available): + """Set entity availability.""" + self._available = available + self.async_schedule_update_ha_state() + + def async_update_state_attribute(self, key, value): + """Update a single device state attribute.""" + self._device_state_attributes.update({ + key: value + }) + self.async_schedule_update_ha_state() + + def async_set_state(self, state): + """Set the entity state.""" + pass + + async def async_added_to_hass(self): + """Run when about to be added to hass.""" + await super().async_added_to_hass() + await self.async_accept_signal( + None, "{}_{}".format(self.zha_device.available_signal, 'entity'), + self.async_set_available, + signal_override=True) + await self.async_accept_signal( + None, "{}_{}".format(SIGNAL_REMOVE, str(self.zha_device.ieee)), + self.async_remove, + signal_override=True + ) + self._zha_device.gateway.register_entity_reference( + self._zha_device.ieee, self.entity_id, self._zha_device, + self.cluster_channels, self.device_info) + + async def async_will_remove_from_hass(self) -> None: + """Disconnect entity object when removed.""" + for unsub in self._unsubs: + unsub() + + async def async_update(self): + """Retrieve latest state.""" + for channel in self.cluster_channels: + if hasattr(channel, 'async_update'): + await channel.async_update() + + async def async_accept_signal(self, channel, signal, func, + signal_override=False): + """Accept a signal from a channel.""" + unsub = None + if signal_override: + unsub = async_dispatcher_connect( + self.hass, + signal, + func + ) + else: + unsub = async_dispatcher_connect( + self.hass, + "{}_{}".format(channel.unique_id, signal), + func + ) + self._unsubs.append(unsub) diff --git a/homeassistant/components/zha/event.py b/homeassistant/components/zha/event.py deleted file mode 100644 index 7828a695a7b..00000000000 --- a/homeassistant/components/zha/event.py +++ /dev/null @@ -1,99 +0,0 @@ -""" -Event for Zigbee Home Automation. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ -""" -import logging - -from homeassistant.core import EventOrigin, callback -from homeassistant.util import slugify - -_LOGGER = logging.getLogger(__name__) - - -class ZhaEvent(): - """A base class for ZHA events.""" - - def __init__(self, hass, cluster, **kwargs): - """Init ZHA event.""" - self._hass = hass - self._cluster = cluster - cluster.add_listener(self) - ieee = cluster.endpoint.device.ieee - ieeetail = ''.join(['%02x' % (o, ) for o in ieee[-4:]]) - endpoint = cluster.endpoint - if endpoint.manufacturer and endpoint.model is not None: - self._unique_id = "{}.{}_{}_{}_{}{}".format( - 'zha_event', - slugify(endpoint.manufacturer), - slugify(endpoint.model), - ieeetail, - cluster.endpoint.endpoint_id, - kwargs.get('entity_suffix', ''), - ) - else: - self._unique_id = "{}.zha_{}_{}{}".format( - 'zha_event', - ieeetail, - cluster.endpoint.endpoint_id, - kwargs.get('entity_suffix', ''), - ) - - @callback - def attribute_updated(self, attribute, value): - """Handle an attribute updated on this cluster.""" - pass - - @callback - def zdo_command(self, tsn, command_id, args): - """Handle a ZDO command received on this cluster.""" - pass - - @callback - def cluster_command(self, tsn, command_id, args): - """Handle a cluster command received on this cluster.""" - pass - - @callback - def zha_send_event(self, cluster, command, args): - """Relay entity events to hass.""" - self._hass.bus.async_fire( - 'zha_event', - { - 'unique_id': self._unique_id, - 'command': command, - 'args': args - }, - EventOrigin.remote - ) - - -class ZhaRelayEvent(ZhaEvent): - """Event relay that can be attached to zigbee clusters.""" - - @callback - def attribute_updated(self, attribute, value): - """Handle an attribute updated on this cluster.""" - self.zha_send_event( - self._cluster, - 'attribute_updated', - { - 'attribute_id': attribute, - 'attribute_name': self._cluster.attributes.get( - attribute, - ['Unknown'])[0], - 'value': value - } - ) - - @callback - def cluster_command(self, tsn, command_id, args): - """Handle a cluster command received on this cluster.""" - if self._cluster.server_commands is not None and\ - self._cluster.server_commands.get(command_id) is not None: - self.zha_send_event( - self._cluster, - self._cluster.server_commands.get(command_id)[0], - args - ) diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index f6dbef50923..761dfaede1e 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -10,9 +10,10 @@ from homeassistant.components.fan import ( DOMAIN, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, SPEED_OFF, SUPPORT_SET_SPEED, FanEntity) from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .core import helpers from .core.const import ( - DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_OP, ZHA_DISCOVERY_NEW) + DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, FAN_CHANNEL, + SIGNAL_ATTR_UPDATED +) from .entity import ZhaEntity DEPENDENCIES = ['zha'] @@ -79,19 +80,17 @@ class ZhaFan(ZhaEntity, FanEntity): """Representation of a ZHA fan.""" _domain = DOMAIN - value_attribute = 0 # fan_mode - @property - def zcl_reporting_config(self) -> dict: - """Return a dict of attribute reporting configuration.""" - return { - self.cluster: {self.value_attribute: REPORT_CONFIG_OP} - } + def __init__(self, unique_id, zha_device, channels, **kwargs): + """Init this sensor.""" + super().__init__(unique_id, zha_device, channels, **kwargs) + self._fan_channel = self.cluster_channels.get(FAN_CHANNEL) - @property - def cluster(self): - """Fan ZCL Cluster.""" - return self._endpoint.fan + async def async_added_to_hass(self): + """Run when about to be added to hass.""" + await super().async_added_to_hass() + await self.async_accept_signal( + self._fan_channel, SIGNAL_ATTR_UPDATED, self.async_set_state) @property def supported_features(self) -> int: @@ -115,6 +114,16 @@ class ZhaFan(ZhaEntity, FanEntity): return False return self._state != SPEED_OFF + @property + def device_state_attributes(self): + """Return state attributes.""" + return self.state_attributes + + def async_set_state(self, state): + """Handle state update from channel.""" + self._state = VALUE_TO_SPEED.get(state, self._state) + self.async_schedule_update_ha_state() + async def async_turn_on(self, speed: str = None, **kwargs) -> None: """Turn the entity on.""" if speed is None: @@ -128,31 +137,5 @@ class ZhaFan(ZhaEntity, FanEntity): async def async_set_speed(self, speed: str) -> None: """Set the speed of the fan.""" - from zigpy.exceptions import DeliveryError - try: - await self._endpoint.fan.write_attributes( - {'fan_mode': SPEED_TO_VALUE[speed]} - ) - except DeliveryError as ex: - _LOGGER.error("%s: Could not set speed: %s", self.entity_id, ex) - return - - self._state = speed - self.async_schedule_update_ha_state() - - async def async_update(self): - """Retrieve latest state.""" - result = await helpers.safe_read(self.cluster, ['fan_mode'], - allow_cache=False, - only_cache=(not self._initialized)) - new_value = result.get('fan_mode', None) - self._state = VALUE_TO_SPEED.get(new_value, None) - - def attribute_updated(self, attribute, value): - """Handle attribute update from device.""" - attr_name = self.cluster.attributes.get(attribute, [attribute])[0] - _LOGGER.debug("%s: Attribute report '%s'[%s] = %s", - self.entity_id, self.cluster.name, attr_name, value) - if attribute == self.value_attribute: - self._state = VALUE_TO_SPEED.get(value, self._state) - self.async_schedule_update_ha_state() + await self._fan_channel.async_set_speed(SPEED_TO_VALUE[speed]) + self.async_set_state(speed) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 49a09112b31..efa6f679ae8 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -9,14 +9,12 @@ import logging from homeassistant.components import light from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.color as color_util -from .core import helpers -from .core.const import ( - DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_ASAP, REPORT_CONFIG_DEFAULT, - REPORT_CONFIG_IMMEDIATE, ZHA_DISCOVERY_NEW) +from .const import ( + DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, COLOR_CHANNEL, + ON_OFF_CHANNEL, LEVEL_CHANNEL, SIGNAL_ATTR_UPDATED, SIGNAL_SET_LEVEL + ) from .entity import ZhaEntity -from .core.listeners import ( - OnOffListener, LevelListener -) + _LOGGER = logging.getLogger(__name__) @@ -58,26 +56,6 @@ async def _async_setup_entities(hass, config_entry, async_add_entities, """Set up the ZHA lights.""" entities = [] for discovery_info in discovery_infos: - endpoint = discovery_info['endpoint'] - if hasattr(endpoint, 'light_color'): - caps = await helpers.safe_read( - endpoint.light_color, ['color_capabilities']) - discovery_info['color_capabilities'] = caps.get( - 'color_capabilities') - if discovery_info['color_capabilities'] is None: - # ZCL Version 4 devices don't support the color_capabilities - # attribute. In this version XY support is mandatory, but we - # need to probe to determine if the device supports color - # temperature. - discovery_info['color_capabilities'] = \ - CAPABILITIES_COLOR_XY - result = await helpers.safe_read( - endpoint.light_color, ['color_temperature']) - if (result.get('color_temperature') is not - UNSUPPORTED_ATTRIBUTE): - discovery_info['color_capabilities'] |= \ - CAPABILITIES_COLOR_TEMP - zha_light = Light(**discovery_info) entities.append(zha_light) @@ -89,34 +67,24 @@ class Light(ZhaEntity, light.Light): _domain = light.DOMAIN - def __init__(self, **kwargs): + def __init__(self, unique_id, zha_device, channels, **kwargs): """Initialize the ZHA light.""" - super().__init__(**kwargs) + super().__init__(unique_id, zha_device, channels, **kwargs) self._supported_features = 0 self._color_temp = None self._hs_color = None self._brightness = None - from zigpy.zcl.clusters.general import OnOff, LevelControl - self._in_listeners = { - OnOff.cluster_id: OnOffListener( - self, - self._in_clusters[OnOff.cluster_id] - ), - } + self._on_off_channel = self.cluster_channels.get(ON_OFF_CHANNEL) + self._level_channel = self.cluster_channels.get(LEVEL_CHANNEL) + self._color_channel = self.cluster_channels.get(COLOR_CHANNEL) - if LevelControl.cluster_id in self._in_clusters: + if self._level_channel: self._supported_features |= light.SUPPORT_BRIGHTNESS self._supported_features |= light.SUPPORT_TRANSITION self._brightness = 0 - self._in_listeners.update({ - LevelControl.cluster_id: LevelListener( - self, - self._in_clusters[LevelControl.cluster_id] - ) - }) - import zigpy.zcl.clusters as zcl_clusters - if zcl_clusters.lighting.Color.cluster_id in self._in_clusters: - color_capabilities = kwargs['color_capabilities'] + + if self._color_channel: + color_capabilities = self._color_channel.get_color_capabilities() if color_capabilities & CAPABILITIES_COLOR_TEMP: self._supported_features |= light.SUPPORT_COLOR_TEMP @@ -124,129 +92,26 @@ class Light(ZhaEntity, light.Light): self._supported_features |= light.SUPPORT_COLOR self._hs_color = (0, 0) - @property - def zcl_reporting_config(self) -> dict: - """Return attribute reporting configuration.""" - return { - 'on_off': {'on_off': REPORT_CONFIG_IMMEDIATE}, - 'level': {'current_level': REPORT_CONFIG_ASAP}, - 'light_color': { - 'current_x': REPORT_CONFIG_DEFAULT, - 'current_y': REPORT_CONFIG_DEFAULT, - 'color_temperature': REPORT_CONFIG_DEFAULT, - } - } - @property def is_on(self) -> bool: """Return true if entity is on.""" if self._state is None: return False - return bool(self._state) - - def set_state(self, state): - """Set the state.""" - self._state = state - self.async_schedule_update_ha_state() - - async def async_turn_on(self, **kwargs): - """Turn the entity on.""" - from zigpy.exceptions import DeliveryError - - duration = kwargs.get(light.ATTR_TRANSITION, DEFAULT_DURATION) - duration = duration * 10 # tenths of s - if light.ATTR_COLOR_TEMP in kwargs and \ - self.supported_features & light.SUPPORT_COLOR_TEMP: - temperature = kwargs[light.ATTR_COLOR_TEMP] - try: - res = await self._endpoint.light_color.move_to_color_temp( - temperature, duration) - _LOGGER.debug("%s: moved to %i color temp: %s", - self.entity_id, temperature, res) - except DeliveryError as ex: - _LOGGER.error("%s: Couldn't change color temp: %s", - self.entity_id, ex) - return - self._color_temp = temperature - - if light.ATTR_HS_COLOR in kwargs and \ - self.supported_features & light.SUPPORT_COLOR: - self._hs_color = kwargs[light.ATTR_HS_COLOR] - xy_color = color_util.color_hs_to_xy(*self._hs_color) - try: - res = await self._endpoint.light_color.move_to_color( - int(xy_color[0] * 65535), - int(xy_color[1] * 65535), - duration, - ) - _LOGGER.debug("%s: moved XY color to (%1.2f, %1.2f): %s", - self.entity_id, xy_color[0], xy_color[1], res) - except DeliveryError as ex: - _LOGGER.error("%s: Couldn't change color temp: %s", - self.entity_id, ex) - return - - if self._brightness is not None: - brightness = kwargs.get( - light.ATTR_BRIGHTNESS, self._brightness or 255) - # Move to level with on/off: - try: - res = await self._endpoint.level.move_to_level_with_on_off( - brightness, - duration - ) - _LOGGER.debug("%s: moved to %i level with on/off: %s", - self.entity_id, brightness, res) - except DeliveryError as ex: - _LOGGER.error("%s: Couldn't change brightness level: %s", - self.entity_id, ex) - return - self._state = 1 - self._brightness = brightness - self.async_schedule_update_ha_state() - return - - try: - res = await self._endpoint.on_off.on() - _LOGGER.debug("%s was turned on: %s", self.entity_id, res) - except DeliveryError as ex: - _LOGGER.error("%s: Unable to turn the light on: %s", - self.entity_id, ex) - return - - self._state = 1 - self.async_schedule_update_ha_state() - - async def async_turn_off(self, **kwargs): - """Turn the entity off.""" - from zigpy.exceptions import DeliveryError - duration = kwargs.get(light.ATTR_TRANSITION) - try: - supports_level = self.supported_features & light.SUPPORT_BRIGHTNESS - if duration and supports_level: - res = await self._endpoint.level.move_to_level_with_on_off( - 0, duration*10 - ) - else: - res = await self._endpoint.on_off.off() - _LOGGER.debug("%s was turned off: %s", self.entity_id, res) - except DeliveryError as ex: - _LOGGER.error("%s: Unable to turn the light off: %s", - self.entity_id, ex) - return - - self._state = 0 - self.async_schedule_update_ha_state() + return self._state @property def brightness(self): - """Return the brightness of this light between 0..255.""" + """Return the brightness of this light.""" return self._brightness + @property + def device_state_attributes(self): + """Return state attributes.""" + return self.state_attributes + def set_level(self, value): """Set the brightness of this light between 0..255.""" - if value < 0 or value > 255: - return + value = max(0, min(255, value)) self._brightness = value self.async_schedule_update_ha_state() @@ -265,40 +130,82 @@ class Light(ZhaEntity, light.Light): """Flag supported features.""" return self._supported_features - async def async_update(self): - """Retrieve latest state.""" - result = await helpers.safe_read(self._endpoint.on_off, ['on_off'], - allow_cache=False, - only_cache=(not self._initialized)) - self._state = result.get('on_off', self._state) + def async_set_state(self, state): + """Set the state.""" + self._state = bool(state) + self.async_schedule_update_ha_state() - if self._supported_features & light.SUPPORT_BRIGHTNESS: - result = await helpers.safe_read(self._endpoint.level, - ['current_level'], - allow_cache=False, - only_cache=( - not self._initialized - )) - self._brightness = result.get('current_level', self._brightness) + async def async_added_to_hass(self): + """Run when about to be added to hass.""" + await super().async_added_to_hass() + await self.async_accept_signal( + self._on_off_channel, SIGNAL_ATTR_UPDATED, self.async_set_state) + if self._level_channel: + await self.async_accept_signal( + self._level_channel, SIGNAL_SET_LEVEL, self.set_level) - if self._supported_features & light.SUPPORT_COLOR_TEMP: - result = await helpers.safe_read(self._endpoint.light_color, - ['color_temperature'], - allow_cache=False, - only_cache=( - not self._initialized - )) - self._color_temp = result.get('color_temperature', - self._color_temp) + async def async_turn_on(self, **kwargs): + """Turn the entity on.""" + duration = kwargs.get(light.ATTR_TRANSITION, DEFAULT_DURATION) + duration = duration * 10 # tenths of s - if self._supported_features & light.SUPPORT_COLOR: - result = await helpers.safe_read(self._endpoint.light_color, - ['current_x', 'current_y'], - allow_cache=False, - only_cache=( - not self._initialized - )) - if 'current_x' in result and 'current_y' in result: - xy_color = (round(result['current_x']/65535, 3), - round(result['current_y']/65535, 3)) - self._hs_color = color_util.color_xy_to_hs(*xy_color) + if light.ATTR_COLOR_TEMP in kwargs and \ + self.supported_features & light.SUPPORT_COLOR_TEMP: + temperature = kwargs[light.ATTR_COLOR_TEMP] + success = await self._color_channel.move_to_color_temp( + temperature, duration) + if not success: + return + self._color_temp = temperature + + if light.ATTR_HS_COLOR in kwargs and \ + self.supported_features & light.SUPPORT_COLOR: + hs_color = kwargs[light.ATTR_HS_COLOR] + xy_color = color_util.color_hs_to_xy(*hs_color) + success = await self._color_channel.move_to_color( + int(xy_color[0] * 65535), + int(xy_color[1] * 65535), + duration, + ) + if not success: + return + self._hs_color = hs_color + + if self._brightness is not None: + brightness = kwargs.get( + light.ATTR_BRIGHTNESS, self._brightness or 255) + success = await self._level_channel.move_to_level_with_on_off( + brightness, + duration + ) + if not success: + return + self._state = True + self._brightness = brightness + self.async_schedule_update_ha_state() + return + + success = await self._on_off_channel.on() + if not success: + return + + self._state = True + self.async_schedule_update_ha_state() + + async def async_turn_off(self, **kwargs): + """Turn the entity off.""" + duration = kwargs.get(light.ATTR_TRANSITION) + supports_level = self.supported_features & light.SUPPORT_BRIGHTNESS + success = None + if duration and supports_level: + success = await self._level_channel.move_to_level_with_on_off( + 0, + duration*10 + ) + else: + success = await self._on_off_channel.off() + _LOGGER.debug("%s was turned off: %s", self.entity_id, success) + if not success: + return + self._state = False + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index ae45fad0826..6dcdbb845dc 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -9,11 +9,11 @@ import logging from homeassistant.components.sensor import DOMAIN from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.util.temperature import convert as convert_temperature -from .core import helpers from .core.const import ( - DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_MAX_INT, - REPORT_CONFIG_MIN_INT, REPORT_CONFIG_RPT_CHANGE, ZHA_DISCOVERY_NEW) + DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, HUMIDITY, TEMPERATURE, + ILLUMINANCE, PRESSURE, METERING, ELECTRICAL_MEASUREMENT, + GENERIC, SENSOR_TYPE, ATTRIBUTE_CHANNEL, ELECTRICAL_MEASUREMENT_CHANNEL, + SIGNAL_ATTR_UPDATED, SIGNAL_STATE_ATTR) from .entity import ZhaEntity _LOGGER = logging.getLogger(__name__) @@ -21,6 +21,72 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['zha'] +# Formatter functions +def pass_through_formatter(value): + """No op update function.""" + return value + + +def temperature_formatter(value): + """Convert temperature data.""" + if value is None: + return None + return round(value / 100, 1) + + +def humidity_formatter(value): + """Return the state of the entity.""" + if value is None: + return None + return round(float(value) / 100, 1) + + +def active_power_formatter(value): + """Return the state of the entity.""" + if value is None: + return None + return round(float(value) / 10, 1) + + +def pressure_formatter(value): + """Return the state of the entity.""" + if value is None: + return None + + return round(float(value)) + + +FORMATTER_FUNC_REGISTRY = { + HUMIDITY: humidity_formatter, + TEMPERATURE: temperature_formatter, + PRESSURE: pressure_formatter, + ELECTRICAL_MEASUREMENT: active_power_formatter, + GENERIC: pass_through_formatter, +} + +UNIT_REGISTRY = { + HUMIDITY: '%', + TEMPERATURE: TEMP_CELSIUS, + PRESSURE: 'hPa', + ILLUMINANCE: 'lx', + METERING: 'W', + ELECTRICAL_MEASUREMENT: 'W', + GENERIC: None +} + +CHANNEL_REGISTRY = { + ELECTRICAL_MEASUREMENT: ELECTRICAL_MEASUREMENT_CHANNEL, +} + +POLLING_REGISTRY = { + ELECTRICAL_MEASUREMENT: True +} + +FORCE_UPDATE_REGISTRY = { + ELECTRICAL_MEASUREMENT: True +} + + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up Zigbee Home Automation sensors.""" @@ -56,279 +122,59 @@ async def _async_setup_entities(hass, config_entry, async_add_entities, async def make_sensor(discovery_info): """Create ZHA sensors factory.""" - from zigpy.zcl.clusters.measurement import ( - RelativeHumidity, TemperatureMeasurement, PressureMeasurement, - IlluminanceMeasurement - ) - from zigpy.zcl.clusters.smartenergy import Metering - from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement - from zigpy.zcl.clusters.general import PowerConfiguration - in_clusters = discovery_info['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 PowerConfiguration.cluster_id in in_clusters: - sensor = GenericBatterySensor(**discovery_info) - elif TemperatureMeasurement.cluster_id in in_clusters: - sensor = TemperatureSensor(**discovery_info) - elif PressureMeasurement.cluster_id in in_clusters: - sensor = PressureSensor(**discovery_info) - elif IlluminanceMeasurement.cluster_id in in_clusters: - sensor = IlluminanceMeasurementSensor(**discovery_info) - elif Metering.cluster_id in in_clusters: - sensor = MeteringSensor(**discovery_info) - elif ElectricalMeasurement.cluster_id in in_clusters: - sensor = ElectricalMeasurementSensor(**discovery_info) - return sensor - else: - sensor = Sensor(**discovery_info) - - return sensor + return Sensor(**discovery_info) class Sensor(ZhaEntity): """Base ZHA sensor.""" _domain = DOMAIN - value_attribute = 0 - min_report_interval = REPORT_CONFIG_MIN_INT - max_report_interval = REPORT_CONFIG_MAX_INT - min_reportable_change = REPORT_CONFIG_RPT_CHANGE - report_config = (min_report_interval, max_report_interval, - min_reportable_change) - def __init__(self, **kwargs): - """Init ZHA Sensor instance.""" - super().__init__(**kwargs) - self._cluster = list(kwargs['in_clusters'].values())[0] + def __init__(self, unique_id, zha_device, channels, **kwargs): + """Init this sensor.""" + super().__init__(unique_id, zha_device, channels, **kwargs) + sensor_type = kwargs.get(SENSOR_TYPE, GENERIC) + self._unit = UNIT_REGISTRY.get(sensor_type) + self._formatter_function = FORMATTER_FUNC_REGISTRY.get( + sensor_type, + pass_through_formatter + ) + self._force_update = FORCE_UPDATE_REGISTRY.get( + sensor_type, + False + ) + self._should_poll = POLLING_REGISTRY.get( + sensor_type, + False + ) + self._channel = self.cluster_channels.get( + CHANNEL_REGISTRY.get(sensor_type, ATTRIBUTE_CHANNEL) + ) + + async def async_added_to_hass(self): + """Run when about to be added to hass.""" + await super().async_added_to_hass() + await self.async_accept_signal( + self._channel, SIGNAL_ATTR_UPDATED, self.async_set_state) + await self.async_accept_signal( + self._channel, SIGNAL_STATE_ATTR, + self.async_update_state_attribute) @property - def zcl_reporting_config(self) -> dict: - """Return a dict of attribute reporting configuration.""" - return { - self.cluster: {self.value_attribute: self.report_config} - } - - @property - def cluster(self): - """Return Sensor's cluster.""" - return self._cluster + def unit_of_measurement(self): + """Return the unit of measurement of this entity.""" + return self._unit @property def state(self) -> str: """Return the state of the entity.""" + if self._state is None: + return None if isinstance(self._state, float): return str(round(self._state, 2)) return self._state - def attribute_updated(self, attribute, value): - """Handle attribute update from device.""" - _LOGGER.debug("Attribute updated: %s %s %s", self, attribute, value) - if attribute == self.value_attribute: - self._state = value - self.async_schedule_update_ha_state() - - async def async_update(self): - """Retrieve latest state.""" - result = await helpers.safe_read( - self.cluster, - [self.value_attribute], - allow_cache=False, - only_cache=(not self._initialized) - ) - self._state = result.get(self.value_attribute, self._state) - - -class GenericBatterySensor(Sensor): - """ZHA generic battery sensor.""" - - report_attribute = 32 - value_attribute = 33 - battery_sizes = { - 0: 'No battery', - 1: 'Built in', - 2: 'Other', - 3: 'AA', - 4: 'AAA', - 5: 'C', - 6: 'D', - 7: 'CR2', - 8: 'CR123A', - 9: 'CR2450', - 10: 'CR2032', - 11: 'CR1632', - 255: 'Unknown' - } - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity.""" - return '%' - - @property - def zcl_reporting_config(self) -> dict: - """Return a dict of attribute reporting configuration.""" - return { - self.cluster: { - self.value_attribute: self.report_config, - self.report_attribute: self.report_config - } - } - - async def async_update(self): - """Retrieve latest state.""" - _LOGGER.debug("%s async_update", self.entity_id) - - result = await helpers.safe_read( - self._endpoint.power, - [ - 'battery_size', - 'battery_quantity', - 'battery_percentage_remaining' - ], - allow_cache=False, - only_cache=(not self._initialized) - ) - self._device_state_attributes['battery_size'] = self.battery_sizes.get( - result.get('battery_size', 255), 'Unknown') - self._device_state_attributes['battery_quantity'] = result.get( - 'battery_quantity', 'Unknown') - self._state = result.get('battery_percentage_remaining', self._state) - - @property - def state(self): - """Return the state of the entity.""" - if self._state == 'unknown' or self._state is None: - return None - - return self._state - - -class TemperatureSensor(Sensor): - """ZHA temperature sensor.""" - - min_reportable_change = 50 # 0.5'C - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity.""" - return self.hass.config.units.temperature_unit - - @property - def state(self): - """Return the state of the entity.""" - if self._state is None: - return None - celsius = self._state / 100 - return round(convert_temperature(celsius, - TEMP_CELSIUS, - self.unit_of_measurement), - 1) - - -class RelativeHumiditySensor(Sensor): - """ZHA relative humidity sensor.""" - - min_reportable_change = 50 # 0.5% - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity.""" - return '%' - - @property - def state(self): - """Return the state of the entity.""" - if self._state is None: - return None - - return round(float(self._state) / 100, 1) - - -class PressureSensor(Sensor): - """ZHA pressure sensor.""" - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity.""" - return 'hPa' - - @property - def state(self): - """Return the state of the entity.""" - if self._state is None: - return None - - return round(float(self._state)) - - -class IlluminanceMeasurementSensor(Sensor): - """ZHA lux sensor.""" - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity.""" - return 'lx' - - @property - def state(self): - """Return the state of the entity.""" - return self._state - - -class MeteringSensor(Sensor): - """ZHA Metering sensor.""" - - value_attribute = 1024 - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity.""" - return 'W' - - @property - def state(self): - """Return the state of the entity.""" - if self._state is None: - return None - - return round(float(self._state)) - - -class ElectricalMeasurementSensor(Sensor): - """ZHA Electrical Measurement sensor.""" - - value_attribute = 1291 - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity.""" - return 'W' - - @property - def force_update(self) -> bool: - """Force update this entity.""" - return True - - @property - def state(self): - """Return the state of the entity.""" - if self._state is None: - return None - - return round(float(self._state) / 10, 1) - - @property - def should_poll(self) -> bool: - """Poll state from device.""" - return True - - async def async_update(self): - """Retrieve latest state.""" - _LOGGER.debug("%s async_update", self.entity_id) - - result = await helpers.safe_read( - self.cluster, ['active_power'], - allow_cache=False, only_cache=(not self._initialized)) - self._state = result.get('active_power', self._state) + def async_set_state(self, state): + """Handle state update from channel.""" + self._state = self._formatter_function(state) + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 09c20acd088..bdbdd7a6a76 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -8,9 +8,10 @@ import logging from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .core import helpers from .core.const import ( - DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_IMMEDIATE, ZHA_DISCOVERY_NEW) + DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, ON_OFF_CHANNEL, + SIGNAL_ATTR_UPDATED +) from .entity import ZhaEntity _LOGGER = logging.getLogger(__name__) @@ -55,69 +56,39 @@ class Switch(ZhaEntity, SwitchDevice): """ZHA switch.""" _domain = DOMAIN - value_attribute = 0 - def attribute_updated(self, attribute, value): - """Handle attribute update from device.""" - cluster = self._endpoint.on_off - attr_name = cluster.attributes.get(attribute, [attribute])[0] - _LOGGER.debug("%s: Attribute '%s' on cluster '%s' updated to %s", - self.entity_id, attr_name, cluster.ep_attribute, value) - if attribute == self.value_attribute: - self._state = value - self.async_schedule_update_ha_state() - - @property - def zcl_reporting_config(self) -> dict: - """Retrun a dict of attribute reporting configuration.""" - return { - self.cluster: {'on_off': REPORT_CONFIG_IMMEDIATE} - } - - @property - def cluster(self): - """Entity's cluster.""" - return self._endpoint.on_off + def __init__(self, **kwargs): + """Initialize the ZHA switch.""" + super().__init__(**kwargs) + self._on_off_channel = self.cluster_channels.get(ON_OFF_CHANNEL) @property def is_on(self) -> bool: """Return if the switch is on based on the statemachine.""" if self._state is None: return False - return bool(self._state) + return self._state async def async_turn_on(self, **kwargs): """Turn the entity on.""" - from zigpy.exceptions import DeliveryError - try: - res = await self._endpoint.on_off.on() - _LOGGER.debug("%s: turned 'on': %s", self.entity_id, res[1]) - except DeliveryError as ex: - _LOGGER.error("%s: Unable to turn the switch on: %s", - self.entity_id, ex) - return - - self._state = 1 - self.async_schedule_update_ha_state() + await self._on_off_channel.on() async def async_turn_off(self, **kwargs): """Turn the entity off.""" - from zigpy.exceptions import DeliveryError - try: - res = await self._endpoint.on_off.off() - _LOGGER.debug("%s: turned 'off': %s", self.entity_id, res[1]) - except DeliveryError as ex: - _LOGGER.error("%s: Unable to turn the switch off: %s", - self.entity_id, ex) - return + await self._on_off_channel.off() - self._state = 0 + def async_set_state(self, state): + """Handle state update from channel.""" + self._state = bool(state) self.async_schedule_update_ha_state() - async def async_update(self): - """Retrieve latest state.""" - result = await helpers.safe_read(self.cluster, - ['on_off'], - allow_cache=False, - only_cache=(not self._initialized)) - self._state = result.get('on_off', self._state) + @property + def device_state_attributes(self): + """Return state attributes.""" + return self.state_attributes + + async def async_added_to_hass(self): + """Run when about to be added to hass.""" + await super().async_added_to_hass() + await self.async_accept_signal( + self._on_off_channel, SIGNAL_ATTR_UPDATED, self.async_set_state) diff --git a/homeassistant/components/zigbee.py b/homeassistant/components/zigbee/__init__.py similarity index 98% rename from homeassistant/components/zigbee.py rename to homeassistant/components/zigbee/__init__.py index 8829a3bb928..0e2d3587829 100644 --- a/homeassistant/components/zigbee.py +++ b/homeassistant/components/zigbee/__init__.py @@ -1,16 +1,11 @@ -""" -Support for Zigbee devices. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zigbee/ -""" +"""Support for Zigbee devices.""" import logging from binascii import hexlify, unhexlify import voluptuous as vol from homeassistant.const import ( - EVENT_HOMEASSISTANT_STOP, CONF_DEVICE, CONF_NAME, CONF_PIN) + EVENT_HOMEASSISTANT_STOP, CONF_DEVICE, CONF_NAME, CONF_PIN, CONF_ADDRESS) from homeassistant.helpers.entity import Entity from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import ( @@ -24,7 +19,6 @@ DOMAIN = 'zigbee' SIGNAL_ZIGBEE_FRAME_RECEIVED = 'zigbee_frame_received' -CONF_ADDRESS = 'address' CONF_BAUD = 'baud' DEFAULT_DEVICE = '/dev/ttyUSB0' diff --git a/homeassistant/components/binary_sensor/zigbee.py b/homeassistant/components/zigbee/binary_sensor.py similarity index 78% rename from homeassistant/components/binary_sensor/zigbee.py rename to homeassistant/components/zigbee/binary_sensor.py index 67c05f47094..eec1832f07d 100644 --- a/homeassistant/components/binary_sensor/zigbee.py +++ b/homeassistant/components/zigbee/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Contains functionality to use a Zigbee device as a binary sensor. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.zigbee/ -""" +"""Support for Zigbee binary sensors.""" import voluptuous as vol from homeassistant.components.binary_sensor import BinarySensorDevice diff --git a/homeassistant/components/light/zigbee.py b/homeassistant/components/zigbee/light.py similarity index 79% rename from homeassistant/components/light/zigbee.py rename to homeassistant/components/zigbee/light.py index 42dc95d1163..e5016900be7 100644 --- a/homeassistant/components/light/zigbee.py +++ b/homeassistant/components/zigbee/light.py @@ -1,9 +1,4 @@ -""" -Functionality to use a ZigBee device as a light. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.zigbee/ -""" +"""Support for Zigbee lights.""" import voluptuous as vol from homeassistant.components.light import Light diff --git a/homeassistant/components/sensor/zigbee.py b/homeassistant/components/zigbee/sensor.py similarity index 92% rename from homeassistant/components/sensor/zigbee.py rename to homeassistant/components/zigbee/sensor.py index a0a1b8bb7fd..48503e396a4 100644 --- a/homeassistant/components/sensor/zigbee.py +++ b/homeassistant/components/zigbee/sensor.py @@ -1,9 +1,4 @@ -""" -Support for functionality to use a ZigBee device as a sensor. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.zigbee/ -""" +"""Support for Zigbee sensors.""" import logging from binascii import hexlify diff --git a/homeassistant/components/switch/zigbee.py b/homeassistant/components/zigbee/switch.py similarity index 78% rename from homeassistant/components/switch/zigbee.py rename to homeassistant/components/zigbee/switch.py index 81fb8348c4e..ef36e17d74b 100644 --- a/homeassistant/components/switch/zigbee.py +++ b/homeassistant/components/zigbee/switch.py @@ -1,9 +1,4 @@ -""" -Contains functionality to use a Zigbee device as a switch. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.zigbee/ -""" +"""Support for Zigbee switches.""" import voluptuous as vol from homeassistant.components.switch import SwitchDevice diff --git a/homeassistant/components/zone/.translations/da.json b/homeassistant/components/zone/.translations/da.json index 908ef9dc43a..c6981f242d2 100644 --- a/homeassistant/components/zone/.translations/da.json +++ b/homeassistant/components/zone/.translations/da.json @@ -10,7 +10,8 @@ "latitude": "Breddegrad", "longitude": "L\u00e6ngdegrad", "name": "Navn", - "passive": "Passiv" + "passive": "Passiv", + "radius": "Radius" }, "title": "Definer zoneparametre" } diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index 370b52d1360..242f0362088 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -1,10 +1,4 @@ -""" -Support for the definition of zones. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zone/ -""" - +"""Support for the definition of zones.""" import logging import voluptuous as vol diff --git a/homeassistant/components/zone/zone.py b/homeassistant/components/zone/zone.py index ee8b53d6ee4..21084e18f06 100644 --- a/homeassistant/components/zone/zone.py +++ b/homeassistant/components/zone/zone.py @@ -1,5 +1,4 @@ -"""Component entity and functionality.""" - +"""Zone entity and functionality.""" from homeassistant.const import ATTR_HIDDEN, ATTR_LATITUDE, ATTR_LONGITUDE from homeassistant.helpers.entity import Entity from homeassistant.loader import bind_hass diff --git a/homeassistant/components/zoneminder/__init__.py b/homeassistant/components/zoneminder/__init__.py index 2cefa2e1049..a4d90d523aa 100644 --- a/homeassistant/components/zoneminder/__init__.py +++ b/homeassistant/components/zoneminder/__init__.py @@ -1,9 +1,4 @@ -""" -Support for ZoneMinder. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zoneminder/ -""" +"""Support for ZoneMinder.""" import logging import voluptuous as vol diff --git a/homeassistant/components/zoneminder/binary_sensor.py b/homeassistant/components/zoneminder/binary_sensor.py index e206ffa80f1..f20f09e70a6 100644 --- a/homeassistant/components/zoneminder/binary_sensor.py +++ b/homeassistant/components/zoneminder/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for ZoneMinder Binary Sensor. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.zoneminder/ -""" +"""Support for ZoneMinder binary sensors.""" from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.zoneminder import DOMAIN as ZONEMINDER_DOMAIN @@ -11,8 +6,8 @@ from homeassistant.components.zoneminder import DOMAIN as ZONEMINDER_DOMAIN DEPENDENCIES = ['zoneminder'] -async def async_setup_platform(hass, config, add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, add_entities, discovery_info=None): """Set up the ZoneMinder binary sensor platform.""" sensors = [] for host_name, zm_client in hass.data[ZONEMINDER_DOMAIN].items(): diff --git a/homeassistant/components/zoneminder/camera.py b/homeassistant/components/zoneminder/camera.py index 8556fbf1e35..7f74712335c 100644 --- a/homeassistant/components/zoneminder/camera.py +++ b/homeassistant/components/zoneminder/camera.py @@ -1,9 +1,4 @@ -""" -Support for ZoneMinder camera streaming. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/camera.zoneminder/ -""" +"""Support for ZoneMinder camera streaming.""" import logging from homeassistant.const import CONF_NAME, CONF_VERIFY_SSL diff --git a/homeassistant/components/zoneminder/sensor.py b/homeassistant/components/zoneminder/sensor.py index abecc99e278..9eb6beb491c 100644 --- a/homeassistant/components/zoneminder/sensor.py +++ b/homeassistant/components/zoneminder/sensor.py @@ -1,9 +1,4 @@ -""" -Support for ZoneMinder Sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.zoneminder/ -""" +"""Support for ZoneMinder sensors.""" import logging import voluptuous as vol diff --git a/homeassistant/components/zoneminder/switch.py b/homeassistant/components/zoneminder/switch.py index b039d2e3ce9..b411a148d43 100644 --- a/homeassistant/components/zoneminder/switch.py +++ b/homeassistant/components/zoneminder/switch.py @@ -1,9 +1,4 @@ -""" -Support for ZoneMinder switches. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.zoneminder/ -""" +"""Support for ZoneMinder switches.""" import logging import voluptuous as vol @@ -33,7 +28,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for zm_client in hass.data[ZONEMINDER_DOMAIN].values(): monitors = zm_client.get_monitors() if not monitors: - _LOGGER.warning('Could not fetch monitors from ZoneMinder') + _LOGGER.warning("Could not fetch monitors from ZoneMinder") return for monitor in monitors: diff --git a/homeassistant/components/zwave/.translations/da.json b/homeassistant/components/zwave/.translations/da.json new file mode 100644 index 00000000000..e9049026a4f --- /dev/null +++ b/homeassistant/components/zwave/.translations/da.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave er allerede konfigureret", + "one_instance_only": "Komponenten underst\u00f8tter kun \u00e9n Z-Wave forekomst" + }, + "error": { + "option_error": "Z-Wave validering mislykkedes. Er stien til USB enhed korrekt?" + }, + "step": { + "user": { + "data": { + "network_key": "Netv\u00e6rksn\u00f8gle (efterlad blank for autogenerering)", + "usb_path": "Sti til USB enhed" + }, + "description": "Se https://www.home-assistant.io/docs/z-wave/installation/ for oplysninger om konfigurationsvariabler", + "title": "Ops\u00e6t Z-Wave" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/fr.json b/homeassistant/components/zwave/.translations/fr.json index c667965bebc..797a64b2076 100644 --- a/homeassistant/components/zwave/.translations/fr.json +++ b/homeassistant/components/zwave/.translations/fr.json @@ -13,6 +13,7 @@ "network_key": "Cl\u00e9 r\u00e9seau (laisser vide pour g\u00e9n\u00e9rer automatiquement)", "usb_path": "Chemin USB" }, + "description": "Voir https://www.home-assistant.io/docs/z-wave/installation/ pour plus d'informations sur les variables de configuration.", "title": "Configurer Z-Wave" } }, diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 093e6071bb4..be76eca4efd 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Z-Wave. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zwave/ -""" +"""Support for Z-Wave.""" import asyncio import copy import logging diff --git a/homeassistant/components/binary_sensor/zwave.py b/homeassistant/components/zwave/binary_sensor.py similarity index 89% rename from homeassistant/components/binary_sensor/zwave.py rename to homeassistant/components/zwave/binary_sensor.py index ca07986976d..478bfcbda7b 100644 --- a/homeassistant/components/binary_sensor/zwave.py +++ b/homeassistant/components/zwave/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Interfaces with Z-Wave sensors. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/binary_sensor.zwave/ -""" +"""Support for Z-Wave binary sensors.""" import logging import datetime import homeassistant.util.dt as dt_util @@ -17,11 +12,10 @@ from homeassistant.components.binary_sensor import ( BinarySensorDevice) _LOGGER = logging.getLogger(__name__) -DEPENDENCIES = [] -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Old method of setting up Z-Wave binary sensors.""" pass @@ -33,8 +27,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Add Z-Wave binary sensor.""" async_add_entities([binary_sensor]) - async_dispatcher_connect(hass, 'zwave_new_binary_sensor', - async_add_binary_sensor) + async_dispatcher_connect( + hass, 'zwave_new_binary_sensor', async_add_binary_sensor) def get_device(values, **kwargs): diff --git a/homeassistant/components/climate/zwave.py b/homeassistant/components/zwave/climate.py similarity index 96% rename from homeassistant/components/climate/zwave.py rename to homeassistant/components/zwave/climate.py index 561af9c9f57..bf7b64549ac 100644 --- a/homeassistant/components/climate/zwave.py +++ b/homeassistant/components/zwave/climate.py @@ -1,9 +1,4 @@ -""" -Support for Z-Wave climate devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/climate.zwave/ -""" +"""Support for Z-Wave climate devices.""" # Because we do not compile openzwave on CI import logging from homeassistant.core import callback @@ -43,8 +38,8 @@ STATE_MAPPINGS = { } -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Old method of setting up Z-Wave climate devices.""" pass diff --git a/homeassistant/components/cover/zwave.py b/homeassistant/components/zwave/cover.py similarity index 95% rename from homeassistant/components/cover/zwave.py rename to homeassistant/components/zwave/cover.py index 835305449e8..e40a885ede1 100644 --- a/homeassistant/components/cover/zwave.py +++ b/homeassistant/components/zwave/cover.py @@ -1,9 +1,4 @@ -""" -Support for Z-Wave cover components. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/cover.zwave/ -""" +"""Support for Z-Wave covers.""" import logging from homeassistant.core import callback from homeassistant.components.cover import ( @@ -19,8 +14,8 @@ _LOGGER = logging.getLogger(__name__) SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Old method of setting up Z-Wave covers.""" pass diff --git a/homeassistant/components/zwave/discovery_schemas.py b/homeassistant/components/zwave/discovery_schemas.py index 56d63d658a9..0141f4392dd 100644 --- a/homeassistant/components/zwave/discovery_schemas.py +++ b/homeassistant/components/zwave/discovery_schemas.py @@ -1,4 +1,4 @@ -"""Zwave discovery schemas.""" +"""Z-Wave discovery schemas.""" from . import const DEFAULT_VALUES_SCHEMA = { diff --git a/homeassistant/components/fan/zwave.py b/homeassistant/components/zwave/fan.py similarity index 89% rename from homeassistant/components/fan/zwave.py rename to homeassistant/components/zwave/fan.py index 4b4204aa454..b2731f7d9a7 100644 --- a/homeassistant/components/fan/zwave.py +++ b/homeassistant/components/zwave/fan.py @@ -1,9 +1,4 @@ -""" -Z-Wave platform that handles fans. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/fan.zwave/ -""" +"""Support for Z-Wave fans.""" import logging import math @@ -36,8 +31,8 @@ SPEED_TO_VALUE = { } -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Old method of setting up Z-Wave fans.""" pass diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/zwave/light.py similarity index 97% rename from homeassistant/components/light/zwave.py rename to homeassistant/components/zwave/light.py index da712a2f183..0af85b84177 100644 --- a/homeassistant/components/light/zwave.py +++ b/homeassistant/components/zwave/light.py @@ -1,9 +1,4 @@ -""" -Support for Z-Wave lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.zwave/ -""" +"""Support for Z-Wave lights.""" import logging from threading import Timer @@ -55,8 +50,8 @@ 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): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Old method of setting up Z-Wave lights.""" pass diff --git a/homeassistant/components/lock/zwave.py b/homeassistant/components/zwave/lock.py similarity index 98% rename from homeassistant/components/lock/zwave.py rename to homeassistant/components/zwave/lock.py index c907d5101a9..7c0958e596a 100644 --- a/homeassistant/components/lock/zwave.py +++ b/homeassistant/components/zwave/lock.py @@ -1,9 +1,4 @@ -""" -Z-Wave platform that handles simple door locks. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/lock.zwave/ -""" +"""Support for Z-Wave door locks.""" import logging import voluptuous as vol @@ -42,7 +37,10 @@ DEVICE_MAPPINGS = { # Yale YRD210, Yale YRD240 (0x0129, 0x0209): WORKAROUND_DEVICE_STATE | WORKAROUND_ALARM_TYPE, (0x0129, 0xAA00): WORKAROUND_DEVICE_STATE, - (0x0129, 0x0000): WORKAROUND_DEVICE_STATE, + # Yale YRL220/YRD220 + (0x0129, 0x0000): WORKAROUND_DEVICE_STATE | WORKAROUND_ALARM_TYPE, + # Yale YRD120 + (0x0129, 0x0800): WORKAROUND_DEVICE_STATE | WORKAROUND_ALARM_TYPE, # Yale YRD220 (as reported by adrum in PR #17386) (0x0109, 0x0000): WORKAROUND_DEVICE_STATE | WORKAROUND_ALARM_TYPE, # Schlage BE469 diff --git a/homeassistant/components/sensor/zwave.py b/homeassistant/components/zwave/sensor.py similarity index 91% rename from homeassistant/components/sensor/zwave.py rename to homeassistant/components/zwave/sensor.py index ce25b61146b..44fc132cf77 100644 --- a/homeassistant/components/sensor/zwave.py +++ b/homeassistant/components/zwave/sensor.py @@ -1,9 +1,4 @@ -""" -Interfaces with Z-Wave sensors. - -For more details about this platform, please refer to the documentation -at https://home-assistant.io/components/sensor.zwave/ -""" +"""Support for Z-Wave sensors.""" import logging from homeassistant.core import callback from homeassistant.components.sensor import DOMAIN @@ -14,8 +9,8 @@ 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): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Old method of setting up Z-Wave sensors.""" pass diff --git a/homeassistant/components/switch/zwave.py b/homeassistant/components/zwave/switch.py similarity index 91% rename from homeassistant/components/switch/zwave.py rename to homeassistant/components/zwave/switch.py index 54a2a729d04..ef544222546 100644 --- a/homeassistant/components/switch/zwave.py +++ b/homeassistant/components/zwave/switch.py @@ -1,9 +1,4 @@ -""" -Z-Wave platform that handles simple binary switches. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.zwave/ -""" +"""Support for Z-Wave switches.""" import logging import time from homeassistant.core import callback diff --git a/homeassistant/components/zwave/workaround.py b/homeassistant/components/zwave/workaround.py index 0a882a093c6..ff4db3c0706 100644 --- a/homeassistant/components/zwave/workaround.py +++ b/homeassistant/components/zwave/workaround.py @@ -1,4 +1,4 @@ -"""Zwave workarounds.""" +"""Z-Wave workarounds.""" from . import const # Manufacturers diff --git a/homeassistant/config.py b/homeassistant/config.py index 5dbf226ca25..3310cd3e160 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -65,49 +65,16 @@ DEFAULT_CORE_CONFIG = ( (CONF_CUSTOMIZE, '!include customize.yaml', None, 'Customization file'), ) # type: Tuple[Tuple[str, Any, Any, Optional[str]], ...] DEFAULT_CONFIG = """ -# Show links to resources in log and frontend +# Configure a default setup of Home Assistant (frontend, api, etc) +default_config: + +# Show the introduction message on startup. introduction: -# Enables the frontend -frontend: - -# Enables configuration UI -config: - # Uncomment this if you are using SSL/TLS, running in Docker container, etc. # http: # base_url: example.duckdns.org:8123 -# Checks for available updates -# Note: This component will send some information about your system to -# the developers to assist with development of Home Assistant. -# For more information, please see: -# https://home-assistant.io/blog/2016/10/25/explaining-the-updater/ -updater: - # Optional, allows Home Assistant developers to focus on popular components. - # include_used_components: true - -# Discover some devices automatically -discovery: - -# Allows you to issue voice commands from the frontend in enabled browsers -conversation: - -# Enables support for tracking state changes over time -history: - -# View all events in a logbook -logbook: - -# Enables a map showing the location of tracked devices -map: - -# Track the sun -sun: - -# Allow diagnosing system problems -system_health: - # Sensors sensor: # Weather prediction @@ -117,9 +84,6 @@ sensor: tts: - platform: google -# Cloud -cloud: - group: !include groups.yaml automation: !include automations.yaml script: !include scripts.yaml diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 9c4c127f52e..c7dfc0c889b 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -30,7 +30,7 @@ At a minimum, each config flow will have to define a version number and the class ExampleConfigFlow(config_entries.ConfigFlow): VERSION = 1 - CONNETION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH async def async_step_user(self, user_input=None): … @@ -127,6 +127,7 @@ from homeassistant.util.decorator import Registry _LOGGER = logging.getLogger(__name__) +_UNDEF = object() SOURCE_USER = 'user' SOURCE_DISCOVERY = 'discovery' @@ -149,6 +150,7 @@ FLOWS = [ 'hue', 'ifttt', 'ios', + 'ipma', 'lifx', 'locative', 'luftdaten', @@ -440,9 +442,10 @@ class ConfigEntries: for entry in config['entries']] @callback - def async_update_entry(self, entry, *, data): + def async_update_entry(self, entry, *, data=_UNDEF): """Update a config entry.""" - entry.data = data + if data is not _UNDEF: + entry.data = data self._async_schedule_save() async def async_forward_entry_setup(self, entry, component): diff --git a/homeassistant/const.py b/homeassistant/const.py index c230a1c52d1..9dbb06e8adf 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,14 +1,14 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 87 -PATCH_VERSION = '1' +MINOR_VERSION = 88 +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) -# Format for platforms -PLATFORM_FORMAT = '{domain}.{platform}' +# Format for platform files +PLATFORM_FORMAT = '{platform}.{domain}' # Can be used to specify a catch all when registering state or event listeners. MATCH_ALL = '*' @@ -147,7 +147,11 @@ CONF_TTL = 'ttl' CONF_TYPE = 'type' CONF_UNIT_OF_MEASUREMENT = 'unit_of_measurement' CONF_UNIT_SYSTEM = 'unit_system' + +# Deprecated in 0.88.0, invalidated in 0.91.0, remove in 0.92.0 CONF_UPDATE_INTERVAL = 'update_interval' +CONF_UPDATE_INTERVAL_INVALIDATION_VERSION = '0.91.0' + CONF_URL = 'url' CONF_USERNAME = 'username' CONF_VALUE_TEMPLATE = 'value_template' diff --git a/homeassistant/core.py b/homeassistant/core.py index 6ddefd2022d..e7f654f5184 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -744,7 +744,10 @@ class State: context = json_dict.get('context') if context: - context = Context(**context) + context = Context( + id=context.get('id'), + user_id=context.get('user_id'), + ) return cls(json_dict['entity_id'], json_dict['state'], json_dict.get('attributes'), last_changed, last_updated, @@ -778,7 +781,7 @@ class StateMachine: self._bus = bus self._loop = loop - def entity_ids(self, domain_filter: Optional[str] = None)-> List[str]: + def entity_ids(self, domain_filter: Optional[str] = None) -> List[str]: """List of entity ids that are being tracked.""" future = run_callback_threadsafe( self._loop, self.async_entity_ids, domain_filter @@ -800,13 +803,13 @@ class StateMachine: return [state.entity_id for state in self._states.values() if state.domain == domain_filter] - def all(self)-> List[State]: + def all(self) -> List[State]: """Create a list of all states.""" return run_callback_threadsafe( # type: ignore self._loop, self.async_all).result() @callback - def async_all(self)-> List[State]: + def async_all(self) -> List[State]: """Create a list of all states. This method must be run in the event loop. diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index b2669312e38..6b1dd10bd5b 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -1,6 +1,9 @@ """Helper for aiohttp webclient stuff.""" import asyncio import sys +from ssl import SSLContext # noqa: F401 +from typing import Any, Awaitable, Optional, cast +from typing import Union # noqa: F401 import aiohttp from aiohttp.hdrs import USER_AGENT, CONTENT_TYPE @@ -8,8 +11,9 @@ from aiohttp import web from aiohttp.web_exceptions import HTTPGatewayTimeout, HTTPBadGateway import async_timeout -from homeassistant.core import callback +from homeassistant.core import callback, Event from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, __version__ +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import bind_hass from homeassistant.util import ssl as ssl_util @@ -23,7 +27,8 @@ SERVER_SOFTWARE = 'HomeAssistant/{0} aiohttp/{1} Python/{2[0]}.{2[1]}'.format( @callback @bind_hass -def async_get_clientsession(hass, verify_ssl=True): +def async_get_clientsession(hass: HomeAssistantType, + verify_ssl: bool = True) -> aiohttp.ClientSession: """Return default aiohttp ClientSession. This method must be run in the event loop. @@ -36,13 +41,15 @@ def async_get_clientsession(hass, verify_ssl=True): if key not in hass.data: hass.data[key] = async_create_clientsession(hass, verify_ssl) - return hass.data[key] + return cast(aiohttp.ClientSession, hass.data[key]) @callback @bind_hass -def async_create_clientsession(hass, verify_ssl=True, auto_cleanup=True, - **kwargs): +def async_create_clientsession(hass: HomeAssistantType, + verify_ssl: bool = True, + auto_cleanup: bool = True, + **kwargs: Any) -> aiohttp.ClientSession: """Create a new ClientSession with kwargs, i.e. for cookies. If auto_cleanup is False, you need to call detach() after the session @@ -67,8 +74,10 @@ def async_create_clientsession(hass, verify_ssl=True, auto_cleanup=True, @bind_hass -async def async_aiohttp_proxy_web(hass, request, web_coro, - buffer_size=102400, timeout=10): +async def async_aiohttp_proxy_web( + hass: HomeAssistantType, request: web.BaseRequest, + web_coro: Awaitable[aiohttp.ClientResponse], buffer_size: int = 102400, + timeout: int = 10) -> Optional[web.StreamResponse]: """Stream websession request to aiohttp web response.""" try: with async_timeout.timeout(timeout, loop=hass.loop): @@ -76,7 +85,7 @@ async def async_aiohttp_proxy_web(hass, request, web_coro, except asyncio.CancelledError: # The user cancelled the request - return + return None except asyncio.TimeoutError as err: # Timeout trying to start the web request @@ -98,8 +107,12 @@ async def async_aiohttp_proxy_web(hass, request, web_coro, @bind_hass -async def async_aiohttp_proxy_stream(hass, request, stream, content_type, - buffer_size=102400, timeout=10): +async def async_aiohttp_proxy_stream(hass: HomeAssistantType, + request: web.BaseRequest, + stream: aiohttp.StreamReader, + content_type: str, + buffer_size: int = 102400, + timeout: int = 10) -> web.StreamResponse: """Stream a stream to aiohttp web response.""" response = web.StreamResponse() response.content_type = content_type @@ -122,13 +135,14 @@ async def async_aiohttp_proxy_stream(hass, request, stream, content_type, @callback -def _async_register_clientsession_shutdown(hass, clientsession): +def _async_register_clientsession_shutdown( + hass: HomeAssistantType, clientsession: aiohttp.ClientSession) -> None: """Register ClientSession close on Home Assistant shutdown. This method must be run in the event loop. """ @callback - def _async_close_websession(event): + def _async_close_websession(event: Event) -> None: """Close websession.""" clientsession.detach() @@ -137,7 +151,8 @@ def _async_register_clientsession_shutdown(hass, clientsession): @callback -def _async_get_connector(hass, verify_ssl=True): +def _async_get_connector(hass: HomeAssistantType, + verify_ssl: bool = True) -> aiohttp.BaseConnector: """Return the connector pool for aiohttp. This method must be run in the event loop. @@ -145,17 +160,18 @@ def _async_get_connector(hass, verify_ssl=True): key = DATA_CONNECTOR if verify_ssl else DATA_CONNECTOR_NOTVERIFY if key in hass.data: - return hass.data[key] + return cast(aiohttp.BaseConnector, hass.data[key]) if verify_ssl: - ssl_context = ssl_util.client_context() + ssl_context = \ + ssl_util.client_context() # type: Union[bool, SSLContext] else: ssl_context = False connector = aiohttp.TCPConnector(loop=hass.loop, ssl=ssl_context) hass.data[key] = connector - async def _async_close_connector(event): + async def _async_close_connector(event: Event) -> None: """Close connector pool.""" await connector.close() diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index 19ad52534cb..bc8c05ed0a6 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -2,12 +2,14 @@ import logging import uuid from collections import OrderedDict -from typing import List, Optional +from typing import MutableMapping # noqa: F401 +from typing import Iterable, Optional, cast import attr from homeassistant.core import callback from homeassistant.loader import bind_hass +from .typing import HomeAssistantType _LOGGER = logging.getLogger(__name__) @@ -29,14 +31,14 @@ class AreaEntry: class AreaRegistry: """Class to hold a registry of areas.""" - def __init__(self, hass) -> None: + def __init__(self, hass: HomeAssistantType) -> None: """Initialize the area registry.""" self.hass = hass - self.areas = None + self.areas = {} # type: MutableMapping[str, AreaEntry] self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) @callback - def async_list_areas(self) -> List[AreaEntry]: + def async_list_areas(self) -> Iterable[AreaEntry]: """Get all areas.""" return self.areas.values() @@ -81,18 +83,18 @@ class AreaRegistry: return new @callback - def _async_is_registered(self, name) -> Optional[AreaEntry]: + def _async_is_registered(self, name: str) -> Optional[AreaEntry]: """Check if a name is currently registered.""" for area in self.areas.values(): if name == area.name: return area - return False + return None async def async_load(self) -> None: """Load the area registry.""" data = await self._store.async_load() - areas = OrderedDict() + areas = OrderedDict() # type: OrderedDict[str, AreaEntry] if data is not None: for area in data['areas']: @@ -124,16 +126,16 @@ class AreaRegistry: @bind_hass -async def async_get_registry(hass) -> AreaRegistry: +async def async_get_registry(hass: HomeAssistantType) -> AreaRegistry: """Return area registry instance.""" task = hass.data.get(DATA_REGISTRY) if task is None: - async def _load_reg(): + async def _load_reg() -> AreaRegistry: registry = AreaRegistry(hass) await registry.async_load() return registry task = hass.data[DATA_REGISTRY] = hass.async_create_task(_load_reg()) - return await task + return cast(AreaRegistry, await task) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index b148a875398..b5716431217 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1,27 +1,29 @@ """Helpers for config validation using voluptuous.""" -from datetime import (timedelta, datetime as datetime_sys, - time as time_sys, date as date_sys) +import inspect +import logging import os import re -from urllib.parse import urlparse +from datetime import (timedelta, datetime as datetime_sys, + time as time_sys, date as date_sys) from socket import _GLOBAL_DEFAULT_TIMEOUT -import logging -import inspect -from typing import Any, Union, TypeVar, Callable, Sequence, Dict +from typing import Any, Union, TypeVar, Callable, Sequence, Dict, Optional +from urllib.parse import urlparse import voluptuous as vol +from pkg_resources import parse_version +import homeassistant.util.dt as dt_util from homeassistant.const import ( CONF_PLATFORM, CONF_SCAN_INTERVAL, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_ALIAS, CONF_ENTITY_ID, CONF_VALUE_TEMPLATE, WEEKDAYS, CONF_CONDITION, CONF_BELOW, CONF_ABOVE, CONF_TIMEOUT, SUN_EVENT_SUNSET, SUN_EVENT_SUNRISE, CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC, - ENTITY_MATCH_ALL, CONF_ENTITY_NAMESPACE) + ENTITY_MATCH_ALL, CONF_ENTITY_NAMESPACE, __version__) from homeassistant.core import valid_entity_id, split_entity_id from homeassistant.exceptions import TemplateError -import homeassistant.util.dt as dt_util -from homeassistant.util import slugify as util_slugify from homeassistant.helpers import template as template_helper +from homeassistant.helpers.logging import KeywordStyleAdapter +from homeassistant.util import slugify as util_slugify # pylint: disable=invalid-name @@ -32,6 +34,7 @@ OLD_ENTITY_ID_VALIDATION = r"^(\w+)\.(\w+)$" # persistent notification. Rare temporary exception to use a global. INVALID_SLUGS_FOUND = {} INVALID_ENTITY_IDS_FOUND = {} +INVALID_EXTRA_KEYS_FOUND = [] # Home Assistant types @@ -67,6 +70,22 @@ def has_at_least_one_key(*keys: str) -> Callable: return validate +def has_at_most_one_key(*keys: str) -> Callable: + """Validate that zero keys exist or one key exists.""" + def validate(obj: Dict) -> Dict: + """Test zero keys exist or one key exists in dict.""" + if not isinstance(obj, dict): + raise vol.Invalid('expected dictionary') + + if len(set(keys) & set(obj)) > 1: + raise vol.Invalid( + 'must contain at most one of {}.'.format(', '.join(keys)) + ) + return obj + + return validate + + def boolean(value: Any) -> bool: """Validate and coerce a boolean value.""" if isinstance(value, str): @@ -436,13 +455,15 @@ def template(value): def template_complex(value): """Validate a complex jinja2 template.""" if isinstance(value, list): - for idx, element in enumerate(value): - value[idx] = template_complex(element) - return value + return_value = value.copy() + for idx, element in enumerate(return_value): + return_value[idx] = template_complex(element) + return return_value if isinstance(value, dict): - for key, element in value.items(): - value[key] = template_complex(element) - return value + return_value = value.copy() + for key, element in return_value.items(): + return_value[key] = template_complex(element) + return return_value return template(value) @@ -518,18 +539,80 @@ def ensure_list_csv(value: Any) -> Sequence: return ensure_list(value) -def deprecated(key): - """Log key as deprecated.""" +def deprecated(key: str, + replacement_key: Optional[str] = None, + invalidation_version: Optional[str] = None, + default: Optional[Any] = None): + """ + Log key as deprecated and provide a replacement (if exists). + + Expected behavior: + - Outputs the appropriate deprecation warning if key is detected + - Processes schema moving the value from key to replacement_key + - Processes schema changing nothing if only replacement_key provided + - No warning if only replacement_key provided + - No warning if neither key nor replacement_key are provided + - Adds replacement_key with default value in this case + - Once the invalidation_version is crossed, raises vol.Invalid if key + is detected + """ module_name = inspect.getmodule(inspect.stack()[1][0]).__name__ - def validator(config): + if replacement_key and invalidation_version: + warning = ("The '{key}' option (with value '{value}') is" + " deprecated, please replace it with '{replacement_key}'." + " This option will become invalid in version" + " {invalidation_version}") + elif replacement_key: + warning = ("The '{key}' option (with value '{value}') is" + " deprecated, please replace it with '{replacement_key}'") + elif invalidation_version: + warning = ("The '{key}' option (with value '{value}') is" + " deprecated, please remove it from your configuration." + " This option will become invalid in version" + " {invalidation_version}") + else: + warning = ("The '{key}' option (with value '{value}') is" + " deprecated, please remove it from your configuration") + + def check_for_invalid_version(value: Optional[Any]): + """Raise error if current version has reached invalidation.""" + if not invalidation_version: + return + + if parse_version(__version__) >= parse_version(invalidation_version): + raise vol.Invalid( + warning.format( + key=key, + value=value, + replacement_key=replacement_key, + invalidation_version=invalidation_version + ) + ) + + def validator(config: Dict): """Check if key is in config and log warning.""" if key in config: - logging.getLogger(module_name).warning( - "The '%s' option (with value '%s') is deprecated, please " - "remove it from your configuration.", key, config[key]) + value = config[key] + check_for_invalid_version(value) + KeywordStyleAdapter(logging.getLogger(module_name)).warning( + warning, + key=key, + value=value, + replacement_key=replacement_key, + invalidation_version=invalidation_version + ) + if replacement_key: + config.pop(key) + else: + value = default + if (replacement_key + and (replacement_key not in config + or default == config.get(replacement_key)) + and value is not None): + config[replacement_key] = value - return config + return has_at_most_one_key(key, replacement_key)(config) return validator @@ -551,21 +634,67 @@ def key_dependency(key, dependency): # Schemas +class HASchema(vol.Schema): + """Schema class that allows us to mark PREVENT_EXTRA errors as warnings.""" -PLATFORM_SCHEMA = vol.Schema({ - vol.Required(CONF_PLATFORM): string, - vol.Optional(CONF_ENTITY_NAMESPACE): string, - vol.Optional(CONF_SCAN_INTERVAL): time_period -}, extra=vol.ALLOW_EXTRA) + def __call__(self, data): + """Override __call__ to mark PREVENT_EXTRA as warning.""" + try: + return super().__call__(data) + except vol.Invalid as orig_err: + if self.extra != vol.PREVENT_EXTRA: + raise -# This will replace PLATFORM_SCHEMA once all base components are updated -PLATFORM_SCHEMA_2 = vol.Schema({ + # orig_error is of type vol.MultipleInvalid (see super __call__) + assert isinstance(orig_err, vol.MultipleInvalid) + # pylint: disable=no-member + # If it fails with PREVENT_EXTRA, try with ALLOW_EXTRA + self.extra = vol.ALLOW_EXTRA + # In case it still fails the following will raise + try: + validated = super().__call__(data) + finally: + self.extra = vol.PREVENT_EXTRA + + # This is a legacy config, print warning + extra_key_errs = [err for err in orig_err.errors + if err.error_message == 'extra keys not allowed'] + if extra_key_errs: + msg = "Your configuration contains extra keys " \ + "that the platform does not support.\n" \ + "Please remove " + submsg = ', '.join('[{}]'.format(err.path[-1]) for err in + extra_key_errs) + submsg += '. ' + if hasattr(data, '__config_file__'): + submsg += " (See {}, line {}). ".format( + data.__config_file__, data.__line__) + msg += submsg + logging.getLogger(__name__).warning(msg) + INVALID_EXTRA_KEYS_FOUND.append(submsg) + else: + # This should not happen (all errors should be extra key + # errors). Let's raise the original error anyway. + raise orig_err + + # Return legacy validated config + return validated + + def extend(self, schema, required=None, extra=None): + """Extend this schema and convert it to HASchema if necessary.""" + ret = super().extend(schema, required=required, extra=extra) + if extra is not None: + return ret + return HASchema(ret.schema, required=required, extra=self.extra) + + +PLATFORM_SCHEMA = HASchema({ vol.Required(CONF_PLATFORM): string, vol.Optional(CONF_ENTITY_NAMESPACE): string, vol.Optional(CONF_SCAN_INTERVAL): time_period }) -PLATFORM_SCHEMA_BASE = PLATFORM_SCHEMA_2.extend({ +PLATFORM_SCHEMA_BASE = PLATFORM_SCHEMA.extend({ }, extra=vol.ALLOW_EXTRA) EVENT_SCHEMA = vol.Schema({ diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 2d4ad68dbbe..c13ebe7cfab 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -319,7 +319,7 @@ class Entity: @callback def async_schedule_update_ha_state(self, force_refresh=False): """Schedule an update ha state change task.""" - self.hass.async_add_job(self.async_update_ha_state(force_refresh)) + self.hass.async_create_task(self.async_update_ha_state(force_refresh)) async def async_device_update(self, warning=True): """Process 'update' or 'async_update' from entity. diff --git a/homeassistant/helpers/logging.py b/homeassistant/helpers/logging.py new file mode 100644 index 00000000000..ea596eb3c15 --- /dev/null +++ b/homeassistant/helpers/logging.py @@ -0,0 +1,49 @@ +"""Helpers for logging allowing more advanced logging styles to be used.""" +import inspect +import logging + + +class KeywordMessage: + """ + Represents a logging message with keyword arguments. + + Adapted from: https://stackoverflow.com/a/24683360/2267718 + """ + + def __init__(self, fmt, args, kwargs): + """Initialize a new BraceMessage object.""" + self._fmt = fmt + self._args = args + self._kwargs = kwargs + + def __str__(self): + """Convert the object to a string for logging.""" + return str(self._fmt).format(*self._args, **self._kwargs) + + +class KeywordStyleAdapter(logging.LoggerAdapter): + """Represents an adapter wrapping the logger allowing KeywordMessages.""" + + def __init__(self, logger, extra=None): + """Initialize a new StyleAdapter for the provided logger.""" + super(KeywordStyleAdapter, self).__init__(logger, extra or {}) + + def log(self, level, msg, *args, **kwargs): + """Log the message provided at the appropriate level.""" + if self.isEnabledFor(level): + msg, log_kwargs = self.process(msg, kwargs) + self.logger._log( # pylint: disable=protected-access + level, KeywordMessage(msg, args, kwargs), (), **log_kwargs + ) + + def process(self, msg, kwargs): + """Process the keyward args in preparation for logging.""" + return ( + msg, + { + k: kwargs[k] + for k in inspect.getfullargspec( + self.logger._log # pylint: disable=protected-access + ).args[1:] if k in kwargs + } + ) diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 61dc49ce2ec..7d69defed48 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -10,41 +10,27 @@ from typing import ( # noqa: F401 pylint: disable=unused-import from homeassistant.loader import bind_hass import homeassistant.util.dt as dt_util -from homeassistant.components.media_player import ( - ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_SEEK_POSITION, - ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, SERVICE_PLAY_MEDIA, - SERVICE_SELECT_SOURCE, ATTR_INPUT_SOURCE) from homeassistant.components.notify import ( ATTR_MESSAGE, SERVICE_NOTIFY) from homeassistant.components.sun import ( STATE_ABOVE_HORIZON, STATE_BELOW_HORIZON) -from homeassistant.components.switch.mysensors import ( +from homeassistant.components.mysensors.switch import ( ATTR_IR_CODE, SERVICE_SEND_IR_CODE) -from homeassistant.components.climate import ( - ATTR_AUX_HEAT, ATTR_AWAY_MODE, ATTR_FAN_MODE, ATTR_HOLD_MODE, - ATTR_HUMIDITY, ATTR_OPERATION_MODE, ATTR_SWING_MODE, - SERVICE_SET_AUX_HEAT, SERVICE_SET_AWAY_MODE, SERVICE_SET_HOLD_MODE, - SERVICE_SET_FAN_MODE, SERVICE_SET_HUMIDITY, SERVICE_SET_OPERATION_MODE, - SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE, STATE_HEAT, STATE_COOL, - STATE_IDLE) -from homeassistant.components.climate.ecobee import ( - ATTR_FAN_MIN_ON_TIME, SERVICE_SET_FAN_MIN_ON_TIME, - ATTR_RESUME_ALL, SERVICE_RESUME_PROGRAM) from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_OPTION, ATTR_TEMPERATURE, SERVICE_ALARM_ARM_AWAY, + ATTR_ENTITY_ID, ATTR_OPTION, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_DISARM, SERVICE_ALARM_TRIGGER, - SERVICE_LOCK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_STOP, - SERVICE_MEDIA_SEEK, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_UNLOCK, - SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_OPEN_COVER, + SERVICE_LOCK, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_UNLOCK, + SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_SET_COVER_POSITION, SERVICE_SET_COVER_TILT_POSITION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, STATE_CLOSED, STATE_HOME, STATE_LOCKED, STATE_NOT_HOME, STATE_OFF, - STATE_ON, STATE_OPEN, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, + STATE_ON, STATE_OPEN, STATE_UNKNOWN, STATE_UNLOCKED, SERVICE_SELECT_OPTION) -from homeassistant.core import State, DOMAIN as HASS_DOMAIN +from homeassistant.core import ( + Context, State, DOMAIN as HASS_DOMAIN) from homeassistant.util.async_ import run_coroutine_threadsafe from .typing import HomeAssistantType @@ -55,22 +41,7 @@ GROUP_DOMAIN = 'group' # Update this dict of lists when new services are added to HA. # Each item is a service with a list of required attributes. SERVICE_ATTRIBUTES = { - SERVICE_PLAY_MEDIA: [ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_CONTENT_ID], - SERVICE_MEDIA_SEEK: [ATTR_MEDIA_SEEK_POSITION], - SERVICE_VOLUME_MUTE: [ATTR_MEDIA_VOLUME_MUTED], - SERVICE_VOLUME_SET: [ATTR_MEDIA_VOLUME_LEVEL], SERVICE_NOTIFY: [ATTR_MESSAGE], - SERVICE_SET_AWAY_MODE: [ATTR_AWAY_MODE], - SERVICE_SET_FAN_MODE: [ATTR_FAN_MODE], - SERVICE_SET_FAN_MIN_ON_TIME: [ATTR_FAN_MIN_ON_TIME], - SERVICE_RESUME_PROGRAM: [ATTR_RESUME_ALL], - SERVICE_SET_TEMPERATURE: [ATTR_TEMPERATURE], - SERVICE_SET_HUMIDITY: [ATTR_HUMIDITY], - SERVICE_SET_SWING_MODE: [ATTR_SWING_MODE], - SERVICE_SET_HOLD_MODE: [ATTR_HOLD_MODE], - SERVICE_SET_OPERATION_MODE: [ATTR_OPERATION_MODE], - SERVICE_SET_AUX_HEAT: [ATTR_AUX_HEAT], - SERVICE_SELECT_SOURCE: [ATTR_INPUT_SOURCE], SERVICE_SEND_IR_CODE: [ATTR_IR_CODE], SERVICE_SELECT_OPTION: [ATTR_OPTION], SERVICE_SET_COVER_POSITION: [ATTR_POSITION], @@ -82,9 +53,6 @@ SERVICE_ATTRIBUTES = { SERVICE_TO_STATE = { SERVICE_TURN_ON: STATE_ON, SERVICE_TURN_OFF: STATE_OFF, - SERVICE_MEDIA_PLAY: STATE_PLAYING, - SERVICE_MEDIA_PAUSE: STATE_PAUSED, - SERVICE_MEDIA_STOP: STATE_IDLE, SERVICE_ALARM_ARM_AWAY: STATE_ALARM_ARMED_AWAY, SERVICE_ALARM_ARM_HOME: STATE_ALARM_ARMED_HOME, SERVICE_ALARM_DISARM: STATE_ALARM_DISARMED, @@ -142,14 +110,56 @@ def reproduce_state(hass: HomeAssistantType, @bind_hass -async def async_reproduce_state(hass: HomeAssistantType, - states: Union[State, Iterable[State]], - blocking: bool = False) -> None: - """Reproduce given state.""" +async def async_reproduce_state( + hass: HomeAssistantType, + states: Union[State, Iterable[State]], + blocking: bool = False, + context: Optional[Context] = None) -> None: + """Reproduce a list of states on multiple domains.""" if isinstance(states, State): states = [states] - to_call = defaultdict(list) # type: Dict[Tuple[str, str, str], List[str]] + to_call = defaultdict(list) # type: Dict[str, List[State]] + + for state in states: + to_call[state.domain].append(state) + + async def worker(domain: str, data: List[State]) -> None: + component = getattr(hass.components, domain) + if hasattr(component, 'async_reproduce_states'): + await component.async_reproduce_states( + data, + context=context) + else: + await async_reproduce_state_legacy( + hass, + domain, + data, + blocking=blocking, + context=context) + + if to_call: + # run all domains in parallel + await asyncio.gather(*[ + worker(domain, data) + for domain, data in to_call.items() + ]) + + +@bind_hass +async def async_reproduce_state_legacy( + hass: HomeAssistantType, + domain: str, + states: Iterable[State], + blocking: bool = False, + context: Optional[Context] = None) -> None: + """Reproduce given state.""" + to_call = defaultdict(list) # type: Dict[Tuple[str, str], List[str]] + + if domain == GROUP_DOMAIN: + service_domain = HASS_DOMAIN + else: + service_domain = domain for state in states: @@ -158,11 +168,6 @@ async def async_reproduce_state(hass: HomeAssistantType, state.entity_id) continue - if state.domain == GROUP_DOMAIN: - service_domain = HASS_DOMAIN - else: - service_domain = state.domain - domain_services = hass.services.async_services().get(service_domain) if not domain_services: @@ -189,32 +194,22 @@ async def async_reproduce_state(hass: HomeAssistantType, # We group service calls for entities by service call # json used to create a hashable version of dict with maybe lists in it - key = (service_domain, service, + key = (service, json.dumps(dict(state.attributes), sort_keys=True)) to_call[key].append(state.entity_id) - domain_tasks = {} # type: Dict[str, List[Awaitable[Optional[bool]]]] - for (service_domain, service, service_data), entity_ids in to_call.items(): + domain_tasks = [] # type: List[Awaitable[Optional[bool]]] + for (service, service_data), entity_ids in to_call.items(): data = json.loads(service_data) data[ATTR_ENTITY_ID] = entity_ids - if service_domain not in domain_tasks: - domain_tasks[service_domain] = [] - - domain_tasks[service_domain].append( - hass.services.async_call(service_domain, service, data, blocking) + domain_tasks.append( + hass.services.async_call(service_domain, service, data, blocking, + context) ) - async def async_handle_service_calls( - coro_list: Iterable[Awaitable]) -> None: - """Handle service calls by domain sequence.""" - for coro in coro_list: - await coro - - execute_tasks = [async_handle_service_calls(coro_list) - for coro_list in domain_tasks.values()] - if execute_tasks: - await asyncio.wait(execute_tasks, loop=hass.loop) + if domain_tasks: + await asyncio.wait(domain_tasks, loop=hass.loop) def state_as_number(state: State) -> float: @@ -223,6 +218,9 @@ def state_as_number(state: State) -> float: Raises ValueError if this is not possible. """ + from homeassistant.components.climate import ( + STATE_HEAT, STATE_COOL, STATE_IDLE) + if state.state in (STATE_ON, STATE_LOCKED, STATE_ABOVE_HORIZON, STATE_OPEN, STATE_HOME, STATE_HEAT, STATE_COOL): return 1 diff --git a/homeassistant/loader.py b/homeassistant/loader.py index cd22a69dab1..962b168aa97 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -18,7 +18,6 @@ from types import ModuleType from typing import Optional, Set, TYPE_CHECKING, Callable, Any, TypeVar # noqa pylint: disable=unused-import from homeassistant.const import PLATFORM_FORMAT -from homeassistant.util import OrderedSet # Typing imports that create a circular dependency # pylint: disable=using-constant-test,unused-import @@ -39,15 +38,37 @@ PATH_CUSTOM_COMPONENTS = 'custom_components' PACKAGE_COMPONENTS = 'homeassistant.components' +class LoaderError(Exception): + """Loader base error.""" + + +class ComponentNotFound(LoaderError): + """Raised when a component is not found.""" + + def __init__(self, domain: str) -> None: + """Initialize a component not found error.""" + super().__init__("Component {} not found.".format(domain)) + self.domain = domain + + +class CircularDependency(LoaderError): + """Raised when a circular dependency is found when resolving components.""" + + def __init__(self, from_domain: str, to_domain: str) -> None: + """Initialize circular dependency error.""" + super().__init__("Circular dependency detected: {} -> {}.".format( + from_domain, to_domain)) + self.from_domain = from_domain + self.to_domain = to_domain + + def set_component(hass, # type: HomeAssistant comp_name: str, component: Optional[ModuleType]) -> None: """Set a component in the cache. Async friendly. """ - cache = hass.data.get(DATA_KEY) - if cache is None: - cache = hass.data[DATA_KEY] = {} + cache = hass.data.setdefault(DATA_KEY, {}) cache[comp_name] = component @@ -60,13 +81,22 @@ def get_platform(hass, # type: HomeAssistant platform = _load_file(hass, PLATFORM_FORMAT.format( domain=domain, platform=platform_name)) - if platform is None: - # Turn it around for legacy purposes - platform = _load_file(hass, PLATFORM_FORMAT.format( - domain=platform_name, platform=domain)) + if platform is not None: + return platform + + # Legacy platform check: light/hue.py + platform = _load_file(hass, PLATFORM_FORMAT.format( + domain=platform_name, platform=domain)) if platform is None: _LOGGER.error("Unable to find platform %s", platform_name) + return None + + if platform.__name__.startswith(PATH_CUSTOM_COMPONENTS): + _LOGGER.warning( + "Integrations need to be in their own folder. Change %s/%s.py to " + "%s/%s.py. This will stop working soon.", + domain, platform_name, platform_name, domain) return platform @@ -228,57 +258,46 @@ def bind_hass(func: CALLABLE_T) -> CALLABLE_T: return func -def load_order_component(hass, # type: HomeAssistant - comp_name: str) -> OrderedSet: - """Return an OrderedSet of components in the correct order of loading. +def component_dependencies(hass, # type: HomeAssistant + comp_name: str) -> Set[str]: + """Return all dependencies and subdependencies of components. - Returns an empty list if a circular dependency is detected - or the component could not be loaded. In both cases, the error is - logged. + Raises CircularDependency if a circular dependency is found. Async friendly. """ - return _load_order_component(hass, comp_name, OrderedSet(), set()) + return _component_dependencies(hass, comp_name, set(), set()) -def _load_order_component(hass, # type: HomeAssistant - comp_name: str, load_order: OrderedSet, - loading: Set) -> OrderedSet: - """Recursive function to get load order of components. +def _component_dependencies(hass, # type: HomeAssistant + comp_name: str, loaded: Set[str], + loading: Set) -> Set[str]: + """Recursive function to get component dependencies. Async friendly. """ component = get_component(hass, comp_name) - # If None it does not exist, error already thrown by get_component. if component is None: - return OrderedSet() + raise ComponentNotFound(comp_name) loading.add(comp_name) for dependency in getattr(component, 'DEPENDENCIES', []): # Check not already loaded - if dependency in load_order: + if dependency in loaded: continue # If we are already loading it, we have a circular dependency. if dependency in loading: - _LOGGER.error("Circular dependency detected: %s -> %s", - comp_name, dependency) - return OrderedSet() + raise CircularDependency(comp_name, dependency) - dep_load_order = _load_order_component( - hass, dependency, load_order, loading) + dep_loaded = _component_dependencies( + hass, dependency, loaded, loading) - # length == 0 means error loading dependency or children - if not dep_load_order: - _LOGGER.error("Error loading %s dependency: %s", - comp_name, dependency) - return OrderedSet() + loaded.update(dep_loaded) - load_order.update(dep_load_order) - - load_order.add(comp_name) + loaded.add(comp_name) loading.remove(comp_name) - return load_order + return loaded diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b9443c287a1..d0a192a9dc7 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,18 +1,18 @@ aiohttp==3.5.4 -astral==1.8 +astral==1.9.2 async_timeout==3.0.1 attrs==18.2.0 bcrypt==3.1.5 certifi>=2018.04.16 jinja2>=2.10 PyJWT==1.6.4 -cryptography==2.3.1 +cryptography==2.5 pip>=8.0.3 python-slugify==1.2.6 pytz>=2018.07 pyyaml>=3.13,<4 requests==2.21.0 -ruamel.yaml==0.15.85 +ruamel.yaml==0.15.88 voluptuous==0.11.5 voluptuous-serialize==2.0.0 diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 49aae2178fc..29c8e22d45d 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -63,7 +63,7 @@ async def _async_process_dependencies( blacklisted = [dep for dep in dependencies if dep in loader.DEPENDENCY_BLACKLIST] - if blacklisted: + if blacklisted and name != 'default_config': _LOGGER.error("Unable to set up dependencies of %s: " "found blacklisted dependencies: %s", name, ', '.join(blacklisted)) @@ -106,12 +106,18 @@ async def _async_setup_component(hass: core.HomeAssistant, log_error("Component not found.", False) return False - # Validate no circular dependencies - components = loader.load_order_component(hass, domain) - - # OrderedSet is empty if component or dependencies could not be resolved - if not components: - log_error("Unable to resolve component or dependencies.") + # Validate all dependencies exist and there are no circular dependencies + try: + loader.component_dependencies(hass, domain) + except loader.ComponentNotFound as err: + _LOGGER.error( + "Not setting up %s because we are unable to resolve " + "(sub)dependency %s", domain, err.domain) + return False + except loader.CircularDependency as err: + _LOGGER.error( + "Not setting up %s because it contains a circular dependency: " + "%s -> %s", domain, err.from_domain, err.to_domain) return False processed_config = \ diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index b4d45b48079..12cd543a872 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -1,7 +1,6 @@ """Helper methods for various modules.""" import asyncio from datetime import datetime, timedelta -from itertools import chain import threading import re import enum @@ -141,96 +140,6 @@ class OrderedEnum(enum.Enum): return NotImplemented -class OrderedSet(MutableSet[T]): - """Ordered set taken from http://code.activestate.com/recipes/576694/.""" - - def __init__(self, iterable: Optional[Iterable[T]] = None) -> None: - """Initialize the set.""" - self.end = end = [] # type: List[Any] - end += [None, end, end] # sentinel node for doubly linked list - self.map = {} # type: Dict[T, List] # key --> [key, prev, next] - if iterable is not None: - self |= iterable # type: ignore - - def __len__(self) -> int: - """Return the length of the set.""" - return len(self.map) - - def __contains__(self, key: T) -> bool: # type: ignore - """Check if key is in set.""" - return key in self.map - - # pylint: disable=arguments-differ - def add(self, key: T) -> None: - """Add an element to the end of the set.""" - if key not in self.map: - end = self.end - curr = end[1] - curr[2] = end[1] = self.map[key] = [key, curr, end] - - def promote(self, key: T) -> None: - """Promote element to beginning of the set, add if not there.""" - if key in self.map: - self.discard(key) - - begin = self.end[2] - curr = begin[1] - curr[2] = begin[1] = self.map[key] = [key, curr, begin] - - # pylint: disable=arguments-differ - def discard(self, key: T) -> None: - """Discard an element from the set.""" - if key in self.map: - key, prev_item, next_item = self.map.pop(key) - prev_item[2] = next_item - next_item[1] = prev_item - - def __iter__(self) -> Iterator[T]: - """Iterate of the set.""" - end = self.end - curr = end[2] - while curr is not end: - yield curr[0] - curr = curr[2] - - def __reversed__(self) -> Iterator[T]: - """Reverse the ordering.""" - end = self.end - curr = end[1] - while curr is not end: - yield curr[0] - curr = curr[1] - - # pylint: disable=arguments-differ - def pop(self, last: bool = True) -> T: - """Pop element of the end of the set. - - Set last=False to pop from the beginning. - """ - if not self: - raise KeyError('set is empty') - key = self.end[1][0] if last else self.end[2][0] - self.discard(key) - return key # type: ignore - - def update(self, *args: Any) -> None: - """Add elements from args to the set.""" - for item in chain(*args): - self.add(item) - - def __repr__(self) -> str: - """Return the representation.""" - if not self: - return '%s()' % (self.__class__.__name__,) - return '%s(%r)' % (self.__class__.__name__, list(self)) - - def __eq__(self, other: Any) -> bool: - """Return the comparison.""" - if isinstance(other, OrderedSet): - return len(self) == len(other) and list(self) == list(other) - return set(self) == set(other) - - class Throttle: """A class for throttling the execution of tasks. diff --git a/requirements_all.txt b/requirements_all.txt index 171be8718e0..ac85efe3be7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1,19 +1,19 @@ # Home Assistant core aiohttp==3.5.4 -astral==1.8 +astral==1.9.2 async_timeout==3.0.1 attrs==18.2.0 bcrypt==3.1.5 certifi>=2018.04.16 jinja2>=2.10 PyJWT==1.6.4 -cryptography==2.3.1 +cryptography==2.5 pip>=8.0.3 python-slugify==1.2.6 pytz>=2018.07 pyyaml>=3.13,<4 requests==2.21.0 -ruamel.yaml==0.15.85 +ruamel.yaml==0.15.88 voluptuous==0.11.5 voluptuous-serialize==2.0.0 @@ -38,6 +38,9 @@ HAP-python==2.4.2 # homeassistant.components.notify.mastodon Mastodon.py==1.3.1 +# homeassistant.components.sensor.github +PyGithub==1.43.5 + # homeassistant.components.isy994 PyISY==1.1.1 @@ -87,7 +90,7 @@ abodepy==0.15.0 afsapi==0.0.4 # homeassistant.components.ambient_station -aioambient==0.1.0 +aioambient==0.1.2 # homeassistant.components.asuswrt aioasuswrt==1.1.20 @@ -99,7 +102,7 @@ aioautomatic==0.6.5 aiodns==1.1.1 # homeassistant.components.esphome -aioesphomeapi==1.4.2 +aioesphomeapi==1.5.0 # homeassistant.components.freebox aiofreepybox==0.0.6 @@ -107,8 +110,8 @@ aiofreepybox==0.0.6 # homeassistant.components.camera.yi aioftp==0.12.0 -# homeassistant.components.remote.harmony -aioharmony==0.1.5 +# homeassistant.components.harmony.remote +aioharmony==0.1.8 # homeassistant.components.emulated_hue # homeassistant.components.http @@ -121,12 +124,12 @@ aiohue==1.9.0 aioiliad==0.1.1 # homeassistant.components.sensor.imap -aioimaplib==0.7.13 +aioimaplib==0.7.15 # homeassistant.components.lifx aiolifx==0.6.7 -# homeassistant.components.light.lifx +# homeassistant.components.lifx.light aiolifx_effects==0.2.1 # homeassistant.components.scene.hunterdouglas_powerview @@ -259,7 +262,7 @@ caldav==0.5.0 ciscosparkapi==0.4.2 # homeassistant.components.sensor.co2signal -co2signal==0.4.1 +co2signal==0.4.2 # homeassistant.components.coinbase coinbase==2.1.0 @@ -275,14 +278,14 @@ colorlog==4.0.2 concord232==0.15 # homeassistant.components.climate.eq3btsmart -# homeassistant.components.device_tracker.xiaomi_miio -# homeassistant.components.fan.xiaomi_miio -# homeassistant.components.light.xiaomi_miio -# homeassistant.components.remote.xiaomi_miio # homeassistant.components.sensor.eddystone_temperature -# homeassistant.components.sensor.xiaomi_miio -# homeassistant.components.switch.xiaomi_miio -# homeassistant.components.vacuum.xiaomi_miio +# homeassistant.components.xiaomi_miio.device_tracker +# homeassistant.components.xiaomi_miio.fan +# homeassistant.components.xiaomi_miio.light +# homeassistant.components.xiaomi_miio.remote +# homeassistant.components.xiaomi_miio.sensor +# homeassistant.components.xiaomi_miio.switch +# homeassistant.components.xiaomi_miio.vacuum construct==2.9.45 # homeassistant.scripts.credstash @@ -315,7 +318,7 @@ defusedxml==0.5.0 deluge-client==1.4.0 # homeassistant.components.media_player.denonavr -denonavr==0.7.7 +denonavr==0.7.8 # homeassistant.components.media_player.directv directpy==0.5 @@ -327,7 +330,7 @@ discogs_client==2.2.1 discord.py==0.16.12 # homeassistant.components.updater -distro==1.3.0 +distro==1.4.0 # homeassistant.components.switch.digitalloggers dlipower==0.7.165 @@ -342,9 +345,12 @@ dovado==0.4.1 dsmr_parser==0.12 # homeassistant.components.dweet -# homeassistant.components.sensor.dweet +# homeassistant.components.dweet.sensor dweepy==0.3.0 +# homeassistant.components.ebusd +ebusdpy==0.0.16 + # homeassistant.components.ecoal_boiler ecoaliface==0.4.0 @@ -398,14 +404,14 @@ evohomeclient==0.2.8 # homeassistant.components.image_processing.dlib_face_identify # face_recognition==1.0.0 -# homeassistant.components.sensor.fastdotcom +# homeassistant.components.fastdotcom fastdotcom==0.0.3 # homeassistant.components.sensor.fedex fedexdeliverymanager==1.0.6 # homeassistant.components.feedreader -feedparser==5.2.1 +feedparser-homeassistant==5.2.2.dev1 # homeassistant.components.fibaro fiblary3==0.1.7 @@ -439,7 +445,7 @@ freesms==0.1.2 # homeassistant.components.switch.fritzdect fritzhome==1.0.4 -# homeassistant.components.tts.google +# homeassistant.components.google.tts gTTS-token==1.1.3 # homeassistant.components.sensor.gearbest @@ -456,9 +462,6 @@ geojson_client==0.3 # homeassistant.components.sensor.geo_rss_events georss_client==0.5 -# homeassistant.components.device_tracker.googlehome -ghlocalapi==0.3.5 - # homeassistant.components.sensor.gitter gitterpy==0.1.7 @@ -471,6 +474,12 @@ gntp==1.0.3 # homeassistant.components.google google-api-python-client==1.6.4 +# homeassistant.components.google_pubsub +google-cloud-pubsub==0.39.1 + +# homeassistant.components.googlehome +googledevices==1.0.2 + # homeassistant.components.sensor.google_travel_time googlemaps==2.5.1 @@ -526,7 +535,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190203.0 +home-assistant-frontend==20190220.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 @@ -535,7 +544,7 @@ homeassistant-pyozw==0.1.2 homekit==0.12.2 # homeassistant.components.homematicip_cloud -homematicip==0.10.4 +homematicip==0.10.5 # homeassistant.components.google # homeassistant.components.remember_the_milk @@ -633,7 +642,7 @@ linode-api==4.1.9b1 liveboxplaytv==2.0.2 # homeassistant.components.lametric -# homeassistant.components.notify.lametric +# homeassistant.components.lametric.notify lmnotify==0.0.4 # homeassistant.components.device_tracker.google_maps @@ -882,16 +891,17 @@ pyCEC==0.4.13 # homeassistant.components.light.tplink # homeassistant.components.switch.tplink -pyHS100==0.3.3 +pyHS100==0.3.4 +# homeassistant.components.air_quality.norway_air # homeassistant.components.weather.met -pyMetno==0.3.0 +pyMetno==0.4.5 # homeassistant.components.rfxtrx pyRFXtrx==0.23 # homeassistant.components.switch.switchmate -pySwitchmate==0.4.4 +pySwitchmate==0.4.5 # homeassistant.components.tibber pyTibber==0.9.4 @@ -906,7 +916,7 @@ pyW800rf32==0.1 # py_noaa==0.3.0 # homeassistant.components.ads -pyads==2.2.6 +pyads==3.0.7 # homeassistant.components.sensor.aftership pyaftership==0.1.2 @@ -921,7 +931,7 @@ pyalarmdotcom==0.3.2 pyarlo==0.2.3 # homeassistant.components.netatmo -pyatmo==1.4 +pyatmo==1.8 # homeassistant.components.apple_tv pyatv==0.3.12 @@ -946,7 +956,7 @@ pycfdns==0.0.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==2.1.0 +pychromecast==2.5.2 # homeassistant.components.media_player.cmus pycmus==0.1.1 @@ -954,6 +964,9 @@ pycmus==0.1.1 # homeassistant.components.comfoconnect pycomfoconnect==0.3 +# homeassistant.components.climate.coolmaster +pycoolmasternet==0.0.4 + # homeassistant.components.tts.microsoft pycsspeechtts==1.0.2 @@ -988,7 +1001,7 @@ pyeconet==0.0.6 pyedimax==0.1 # homeassistant.components.eight_sleep -pyeight==0.1.0 +pyeight==0.1.1 # homeassistant.components.media_player.emby pyemby==1.6 @@ -1060,8 +1073,8 @@ pyialarm==0.3 # homeassistant.components.device_tracker.icloud pyicloud==0.9.1 -# homeassistant.components.weather.ipma -pyipma==1.1.6 +# homeassistant.components.ipma.weather +pyipma==1.2.1 # homeassistant.components.sensor.irish_rail_transport pyirishrail==0.0.2 @@ -1069,7 +1082,7 @@ pyirishrail==0.0.2 # homeassistant.components.binary_sensor.iss pyiss==1.0.1 -# homeassistant.components.remote.itach +# homeassistant.components.itach.remote pyitachip2ir==0.0.7 # homeassistant.components.kira @@ -1090,8 +1103,8 @@ pylaunches==0.2.0 # homeassistant.components.media_player.lg_netcast pylgnetcast-homeassistant==0.2.0.dev0 -# homeassistant.components.media_player.webostv -# homeassistant.components.notify.webostv +# homeassistant.components.webostv.media_player +# homeassistant.components.webostv.notify pylgtv==0.1.9 # homeassistant.components.sensor.linky @@ -1183,7 +1196,7 @@ pypck==0.5.9 pypjlink2==1.2.0 # homeassistant.components.point -pypoint==1.0.6 +pypoint==1.0.8 # homeassistant.components.sensor.pollen pypollencom==2.2.2 @@ -1228,7 +1241,7 @@ pysma==0.3.1 pysmartapp==0.3.0 # homeassistant.components.smartthings -pysmartthings==0.4.2 +pysmartthings==0.6.2 # homeassistant.components.device_tracker.snmp # homeassistant.components.sensor.snmp @@ -1253,8 +1266,8 @@ pytautulli==0.5.0 # homeassistant.components.media_player.liveboxplaytv pyteleloisirs==3.4 -# homeassistant.components.sensor.thinkingcleaner -# homeassistant.components.switch.thinkingcleaner +# homeassistant.components.thinkingcleaner.sensor +# homeassistant.components.thinkingcleaner.switch pythinkingcleaner==0.0.3 # homeassistant.components.sensor.blockchain @@ -1292,7 +1305,7 @@ python-gitlab==1.6.0 python-hpilo==3.9 # homeassistant.components.joaoapps_join -# homeassistant.components.notify.joaoapps_join +# homeassistant.components.joaoapps_join.notify python-join-api==0.0.2 # homeassistant.components.juicenet @@ -1301,13 +1314,13 @@ python-juicenet==0.0.5 # homeassistant.components.lirc # python-lirc==1.2.3 -# homeassistant.components.device_tracker.xiaomi_miio -# homeassistant.components.fan.xiaomi_miio -# homeassistant.components.light.xiaomi_miio -# homeassistant.components.remote.xiaomi_miio -# homeassistant.components.sensor.xiaomi_miio -# homeassistant.components.switch.xiaomi_miio -# homeassistant.components.vacuum.xiaomi_miio +# homeassistant.components.xiaomi_miio.device_tracker +# homeassistant.components.xiaomi_miio.fan +# homeassistant.components.xiaomi_miio.light +# homeassistant.components.xiaomi_miio.remote +# homeassistant.components.xiaomi_miio.sensor +# homeassistant.components.xiaomi_miio.switch +# homeassistant.components.xiaomi_miio.vacuum python-miio==0.4.4 # homeassistant.components.media_player.mpd @@ -1411,7 +1424,7 @@ pyvesync==0.1.1 pyvizio==0.0.4 # homeassistant.components.velux -pyvlx==0.1.3 +pyvlx==0.2.8 # homeassistant.components.notify.html5 pywebpush==1.6.0 @@ -1456,7 +1469,7 @@ recollect-waste==1.0.1 regenmaschine==1.1.0 # homeassistant.components.python_script -restrictedpython==4.0b7 +restrictedpython==4.0b8 # homeassistant.components.idteck_prox rfk101py==0.0.1 @@ -1470,6 +1483,9 @@ ring_doorbell==0.2.2 # homeassistant.components.device_tracker.ritassist ritassist==0.9.2 +# homeassistant.components.sensor.rejseplanen +rjpl==0.3.5 + # homeassistant.components.notify.rocketchat rocketchat-API==0.6.1 @@ -1489,7 +1505,7 @@ russound==0.1.9 russound_rio==0.1.4 # homeassistant.components.media_player.yamaha -rxv==0.5.1 +rxv==0.6.0 # homeassistant.components.media_player.samsungtv samsungctl[websocket]==0.7.1 @@ -1538,7 +1554,7 @@ slacker==0.12.0 sleepyq==0.6 # homeassistant.components.notify.xmpp -slixmpp==1.4.1 +slixmpp==1.4.2 # homeassistant.components.smappee smappy==0.2.16 @@ -1552,7 +1568,7 @@ smappy==0.2.16 # smbus-cffi==0.5.1 # homeassistant.components.smhi -smhi-pkg==1.0.5 +smhi-pkg==1.0.8 # homeassistant.components.media_player.snapcast snapcast==2.0.9 @@ -1566,11 +1582,11 @@ solaredge==0.0.2 # homeassistant.components.climate.honeywell somecomfort==0.5.2 -# homeassistant.components.sensor.speedtest +# homeassistant.components.speedtestdotnet speedtest-cli==2.0.2 # homeassistant.components.spider -spiderpy==1.2.0 +spiderpy==1.3.1 # homeassistant.components.sensor.spotcrime spotcrime==1.0.3 @@ -1580,7 +1596,7 @@ spotipy-homeassistant==2.4.4.dev1 # homeassistant.components.recorder # homeassistant.components.sensor.sql -sqlalchemy==1.2.16 +sqlalchemy==1.2.17 # homeassistant.components.sensor.srp_energy srpenergy==1.0.5 @@ -1610,7 +1626,7 @@ suds-py3==1.3.3.0 swisshydrodata==0.0.3 # homeassistant.components.device_tracker.synology_srm -synology-srm==0.0.3 +synology-srm==0.0.4 # homeassistant.components.tahoma tahoma-api==0.0.14 @@ -1640,7 +1656,7 @@ temperusb==1.5.3 teslajsonpy==0.0.23 # homeassistant.components.sensor.thermoworks_smoke -thermoworks_smoke==0.1.7 +thermoworks_smoke==0.1.8 # homeassistant.components.thingspeak thingspeak==0.4.1 @@ -1726,7 +1742,7 @@ waterfurnace==1.1.0 # homeassistant.components.media_player.gpmdp websocket-client==0.54.0 -# homeassistant.components.media_player.webostv +# homeassistant.components.webostv.media_player websockets==6.0 # homeassistant.components.wirelesstag @@ -1742,7 +1758,7 @@ xbee-helper==0.0.7 xboxapi==0.1.1 # homeassistant.components.knx -xknx==0.9.3 +xknx==0.9.4 # homeassistant.components.media_player.bluesound # homeassistant.components.sensor.startca @@ -1751,6 +1767,9 @@ xknx==0.9.3 # homeassistant.components.sensor.zestimate xmltodict==0.11.0 +# homeassistant.components.xs1 +xs1-api-client==2.3.5 + # homeassistant.components.sensor.yweather # homeassistant.components.weather.yweather yahooweather==0.10 @@ -1765,7 +1784,7 @@ yeelight==0.4.3 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2019.01.24 +youtube_dl==2019.02.08 # homeassistant.components.light.zengge zengge==0.2 diff --git a/requirements_docs.txt b/requirements_docs.txt index 73a104b1ffe..1efe929d666 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -1,3 +1,3 @@ -Sphinx==1.8.3 +Sphinx==1.8.4 sphinx-autodoc-typehints==1.6.0 sphinx-autodoc-annotation==1.0.post1 \ No newline at end of file diff --git a/requirements_test.txt b/requirements_test.txt index af256efc709..b9da9890c61 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -4,9 +4,9 @@ asynctest==0.12.2 coveralls==1.2.0 flake8-docstrings==1.3.0 -flake8==3.6.0 +flake8==3.7.5 mock-open==1.3.1 -mypy==0.650 +mypy==0.660 pydocstyle==3.0.0 pylint==2.2.2 pytest-aiohttp==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ec1ece765f9..1ee80607150 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -5,9 +5,9 @@ asynctest==0.12.2 coveralls==1.2.0 flake8-docstrings==1.3.0 -flake8==3.6.0 +flake8==3.7.5 mock-open==1.3.1 -mypy==0.650 +mypy==0.660 pydocstyle==3.0.0 pylint==2.2.2 pytest-aiohttp==0.3.0 @@ -31,7 +31,7 @@ PyTransportNSW==0.1.1 YesssSMS==0.2.3 # homeassistant.components.ambient_station -aioambient==0.1.0 +aioambient==0.1.2 # homeassistant.components.device_tracker.automatic aioautomatic==0.6.5 @@ -49,6 +49,9 @@ aiounifi==4 # homeassistant.components.notify.apns apns2==0.3.0 +# homeassistant.components.zha +bellows==0.7.0 + # homeassistant.components.calendar.caldav caldav==0.5.0 @@ -81,12 +84,12 @@ ephem==3.7.6.0 evohomeclient==0.2.8 # homeassistant.components.feedreader -feedparser==5.2.1 +feedparser-homeassistant==5.2.2.dev1 # homeassistant.components.sensor.foobot foobot_async==0.3.1 -# homeassistant.components.tts.google +# homeassistant.components.google.tts gTTS-token==1.1.3 # homeassistant.components.geo_location.geo_json_events @@ -113,13 +116,13 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190203.0 +home-assistant-frontend==20190220.0 # homeassistant.components.homekit_controller homekit==0.12.2 # homeassistant.components.homematicip_cloud -homematicip==0.10.4 +homematicip==0.10.5 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb @@ -214,7 +217,7 @@ pyqwikswitch==0.8 pysmartapp==0.3.0 # homeassistant.components.smartthings -pysmartthings==0.4.2 +pysmartthings==0.6.2 # homeassistant.components.sonos pysonos==0.0.6 @@ -248,7 +251,7 @@ pywebpush==1.6.0 regenmaschine==1.1.0 # homeassistant.components.python_script -restrictedpython==4.0b7 +restrictedpython==4.0b8 # homeassistant.components.rflink rflink==0.0.37 @@ -257,7 +260,7 @@ rflink==0.0.37 ring_doorbell==0.2.2 # homeassistant.components.media_player.yamaha -rxv==0.5.1 +rxv==0.6.0 # homeassistant.components.simplisafe simplisafe-python==3.1.14 @@ -266,14 +269,14 @@ simplisafe-python==3.1.14 sleepyq==0.6 # homeassistant.components.smhi -smhi-pkg==1.0.5 +smhi-pkg==1.0.8 # homeassistant.components.climate.honeywell somecomfort==0.5.2 # homeassistant.components.recorder # homeassistant.components.sensor.sql -sqlalchemy==1.2.16 +sqlalchemy==1.2.17 # homeassistant.components.sensor.srp_energy srpenergy==1.0.5 @@ -298,3 +301,6 @@ wakeonlan==1.1.6 # homeassistant.components.cloud warrant==0.6.1 + +# homeassistant.components.zha +zigpy==0.2.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 398b2791848..47028ef3530 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -51,7 +51,7 @@ TEST_REQUIREMENTS = ( 'enturclient', 'ephem', 'evohomeclient', - 'feedparser', + 'feedparser-homeassistant', 'foobot_async', 'geojson_client', 'georss_client', @@ -124,6 +124,8 @@ TEST_REQUIREMENTS = ( 'vultr', 'YesssSMS', 'ruamel.yaml', + 'zigpy', + 'bellows', ) IGNORE_PACKAGES = ( diff --git a/setup.py b/setup.py index 0ceaa7d55b3..52be310574a 100755 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ PACKAGES = find_packages(exclude=['tests', 'tests.*']) REQUIRES = [ 'aiohttp==3.5.4', - 'astral==1.8', + 'astral==1.9.2', 'async_timeout==3.0.1', 'attrs==18.2.0', 'bcrypt==3.1.5', @@ -41,13 +41,13 @@ REQUIRES = [ 'jinja2>=2.10', 'PyJWT==1.6.4', # PyJWT has loose dependency. We want the latest one. - 'cryptography==2.3.1', + 'cryptography==2.5', 'pip>=8.0.3', 'python-slugify==1.2.6', 'pytz>=2018.07', 'pyyaml>=3.13,<4', 'requests==2.21.0', - 'ruamel.yaml==0.15.85', + 'ruamel.yaml==0.15.88', 'voluptuous==0.11.5', 'voluptuous-serialize==2.0.0', ] diff --git a/tests/auth/providers/test_command_line.py b/tests/auth/providers/test_command_line.py new file mode 100644 index 00000000000..f22958e7e38 --- /dev/null +++ b/tests/auth/providers/test_command_line.py @@ -0,0 +1,148 @@ +"""Tests for the command_line auth provider.""" + +from unittest.mock import Mock +import os +import uuid + +import pytest + +from homeassistant import data_entry_flow +from homeassistant.auth import auth_store, models as auth_models, AuthManager +from homeassistant.auth.providers import command_line +from homeassistant.const import CONF_TYPE + +from tests.common import mock_coro + + +@pytest.fixture +def store(hass): + """Mock store.""" + return auth_store.AuthStore(hass) + + +@pytest.fixture +def provider(hass, store): + """Mock provider.""" + return command_line.CommandLineAuthProvider(hass, store, { + CONF_TYPE: "command_line", + command_line.CONF_COMMAND: os.path.join( + os.path.dirname(__file__), "test_command_line_cmd.sh" + ), + command_line.CONF_ARGS: [], + command_line.CONF_META: False, + }) + + +@pytest.fixture +def manager(hass, store, provider): + """Mock manager.""" + return AuthManager(hass, store, { + (provider.type, provider.id): provider + }, {}) + + +async def test_create_new_credential(manager, provider): + """Test that we create a new credential.""" + credentials = await provider.async_get_or_create_credentials({ + "username": "good-user", + "password": "good-pass", + }) + assert credentials.is_new is True + + user = await manager.async_get_or_create_user(credentials) + assert user.is_active + + +async def test_match_existing_credentials(store, provider): + """See if we match existing users.""" + existing = auth_models.Credentials( + id=uuid.uuid4(), + auth_provider_type="command_line", + auth_provider_id=None, + data={ + "username": "good-user" + }, + is_new=False, + ) + provider.async_credentials = Mock(return_value=mock_coro([existing])) + credentials = await provider.async_get_or_create_credentials({ + "username": "good-user", + "password": "irrelevant", + }) + assert credentials is existing + + +async def test_invalid_username(provider): + """Test we raise if incorrect user specified.""" + with pytest.raises(command_line.InvalidAuthError): + await provider.async_validate_login("bad-user", "good-pass") + + +async def test_invalid_password(provider): + """Test we raise if incorrect password specified.""" + with pytest.raises(command_line.InvalidAuthError): + await provider.async_validate_login("good-user", "bad-pass") + + +async def test_good_auth(provider): + """Test nothing is raised with good credentials.""" + await provider.async_validate_login("good-user", "good-pass") + + +async def test_good_auth_with_meta(manager, provider): + """Test metadata is added upon successful authentication.""" + provider.config[command_line.CONF_ARGS] = ["--with-meta"] + provider.config[command_line.CONF_META] = True + + await provider.async_validate_login("good-user", "good-pass") + + credentials = await provider.async_get_or_create_credentials({ + "username": "good-user", + "password": "good-pass", + }) + assert credentials.is_new is True + + user = await manager.async_get_or_create_user(credentials) + assert user.name == "Bob" + assert user.is_active + + +async def test_utf_8_username_password(provider): + """Test that we create a new credential.""" + credentials = await provider.async_get_or_create_credentials({ + "username": "ßßß", + "password": "äöü", + }) + assert credentials.is_new is True + + +async def test_login_flow_validates(provider): + """Test login flow.""" + flow = await provider.async_login_flow({}) + result = await flow.async_step_init() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await flow.async_step_init({ + "username": "bad-user", + "password": "bad-pass", + }) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result['errors']["base"] == "invalid_auth" + + result = await flow.async_step_init({ + "username": "good-user", + "password": "good-pass", + }) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"]["username"] == "good-user" + + +async def test_strip_username(provider): + """Test authentication works with username with whitespace around.""" + flow = await provider.async_login_flow({}) + result = await flow.async_step_init({ + "username": "\t\ngood-user ", + "password": "good-pass", + }) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"]["username"] == "good-user" diff --git a/tests/auth/providers/test_command_line_cmd.sh b/tests/auth/providers/test_command_line_cmd.sh new file mode 100755 index 00000000000..0e689e338f1 --- /dev/null +++ b/tests/auth/providers/test_command_line_cmd.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +if [ "$username" = "good-user" ] && [ "$password" = "good-pass" ]; then + echo "Auth should succeed." >&2 + if [ "$1" = "--with-meta" ]; then + echo "name=Bob" + fi + exit 0 +fi + +echo "Auth should fail." >&2 +exit 1 diff --git a/tests/common.py b/tests/common.py index 3642c5da6ec..409b020f728 100644 --- a/tests/common.py +++ b/tests/common.py @@ -486,6 +486,8 @@ class MockModule: class MockPlatform: """Provide a fake platform.""" + __name__ = "homeassistant.components.light.bla" + # pylint: disable=invalid-name def __init__(self, setup_platform=None, dependencies=None, platform_schema=None, async_setup_platform=None, diff --git a/tests/components/alert/__init__.py b/tests/components/alert/__init__.py new file mode 100644 index 00000000000..80e51236d53 --- /dev/null +++ b/tests/components/alert/__init__.py @@ -0,0 +1 @@ +"""Tests for the alert component.""" diff --git a/tests/components/test_alert.py b/tests/components/alert/test_init.py similarity index 100% rename from tests/components/test_alert.py rename to tests/components/alert/test_init.py diff --git a/tests/components/api/__init__.py b/tests/components/api/__init__.py new file mode 100644 index 00000000000..c72fd03f7de --- /dev/null +++ b/tests/components/api/__init__.py @@ -0,0 +1 @@ +"""Tests for the api component.""" diff --git a/tests/components/test_api.py b/tests/components/api/test_init.py similarity index 100% rename from tests/components/test_api.py rename to tests/components/api/test_init.py diff --git a/tests/components/arlo/__init__.py b/tests/components/arlo/__init__.py new file mode 100644 index 00000000000..82c69bf3755 --- /dev/null +++ b/tests/components/arlo/__init__.py @@ -0,0 +1 @@ +"""Tests for the Arlo integration.""" diff --git a/tests/components/sensor/test_arlo.py b/tests/components/arlo/test_sensor.py similarity index 98% rename from tests/components/sensor/test_arlo.py rename to tests/components/arlo/test_sensor.py index 732e47099c4..ffb879571dc 100644 --- a/tests/components/sensor/test_arlo.py +++ b/tests/components/arlo/test_sensor.py @@ -4,7 +4,7 @@ from unittest.mock import patch, MagicMock import pytest from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, ATTR_ATTRIBUTION) -from homeassistant.components.sensor import arlo +from homeassistant.components.arlo import sensor as arlo from homeassistant.components.arlo import DATA_ARLO @@ -94,7 +94,7 @@ def sensor_with_hass_data(default_sensor, hass): @pytest.fixture() def mock_dispatch(): """Mock the dispatcher connect method.""" - target = 'homeassistant.components.sensor.arlo.async_dispatcher_connect' + target = 'homeassistant.components.arlo.sensor.async_dispatcher_connect' with patch(target, MagicMock()) as _mock: yield _mock diff --git a/tests/components/binary_sensor/test_rest.py b/tests/components/binary_sensor/test_rest.py index d3df1a32ac7..befeca07251 100644 --- a/tests/components/binary_sensor/test_rest.py +++ b/tests/components/binary_sensor/test_rest.py @@ -99,7 +99,6 @@ class TestRestBinarySensorSetup(unittest.TestCase): 'method': 'GET', 'value_template': '{{ value_json.key }}', 'name': 'foo', - 'unit_of_measurement': 'MB', 'verify_ssl': 'true', 'authentication': 'basic', 'username': 'my username', @@ -122,7 +121,6 @@ class TestRestBinarySensorSetup(unittest.TestCase): 'value_template': '{{ value_json.key }}', 'payload': '{ "device": "toaster"}', 'name': 'foo', - 'unit_of_measurement': 'MB', 'verify_ssl': 'true', 'authentication': 'basic', 'username': 'my username', diff --git a/tests/components/binary_sensor/test_rflink.py b/tests/components/binary_sensor/test_rflink.py index 98147ff7e6f..c14107438b3 100644 --- a/tests/components/binary_sensor/test_rflink.py +++ b/tests/components/binary_sensor/test_rflink.py @@ -16,7 +16,7 @@ from homeassistant.const import ( import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed -from ..test_rflink import mock_rflink +from tests.components.rflink.test_init import mock_rflink DOMAIN = 'binary_sensor' diff --git a/tests/components/binary_sensor/test_ring.py b/tests/components/binary_sensor/test_ring.py index a4b243e7420..924ed01f9e8 100644 --- a/tests/components/binary_sensor/test_ring.py +++ b/tests/components/binary_sensor/test_ring.py @@ -6,7 +6,7 @@ import requests_mock from homeassistant.components.binary_sensor import ring from homeassistant.components import ring as base_ring -from tests.components.test_ring import ATTRIBUTION, VALID_CONFIG +from tests.components.ring.test_init import ATTRIBUTION, VALID_CONFIG from tests.common import ( get_test_config_dir, get_test_home_assistant, load_fixture) diff --git a/tests/components/binary_sensor/test_sleepiq.py b/tests/components/binary_sensor/test_sleepiq.py index 3f3d69ad9d3..524093d76b3 100644 --- a/tests/components/binary_sensor/test_sleepiq.py +++ b/tests/components/binary_sensor/test_sleepiq.py @@ -7,7 +7,7 @@ import requests_mock from homeassistant.setup import setup_component from homeassistant.components.binary_sensor import sleepiq -from tests.components.test_sleepiq import mock_responses +from tests.components.sleepiq.test_init import mock_responses from tests.common import get_test_home_assistant diff --git a/tests/components/binary_sensor/test_vultr.py b/tests/components/binary_sensor/test_vultr.py index ee19cf941a3..3dcba033d73 100644 --- a/tests/components/binary_sensor/test_vultr.py +++ b/tests/components/binary_sensor/test_vultr.py @@ -16,7 +16,7 @@ from homeassistant.components.vultr import ( from homeassistant.const import ( CONF_PLATFORM, CONF_NAME) -from tests.components.test_vultr import VALID_CONFIG +from tests.components.vultr.test_init import VALID_CONFIG from tests.common import ( get_test_home_assistant, load_fixture) diff --git a/tests/components/canary/__init__.py b/tests/components/canary/__init__.py new file mode 100644 index 00000000000..cc85edd806a --- /dev/null +++ b/tests/components/canary/__init__.py @@ -0,0 +1 @@ +"""Tests for the canary component.""" diff --git a/tests/components/test_canary.py b/tests/components/canary/test_init.py similarity index 100% rename from tests/components/test_canary.py rename to tests/components/canary/test_init.py diff --git a/tests/components/cast/test_init.py b/tests/components/cast/test_init.py index 0121cd1c794..9f8e07809cb 100644 --- a/tests/components/cast/test_init.py +++ b/tests/components/cast/test_init.py @@ -10,7 +10,7 @@ from tests.common import MockDependency, mock_coro async def test_creating_entry_sets_up_media_player(hass): """Test setting up Cast loads the media player.""" - with patch('homeassistant.components.media_player.cast.async_setup_entry', + with patch('homeassistant.components.cast.media_player.async_setup_entry', return_value=mock_coro(True)) as mock_setup, \ MockDependency('pychromecast', 'discovery'), \ patch('pychromecast.discovery.discover_chromecasts', diff --git a/tests/components/media_player/test_cast.py b/tests/components/cast/test_media_player.py similarity index 82% rename from tests/components/media_player/test_cast.py rename to tests/components/cast/test_media_player.py index 8fd1ae18841..b5d6220904f 100644 --- a/tests/components/media_player/test_cast.py +++ b/tests/components/cast/test_media_player.py @@ -10,11 +10,10 @@ import pytest from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.typing import HomeAssistantType -from homeassistant.components.media_player.cast import ChromecastInfo +from homeassistant.components.cast.media_player import ChromecastInfo from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.helpers.dispatcher import async_dispatcher_connect, \ - async_dispatcher_send -from homeassistant.components.media_player import cast +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.components.cast import media_player as cast from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, mock_coro @@ -44,7 +43,7 @@ def get_fake_chromecast_info(host='192.168.178.42', port=8009, uuid: Optional[UUID] = FakeUUID): """Generate a Fake ChromecastInfo with the specified arguments.""" return ChromecastInfo(host=host, port=port, uuid=uuid, - friendly_name="Speaker") + friendly_name="Speaker", service='the-service') async def async_setup_cast(hass, config=None, discovery_info=None): @@ -64,9 +63,10 @@ async def async_setup_cast_internal_discovery(hass, config=None, discovery_info=None): """Set up the cast platform and the discovery.""" listener = MagicMock(services={}) + browser = MagicMock(zc={}) with patch('pychromecast.start_discovery', - return_value=(listener, None)) as start_discovery: + return_value=(listener, browser)) as start_discovery: add_entities = await async_setup_cast(hass, config, discovery_info) await hass.async_block_till_done() await hass.async_block_till_done() @@ -120,8 +120,10 @@ def test_start_discovery_called_once(hass): @asyncio.coroutine def test_stop_discovery_called_on_stop(hass): """Test pychromecast.stop_discovery called on shutdown.""" + browser = MagicMock(zc={}) + with patch('pychromecast.start_discovery', - return_value=(None, 'the-browser')) as start_discovery: + return_value=(None, browser)) as start_discovery: # start_discovery should be called with empty config yield from async_setup_cast(hass, {}) @@ -132,38 +134,16 @@ def test_stop_discovery_called_on_stop(hass): hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) yield from hass.async_block_till_done() - stop_discovery.assert_called_once_with('the-browser') + stop_discovery.assert_called_once_with(browser) with patch('pychromecast.start_discovery', - return_value=(None, 'the-browser')) as start_discovery: + return_value=(None, browser)) as start_discovery: # start_discovery should be called again on re-startup yield from async_setup_cast(hass) assert start_discovery.call_count == 1 -async def test_internal_discovery_callback_only_generates_once(hass): - """Test discovery only called once per device.""" - discover_cast, _ = await async_setup_cast_internal_discovery(hass) - info = get_fake_chromecast_info() - - signal = MagicMock() - async_dispatcher_connect(hass, 'cast_discovered', signal) - - with patch('pychromecast.dial.get_device_status', return_value=None): - # discovering a cast device should call the dispatcher - discover_cast('the-service', info) - await hass.async_block_till_done() - discover = signal.mock_calls[0][1][0] - assert discover == info - signal.reset_mock() - - # discovering it a second time shouldn't - discover_cast('the-service', info) - await hass.async_block_till_done() - assert signal.call_count == 0 - - async def test_internal_discovery_callback_fill_out(hass): """Test internal discovery automatically filling out information.""" import pychromecast # imports mock pychromecast @@ -212,7 +192,7 @@ async def test_create_cast_device_with_uuid(hass): async def test_normal_chromecast_not_starting_discovery(hass): """Test cast platform not starting discovery when not required.""" # pylint: disable=no-member - with patch('homeassistant.components.media_player.cast.' + with patch('homeassistant.components.cast.media_player.' '_setup_internal_discovery') as setup_discovery: # normal (non-group) chromecast shouldn't start discovery. add_entities = await async_setup_cast(hass, {'host': 'host1'}) @@ -323,35 +303,6 @@ async def test_entity_media_states(hass: HomeAssistantType): assert state.state == 'unknown' -async def test_switched_host(hass: HomeAssistantType): - """Test cast device listens for changed hosts and disconnects old cast.""" - info = get_fake_chromecast_info() - full_info = attr.evolve(info, model_name='google home', - friendly_name='Speaker', uuid=FakeUUID) - - with patch('pychromecast.dial.get_device_status', - return_value=full_info): - chromecast, _ = await async_setup_media_player_cast(hass, full_info) - - chromecast2 = get_fake_chromecast(info) - with patch('pychromecast._get_chromecast_from_host', - return_value=chromecast2) as get_chromecast: - async_dispatcher_send(hass, cast.SIGNAL_CAST_DISCOVERED, full_info) - await hass.async_block_till_done() - assert get_chromecast.call_count == 0 - - changed = attr.evolve(full_info, friendly_name='Speaker 2') - async_dispatcher_send(hass, cast.SIGNAL_CAST_DISCOVERED, changed) - await hass.async_block_till_done() - assert get_chromecast.call_count == 0 - - changed = attr.evolve(changed, host='host2') - async_dispatcher_send(hass, cast.SIGNAL_CAST_DISCOVERED, changed) - await hass.async_block_till_done() - assert get_chromecast.call_count == 1 - assert chromecast.disconnect.call_count == 1 - - async def test_disconnect_on_stop(hass: HomeAssistantType): """Test cast device disconnects socket on stop.""" info = get_fake_chromecast_info() @@ -369,7 +320,7 @@ async def test_entry_setup_no_config(hass: HomeAssistantType): await async_setup_component(hass, 'cast', {}) with patch( - 'homeassistant.components.media_player.cast._async_setup_platform', + 'homeassistant.components.cast.media_player._async_setup_platform', return_value=mock_coro()) as mock_setup: await cast.async_setup_entry(hass, MockConfigEntry(), None) @@ -388,7 +339,7 @@ async def test_entry_setup_single_config(hass: HomeAssistantType): }) with patch( - 'homeassistant.components.media_player.cast._async_setup_platform', + 'homeassistant.components.cast.media_player._async_setup_platform', return_value=mock_coro()) as mock_setup: await cast.async_setup_entry(hass, MockConfigEntry(), None) @@ -408,7 +359,7 @@ async def test_entry_setup_list_config(hass: HomeAssistantType): }) with patch( - 'homeassistant.components.media_player.cast._async_setup_platform', + 'homeassistant.components.cast.media_player._async_setup_platform', return_value=mock_coro()) as mock_setup: await cast.async_setup_entry(hass, MockConfigEntry(), None) @@ -428,7 +379,7 @@ async def test_entry_setup_platform_not_ready(hass: HomeAssistantType): }) with patch( - 'homeassistant.components.media_player.cast._async_setup_platform', + 'homeassistant.components.cast.media_player._async_setup_platform', return_value=mock_coro(exception=Exception)) as mock_setup: with pytest.raises(PlatformNotReady): await cast.async_setup_entry(hass, MockConfigEntry(), None) diff --git a/tests/components/climate/test_reproduce_state.py b/tests/components/climate/test_reproduce_state.py new file mode 100644 index 00000000000..c16151320b4 --- /dev/null +++ b/tests/components/climate/test_reproduce_state.py @@ -0,0 +1,162 @@ +"""The tests for reproduction of state.""" + +import pytest + +from homeassistant.components.climate import STATE_HEAT, async_reproduce_states +from homeassistant.components.climate.const import ( + ATTR_AUX_HEAT, ATTR_AWAY_MODE, ATTR_HOLD_MODE, ATTR_HUMIDITY, + ATTR_OPERATION_MODE, ATTR_SWING_MODE, ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, DOMAIN, SERVICE_SET_AUX_HEAT, SERVICE_SET_AWAY_MODE, + SERVICE_SET_HOLD_MODE, SERVICE_SET_HUMIDITY, SERVICE_SET_OPERATION_MODE, + SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE) +from homeassistant.const import ( + ATTR_TEMPERATURE, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, STATE_ON) +from homeassistant.core import Context, State + +from tests.common import async_mock_service + +ENTITY_1 = 'climate.test1' +ENTITY_2 = 'climate.test2' + + +@pytest.mark.parametrize( + 'service,state', [ + (SERVICE_TURN_ON, STATE_ON), + (SERVICE_TURN_OFF, STATE_OFF), + ]) +async def test_state(hass, service, state): + """Test that we can turn a state into a service call.""" + calls_1 = async_mock_service(hass, DOMAIN, service) + + await async_reproduce_states(hass, [ + State(ENTITY_1, state) + ]) + + await hass.async_block_till_done() + + assert len(calls_1) == 1 + assert calls_1[0].data == {'entity_id': ENTITY_1} + + +async def test_turn_on_with_mode(hass): + """Test that state with additional attributes call multiple services.""" + calls_1 = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) + calls_2 = async_mock_service(hass, DOMAIN, SERVICE_SET_OPERATION_MODE) + + await async_reproduce_states(hass, [ + State(ENTITY_1, 'on', + {ATTR_OPERATION_MODE: STATE_HEAT}) + ]) + + await hass.async_block_till_done() + + assert len(calls_1) == 1 + assert calls_1[0].data == {'entity_id': ENTITY_1} + + assert len(calls_2) == 1 + assert calls_2[0].data == {'entity_id': ENTITY_1, + ATTR_OPERATION_MODE: STATE_HEAT} + + +async def test_multiple_same_state(hass): + """Test that multiple states with same state gets calls.""" + calls_1 = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) + + await async_reproduce_states(hass, [ + State(ENTITY_1, 'on'), + State(ENTITY_2, 'on'), + ]) + + await hass.async_block_till_done() + + assert len(calls_1) == 2 + # order is not guaranteed + assert any(call.data == {'entity_id': ENTITY_1} for call in calls_1) + assert any(call.data == {'entity_id': ENTITY_2} for call in calls_1) + + +async def test_multiple_different_state(hass): + """Test that multiple states with different state gets calls.""" + calls_1 = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) + calls_2 = async_mock_service(hass, DOMAIN, SERVICE_TURN_OFF) + + await async_reproduce_states(hass, [ + State(ENTITY_1, 'on'), + State(ENTITY_2, 'off'), + ]) + + await hass.async_block_till_done() + + assert len(calls_1) == 1 + assert calls_1[0].data == {'entity_id': ENTITY_1} + assert len(calls_2) == 1 + assert calls_2[0].data == {'entity_id': ENTITY_2} + + +async def test_state_with_context(hass): + """Test that context is forwarded.""" + calls = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) + + context = Context() + + await async_reproduce_states(hass, [ + State(ENTITY_1, 'on') + ], context) + + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].data == {'entity_id': ENTITY_1} + assert calls[0].context == context + + +async def test_attribute_no_state(hass): + """Test that no state service call is made with none state.""" + calls_1 = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) + calls_2 = async_mock_service(hass, DOMAIN, SERVICE_TURN_OFF) + calls_3 = async_mock_service(hass, DOMAIN, SERVICE_SET_OPERATION_MODE) + + value = "dummy" + + await async_reproduce_states(hass, [ + State(ENTITY_1, None, + {ATTR_OPERATION_MODE: value}) + ]) + + await hass.async_block_till_done() + + assert len(calls_1) == 0 + assert len(calls_2) == 0 + assert len(calls_3) == 1 + assert calls_3[0].data == {'entity_id': ENTITY_1, + ATTR_OPERATION_MODE: value} + + +@pytest.mark.parametrize( + 'service,attribute', [ + (SERVICE_SET_OPERATION_MODE, ATTR_OPERATION_MODE), + (SERVICE_SET_AUX_HEAT, ATTR_AUX_HEAT), + (SERVICE_SET_AWAY_MODE, ATTR_AWAY_MODE), + (SERVICE_SET_HOLD_MODE, ATTR_HOLD_MODE), + (SERVICE_SET_SWING_MODE, ATTR_SWING_MODE), + (SERVICE_SET_HUMIDITY, ATTR_HUMIDITY), + (SERVICE_SET_TEMPERATURE, ATTR_TEMPERATURE), + (SERVICE_SET_TEMPERATURE, ATTR_TARGET_TEMP_HIGH), + (SERVICE_SET_TEMPERATURE, ATTR_TARGET_TEMP_LOW), + ]) +async def test_attribute(hass, service, attribute): + """Test that service call is made for each attribute.""" + calls_1 = async_mock_service(hass, DOMAIN, service) + + value = "dummy" + + await async_reproduce_states(hass, [ + State(ENTITY_1, None, + {attribute: value}) + ]) + + await hass.async_block_till_done() + + assert len(calls_1) == 1 + assert calls_1[0].data == {'entity_id': ENTITY_1, + attribute: value} diff --git a/tests/components/configurator/__init__.py b/tests/components/configurator/__init__.py new file mode 100644 index 00000000000..a533a39a93c --- /dev/null +++ b/tests/components/configurator/__init__.py @@ -0,0 +1 @@ +"""Tests for the configurator component.""" diff --git a/tests/components/test_configurator.py b/tests/components/configurator/test_init.py similarity index 100% rename from tests/components/test_configurator.py rename to tests/components/configurator/test_init.py diff --git a/tests/components/conversation/__init__.py b/tests/components/conversation/__init__.py new file mode 100644 index 00000000000..ea244c00df8 --- /dev/null +++ b/tests/components/conversation/__init__.py @@ -0,0 +1 @@ +"""Tests for the conversation component.""" diff --git a/tests/components/test_conversation.py b/tests/components/conversation/test_init.py similarity index 100% rename from tests/components/test_conversation.py rename to tests/components/conversation/test_init.py diff --git a/tests/components/cover/test_rflink.py b/tests/components/cover/test_rflink.py index 4f88d24d97f..d531574d34f 100644 --- a/tests/components/cover/test_rflink.py +++ b/tests/components/cover/test_rflink.py @@ -14,7 +14,7 @@ from homeassistant.const import ( from homeassistant.core import callback, State, CoreState from tests.common import mock_restore_cache -from ..test_rflink import mock_rflink +from tests.components.rflink.test_init import mock_rflink DOMAIN = 'cover' @@ -416,33 +416,6 @@ async def test_nogroup_device_id(hass, monkeypatch): assert hass.states.get(DOMAIN + '.test').state == STATE_OPEN -async def test_disable_automatic_add(hass, monkeypatch): - """If disabled new devices should not be automatically added.""" - config = { - 'rflink': { - 'port': '/dev/ttyABC0', - }, - DOMAIN: { - 'platform': 'rflink', - 'automatic_add': False, - }, - } - - # setup mocking rflink module - event_callback, _, _, _ = await mock_rflink( - hass, config, DOMAIN, monkeypatch) - - # test event for new unconfigured sensor - event_callback({ - 'id': 'protocol_0_0', - 'command': 'down', - }) - await hass.async_block_till_done() - - # make sure new device is not added - assert not hass.states.get(DOMAIN + '.protocol_0_0') - - async def test_restore_state(hass, monkeypatch): """Ensure states are restored on startup.""" config = { diff --git a/tests/components/datadog/__init__.py b/tests/components/datadog/__init__.py new file mode 100644 index 00000000000..4f29b839b85 --- /dev/null +++ b/tests/components/datadog/__init__.py @@ -0,0 +1 @@ +"""Tests for the datadog component.""" diff --git a/tests/components/test_datadog.py b/tests/components/datadog/test_init.py similarity index 100% rename from tests/components/test_datadog.py rename to tests/components/datadog/test_init.py diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index 3411f96b981..dbc45c955b5 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -1,6 +1,9 @@ """Test deCONZ gateway.""" from unittest.mock import Mock, patch +import pytest + +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.components.deconz import gateway from tests.common import mock_coro @@ -56,8 +59,10 @@ async def test_gateway_retry(): deconz_gateway = gateway.DeconzGateway(hass, entry) - with patch.object(gateway, 'get_gateway', return_value=mock_coro(False)): - assert await deconz_gateway.async_setup() is False + with patch.object( + gateway, 'get_gateway', return_value=mock_coro(False) + ), pytest.raises(ConfigEntryNotReady): + await deconz_gateway.async_setup() async def test_connection_status(hass): @@ -118,22 +123,6 @@ async def test_shutdown(): assert len(deconz_gateway.api.close.mock_calls) == 1 -async def test_reset_cancel_retry(): - """Verify async reset can handle a scheduled retry.""" - hass = Mock() - entry = Mock() - entry.data = ENTRY_CONFIG - - deconz_gateway = gateway.DeconzGateway(hass, entry) - - with patch.object(gateway, 'get_gateway', return_value=mock_coro(False)): - assert await deconz_gateway.async_setup() is False - - assert deconz_gateway._cancel_retry_setup is not None - - assert await deconz_gateway.async_reset() is True - - async def test_reset_after_successful_setup(): """Verify that reset works on a setup component.""" hass = Mock() diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index 5fa8ddcfe38..cbba47eb431 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -4,6 +4,7 @@ from unittest.mock import Mock, patch import pytest import voluptuous as vol +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.setup import async_setup_component from homeassistant.components import deconz @@ -79,9 +80,11 @@ async def test_setup_entry_no_available_bridge(hass): """Test setup entry fails if deCONZ is not available.""" entry = Mock() entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} - with patch('pydeconz.DeconzSession.async_load_parameters', - return_value=mock_coro(False)): - assert await deconz.async_setup_entry(hass, entry) is False + with patch( + 'pydeconz.DeconzSession.async_load_parameters', + return_value=mock_coro(False) + ), pytest.raises(ConfigEntryNotReady): + await deconz.async_setup_entry(hass, entry) async def test_setup_entry_successful(hass): diff --git a/tests/components/default_config/__init__.py b/tests/components/default_config/__init__.py new file mode 100644 index 00000000000..7ee4658fed5 --- /dev/null +++ b/tests/components/default_config/__init__.py @@ -0,0 +1 @@ +"""Tests for the default config component.""" diff --git a/tests/components/default_config/test_init.py b/tests/components/default_config/test_init.py new file mode 100644 index 00000000000..94adf53cb2d --- /dev/null +++ b/tests/components/default_config/test_init.py @@ -0,0 +1,27 @@ +"""Test the default_config init.""" +from unittest.mock import patch + +from homeassistant.setup import async_setup_component + +import pytest + +from tests.common import MockDependency + + +@pytest.fixture(autouse=True) +def netdisco_mock(): + """Mock netdisco.""" + with MockDependency('netdisco', 'discovery'): + yield + + +@pytest.fixture(autouse=True) +def recorder_url_mock(): + """Mock recorder url.""" + with patch('homeassistant.components.recorder.DEFAULT_URL', 'sqlite://'): + yield + + +async def test_setup(hass): + """Test setup.""" + assert await async_setup_component(hass, 'default_config', {}) diff --git a/tests/components/demo/__init__.py b/tests/components/demo/__init__.py new file mode 100644 index 00000000000..68d228076b9 --- /dev/null +++ b/tests/components/demo/__init__.py @@ -0,0 +1 @@ +"""Tests for the demo component.""" diff --git a/tests/components/test_demo.py b/tests/components/demo/test_init.py similarity index 100% rename from tests/components/test_demo.py rename to tests/components/demo/test_init.py diff --git a/tests/components/remote/test_demo.py b/tests/components/demo/test_remote.py similarity index 100% rename from tests/components/remote/test_demo.py rename to tests/components/demo/test_remote.py diff --git a/tests/components/device_sun_light_trigger/__init__.py b/tests/components/device_sun_light_trigger/__init__.py new file mode 100644 index 00000000000..4400ace7cd8 --- /dev/null +++ b/tests/components/device_sun_light_trigger/__init__.py @@ -0,0 +1 @@ +"""Tests for the device_sun_light_trigger component.""" diff --git a/tests/components/test_device_sun_light_trigger.py b/tests/components/device_sun_light_trigger/test_init.py similarity index 100% rename from tests/components/test_device_sun_light_trigger.py rename to tests/components/device_sun_light_trigger/test_init.py diff --git a/tests/components/device_tracker/test_mqtt.py b/tests/components/device_tracker/test_mqtt.py index abfa32ca06b..3e584aa541c 100644 --- a/tests/components/device_tracker/test_mqtt.py +++ b/tests/components/device_tracker/test_mqtt.py @@ -30,7 +30,7 @@ async def test_ensure_device_tracker_platform_validation(hass): """Check that Qos was added by validation.""" assert 'qos' in config - with patch('homeassistant.components.device_tracker.mqtt.' + with patch('homeassistant.components.mqtt.device_tracker.' 'async_setup_scanner', autospec=True, side_effect=mock_setup_scanner) as mock_sp: diff --git a/tests/components/device_tracker/test_owntracks.py b/tests/components/device_tracker/test_owntracks.py index 79d0bdb6f73..5e65e0a75c7 100644 --- a/tests/components/device_tracker/test_owntracks.py +++ b/tests/components/device_tracker/test_owntracks.py @@ -1273,8 +1273,8 @@ async def test_single_waypoint_import(hass, context): async def test_not_implemented_message(hass, context): """Handle not implemented message type.""" - patch_handler = patch('homeassistant.components.device_tracker.' - 'owntracks.async_handle_not_impl_msg', + patch_handler = patch('homeassistant.components.owntracks.' + 'device_tracker.async_handle_not_impl_msg', return_value=mock_coro(False)) patch_handler.start() assert not await send_message(hass, LWT_TOPIC, LWT_MESSAGE) @@ -1283,8 +1283,8 @@ async def test_not_implemented_message(hass, context): async def test_unsupported_message(hass, context): """Handle not implemented message type.""" - patch_handler = patch('homeassistant.components.device_tracker.' - 'owntracks.async_handle_unsupported_msg', + patch_handler = patch('homeassistant.components.owntracks.' + 'device_tracker.async_handle_unsupported_msg', return_value=mock_coro(False)) patch_handler.start() assert not await send_message(hass, BAD_TOPIC, BAD_MESSAGE) @@ -1366,7 +1366,7 @@ def config_context(hass, setup_comp): patch_save.stop() -@patch('homeassistant.components.device_tracker.owntracks.get_cipher', +@patch('homeassistant.components.owntracks.device_tracker.get_cipher', mock_cipher) async def test_encrypted_payload(hass, config_context): """Test encrypted payload.""" @@ -1377,7 +1377,7 @@ async def test_encrypted_payload(hass, config_context): assert_location_latitude(hass, LOCATION_MESSAGE['lat']) -@patch('homeassistant.components.device_tracker.owntracks.get_cipher', +@patch('homeassistant.components.owntracks.device_tracker.get_cipher', mock_cipher) async def test_encrypted_payload_topic_key(hass, config_context): """Test encrypted payload with a topic key.""" @@ -1390,7 +1390,7 @@ async def test_encrypted_payload_topic_key(hass, config_context): assert_location_latitude(hass, LOCATION_MESSAGE['lat']) -@patch('homeassistant.components.device_tracker.owntracks.get_cipher', +@patch('homeassistant.components.owntracks.device_tracker.get_cipher', mock_cipher) async def test_encrypted_payload_no_key(hass, config_context): """Test encrypted payload with no key, .""" @@ -1403,7 +1403,7 @@ async def test_encrypted_payload_no_key(hass, config_context): assert hass.states.get(DEVICE_TRACKER_STATE) is None -@patch('homeassistant.components.device_tracker.owntracks.get_cipher', +@patch('homeassistant.components.owntracks.device_tracker.get_cipher', mock_cipher) async def test_encrypted_payload_wrong_key(hass, config_context): """Test encrypted payload with wrong key.""" @@ -1414,7 +1414,7 @@ async def test_encrypted_payload_wrong_key(hass, config_context): assert hass.states.get(DEVICE_TRACKER_STATE) is None -@patch('homeassistant.components.device_tracker.owntracks.get_cipher', +@patch('homeassistant.components.owntracks.device_tracker.get_cipher', mock_cipher) async def test_encrypted_payload_wrong_topic_key(hass, config_context): """Test encrypted payload with wrong topic key.""" @@ -1427,7 +1427,7 @@ async def test_encrypted_payload_wrong_topic_key(hass, config_context): assert hass.states.get(DEVICE_TRACKER_STATE) is None -@patch('homeassistant.components.device_tracker.owntracks.get_cipher', +@patch('homeassistant.components.owntracks.device_tracker.get_cipher', mock_cipher) async def test_encrypted_payload_no_topic_key(hass, config_context): """Test encrypted payload with no topic key.""" diff --git a/tests/components/device_tracker/test_unifi.py b/tests/components/device_tracker/test_unifi.py index 33adff9adf8..b3ce1e93e4c 100644 --- a/tests/components/device_tracker/test_unifi.py +++ b/tests/components/device_tracker/test_unifi.py @@ -25,8 +25,8 @@ def mock_ctrl(): @pytest.fixture def mock_scanner(): """Mock UnifyScanner.""" - with mock.patch('homeassistant.components.device_tracker' - '.unifi.UnifiScanner') as scanner: + with mock.patch('homeassistant.components.device_tracker.unifi' + '.UnifiScanner') as scanner: yield scanner diff --git a/tests/components/discovery/__init__.py b/tests/components/discovery/__init__.py new file mode 100644 index 00000000000..b5744b42d6b --- /dev/null +++ b/tests/components/discovery/__init__.py @@ -0,0 +1 @@ +"""Tests for the discovery component.""" diff --git a/tests/components/test_discovery.py b/tests/components/discovery/test_init.py similarity index 100% rename from tests/components/test_discovery.py rename to tests/components/discovery/test_init.py diff --git a/tests/components/duckdns/__init__.py b/tests/components/duckdns/__init__.py new file mode 100644 index 00000000000..d8304b7cf68 --- /dev/null +++ b/tests/components/duckdns/__init__.py @@ -0,0 +1 @@ +"""Tests for the duckdns component.""" diff --git a/tests/components/test_duckdns.py b/tests/components/duckdns/test_init.py similarity index 100% rename from tests/components/test_duckdns.py rename to tests/components/duckdns/test_init.py diff --git a/tests/components/dyson/__init__.py b/tests/components/dyson/__init__.py new file mode 100644 index 00000000000..d4c814a37db --- /dev/null +++ b/tests/components/dyson/__init__.py @@ -0,0 +1 @@ +"""Tests for the dyson component.""" diff --git a/tests/components/test_dyson.py b/tests/components/dyson/test_init.py similarity index 100% rename from tests/components/test_dyson.py rename to tests/components/dyson/test_init.py diff --git a/tests/components/ecobee/__init__.py b/tests/components/ecobee/__init__.py new file mode 100644 index 00000000000..389dc7101f9 --- /dev/null +++ b/tests/components/ecobee/__init__.py @@ -0,0 +1 @@ +"""Tests for Ecobee integration.""" diff --git a/tests/components/climate/test_ecobee.py b/tests/components/ecobee/test_climate.py similarity index 99% rename from tests/components/climate/test_ecobee.py rename to tests/components/ecobee/test_climate.py index 8a03cbcd191..965fb37dcb8 100644 --- a/tests/components/climate/test_ecobee.py +++ b/tests/components/ecobee/test_climate.py @@ -2,7 +2,7 @@ import unittest from unittest import mock import homeassistant.const as const -import homeassistant.components.climate.ecobee as ecobee +from homeassistant.components.ecobee import climate as ecobee from homeassistant.components.climate import STATE_OFF diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 70fe894debf..5e3d6d1019c 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -11,13 +11,17 @@ from tests.common import get_test_instance_port from homeassistant import core, const, setup import homeassistant.components as core_components from homeassistant.components import ( - fan, http, light, script, emulated_hue, media_player) + fan, http, light, script, emulated_hue, media_player, cover) from homeassistant.components.emulated_hue import Config from homeassistant.components.emulated_hue.hue_api import ( HUE_API_STATE_ON, HUE_API_STATE_BRI, HueUsernameView, HueOneLightStateView, HueAllLightsStateView, HueOneLightChangeView, HueAllGroupsStateView) from homeassistant.const import STATE_ON, STATE_OFF +import homeassistant.util.dt as dt_util +from datetime import timedelta +from tests.common import async_fire_time_changed + HTTP_SERVER_PORT = get_test_instance_port() BRIDGE_SERVER_PORT = get_test_instance_port() @@ -91,6 +95,15 @@ def hass_hue(loop, hass): ] })) + loop.run_until_complete( + setup.async_setup_component(hass, cover.DOMAIN, { + 'cover': [ + { + 'platform': 'demo', + } + ] + })) + # Kitchen light is explicitly excluded from being exposed kitchen_light_entity = hass.states.get('light.kitchen_lights') attrs = dict(kitchen_light_entity.attributes) @@ -115,6 +128,14 @@ def hass_hue(loop, hass): script_entity.entity_id, script_entity.state, attributes=attrs ) + # Expose cover + cover_entity = hass.states.get('cover.living_room_window') + attrs = dict(cover_entity.attributes) + attrs[emulated_hue.ATTR_EMULATED_HUE_HIDDEN] = False + hass.states.async_set( + cover_entity.entity_id, cover_entity.state, attributes=attrs + ) + return hass @@ -127,7 +148,11 @@ def hue_client(loop, hass_hue, aiohttp_client): emulated_hue.CONF_ENTITIES: { 'light.bed_light': { emulated_hue.CONF_ENTITY_HIDDEN: True + }, + 'cover.living_room_window': { + emulated_hue.CONF_ENTITY_HIDDEN: False } + } }) @@ -163,6 +188,7 @@ def test_discover_lights(hue_client): assert 'media_player.lounge_room' in devices assert 'fan.living_room_fan' in devices assert 'fan.ceiling_fan' not in devices + assert 'cover.living_room_window' in devices @asyncio.coroutine @@ -317,6 +343,98 @@ def test_put_light_state_media_player(hass_hue, hue_client): assert walkman.attributes[media_player.ATTR_MEDIA_VOLUME_LEVEL] == level +async def test_close_cover(hass_hue, hue_client): + """Test opening cover .""" + COVER_ID = "cover.living_room_window" + # Turn the office light off first + await hass_hue.services.async_call( + cover.DOMAIN, const.SERVICE_CLOSE_COVER, + {const.ATTR_ENTITY_ID: COVER_ID}, + blocking=True) + + cover_test = hass_hue.states.get(COVER_ID) + assert cover_test.state == 'closing' + + for _ in range(7): + future = dt_util.utcnow() + timedelta(seconds=1) + async_fire_time_changed(hass_hue, future) + await hass_hue.async_block_till_done() + + cover_test = hass_hue.states.get(COVER_ID) + assert cover_test.state == 'closed' + + # Go through the API to turn it on + cover_result = await perform_put_light_state( + hass_hue, hue_client, + COVER_ID, True, 100) + + assert cover_result.status == 200 + assert 'application/json' in cover_result.headers['content-type'] + + for _ in range(7): + future = dt_util.utcnow() + timedelta(seconds=1) + async_fire_time_changed(hass_hue, future) + await hass_hue.async_block_till_done() + + cover_result_json = await cover_result.json() + + assert len(cover_result_json) == 2 + + # Check to make sure the state changed + cover_test_2 = hass_hue.states.get(COVER_ID) + assert cover_test_2.state == 'open' + + +async def test_set_position_cover(hass_hue, hue_client): + """Test setting postion cover .""" + COVER_ID = "cover.living_room_window" + # Turn the office light off first + await hass_hue.services.async_call( + cover.DOMAIN, const.SERVICE_CLOSE_COVER, + {const.ATTR_ENTITY_ID: COVER_ID}, + blocking=True) + + cover_test = hass_hue.states.get(COVER_ID) + assert cover_test.state == 'closing' + + for _ in range(7): + future = dt_util.utcnow() + timedelta(seconds=1) + async_fire_time_changed(hass_hue, future) + await hass_hue.async_block_till_done() + + cover_test = hass_hue.states.get(COVER_ID) + assert cover_test.state == 'closed' + + level = 20 + brightness = round(level/100*255) + + # Go through the API to open + cover_result = await perform_put_light_state( + hass_hue, hue_client, + COVER_ID, False, brightness) + + assert cover_result.status == 200 + assert 'application/json' in cover_result.headers['content-type'] + + cover_result_json = await cover_result.json() + + assert len(cover_result_json) == 2 + assert True, cover_result_json[0]['success'][ + '/lights/cover.living_room_window/state/on'] + assert cover_result_json[1]['success'][ + '/lights/cover.living_room_window/state/bri'] == level + + for _ in range(100): + future = dt_util.utcnow() + timedelta(seconds=1) + async_fire_time_changed(hass_hue, future) + await hass_hue.async_block_till_done() + + # Check to make sure the state changed + cover_test_2 = hass_hue.states.get(COVER_ID) + assert cover_test_2.state == 'open' + assert cover_test_2.attributes.get('current_position') == level + + @asyncio.coroutine def test_put_light_state_fan(hass_hue, hue_client): """Test turning on fan and setting speed.""" diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 1291aa53123..8c870c6ad73 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -242,7 +242,9 @@ async def test_discovery_already_configured_ip(hass, mock_client): 'host': '192.168.43.183', 'port': 6053, 'hostname': 'test8266.local.', - 'properties': {} + 'properties': { + "address": "192.168.43.183" + } } result = await flow.async_step_discovery(user_input=service_info) assert result['type'] == 'abort' diff --git a/tests/components/feedreader/__init__.py b/tests/components/feedreader/__init__.py new file mode 100644 index 00000000000..3667f7c75ea --- /dev/null +++ b/tests/components/feedreader/__init__.py @@ -0,0 +1 @@ +"""Tests for the feedreader component.""" diff --git a/tests/components/test_feedreader.py b/tests/components/feedreader/test_init.py similarity index 100% rename from tests/components/test_feedreader.py rename to tests/components/feedreader/test_init.py diff --git a/tests/components/ffmpeg/__init__.py b/tests/components/ffmpeg/__init__.py new file mode 100644 index 00000000000..c69f1dac44a --- /dev/null +++ b/tests/components/ffmpeg/__init__.py @@ -0,0 +1 @@ +"""Tests for the ffmpeg component.""" diff --git a/tests/components/test_ffmpeg.py b/tests/components/ffmpeg/test_init.py similarity index 100% rename from tests/components/test_ffmpeg.py rename to tests/components/ffmpeg/test_init.py diff --git a/tests/components/folder_watcher/__init__.py b/tests/components/folder_watcher/__init__.py new file mode 100644 index 00000000000..8508216226f --- /dev/null +++ b/tests/components/folder_watcher/__init__.py @@ -0,0 +1 @@ +"""Tests for the folder_watcher component.""" diff --git a/tests/components/test_folder_watcher.py b/tests/components/folder_watcher/test_init.py similarity index 100% rename from tests/components/test_folder_watcher.py rename to tests/components/folder_watcher/test_init.py diff --git a/tests/components/freedns/__init__.py b/tests/components/freedns/__init__.py new file mode 100644 index 00000000000..ab0f8df6411 --- /dev/null +++ b/tests/components/freedns/__init__.py @@ -0,0 +1 @@ +"""Tests for the freedns component.""" diff --git a/tests/components/test_freedns.py b/tests/components/freedns/test_init.py similarity index 92% rename from tests/components/test_freedns.py rename to tests/components/freedns/test_init.py index b8e38e9c3a8..1996b02d8d0 100644 --- a/tests/components/test_freedns.py +++ b/tests/components/freedns/test_init.py @@ -24,7 +24,7 @@ def setup_freedns(hass, aioclient_mock): hass.loop.run_until_complete(async_setup_component(hass, freedns.DOMAIN, { freedns.DOMAIN: { 'access_token': ACCESS_TOKEN, - 'update_interval': UPDATE_INTERVAL, + 'scan_interval': UPDATE_INTERVAL, } })) @@ -40,7 +40,7 @@ def test_setup(hass, aioclient_mock): result = yield from async_setup_component(hass, freedns.DOMAIN, { freedns.DOMAIN: { 'access_token': ACCESS_TOKEN, - 'update_interval': UPDATE_INTERVAL, + 'scan_interval': UPDATE_INTERVAL, } }) assert result @@ -62,7 +62,7 @@ def test_setup_fails_if_wrong_token(hass, aioclient_mock): result = yield from async_setup_component(hass, freedns.DOMAIN, { freedns.DOMAIN: { 'access_token': ACCESS_TOKEN, - 'update_interval': UPDATE_INTERVAL, + 'scan_interval': UPDATE_INTERVAL, } }) assert not result diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py new file mode 100644 index 00000000000..24bb7c3a181 --- /dev/null +++ b/tests/components/fritzbox/__init__.py @@ -0,0 +1 @@ +"""Tests for the FritzBox! integration.""" diff --git a/tests/components/climate/test_fritzbox.py b/tests/components/fritzbox/test_climate.py similarity index 99% rename from tests/components/climate/test_fritzbox.py rename to tests/components/fritzbox/test_climate.py index 1cd15e3655f..95361170a2c 100644 --- a/tests/components/climate/test_fritzbox.py +++ b/tests/components/fritzbox/test_climate.py @@ -4,7 +4,7 @@ from unittest.mock import Mock, patch import requests -from homeassistant.components.climate.fritzbox import FritzboxThermostat +from homeassistant.components.fritzbox.climate import FritzboxThermostat class TestFritzboxClimate(unittest.TestCase): diff --git a/tests/components/frontend/test_storage.py b/tests/components/frontend/test_storage.py new file mode 100644 index 00000000000..97b132cfd13 --- /dev/null +++ b/tests/components/frontend/test_storage.py @@ -0,0 +1,186 @@ +"""The tests for frontend storage.""" +import pytest + +from homeassistant.setup import async_setup_component +from homeassistant.components.frontend import storage + + +@pytest.fixture(autouse=True) +def setup_frontend(hass): + """Fixture to setup the frontend.""" + hass.loop.run_until_complete(async_setup_component(hass, 'frontend', {})) + + +async def test_get_user_data_empty(hass, hass_ws_client, hass_storage): + """Test get_user_data command.""" + client = await hass_ws_client(hass) + + await client.send_json({ + 'id': 5, + 'type': 'frontend/get_user_data', + 'key': 'non-existing-key', + }) + + res = await client.receive_json() + assert res['success'], res + assert res['result']['value'] is None + + +async def test_get_user_data(hass, hass_ws_client, hass_admin_user, + hass_storage): + """Test get_user_data command.""" + storage_key = storage.STORAGE_KEY_USER_DATA.format(hass_admin_user.id) + hass_storage[storage_key] = { + 'key': storage_key, + 'version': 1, + 'data': { + 'test-key': 'test-value', + 'test-complex': [{'foo': 'bar'}] + } + } + + client = await hass_ws_client(hass) + + # Get a simple string key + + await client.send_json({ + 'id': 6, + 'type': 'frontend/get_user_data', + 'key': 'test-key', + }) + + res = await client.receive_json() + assert res['success'], res + assert res['result']['value'] == 'test-value' + + # Get a more complex key + + await client.send_json({ + 'id': 7, + 'type': 'frontend/get_user_data', + 'key': 'test-complex', + }) + + res = await client.receive_json() + assert res['success'], res + assert res['result']['value'][0]['foo'] == 'bar' + + # Get all data (no key) + + await client.send_json({ + 'id': 8, + 'type': 'frontend/get_user_data', + }) + + res = await client.receive_json() + assert res['success'], res + assert res['result']['value']['test-key'] == 'test-value' + assert res['result']['value']['test-complex'][0]['foo'] == 'bar' + + +async def test_set_user_data_empty(hass, hass_ws_client, hass_storage): + """Test set_user_data command.""" + client = await hass_ws_client(hass) + + # test creating + + await client.send_json({ + 'id': 6, + 'type': 'frontend/get_user_data', + 'key': 'test-key', + }) + + res = await client.receive_json() + assert res['success'], res + assert res['result']['value'] is None + + await client.send_json({ + 'id': 7, + 'type': 'frontend/set_user_data', + 'key': 'test-key', + 'value': 'test-value' + }) + + res = await client.receive_json() + assert res['success'], res + + await client.send_json({ + 'id': 8, + 'type': 'frontend/get_user_data', + 'key': 'test-key', + }) + + res = await client.receive_json() + assert res['success'], res + assert res['result']['value'] == 'test-value' + + +async def test_set_user_data(hass, hass_ws_client, hass_storage, + hass_admin_user): + """Test set_user_data command with initial data.""" + storage_key = storage.STORAGE_KEY_USER_DATA.format(hass_admin_user.id) + hass_storage[storage_key] = { + 'version': 1, + 'data': { + 'test-key': 'test-value', + 'test-complex': 'string', + } + } + + client = await hass_ws_client(hass) + + # test creating + + await client.send_json({ + 'id': 5, + 'type': 'frontend/set_user_data', + 'key': 'test-non-existent-key', + 'value': 'test-value-new' + }) + + res = await client.receive_json() + assert res['success'], res + + await client.send_json({ + 'id': 6, + 'type': 'frontend/get_user_data', + 'key': 'test-non-existent-key', + }) + + res = await client.receive_json() + assert res['success'], res + assert res['result']['value'] == 'test-value-new' + + # test updating with complex data + + await client.send_json({ + 'id': 7, + 'type': 'frontend/set_user_data', + 'key': 'test-complex', + 'value': [{'foo': 'bar'}] + }) + + res = await client.receive_json() + assert res['success'], res + + await client.send_json({ + 'id': 8, + 'type': 'frontend/get_user_data', + 'key': 'test-complex', + }) + + res = await client.receive_json() + assert res['success'], res + assert res['result']['value'][0]['foo'] == 'bar' + + # ensure other existing key was not modified + + await client.send_json({ + 'id': 9, + 'type': 'frontend/get_user_data', + 'key': 'test-key', + }) + + res = await client.receive_json() + assert res['success'], res + assert res['result']['value'] == 'test-value' diff --git a/tests/components/google/__init__.py b/tests/components/google/__init__.py new file mode 100644 index 00000000000..d5524765d07 --- /dev/null +++ b/tests/components/google/__init__.py @@ -0,0 +1 @@ +"""Tests for the Google integration.""" diff --git a/tests/components/calendar/test_google.py b/tests/components/google/test_calendar.py similarity index 97% rename from tests/components/calendar/test_google.py rename to tests/components/google/test_calendar.py index ec4089677d8..6329c2c1d14 100644 --- a/tests/components/calendar/test_google.py +++ b/tests/components/google/test_calendar.py @@ -7,7 +7,7 @@ from unittest.mock import patch, Mock import pytest import homeassistant.components.calendar as calendar_base -import homeassistant.components.calendar.google as calendar +from homeassistant.components.google import calendar import homeassistant.util.dt as dt_util from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.helpers.template import DATE_STR_FORMAT @@ -40,7 +40,7 @@ class TestComponentsGoogleCalendar(unittest.TestCase): self.hass.stop() - @patch('homeassistant.components.calendar.google.GoogleCalendarData') + @patch('homeassistant.components.google.calendar.GoogleCalendarData') def test_all_day_event(self, mock_next_event): """Test that we can create an event trigger on device.""" week_from_today = dt_util.dt.date.today() \ @@ -103,7 +103,7 @@ class TestComponentsGoogleCalendar(unittest.TestCase): 'description': event['description'], } - @patch('homeassistant.components.calendar.google.GoogleCalendarData') + @patch('homeassistant.components.google.calendar.GoogleCalendarData') def test_future_event(self, mock_next_event): """Test that we can create an event trigger on device.""" one_hour_from_now = dt_util.now() \ @@ -164,7 +164,7 @@ class TestComponentsGoogleCalendar(unittest.TestCase): 'description': '', } - @patch('homeassistant.components.calendar.google.GoogleCalendarData') + @patch('homeassistant.components.google.calendar.GoogleCalendarData') def test_in_progress_event(self, mock_next_event): """Test that we can create an event trigger on device.""" middle_of_event = dt_util.now() \ @@ -226,7 +226,7 @@ class TestComponentsGoogleCalendar(unittest.TestCase): 'description': '', } - @patch('homeassistant.components.calendar.google.GoogleCalendarData') + @patch('homeassistant.components.google.calendar.GoogleCalendarData') def test_offset_in_progress_event(self, mock_next_event): """Test that we can create an event trigger on device.""" middle_of_event = dt_util.now() \ @@ -290,7 +290,7 @@ class TestComponentsGoogleCalendar(unittest.TestCase): } @pytest.mark.skip - @patch('homeassistant.components.calendar.google.GoogleCalendarData') + @patch('homeassistant.components.google.calendar.GoogleCalendarData') def test_all_day_offset_in_progress_event(self, mock_next_event): """Test that we can create an event trigger on device.""" tomorrow = dt_util.dt.date.today() \ @@ -356,7 +356,7 @@ class TestComponentsGoogleCalendar(unittest.TestCase): 'description': event['description'], } - @patch('homeassistant.components.calendar.google.GoogleCalendarData') + @patch('homeassistant.components.google.calendar.GoogleCalendarData') def test_all_day_offset_event(self, mock_next_event): """Test that we can create an event trigger on device.""" tomorrow = dt_util.dt.date.today() \ diff --git a/tests/components/test_google.py b/tests/components/google/test_init.py similarity index 100% rename from tests/components/test_google.py rename to tests/components/google/test_init.py diff --git a/tests/components/tts/test_google.py b/tests/components/google/test_tts.py similarity index 98% rename from tests/components/tts/test_google.py rename to tests/components/google/test_tts.py index f328e3e9f16..78bdd50b6d7 100644 --- a/tests/components/tts/test_google.py +++ b/tests/components/google/test_tts.py @@ -5,14 +5,14 @@ import shutil from unittest.mock import patch import homeassistant.components.tts as tts -from homeassistant.components.media_player import ( +from homeassistant.components.media_player.const import ( SERVICE_PLAY_MEDIA, ATTR_MEDIA_CONTENT_ID, DOMAIN as DOMAIN_MP) from homeassistant.setup import setup_component from tests.common import ( get_test_home_assistant, assert_setup_component, mock_service) -from .test_init import mutagen_mock # noqa +from tests.components.tts.test_init import mutagen_mock # noqa class TestTTSGooglePlatform: diff --git a/tests/components/google_domains/__init__.py b/tests/components/google_domains/__init__.py new file mode 100644 index 00000000000..3466a3be489 --- /dev/null +++ b/tests/components/google_domains/__init__.py @@ -0,0 +1 @@ +"""Tests for the google_domains component.""" diff --git a/tests/components/test_google_domains.py b/tests/components/google_domains/test_init.py similarity index 100% rename from tests/components/test_google_domains.py rename to tests/components/google_domains/test_init.py diff --git a/tests/components/google_pubsub/test_pubsub.py b/tests/components/google_pubsub/test_pubsub.py new file mode 100644 index 00000000000..b97dc33f8b1 --- /dev/null +++ b/tests/components/google_pubsub/test_pubsub.py @@ -0,0 +1,22 @@ +"""The tests for the Google Pub/Sub component.""" +from datetime import datetime + +from homeassistant.components.google_pubsub import ( + DateTimeJSONEncoder as victim) + + +class TestDateTimeJSONEncoder(object): + """Bundle for DateTimeJSONEncoder tests.""" + + def test_datetime(self): + """Test datetime encoding.""" + time = datetime(2019, 1, 13, 12, 30, 5) + assert victim().encode(time) == '"2019-01-13T12:30:05"' + + def test_no_datetime(self): + """Test integer encoding.""" + assert victim().encode(42) == '42' + + def test_nested(self): + """Test dictionary encoding.""" + assert victim().encode({'foo': 'bar'}) == '{"foo": "bar"}' diff --git a/tests/components/graphite/__init__.py b/tests/components/graphite/__init__.py new file mode 100644 index 00000000000..e62487ad79e --- /dev/null +++ b/tests/components/graphite/__init__.py @@ -0,0 +1 @@ +"""Tests for the graphite component.""" diff --git a/tests/components/test_graphite.py b/tests/components/graphite/test_init.py similarity index 100% rename from tests/components/test_graphite.py rename to tests/components/graphite/test_init.py diff --git a/tests/components/group/test_reproduce_state.py b/tests/components/group/test_reproduce_state.py new file mode 100644 index 00000000000..43bc2a03fe9 --- /dev/null +++ b/tests/components/group/test_reproduce_state.py @@ -0,0 +1,45 @@ +"""The tests for reproduction of state.""" + +from asyncio import Future +from unittest.mock import patch +from homeassistant.components.group import async_reproduce_states +from homeassistant.core import Context, State + + +async def test_reproduce_group(hass): + """Test reproduce_state with group.""" + context = Context() + + def clone_state(state, entity_id): + """Return a cloned state with different entity_id.""" + return State(entity_id, + state.state, + state.attributes, + last_changed=state.last_changed, + last_updated=state.last_updated, + context=state.context) + + with patch('homeassistant.helpers.state.async_reproduce_state') as fun: + fun.return_value = Future() + fun.return_value.set_result(None) + + hass.states.async_set('group.test', 'off', { + 'entity_id': ['light.test1', 'light.test2', 'switch.test1']}) + hass.states.async_set('light.test1', 'off') + hass.states.async_set('light.test2', 'off') + hass.states.async_set('switch.test1', 'off') + + state = State('group.test', 'on') + + await async_reproduce_states( + hass, + [state], + context) + + fun.assert_called_once_with( + hass, + [clone_state(state, 'light.test1'), + clone_state(state, 'light.test2'), + clone_state(state, 'switch.test1')], + blocking=True, + context=context) diff --git a/tests/components/history/__init__.py b/tests/components/history/__init__.py new file mode 100644 index 00000000000..662e70a7bff --- /dev/null +++ b/tests/components/history/__init__.py @@ -0,0 +1 @@ +"""Tests for the history component.""" diff --git a/tests/components/test_history.py b/tests/components/history/test_init.py similarity index 100% rename from tests/components/test_history.py rename to tests/components/history/test_init.py diff --git a/tests/components/history_graph/__init__.py b/tests/components/history_graph/__init__.py new file mode 100644 index 00000000000..2cb34499938 --- /dev/null +++ b/tests/components/history_graph/__init__.py @@ -0,0 +1 @@ +"""Tests for the history_graph component.""" diff --git a/tests/components/test_history_graph.py b/tests/components/history_graph/test_init.py similarity index 100% rename from tests/components/test_history_graph.py rename to tests/components/history_graph/test_init.py diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 15ab6d7413e..6f3957827eb 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -100,7 +100,7 @@ async def test_home_accessory(hass, hk_driver): assert serv.get_characteristic(CHAR_MODEL).value == 'Test Model' -async def test_battery_service(hass, hk_driver): +async def test_battery_service(hass, hk_driver, caplog): """Test battery service.""" entity_id = 'homekit.accessory' hass.states.async_set(entity_id, None, {ATTR_BATTERY_LEVEL: 50}) @@ -124,6 +124,13 @@ async def test_battery_service(hass, hk_driver): assert acc._char_low_battery.value == 1 assert acc._char_charging.value == 2 + hass.states.async_set(entity_id, None, {ATTR_BATTERY_LEVEL: 'error'}) + await hass.async_block_till_done() + assert acc._char_battery.value == 15 + assert acc._char_low_battery.value == 1 + assert acc._char_charging.value == 2 + assert 'ERROR' not in caplog.text + # Test charging hass.states.async_set(entity_id, None, { ATTR_BATTERY_LEVEL: 10, ATTR_BATTERY_CHARGING: True}) diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index d39609b079a..e395402b958 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -6,7 +6,7 @@ import pytest from homeassistant.core import State import homeassistant.components.cover as cover import homeassistant.components.climate as climate -import homeassistant.components.media_player as media_player +import homeassistant.components.media_player.const as media_player_c from homeassistant.components.homekit import get_accessory, TYPES from homeassistant.components.homekit.const import ( CONF_FEATURE_LIST, FEATURE_ON_OFF, TYPE_FAUCET, TYPE_OUTLET, TYPE_SHOWER, @@ -60,9 +60,9 @@ def test_customize_options(config, name): ('Light', 'light.test', 'on', {}, {}), ('Lock', 'lock.test', 'locked', {}, {ATTR_CODE: '1234'}), ('MediaPlayer', 'media_player.test', 'on', - {ATTR_SUPPORTED_FEATURES: media_player.SUPPORT_TURN_ON | - media_player.SUPPORT_TURN_OFF}, {CONF_FEATURE_LIST: - {FEATURE_ON_OFF: None}}), + {ATTR_SUPPORTED_FEATURES: media_player_c.SUPPORT_TURN_ON | + media_player_c.SUPPORT_TURN_OFF}, {CONF_FEATURE_LIST: + {FEATURE_ON_OFF: None}}), ('SecuritySystem', 'alarm_control_panel.test', 'armed_away', {}, {ATTR_CODE: '1234'}), ('Thermostat', 'climate.test', 'auto', {}, {}), diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index 27b6cec0790..b620ef50e0f 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -1,14 +1,17 @@ """Test different accessory types: Fans.""" from collections import namedtuple +from unittest.mock import Mock import pytest from homeassistant.components.fan import ( - ATTR_DIRECTION, ATTR_OSCILLATING, DIRECTION_FORWARD, DIRECTION_REVERSE, - DOMAIN, SUPPORT_DIRECTION, SUPPORT_OSCILLATE) + ATTR_DIRECTION, ATTR_OSCILLATING, ATTR_SPEED, ATTR_SPEED_LIST, + DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN, SPEED_HIGH, SPEED_LOW, + SPEED_OFF, SUPPORT_DIRECTION, SUPPORT_OSCILLATE, SUPPORT_SET_SPEED) from homeassistant.components.homekit.const import ATTR_VALUE +from homeassistant.components.homekit.util import HomeKitSpeedMapping from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF, + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON, STATE_UNKNOWN) from tests.common import async_mock_service @@ -39,6 +42,9 @@ async def test_fan_basic(hass, hk_driver, cls, events): assert acc.category == 3 # Fan assert acc.char_active.value == 0 + # If there are no speed_list values, then HomeKit speed is unsupported + assert acc.char_speed is None + await hass.async_add_job(acc.run) await hass.async_block_till_done() assert acc.char_active.value == 1 @@ -155,3 +161,40 @@ async def test_fan_oscillate(hass, hk_driver, cls, events): assert call_oscillate[1].data[ATTR_OSCILLATING] is True assert len(events) == 2 assert events[-1].data[ATTR_VALUE] is True + + +async def test_fan_speed(hass, hk_driver, cls, events): + """Test fan with speed.""" + entity_id = 'fan.demo' + speed_list = [SPEED_OFF, SPEED_LOW, SPEED_HIGH] + + hass.states.async_set(entity_id, STATE_ON, { + ATTR_SUPPORTED_FEATURES: SUPPORT_SET_SPEED, ATTR_SPEED: SPEED_OFF, + ATTR_SPEED_LIST: speed_list}) + await hass.async_block_till_done() + acc = cls.fan(hass, hk_driver, 'Fan', entity_id, 2, None) + assert acc.char_speed.value == 0 + + await hass.async_add_job(acc.run) + assert acc.speed_mapping.speed_ranges == \ + HomeKitSpeedMapping(speed_list).speed_ranges + + acc.speed_mapping.speed_to_homekit = Mock(return_value=42) + acc.speed_mapping.speed_to_states = Mock(return_value='ludicrous') + + hass.states.async_set(entity_id, STATE_ON, {ATTR_SPEED: SPEED_HIGH}) + await hass.async_block_till_done() + acc.speed_mapping.speed_to_homekit.assert_called_with(SPEED_HIGH) + assert acc.char_speed.value == 42 + + # Set from HomeKit + call_set_speed = async_mock_service(hass, DOMAIN, 'set_speed') + + await hass.async_add_job(acc.char_speed.client_update_value, 42) + await hass.async_block_till_done() + acc.speed_mapping.speed_to_states.assert_called_with(42) + assert call_set_speed[0] + assert call_set_speed[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_speed[0].data[ATTR_SPEED] == 'ludicrous' + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] == 'ludicrous' diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py index 6b23b3cc58e..065d1845fdb 100644 --- a/tests/components/homekit/test_type_media_players.py +++ b/tests/components/homekit/test_type_media_players.py @@ -4,7 +4,7 @@ from homeassistant.components.homekit.const import ( 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 ( +from homeassistant.components.media_player.const import ( ATTR_MEDIA_VOLUME_MUTED, DOMAIN) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_IDLE, STATE_OFF, STATE_ON, diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index a2849a77396..c86b1353c48 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -3,15 +3,14 @@ import pytest import voluptuous as vol from homeassistant.components.homekit.const import ( - CONF_FEATURE, CONF_FEATURE_LIST, HOMEKIT_NOTIFY_ID, FEATURE_ON_OFF, - FEATURE_PLAY_PAUSE, TYPE_FAUCET, TYPE_OUTLET, TYPE_SHOWER, TYPE_SPRINKLER, + CONF_FEATURE, CONF_FEATURE_LIST, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, + HOMEKIT_NOTIFY_ID, TYPE_FAUCET, TYPE_OUTLET, TYPE_SHOWER, TYPE_SPRINKLER, TYPE_SWITCH, TYPE_VALVE) from homeassistant.components.homekit.util import ( - convert_to_float, density_to_air_quality, dismiss_setup_message, - show_setup_message, temperature_to_homekit, temperature_to_states, + HomeKitSpeedMapping, SpeedRange, convert_to_float, density_to_air_quality, + dismiss_setup_message, show_setup_message, temperature_to_homekit, + temperature_to_states, validate_entity_config as vec, validate_media_player_features) -from homeassistant.components.homekit.util import validate_entity_config \ - as vec from homeassistant.components.persistent_notification import ( ATTR_MESSAGE, ATTR_NOTIFICATION_ID, DOMAIN) from homeassistant.const import ( @@ -144,3 +143,58 @@ async def test_dismiss_setup_msg(hass): assert call_dismiss_notification assert call_dismiss_notification[0].data[ATTR_NOTIFICATION_ID] == \ HOMEKIT_NOTIFY_ID + + +def test_homekit_speed_mapping(): + """Test if the SpeedRanges from a speed_list are as expected.""" + # A standard 2-speed fan + speed_mapping = HomeKitSpeedMapping(['off', 'low', 'high']) + assert speed_mapping.speed_ranges == { + 'off': SpeedRange(0, 0), + 'low': SpeedRange(100 / 3, 50), + 'high': SpeedRange(200 / 3, 100), + } + + # A standard 3-speed fan + speed_mapping = HomeKitSpeedMapping(['off', 'low', 'medium', 'high']) + assert speed_mapping.speed_ranges == { + 'off': SpeedRange(0, 0), + 'low': SpeedRange(100 / 4, 100 / 3), + 'medium': SpeedRange(200 / 4, 200 / 3), + 'high': SpeedRange(300 / 4, 100), + } + + # a Dyson-like fan with 10 speeds + speed_mapping = HomeKitSpeedMapping([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + assert speed_mapping.speed_ranges == { + 0: SpeedRange(0, 0), + 1: SpeedRange(10, 100 / 9), + 2: SpeedRange(20, 200 / 9), + 3: SpeedRange(30, 300 / 9), + 4: SpeedRange(40, 400 / 9), + 5: SpeedRange(50, 500 / 9), + 6: SpeedRange(60, 600 / 9), + 7: SpeedRange(70, 700 / 9), + 8: SpeedRange(80, 800 / 9), + 9: SpeedRange(90, 100), + } + + +def test_speed_to_homekit(): + """Test speed conversion from HA to Homekit.""" + speed_mapping = HomeKitSpeedMapping(['off', 'low', 'high']) + assert speed_mapping.speed_to_homekit('off') == 0 + assert speed_mapping.speed_to_homekit('low') == 50 + assert speed_mapping.speed_to_homekit('high') == 100 + + +def test_speed_to_states(): + """Test speed conversion from Homekit to HA.""" + speed_mapping = HomeKitSpeedMapping(['off', 'low', 'high']) + assert speed_mapping.speed_to_states(0) == 'off' + assert speed_mapping.speed_to_states(33) == 'off' + assert speed_mapping.speed_to_states(34) == 'low' + assert speed_mapping.speed_to_states(50) == 'low' + assert speed_mapping.speed_to_states(66) == 'low' + assert speed_mapping.speed_to_states(67) == 'high' + assert speed_mapping.speed_to_states(100) == 'high' diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index 521920b9281..c39e7d4e26b 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -1,6 +1,9 @@ """Test HomematicIP Cloud accesspoint.""" from unittest.mock import Mock, patch +import pytest + +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.components.homematicip_cloud import hap as hmipc from homeassistant.components.homematicip_cloud import const, errors from tests.common import mock_coro, mock_coro_func @@ -82,9 +85,10 @@ async def test_hap_setup_connection_error(): hmipc.HMIPC_NAME: 'hmip', } hap = hmipc.HomematicipHAP(hass, entry) - with patch.object(hap, 'get_hap', - side_effect=errors.HmipcConnectionError): - assert await hap.async_setup() is False + with patch.object( + hap, 'get_hap', side_effect=errors.HmipcConnectionError + ), pytest.raises(ConfigEntryNotReady): + await hap.async_setup() assert len(hass.async_add_job.mock_calls) == 0 assert len(hass.config_entries.flow.async_init.mock_calls) == 0 diff --git a/tests/components/huawei_lte/__init__.py b/tests/components/huawei_lte/__init__.py new file mode 100644 index 00000000000..79602ecfb44 --- /dev/null +++ b/tests/components/huawei_lte/__init__.py @@ -0,0 +1 @@ +"""Tests for the huawei_lte component.""" diff --git a/tests/components/test_huawei_lte.py b/tests/components/huawei_lte/test_init.py similarity index 100% rename from tests/components/test_huawei_lte.py rename to tests/components/huawei_lte/test_init.py diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index ceb30091970..855a12e2620 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -1,6 +1,9 @@ """Test Hue bridge.""" from unittest.mock import Mock, patch +import pytest + +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.components.hue import bridge, errors from tests.common import mock_coro @@ -48,32 +51,10 @@ async def test_bridge_setup_timeout(hass): entry.data = {'host': '1.2.3.4', 'username': 'mock-username'} hue_bridge = bridge.HueBridge(hass, entry, False, False) - with patch.object(bridge, 'get_bridge', side_effect=errors.CannotConnect): - assert await hue_bridge.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_reset_cancels_retry_setup(): - """Test resetting a bridge while we're waiting to retry setup.""" - hass = Mock() - entry = Mock() - entry.data = {'host': '1.2.3.4', 'username': 'mock-username'} - hue_bridge = bridge.HueBridge(hass, entry, False, False) - - with patch.object(bridge, 'get_bridge', side_effect=errors.CannotConnect): - assert await hue_bridge.async_setup() is False - - mock_call_later = hass.helpers.event.async_call_later - - assert len(mock_call_later.mock_calls) == 1 - - assert await hue_bridge.async_reset() - - assert len(mock_call_later.mock_calls) == 2 - assert len(mock_call_later.return_value.mock_calls) == 1 + with patch.object( + bridge, 'get_bridge', side_effect=errors.CannotConnect + ), pytest.raises(ConfigEntryNotReady): + await hue_bridge.async_setup() async def test_reset_if_entry_had_wrong_auth(): diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py index 1fcc092dd30..6c89995a1a1 100644 --- a/tests/components/hue/test_init.py +++ b/tests/components/hue/test_init.py @@ -39,7 +39,7 @@ async def test_setup_defined_hosts_known_auth(hass): assert len(mock_config_entries.flow.mock_calls) == 0 # Config stored for domain. - assert hass.data[hue.DOMAIN] == { + assert hass.data[hue.DATA_CONFIGS] == { '0.0.0.0': { hue.CONF_HOST: '0.0.0.0', hue.CONF_FILENAME: 'bla.conf', @@ -73,7 +73,7 @@ async def test_setup_defined_hosts_no_known_auth(hass): } # Config stored for domain. - assert hass.data[hue.DOMAIN] == { + assert hass.data[hue.DATA_CONFIGS] == { '0.0.0.0': { hue.CONF_HOST: '0.0.0.0', hue.CONF_FILENAME: 'bla.conf', diff --git a/tests/components/influxdb/__init__.py b/tests/components/influxdb/__init__.py new file mode 100644 index 00000000000..7a215bea197 --- /dev/null +++ b/tests/components/influxdb/__init__.py @@ -0,0 +1 @@ +"""Tests for the influxdb component.""" diff --git a/tests/components/test_influxdb.py b/tests/components/influxdb/test_init.py similarity index 100% rename from tests/components/test_influxdb.py rename to tests/components/influxdb/test_init.py diff --git a/tests/components/init/__init__.py b/tests/components/init/__init__.py new file mode 100644 index 00000000000..b935cf060c8 --- /dev/null +++ b/tests/components/init/__init__.py @@ -0,0 +1 @@ +"""Tests for the init component.""" diff --git a/tests/components/test_init.py b/tests/components/init/test_init.py similarity index 100% rename from tests/components/test_init.py rename to tests/components/init/test_init.py diff --git a/tests/components/input_boolean/__init__.py b/tests/components/input_boolean/__init__.py new file mode 100644 index 00000000000..164d6a0ba5e --- /dev/null +++ b/tests/components/input_boolean/__init__.py @@ -0,0 +1 @@ +"""Tests for the input_boolean component.""" diff --git a/tests/components/test_input_boolean.py b/tests/components/input_boolean/test_init.py similarity index 100% rename from tests/components/test_input_boolean.py rename to tests/components/input_boolean/test_init.py diff --git a/tests/components/input_datetime/__init__.py b/tests/components/input_datetime/__init__.py new file mode 100644 index 00000000000..b408528a4ff --- /dev/null +++ b/tests/components/input_datetime/__init__.py @@ -0,0 +1 @@ +"""Tests for the input_datetime component.""" diff --git a/tests/components/test_input_datetime.py b/tests/components/input_datetime/test_init.py similarity index 100% rename from tests/components/test_input_datetime.py rename to tests/components/input_datetime/test_init.py diff --git a/tests/components/input_number/__init__.py b/tests/components/input_number/__init__.py new file mode 100644 index 00000000000..e40cd77455c --- /dev/null +++ b/tests/components/input_number/__init__.py @@ -0,0 +1 @@ +"""Tests for the input_number component.""" diff --git a/tests/components/test_input_number.py b/tests/components/input_number/test_init.py similarity index 100% rename from tests/components/test_input_number.py rename to tests/components/input_number/test_init.py diff --git a/tests/components/input_select/__init__.py b/tests/components/input_select/__init__.py new file mode 100644 index 00000000000..2d817e8a59c --- /dev/null +++ b/tests/components/input_select/__init__.py @@ -0,0 +1 @@ +"""Tests for the input_select component.""" diff --git a/tests/components/test_input_select.py b/tests/components/input_select/test_init.py similarity index 100% rename from tests/components/test_input_select.py rename to tests/components/input_select/test_init.py diff --git a/tests/components/input_text/__init__.py b/tests/components/input_text/__init__.py new file mode 100644 index 00000000000..b035eed0c9e --- /dev/null +++ b/tests/components/input_text/__init__.py @@ -0,0 +1 @@ +"""Tests for the input_text component.""" diff --git a/tests/components/test_input_text.py b/tests/components/input_text/test_init.py similarity index 100% rename from tests/components/test_input_text.py rename to tests/components/input_text/test_init.py diff --git a/tests/components/intent_script/__init__.py b/tests/components/intent_script/__init__.py new file mode 100644 index 00000000000..5328df0b0b1 --- /dev/null +++ b/tests/components/intent_script/__init__.py @@ -0,0 +1 @@ +"""Tests for the intent_script component.""" diff --git a/tests/components/test_intent_script.py b/tests/components/intent_script/test_init.py similarity index 100% rename from tests/components/test_intent_script.py rename to tests/components/intent_script/test_init.py diff --git a/tests/components/introduction/__init__.py b/tests/components/introduction/__init__.py new file mode 100644 index 00000000000..99cea29581c --- /dev/null +++ b/tests/components/introduction/__init__.py @@ -0,0 +1 @@ +"""Tests for the introduction component.""" diff --git a/tests/components/test_introduction.py b/tests/components/introduction/test_init.py similarity index 100% rename from tests/components/test_introduction.py rename to tests/components/introduction/test_init.py diff --git a/tests/components/ios/test_init.py b/tests/components/ios/test_init.py index ad1ab328325..7141c8c9d3a 100644 --- a/tests/components/ios/test_init.py +++ b/tests/components/ios/test_init.py @@ -26,7 +26,7 @@ def mock_dependencies(hass): async def test_creating_entry_sets_up_sensor(hass): """Test setting up iOS loads the sensor component.""" - with patch('homeassistant.components.sensor.ios.async_setup_entry', + with patch('homeassistant.components.ios.sensor.async_setup_entry', return_value=mock_coro(True)) as mock_setup: result = await hass.config_entries.flow.async_init( ios.DOMAIN, context={'source': config_entries.SOURCE_USER}) diff --git a/tests/components/ipma/__init__.py b/tests/components/ipma/__init__.py new file mode 100644 index 00000000000..35099c405bb --- /dev/null +++ b/tests/components/ipma/__init__.py @@ -0,0 +1 @@ +"""Tests for the IPMA component.""" diff --git a/tests/components/ipma/test_config_flow.py b/tests/components/ipma/test_config_flow.py new file mode 100644 index 00000000000..4a72318128e --- /dev/null +++ b/tests/components/ipma/test_config_flow.py @@ -0,0 +1,118 @@ +"""Tests for IPMA config flow.""" +from unittest.mock import Mock, patch + +from tests.common import mock_coro + +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.components.ipma import config_flow + + +async def test_show_config_form(): + """Test show configuration form.""" + hass = Mock() + flow = config_flow.IpmaFlowHandler() + 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(): + """Test show configuration form.""" + hass = Mock() + flow = config_flow.IpmaFlowHandler() + flow.hass = hass + + result = await flow._show_config_form( + name="test", latitude='0', longitude='0') + + assert result['type'] == 'form' + assert result['step_id'] == 'user' + + +async def test_flow_with_home_location(hass): + """Test config flow . + + Tests the flow when a default location is configured + then it should return a form with default values + """ + flow = config_flow.IpmaFlowHandler() + flow.hass = hass + + hass.config.location_name = 'Home' + hass.config.latitude = 1 + hass.config.longitude = 1 + + result = await flow.async_step_user() + assert result['type'] == 'form' + assert result['step_id'] == 'user' + + +async def test_flow_show_form(): + """Test show form scenarios first time. + + Test when the form should show when no configurations exists + """ + hass = Mock() + flow = config_flow.IpmaFlowHandler() + flow.hass = hass + + with \ + patch.object(flow, '_show_config_form', + return_value=mock_coro()) as config_form: + await flow.async_step_user() + assert len(config_form.mock_calls) == 1 + + +async def test_flow_entry_created_from_user_input(): + """Test that create data from user input. + + Test when the form should show when no configurations exists + """ + hass = Mock() + flow = config_flow.IpmaFlowHandler() + 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.hass.config_entries, 'async_entries', + return_value=mock_coro()) as config_entries: + + result = await flow.async_step_user(user_input=test_data) + + assert result['type'] == 'create_entry' + assert result['data'] == test_data + assert len(config_entries.mock_calls) == 1 + assert not config_form.mock_calls + + +async def test_flow_entry_config_entry_already_exists(): + """Test that create data from user input and config_entry already exists. + + Test when the form should show when user puts existing name + in the config gui. Then the form should show with error + """ + hass = Mock() + flow = config_flow.IpmaFlowHandler() + 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.hass.config_entries, 'async_entries', + return_value={'home': test_data}) as config_entries: + + await flow.async_step_user(user_input=test_data) + + assert len(config_form.mock_calls) == 1 + assert len(config_entries.mock_calls) == 1 + assert len(flow._errors) == 1 diff --git a/tests/components/ipma/test_weather.py b/tests/components/ipma/test_weather.py new file mode 100644 index 00000000000..c141fbae7f1 --- /dev/null +++ b/tests/components/ipma/test_weather.py @@ -0,0 +1,103 @@ +"""The tests for the IPMA weather component.""" +from unittest.mock import patch +from collections import namedtuple + +from homeassistant.components import weather +from homeassistant.components.weather import ( + ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, ATTR_WEATHER_TEMPERATURE, + ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED, + DOMAIN as WEATHER_DOMAIN) + +from tests.common import MockConfigEntry, mock_coro +from homeassistant.setup import async_setup_component + +TEST_CONFIG = { + "name": "HomeTown", + "latitude": "40.00", + "longitude": "-8.00", +} + + +class MockStation(): + """Mock Station from pyipma.""" + + async def observation(self): + """Mock Observation.""" + Observation = namedtuple('Observation', ['temperature', 'humidity', + 'windspeed', 'winddirection', + 'precipitation', 'pressure', + 'description']) + + return Observation(18, 71.0, 3.94, 'NW', 0, 1000.0, '---') + + async def forecast(self): + """Mock Forecast.""" + Forecast = namedtuple('Forecast', ['precipitaProb', 'tMin', 'tMax', + 'predWindDir', 'idWeatherType', + 'classWindSpeed', 'longitude', + 'forecastDate', 'classPrecInt', + 'latitude', 'description']) + + return [Forecast(73.0, 13.7, 18.7, 'NW', 6, 2, -8.64, + '2018-05-31', 2, 40.61, + 'Aguaceiros, com vento Moderado de Noroeste')] + + @property + def local(self): + """Mock location.""" + return "HomeTown" + + @property + def latitude(self): + """Mock latitude.""" + return 0 + + @property + def longitude(self): + """Mock longitude.""" + return 0 + + +async def test_setup_configuration(hass): + """Test for successfully setting up the IPMA platform.""" + with patch('homeassistant.components.ipma.weather.async_get_station', + return_value=mock_coro(MockStation())): + assert await async_setup_component(hass, weather.DOMAIN, { + 'weather': { + 'name': 'HomeTown', + 'platform': 'ipma', + } + }) + await hass.async_block_till_done() + + state = hass.states.get('weather.hometown') + assert state.state == 'rainy' + + data = state.attributes + assert data.get(ATTR_WEATHER_TEMPERATURE) == 18.0 + assert data.get(ATTR_WEATHER_HUMIDITY) == 71 + assert data.get(ATTR_WEATHER_PRESSURE) == 1000.0 + assert data.get(ATTR_WEATHER_WIND_SPEED) == 3.94 + assert data.get(ATTR_WEATHER_WIND_BEARING) == 'NW' + assert state.attributes.get('friendly_name') == 'HomeTown' + + +async def test_setup_config_flow(hass): + """Test for successfully setting up the IPMA platform.""" + with patch('homeassistant.components.ipma.weather.async_get_station', + return_value=mock_coro(MockStation())): + entry = MockConfigEntry(domain='ipma', data=TEST_CONFIG) + await hass.config_entries.async_forward_entry_setup( + entry, WEATHER_DOMAIN) + await hass.async_block_till_done() + + state = hass.states.get('weather.hometown') + assert state.state == 'rainy' + + data = state.attributes + assert data.get(ATTR_WEATHER_TEMPERATURE) == 18.0 + assert data.get(ATTR_WEATHER_HUMIDITY) == 71 + assert data.get(ATTR_WEATHER_PRESSURE) == 1000.0 + assert data.get(ATTR_WEATHER_WIND_SPEED) == 3.94 + assert data.get(ATTR_WEATHER_WIND_BEARING) == 'NW' + assert state.attributes.get('friendly_name') == 'HomeTown' diff --git a/tests/components/kira/__init__.py b/tests/components/kira/__init__.py new file mode 100644 index 00000000000..b92ba05bdb1 --- /dev/null +++ b/tests/components/kira/__init__.py @@ -0,0 +1 @@ +"""Tests for the Kira integration.""" diff --git a/tests/components/test_kira.py b/tests/components/kira/test_init.py similarity index 100% rename from tests/components/test_kira.py rename to tests/components/kira/test_init.py diff --git a/tests/components/remote/test_kira.py b/tests/components/kira/test_remote.py similarity index 96% rename from tests/components/remote/test_kira.py rename to tests/components/kira/test_remote.py index 74c8e2854d0..afa5f201422 100644 --- a/tests/components/remote/test_kira.py +++ b/tests/components/kira/test_remote.py @@ -2,7 +2,7 @@ import unittest from unittest.mock import MagicMock -from homeassistant.components.remote import kira as kira +from homeassistant.components.kira import remote as kira from tests.common import get_test_home_assistant diff --git a/tests/components/sensor/test_kira.py b/tests/components/kira/test_sensor.py similarity index 96% rename from tests/components/sensor/test_kira.py rename to tests/components/kira/test_sensor.py index 76aba46d514..5fe4ca2ee0a 100644 --- a/tests/components/sensor/test_kira.py +++ b/tests/components/kira/test_sensor.py @@ -2,7 +2,7 @@ import unittest from unittest.mock import MagicMock -from homeassistant.components.sensor import kira as kira +from homeassistant.components.kira import sensor as kira from tests.common import get_test_home_assistant diff --git a/tests/components/light/test_rflink.py b/tests/components/light/test_rflink.py index 17898bd2451..901a2d3a286 100644 --- a/tests/components/light/test_rflink.py +++ b/tests/components/light/test_rflink.py @@ -12,7 +12,7 @@ from homeassistant.const import ( from homeassistant.core import callback, State, CoreState from tests.common import mock_restore_cache -from ..test_rflink import mock_rflink +from tests.components.rflink.test_init import mock_rflink DOMAIN = 'light' @@ -632,7 +632,7 @@ async def test_restore_state(hass, monkeypatch): state = hass.states.get(DOMAIN + '.l4') assert state assert state.state == STATE_OFF - assert not state.attributes.get(ATTR_BRIGHTNESS) + assert state.attributes[ATTR_BRIGHTNESS] == 255 assert state.attributes['assumed_state'] # test coverage for dimmable light diff --git a/tests/components/litejet/__init__.py b/tests/components/litejet/__init__.py new file mode 100644 index 00000000000..9a01fbe5114 --- /dev/null +++ b/tests/components/litejet/__init__.py @@ -0,0 +1 @@ +"""Tests for the litejet component.""" diff --git a/tests/components/test_litejet.py b/tests/components/litejet/test_init.py similarity index 100% rename from tests/components/test_litejet.py rename to tests/components/litejet/test_init.py diff --git a/tests/components/logbook/__init__.py b/tests/components/logbook/__init__.py new file mode 100644 index 00000000000..31f9305a213 --- /dev/null +++ b/tests/components/logbook/__init__.py @@ -0,0 +1 @@ +"""Tests for the logbook component.""" diff --git a/tests/components/test_logbook.py b/tests/components/logbook/test_init.py similarity index 100% rename from tests/components/test_logbook.py rename to tests/components/logbook/test_init.py diff --git a/tests/components/logentries/__init__.py b/tests/components/logentries/__init__.py new file mode 100644 index 00000000000..c01e58c9d40 --- /dev/null +++ b/tests/components/logentries/__init__.py @@ -0,0 +1 @@ +"""Tests for the logentries component.""" diff --git a/tests/components/test_logentries.py b/tests/components/logentries/test_init.py similarity index 100% rename from tests/components/test_logentries.py rename to tests/components/logentries/test_init.py diff --git a/tests/components/logger/__init__.py b/tests/components/logger/__init__.py new file mode 100644 index 00000000000..8fb7f0dab02 --- /dev/null +++ b/tests/components/logger/__init__.py @@ -0,0 +1 @@ +"""Tests for the logger component.""" diff --git a/tests/components/test_logger.py b/tests/components/logger/test_init.py similarity index 100% rename from tests/components/test_logger.py rename to tests/components/logger/test_init.py diff --git a/tests/components/media_player/common.py b/tests/components/media_player/common.py index 2174967eae5..4a53920d758 100644 --- a/tests/components/media_player/common.py +++ b/tests/components/media_player/common.py @@ -3,7 +3,7 @@ All containing methods are legacy helpers that should not be used by new components. Instead call the service directly. """ -from homeassistant.components.media_player import ( +from homeassistant.components.media_player.const import ( ATTR_INPUT_SOURCE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_ENQUEUE, ATTR_MEDIA_SEEK_POSITION, ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, DOMAIN, SERVICE_CLEAR_PLAYLIST, diff --git a/tests/components/media_player/test_blackbird.py b/tests/components/media_player/test_blackbird.py index 38a63f294f9..6ab3b69b558 100644 --- a/tests/components/media_player/test_blackbird.py +++ b/tests/components/media_player/test_blackbird.py @@ -4,7 +4,7 @@ from unittest import mock import voluptuous as vol from collections import defaultdict -from homeassistant.components.media_player import ( +from homeassistant.components.media_player.const import ( DOMAIN, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_SELECT_SOURCE) from homeassistant.const import STATE_ON, STATE_OFF diff --git a/tests/components/media_player/test_directv.py b/tests/components/media_player/test_directv.py index d8e561d8d2a..5918946fe6c 100644 --- a/tests/components/media_player/test_directv.py +++ b/tests/components/media_player/test_directv.py @@ -5,10 +5,14 @@ from datetime import datetime, timedelta import requests import pytest -import homeassistant.components.media_player as mp -from homeassistant.components.media_player import ( - ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_ENQUEUE, DOMAIN, - SERVICE_PLAY_MEDIA) +from homeassistant.components.media_player.const import ( + ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, MEDIA_TYPE_TVSHOW, + ATTR_MEDIA_ENQUEUE, ATTR_MEDIA_DURATION, ATTR_MEDIA_TITLE, + ATTR_MEDIA_POSITION, ATTR_MEDIA_SERIES_TITLE, ATTR_MEDIA_CHANNEL, + ATTR_INPUT_SOURCE, ATTR_MEDIA_POSITION_UPDATED_AT, DOMAIN, + SERVICE_PLAY_MEDIA, SUPPORT_PAUSE, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, + SUPPORT_PLAY_MEDIA, SUPPORT_STOP, SUPPORT_NEXT_TRACK, + SUPPORT_PREVIOUS_TRACK, SUPPORT_PLAY) from homeassistant.components.media_player.directv import ( ATTR_MEDIA_CURRENTLY_RECORDING, ATTR_MEDIA_RATING, ATTR_MEDIA_RECORDED, ATTR_MEDIA_START_TIME, DEFAULT_DEVICE, DEFAULT_PORT) @@ -149,7 +153,7 @@ def platforms(hass, dtv_side_effect, mock_now): patch('DirectPy.DIRECTV', side_effect=dtv_side_effect), \ patch('homeassistant.util.dt.utcnow', return_value=mock_now): hass.loop.run_until_complete(async_setup_component( - hass, mp.DOMAIN, config)) + hass, DOMAIN, config)) hass.loop.run_until_complete(hass.async_block_till_done()) yield @@ -281,7 +285,7 @@ async def test_setup_platform_config(hass): with MockDependency('DirectPy'), \ patch('DirectPy.DIRECTV', new=MockDirectvClass): - await async_setup_component(hass, mp.DOMAIN, WORKING_CONFIG) + await async_setup_component(hass, DOMAIN, WORKING_CONFIG) await hass.async_block_till_done() state = hass.states.get(MAIN_ENTITY_ID) @@ -295,7 +299,7 @@ async def test_setup_platform_discover(hass): patch('DirectPy.DIRECTV', new=MockDirectvClass): hass.async_create_task( - async_load_platform(hass, mp.DOMAIN, 'directv', DISCOVERY_INFO, + async_load_platform(hass, DOMAIN, 'directv', DISCOVERY_INFO, {'media_player': {}}) ) await hass.async_block_till_done() @@ -310,10 +314,10 @@ async def test_setup_platform_discover_duplicate(hass): with MockDependency('DirectPy'), \ patch('DirectPy.DIRECTV', new=MockDirectvClass): - await async_setup_component(hass, mp.DOMAIN, WORKING_CONFIG) + await async_setup_component(hass, DOMAIN, WORKING_CONFIG) await hass.async_block_till_done() hass.async_create_task( - async_load_platform(hass, mp.DOMAIN, 'directv', DISCOVERY_INFO, + async_load_platform(hass, DOMAIN, 'directv', DISCOVERY_INFO, {'media_player': {}}) ) await hass.async_block_till_done() @@ -337,11 +341,11 @@ async def test_setup_platform_discover_client(hass): with MockDependency('DirectPy'), \ patch('DirectPy.DIRECTV', new=MockDirectvClass): - await async_setup_component(hass, mp.DOMAIN, WORKING_CONFIG) + await async_setup_component(hass, DOMAIN, WORKING_CONFIG) await hass.async_block_till_done() hass.async_create_task( - async_load_platform(hass, mp.DOMAIN, 'directv', DISCOVERY_INFO, + async_load_platform(hass, DOMAIN, 'directv', DISCOVERY_INFO, {'media_player': {}}) ) await hass.async_block_till_done() @@ -362,16 +366,16 @@ async def test_supported_features(hass, platforms): """Test supported features.""" # Features supported for main DVR state = hass.states.get(MAIN_ENTITY_ID) - assert mp.SUPPORT_PAUSE | mp.SUPPORT_TURN_ON | mp.SUPPORT_TURN_OFF |\ - mp.SUPPORT_PLAY_MEDIA | mp.SUPPORT_STOP | mp.SUPPORT_NEXT_TRACK |\ - mp.SUPPORT_PREVIOUS_TRACK | mp.SUPPORT_PLAY ==\ + assert SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF |\ + SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_NEXT_TRACK |\ + SUPPORT_PREVIOUS_TRACK | SUPPORT_PLAY ==\ state.attributes.get('supported_features') # Feature supported for clients. state = hass.states.get(CLIENT_ENTITY_ID) - assert mp.SUPPORT_PAUSE |\ - mp.SUPPORT_PLAY_MEDIA | mp.SUPPORT_STOP | mp.SUPPORT_NEXT_TRACK |\ - mp.SUPPORT_PREVIOUS_TRACK | mp.SUPPORT_PLAY ==\ + assert SUPPORT_PAUSE |\ + SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_NEXT_TRACK |\ + SUPPORT_PREVIOUS_TRACK | SUPPORT_PLAY ==\ state.attributes.get('supported_features') @@ -391,21 +395,21 @@ async def test_check_attributes(hass, platforms, mock_now): state = hass.states.get(CLIENT_ENTITY_ID) assert state.state == STATE_PLAYING - assert state.attributes.get(mp.ATTR_MEDIA_CONTENT_ID) == \ + assert state.attributes.get(ATTR_MEDIA_CONTENT_ID) == \ RECORDING['programId'] - assert state.attributes.get(mp.ATTR_MEDIA_CONTENT_TYPE) == \ - mp.MEDIA_TYPE_TVSHOW - assert state.attributes.get(mp.ATTR_MEDIA_DURATION) == \ + assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) == \ + MEDIA_TYPE_TVSHOW + assert state.attributes.get(ATTR_MEDIA_DURATION) == \ RECORDING['duration'] - assert state.attributes.get(mp.ATTR_MEDIA_POSITION) == 2 + assert state.attributes.get(ATTR_MEDIA_POSITION) == 2 assert state.attributes.get( - mp.ATTR_MEDIA_POSITION_UPDATED_AT) == next_update - assert state.attributes.get(mp.ATTR_MEDIA_TITLE) == RECORDING['title'] - assert state.attributes.get(mp.ATTR_MEDIA_SERIES_TITLE) == \ + ATTR_MEDIA_POSITION_UPDATED_AT) == next_update + assert state.attributes.get(ATTR_MEDIA_TITLE) == RECORDING['title'] + assert state.attributes.get(ATTR_MEDIA_SERIES_TITLE) == \ RECORDING['episodeTitle'] - assert state.attributes.get(mp.ATTR_MEDIA_CHANNEL) == \ + assert state.attributes.get(ATTR_MEDIA_CHANNEL) == \ "{} ({})".format(RECORDING['callsign'], RECORDING['major']) - assert state.attributes.get(mp.ATTR_INPUT_SOURCE) == RECORDING['major'] + assert state.attributes.get(ATTR_INPUT_SOURCE) == RECORDING['major'] assert state.attributes.get(ATTR_MEDIA_CURRENTLY_RECORDING) == \ RECORDING['isRecording'] assert state.attributes.get(ATTR_MEDIA_RATING) == RECORDING['rating'] @@ -423,7 +427,7 @@ async def test_check_attributes(hass, platforms, mock_now): state = hass.states.get(CLIENT_ENTITY_ID) assert state.state == STATE_PAUSED assert state.attributes.get( - mp.ATTR_MEDIA_POSITION_UPDATED_AT) == next_update + ATTR_MEDIA_POSITION_UPDATED_AT) == next_update async def test_main_services(hass, platforms, main_dtv, mock_now): diff --git a/tests/components/media_player/test_monoprice.py b/tests/components/media_player/test_monoprice.py index c6a6b3036d9..d6374cf9dd7 100644 --- a/tests/components/media_player/test_monoprice.py +++ b/tests/components/media_player/test_monoprice.py @@ -4,7 +4,7 @@ from unittest import mock import voluptuous as vol from collections import defaultdict -from homeassistant.components.media_player import ( +from homeassistant.components.media_player.const import ( DOMAIN, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE) from homeassistant.const import STATE_ON, STATE_OFF diff --git a/tests/components/media_player/test_reproduce_state.py b/tests/components/media_player/test_reproduce_state.py new file mode 100644 index 00000000000..f39733178b1 --- /dev/null +++ b/tests/components/media_player/test_reproduce_state.py @@ -0,0 +1,199 @@ +"""The tests for reproduction of state.""" + +import pytest + +from homeassistant.components.media_player import async_reproduce_states +from homeassistant.components.media_player.const import ( + ATTR_INPUT_SOURCE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, + ATTR_MEDIA_ENQUEUE, ATTR_MEDIA_SEEK_POSITION, ATTR_MEDIA_VOLUME_LEVEL, + ATTR_MEDIA_VOLUME_MUTED, ATTR_SOUND_MODE, DOMAIN, SERVICE_PLAY_MEDIA, + SERVICE_SELECT_SOUND_MODE, SERVICE_SELECT_SOURCE) +from homeassistant.const import ( + SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_SEEK, + SERVICE_MEDIA_STOP, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_MUTE, + SERVICE_VOLUME_SET, STATE_IDLE, STATE_OFF, STATE_ON, STATE_PAUSED, + STATE_PLAYING) +from homeassistant.core import Context, State + +from tests.common import async_mock_service + +ENTITY_1 = 'media_player.test1' +ENTITY_2 = 'media_player.test2' + + +@pytest.mark.parametrize( + 'service,state', [ + (SERVICE_TURN_ON, STATE_ON), + (SERVICE_TURN_OFF, STATE_OFF), + (SERVICE_MEDIA_PLAY, STATE_PLAYING), + (SERVICE_MEDIA_STOP, STATE_IDLE), + (SERVICE_MEDIA_PAUSE, STATE_PAUSED), + ]) +async def test_state(hass, service, state): + """Test that we can turn a state into a service call.""" + calls_1 = async_mock_service(hass, DOMAIN, service) + + await async_reproduce_states(hass, [ + State(ENTITY_1, state) + ]) + + await hass.async_block_till_done() + + assert len(calls_1) == 1 + assert calls_1[0].data == {'entity_id': ENTITY_1} + + +async def test_turn_on_with_mode(hass): + """Test that state with additional attributes call multiple services.""" + calls_1 = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) + calls_2 = async_mock_service(hass, DOMAIN, SERVICE_SELECT_SOUND_MODE) + + await async_reproduce_states(hass, [ + State(ENTITY_1, 'on', + {ATTR_SOUND_MODE: 'dummy'}) + ]) + + await hass.async_block_till_done() + + assert len(calls_1) == 1 + assert calls_1[0].data == {'entity_id': ENTITY_1} + + assert len(calls_2) == 1 + assert calls_2[0].data == {'entity_id': ENTITY_1, + ATTR_SOUND_MODE: 'dummy'} + + +async def test_multiple_same_state(hass): + """Test that multiple states with same state gets calls.""" + calls_1 = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) + + await async_reproduce_states(hass, [ + State(ENTITY_1, 'on'), + State(ENTITY_2, 'on'), + ]) + + await hass.async_block_till_done() + + assert len(calls_1) == 2 + # order is not guaranteed + assert any(call.data == {'entity_id': 'media_player.test1'} + for call in calls_1) + assert any(call.data == {'entity_id': 'media_player.test2'} + for call in calls_1) + + +async def test_multiple_different_state(hass): + """Test that multiple states with different state gets calls.""" + calls_1 = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) + calls_2 = async_mock_service(hass, DOMAIN, SERVICE_TURN_OFF) + + await async_reproduce_states(hass, [ + State(ENTITY_1, 'on'), + State(ENTITY_2, 'off'), + ]) + + await hass.async_block_till_done() + + assert len(calls_1) == 1 + assert calls_1[0].data == {'entity_id': 'media_player.test1'} + assert len(calls_2) == 1 + assert calls_2[0].data == {'entity_id': 'media_player.test2'} + + +async def test_state_with_context(hass): + """Test that context is forwarded.""" + calls = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) + + context = Context() + + await async_reproduce_states(hass, [ + State(ENTITY_1, 'on') + ], context) + + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].data == {'entity_id': ENTITY_1} + assert calls[0].context == context + + +async def test_attribute_no_state(hass): + """Test that no state service call is made with none state.""" + calls_1 = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) + calls_2 = async_mock_service(hass, DOMAIN, SERVICE_TURN_OFF) + calls_3 = async_mock_service(hass, DOMAIN, SERVICE_SELECT_SOUND_MODE) + + value = "dummy" + + await async_reproduce_states(hass, [ + State(ENTITY_1, None, + {ATTR_SOUND_MODE: value}) + ]) + + await hass.async_block_till_done() + + assert len(calls_1) == 0 + assert len(calls_2) == 0 + assert len(calls_3) == 1 + assert calls_3[0].data == {'entity_id': ENTITY_1, + ATTR_SOUND_MODE: value} + + +@pytest.mark.parametrize( + 'service,attribute', [ + (SERVICE_VOLUME_SET, ATTR_MEDIA_VOLUME_LEVEL), + (SERVICE_VOLUME_MUTE, ATTR_MEDIA_VOLUME_MUTED), + (SERVICE_MEDIA_SEEK, ATTR_MEDIA_SEEK_POSITION), + (SERVICE_SELECT_SOURCE, ATTR_INPUT_SOURCE), + (SERVICE_SELECT_SOUND_MODE, ATTR_SOUND_MODE), + ]) +async def test_attribute(hass, service, attribute): + """Test that service call is made for each attribute.""" + calls_1 = async_mock_service(hass, DOMAIN, service) + + value = "dummy" + + await async_reproduce_states(hass, [ + State(ENTITY_1, None, + {attribute: value}) + ]) + + await hass.async_block_till_done() + + assert len(calls_1) == 1 + assert calls_1[0].data == {'entity_id': ENTITY_1, + attribute: value} + + +async def test_play_media(hass): + """Test that no state service call is made with none state.""" + calls_1 = async_mock_service(hass, DOMAIN, SERVICE_PLAY_MEDIA) + + value_1 = "dummy_1" + value_2 = "dummy_2" + value_3 = "dummy_3" + + await async_reproduce_states(hass, [ + State(ENTITY_1, None, + {ATTR_MEDIA_CONTENT_TYPE: value_1, + ATTR_MEDIA_CONTENT_ID: value_2}) + ]) + + await async_reproduce_states(hass, [ + State(ENTITY_1, None, + {ATTR_MEDIA_CONTENT_TYPE: value_1, + ATTR_MEDIA_CONTENT_ID: value_2, + ATTR_MEDIA_ENQUEUE: value_3}) + ]) + + await hass.async_block_till_done() + + assert len(calls_1) == 2 + assert calls_1[0].data == {'entity_id': ENTITY_1, + ATTR_MEDIA_CONTENT_TYPE: value_1, + ATTR_MEDIA_CONTENT_ID: value_2} + + assert calls_1[1].data == {'entity_id': ENTITY_1, + ATTR_MEDIA_CONTENT_TYPE: value_1, + ATTR_MEDIA_CONTENT_ID: value_2, + ATTR_MEDIA_ENQUEUE: value_3} diff --git a/tests/components/media_player/test_samsungtv.py b/tests/components/media_player/test_samsungtv.py index c2f5d28fd5d..a74f476981a 100644 --- a/tests/components/media_player/test_samsungtv.py +++ b/tests/components/media_player/test_samsungtv.py @@ -8,7 +8,7 @@ from asynctest import mock import pytest import tests.common -from homeassistant.components.media_player import SUPPORT_TURN_ON, \ +from homeassistant.components.media_player.const import SUPPORT_TURN_ON, \ MEDIA_TYPE_CHANNEL, MEDIA_TYPE_URL from homeassistant.components.media_player.samsungtv import setup_platform, \ CONF_TIMEOUT, SamsungTVDevice, SUPPORT_SAMSUNGTV diff --git a/tests/components/media_player/test_universal.py b/tests/components/media_player/test_universal.py index 58911776836..ee86735a063 100644 --- a/tests/components/media_player/test_universal.py +++ b/tests/components/media_player/test_universal.py @@ -584,7 +584,8 @@ class TestMediaPlayer(unittest.TestCase): check_flags = universal.SUPPORT_TURN_ON | universal.SUPPORT_TURN_OFF \ | universal.SUPPORT_VOLUME_STEP | universal.SUPPORT_VOLUME_MUTE \ - | universal.SUPPORT_SELECT_SOURCE | universal.SUPPORT_SHUFFLE_SET + | universal.SUPPORT_SELECT_SOURCE | universal.SUPPORT_SHUFFLE_SET \ + | universal.SUPPORT_VOLUME_SET assert check_flags == ump.supported_features diff --git a/tests/components/melissa/__init__.py b/tests/components/melissa/__init__.py new file mode 100644 index 00000000000..c4caf0fe671 --- /dev/null +++ b/tests/components/melissa/__init__.py @@ -0,0 +1 @@ +"""Tests for the melissa component.""" diff --git a/tests/components/test_melissa.py b/tests/components/melissa/test_init.py similarity index 100% rename from tests/components/test_melissa.py rename to tests/components/melissa/test_init.py diff --git a/tests/components/microsoft_face/__init__.py b/tests/components/microsoft_face/__init__.py new file mode 100644 index 00000000000..0b07c35b515 --- /dev/null +++ b/tests/components/microsoft_face/__init__.py @@ -0,0 +1 @@ +"""Tests for the microsoft_face component.""" diff --git a/tests/components/test_microsoft_face.py b/tests/components/microsoft_face/test_init.py similarity index 100% rename from tests/components/test_microsoft_face.py rename to tests/components/microsoft_face/test_init.py diff --git a/tests/components/mochad/__init__.py b/tests/components/mochad/__init__.py new file mode 100644 index 00000000000..12584aba239 --- /dev/null +++ b/tests/components/mochad/__init__.py @@ -0,0 +1 @@ +"""Tests for the Mochad integration.""" diff --git a/tests/components/light/test_mochad.py b/tests/components/mochad/test_light.py similarity index 97% rename from tests/components/light/test_mochad.py rename to tests/components/mochad/test_light.py index d96bf8f5abb..33bf1fd333b 100644 --- a/tests/components/light/test_mochad.py +++ b/tests/components/mochad/test_light.py @@ -5,7 +5,7 @@ import unittest.mock as mock import pytest from homeassistant.components import light -from homeassistant.components.light import mochad +from homeassistant.components.mochad import light as mochad from homeassistant.setup import setup_component from tests.common import get_test_home_assistant @@ -35,7 +35,7 @@ class TestMochadSwitchSetup(unittest.TestCase): """Stop everything that was started.""" self.hass.stop() - @mock.patch('homeassistant.components.light.mochad.MochadLight') + @mock.patch('homeassistant.components.mochad.light.MochadLight') def test_setup_adds_proper_devices(self, mock_light): """Test if setup adds devices.""" good_config = { diff --git a/tests/components/switch/test_mochad.py b/tests/components/mochad/test_switch.py similarity index 94% rename from tests/components/switch/test_mochad.py rename to tests/components/mochad/test_switch.py index 76640f88723..e5216b276fa 100644 --- a/tests/components/switch/test_mochad.py +++ b/tests/components/mochad/test_switch.py @@ -6,7 +6,7 @@ import pytest from homeassistant.setup import setup_component from homeassistant.components import switch -from homeassistant.components.switch import mochad +from homeassistant.components.mochad import switch as mochad from tests.common import get_test_home_assistant @@ -36,7 +36,7 @@ class TestMochadSwitchSetup(unittest.TestCase): """Stop everything that was started.""" self.hass.stop() - @mock.patch('homeassistant.components.switch.mochad.MochadSwitch') + @mock.patch('homeassistant.components.mochad.switch.MochadSwitch') def test_setup_adds_proper_devices(self, mock_switch): """Test if setup adds devices.""" good_config = { diff --git a/tests/components/mqtt/conftest.py b/tests/components/mqtt/conftest.py new file mode 100644 index 00000000000..290682549f5 --- /dev/null +++ b/tests/components/mqtt/conftest.py @@ -0,0 +1,12 @@ +"""Test fixtures for mqtt component.""" +import pytest + +from tests.common import async_mock_mqtt_component + + +@pytest.fixture +def mqtt_mock(loop, hass): + """Fixture to mock MQTT.""" + client = loop.run_until_complete(async_mock_mqtt_component(hass)) + client.reset_mock() + return client diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index 572cbdb0e10..81c993ed311 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -6,9 +6,9 @@ from unittest.mock import ANY from homeassistant.components import alarm_control_panel, mqtt from homeassistant.components.mqtt.discovery import async_start from homeassistant.const import ( - STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, - STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNAVAILABLE, - STATE_UNKNOWN) + STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, + STATE_UNAVAILABLE, STATE_UNKNOWN) from homeassistant.setup import setup_component from tests.common import ( @@ -72,8 +72,8 @@ class TestAlarmControlPanelMQTT(unittest.TestCase): self.hass.states.get(entity_id).state for state in (STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_AWAY, STATE_ALARM_PENDING, - STATE_ALARM_TRIGGERED): + STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED): fire_mqtt_message(self.hass, 'alarm/state', state) self.hass.block_till_done() assert state == self.hass.states.get(entity_id).state @@ -164,6 +164,39 @@ class TestAlarmControlPanelMQTT(unittest.TestCase): self.hass.block_till_done() assert call_count == self.mock_publish.call_count + def test_arm_night_publishes_mqtt(self): + """Test publishing of MQTT messages while armed.""" + assert setup_component(self.hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'alarm/state', + 'command_topic': 'alarm/command', + } + }) + + common.alarm_arm_night(self.hass) + self.hass.block_till_done() + self.mock_publish.async_publish.assert_called_once_with( + 'alarm/command', 'ARM_NIGHT', 0, False) + + def test_arm_night_not_publishes_mqtt_with_invalid_code(self): + """Test not publishing of MQTT messages with invalid code.""" + assert setup_component(self.hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'alarm/state', + 'command_topic': 'alarm/command', + 'code': '1234' + } + }) + + call_count = self.mock_publish.call_count + common.alarm_arm_night(self.hass, 'abcd') + self.hass.block_till_done() + assert call_count == self.mock_publish.call_count + def test_disarm_publishes_mqtt(self): """Test publishing of MQTT messages while disarmed.""" assert setup_component(self.hass, alarm_control_panel.DOMAIN, { diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index 15b4ed22378..5726a64ba11 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -168,7 +168,7 @@ async def test_entity_id_update(hass, mqtt_mock): state = hass.states.get('camera.beer') assert state is not None assert mock_mqtt.async_subscribe.call_count == 1 - mock_mqtt.async_subscribe.assert_any_call('test-topic', ANY, 0, 'utf-8') + mock_mqtt.async_subscribe.assert_any_call('test-topic', ANY, 0, None) mock_mqtt.async_subscribe.reset_mock() registry.async_update_entity('camera.beer', new_entity_id='camera.milk') @@ -181,4 +181,4 @@ async def test_entity_id_update(hass, mqtt_mock): state = hass.states.get('camera.milk') assert state is not None assert mock_mqtt.async_subscribe.call_count == 1 - mock_mqtt.async_subscribe.assert_any_call('test-topic', ANY, 0, 'utf-8') + mock_mqtt.async_subscribe.assert_any_call('test-topic', ANY, 0, None) diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index c9b7c748ea5..ecbdc39e22b 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -679,7 +679,8 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): climate.DOMAIN: { 'platform': 'mqtt', 'name': 'test', - 'state_topic': 'test-topic', + 'power_state_topic': 'test-topic', + 'power_command_topic': 'test_topic', 'json_attributes_topic': 'attr-topic' } }) @@ -697,7 +698,8 @@ async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): climate.DOMAIN: { 'platform': 'mqtt', 'name': 'test', - 'state_topic': 'test-topic', + 'power_state_topic': 'test-topic', + 'power_command_topic': 'test_topic', 'json_attributes_topic': 'attr-topic' } }) @@ -716,7 +718,8 @@ async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): climate.DOMAIN: { 'platform': 'mqtt', 'name': 'test', - 'state_topic': 'test-topic', + 'power_state_topic': 'test-topic', + 'power_command_topic': 'test_topic', 'json_attributes_topic': 'attr-topic' } }) @@ -735,12 +738,14 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): await async_start(hass, 'homeassistant', {}, entry) data1 = ( '{ "name": "Beer",' - ' "command_topic": "test_topic",' + ' "power_state_topic": "test-topic",' + ' "power_command_topic": "test_topic",' ' "json_attributes_topic": "attr-topic1" }' ) data2 = ( '{ "name": "Beer",' - ' "command_topic": "test_topic",' + ' "power_state_topic": "test-topic",' + ' "power_command_topic": "test_topic",' ' "json_attributes_topic": "attr-topic2" }' ) async_fire_mqtt_message(hass, 'homeassistant/climate/bla/config', @@ -780,14 +785,14 @@ async def test_unique_id(hass): climate.DOMAIN: [{ 'platform': 'mqtt', 'name': 'Test 1', - 'status_topic': 'test-topic', - 'command_topic': 'test_topic', + 'power_state_topic': 'test-topic', + 'power_command_topic': 'test_topic', 'unique_id': 'TOTALLY_UNIQUE' }, { 'platform': 'mqtt', 'name': 'Test 2', - 'status_topic': 'test-topic', - 'command_topic': 'test_topic', + 'power_state_topic': 'test-topic', + 'power_command_topic': 'test_topic', 'unique_id': 'TOTALLY_UNIQUE' }] }) @@ -891,8 +896,6 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): data = json.dumps({ 'platform': 'mqtt', 'name': 'Test 1', - 'state_topic': 'test-topic', - 'command_topic': 'test-topic', 'device': { 'identifiers': ['helloworld'], 'connections': [ @@ -930,8 +933,8 @@ async def test_entity_device_info_update(hass, mqtt_mock): config = { 'platform': 'mqtt', 'name': 'Test 1', - 'state_topic': 'test-topic', - 'command_topic': 'test-command-topic', + 'power_state_topic': 'test-topic', + 'power_command_topic': 'test-command-topic', 'device': { 'identifiers': ['helloworld'], 'connections': [ diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 343bb3643c6..47681e0de10 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -1051,6 +1051,34 @@ class TestCoverMQTT(unittest.TestCase): state = self.hass.states.get('cover.test') assert STATE_UNAVAILABLE == state.state + def test_valid_device_class(self): + """Test the setting of a valid sensor class.""" + assert setup_component(self.hass, cover.DOMAIN, { + cover.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'device_class': 'garage', + 'state_topic': 'test-topic', + } + }) + + state = self.hass.states.get('cover.test') + assert 'garage' == state.attributes.get('device_class') + + def test_invalid_device_class(self): + """Test the setting of an invalid sensor class.""" + assert setup_component(self.hass, cover.DOMAIN, { + cover.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'device_class': 'abc123', + 'state_topic': 'test-topic', + } + }) + + state = self.hass.states.get('cover.test') + assert state is None + async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): """Test the setting of attribute via MQTT with JSON payload.""" diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 47bd912fbc8..ffc385021d7 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -222,7 +222,15 @@ def test_discovery_expansion(hass, mqtt_mock, caplog): '{ "~": "some/base/topic",' ' "name": "DiscoveryExpansionTest1",' ' "stat_t": "test_topic/~",' - ' "cmd_t": "~/test_topic" }' + ' "cmd_t": "~/test_topic",' + ' "dev":{' + ' "ids":["5706DF"],' + ' "name":"DiscoveryExpansionTest1 Device",' + ' "mdl":"Generic",' + ' "sw":"1.2.3.4",' + ' "mf":"Noone"' + ' }' + '}' ) async_fire_mqtt_message( diff --git a/tests/components/mqtt_eventstream/__init__.py b/tests/components/mqtt_eventstream/__init__.py new file mode 100644 index 00000000000..e5c1f19d094 --- /dev/null +++ b/tests/components/mqtt_eventstream/__init__.py @@ -0,0 +1 @@ +"""Tests for the mqtt_eventstream component.""" diff --git a/tests/components/test_mqtt_eventstream.py b/tests/components/mqtt_eventstream/test_init.py similarity index 100% rename from tests/components/test_mqtt_eventstream.py rename to tests/components/mqtt_eventstream/test_init.py diff --git a/tests/components/mqtt_statestream/__init__.py b/tests/components/mqtt_statestream/__init__.py new file mode 100644 index 00000000000..cc104a104c2 --- /dev/null +++ b/tests/components/mqtt_statestream/__init__.py @@ -0,0 +1 @@ +"""Tests for the mqtt_statestream component.""" diff --git a/tests/components/test_mqtt_statestream.py b/tests/components/mqtt_statestream/test_init.py similarity index 100% rename from tests/components/test_mqtt_statestream.py rename to tests/components/mqtt_statestream/test_init.py diff --git a/tests/components/mythicbeastsdns/__init__.py b/tests/components/mythicbeastsdns/__init__.py new file mode 100644 index 00000000000..b12d296455c --- /dev/null +++ b/tests/components/mythicbeastsdns/__init__.py @@ -0,0 +1 @@ +"""Tests for the mythicbeastsdns component.""" diff --git a/tests/components/test_mythicbeastsdns.py b/tests/components/mythicbeastsdns/test_init.py similarity index 100% rename from tests/components/test_mythicbeastsdns.py rename to tests/components/mythicbeastsdns/test_init.py diff --git a/tests/components/namecheapdns/__init__.py b/tests/components/namecheapdns/__init__.py new file mode 100644 index 00000000000..db064f4405d --- /dev/null +++ b/tests/components/namecheapdns/__init__.py @@ -0,0 +1 @@ +"""Tests for the namecheapdns component.""" diff --git a/tests/components/test_namecheapdns.py b/tests/components/namecheapdns/test_init.py similarity index 100% rename from tests/components/test_namecheapdns.py rename to tests/components/namecheapdns/test_init.py diff --git a/tests/components/ness_alarm/__init__.py b/tests/components/ness_alarm/__init__.py new file mode 100644 index 00000000000..ed901068c69 --- /dev/null +++ b/tests/components/ness_alarm/__init__.py @@ -0,0 +1 @@ +"""Tests for the ness_alarm component.""" diff --git a/tests/components/test_ness_alarm.py b/tests/components/ness_alarm/test_init.py similarity index 100% rename from tests/components/test_ness_alarm.py rename to tests/components/ness_alarm/test_init.py diff --git a/tests/components/no_ip/__init__.py b/tests/components/no_ip/__init__.py new file mode 100644 index 00000000000..d2c1c62b17e --- /dev/null +++ b/tests/components/no_ip/__init__.py @@ -0,0 +1 @@ +"""Tests for the no_ip component.""" diff --git a/tests/components/test_no_ip.py b/tests/components/no_ip/test_init.py similarity index 100% rename from tests/components/test_no_ip.py rename to tests/components/no_ip/test_init.py diff --git a/tests/components/nuheat/__init__.py b/tests/components/nuheat/__init__.py new file mode 100644 index 00000000000..c238d9b0c72 --- /dev/null +++ b/tests/components/nuheat/__init__.py @@ -0,0 +1 @@ +"""Tests for the nuheat component.""" diff --git a/tests/components/test_nuheat.py b/tests/components/nuheat/test_init.py similarity index 100% rename from tests/components/test_nuheat.py rename to tests/components/nuheat/test_init.py diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index d6a4030190d..5b303943747 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -69,6 +69,7 @@ async def test_onboarding_user_already_done(hass, hass_storage, async def test_onboarding_user(hass, hass_storage, aiohttp_client): """Test creating a new user.""" + assert await async_setup_component(hass, 'person', {}) mock_storage(hass_storage, { 'done': ['hello'] }) @@ -90,6 +91,7 @@ async def test_onboarding_user(hass, hass_storage, aiohttp_client): assert user.name == 'Test Name' assert len(user.credentials) == 1 assert user.credentials[0].data['username'] == 'test-user' + assert len(hass.data['person'].storage_data) == 1 async def test_onboarding_user_invalid_name(hass, hass_storage, diff --git a/tests/components/panel_custom/__init__.py b/tests/components/panel_custom/__init__.py new file mode 100644 index 00000000000..543978d21fb --- /dev/null +++ b/tests/components/panel_custom/__init__.py @@ -0,0 +1 @@ +"""Tests for the panel_custom component.""" diff --git a/tests/components/test_panel_custom.py b/tests/components/panel_custom/test_init.py similarity index 100% rename from tests/components/test_panel_custom.py rename to tests/components/panel_custom/test_init.py diff --git a/tests/components/panel_iframe/__init__.py b/tests/components/panel_iframe/__init__.py new file mode 100644 index 00000000000..df7115d9e97 --- /dev/null +++ b/tests/components/panel_iframe/__init__.py @@ -0,0 +1 @@ +"""Tests for the panel_iframe component.""" diff --git a/tests/components/test_panel_iframe.py b/tests/components/panel_iframe/test_init.py similarity index 100% rename from tests/components/test_panel_iframe.py rename to tests/components/panel_iframe/test_init.py diff --git a/tests/components/person/__init__.py b/tests/components/person/__init__.py new file mode 100644 index 00000000000..217189a78a9 --- /dev/null +++ b/tests/components/person/__init__.py @@ -0,0 +1 @@ +"""The tests for the person component.""" diff --git a/tests/components/person/test_init.py b/tests/components/person/test_init.py new file mode 100644 index 00000000000..f2d796fb204 --- /dev/null +++ b/tests/components/person/test_init.py @@ -0,0 +1,563 @@ +"""The tests for the person component.""" +from unittest.mock import Mock + +from homeassistant.components.person import ( + ATTR_SOURCE, ATTR_USER_ID, DOMAIN, PersonManager) +from homeassistant.const import ( + ATTR_ID, ATTR_LATITUDE, ATTR_LONGITUDE, STATE_UNKNOWN, + EVENT_HOMEASSISTANT_START) +from homeassistant.core import CoreState, State +from homeassistant.setup import async_setup_component + +import pytest + +from tests.common import mock_component, mock_restore_cache, mock_coro_func + +DEVICE_TRACKER = 'device_tracker.test_tracker' +DEVICE_TRACKER_2 = 'device_tracker.test_tracker_2' + + +@pytest.fixture +def storage_setup(hass, hass_storage, hass_admin_user): + """Storage setup.""" + hass_storage[DOMAIN] = { + 'key': DOMAIN, + 'version': 1, + 'data': { + 'persons': [ + { + 'id': '1234', + 'name': 'tracked person', + 'user_id': hass_admin_user.id, + 'device_trackers': [DEVICE_TRACKER] + } + ] + } + } + assert hass.loop.run_until_complete( + async_setup_component(hass, DOMAIN, {}) + ) + + +async def test_minimal_setup(hass): + """Test minimal config with only name.""" + config = {DOMAIN: {'id': '1234', 'name': 'test person'}} + assert await async_setup_component(hass, DOMAIN, config) + + state = hass.states.get('person.test_person') + assert state.state == STATE_UNKNOWN + assert state.attributes.get(ATTR_LATITUDE) is None + assert state.attributes.get(ATTR_LONGITUDE) is None + assert state.attributes.get(ATTR_SOURCE) is None + assert state.attributes.get(ATTR_USER_ID) is None + + +async def test_setup_no_id(hass): + """Test config with no id.""" + config = {DOMAIN: {'name': 'test user'}} + assert not await async_setup_component(hass, DOMAIN, config) + + +async def test_setup_no_name(hass): + """Test config with no name.""" + config = {DOMAIN: {'id': '1234'}} + assert not await async_setup_component(hass, DOMAIN, config) + + +async def test_setup_user_id(hass, hass_admin_user): + """Test config with user id.""" + user_id = hass_admin_user.id + config = { + DOMAIN: {'id': '1234', 'name': 'test person', 'user_id': user_id}} + assert await async_setup_component(hass, DOMAIN, config) + + state = hass.states.get('person.test_person') + assert state.state == STATE_UNKNOWN + assert state.attributes.get(ATTR_ID) == '1234' + assert state.attributes.get(ATTR_LATITUDE) is None + assert state.attributes.get(ATTR_LONGITUDE) is None + assert state.attributes.get(ATTR_SOURCE) is None + assert state.attributes.get(ATTR_USER_ID) == user_id + + +async def test_valid_invalid_user_ids(hass, hass_admin_user): + """Test a person with valid user id and a person with invalid user id .""" + user_id = hass_admin_user.id + config = {DOMAIN: [ + {'id': '1234', 'name': 'test valid user', 'user_id': user_id}, + {'id': '5678', 'name': 'test bad user', 'user_id': 'bad_user_id'}]} + assert await async_setup_component(hass, DOMAIN, config) + + state = hass.states.get('person.test_valid_user') + assert state.state == STATE_UNKNOWN + assert state.attributes.get(ATTR_ID) == '1234' + assert state.attributes.get(ATTR_LATITUDE) is None + assert state.attributes.get(ATTR_LONGITUDE) is None + assert state.attributes.get(ATTR_SOURCE) is None + assert state.attributes.get(ATTR_USER_ID) == user_id + state = hass.states.get('person.test_bad_user') + assert state is None + + +async def test_setup_tracker(hass, hass_admin_user): + """Test set up person with one device tracker.""" + user_id = hass_admin_user.id + config = {DOMAIN: { + 'id': '1234', 'name': 'tracked person', 'user_id': user_id, + 'device_trackers': DEVICE_TRACKER}} + assert await async_setup_component(hass, DOMAIN, config) + + state = hass.states.get('person.tracked_person') + assert state.state == STATE_UNKNOWN + assert state.attributes.get(ATTR_ID) == '1234' + assert state.attributes.get(ATTR_LATITUDE) is None + assert state.attributes.get(ATTR_LONGITUDE) is None + assert state.attributes.get(ATTR_SOURCE) is None + assert state.attributes.get(ATTR_USER_ID) == user_id + + hass.states.async_set(DEVICE_TRACKER, 'home') + await hass.async_block_till_done() + + state = hass.states.get('person.tracked_person') + assert state.state == STATE_UNKNOWN + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + state = hass.states.get('person.tracked_person') + assert state.state == 'home' + assert state.attributes.get(ATTR_ID) == '1234' + assert state.attributes.get(ATTR_LATITUDE) is None + assert state.attributes.get(ATTR_LONGITUDE) is None + assert state.attributes.get(ATTR_SOURCE) == DEVICE_TRACKER + assert state.attributes.get(ATTR_USER_ID) == user_id + + hass.states.async_set( + DEVICE_TRACKER, 'not_home', + {ATTR_LATITUDE: 10.123456, ATTR_LONGITUDE: 11.123456}) + await hass.async_block_till_done() + + state = hass.states.get('person.tracked_person') + assert state.state == 'not_home' + assert state.attributes.get(ATTR_ID) == '1234' + assert state.attributes.get(ATTR_LATITUDE) == 10.12346 + assert state.attributes.get(ATTR_LONGITUDE) == 11.12346 + assert state.attributes.get(ATTR_SOURCE) == DEVICE_TRACKER + assert state.attributes.get(ATTR_USER_ID) == user_id + + +async def test_setup_two_trackers(hass, hass_admin_user): + """Test set up person with two device trackers.""" + user_id = hass_admin_user.id + config = {DOMAIN: { + 'id': '1234', 'name': 'tracked person', 'user_id': user_id, + 'device_trackers': [DEVICE_TRACKER, DEVICE_TRACKER_2]}} + assert await async_setup_component(hass, DOMAIN, config) + + state = hass.states.get('person.tracked_person') + assert state.state == STATE_UNKNOWN + assert state.attributes.get(ATTR_ID) == '1234' + assert state.attributes.get(ATTR_LATITUDE) is None + assert state.attributes.get(ATTR_LONGITUDE) is None + assert state.attributes.get(ATTR_SOURCE) is None + assert state.attributes.get(ATTR_USER_ID) == user_id + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + hass.states.async_set(DEVICE_TRACKER, 'home') + await hass.async_block_till_done() + + state = hass.states.get('person.tracked_person') + assert state.state == 'home' + assert state.attributes.get(ATTR_ID) == '1234' + assert state.attributes.get(ATTR_LATITUDE) is None + assert state.attributes.get(ATTR_LONGITUDE) is None + assert state.attributes.get(ATTR_SOURCE) == DEVICE_TRACKER + assert state.attributes.get(ATTR_USER_ID) == user_id + + hass.states.async_set( + DEVICE_TRACKER_2, 'not_home', + {ATTR_LATITUDE: 12.123456, ATTR_LONGITUDE: 13.123456}) + await hass.async_block_till_done() + + state = hass.states.get('person.tracked_person') + assert state.state == 'not_home' + assert state.attributes.get(ATTR_ID) == '1234' + assert state.attributes.get(ATTR_LATITUDE) == 12.12346 + assert state.attributes.get(ATTR_LONGITUDE) == 13.12346 + assert state.attributes.get(ATTR_SOURCE) == DEVICE_TRACKER_2 + assert state.attributes.get(ATTR_USER_ID) == user_id + + +async def test_ignore_unavailable_states(hass, hass_admin_user): + """Test set up person with two device trackers, one unavailable.""" + user_id = hass_admin_user.id + config = {DOMAIN: { + 'id': '1234', 'name': 'tracked person', 'user_id': user_id, + 'device_trackers': [DEVICE_TRACKER, DEVICE_TRACKER_2]}} + assert await async_setup_component(hass, DOMAIN, config) + + state = hass.states.get('person.tracked_person') + assert state.state == STATE_UNKNOWN + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + hass.states.async_set(DEVICE_TRACKER, 'home') + await hass.async_block_till_done() + hass.states.async_set(DEVICE_TRACKER, 'unavailable') + await hass.async_block_till_done() + + # Unknown, as only 1 device tracker has a state, but we ignore that one + state = hass.states.get('person.tracked_person') + assert state.state == STATE_UNKNOWN + + hass.states.async_set(DEVICE_TRACKER_2, 'not_home') + await hass.async_block_till_done() + + # Take state of tracker 2 + state = hass.states.get('person.tracked_person') + assert state.state == 'not_home' + + # state 1 is newer but ignored, keep tracker 2 state + hass.states.async_set(DEVICE_TRACKER, 'unknown') + await hass.async_block_till_done() + + state = hass.states.get('person.tracked_person') + assert state.state == 'not_home' + + +async def test_restore_home_state(hass, hass_admin_user): + """Test that the state is restored for a person on startup.""" + user_id = hass_admin_user.id + attrs = { + ATTR_ID: '1234', ATTR_LATITUDE: 10.12346, ATTR_LONGITUDE: 11.12346, + ATTR_SOURCE: DEVICE_TRACKER, ATTR_USER_ID: user_id} + state = State('person.tracked_person', 'home', attrs) + mock_restore_cache(hass, (state, )) + hass.state = CoreState.starting + mock_component(hass, 'recorder') + config = {DOMAIN: { + 'id': '1234', 'name': 'tracked person', 'user_id': user_id, + 'device_trackers': DEVICE_TRACKER}} + assert await async_setup_component(hass, DOMAIN, config) + + state = hass.states.get('person.tracked_person') + assert state.state == 'home' + assert state.attributes.get(ATTR_ID) == '1234' + assert state.attributes.get(ATTR_LATITUDE) == 10.12346 + assert state.attributes.get(ATTR_LONGITUDE) == 11.12346 + # When restoring state the entity_id of the person will be used as source. + assert state.attributes.get(ATTR_SOURCE) == 'person.tracked_person' + assert state.attributes.get(ATTR_USER_ID) == user_id + + +async def test_duplicate_ids(hass, hass_admin_user): + """Test we don't allow duplicate IDs.""" + config = {DOMAIN: [ + {'id': '1234', 'name': 'test user 1'}, + {'id': '1234', 'name': 'test user 2'}]} + assert await async_setup_component(hass, DOMAIN, config) + + assert len(hass.states.async_entity_ids('person')) == 1 + assert hass.states.get('person.test_user_1') is not None + assert hass.states.get('person.test_user_2') is None + + +async def test_load_person_storage(hass, hass_admin_user, storage_setup): + """Test set up person from storage.""" + state = hass.states.get('person.tracked_person') + assert state.state == STATE_UNKNOWN + assert state.attributes.get(ATTR_ID) == '1234' + assert state.attributes.get(ATTR_LATITUDE) is None + assert state.attributes.get(ATTR_LONGITUDE) is None + assert state.attributes.get(ATTR_SOURCE) is None + assert state.attributes.get(ATTR_USER_ID) == hass_admin_user.id + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + hass.states.async_set(DEVICE_TRACKER, 'home') + await hass.async_block_till_done() + + state = hass.states.get('person.tracked_person') + assert state.state == 'home' + assert state.attributes.get(ATTR_ID) == '1234' + assert state.attributes.get(ATTR_LATITUDE) is None + assert state.attributes.get(ATTR_LONGITUDE) is None + assert state.attributes.get(ATTR_SOURCE) == DEVICE_TRACKER + assert state.attributes.get(ATTR_USER_ID) == hass_admin_user.id + + +async def test_load_person_storage_two_nonlinked(hass, hass_storage): + """Test loading two users with both not having a user linked.""" + hass_storage[DOMAIN] = { + 'key': DOMAIN, + 'version': 1, + 'data': { + 'persons': [ + { + 'id': '1234', + 'name': 'tracked person 1', + 'user_id': None, + 'device_trackers': [] + }, + { + 'id': '5678', + 'name': 'tracked person 2', + 'user_id': None, + 'device_trackers': [] + }, + ] + } + } + await async_setup_component(hass, DOMAIN, {}) + + assert len(hass.states.async_entity_ids('person')) == 2 + assert hass.states.get('person.tracked_person_1') is not None + assert hass.states.get('person.tracked_person_2') is not None + + +async def test_ws_list(hass, hass_ws_client, storage_setup): + """Test listing via WS.""" + manager = hass.data[DOMAIN] + + client = await hass_ws_client(hass) + + resp = await client.send_json({ + 'id': 6, + 'type': 'person/list', + }) + resp = await client.receive_json() + assert resp['success'] + assert resp['result']['storage'] == manager.storage_persons + assert len(resp['result']['storage']) == 1 + assert len(resp['result']['config']) == 0 + + +async def test_ws_create(hass, hass_ws_client, storage_setup, + hass_read_only_user): + """Test creating via WS.""" + manager = hass.data[DOMAIN] + + client = await hass_ws_client(hass) + + resp = await client.send_json({ + 'id': 6, + 'type': 'person/create', + 'name': 'Hello', + 'device_trackers': [DEVICE_TRACKER], + 'user_id': hass_read_only_user.id, + }) + resp = await client.receive_json() + + persons = manager.storage_persons + assert len(persons) == 2 + + assert resp['success'] + assert resp['result'] == persons[1] + + +async def test_ws_create_requires_admin(hass, hass_ws_client, storage_setup, + hass_admin_user, hass_read_only_user): + """Test creating via WS requires admin.""" + hass_admin_user.groups = [] + manager = hass.data[DOMAIN] + + client = await hass_ws_client(hass) + + resp = await client.send_json({ + 'id': 6, + 'type': 'person/create', + 'name': 'Hello', + 'device_trackers': [DEVICE_TRACKER], + 'user_id': hass_read_only_user.id, + }) + resp = await client.receive_json() + + persons = manager.storage_persons + assert len(persons) == 1 + + assert not resp['success'] + + +async def test_ws_update(hass, hass_ws_client, storage_setup): + """Test updating via WS.""" + manager = hass.data[DOMAIN] + + client = await hass_ws_client(hass) + persons = manager.storage_persons + + resp = await client.send_json({ + 'id': 6, + 'type': 'person/update', + 'person_id': persons[0]['id'], + 'name': 'Updated Name', + 'device_trackers': [DEVICE_TRACKER_2], + 'user_id': None, + }) + resp = await client.receive_json() + + persons = manager.storage_persons + assert len(persons) == 1 + + assert resp['success'] + assert resp['result'] == persons[0] + assert persons[0]['name'] == 'Updated Name' + assert persons[0]['name'] == 'Updated Name' + assert persons[0]['device_trackers'] == [DEVICE_TRACKER_2] + assert persons[0]['user_id'] is None + + state = hass.states.get('person.tracked_person') + assert state.name == 'Updated Name' + + +async def test_ws_update_require_admin(hass, hass_ws_client, storage_setup, + hass_admin_user): + """Test updating via WS requires admin.""" + hass_admin_user.groups = [] + manager = hass.data[DOMAIN] + + client = await hass_ws_client(hass) + original = dict(manager.storage_persons[0]) + + resp = await client.send_json({ + 'id': 6, + 'type': 'person/update', + 'person_id': original['id'], + 'name': 'Updated Name', + 'device_trackers': [DEVICE_TRACKER_2], + 'user_id': None, + }) + resp = await client.receive_json() + assert not resp['success'] + + not_updated = dict(manager.storage_persons[0]) + assert original == not_updated + + +async def test_ws_delete(hass, hass_ws_client, storage_setup): + """Test deleting via WS.""" + manager = hass.data[DOMAIN] + + client = await hass_ws_client(hass) + persons = manager.storage_persons + + resp = await client.send_json({ + 'id': 6, + 'type': 'person/delete', + 'person_id': persons[0]['id'], + }) + resp = await client.receive_json() + + persons = manager.storage_persons + assert len(persons) == 0 + + assert resp['success'] + assert len(hass.states.async_entity_ids('person')) == 0 + ent_reg = await hass.helpers.entity_registry.async_get_registry() + assert not ent_reg.async_is_registered('person.tracked_person') + + +async def test_ws_delete_require_admin(hass, hass_ws_client, storage_setup, + hass_admin_user): + """Test deleting via WS requires admin.""" + hass_admin_user.groups = [] + manager = hass.data[DOMAIN] + + client = await hass_ws_client(hass) + + resp = await client.send_json({ + 'id': 6, + 'type': 'person/delete', + 'person_id': manager.storage_persons[0]['id'], + 'name': 'Updated Name', + 'device_trackers': [DEVICE_TRACKER_2], + 'user_id': None, + }) + resp = await client.receive_json() + assert not resp['success'] + + persons = manager.storage_persons + assert len(persons) == 1 + + +async def test_create_invalid_user_id(hass): + """Test we do not allow invalid user ID during creation.""" + manager = PersonManager(hass, Mock(), []) + await manager.async_initialize() + with pytest.raises(ValueError): + await manager.async_create_person( + name='Hello', + user_id='non-existing' + ) + + +async def test_create_duplicate_user_id(hass, hass_admin_user): + """Test we do not allow duplicate user ID during creation.""" + manager = PersonManager( + hass, Mock(async_add_entities=mock_coro_func()), [] + ) + await manager.async_initialize() + await manager.async_create_person( + name='Hello', + user_id=hass_admin_user.id + ) + + with pytest.raises(ValueError): + await manager.async_create_person( + name='Hello', + user_id=hass_admin_user.id + ) + + +async def test_update_double_user_id(hass, hass_admin_user): + """Test we do not allow double user ID during update.""" + manager = PersonManager( + hass, Mock(async_add_entities=mock_coro_func()), [] + ) + await manager.async_initialize() + await manager.async_create_person( + name='Hello', + user_id=hass_admin_user.id + ) + person = await manager.async_create_person( + name='Hello', + ) + + with pytest.raises(ValueError): + await manager.async_update_person( + person_id=person['id'], + user_id=hass_admin_user.id + ) + + +async def test_update_invalid_user_id(hass): + """Test updating to invalid user ID.""" + manager = PersonManager( + hass, Mock(async_add_entities=mock_coro_func()), [] + ) + await manager.async_initialize() + person = await manager.async_create_person( + name='Hello', + ) + + with pytest.raises(ValueError): + await manager.async_update_person( + person_id=person['id'], + user_id='non-existing' + ) + + +async def test_update_person_when_user_removed(hass, hass_read_only_user): + """Update person when user is removed.""" + manager = PersonManager( + hass, Mock(async_add_entities=mock_coro_func()), [] + ) + await manager.async_initialize() + person = await manager.async_create_person( + name='Hello', + user_id=hass_read_only_user.id + ) + + await hass.auth.async_remove_user(hass_read_only_user) + await hass.async_block_till_done() + assert person['user_id'] is None diff --git a/tests/components/pilight/__init__.py b/tests/components/pilight/__init__.py new file mode 100644 index 00000000000..02827305936 --- /dev/null +++ b/tests/components/pilight/__init__.py @@ -0,0 +1 @@ +"""Tests for the pilight component.""" diff --git a/tests/components/test_pilight.py b/tests/components/pilight/test_init.py similarity index 100% rename from tests/components/test_pilight.py rename to tests/components/pilight/test_init.py diff --git a/tests/components/plant/__init__.py b/tests/components/plant/__init__.py new file mode 100644 index 00000000000..43a00130db9 --- /dev/null +++ b/tests/components/plant/__init__.py @@ -0,0 +1 @@ +"""Tests for the plant component.""" diff --git a/tests/components/test_plant.py b/tests/components/plant/test_init.py similarity index 100% rename from tests/components/test_plant.py rename to tests/components/plant/test_init.py diff --git a/tests/components/prometheus/__init__.py b/tests/components/prometheus/__init__.py new file mode 100644 index 00000000000..d60de3cf49c --- /dev/null +++ b/tests/components/prometheus/__init__.py @@ -0,0 +1 @@ +"""Tests for the prometheus component.""" diff --git a/tests/components/test_prometheus.py b/tests/components/prometheus/test_init.py similarity index 100% rename from tests/components/test_prometheus.py rename to tests/components/prometheus/test_init.py diff --git a/tests/components/proximity/__init__.py b/tests/components/proximity/__init__.py new file mode 100644 index 00000000000..659d609edb6 --- /dev/null +++ b/tests/components/proximity/__init__.py @@ -0,0 +1 @@ +"""Tests for the proximity component.""" diff --git a/tests/components/test_proximity.py b/tests/components/proximity/test_init.py similarity index 100% rename from tests/components/test_proximity.py rename to tests/components/proximity/test_init.py diff --git a/tests/components/python_script/__init__.py b/tests/components/python_script/__init__.py new file mode 100644 index 00000000000..893f5a7eccd --- /dev/null +++ b/tests/components/python_script/__init__.py @@ -0,0 +1 @@ +"""Tests for the python_script component.""" diff --git a/tests/components/test_python_script.py b/tests/components/python_script/test_init.py similarity index 100% rename from tests/components/test_python_script.py rename to tests/components/python_script/test_init.py diff --git a/tests/components/qwikswitch/__init__.py b/tests/components/qwikswitch/__init__.py new file mode 100644 index 00000000000..e0617b4163b --- /dev/null +++ b/tests/components/qwikswitch/__init__.py @@ -0,0 +1 @@ +"""Tests for the qwikswitch component.""" diff --git a/tests/components/test_qwikswitch.py b/tests/components/qwikswitch/test_init.py similarity index 100% rename from tests/components/test_qwikswitch.py rename to tests/components/qwikswitch/test_init.py diff --git a/tests/components/remember_the_milk/__init__.py b/tests/components/remember_the_milk/__init__.py new file mode 100644 index 00000000000..c5cc359ab76 --- /dev/null +++ b/tests/components/remember_the_milk/__init__.py @@ -0,0 +1 @@ +"""Tests for the remember_the_milk component.""" diff --git a/tests/components/test_remember_the_milk.py b/tests/components/remember_the_milk/test_init.py similarity index 100% rename from tests/components/test_remember_the_milk.py rename to tests/components/remember_the_milk/test_init.py diff --git a/tests/components/rest_command/__init__.py b/tests/components/rest_command/__init__.py new file mode 100644 index 00000000000..7fbc4588ccb --- /dev/null +++ b/tests/components/rest_command/__init__.py @@ -0,0 +1 @@ +"""Tests for the rest_command component.""" diff --git a/tests/components/test_rest_command.py b/tests/components/rest_command/test_init.py similarity index 100% rename from tests/components/test_rest_command.py rename to tests/components/rest_command/test_init.py diff --git a/tests/components/rflink/__init__.py b/tests/components/rflink/__init__.py new file mode 100644 index 00000000000..fac6bf58dd8 --- /dev/null +++ b/tests/components/rflink/__init__.py @@ -0,0 +1 @@ +"""Tests for the rflink component.""" diff --git a/tests/components/test_rflink.py b/tests/components/rflink/test_init.py similarity index 100% rename from tests/components/test_rflink.py rename to tests/components/rflink/test_init.py diff --git a/tests/components/rfxtrx/__init__.py b/tests/components/rfxtrx/__init__.py new file mode 100644 index 00000000000..81b2db8f4df --- /dev/null +++ b/tests/components/rfxtrx/__init__.py @@ -0,0 +1 @@ +"""Tests for the rfxtrx component.""" diff --git a/tests/components/test_rfxtrx.py b/tests/components/rfxtrx/test_init.py similarity index 100% rename from tests/components/test_rfxtrx.py rename to tests/components/rfxtrx/test_init.py diff --git a/tests/components/ring/__init__.py b/tests/components/ring/__init__.py new file mode 100644 index 00000000000..b159d356d5b --- /dev/null +++ b/tests/components/ring/__init__.py @@ -0,0 +1 @@ +"""Tests for the ring component.""" diff --git a/tests/components/test_ring.py b/tests/components/ring/test_init.py similarity index 100% rename from tests/components/test_ring.py rename to tests/components/ring/test_init.py diff --git a/tests/components/rss_feed_template/__init__.py b/tests/components/rss_feed_template/__init__.py new file mode 100644 index 00000000000..4200aea1e32 --- /dev/null +++ b/tests/components/rss_feed_template/__init__.py @@ -0,0 +1 @@ +"""Tests for the rss_feed_template component.""" diff --git a/tests/components/test_rss_feed_template.py b/tests/components/rss_feed_template/test_init.py similarity index 100% rename from tests/components/test_rss_feed_template.py rename to tests/components/rss_feed_template/test_init.py diff --git a/tests/components/script/__init__.py b/tests/components/script/__init__.py new file mode 100644 index 00000000000..67b9b4e3670 --- /dev/null +++ b/tests/components/script/__init__.py @@ -0,0 +1 @@ +"""Tests for the script component.""" diff --git a/tests/components/test_script.py b/tests/components/script/test_init.py similarity index 100% rename from tests/components/test_script.py rename to tests/components/script/test_init.py diff --git a/tests/components/sensor/test_canary.py b/tests/components/sensor/test_canary.py index 7908e22e579..dde8f4e0f94 100644 --- a/tests/components/sensor/test_canary.py +++ b/tests/components/sensor/test_canary.py @@ -9,7 +9,7 @@ from homeassistant.components.sensor.canary import CanarySensor, \ SENSOR_TYPES, ATTR_AIR_QUALITY, STATE_AIR_QUALITY_NORMAL, \ STATE_AIR_QUALITY_ABNORMAL, STATE_AIR_QUALITY_VERY_ABNORMAL from tests.common import (get_test_home_assistant) -from tests.components.test_canary import mock_device, mock_location +from tests.components.canary.test_init import mock_device, mock_location VALID_CONFIG = { "canary": { diff --git a/tests/components/sensor/test_darksky.py b/tests/components/sensor/test_darksky.py index 33a13f013de..58ce932020a 100644 --- a/tests/components/sensor/test_darksky.py +++ b/tests/components/sensor/test_darksky.py @@ -21,7 +21,7 @@ VALID_CONFIG_MINIMAL = { 'api_key': 'foo', 'forecast': [1, 2], 'monitored_conditions': ['summary', 'icon', 'temperature_high'], - 'update_interval': timedelta(seconds=120), + 'scan_interval': timedelta(seconds=120), } } @@ -31,7 +31,7 @@ INVALID_CONFIG_MINIMAL = { 'api_key': 'foo', 'forecast': [1, 2], 'monitored_conditions': ['sumary', 'iocn', 'temperature_high'], - 'update_interval': timedelta(seconds=120), + 'scan_interval': timedelta(seconds=120), } } @@ -45,7 +45,7 @@ VALID_CONFIG_LANG_DE = { 'monitored_conditions': ['summary', 'icon', 'temperature_high', 'minutely_summary', 'hourly_summary', 'daily_summary', 'humidity', ], - 'update_interval': timedelta(seconds=120), + 'scan_interval': timedelta(seconds=120), } } @@ -56,7 +56,7 @@ INVALID_CONFIG_LANG = { 'forecast': [1, 2], 'language': 'yz', 'monitored_conditions': ['summary', 'icon', 'temperature_high'], - 'update_interval': timedelta(seconds=120), + 'scan_interval': timedelta(seconds=120), } } @@ -138,8 +138,11 @@ class TestDarkSkySetup(unittest.TestCase): msg = '400 Client Error: Bad Request for url: {}'.format(url) mock_get_forecast.side_effect = HTTPError(msg,) - response = darksky.setup_platform(self.hass, VALID_CONFIG_MINIMAL, - MagicMock()) + response = darksky.setup_platform( + self.hass, + VALID_CONFIG_MINIMAL['sensor'], + MagicMock() + ) assert not response @requests_mock.Mocker() diff --git a/tests/components/sensor/test_dsmr.py b/tests/components/sensor/test_dsmr.py index 673cadd6208..dbf1e1fe7dd 100644 --- a/tests/components/sensor/test_dsmr.py +++ b/tests/components/sensor/test_dsmr.py @@ -82,7 +82,7 @@ def test_default_setup(hass, mock_connection_factory): # ensure entities have new state value after incoming telegram power_consumption = hass.states.get('sensor.power_consumption') assert power_consumption.state == '0.0' - assert power_consumption.attributes.get('unit_of_measurement') is 'kWh' + assert power_consumption.attributes.get('unit_of_measurement') == 'kWh' # tariff should be translated in human readable and have no unit power_tariff = hass.states.get('sensor.power_tariff') @@ -95,7 +95,9 @@ def test_derivative(): """Test calculation of derivative value.""" from dsmr_parser.objects import MBusObject - entity = DerivativeDSMREntity('test', '1.0.0') + config = {'platform': 'dsmr'} + + entity = DerivativeDSMREntity('test', '1.0.0', config) yield from entity.async_update() assert entity.state is None, 'initial state not unknown' diff --git a/tests/components/sensor/test_filter.py b/tests/components/sensor/test_filter.py index 29718314ef4..b43d38da5e8 100644 --- a/tests/components/sensor/test_filter.py +++ b/tests/components/sensor/test_filter.py @@ -59,7 +59,6 @@ class TestFilterSensor(unittest.TestCase): 'platform': 'filter', 'name': 'test', 'entity_id': 'sensor.test_monitored', - 'history_period': '00:05', 'filters': [{ 'filter': 'outlier', 'window_size': 10, diff --git a/tests/components/sensor/test_history_stats.py b/tests/components/sensor/test_history_stats.py index 28d01de4b34..eda0034c922 100644 --- a/tests/components/sensor/test_history_stats.py +++ b/tests/components/sensor/test_history_stats.py @@ -54,7 +54,6 @@ class TestHistoryStatsSensor(unittest.TestCase): """Test the conversion from templates to period.""" now = datetime(2019, 1, 1, 23, 30, 0, tzinfo=pytz.utc) with patch.dict(template.ENV.globals, {'now': lambda: now}): - print(dt_util.now()) today = Template('{{ now().replace(hour=0).replace(minute=0)' '.replace(second=0) }}', self.hass) duration = timedelta(hours=2, minutes=1) diff --git a/tests/components/sensor/test_integration.py b/tests/components/sensor/test_integration.py index bb4a02c042b..7f02d59f591 100644 --- a/tests/components/sensor/test_integration.py +++ b/tests/components/sensor/test_integration.py @@ -39,6 +39,110 @@ async def test_state(hass): assert state.attributes.get('unit_of_measurement') == 'kWh' +async def test_trapezoidal(hass): + """Test integration sensor state.""" + config = { + 'sensor': { + 'platform': 'integration', + 'name': 'integration', + 'source': 'sensor.power', + 'unit': 'kWh', + 'round': 2, + } + } + + assert await async_setup_component(hass, 'sensor', config) + + entity_id = config['sensor']['source'] + hass.states.async_set(entity_id, 0, {}) + await hass.async_block_till_done() + + # Testing a power sensor with non-monotonic intervals and values + for time, value in [(20, 10), (30, 30), (40, 5), (50, 0)]: + now = dt_util.utcnow() + timedelta(minutes=time) + with patch('homeassistant.util.dt.utcnow', + return_value=now): + hass.states.async_set(entity_id, value, {}, force_update=True) + await hass.async_block_till_done() + + state = hass.states.get('sensor.integration') + assert state is not None + + assert round(float(state.state), config['sensor']['round']) == 8.33 + + assert state.attributes.get('unit_of_measurement') == 'kWh' + + +async def test_left(hass): + """Test integration sensor state with left reimann method.""" + config = { + 'sensor': { + 'platform': 'integration', + 'name': 'integration', + 'method': 'left', + 'source': 'sensor.power', + 'unit': 'kWh', + 'round': 2, + } + } + + assert await async_setup_component(hass, 'sensor', config) + + entity_id = config['sensor']['source'] + hass.states.async_set(entity_id, 0, {}) + await hass.async_block_till_done() + + # Testing a power sensor with non-monotonic intervals and values + for time, value in [(20, 10), (30, 30), (40, 5), (50, 0)]: + now = dt_util.utcnow() + timedelta(minutes=time) + with patch('homeassistant.util.dt.utcnow', + return_value=now): + hass.states.async_set(entity_id, value, {}, force_update=True) + await hass.async_block_till_done() + + state = hass.states.get('sensor.integration') + assert state is not None + + assert round(float(state.state), config['sensor']['round']) == 7.5 + + assert state.attributes.get('unit_of_measurement') == 'kWh' + + +async def test_right(hass): + """Test integration sensor state with left reimann method.""" + config = { + 'sensor': { + 'platform': 'integration', + 'name': 'integration', + 'method': 'right', + 'source': 'sensor.power', + 'unit': 'kWh', + 'round': 2, + } + } + + assert await async_setup_component(hass, 'sensor', config) + + entity_id = config['sensor']['source'] + hass.states.async_set(entity_id, 0, {}) + await hass.async_block_till_done() + + # Testing a power sensor with non-monotonic intervals and values + for time, value in [(20, 10), (30, 30), (40, 5), (50, 0)]: + now = dt_util.utcnow() + timedelta(minutes=time) + with patch('homeassistant.util.dt.utcnow', + return_value=now): + hass.states.async_set(entity_id, value, {}, force_update=True) + await hass.async_block_till_done() + + state = hass.states.get('sensor.integration') + assert state is not None + + assert round(float(state.state), config['sensor']['round']) == 9.17 + + assert state.attributes.get('unit_of_measurement') == 'kWh' + + async def test_prefix(hass): """Test integration sensor state using a power source.""" config = { diff --git a/tests/components/sensor/test_jewish_calendar.py b/tests/components/sensor/test_jewish_calendar.py index 639364164e0..7243874a41d 100644 --- a/tests/components/sensor/test_jewish_calendar.py +++ b/tests/components/sensor/test_jewish_calendar.py @@ -158,6 +158,14 @@ class TestJewishCalenderSensor(): 'weekly_portion': 'Ki Tavo', 'hebrew_weekly_portion': 'כי תבוא'}, havdalah_offset=50), + make_nyc_test_params( + dt(2018, 9, 1, 20, 0), + {'upcoming_shabbat_candle_lighting': dt(2018, 8, 31, 19, 15), + 'upcoming_shabbat_havdalah': dt(2018, 9, 1, 20, 14), + 'upcoming_candle_lighting': dt(2018, 8, 31, 19, 15), + 'upcoming_havdalah': dt(2018, 9, 1, 20, 14), + 'weekly_portion': 'Ki Tavo', + 'hebrew_weekly_portion': 'כי תבוא'}), make_nyc_test_params( dt(2018, 9, 1, 20, 21), {'upcoming_shabbat_candle_lighting': dt(2018, 9, 7, 19, 4), @@ -317,6 +325,7 @@ class TestJewishCalenderSensor(): shabbat_test_ids = [ "currently_first_shabbat", "currently_first_shabbat_with_havdalah_offset", + "currently_first_shabbat_bein_hashmashot_lagging_date", "after_first_shabbat", "friday_upcoming_shabbat", "upcoming_rosh_hashana", diff --git a/tests/components/sensor/test_mqtt_room.py b/tests/components/sensor/test_mqtt_room.py index 980655bac78..fe1fa318016 100644 --- a/tests/components/sensor/test_mqtt_room.py +++ b/tests/components/sensor/test_mqtt_room.py @@ -10,7 +10,7 @@ from homeassistant.components.mqtt import (CONF_STATE_TOPIC, CONF_QOS, from homeassistant.const import (CONF_NAME, CONF_PLATFORM) from homeassistant.util import dt -from tests.common import async_fire_mqtt_message +from tests.common import async_fire_mqtt_message, async_mock_mqtt_component DEVICE_ID = '123TESTMAC' NAME = 'test_device' @@ -64,8 +64,10 @@ async def assert_distance(hass, distance): assert state.attributes.get('distance') == distance -async def test_room_update(hass, mqtt_mock): +async def test_room_update(hass): """Test the updating between rooms.""" + await async_mock_mqtt_component(hass) + assert await async_setup_component(hass, sensor.DOMAIN, { sensor.DOMAIN: { CONF_PLATFORM: 'mqtt_room', diff --git a/tests/components/sensor/test_rflink.py b/tests/components/sensor/test_rflink.py index 8ab568905f9..4cf75857a9a 100644 --- a/tests/components/sensor/test_rflink.py +++ b/tests/components/sensor/test_rflink.py @@ -9,7 +9,7 @@ from homeassistant.components.rflink import ( CONF_RECONNECT_INTERVAL, TMP_ENTITY, DATA_ENTITY_LOOKUP, EVENT_KEY_COMMAND, EVENT_KEY_SENSOR) from homeassistant.const import STATE_UNKNOWN -from ..test_rflink import mock_rflink +from tests.components.rflink.test_init import mock_rflink DOMAIN = 'sensor' diff --git a/tests/components/sensor/test_ring.py b/tests/components/sensor/test_ring.py index 3844079172a..c54f22af8dc 100644 --- a/tests/components/sensor/test_ring.py +++ b/tests/components/sensor/test_ring.py @@ -6,7 +6,7 @@ import requests_mock from homeassistant.components.sensor import ring from homeassistant.components import ring as base_ring -from tests.components.test_ring import ATTRIBUTION, VALID_CONFIG +from tests.components.ring.test_init import ATTRIBUTION, VALID_CONFIG from tests.common import ( get_test_config_dir, get_test_home_assistant, load_fixture) diff --git a/tests/components/sensor/test_sleepiq.py b/tests/components/sensor/test_sleepiq.py index 96787473abf..c667a6a2cdf 100644 --- a/tests/components/sensor/test_sleepiq.py +++ b/tests/components/sensor/test_sleepiq.py @@ -7,7 +7,7 @@ import requests_mock from homeassistant.setup import setup_component from homeassistant.components.sensor import sleepiq -from tests.components.test_sleepiq import mock_responses +from tests.components.sleepiq.test_init import mock_responses from tests.common import get_test_home_assistant diff --git a/tests/components/sensor/test_vultr.py b/tests/components/sensor/test_vultr.py index 294657c22ec..333e4938ba4 100644 --- a/tests/components/sensor/test_vultr.py +++ b/tests/components/sensor/test_vultr.py @@ -13,7 +13,7 @@ from homeassistant.components.vultr import CONF_SUBSCRIPTION from homeassistant.const import ( CONF_NAME, CONF_MONITORED_CONDITIONS, CONF_PLATFORM) -from tests.components.test_vultr import VALID_CONFIG +from tests.components.vultr.test_init import VALID_CONFIG from tests.common import ( get_test_home_assistant, load_fixture) diff --git a/tests/components/shell_command/__init__.py b/tests/components/shell_command/__init__.py new file mode 100644 index 00000000000..5effcdb3cce --- /dev/null +++ b/tests/components/shell_command/__init__.py @@ -0,0 +1 @@ +"""Tests for the shell_command component.""" diff --git a/tests/components/test_shell_command.py b/tests/components/shell_command/test_init.py similarity index 100% rename from tests/components/test_shell_command.py rename to tests/components/shell_command/test_init.py diff --git a/tests/components/shopping_list/__init__.py b/tests/components/shopping_list/__init__.py new file mode 100644 index 00000000000..26be3c90736 --- /dev/null +++ b/tests/components/shopping_list/__init__.py @@ -0,0 +1 @@ +"""Tests for the shopping_list component.""" diff --git a/tests/components/test_shopping_list.py b/tests/components/shopping_list/test_init.py similarity index 100% rename from tests/components/test_shopping_list.py rename to tests/components/shopping_list/test_init.py diff --git a/tests/components/sleepiq/__init__.py b/tests/components/sleepiq/__init__.py new file mode 100644 index 00000000000..751f227a003 --- /dev/null +++ b/tests/components/sleepiq/__init__.py @@ -0,0 +1 @@ +"""Tests for the sleepiq component.""" diff --git a/tests/components/test_sleepiq.py b/tests/components/sleepiq/test_init.py similarity index 100% rename from tests/components/test_sleepiq.py rename to tests/components/sleepiq/test_init.py diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index 7358e05f346..ee892fb03b9 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -10,9 +10,10 @@ from pysmartthings.api import Api import pytest from homeassistant.components import webhook +from homeassistant.components.smartthings import DeviceBroker from homeassistant.components.smartthings.const import ( APP_NAME_PREFIX, CONF_APP_ID, CONF_INSTALLED_APP_ID, CONF_INSTANCE_ID, - CONF_LOCATION_ID, DOMAIN, SETTINGS_INSTANCE_ID, STORAGE_KEY, + CONF_LOCATION_ID, DATA_BROKERS, DOMAIN, SETTINGS_INSTANCE_ID, STORAGE_KEY, STORAGE_VERSION) from homeassistant.config_entries import ( CONN_CLASS_CLOUD_PUSH, SOURCE_USER, ConfigEntry) @@ -22,6 +23,23 @@ from homeassistant.setup import async_setup_component from tests.common import mock_coro +async def setup_platform(hass, platform: str, *devices): + """Set up the SmartThings platform and prerequisites.""" + hass.config.components.add(DOMAIN) + broker = DeviceBroker(hass, devices, '') + config_entry = ConfigEntry("1", DOMAIN, "Test", {}, + SOURCE_USER, CONN_CLASS_CLOUD_PUSH) + hass.data[DOMAIN] = { + DATA_BROKERS: { + config_entry.entry_id: broker + } + } + await hass.config_entries.async_forward_entry_setup( + config_entry, platform) + await hass.async_block_till_done() + return config_entry + + @pytest.fixture(autouse=True) async def setup_component(hass, config_file, hass_storage): """Load the SmartThing component.""" @@ -217,7 +235,8 @@ def config_entry_fixture(hass, installed_app, location): def device_factory_fixture(): """Fixture for creating mock devices.""" api = Mock(spec=Api) - api.post_device_command.return_value = mock_coro(return_value={}) + api.post_device_command.side_effect = \ + lambda *args, **kwargs: mock_coro(return_value={}) def _factory(label, capabilities, status: dict = None): device_data = { diff --git a/tests/components/smartthings/test_binary_sensor.py b/tests/components/smartthings/test_binary_sensor.py index 92d891c06d6..4b47537fa19 100644 --- a/tests/components/smartthings/test_binary_sensor.py +++ b/tests/components/smartthings/test_binary_sensor.py @@ -4,12 +4,12 @@ Test for the SmartThings binary_sensor platform. The only mocking required is of the underlying SmartThings API object so real HTTP calls are not initiated during testing. """ -from pysmartthings import Attribute, Capability +from pysmartthings import ATTRIBUTES, CAPABILITIES, Attribute, Capability from homeassistant.components.binary_sensor import DEVICE_CLASSES from homeassistant.components.smartthings import DeviceBroker, binary_sensor from homeassistant.components.smartthings.const import ( - DATA_BROKERS, DOMAIN, SIGNAL_SMARTTHINGS_UPDATE, SUPPORTED_CAPABILITIES) + DATA_BROKERS, DOMAIN, SIGNAL_SMARTTHINGS_UPDATE) from homeassistant.config_entries import ( CONN_CLASS_CLOUD_PUSH, SOURCE_USER, ConfigEntry) from homeassistant.const import ATTR_FRIENDLY_NAME @@ -35,14 +35,16 @@ async def _setup_platform(hass, *devices): async def test_mapping_integrity(): """Test ensures the map dicts have proper integrity.""" - # Ensure every CAPABILITY_TO_ATTRIB key is in SUPPORTED_CAPABILITIES + # Ensure every CAPABILITY_TO_ATTRIB key is in CAPABILITIES # Ensure every CAPABILITY_TO_ATTRIB value is in ATTRIB_TO_CLASS keys for capability, attrib in binary_sensor.CAPABILITY_TO_ATTRIB.items(): - assert capability in SUPPORTED_CAPABILITIES, capability + assert capability in CAPABILITIES, capability + assert attrib in ATTRIBUTES, attrib assert attrib in binary_sensor.ATTRIB_TO_CLASS.keys(), attrib # Ensure every ATTRIB_TO_CLASS value is in DEVICE_CLASSES - for device_class in binary_sensor.ATTRIB_TO_CLASS.values(): - assert device_class in DEVICE_CLASSES + for attrib, device_class in binary_sensor.ATTRIB_TO_CLASS.items(): + assert attrib in ATTRIBUTES, attrib + assert device_class in DEVICE_CLASSES, device_class async def test_async_setup_platform(): diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py new file mode 100644 index 00000000000..c5646fb400f --- /dev/null +++ b/tests/components/smartthings/test_climate.py @@ -0,0 +1,253 @@ +""" +Test for the SmartThings climate platform. + +The only mocking required is of the underlying SmartThings API object so +real HTTP calls are not initiated during testing. +""" +from pysmartthings import Attribute, Capability +from pysmartthings.device import Status +import pytest + +from homeassistant.components.climate import ( + ATTR_CURRENT_HUMIDITY, ATTR_CURRENT_TEMPERATURE, ATTR_FAN_LIST, + ATTR_FAN_MODE, ATTR_OPERATION_LIST, ATTR_OPERATION_MODE, + ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DOMAIN as CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, SERVICE_SET_OPERATION_MODE, SERVICE_SET_TEMPERATURE, + STATE_AUTO, STATE_COOL, STATE_ECO, STATE_HEAT, STATE_OFF, SUPPORT_FAN_MODE, + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) +from homeassistant.components.smartthings import climate +from homeassistant.components.smartthings.const import DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE) + +from .conftest import setup_platform + + +@pytest.fixture(name="legacy_thermostat") +def legacy_thermostat_fixture(device_factory): + """Fixture returns a legacy thermostat.""" + device = device_factory( + "Legacy Thermostat", + capabilities=[Capability.thermostat], + status={ + Attribute.cooling_setpoint: 74, + Attribute.heating_setpoint: 68, + Attribute.thermostat_fan_mode: 'auto', + Attribute.supported_thermostat_fan_modes: ['auto', 'on'], + Attribute.thermostat_mode: 'auto', + Attribute.supported_thermostat_modes: climate.MODE_TO_STATE.keys(), + Attribute.thermostat_operating_state: 'idle' + } + ) + device.status.attributes[Attribute.temperature] = Status(70, 'F', None) + return device + + +@pytest.fixture(name="basic_thermostat") +def basic_thermostat_fixture(device_factory): + """Fixture returns a basic thermostat.""" + device = device_factory( + "Basic Thermostat", + capabilities=[ + Capability.temperature_measurement, + Capability.thermostat_cooling_setpoint, + Capability.thermostat_heating_setpoint, + Capability.thermostat_mode], + status={ + Attribute.cooling_setpoint: 74, + Attribute.heating_setpoint: 68, + Attribute.thermostat_mode: 'off', + Attribute.supported_thermostat_modes: + ['off', 'auto', 'heat', 'cool'] + } + ) + device.status.attributes[Attribute.temperature] = Status(70, 'F', None) + return device + + +@pytest.fixture(name="thermostat") +def thermostat_fixture(device_factory): + """Fixture returns a fully-featured thermostat.""" + device = device_factory( + "Thermostat", + capabilities=[ + Capability.temperature_measurement, + Capability.relative_humidity_measurement, + Capability.thermostat_cooling_setpoint, + Capability.thermostat_heating_setpoint, + Capability.thermostat_mode, + Capability.thermostat_operating_state, + Capability.thermostat_fan_mode], + status={ + Attribute.cooling_setpoint: 74, + Attribute.heating_setpoint: 68, + Attribute.thermostat_fan_mode: 'on', + Attribute.supported_thermostat_fan_modes: ['auto', 'on'], + Attribute.thermostat_mode: 'heat', + Attribute.supported_thermostat_modes: + ['auto', 'heat', 'cool', 'off', 'eco'], + Attribute.thermostat_operating_state: 'fan only', + Attribute.humidity: 34 + } + ) + device.status.attributes[Attribute.temperature] = Status(70, 'F', None) + return device + + +async def test_async_setup_platform(): + """Test setup platform does nothing (it uses config entries).""" + await climate.async_setup_platform(None, None, None) + + +async def test_legacy_thermostat_entity_state(hass, legacy_thermostat): + """Tests the state attributes properly match the thermostat type.""" + await setup_platform(hass, CLIMATE_DOMAIN, legacy_thermostat) + state = hass.states.get('climate.legacy_thermostat') + assert state.state == STATE_AUTO + assert state.attributes[ATTR_SUPPORTED_FEATURES] == \ + SUPPORT_OPERATION_MODE | SUPPORT_FAN_MODE | \ + SUPPORT_TARGET_TEMPERATURE_HIGH | SUPPORT_TARGET_TEMPERATURE_LOW | \ + SUPPORT_TARGET_TEMPERATURE + assert state.attributes[climate.ATTR_OPERATION_STATE] == 'idle' + assert state.attributes[ATTR_OPERATION_LIST] == { + STATE_AUTO, STATE_COOL, STATE_ECO, STATE_HEAT, STATE_OFF} + assert state.attributes[ATTR_FAN_MODE] == 'auto' + assert state.attributes[ATTR_FAN_LIST] == ['auto', 'on'] + assert state.attributes[ATTR_TARGET_TEMP_LOW] == 20 # celsius + assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 23.3 # celsius + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 21.1 # celsius + + +async def test_basic_thermostat_entity_state(hass, basic_thermostat): + """Tests the state attributes properly match the thermostat type.""" + await setup_platform(hass, CLIMATE_DOMAIN, basic_thermostat) + state = hass.states.get('climate.basic_thermostat') + assert state.state == STATE_OFF + assert state.attributes[ATTR_SUPPORTED_FEATURES] == \ + SUPPORT_OPERATION_MODE | SUPPORT_TARGET_TEMPERATURE_HIGH | \ + SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_TARGET_TEMPERATURE + assert state.attributes[climate.ATTR_OPERATION_STATE] is None + assert state.attributes[ATTR_OPERATION_LIST] == { + STATE_OFF, STATE_AUTO, STATE_HEAT, STATE_COOL} + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 21.1 # celsius + + +async def test_thermostat_entity_state(hass, thermostat): + """Tests the state attributes properly match the thermostat type.""" + await setup_platform(hass, CLIMATE_DOMAIN, thermostat) + state = hass.states.get('climate.thermostat') + assert state.state == STATE_HEAT + assert state.attributes[ATTR_SUPPORTED_FEATURES] == \ + SUPPORT_OPERATION_MODE | SUPPORT_FAN_MODE | \ + SUPPORT_TARGET_TEMPERATURE_HIGH | SUPPORT_TARGET_TEMPERATURE_LOW | \ + SUPPORT_TARGET_TEMPERATURE + assert state.attributes[climate.ATTR_OPERATION_STATE] == 'fan only' + assert state.attributes[ATTR_OPERATION_LIST] == { + STATE_AUTO, STATE_HEAT, STATE_COOL, STATE_OFF, STATE_ECO} + assert state.attributes[ATTR_FAN_MODE] == 'on' + assert state.attributes[ATTR_FAN_LIST] == ['auto', 'on'] + assert state.attributes[ATTR_TEMPERATURE] == 20 # celsius + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 21.1 # celsius + assert state.attributes[ATTR_CURRENT_HUMIDITY] == 34 + + +async def test_set_fan_mode(hass, thermostat): + """Test the fan mode is set successfully.""" + await setup_platform(hass, CLIMATE_DOMAIN, thermostat) + await hass.services.async_call( + CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, { + ATTR_ENTITY_ID: 'climate.thermostat', + ATTR_FAN_MODE: 'auto'}, + blocking=True) + state = hass.states.get('climate.thermostat') + assert state.attributes[ATTR_FAN_MODE] == 'auto' + + +async def test_set_operation_mode(hass, thermostat): + """Test the operation mode is set successfully.""" + await setup_platform(hass, CLIMATE_DOMAIN, thermostat) + await hass.services.async_call( + CLIMATE_DOMAIN, SERVICE_SET_OPERATION_MODE, { + ATTR_ENTITY_ID: 'climate.thermostat', + ATTR_OPERATION_MODE: STATE_ECO}, + blocking=True) + state = hass.states.get('climate.thermostat') + assert state.state == STATE_ECO + + +async def test_set_temperature_heat_mode(hass, thermostat): + """Test the temperature is set successfully when in heat mode.""" + thermostat.status.thermostat_mode = 'heat' + await setup_platform(hass, CLIMATE_DOMAIN, thermostat) + await hass.services.async_call( + CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, { + ATTR_ENTITY_ID: 'climate.thermostat', + ATTR_TEMPERATURE: 21}, + blocking=True) + state = hass.states.get('climate.thermostat') + assert state.attributes[ATTR_OPERATION_MODE] == STATE_HEAT + assert state.attributes[ATTR_TEMPERATURE] == 21 + assert thermostat.status.heating_setpoint == 69.8 + + +async def test_set_temperature_cool_mode(hass, thermostat): + """Test the temperature is set successfully when in cool mode.""" + thermostat.status.thermostat_mode = 'cool' + await setup_platform(hass, CLIMATE_DOMAIN, thermostat) + await hass.services.async_call( + CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, { + ATTR_ENTITY_ID: 'climate.thermostat', + ATTR_TEMPERATURE: 21}, + blocking=True) + state = hass.states.get('climate.thermostat') + assert state.attributes[ATTR_TEMPERATURE] == 21 + + +async def test_set_temperature(hass, thermostat): + """Test the temperature is set successfully.""" + thermostat.status.thermostat_mode = 'auto' + await setup_platform(hass, CLIMATE_DOMAIN, thermostat) + await hass.services.async_call( + CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, { + ATTR_ENTITY_ID: 'climate.thermostat', + ATTR_TARGET_TEMP_HIGH: 25.5, + ATTR_TARGET_TEMP_LOW: 22.2}, + blocking=True) + state = hass.states.get('climate.thermostat') + assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 25.5 + assert state.attributes[ATTR_TARGET_TEMP_LOW] == 22.2 + + +async def test_set_temperature_with_mode(hass, thermostat): + """Test the temperature and mode is set successfully.""" + await setup_platform(hass, CLIMATE_DOMAIN, thermostat) + await hass.services.async_call( + CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, { + ATTR_ENTITY_ID: 'climate.thermostat', + ATTR_TARGET_TEMP_HIGH: 25.5, + ATTR_TARGET_TEMP_LOW: 22.2, + ATTR_OPERATION_MODE: STATE_AUTO}, + blocking=True) + state = hass.states.get('climate.thermostat') + assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 25.5 + assert state.attributes[ATTR_TARGET_TEMP_LOW] == 22.2 + assert state.state == STATE_AUTO + + +async def test_entity_and_device_attributes(hass, thermostat): + """Test the attributes of the entries are correct.""" + await setup_platform(hass, CLIMATE_DOMAIN, thermostat) + entity_registry = await hass.helpers.entity_registry.async_get_registry() + device_registry = await hass.helpers.device_registry.async_get_registry() + + entry = entity_registry.async_get("climate.thermostat") + assert entry + assert entry.unique_id == thermostat.device_id + + entry = device_registry.async_get_device( + {(DOMAIN, thermostat.device_id)}, []) + assert entry + assert entry.name == thermostat.label + assert entry.model == thermostat.device_type_name + assert entry.manufacturer == 'Unavailable' diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py index 4d2a43a52c7..7d335703131 100644 --- a/tests/components/smartthings/test_config_flow.py +++ b/tests/components/smartthings/test_config_flow.py @@ -1,8 +1,9 @@ """Tests for the SmartThings config flow module.""" -from unittest.mock import patch +from unittest.mock import Mock, patch from uuid import uuid4 -from aiohttp.client_exceptions import ClientResponseError +from aiohttp import ClientResponseError +from pysmartthings import APIResponseError from homeassistant import data_entry_flow from homeassistant.components.smartthings.config_flow import ( @@ -103,13 +104,50 @@ async def test_token_forbidden(hass, smartthings_mock): assert result['errors'] == {'access_token': 'token_forbidden'} +async def test_webhook_error(hass, smartthings_mock): + """Test an error is when there's an error with the webhook endpoint.""" + flow = SmartThingsFlowHandler() + flow.hass = hass + + data = {'error': {}} + error = APIResponseError(None, None, data=data, status=422) + error.is_target_error = Mock(return_value=True) + + smartthings_mock.return_value.apps.return_value = mock_coro( + exception=error) + + result = await flow.async_step_user({'access_token': str(uuid4())}) + + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'user' + assert result['errors'] == {'base': 'webhook_error'} + + +async def test_api_error(hass, smartthings_mock): + """Test an error is shown when other API errors occur.""" + flow = SmartThingsFlowHandler() + flow.hass = hass + + data = {'error': {}} + error = APIResponseError(None, None, data=data, status=400) + + smartthings_mock.return_value.apps.return_value = mock_coro( + exception=error) + + result = await flow.async_step_user({'access_token': str(uuid4())}) + + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'user' + assert result['errors'] == {'base': 'app_setup_error'} + + async def test_unknown_api_error(hass, smartthings_mock): """Test an error is shown when there is an unknown API error.""" flow = SmartThingsFlowHandler() flow.hass = hass smartthings_mock.return_value.apps.return_value = mock_coro( - exception=ClientResponseError(None, None, status=500)) + exception=ClientResponseError(None, None, status=404)) result = await flow.async_step_user({'access_token': str(uuid4())}) diff --git a/tests/components/smartthings/test_fan.py b/tests/components/smartthings/test_fan.py index 99627e866d9..db8d9b512de 100644 --- a/tests/components/smartthings/test_fan.py +++ b/tests/components/smartthings/test_fan.py @@ -39,26 +39,6 @@ async def test_async_setup_platform(): await fan.async_setup_platform(None, None, None) -def test_is_fan(device_factory): - """Test fans are correctly identified.""" - non_fans = [ - device_factory('Unknown', ['Unknown']), - device_factory("Switch 1", [Capability.switch]), - device_factory("Non-Switchable Fan", [Capability.fan_speed]), - device_factory("Color Light", - [Capability.switch, Capability.switch_level, - Capability.color_control, - Capability.color_temperature]) - ] - fan_device = device_factory( - "Fan 1", [Capability.switch, Capability.switch_level, - Capability.fan_speed]) - - assert fan.is_fan(fan_device), fan_device.name - for device in non_fans: - assert not fan.is_fan(device), device.name - - async def test_entity_state(hass, device_factory): """Tests the state attributes properly match the fan types.""" device = device_factory( diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index 4aef42c1b6f..014cfe7da98 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -162,7 +162,7 @@ async def test_event_handler_dispatches_updated_devices( assert called for device in devices: - assert device.status.attributes['Updated'] == 'Value' + assert device.status.values['Updated'] == 'Value' async def test_event_handler_ignores_other_installed_app( diff --git a/tests/components/smartthings/test_light.py b/tests/components/smartthings/test_light.py index a4f1103f270..72bc5da9063 100644 --- a/tests/components/smartthings/test_light.py +++ b/tests/components/smartthings/test_light.py @@ -65,25 +65,6 @@ async def test_async_setup_platform(): await light.async_setup_platform(None, None, None) -def test_is_light(device_factory, light_devices): - """Test lights are correctly identified.""" - non_lights = [ - device_factory('Unknown', ['Unknown']), - device_factory("Fan 1", - [Capability.switch, Capability.switch_level, - Capability.fan_speed]), - device_factory("Switch 1", [Capability.switch]), - device_factory("Can't be turned off", - [Capability.switch_level, Capability.color_control, - Capability.color_temperature]) - ] - - for device in light_devices: - assert light.is_light(device), device.name - for device in non_lights: - assert not light.is_light(device), device.name - - async def test_entity_state(hass, light_devices): """Tests the state attributes properly match the light types.""" await _setup_platform(hass, *light_devices) diff --git a/tests/components/smartthings/test_lock.py b/tests/components/smartthings/test_lock.py new file mode 100644 index 00000000000..3739a2dc9b5 --- /dev/null +++ b/tests/components/smartthings/test_lock.py @@ -0,0 +1,104 @@ +""" +Test for the SmartThings lock platform. + +The only mocking required is of the underlying SmartThings API object so +real HTTP calls are not initiated during testing. +""" +from pysmartthings import Attribute, Capability + +from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN +from homeassistant.components.smartthings import lock +from homeassistant.components.smartthings.const import ( + DOMAIN, SIGNAL_SMARTTHINGS_UPDATE) +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from .conftest import setup_platform + + +async def test_async_setup_platform(): + """Test setup platform does nothing (it uses config entries).""" + await lock.async_setup_platform(None, None, None) + + +async def test_entity_and_device_attributes(hass, device_factory): + """Test the attributes of the entity are correct.""" + # Arrange + device = device_factory('Lock_1', [Capability.lock], + {Attribute.lock: 'unlocked'}) + entity_registry = await hass.helpers.entity_registry.async_get_registry() + device_registry = await hass.helpers.device_registry.async_get_registry() + # Act + await setup_platform(hass, LOCK_DOMAIN, device) + # Assert + entry = entity_registry.async_get('lock.lock_1') + assert entry + assert entry.unique_id == device.device_id + + entry = device_registry.async_get_device( + {(DOMAIN, device.device_id)}, []) + assert entry + assert entry.name == device.label + assert entry.model == device.device_type_name + assert entry.manufacturer == 'Unavailable' + + +async def test_lock(hass, device_factory): + """Test the lock locks successfully.""" + # Arrange + device = device_factory('Lock_1', [Capability.lock], + {Attribute.lock: 'unlocked'}) + await setup_platform(hass, LOCK_DOMAIN, device) + # Act + await hass.services.async_call( + LOCK_DOMAIN, 'lock', {'entity_id': 'lock.lock_1'}, + blocking=True) + # Assert + state = hass.states.get('lock.lock_1') + assert state is not None + assert state.state == 'locked' + + +async def test_unlock(hass, device_factory): + """Test the lock unlocks successfully.""" + # Arrange + device = device_factory('Lock_1', [Capability.lock], + {Attribute.lock: 'locked'}) + await setup_platform(hass, LOCK_DOMAIN, device) + # Act + await hass.services.async_call( + LOCK_DOMAIN, 'unlock', {'entity_id': 'lock.lock_1'}, + blocking=True) + # Assert + state = hass.states.get('lock.lock_1') + assert state is not None + assert state.state == 'unlocked' + + +async def test_update_from_signal(hass, device_factory): + """Test the lock updates when receiving a signal.""" + # Arrange + device = device_factory('Lock_1', [Capability.lock], + {Attribute.lock: 'unlocked'}) + await setup_platform(hass, LOCK_DOMAIN, device) + await device.lock(True) + # Act + async_dispatcher_send(hass, SIGNAL_SMARTTHINGS_UPDATE, + [device.device_id]) + # Assert + await hass.async_block_till_done() + state = hass.states.get('lock.lock_1') + assert state is not None + assert state.state == 'locked' + + +async def test_unload_config_entry(hass, device_factory): + """Test the lock is removed when the config entry is unloaded.""" + # Arrange + device = device_factory('Lock_1', [Capability.lock], + {Attribute.lock: 'locked'}) + config_entry = await setup_platform(hass, LOCK_DOMAIN, device) + # Act + await hass.config_entries.async_forward_entry_unload( + config_entry, 'lock') + # Assert + assert not hass.states.get('lock.lock_1') diff --git a/tests/components/smartthings/test_sensor.py b/tests/components/smartthings/test_sensor.py new file mode 100644 index 00000000000..773f157dd87 --- /dev/null +++ b/tests/components/smartthings/test_sensor.py @@ -0,0 +1,97 @@ +""" +Test for the SmartThings sensors platform. + +The only mocking required is of the underlying SmartThings API object so +real HTTP calls are not initiated during testing. +""" +from pysmartthings import ATTRIBUTES, CAPABILITIES, Attribute, Capability + +from homeassistant.components.sensor import ( + DEVICE_CLASSES, DOMAIN as SENSOR_DOMAIN) +from homeassistant.components.smartthings import sensor +from homeassistant.components.smartthings.const import ( + DOMAIN, SIGNAL_SMARTTHINGS_UPDATE) +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from .conftest import setup_platform + + +async def test_mapping_integrity(): + """Test ensures the map dicts have proper integrity.""" + for capability, maps in sensor.CAPABILITY_TO_SENSORS.items(): + assert capability in CAPABILITIES, capability + for sensor_map in maps: + assert sensor_map.attribute in ATTRIBUTES, sensor_map.attribute + if sensor_map.device_class: + assert sensor_map.device_class in DEVICE_CLASSES, \ + sensor_map.device_class + + +async def test_async_setup_platform(): + """Test setup platform does nothing (it uses config entries).""" + await sensor.async_setup_platform(None, None, None) + + +async def test_entity_state(hass, device_factory): + """Tests the state attributes properly match the light types.""" + device = device_factory('Sensor 1', [Capability.battery], + {Attribute.battery: 100}) + await setup_platform(hass, SENSOR_DOMAIN, device) + state = hass.states.get('sensor.sensor_1_battery') + assert state.state == '100' + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == '%' + assert state.attributes[ATTR_FRIENDLY_NAME] ==\ + device.label + " Battery" + + +async def test_entity_and_device_attributes(hass, device_factory): + """Test the attributes of the entity are correct.""" + # Arrange + device = device_factory('Sensor 1', [Capability.battery], + {Attribute.battery: 100}) + entity_registry = await hass.helpers.entity_registry.async_get_registry() + device_registry = await hass.helpers.device_registry.async_get_registry() + # Act + await setup_platform(hass, SENSOR_DOMAIN, device) + # Assert + entry = entity_registry.async_get('sensor.sensor_1_battery') + assert entry + assert entry.unique_id == device.device_id + '.' + Attribute.battery + entry = device_registry.async_get_device( + {(DOMAIN, device.device_id)}, []) + assert entry + assert entry.name == device.label + assert entry.model == device.device_type_name + assert entry.manufacturer == 'Unavailable' + + +async def test_update_from_signal(hass, device_factory): + """Test the binary_sensor updates when receiving a signal.""" + # Arrange + device = device_factory('Sensor 1', [Capability.battery], + {Attribute.battery: 100}) + await setup_platform(hass, SENSOR_DOMAIN, device) + device.status.apply_attribute_update( + 'main', Capability.battery, Attribute.battery, 75) + # Act + async_dispatcher_send(hass, SIGNAL_SMARTTHINGS_UPDATE, + [device.device_id]) + # Assert + await hass.async_block_till_done() + state = hass.states.get('sensor.sensor_1_battery') + assert state is not None + assert state.state == '75' + + +async def test_unload_config_entry(hass, device_factory): + """Test the binary_sensor is removed when the config entry is unloaded.""" + # Arrange + device = device_factory('Sensor 1', [Capability.battery], + {Attribute.battery: 100}) + config_entry = await setup_platform(hass, SENSOR_DOMAIN, device) + # Act + await hass.config_entries.async_forward_entry_unload( + config_entry, 'sensor') + # Assert + assert not hass.states.get('sensor.sensor_1_battery') diff --git a/tests/components/smartthings/test_smartapp.py b/tests/components/smartthings/test_smartapp.py index 0f517222c4a..162a8f9a4e5 100644 --- a/tests/components/smartthings/test_smartapp.py +++ b/tests/components/smartthings/test_smartapp.py @@ -2,11 +2,10 @@ from unittest.mock import Mock, patch from uuid import uuid4 -from pysmartthings import AppEntity +from pysmartthings import AppEntity, Capability from homeassistant.components.smartthings import smartapp -from homeassistant.components.smartthings.const import ( - DATA_MANAGER, DOMAIN, SUPPORTED_CAPABILITIES) +from homeassistant.components.smartthings.const import DATA_MANAGER, DOMAIN from tests.common import mock_coro @@ -36,8 +35,10 @@ async def test_update_app_updated_needed(hass, app): assert mock_app.classifications == app.classifications -async def test_smartapp_install_abort_if_no_other(hass, smartthings_mock): +async def test_smartapp_install_abort_if_no_other( + hass, smartthings_mock, device_factory): """Test aborts if no other app was configured already.""" + # Arrange api = smartthings_mock.return_value api.create_subscription.return_value = mock_coro() app = Mock() @@ -46,17 +47,23 @@ async def test_smartapp_install_abort_if_no_other(hass, smartthings_mock): request.installed_app_id = uuid4() request.auth_token = uuid4() request.location_id = uuid4() - + devices = [ + device_factory('', [Capability.battery, 'ping']), + device_factory('', [Capability.switch, Capability.switch_level]), + device_factory('', [Capability.switch]) + ] + api.devices = Mock() + api.devices.return_value = mock_coro(return_value=devices) + # Act await smartapp.smartapp_install(hass, request, None, app) - + # Assert entries = hass.config_entries.async_entries('smartthings') assert not entries - assert api.create_subscription.call_count == \ - len(SUPPORTED_CAPABILITIES) + assert api.create_subscription.call_count == 3 async def test_smartapp_install_creates_flow( - hass, smartthings_mock, config_entry, location): + hass, smartthings_mock, config_entry, location, device_factory): """Test installation creates flow.""" # Arrange setattr(hass.config_entries, '_entries', [config_entry]) @@ -68,14 +75,20 @@ async def test_smartapp_install_creates_flow( request.installed_app_id = str(uuid4()) request.auth_token = str(uuid4()) request.location_id = location.location_id + devices = [ + device_factory('', [Capability.battery, 'ping']), + device_factory('', [Capability.switch, Capability.switch_level]), + device_factory('', [Capability.switch]) + ] + api.devices = Mock() + api.devices.return_value = mock_coro(return_value=devices) # Act await smartapp.smartapp_install(hass, request, None, app) # Assert await hass.async_block_till_done() entries = hass.config_entries.async_entries('smartthings') assert len(entries) == 2 - assert api.create_subscription.call_count == \ - len(SUPPORTED_CAPABILITIES) + assert api.create_subscription.call_count == 3 assert entries[1].data['app_id'] == app.app_id assert entries[1].data['installed_app_id'] == request.installed_app_id assert entries[1].data['location_id'] == request.location_id @@ -84,6 +97,35 @@ async def test_smartapp_install_creates_flow( assert entries[1].title == location.name +async def test_smartapp_update_syncs_subs( + hass, smartthings_mock, config_entry, location, device_factory): + """Test update synchronizes subscriptions.""" + # Arrange + setattr(hass.config_entries, '_entries', [config_entry]) + app = Mock() + app.app_id = config_entry.data['app_id'] + api = smartthings_mock.return_value + api.delete_subscriptions = Mock() + api.delete_subscriptions.return_value = mock_coro() + api.create_subscription.return_value = mock_coro() + request = Mock() + request.installed_app_id = str(uuid4()) + request.auth_token = str(uuid4()) + request.location_id = location.location_id + devices = [ + device_factory('', [Capability.battery, 'ping']), + device_factory('', [Capability.switch, Capability.switch_level]), + device_factory('', [Capability.switch]) + ] + api.devices = Mock() + api.devices.return_value = mock_coro(return_value=devices) + # Act + await smartapp.smartapp_update(hass, request, None, app) + # Assert + assert api.create_subscription.call_count == 3 + assert api.delete_subscriptions.call_count == 1 + + async def test_smartapp_uninstall(hass, config_entry): """Test the config entry is unloaded when the app is uninstalled.""" setattr(hass.config_entries, '_entries', [config_entry]) diff --git a/tests/components/smartthings/test_switch.py b/tests/components/smartthings/test_switch.py index a8013105291..3f2bedd4f13 100644 --- a/tests/components/smartthings/test_switch.py +++ b/tests/components/smartthings/test_switch.py @@ -35,23 +35,6 @@ async def test_async_setup_platform(): await switch.async_setup_platform(None, None, None) -def test_is_switch(device_factory): - """Test switches are correctly identified.""" - switch_device = device_factory('Switch', [Capability.switch]) - non_switch_devices = [ - device_factory('Light', [Capability.switch, Capability.switch_level]), - device_factory('Fan', [Capability.switch, Capability.fan_speed]), - device_factory('Color Light', [Capability.switch, - Capability.color_control]), - device_factory('Temp Light', [Capability.switch, - Capability.color_temperature]), - device_factory('Unknown', ['Unknown']), - ] - assert switch.is_switch(switch_device) - for non_switch_device in non_switch_devices: - assert not switch.is_switch(non_switch_device) - - async def test_entity_and_device_attributes(hass, device_factory): """Test the attributes of the entity are correct.""" # Arrange @@ -126,7 +109,7 @@ async def test_update_from_signal(hass, device_factory): async def test_unload_config_entry(hass, device_factory): """Test the switch is removed when the config entry is unloaded.""" # Arrange - device = device_factory('Switch', [Capability.switch], + device = device_factory('Switch 1', [Capability.switch], {Attribute.switch: 'on'}) config_entry = await _setup_platform(hass, device) # Act diff --git a/tests/components/weather/test_smhi.py b/tests/components/smhi/test_weather.py similarity index 94% rename from tests/components/weather/test_smhi.py rename to tests/components/smhi/test_weather.py index 11a5028842b..f0acd231ebe 100644 --- a/tests/components/weather/test_smhi.py +++ b/tests/components/smhi/test_weather.py @@ -1,7 +1,7 @@ """Test for the smhi weather entity.""" import asyncio import logging -from datetime import datetime, timezone +from datetime import datetime from unittest.mock import Mock, patch from homeassistant.components.weather import ( @@ -9,8 +9,8 @@ from homeassistant.components.weather import ( 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) + ATTR_FORECAST_PRECIPITATION, DOMAIN as WEATHER_DOMAIN) +from homeassistant.components.smhi import weather as weather_smhi from homeassistant.const import TEMP_CELSIUS from homeassistant.core import HomeAssistant @@ -61,12 +61,11 @@ async def test_setup_hass(hass: HomeAssistant, aioclient_mock) -> None: 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 + assert len(state.attributes['forecast']) == 4 - 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 + forecast = state.attributes['forecast'][1] + assert forecast[ATTR_FORECAST_TIME] == '2018-09-02T12:00:00' + assert forecast[ATTR_FORECAST_TEMP] == 21 assert forecast[ATTR_FORECAST_TEMP_LOW] == 6 assert forecast[ATTR_FORECAST_PRECIPITATION] == 0 assert forecast[ATTR_FORECAST_CONDITION] == 'partlycloudy' @@ -102,7 +101,8 @@ def test_properties_unknown_symbol() -> None: hass = Mock() data = Mock() data.temperature = 5 - data.mean_precipitation = 1 + data.mean_precipitation = 0.5 + data.total_precipitation = 1 data.humidity = 5 data.wind_speed = 10 data.wind_direction = 180 @@ -114,7 +114,8 @@ def test_properties_unknown_symbol() -> None: data2 = Mock() data2.temperature = 5 - data2.mean_precipitation = 1 + data2.mean_precipitation = 0.5 + data2.total_precipitation = 1 data2.humidity = 5 data2.wind_speed = 10 data2.wind_direction = 180 @@ -126,7 +127,8 @@ def test_properties_unknown_symbol() -> None: data3 = Mock() data3.temperature = 5 - data3.mean_precipitation = 1 + data3.mean_precipitation = 0.5 + data3.total_precipitation = 1 data3.humidity = 5 data3.wind_speed = 10 data3.wind_direction = 180 diff --git a/tests/components/snips/__init__.py b/tests/components/snips/__init__.py new file mode 100644 index 00000000000..d7ac8b5f822 --- /dev/null +++ b/tests/components/snips/__init__.py @@ -0,0 +1 @@ +"""Tests for the snips component.""" diff --git a/tests/components/test_snips.py b/tests/components/snips/test_init.py similarity index 90% rename from tests/components/test_snips.py rename to tests/components/snips/test_init.py index 977cd966981..7bcfb11ff5c 100644 --- a/tests/components/test_snips.py +++ b/tests/components/snips/test_init.py @@ -10,11 +10,13 @@ from homeassistant.components.mqtt import MQTT_PUBLISH_SCHEMA import homeassistant.components.snips as snips from homeassistant.helpers.intent import (ServiceIntentHandler, async_register) from tests.common import (async_fire_mqtt_message, async_mock_intent, - async_mock_service) + async_mock_service, async_mock_mqtt_component) -async def test_snips_config(hass, mqtt_mock): +async def test_snips_config(hass): """Test Snips Config.""" + await async_mock_mqtt_component(hass) + result = await async_setup_component(hass, "snips", { "snips": { "feedback_sounds": True, @@ -25,8 +27,10 @@ async def test_snips_config(hass, mqtt_mock): assert result -async def test_snips_bad_config(hass, mqtt_mock): +async def test_snips_bad_config(hass): """Test Snips bad config.""" + await async_mock_mqtt_component(hass) + result = await async_setup_component(hass, "snips", { "snips": { "feedback_sounds": "on", @@ -37,8 +41,10 @@ async def test_snips_bad_config(hass, mqtt_mock): assert not result -async def test_snips_config_feedback_on(hass, mqtt_mock): +async def test_snips_config_feedback_on(hass): """Test Snips Config.""" + await async_mock_mqtt_component(hass) + calls = async_mock_service(hass, 'mqtt', 'publish', MQTT_PUBLISH_SCHEMA) result = await async_setup_component(hass, "snips", { "snips": { @@ -57,8 +63,10 @@ async def test_snips_config_feedback_on(hass, mqtt_mock): assert calls[1].data['retain'] -async def test_snips_config_feedback_off(hass, mqtt_mock): +async def test_snips_config_feedback_off(hass): """Test Snips Config.""" + await async_mock_mqtt_component(hass) + calls = async_mock_service(hass, 'mqtt', 'publish', MQTT_PUBLISH_SCHEMA) result = await async_setup_component(hass, "snips", { "snips": { @@ -77,8 +85,10 @@ async def test_snips_config_feedback_off(hass, mqtt_mock): assert not calls[1].data['retain'] -async def test_snips_config_no_feedback(hass, mqtt_mock): +async def test_snips_config_no_feedback(hass): """Test Snips Config.""" + await async_mock_mqtt_component(hass) + calls = async_mock_service(hass, 'snips', 'say') result = await async_setup_component(hass, "snips", { "snips": {}, @@ -88,8 +98,10 @@ async def test_snips_config_no_feedback(hass, mqtt_mock): assert len(calls) == 0 -async def test_snips_intent(hass, mqtt_mock): +async def test_snips_intent(hass): """Test intent via Snips.""" + await async_mock_mqtt_component(hass) + result = await async_setup_component(hass, "snips", { "snips": {}, }) @@ -134,8 +146,10 @@ async def test_snips_intent(hass, mqtt_mock): assert intent.text_input == 'turn the lights green' -async def test_snips_service_intent(hass, mqtt_mock): +async def test_snips_service_intent(hass): """Test ServiceIntentHandler via Snips.""" + await async_mock_mqtt_component(hass) + hass.states.async_set('light.kitchen', 'off') calls = async_mock_service(hass, 'light', 'turn_on') result = await async_setup_component(hass, "snips", { @@ -178,8 +192,10 @@ async def test_snips_service_intent(hass, mqtt_mock): assert 'site_id' not in calls[0].data -async def test_snips_intent_with_duration(hass, mqtt_mock): +async def test_snips_intent_with_duration(hass): """Test intent with Snips duration.""" + await async_mock_mqtt_component(hass) + result = await async_setup_component(hass, "snips", { "snips": {}, }) @@ -232,8 +248,10 @@ async def test_snips_intent_with_duration(hass, mqtt_mock): 'timer_duration_raw': {'value': 'five minutes'}} -async def test_intent_speech_response(hass, mqtt_mock): +async def test_intent_speech_response(hass): """Test intent speech response via Snips.""" + await async_mock_mqtt_component(hass) + calls = async_mock_service(hass, 'mqtt', 'publish', MQTT_PUBLISH_SCHEMA) result = await async_setup_component(hass, "snips", { "snips": {}, @@ -273,8 +291,10 @@ async def test_intent_speech_response(hass, mqtt_mock): assert topic == 'hermes/dialogueManager/endSession' -async def test_unknown_intent(hass, mqtt_mock, caplog): +async def test_unknown_intent(hass, caplog): """Test unknown intent.""" + await async_mock_mqtt_component(hass) + caplog.set_level(logging.WARNING) result = await async_setup_component(hass, "snips", { "snips": {}, @@ -297,8 +317,10 @@ async def test_unknown_intent(hass, mqtt_mock, caplog): assert 'Received unknown intent unknownIntent' in caplog.text -async def test_snips_intent_user(hass, mqtt_mock): +async def test_snips_intent_user(hass): """Test intentName format user_XXX__intentName.""" + await async_mock_mqtt_component(hass) + result = await async_setup_component(hass, "snips", { "snips": {}, }) @@ -324,8 +346,10 @@ async def test_snips_intent_user(hass, mqtt_mock): assert intent.intent_type == 'Lights' -async def test_snips_intent_username(hass, mqtt_mock): +async def test_snips_intent_username(hass): """Test intentName format username:intentName.""" + await async_mock_mqtt_component(hass) + result = await async_setup_component(hass, "snips", { "snips": {}, }) @@ -351,8 +375,10 @@ async def test_snips_intent_username(hass, mqtt_mock): assert intent.intent_type == 'Lights' -async def test_snips_low_probability(hass, mqtt_mock, caplog): +async def test_snips_low_probability(hass, caplog): """Test intent via Snips.""" + await async_mock_mqtt_component(hass) + caplog.set_level(logging.WARNING) result = await async_setup_component(hass, "snips", { "snips": { @@ -378,8 +404,10 @@ async def test_snips_low_probability(hass, mqtt_mock, caplog): assert 'Intent below probaility threshold 0.49 < 0.5' in caplog.text -async def test_intent_special_slots(hass, mqtt_mock): +async def test_intent_special_slots(hass): """Test intent special slot values via Snips.""" + await async_mock_mqtt_component(hass) + calls = async_mock_service(hass, 'light', 'turn_on') result = await async_setup_component(hass, "snips", { "snips": {}, @@ -420,7 +448,7 @@ async def test_intent_special_slots(hass, mqtt_mock): assert calls[0].data['site_id'] == 'default' -async def test_snips_say(hass, caplog): +async def test_snips_say(hass): """Test snips say with invalid config.""" calls = async_mock_service(hass, 'snips', 'say', snips.SERVICE_SCHEMA_SAY) data = {'text': 'Hello'} @@ -433,7 +461,7 @@ async def test_snips_say(hass, caplog): assert calls[0].data['text'] == 'Hello' -async def test_snips_say_action(hass, caplog): +async def test_snips_say_action(hass): """Test snips say_action with invalid config.""" calls = async_mock_service(hass, 'snips', 'say_action', snips.SERVICE_SCHEMA_SAY_ACTION) @@ -449,7 +477,7 @@ async def test_snips_say_action(hass, caplog): assert calls[0].data['intent_filter'] == ['myIntent'] -async def test_snips_say_invalid_config(hass, caplog): +async def test_snips_say_invalid_config(hass): """Test snips say with invalid config.""" calls = async_mock_service(hass, 'snips', 'say', snips.SERVICE_SCHEMA_SAY) @@ -462,7 +490,7 @@ async def test_snips_say_invalid_config(hass, caplog): assert len(calls) == 0 -async def test_snips_say_action_invalid(hass, caplog): +async def test_snips_say_action_invalid(hass): """Test snips say_action with invalid config.""" calls = async_mock_service(hass, 'snips', 'say_action', snips.SERVICE_SCHEMA_SAY_ACTION) @@ -476,7 +504,7 @@ async def test_snips_say_action_invalid(hass, caplog): assert len(calls) == 0 -async def test_snips_feedback_on(hass, caplog): +async def test_snips_feedback_on(hass): """Test snips say with invalid config.""" calls = async_mock_service(hass, 'snips', 'feedback_on', snips.SERVICE_SCHEMA_FEEDBACK) @@ -491,7 +519,7 @@ async def test_snips_feedback_on(hass, caplog): assert calls[0].data['site_id'] == 'remote' -async def test_snips_feedback_off(hass, caplog): +async def test_snips_feedback_off(hass): """Test snips say with invalid config.""" calls = async_mock_service(hass, 'snips', 'feedback_off', snips.SERVICE_SCHEMA_FEEDBACK) @@ -506,7 +534,7 @@ async def test_snips_feedback_off(hass, caplog): assert calls[0].data['site_id'] == 'remote' -async def test_snips_feedback_config(hass, caplog): +async def test_snips_feedback_config(hass): """Test snips say with invalid config.""" calls = async_mock_service(hass, 'snips', 'feedback_on', snips.SERVICE_SCHEMA_FEEDBACK) diff --git a/tests/components/sonos/test_init.py b/tests/components/sonos/test_init.py index 8d46f4d57a3..a09fa7d2615 100644 --- a/tests/components/sonos/test_init.py +++ b/tests/components/sonos/test_init.py @@ -10,7 +10,7 @@ from tests.common import mock_coro async def test_creating_entry_sets_up_media_player(hass): """Test setting up Sonos loads the media player.""" - with patch('homeassistant.components.media_player.sonos.async_setup_entry', + with patch('homeassistant.components.sonos.media_player.async_setup_entry', return_value=mock_coro(True)) as mock_setup, \ patch('pysonos.discover', return_value=True): result = await hass.config_entries.flow.async_init( diff --git a/tests/components/media_player/test_sonos.py b/tests/components/sonos/test_media_player.py similarity index 98% rename from tests/components/media_player/test_sonos.py rename to tests/components/sonos/test_media_player.py index bf81aee5982..55ff96f202a 100644 --- a/tests/components/media_player/test_sonos.py +++ b/tests/components/sonos/test_media_player.py @@ -8,8 +8,9 @@ import pysonos from pysonos import alarms from homeassistant.setup import setup_component -from homeassistant.components.media_player import sonos, DOMAIN -from homeassistant.components.media_player.sonos import CONF_INTERFACE_ADDR +from homeassistant.components.sonos import media_player as sonos +from homeassistant.components.media_player.const import DOMAIN +from homeassistant.components.sonos.media_player import CONF_INTERFACE_ADDR from homeassistant.const import CONF_HOSTS, CONF_PLATFORM from tests.common import get_test_home_assistant diff --git a/tests/components/spaceapi/__init__.py b/tests/components/spaceapi/__init__.py new file mode 100644 index 00000000000..0b24e36acb2 --- /dev/null +++ b/tests/components/spaceapi/__init__.py @@ -0,0 +1 @@ +"""Tests for the spaceapi component.""" diff --git a/tests/components/test_spaceapi.py b/tests/components/spaceapi/test_init.py similarity index 100% rename from tests/components/test_spaceapi.py rename to tests/components/spaceapi/test_init.py diff --git a/tests/components/spc/__init__.py b/tests/components/spc/__init__.py new file mode 100644 index 00000000000..a86adf13be6 --- /dev/null +++ b/tests/components/spc/__init__.py @@ -0,0 +1 @@ +"""Tests for the spc component.""" diff --git a/tests/components/test_spc.py b/tests/components/spc/test_init.py similarity index 100% rename from tests/components/test_spc.py rename to tests/components/spc/test_init.py diff --git a/tests/components/splunk/__init__.py b/tests/components/splunk/__init__.py new file mode 100644 index 00000000000..709483291e3 --- /dev/null +++ b/tests/components/splunk/__init__.py @@ -0,0 +1 @@ +"""Tests for the splunk component.""" diff --git a/tests/components/test_splunk.py b/tests/components/splunk/test_init.py similarity index 100% rename from tests/components/test_splunk.py rename to tests/components/splunk/test_init.py diff --git a/tests/components/statsd/__init__.py b/tests/components/statsd/__init__.py new file mode 100644 index 00000000000..f72ec8d5be1 --- /dev/null +++ b/tests/components/statsd/__init__.py @@ -0,0 +1 @@ +"""Tests for the statsd component.""" diff --git a/tests/components/test_statsd.py b/tests/components/statsd/test_init.py similarity index 100% rename from tests/components/test_statsd.py rename to tests/components/statsd/test_init.py diff --git a/tests/components/sun/__init__.py b/tests/components/sun/__init__.py new file mode 100644 index 00000000000..11448700dcd --- /dev/null +++ b/tests/components/sun/__init__.py @@ -0,0 +1 @@ +"""Tests for the sun component.""" diff --git a/tests/components/test_sun.py b/tests/components/sun/test_init.py similarity index 100% rename from tests/components/test_sun.py rename to tests/components/sun/test_init.py diff --git a/tests/components/switch/test_rflink.py b/tests/components/switch/test_rflink.py index b50a223fe8b..a91d18ce19e 100644 --- a/tests/components/switch/test_rflink.py +++ b/tests/components/switch/test_rflink.py @@ -11,7 +11,7 @@ from homeassistant.const import ( from homeassistant.core import callback, State, CoreState from tests.common import mock_restore_cache -from ..test_rflink import mock_rflink +from tests.components.rflink.test_init import mock_rflink DOMAIN = 'switch' diff --git a/tests/components/switch/test_vultr.py b/tests/components/switch/test_vultr.py index 699da34319a..f5e94e3e1b1 100644 --- a/tests/components/switch/test_vultr.py +++ b/tests/components/switch/test_vultr.py @@ -16,7 +16,7 @@ from homeassistant.components.vultr import ( from homeassistant.const import ( CONF_PLATFORM, CONF_NAME) -from tests.components.test_vultr import VALID_CONFIG +from tests.components.vultr.test_init import VALID_CONFIG from tests.common import ( get_test_home_assistant, load_fixture) diff --git a/tests/components/system_log/__init__.py b/tests/components/system_log/__init__.py new file mode 100644 index 00000000000..691a4f221ca --- /dev/null +++ b/tests/components/system_log/__init__.py @@ -0,0 +1 @@ +"""Tests for the system_log component.""" diff --git a/tests/components/test_system_log.py b/tests/components/system_log/test_init.py similarity index 92% rename from tests/components/test_system_log.py rename to tests/components/system_log/test_init.py index 6afd792be9c..14047399aff 100644 --- a/tests/components/test_system_log.py +++ b/tests/components/system_log/test_init.py @@ -140,6 +140,24 @@ async def test_remove_older_logs(hass, hass_client): assert_log(log[1], '', 'error message 2', 'ERROR') +async def test_dedup_logs(hass, hass_client): + """Test that duplicate log entries are dedup.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + _LOGGER.error('error message 1') + _LOGGER.error('error message 2') + _LOGGER.error('error message 2') + _LOGGER.error('error message 3') + log = await get_error_log(hass, hass_client, 2) + assert_log(log[0], '', 'error message 3', 'ERROR') + assert log[1]["count"] == 2 + assert_log(log[1], '', 'error message 2', 'ERROR') + + _LOGGER.error('error message 2') + log = await get_error_log(hass, hass_client, 2) + assert_log(log[0], '', 'error message 2', 'ERROR') + assert log[0]["timestamp"] > log[0]["first_occured"] + + async def test_clear_logs(hass, hass_client): """Test that the log can be cleared via a service call.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index 977b0669880..4786370f24f 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -10,7 +10,7 @@ import requests import homeassistant.components.http as http import homeassistant.components.tts as tts from homeassistant.components.tts.demo import DemoProvider -from homeassistant.components.media_player import ( +from homeassistant.components.media_player.const import ( SERVICE_PLAY_MEDIA, MEDIA_TYPE_MUSIC, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, DOMAIN as DOMAIN_MP) from homeassistant.setup import setup_component, async_setup_component diff --git a/tests/components/tts/test_marytts.py b/tests/components/tts/test_marytts.py index 110473d75a8..7520ba2fbaa 100644 --- a/tests/components/tts/test_marytts.py +++ b/tests/components/tts/test_marytts.py @@ -5,7 +5,7 @@ import shutil import homeassistant.components.tts as tts from homeassistant.setup import setup_component -from homeassistant.components.media_player import ( +from homeassistant.components.media_player.const import ( SERVICE_PLAY_MEDIA, DOMAIN as DOMAIN_MP) from tests.common import ( diff --git a/tests/components/tts/test_voicerss.py b/tests/components/tts/test_voicerss.py index 334e35a9386..af4bdf3976c 100644 --- a/tests/components/tts/test_voicerss.py +++ b/tests/components/tts/test_voicerss.py @@ -4,7 +4,7 @@ import os import shutil import homeassistant.components.tts as tts -from homeassistant.components.media_player import ( +from homeassistant.components.media_player.const import ( SERVICE_PLAY_MEDIA, ATTR_MEDIA_CONTENT_ID, DOMAIN as DOMAIN_MP) from homeassistant.setup import setup_component diff --git a/tests/components/tts/test_yandextts.py b/tests/components/tts/test_yandextts.py index 2675e322507..70c75b1f2ed 100644 --- a/tests/components/tts/test_yandextts.py +++ b/tests/components/tts/test_yandextts.py @@ -5,7 +5,7 @@ import shutil import homeassistant.components.tts as tts from homeassistant.setup import setup_component -from homeassistant.components.media_player import ( +from homeassistant.components.media_player.const import ( SERVICE_PLAY_MEDIA, DOMAIN as DOMAIN_MP) from tests.common import ( get_test_home_assistant, assert_setup_component, mock_service) diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index b3b222d902a..e5e1d84bfcd 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -1,6 +1,9 @@ """Test UniFi Controller.""" from unittest.mock import Mock, patch +import pytest + +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.components import unifi from homeassistant.components.unifi import controller, errors @@ -103,13 +106,10 @@ async def test_controller_not_accessible(): 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 + with patch.object( + controller, 'get_controller', side_effect=errors.CannotConnect + ), pytest.raises(ConfigEntryNotReady): + await unifi_controller.async_setup() async def test_controller_unknown_error(): @@ -128,28 +128,6 @@ async def test_controller_unknown_error(): 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() diff --git a/tests/components/updater/__init__.py b/tests/components/updater/__init__.py new file mode 100644 index 00000000000..31a19cb3bf7 --- /dev/null +++ b/tests/components/updater/__init__.py @@ -0,0 +1 @@ +"""Tests for the updater component.""" diff --git a/tests/components/test_updater.py b/tests/components/updater/test_init.py similarity index 100% rename from tests/components/test_updater.py rename to tests/components/updater/test_init.py diff --git a/tests/components/verisure/__init__.py b/tests/components/verisure/__init__.py new file mode 100644 index 00000000000..0382661dbe3 --- /dev/null +++ b/tests/components/verisure/__init__.py @@ -0,0 +1 @@ +"""Tests for Verisure integration.""" diff --git a/tests/components/lock/test_verisure.py b/tests/components/verisure/test_lock.py similarity index 98% rename from tests/components/lock/test_verisure.py rename to tests/components/verisure/test_lock.py index 03dd202e838..20af71cfca5 100644 --- a/tests/components/lock/test_verisure.py +++ b/tests/components/verisure/test_lock.py @@ -46,7 +46,7 @@ LOCKS = ['door_lock'] @contextmanager def mock_hub(config, get_response=LOCKS[0]): """Extensively mock out a verisure hub.""" - hub_prefix = 'homeassistant.components.lock.verisure.hub' + hub_prefix = 'homeassistant.components.verisure.lock.hub' verisure_prefix = 'verisure.Session' with patch(verisure_prefix) as session, \ patch(hub_prefix) as hub: diff --git a/tests/components/vultr/__init__.py b/tests/components/vultr/__init__.py new file mode 100644 index 00000000000..fb25b7e145e --- /dev/null +++ b/tests/components/vultr/__init__.py @@ -0,0 +1 @@ +"""Tests for the vultr component.""" diff --git a/tests/components/test_vultr.py b/tests/components/vultr/test_init.py similarity index 100% rename from tests/components/test_vultr.py rename to tests/components/vultr/test_init.py diff --git a/tests/components/wake_on_lan/__init__.py b/tests/components/wake_on_lan/__init__.py new file mode 100644 index 00000000000..f691e3973f3 --- /dev/null +++ b/tests/components/wake_on_lan/__init__.py @@ -0,0 +1 @@ +"""Tests for the wake_on_lan component.""" diff --git a/tests/components/test_wake_on_lan.py b/tests/components/wake_on_lan/test_init.py similarity index 100% rename from tests/components/test_wake_on_lan.py rename to tests/components/wake_on_lan/test_init.py diff --git a/tests/components/weather/test_ipma.py b/tests/components/weather/test_ipma.py deleted file mode 100644 index c7c89ecdbdb..00000000000 --- a/tests/components/weather/test_ipma.py +++ /dev/null @@ -1,85 +0,0 @@ -"""The tests for the IPMA weather component.""" -import unittest -from unittest.mock import patch -from collections import namedtuple - -from homeassistant.components import weather -from homeassistant.components.weather import ( - ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, ATTR_WEATHER_TEMPERATURE, - ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED) -from homeassistant.util.unit_system import METRIC_SYSTEM -from homeassistant.setup import setup_component - -from tests.common import get_test_home_assistant, MockDependency - - -class MockStation(): - """Mock Station from pyipma.""" - - @classmethod - async def get(cls, websession, lat, lon): - """Mock Factory.""" - return MockStation() - - async def observation(self): - """Mock Observation.""" - Observation = namedtuple('Observation', ['temperature', 'humidity', - 'windspeed', 'winddirection', - 'precipitation', 'pressure', - 'description']) - - return Observation(18, 71.0, 3.94, 'NW', 0, 1000.0, '---') - - async def forecast(self): - """Mock Forecast.""" - Forecast = namedtuple('Forecast', ['precipitaProb', 'tMin', 'tMax', - 'predWindDir', 'idWeatherType', - 'classWindSpeed', 'longitude', - 'forecastDate', 'classPrecInt', - 'latitude', 'description']) - - return [Forecast(73.0, 13.7, 18.7, 'NW', 6, 2, -8.64, - '2018-05-31', 2, 40.61, - 'Aguaceiros, com vento Moderado de Noroeste')] - - @property - def local(self): - """Mock location.""" - return "HomeTown" - - -class TestIPMA(unittest.TestCase): - """Test the IPMA weather component.""" - - def setUp(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.hass.config.units = METRIC_SYSTEM - self.lat = self.hass.config.latitude = 40.00 - self.lon = self.hass.config.longitude = -8.00 - - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() - - @MockDependency("pyipma") - @patch("pyipma.Station", new=MockStation) - def test_setup(self, mock_pyipma): - """Test for successfully setting up the IPMA platform.""" - assert setup_component(self.hass, weather.DOMAIN, { - 'weather': { - 'name': 'HomeTown', - 'platform': 'ipma', - } - }) - - state = self.hass.states.get('weather.hometown') - assert state.state == 'rainy' - - data = state.attributes - assert data.get(ATTR_WEATHER_TEMPERATURE) == 18.0 - assert data.get(ATTR_WEATHER_HUMIDITY) == 71 - assert data.get(ATTR_WEATHER_PRESSURE) == 1000.0 - assert data.get(ATTR_WEATHER_WIND_SPEED) == 3.94 - assert data.get(ATTR_WEATHER_WIND_BEARING) == 'NW' - assert state.attributes.get('friendly_name') == 'HomeTown' diff --git a/tests/components/webhook/__init__.py b/tests/components/webhook/__init__.py new file mode 100644 index 00000000000..7064c578b1c --- /dev/null +++ b/tests/components/webhook/__init__.py @@ -0,0 +1 @@ +"""Tests for the webhook component.""" diff --git a/tests/components/test_webhook.py b/tests/components/webhook/test_init.py similarity index 100% rename from tests/components/test_webhook.py rename to tests/components/webhook/test_init.py diff --git a/tests/components/weblink/__init__.py b/tests/components/weblink/__init__.py new file mode 100644 index 00000000000..1d58e9c24d6 --- /dev/null +++ b/tests/components/weblink/__init__.py @@ -0,0 +1 @@ +"""Tests for the weblink component.""" diff --git a/tests/components/test_weblink.py b/tests/components/weblink/test_init.py similarity index 100% rename from tests/components/test_weblink.py rename to tests/components/weblink/test_init.py diff --git a/tests/components/webostv/__init__.py b/tests/components/webostv/__init__.py new file mode 100644 index 00000000000..adef8e9b86a --- /dev/null +++ b/tests/components/webostv/__init__.py @@ -0,0 +1 @@ +"""Tests for the WebOS TV integration.""" diff --git a/tests/components/media_player/test_webostv.py b/tests/components/webostv/test_media_player.py similarity index 96% rename from tests/components/media_player/test_webostv.py rename to tests/components/webostv/test_media_player.py index 8017ad6cd54..c552775c023 100644 --- a/tests/components/media_player/test_webostv.py +++ b/tests/components/webostv/test_media_player.py @@ -2,7 +2,7 @@ import unittest from unittest import mock -from homeassistant.components.media_player import webostv +from homeassistant.components.webostv import media_player as webostv class FakeLgWebOSDevice(webostv.LgWebOSDevice): diff --git a/tests/components/xiaomi_miio/__init__.py b/tests/components/xiaomi_miio/__init__.py new file mode 100644 index 00000000000..9f162e02f28 --- /dev/null +++ b/tests/components/xiaomi_miio/__init__.py @@ -0,0 +1 @@ +"""Tests for the Xiaomi Miio integration.""" diff --git a/tests/components/vacuum/test_xiaomi_miio.py b/tests/components/xiaomi_miio/test_vacuum.py similarity index 99% rename from tests/components/vacuum/test_xiaomi_miio.py rename to tests/components/xiaomi_miio/test_vacuum.py index c4c1fb0e1b4..a1e937cb244 100644 --- a/tests/components/vacuum/test_xiaomi_miio.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -11,7 +11,7 @@ from homeassistant.components.vacuum import ( SERVICE_CLEAN_SPOT, SERVICE_LOCATE, SERVICE_RETURN_TO_BASE, SERVICE_SEND_COMMAND, SERVICE_SET_FAN_SPEED, SERVICE_START_PAUSE, SERVICE_STOP, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON) -from homeassistant.components.vacuum.xiaomi_miio import ( +from homeassistant.components.xiaomi_miio.vacuum import ( ATTR_CLEANED_AREA, ATTR_CLEANING_TIME, ATTR_DO_NOT_DISTURB, ATTR_DO_NOT_DISTURB_START, ATTR_DO_NOT_DISTURB_END, ATTR_ERROR, ATTR_MAIN_BRUSH_LEFT, ATTR_SIDE_BRUSH_LEFT, ATTR_FILTER_LEFT, diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py new file mode 100644 index 00000000000..cd2eb53c3fe --- /dev/null +++ b/tests/components/zha/common.py @@ -0,0 +1,195 @@ +"""Common test objects.""" +import time +from unittest.mock import patch, Mock +from homeassistant.components.zha.core.helpers import convert_ieee +from homeassistant.components.zha.core.const import ( + DATA_ZHA, DATA_ZHA_CONFIG, DATA_ZHA_DISPATCHERS, DATA_ZHA_BRIDGE_ID +) +from homeassistant.util import slugify +from tests.common import mock_coro + + +class FakeApplication: + """Fake application for mocking zigpy.""" + + def __init__(self): + """Init fake application.""" + self.ieee = convert_ieee("00:15:8d:00:02:32:4f:32") + self.nwk = 0x087d + + +APPLICATION = FakeApplication() + + +class FakeEndpoint: + """Fake endpoint for moking zigpy.""" + + def __init__(self, manufacturer, model): + """Init fake endpoint.""" + from zigpy.profiles.zha import PROFILE_ID + self.device = None + self.endpoint_id = 1 + self.in_clusters = {} + self.out_clusters = {} + self._cluster_attr = {} + self.status = 1 + self.manufacturer = manufacturer + self.model = model + self.profile_id = PROFILE_ID + self.device_type = None + + def add_input_cluster(self, cluster_id): + """Add an input cluster.""" + from zigpy.zcl import Cluster + cluster = Cluster.from_id(self, cluster_id) + patch_cluster(cluster) + self.in_clusters[cluster_id] = cluster + if hasattr(cluster, 'ep_attribute'): + setattr(self, cluster.ep_attribute, cluster) + + def add_output_cluster(self, cluster_id): + """Add an output cluster.""" + from zigpy.zcl import Cluster + cluster = Cluster.from_id(self, cluster_id) + patch_cluster(cluster) + self.out_clusters[cluster_id] = cluster + + +def patch_cluster(cluster): + """Patch a cluster for testing.""" + cluster.deserialize = Mock() + cluster.handle_cluster_request = Mock() + cluster.handle_cluster_general_request = Mock() + cluster.read_attributes_raw = Mock() + cluster.read_attributes = Mock() + cluster.unbind = Mock() + + +class FakeDevice: + """Fake device for mocking zigpy.""" + + def __init__(self, ieee, manufacturer, model): + """Init fake device.""" + self._application = APPLICATION + self.ieee = convert_ieee(ieee) + self.nwk = 0xb79c + self.zdo = Mock() + self.endpoints = {0: self.zdo} + self.lqi = 255 + self.rssi = 8 + self.last_seen = time.time() + self.status = 2 + self.initializing = False + self.manufacturer = manufacturer + self.model = model + + +def make_device(in_cluster_ids, out_cluster_ids, device_type, ieee, + manufacturer, model): + """Make a fake device using the specified cluster classes.""" + device = FakeDevice(ieee, manufacturer, model) + endpoint = FakeEndpoint(manufacturer, model) + endpoint.device = device + device.endpoints[endpoint.endpoint_id] = endpoint + endpoint.device_type = device_type + + for cluster_id in in_cluster_ids: + endpoint.add_input_cluster(cluster_id) + + for cluster_id in out_cluster_ids: + endpoint.add_output_cluster(cluster_id) + + return device + + +async def async_init_zigpy_device( + hass, in_cluster_ids, out_cluster_ids, device_type, gateway, + ieee="00:0d:6f:00:0a:90:69:e7", manufacturer="FakeManufacturer", + model="FakeModel", is_new_join=False): + """Create and initialize a device. + + This creates a fake device and adds it to the "network". It can be used to + test existing device functionality and new device pairing functionality. + The is_new_join parameter influences whether or not the device will go + through cluster binding and zigbee cluster configure reporting. That only + happens when the device is paired to the network for the first time. + """ + device = make_device(in_cluster_ids, out_cluster_ids, device_type, ieee, + manufacturer, model) + await gateway.async_device_initialized(device, is_new_join) + await hass.async_block_till_done() + return device + + +def make_attribute(attrid, value, status=0): + """Make an attribute.""" + from zigpy.zcl.foundation import Attribute, TypeValue + attr = Attribute() + attr.attrid = attrid + attr.value = TypeValue() + attr.value.value = value + return attr + + +async def async_setup_entry(hass, config_entry): + """Mock setup entry for zha.""" + hass.data[DATA_ZHA][DATA_ZHA_CONFIG] = {} + hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS] = [] + hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = APPLICATION.ieee + return True + + +def make_entity_id(domain, device, cluster, use_suffix=True): + """Make the entity id for the entity under testing. + + This is used to get the entity id in order to get the state from the state + machine so that we can test state changes. + """ + ieee = device.ieee + ieeetail = ''.join(['%02x' % (o, ) for o in ieee[-4:]]) + entity_id = "{}.{}_{}_{}_{}{}".format( + domain, + slugify(device.manufacturer), + slugify(device.model), + ieeetail, + cluster.endpoint.endpoint_id, + ("", "_{}".format(cluster.cluster_id))[use_suffix], + ) + return entity_id + + +async def async_enable_traffic(hass, zha_gateway, zha_devices): + """Allow traffic to flow through the gateway and the zha device.""" + for zha_device in zha_devices: + zha_device.update_available(True) + await hass.async_block_till_done() + + +async def async_test_device_join( + hass, zha_gateway, cluster_id, domain, device_type=None): + """Test a newly joining device. + + This creates a new fake device and adds it to the network. It is meant to + simulate pairing a new device to the network so that code pathways that + only trigger during device joins can be tested. + """ + from zigpy.zcl.foundation import Status + from zigpy.zcl.clusters.general import Basic + # create zigpy device mocking out the zigbee network operations + with patch( + 'zigpy.zcl.Cluster.configure_reporting', + return_value=mock_coro([Status.SUCCESS, Status.SUCCESS])): + with patch( + 'zigpy.zcl.Cluster.bind', + return_value=mock_coro([Status.SUCCESS, Status.SUCCESS])): + zigpy_device = await async_init_zigpy_device( + hass, [cluster_id, Basic.cluster_id], [], device_type, + zha_gateway, + ieee="00:0d:6f:00:0a:90:69:f7", + manufacturer="FakeMan{}".format(cluster_id), + model="FakeMod{}".format(cluster_id), + is_new_join=True) + cluster = zigpy_device.endpoints.get(1).in_clusters[cluster_id] + entity_id = make_entity_id( + domain, zigpy_device, cluster, use_suffix=device_type is None) + assert hass.states.get(entity_id) is not None diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py new file mode 100644 index 00000000000..bd594941da1 --- /dev/null +++ b/tests/components/zha/conftest.py @@ -0,0 +1,56 @@ +"""Test configuration for the ZHA component.""" +from unittest.mock import patch +import pytest +from homeassistant import config_entries +from homeassistant.components.zha.core.const import ( + DOMAIN, DATA_ZHA, COMPONENTS +) +from homeassistant.components.zha.core.gateway import ZHAGateway +from homeassistant.components.zha.core.gateway import establish_device_mappings +from homeassistant.components.zha.core.channels.registry \ + import populate_channel_registry +from .common import async_setup_entry + + +@pytest.fixture(name='config_entry') +def config_entry_fixture(hass): + """Fixture representing a config entry.""" + config_entry = config_entries.ConfigEntry( + 1, DOMAIN, 'Mock Title', {}, 'test', + config_entries.CONN_CLASS_LOCAL_PUSH) + return config_entry + + +@pytest.fixture(name='zha_gateway') +def zha_gateway_fixture(hass): + """Fixture representing a zha gateway. + + Create a ZHAGateway object that can be used to interact with as if we + had a real zigbee network running. + """ + populate_channel_registry() + establish_device_mappings() + for component in COMPONENTS: + hass.data[DATA_ZHA][component] = ( + hass.data[DATA_ZHA].get(component, {}) + ) + return ZHAGateway(hass, {}) + + +@pytest.fixture(autouse=True) +async def setup_zha(hass, config_entry): + """Load the ZHA component. + + This will init the ZHA component. It loads the component in HA so that + we can test the domains that ZHA supports without actually having a zigbee + network running. + """ + # this prevents needing an actual radio and zigbee network available + with patch('homeassistant.components.zha.async_setup_entry', + async_setup_entry): + hass.data[DATA_ZHA] = {} + + # init ZHA + await hass.config_entries.async_forward_entry_setup( + config_entry, DOMAIN) + await hass.async_block_till_done() diff --git a/tests/components/zha/test_api.py b/tests/components/zha/test_api.py new file mode 100644 index 00000000000..616a94e8b89 --- /dev/null +++ b/tests/components/zha/test_api.py @@ -0,0 +1,130 @@ +"""Test ZHA API.""" +from unittest.mock import Mock +import pytest +from homeassistant.components.switch import DOMAIN +from homeassistant.components.zha.api import ( + async_load_api, WS_DEVICE_CLUSTERS, ATTR_IEEE, TYPE, + ID, WS_DEVICE_CLUSTER_ATTRIBUTES, WS_DEVICE_CLUSTER_COMMANDS, + WS_DEVICES +) +from homeassistant.components.zha.core.const import ( + ATTR_CLUSTER_ID, ATTR_CLUSTER_TYPE, IN, IEEE, MODEL, NAME, QUIRK_APPLIED, + ATTR_MANUFACTURER, ATTR_ENDPOINT_ID +) +from .common import async_init_zigpy_device + + +@pytest.fixture +async def zha_client(hass, config_entry, zha_gateway, hass_ws_client): + """Test zha switch platform.""" + from zigpy.zcl.clusters.general import OnOff, Basic + + # load the ZHA API + async_load_api(hass, Mock(), zha_gateway) + + # create zigpy device + await async_init_zigpy_device( + hass, [OnOff.cluster_id, Basic.cluster_id], [], None, zha_gateway) + + # load up switch domain + await hass.config_entries.async_forward_entry_setup( + config_entry, DOMAIN) + await hass.async_block_till_done() + + return await hass_ws_client(hass) + + +async def test_device_clusters(hass, config_entry, zha_gateway, zha_client): + """Test getting device cluster info.""" + await zha_client.send_json({ + ID: 5, + TYPE: WS_DEVICE_CLUSTERS, + ATTR_IEEE: '00:0d:6f:00:0a:90:69:e7' + }) + + msg = await zha_client.receive_json() + + assert len(msg['result']) == 2 + + cluster_infos = sorted(msg['result'], key=lambda k: k[ID]) + + cluster_info = cluster_infos[0] + assert cluster_info[TYPE] == IN + assert cluster_info[ID] == 0 + assert cluster_info[NAME] == 'Basic' + + cluster_info = cluster_infos[1] + assert cluster_info[TYPE] == IN + assert cluster_info[ID] == 6 + assert cluster_info[NAME] == 'OnOff' + + +async def test_device_cluster_attributes( + hass, config_entry, zha_gateway, zha_client): + """Test getting device cluster attributes.""" + await zha_client.send_json({ + ID: 5, + TYPE: WS_DEVICE_CLUSTER_ATTRIBUTES, + ATTR_ENDPOINT_ID: 1, + ATTR_IEEE: '00:0d:6f:00:0a:90:69:e7', + ATTR_CLUSTER_ID: 6, + ATTR_CLUSTER_TYPE: IN + }) + + msg = await zha_client.receive_json() + + attributes = msg['result'] + assert len(attributes) == 4 + + for attribute in attributes: + assert attribute[ID] is not None + assert attribute[NAME] is not None + + +async def test_device_cluster_commands( + hass, config_entry, zha_gateway, zha_client): + """Test getting device cluster commands.""" + await zha_client.send_json({ + ID: 5, + TYPE: WS_DEVICE_CLUSTER_COMMANDS, + ATTR_ENDPOINT_ID: 1, + ATTR_IEEE: '00:0d:6f:00:0a:90:69:e7', + ATTR_CLUSTER_ID: 6, + ATTR_CLUSTER_TYPE: IN + }) + + msg = await zha_client.receive_json() + + commands = msg['result'] + assert len(commands) == 6 + + for command in commands: + assert command[ID] is not None + assert command[NAME] is not None + assert command[TYPE] is not None + + +async def test_list_devices( + hass, config_entry, zha_gateway, zha_client): + """Test getting entity cluster commands.""" + await zha_client.send_json({ + ID: 5, + TYPE: WS_DEVICES + }) + + msg = await zha_client.receive_json() + + devices = msg['result'] + assert len(devices) == 1 + + for device in devices: + assert device[IEEE] is not None + assert device[ATTR_MANUFACTURER] is not None + assert device[MODEL] is not None + assert device[NAME] is not None + assert device[QUIRK_APPLIED] is not None + assert device['entities'] is not None + + for entity_reference in device['entities']: + assert entity_reference[NAME] is not None + assert entity_reference['entity_id'] is not None diff --git a/tests/components/zha/test_binary_sensor.py b/tests/components/zha/test_binary_sensor.py new file mode 100644 index 00000000000..d0763b8fb10 --- /dev/null +++ b/tests/components/zha/test_binary_sensor.py @@ -0,0 +1,161 @@ +"""Test zha binary sensor.""" +from homeassistant.components.binary_sensor import DOMAIN +from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE +from .common import ( + async_init_zigpy_device, make_attribute, make_entity_id, + async_test_device_join, async_enable_traffic +) + + +async def test_binary_sensor(hass, config_entry, zha_gateway): + """Test zha binary_sensor platform.""" + from zigpy.zcl.clusters.security import IasZone + from zigpy.zcl.clusters.measurement import OccupancySensing + from zigpy.zcl.clusters.general import OnOff, LevelControl, Basic + from zigpy.profiles.zha import DeviceType + + # create zigpy devices + zigpy_device_zone = await async_init_zigpy_device( + hass, + [IasZone.cluster_id, Basic.cluster_id], + [], + None, + zha_gateway + ) + + zigpy_device_remote = await async_init_zigpy_device( + hass, + [Basic.cluster_id], + [OnOff.cluster_id, LevelControl.cluster_id], + DeviceType.LEVEL_CONTROL_SWITCH, + zha_gateway, + ieee="00:0d:6f:11:0a:90:69:e7", + manufacturer="FakeManufacturer", + model="FakeRemoteModel" + ) + + zigpy_device_occupancy = await async_init_zigpy_device( + hass, + [OccupancySensing.cluster_id, Basic.cluster_id], + [], + None, + zha_gateway, + ieee="00:0d:6f:11:9a:90:69:e7", + manufacturer="FakeOccupancy", + model="FakeOccupancyModel" + ) + + # load up binary_sensor domain + await hass.config_entries.async_forward_entry_setup( + config_entry, DOMAIN) + await hass.async_block_till_done() + + # on off binary_sensor + zone_cluster = zigpy_device_zone.endpoints.get( + 1).ias_zone + zone_entity_id = make_entity_id(DOMAIN, zigpy_device_zone, zone_cluster) + zone_zha_device = zha_gateway.get_device(str(zigpy_device_zone.ieee)) + + # occupancy binary_sensor + occupancy_cluster = zigpy_device_occupancy.endpoints.get( + 1).occupancy + occupancy_entity_id = make_entity_id( + DOMAIN, zigpy_device_occupancy, occupancy_cluster) + occupancy_zha_device = zha_gateway.get_device( + str(zigpy_device_occupancy.ieee)) + + # dimmable binary_sensor + remote_on_off_cluster = zigpy_device_remote.endpoints.get( + 1).out_clusters[OnOff.cluster_id] + remote_level_cluster = zigpy_device_remote.endpoints.get( + 1).out_clusters[LevelControl.cluster_id] + remote_entity_id = make_entity_id(DOMAIN, zigpy_device_remote, + remote_on_off_cluster, + use_suffix=False) + remote_zha_device = zha_gateway.get_device(str(zigpy_device_remote.ieee)) + + # test that the sensors exist and are in the unavailable state + assert hass.states.get(zone_entity_id).state == STATE_UNAVAILABLE + assert hass.states.get(remote_entity_id).state == STATE_UNAVAILABLE + assert hass.states.get(occupancy_entity_id).state == STATE_UNAVAILABLE + + await async_enable_traffic(hass, zha_gateway, + [zone_zha_device, remote_zha_device, + occupancy_zha_device]) + + # test that the sensors exist and are in the off state + assert hass.states.get(zone_entity_id).state == STATE_OFF + assert hass.states.get(remote_entity_id).state == STATE_OFF + assert hass.states.get(occupancy_entity_id).state == STATE_OFF + + # test getting messages that trigger and reset the sensors + await async_test_binary_sensor_on_off(hass, occupancy_cluster, + occupancy_entity_id) + await async_test_binary_sensor_on_off(hass, remote_on_off_cluster, + remote_entity_id) + + # test changing the level attribute for dimming remotes + await async_test_remote_level( + hass, remote_level_cluster, remote_entity_id, 150, STATE_ON) + await async_test_remote_level( + hass, remote_level_cluster, remote_entity_id, 0, STATE_OFF) + await async_test_remote_level( + hass, remote_level_cluster, remote_entity_id, 255, STATE_ON) + + await async_test_remote_move_level( + hass, remote_level_cluster, remote_entity_id, 20, STATE_ON) + + # test IASZone binary sensors + await async_test_iaszone_on_off(hass, zone_cluster, zone_entity_id) + + # test new sensor join + await async_test_device_join( + hass, zha_gateway, OccupancySensing.cluster_id, DOMAIN) + + +async def async_test_binary_sensor_on_off(hass, cluster, entity_id): + """Test getting on and off messages for binary sensors.""" + # binary sensor on + attr = make_attribute(0, 1) + cluster.handle_message(False, 1, 0x0a, [[attr]]) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_ON + + # binary sensor off + attr.value.value = 0 + cluster.handle_message(False, 0, 0x0a, [[attr]]) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_OFF + + +async def async_test_remote_level(hass, cluster, entity_id, level, + expected_state): + """Test dimmer functionality from the remote.""" + attr = make_attribute(0, level) + cluster.handle_message(False, 1, 0x0a, [[attr]]) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == expected_state + assert hass.states.get(entity_id).attributes.get('level') == level + + +async def async_test_remote_move_level(hass, cluster, entity_id, change, + expected_state): + """Test move to level command.""" + level = hass.states.get(entity_id).attributes.get('level') + cluster.listener_event('cluster_command', 1, 1, [1, change]) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == expected_state + assert hass.states.get(entity_id).attributes.get('level') == level - change + + +async def async_test_iaszone_on_off(hass, cluster, entity_id): + """Test getting on and off messages for iaszone binary sensors.""" + # binary sensor on + cluster.listener_event('cluster_command', 1, 0, [1]) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_ON + + # binary sensor off + cluster.listener_event('cluster_command', 1, 0, [0]) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_OFF diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py new file mode 100644 index 00000000000..a70e0e5ea40 --- /dev/null +++ b/tests/components/zha/test_fan.py @@ -0,0 +1,122 @@ +"""Test zha fan.""" +from unittest.mock import call, patch +from homeassistant.components import fan +from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE +from homeassistant.components.fan import ( + ATTR_SPEED, DOMAIN, SERVICE_SET_SPEED +) +from homeassistant.const import ( + ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF) +from tests.common import mock_coro +from .common import ( + async_init_zigpy_device, make_attribute, make_entity_id, + async_test_device_join, async_enable_traffic +) + + +async def test_fan(hass, config_entry, zha_gateway): + """Test zha fan platform.""" + from zigpy.zcl.clusters.hvac import Fan + from zigpy.zcl.clusters.general import Basic + from zigpy.zcl.foundation import Status + + # create zigpy device + zigpy_device = await async_init_zigpy_device( + hass, [Fan.cluster_id, Basic.cluster_id], [], None, zha_gateway) + + # load up fan domain + await hass.config_entries.async_forward_entry_setup( + config_entry, DOMAIN) + await hass.async_block_till_done() + + cluster = zigpy_device.endpoints.get(1).fan + entity_id = make_entity_id(DOMAIN, zigpy_device, cluster) + zha_device = zha_gateway.get_device(str(zigpy_device.ieee)) + + # test that the fan was created and that it is unavailable + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, zha_gateway, [zha_device]) + + # test that the state has changed from unavailable to off + assert hass.states.get(entity_id).state == STATE_OFF + + # turn on at fan + attr = make_attribute(0, 1) + cluster.handle_message(False, 1, 0x0a, [[attr]]) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_ON + + # turn off at fan + attr.value.value = 0 + cluster.handle_message(False, 0, 0x0a, [[attr]]) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_OFF + + # turn on from HA + with patch( + 'zigpy.zcl.Cluster.write_attributes', + return_value=mock_coro([Status.SUCCESS, Status.SUCCESS])): + # turn on via UI + await async_turn_on(hass, entity_id) + assert len(cluster.write_attributes.mock_calls) == 1 + assert cluster.write_attributes.call_args == call( + {'fan_mode': 2}) + + # turn off from HA + with patch( + 'zigpy.zcl.Cluster.write_attributes', + return_value=mock_coro([Status.SUCCESS, Status.SUCCESS])): + # turn off via UI + await async_turn_off(hass, entity_id) + assert len(cluster.write_attributes.mock_calls) == 1 + assert cluster.write_attributes.call_args == call( + {'fan_mode': 0}) + + # change speed from HA + with patch( + 'zigpy.zcl.Cluster.write_attributes', + return_value=mock_coro([Status.SUCCESS, Status.SUCCESS])): + # turn on via UI + await async_set_speed(hass, entity_id, speed=fan.SPEED_HIGH) + assert len(cluster.write_attributes.mock_calls) == 1 + assert cluster.write_attributes.call_args == call( + {'fan_mode': 3}) + + # test adding new fan to the network and HA + await async_test_device_join(hass, zha_gateway, Fan.cluster_id, DOMAIN) + + +async def async_turn_on(hass, entity_id, speed=None): + """Turn fan on.""" + data = { + key: value for key, value in [ + (ATTR_ENTITY_ID, entity_id), + (ATTR_SPEED, speed), + ] if value is not None + } + + await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, data, blocking=True) + + +async def async_turn_off(hass, entity_id): + """Turn fan off.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + + await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, data, blocking=True) + + +async def async_set_speed(hass, entity_id, speed=None): + """Set speed for specified fan.""" + data = { + key: value for key, value in [ + (ATTR_ENTITY_ID, entity_id), + (ATTR_SPEED, speed), + ] if value is not None + } + + await hass.services.async_call( + DOMAIN, SERVICE_SET_SPEED, data, blocking=True) diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py new file mode 100644 index 00000000000..38d7caedaad --- /dev/null +++ b/tests/components/zha/test_light.py @@ -0,0 +1,185 @@ +"""Test zha light.""" +from unittest.mock import call, patch +from homeassistant.components.light import DOMAIN +from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE +from tests.common import mock_coro +from .common import ( + async_init_zigpy_device, make_attribute, make_entity_id, + async_test_device_join, async_enable_traffic +) + +ON = 1 +OFF = 0 + + +async def test_light(hass, config_entry, zha_gateway): + """Test zha light platform.""" + from zigpy.zcl.clusters.general import OnOff, LevelControl, Basic + from zigpy.profiles.zha import DeviceType + + # create zigpy devices + zigpy_device_on_off = await async_init_zigpy_device( + hass, + [OnOff.cluster_id, Basic.cluster_id], + [], + DeviceType.ON_OFF_LIGHT, + zha_gateway + ) + + zigpy_device_level = await async_init_zigpy_device( + hass, + [OnOff.cluster_id, LevelControl.cluster_id, Basic.cluster_id], + [], + DeviceType.ON_OFF_LIGHT, + zha_gateway, + ieee="00:0d:6f:11:0a:90:69:e7", + manufacturer="FakeLevelManufacturer", + model="FakeLevelModel" + ) + + # load up light domain + await hass.config_entries.async_forward_entry_setup( + config_entry, DOMAIN) + await hass.async_block_till_done() + + # on off light + on_off_device_on_off_cluster = zigpy_device_on_off.endpoints.get(1).on_off + on_off_entity_id = make_entity_id(DOMAIN, zigpy_device_on_off, + on_off_device_on_off_cluster, + use_suffix=False) + on_off_zha_device = zha_gateway.get_device(str(zigpy_device_on_off.ieee)) + + # dimmable light + level_device_on_off_cluster = zigpy_device_level.endpoints.get(1).on_off + level_device_level_cluster = zigpy_device_level.endpoints.get(1).level + level_entity_id = make_entity_id(DOMAIN, zigpy_device_level, + level_device_on_off_cluster, + use_suffix=False) + level_zha_device = zha_gateway.get_device(str(zigpy_device_level.ieee)) + + # test that the lights were created and that they are unavailable + assert hass.states.get(on_off_entity_id).state == STATE_UNAVAILABLE + assert hass.states.get(level_entity_id).state == STATE_UNAVAILABLE + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, zha_gateway, + [on_off_zha_device, level_zha_device]) + + # test that the lights were created and are off + assert hass.states.get(on_off_entity_id).state == STATE_OFF + assert hass.states.get(level_entity_id).state == STATE_OFF + + # test turning the lights on and off from the light + await async_test_on_off_from_light( + hass, on_off_device_on_off_cluster, on_off_entity_id) + + await async_test_on_off_from_light( + hass, level_device_on_off_cluster, level_entity_id) + + # test turning the lights on and off from the HA + await async_test_on_off_from_hass( + hass, on_off_device_on_off_cluster, on_off_entity_id) + + await async_test_level_on_off_from_hass( + hass, level_device_on_off_cluster, level_entity_id) + + # test turning the lights on and off from the light + await async_test_on_from_light( + hass, level_device_on_off_cluster, level_entity_id) + + # test getting a brightness change from the network + await async_test_dimmer_from_light( + hass, level_device_level_cluster, level_entity_id, 150, STATE_ON) + + # test adding a new light to the network and HA + await async_test_device_join( + hass, zha_gateway, OnOff.cluster_id, + DOMAIN, device_type=DeviceType.ON_OFF_LIGHT) + + +async def async_test_on_off_from_light(hass, cluster, entity_id): + """Test on off functionality from the light.""" + # turn on at light + attr = make_attribute(0, 1) + cluster.handle_message(False, 1, 0x0a, [[attr]]) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_ON + + # turn off at light + attr.value.value = 0 + cluster.handle_message(False, 0, 0x0a, [[attr]]) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_OFF + + +async def async_test_on_from_light(hass, cluster, entity_id): + """Test on off functionality from the light.""" + # turn on at light + attr = make_attribute(0, 1) + cluster.handle_message(False, 1, 0x0a, [[attr]]) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_ON + + +async def async_test_on_off_from_hass(hass, cluster, entity_id): + """Test on off functionality from hass.""" + from zigpy.zcl.foundation import Status + with patch( + 'zigpy.zcl.Cluster.request', + return_value=mock_coro([Status.SUCCESS, Status.SUCCESS])): + # turn on via UI + await hass.services.async_call(DOMAIN, 'turn_on', { + 'entity_id': entity_id + }, blocking=True) + assert len(cluster.request.mock_calls) == 1 + assert cluster.request.call_args == call( + False, ON, (), expect_reply=True, manufacturer=None) + + await async_test_off_from_hass(hass, cluster, entity_id) + + +async def async_test_off_from_hass(hass, cluster, entity_id): + """Test turning off the light from homeassistant.""" + from zigpy.zcl.foundation import Status + with patch( + 'zigpy.zcl.Cluster.request', + return_value=mock_coro([Status.SUCCESS, Status.SUCCESS])): + # turn off via UI + await hass.services.async_call(DOMAIN, 'turn_off', { + 'entity_id': entity_id + }, blocking=True) + assert len(cluster.request.mock_calls) == 1 + assert cluster.request.call_args == call( + False, OFF, (), expect_reply=True, manufacturer=None) + + +async def async_test_level_on_off_from_hass(hass, cluster, entity_id): + """Test on off functionality from hass.""" + from zigpy import types + from zigpy.zcl.foundation import Status + with patch( + 'zigpy.zcl.Cluster.request', + return_value=mock_coro([Status.SUCCESS, Status.SUCCESS])): + # turn on via UI + await hass.services.async_call(DOMAIN, 'turn_on', { + 'entity_id': entity_id + }, blocking=True) + assert len(cluster.request.mock_calls) == 1 + assert cluster.request.call_args == call( + False, 4, (types.uint8_t, types.uint16_t), 255, 5.0, + expect_reply=True, manufacturer=None) + + await async_test_off_from_hass(hass, cluster, entity_id) + + +async def async_test_dimmer_from_light(hass, cluster, entity_id, + level, expected_state): + """Test dimmer functionality from the light.""" + attr = make_attribute(0, level) + cluster.handle_message(False, 1, 0x0a, [[attr]]) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == expected_state + # hass uses None for brightness of 0 in state attributes + if level == 0: + level = None + assert hass.states.get(entity_id).attributes.get('brightness') == level diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py new file mode 100644 index 00000000000..c348ef0d0a7 --- /dev/null +++ b/tests/components/zha/test_sensor.py @@ -0,0 +1,178 @@ +"""Test zha sensor.""" +from homeassistant.components.sensor import DOMAIN +from homeassistant.const import STATE_UNKNOWN, STATE_UNAVAILABLE +from .common import ( + async_init_zigpy_device, make_attribute, make_entity_id, + async_test_device_join, async_enable_traffic +) + + +async def test_sensor(hass, config_entry, zha_gateway): + """Test zha sensor platform.""" + from zigpy.zcl.clusters.measurement import ( + RelativeHumidity, TemperatureMeasurement, PressureMeasurement, + IlluminanceMeasurement + ) + from zigpy.zcl.clusters.smartenergy import Metering + from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement + + # list of cluster ids to create devices and sensor entities for + cluster_ids = [ + RelativeHumidity.cluster_id, + TemperatureMeasurement.cluster_id, + PressureMeasurement.cluster_id, + IlluminanceMeasurement.cluster_id, + Metering.cluster_id, + ElectricalMeasurement.cluster_id + ] + + # devices that were created from cluster_ids list above + zigpy_device_infos = await async_build_devices( + hass, zha_gateway, config_entry, cluster_ids) + + # ensure the sensor entity was created for each id in cluster_ids + for cluster_id in cluster_ids: + zigpy_device_info = zigpy_device_infos[cluster_id] + entity_id = zigpy_device_info["entity_id"] + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + # allow traffic to flow through the gateway and devices + await async_enable_traffic(hass, zha_gateway, [ + zigpy_device_info["zha_device"] for zigpy_device_info in + zigpy_device_infos.values()]) + + # test that the sensors now have a state of unknown + for cluster_id in cluster_ids: + zigpy_device_info = zigpy_device_infos[cluster_id] + entity_id = zigpy_device_info["entity_id"] + assert hass.states.get(entity_id).state == STATE_UNKNOWN + + # get the humidity device info and test the associated sensor logic + device_info = zigpy_device_infos[RelativeHumidity.cluster_id] + await async_test_humidity(hass, device_info) + + # get the temperature device info and test the associated sensor logic + device_info = zigpy_device_infos[TemperatureMeasurement.cluster_id] + await async_test_temperature(hass, device_info) + + # get the pressure device info and test the associated sensor logic + device_info = zigpy_device_infos[PressureMeasurement.cluster_id] + await async_test_pressure(hass, device_info) + + # get the illuminance device info and test the associated sensor logic + device_info = zigpy_device_infos[IlluminanceMeasurement.cluster_id] + await async_test_illuminance(hass, device_info) + + # get the metering device info and test the associated sensor logic + device_info = zigpy_device_infos[Metering.cluster_id] + await async_test_metering(hass, device_info) + + # get the electrical_measurement device info and test the associated + # sensor logic + device_info = zigpy_device_infos[ElectricalMeasurement.cluster_id] + await async_test_electrical_measurement(hass, device_info) + + # test joining a new temperature sensor to the network + await async_test_device_join( + hass, zha_gateway, TemperatureMeasurement.cluster_id, DOMAIN) + + +async def async_build_devices(hass, zha_gateway, config_entry, cluster_ids): + """Build a zigpy device for each cluster id. + + This will build devices for all cluster ids that exist in cluster_ids. + They get added to the network and then the sensor component is loaded + which will cause sensor entites to get created for each device. + A dict containing relevant device info for testing is returned. It contains + the entity id, zigpy device, and the zigbee cluster for the sensor. + """ + from zigpy.zcl.clusters.general import Basic + device_infos = {} + counter = 0 + for cluster_id in cluster_ids: + # create zigpy device + device_infos[cluster_id] = {"zigpy_device": None} + device_infos[cluster_id]["zigpy_device"] = await \ + async_init_zigpy_device( + hass, [cluster_id, Basic.cluster_id], [], None, zha_gateway, + ieee="{}0:15:8d:00:02:32:4f:32".format(counter), + manufacturer="Fake{}".format(cluster_id), + model="FakeModel{}".format(cluster_id)) + + counter += 1 + + # load up sensor domain + await hass.config_entries.async_forward_entry_setup( + config_entry, DOMAIN) + await hass.async_block_till_done() + + # put the other relevant info in the device info dict + for cluster_id in cluster_ids: + device_info = device_infos[cluster_id] + zigpy_device = device_info["zigpy_device"] + device_info["cluster"] = zigpy_device.endpoints.get( + 1).in_clusters[cluster_id] + device_info["entity_id"] = make_entity_id( + DOMAIN, zigpy_device, device_info["cluster"]) + device_info["zha_device"] = zha_gateway.get_device( + str(zigpy_device.ieee)) + return device_infos + + +async def async_test_humidity(hass, device_info): + """Test humidity sensor.""" + await send_attribute_report(hass, device_info["cluster"], 0, 1000) + assert_state(hass, device_info, '10.0', '%') + + +async def async_test_temperature(hass, device_info): + """Test temperature sensor.""" + await send_attribute_report(hass, device_info["cluster"], 0, 2900) + assert_state(hass, device_info, '29.0', '°C') + + +async def async_test_pressure(hass, device_info): + """Test pressure sensor.""" + await send_attribute_report(hass, device_info["cluster"], 0, 1000) + assert_state(hass, device_info, '1000', 'hPa') + + +async def async_test_illuminance(hass, device_info): + """Test illuminance sensor.""" + await send_attribute_report(hass, device_info["cluster"], 0, 10) + assert_state(hass, device_info, '10', 'lx') + + +async def async_test_metering(hass, device_info): + """Test metering sensor.""" + await send_attribute_report(hass, device_info["cluster"], 1024, 10) + assert_state(hass, device_info, '10', 'W') + + +async def async_test_electrical_measurement(hass, device_info): + """Test electrical measurement sensor.""" + await send_attribute_report(hass, device_info["cluster"], 1291, 100) + assert_state(hass, device_info, '10.0', 'W') + + +async def send_attribute_report(hass, cluster, attrid, value): + """Cause the sensor to receive an attribute report from the network. + + This is to simulate the normal device communication that happens when a + device is paired to the zigbee network. + """ + attr = make_attribute(attrid, value) + cluster.handle_message(False, 1, 0x0a, [[attr]]) + await hass.async_block_till_done() + + +def assert_state(hass, device_info, state, unit_of_measurement): + """Check that the state is what is expected. + + This is used to ensure that the logic in each sensor class handled the + attribute report it received correctly. + """ + hass_state = hass.states.get(device_info["entity_id"]) + assert hass_state.state == state + assert hass_state.attributes.get('unit_of_measurement') == \ + unit_of_measurement diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py new file mode 100644 index 00000000000..1fc21e34cd8 --- /dev/null +++ b/tests/components/zha/test_switch.py @@ -0,0 +1,80 @@ +"""Test zha switch.""" +from unittest.mock import call, patch +from homeassistant.components.switch import DOMAIN +from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE +from tests.common import mock_coro +from .common import ( + async_init_zigpy_device, make_attribute, make_entity_id, + async_test_device_join, async_enable_traffic +) + +ON = 1 +OFF = 0 + + +async def test_switch(hass, config_entry, zha_gateway): + """Test zha switch platform.""" + from zigpy.zcl.clusters.general import OnOff, Basic + from zigpy.zcl.foundation import Status + + # create zigpy device + zigpy_device = await async_init_zigpy_device( + hass, [OnOff.cluster_id, Basic.cluster_id], [], None, zha_gateway) + + # load up switch domain + await hass.config_entries.async_forward_entry_setup( + config_entry, DOMAIN) + await hass.async_block_till_done() + + cluster = zigpy_device.endpoints.get(1).on_off + entity_id = make_entity_id(DOMAIN, zigpy_device, cluster) + zha_device = zha_gateway.get_device(str(zigpy_device.ieee)) + + # test that the switch was created and that its state is unavailable + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, zha_gateway, [zha_device]) + + # test that the state has changed from unavailable to off + assert hass.states.get(entity_id).state == STATE_OFF + + # turn on at switch + attr = make_attribute(0, 1) + cluster.handle_message(False, 1, 0x0a, [[attr]]) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_ON + + # turn off at switch + attr.value.value = 0 + cluster.handle_message(False, 0, 0x0a, [[attr]]) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_OFF + + # turn on from HA + with patch( + 'zigpy.zcl.Cluster.request', + return_value=mock_coro([Status.SUCCESS, Status.SUCCESS])): + # turn on via UI + await hass.services.async_call(DOMAIN, 'turn_on', { + 'entity_id': entity_id + }, blocking=True) + assert len(cluster.request.mock_calls) == 1 + assert cluster.request.call_args == call( + False, ON, (), expect_reply=True, manufacturer=None) + + # turn off from HA + with patch( + 'zigpy.zcl.Cluster.request', + return_value=mock_coro([Status.SUCCESS, Status.SUCCESS])): + # turn off via UI + await hass.services.async_call(DOMAIN, 'turn_off', { + 'entity_id': entity_id + }, blocking=True) + assert len(cluster.request.mock_calls) == 1 + assert cluster.request.call_args == call( + False, OFF, (), expect_reply=True, manufacturer=None) + + # test joining a new switch to the network and HA + await async_test_device_join( + hass, zha_gateway, OnOff.cluster_id, DOMAIN) diff --git a/tests/components/zwave/conftest.py b/tests/components/zwave/conftest.py new file mode 100644 index 00000000000..7a1aae357ad --- /dev/null +++ b/tests/components/zwave/conftest.py @@ -0,0 +1,24 @@ +"""Fixtures for Z-Wave tests.""" +from unittest.mock import patch, MagicMock + +import pytest + +from tests.mock.zwave import MockNetwork, MockOption + + +@pytest.fixture +def mock_openzwave(): + """Mock out Open Z-Wave.""" + base_mock = MagicMock() + libopenzwave = base_mock.libopenzwave + libopenzwave.__file__ = 'test' + base_mock.network.ZWaveNetwork = MockNetwork + base_mock.option.ZWaveOption = MockOption + + with patch.dict('sys.modules', { + 'libopenzwave': libopenzwave, + 'openzwave.option': base_mock.option, + 'openzwave.network': base_mock.network, + 'openzwave.group': base_mock.group, + }): + yield base_mock diff --git a/tests/components/binary_sensor/test_zwave.py b/tests/components/zwave/test_binary_sensor.py similarity index 78% rename from tests/components/binary_sensor/test_zwave.py rename to tests/components/zwave/test_binary_sensor.py index f33e8a83e1e..ee68971bc3e 100644 --- a/tests/components/binary_sensor/test_zwave.py +++ b/tests/components/zwave/test_binary_sensor.py @@ -3,8 +3,7 @@ import datetime from unittest.mock import patch -from homeassistant.components.zwave import const -from homeassistant.components.binary_sensor import zwave +from homeassistant.components.zwave import const, binary_sensor from tests.mock.zwave import ( MockNode, MockValue, MockEntityValues, value_changed) @@ -16,7 +15,7 @@ def test_get_device_detects_none(mock_openzwave): value = MockValue(data=False, node=node) values = MockEntityValues(primary=value) - device = zwave.get_device(node=node, values=values, node_config={}) + device = binary_sensor.get_device(node=node, values=values, node_config={}) assert device is None @@ -27,8 +26,8 @@ def test_get_device_detects_trigger_sensor(mock_openzwave): value = MockValue(data=False, node=node) values = MockEntityValues(primary=value) - device = zwave.get_device(node=node, values=values, node_config={}) - assert isinstance(device, zwave.ZWaveTriggerSensor) + device = binary_sensor.get_device(node=node, values=values, node_config={}) + assert isinstance(device, binary_sensor.ZWaveTriggerSensor) assert device.device_class == "motion" @@ -39,8 +38,8 @@ def test_get_device_detects_workaround_sensor(mock_openzwave): command_class=const.COMMAND_CLASS_SENSOR_ALARM) values = MockEntityValues(primary=value) - device = zwave.get_device(node=node, values=values, node_config={}) - assert isinstance(device, zwave.ZWaveBinarySensor) + device = binary_sensor.get_device(node=node, values=values, node_config={}) + assert isinstance(device, binary_sensor.ZWaveBinarySensor) def test_get_device_detects_sensor(mock_openzwave): @@ -50,8 +49,8 @@ def test_get_device_detects_sensor(mock_openzwave): command_class=const.COMMAND_CLASS_SENSOR_BINARY) values = MockEntityValues(primary=value) - device = zwave.get_device(node=node, values=values, node_config={}) - assert isinstance(device, zwave.ZWaveBinarySensor) + device = binary_sensor.get_device(node=node, values=values, node_config={}) + assert isinstance(device, binary_sensor.ZWaveBinarySensor) def test_binary_sensor_value_changed(mock_openzwave): @@ -60,7 +59,7 @@ def test_binary_sensor_value_changed(mock_openzwave): value = MockValue(data=False, node=node, command_class=const.COMMAND_CLASS_SENSOR_BINARY) values = MockEntityValues(primary=value) - device = zwave.get_device(node=node, values=values, node_config={}) + device = binary_sensor.get_device(node=node, values=values, node_config={}) assert not device.is_on @@ -77,7 +76,7 @@ async def test_trigger_sensor_value_changed(hass, mock_openzwave): value = MockValue(data=False, node=node) value_off_delay = MockValue(data=15, node=node) values = MockEntityValues(primary=value, off_delay=value_off_delay) - device = zwave.get_device(node=node, values=values, node_config={}) + device = binary_sensor.get_device(node=node, values=values, node_config={}) assert not device.is_on diff --git a/tests/components/climate/test_zwave.py b/tests/components/zwave/test_climate.py similarity index 91% rename from tests/components/climate/test_zwave.py rename to tests/components/zwave/test_climate.py index 39a85ab493f..9a9ed41381f 100644 --- a/tests/components/climate/test_zwave.py +++ b/tests/components/zwave/test_climate.py @@ -1,7 +1,8 @@ """Test Z-Wave climate devices.""" import pytest -from homeassistant.components.climate import zwave, STATE_COOL, STATE_HEAT +from homeassistant.components.climate import STATE_COOL, STATE_HEAT +from homeassistant.components.zwave import climate from homeassistant.const import ( STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) @@ -21,7 +22,7 @@ def device(hass, mock_openzwave): operating_state=MockValue(data=6, node=node), fan_state=MockValue(data=7, node=node), ) - device = zwave.get_device(hass, node=node, values=values, node_config={}) + device = climate.get_device(hass, node=node, values=values, node_config={}) yield device @@ -41,7 +42,7 @@ def device_zxt_120(hass, mock_openzwave): zxt_120_swing_mode=MockValue( data='test3', data_items=[6, 7, 8], node=node), ) - device = zwave.get_device(hass, node=node, values=values, node_config={}) + device = climate.get_device(hass, node=node, values=values, node_config={}) yield device @@ -59,7 +60,7 @@ def device_mapping(hass, mock_openzwave): operating_state=MockValue(data=6, node=node), fan_state=MockValue(data=7, node=node), ) - device = zwave.get_device(hass, node=node, values=values, node_config={}) + device = climate.get_device(hass, node=node, values=values, node_config={}) yield device @@ -195,15 +196,15 @@ def test_fan_mode_value_changed(device): def test_operating_state_value_changed(device): """Test values changed for climate device.""" - assert device.device_state_attributes[zwave.ATTR_OPERATING_STATE] == 6 + assert device.device_state_attributes[climate.ATTR_OPERATING_STATE] == 6 device.values.operating_state.data = 8 value_changed(device.values.operating_state) - assert device.device_state_attributes[zwave.ATTR_OPERATING_STATE] == 8 + assert device.device_state_attributes[climate.ATTR_OPERATING_STATE] == 8 def test_fan_state_value_changed(device): """Test values changed for climate device.""" - assert device.device_state_attributes[zwave.ATTR_FAN_STATE] == 7 + assert device.device_state_attributes[climate.ATTR_FAN_STATE] == 7 device.values.fan_state.data = 9 value_changed(device.values.fan_state) - assert device.device_state_attributes[zwave.ATTR_FAN_STATE] == 9 + assert device.device_state_attributes[climate.ATTR_FAN_STATE] == 9 diff --git a/tests/components/cover/test_zwave.py b/tests/components/zwave/test_cover.py similarity index 84% rename from tests/components/cover/test_zwave.py rename to tests/components/zwave/test_cover.py index b870075d39f..ce34111c612 100644 --- a/tests/components/cover/test_zwave.py +++ b/tests/components/zwave/test_cover.py @@ -1,8 +1,9 @@ """Test Z-Wave cover devices.""" from unittest.mock import MagicMock -from homeassistant.components.cover import zwave, SUPPORT_OPEN, SUPPORT_CLOSE -from homeassistant.components.zwave import const +from homeassistant.components.cover import SUPPORT_OPEN, SUPPORT_CLOSE +from homeassistant.components.zwave import ( + const, cover, CONF_INVERT_OPENCLOSE_BUTTONS) from tests.mock.zwave import ( MockNode, MockValue, MockEntityValues, value_changed) @@ -14,22 +15,22 @@ def test_get_device_detects_none(hass, mock_openzwave): value = MockValue(data=0, node=node) values = MockEntityValues(primary=value, node=node) - device = zwave.get_device(hass=hass, node=node, values=values, + device = cover.get_device(hass=hass, node=node, values=values, node_config={}) assert device is None def test_get_device_detects_rollershutter(hass, mock_openzwave): """Test device returns rollershutter.""" - hass.data[zwave.zwave.DATA_NETWORK] = MagicMock() + hass.data[const.DATA_NETWORK] = MagicMock() node = MockNode() value = MockValue(data=0, node=node, command_class=const.COMMAND_CLASS_SWITCH_MULTILEVEL) values = MockEntityValues(primary=value, open=None, close=None, node=node) - device = zwave.get_device(hass=hass, node=node, values=values, + device = cover.get_device(hass=hass, node=node, values=values, node_config={}) - assert isinstance(device, zwave.ZwaveRollershutter) + assert isinstance(device, cover.ZwaveRollershutter) def test_get_device_detects_garagedoor_switch(hass, mock_openzwave): @@ -39,9 +40,9 @@ def test_get_device_detects_garagedoor_switch(hass, mock_openzwave): command_class=const.COMMAND_CLASS_SWITCH_BINARY) values = MockEntityValues(primary=value, node=node) - device = zwave.get_device(hass=hass, node=node, values=values, + device = cover.get_device(hass=hass, node=node, values=values, node_config={}) - assert isinstance(device, zwave.ZwaveGarageDoorSwitch) + assert isinstance(device, cover.ZwaveGarageDoorSwitch) assert device.device_class == "garage" assert device.supported_features == SUPPORT_OPEN | SUPPORT_CLOSE @@ -53,21 +54,21 @@ def test_get_device_detects_garagedoor_barrier(hass, mock_openzwave): command_class=const.COMMAND_CLASS_BARRIER_OPERATOR) values = MockEntityValues(primary=value, node=node) - device = zwave.get_device(hass=hass, node=node, values=values, + device = cover.get_device(hass=hass, node=node, values=values, node_config={}) - assert isinstance(device, zwave.ZwaveGarageDoorBarrier) + assert isinstance(device, cover.ZwaveGarageDoorBarrier) assert device.device_class == "garage" assert device.supported_features == SUPPORT_OPEN | SUPPORT_CLOSE def test_roller_no_position_workaround(hass, mock_openzwave): """Test position changed.""" - hass.data[zwave.zwave.DATA_NETWORK] = MagicMock() + hass.data[const.DATA_NETWORK] = MagicMock() node = MockNode(manufacturer_id='0047', product_type='5a52') value = MockValue(data=45, node=node, command_class=const.COMMAND_CLASS_SWITCH_MULTILEVEL) values = MockEntityValues(primary=value, open=None, close=None, node=node) - device = zwave.get_device(hass=hass, node=node, values=values, + device = cover.get_device(hass=hass, node=node, values=values, node_config={}) assert device.current_cover_position is None @@ -75,12 +76,12 @@ def test_roller_no_position_workaround(hass, mock_openzwave): def test_roller_value_changed(hass, mock_openzwave): """Test position changed.""" - hass.data[zwave.zwave.DATA_NETWORK] = MagicMock() + hass.data[const.DATA_NETWORK] = MagicMock() node = MockNode() value = MockValue(data=None, node=node, command_class=const.COMMAND_CLASS_SWITCH_MULTILEVEL) values = MockEntityValues(primary=value, open=None, close=None, node=node) - device = zwave.get_device(hass=hass, node=node, values=values, + device = cover.get_device(hass=hass, node=node, values=values, node_config={}) assert device.current_cover_position is None @@ -107,7 +108,7 @@ def test_roller_value_changed(hass, mock_openzwave): def test_roller_commands(hass, mock_openzwave): """Test position changed.""" - mock_network = hass.data[zwave.zwave.DATA_NETWORK] = MagicMock() + mock_network = hass.data[const.DATA_NETWORK] = MagicMock() node = MockNode() value = MockValue(data=50, node=node, command_class=const.COMMAND_CLASS_SWITCH_MULTILEVEL) @@ -115,7 +116,7 @@ def test_roller_commands(hass, mock_openzwave): close_value = MockValue(data=False, node=node) values = MockEntityValues(primary=value, open=open_value, close=close_value, node=node) - device = zwave.get_device(hass=hass, node=node, values=values, + device = cover.get_device(hass=hass, node=node, values=values, node_config={}) device.set_cover_position(position=25) @@ -142,7 +143,7 @@ def test_roller_commands(hass, mock_openzwave): def test_roller_reverse_open_close(hass, mock_openzwave): """Test position changed.""" - mock_network = hass.data[zwave.zwave.DATA_NETWORK] = MagicMock() + mock_network = hass.data[const.DATA_NETWORK] = MagicMock() node = MockNode() value = MockValue(data=50, node=node, command_class=const.COMMAND_CLASS_SWITCH_MULTILEVEL) @@ -150,11 +151,11 @@ def test_roller_reverse_open_close(hass, mock_openzwave): close_value = MockValue(data=False, node=node) values = MockEntityValues(primary=value, open=open_value, close=close_value, node=node) - device = zwave.get_device( + device = cover.get_device( hass=hass, node=node, values=values, - node_config={zwave.zwave.CONF_INVERT_OPENCLOSE_BUTTONS: True}) + node_config={CONF_INVERT_OPENCLOSE_BUTTONS: True}) device.open_cover() assert mock_network.manager.pressButton.called @@ -178,7 +179,7 @@ def test_switch_garage_value_changed(hass, mock_openzwave): value = MockValue(data=False, node=node, command_class=const.COMMAND_CLASS_SWITCH_BINARY) values = MockEntityValues(primary=value, node=node) - device = zwave.get_device(hass=hass, node=node, values=values, + device = cover.get_device(hass=hass, node=node, values=values, node_config={}) assert device.is_closed @@ -194,7 +195,7 @@ def test_switch_garage_commands(hass, mock_openzwave): value = MockValue(data=False, node=node, command_class=const.COMMAND_CLASS_SWITCH_BINARY) values = MockEntityValues(primary=value, node=node) - device = zwave.get_device(hass=hass, node=node, values=values, + device = cover.get_device(hass=hass, node=node, values=values, node_config={}) assert value.data is False @@ -210,7 +211,7 @@ def test_barrier_garage_value_changed(hass, mock_openzwave): value = MockValue(data="Closed", node=node, command_class=const.COMMAND_CLASS_BARRIER_OPERATOR) values = MockEntityValues(primary=value, node=node) - device = zwave.get_device(hass=hass, node=node, values=values, + device = cover.get_device(hass=hass, node=node, values=values, node_config={}) assert device.is_closed @@ -242,7 +243,7 @@ def test_barrier_garage_commands(hass, mock_openzwave): value = MockValue(data="Closed", node=node, command_class=const.COMMAND_CLASS_BARRIER_OPERATOR) values = MockEntityValues(primary=value, node=node) - device = zwave.get_device(hass=hass, node=node, values=values, + device = cover.get_device(hass=hass, node=node, values=values, node_config={}) assert value.data == "Closed" diff --git a/tests/components/fan/test_zwave.py b/tests/components/zwave/test_fan.py similarity index 85% rename from tests/components/fan/test_zwave.py rename to tests/components/zwave/test_fan.py index b7d7e497c03..57a60cfa303 100644 --- a/tests/components/fan/test_zwave.py +++ b/tests/components/zwave/test_fan.py @@ -1,6 +1,7 @@ """Test Z-Wave fans.""" +from homeassistant.components.zwave import fan from homeassistant.components.fan import ( - zwave, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, SUPPORT_SET_SPEED) + SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, SUPPORT_SET_SPEED) from tests.mock.zwave import ( MockNode, MockValue, MockEntityValues, value_changed) @@ -12,8 +13,8 @@ def test_get_device_detects_fan(mock_openzwave): value = MockValue(data=0, node=node) values = MockEntityValues(primary=value) - device = zwave.get_device(node=node, values=values, node_config={}) - assert isinstance(device, zwave.ZwaveFan) + device = fan.get_device(node=node, values=values, node_config={}) + assert isinstance(device, fan.ZwaveFan) assert device.supported_features == SUPPORT_SET_SPEED assert device.speed_list == [ SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] @@ -24,7 +25,7 @@ def test_fan_turn_on(mock_openzwave): node = MockNode() value = MockValue(data=0, node=node) values = MockEntityValues(primary=value) - device = zwave.get_device(node=node, values=values, node_config={}) + device = fan.get_device(node=node, values=values, node_config={}) device.turn_on() @@ -79,7 +80,7 @@ def test_fan_turn_off(mock_openzwave): node = MockNode() value = MockValue(data=46, node=node) values = MockEntityValues(primary=value) - device = zwave.get_device(node=node, values=values, node_config={}) + device = fan.get_device(node=node, values=values, node_config={}) device.turn_off() @@ -94,7 +95,7 @@ def test_fan_value_changed(mock_openzwave): node = MockNode() value = MockValue(data=0, node=node) values = MockEntityValues(primary=value) - device = zwave.get_device(node=node, values=values, node_config={}) + device = fan.get_device(node=node, values=values, node_config={}) assert not device.is_on diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 85cca89eefc..66011f3e6ee 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -10,7 +10,7 @@ from unittest.mock import patch, MagicMock from homeassistant.bootstrap import async_setup_component from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START from homeassistant.components import zwave -from homeassistant.components.binary_sensor.zwave import get_device +from homeassistant.components.zwave.binary_sensor import get_device from homeassistant.components.zwave import ( const, CONFIG_SCHEMA, CONF_DEVICE_CONFIG_GLOB, DATA_NETWORK) from homeassistant.setup import setup_component @@ -214,7 +214,7 @@ async def test_node_discovery(hass, mock_openzwave): hass.async_add_job(mock_receivers[0], node) await hass.async_block_till_done() - assert hass.states.get('zwave.mock_node').state is 'unknown' + assert hass.states.get('zwave.mock_node').state == 'unknown' async def test_unparsed_node_discovery(hass, mock_openzwave): @@ -257,7 +257,7 @@ async def test_unparsed_node_discovery(hass, mock_openzwave): assert len(mock_logger.warning.mock_calls) == 1 assert mock_logger.warning.mock_calls[0][1][1:] == \ (14, const.NODE_READY_WAIT_SECS) - assert hass.states.get('zwave.unknown_node_14').state is 'unknown' + assert hass.states.get('zwave.unknown_node_14').state == 'unknown' async def test_node_ignored(hass, mock_openzwave): @@ -307,7 +307,7 @@ async def test_value_discovery(hass, mock_openzwave): await hass.async_block_till_done() assert hass.states.get( - 'binary_sensor.mock_node_mock_value').state is 'off' + 'binary_sensor.mock_node_mock_value').state == 'off' async def test_value_discovery_existing_entity(hass, mock_openzwave): diff --git a/tests/components/light/test_zwave.py b/tests/components/zwave/test_light.py similarity index 85% rename from tests/components/light/test_zwave.py rename to tests/components/zwave/test_light.py index 5805c8eb2fb..61e960077c9 100644 --- a/tests/components/light/test_zwave.py +++ b/tests/components/zwave/test_light.py @@ -1,10 +1,10 @@ """Test Z-Wave lights.""" from unittest.mock import patch, MagicMock -import homeassistant.components.zwave -from homeassistant.components.zwave import const +from homeassistant.components import zwave +from homeassistant.components.zwave import const, light from homeassistant.components.light import ( - zwave, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, SUPPORT_COLOR, ATTR_WHITE_VALUE, SUPPORT_COLOR_TEMP, SUPPORT_WHITE_VALUE) @@ -29,8 +29,8 @@ def test_get_device_detects_dimmer(mock_openzwave): value = MockValue(data=0, node=node) values = MockLightValues(primary=value) - device = zwave.get_device(node=node, values=values, node_config={}) - assert isinstance(device, zwave.ZwaveDimmer) + device = light.get_device(node=node, values=values, node_config={}) + assert isinstance(device, light.ZwaveDimmer) assert device.supported_features == SUPPORT_BRIGHTNESS @@ -40,8 +40,8 @@ def test_get_device_detects_colorlight(mock_openzwave): value = MockValue(data=0, node=node) values = MockLightValues(primary=value) - device = zwave.get_device(node=node, values=values, node_config={}) - assert isinstance(device, zwave.ZwaveColorLight) + device = light.get_device(node=node, values=values, node_config={}) + assert isinstance(device, light.ZwaveColorLight) assert device.supported_features == SUPPORT_BRIGHTNESS | SUPPORT_COLOR @@ -51,8 +51,8 @@ def test_get_device_detects_zw098(mock_openzwave): command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) value = MockValue(data=0, node=node) values = MockLightValues(primary=value) - device = zwave.get_device(node=node, values=values, node_config={}) - assert isinstance(device, zwave.ZwaveColorLight) + device = light.get_device(node=node, values=values, node_config={}) + assert isinstance(device, light.ZwaveColorLight) assert device.supported_features == ( SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_COLOR_TEMP) @@ -66,9 +66,9 @@ def test_get_device_detects_rgbw_light(mock_openzwave): values = MockLightValues( primary=value, color=color, color_channels=color_channels) - device = zwave.get_device(node=node, values=values, node_config={}) + device = light.get_device(node=node, values=values, node_config={}) device.value_added() - assert isinstance(device, zwave.ZwaveColorLight) + assert isinstance(device, light.ZwaveColorLight) assert device.supported_features == ( SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_WHITE_VALUE) @@ -78,7 +78,7 @@ def test_dimmer_turn_on(mock_openzwave): node = MockNode() value = MockValue(data=0, node=node) values = MockLightValues(primary=value) - device = zwave.get_device(node=node, values=values, node_config={}) + device = light.get_device(node=node, values=values, node_config={}) device.turn_on() @@ -97,7 +97,7 @@ def test_dimmer_turn_on(mock_openzwave): assert value_id == value.value_id assert brightness == 46 # int(120 / 255 * 99) - with patch.object(zwave, '_LOGGER', MagicMock()) as mock_logger: + with patch.object(light, '_LOGGER', MagicMock()) as mock_logger: device.turn_on(**{ATTR_TRANSITION: 35}) assert mock_logger.debug.called assert node.set_dimmer.called @@ -110,7 +110,7 @@ def test_dimmer_min_brightness(mock_openzwave): node = MockNode() value = MockValue(data=0, node=node) values = MockLightValues(primary=value) - device = zwave.get_device(node=node, values=values, node_config={}) + device = light.get_device(node=node, values=values, node_config={}) assert not device.is_on @@ -131,7 +131,7 @@ def test_dimmer_transitions(mock_openzwave): value = MockValue(data=0, node=node) duration = MockValue(data=0, node=node) values = MockLightValues(primary=value, dimming_duration=duration) - device = zwave.get_device(node=node, values=values, node_config={}) + device = light.get_device(node=node, values=values, node_config={}) assert device.supported_features == SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION # Test turn_on @@ -174,7 +174,7 @@ def test_dimmer_turn_off(mock_openzwave): node = MockNode() value = MockValue(data=46, node=node) values = MockLightValues(primary=value) - device = zwave.get_device(node=node, values=values, node_config={}) + device = light.get_device(node=node, values=values, node_config={}) device.turn_off() @@ -189,7 +189,7 @@ def test_dimmer_value_changed(mock_openzwave): node = MockNode() value = MockValue(data=0, node=node) values = MockLightValues(primary=value) - device = zwave.get_device(node=node, values=values, node_config={}) + device = light.get_device(node=node, values=values, node_config={}) assert not device.is_on @@ -205,14 +205,14 @@ def test_dimmer_refresh_value(mock_openzwave): node = MockNode() value = MockValue(data=0, node=node) values = MockLightValues(primary=value) - device = zwave.get_device(node=node, values=values, node_config={ - homeassistant.components.zwave.CONF_REFRESH_VALUE: True, - homeassistant.components.zwave.CONF_REFRESH_DELAY: 5, + device = light.get_device(node=node, values=values, node_config={ + zwave.CONF_REFRESH_VALUE: True, + zwave.CONF_REFRESH_DELAY: 5, }) assert not device.is_on - with patch.object(zwave, 'Timer', MagicMock()) as mock_timer: + with patch.object(light, 'Timer', MagicMock()) as mock_timer: value.data = 46 value_changed(value) @@ -224,7 +224,7 @@ def test_dimmer_refresh_value(mock_openzwave): assert mock_timer().start.called assert len(mock_timer().start.mock_calls) == 1 - with patch.object(zwave, 'Timer', MagicMock()) as mock_timer_2: + with patch.object(light, 'Timer', MagicMock()) as mock_timer_2: value_changed(value) assert not device.is_on assert mock_timer().cancel.called @@ -248,7 +248,7 @@ def test_set_hs_color(mock_openzwave): color_channels = MockValue(data=0x1c, node=node) values = MockLightValues(primary=value, color=color, color_channels=color_channels) - device = zwave.get_device(node=node, values=values, node_config={}) + device = light.get_device(node=node, values=values, node_config={}) assert color.data == '#0000000000' @@ -266,7 +266,7 @@ def test_set_white_value(mock_openzwave): color_channels = MockValue(data=0x1d, node=node) values = MockLightValues(primary=value, color=color, color_channels=color_channels) - device = zwave.get_device(node=node, values=values, node_config={}) + device = light.get_device(node=node, values=values, node_config={}) assert color.data == '#0000000000' @@ -289,7 +289,7 @@ def test_disable_white_if_set_color(mock_openzwave): color_channels = MockValue(data=0x1c, node=node) values = MockLightValues(primary=value, color=color, color_channels=color_channels) - device = zwave.get_device(node=node, values=values, node_config={}) + device = light.get_device(node=node, values=values, node_config={}) device._white = 234 assert color.data == '#0000000000' @@ -311,7 +311,7 @@ def test_zw098_set_color_temp(mock_openzwave): color_channels = MockValue(data=0x1f, node=node) values = MockLightValues(primary=value, color=color, color_channels=color_channels) - device = zwave.get_device(node=node, values=values, node_config={}) + device = light.get_device(node=node, values=values, node_config={}) assert color.data == '#0000000000' @@ -333,7 +333,7 @@ def test_rgb_not_supported(mock_openzwave): color_channels = MockValue(data=0x01, node=node) values = MockLightValues(primary=value, color=color, color_channels=color_channels) - device = zwave.get_device(node=node, values=values, node_config={}) + device = light.get_device(node=node, values=values, node_config={}) assert device.hs_color is None @@ -343,7 +343,7 @@ def test_no_color_value(mock_openzwave): node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) value = MockValue(data=0, node=node) values = MockLightValues(primary=value) - device = zwave.get_device(node=node, values=values, node_config={}) + device = light.get_device(node=node, values=values, node_config={}) assert device.hs_color is None @@ -354,7 +354,7 @@ def test_no_color_channels_value(mock_openzwave): value = MockValue(data=0, node=node) color = MockValue(data='#0000000000', node=node) values = MockLightValues(primary=value, color=color) - device = zwave.get_device(node=node, values=values, node_config={}) + device = light.get_device(node=node, values=values, node_config={}) assert device.hs_color is None @@ -368,7 +368,7 @@ def test_rgb_value_changed(mock_openzwave): color_channels = MockValue(data=0x1c, node=node) values = MockLightValues(primary=value, color=color, color_channels=color_channels) - device = zwave.get_device(node=node, values=values, node_config={}) + device = light.get_device(node=node, values=values, node_config={}) assert device.hs_color == (0, 0) @@ -387,7 +387,7 @@ def test_rgbww_value_changed(mock_openzwave): color_channels = MockValue(data=0x1d, node=node) values = MockLightValues(primary=value, color=color, color_channels=color_channels) - device = zwave.get_device(node=node, values=values, node_config={}) + device = light.get_device(node=node, values=values, node_config={}) assert device.hs_color == (0, 0) assert device.white_value == 0 @@ -408,7 +408,7 @@ def test_rgbcw_value_changed(mock_openzwave): color_channels = MockValue(data=0x1e, node=node) values = MockLightValues(primary=value, color=color, color_channels=color_channels) - device = zwave.get_device(node=node, values=values, node_config={}) + device = light.get_device(node=node, values=values, node_config={}) assert device.hs_color == (0, 0) assert device.white_value == 0 @@ -430,16 +430,16 @@ def test_ct_value_changed(mock_openzwave): color_channels = MockValue(data=0x1f, node=node) values = MockLightValues(primary=value, color=color, color_channels=color_channels) - device = zwave.get_device(node=node, values=values, node_config={}) + device = light.get_device(node=node, values=values, node_config={}) - assert device.color_temp == zwave.TEMP_MID_HASS + assert device.color_temp == light.TEMP_MID_HASS color.data = '#000000ff00' value_changed(color) - assert device.color_temp == zwave.TEMP_WARM_HASS + assert device.color_temp == light.TEMP_WARM_HASS color.data = '#00000000ff' value_changed(color) - assert device.color_temp == zwave.TEMP_COLD_HASS + assert device.color_temp == light.TEMP_COLD_HASS diff --git a/tests/components/lock/test_zwave.py b/tests/components/zwave/test_lock.py similarity index 79% rename from tests/components/lock/test_zwave.py rename to tests/components/zwave/test_lock.py index 07095e4fe3e..2c49c79f4a8 100644 --- a/tests/components/lock/test_zwave.py +++ b/tests/components/zwave/test_lock.py @@ -2,8 +2,7 @@ from unittest.mock import patch, MagicMock from homeassistant import config_entries -from homeassistant.components.lock import zwave -from homeassistant.components.zwave import const +from homeassistant.components.zwave import const, lock from tests.mock.zwave import ( MockNode, MockValue, MockEntityValues, value_changed) @@ -19,8 +18,8 @@ def test_get_device_detects_lock(mock_openzwave): alarm_level=None, ) - device = zwave.get_device(node=node, values=values, node_config={}) - assert isinstance(device, zwave.ZwaveLock) + device = lock.get_device(node=node, values=values, node_config={}) + assert isinstance(device, lock.ZwaveLock) def test_lock_turn_on_and_off(mock_openzwave): @@ -32,7 +31,7 @@ def test_lock_turn_on_and_off(mock_openzwave): alarm_type=None, alarm_level=None, ) - device = zwave.get_device(node=node, values=values, node_config={}) + device = lock.get_device(node=node, values=values, node_config={}) assert not values.primary.data @@ -52,7 +51,7 @@ def test_lock_value_changed(mock_openzwave): alarm_type=None, alarm_level=None, ) - device = zwave.get_device(node=node, values=values, node_config={}) + device = lock.get_device(node=node, values=values, node_config={}) assert not device.is_locked @@ -71,7 +70,7 @@ def test_lock_state_workaround(mock_openzwave): alarm_type=None, alarm_level=None, ) - device = zwave.get_device(node=node, values=values) + device = lock.get_device(node=node, values=values) assert device.is_locked values.access_control.data = 2 value_changed(values.access_control) @@ -89,15 +88,15 @@ def test_track_message_workaround(mock_openzwave): alarm_level=None, ) - # Here we simulate an RF lock. The first zwave.get_device will call + # Here we simulate an RF lock. The first lock.get_device will call # update properties, simulating the first DoorLock report. We then trigger # a change, simulating the openzwave automatic refreshing behavior (which # is enabled for at least the lock that needs this workaround) node.stats['lastReceivedMessage'][5] = const.COMMAND_CLASS_DOOR_LOCK - device = zwave.get_device(node=node, values=values) + device = lock.get_device(node=node, values=values) value_changed(values.primary) assert device.is_locked - assert device.device_state_attributes[zwave.ATTR_NOTIFICATION] == 'RF Lock' + assert device.device_state_attributes[lock.ATTR_NOTIFICATION] == 'RF Lock' # Simulate a keypad unlock. We trigger a value_changed() which simulates # the Alarm notification received from the lock. Then, we trigger @@ -111,7 +110,7 @@ def test_track_message_workaround(mock_openzwave): values.primary.data = False value_changed(values.primary) assert not device.is_locked - assert device.device_state_attributes[zwave.ATTR_LOCK_STATUS] == \ + assert device.device_state_attributes[lock.ATTR_LOCK_STATUS] == \ 'Unlocked with Keypad by user 3' # Again, simulate an RF lock. @@ -119,7 +118,7 @@ def test_track_message_workaround(mock_openzwave): node.stats['lastReceivedMessage'][5] = const.COMMAND_CLASS_DOOR_LOCK value_changed(values.primary) assert device.is_locked - assert device.device_state_attributes[zwave.ATTR_NOTIFICATION] == 'RF Lock' + assert device.device_state_attributes[lock.ATTR_NOTIFICATION] == 'RF Lock' def test_v2btze_value_changed(mock_openzwave): @@ -132,7 +131,7 @@ def test_v2btze_value_changed(mock_openzwave): alarm_type=None, alarm_level=None, ) - device = zwave.get_device(node=node, values=values, node_config={}) + device = lock.get_device(node=node, values=values, node_config={}) assert device._v2btze assert not device.is_locked @@ -152,7 +151,7 @@ def test_alarm_type_workaround(mock_openzwave): alarm_type=MockValue(data=16, node=node), alarm_level=None, ) - device = zwave.get_device(node=node, values=values) + device = lock.get_device(node=node, values=values) assert not device.is_locked values.alarm_type.data = 18 @@ -193,9 +192,9 @@ def test_lock_access_control(mock_openzwave): alarm_type=None, alarm_level=None, ) - device = zwave.get_device(node=node, values=values, node_config={}) + device = lock.get_device(node=node, values=values, node_config={}) - assert device.device_state_attributes[zwave.ATTR_NOTIFICATION] == \ + assert device.device_state_attributes[lock.ATTR_NOTIFICATION] == \ 'Lock Jammed' @@ -208,28 +207,28 @@ def test_lock_alarm_type(mock_openzwave): alarm_type=MockValue(data=None, node=node), alarm_level=None, ) - device = zwave.get_device(node=node, values=values, node_config={}) + device = lock.get_device(node=node, values=values, node_config={}) - assert zwave.ATTR_LOCK_STATUS not in device.device_state_attributes + assert lock.ATTR_LOCK_STATUS not in device.device_state_attributes values.alarm_type.data = 21 value_changed(values.alarm_type) - assert device.device_state_attributes[zwave.ATTR_LOCK_STATUS] == \ + assert device.device_state_attributes[lock.ATTR_LOCK_STATUS] == \ 'Manually Locked None' values.alarm_type.data = 18 value_changed(values.alarm_type) - assert device.device_state_attributes[zwave.ATTR_LOCK_STATUS] == \ + assert device.device_state_attributes[lock.ATTR_LOCK_STATUS] == \ 'Locked with Keypad by user None' values.alarm_type.data = 161 value_changed(values.alarm_type) - assert device.device_state_attributes[zwave.ATTR_LOCK_STATUS] == \ + assert device.device_state_attributes[lock.ATTR_LOCK_STATUS] == \ 'Tamper Alarm: None' values.alarm_type.data = 9 value_changed(values.alarm_type) - assert device.device_state_attributes[zwave.ATTR_LOCK_STATUS] == \ + assert device.device_state_attributes[lock.ATTR_LOCK_STATUS] == \ 'Deadbolt Jammed' @@ -242,29 +241,29 @@ def test_lock_alarm_level(mock_openzwave): alarm_type=MockValue(data=None, node=node), alarm_level=MockValue(data=None, node=node), ) - device = zwave.get_device(node=node, values=values, node_config={}) + device = lock.get_device(node=node, values=values, node_config={}) - assert zwave.ATTR_LOCK_STATUS not in device.device_state_attributes + assert lock.ATTR_LOCK_STATUS not in device.device_state_attributes values.alarm_type.data = 21 values.alarm_level.data = 1 value_changed(values.alarm_type) value_changed(values.alarm_level) - assert device.device_state_attributes[zwave.ATTR_LOCK_STATUS] == \ + assert device.device_state_attributes[lock.ATTR_LOCK_STATUS] == \ 'Manually Locked by Key Cylinder or Inside thumb turn' values.alarm_type.data = 18 values.alarm_level.data = 'alice' value_changed(values.alarm_type) value_changed(values.alarm_level) - assert device.device_state_attributes[zwave.ATTR_LOCK_STATUS] == \ + assert device.device_state_attributes[lock.ATTR_LOCK_STATUS] == \ 'Locked with Keypad by user alice' values.alarm_type.data = 161 values.alarm_level.data = 1 value_changed(values.alarm_type) value_changed(values.alarm_level) - assert device.device_state_attributes[zwave.ATTR_LOCK_STATUS] == \ + assert device.device_state_attributes[lock.ATTR_LOCK_STATUS] == \ 'Tamper Alarm: Too many keypresses' @@ -282,7 +281,7 @@ async def setup_ozw(hass, mock_openzwave): async def test_lock_set_usercode_service(hass, mock_openzwave): """Test the zwave lock set_usercode service.""" - mock_network = hass.data[zwave.zwave.DATA_NETWORK] = MagicMock() + mock_network = hass.data[const.DATA_NETWORK] = MagicMock() node = MockNode(node_id=12) value0 = MockValue(data=' ', node=node, index=0) @@ -301,10 +300,10 @@ async def test_lock_set_usercode_service(hass, mock_openzwave): await hass.async_block_till_done() await hass.services.async_call( - zwave.DOMAIN, zwave.SERVICE_SET_USERCODE, { + lock.DOMAIN, lock.SERVICE_SET_USERCODE, { const.ATTR_NODE_ID: node.node_id, - zwave.ATTR_USERCODE: '1234', - zwave.ATTR_CODE_SLOT: 1, + lock.ATTR_USERCODE: '1234', + lock.ATTR_CODE_SLOT: 1, }) await hass.async_block_till_done() @@ -314,10 +313,10 @@ async def test_lock_set_usercode_service(hass, mock_openzwave): node.node_id: node } await hass.services.async_call( - zwave.DOMAIN, zwave.SERVICE_SET_USERCODE, { + lock.DOMAIN, lock.SERVICE_SET_USERCODE, { const.ATTR_NODE_ID: node.node_id, - zwave.ATTR_USERCODE: '123', - zwave.ATTR_CODE_SLOT: 1, + lock.ATTR_USERCODE: '123', + lock.ATTR_CODE_SLOT: 1, }) await hass.async_block_till_done() @@ -326,7 +325,7 @@ async def test_lock_set_usercode_service(hass, mock_openzwave): async def test_lock_get_usercode_service(hass, mock_openzwave): """Test the zwave lock get_usercode service.""" - mock_network = hass.data[zwave.zwave.DATA_NETWORK] = MagicMock() + mock_network = hass.data[const.DATA_NETWORK] = MagicMock() node = MockNode(node_id=12) value0 = MockValue(data=None, node=node, index=0) value1 = MockValue(data='1234', node=node, index=1) @@ -339,12 +338,12 @@ async def test_lock_get_usercode_service(hass, mock_openzwave): await setup_ozw(hass, mock_openzwave) await hass.async_block_till_done() - with patch.object(zwave, '_LOGGER') as mock_logger: + with patch.object(lock, '_LOGGER') as mock_logger: mock_network.nodes = {node.node_id: node} await hass.services.async_call( - zwave.DOMAIN, zwave.SERVICE_GET_USERCODE, { + lock.DOMAIN, lock.SERVICE_GET_USERCODE, { const.ATTR_NODE_ID: node.node_id, - zwave.ATTR_CODE_SLOT: 1, + lock.ATTR_CODE_SLOT: 1, }) await hass.async_block_till_done() # This service only seems to write to the log @@ -355,7 +354,7 @@ async def test_lock_get_usercode_service(hass, mock_openzwave): async def test_lock_clear_usercode_service(hass, mock_openzwave): """Test the zwave lock clear_usercode service.""" - mock_network = hass.data[zwave.zwave.DATA_NETWORK] = MagicMock() + mock_network = hass.data[const.DATA_NETWORK] = MagicMock() node = MockNode(node_id=12) value0 = MockValue(data=None, node=node, index=0) value1 = MockValue(data='123', node=node, index=1) @@ -373,9 +372,9 @@ async def test_lock_clear_usercode_service(hass, mock_openzwave): await hass.async_block_till_done() await hass.services.async_call( - zwave.DOMAIN, zwave.SERVICE_CLEAR_USERCODE, { + lock.DOMAIN, lock.SERVICE_CLEAR_USERCODE, { const.ATTR_NODE_ID: node.node_id, - zwave.ATTR_CODE_SLOT: 1 + lock.ATTR_CODE_SLOT: 1 }) await hass.async_block_till_done() diff --git a/tests/components/sensor/test_zwave.py b/tests/components/zwave/test_sensor.py similarity index 81% rename from tests/components/sensor/test_zwave.py rename to tests/components/zwave/test_sensor.py index e3792c27d77..73613424d84 100644 --- a/tests/components/sensor/test_zwave.py +++ b/tests/components/zwave/test_sensor.py @@ -1,6 +1,5 @@ """Test Z-Wave sensor.""" -from homeassistant.components.sensor import zwave -from homeassistant.components.zwave import const +from homeassistant.components.zwave import const, sensor import homeassistant.const from tests.mock.zwave import ( @@ -13,7 +12,7 @@ def test_get_device_detects_none(mock_openzwave): value = MockValue(data=0, node=node) values = MockEntityValues(primary=value) - device = zwave.get_device(node=node, values=values, node_config={}) + device = sensor.get_device(node=node, values=values, node_config={}) assert device is None @@ -24,8 +23,8 @@ def test_get_device_detects_alarmsensor(mock_openzwave): value = MockValue(data=0, node=node) values = MockEntityValues(primary=value) - device = zwave.get_device(node=node, values=values, node_config={}) - assert isinstance(device, zwave.ZWaveAlarmSensor) + device = sensor.get_device(node=node, values=values, node_config={}) + assert isinstance(device, sensor.ZWaveAlarmSensor) def test_get_device_detects_multilevelsensor(mock_openzwave): @@ -35,8 +34,8 @@ def test_get_device_detects_multilevelsensor(mock_openzwave): value = MockValue(data=0, node=node) values = MockEntityValues(primary=value) - device = zwave.get_device(node=node, values=values, node_config={}) - assert isinstance(device, zwave.ZWaveMultilevelSensor) + device = sensor.get_device(node=node, values=values, node_config={}) + assert isinstance(device, sensor.ZWaveMultilevelSensor) assert device.force_update @@ -46,8 +45,8 @@ def test_get_device_detects_multilevel_meter(mock_openzwave): value = MockValue(data=0, node=node, type=const.TYPE_DECIMAL) values = MockEntityValues(primary=value) - device = zwave.get_device(node=node, values=values, node_config={}) - assert isinstance(device, zwave.ZWaveMultilevelSensor) + device = sensor.get_device(node=node, values=values, node_config={}) + assert isinstance(device, sensor.ZWaveMultilevelSensor) def test_multilevelsensor_value_changed_temp_fahrenheit(mock_openzwave): @@ -57,7 +56,7 @@ def test_multilevelsensor_value_changed_temp_fahrenheit(mock_openzwave): value = MockValue(data=190.95555, units='F', node=node) values = MockEntityValues(primary=value) - device = zwave.get_device(node=node, values=values, node_config={}) + device = sensor.get_device(node=node, values=values, node_config={}) assert device.state == 191.0 assert device.unit_of_measurement == homeassistant.const.TEMP_FAHRENHEIT value.data = 197.95555 @@ -72,7 +71,7 @@ def test_multilevelsensor_value_changed_temp_celsius(mock_openzwave): value = MockValue(data=38.85555, units='C', node=node) values = MockEntityValues(primary=value) - device = zwave.get_device(node=node, values=values, node_config={}) + device = sensor.get_device(node=node, values=values, node_config={}) assert device.state == 38.9 assert device.unit_of_measurement == homeassistant.const.TEMP_CELSIUS value.data = 37.95555 @@ -87,7 +86,7 @@ def test_multilevelsensor_value_changed_other_units(mock_openzwave): value = MockValue(data=190.95555, units='kWh', node=node) values = MockEntityValues(primary=value) - device = zwave.get_device(node=node, values=values, node_config={}) + device = sensor.get_device(node=node, values=values, node_config={}) assert device.state == 190.96 assert device.unit_of_measurement == 'kWh' value.data = 197.95555 @@ -102,7 +101,7 @@ def test_multilevelsensor_value_changed_integer(mock_openzwave): value = MockValue(data=5, units='counts', node=node) values = MockEntityValues(primary=value) - device = zwave.get_device(node=node, values=values, node_config={}) + device = sensor.get_device(node=node, values=values, node_config={}) assert device.state == 5 assert device.unit_of_measurement == 'counts' value.data = 6 @@ -117,7 +116,7 @@ def test_alarm_sensor_value_changed(mock_openzwave): value = MockValue(data=12.34, node=node, units='%') values = MockEntityValues(primary=value) - device = zwave.get_device(node=node, values=values, node_config={}) + device = sensor.get_device(node=node, values=values, node_config={}) assert device.state == 12.34 assert device.unit_of_measurement == '%' value.data = 45.67 diff --git a/tests/components/switch/test_zwave.py b/tests/components/zwave/test_switch.py similarity index 83% rename from tests/components/switch/test_zwave.py rename to tests/components/zwave/test_switch.py index 3769eef828b..e68f765ae38 100644 --- a/tests/components/switch/test_zwave.py +++ b/tests/components/zwave/test_switch.py @@ -1,7 +1,7 @@ """Test Z-Wave switches.""" from unittest.mock import patch -from homeassistant.components.switch import zwave +from homeassistant.components.zwave import switch from tests.mock.zwave import ( MockNode, MockValue, MockEntityValues, value_changed) @@ -13,8 +13,8 @@ def test_get_device_detects_switch(mock_openzwave): value = MockValue(data=0, node=node) values = MockEntityValues(primary=value) - device = zwave.get_device(node=node, values=values, node_config={}) - assert isinstance(device, zwave.ZwaveSwitch) + device = switch.get_device(node=node, values=values, node_config={}) + assert isinstance(device, switch.ZwaveSwitch) def test_switch_turn_on_and_off(mock_openzwave): @@ -22,7 +22,7 @@ def test_switch_turn_on_and_off(mock_openzwave): node = MockNode() value = MockValue(data=0, node=node) values = MockEntityValues(primary=value) - device = zwave.get_device(node=node, values=values, node_config={}) + device = switch.get_device(node=node, values=values, node_config={}) device.turn_on() @@ -45,7 +45,7 @@ def test_switch_value_changed(mock_openzwave): node = MockNode() value = MockValue(data=False, node=node) values = MockEntityValues(primary=value) - device = zwave.get_device(node=node, values=values, node_config={}) + device = switch.get_device(node=node, values=values, node_config={}) assert not device.is_on @@ -63,7 +63,7 @@ def test_switch_refresh_on_update(mock_counter, mock_openzwave): product_id='0005') value = MockValue(data=False, node=node, instance=1) values = MockEntityValues(primary=value) - device = zwave.get_device(node=node, values=values, node_config={}) + device = switch.get_device(node=node, values=values, node_config={}) assert not device.is_on diff --git a/tests/conftest.py b/tests/conftest.py index 528ad195195..1dc5733cf40 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,7 @@ import asyncio import functools import logging import os -from unittest.mock import patch, MagicMock +from unittest.mock import patch import pytest import requests_mock as _requests_mock @@ -14,10 +14,9 @@ from homeassistant.auth.const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY from homeassistant.auth.providers import legacy_api_password, homeassistant from tests.common import ( - async_test_home_assistant, INSTANCES, async_mock_mqtt_component, mock_coro, + async_test_home_assistant, INSTANCES, mock_coro, mock_storage as mock_storage, MockUser, CLIENT_ID) from tests.test_util.aiohttp import mock_aiohttp_client -from tests.mock.zwave import MockNetwork, MockOption if os.environ.get('UVLOOP') == '1': import uvloop @@ -92,32 +91,6 @@ def aioclient_mock(): yield mock_session -@pytest.fixture -def mqtt_mock(loop, hass): - """Fixture to mock MQTT.""" - client = loop.run_until_complete(async_mock_mqtt_component(hass)) - client.reset_mock() - return client - - -@pytest.fixture -def mock_openzwave(): - """Mock out Open Z-Wave.""" - base_mock = MagicMock() - libopenzwave = base_mock.libopenzwave - libopenzwave.__file__ = 'test' - base_mock.network.ZWaveNetwork = MockNetwork - base_mock.option.ZWaveOption = MockOption - - with patch.dict('sys.modules', { - 'libopenzwave': libopenzwave, - 'openzwave.option': base_mock.option, - 'openzwave.network': base_mock.network, - 'openzwave.group': base_mock.group, - }): - yield base_mock - - @pytest.fixture def mock_device_tracker_conf(): """Prevent device tracker from reading/writing data.""" diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 1bae84b5320..d83d32c88e3 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -5,6 +5,7 @@ import os from socket import _GLOBAL_DEFAULT_TIMEOUT from unittest.mock import Mock, patch +import homeassistant import pytest import voluptuous as vol @@ -110,7 +111,7 @@ def test_platform_config(): {'platform': 'mqtt', 'beer': 'yes'}, ) for value in options: - cv.PLATFORM_SCHEMA(value) + cv.PLATFORM_SCHEMA_BASE(value) def test_ensure_list(): @@ -275,7 +276,6 @@ def test_time_period(): {}, {'wrong_key': -10} ) for value in options: - with pytest.raises(vol.MultipleInvalid): schema(value) @@ -402,8 +402,7 @@ def test_template(): schema = vol.Schema(cv.template) for value in (None, '{{ partial_print }', '{% if True %}Hello', ['test']): - with pytest.raises(vol.Invalid, - message='{} not considered invalid'.format(value)): + with pytest.raises(vol.Invalid): schema(value) options = ( @@ -433,6 +432,15 @@ def test_template_complex(): for value in options: schema(value) + # ensure the validator didn't mutate the input + assert options == ( + 1, 'Hello', + '{{ beer }}', + '{% if 1 == 1 %}Hello{% else %}World{% endif %}', + {'test': 1, 'test2': '{{ beer }}'}, + ['{{ beer }}', 1] + ) + def test_time_zone(): """Test time zone validation.""" @@ -481,26 +489,339 @@ def test_datetime(): schema('2016-11-23T18:59:08') -def test_deprecated(caplog): - """Test deprecation log.""" - schema = vol.Schema({ +@pytest.fixture +def schema(): + """Create a schema used for testing deprecation.""" + return vol.Schema({ 'venus': cv.boolean, - 'mars': cv.boolean + 'mars': cv.boolean, + 'jupiter': cv.boolean }) + + +@pytest.fixture +def version(monkeypatch): + """Patch the version used for testing to 0.5.0.""" + monkeypatch.setattr(homeassistant.const, '__version__', '0.5.0') + + +def test_deprecated_with_no_optionals(caplog, schema): + """ + Test deprecation behaves correctly when optional params are None. + + Expected behavior: + - Outputs the appropriate deprecation warning if key is detected + - Processes schema without changing any values + - No warning or difference in output if key is not provided + """ deprecated_schema = vol.All( cv.deprecated('mars'), schema ) - deprecated_schema({'venus': True}) - # pylint: disable=len-as-condition - assert len(caplog.records) == 0 - - deprecated_schema({'mars': True}) + test_data = {'mars': True} + output = deprecated_schema(test_data.copy()) assert len(caplog.records) == 1 assert caplog.records[0].name == __name__ assert ("The 'mars' option (with value 'True') is deprecated, " - "please remove it from your configuration.") in caplog.text + "please remove it from your configuration") in caplog.text + assert test_data == output + + caplog.clear() + assert len(caplog.records) == 0 + + test_data = {'venus': True} + output = deprecated_schema(test_data.copy()) + assert len(caplog.records) == 0 + assert test_data == output + + +def test_deprecated_with_replacement_key(caplog, schema): + """ + Test deprecation behaves correctly when only a replacement key is provided. + + Expected behavior: + - Outputs the appropriate deprecation warning if key is detected + - Processes schema moving the value from key to replacement_key + - Processes schema changing nothing if only replacement_key provided + - No warning if only replacement_key provided + - No warning or difference in output if neither key nor + replacement_key are provided + """ + deprecated_schema = vol.All( + cv.deprecated('mars', replacement_key='jupiter'), + schema + ) + + test_data = {'mars': True} + output = deprecated_schema(test_data.copy()) + assert len(caplog.records) == 1 + assert ("The 'mars' option (with value 'True') is deprecated, " + "please replace it with 'jupiter'") in caplog.text + assert {'jupiter': True} == output + + caplog.clear() + assert len(caplog.records) == 0 + + test_data = {'jupiter': True} + output = deprecated_schema(test_data.copy()) + assert len(caplog.records) == 0 + assert test_data == output + + test_data = {'venus': True} + output = deprecated_schema(test_data.copy()) + assert len(caplog.records) == 0 + assert test_data == output + + +def test_deprecated_with_invalidation_version(caplog, schema, version): + """ + Test deprecation behaves correctly with only an invalidation_version. + + Expected behavior: + - Outputs the appropriate deprecation warning if key is detected + - Processes schema without changing any values + - No warning or difference in output if key is not provided + - Once the invalidation_version is crossed, raises vol.Invalid if key + is detected + """ + deprecated_schema = vol.All( + cv.deprecated('mars', invalidation_version='1.0.0'), + schema + ) + + message = ("The 'mars' option (with value 'True') is deprecated, " + "please remove it from your configuration. " + "This option will become invalid in version 1.0.0") + + test_data = {'mars': True} + output = deprecated_schema(test_data.copy()) + assert len(caplog.records) == 1 + assert message in caplog.text + assert test_data == output + + caplog.clear() + assert len(caplog.records) == 0 + + test_data = {'venus': False} + output = deprecated_schema(test_data.copy()) + assert len(caplog.records) == 0 + assert test_data == output + + invalidated_schema = vol.All( + cv.deprecated('mars', invalidation_version='0.1.0'), + schema + ) + test_data = {'mars': True} + with pytest.raises(vol.MultipleInvalid) as exc_info: + invalidated_schema(test_data) + assert ("The 'mars' option (with value 'True') is deprecated, " + "please remove it from your configuration. This option will " + "become invalid in version 0.1.0") == str(exc_info.value) + + +def test_deprecated_with_replacement_key_and_invalidation_version( + caplog, schema, version +): + """ + Test deprecation behaves with a replacement key & invalidation_version. + + Expected behavior: + - Outputs the appropriate deprecation warning if key is detected + - Processes schema moving the value from key to replacement_key + - Processes schema changing nothing if only replacement_key provided + - No warning if only replacement_key provided + - No warning or difference in output if neither key nor + replacement_key are provided + - Once the invalidation_version is crossed, raises vol.Invalid if key + is detected + """ + deprecated_schema = vol.All( + cv.deprecated( + 'mars', replacement_key='jupiter', invalidation_version='1.0.0' + ), + schema + ) + + warning = ("The 'mars' option (with value 'True') is deprecated, " + "please replace it with 'jupiter'. This option will become " + "invalid in version 1.0.0") + + test_data = {'mars': True} + output = deprecated_schema(test_data.copy()) + assert len(caplog.records) == 1 + assert warning in caplog.text + assert {'jupiter': True} == output + + caplog.clear() + assert len(caplog.records) == 0 + + test_data = {'jupiter': True} + output = deprecated_schema(test_data.copy()) + assert len(caplog.records) == 0 + assert test_data == output + + test_data = {'venus': True} + output = deprecated_schema(test_data.copy()) + assert len(caplog.records) == 0 + assert test_data == output + + invalidated_schema = vol.All( + cv.deprecated( + 'mars', replacement_key='jupiter', invalidation_version='0.1.0' + ), + schema + ) + test_data = {'mars': True} + with pytest.raises(vol.MultipleInvalid) as exc_info: + invalidated_schema(test_data) + assert ("The 'mars' option (with value 'True') is deprecated, " + "please replace it with 'jupiter'. This option will become " + "invalid in version 0.1.0") == str(exc_info.value) + + +def test_deprecated_with_default(caplog, schema): + """ + Test deprecation behaves correctly with a default value. + + This is likely a scenario that would never occur. + + Expected behavior: + - Behaves identically as when the default value was not present + """ + deprecated_schema = vol.All( + cv.deprecated('mars', default=False), + schema + ) + + test_data = {'mars': True} + output = deprecated_schema(test_data.copy()) + assert len(caplog.records) == 1 + assert caplog.records[0].name == __name__ + assert ("The 'mars' option (with value 'True') is deprecated, " + "please remove it from your configuration") in caplog.text + assert test_data == output + + caplog.clear() + assert len(caplog.records) == 0 + + test_data = {'venus': True} + output = deprecated_schema(test_data.copy()) + assert len(caplog.records) == 0 + assert test_data == output + + +def test_deprecated_with_replacement_key_and_default(caplog, schema): + """ + Test deprecation with a replacement key and default. + + Expected behavior: + - Outputs the appropriate deprecation warning if key is detected + - Processes schema moving the value from key to replacement_key + - Processes schema changing nothing if only replacement_key provided + - No warning if only replacement_key provided + - No warning if neither key nor replacement_key are provided + - Adds replacement_key with default value in this case + """ + deprecated_schema = vol.All( + cv.deprecated('mars', replacement_key='jupiter', default=False), + schema + ) + + test_data = {'mars': True} + output = deprecated_schema(test_data.copy()) + assert len(caplog.records) == 1 + assert ("The 'mars' option (with value 'True') is deprecated, " + "please replace it with 'jupiter'") in caplog.text + assert {'jupiter': True} == output + + caplog.clear() + assert len(caplog.records) == 0 + + test_data = {'jupiter': True} + output = deprecated_schema(test_data.copy()) + assert len(caplog.records) == 0 + assert test_data == output + + test_data = {'venus': True} + output = deprecated_schema(test_data.copy()) + assert len(caplog.records) == 0 + assert {'venus': True, 'jupiter': False} == output + + deprecated_schema_with_default = vol.All( + vol.Schema({ + 'venus': cv.boolean, + vol.Optional('mars', default=False): cv.boolean, + vol.Optional('jupiter', default=False): cv.boolean + }), + cv.deprecated('mars', replacement_key='jupiter', default=False) + ) + + test_data = {'mars': True} + output = deprecated_schema_with_default(test_data.copy()) + assert len(caplog.records) == 1 + assert ("The 'mars' option (with value 'True') is deprecated, " + "please replace it with 'jupiter'") in caplog.text + assert {'jupiter': True} == output + + +def test_deprecated_with_replacement_key_invalidation_version_default( + caplog, schema, version +): + """ + Test deprecation with a replacement key, invalidation_version & default. + + Expected behavior: + - Outputs the appropriate deprecation warning if key is detected + - Processes schema moving the value from key to replacement_key + - Processes schema changing nothing if only replacement_key provided + - No warning if only replacement_key provided + - No warning if neither key nor replacement_key are provided + - Adds replacement_key with default value in this case + - Once the invalidation_version is crossed, raises vol.Invalid if key + is detected + """ + deprecated_schema = vol.All( + cv.deprecated( + 'mars', replacement_key='jupiter', invalidation_version='1.0.0', + default=False + ), + schema + ) + + test_data = {'mars': True} + output = deprecated_schema(test_data.copy()) + assert len(caplog.records) == 1 + assert ("The 'mars' option (with value 'True') is deprecated, " + "please replace it with 'jupiter'. This option will become " + "invalid in version 1.0.0") in caplog.text + assert {'jupiter': True} == output + + caplog.clear() + assert len(caplog.records) == 0 + + test_data = {'jupiter': True} + output = deprecated_schema(test_data.copy()) + assert len(caplog.records) == 0 + assert test_data == output + + test_data = {'venus': True} + output = deprecated_schema(test_data.copy()) + assert len(caplog.records) == 0 + assert {'venus': True, 'jupiter': False} == output + + invalidated_schema = vol.All( + cv.deprecated( + 'mars', replacement_key='jupiter', invalidation_version='0.1.0' + ), + schema + ) + test_data = {'mars': True} + with pytest.raises(vol.MultipleInvalid) as exc_info: + invalidated_schema(test_data) + assert ("The 'mars' option (with value 'True') is deprecated, " + "please replace it with 'jupiter'. This option will become " + "invalid in version 0.1.0") == str(exc_info.value) def test_key_dependency(): @@ -522,6 +843,18 @@ def test_key_dependency(): schema(value) +def test_has_at_most_one_key(): + """Test has_at_most_one_key validator.""" + schema = vol.Schema(cv.has_at_most_one_key('beer', 'soda')) + + for value in (None, [], {'beer': None, 'soda': None}): + with pytest.raises(vol.MultipleInvalid): + schema(value) + + for value in ({}, {'beer': None}, {'soda': None}): + schema(value) + + def test_has_at_least_one_key(): """Test has_at_least_one_key validator.""" schema = vol.Schema(cv.has_at_least_one_key('beer', 'soda')) @@ -574,7 +907,7 @@ def test_matches_regex(): schema(" nrtd ") test_str = "This is a test including uiae." - assert(schema(test_str) == test_str) + assert (schema(test_str) == test_str) def test_is_regex(): diff --git a/tests/helpers/test_state.py b/tests/helpers/test_state.py index 07654744492..5c04f085c86 100644 --- a/tests/helpers/test_state.py +++ b/tests/helpers/test_state.py @@ -15,8 +15,6 @@ from homeassistant.const import ( STATE_LOCKED, STATE_UNLOCKED, STATE_ON, STATE_OFF, STATE_HOME, STATE_NOT_HOME) -from homeassistant.components.media_player import ( - SERVICE_PLAY_MEDIA, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE) from homeassistant.components.sun import (STATE_ABOVE_HORIZON, STATE_BELOW_HORIZON) @@ -50,6 +48,40 @@ def test_async_track_states(hass): sorted(states, key=lambda state: state.entity_id) +@asyncio.coroutine +def test_call_to_component(hass): + """Test calls to components state reproduction functions.""" + with patch(('homeassistant.components.media_player.' + 'async_reproduce_states')) as media_player_fun: + media_player_fun.return_value = asyncio.Future() + media_player_fun.return_value.set_result(None) + + with patch(('homeassistant.components.climate.' + 'async_reproduce_states')) as climate_fun: + climate_fun.return_value = asyncio.Future() + climate_fun.return_value.set_result(None) + + state_media_player = ha.State('media_player.test', 'bad') + state_climate = ha.State('climate.test', 'bad') + context = "dummy_context" + + yield from state.async_reproduce_state( + hass, + [state_media_player, state_climate], + blocking=True, + context=context) + + media_player_fun.assert_called_once_with( + hass, + [state_media_player], + context=context) + + climate_fun.assert_called_once_with( + hass, + [state_climate], + context=context) + + class TestStateHelpers(unittest.TestCase): """Test the Home Assistant event helpers.""" @@ -147,63 +179,6 @@ class TestStateHelpers(unittest.TestCase): assert SERVICE_TURN_ON == last_call.service assert complex_data == last_call.data.get('complex') - def test_reproduce_media_data(self): - """Test reproduce_state with SERVICE_PLAY_MEDIA.""" - calls = mock_service(self.hass, 'media_player', SERVICE_PLAY_MEDIA) - - self.hass.states.set('media_player.test', 'off') - - media_attributes = {'media_content_type': 'movie', - 'media_content_id': 'batman'} - - state.reproduce_state(self.hass, ha.State('media_player.test', 'None', - media_attributes)) - - self.hass.block_till_done() - - assert len(calls) > 0 - last_call = calls[-1] - assert 'media_player' == last_call.domain - assert SERVICE_PLAY_MEDIA == last_call.service - assert 'movie' == last_call.data.get('media_content_type') - assert 'batman' == last_call.data.get('media_content_id') - - def test_reproduce_media_play(self): - """Test reproduce_state with SERVICE_MEDIA_PLAY.""" - calls = mock_service(self.hass, 'media_player', SERVICE_MEDIA_PLAY) - - self.hass.states.set('media_player.test', 'off') - - state.reproduce_state( - self.hass, ha.State('media_player.test', 'playing')) - - self.hass.block_till_done() - - assert len(calls) > 0 - last_call = calls[-1] - assert 'media_player' == last_call.domain - assert SERVICE_MEDIA_PLAY == last_call.service - assert ['media_player.test'] == \ - last_call.data.get('entity_id') - - def test_reproduce_media_pause(self): - """Test reproduce_state with SERVICE_MEDIA_PAUSE.""" - calls = mock_service(self.hass, 'media_player', SERVICE_MEDIA_PAUSE) - - self.hass.states.set('media_player.test', 'playing') - - state.reproduce_state( - self.hass, ha.State('media_player.test', 'paused')) - - self.hass.block_till_done() - - assert len(calls) > 0 - last_call = calls[-1] - assert 'media_player' == last_call.domain - assert SERVICE_MEDIA_PAUSE == last_call.service - assert ['media_player.test'] == \ - last_call.data.get('entity_id') - def test_reproduce_bad_state(self): """Test reproduce_state with bad state.""" calls = mock_service(self.hass, 'light', SERVICE_TURN_ON) @@ -217,45 +192,6 @@ class TestStateHelpers(unittest.TestCase): assert len(calls) == 0 assert 'off' == self.hass.states.get('light.test').state - def test_reproduce_group(self): - """Test reproduce_state with group.""" - light_calls = mock_service(self.hass, 'light', SERVICE_TURN_ON) - - self.hass.states.set('group.test', 'off', { - 'entity_id': ['light.test1', 'light.test2']}) - - state.reproduce_state(self.hass, ha.State('group.test', 'on')) - - self.hass.block_till_done() - - assert 1 == len(light_calls) - last_call = light_calls[-1] - assert 'light' == last_call.domain - assert SERVICE_TURN_ON == last_call.service - assert ['light.test1', 'light.test2'] == \ - last_call.data.get('entity_id') - - def test_reproduce_group_same_data(self): - """Test reproduce_state with group with same domain and data.""" - light_calls = mock_service(self.hass, 'light', SERVICE_TURN_ON) - - self.hass.states.set('light.test1', 'off') - self.hass.states.set('light.test2', 'off') - - state.reproduce_state(self.hass, [ - ha.State('light.test1', 'on', {'brightness': 95}), - ha.State('light.test2', 'on', {'brightness': 95})]) - - self.hass.block_till_done() - - assert 1 == len(light_calls) - last_call = light_calls[-1] - assert 'light' == last_call.domain - assert SERVICE_TURN_ON == last_call.service - assert ['light.test1', 'light.test2'] == \ - last_call.data.get('entity_id') - assert 95 == last_call.data.get('brightness') - def test_as_number_states(self): """Test state_as_number with states.""" zero_states = (STATE_OFF, STATE_CLOSED, STATE_UNLOCKED, diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 59777e2e6bb..496ad785275 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -411,13 +411,18 @@ async def test_updating_entry_data(manager): entry = MockConfigEntry( domain='test', data={'first': True}, + state=config_entries.ENTRY_STATE_SETUP_ERROR, ) entry.add_to_manager(manager) + manager.async_update_entry(entry) + assert entry.data == { + 'first': True + } + manager.async_update_entry(entry, data={ 'second': True }) - assert entry.data == { 'second': True } diff --git a/tests/test_core.py b/tests/test_core.py index f1900979bec..4acb1de6677 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -58,9 +58,9 @@ def test_async_add_job_schedule_partial_callback(): assert len(hass.add_job.mock_calls) == 0 -def test_async_add_job_schedule_coroutinefunction(): +def test_async_add_job_schedule_coroutinefunction(loop): """Test that we schedule coroutines and add jobs to the job pool.""" - hass = MagicMock() + hass = MagicMock(loop=MagicMock(wraps=loop)) async def job(): pass @@ -71,9 +71,9 @@ def test_async_add_job_schedule_coroutinefunction(): assert len(hass.add_job.mock_calls) == 0 -def test_async_add_job_schedule_partial_coroutinefunction(): +def test_async_add_job_schedule_partial_coroutinefunction(loop): """Test that we schedule partial coros and add jobs to the job pool.""" - hass = MagicMock() + hass = MagicMock(loop=MagicMock(wraps=loop)) async def job(): pass @@ -98,9 +98,9 @@ def test_async_add_job_add_threaded_job_to_pool(): assert len(hass.loop.run_in_executor.mock_calls) == 1 -def test_async_create_task_schedule_coroutine(): +def test_async_create_task_schedule_coroutine(loop): """Test that we schedule coroutines and add jobs to the job pool.""" - hass = MagicMock() + hass = MagicMock(loop=MagicMock(wraps=loop)) async def job(): pass @@ -460,61 +460,76 @@ class TestEventBus(unittest.TestCase): assert len(coroutine_calls) == 1 -class TestState(unittest.TestCase): - """Test State methods.""" +def test_state_init(): + """Test state.init.""" + with pytest.raises(InvalidEntityFormatError): + ha.State('invalid_entity_format', 'test_state') - def test_init(self): - """Test state.init.""" - with pytest.raises(InvalidEntityFormatError): - ha.State('invalid_entity_format', 'test_state') + with pytest.raises(InvalidStateError): + ha.State('domain.long_state', 't' * 256) - with pytest.raises(InvalidStateError): - ha.State('domain.long_state', 't' * 256) - def test_domain(self): - """Test domain.""" - state = ha.State('some_domain.hello', 'world') - assert 'some_domain' == state.domain +def test_state_domain(): + """Test domain.""" + state = ha.State('some_domain.hello', 'world') + assert 'some_domain' == state.domain - def test_object_id(self): - """Test object ID.""" - state = ha.State('domain.hello', 'world') - assert 'hello' == state.object_id - def test_name_if_no_friendly_name_attr(self): - """Test if there is no friendly name.""" - state = ha.State('domain.hello_world', 'world') - assert 'hello world' == state.name +def test_state_object_id(): + """Test object ID.""" + state = ha.State('domain.hello', 'world') + assert 'hello' == state.object_id - def test_name_if_friendly_name_attr(self): - """Test if there is a friendly name.""" - name = 'Some Unique Name' - state = ha.State('domain.hello_world', 'world', - {ATTR_FRIENDLY_NAME: name}) - assert name == state.name - def test_dict_conversion(self): - """Test conversion of dict.""" - state = ha.State('domain.hello', 'world', {'some': 'attr'}) - assert state == ha.State.from_dict(state.as_dict()) +def test_state_name_if_no_friendly_name_attr(): + """Test if there is no friendly name.""" + state = ha.State('domain.hello_world', 'world') + assert 'hello world' == state.name - def test_dict_conversion_with_wrong_data(self): - """Test conversion with wrong data.""" - assert ha.State.from_dict(None) is None - assert ha.State.from_dict({'state': 'yes'}) is None - assert ha.State.from_dict({'entity_id': 'yes'}) is None - def test_repr(self): - """Test state.repr.""" - assert "" == \ - str(ha.State( - "happy.happy", "on", - last_changed=datetime(1984, 12, 8, 12, 0, 0))) +def test_state_name_if_friendly_name_attr(): + """Test if there is a friendly name.""" + name = 'Some Unique Name' + state = ha.State('domain.hello_world', 'world', + {ATTR_FRIENDLY_NAME: name}) + assert name == state.name - assert "" == \ - str(ha.State("happy.happy", "on", {"brightness": 144}, - datetime(1984, 12, 8, 12, 0, 0))) + +def test_state_dict_conversion(): + """Test conversion of dict.""" + state = ha.State('domain.hello', 'world', {'some': 'attr'}) + assert state == ha.State.from_dict(state.as_dict()) + + +def test_state_dict_conversion_with_wrong_data(): + """Test conversion with wrong data.""" + assert ha.State.from_dict(None) is None + assert ha.State.from_dict({'state': 'yes'}) is None + assert ha.State.from_dict({'entity_id': 'yes'}) is None + # Make sure invalid context data doesn't crash + wrong_context = ha.State.from_dict({ + 'entity_id': 'light.kitchen', + 'state': 'on', + 'context': { + 'id': '123', + 'non-existing': 'crash' + } + }) + assert wrong_context is not None + assert wrong_context.context.id == '123' + + +def test_state_repr(): + """Test state.repr.""" + assert "" == \ + str(ha.State( + "happy.happy", "on", + last_changed=datetime(1984, 12, 8, 12, 0, 0))) + + assert "" == \ + str(ha.State("happy.happy", "on", {"brightness": 144}, + datetime(1984, 12, 8, 12, 0, 0))) class TestStateMachine(unittest.TestCase): diff --git a/tests/test_loader.py b/tests/test_loader.py index 5bd273ea16a..cceb9839d99 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -1,63 +1,52 @@ """Test to verify that we can load components.""" -# pylint: disable=protected-access import asyncio -import unittest import pytest import homeassistant.loader as loader import homeassistant.components.http as http -from tests.common import ( - get_test_home_assistant, MockModule, async_mock_service) +from tests.common import MockModule, async_mock_service -class TestLoader(unittest.TestCase): - """Test the loader module.""" +def test_set_component(hass): + """Test if set_component works.""" + comp = object() + loader.set_component(hass, 'switch.test_set', comp) - # pylint: disable=invalid-name - def setUp(self): - """Set up tests.""" - self.hass = get_test_home_assistant() + assert loader.get_component(hass, 'switch.test_set') is comp - # pylint: disable=invalid-name - def tearDown(self): - """Stop everything that was started.""" - self.hass.stop() - def test_set_component(self): - """Test if set_component works.""" - comp = object() - loader.set_component(self.hass, 'switch.test_set', comp) +def test_get_component(hass): + """Test if get_component works.""" + assert http == loader.get_component(hass, 'http') - assert loader.get_component(self.hass, 'switch.test_set') is comp - def test_get_component(self): - """Test if get_component works.""" - assert http == loader.get_component(self.hass, 'http') +def test_component_dependencies(hass): + """Test if we can get the proper load order of components.""" + loader.set_component(hass, 'mod1', MockModule('mod1')) + loader.set_component(hass, 'mod2', MockModule('mod2', ['mod1'])) + loader.set_component(hass, 'mod3', MockModule('mod3', ['mod2'])) - def test_load_order_component(self): - """Test if we can get the proper load order of components.""" - loader.set_component(self.hass, 'mod1', MockModule('mod1')) - loader.set_component(self.hass, 'mod2', MockModule('mod2', ['mod1'])) - loader.set_component(self.hass, 'mod3', MockModule('mod3', ['mod2'])) + assert {'mod1', 'mod2', 'mod3'} == \ + loader.component_dependencies(hass, 'mod3') - assert ['mod1', 'mod2', 'mod3'] == \ - loader.load_order_component(self.hass, 'mod3') + # Create circular dependency + loader.set_component(hass, 'mod1', MockModule('mod1', ['mod3'])) - # Create circular dependency - loader.set_component(self.hass, 'mod1', MockModule('mod1', ['mod3'])) + with pytest.raises(loader.CircularDependency): + print(loader.component_dependencies(hass, 'mod3')) - assert [] == loader.load_order_component(self.hass, 'mod3') + # Depend on non-existing component + loader.set_component(hass, 'mod1', + MockModule('mod1', ['nonexisting'])) - # Depend on non-existing component - loader.set_component(self.hass, 'mod1', - MockModule('mod1', ['nonexisting'])) + with pytest.raises(loader.ComponentNotFound): + print(loader.component_dependencies(hass, 'mod1')) - assert [] == loader.load_order_component(self.hass, 'mod1') - - # Try to get load order for non-existing component - assert [] == loader.load_order_component(self.hass, 'mod1') + # Try to get dependencies for non-existing component + with pytest.raises(loader.ComponentNotFound): + print(loader.component_dependencies(hass, 'nonexisting')) def test_component_loader(hass): @@ -132,3 +121,17 @@ async def test_log_warning_custom_component(hass, caplog): loader.get_component(hass, 'light.test') assert 'You are using a custom component for light.test' in caplog.text + + +async def test_get_platform(hass, caplog): + """Test get_platform.""" + # Test we prefer embedded over normal platforms.""" + embedded_platform = loader.get_platform(hass, 'switch', 'test_embedded') + assert embedded_platform.__name__ == \ + 'custom_components.test_embedded.switch' + + caplog.clear() + + legacy_platform = loader.get_platform(hass, 'switch', 'test') + assert legacy_platform.__name__ == 'custom_components.switch.test' + assert 'Integrations need to be in their own folder.' in caplog.text diff --git a/tests/test_setup.py b/tests/test_setup.py index 6d0d2a35847..c6126bc4a3b 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -15,7 +15,7 @@ import homeassistant.config as config_util from homeassistant import setup, loader import homeassistant.util.dt as dt_util from homeassistant.helpers.config_validation import ( - PLATFORM_SCHEMA_2 as PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) + PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) from homeassistant.helpers import discovery from tests.common import \ @@ -90,7 +90,7 @@ class TestSetup: } }) - def test_validate_platform_config(self): + def test_validate_platform_config(self, caplog): """Test validating platform configuration.""" platform_schema = PLATFORM_SCHEMA.extend({ 'hello': str, @@ -109,7 +109,7 @@ class TestSetup: MockPlatform('whatever', platform_schema=platform_schema)) - with assert_setup_component(0): + with assert_setup_component(1): assert setup.setup_component(self.hass, 'platform_conf', { 'platform_conf': { 'platform': 'whatever', @@ -117,11 +117,13 @@ class TestSetup: 'invalid': 'extra', } }) + assert caplog.text.count('Your configuration contains ' + 'extra keys') == 1 self.hass.data.pop(setup.DATA_SETUP) self.hass.config.components.remove('platform_conf') - with assert_setup_component(1): + with assert_setup_component(2): assert setup.setup_component(self.hass, 'platform_conf', { 'platform_conf': { 'platform': 'whatever', @@ -132,6 +134,8 @@ class TestSetup: 'invalid': True } }) + assert caplog.text.count('Your configuration contains ' + 'extra keys') == 2 self.hass.data.pop(setup.DATA_SETUP) self.hass.config.components.remove('platform_conf') @@ -183,7 +187,7 @@ class TestSetup: assert 'platform_conf' in self.hass.config.components assert not config['platform_conf'] # empty - def test_validate_platform_config_2(self): + def test_validate_platform_config_2(self, caplog): """Test component PLATFORM_SCHEMA_BASE prio over PLATFORM_SCHEMA.""" platform_schema = PLATFORM_SCHEMA.extend({ 'hello': str, @@ -204,7 +208,7 @@ class TestSetup: MockPlatform('whatever', platform_schema=platform_schema)) - with assert_setup_component(0): + with assert_setup_component(1): assert setup.setup_component(self.hass, 'platform_conf', { # fail: no extra keys allowed in platform schema 'platform_conf': { @@ -213,6 +217,8 @@ class TestSetup: 'invalid': 'extra', } }) + assert caplog.text.count('Your configuration contains ' + 'extra keys') == 1 self.hass.data.pop(setup.DATA_SETUP) self.hass.config.components.remove('platform_conf') @@ -234,7 +240,7 @@ class TestSetup: self.hass.data.pop(setup.DATA_SETUP) self.hass.config.components.remove('platform_conf') - def test_validate_platform_config_3(self): + def test_validate_platform_config_3(self, caplog): """Test fallback to component PLATFORM_SCHEMA.""" component_schema = PLATFORM_SCHEMA_BASE.extend({ 'hello': str, @@ -255,15 +261,16 @@ class TestSetup: MockPlatform('whatever', platform_schema=platform_schema)) - with assert_setup_component(0): + with assert_setup_component(1): assert setup.setup_component(self.hass, 'platform_conf', { 'platform_conf': { - # fail: no extra keys allowed 'platform': 'whatever', 'hello': 'world', 'invalid': 'extra', } }) + assert caplog.text.count('Your configuration contains ' + 'extra keys') == 1 self.hass.data.pop(setup.DATA_SETUP) self.hass.config.components.remove('platform_conf') diff --git a/tests/testing_config/custom_components/switch/test_embedded.py b/tests/testing_config/custom_components/switch/test_embedded.py new file mode 100644 index 00000000000..0023aa8a1f2 --- /dev/null +++ b/tests/testing_config/custom_components/switch/test_embedded.py @@ -0,0 +1 @@ +"""Test switch platform for test_embedded component.""" diff --git a/tests/util/test_init.py b/tests/util/test_init.py index 98fe8774b96..af957582ec0 100644 --- a/tests/util/test_init.py +++ b/tests/util/test_init.py @@ -105,67 +105,6 @@ class TestUtil(unittest.TestCase): with pytest.raises(TypeError): TestEnum.FIRST >= 1 - def test_ordered_set(self): - """Test ordering of set.""" - set1 = util.OrderedSet([1, 2, 3, 4]) - set2 = util.OrderedSet([3, 4, 5]) - - assert 4 == len(set1) - assert 3 == len(set2) - - assert 1 in set1 - assert 2 in set1 - assert 3 in set1 - assert 4 in set1 - assert 5 not in set1 - - assert 1 not in set2 - assert 2 not in set2 - assert 3 in set2 - assert 4 in set2 - assert 5 in set2 - - set1.add(5) - assert 5 in set1 - - set1.discard(5) - assert 5 not in set1 - - # Try again while key is not in - set1.discard(5) - assert 5 not in set1 - - assert [1, 2, 3, 4] == list(set1) - assert [4, 3, 2, 1] == list(reversed(set1)) - - assert 1 == set1.pop(False) - assert [2, 3, 4] == list(set1) - - assert 4 == set1.pop() - assert [2, 3] == list(set1) - - assert 'OrderedSet()' == str(util.OrderedSet()) - assert 'OrderedSet([2, 3])' == str(set1) - - assert set1 == util.OrderedSet([2, 3]) - assert set1 != util.OrderedSet([3, 2]) - assert set1 == set([2, 3]) - assert set1 == {3, 2} - assert set1 == [2, 3] - assert set1 == [3, 2] - assert set1 != {2} - - set3 = util.OrderedSet(set1) - set3.update(set2) - - assert [3, 4, 5, 2] == set3 - assert [3, 4, 5, 2] == set1 | set2 - assert [3] == set1 & set2 - assert [2] == set1 - set2 - - set1.update([1, 2], [5, 6]) - assert [2, 3, 1, 5, 6] == set1 - def test_throttle(self): """Test the add cooldown decorator.""" calls1 = [] diff --git a/tox.ini b/tox.ini index d240149cff8..1dfa77c14f1 100644 --- a/tox.ini +++ b/tox.ini @@ -60,4 +60,4 @@ whitelist_externals=/bin/bash deps = -r{toxinidir}/requirements_test.txt commands = - /bin/bash -c 'mypy homeassistant/*.py homeassistant/{auth,util}/ homeassistant/helpers/{__init__,condition,deprecation,dispatcher,entity_values,entityfilter,icon,intent,json,location,signal,state,sun,temperature,translation,typing}.py' + /bin/bash -c 'mypy homeassistant/*.py homeassistant/{auth,util}/ homeassistant/helpers/{__init__,aiohttp_client,area_registry,condition,deprecation,dispatcher,entity_values,entityfilter,icon,intent,json,location,signal,state,sun,temperature,translation,typing}.py' diff --git a/virtualization/Docker/Dockerfile.dev b/virtualization/Docker/Dockerfile.dev index c01706782a0..03d6ab47c24 100644 --- a/virtualization/Docker/Dockerfile.dev +++ b/virtualization/Docker/Dockerfile.dev @@ -2,7 +2,7 @@ # Based on the production Dockerfile, but with development additions. # Keep this file as close as possible to the production Dockerfile, so the environments match. -FROM python:3.6 +FROM python:3.7 LABEL maintainer="Paulus Schoutsen " # Uncomment any of the following lines to disable the installation. @@ -29,7 +29,7 @@ COPY requirements_all.txt requirements_all.txt # Uninstall enum34 because some dependencies install it but breaks Python 3.4+. # See PR #8103 for more info. RUN pip3 install --no-cache-dir -r requirements_all.txt && \ - pip3 install --no-cache-dir mysqlclient psycopg2 uvloop cchardet cython + pip3 install --no-cache-dir mysqlclient psycopg2 uvloop==0.11.3 cchardet cython # BEGIN: Development additions