diff --git a/.coveragerc b/.coveragerc index f1ff771558019..8e5b61136c02c 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 3bc284627fcff..53cc6960fc305 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 98eaca900764d..6426359812113 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 0dcd0f666c7c8..aa9415fd1e080 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 af186fb1341ee..28f4059d60da8 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.icon + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.intent module +----------------------------------- -.. automodule:: homeassistant.helpers.event_decorators +.. 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 0000000000000..9cec34c134079 --- /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 5dd620056094e..a018d5400338b 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -10,7 +10,8 @@ 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.py deleted file mode 100644 index 8a1a39a726f0d..0000000000000 --- a/homeassistant/components/abode.py +++ /dev/null @@ -1,344 +0,0 @@ -""" -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/ -""" -import logging -from functools import partial -from requests.exceptions import HTTPError, ConnectTimeout - -import voluptuous as vol - -from homeassistant.const import ( - ATTR_ATTRIBUTION, ATTR_DATE, ATTR_TIME, ATTR_ENTITY_ID, CONF_USERNAME, - CONF_PASSWORD, CONF_EXCLUDE, CONF_NAME, CONF_LIGHTS, - EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START) -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery -from homeassistant.helpers.entity import Entity - -REQUIREMENTS = ['abodepy==0.15.0'] - -_LOGGER = logging.getLogger(__name__) - -CONF_ATTRIBUTION = "Data provided by goabode.com" -CONF_POLLING = 'polling' - -DOMAIN = 'abode' -DEFAULT_CACHEDB = './abodepy_cache.pickle' - -NOTIFICATION_ID = 'abode_notification' -NOTIFICATION_TITLE = 'Abode Security Setup' - -EVENT_ABODE_ALARM = 'abode_alarm' -EVENT_ABODE_ALARM_END = 'abode_alarm_end' -EVENT_ABODE_AUTOMATION = 'abode_automation' -EVENT_ABODE_FAULT = 'abode_panel_fault' -EVENT_ABODE_RESTORE = 'abode_panel_restore' - -SERVICE_SETTINGS = 'change_setting' -SERVICE_CAPTURE_IMAGE = 'capture_image' -SERVICE_TRIGGER = 'trigger_quick_action' - -ATTR_DEVICE_ID = 'device_id' -ATTR_DEVICE_NAME = 'device_name' -ATTR_DEVICE_TYPE = 'device_type' -ATTR_EVENT_CODE = 'event_code' -ATTR_EVENT_NAME = 'event_name' -ATTR_EVENT_TYPE = 'event_type' -ATTR_EVENT_UTC = 'event_utc' -ATTR_SETTING = 'setting' -ATTR_USER_NAME = 'user_name' -ATTR_VALUE = 'value' - -ABODE_DEVICE_ID_LIST_SCHEMA = vol.Schema([str]) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_POLLING, default=False): cv.boolean, - vol.Optional(CONF_EXCLUDE, default=[]): ABODE_DEVICE_ID_LIST_SCHEMA, - vol.Optional(CONF_LIGHTS, default=[]): ABODE_DEVICE_ID_LIST_SCHEMA - }), -}, extra=vol.ALLOW_EXTRA) - -CHANGE_SETTING_SCHEMA = vol.Schema({ - vol.Required(ATTR_SETTING): cv.string, - vol.Required(ATTR_VALUE): cv.string -}) - -CAPTURE_IMAGE_SCHEMA = vol.Schema({ - ATTR_ENTITY_ID: cv.entity_ids, -}) - -TRIGGER_SCHEMA = vol.Schema({ - ATTR_ENTITY_ID: cv.entity_ids, -}) - -ABODE_PLATFORMS = [ - 'alarm_control_panel', 'binary_sensor', 'lock', 'switch', 'cover', - 'camera', 'light', 'sensor' -] - - -class AbodeSystem: - """Abode System class.""" - - def __init__(self, username, password, cache, - name, polling, exclude, lights): - """Initialize the system.""" - import abodepy - self.abode = abodepy.Abode( - username, password, auto_login=True, get_devices=True, - get_automations=True, cache_path=cache) - self.name = name - self.polling = polling - self.exclude = exclude - self.lights = lights - self.devices = [] - - def is_excluded(self, device): - """Check if a device is configured to be excluded.""" - return device.device_id in self.exclude - - def is_automation_excluded(self, automation): - """Check if an automation is configured to be excluded.""" - return automation.automation_id in self.exclude - - def is_light(self, device): - """Check if a switch device is configured as a light.""" - import abodepy.helpers.constants as CONST - - return (device.generic_type == CONST.TYPE_LIGHT or - (device.generic_type == CONST.TYPE_SWITCH and - device.device_id in self.lights)) - - -def setup(hass, config): - """Set up Abode component.""" - from abodepy.exceptions import AbodeException - - conf = config[DOMAIN] - username = conf.get(CONF_USERNAME) - password = conf.get(CONF_PASSWORD) - name = conf.get(CONF_NAME) - polling = conf.get(CONF_POLLING) - exclude = conf.get(CONF_EXCLUDE) - lights = conf.get(CONF_LIGHTS) - - try: - cache = hass.config.path(DEFAULT_CACHEDB) - hass.data[DOMAIN] = AbodeSystem( - username, password, cache, name, polling, exclude, lights) - except (AbodeException, ConnectTimeout, HTTPError) as ex: - _LOGGER.error("Unable to connect to Abode: %s", str(ex)) - - hass.components.persistent_notification.create( - 'Error: {}
' - 'You will need to restart hass after fixing.' - ''.format(ex), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) - return False - - setup_hass_services(hass) - setup_hass_events(hass) - setup_abode_events(hass) - - for platform in ABODE_PLATFORMS: - discovery.load_platform(hass, platform, DOMAIN, {}, config) - - return True - - -def setup_hass_services(hass): - """Home assistant services.""" - from abodepy.exceptions import AbodeException - - def change_setting(call): - """Change an Abode system setting.""" - setting = call.data.get(ATTR_SETTING) - value = call.data.get(ATTR_VALUE) - - try: - hass.data[DOMAIN].abode.set_setting(setting, value) - except AbodeException as ex: - _LOGGER.warning(ex) - - def capture_image(call): - """Capture a new image.""" - entity_ids = call.data.get(ATTR_ENTITY_ID) - - target_devices = [device for device in hass.data[DOMAIN].devices - if device.entity_id in entity_ids] - - for device in target_devices: - device.capture() - - def trigger_quick_action(call): - """Trigger a quick action.""" - entity_ids = call.data.get(ATTR_ENTITY_ID, None) - - target_devices = [device for device in hass.data[DOMAIN].devices - if device.entity_id in entity_ids] - - for device in target_devices: - device.trigger() - - hass.services.register( - DOMAIN, SERVICE_SETTINGS, change_setting, - schema=CHANGE_SETTING_SCHEMA) - - hass.services.register( - DOMAIN, SERVICE_CAPTURE_IMAGE, capture_image, - schema=CAPTURE_IMAGE_SCHEMA) - - hass.services.register( - DOMAIN, SERVICE_TRIGGER, trigger_quick_action, - schema=TRIGGER_SCHEMA) - - -def setup_hass_events(hass): - """Home Assistant start and stop callbacks.""" - def startup(event): - """Listen for push events.""" - hass.data[DOMAIN].abode.events.start() - - def logout(event): - """Logout of Abode.""" - if not hass.data[DOMAIN].polling: - hass.data[DOMAIN].abode.events.stop() - - hass.data[DOMAIN].abode.logout() - _LOGGER.info("Logged out of Abode") - - if not hass.data[DOMAIN].polling: - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, startup) - - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, logout) - - -def setup_abode_events(hass): - """Event callbacks.""" - import abodepy.helpers.timeline as TIMELINE - - def event_callback(event, event_json): - """Handle an event callback from Abode.""" - data = { - ATTR_DEVICE_ID: event_json.get(ATTR_DEVICE_ID, ''), - ATTR_DEVICE_NAME: event_json.get(ATTR_DEVICE_NAME, ''), - ATTR_DEVICE_TYPE: event_json.get(ATTR_DEVICE_TYPE, ''), - ATTR_EVENT_CODE: event_json.get(ATTR_EVENT_CODE, ''), - ATTR_EVENT_NAME: event_json.get(ATTR_EVENT_NAME, ''), - ATTR_EVENT_TYPE: event_json.get(ATTR_EVENT_TYPE, ''), - ATTR_EVENT_UTC: event_json.get(ATTR_EVENT_UTC, ''), - ATTR_USER_NAME: event_json.get(ATTR_USER_NAME, ''), - ATTR_DATE: event_json.get(ATTR_DATE, ''), - ATTR_TIME: event_json.get(ATTR_TIME, ''), - } - - hass.bus.fire(event, data) - - events = [TIMELINE.ALARM_GROUP, TIMELINE.ALARM_END_GROUP, - TIMELINE.PANEL_FAULT_GROUP, TIMELINE.PANEL_RESTORE_GROUP, - TIMELINE.AUTOMATION_GROUP] - - for event in events: - hass.data[DOMAIN].abode.events.add_event_callback( - event, - partial(event_callback, event)) - - -class AbodeDevice(Entity): - """Representation of an Abode device.""" - - def __init__(self, data, device): - """Initialize a sensor for Abode device.""" - self._data = data - self._device = device - - async def async_added_to_hass(self): - """Subscribe Abode events.""" - self.hass.async_add_job( - self._data.abode.events.add_device_callback, - self._device.device_id, self._update_callback - ) - - @property - def should_poll(self): - """Return the polling state.""" - return self._data.polling - - def update(self): - """Update automation state.""" - self._device.refresh() - - @property - def name(self): - """Return the name of the sensor.""" - return self._device.name - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - 'device_id': self._device.device_id, - 'battery_low': self._device.battery_low, - 'no_response': self._device.no_response, - 'device_type': self._device.type - } - - def _update_callback(self, device): - """Update the device state.""" - self.schedule_update_ha_state() - - -class AbodeAutomation(Entity): - """Representation of an Abode automation.""" - - def __init__(self, data, automation, event=None): - """Initialize for Abode automation.""" - self._data = data - self._automation = automation - self._event = event - - async def async_added_to_hass(self): - """Subscribe Abode events.""" - if self._event: - self.hass.async_add_job( - self._data.abode.events.add_event_callback, - self._event, self._update_callback - ) - - @property - def should_poll(self): - """Return the polling state.""" - return self._data.polling - - def update(self): - """Update automation state.""" - self._automation.refresh() - - @property - def name(self): - """Return the name of the sensor.""" - return self._automation.name - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - 'automation_id': self._automation.automation_id, - 'type': self._automation.type, - 'sub_type': self._automation.sub_type - } - - def _update_callback(self, device): - """Update the device state.""" - self._automation.refresh() - self.schedule_update_ha_state() diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py new file mode 100644 index 0000000000000..71a1dcdd590bb --- /dev/null +++ b/homeassistant/components/abode/__init__.py @@ -0,0 +1,339 @@ +"""Support for Abode Home Security system.""" +import logging +from functools import partial +from requests.exceptions import HTTPError, ConnectTimeout + +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ATTRIBUTION, ATTR_DATE, ATTR_TIME, ATTR_ENTITY_ID, CONF_USERNAME, + CONF_PASSWORD, CONF_EXCLUDE, CONF_NAME, CONF_LIGHTS, + EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START) +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import discovery +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['abodepy==0.15.0'] + +_LOGGER = logging.getLogger(__name__) + +CONF_ATTRIBUTION = "Data provided by goabode.com" +CONF_POLLING = 'polling' + +DOMAIN = 'abode' +DEFAULT_CACHEDB = './abodepy_cache.pickle' + +NOTIFICATION_ID = 'abode_notification' +NOTIFICATION_TITLE = 'Abode Security Setup' + +EVENT_ABODE_ALARM = 'abode_alarm' +EVENT_ABODE_ALARM_END = 'abode_alarm_end' +EVENT_ABODE_AUTOMATION = 'abode_automation' +EVENT_ABODE_FAULT = 'abode_panel_fault' +EVENT_ABODE_RESTORE = 'abode_panel_restore' + +SERVICE_SETTINGS = 'change_setting' +SERVICE_CAPTURE_IMAGE = 'capture_image' +SERVICE_TRIGGER = 'trigger_quick_action' + +ATTR_DEVICE_ID = 'device_id' +ATTR_DEVICE_NAME = 'device_name' +ATTR_DEVICE_TYPE = 'device_type' +ATTR_EVENT_CODE = 'event_code' +ATTR_EVENT_NAME = 'event_name' +ATTR_EVENT_TYPE = 'event_type' +ATTR_EVENT_UTC = 'event_utc' +ATTR_SETTING = 'setting' +ATTR_USER_NAME = 'user_name' +ATTR_VALUE = 'value' + +ABODE_DEVICE_ID_LIST_SCHEMA = vol.Schema([str]) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_POLLING, default=False): cv.boolean, + vol.Optional(CONF_EXCLUDE, default=[]): ABODE_DEVICE_ID_LIST_SCHEMA, + vol.Optional(CONF_LIGHTS, default=[]): ABODE_DEVICE_ID_LIST_SCHEMA + }), +}, extra=vol.ALLOW_EXTRA) + +CHANGE_SETTING_SCHEMA = vol.Schema({ + vol.Required(ATTR_SETTING): cv.string, + vol.Required(ATTR_VALUE): cv.string +}) + +CAPTURE_IMAGE_SCHEMA = vol.Schema({ + ATTR_ENTITY_ID: cv.entity_ids, +}) + +TRIGGER_SCHEMA = vol.Schema({ + ATTR_ENTITY_ID: cv.entity_ids, +}) + +ABODE_PLATFORMS = [ + 'alarm_control_panel', 'binary_sensor', 'lock', 'switch', 'cover', + 'camera', 'light', 'sensor' +] + + +class AbodeSystem: + """Abode System class.""" + + def __init__(self, username, password, cache, + name, polling, exclude, lights): + """Initialize the system.""" + import abodepy + self.abode = abodepy.Abode( + username, password, auto_login=True, get_devices=True, + get_automations=True, cache_path=cache) + self.name = name + self.polling = polling + self.exclude = exclude + self.lights = lights + self.devices = [] + + def is_excluded(self, device): + """Check if a device is configured to be excluded.""" + return device.device_id in self.exclude + + def is_automation_excluded(self, automation): + """Check if an automation is configured to be excluded.""" + return automation.automation_id in self.exclude + + def is_light(self, device): + """Check if a switch device is configured as a light.""" + import abodepy.helpers.constants as CONST + + return (device.generic_type == CONST.TYPE_LIGHT or + (device.generic_type == CONST.TYPE_SWITCH and + device.device_id in self.lights)) + + +def setup(hass, config): + """Set up Abode component.""" + from abodepy.exceptions import AbodeException + + conf = config[DOMAIN] + username = conf.get(CONF_USERNAME) + password = conf.get(CONF_PASSWORD) + name = conf.get(CONF_NAME) + polling = conf.get(CONF_POLLING) + exclude = conf.get(CONF_EXCLUDE) + lights = conf.get(CONF_LIGHTS) + + try: + cache = hass.config.path(DEFAULT_CACHEDB) + hass.data[DOMAIN] = AbodeSystem( + username, password, cache, name, polling, exclude, lights) + except (AbodeException, ConnectTimeout, HTTPError) as ex: + _LOGGER.error("Unable to connect to Abode: %s", str(ex)) + + hass.components.persistent_notification.create( + 'Error: {}
' + 'You will need to restart hass after fixing.' + ''.format(ex), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + return False + + setup_hass_services(hass) + setup_hass_events(hass) + setup_abode_events(hass) + + for platform in ABODE_PLATFORMS: + discovery.load_platform(hass, platform, DOMAIN, {}, config) + + return True + + +def setup_hass_services(hass): + """Home assistant services.""" + from abodepy.exceptions import AbodeException + + def change_setting(call): + """Change an Abode system setting.""" + setting = call.data.get(ATTR_SETTING) + value = call.data.get(ATTR_VALUE) + + try: + hass.data[DOMAIN].abode.set_setting(setting, value) + except AbodeException as ex: + _LOGGER.warning(ex) + + def capture_image(call): + """Capture a new image.""" + entity_ids = call.data.get(ATTR_ENTITY_ID) + + target_devices = [device for device in hass.data[DOMAIN].devices + if device.entity_id in entity_ids] + + for device in target_devices: + device.capture() + + def trigger_quick_action(call): + """Trigger a quick action.""" + entity_ids = call.data.get(ATTR_ENTITY_ID, None) + + target_devices = [device for device in hass.data[DOMAIN].devices + if device.entity_id in entity_ids] + + for device in target_devices: + device.trigger() + + hass.services.register( + DOMAIN, SERVICE_SETTINGS, change_setting, + schema=CHANGE_SETTING_SCHEMA) + + hass.services.register( + DOMAIN, SERVICE_CAPTURE_IMAGE, capture_image, + schema=CAPTURE_IMAGE_SCHEMA) + + hass.services.register( + DOMAIN, SERVICE_TRIGGER, trigger_quick_action, + schema=TRIGGER_SCHEMA) + + +def setup_hass_events(hass): + """Home Assistant start and stop callbacks.""" + def startup(event): + """Listen for push events.""" + hass.data[DOMAIN].abode.events.start() + + def logout(event): + """Logout of Abode.""" + if not hass.data[DOMAIN].polling: + hass.data[DOMAIN].abode.events.stop() + + hass.data[DOMAIN].abode.logout() + _LOGGER.info("Logged out of Abode") + + if not hass.data[DOMAIN].polling: + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, startup) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, logout) + + +def setup_abode_events(hass): + """Event callbacks.""" + import abodepy.helpers.timeline as TIMELINE + + def event_callback(event, event_json): + """Handle an event callback from Abode.""" + data = { + ATTR_DEVICE_ID: event_json.get(ATTR_DEVICE_ID, ''), + ATTR_DEVICE_NAME: event_json.get(ATTR_DEVICE_NAME, ''), + ATTR_DEVICE_TYPE: event_json.get(ATTR_DEVICE_TYPE, ''), + ATTR_EVENT_CODE: event_json.get(ATTR_EVENT_CODE, ''), + ATTR_EVENT_NAME: event_json.get(ATTR_EVENT_NAME, ''), + ATTR_EVENT_TYPE: event_json.get(ATTR_EVENT_TYPE, ''), + ATTR_EVENT_UTC: event_json.get(ATTR_EVENT_UTC, ''), + ATTR_USER_NAME: event_json.get(ATTR_USER_NAME, ''), + ATTR_DATE: event_json.get(ATTR_DATE, ''), + ATTR_TIME: event_json.get(ATTR_TIME, ''), + } + + hass.bus.fire(event, data) + + events = [TIMELINE.ALARM_GROUP, TIMELINE.ALARM_END_GROUP, + TIMELINE.PANEL_FAULT_GROUP, TIMELINE.PANEL_RESTORE_GROUP, + TIMELINE.AUTOMATION_GROUP] + + for event in events: + hass.data[DOMAIN].abode.events.add_event_callback( + event, + partial(event_callback, event)) + + +class AbodeDevice(Entity): + """Representation of an Abode device.""" + + def __init__(self, data, device): + """Initialize a sensor for Abode device.""" + self._data = data + self._device = device + + async def async_added_to_hass(self): + """Subscribe Abode events.""" + self.hass.async_add_job( + self._data.abode.events.add_device_callback, + self._device.device_id, self._update_callback + ) + + @property + def should_poll(self): + """Return the polling state.""" + return self._data.polling + + def update(self): + """Update automation state.""" + self._device.refresh() + + @property + def name(self): + """Return the name of the sensor.""" + return self._device.name + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + 'device_id': self._device.device_id, + 'battery_low': self._device.battery_low, + 'no_response': self._device.no_response, + 'device_type': self._device.type + } + + def _update_callback(self, device): + """Update the device state.""" + self.schedule_update_ha_state() + + +class AbodeAutomation(Entity): + """Representation of an Abode automation.""" + + def __init__(self, data, automation, event=None): + """Initialize for Abode automation.""" + self._data = data + self._automation = automation + self._event = event + + async def async_added_to_hass(self): + """Subscribe Abode events.""" + if self._event: + self.hass.async_add_job( + self._data.abode.events.add_event_callback, + self._event, self._update_callback + ) + + @property + def should_poll(self): + """Return the polling state.""" + return self._data.polling + + def update(self): + """Update automation state.""" + self._automation.refresh() + + @property + def name(self): + """Return the name of the sensor.""" + return self._automation.name + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + 'automation_id': self._automation.automation_id, + 'type': self._automation.type, + 'sub_type': self._automation.sub_type + } + + def _update_callback(self, device): + """Update the device state.""" + self._automation.refresh() + self.schedule_update_ha_state() diff --git a/homeassistant/components/abode/alarm_control_panel.py b/homeassistant/components/abode/alarm_control_panel.py new file mode 100644 index 0000000000000..ec5038a7a8440 --- /dev/null +++ b/homeassistant/components/abode/alarm_control_panel.py @@ -0,0 +1,80 @@ +"""Support for Abode Security System alarm control panels.""" +import logging + +import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.abode import CONF_ATTRIBUTION, AbodeDevice +from homeassistant.components.abode import DOMAIN as ABODE_DOMAIN +from homeassistant.const import ( + ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, + STATE_ALARM_DISARMED) + +DEPENDENCIES = ['abode'] + +_LOGGER = logging.getLogger(__name__) + +ICON = 'mdi:security' + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up an alarm control panel for an Abode device.""" + data = hass.data[ABODE_DOMAIN] + + alarm_devices = [AbodeAlarm(data, data.abode.get_alarm(), data.name)] + + data.devices.extend(alarm_devices) + + add_entities(alarm_devices) + + +class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanel): + """An alarm_control_panel implementation for Abode.""" + + def __init__(self, data, device, name): + """Initialize the alarm control panel.""" + super().__init__(data, device) + self._name = name + + @property + def icon(self): + """Return the icon.""" + return ICON + + @property + def state(self): + """Return the state of the device.""" + if self._device.is_standby: + state = STATE_ALARM_DISARMED + elif self._device.is_away: + state = STATE_ALARM_ARMED_AWAY + elif self._device.is_home: + state = STATE_ALARM_ARMED_HOME + else: + state = None + return state + + def alarm_disarm(self, code=None): + """Send disarm command.""" + self._device.set_standby() + + def alarm_arm_home(self, code=None): + """Send arm home command.""" + self._device.set_home() + + def alarm_arm_away(self, code=None): + """Send arm away command.""" + self._device.set_away() + + @property + def name(self): + """Return the name of the alarm.""" + return self._name or super().name + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + 'device_id': self._device.device_id, + 'battery_backup': self._device.battery, + 'cellular_backup': self._device.is_cellular, + } diff --git a/homeassistant/components/abode/binary_sensor.py b/homeassistant/components/abode/binary_sensor.py new file mode 100644 index 0000000000000..47baef1d7e5d7 --- /dev/null +++ b/homeassistant/components/abode/binary_sensor.py @@ -0,0 +1,68 @@ +"""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'] + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up a sensor for an Abode device.""" + import abodepy.helpers.constants as CONST + import abodepy.helpers.timeline as TIMELINE + + data = hass.data[ABODE_DOMAIN] + + device_types = [CONST.TYPE_CONNECTIVITY, CONST.TYPE_MOISTURE, + CONST.TYPE_MOTION, CONST.TYPE_OCCUPANCY, + CONST.TYPE_OPENING] + + devices = [] + for device in data.abode.get_devices(generic_type=device_types): + if data.is_excluded(device): + continue + + devices.append(AbodeBinarySensor(data, device)) + + for automation in data.abode.get_automations( + generic_type=CONST.TYPE_QUICK_ACTION): + if data.is_automation_excluded(automation): + continue + + devices.append(AbodeQuickActionBinarySensor( + data, automation, TIMELINE.AUTOMATION_EDIT_GROUP)) + + data.devices.extend(devices) + + add_entities(devices) + + +class AbodeBinarySensor(AbodeDevice, BinarySensorDevice): + """A binary sensor implementation for Abode device.""" + + @property + def is_on(self): + """Return True if the binary sensor is on.""" + return self._device.is_on + + @property + def device_class(self): + """Return the class of the binary sensor.""" + return self._device.generic_type + + +class AbodeQuickActionBinarySensor(AbodeAutomation, BinarySensorDevice): + """A binary sensor implementation for Abode quick action automations.""" + + def trigger(self): + """Trigger a quick automation.""" + self._automation.trigger() + + @property + def is_on(self): + """Return True if the binary sensor is on.""" + return self._automation.is_active diff --git a/homeassistant/components/abode/camera.py b/homeassistant/components/abode/camera.py new file mode 100644 index 0000000000000..99613d07c47e8 --- /dev/null +++ b/homeassistant/components/abode/camera.py @@ -0,0 +1,93 @@ +"""Support for Abode Security System cameras.""" +import logging + +from datetime import timedelta +import requests + +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) + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up Abode camera devices.""" + import abodepy.helpers.constants as CONST + import abodepy.helpers.timeline as TIMELINE + + data = hass.data[ABODE_DOMAIN] + + devices = [] + for device in data.abode.get_devices(generic_type=CONST.TYPE_CAMERA): + if data.is_excluded(device): + continue + + devices.append(AbodeCamera(data, device, TIMELINE.CAPTURE_IMAGE)) + + data.devices.extend(devices) + + add_entities(devices) + + +class AbodeCamera(AbodeDevice, Camera): + """Representation of an Abode camera.""" + + def __init__(self, data, device, event): + """Initialize the Abode device.""" + AbodeDevice.__init__(self, data, device) + Camera.__init__(self) + self._event = event + self._response = None + + async def async_added_to_hass(self): + """Subscribe Abode events.""" + await super().async_added_to_hass() + + self.hass.async_add_job( + self._data.abode.events.add_timeline_callback, + self._event, self._capture_callback + ) + + def capture(self): + """Request a new image capture.""" + return self._device.capture() + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def refresh_image(self): + """Find a new image on the timeline.""" + if self._device.refresh_image(): + self.get_image() + + def get_image(self): + """Attempt to download the most recent capture.""" + if self._device.image_url: + try: + self._response = requests.get( + self._device.image_url, stream=True) + + self._response.raise_for_status() + except requests.HTTPError as err: + _LOGGER.warning("Failed to get camera image: %s", err) + self._response = None + else: + self._response = None + + def camera_image(self): + """Get a camera image.""" + self.refresh_image() + + if self._response: + return self._response.content + + return None + + def _capture_callback(self, capture): + """Update the image with the device then refresh device.""" + self._device.update_image_location(capture) + self.get_image() + self.schedule_update_ha_state() diff --git a/homeassistant/components/abode/cover.py b/homeassistant/components/abode/cover.py new file mode 100644 index 0000000000000..03d6219ebce4b --- /dev/null +++ b/homeassistant/components/abode/cover.py @@ -0,0 +1,44 @@ +"""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__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up Abode cover devices.""" + import abodepy.helpers.constants as CONST + + data = hass.data[ABODE_DOMAIN] + + devices = [] + for device in data.abode.get_devices(generic_type=CONST.TYPE_COVER): + if data.is_excluded(device): + continue + + devices.append(AbodeCover(data, device)) + + data.devices.extend(devices) + + add_entities(devices) + + +class AbodeCover(AbodeDevice, CoverDevice): + """Representation of an Abode cover.""" + + @property + def is_closed(self): + """Return true if cover is closed, else False.""" + return not self._device.is_open + + def close_cover(self, **kwargs): + """Issue close command to cover.""" + self._device.close_cover() + + def open_cover(self, **kwargs): + """Issue open command to cover.""" + self._device.open_cover() diff --git a/homeassistant/components/abode/light.py b/homeassistant/components/abode/light.py new file mode 100644 index 0000000000000..aabf5fbccdc23 --- /dev/null +++ b/homeassistant/components/abode/light.py @@ -0,0 +1,98 @@ +"""Support for Abode Security System lights.""" +import logging +from math import ceil +from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_COLOR_TEMP, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, Light) +from homeassistant.util.color import ( + color_temperature_kelvin_to_mired, color_temperature_mired_to_kelvin) + + +DEPENDENCIES = ['abode'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up Abode light devices.""" + import abodepy.helpers.constants as CONST + + data = hass.data[ABODE_DOMAIN] + + device_types = [CONST.TYPE_LIGHT, CONST.TYPE_SWITCH] + + devices = [] + + # Get all regular lights that are not excluded or switches marked as lights + for device in data.abode.get_devices(generic_type=device_types): + if data.is_excluded(device) or not data.is_light(device): + continue + + devices.append(AbodeLight(data, device)) + + data.devices.extend(devices) + + add_entities(devices) + + +class AbodeLight(AbodeDevice, Light): + """Representation of an Abode light.""" + + def turn_on(self, **kwargs): + """Turn on the light.""" + if ATTR_COLOR_TEMP in kwargs and self._device.is_color_capable: + self._device.set_color_temp( + int(color_temperature_mired_to_kelvin( + kwargs[ATTR_COLOR_TEMP]))) + + if ATTR_HS_COLOR in kwargs and self._device.is_color_capable: + self._device.set_color(kwargs[ATTR_HS_COLOR]) + + if ATTR_BRIGHTNESS in kwargs and self._device.is_dimmable: + # Convert HASS brightness (0-255) to Abode brightness (0-99) + # If 100 is sent to Abode, response is 99 causing an error + self._device.set_level(ceil(kwargs[ATTR_BRIGHTNESS] * 99 / 255.0)) + else: + self._device.switch_on() + + def turn_off(self, **kwargs): + """Turn off the light.""" + self._device.switch_off() + + @property + def is_on(self): + """Return true if device is on.""" + return self._device.is_on + + @property + def brightness(self): + """Return the brightness of the light.""" + if self._device.is_dimmable and self._device.has_brightness: + brightness = int(self._device.brightness) + # Abode returns 100 during device initialization and device refresh + if brightness == 100: + return 255 + # Convert Abode brightness (0-99) to HASS brightness (0-255) + return ceil(brightness * 255 / 99.0) + + @property + def color_temp(self): + """Return the color temp of the light.""" + if self._device.has_color: + return color_temperature_kelvin_to_mired(self._device.color_temp) + + @property + def hs_color(self): + """Return the color of the light.""" + if self._device.has_color: + return self._device.color + + @property + def supported_features(self): + """Flag supported features.""" + if self._device.is_dimmable and self._device.is_color_capable: + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_COLOR_TEMP + if self._device.is_dimmable: + return SUPPORT_BRIGHTNESS + return 0 diff --git a/homeassistant/components/abode/lock.py b/homeassistant/components/abode/lock.py new file mode 100644 index 0000000000000..ce6634268e96e --- /dev/null +++ b/homeassistant/components/abode/lock.py @@ -0,0 +1,44 @@ +"""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__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up Abode lock devices.""" + import abodepy.helpers.constants as CONST + + data = hass.data[ABODE_DOMAIN] + + devices = [] + for device in data.abode.get_devices(generic_type=CONST.TYPE_LOCK): + if data.is_excluded(device): + continue + + devices.append(AbodeLock(data, device)) + + data.devices.extend(devices) + + add_entities(devices) + + +class AbodeLock(AbodeDevice, LockDevice): + """Representation of an Abode lock.""" + + def lock(self, **kwargs): + """Lock the device.""" + self._device.lock() + + def unlock(self, **kwargs): + """Unlock the device.""" + self._device.unlock() + + @property + def is_locked(self): + """Return true if device is on.""" + return self._device.is_locked diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py new file mode 100644 index 0000000000000..fa6cb9323bf28 --- /dev/null +++ b/homeassistant/components/abode/sensor.py @@ -0,0 +1,78 @@ +"""Support for Abode Security System sensors.""" +import logging + +from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN +from homeassistant.const import ( + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['abode'] + +# Sensor types: Name, icon +SENSOR_TYPES = { + 'temp': ['Temperature', DEVICE_CLASS_TEMPERATURE], + 'humidity': ['Humidity', DEVICE_CLASS_HUMIDITY], + 'lux': ['Lux', DEVICE_CLASS_ILLUMINANCE], +} + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up a sensor for an Abode device.""" + import abodepy.helpers.constants as CONST + + data = hass.data[ABODE_DOMAIN] + + devices = [] + for device in data.abode.get_devices(generic_type=CONST.TYPE_SENSOR): + if data.is_excluded(device): + continue + + for sensor_type in SENSOR_TYPES: + devices.append(AbodeSensor(data, device, sensor_type)) + + data.devices.extend(devices) + + add_entities(devices) + + +class AbodeSensor(AbodeDevice): + """A sensor implementation for Abode devices.""" + + def __init__(self, data, device, sensor_type): + """Initialize a sensor for an Abode device.""" + super().__init__(data, device) + self._sensor_type = sensor_type + self._name = '{0} {1}'.format( + self._device.name, SENSOR_TYPES[self._sensor_type][0]) + self._device_class = SENSOR_TYPES[self._sensor_type][1] + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def device_class(self): + """Return the device class.""" + return self._device_class + + @property + def state(self): + """Return the state of the sensor.""" + if self._sensor_type == 'temp': + return self._device.temp + if self._sensor_type == 'humidity': + return self._device.humidity + if self._sensor_type == 'lux': + return self._device.lux + + @property + def unit_of_measurement(self): + """Return the units of measurement.""" + if self._sensor_type == 'temp': + return self._device.temp_unit + if self._sensor_type == 'humidity': + return self._device.humidity_unit + if self._sensor_type == 'lux': + return self._device.lux_unit diff --git a/homeassistant/components/abode/services.yaml b/homeassistant/components/abode/services.yaml new file mode 100644 index 0000000000000..ad0bb076d90b4 --- /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/abode/switch.py b/homeassistant/components/abode/switch.py new file mode 100644 index 0000000000000..d5303a27cd2fb --- /dev/null +++ b/homeassistant/components/abode/switch.py @@ -0,0 +1,74 @@ +"""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'] + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up Abode switch devices.""" + import abodepy.helpers.constants as CONST + import abodepy.helpers.timeline as TIMELINE + + data = hass.data[ABODE_DOMAIN] + + devices = [] + + # Get all regular switches that are not excluded or marked as lights + for device in data.abode.get_devices(generic_type=CONST.TYPE_SWITCH): + if data.is_excluded(device) or data.is_light(device): + continue + + devices.append(AbodeSwitch(data, device)) + + # Get all Abode automations that can be enabled/disabled + for automation in data.abode.get_automations( + generic_type=CONST.TYPE_AUTOMATION): + if data.is_automation_excluded(automation): + continue + + devices.append(AbodeAutomationSwitch( + data, automation, TIMELINE.AUTOMATION_EDIT_GROUP)) + + data.devices.extend(devices) + + add_entities(devices) + + +class AbodeSwitch(AbodeDevice, SwitchDevice): + """Representation of an Abode switch.""" + + def turn_on(self, **kwargs): + """Turn on the device.""" + self._device.switch_on() + + def turn_off(self, **kwargs): + """Turn off the device.""" + self._device.switch_off() + + @property + def is_on(self): + """Return true if device is on.""" + return self._device.is_on + + +class AbodeAutomationSwitch(AbodeAutomation, SwitchDevice): + """A switch implementation for Abode automations.""" + + def turn_on(self, **kwargs): + """Turn on the device.""" + self._automation.set_active(True) + + def turn_off(self, **kwargs): + """Turn off the device.""" + self._automation.set_active(False) + + @property + def is_on(self): + """Return True if the binary sensor is on.""" + return self._automation.is_active diff --git a/homeassistant/components/ads/__init__.py b/homeassistant/components/ads/__init__.py index 360236790f880..cfd0f37caa0d6 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 @@ 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 @@ def add_device_notification(self, name, plc_datatype, callback): 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/ads/binary_sensor.py b/homeassistant/components/ads/binary_sensor.py new file mode 100644 index 0000000000000..6771e99cd77d1 --- /dev/null +++ b/homeassistant/components/ads/binary_sensor.py @@ -0,0 +1,83 @@ +"""Support for ADS binary sensors.""" +import logging + +import voluptuous as vol + +from homeassistant.components.ads import CONF_ADS_VAR, DATA_ADS +from homeassistant.components.binary_sensor import ( + DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice) +from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'ADS binary sensor' +DEPENDENCIES = ['ads'] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_ADS_VAR): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Binary Sensor platform for ADS.""" + ads_hub = hass.data.get(DATA_ADS) + + ads_var = config.get(CONF_ADS_VAR) + name = config.get(CONF_NAME) + device_class = config.get(CONF_DEVICE_CLASS) + + ads_sensor = AdsBinarySensor(ads_hub, name, ads_var, device_class) + add_entities([ads_sensor]) + + +class AdsBinarySensor(BinarySensorDevice): + """Representation of ADS binary sensors.""" + + 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 + self.ads_var = ads_var + + async def async_added_to_hass(self): + """Register device notification.""" + def update(name, value): + """Handle device notifications.""" + _LOGGER.debug('Variable %s changed its value to %d', name, value) + self._state = value + self.schedule_update_ha_state() + + self.hass.async_add_job( + self._ads_hub.add_device_notification, + self.ads_var, self._ads_hub.PLCTYPE_BOOL, update) + + @property + def name(self): + """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.""" + return self._device_class + + @property + def is_on(self): + """Return if the binary sensor is on.""" + return self._state + + @property + def should_poll(self): + """Return False because entity pushes its state to HA.""" + return False diff --git a/homeassistant/components/ads/light.py b/homeassistant/components/ads/light.py new file mode 100644 index 0000000000000..e5299821e39c9 --- /dev/null +++ b/homeassistant/components/ads/light.py @@ -0,0 +1,118 @@ +"""Support for ADS light sources.""" +import logging +import voluptuous as vol +from homeassistant.components.light import Light, ATTR_BRIGHTNESS, \ + SUPPORT_BRIGHTNESS, PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME +from homeassistant.components.ads import DATA_ADS, CONF_ADS_VAR, \ + CONF_ADS_VAR_BRIGHTNESS +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['ads'] +DEFAULT_NAME = 'ADS Light' +CONF_ADSVAR_BRIGHTNESS = 'adsvar_brightness' +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_ADS_VAR): cv.string, + vol.Optional(CONF_ADS_VAR_BRIGHTNESS): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the light platform for ADS.""" + ads_hub = hass.data.get(DATA_ADS) + + ads_var_enable = config.get(CONF_ADS_VAR) + ads_var_brightness = config.get(CONF_ADS_VAR_BRIGHTNESS) + name = config.get(CONF_NAME) + + add_entities([AdsLight(ads_hub, ads_var_enable, ads_var_brightness, + name)], True) + + +class AdsLight(Light): + """Representation of ADS light.""" + + def __init__(self, ads_hub, ads_var_enable, ads_var_brightness, name): + """Initialize AdsLight entity.""" + self._ads_hub = ads_hub + 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 + + async def async_added_to_hass(self): + """Register device notification.""" + def update_on_state(name, value): + """Handle device notifications for state.""" + _LOGGER.debug('Variable %s changed its value to %d', name, value) + self._on_state = value + self.schedule_update_ha_state() + + def update_brightness(name, value): + """Handle device notification for brightness.""" + _LOGGER.debug('Variable %s changed its value to %d', name, value) + self._brightness = value + self.schedule_update_ha_state() + + self.hass.async_add_executor_job( + self._ads_hub.add_device_notification, + self.ads_var_enable, self._ads_hub.PLCTYPE_BOOL, update_on_state + ) + if self.ads_var_brightness is not None: + self.hass.async_add_executor_job( + self._ads_hub.add_device_notification, + self.ads_var_brightness, self._ads_hub.PLCTYPE_INT, + update_brightness + ) + + @property + def name(self): + """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).""" + return self._brightness + + @property + def is_on(self): + """Return if light is on.""" + return self._on_state + + @property + def should_poll(self): + """Return False because entity pushes its state to HA.""" + return False + + @property + def supported_features(self): + """Flag supported features.""" + support = 0 + if self.ads_var_brightness is not None: + support = SUPPORT_BRIGHTNESS + return support + + def turn_on(self, **kwargs): + """Turn the light on or set a specific dimmer value.""" + brightness = kwargs.get(ATTR_BRIGHTNESS) + self._ads_hub.write_by_name(self.ads_var_enable, True, + self._ads_hub.PLCTYPE_BOOL) + + if self.ads_var_brightness is not None and brightness is not None: + self._ads_hub.write_by_name(self.ads_var_brightness, brightness, + self._ads_hub.PLCTYPE_UINT) + + def turn_off(self, **kwargs): + """Turn the light off.""" + self._ads_hub.write_by_name(self.ads_var_enable, False, + self._ads_hub.PLCTYPE_BOOL) diff --git a/homeassistant/components/ads/sensor.py b/homeassistant/components/ads/sensor.py new file mode 100644 index 0000000000000..2972f50d8040a --- /dev/null +++ b/homeassistant/components/ads/sensor.py @@ -0,0 +1,101 @@ +"""Support for ADS sensors.""" +import logging + +import voluptuous as vol + +from homeassistant.components import ads +from homeassistant.components.ads import ( + CONF_ADS_FACTOR, CONF_ADS_TYPE, CONF_ADS_VAR) +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = "ADS sensor" +DEPENDENCIES = ['ads'] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_ADS_VAR): cv.string, + vol.Optional(CONF_ADS_FACTOR): cv.positive_int, + vol.Optional(CONF_ADS_TYPE, default=ads.ADSTYPE_INT): + vol.In([ads.ADSTYPE_INT, ads.ADSTYPE_UINT, ads.ADSTYPE_BYTE]), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_UNIT_OF_MEASUREMENT, default=''): cv.string, +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up an ADS sensor device.""" + ads_hub = hass.data.get(ads.DATA_ADS) + + ads_var = config.get(CONF_ADS_VAR) + ads_type = config.get(CONF_ADS_TYPE) + name = config.get(CONF_NAME) + unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT) + factor = config.get(CONF_ADS_FACTOR) + + entity = AdsSensor( + ads_hub, ads_var, ads_type, name, unit_of_measurement, factor) + + add_entities([entity]) + + +class AdsSensor(Entity): + """Representation of an ADS sensor entity.""" + + def __init__(self, ads_hub, ads_var, ads_type, name, unit_of_measurement, + factor): + """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 + self.ads_type = ads_type + self.factor = factor + + async def async_added_to_hass(self): + """Register device notification.""" + def update(name, value): + """Handle device notifications.""" + _LOGGER.debug("Variable %s changed its value to %d", name, value) + + # If factor is set use it otherwise not + if self.factor is None: + self._value = value + else: + self._value = value / self.factor + self.schedule_update_ha_state() + + self.hass.async_add_job( + self._ads_hub.add_device_notification, + self.ads_var, self._ads_hub.ADS_TYPEMAP[self.ads_type], update + ) + + @property + def name(self): + """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.""" + return self._value + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit_of_measurement + + @property + def should_poll(self): + """Return False because entity pushes its state.""" + return False diff --git a/homeassistant/components/ads/switch.py b/homeassistant/components/ads/switch.py new file mode 100644 index 0000000000000..e3aee023f21f2 --- /dev/null +++ b/homeassistant/components/ads/switch.py @@ -0,0 +1,85 @@ +"""Support for ADS switch platform.""" +import logging + +import voluptuous as vol + +from homeassistant.components.ads import CONF_ADS_VAR, DATA_ADS +from homeassistant.components.switch import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import ToggleEntity + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['ads'] + +DEFAULT_NAME = 'ADS Switch' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_ADS_VAR): cv.string, + vol.Optional(CONF_NAME): cv.string, +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up switch platform for ADS.""" + ads_hub = hass.data.get(DATA_ADS) + + name = config.get(CONF_NAME) + ads_var = config.get(CONF_ADS_VAR) + + add_entities([AdsSwitch(ads_hub, name, ads_var)], True) + + +class AdsSwitch(ToggleEntity): + """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) + self._on_state = value + self.schedule_update_ha_state() + + self.hass.async_add_job( + self._ads_hub.add_device_notification, + self.ads_var, self._ads_hub.PLCTYPE_BOOL, update) + + @property + def is_on(self): + """Return if the switch is turned on.""" + return self._on_state + + @property + def name(self): + """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.""" + return False + + def turn_on(self, **kwargs): + """Turn the switch on.""" + self._ads_hub.write_by_name( + self.ads_var, True, self._ads_hub.PLCTYPE_BOOL) + + def turn_off(self, **kwargs): + """Turn the switch off.""" + self._ads_hub.write_by_name( + self.ads_var, False, self._ads_hub.PLCTYPE_BOOL) diff --git a/homeassistant/components/air_quality/__init__.py b/homeassistant/components/air_quality/__init__.py index 5f770e84b37df..66af51efcb183 100644 --- a/homeassistant/components/air_quality/__init__.py +++ b/homeassistant/components/air_quality/__init__.py @@ -8,7 +8,8 @@ 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 0000000000000..372f3ec079dde --- /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 fe3cca4876ea3..d77c0c9bfe20a 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 e02e074189cc1..86bb3e73bdab9 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -14,7 +14,7 @@ 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/alarm_control_panel/abode.py b/homeassistant/components/alarm_control_panel/abode.py deleted file mode 100644 index 6d4e28243ea78..0000000000000 --- a/homeassistant/components/alarm_control_panel/abode.py +++ /dev/null @@ -1,90 +0,0 @@ -""" -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/ -""" -import logging - -import homeassistant.components.alarm_control_panel as alarm -from homeassistant.components.abode import CONF_ATTRIBUTION, AbodeDevice -from homeassistant.components.abode import DOMAIN as ABODE_DOMAIN -from homeassistant.const import ( - ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_DISARMED) - -DEPENDENCIES = ['abode'] - -_LOGGER = logging.getLogger(__name__) - -ICON = 'mdi:security' - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up an alarm control panel for an Abode device.""" - data = hass.data[ABODE_DOMAIN] - - alarm_devices = [AbodeAlarm(data, data.abode.get_alarm(), data.name)] - - data.devices.extend(alarm_devices) - - add_entities(alarm_devices) - - -class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanel): - """An alarm_control_panel implementation for Abode.""" - - def __init__(self, data, device, name): - """Initialize the alarm control panel.""" - super().__init__(data, device) - self._name = name - - @property - def icon(self): - """Return the icon.""" - return ICON - - @property - def state(self): - """Return the state of the device.""" - if self._device.is_standby: - state = STATE_ALARM_DISARMED - elif self._device.is_away: - state = STATE_ALARM_ARMED_AWAY - elif self._device.is_home: - state = STATE_ALARM_ARMED_HOME - else: - 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() - - def alarm_arm_home(self, code=None): - """Send arm home command.""" - self._device.set_home() - - def alarm_arm_away(self, code=None): - """Send arm away command.""" - self._device.set_away() - - @property - def name(self): - """Return the name of the alarm.""" - return self._name or super().name - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - 'device_id': self._device.device_id, - 'battery_backup': self._device.battery, - 'cellular_backup': self._device.is_cellular, - } diff --git a/homeassistant/components/alarm_control_panel/alarmdecoder.py b/homeassistant/components/alarm_control_panel/alarmdecoder.py deleted file mode 100644 index 16e82280433d7..0000000000000 --- a/homeassistant/components/alarm_control_panel/alarmdecoder.py +++ /dev/null @@ -1,142 +0,0 @@ -""" -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/ -""" -import logging - -import voluptuous as vol - -import homeassistant.components.alarm_control_panel as alarm -from homeassistant.components.alarmdecoder import DATA_AD, SIGNAL_PANEL_MESSAGE -from homeassistant.const import ( - ATTR_CODE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED) -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['alarmdecoder'] - -SERVICE_ALARM_TOGGLE_CHIME = 'alarmdecoder_alarm_toggle_chime' -ALARM_TOGGLE_CHIME_SCHEMA = vol.Schema({ - vol.Required(ATTR_CODE): cv.string, -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up for AlarmDecoder alarm panels.""" - device = AlarmDecoderAlarmPanel() - add_entities([device]) - - def alarm_toggle_chime_handler(service): - """Register toggle chime handler.""" - code = service.data.get(ATTR_CODE) - device.alarm_toggle_chime(code) - - hass.services.register( - alarm.DOMAIN, SERVICE_ALARM_TOGGLE_CHIME, alarm_toggle_chime_handler, - schema=ALARM_TOGGLE_CHIME_SCHEMA) - - -class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel): - """Representation of an AlarmDecoder-based alarm panel.""" - - def __init__(self): - """Initialize the alarm panel.""" - self._display = "" - self._name = "Alarm Panel" - self._state = None - self._ac_power = None - self._backlight_on = None - self._battery_low = None - self._check_zone = None - self._chime = None - self._entry_delay_off = None - self._programming_mode = None - self._ready = None - self._zone_bypassed = None - - async def async_added_to_hass(self): - """Register callbacks.""" - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_PANEL_MESSAGE, self._message_callback) - - def _message_callback(self, message): - """Handle received messages.""" - if message.alarm_sounding or message.fire_alarm: - self._state = STATE_ALARM_TRIGGERED - elif message.armed_away: - self._state = STATE_ALARM_ARMED_AWAY - elif message.armed_home: - self._state = STATE_ALARM_ARMED_HOME - else: - self._state = STATE_ALARM_DISARMED - - self._ac_power = message.ac_power - self._backlight_on = message.backlight_on - self._battery_low = message.battery_low - self._check_zone = message.check_zone - self._chime = message.chime_on - self._entry_delay_off = message.entry_delay_off - self._programming_mode = message.programming_mode - self._ready = message.ready - self._zone_bypassed = message.zone_bypassed - - self.schedule_update_ha_state() - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def should_poll(self): - """Return the polling state.""" - return False - - @property - def code_format(self): - """Return one or more digits/characters.""" - return alarm.FORMAT_NUMBER - - @property - def state(self): - """Return the state of the device.""" - return self._state - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return { - 'ac_power': self._ac_power, - 'backlight_on': self._backlight_on, - 'battery_low': self._battery_low, - 'check_zone': self._check_zone, - 'chime': self._chime, - 'entry_delay_off': self._entry_delay_off, - 'programming_mode': self._programming_mode, - 'ready': self._ready, - 'zone_bypassed': self._zone_bypassed, - } - - def alarm_disarm(self, code=None): - """Send disarm command.""" - if code: - self.hass.data[DATA_AD].send("{!s}1".format(code)) - - def alarm_arm_away(self, code=None): - """Send arm away command.""" - if code: - self.hass.data[DATA_AD].send("{!s}2".format(code)) - - def alarm_arm_home(self, code=None): - """Send arm home command.""" - if code: - self.hass.data[DATA_AD].send("{!s}3".format(code)) - - def alarm_toggle_chime(self, code=None): - """Send toggle chime command.""" - if code: - self.hass.data[DATA_AD].send("{!s}9".format(code)) diff --git a/homeassistant/components/alarm_control_panel/arlo.py b/homeassistant/components/alarm_control_panel/arlo.py deleted file mode 100644 index 66f11fab83f74..0000000000000 --- a/homeassistant/components/alarm_control_panel/arlo.py +++ /dev/null @@ -1,141 +0,0 @@ -""" -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/ -""" -import logging - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.components.alarm_control_panel import ( - AlarmControlPanel, PLATFORM_SCHEMA) -from homeassistant.components.arlo import ( - DATA_ARLO, CONF_ATTRIBUTION, SIGNAL_UPDATE_ARLO) -from homeassistant.const import ( - ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT) - -_LOGGER = logging.getLogger(__name__) - -ARMED = 'armed' - -CONF_HOME_MODE_NAME = 'home_mode_name' -CONF_AWAY_MODE_NAME = 'away_mode_name' -CONF_NIGHT_MODE_NAME = 'night_mode_name' - -DEPENDENCIES = ['arlo'] - -DISARMED = 'disarmed' - -ICON = 'mdi:security' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_HOME_MODE_NAME, default=ARMED): cv.string, - vol.Optional(CONF_AWAY_MODE_NAME, default=ARMED): cv.string, - vol.Optional(CONF_NIGHT_MODE_NAME, default=ARMED): cv.string, -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Arlo Alarm Control Panels.""" - arlo = hass.data[DATA_ARLO] - - if not arlo.base_stations: - return - - home_mode_name = config.get(CONF_HOME_MODE_NAME) - away_mode_name = config.get(CONF_AWAY_MODE_NAME) - night_mode_name = config.get(CONF_NIGHT_MODE_NAME) - base_stations = [] - for base_station in arlo.base_stations: - base_stations.append(ArloBaseStation(base_station, home_mode_name, - away_mode_name, night_mode_name)) - add_entities(base_stations, True) - - -class ArloBaseStation(AlarmControlPanel): - """Representation of an Arlo Alarm Control Panel.""" - - def __init__(self, data, home_mode_name, away_mode_name, night_mode_name): - """Initialize the alarm control panel.""" - self._base_station = data - self._home_mode_name = home_mode_name - self._away_mode_name = away_mode_name - self._night_mode_name = night_mode_name - self._state = None - - @property - def icon(self): - """Return icon.""" - return ICON - - async def async_added_to_hass(self): - """Register callbacks.""" - async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_ARLO, self._update_callback) - - @callback - def _update_callback(self): - """Call update method.""" - self.async_schedule_update_ha_state(True) - - @property - def state(self): - """Return the state of the device.""" - return self._state - - def update(self): - """Update the state of the device.""" - _LOGGER.debug("Updating Arlo Alarm Control Panel %s", self.name) - mode = self._base_station.mode - if mode: - self._state = self._get_state_from_mode(mode) - else: - self._state = None - - async def async_alarm_disarm(self, code=None): - """Send disarm command.""" - self._base_station.mode = DISARMED - - async def async_alarm_arm_away(self, code=None): - """Send arm away command. Uses custom mode.""" - self._base_station.mode = self._away_mode_name - - async def async_alarm_arm_home(self, code=None): - """Send arm home command. Uses custom mode.""" - self._base_station.mode = self._home_mode_name - - async def async_alarm_arm_night(self, code=None): - """Send arm night command. Uses custom mode.""" - self._base_station.mode = self._night_mode_name - - @property - def name(self): - """Return the name of the base station.""" - return self._base_station.name - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - 'device_id': self._base_station.device_id - } - - def _get_state_from_mode(self, mode): - """Convert Arlo mode to Home Assistant state.""" - if mode == ARMED: - return STATE_ALARM_ARMED_AWAY - if mode == DISARMED: - return STATE_ALARM_DISARMED - if mode == self._home_mode_name: - return STATE_ALARM_ARMED_HOME - if mode == self._away_mode_name: - return STATE_ALARM_ARMED_AWAY - if mode == self._night_mode_name: - return STATE_ALARM_ARMED_NIGHT - return mode diff --git a/homeassistant/components/alarm_control_panel/blink.py b/homeassistant/components/alarm_control_panel/blink.py deleted file mode 100644 index 77267fd7516c8..0000000000000 --- a/homeassistant/components/alarm_control_panel/blink.py +++ /dev/null @@ -1,91 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.components.alarm_control_panel import AlarmControlPanel -from homeassistant.components.blink import ( - BLINK_DATA, DEFAULT_ATTRIBUTION) -from homeassistant.const import ( - ATTR_ATTRIBUTION, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY) - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['blink'] - -ICON = 'mdi:security' - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Arlo Alarm Control Panels.""" - if discovery_info is None: - return - data = hass.data[BLINK_DATA] - - sync_modules = [] - for sync_name, sync_module in data.sync.items(): - sync_modules.append(BlinkSyncModule(data, sync_name, sync_module)) - add_entities(sync_modules, True) - - -class BlinkSyncModule(AlarmControlPanel): - """Representation of a Blink Alarm Control Panel.""" - - def __init__(self, data, name, sync): - """Initialize the alarm control panel.""" - self.data = data - self.sync = sync - self._name = name - self._state = None - - @property - def unique_id(self): - """Return the unique id for the sync module.""" - return self.sync.serial - - @property - def icon(self): - """Return icon.""" - return ICON - - @property - def state(self): - """Return the state of the device.""" - return self._state - - @property - def name(self): - """Return the name of the panel.""" - return "{} {}".format(BLINK_DATA, self._name) - - @property - def device_state_attributes(self): - """Return the state attributes.""" - attr = self.sync.attributes - attr['network_info'] = self.data.networks - attr['associated_cameras'] = list(self.sync.cameras.keys()) - attr[ATTR_ATTRIBUTION] = DEFAULT_ATTRIBUTION - return attr - - def update(self): - """Update the state of the device.""" - _LOGGER.debug("Updating Blink Alarm Control Panel %s", self._name) - self.data.refresh() - mode = self.sync.arm - if mode: - self._state = STATE_ALARM_ARMED_AWAY - else: - self._state = STATE_ALARM_DISARMED - - def alarm_disarm(self, code=None): - """Send disarm command.""" - self.sync.arm = False - self.sync.refresh() - - def alarm_arm_away(self, code=None): - """Send arm command.""" - self.sync.arm = True - self.sync.refresh() diff --git a/homeassistant/components/alarm_control_panel/egardia.py b/homeassistant/components/alarm_control_panel/egardia.py deleted file mode 100644 index dfd60c4abde14..0000000000000 --- a/homeassistant/components/alarm_control_panel/egardia.py +++ /dev/null @@ -1,142 +0,0 @@ -""" -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/ -""" -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 - ) -DEPENDENCIES = ['egardia'] - -_LOGGER = logging.getLogger(__name__) - -STATES = { - 'ARM': STATE_ALARM_ARMED_AWAY, - 'DAY HOME': STATE_ALARM_ARMED_HOME, - 'DISARM': STATE_ALARM_DISARMED, - 'ARMHOME': STATE_ALARM_ARMED_HOME, - 'HOME': STATE_ALARM_ARMED_HOME, - 'NIGHT HOME': STATE_ALARM_ARMED_NIGHT, - 'TRIGGERED': STATE_ALARM_TRIGGERED -} - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Egardia platform.""" - if discovery_info is None: - return - device = EgardiaAlarm( - discovery_info['name'], - hass.data[EGARDIA_DEVICE], - 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) - - -class EgardiaAlarm(alarm.AlarmControlPanel): - """Representation of a Egardia alarm.""" - - def __init__(self, name, egardiasystem, - rs_enabled=False, rs_codes=None, rs_port=52010): - """Initialize the Egardia alarm.""" - self._name = name - self._egardiasystem = egardiasystem - self._status = None - self._rs_enabled = rs_enabled - self._rs_codes = rs_codes - self._rs_port = rs_port - - async def async_added_to_hass(self): - """Add Egardiaserver callback if enabled.""" - if self._rs_enabled: - _LOGGER.debug("Registering callback to Egardiaserver") - self.hass.data[EGARDIA_SERVER].register_callback( - self.handle_status_event) - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def state(self): - """Return the state of the device.""" - return self._status - - @property - def should_poll(self): - """Poll if no report server is enabled.""" - if not self._rs_enabled: - return True - return False - - def handle_status_event(self, event): - """Handle the Egardia system status event.""" - statuscode = event.get('status') - if statuscode is not None: - status = self.lookupstatusfromcode(statuscode) - self.parsestatus(status) - self.schedule_update_ha_state() - - def lookupstatusfromcode(self, statuscode): - """Look at the rs_codes and returns the status from the code.""" - status = next(( - status_group.upper() for status_group, codes - in self._rs_codes.items() for code in codes - if statuscode == code), 'UNKNOWN') - return status - - def parsestatus(self, status): - """Parse the status.""" - _LOGGER.debug("Parsing status %s", status) - # Ignore the statuscode if it is IGNORE - if status.lower().strip() != REPORT_SERVER_CODES_IGNORE: - _LOGGER.debug("Not ignoring status %s", status) - newstatus = STATES.get(status.upper()) - _LOGGER.debug("newstatus %s", newstatus) - self._status = newstatus - else: - _LOGGER.error("Ignoring status") - - def update(self): - """Update the alarm status.""" - status = self._egardiasystem.getstate() - self.parsestatus(status) - - def alarm_disarm(self, code=None): - """Send disarm command.""" - try: - self._egardiasystem.alarm_disarm() - except requests.exceptions.RequestException as err: - _LOGGER.error("Egardia device exception occurred when " - "sending disarm command: %s", err) - - def alarm_arm_home(self, code=None): - """Send arm home command.""" - try: - self._egardiasystem.alarm_arm_home() - except requests.exceptions.RequestException as err: - _LOGGER.error("Egardia device exception occurred when " - "sending arm home command: %s", err) - - def alarm_arm_away(self, code=None): - """Send arm away command.""" - try: - self._egardiasystem.alarm_arm_away() - except requests.exceptions.RequestException as err: - _LOGGER.error("Egardia device exception occurred when " - "sending arm away command: %s", err) diff --git a/homeassistant/components/alarm_control_panel/elkm1.py b/homeassistant/components/alarm_control_panel/elkm1.py deleted file mode 100644 index c6405f953fdeb..0000000000000 --- a/homeassistant/components/alarm_control_panel/elkm1.py +++ /dev/null @@ -1,204 +0,0 @@ -""" -Each ElkM1 area will be created as a separate alarm_control_panel in HASS. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/alarm_control_panel.elkm1/ -""" - -import voluptuous as vol -import homeassistant.components.alarm_control_panel as alarm -from homeassistant.const import ( - ATTR_CODE, ATTR_ENTITY_ID, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMING, STATE_ALARM_DISARMED, - STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED) -from homeassistant.components.elkm1 import ( - DOMAIN as ELK_DOMAIN, create_elk_entities, ElkEntity) -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, async_dispatcher_send) - -DEPENDENCIES = [ELK_DOMAIN] - -SIGNAL_ARM_ENTITY = 'elkm1_arm' -SIGNAL_DISPLAY_MESSAGE = 'elkm1_display_message' - -ELK_ALARM_SERVICE_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID, default=[]): cv.entity_ids, - vol.Required(ATTR_CODE): vol.All(vol.Coerce(int), vol.Range(0, 999999)), -}) - -DISPLAY_MESSAGE_SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID, default=[]): cv.entity_ids, - vol.Optional('clear', default=2): vol.In([0, 1, 2]), - vol.Optional('beep', default=False): cv.boolean, - vol.Optional('timeout', default=0): vol.Range(min=0, max=65535), - vol.Optional('line1', default=''): cv.string, - vol.Optional('line2', default=''): cv.string, -}) - - -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up the ElkM1 alarm platform.""" - if discovery_info is None: - return - - elk = hass.data[ELK_DOMAIN]['elk'] - entities = create_elk_entities(hass, elk.areas, 'area', ElkArea, []) - async_add_entities(entities, True) - - def _dispatch(signal, entity_ids, *args): - for entity_id in entity_ids: - async_dispatcher_send( - hass, '{}_{}'.format(signal, entity_id), *args) - - def _arm_service(service): - entity_ids = service.data.get(ATTR_ENTITY_ID, []) - arm_level = _arm_services().get(service.service) - args = (arm_level, service.data.get(ATTR_CODE)) - _dispatch(SIGNAL_ARM_ENTITY, entity_ids, *args) - - for service in _arm_services(): - hass.services.async_register( - alarm.DOMAIN, service, _arm_service, ELK_ALARM_SERVICE_SCHEMA) - - def _display_message_service(service): - entity_ids = service.data.get(ATTR_ENTITY_ID, []) - data = service.data - args = (data['clear'], data['beep'], data['timeout'], - data['line1'], data['line2']) - _dispatch(SIGNAL_DISPLAY_MESSAGE, entity_ids, *args) - - hass.services.async_register( - alarm.DOMAIN, 'elkm1_alarm_display_message', - _display_message_service, DISPLAY_MESSAGE_SERVICE_SCHEMA) - - -def _arm_services(): - from elkm1_lib.const import ArmLevel - - return { - 'elkm1_alarm_arm_vacation': ArmLevel.ARMED_VACATION.value, - 'elkm1_alarm_arm_home_instant': ArmLevel.ARMED_STAY_INSTANT.value, - 'elkm1_alarm_arm_night_instant': ArmLevel.ARMED_NIGHT_INSTANT.value, - } - - -class ElkArea(ElkEntity, alarm.AlarmControlPanel): - """Representation of an Area / Partition within the ElkM1 alarm panel.""" - - def __init__(self, element, elk, elk_data): - """Initialize Area as Alarm Control Panel.""" - super().__init__(element, elk, elk_data) - self._changed_by_entity_id = '' - self._state = None - - async def async_added_to_hass(self): - """Register callback for ElkM1 changes.""" - await super().async_added_to_hass() - for keypad in self._elk.keypads: - keypad.add_callback(self._watch_keypad) - async_dispatcher_connect( - self.hass, '{}_{}'.format(SIGNAL_ARM_ENTITY, self.entity_id), - self._arm_service) - async_dispatcher_connect( - self.hass, '{}_{}'.format(SIGNAL_DISPLAY_MESSAGE, self.entity_id), - self._display_message) - - def _watch_keypad(self, keypad, changeset): - if keypad.area != self._element.index: - return - if changeset.get('last_user') is not None: - self._changed_by_entity_id = self.hass.data[ - ELK_DOMAIN]['keypads'].get(keypad.index, '') - self.async_schedule_update_ha_state(True) - - @property - def code_format(self): - """Return the alarm code format.""" - return alarm.FORMAT_NUMBER - - @property - def state(self): - """Return the state of the element.""" - return self._state - - @property - def device_state_attributes(self): - """Attributes of the area.""" - from elkm1_lib.const import AlarmState, ArmedStatus, ArmUpState - - attrs = self.initial_attrs() - elmt = self._element - attrs['is_exit'] = elmt.is_exit - attrs['timer1'] = elmt.timer1 - attrs['timer2'] = elmt.timer2 - if elmt.armed_status is not None: - attrs['armed_status'] = \ - ArmedStatus(elmt.armed_status).name.lower() - if elmt.arm_up_state is not None: - attrs['arm_up_state'] = ArmUpState(elmt.arm_up_state).name.lower() - if elmt.alarm_state is not None: - attrs['alarm_state'] = AlarmState(elmt.alarm_state).name.lower() - attrs['changed_by_entity_id'] = self._changed_by_entity_id - return attrs - - def _element_changed(self, element, changeset): - from elkm1_lib.const import ArmedStatus - - elk_state_to_hass_state = { - ArmedStatus.DISARMED.value: STATE_ALARM_DISARMED, - ArmedStatus.ARMED_AWAY.value: STATE_ALARM_ARMED_AWAY, - ArmedStatus.ARMED_STAY.value: STATE_ALARM_ARMED_HOME, - ArmedStatus.ARMED_STAY_INSTANT.value: STATE_ALARM_ARMED_HOME, - ArmedStatus.ARMED_TO_NIGHT.value: STATE_ALARM_ARMED_NIGHT, - ArmedStatus.ARMED_TO_NIGHT_INSTANT.value: STATE_ALARM_ARMED_NIGHT, - ArmedStatus.ARMED_TO_VACATION.value: STATE_ALARM_ARMED_AWAY, - } - - if self._element.alarm_state is None: - self._state = None - elif self._area_is_in_alarm_state(): - self._state = STATE_ALARM_TRIGGERED - elif self._entry_exit_timer_is_running(): - self._state = STATE_ALARM_ARMING \ - if self._element.is_exit else STATE_ALARM_PENDING - else: - self._state = elk_state_to_hass_state[self._element.armed_status] - - def _entry_exit_timer_is_running(self): - return self._element.timer1 > 0 or self._element.timer2 > 0 - - def _area_is_in_alarm_state(self): - from elkm1_lib.const import AlarmState - - return self._element.alarm_state >= AlarmState.FIRE_ALARM.value - - async def async_alarm_disarm(self, code=None): - """Send disarm command.""" - self._element.disarm(int(code)) - - async def async_alarm_arm_home(self, code=None): - """Send arm home command.""" - from elkm1_lib.const import ArmLevel - - self._element.arm(ArmLevel.ARMED_STAY.value, int(code)) - - async def async_alarm_arm_away(self, code=None): - """Send arm away command.""" - from elkm1_lib.const import ArmLevel - - self._element.arm(ArmLevel.ARMED_AWAY.value, int(code)) - - async def async_alarm_arm_night(self, code=None): - """Send arm night command.""" - from elkm1_lib.const import ArmLevel - - self._element.arm(ArmLevel.ARMED_NIGHT.value, int(code)) - - async def _arm_service(self, arm_level, code): - self._element.arm(arm_level, code) - - async def _display_message(self, clear, beep, timeout, line1, line2): - """Display a message on all keypads for the area.""" - self._element.display_message(clear, beep, timeout, line1, line2) diff --git a/homeassistant/components/alarm_control_panel/envisalink.py b/homeassistant/components/alarm_control_panel/envisalink.py deleted file mode 100644 index 9b772d9bdf0f4..0000000000000 --- a/homeassistant/components/alarm_control_panel/envisalink.py +++ /dev/null @@ -1,164 +0,0 @@ -""" -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/ -""" -import logging - -import voluptuous as vol - -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -import homeassistant.components.alarm_control_panel as alarm -import homeassistant.helpers.config_validation as cv -from homeassistant.components.envisalink import ( - DATA_EVL, EnvisalinkDevice, PARTITION_SCHEMA, CONF_CODE, CONF_PANIC, - CONF_PARTITIONNAME, SIGNAL_KEYPAD_UPDATE, SIGNAL_PARTITION_UPDATE) -from homeassistant.const import ( - STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, - STATE_UNKNOWN, STATE_ALARM_TRIGGERED, STATE_ALARM_PENDING, ATTR_ENTITY_ID) - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['envisalink'] - -SERVICE_ALARM_KEYPRESS = 'envisalink_alarm_keypress' -ATTR_KEYPRESS = 'keypress' -ALARM_KEYPRESS_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_KEYPRESS): cv.string -}) - - -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] - panic_type = discovery_info[CONF_PANIC] - - devices = [] - 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] - ) - devices.append(device) - - async_add_entities(devices) - - @callback - def alarm_keypress_handler(service): - """Map services to methods on Alarm.""" - entity_ids = service.data.get(ATTR_ENTITY_ID) - keypress = service.data.get(ATTR_KEYPRESS) - - target_devices = [device for device in devices - if device.entity_id in entity_ids] - - for device in target_devices: - device.async_alarm_keypress(keypress) - - hass.services.async_register( - alarm.DOMAIN, SERVICE_ALARM_KEYPRESS, alarm_keypress_handler, - schema=ALARM_KEYPRESS_SCHEMA) - - return True - - -class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): - """Representation of an Envisalink-based alarm panel.""" - - def __init__(self, hass, partition_number, alarm_name, code, panic_type, - info, controller): - """Initialize the alarm panel.""" - self._partition_number = partition_number - self._code = code - self._panic_type = panic_type - - _LOGGER.debug("Setting up alarm: %s", alarm_name) - super().__init__(alarm_name, info, controller) - - async def async_added_to_hass(self): - """Register callbacks.""" - async_dispatcher_connect( - self.hass, SIGNAL_KEYPAD_UPDATE, self._update_callback) - async_dispatcher_connect( - self.hass, SIGNAL_PARTITION_UPDATE, self._update_callback) - - @callback - def _update_callback(self, partition): - """Update Home Assistant state, if needed.""" - if partition is None or int(partition) == self._partition_number: - self.async_schedule_update_ha_state() - - @property - def code_format(self): - """Regex for code format or None if no code is required.""" - if self._code: - return None - return alarm.FORMAT_NUMBER - - @property - def state(self): - """Return the state of the device.""" - state = STATE_UNKNOWN - - if self._info['status']['alarm']: - state = STATE_ALARM_TRIGGERED - elif self._info['status']['armed_away']: - state = STATE_ALARM_ARMED_AWAY - elif self._info['status']['armed_stay']: - state = STATE_ALARM_ARMED_HOME - elif self._info['status']['exit_delay']: - state = STATE_ALARM_PENDING - elif self._info['status']['entry_delay']: - state = STATE_ALARM_PENDING - elif self._info['status']['alpha']: - state = STATE_ALARM_DISARMED - return state - - async def async_alarm_disarm(self, code=None): - """Send disarm command.""" - if code: - self.hass.data[DATA_EVL].disarm_partition( - str(code), self._partition_number) - else: - self.hass.data[DATA_EVL].disarm_partition( - str(self._code), self._partition_number) - - async def async_alarm_arm_home(self, code=None): - """Send arm home command.""" - if code: - self.hass.data[DATA_EVL].arm_stay_partition( - str(code), self._partition_number) - else: - self.hass.data[DATA_EVL].arm_stay_partition( - str(self._code), self._partition_number) - - async def async_alarm_arm_away(self, code=None): - """Send arm away command.""" - if code: - self.hass.data[DATA_EVL].arm_away_partition( - str(code), self._partition_number) - else: - self.hass.data[DATA_EVL].arm_away_partition( - str(self._code), self._partition_number) - - async def async_alarm_trigger(self, code=None): - """Alarm trigger command. Will be used to trigger a panic alarm.""" - self.hass.data[DATA_EVL].panic_alarm(self._panic_type) - - @callback - def async_alarm_keypress(self, keypress=None): - """Send custom keypress.""" - if keypress: - self.hass.data[DATA_EVL].keypresses_to_partition( - self._partition_number, keypress) diff --git a/homeassistant/components/alarm_control_panel/ifttt.py b/homeassistant/components/alarm_control_panel/ifttt.py deleted file mode 100644 index fe9c96a0083da..0000000000000 --- a/homeassistant/components/alarm_control_panel/ifttt.py +++ /dev/null @@ -1,175 +0,0 @@ -""" -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/ -""" -import logging -import re - -import voluptuous as vol - -import homeassistant.components.alarm_control_panel as alarm -from homeassistant.components.alarm_control_panel import ( - DOMAIN, PLATFORM_SCHEMA) -from homeassistant.components.ifttt import ( - ATTR_EVENT, DOMAIN as IFTTT_DOMAIN, SERVICE_TRIGGER) -from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_STATE, CONF_NAME, CONF_CODE, - CONF_OPTIMISTIC, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT, - STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY) -import homeassistant.helpers.config_validation as cv - -DEPENDENCIES = ['ifttt'] - -_LOGGER = logging.getLogger(__name__) - -ALLOWED_STATES = [ - STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT, - STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME] - -DATA_IFTTT_ALARM = 'ifttt_alarm' -DEFAULT_NAME = "Home" - -CONF_EVENT_AWAY = "event_arm_away" -CONF_EVENT_HOME = "event_arm_home" -CONF_EVENT_NIGHT = "event_arm_night" -CONF_EVENT_DISARM = "event_disarm" - -DEFAULT_EVENT_AWAY = "alarm_arm_away" -DEFAULT_EVENT_HOME = "alarm_arm_home" -DEFAULT_EVENT_NIGHT = "alarm_arm_night" -DEFAULT_EVENT_DISARM = "alarm_disarm" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_CODE): cv.string, - vol.Optional(CONF_EVENT_AWAY, default=DEFAULT_EVENT_AWAY): cv.string, - vol.Optional(CONF_EVENT_HOME, default=DEFAULT_EVENT_HOME): cv.string, - vol.Optional(CONF_EVENT_NIGHT, default=DEFAULT_EVENT_NIGHT): cv.string, - vol.Optional(CONF_EVENT_DISARM, default=DEFAULT_EVENT_DISARM): cv.string, - vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, -}) - -SERVICE_PUSH_ALARM_STATE = "ifttt_push_alarm_state" - -PUSH_ALARM_STATE_SERVICE_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_STATE): cv.string, -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up a control panel managed through IFTTT.""" - if DATA_IFTTT_ALARM not in hass.data: - hass.data[DATA_IFTTT_ALARM] = [] - - name = config.get(CONF_NAME) - code = config.get(CONF_CODE) - event_away = config.get(CONF_EVENT_AWAY) - event_home = config.get(CONF_EVENT_HOME) - event_night = config.get(CONF_EVENT_NIGHT) - event_disarm = config.get(CONF_EVENT_DISARM) - optimistic = config.get(CONF_OPTIMISTIC) - - alarmpanel = IFTTTAlarmPanel(name, code, event_away, event_home, - event_night, event_disarm, optimistic) - hass.data[DATA_IFTTT_ALARM].append(alarmpanel) - add_entities([alarmpanel]) - - async def push_state_update(service): - """Set the service state as device state attribute.""" - entity_ids = service.data.get(ATTR_ENTITY_ID) - state = service.data.get(ATTR_STATE) - devices = hass.data[DATA_IFTTT_ALARM] - if entity_ids: - devices = [d for d in devices if d.entity_id in entity_ids] - - for device in devices: - device.push_alarm_state(state) - device.async_schedule_update_ha_state() - - hass.services.register(DOMAIN, SERVICE_PUSH_ALARM_STATE, push_state_update, - schema=PUSH_ALARM_STATE_SERVICE_SCHEMA) - - -class IFTTTAlarmPanel(alarm.AlarmControlPanel): - """Representation of an alarm control panel controlled through IFTTT.""" - - def __init__(self, name, code, event_away, event_home, event_night, - event_disarm, optimistic): - """Initialize the alarm control panel.""" - self._name = name - self._code = code - self._event_away = event_away - self._event_home = event_home - self._event_night = event_night - self._event_disarm = event_disarm - self._optimistic = optimistic - self._state = None - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def state(self): - """Return the state of the device.""" - return self._state - - @property - def assumed_state(self): - """Notify that this platform return an assumed state.""" - return True - - @property - def code_format(self): - """Return one or more digits/characters.""" - if self._code is None: - return None - if isinstance(self._code, str) and re.search('^\\d+$', self._code): - return alarm.FORMAT_NUMBER - return alarm.FORMAT_TEXT - - def alarm_disarm(self, code=None): - """Send disarm command.""" - if not self._check_code(code): - return - self.set_alarm_state(self._event_disarm, STATE_ALARM_DISARMED) - - def alarm_arm_away(self, code=None): - """Send arm away command.""" - if not self._check_code(code): - return - self.set_alarm_state(self._event_away, STATE_ALARM_ARMED_AWAY) - - def alarm_arm_home(self, code=None): - """Send arm home command.""" - if not self._check_code(code): - return - self.set_alarm_state(self._event_home, STATE_ALARM_ARMED_HOME) - - def alarm_arm_night(self, code=None): - """Send arm night command.""" - if not self._check_code(code): - return - self.set_alarm_state(self._event_night, STATE_ALARM_ARMED_NIGHT) - - def set_alarm_state(self, event, state): - """Call the IFTTT trigger service to change the alarm state.""" - data = {ATTR_EVENT: event} - - self.hass.services.call(IFTTT_DOMAIN, SERVICE_TRIGGER, data) - _LOGGER.debug("Called IFTTT component to trigger event %s", event) - if self._optimistic: - self._state = state - - def push_alarm_state(self, value): - """Push the alarm state to the given value.""" - if value in ALLOWED_STATES: - _LOGGER.debug("Pushed the alarm state to %s", value) - self._state = value - - def _check_code(self, code): - return self._code is None or self._code == code diff --git a/homeassistant/components/alarm_control_panel/lupusec.py b/homeassistant/components/alarm_control_panel/lupusec.py deleted file mode 100644 index 21eefc238a051..0000000000000 --- a/homeassistant/components/alarm_control_panel/lupusec.py +++ /dev/null @@ -1,70 +0,0 @@ -""" -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/ -""" - -from datetime import timedelta - -from homeassistant.components.alarm_control_panel import AlarmControlPanel -from homeassistant.components.lupusec import DOMAIN as LUPUSEC_DOMAIN -from homeassistant.components.lupusec import LupusecDevice -from homeassistant.const import (STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_HOME, - STATE_ALARM_DISARMED, - STATE_ALARM_TRIGGERED) - -DEPENDENCIES = ['lupusec'] - -ICON = 'mdi:security' - -SCAN_INTERVAL = timedelta(seconds=2) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up an alarm control panel for a Lupusec device.""" - if discovery_info is None: - return - - data = hass.data[LUPUSEC_DOMAIN] - - alarm_devices = [LupusecAlarm(data, data.lupusec.get_alarm())] - - add_entities(alarm_devices) - - -class LupusecAlarm(LupusecDevice, AlarmControlPanel): - """An alarm_control_panel implementation for Lupusec.""" - - @property - def icon(self): - """Return the icon.""" - return ICON - - @property - def state(self): - """Return the state of the device.""" - if self._device.is_standby: - state = STATE_ALARM_DISARMED - elif self._device.is_away: - state = STATE_ALARM_ARMED_AWAY - elif self._device.is_home: - state = STATE_ALARM_ARMED_HOME - elif self._device.is_alarm_triggered: - state = STATE_ALARM_TRIGGERED - else: - state = None - return state - - def alarm_arm_away(self, code=None): - """Send arm away command.""" - self._device.set_away() - - def alarm_disarm(self, code=None): - """Send disarm command.""" - self._device.set_standby() - - def alarm_arm_home(self, code=None): - """Send arm home command.""" - self._device.set_home() diff --git a/homeassistant/components/alarm_control_panel/satel_integra.py b/homeassistant/components/alarm_control_panel/satel_integra.py deleted file mode 100644 index b704677800f7c..0000000000000 --- a/homeassistant/components/alarm_control_panel/satel_integra.py +++ /dev/null @@ -1,88 +0,0 @@ -""" -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/ -""" -import logging - -import homeassistant.components.alarm_control_panel as alarm -from homeassistant.components.satel_integra import ( - CONF_ARM_HOME_MODE, DATA_SATEL, SIGNAL_PANEL_MESSAGE) -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['satel_integra'] - - -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 - - device = SatelIntegraAlarmPanel( - "Alarm Panel", discovery_info.get(CONF_ARM_HOME_MODE)) - async_add_entities([device]) - - -class SatelIntegraAlarmPanel(alarm.AlarmControlPanel): - """Representation of an AlarmDecoder-based alarm panel.""" - - def __init__(self, name, arm_home_mode): - """Initialize the alarm panel.""" - self._name = name - self._state = None - self._arm_home_mode = arm_home_mode - - async def async_added_to_hass(self): - """Register callbacks.""" - async_dispatcher_connect( - self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback) - - @callback - def _message_callback(self, message): - """Handle received messages.""" - if message != self._state: - self._state = message - self.async_schedule_update_ha_state() - else: - _LOGGER.warning("Ignoring alarm status message, same state") - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def should_poll(self): - """Return the polling state.""" - return False - - @property - def code_format(self): - """Return the regex for code format or None if no code is required.""" - return alarm.FORMAT_NUMBER - - @property - def state(self): - """Return the state of the device.""" - return self._state - - async def async_alarm_disarm(self, code=None): - """Send disarm command.""" - if code: - await self.hass.data[DATA_SATEL].disarm(code) - - async def async_alarm_arm_away(self, code=None): - """Send arm away command.""" - if code: - await self.hass.data[DATA_SATEL].arm(code) - - async def async_alarm_arm_home(self, code=None): - """Send arm home command.""" - if code: - await self.hass.data[DATA_SATEL].arm( - code, self._arm_home_mode) diff --git a/homeassistant/components/alarm_control_panel/verisure.py b/homeassistant/components/alarm_control_panel/verisure.py deleted file mode 100644 index 160f152ef8ac0..0000000000000 --- a/homeassistant/components/alarm_control_panel/verisure.py +++ /dev/null @@ -1,94 +0,0 @@ -""" -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/ -""" -import logging -from time import sleep - -import homeassistant.components.alarm_control_panel as alarm -from homeassistant.components.verisure import CONF_ALARM, CONF_CODE_DIGITS -from homeassistant.components.verisure import HUB as hub -from homeassistant.const import ( - STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED) - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Verisure platform.""" - alarms = [] - if int(hub.config.get(CONF_ALARM, 1)): - hub.update_overview() - alarms.append(VerisureAlarm()) - add_entities(alarms) - - -def set_arm_state(state, code=None): - """Send set arm state command.""" - transaction_id = hub.session.set_arm_state(code, state)[ - 'armStateChangeTransactionId'] - _LOGGER.info('verisure set arm state %s', state) - transaction = {} - while 'result' not in transaction: - sleep(0.5) - transaction = hub.session.get_arm_state_transaction(transaction_id) - # pylint: disable=unexpected-keyword-arg - hub.update_overview(no_throttle=True) - - -class VerisureAlarm(alarm.AlarmControlPanel): - """Representation of a Verisure alarm status.""" - - def __init__(self): - """Initialize the Verisure alarm panel.""" - self._state = None - self._digits = hub.config.get(CONF_CODE_DIGITS) - self._changed_by = None - - @property - def name(self): - """Return the name of the device.""" - return '{} alarm'.format(hub.session.installations[0]['alias']) - - @property - def state(self): - """Return the state of the device.""" - return self._state - - @property - def code_format(self): - """Return one or more digits/characters.""" - return alarm.FORMAT_NUMBER - - @property - def changed_by(self): - """Return the last change triggered by.""" - return self._changed_by - - def update(self): - """Update alarm status.""" - hub.update_overview() - status = hub.get_first("$.armState.statusType") - if status == 'DISARMED': - self._state = STATE_ALARM_DISARMED - elif status == 'ARMED_HOME': - self._state = STATE_ALARM_ARMED_HOME - elif status == 'ARMED_AWAY': - self._state = STATE_ALARM_ARMED_AWAY - elif status != 'PENDING': - _LOGGER.error('Unknown alarm state %s', status) - self._changed_by = hub.get_first("$.armState.name") - - def alarm_disarm(self, code=None): - """Send disarm command.""" - set_arm_state('DISARMED', code) - - def alarm_arm_home(self, code=None): - """Send arm home command.""" - set_arm_state('ARMED_HOME', code) - - def alarm_arm_away(self, code=None): - """Send arm away command.""" - set_arm_state('ARMED_AWAY', code) diff --git a/homeassistant/components/alarm_control_panel/wink.py b/homeassistant/components/alarm_control_panel/wink.py deleted file mode 100644 index b2ae3578133fa..0000000000000 --- a/homeassistant/components/alarm_control_panel/wink.py +++ /dev/null @@ -1,74 +0,0 @@ -""" -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/ -""" -import logging - -import homeassistant.components.alarm_control_panel as alarm -from homeassistant.components.wink import DOMAIN, WinkDevice -from homeassistant.const import ( - STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED) - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['wink'] - -STATE_ALARM_PRIVACY = 'Private' - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Wink platform.""" - import pywink - - for camera in pywink.get_cameras(): - # get_cameras returns multiple device types. - # Only add those that aren't sensors. - try: - camera.capability() - except AttributeError: - _id = camera.object_id() + camera.name() - if _id not in hass.data[DOMAIN]['unique_ids']: - add_entities([WinkCameraDevice(camera, hass)]) - - -class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel): - """Representation a Wink camera alarm.""" - - async def async_added_to_hass(self): - """Call when entity is added to hass.""" - self.hass.data[DOMAIN]['entities']['alarm_control_panel'].append(self) - - @property - def state(self): - """Return the state of the device.""" - wink_state = self.wink.state() - if wink_state == "away": - state = STATE_ALARM_ARMED_AWAY - elif wink_state == "home": - state = STATE_ALARM_DISARMED - elif wink_state == "night": - state = STATE_ALARM_ARMED_HOME - else: - state = None - return state - - def alarm_disarm(self, code=None): - """Send disarm command.""" - self.wink.set_mode("home") - - def alarm_arm_home(self, code=None): - """Send arm home command.""" - self.wink.set_mode("night") - - def alarm_arm_away(self, code=None): - """Send arm away command.""" - self.wink.set_mode("away") - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return { - 'private': self.wink.private() - } diff --git a/homeassistant/components/alarmdecoder.py b/homeassistant/components/alarmdecoder.py deleted file mode 100644 index 92eab728210c8..0000000000000 --- a/homeassistant/components/alarmdecoder.py +++ /dev/null @@ -1,206 +0,0 @@ -""" -Support for AlarmDecoder devices. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/alarmdecoder/ -""" -import logging - -from datetime import timedelta -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.helpers.discovery import load_platform -from homeassistant.util import dt as dt_util -from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA - -REQUIREMENTS = ['alarmdecoder==1.13.2'] - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'alarmdecoder' - -DATA_AD = 'alarmdecoder' - -CONF_DEVICE = 'device' -CONF_DEVICE_BAUD = 'baudrate' -CONF_DEVICE_HOST = 'host' -CONF_DEVICE_PATH = 'path' -CONF_DEVICE_PORT = 'port' -CONF_DEVICE_TYPE = 'type' -CONF_PANEL_DISPLAY = 'panel_display' -CONF_ZONE_NAME = 'name' -CONF_ZONE_TYPE = 'type' -CONF_ZONE_LOOP = 'loop' -CONF_ZONE_RFID = 'rfid' -CONF_ZONES = 'zones' -CONF_RELAY_ADDR = 'relayaddr' -CONF_RELAY_CHAN = 'relaychan' - -DEFAULT_DEVICE_TYPE = 'socket' -DEFAULT_DEVICE_HOST = 'localhost' -DEFAULT_DEVICE_PORT = 10000 -DEFAULT_DEVICE_PATH = '/dev/ttyUSB0' -DEFAULT_DEVICE_BAUD = 115200 - -DEFAULT_PANEL_DISPLAY = False - -DEFAULT_ZONE_TYPE = 'opening' - -SIGNAL_PANEL_MESSAGE = 'alarmdecoder.panel_message' -SIGNAL_PANEL_ARM_AWAY = 'alarmdecoder.panel_arm_away' -SIGNAL_PANEL_ARM_HOME = 'alarmdecoder.panel_arm_home' -SIGNAL_PANEL_DISARM = 'alarmdecoder.panel_disarm' - -SIGNAL_ZONE_FAULT = 'alarmdecoder.zone_fault' -SIGNAL_ZONE_RESTORE = 'alarmdecoder.zone_restore' -SIGNAL_RFX_MESSAGE = 'alarmdecoder.rfx_message' -SIGNAL_REL_MESSAGE = 'alarmdecoder.rel_message' - -DEVICE_SOCKET_SCHEMA = vol.Schema({ - vol.Required(CONF_DEVICE_TYPE): 'socket', - vol.Optional(CONF_DEVICE_HOST, default=DEFAULT_DEVICE_HOST): cv.string, - vol.Optional(CONF_DEVICE_PORT, default=DEFAULT_DEVICE_PORT): cv.port}) - -DEVICE_SERIAL_SCHEMA = vol.Schema({ - vol.Required(CONF_DEVICE_TYPE): 'serial', - vol.Optional(CONF_DEVICE_PATH, default=DEFAULT_DEVICE_PATH): cv.string, - vol.Optional(CONF_DEVICE_BAUD, default=DEFAULT_DEVICE_BAUD): cv.string}) - -DEVICE_USB_SCHEMA = vol.Schema({ - vol.Required(CONF_DEVICE_TYPE): 'usb'}) - -ZONE_SCHEMA = vol.Schema({ - vol.Required(CONF_ZONE_NAME): cv.string, - vol.Optional(CONF_ZONE_TYPE, - default=DEFAULT_ZONE_TYPE): vol.Any(DEVICE_CLASSES_SCHEMA), - vol.Optional(CONF_ZONE_RFID): cv.string, - vol.Optional(CONF_ZONE_LOOP): - vol.All(vol.Coerce(int), vol.Range(min=1, max=4)), - vol.Inclusive(CONF_RELAY_ADDR, 'relaylocation', - 'Relay address and channel must exist together'): cv.byte, - vol.Inclusive(CONF_RELAY_CHAN, 'relaylocation', - 'Relay address and channel must exist together'): cv.byte}) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_DEVICE): vol.Any( - DEVICE_SOCKET_SCHEMA, DEVICE_SERIAL_SCHEMA, - DEVICE_USB_SCHEMA), - vol.Optional(CONF_PANEL_DISPLAY, - default=DEFAULT_PANEL_DISPLAY): cv.boolean, - vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA}, - }), -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up for the AlarmDecoder devices.""" - from alarmdecoder import AlarmDecoder - from alarmdecoder.devices import (SocketDevice, SerialDevice, USBDevice) - - conf = config.get(DOMAIN) - - restart = False - device = conf.get(CONF_DEVICE) - display = conf.get(CONF_PANEL_DISPLAY) - zones = conf.get(CONF_ZONES) - - device_type = device.get(CONF_DEVICE_TYPE) - host = DEFAULT_DEVICE_HOST - port = DEFAULT_DEVICE_PORT - path = DEFAULT_DEVICE_PATH - baud = DEFAULT_DEVICE_BAUD - - def stop_alarmdecoder(event): - """Handle the shutdown of AlarmDecoder.""" - _LOGGER.debug("Shutting down alarmdecoder") - nonlocal restart - restart = False - controller.close() - - def open_connection(now=None): - """Open a connection to AlarmDecoder.""" - from alarmdecoder.util import NoDeviceError - nonlocal restart - try: - controller.open(baud) - except NoDeviceError: - _LOGGER.debug("Failed to connect. Retrying in 5 seconds") - hass.helpers.event.track_point_in_time( - open_connection, dt_util.utcnow() + timedelta(seconds=5)) - return - _LOGGER.debug("Established a connection with the alarmdecoder") - restart = True - - def handle_closed_connection(event): - """Restart after unexpected loss of connection.""" - nonlocal restart - if not restart: - return - restart = False - _LOGGER.warning("AlarmDecoder unexpectedly lost connection.") - hass.add_job(open_connection) - - def handle_message(sender, message): - """Handle message from AlarmDecoder.""" - hass.helpers.dispatcher.dispatcher_send( - SIGNAL_PANEL_MESSAGE, message) - - def handle_rfx_message(sender, message): - """Handle RFX message from AlarmDecoder.""" - hass.helpers.dispatcher.dispatcher_send( - SIGNAL_RFX_MESSAGE, message) - - def zone_fault_callback(sender, zone): - """Handle zone fault from AlarmDecoder.""" - hass.helpers.dispatcher.dispatcher_send( - SIGNAL_ZONE_FAULT, zone) - - def zone_restore_callback(sender, zone): - """Handle zone restore from AlarmDecoder.""" - hass.helpers.dispatcher.dispatcher_send( - SIGNAL_ZONE_RESTORE, zone) - - def handle_rel_message(sender, message): - """Handle relay message from AlarmDecoder.""" - hass.helpers.dispatcher.dispatcher_send( - SIGNAL_REL_MESSAGE, message) - - controller = False - if device_type == 'socket': - host = device.get(CONF_DEVICE_HOST) - port = device.get(CONF_DEVICE_PORT) - controller = AlarmDecoder(SocketDevice(interface=(host, port))) - elif device_type == 'serial': - path = device.get(CONF_DEVICE_PATH) - baud = device.get(CONF_DEVICE_BAUD) - controller = AlarmDecoder(SerialDevice(interface=path)) - elif device_type == 'usb': - AlarmDecoder(USBDevice.find()) - return False - - controller.on_message += handle_message - controller.on_rfx_message += handle_rfx_message - controller.on_zone_fault += zone_fault_callback - controller.on_zone_restore += zone_restore_callback - controller.on_close += handle_closed_connection - controller.on_relay_changed += handle_rel_message - - hass.data[DATA_AD] = controller - - open_connection() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder) - - load_platform(hass, 'alarm_control_panel', DOMAIN, conf, config) - - if zones: - load_platform( - hass, 'binary_sensor', DOMAIN, {CONF_ZONES: zones}, config) - - if display: - load_platform(hass, 'sensor', DOMAIN, conf, config) - - return True diff --git a/homeassistant/components/alarmdecoder/__init__.py b/homeassistant/components/alarmdecoder/__init__.py new file mode 100644 index 0000000000000..1f74d72809b29 --- /dev/null +++ b/homeassistant/components/alarmdecoder/__init__.py @@ -0,0 +1,201 @@ +"""Support for AlarmDecoder devices.""" +import logging + +from datetime import timedelta +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers.discovery import load_platform +from homeassistant.util import dt as dt_util +from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA + +REQUIREMENTS = ['alarmdecoder==1.13.2'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'alarmdecoder' + +DATA_AD = 'alarmdecoder' + +CONF_DEVICE = 'device' +CONF_DEVICE_BAUD = 'baudrate' +CONF_DEVICE_HOST = 'host' +CONF_DEVICE_PATH = 'path' +CONF_DEVICE_PORT = 'port' +CONF_DEVICE_TYPE = 'type' +CONF_PANEL_DISPLAY = 'panel_display' +CONF_ZONE_NAME = 'name' +CONF_ZONE_TYPE = 'type' +CONF_ZONE_LOOP = 'loop' +CONF_ZONE_RFID = 'rfid' +CONF_ZONES = 'zones' +CONF_RELAY_ADDR = 'relayaddr' +CONF_RELAY_CHAN = 'relaychan' + +DEFAULT_DEVICE_TYPE = 'socket' +DEFAULT_DEVICE_HOST = 'localhost' +DEFAULT_DEVICE_PORT = 10000 +DEFAULT_DEVICE_PATH = '/dev/ttyUSB0' +DEFAULT_DEVICE_BAUD = 115200 + +DEFAULT_PANEL_DISPLAY = False + +DEFAULT_ZONE_TYPE = 'opening' + +SIGNAL_PANEL_MESSAGE = 'alarmdecoder.panel_message' +SIGNAL_PANEL_ARM_AWAY = 'alarmdecoder.panel_arm_away' +SIGNAL_PANEL_ARM_HOME = 'alarmdecoder.panel_arm_home' +SIGNAL_PANEL_DISARM = 'alarmdecoder.panel_disarm' + +SIGNAL_ZONE_FAULT = 'alarmdecoder.zone_fault' +SIGNAL_ZONE_RESTORE = 'alarmdecoder.zone_restore' +SIGNAL_RFX_MESSAGE = 'alarmdecoder.rfx_message' +SIGNAL_REL_MESSAGE = 'alarmdecoder.rel_message' + +DEVICE_SOCKET_SCHEMA = vol.Schema({ + vol.Required(CONF_DEVICE_TYPE): 'socket', + vol.Optional(CONF_DEVICE_HOST, default=DEFAULT_DEVICE_HOST): cv.string, + vol.Optional(CONF_DEVICE_PORT, default=DEFAULT_DEVICE_PORT): cv.port}) + +DEVICE_SERIAL_SCHEMA = vol.Schema({ + vol.Required(CONF_DEVICE_TYPE): 'serial', + vol.Optional(CONF_DEVICE_PATH, default=DEFAULT_DEVICE_PATH): cv.string, + vol.Optional(CONF_DEVICE_BAUD, default=DEFAULT_DEVICE_BAUD): cv.string}) + +DEVICE_USB_SCHEMA = vol.Schema({ + vol.Required(CONF_DEVICE_TYPE): 'usb'}) + +ZONE_SCHEMA = vol.Schema({ + vol.Required(CONF_ZONE_NAME): cv.string, + vol.Optional(CONF_ZONE_TYPE, + default=DEFAULT_ZONE_TYPE): vol.Any(DEVICE_CLASSES_SCHEMA), + vol.Optional(CONF_ZONE_RFID): cv.string, + vol.Optional(CONF_ZONE_LOOP): + vol.All(vol.Coerce(int), vol.Range(min=1, max=4)), + vol.Inclusive(CONF_RELAY_ADDR, 'relaylocation', + 'Relay address and channel must exist together'): cv.byte, + vol.Inclusive(CONF_RELAY_CHAN, 'relaylocation', + 'Relay address and channel must exist together'): cv.byte}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_DEVICE): vol.Any( + DEVICE_SOCKET_SCHEMA, DEVICE_SERIAL_SCHEMA, + DEVICE_USB_SCHEMA), + vol.Optional(CONF_PANEL_DISPLAY, + default=DEFAULT_PANEL_DISPLAY): cv.boolean, + vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA}, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up for the AlarmDecoder devices.""" + from alarmdecoder import AlarmDecoder + from alarmdecoder.devices import (SocketDevice, SerialDevice, USBDevice) + + conf = config.get(DOMAIN) + + restart = False + device = conf.get(CONF_DEVICE) + display = conf.get(CONF_PANEL_DISPLAY) + zones = conf.get(CONF_ZONES) + + device_type = device.get(CONF_DEVICE_TYPE) + host = DEFAULT_DEVICE_HOST + port = DEFAULT_DEVICE_PORT + path = DEFAULT_DEVICE_PATH + baud = DEFAULT_DEVICE_BAUD + + def stop_alarmdecoder(event): + """Handle the shutdown of AlarmDecoder.""" + _LOGGER.debug("Shutting down alarmdecoder") + nonlocal restart + restart = False + controller.close() + + def open_connection(now=None): + """Open a connection to AlarmDecoder.""" + from alarmdecoder.util import NoDeviceError + nonlocal restart + try: + controller.open(baud) + except NoDeviceError: + _LOGGER.debug("Failed to connect. Retrying in 5 seconds") + hass.helpers.event.track_point_in_time( + open_connection, dt_util.utcnow() + timedelta(seconds=5)) + return + _LOGGER.debug("Established a connection with the alarmdecoder") + restart = True + + def handle_closed_connection(event): + """Restart after unexpected loss of connection.""" + nonlocal restart + if not restart: + return + restart = False + _LOGGER.warning("AlarmDecoder unexpectedly lost connection.") + hass.add_job(open_connection) + + def handle_message(sender, message): + """Handle message from AlarmDecoder.""" + hass.helpers.dispatcher.dispatcher_send( + SIGNAL_PANEL_MESSAGE, message) + + def handle_rfx_message(sender, message): + """Handle RFX message from AlarmDecoder.""" + hass.helpers.dispatcher.dispatcher_send( + SIGNAL_RFX_MESSAGE, message) + + def zone_fault_callback(sender, zone): + """Handle zone fault from AlarmDecoder.""" + hass.helpers.dispatcher.dispatcher_send( + SIGNAL_ZONE_FAULT, zone) + + def zone_restore_callback(sender, zone): + """Handle zone restore from AlarmDecoder.""" + hass.helpers.dispatcher.dispatcher_send( + SIGNAL_ZONE_RESTORE, zone) + + def handle_rel_message(sender, message): + """Handle relay message from AlarmDecoder.""" + hass.helpers.dispatcher.dispatcher_send( + SIGNAL_REL_MESSAGE, message) + + controller = False + if device_type == 'socket': + host = device.get(CONF_DEVICE_HOST) + port = device.get(CONF_DEVICE_PORT) + controller = AlarmDecoder(SocketDevice(interface=(host, port))) + elif device_type == 'serial': + path = device.get(CONF_DEVICE_PATH) + baud = device.get(CONF_DEVICE_BAUD) + controller = AlarmDecoder(SerialDevice(interface=path)) + elif device_type == 'usb': + AlarmDecoder(USBDevice.find()) + return False + + controller.on_message += handle_message + controller.on_rfx_message += handle_rfx_message + controller.on_zone_fault += zone_fault_callback + controller.on_zone_restore += zone_restore_callback + controller.on_close += handle_closed_connection + controller.on_relay_changed += handle_rel_message + + hass.data[DATA_AD] = controller + + open_connection() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder) + + load_platform(hass, 'alarm_control_panel', DOMAIN, conf, config) + + if zones: + load_platform( + hass, 'binary_sensor', DOMAIN, {CONF_ZONES: zones}, config) + + if display: + load_platform(hass, 'sensor', DOMAIN, conf, config) + + return True diff --git a/homeassistant/components/alarmdecoder/alarm_control_panel.py b/homeassistant/components/alarmdecoder/alarm_control_panel.py new file mode 100644 index 0000000000000..986907622b1c7 --- /dev/null +++ b/homeassistant/components/alarmdecoder/alarm_control_panel.py @@ -0,0 +1,137 @@ +"""Support for AlarmDecoder-based alarm control panels (Honeywell/DSC).""" +import logging + +import voluptuous as vol + +import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarmdecoder import DATA_AD, SIGNAL_PANEL_MESSAGE +from homeassistant.const import ( + ATTR_CODE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, + STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED) +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['alarmdecoder'] + +SERVICE_ALARM_TOGGLE_CHIME = 'alarmdecoder_alarm_toggle_chime' +ALARM_TOGGLE_CHIME_SCHEMA = vol.Schema({ + vol.Required(ATTR_CODE): cv.string, +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up for AlarmDecoder alarm panels.""" + device = AlarmDecoderAlarmPanel() + add_entities([device]) + + def alarm_toggle_chime_handler(service): + """Register toggle chime handler.""" + code = service.data.get(ATTR_CODE) + device.alarm_toggle_chime(code) + + hass.services.register( + alarm.DOMAIN, SERVICE_ALARM_TOGGLE_CHIME, alarm_toggle_chime_handler, + schema=ALARM_TOGGLE_CHIME_SCHEMA) + + +class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel): + """Representation of an AlarmDecoder-based alarm panel.""" + + def __init__(self): + """Initialize the alarm panel.""" + self._display = "" + self._name = "Alarm Panel" + self._state = None + self._ac_power = None + self._backlight_on = None + self._battery_low = None + self._check_zone = None + self._chime = None + self._entry_delay_off = None + self._programming_mode = None + self._ready = None + self._zone_bypassed = None + + async def async_added_to_hass(self): + """Register callbacks.""" + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_PANEL_MESSAGE, self._message_callback) + + def _message_callback(self, message): + """Handle received messages.""" + if message.alarm_sounding or message.fire_alarm: + self._state = STATE_ALARM_TRIGGERED + elif message.armed_away: + self._state = STATE_ALARM_ARMED_AWAY + elif message.armed_home: + self._state = STATE_ALARM_ARMED_HOME + else: + self._state = STATE_ALARM_DISARMED + + self._ac_power = message.ac_power + self._backlight_on = message.backlight_on + self._battery_low = message.battery_low + self._check_zone = message.check_zone + self._chime = message.chime_on + self._entry_delay_off = message.entry_delay_off + self._programming_mode = message.programming_mode + self._ready = message.ready + self._zone_bypassed = message.zone_bypassed + + self.schedule_update_ha_state() + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def should_poll(self): + """Return the polling state.""" + return False + + @property + def code_format(self): + """Return one or more digits/characters.""" + return alarm.FORMAT_NUMBER + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + 'ac_power': self._ac_power, + 'backlight_on': self._backlight_on, + 'battery_low': self._battery_low, + 'check_zone': self._check_zone, + 'chime': self._chime, + 'entry_delay_off': self._entry_delay_off, + 'programming_mode': self._programming_mode, + 'ready': self._ready, + 'zone_bypassed': self._zone_bypassed, + } + + def alarm_disarm(self, code=None): + """Send disarm command.""" + if code: + self.hass.data[DATA_AD].send("{!s}1".format(code)) + + def alarm_arm_away(self, code=None): + """Send arm away command.""" + if code: + self.hass.data[DATA_AD].send("{!s}2".format(code)) + + def alarm_arm_home(self, code=None): + """Send arm home command.""" + if code: + self.hass.data[DATA_AD].send("{!s}3".format(code)) + + def alarm_toggle_chime(self, code=None): + """Send toggle chime command.""" + if code: + self.hass.data[DATA_AD].send("{!s}9".format(code)) diff --git a/homeassistant/components/alarmdecoder/binary_sensor.py b/homeassistant/components/alarmdecoder/binary_sensor.py new file mode 100644 index 0000000000000..c5af6ea79cb7c --- /dev/null +++ b/homeassistant/components/alarmdecoder/binary_sensor.py @@ -0,0 +1,140 @@ +"""Support for AlarmDecoder zone states- represented as binary sensors.""" +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.alarmdecoder import ( + ZONE_SCHEMA, CONF_ZONES, CONF_ZONE_NAME, CONF_ZONE_TYPE, + CONF_ZONE_RFID, CONF_ZONE_LOOP, SIGNAL_ZONE_FAULT, SIGNAL_ZONE_RESTORE, + SIGNAL_RFX_MESSAGE, SIGNAL_REL_MESSAGE, CONF_RELAY_ADDR, + CONF_RELAY_CHAN) + +DEPENDENCIES = ['alarmdecoder'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_RF_BIT0 = 'rf_bit0' +ATTR_RF_LOW_BAT = 'rf_low_battery' +ATTR_RF_SUPERVISED = 'rf_supervised' +ATTR_RF_BIT3 = 'rf_bit3' +ATTR_RF_LOOP3 = 'rf_loop3' +ATTR_RF_LOOP2 = 'rf_loop2' +ATTR_RF_LOOP4 = 'rf_loop4' +ATTR_RF_LOOP1 = 'rf_loop1' + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the AlarmDecoder binary sensor devices.""" + configured_zones = discovery_info[CONF_ZONES] + + devices = [] + for zone_num in configured_zones: + device_config_data = ZONE_SCHEMA(configured_zones[zone_num]) + zone_type = device_config_data[CONF_ZONE_TYPE] + zone_name = device_config_data[CONF_ZONE_NAME] + zone_rfid = device_config_data.get(CONF_ZONE_RFID) + zone_loop = device_config_data.get(CONF_ZONE_LOOP) + relay_addr = device_config_data.get(CONF_RELAY_ADDR) + relay_chan = device_config_data.get(CONF_RELAY_CHAN) + device = AlarmDecoderBinarySensor( + zone_num, zone_name, zone_type, zone_rfid, zone_loop, relay_addr, + relay_chan) + devices.append(device) + + add_entities(devices) + + return True + + +class AlarmDecoderBinarySensor(BinarySensorDevice): + """Representation of an AlarmDecoder binary sensor.""" + + def __init__(self, zone_number, zone_name, zone_type, zone_rfid, zone_loop, + relay_addr, relay_chan): + """Initialize the binary_sensor.""" + self._zone_number = zone_number + self._zone_type = zone_type + self._state = None + self._name = zone_name + self._rfid = zone_rfid + self._loop = zone_loop + self._rfstate = None + self._relay_addr = relay_addr + self._relay_chan = relay_chan + + async def async_added_to_hass(self): + """Register callbacks.""" + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_ZONE_FAULT, self._fault_callback) + + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_ZONE_RESTORE, self._restore_callback) + + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_RFX_MESSAGE, self._rfx_message_callback) + + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_REL_MESSAGE, self._rel_message_callback) + + @property + def name(self): + """Return the name of the entity.""" + return self._name + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def device_state_attributes(self): + """Return the state attributes.""" + attr = {} + if self._rfid and self._rfstate is not None: + attr[ATTR_RF_BIT0] = bool(self._rfstate & 0x01) + attr[ATTR_RF_LOW_BAT] = bool(self._rfstate & 0x02) + attr[ATTR_RF_SUPERVISED] = bool(self._rfstate & 0x04) + attr[ATTR_RF_BIT3] = bool(self._rfstate & 0x08) + attr[ATTR_RF_LOOP3] = bool(self._rfstate & 0x10) + attr[ATTR_RF_LOOP2] = bool(self._rfstate & 0x20) + attr[ATTR_RF_LOOP4] = bool(self._rfstate & 0x40) + attr[ATTR_RF_LOOP1] = bool(self._rfstate & 0x80) + return attr + + @property + def is_on(self): + """Return true if sensor is on.""" + return self._state == 1 + + @property + def device_class(self): + """Return the class of this sensor, from DEVICE_CLASSES.""" + return self._zone_type + + def _fault_callback(self, zone): + """Update the zone's state, if needed.""" + if zone is None or int(zone) == self._zone_number: + self._state = 1 + self.schedule_update_ha_state() + + def _restore_callback(self, zone): + """Update the zone's state, if needed.""" + if zone is None or int(zone) == self._zone_number: + self._state = 0 + self.schedule_update_ha_state() + + def _rfx_message_callback(self, message): + """Update RF state.""" + if self._rfid and message and message.serial_number == self._rfid: + self._rfstate = message.value + if self._loop: + self._state = 1 if message.loop[self._loop - 1] else 0 + self.schedule_update_ha_state() + + def _rel_message_callback(self, message): + """Update relay state.""" + if (self._relay_addr == message.address and + self._relay_chan == message.channel): + _LOGGER.debug("Relay %d:%d value:%d", message.address, + message.channel, message.value) + self._state = message.value + self.schedule_update_ha_state() diff --git a/homeassistant/components/alarmdecoder/sensor.py b/homeassistant/components/alarmdecoder/sensor.py new file mode 100644 index 0000000000000..b2f697ea83f22 --- /dev/null +++ b/homeassistant/components/alarmdecoder/sensor.py @@ -0,0 +1,59 @@ +"""Support for AlarmDecoder sensors (Shows Panel Display).""" +import logging + +from homeassistant.helpers.entity import Entity +from homeassistant.components.alarmdecoder import (SIGNAL_PANEL_MESSAGE) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['alarmdecoder'] + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up for AlarmDecoder sensor devices.""" + _LOGGER.debug("AlarmDecoderSensor: setup_platform") + + device = AlarmDecoderSensor(hass) + + add_entities([device]) + + +class AlarmDecoderSensor(Entity): + """Representation of an AlarmDecoder keypad.""" + + def __init__(self, hass): + """Initialize the alarm panel.""" + self._display = "" + self._state = None + self._icon = 'mdi:alarm-check' + self._name = 'Alarm Panel Display' + + async def async_added_to_hass(self): + """Register callbacks.""" + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_PANEL_MESSAGE, self._message_callback) + + def _message_callback(self, message): + if self._display != message.text: + self._display = message.text + self.schedule_update_ha_state() + + @property + def icon(self): + """Return the icon if any.""" + return self._icon + + @property + def state(self): + """Return the overall state.""" + return self._display + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def should_poll(self): + """No polling needed.""" + return False diff --git a/homeassistant/components/alert.py b/homeassistant/components/alert.py deleted file mode 100644 index 579a19c1b521f..0000000000000 --- a/homeassistant/components/alert.py +++ /dev/null @@ -1,296 +0,0 @@ -""" -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/ -""" -import asyncio -import logging -from datetime import datetime, timedelta - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import ( - ATTR_MESSAGE, ATTR_TITLE, ATTR_DATA, DOMAIN as DOMAIN_NOTIFY) -from homeassistant.const import ( - CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, CONF_STATE, STATE_ON, STATE_OFF, - SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID) -from homeassistant.helpers import service, event -from homeassistant.helpers.entity import ToggleEntity - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'alert' -ENTITY_ID_FORMAT = DOMAIN + '.{}' - -CONF_CAN_ACK = 'can_acknowledge' -CONF_NOTIFIERS = 'notifiers' -CONF_REPEAT = 'repeat' -CONF_SKIP_FIRST = 'skip_first' -CONF_ALERT_MESSAGE = 'message' -CONF_DONE_MESSAGE = 'done_message' -CONF_TITLE = 'title' -CONF_DATA = 'data' - -DEFAULT_CAN_ACK = True -DEFAULT_SKIP_FIRST = False - -ALERT_SCHEMA = vol.Schema({ - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_ENTITY_ID): cv.entity_id, - vol.Required(CONF_STATE, default=STATE_ON): cv.string, - vol.Required(CONF_REPEAT): vol.All(cv.ensure_list, [vol.Coerce(float)]), - vol.Required(CONF_CAN_ACK, default=DEFAULT_CAN_ACK): cv.boolean, - vol.Required(CONF_SKIP_FIRST, default=DEFAULT_SKIP_FIRST): cv.boolean, - vol.Optional(CONF_ALERT_MESSAGE): cv.template, - vol.Optional(CONF_DONE_MESSAGE): cv.template, - vol.Optional(CONF_TITLE): cv.template, - vol.Optional(CONF_DATA): dict, - vol.Required(CONF_NOTIFIERS): cv.ensure_list}) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: cv.schema_with_slug_keys(ALERT_SCHEMA), -}, extra=vol.ALLOW_EXTRA) - -ALERT_SERVICE_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, -}) - - -def is_on(hass, entity_id): - """Return if the alert is firing and not acknowledged.""" - return hass.states.is_state(entity_id, STATE_ON) - - -async def async_setup(hass, config): - """Set up the Alert component.""" - entities = [] - - for object_id, cfg in config[DOMAIN].items(): - if not cfg: - cfg = {} - - name = cfg.get(CONF_NAME) - watched_entity_id = cfg.get(CONF_ENTITY_ID) - alert_state = cfg.get(CONF_STATE) - repeat = cfg.get(CONF_REPEAT) - skip_first = cfg.get(CONF_SKIP_FIRST) - message_template = cfg.get(CONF_ALERT_MESSAGE) - done_message_template = cfg.get(CONF_DONE_MESSAGE) - notifiers = cfg.get(CONF_NOTIFIERS) - can_ack = cfg.get(CONF_CAN_ACK) - title_template = cfg.get(CONF_TITLE) - data = cfg.get(CONF_DATA) - - entities.append(Alert(hass, object_id, name, - watched_entity_id, alert_state, repeat, - skip_first, message_template, - done_message_template, notifiers, - can_ack, title_template, data)) - - if not entities: - return False - - async def async_handle_alert_service(service_call): - """Handle calls to alert services.""" - alert_ids = service.extract_entity_ids(hass, service_call) - - for alert_id in alert_ids: - for alert in entities: - if alert.entity_id != alert_id: - continue - - alert.async_set_context(service_call.context) - if service_call.service == SERVICE_TURN_ON: - await alert.async_turn_on() - elif service_call.service == SERVICE_TOGGLE: - await alert.async_toggle() - else: - await alert.async_turn_off() - - # Setup service calls - hass.services.async_register( - DOMAIN, SERVICE_TURN_OFF, async_handle_alert_service, - schema=ALERT_SERVICE_SCHEMA) - hass.services.async_register( - DOMAIN, SERVICE_TURN_ON, async_handle_alert_service, - schema=ALERT_SERVICE_SCHEMA) - hass.services.async_register( - DOMAIN, SERVICE_TOGGLE, async_handle_alert_service, - schema=ALERT_SERVICE_SCHEMA) - - tasks = [alert.async_update_ha_state() for alert in entities] - if tasks: - await asyncio.wait(tasks, loop=hass.loop) - - return True - - -class Alert(ToggleEntity): - """Representation of an alert.""" - - def __init__(self, hass, entity_id, name, watched_entity_id, - state, repeat, skip_first, message_template, - done_message_template, notifiers, can_ack, title_template, - data): - """Initialize the alert.""" - self.hass = hass - self._name = name - self._alert_state = state - self._skip_first = skip_first - self._data = data - - self._message_template = message_template - if self._message_template is not None: - self._message_template.hass = hass - - self._done_message_template = done_message_template - if self._done_message_template is not None: - self._done_message_template.hass = hass - - self._title_template = title_template - if self._title_template is not None: - self._title_template.hass = hass - - self._notifiers = notifiers - self._can_ack = can_ack - - self._delay = [timedelta(minutes=val) for val in repeat] - self._next_delay = 0 - - self._firing = False - self._ack = False - self._cancel = None - self._send_done_message = False - self.entity_id = ENTITY_ID_FORMAT.format(entity_id) - - event.async_track_state_change( - hass, watched_entity_id, self.watched_entity_change) - - @property - def name(self): - """Return the name of the alert.""" - return self._name - - @property - def should_poll(self): - """HASS need not poll these entities.""" - return False - - @property - def state(self): - """Return the alert status.""" - if self._firing: - if self._ack: - return STATE_OFF - return STATE_ON - return STATE_IDLE - - @property - def hidden(self): - """Hide the alert when it is not firing.""" - return not self._can_ack or not self._firing - - async def watched_entity_change(self, entity, from_state, to_state): - """Determine if the alert should start or stop.""" - _LOGGER.debug("Watched entity (%s) has changed", entity) - if to_state.state == self._alert_state and not self._firing: - await self.begin_alerting() - if to_state.state != self._alert_state and self._firing: - await self.end_alerting() - - async def begin_alerting(self): - """Begin the alert procedures.""" - _LOGGER.debug("Beginning Alert: %s", self._name) - self._ack = False - self._firing = True - self._next_delay = 0 - - if not self._skip_first: - await self._notify() - else: - await self._schedule_notify() - - self.async_schedule_update_ha_state() - - async def end_alerting(self): - """End the alert procedures.""" - _LOGGER.debug("Ending Alert: %s", self._name) - self._cancel() - self._ack = False - self._firing = False - if self._send_done_message: - await self._notify_done_message() - self.async_schedule_update_ha_state() - - async def _schedule_notify(self): - """Schedule a notification.""" - delay = self._delay[self._next_delay] - next_msg = datetime.now() + delay - self._cancel = \ - event.async_track_point_in_time(self.hass, self._notify, next_msg) - self._next_delay = min(self._next_delay + 1, len(self._delay) - 1) - - async def _notify(self, *args): - """Send the alert notification.""" - if not self._firing: - return - - if not self._ack: - _LOGGER.info("Alerting: %s", self._name) - self._send_done_message = True - - if self._message_template is not None: - message = self._message_template.async_render() - else: - message = self._name - - await self._send_notification_message(message) - await self._schedule_notify() - - async def _notify_done_message(self, *args): - """Send notification of complete alert.""" - _LOGGER.info("Alerting: %s", self._done_message_template) - self._send_done_message = False - - if self._done_message_template is None: - return - - message = self._done_message_template.async_render() - - await self._send_notification_message(message) - - async def _send_notification_message(self, message): - - msg_payload = {ATTR_MESSAGE: message} - - if self._title_template is not None: - title = self._title_template.async_render() - msg_payload.update({ATTR_TITLE: title}) - if self._data: - msg_payload.update({ATTR_DATA: self._data}) - - _LOGGER.debug(msg_payload) - - for target in self._notifiers: - await self.hass.services.async_call( - DOMAIN_NOTIFY, target, msg_payload) - - async def async_turn_on(self, **kwargs): - """Async Unacknowledge alert.""" - _LOGGER.debug("Reset Alert: %s", self._name) - self._ack = False - await self.async_update_ha_state() - - async def async_turn_off(self, **kwargs): - """Async Acknowledge alert.""" - _LOGGER.debug("Acknowledged Alert: %s", self._name) - self._ack = True - await self.async_update_ha_state() - - async def async_toggle(self, **kwargs): - """Async toggle alert.""" - if self._ack: - return await self.async_turn_on() - return await self.async_turn_off() diff --git a/homeassistant/components/alert/__init__.py b/homeassistant/components/alert/__init__.py new file mode 100644 index 0000000000000..f92fd6b187b61 --- /dev/null +++ b/homeassistant/components/alert/__init__.py @@ -0,0 +1,291 @@ +"""Support for repeating alerts when conditions are met.""" +import asyncio +import logging +from datetime import datetime, timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.notify import ( + ATTR_MESSAGE, ATTR_TITLE, ATTR_DATA, DOMAIN as DOMAIN_NOTIFY) +from homeassistant.const import ( + CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, CONF_STATE, STATE_ON, STATE_OFF, + SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID) +from homeassistant.helpers import service, event +from homeassistant.helpers.entity import ToggleEntity + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'alert' +ENTITY_ID_FORMAT = DOMAIN + '.{}' + +CONF_CAN_ACK = 'can_acknowledge' +CONF_NOTIFIERS = 'notifiers' +CONF_REPEAT = 'repeat' +CONF_SKIP_FIRST = 'skip_first' +CONF_ALERT_MESSAGE = 'message' +CONF_DONE_MESSAGE = 'done_message' +CONF_TITLE = 'title' +CONF_DATA = 'data' + +DEFAULT_CAN_ACK = True +DEFAULT_SKIP_FIRST = False + +ALERT_SCHEMA = vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_STATE, default=STATE_ON): cv.string, + vol.Required(CONF_REPEAT): vol.All(cv.ensure_list, [vol.Coerce(float)]), + vol.Required(CONF_CAN_ACK, default=DEFAULT_CAN_ACK): cv.boolean, + vol.Required(CONF_SKIP_FIRST, default=DEFAULT_SKIP_FIRST): cv.boolean, + vol.Optional(CONF_ALERT_MESSAGE): cv.template, + vol.Optional(CONF_DONE_MESSAGE): cv.template, + vol.Optional(CONF_TITLE): cv.template, + vol.Optional(CONF_DATA): dict, + vol.Required(CONF_NOTIFIERS): cv.ensure_list}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: cv.schema_with_slug_keys(ALERT_SCHEMA), +}, extra=vol.ALLOW_EXTRA) + +ALERT_SERVICE_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, +}) + + +def is_on(hass, entity_id): + """Return if the alert is firing and not acknowledged.""" + return hass.states.is_state(entity_id, STATE_ON) + + +async def async_setup(hass, config): + """Set up the Alert component.""" + entities = [] + + for object_id, cfg in config[DOMAIN].items(): + if not cfg: + cfg = {} + + name = cfg.get(CONF_NAME) + watched_entity_id = cfg.get(CONF_ENTITY_ID) + alert_state = cfg.get(CONF_STATE) + repeat = cfg.get(CONF_REPEAT) + skip_first = cfg.get(CONF_SKIP_FIRST) + message_template = cfg.get(CONF_ALERT_MESSAGE) + done_message_template = cfg.get(CONF_DONE_MESSAGE) + notifiers = cfg.get(CONF_NOTIFIERS) + can_ack = cfg.get(CONF_CAN_ACK) + title_template = cfg.get(CONF_TITLE) + data = cfg.get(CONF_DATA) + + entities.append(Alert(hass, object_id, name, + watched_entity_id, alert_state, repeat, + skip_first, message_template, + done_message_template, notifiers, + can_ack, title_template, data)) + + if not entities: + return False + + async def async_handle_alert_service(service_call): + """Handle calls to alert services.""" + alert_ids = service.extract_entity_ids(hass, service_call) + + for alert_id in alert_ids: + for alert in entities: + if alert.entity_id != alert_id: + continue + + alert.async_set_context(service_call.context) + if service_call.service == SERVICE_TURN_ON: + await alert.async_turn_on() + elif service_call.service == SERVICE_TOGGLE: + await alert.async_toggle() + else: + await alert.async_turn_off() + + # Setup service calls + hass.services.async_register( + DOMAIN, SERVICE_TURN_OFF, async_handle_alert_service, + schema=ALERT_SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_TURN_ON, async_handle_alert_service, + schema=ALERT_SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_TOGGLE, async_handle_alert_service, + schema=ALERT_SERVICE_SCHEMA) + + tasks = [alert.async_update_ha_state() for alert in entities] + if tasks: + await asyncio.wait(tasks, loop=hass.loop) + + return True + + +class Alert(ToggleEntity): + """Representation of an alert.""" + + def __init__(self, hass, entity_id, name, watched_entity_id, + state, repeat, skip_first, message_template, + done_message_template, notifiers, can_ack, title_template, + data): + """Initialize the alert.""" + self.hass = hass + self._name = name + self._alert_state = state + self._skip_first = skip_first + self._data = data + + self._message_template = message_template + if self._message_template is not None: + self._message_template.hass = hass + + self._done_message_template = done_message_template + if self._done_message_template is not None: + self._done_message_template.hass = hass + + self._title_template = title_template + if self._title_template is not None: + self._title_template.hass = hass + + self._notifiers = notifiers + self._can_ack = can_ack + + self._delay = [timedelta(minutes=val) for val in repeat] + self._next_delay = 0 + + self._firing = False + self._ack = False + self._cancel = None + self._send_done_message = False + self.entity_id = ENTITY_ID_FORMAT.format(entity_id) + + event.async_track_state_change( + hass, watched_entity_id, self.watched_entity_change) + + @property + def name(self): + """Return the name of the alert.""" + return self._name + + @property + def should_poll(self): + """HASS need not poll these entities.""" + return False + + @property + def state(self): + """Return the alert status.""" + if self._firing: + if self._ack: + return STATE_OFF + return STATE_ON + return STATE_IDLE + + @property + def hidden(self): + """Hide the alert when it is not firing.""" + return not self._can_ack or not self._firing + + async def watched_entity_change(self, entity, from_state, to_state): + """Determine if the alert should start or stop.""" + _LOGGER.debug("Watched entity (%s) has changed", entity) + if to_state.state == self._alert_state and not self._firing: + await self.begin_alerting() + if to_state.state != self._alert_state and self._firing: + await self.end_alerting() + + async def begin_alerting(self): + """Begin the alert procedures.""" + _LOGGER.debug("Beginning Alert: %s", self._name) + self._ack = False + self._firing = True + self._next_delay = 0 + + if not self._skip_first: + await self._notify() + else: + await self._schedule_notify() + + self.async_schedule_update_ha_state() + + async def end_alerting(self): + """End the alert procedures.""" + _LOGGER.debug("Ending Alert: %s", self._name) + self._cancel() + self._ack = False + self._firing = False + if self._send_done_message: + await self._notify_done_message() + self.async_schedule_update_ha_state() + + async def _schedule_notify(self): + """Schedule a notification.""" + delay = self._delay[self._next_delay] + next_msg = datetime.now() + delay + self._cancel = \ + event.async_track_point_in_time(self.hass, self._notify, next_msg) + self._next_delay = min(self._next_delay + 1, len(self._delay) - 1) + + async def _notify(self, *args): + """Send the alert notification.""" + if not self._firing: + return + + if not self._ack: + _LOGGER.info("Alerting: %s", self._name) + self._send_done_message = True + + if self._message_template is not None: + message = self._message_template.async_render() + else: + message = self._name + + await self._send_notification_message(message) + await self._schedule_notify() + + async def _notify_done_message(self, *args): + """Send notification of complete alert.""" + _LOGGER.info("Alerting: %s", self._done_message_template) + self._send_done_message = False + + if self._done_message_template is None: + return + + message = self._done_message_template.async_render() + + await self._send_notification_message(message) + + async def _send_notification_message(self, message): + + msg_payload = {ATTR_MESSAGE: message} + + if self._title_template is not None: + title = self._title_template.async_render() + msg_payload.update({ATTR_TITLE: title}) + if self._data: + msg_payload.update({ATTR_DATA: self._data}) + + _LOGGER.debug(msg_payload) + + for target in self._notifiers: + await self.hass.services.async_call( + DOMAIN_NOTIFY, target, msg_payload) + + async def async_turn_on(self, **kwargs): + """Async Unacknowledge alert.""" + _LOGGER.debug("Reset Alert: %s", self._name) + self._ack = False + await self.async_update_ha_state() + + async def async_turn_off(self, **kwargs): + """Async Acknowledge alert.""" + _LOGGER.debug("Acknowledged Alert: %s", self._name) + self._ack = True + await self.async_update_ha_state() + + async def async_toggle(self, **kwargs): + """Async toggle alert.""" + if self._ack: + return await self.async_turn_on() + return await self.async_turn_off() diff --git a/homeassistant/components/alert/services.yaml b/homeassistant/components/alert/services.yaml new file mode 100644 index 0000000000000..1cdd1f02e7eed --- /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 8491268dfd6de..062d698d5122a 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 @@ 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 978cb61189557..6918ec1e54f04 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 02f47b0561794..537f04b20be4d 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 85cb4f105cd41..b30a7238b3e04 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 7240912883a88..4e2383bb43d01 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 @@ 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 @@ (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 @@ def interfaces(self): _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 @@ def interfaces(self): 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 0000000000000..ac3d86a995bd0 --- /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 0000000000000..ede25d0bd4b7d --- /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 51a09514159ef..c316741d36b8e 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 0000000000000..a070128eefe41 --- /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 0000000000000..0b9d377718ba2 --- /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 0000000000000..2140b4e29fe27 --- /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 0000000000000..01078bbddfea9 --- /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 0991336f42a2c..4464992e5fa58 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.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 @@ def on_subscribed(data): _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 @@ def on_subscribed(data): 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 @@ def on_subscribed(data): 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 0000000000000..2defa032809a1 --- /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 56e747ce5e096..f01bfd8f791d7 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 75606a1c699fc..27ec7afefaa4a 100644 --- a/homeassistant/components/ambient_station/const.py +++ b/homeassistant/components/ambient_station/const.py @@ -8,3 +8,6 @@ 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 9e0833e344132..fa3222bf0e44e 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -1,70 +1,51 @@ -""" -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 -DEPENDENCIES = ['ambient_station'] _LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['ambient_station'] + 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 - self._unit = unit + super().__init__( + ambient, mac_address, station_name, sensor_type, sensor_name) - @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 + self._unit = unit @property def state(self): @@ -76,26 +57,6 @@ def unit_of_measurement(self): """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.py deleted file mode 100644 index bcd0c38c3bdff..0000000000000 --- a/homeassistant/components/amcrest.py +++ /dev/null @@ -1,178 +0,0 @@ -""" -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/ -""" -import logging -from datetime import timedelta - -import aiohttp -import voluptuous as vol -from requests.exceptions import HTTPError, ConnectTimeout -from requests.exceptions import ConnectionError as ConnectError - -from homeassistant.const import ( - CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD, - CONF_SENSORS, CONF_SWITCHES, CONF_SCAN_INTERVAL, HTTP_BASIC_AUTHENTICATION) -from homeassistant.helpers import discovery -import homeassistant.helpers.config_validation as cv - -REQUIREMENTS = ['amcrest==1.2.3'] -DEPENDENCIES = ['ffmpeg'] - -_LOGGER = logging.getLogger(__name__) - -CONF_AUTHENTICATION = 'authentication' -CONF_RESOLUTION = 'resolution' -CONF_STREAM_SOURCE = 'stream_source' -CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments' - -DEFAULT_NAME = 'Amcrest Camera' -DEFAULT_PORT = 80 -DEFAULT_RESOLUTION = 'high' -DEFAULT_STREAM_SOURCE = 'snapshot' -TIMEOUT = 10 - -DATA_AMCREST = 'amcrest' -DOMAIN = 'amcrest' - -NOTIFICATION_ID = 'amcrest_notification' -NOTIFICATION_TITLE = 'Amcrest Camera Setup' - -RESOLUTION_LIST = { - 'high': 0, - 'low': 1, -} - -SCAN_INTERVAL = timedelta(seconds=10) - -AUTHENTICATION_LIST = { - 'basic': 'basic' -} - -STREAM_SOURCE_LIST = { - 'mjpeg': 0, - 'snapshot': 1, - 'rtsp': 2, -} - -# Sensor types are defined like: Name, units, icon -SENSORS = { - 'motion_detector': ['Motion Detected', None, 'mdi:run'], - 'sdcard': ['SD Used', '%', 'mdi:sd'], - 'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'], -} - -# Switch types are defined like: Name, icon -SWITCHES = { - 'motion_detection': ['Motion Detection', 'mdi:run-fast'], - 'motion_recording': ['Motion Recording', 'mdi:record-rec'] -} - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION): - vol.All(vol.In(AUTHENTICATION_LIST)), - vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION): - vol.All(vol.In(RESOLUTION_LIST)), - vol.Optional(CONF_STREAM_SOURCE, default=DEFAULT_STREAM_SOURCE): - vol.All(vol.In(STREAM_SOURCE_LIST)), - vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string, - vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): - cv.time_period, - vol.Optional(CONF_SENSORS): - vol.All(cv.ensure_list, [vol.In(SENSORS)]), - vol.Optional(CONF_SWITCHES): - vol.All(cv.ensure_list, [vol.In(SWITCHES)]), - })]) -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the Amcrest IP Camera component.""" - from amcrest import AmcrestCamera - - hass.data[DATA_AMCREST] = {} - amcrest_cams = config[DOMAIN] - - for device in amcrest_cams: - try: - camera = AmcrestCamera(device.get(CONF_HOST), - device.get(CONF_PORT), - device.get(CONF_USERNAME), - device.get(CONF_PASSWORD)).camera - # pylint: disable=pointless-statement - camera.current_time - - except (ConnectError, ConnectTimeout, HTTPError) as ex: - _LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex)) - hass.components.persistent_notification.create( - 'Error: {}
' - 'You will need to restart hass after fixing.' - ''.format(ex), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) - continue - - ffmpeg_arguments = device.get(CONF_FFMPEG_ARGUMENTS) - name = device.get(CONF_NAME) - resolution = RESOLUTION_LIST[device.get(CONF_RESOLUTION)] - sensors = device.get(CONF_SENSORS) - switches = device.get(CONF_SWITCHES) - stream_source = STREAM_SOURCE_LIST[device.get(CONF_STREAM_SOURCE)] - - username = device.get(CONF_USERNAME) - password = device.get(CONF_PASSWORD) - - # currently aiohttp only works with basic authentication - # only valid for mjpeg streaming - if username is not None and password is not None: - if device.get(CONF_AUTHENTICATION) == HTTP_BASIC_AUTHENTICATION: - authentication = aiohttp.BasicAuth(username, password) - else: - authentication = None - - hass.data[DATA_AMCREST][name] = AmcrestDevice( - camera, name, authentication, ffmpeg_arguments, stream_source, - resolution) - - discovery.load_platform( - hass, 'camera', DOMAIN, { - CONF_NAME: name, - }, config) - - if sensors: - discovery.load_platform( - hass, 'sensor', DOMAIN, { - CONF_NAME: name, - CONF_SENSORS: sensors, - }, config) - - if switches: - discovery.load_platform( - hass, 'switch', DOMAIN, { - CONF_NAME: name, - CONF_SWITCHES: switches - }, config) - - return True - - -class AmcrestDevice: - """Representation of a base Amcrest discovery device.""" - - def __init__(self, camera, name, authentication, ffmpeg_arguments, - stream_source, resolution): - """Initialize the entity.""" - self.device = camera - self.name = name - self.authentication = authentication - self.ffmpeg_arguments = ffmpeg_arguments - self.stream_source = stream_source - self.resolution = resolution diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py new file mode 100644 index 0000000000000..49f11570b21de --- /dev/null +++ b/homeassistant/components/amcrest/__init__.py @@ -0,0 +1,173 @@ +"""Support for Amcrest IP cameras.""" +import logging +from datetime import timedelta + +import aiohttp +import voluptuous as vol +from requests.exceptions import HTTPError, ConnectTimeout +from requests.exceptions import ConnectionError as ConnectError + +from homeassistant.const import ( + CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD, + CONF_SENSORS, CONF_SWITCHES, CONF_SCAN_INTERVAL, HTTP_BASIC_AUTHENTICATION) +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['amcrest==1.2.3'] +DEPENDENCIES = ['ffmpeg'] + +_LOGGER = logging.getLogger(__name__) + +CONF_AUTHENTICATION = 'authentication' +CONF_RESOLUTION = 'resolution' +CONF_STREAM_SOURCE = 'stream_source' +CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments' + +DEFAULT_NAME = 'Amcrest Camera' +DEFAULT_PORT = 80 +DEFAULT_RESOLUTION = 'high' +DEFAULT_STREAM_SOURCE = 'snapshot' +TIMEOUT = 10 + +DATA_AMCREST = 'amcrest' +DOMAIN = 'amcrest' + +NOTIFICATION_ID = 'amcrest_notification' +NOTIFICATION_TITLE = 'Amcrest Camera Setup' + +RESOLUTION_LIST = { + 'high': 0, + 'low': 1, +} + +SCAN_INTERVAL = timedelta(seconds=10) + +AUTHENTICATION_LIST = { + 'basic': 'basic' +} + +STREAM_SOURCE_LIST = { + 'mjpeg': 0, + 'snapshot': 1, + 'rtsp': 2, +} + +# Sensor types are defined like: Name, units, icon +SENSORS = { + 'motion_detector': ['Motion Detected', None, 'mdi:run'], + 'sdcard': ['SD Used', '%', 'mdi:sd'], + 'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'], +} + +# Switch types are defined like: Name, icon +SWITCHES = { + 'motion_detection': ['Motion Detection', 'mdi:run-fast'], + 'motion_recording': ['Motion Recording', 'mdi:record-rec'] +} + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION): + vol.All(vol.In(AUTHENTICATION_LIST)), + vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION): + vol.All(vol.In(RESOLUTION_LIST)), + vol.Optional(CONF_STREAM_SOURCE, default=DEFAULT_STREAM_SOURCE): + vol.All(vol.In(STREAM_SOURCE_LIST)), + vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): + cv.time_period, + vol.Optional(CONF_SENSORS): + vol.All(cv.ensure_list, [vol.In(SENSORS)]), + vol.Optional(CONF_SWITCHES): + vol.All(cv.ensure_list, [vol.In(SWITCHES)]), + })]) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Amcrest IP Camera component.""" + from amcrest import AmcrestCamera + + hass.data[DATA_AMCREST] = {} + amcrest_cams = config[DOMAIN] + + for device in amcrest_cams: + try: + camera = AmcrestCamera(device.get(CONF_HOST), + device.get(CONF_PORT), + device.get(CONF_USERNAME), + device.get(CONF_PASSWORD)).camera + # pylint: disable=pointless-statement + camera.current_time + + except (ConnectError, ConnectTimeout, HTTPError) as ex: + _LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex)) + hass.components.persistent_notification.create( + 'Error: {}
' + 'You will need to restart hass after fixing.' + ''.format(ex), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + continue + + ffmpeg_arguments = device.get(CONF_FFMPEG_ARGUMENTS) + name = device.get(CONF_NAME) + resolution = RESOLUTION_LIST[device.get(CONF_RESOLUTION)] + sensors = device.get(CONF_SENSORS) + switches = device.get(CONF_SWITCHES) + stream_source = STREAM_SOURCE_LIST[device.get(CONF_STREAM_SOURCE)] + + username = device.get(CONF_USERNAME) + password = device.get(CONF_PASSWORD) + + # currently aiohttp only works with basic authentication + # only valid for mjpeg streaming + if username is not None and password is not None: + if device.get(CONF_AUTHENTICATION) == HTTP_BASIC_AUTHENTICATION: + authentication = aiohttp.BasicAuth(username, password) + else: + authentication = None + + hass.data[DATA_AMCREST][name] = AmcrestDevice( + camera, name, authentication, ffmpeg_arguments, stream_source, + resolution) + + discovery.load_platform( + hass, 'camera', DOMAIN, { + CONF_NAME: name, + }, config) + + if sensors: + discovery.load_platform( + hass, 'sensor', DOMAIN, { + CONF_NAME: name, + CONF_SENSORS: sensors, + }, config) + + if switches: + discovery.load_platform( + hass, 'switch', DOMAIN, { + CONF_NAME: name, + CONF_SWITCHES: switches + }, config) + + return True + + +class AmcrestDevice: + """Representation of a base Amcrest discovery device.""" + + def __init__(self, camera, name, authentication, ffmpeg_arguments, + stream_source, resolution): + """Initialize the entity.""" + self.device = camera + self.name = name + self.authentication = authentication + self.ffmpeg_arguments = ffmpeg_arguments + self.stream_source = stream_source + self.resolution = resolution diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py new file mode 100644 index 0000000000000..7c943b897343a --- /dev/null +++ b/homeassistant/components/amcrest/camera.py @@ -0,0 +1,87 @@ +"""Support for Amcrest IP cameras.""" +import logging + +from homeassistant.components.amcrest import ( + DATA_AMCREST, STREAM_SOURCE_LIST, TIMEOUT) +from homeassistant.components.camera import Camera +from homeassistant.components.ffmpeg import DATA_FFMPEG +from homeassistant.const import CONF_NAME +from homeassistant.helpers.aiohttp_client import ( + async_get_clientsession, async_aiohttp_proxy_web, + async_aiohttp_proxy_stream) + +DEPENDENCIES = ['amcrest', 'ffmpeg'] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up an Amcrest IP Camera.""" + if discovery_info is None: + return + + device_name = discovery_info[CONF_NAME] + amcrest = hass.data[DATA_AMCREST][device_name] + + async_add_entities([AmcrestCam(hass, amcrest)], True) + + return True + + +class AmcrestCam(Camera): + """An implementation of an Amcrest IP camera.""" + + def __init__(self, hass, amcrest): + """Initialize an Amcrest camera.""" + super(AmcrestCam, self).__init__() + self._name = amcrest.name + self._camera = amcrest.device + self._base_url = self._camera.get_base_url() + self._ffmpeg = hass.data[DATA_FFMPEG] + self._ffmpeg_arguments = amcrest.ffmpeg_arguments + self._stream_source = amcrest.stream_source + self._resolution = amcrest.resolution + self._token = self._auth = amcrest.authentication + + def camera_image(self): + """Return a still image response from the camera.""" + # Send the request to snap a picture and return raw jpg data + response = self._camera.snapshot(channel=self._resolution) + return response.data + + async def handle_async_mjpeg_stream(self, request): + """Return an MJPEG stream.""" + # The snapshot implementation is handled by the parent class + if self._stream_source == STREAM_SOURCE_LIST['snapshot']: + return await super().handle_async_mjpeg_stream(request) + + if self._stream_source == STREAM_SOURCE_LIST['mjpeg']: + # stream an MJPEG image stream directly from the camera + websession = async_get_clientsession(self.hass) + streaming_url = self._camera.mjpeg_url(typeno=self._resolution) + stream_coro = websession.get( + streaming_url, auth=self._token, timeout=TIMEOUT) + + return await async_aiohttp_proxy_web( + self.hass, request, stream_coro) + + # streaming via ffmpeg + from haffmpeg import CameraMjpeg + + streaming_url = self._camera.rtsp_url(typeno=self._resolution) + stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop) + await stream.open_camera( + streaming_url, extra_cmd=self._ffmpeg_arguments) + + try: + return await async_aiohttp_proxy_stream( + self.hass, request, stream, + self._ffmpeg.ffmpeg_stream_content_type) + finally: + await stream.close() + + @property + def name(self): + """Return the name of this camera.""" + return self._name diff --git a/homeassistant/components/amcrest/sensor.py b/homeassistant/components/amcrest/sensor.py new file mode 100644 index 0000000000000..4869dfffa6e5b --- /dev/null +++ b/homeassistant/components/amcrest/sensor.py @@ -0,0 +1,102 @@ +"""Suppoort for Amcrest IP camera sensors.""" +from datetime import timedelta +import logging + +from homeassistant.components.amcrest import DATA_AMCREST, SENSORS +from homeassistant.helpers.entity import Entity +from homeassistant.const import CONF_NAME, CONF_SENSORS + +DEPENDENCIES = ['amcrest'] + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=10) + + +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 + + device_name = discovery_info[CONF_NAME] + sensors = discovery_info[CONF_SENSORS] + amcrest = hass.data[DATA_AMCREST][device_name] + + amcrest_sensors = [] + for sensor_type in sensors: + amcrest_sensors.append( + AmcrestSensor(amcrest.name, amcrest.device, sensor_type)) + + async_add_entities(amcrest_sensors, True) + return True + + +class AmcrestSensor(Entity): + """A sensor implementation for Amcrest IP camera.""" + + def __init__(self, name, camera, sensor_type): + """Initialize a sensor for Amcrest camera.""" + self._attrs = {} + self._camera = camera + self._sensor_type = sensor_type + 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 + + @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.""" + return self._attrs + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self._icon + + @property + def unit_of_measurement(self): + """Return the units of measurement.""" + return SENSORS.get(self._sensor_type)[1] + + def update(self): + """Get the latest data and updates the state.""" + _LOGGER.debug("Pulling data from %s sensor.", self._name) + + try: + version, build_date = self._camera.software_information + self._attrs['Build Date'] = build_date.split('=')[-1] + self._attrs['Version'] = version.split('=')[-1] + except ValueError: + self._attrs['Build Date'] = 'Not Available' + self._attrs['Version'] = 'Not Available' + + try: + self._attrs['Serial Number'] = self._camera.serial_number + except ValueError: + self._attrs['Serial Number'] = 'Not Available' + + if self._sensor_type == 'motion_detector': + self._state = self._camera.is_motion_detected + self._attrs['Record Mode'] = self._camera.record_mode + + elif self._sensor_type == 'ptz_preset': + self._state = self._camera.ptz_presets_count + + elif self._sensor_type == 'sdcard': + sd_used = self._camera.storage_used + sd_total = self._camera.storage_total + self._attrs['Total'] = '{0} {1}'.format(*sd_total) + self._attrs['Used'] = '{0} {1}'.format(*sd_used) + self._state = self._camera.storage_used_percent diff --git a/homeassistant/components/amcrest/switch.py b/homeassistant/components/amcrest/switch.py new file mode 100644 index 0000000000000..3c1f03f01452a --- /dev/null +++ b/homeassistant/components/amcrest/switch.py @@ -0,0 +1,86 @@ +"""Support for toggling Amcrest IP camera settings.""" +import logging + +from homeassistant.components.amcrest import DATA_AMCREST, SWITCHES +from homeassistant.const import ( + CONF_NAME, CONF_SWITCHES, STATE_OFF, STATE_ON) +from homeassistant.helpers.entity import ToggleEntity + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['amcrest'] + + +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 + + name = discovery_info[CONF_NAME] + switches = discovery_info[CONF_SWITCHES] + camera = hass.data[DATA_AMCREST][name].device + + all_switches = [] + + for setting in switches: + all_switches.append(AmcrestSwitch(setting, camera, name)) + + async_add_entities(all_switches, True) + + +class AmcrestSwitch(ToggleEntity): + """Representation of an Amcrest IP camera switch.""" + + def __init__(self, setting, camera, name): + """Initialize the Amcrest switch.""" + self._setting = setting + self._camera = camera + self._name = '{} {}'.format(SWITCHES[setting][0], name) + self._icon = SWITCHES[setting][1] + self._state = None + + @property + def name(self): + """Return the name of the switch if any.""" + return self._name + + @property + def state(self): + """Return the state of the switch.""" + return self._state + + @property + def is_on(self): + """Return true if switch is on.""" + return self._state == STATE_ON + + def turn_on(self, **kwargs): + """Turn setting on.""" + if self._setting == 'motion_detection': + self._camera.motion_detection = 'true' + elif self._setting == 'motion_recording': + self._camera.motion_recording = 'true' + + def turn_off(self, **kwargs): + """Turn setting off.""" + if self._setting == 'motion_detection': + self._camera.motion_detection = 'false' + elif self._setting == 'motion_recording': + self._camera.motion_recording = 'false' + + def update(self): + """Update setting state.""" + _LOGGER.debug("Polling state for setting: %s ", self._name) + + if self._setting == 'motion_detection': + detection = self._camera.is_motion_detector_on() + elif self._setting == 'motion_recording': + detection = self._camera.is_record_on_motion_detection() + + self._state = STATE_ON if detection else STATE_OFF + + @property + def icon(self): + """Return the icon for the switch.""" + return self._icon diff --git a/homeassistant/components/android_ip_webcam.py b/homeassistant/components/android_ip_webcam.py deleted file mode 100644 index 1cf46174371e5..0000000000000 --- a/homeassistant/components/android_ip_webcam.py +++ /dev/null @@ -1,290 +0,0 @@ -""" -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/ -""" -import asyncio -import logging -from datetime import timedelta - -import voluptuous as vol - -from homeassistant.core import callback -from homeassistant.const import ( - CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD, - CONF_SENSORS, CONF_SWITCHES, CONF_TIMEOUT, CONF_SCAN_INTERVAL, - CONF_PLATFORM) -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers import discovery -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_send, async_dispatcher_connect) -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.util.dt import utcnow -from homeassistant.components.camera.mjpeg import ( - CONF_MJPEG_URL, CONF_STILL_IMAGE_URL) - -REQUIREMENTS = ['pydroid-ipcam==0.8'] - -_LOGGER = logging.getLogger(__name__) - -ATTR_AUD_CONNS = 'Audio Connections' -ATTR_HOST = 'host' -ATTR_VID_CONNS = 'Video Connections' - -CONF_MOTION_SENSOR = 'motion_sensor' - -DATA_IP_WEBCAM = 'android_ip_webcam' -DEFAULT_NAME = 'IP Webcam' -DEFAULT_PORT = 8080 -DEFAULT_TIMEOUT = 10 -DOMAIN = 'android_ip_webcam' - -SCAN_INTERVAL = timedelta(seconds=10) -SIGNAL_UPDATE_DATA = 'android_ip_webcam_update' - -KEY_MAP = { - 'audio_connections': 'Audio Connections', - 'adet_limit': 'Audio Trigger Limit', - 'antibanding': 'Anti-banding', - 'audio_only': 'Audio Only', - 'battery_level': 'Battery Level', - 'battery_temp': 'Battery Temperature', - 'battery_voltage': 'Battery Voltage', - 'coloreffect': 'Color Effect', - 'exposure': 'Exposure Level', - 'exposure_lock': 'Exposure Lock', - 'ffc': 'Front-facing Camera', - 'flashmode': 'Flash Mode', - 'focus': 'Focus', - 'focus_homing': 'Focus Homing', - 'focus_region': 'Focus Region', - 'focusmode': 'Focus Mode', - 'gps_active': 'GPS Active', - 'idle': 'Idle', - 'ip_address': 'IPv4 Address', - 'ipv6_address': 'IPv6 Address', - 'ivideon_streaming': 'Ivideon Streaming', - 'light': 'Light Level', - 'mirror_flip': 'Mirror Flip', - 'motion': 'Motion', - 'motion_active': 'Motion Active', - 'motion_detect': 'Motion Detection', - 'motion_event': 'Motion Event', - 'motion_limit': 'Motion Limit', - 'night_vision': 'Night Vision', - 'night_vision_average': 'Night Vision Average', - 'night_vision_gain': 'Night Vision Gain', - 'orientation': 'Orientation', - 'overlay': 'Overlay', - 'photo_size': 'Photo Size', - 'pressure': 'Pressure', - 'proximity': 'Proximity', - 'quality': 'Quality', - 'scenemode': 'Scene Mode', - 'sound': 'Sound', - 'sound_event': 'Sound Event', - 'sound_timeout': 'Sound Timeout', - 'torch': 'Torch', - 'video_connections': 'Video Connections', - 'video_chunk_len': 'Video Chunk Length', - 'video_recording': 'Video Recording', - 'video_size': 'Video Size', - 'whitebalance': 'White Balance', - 'whitebalance_lock': 'White Balance Lock', - 'zoom': 'Zoom' -} - -ICON_MAP = { - 'audio_connections': 'mdi:speaker', - 'battery_level': 'mdi:battery', - 'battery_temp': 'mdi:thermometer', - 'battery_voltage': 'mdi:battery-charging-100', - 'exposure_lock': 'mdi:camera', - 'ffc': 'mdi:camera-front-variant', - 'focus': 'mdi:image-filter-center-focus', - 'gps_active': 'mdi:crosshairs-gps', - 'light': 'mdi:flashlight', - 'motion': 'mdi:run', - 'night_vision': 'mdi:weather-night', - 'overlay': 'mdi:monitor', - 'pressure': 'mdi:gauge', - 'proximity': 'mdi:map-marker-radius', - 'quality': 'mdi:quality-high', - 'sound': 'mdi:speaker', - 'sound_event': 'mdi:speaker', - 'sound_timeout': 'mdi:speaker', - 'torch': 'mdi:white-balance-sunny', - 'video_chunk_len': 'mdi:video', - 'video_connections': 'mdi:eye', - 'video_recording': 'mdi:record-rec', - 'whitebalance_lock': 'mdi:white-balance-auto' -} - -SWITCHES = ['exposure_lock', 'ffc', 'focus', 'gps_active', - 'motion_detect', 'night_vision', 'overlay', - 'torch', 'whitebalance_lock', 'video_recording'] - -SENSORS = ['audio_connections', 'battery_level', 'battery_temp', - 'battery_voltage', 'light', 'motion', 'pressure', 'proximity', - 'sound', 'video_connections'] - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): - cv.time_period, - vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string, - vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string, - vol.Optional(CONF_SWITCHES): - vol.All(cv.ensure_list, [vol.In(SWITCHES)]), - vol.Optional(CONF_SENSORS): - vol.All(cv.ensure_list, [vol.In(SENSORS)]), - vol.Optional(CONF_MOTION_SENSOR): cv.boolean, - })]) -}, extra=vol.ALLOW_EXTRA) - - -async def async_setup(hass, config): - """Set up the IP Webcam component.""" - from pydroid_ipcam import PyDroidIPCam - - webcams = hass.data[DATA_IP_WEBCAM] = {} - websession = async_get_clientsession(hass) - - async def async_setup_ipcamera(cam_config): - """Set up an IP camera.""" - host = cam_config[CONF_HOST] - username = cam_config.get(CONF_USERNAME) - password = cam_config.get(CONF_PASSWORD) - name = cam_config[CONF_NAME] - interval = cam_config[CONF_SCAN_INTERVAL] - switches = cam_config.get(CONF_SWITCHES) - sensors = cam_config.get(CONF_SENSORS) - motion = cam_config.get(CONF_MOTION_SENSOR) - - # Init ip webcam - cam = PyDroidIPCam( - hass.loop, websession, host, cam_config[CONF_PORT], - username=username, password=password, - timeout=cam_config[CONF_TIMEOUT] - ) - - if switches is None: - switches = [setting for setting in cam.enabled_settings - if setting in SWITCHES] - - if sensors is None: - sensors = [sensor for sensor in cam.enabled_sensors - if sensor in SENSORS] - sensors.extend(['audio_connections', 'video_connections']) - - if motion is None: - motion = 'motion_active' in cam.enabled_sensors - - async def async_update_data(now): - """Update data from IP camera in SCAN_INTERVAL.""" - await cam.update() - async_dispatcher_send(hass, SIGNAL_UPDATE_DATA, host) - - async_track_point_in_utc_time( - hass, async_update_data, utcnow() + interval) - - await async_update_data(None) - - # Load platforms - webcams[host] = cam - - mjpeg_camera = { - CONF_PLATFORM: 'mjpeg', - CONF_MJPEG_URL: cam.mjpeg_url, - CONF_STILL_IMAGE_URL: cam.image_url, - CONF_NAME: name, - } - if username and password: - mjpeg_camera.update({ - CONF_USERNAME: username, - CONF_PASSWORD: password - }) - - hass.async_create_task(discovery.async_load_platform( - hass, 'camera', 'mjpeg', mjpeg_camera, config)) - - if sensors: - hass.async_create_task(discovery.async_load_platform( - hass, 'sensor', DOMAIN, { - CONF_NAME: name, - CONF_HOST: host, - CONF_SENSORS: sensors, - }, config)) - - if switches: - hass.async_create_task(discovery.async_load_platform( - hass, 'switch', DOMAIN, { - CONF_NAME: name, - CONF_HOST: host, - CONF_SWITCHES: switches, - }, config)) - - if motion: - hass.async_create_task(discovery.async_load_platform( - hass, 'binary_sensor', DOMAIN, { - CONF_HOST: host, - CONF_NAME: name, - }, config)) - - tasks = [async_setup_ipcamera(conf) for conf in config[DOMAIN]] - if tasks: - await asyncio.wait(tasks, loop=hass.loop) - - return True - - -class AndroidIPCamEntity(Entity): - """The Android device running IP Webcam.""" - - def __init__(self, host, ipcam): - """Initialize the data object.""" - self._host = host - self._ipcam = ipcam - - async def async_added_to_hass(self): - """Register update dispatcher.""" - @callback - def async_ipcam_update(host): - """Update callback.""" - if self._host != host: - return - self.async_schedule_update_ha_state(True) - - async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_DATA, async_ipcam_update) - - @property - def should_poll(self): - """Return True if entity has to be polled for state.""" - return False - - @property - def available(self): - """Return True if entity is available.""" - return self._ipcam.available - - @property - def device_state_attributes(self): - """Return the state attributes.""" - state_attr = {ATTR_HOST: self._host} - if self._ipcam.status_data is None: - return state_attr - - state_attr[ATTR_VID_CONNS] = \ - self._ipcam.status_data.get('video_connections') - state_attr[ATTR_AUD_CONNS] = \ - self._ipcam.status_data.get('audio_connections') - - return state_attr diff --git a/homeassistant/components/android_ip_webcam/__init__.py b/homeassistant/components/android_ip_webcam/__init__.py new file mode 100644 index 0000000000000..c5424b3d0fa70 --- /dev/null +++ b/homeassistant/components/android_ip_webcam/__init__.py @@ -0,0 +1,285 @@ +"""Support for Android IP Webcam.""" +import asyncio +import logging +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.const import ( + CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD, + CONF_SENSORS, CONF_SWITCHES, CONF_TIMEOUT, CONF_SCAN_INTERVAL, + CONF_PLATFORM) +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_send, async_dispatcher_connect) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.util.dt import utcnow +from homeassistant.components.camera.mjpeg import ( + CONF_MJPEG_URL, CONF_STILL_IMAGE_URL) + +REQUIREMENTS = ['pydroid-ipcam==0.8'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_AUD_CONNS = 'Audio Connections' +ATTR_HOST = 'host' +ATTR_VID_CONNS = 'Video Connections' + +CONF_MOTION_SENSOR = 'motion_sensor' + +DATA_IP_WEBCAM = 'android_ip_webcam' +DEFAULT_NAME = 'IP Webcam' +DEFAULT_PORT = 8080 +DEFAULT_TIMEOUT = 10 +DOMAIN = 'android_ip_webcam' + +SCAN_INTERVAL = timedelta(seconds=10) +SIGNAL_UPDATE_DATA = 'android_ip_webcam_update' + +KEY_MAP = { + 'audio_connections': 'Audio Connections', + 'adet_limit': 'Audio Trigger Limit', + 'antibanding': 'Anti-banding', + 'audio_only': 'Audio Only', + 'battery_level': 'Battery Level', + 'battery_temp': 'Battery Temperature', + 'battery_voltage': 'Battery Voltage', + 'coloreffect': 'Color Effect', + 'exposure': 'Exposure Level', + 'exposure_lock': 'Exposure Lock', + 'ffc': 'Front-facing Camera', + 'flashmode': 'Flash Mode', + 'focus': 'Focus', + 'focus_homing': 'Focus Homing', + 'focus_region': 'Focus Region', + 'focusmode': 'Focus Mode', + 'gps_active': 'GPS Active', + 'idle': 'Idle', + 'ip_address': 'IPv4 Address', + 'ipv6_address': 'IPv6 Address', + 'ivideon_streaming': 'Ivideon Streaming', + 'light': 'Light Level', + 'mirror_flip': 'Mirror Flip', + 'motion': 'Motion', + 'motion_active': 'Motion Active', + 'motion_detect': 'Motion Detection', + 'motion_event': 'Motion Event', + 'motion_limit': 'Motion Limit', + 'night_vision': 'Night Vision', + 'night_vision_average': 'Night Vision Average', + 'night_vision_gain': 'Night Vision Gain', + 'orientation': 'Orientation', + 'overlay': 'Overlay', + 'photo_size': 'Photo Size', + 'pressure': 'Pressure', + 'proximity': 'Proximity', + 'quality': 'Quality', + 'scenemode': 'Scene Mode', + 'sound': 'Sound', + 'sound_event': 'Sound Event', + 'sound_timeout': 'Sound Timeout', + 'torch': 'Torch', + 'video_connections': 'Video Connections', + 'video_chunk_len': 'Video Chunk Length', + 'video_recording': 'Video Recording', + 'video_size': 'Video Size', + 'whitebalance': 'White Balance', + 'whitebalance_lock': 'White Balance Lock', + 'zoom': 'Zoom' +} + +ICON_MAP = { + 'audio_connections': 'mdi:speaker', + 'battery_level': 'mdi:battery', + 'battery_temp': 'mdi:thermometer', + 'battery_voltage': 'mdi:battery-charging-100', + 'exposure_lock': 'mdi:camera', + 'ffc': 'mdi:camera-front-variant', + 'focus': 'mdi:image-filter-center-focus', + 'gps_active': 'mdi:crosshairs-gps', + 'light': 'mdi:flashlight', + 'motion': 'mdi:run', + 'night_vision': 'mdi:weather-night', + 'overlay': 'mdi:monitor', + 'pressure': 'mdi:gauge', + 'proximity': 'mdi:map-marker-radius', + 'quality': 'mdi:quality-high', + 'sound': 'mdi:speaker', + 'sound_event': 'mdi:speaker', + 'sound_timeout': 'mdi:speaker', + 'torch': 'mdi:white-balance-sunny', + 'video_chunk_len': 'mdi:video', + 'video_connections': 'mdi:eye', + 'video_recording': 'mdi:record-rec', + 'whitebalance_lock': 'mdi:white-balance-auto' +} + +SWITCHES = ['exposure_lock', 'ffc', 'focus', 'gps_active', + 'motion_detect', 'night_vision', 'overlay', + 'torch', 'whitebalance_lock', 'video_recording'] + +SENSORS = ['audio_connections', 'battery_level', 'battery_temp', + 'battery_voltage', 'light', 'motion', 'pressure', 'proximity', + 'sound', 'video_connections'] + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): + cv.time_period, + vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string, + vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string, + vol.Optional(CONF_SWITCHES): + vol.All(cv.ensure_list, [vol.In(SWITCHES)]), + vol.Optional(CONF_SENSORS): + vol.All(cv.ensure_list, [vol.In(SENSORS)]), + vol.Optional(CONF_MOTION_SENSOR): cv.boolean, + })]) +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the IP Webcam component.""" + from pydroid_ipcam import PyDroidIPCam + + webcams = hass.data[DATA_IP_WEBCAM] = {} + websession = async_get_clientsession(hass) + + async def async_setup_ipcamera(cam_config): + """Set up an IP camera.""" + host = cam_config[CONF_HOST] + username = cam_config.get(CONF_USERNAME) + password = cam_config.get(CONF_PASSWORD) + name = cam_config[CONF_NAME] + interval = cam_config[CONF_SCAN_INTERVAL] + switches = cam_config.get(CONF_SWITCHES) + sensors = cam_config.get(CONF_SENSORS) + motion = cam_config.get(CONF_MOTION_SENSOR) + + # Init ip webcam + cam = PyDroidIPCam( + hass.loop, websession, host, cam_config[CONF_PORT], + username=username, password=password, + timeout=cam_config[CONF_TIMEOUT] + ) + + if switches is None: + switches = [setting for setting in cam.enabled_settings + if setting in SWITCHES] + + if sensors is None: + sensors = [sensor for sensor in cam.enabled_sensors + if sensor in SENSORS] + sensors.extend(['audio_connections', 'video_connections']) + + if motion is None: + motion = 'motion_active' in cam.enabled_sensors + + async def async_update_data(now): + """Update data from IP camera in SCAN_INTERVAL.""" + await cam.update() + async_dispatcher_send(hass, SIGNAL_UPDATE_DATA, host) + + async_track_point_in_utc_time( + hass, async_update_data, utcnow() + interval) + + await async_update_data(None) + + # Load platforms + webcams[host] = cam + + mjpeg_camera = { + CONF_PLATFORM: 'mjpeg', + CONF_MJPEG_URL: cam.mjpeg_url, + CONF_STILL_IMAGE_URL: cam.image_url, + CONF_NAME: name, + } + if username and password: + mjpeg_camera.update({ + CONF_USERNAME: username, + CONF_PASSWORD: password + }) + + hass.async_create_task(discovery.async_load_platform( + hass, 'camera', 'mjpeg', mjpeg_camera, config)) + + if sensors: + hass.async_create_task(discovery.async_load_platform( + hass, 'sensor', DOMAIN, { + CONF_NAME: name, + CONF_HOST: host, + CONF_SENSORS: sensors, + }, config)) + + if switches: + hass.async_create_task(discovery.async_load_platform( + hass, 'switch', DOMAIN, { + CONF_NAME: name, + CONF_HOST: host, + CONF_SWITCHES: switches, + }, config)) + + if motion: + hass.async_create_task(discovery.async_load_platform( + hass, 'binary_sensor', DOMAIN, { + CONF_HOST: host, + CONF_NAME: name, + }, config)) + + tasks = [async_setup_ipcamera(conf) for conf in config[DOMAIN]] + if tasks: + await asyncio.wait(tasks, loop=hass.loop) + + return True + + +class AndroidIPCamEntity(Entity): + """The Android device running IP Webcam.""" + + def __init__(self, host, ipcam): + """Initialize the data object.""" + self._host = host + self._ipcam = ipcam + + async def async_added_to_hass(self): + """Register update dispatcher.""" + @callback + def async_ipcam_update(host): + """Update callback.""" + if self._host != host: + return + self.async_schedule_update_ha_state(True) + + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_DATA, async_ipcam_update) + + @property + def should_poll(self): + """Return True if entity has to be polled for state.""" + return False + + @property + def available(self): + """Return True if entity is available.""" + return self._ipcam.available + + @property + def device_state_attributes(self): + """Return the state attributes.""" + state_attr = {ATTR_HOST: self._host} + if self._ipcam.status_data is None: + return state_attr + + state_attr[ATTR_VID_CONNS] = \ + self._ipcam.status_data.get('video_connections') + state_attr[ATTR_AUD_CONNS] = \ + self._ipcam.status_data.get('audio_connections') + + return state_attr diff --git a/homeassistant/components/android_ip_webcam/binary_sensor.py b/homeassistant/components/android_ip_webcam/binary_sensor.py new file mode 100644 index 0000000000000..e33e22f3778ac --- /dev/null +++ b/homeassistant/components/android_ip_webcam/binary_sensor.py @@ -0,0 +1,54 @@ +"""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) + +DEPENDENCIES = ['android_ip_webcam'] + + +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 + + host = discovery_info[CONF_HOST] + name = discovery_info[CONF_NAME] + ipcam = hass.data[DATA_IP_WEBCAM][host] + + async_add_entities( + [IPWebcamBinarySensor(name, host, ipcam, 'motion_active')], True) + + +class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorDevice): + """Representation of an IP Webcam binary sensor.""" + + def __init__(self, name, host, ipcam, sensor): + """Initialize the binary sensor.""" + super().__init__(host, ipcam) + + self._sensor = sensor + self._mapped_name = KEY_MAP.get(self._sensor, self._sensor) + self._name = '{} {}'.format(name, self._mapped_name) + self._state = None + self._unit = None + + @property + def name(self): + """Return the name of the binary sensor, if any.""" + return self._name + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self._state + + async def async_update(self): + """Retrieve latest state.""" + state, _ = self._ipcam.export_sensor(self._sensor) + self._state = state == 1.0 + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return 'motion' diff --git a/homeassistant/components/android_ip_webcam/sensor.py b/homeassistant/components/android_ip_webcam/sensor.py new file mode 100644 index 0000000000000..e98ce7951b8aa --- /dev/null +++ b/homeassistant/components/android_ip_webcam/sensor.py @@ -0,0 +1,72 @@ +"""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) +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): + """Set up the IP Webcam Sensor.""" + if discovery_info is None: + return + + host = discovery_info[CONF_HOST] + name = discovery_info[CONF_NAME] + sensors = discovery_info[CONF_SENSORS] + ipcam = hass.data[DATA_IP_WEBCAM][host] + + all_sensors = [] + + for sensor in sensors: + all_sensors.append(IPWebcamSensor(name, host, ipcam, sensor)) + + async_add_entities(all_sensors, True) + + +class IPWebcamSensor(AndroidIPCamEntity): + """Representation of a IP Webcam sensor.""" + + def __init__(self, name, host, ipcam, sensor): + """Initialize the sensor.""" + super().__init__(host, ipcam) + + self._sensor = sensor + self._mapped_name = KEY_MAP.get(self._sensor, self._sensor) + self._name = '{} {}'.format(name, self._mapped_name) + self._state = None + self._unit = None + + @property + def name(self): + """Return the name of the sensor, if any.""" + return self._name + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + async def async_update(self): + """Retrieve latest state.""" + if self._sensor in ('audio_connections', 'video_connections'): + if not self._ipcam.status_data: + return + self._state = self._ipcam.status_data.get(self._sensor) + self._unit = 'Connections' + else: + self._state, self._unit = self._ipcam.export_sensor(self._sensor) + + @property + def icon(self): + """Return the icon for the sensor.""" + if self._sensor == 'battery_level' and self._state is not None: + return icon_for_battery_level(int(self._state)) + return ICON_MAP.get(self._sensor, 'mdi:eye') diff --git a/homeassistant/components/android_ip_webcam/switch.py b/homeassistant/components/android_ip_webcam/switch.py new file mode 100644 index 0000000000000..73a94acbcdd9e --- /dev/null +++ b/homeassistant/components/android_ip_webcam/switch.py @@ -0,0 +1,84 @@ +"""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, + CONF_NAME, CONF_SWITCHES) + +DEPENDENCIES = ['android_ip_webcam'] + + +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 + + host = discovery_info[CONF_HOST] + name = discovery_info[CONF_NAME] + switches = discovery_info[CONF_SWITCHES] + ipcam = hass.data[DATA_IP_WEBCAM][host] + + all_switches = [] + + for setting in switches: + all_switches.append(IPWebcamSettingsSwitch(name, host, ipcam, setting)) + + async_add_entities(all_switches, True) + + +class IPWebcamSettingsSwitch(AndroidIPCamEntity, SwitchDevice): + """An abstract class for an IP Webcam setting.""" + + def __init__(self, name, host, ipcam, setting): + """Initialize the settings switch.""" + super().__init__(host, ipcam) + + self._setting = setting + self._mapped_name = KEY_MAP.get(self._setting, self._setting) + self._name = '{} {}'.format(name, self._mapped_name) + self._state = False + + @property + def name(self): + """Return the name of the node.""" + return self._name + + async def async_update(self): + """Get the updated status of the switch.""" + self._state = bool(self._ipcam.current_settings.get(self._setting)) + + @property + def is_on(self): + """Return the boolean response if the node is on.""" + return self._state + + async def async_turn_on(self, **kwargs): + """Turn device on.""" + if self._setting == 'torch': + await self._ipcam.torch(activate=True) + elif self._setting == 'focus': + await self._ipcam.focus(activate=True) + elif self._setting == 'video_recording': + await self._ipcam.record(record=True) + else: + await self._ipcam.change_setting(self._setting, True) + self._state = True + self.async_schedule_update_ha_state() + + async def async_turn_off(self, **kwargs): + """Turn device off.""" + if self._setting == 'torch': + await self._ipcam.torch(activate=False) + elif self._setting == 'focus': + await self._ipcam.focus(activate=False) + elif self._setting == 'video_recording': + await self._ipcam.record(record=False) + else: + await self._ipcam.change_setting(self._setting, False) + self._state = False + self.async_schedule_update_ha_state() + + @property + def icon(self): + """Return the icon for the switch.""" + return ICON_MAP.get(self._setting, 'mdi:flash') diff --git a/homeassistant/components/apcupsd.py b/homeassistant/components/apcupsd.py deleted file mode 100644 index 79b8837816941..0000000000000 --- a/homeassistant/components/apcupsd.py +++ /dev/null @@ -1,89 +0,0 @@ -""" -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/ -""" -import logging -from datetime import timedelta - -import voluptuous as vol - -from homeassistant.const import (CONF_HOST, CONF_PORT) -import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle - -REQUIREMENTS = ['apcaccess==0.0.13'] - -_LOGGER = logging.getLogger(__name__) - -CONF_TYPE = 'type' - -DATA = None -DEFAULT_HOST = 'localhost' -DEFAULT_PORT = 3551 -DOMAIN = 'apcupsd' - -KEY_STATUS = 'STATUS' - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) - -VALUE_ONLINE = 'ONLINE' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - }), -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Use config values to set up a function enabling status retrieval.""" - global DATA - conf = config[DOMAIN] - host = conf.get(CONF_HOST) - port = conf.get(CONF_PORT) - - DATA = APCUPSdData(host, port) - - # It doesn't really matter why we're not able to get the status, just that - # we can't. - try: - DATA.update(no_throttle=True) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Failure while testing APCUPSd status retrieval.") - return False - return True - - -class APCUPSdData: - """Stores the data retrieved from APCUPSd. - - For each entity to use, acts as the single point responsible for fetching - updates from the server. - """ - - def __init__(self, host, port): - """Initialize the data object.""" - from apcaccess import status - self._host = host - self._port = port - self._status = None - self._get = status.get - self._parse = status.parse - - @property - def status(self): - """Get latest update if throttle allows. Return status.""" - self.update() - return self._status - - def _get_status(self): - """Get the status from APCUPSd and parse it into a dict.""" - return self._parse(self._get(host=self._host, port=self._port)) - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self, **kwargs): - """Fetch the latest status from APCUPSd.""" - self._status = self._get_status() diff --git a/homeassistant/components/apcupsd/__init__.py b/homeassistant/components/apcupsd/__init__.py new file mode 100644 index 0000000000000..aab6f6dda018c --- /dev/null +++ b/homeassistant/components/apcupsd/__init__.py @@ -0,0 +1,84 @@ +"""Support for APCUPSd via its Network Information Server (NIS).""" +import logging +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.const import (CONF_HOST, CONF_PORT) +import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle + +REQUIREMENTS = ['apcaccess==0.0.13'] + +_LOGGER = logging.getLogger(__name__) + +CONF_TYPE = 'type' + +DATA = None +DEFAULT_HOST = 'localhost' +DEFAULT_PORT = 3551 +DOMAIN = 'apcupsd' + +KEY_STATUS = 'STATUS' + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) + +VALUE_ONLINE = 'ONLINE' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Use config values to set up a function enabling status retrieval.""" + global DATA + conf = config[DOMAIN] + host = conf.get(CONF_HOST) + port = conf.get(CONF_PORT) + + DATA = APCUPSdData(host, port) + + # It doesn't really matter why we're not able to get the status, just that + # we can't. + try: + DATA.update(no_throttle=True) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Failure while testing APCUPSd status retrieval.") + return False + return True + + +class APCUPSdData: + """Stores the data retrieved from APCUPSd. + + For each entity to use, acts as the single point responsible for fetching + updates from the server. + """ + + def __init__(self, host, port): + """Initialize the data object.""" + from apcaccess import status + self._host = host + self._port = port + self._status = None + self._get = status.get + self._parse = status.parse + + @property + def status(self): + """Get latest update if throttle allows. Return status.""" + self.update() + return self._status + + def _get_status(self): + """Get the status from APCUPSd and parse it into a dict.""" + return self._parse(self._get(host=self._host, port=self._port)) + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self, **kwargs): + """Fetch the latest status from APCUPSd.""" + self._status = self._get_status() diff --git a/homeassistant/components/apcupsd/binary_sensor.py b/homeassistant/components/apcupsd/binary_sensor.py new file mode 100644 index 0000000000000..445dab9b0744a --- /dev/null +++ b/homeassistant/components/apcupsd/binary_sensor.py @@ -0,0 +1,44 @@ +"""Support for tracking the online status of a UPS.""" +import voluptuous as vol + +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA) +from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv +from homeassistant.components import apcupsd + +DEFAULT_NAME = 'UPS Online Status' +DEPENDENCIES = [apcupsd.DOMAIN] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up an APCUPSd Online Status binary sensor.""" + add_entities([OnlineStatus(config, apcupsd.DATA)], True) + + +class OnlineStatus(BinarySensorDevice): + """Representation of an UPS online status.""" + + def __init__(self, config, data): + """Initialize the APCUPSd binary device.""" + self._config = config + self._data = data + self._state = None + + @property + def name(self): + """Return the name of the UPS online status sensor.""" + return self._config.get(CONF_NAME) + + @property + def is_on(self): + """Return true if the UPS is online, else false.""" + return self._state == apcupsd.VALUE_ONLINE + + def update(self): + """Get the status report from APCUPSd and set this entity's state.""" + self._state = self._data.status[apcupsd.KEY_STATUS] diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py new file mode 100644 index 0000000000000..4ebe0ac8aaf50 --- /dev/null +++ b/homeassistant/components/apcupsd/sensor.py @@ -0,0 +1,183 @@ +"""Support for APCUPSd sensors.""" +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +import homeassistant.helpers.config_validation as cv +from homeassistant.components import apcupsd +from homeassistant.const import (TEMP_CELSIUS, CONF_RESOURCES) +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = [apcupsd.DOMAIN] + +SENSOR_PREFIX = 'UPS ' +SENSOR_TYPES = { + 'alarmdel': ['Alarm Delay', '', 'mdi:alarm'], + 'ambtemp': ['Ambient Temperature', '', 'mdi:thermometer'], + 'apc': ['Status Data', '', 'mdi:information-outline'], + 'apcmodel': ['Model', '', 'mdi:information-outline'], + 'badbatts': ['Bad Batteries', '', 'mdi:information-outline'], + 'battdate': ['Battery Replaced', '', 'mdi:calendar-clock'], + 'battstat': ['Battery Status', '', 'mdi:information-outline'], + 'battv': ['Battery Voltage', 'V', 'mdi:flash'], + 'bcharge': ['Battery', '%', 'mdi:battery'], + 'cable': ['Cable Type', '', 'mdi:ethernet-cable'], + 'cumonbatt': ['Total Time on Battery', '', 'mdi:timer'], + 'date': ['Status Date', '', 'mdi:calendar-clock'], + 'dipsw': ['Dip Switch Settings', '', 'mdi:information-outline'], + 'dlowbatt': ['Low Battery Signal', '', 'mdi:clock-alert'], + 'driver': ['Driver', '', 'mdi:information-outline'], + 'dshutd': ['Shutdown Delay', '', 'mdi:timer'], + 'dwake': ['Wake Delay', '', 'mdi:timer'], + 'endapc': ['Date and Time', '', 'mdi:calendar-clock'], + 'extbatts': ['External Batteries', '', 'mdi:information-outline'], + 'firmware': ['Firmware Version', '', 'mdi:information-outline'], + 'hitrans': ['Transfer High', 'V', 'mdi:flash'], + 'hostname': ['Hostname', '', 'mdi:information-outline'], + 'humidity': ['Ambient Humidity', '%', 'mdi:water-percent'], + 'itemp': ['Internal Temperature', TEMP_CELSIUS, 'mdi:thermometer'], + 'lastxfer': ['Last Transfer', '', 'mdi:transfer'], + 'linefail': ['Input Voltage Status', '', 'mdi:information-outline'], + 'linefreq': ['Line Frequency', 'Hz', 'mdi:information-outline'], + 'linev': ['Input Voltage', 'V', 'mdi:flash'], + 'loadpct': ['Load', '%', 'mdi:gauge'], + 'loadapnt': ['Load Apparent Power', '%', 'mdi:gauge'], + 'lotrans': ['Transfer Low', 'V', 'mdi:flash'], + 'mandate': ['Manufacture Date', '', 'mdi:calendar'], + 'masterupd': ['Master Update', '', 'mdi:information-outline'], + 'maxlinev': ['Input Voltage High', 'V', 'mdi:flash'], + 'maxtime': ['Battery Timeout', '', 'mdi:timer-off'], + 'mbattchg': ['Battery Shutdown', '%', 'mdi:battery-alert'], + 'minlinev': ['Input Voltage Low', 'V', 'mdi:flash'], + 'mintimel': ['Shutdown Time', '', 'mdi:timer'], + 'model': ['Model', '', 'mdi:information-outline'], + 'nombattv': ['Battery Nominal Voltage', 'V', 'mdi:flash'], + 'nominv': ['Nominal Input Voltage', 'V', 'mdi:flash'], + 'nomoutv': ['Nominal Output Voltage', 'V', 'mdi:flash'], + 'nompower': ['Nominal Output Power', 'W', 'mdi:flash'], + 'nomapnt': ['Nominal Apparent Power', 'VA', 'mdi:flash'], + 'numxfers': ['Transfer Count', '', 'mdi:counter'], + 'outcurnt': ['Output Current', 'A', 'mdi:flash'], + 'outputv': ['Output Voltage', 'V', 'mdi:flash'], + 'reg1': ['Register 1 Fault', '', 'mdi:information-outline'], + 'reg2': ['Register 2 Fault', '', 'mdi:information-outline'], + 'reg3': ['Register 3 Fault', '', 'mdi:information-outline'], + 'retpct': ['Restore Requirement', '%', 'mdi:battery-alert'], + 'selftest': ['Last Self Test', '', 'mdi:calendar-clock'], + 'sense': ['Sensitivity', '', 'mdi:information-outline'], + 'serialno': ['Serial Number', '', 'mdi:information-outline'], + 'starttime': ['Startup Time', '', 'mdi:calendar-clock'], + 'statflag': ['Status Flag', '', 'mdi:information-outline'], + 'status': ['Status', '', 'mdi:information-outline'], + 'stesti': ['Self Test Interval', '', 'mdi:information-outline'], + 'timeleft': ['Time Left', '', 'mdi:clock-alert'], + 'tonbatt': ['Time on Battery', '', 'mdi:timer'], + 'upsmode': ['Mode', '', 'mdi:information-outline'], + 'upsname': ['Name', '', 'mdi:information-outline'], + 'version': ['Daemon Info', '', 'mdi:information-outline'], + 'xoffbat': ['Transfer from Battery', '', 'mdi:transfer'], + 'xoffbatt': ['Transfer from Battery', '', 'mdi:transfer'], + 'xonbatt': ['Transfer to Battery', '', 'mdi:transfer'], +} + +SPECIFIC_UNITS = { + 'ITEMP': TEMP_CELSIUS +} +INFERRED_UNITS = { + ' Minutes': 'min', + ' Seconds': 'sec', + ' Percent': '%', + ' Volts': 'V', + ' Ampere': 'A', + ' Volt-Ampere': 'VA', + ' Watts': 'W', + ' Hz': 'Hz', + ' C': TEMP_CELSIUS, + ' Percent Load Capacity': '%', +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_RESOURCES, default=[]): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the APCUPSd sensors.""" + entities = [] + + for resource in config[CONF_RESOURCES]: + sensor_type = resource.lower() + + if sensor_type not in SENSOR_TYPES: + SENSOR_TYPES[sensor_type] = [ + sensor_type.title(), '', 'mdi:information-outline'] + + if sensor_type.upper() not in apcupsd.DATA.status: + _LOGGER.warning( + "Sensor type: %s does not appear in the APCUPSd status output", + sensor_type) + + entities.append(APCUPSdSensor(apcupsd.DATA, sensor_type)) + + add_entities(entities, True) + + +def infer_unit(value): + """If the value ends with any of the units from ALL_UNITS. + + Split the unit off the end of the value and return the value, unit tuple + pair. Else return the original value and None as the unit. + """ + from apcaccess.status import ALL_UNITS + for unit in ALL_UNITS: + if value.endswith(unit): + return value[:-len(unit)], INFERRED_UNITS.get(unit, unit.strip()) + return value, None + + +class APCUPSdSensor(Entity): + """Representation of a sensor entity for APCUPSd status values.""" + + def __init__(self, data, sensor_type): + """Initialize the sensor.""" + self._data = data + self.type = sensor_type + self._name = SENSOR_PREFIX + SENSOR_TYPES[sensor_type][0] + self._unit = SENSOR_TYPES[sensor_type][1] + self._inferred_unit = None + self._state = None + + @property + def name(self): + """Return the name of the UPS sensor.""" + return self._name + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return SENSOR_TYPES[self.type][2] + + @property + def state(self): + """Return true if the UPS is online, else False.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + if not self._unit: + return self._inferred_unit + return self._unit + + def update(self): + """Get the latest status and use it to update our sensor state.""" + if self.type.upper() not in self._data.status: + self._state = None + self._inferred_unit = None + else: + self._state, self._inferred_unit = infer_unit( + self._data.status[self.type.upper()]) diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py deleted file mode 100644 index 961350bfa8977..0000000000000 --- a/homeassistant/components/api.py +++ /dev/null @@ -1,408 +0,0 @@ -""" -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 -""" -import asyncio -import json -import logging - -from aiohttp import web -from aiohttp.web_exceptions import HTTPBadRequest -import async_timeout -import voluptuous as vol - -from homeassistant.bootstrap import DATA_LOGGING -from homeassistant.components.http import HomeAssistantView -from homeassistant.const import ( - EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, HTTP_BAD_REQUEST, - HTTP_CREATED, HTTP_NOT_FOUND, MATCH_ALL, URL_API, URL_API_COMPONENTS, - URL_API_CONFIG, URL_API_DISCOVERY_INFO, URL_API_ERROR_LOG, URL_API_EVENTS, - URL_API_SERVICES, URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, - URL_API_TEMPLATE, __version__) -import homeassistant.core as ha -from homeassistant.auth.permissions.const import POLICY_READ -from homeassistant.exceptions import ( - TemplateError, Unauthorized, ServiceNotFound) -from homeassistant.helpers import template -from homeassistant.helpers.service import async_get_all_descriptions -from homeassistant.helpers.state import AsyncTrackStates -from homeassistant.helpers.json import JSONEncoder - -_LOGGER = logging.getLogger(__name__) - -ATTR_BASE_URL = 'base_url' -ATTR_LOCATION_NAME = 'location_name' -ATTR_REQUIRES_API_PASSWORD = 'requires_api_password' -ATTR_VERSION = 'version' - -DOMAIN = 'api' -DEPENDENCIES = ['http'] - -STREAM_PING_PAYLOAD = 'ping' -STREAM_PING_INTERVAL = 50 # seconds - - -def setup(hass, config): - """Register the API with the HTTP interface.""" - hass.http.register_view(APIStatusView) - hass.http.register_view(APIEventStream) - hass.http.register_view(APIConfigView) - hass.http.register_view(APIDiscoveryView) - hass.http.register_view(APIStatesView) - hass.http.register_view(APIEntityStateView) - hass.http.register_view(APIEventListenersView) - hass.http.register_view(APIEventView) - hass.http.register_view(APIServicesView) - hass.http.register_view(APIDomainServicesView) - hass.http.register_view(APIComponentsView) - hass.http.register_view(APITemplateView) - - if DATA_LOGGING in hass.data: - hass.http.register_view(APIErrorLog) - - return True - - -class APIStatusView(HomeAssistantView): - """View to handle Status requests.""" - - url = URL_API - name = 'api:status' - - @ha.callback - def get(self, request): - """Retrieve if API is running.""" - return self.json_message("API running.") - - -class APIEventStream(HomeAssistantView): - """View to handle EventStream requests.""" - - url = URL_API_STREAM - name = 'api:stream' - - async def get(self, request): - """Provide a streaming interface for the event bus.""" - if not request['hass_user'].is_admin: - raise Unauthorized() - hass = request.app['hass'] - stop_obj = object() - to_write = asyncio.Queue(loop=hass.loop) - - restrict = request.query.get('restrict') - if restrict: - restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP] - - async def forward_events(event): - """Forward events to the open request.""" - if event.event_type == EVENT_TIME_CHANGED: - return - - if restrict and event.event_type not in restrict: - return - - _LOGGER.debug("STREAM %s FORWARDING %s", id(stop_obj), event) - - if event.event_type == EVENT_HOMEASSISTANT_STOP: - data = stop_obj - else: - data = json.dumps(event, cls=JSONEncoder) - - await to_write.put(data) - - response = web.StreamResponse() - response.content_type = 'text/event-stream' - await response.prepare(request) - - unsub_stream = hass.bus.async_listen(MATCH_ALL, forward_events) - - try: - _LOGGER.debug("STREAM %s ATTACHED", id(stop_obj)) - - # Fire off one message so browsers fire open event right away - await to_write.put(STREAM_PING_PAYLOAD) - - while True: - try: - with async_timeout.timeout(STREAM_PING_INTERVAL, - loop=hass.loop): - payload = await to_write.get() - - if payload is stop_obj: - break - - msg = "data: {}\n\n".format(payload) - _LOGGER.debug( - "STREAM %s WRITING %s", id(stop_obj), msg.strip()) - await response.write(msg.encode('UTF-8')) - except asyncio.TimeoutError: - await to_write.put(STREAM_PING_PAYLOAD) - - except asyncio.CancelledError: - _LOGGER.debug("STREAM %s ABORT", id(stop_obj)) - - finally: - _LOGGER.debug("STREAM %s RESPONSE CLOSED", id(stop_obj)) - unsub_stream() - - return response - - -class APIConfigView(HomeAssistantView): - """View to handle Configuration requests.""" - - url = URL_API_CONFIG - name = 'api:config' - - @ha.callback - def get(self, request): - """Get current configuration.""" - return self.json(request.app['hass'].config.as_dict()) - - -class APIDiscoveryView(HomeAssistantView): - """View to provide Discovery information.""" - - requires_auth = False - url = URL_API_DISCOVERY_INFO - name = 'api:discovery' - - @ha.callback - def get(self, request): - """Get discovery information.""" - hass = request.app['hass'] - needs_auth = hass.config.api.api_password is not None - return self.json({ - ATTR_BASE_URL: hass.config.api.base_url, - ATTR_LOCATION_NAME: hass.config.location_name, - ATTR_REQUIRES_API_PASSWORD: needs_auth, - ATTR_VERSION: __version__, - }) - - -class APIStatesView(HomeAssistantView): - """View to handle States requests.""" - - url = URL_API_STATES - name = "api:states" - - @ha.callback - def get(self, request): - """Get current states.""" - user = request['hass_user'] - entity_perm = user.permissions.check_entity - states = [ - state for state in request.app['hass'].states.async_all() - if entity_perm(state.entity_id, 'read') - ] - return self.json(states) - - -class APIEntityStateView(HomeAssistantView): - """View to handle EntityState requests.""" - - url = '/api/states/{entity_id}' - name = 'api:entity-state' - - @ha.callback - def get(self, request, entity_id): - """Retrieve state of entity.""" - user = request['hass_user'] - if not user.permissions.check_entity(entity_id, POLICY_READ): - raise Unauthorized(entity_id=entity_id) - - state = request.app['hass'].states.get(entity_id) - if state: - return self.json(state) - return self.json_message("Entity not found.", HTTP_NOT_FOUND) - - async def post(self, request, entity_id): - """Update state of entity.""" - if not request['hass_user'].is_admin: - raise Unauthorized(entity_id=entity_id) - hass = request.app['hass'] - try: - data = await request.json() - except ValueError: - return self.json_message( - "Invalid JSON specified.", HTTP_BAD_REQUEST) - - new_state = data.get('state') - - if new_state is None: - return self.json_message("No state specified.", HTTP_BAD_REQUEST) - - attributes = data.get('attributes') - force_update = data.get('force_update', False) - - is_new_state = hass.states.get(entity_id) is None - - # Write state - hass.states.async_set(entity_id, new_state, attributes, force_update, - self.context(request)) - - # Read the state back for our response - status_code = HTTP_CREATED if is_new_state else 200 - resp = self.json(hass.states.get(entity_id), status_code) - - resp.headers.add('Location', URL_API_STATES_ENTITY.format(entity_id)) - - return resp - - @ha.callback - def delete(self, request, entity_id): - """Remove entity.""" - if not request['hass_user'].is_admin: - raise Unauthorized(entity_id=entity_id) - if request.app['hass'].states.async_remove(entity_id): - return self.json_message("Entity removed.") - return self.json_message("Entity not found.", HTTP_NOT_FOUND) - - -class APIEventListenersView(HomeAssistantView): - """View to handle EventListeners requests.""" - - url = URL_API_EVENTS - name = 'api:event-listeners' - - @ha.callback - def get(self, request): - """Get event listeners.""" - return self.json(async_events_json(request.app['hass'])) - - -class APIEventView(HomeAssistantView): - """View to handle Event requests.""" - - url = '/api/events/{event_type}' - name = 'api:event' - - async def post(self, request, event_type): - """Fire events.""" - if not request['hass_user'].is_admin: - raise Unauthorized() - body = await request.text() - try: - event_data = json.loads(body) if body else None - except ValueError: - return self.json_message( - "Event data should be valid JSON.", HTTP_BAD_REQUEST) - - if event_data is not None and not isinstance(event_data, dict): - return self.json_message( - "Event data should be a JSON object", HTTP_BAD_REQUEST) - - # Special case handling for event STATE_CHANGED - # We will try to convert state dicts back to State objects - if event_type == ha.EVENT_STATE_CHANGED and event_data: - for key in ('old_state', 'new_state'): - state = ha.State.from_dict(event_data.get(key)) - - if state: - event_data[key] = state - - request.app['hass'].bus.async_fire( - event_type, event_data, ha.EventOrigin.remote, - self.context(request)) - - return self.json_message("Event {} fired.".format(event_type)) - - -class APIServicesView(HomeAssistantView): - """View to handle Services requests.""" - - url = URL_API_SERVICES - name = 'api:services' - - async def get(self, request): - """Get registered services.""" - services = await async_services_json(request.app['hass']) - return self.json(services) - - -class APIDomainServicesView(HomeAssistantView): - """View to handle DomainServices requests.""" - - url = '/api/services/{domain}/{service}' - name = 'api:domain-services' - - async def post(self, request, domain, service): - """Call a service. - - Returns a list of changed states. - """ - hass = request.app['hass'] - body = await request.text() - try: - data = json.loads(body) if body else None - except ValueError: - return self.json_message( - "Data should be valid JSON.", HTTP_BAD_REQUEST) - - with AsyncTrackStates(hass) as changed_states: - try: - await hass.services.async_call( - domain, service, data, True, self.context(request)) - except (vol.Invalid, ServiceNotFound): - raise HTTPBadRequest() - - return self.json(changed_states) - - -class APIComponentsView(HomeAssistantView): - """View to handle Components requests.""" - - url = URL_API_COMPONENTS - name = 'api:components' - - @ha.callback - def get(self, request): - """Get current loaded components.""" - return self.json(request.app['hass'].config.components) - - -class APITemplateView(HomeAssistantView): - """View to handle Template requests.""" - - url = URL_API_TEMPLATE - name = 'api:template' - - async def post(self, request): - """Render a template.""" - if not request['hass_user'].is_admin: - raise Unauthorized() - try: - data = await request.json() - tpl = template.Template(data['template'], request.app['hass']) - return tpl.async_render(data.get('variables')) - except (ValueError, TemplateError) as ex: - return self.json_message( - "Error rendering template: {}".format(ex), HTTP_BAD_REQUEST) - - -class APIErrorLog(HomeAssistantView): - """View to fetch the API error log.""" - - url = URL_API_ERROR_LOG - name = 'api:error_log' - - async def get(self, request): - """Retrieve API error log.""" - if not request['hass_user'].is_admin: - raise Unauthorized() - return web.FileResponse(request.app['hass'].data[DATA_LOGGING]) - - -async def async_services_json(hass): - """Generate services data to JSONify.""" - descriptions = await async_get_all_descriptions(hass) - return [{'domain': key, 'services': value} - for key, value in descriptions.items()] - - -def async_events_json(hass): - """Generate event data to JSONify.""" - return [{'event': key, 'listener_count': value} - for key, value in hass.bus.async_listeners().items()] diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py new file mode 100644 index 0000000000000..7639ac621feea --- /dev/null +++ b/homeassistant/components/api/__init__.py @@ -0,0 +1,403 @@ +"""Rest API for Home Assistant.""" +import asyncio +import json +import logging + +from aiohttp import web +from aiohttp.web_exceptions import HTTPBadRequest +import async_timeout +import voluptuous as vol + +from homeassistant.bootstrap import DATA_LOGGING +from homeassistant.components.http import HomeAssistantView +from homeassistant.const import ( + EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, HTTP_BAD_REQUEST, + HTTP_CREATED, HTTP_NOT_FOUND, MATCH_ALL, URL_API, URL_API_COMPONENTS, + URL_API_CONFIG, URL_API_DISCOVERY_INFO, URL_API_ERROR_LOG, URL_API_EVENTS, + URL_API_SERVICES, URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, + URL_API_TEMPLATE, __version__) +import homeassistant.core as ha +from homeassistant.auth.permissions.const import POLICY_READ +from homeassistant.exceptions import ( + TemplateError, Unauthorized, ServiceNotFound) +from homeassistant.helpers import template +from homeassistant.helpers.service import async_get_all_descriptions +from homeassistant.helpers.state import AsyncTrackStates +from homeassistant.helpers.json import JSONEncoder + +_LOGGER = logging.getLogger(__name__) + +ATTR_BASE_URL = 'base_url' +ATTR_LOCATION_NAME = 'location_name' +ATTR_REQUIRES_API_PASSWORD = 'requires_api_password' +ATTR_VERSION = 'version' + +DOMAIN = 'api' +DEPENDENCIES = ['http'] + +STREAM_PING_PAYLOAD = 'ping' +STREAM_PING_INTERVAL = 50 # seconds + + +def setup(hass, config): + """Register the API with the HTTP interface.""" + hass.http.register_view(APIStatusView) + hass.http.register_view(APIEventStream) + hass.http.register_view(APIConfigView) + hass.http.register_view(APIDiscoveryView) + hass.http.register_view(APIStatesView) + hass.http.register_view(APIEntityStateView) + hass.http.register_view(APIEventListenersView) + hass.http.register_view(APIEventView) + hass.http.register_view(APIServicesView) + hass.http.register_view(APIDomainServicesView) + hass.http.register_view(APIComponentsView) + hass.http.register_view(APITemplateView) + + if DATA_LOGGING in hass.data: + hass.http.register_view(APIErrorLog) + + return True + + +class APIStatusView(HomeAssistantView): + """View to handle Status requests.""" + + url = URL_API + name = 'api:status' + + @ha.callback + def get(self, request): + """Retrieve if API is running.""" + return self.json_message("API running.") + + +class APIEventStream(HomeAssistantView): + """View to handle EventStream requests.""" + + url = URL_API_STREAM + name = 'api:stream' + + async def get(self, request): + """Provide a streaming interface for the event bus.""" + if not request['hass_user'].is_admin: + raise Unauthorized() + hass = request.app['hass'] + stop_obj = object() + to_write = asyncio.Queue(loop=hass.loop) + + restrict = request.query.get('restrict') + if restrict: + restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP] + + async def forward_events(event): + """Forward events to the open request.""" + if event.event_type == EVENT_TIME_CHANGED: + return + + if restrict and event.event_type not in restrict: + return + + _LOGGER.debug("STREAM %s FORWARDING %s", id(stop_obj), event) + + if event.event_type == EVENT_HOMEASSISTANT_STOP: + data = stop_obj + else: + data = json.dumps(event, cls=JSONEncoder) + + await to_write.put(data) + + response = web.StreamResponse() + response.content_type = 'text/event-stream' + await response.prepare(request) + + unsub_stream = hass.bus.async_listen(MATCH_ALL, forward_events) + + try: + _LOGGER.debug("STREAM %s ATTACHED", id(stop_obj)) + + # Fire off one message so browsers fire open event right away + await to_write.put(STREAM_PING_PAYLOAD) + + while True: + try: + with async_timeout.timeout(STREAM_PING_INTERVAL, + loop=hass.loop): + payload = await to_write.get() + + if payload is stop_obj: + break + + msg = "data: {}\n\n".format(payload) + _LOGGER.debug( + "STREAM %s WRITING %s", id(stop_obj), msg.strip()) + await response.write(msg.encode('UTF-8')) + except asyncio.TimeoutError: + await to_write.put(STREAM_PING_PAYLOAD) + + except asyncio.CancelledError: + _LOGGER.debug("STREAM %s ABORT", id(stop_obj)) + + finally: + _LOGGER.debug("STREAM %s RESPONSE CLOSED", id(stop_obj)) + unsub_stream() + + return response + + +class APIConfigView(HomeAssistantView): + """View to handle Configuration requests.""" + + url = URL_API_CONFIG + name = 'api:config' + + @ha.callback + def get(self, request): + """Get current configuration.""" + return self.json(request.app['hass'].config.as_dict()) + + +class APIDiscoveryView(HomeAssistantView): + """View to provide Discovery information.""" + + requires_auth = False + url = URL_API_DISCOVERY_INFO + name = 'api:discovery' + + @ha.callback + def get(self, request): + """Get discovery information.""" + hass = request.app['hass'] + needs_auth = hass.config.api.api_password is not None + return self.json({ + ATTR_BASE_URL: hass.config.api.base_url, + ATTR_LOCATION_NAME: hass.config.location_name, + ATTR_REQUIRES_API_PASSWORD: needs_auth, + ATTR_VERSION: __version__, + }) + + +class APIStatesView(HomeAssistantView): + """View to handle States requests.""" + + url = URL_API_STATES + name = "api:states" + + @ha.callback + def get(self, request): + """Get current states.""" + user = request['hass_user'] + entity_perm = user.permissions.check_entity + states = [ + state for state in request.app['hass'].states.async_all() + if entity_perm(state.entity_id, 'read') + ] + return self.json(states) + + +class APIEntityStateView(HomeAssistantView): + """View to handle EntityState requests.""" + + url = '/api/states/{entity_id}' + name = 'api:entity-state' + + @ha.callback + def get(self, request, entity_id): + """Retrieve state of entity.""" + user = request['hass_user'] + if not user.permissions.check_entity(entity_id, POLICY_READ): + raise Unauthorized(entity_id=entity_id) + + state = request.app['hass'].states.get(entity_id) + if state: + return self.json(state) + return self.json_message("Entity not found.", HTTP_NOT_FOUND) + + async def post(self, request, entity_id): + """Update state of entity.""" + if not request['hass_user'].is_admin: + raise Unauthorized(entity_id=entity_id) + hass = request.app['hass'] + try: + data = await request.json() + except ValueError: + return self.json_message( + "Invalid JSON specified.", HTTP_BAD_REQUEST) + + new_state = data.get('state') + + if new_state is None: + return self.json_message("No state specified.", HTTP_BAD_REQUEST) + + attributes = data.get('attributes') + force_update = data.get('force_update', False) + + is_new_state = hass.states.get(entity_id) is None + + # Write state + hass.states.async_set(entity_id, new_state, attributes, force_update, + self.context(request)) + + # Read the state back for our response + status_code = HTTP_CREATED if is_new_state else 200 + resp = self.json(hass.states.get(entity_id), status_code) + + resp.headers.add('Location', URL_API_STATES_ENTITY.format(entity_id)) + + return resp + + @ha.callback + def delete(self, request, entity_id): + """Remove entity.""" + if not request['hass_user'].is_admin: + raise Unauthorized(entity_id=entity_id) + if request.app['hass'].states.async_remove(entity_id): + return self.json_message("Entity removed.") + return self.json_message("Entity not found.", HTTP_NOT_FOUND) + + +class APIEventListenersView(HomeAssistantView): + """View to handle EventListeners requests.""" + + url = URL_API_EVENTS + name = 'api:event-listeners' + + @ha.callback + def get(self, request): + """Get event listeners.""" + return self.json(async_events_json(request.app['hass'])) + + +class APIEventView(HomeAssistantView): + """View to handle Event requests.""" + + url = '/api/events/{event_type}' + name = 'api:event' + + async def post(self, request, event_type): + """Fire events.""" + if not request['hass_user'].is_admin: + raise Unauthorized() + body = await request.text() + try: + event_data = json.loads(body) if body else None + except ValueError: + return self.json_message( + "Event data should be valid JSON.", HTTP_BAD_REQUEST) + + if event_data is not None and not isinstance(event_data, dict): + return self.json_message( + "Event data should be a JSON object", HTTP_BAD_REQUEST) + + # Special case handling for event STATE_CHANGED + # We will try to convert state dicts back to State objects + if event_type == ha.EVENT_STATE_CHANGED and event_data: + for key in ('old_state', 'new_state'): + state = ha.State.from_dict(event_data.get(key)) + + if state: + event_data[key] = state + + request.app['hass'].bus.async_fire( + event_type, event_data, ha.EventOrigin.remote, + self.context(request)) + + return self.json_message("Event {} fired.".format(event_type)) + + +class APIServicesView(HomeAssistantView): + """View to handle Services requests.""" + + url = URL_API_SERVICES + name = 'api:services' + + async def get(self, request): + """Get registered services.""" + services = await async_services_json(request.app['hass']) + return self.json(services) + + +class APIDomainServicesView(HomeAssistantView): + """View to handle DomainServices requests.""" + + url = '/api/services/{domain}/{service}' + name = 'api:domain-services' + + async def post(self, request, domain, service): + """Call a service. + + Returns a list of changed states. + """ + hass = request.app['hass'] + body = await request.text() + try: + data = json.loads(body) if body else None + except ValueError: + return self.json_message( + "Data should be valid JSON.", HTTP_BAD_REQUEST) + + with AsyncTrackStates(hass) as changed_states: + try: + await hass.services.async_call( + domain, service, data, True, self.context(request)) + except (vol.Invalid, ServiceNotFound): + raise HTTPBadRequest() + + return self.json(changed_states) + + +class APIComponentsView(HomeAssistantView): + """View to handle Components requests.""" + + url = URL_API_COMPONENTS + name = 'api:components' + + @ha.callback + def get(self, request): + """Get current loaded components.""" + return self.json(request.app['hass'].config.components) + + +class APITemplateView(HomeAssistantView): + """View to handle Template requests.""" + + url = URL_API_TEMPLATE + name = 'api:template' + + async def post(self, request): + """Render a template.""" + if not request['hass_user'].is_admin: + raise Unauthorized() + try: + data = await request.json() + tpl = template.Template(data['template'], request.app['hass']) + return tpl.async_render(data.get('variables')) + except (ValueError, TemplateError) as ex: + return self.json_message( + "Error rendering template: {}".format(ex), HTTP_BAD_REQUEST) + + +class APIErrorLog(HomeAssistantView): + """View to fetch the API error log.""" + + url = URL_API_ERROR_LOG + name = 'api:error_log' + + async def get(self, request): + """Retrieve API error log.""" + if not request['hass_user'].is_admin: + raise Unauthorized() + return web.FileResponse(request.app['hass'].data[DATA_LOGGING]) + + +async def async_services_json(hass): + """Generate services data to JSONify.""" + descriptions = await async_get_all_descriptions(hass) + return [{'domain': key, 'services': value} + for key, value in descriptions.items()] + + +def async_events_json(hass): + """Generate event data to JSONify.""" + return [{'event': key, 'listener_count': value} + for key, value in hass.bus.async_listeners().items()] diff --git a/homeassistant/components/apple_tv.py b/homeassistant/components/apple_tv.py deleted file mode 100644 index 73cabdfbae615..0000000000000 --- a/homeassistant/components/apple_tv.py +++ /dev/null @@ -1,255 +0,0 @@ -""" -Support for Apple TV. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/apple_tv/ -""" -import asyncio -import logging -from typing import Sequence, TypeVar, Union - -import voluptuous as vol - -from homeassistant.components.discovery import SERVICE_APPLE_TV -from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME -from homeassistant.helpers import discovery -from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv - -REQUIREMENTS = ['pyatv==0.3.12'] - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'apple_tv' - -SERVICE_SCAN = 'apple_tv_scan' -SERVICE_AUTHENTICATE = 'apple_tv_authenticate' - -ATTR_ATV = 'atv' -ATTR_POWER = 'power' - -CONF_LOGIN_ID = 'login_id' -CONF_START_OFF = 'start_off' -CONF_CREDENTIALS = 'credentials' - -DEFAULT_NAME = 'Apple TV' - -DATA_APPLE_TV = 'data_apple_tv' -DATA_ENTITIES = 'data_apple_tv_entities' - -KEY_CONFIG = 'apple_tv_configuring' - -NOTIFICATION_AUTH_ID = 'apple_tv_auth_notification' -NOTIFICATION_AUTH_TITLE = 'Apple TV Authentication' -NOTIFICATION_SCAN_ID = 'apple_tv_scan_notification' -NOTIFICATION_SCAN_TITLE = 'Apple TV Scan' - -T = TypeVar('T') # pylint: disable=invalid-name - - -# This version of ensure_list interprets an empty dict as no value -def ensure_list(value: Union[T, Sequence[T]]) -> Sequence[T]: - """Wrap value in list if it is not one.""" - if value is None or (isinstance(value, dict) and not value): - return [] - return value if isinstance(value, list) else [value] - - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.All(ensure_list, [vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_LOGIN_ID): cv.string, - vol.Optional(CONF_CREDENTIALS): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_START_OFF, default=False): cv.boolean, - })]) -}, extra=vol.ALLOW_EXTRA) - -# Currently no attributes but it might change later -APPLE_TV_SCAN_SCHEMA = vol.Schema({}) - -APPLE_TV_AUTHENTICATE_SCHEMA = vol.Schema({ - ATTR_ENTITY_ID: cv.entity_ids, -}) - - -def request_configuration(hass, config, atv, credentials): - """Request configuration steps from the user.""" - configurator = hass.components.configurator - - async def configuration_callback(callback_data): - """Handle the submitted configuration.""" - from pyatv import exceptions - pin = callback_data.get('pin') - - try: - await atv.airplay.finish_authentication(pin) - hass.components.persistent_notification.async_create( - 'Authentication succeeded!

Add the following ' - 'to credentials: in your apple_tv configuration:

' - '{0}'.format(credentials), - title=NOTIFICATION_AUTH_TITLE, - notification_id=NOTIFICATION_AUTH_ID) - except exceptions.DeviceAuthenticationError as ex: - hass.components.persistent_notification.async_create( - 'Authentication failed! Did you enter correct PIN?

' - 'Details: {0}'.format(ex), - title=NOTIFICATION_AUTH_TITLE, - notification_id=NOTIFICATION_AUTH_ID) - - hass.async_add_job(configurator.request_done, instance) - - instance = configurator.request_config( - 'Apple TV Authentication', configuration_callback, - description='Please enter PIN code shown on screen.', - submit_caption='Confirm', - fields=[{'id': 'pin', 'name': 'PIN Code', 'type': 'password'}] - ) - - -async def scan_for_apple_tvs(hass): - """Scan for devices and present a notification of the ones found.""" - import pyatv - atvs = await pyatv.scan_for_apple_tvs(hass.loop, timeout=3) - - devices = [] - for atv in atvs: - login_id = atv.login_id - if login_id is None: - login_id = 'Home Sharing disabled' - devices.append('Name: {0}
Host: {1}
Login ID: {2}'.format( - atv.name, atv.address, login_id)) - - if not devices: - devices = ['No device(s) found'] - - hass.components.persistent_notification.async_create( - 'The following devices were found:

' + - '

'.join(devices), - title=NOTIFICATION_SCAN_TITLE, - notification_id=NOTIFICATION_SCAN_ID) - - -async def async_setup(hass, config): - """Set up the Apple TV component.""" - if DATA_APPLE_TV not in hass.data: - hass.data[DATA_APPLE_TV] = {} - - async def async_service_handler(service): - """Handle service calls.""" - entity_ids = service.data.get(ATTR_ENTITY_ID) - - if service.service == SERVICE_SCAN: - hass.async_add_job(scan_for_apple_tvs, hass) - return - - if entity_ids: - devices = [device for device in hass.data[DATA_ENTITIES] - if device.entity_id in entity_ids] - else: - devices = hass.data[DATA_ENTITIES] - - for device in devices: - if service.service != SERVICE_AUTHENTICATE: - continue - - atv = device.atv - credentials = await atv.airplay.generate_credentials() - await atv.airplay.load_credentials(credentials) - _LOGGER.debug('Generated new credentials: %s', credentials) - await atv.airplay.start_authentication() - hass.async_add_job(request_configuration, - hass, config, atv, credentials) - - async def atv_discovered(service, info): - """Set up an Apple TV that was auto discovered.""" - await _setup_atv(hass, config, { - CONF_NAME: info['name'], - CONF_HOST: info['host'], - CONF_LOGIN_ID: info['properties']['hG'], - CONF_START_OFF: False - }) - - discovery.async_listen(hass, SERVICE_APPLE_TV, atv_discovered) - - tasks = [_setup_atv(hass, config, conf) for conf in config.get(DOMAIN, [])] - if tasks: - await asyncio.wait(tasks, loop=hass.loop) - - hass.services.async_register( - DOMAIN, SERVICE_SCAN, async_service_handler, - schema=APPLE_TV_SCAN_SCHEMA) - - hass.services.async_register( - DOMAIN, SERVICE_AUTHENTICATE, async_service_handler, - schema=APPLE_TV_AUTHENTICATE_SCHEMA) - - return True - - -async def _setup_atv(hass, hass_config, atv_config): - """Set up an Apple TV.""" - import pyatv - name = atv_config.get(CONF_NAME) - host = atv_config.get(CONF_HOST) - login_id = atv_config.get(CONF_LOGIN_ID) - start_off = atv_config.get(CONF_START_OFF) - credentials = atv_config.get(CONF_CREDENTIALS) - - if host in hass.data[DATA_APPLE_TV]: - return - - details = pyatv.AppleTVDevice(name, host, login_id) - session = async_get_clientsession(hass) - atv = pyatv.connect_to_apple_tv(details, hass.loop, session=session) - if credentials: - await atv.airplay.load_credentials(credentials) - - power = AppleTVPowerManager(hass, atv, start_off) - hass.data[DATA_APPLE_TV][host] = { - ATTR_ATV: atv, - ATTR_POWER: power - } - - hass.async_create_task(discovery.async_load_platform( - hass, 'media_player', DOMAIN, atv_config, hass_config)) - - hass.async_create_task(discovery.async_load_platform( - hass, 'remote', DOMAIN, atv_config, hass_config)) - - -class AppleTVPowerManager: - """Manager for global power management of an Apple TV. - - An instance is used per device to share the same power state between - several platforms. - """ - - def __init__(self, hass, atv, is_off): - """Initialize power manager.""" - self.hass = hass - self.atv = atv - self.listeners = [] - self._is_on = not is_off - - def init(self): - """Initialize power management.""" - if self._is_on: - self.atv.push_updater.start() - - @property - def turned_on(self): - """Return true if device is on or off.""" - return self._is_on - - def set_power_on(self, value): - """Change if a device is on or off.""" - if value != self._is_on: - self._is_on = value - if not self._is_on: - self.atv.push_updater.stop() - else: - self.atv.push_updater.start() - - for listener in self.listeners: - self.hass.async_create_task(listener.async_update_ha_state()) diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py new file mode 100644 index 0000000000000..b265dc533eb85 --- /dev/null +++ b/homeassistant/components/apple_tv/__init__.py @@ -0,0 +1,250 @@ +"""Support for Apple TV.""" +import asyncio +import logging +from typing import Sequence, TypeVar, Union + +import voluptuous as vol + +from homeassistant.components.discovery import SERVICE_APPLE_TV +from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME +from homeassistant.helpers import discovery +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['pyatv==0.3.12'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'apple_tv' + +SERVICE_SCAN = 'apple_tv_scan' +SERVICE_AUTHENTICATE = 'apple_tv_authenticate' + +ATTR_ATV = 'atv' +ATTR_POWER = 'power' + +CONF_LOGIN_ID = 'login_id' +CONF_START_OFF = 'start_off' +CONF_CREDENTIALS = 'credentials' + +DEFAULT_NAME = 'Apple TV' + +DATA_APPLE_TV = 'data_apple_tv' +DATA_ENTITIES = 'data_apple_tv_entities' + +KEY_CONFIG = 'apple_tv_configuring' + +NOTIFICATION_AUTH_ID = 'apple_tv_auth_notification' +NOTIFICATION_AUTH_TITLE = 'Apple TV Authentication' +NOTIFICATION_SCAN_ID = 'apple_tv_scan_notification' +NOTIFICATION_SCAN_TITLE = 'Apple TV Scan' + +T = TypeVar('T') # pylint: disable=invalid-name + + +# This version of ensure_list interprets an empty dict as no value +def ensure_list(value: Union[T, Sequence[T]]) -> Sequence[T]: + """Wrap value in list if it is not one.""" + if value is None or (isinstance(value, dict) and not value): + return [] + return value if isinstance(value, list) else [value] + + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All(ensure_list, [vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_LOGIN_ID): cv.string, + vol.Optional(CONF_CREDENTIALS): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_START_OFF, default=False): cv.boolean, + })]) +}, extra=vol.ALLOW_EXTRA) + +# Currently no attributes but it might change later +APPLE_TV_SCAN_SCHEMA = vol.Schema({}) + +APPLE_TV_AUTHENTICATE_SCHEMA = vol.Schema({ + ATTR_ENTITY_ID: cv.entity_ids, +}) + + +def request_configuration(hass, config, atv, credentials): + """Request configuration steps from the user.""" + configurator = hass.components.configurator + + async def configuration_callback(callback_data): + """Handle the submitted configuration.""" + from pyatv import exceptions + pin = callback_data.get('pin') + + try: + await atv.airplay.finish_authentication(pin) + hass.components.persistent_notification.async_create( + 'Authentication succeeded!

Add the following ' + 'to credentials: in your apple_tv configuration:

' + '{0}'.format(credentials), + title=NOTIFICATION_AUTH_TITLE, + notification_id=NOTIFICATION_AUTH_ID) + except exceptions.DeviceAuthenticationError as ex: + hass.components.persistent_notification.async_create( + 'Authentication failed! Did you enter correct PIN?

' + 'Details: {0}'.format(ex), + title=NOTIFICATION_AUTH_TITLE, + notification_id=NOTIFICATION_AUTH_ID) + + hass.async_add_job(configurator.request_done, instance) + + instance = configurator.request_config( + 'Apple TV Authentication', configuration_callback, + description='Please enter PIN code shown on screen.', + submit_caption='Confirm', + fields=[{'id': 'pin', 'name': 'PIN Code', 'type': 'password'}] + ) + + +async def scan_for_apple_tvs(hass): + """Scan for devices and present a notification of the ones found.""" + import pyatv + atvs = await pyatv.scan_for_apple_tvs(hass.loop, timeout=3) + + devices = [] + for atv in atvs: + login_id = atv.login_id + if login_id is None: + login_id = 'Home Sharing disabled' + devices.append('Name: {0}
Host: {1}
Login ID: {2}'.format( + atv.name, atv.address, login_id)) + + if not devices: + devices = ['No device(s) found'] + + hass.components.persistent_notification.async_create( + 'The following devices were found:

' + + '

'.join(devices), + title=NOTIFICATION_SCAN_TITLE, + notification_id=NOTIFICATION_SCAN_ID) + + +async def async_setup(hass, config): + """Set up the Apple TV component.""" + if DATA_APPLE_TV not in hass.data: + hass.data[DATA_APPLE_TV] = {} + + async def async_service_handler(service): + """Handle service calls.""" + entity_ids = service.data.get(ATTR_ENTITY_ID) + + if service.service == SERVICE_SCAN: + hass.async_add_job(scan_for_apple_tvs, hass) + return + + if entity_ids: + devices = [device for device in hass.data[DATA_ENTITIES] + if device.entity_id in entity_ids] + else: + devices = hass.data[DATA_ENTITIES] + + for device in devices: + if service.service != SERVICE_AUTHENTICATE: + continue + + atv = device.atv + credentials = await atv.airplay.generate_credentials() + await atv.airplay.load_credentials(credentials) + _LOGGER.debug('Generated new credentials: %s', credentials) + await atv.airplay.start_authentication() + hass.async_add_job(request_configuration, + hass, config, atv, credentials) + + async def atv_discovered(service, info): + """Set up an Apple TV that was auto discovered.""" + await _setup_atv(hass, config, { + CONF_NAME: info['name'], + CONF_HOST: info['host'], + CONF_LOGIN_ID: info['properties']['hG'], + CONF_START_OFF: False + }) + + discovery.async_listen(hass, SERVICE_APPLE_TV, atv_discovered) + + tasks = [_setup_atv(hass, config, conf) for conf in config.get(DOMAIN, [])] + if tasks: + await asyncio.wait(tasks, loop=hass.loop) + + hass.services.async_register( + DOMAIN, SERVICE_SCAN, async_service_handler, + schema=APPLE_TV_SCAN_SCHEMA) + + hass.services.async_register( + DOMAIN, SERVICE_AUTHENTICATE, async_service_handler, + schema=APPLE_TV_AUTHENTICATE_SCHEMA) + + return True + + +async def _setup_atv(hass, hass_config, atv_config): + """Set up an Apple TV.""" + import pyatv + name = atv_config.get(CONF_NAME) + host = atv_config.get(CONF_HOST) + login_id = atv_config.get(CONF_LOGIN_ID) + start_off = atv_config.get(CONF_START_OFF) + credentials = atv_config.get(CONF_CREDENTIALS) + + if host in hass.data[DATA_APPLE_TV]: + return + + details = pyatv.AppleTVDevice(name, host, login_id) + session = async_get_clientsession(hass) + atv = pyatv.connect_to_apple_tv(details, hass.loop, session=session) + if credentials: + await atv.airplay.load_credentials(credentials) + + power = AppleTVPowerManager(hass, atv, start_off) + hass.data[DATA_APPLE_TV][host] = { + ATTR_ATV: atv, + ATTR_POWER: power + } + + hass.async_create_task(discovery.async_load_platform( + hass, 'media_player', DOMAIN, atv_config, hass_config)) + + hass.async_create_task(discovery.async_load_platform( + hass, 'remote', DOMAIN, atv_config, hass_config)) + + +class AppleTVPowerManager: + """Manager for global power management of an Apple TV. + + An instance is used per device to share the same power state between + several platforms. + """ + + def __init__(self, hass, atv, is_off): + """Initialize power manager.""" + self.hass = hass + self.atv = atv + self.listeners = [] + self._is_on = not is_off + + def init(self): + """Initialize power management.""" + if self._is_on: + self.atv.push_updater.start() + + @property + def turned_on(self): + """Return true if device is on or off.""" + return self._is_on + + def set_power_on(self, value): + """Change if a device is on or off.""" + if value != self._is_on: + self._is_on = value + if not self._is_on: + self.atv.push_updater.stop() + else: + self.atv.push_updater.start() + + for listener in self.listeners: + self.hass.async_create_task(listener.async_update_ha_state()) diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py new file mode 100644 index 0000000000000..03ac5bd2549b9 --- /dev/null +++ b/homeassistant/components/apple_tv/media_player.py @@ -0,0 +1,261 @@ +"""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 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) +from homeassistant.const import ( + CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_OFF, + STATE_PAUSED, STATE_PLAYING, STATE_STANDBY) +from homeassistant.core import callback +import homeassistant.util.dt as dt_util + +DEPENDENCIES = ['apple_tv'] + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_APPLE_TV = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PLAY_MEDIA | \ + SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_SEEK | \ + SUPPORT_STOP | SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Set up the Apple TV platform.""" + if not discovery_info: + return + + # Manage entity cache for service handler + if DATA_ENTITIES not in hass.data: + hass.data[DATA_ENTITIES] = [] + + name = discovery_info[CONF_NAME] + host = discovery_info[CONF_HOST] + atv = hass.data[DATA_APPLE_TV][host][ATTR_ATV] + power = hass.data[DATA_APPLE_TV][host][ATTR_POWER] + entity = AppleTvDevice(atv, name, power) + + @callback + def on_hass_stop(event): + """Stop push updates when hass stops.""" + atv.push_updater.stop() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) + + if entity not in hass.data[DATA_ENTITIES]: + hass.data[DATA_ENTITIES].append(entity) + + async_add_entities([entity]) + + +class AppleTvDevice(MediaPlayerDevice): + """Representation of an Apple TV device.""" + + def __init__(self, atv, name, power): + """Initialize the Apple TV device.""" + self.atv = atv + self._name = name + self._playing = None + self._power = power + self._power.listeners.append(self) + self.atv.push_updater.listener = self + + async def async_added_to_hass(self): + """Handle when an entity is about to be added to Home Assistant.""" + self._power.init() + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def unique_id(self): + """Return a unique ID.""" + return self.atv.metadata.device_id + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def state(self): + """Return the state of the device.""" + if not self._power.turned_on: + return STATE_OFF + + if self._playing: + from pyatv import const + state = self._playing.play_state + if state in (const.PLAY_STATE_IDLE, const.PLAY_STATE_NO_MEDIA, + const.PLAY_STATE_LOADING): + return STATE_IDLE + if state == const.PLAY_STATE_PLAYING: + return STATE_PLAYING + if state in (const.PLAY_STATE_PAUSED, + const.PLAY_STATE_FAST_FORWARD, + const.PLAY_STATE_FAST_BACKWARD): + # Catch fast forward/backward here so "play" is default action + return STATE_PAUSED + return STATE_STANDBY # Bad or unknown state? + + @callback + def playstatus_update(self, updater, playing): + """Print what is currently playing when it changes.""" + self._playing = playing + self.async_schedule_update_ha_state() + + @callback + def playstatus_error(self, updater, exception): + """Inform about an error and restart push updates.""" + _LOGGER.warning('A %s error occurred: %s', + exception.__class__, exception) + + # This will wait 10 seconds before restarting push updates. If the + # connection continues to fail, it will flood the log (every 10 + # seconds) until it succeeds. A better approach should probably be + # implemented here later. + updater.start(initial_delay=10) + self._playing = None + self.async_schedule_update_ha_state() + + @property + def media_content_type(self): + """Content type of current playing media.""" + if self._playing: + from pyatv import const + media_type = self._playing.media_type + if media_type == const.MEDIA_TYPE_VIDEO: + return MEDIA_TYPE_VIDEO + if media_type == const.MEDIA_TYPE_MUSIC: + return MEDIA_TYPE_MUSIC + if media_type == const.MEDIA_TYPE_TV: + return MEDIA_TYPE_TVSHOW + + @property + def media_duration(self): + """Duration of current playing media in seconds.""" + if self._playing: + return self._playing.total_time + + @property + def media_position(self): + """Position of current playing media in seconds.""" + if self._playing: + return self._playing.position + + @property + def media_position_updated_at(self): + """Last valid time of media position.""" + state = self.state + if state in (STATE_PLAYING, STATE_PAUSED): + return dt_util.utcnow() + + async def async_play_media(self, media_type, media_id, **kwargs): + """Send the play_media command to the media player.""" + await self.atv.airplay.play_url(media_id) + + @property + def media_image_hash(self): + """Hash value for media image.""" + state = self.state + if self._playing and state not in [STATE_OFF, STATE_IDLE]: + return self._playing.hash + + async def async_get_media_image(self): + """Fetch media image of current playing image.""" + state = self.state + if self._playing and state not in [STATE_OFF, STATE_IDLE]: + return (await self.atv.metadata.artwork()), 'image/png' + + return None, None + + @property + def media_title(self): + """Title of current playing media.""" + if self._playing: + if self.state == STATE_IDLE: + return 'Nothing playing' + title = self._playing.title + return title if title else 'No title' + + return 'Establishing a connection to {0}...'.format(self._name) + + @property + def supported_features(self): + """Flag media player features that are supported.""" + return SUPPORT_APPLE_TV + + async def async_turn_on(self): + """Turn the media player on.""" + self._power.set_power_on(True) + + async def async_turn_off(self): + """Turn the media player off.""" + self._playing = None + self._power.set_power_on(False) + + def async_media_play_pause(self): + """Pause media on media player. + + This method must be run in the event loop and returns a coroutine. + """ + if self._playing: + state = self.state + if state == STATE_PAUSED: + return self.atv.remote_control.play() + if state == STATE_PLAYING: + return self.atv.remote_control.pause() + + def async_media_play(self): + """Play media. + + This method must be run in the event loop and returns a coroutine. + """ + if self._playing: + return self.atv.remote_control.play() + + def async_media_stop(self): + """Stop the media player. + + This method must be run in the event loop and returns a coroutine. + """ + if self._playing: + return self.atv.remote_control.stop() + + def async_media_pause(self): + """Pause the media player. + + This method must be run in the event loop and returns a coroutine. + """ + if self._playing: + return self.atv.remote_control.pause() + + def async_media_next_track(self): + """Send next track command. + + This method must be run in the event loop and returns a coroutine. + """ + if self._playing: + return self.atv.remote_control.next() + + def async_media_previous_track(self): + """Send previous track command. + + This method must be run in the event loop and returns a coroutine. + """ + if self._playing: + return self.atv.remote_control.previous() + + def async_media_seek(self, position): + """Send seek command. + + This method must be run in the event loop and returns a coroutine. + """ + if self._playing: + return self.atv.remote_control.set_position(position) diff --git a/homeassistant/components/apple_tv/remote.py b/homeassistant/components/apple_tv/remote.py new file mode 100644 index 0000000000000..2d80ded686116 --- /dev/null +++ b/homeassistant/components/apple_tv/remote.py @@ -0,0 +1,80 @@ +"""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): + """Set up the Apple TV remote platform.""" + if not discovery_info: + return + + name = discovery_info[CONF_NAME] + host = discovery_info[CONF_HOST] + atv = hass.data[DATA_APPLE_TV][host][ATTR_ATV] + power = hass.data[DATA_APPLE_TV][host][ATTR_POWER] + async_add_entities([AppleTVRemote(atv, power, name)]) + + +class AppleTVRemote(remote.RemoteDevice): + """Device that sends commands to an Apple TV.""" + + def __init__(self, atv, power, name): + """Initialize device.""" + self._atv = atv + self._name = name + self._power = power + self._power.listeners.append(self) + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def unique_id(self): + """Return a unique ID.""" + return self._atv.metadata.device_id + + @property + def is_on(self): + """Return true if device is on.""" + return self._power.turned_on + + @property + def should_poll(self): + """No polling needed for Apple TV.""" + return False + + async def async_turn_on(self, **kwargs): + """Turn the device on. + + This method is a coroutine. + """ + self._power.set_power_on(True) + + async def async_turn_off(self, **kwargs): + """Turn the device off. + + This method is a coroutine. + """ + self._power.set_power_on(False) + + def async_send_command(self, command, **kwargs): + """Send a command to one device. + + This method must be run in the event loop and returns a coroutine. + """ + # Send commands in specified order but schedule only one coroutine + async def _send_commands(): + for single_command in command: + if not hasattr(self._atv.remote_control, single_command): + continue + + await getattr(self._atv.remote_control, single_command)() + + return _send_commands() diff --git a/homeassistant/components/apple_tv/services.yaml b/homeassistant/components/apple_tv/services.yaml new file mode 100644 index 0000000000000..01e26a5630b76 --- /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.py deleted file mode 100644 index abb61d42ca38b..0000000000000 --- a/homeassistant/components/aqualogic.py +++ /dev/null @@ -1,95 +0,0 @@ -""" -Support for AquaLogic component. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/aqualogic/ -""" -from datetime import timedelta -import logging -import time -import threading - -import voluptuous as vol - -from homeassistant.const import (CONF_HOST, CONF_PORT, - EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP) -from homeassistant.helpers import config_validation as cv - -REQUIREMENTS = ["aqualogic==1.0"] - -_LOGGER = logging.getLogger(__name__) - -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 - }), -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up AquaLogic platform.""" - host = config[DOMAIN][CONF_HOST] - 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) - _LOGGER.debug("AquaLogicProcessor %s:%i initialized", host, port) - return True - - -class AquaLogicProcessor(threading.Thread): - """AquaLogic event processor thread.""" - - def __init__(self, hass, host, port): - """Initialize the data object.""" - super().__init__(daemon=True) - self._hass = hass - self._host = host - self._port = port - self._shutdown = False - self._panel = None - - def start_listen(self, event): - """Start event-processing thread.""" - _LOGGER.debug("Event processing thread started") - self.start() - - def shutdown(self, event): - """Signal shutdown of processing event.""" - _LOGGER.debug("Event processing signaled exit") - self._shutdown = True - - def data_changed(self, panel): - """Aqualogic data changed callback.""" - self._hass.helpers.dispatcher.dispatcher_send(UPDATE_TOPIC) - - def run(self): - """Event thread.""" - from aqualogic.core import AquaLogic - - while True: - self._panel = AquaLogic() - self._panel.connect(self._host, self._port) - self._panel.process(self.data_changed) - - if self._shutdown: - return - - _LOGGER.error("Connection to %s:%d lost", - self._host, self._port) - time.sleep(RECONNECT_INTERVAL.seconds) - - @property - def panel(self): - """Retrieve the AquaLogic object.""" - return self._panel diff --git a/homeassistant/components/aqualogic/__init__.py b/homeassistant/components/aqualogic/__init__.py new file mode 100644 index 0000000000000..a4f83b573f73b --- /dev/null +++ b/homeassistant/components/aqualogic/__init__.py @@ -0,0 +1,87 @@ +"""Support for AquaLogic devices.""" +from datetime import timedelta +import logging +import time +import threading + +import voluptuous as vol + +from homeassistant.const import (CONF_HOST, CONF_PORT, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP) +from homeassistant.helpers import config_validation as cv + +REQUIREMENTS = ["aqualogic==1.0"] + +_LOGGER = logging.getLogger(__name__) + +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, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up AquaLogic platform.""" + host = config[DOMAIN][CONF_HOST] + 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) + _LOGGER.debug("AquaLogicProcessor %s:%i initialized", host, port) + return True + + +class AquaLogicProcessor(threading.Thread): + """AquaLogic event processor thread.""" + + def __init__(self, hass, host, port): + """Initialize the data object.""" + super().__init__(daemon=True) + self._hass = hass + self._host = host + self._port = port + self._shutdown = False + self._panel = None + + def start_listen(self, event): + """Start event-processing thread.""" + _LOGGER.debug("Event processing thread started") + self.start() + + def shutdown(self, event): + """Signal shutdown of processing event.""" + _LOGGER.debug("Event processing signaled exit") + self._shutdown = True + + def data_changed(self, panel): + """Aqualogic data changed callback.""" + self._hass.helpers.dispatcher.dispatcher_send(UPDATE_TOPIC) + + def run(self): + """Event thread.""" + from aqualogic.core import AquaLogic + + while True: + self._panel = AquaLogic() + self._panel.connect(self._host, self._port) + self._panel.process(self.data_changed) + + if self._shutdown: + return + + _LOGGER.error("Connection to %s:%d lost", self._host, self._port) + time.sleep(RECONNECT_INTERVAL.seconds) + + @property + def panel(self): + """Retrieve the AquaLogic object.""" + return self._panel diff --git a/homeassistant/components/aqualogic/sensor.py b/homeassistant/components/aqualogic/sensor.py new file mode 100644 index 0000000000000..9e061ba91bfe8 --- /dev/null +++ b/homeassistant/components/aqualogic/sensor.py @@ -0,0 +1,106 @@ +"""Support for AquaLogic sensors.""" +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import (CONF_MONITORED_CONDITIONS, + TEMP_CELSIUS, TEMP_FAHRENHEIT) +from homeassistant.core import callback +from homeassistant.helpers.entity import Entity +import homeassistant.components.aqualogic as aq +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['aqualogic'] + +TEMP_UNITS = [TEMP_CELSIUS, TEMP_FAHRENHEIT] +PERCENT_UNITS = ['%', '%'] +SALT_UNITS = ['g/L', 'PPM'] +WATT_UNITS = ['W', 'W'] +NO_UNITS = [None, None] + +# sensor_type [ description, unit, icon ] +# sensor_type corresponds to property names in aqualogic.core.AquaLogic +SENSOR_TYPES = { + 'air_temp': ['Air Temperature', TEMP_UNITS, 'mdi:thermometer'], + 'pool_temp': ['Pool Temperature', TEMP_UNITS, 'mdi:oil-temperature'], + 'spa_temp': ['Spa Temperature', TEMP_UNITS, 'mdi:oil-temperature'], + 'pool_chlorinator': ['Pool Chlorinator', PERCENT_UNITS, 'mdi:gauge'], + 'spa_chlorinator': ['Spa Chlorinator', PERCENT_UNITS, 'mdi:gauge'], + 'salt_level': ['Salt Level', SALT_UNITS, 'mdi:gauge'], + 'pump_speed': ['Pump Speed', PERCENT_UNITS, 'mdi:speedometer'], + 'pump_power': ['Pump Power', WATT_UNITS, 'mdi:gauge'], + 'status': ['Status', NO_UNITS, 'mdi:alert'] +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]) +}) + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Set up the sensor platform.""" + sensors = [] + + processor = hass.data[aq.DOMAIN] + for sensor_type in config.get(CONF_MONITORED_CONDITIONS): + sensors.append(AquaLogicSensor(processor, sensor_type)) + + async_add_entities(sensors) + + +class AquaLogicSensor(Entity): + """Sensor implementation for the AquaLogic component.""" + + def __init__(self, processor, sensor_type): + """Initialize sensor.""" + self._processor = processor + self._type = sensor_type + self._state = None + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def name(self): + """Return the name of the sensor.""" + return "AquaLogic {}".format(SENSOR_TYPES[self._type][0]) + + @property + def unit_of_measurement(self): + """Return the unit of measurement the value is expressed in.""" + panel = self._processor.panel + if panel is None: + return None + if panel.is_metric: + return SENSOR_TYPES[self._type][1][0] + return SENSOR_TYPES[self._type][1][1] + + @property + def should_poll(self): + """Return the polling state.""" + return False + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return SENSOR_TYPES[self._type][2] + + async def async_added_to_hass(self): + """Register callbacks.""" + self.hass.helpers.dispatcher.async_dispatcher_connect( + aq.UPDATE_TOPIC, self.async_update_callback) + + @callback + def async_update_callback(self): + """Update callback.""" + panel = self._processor.panel + if panel is not None: + self._state = getattr(panel, self._type) + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/aqualogic/switch.py b/homeassistant/components/aqualogic/switch.py new file mode 100644 index 0000000000000..ee040fa1ba5b8 --- /dev/null +++ b/homeassistant/components/aqualogic/switch.py @@ -0,0 +1,109 @@ +"""Support for AquaLogic switches.""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.core import callback +import homeassistant.components.aqualogic as aq +from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.const import (CONF_MONITORED_CONDITIONS) + +DEPENDENCIES = ['aqualogic'] + +_LOGGER = logging.getLogger(__name__) + +SWITCH_TYPES = { + 'lights': 'Lights', + 'filter': 'Filter', + 'filter_low_speed': 'Filter Low Speed', + 'aux_1': 'Aux 1', + 'aux_2': 'Aux 2', + 'aux_3': 'Aux 3', + 'aux_4': 'Aux 4', + 'aux_5': 'Aux 5', + 'aux_6': 'Aux 6', + 'aux_7': 'Aux 7', +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SWITCH_TYPES)): + vol.All(cv.ensure_list, [vol.In(SWITCH_TYPES)]), +}) + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Set up the switch platform.""" + switches = [] + + processor = hass.data[aq.DOMAIN] + for switch_type in config.get(CONF_MONITORED_CONDITIONS): + switches.append(AquaLogicSwitch(processor, switch_type)) + + async_add_entities(switches) + + +class AquaLogicSwitch(SwitchDevice): + """Switch implementation for the AquaLogic component.""" + + def __init__(self, processor, switch_type): + """Initialize switch.""" + from aqualogic.core import States + self._processor = processor + self._type = switch_type + self._state_name = { + 'lights': States.LIGHTS, + 'filter': States.FILTER, + 'filter_low_speed': States.FILTER_LOW_SPEED, + 'aux_1': States.AUX_1, + 'aux_2': States.AUX_2, + 'aux_3': States.AUX_3, + 'aux_4': States.AUX_4, + 'aux_5': States.AUX_5, + 'aux_6': States.AUX_6, + 'aux_7': States.AUX_7 + }[switch_type] + + @property + def name(self): + """Return the name of the switch.""" + return "AquaLogic {}".format(SWITCH_TYPES[self._type]) + + @property + def should_poll(self): + """Return the polling state.""" + return False + + @property + def is_on(self): + """Return true if device is on.""" + panel = self._processor.panel + if panel is None: + return False + state = panel.get_state(self._state_name) + return state + + def turn_on(self, **kwargs): + """Turn the device on.""" + panel = self._processor.panel + if panel is None: + return + panel.set_state(self._state_name, True) + + def turn_off(self, **kwargs): + """Turn the device off.""" + panel = self._processor.panel + if panel is None: + return + panel.set_state(self._state_name, False) + + async def async_added_to_hass(self): + """Register callbacks.""" + self.hass.helpers.dispatcher.async_dispatcher_connect( + aq.UPDATE_TOPIC, self.async_update_callback) + + @callback + def async_update_callback(self): + """Update callback.""" + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/arduino.py b/homeassistant/components/arduino.py deleted file mode 100644 index 785f8c57f943e..0000000000000 --- a/homeassistant/components/arduino.py +++ /dev/null @@ -1,120 +0,0 @@ -""" -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/ -""" -import logging - -import voluptuous as vol - -from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) -from homeassistant.const import CONF_PORT -import homeassistant.helpers.config_validation as cv - -REQUIREMENTS = ['PyMata==2.14'] - -_LOGGER = logging.getLogger(__name__) - -BOARD = None - -DOMAIN = 'arduino' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_PORT): cv.string, - }), -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the Arduino component.""" - import serial - - port = config[DOMAIN][CONF_PORT] - - global BOARD - try: - BOARD = ArduinoBoard(port) - except (serial.serialutil.SerialException, FileNotFoundError): - _LOGGER.error("Your port %s is not accessible", port) - return False - - try: - if BOARD.get_firmata()[1] <= 2: - _LOGGER.error("The StandardFirmata sketch should be 2.2 or newer") - return False - except IndexError: - _LOGGER.warning("The version of the StandardFirmata sketch was not" - "detected. This may lead to side effects") - - def stop_arduino(event): - """Stop the Arduino service.""" - BOARD.disconnect() - - def start_arduino(event): - """Start the Arduino service.""" - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_arduino) - - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_arduino) - - return True - - -class ArduinoBoard: - """Representation of an Arduino board.""" - - def __init__(self, port): - """Initialize the board.""" - from PyMata.pymata import PyMata - self._port = port - self._board = PyMata(self._port, verbose=False) - - def set_mode(self, pin, direction, mode): - """Set the mode and the direction of a given pin.""" - if mode == 'analog' and direction == 'in': - self._board.set_pin_mode( - pin, self._board.INPUT, self._board.ANALOG) - elif mode == 'analog' and direction == 'out': - self._board.set_pin_mode( - pin, self._board.OUTPUT, self._board.ANALOG) - elif mode == 'digital' and direction == 'in': - self._board.set_pin_mode( - pin, self._board.INPUT, self._board.DIGITAL) - elif mode == 'digital' and direction == 'out': - self._board.set_pin_mode( - pin, self._board.OUTPUT, self._board.DIGITAL) - elif mode == 'pwm': - self._board.set_pin_mode( - pin, self._board.OUTPUT, self._board.PWM) - - def get_analog_inputs(self): - """Get the values from the pins.""" - self._board.capability_query() - return self._board.get_analog_response_table() - - def set_digital_out_high(self, pin): - """Set a given digital pin to high.""" - self._board.digital_write(pin, 1) - - def set_digital_out_low(self, pin): - """Set a given digital pin to low.""" - self._board.digital_write(pin, 0) - - def get_digital_in(self, pin): - """Get the value from a given digital pin.""" - self._board.digital_read(pin) - - def get_analog_in(self, pin): - """Get the value from a given analog pin.""" - self._board.analog_read(pin) - - def get_firmata(self): - """Return the version of the Firmata firmware.""" - return self._board.get_firmata_version() - - def disconnect(self): - """Disconnect the board and close the serial connection.""" - self._board.reset() - self._board.close() diff --git a/homeassistant/components/arduino/__init__.py b/homeassistant/components/arduino/__init__.py new file mode 100644 index 0000000000000..351122e74f0e0 --- /dev/null +++ b/homeassistant/components/arduino/__init__.py @@ -0,0 +1,115 @@ +"""Support for Arduino boards running with the Firmata firmware.""" +import logging + +import voluptuous as vol + +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) +from homeassistant.const import CONF_PORT +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['PyMata==2.14'] + +_LOGGER = logging.getLogger(__name__) + +BOARD = None + +DOMAIN = 'arduino' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_PORT): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Arduino component.""" + import serial + + port = config[DOMAIN][CONF_PORT] + + global BOARD + try: + BOARD = ArduinoBoard(port) + except (serial.serialutil.SerialException, FileNotFoundError): + _LOGGER.error("Your port %s is not accessible", port) + return False + + try: + if BOARD.get_firmata()[1] <= 2: + _LOGGER.error("The StandardFirmata sketch should be 2.2 or newer") + return False + except IndexError: + _LOGGER.warning("The version of the StandardFirmata sketch was not" + "detected. This may lead to side effects") + + def stop_arduino(event): + """Stop the Arduino service.""" + BOARD.disconnect() + + def start_arduino(event): + """Start the Arduino service.""" + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_arduino) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_arduino) + + return True + + +class ArduinoBoard: + """Representation of an Arduino board.""" + + def __init__(self, port): + """Initialize the board.""" + from PyMata.pymata import PyMata + self._port = port + self._board = PyMata(self._port, verbose=False) + + def set_mode(self, pin, direction, mode): + """Set the mode and the direction of a given pin.""" + if mode == 'analog' and direction == 'in': + self._board.set_pin_mode( + pin, self._board.INPUT, self._board.ANALOG) + elif mode == 'analog' and direction == 'out': + self._board.set_pin_mode( + pin, self._board.OUTPUT, self._board.ANALOG) + elif mode == 'digital' and direction == 'in': + self._board.set_pin_mode( + pin, self._board.INPUT, self._board.DIGITAL) + elif mode == 'digital' and direction == 'out': + self._board.set_pin_mode( + pin, self._board.OUTPUT, self._board.DIGITAL) + elif mode == 'pwm': + self._board.set_pin_mode( + pin, self._board.OUTPUT, self._board.PWM) + + def get_analog_inputs(self): + """Get the values from the pins.""" + self._board.capability_query() + return self._board.get_analog_response_table() + + def set_digital_out_high(self, pin): + """Set a given digital pin to high.""" + self._board.digital_write(pin, 1) + + def set_digital_out_low(self, pin): + """Set a given digital pin to low.""" + self._board.digital_write(pin, 0) + + def get_digital_in(self, pin): + """Get the value from a given digital pin.""" + self._board.digital_read(pin) + + def get_analog_in(self, pin): + """Get the value from a given analog pin.""" + self._board.analog_read(pin) + + def get_firmata(self): + """Return the version of the Firmata firmware.""" + return self._board.get_firmata_version() + + def disconnect(self): + """Disconnect the board and close the serial connection.""" + self._board.reset() + self._board.close() diff --git a/homeassistant/components/arduino/sensor.py b/homeassistant/components/arduino/sensor.py new file mode 100644 index 0000000000000..ff758ea58470b --- /dev/null +++ b/homeassistant/components/arduino/sensor.py @@ -0,0 +1,68 @@ +"""Support for getting information from Arduino pins.""" +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components import arduino +from homeassistant.const import CONF_NAME +from homeassistant.helpers.entity import Entity +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +CONF_PINS = 'pins' +CONF_TYPE = 'analog' + +DEPENDENCIES = ['arduino'] + +PIN_SCHEMA = vol.Schema({ + vol.Required(CONF_NAME): cv.string, +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_PINS): + vol.Schema({cv.positive_int: PIN_SCHEMA}), +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Arduino platform.""" + if arduino.BOARD is None: + _LOGGER.error("A connection has not been made to the Arduino board") + return False + + pins = config.get(CONF_PINS) + + sensors = [] + for pinnum, pin in pins.items(): + sensors.append(ArduinoSensor(pin.get(CONF_NAME), pinnum, CONF_TYPE)) + add_entities(sensors) + + +class ArduinoSensor(Entity): + """Representation of an Arduino Sensor.""" + + def __init__(self, name, pin, pin_type): + """Initialize the sensor.""" + self._pin = pin + self._name = name + self.pin_type = pin_type + self.direction = 'in' + self._value = None + + arduino.BOARD.set_mode(self._pin, self.direction, self.pin_type) + + @property + def state(self): + """Return the state of the sensor.""" + return self._value + + @property + def name(self): + """Get the name of the sensor.""" + return self._name + + def update(self): + """Get the latest value from the pin.""" + self._value = arduino.BOARD.get_analog_inputs()[self._pin][1] diff --git a/homeassistant/components/arduino/switch.py b/homeassistant/components/arduino/switch.py new file mode 100644 index 0000000000000..947c5188766cd --- /dev/null +++ b/homeassistant/components/arduino/switch.py @@ -0,0 +1,87 @@ +"""Support for switching Arduino pins on and off.""" +import logging + +import voluptuous as vol + +from homeassistant.components import arduino +from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) +from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv + +DEPENDENCIES = ['arduino'] + +_LOGGER = logging.getLogger(__name__) + +CONF_PINS = 'pins' +CONF_TYPE = 'digital' +CONF_NEGATE = 'negate' +CONF_INITIAL = 'initial' + +PIN_SCHEMA = vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_INITIAL, default=False): cv.boolean, + vol.Optional(CONF_NEGATE, default=False): cv.boolean, +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_PINS, default={}): + vol.Schema({cv.positive_int: PIN_SCHEMA}), +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Arduino platform.""" + # Verify that Arduino board is present + if arduino.BOARD is None: + _LOGGER.error("A connection has not been made to the Arduino board") + return False + + pins = config.get(CONF_PINS) + + switches = [] + for pinnum, pin in pins.items(): + switches.append(ArduinoSwitch(pinnum, pin)) + add_entities(switches) + + +class ArduinoSwitch(SwitchDevice): + """Representation of an Arduino switch.""" + + def __init__(self, pin, options): + """Initialize the Pin.""" + self._pin = pin + self._name = options.get(CONF_NAME) + self.pin_type = CONF_TYPE + self.direction = 'out' + + self._state = options.get(CONF_INITIAL) + + if options.get(CONF_NEGATE): + self.turn_on_handler = arduino.BOARD.set_digital_out_low + self.turn_off_handler = arduino.BOARD.set_digital_out_high + else: + self.turn_on_handler = arduino.BOARD.set_digital_out_high + self.turn_off_handler = arduino.BOARD.set_digital_out_low + + arduino.BOARD.set_mode(self._pin, self.direction, self.pin_type) + (self.turn_on_handler if self._state else self.turn_off_handler)(pin) + + @property + def name(self): + """Get the name of the pin.""" + return self._name + + @property + def is_on(self): + """Return true if pin is high/on.""" + return self._state + + def turn_on(self, **kwargs): + """Turn the pin to high/on.""" + self._state = True + self.turn_on_handler(self._pin) + + def turn_off(self, **kwargs): + """Turn the pin to low/off.""" + self._state = False + self.turn_off_handler(self._pin) diff --git a/homeassistant/components/arlo.py b/homeassistant/components/arlo.py deleted file mode 100644 index aebd57098b55c..0000000000000 --- a/homeassistant/components/arlo.py +++ /dev/null @@ -1,94 +0,0 @@ -""" -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/ -""" -import logging -from datetime import timedelta - -import voluptuous as vol -from requests.exceptions import HTTPError, ConnectTimeout - -from homeassistant.helpers import config_validation as cv -from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL) -from homeassistant.helpers.event import track_time_interval -from homeassistant.helpers.dispatcher import dispatcher_send - -REQUIREMENTS = ['pyarlo==0.2.3'] - -_LOGGER = logging.getLogger(__name__) - -CONF_ATTRIBUTION = "Data provided by arlo.netgear.com" - -DATA_ARLO = 'data_arlo' -DEFAULT_BRAND = 'Netgear Arlo' -DOMAIN = 'arlo' - -NOTIFICATION_ID = 'arlo_notification' -NOTIFICATION_TITLE = 'Arlo Component Setup' - -SCAN_INTERVAL = timedelta(seconds=60) - -SIGNAL_UPDATE_ARLO = "arlo_update" - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): - cv.time_period, - }), -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up an Arlo component.""" - conf = config[DOMAIN] - username = conf.get(CONF_USERNAME) - password = conf.get(CONF_PASSWORD) - scan_interval = conf.get(CONF_SCAN_INTERVAL) - - try: - from pyarlo import PyArlo - - arlo = PyArlo(username, password, preload=False) - if not arlo.is_connected: - return False - - # assign refresh period to base station thread - arlo_base_station = next(( - station for station in arlo.base_stations), None) - - if arlo_base_station is not None: - arlo_base_station.refresh_rate = scan_interval.total_seconds() - elif not arlo.cameras: - _LOGGER.error("No Arlo camera or base station available.") - return False - - hass.data[DATA_ARLO] = arlo - - except (ConnectTimeout, HTTPError) as ex: - _LOGGER.error("Unable to connect to Netgear Arlo: %s", str(ex)) - hass.components.persistent_notification.create( - 'Error: {}
' - 'You will need to restart hass after fixing.' - ''.format(ex), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) - return False - - def hub_refresh(event_time): - """Call ArloHub to refresh information.""" - _LOGGER.debug("Updating Arlo Hub component") - hass.data[DATA_ARLO].update(update_cameras=True, - update_base_station=True) - dispatcher_send(hass, SIGNAL_UPDATE_ARLO) - - # register service - hass.services.register(DOMAIN, 'update', hub_refresh) - - # register scan interval for ArloHub - track_time_interval(hass, hub_refresh, scan_interval) - return True diff --git a/homeassistant/components/arlo/__init__.py b/homeassistant/components/arlo/__init__.py new file mode 100644 index 0000000000000..7e81836e522af --- /dev/null +++ b/homeassistant/components/arlo/__init__.py @@ -0,0 +1,89 @@ +"""Support for Netgear Arlo IP cameras.""" +import logging +from datetime import timedelta + +import voluptuous as vol +from requests.exceptions import HTTPError, ConnectTimeout + +from homeassistant.helpers import config_validation as cv +from homeassistant.const import ( + CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL) +from homeassistant.helpers.event import track_time_interval +from homeassistant.helpers.dispatcher import dispatcher_send + +REQUIREMENTS = ['pyarlo==0.2.3'] + +_LOGGER = logging.getLogger(__name__) + +CONF_ATTRIBUTION = "Data provided by arlo.netgear.com" + +DATA_ARLO = 'data_arlo' +DEFAULT_BRAND = 'Netgear Arlo' +DOMAIN = 'arlo' + +NOTIFICATION_ID = 'arlo_notification' +NOTIFICATION_TITLE = 'Arlo Component Setup' + +SCAN_INTERVAL = timedelta(seconds=60) + +SIGNAL_UPDATE_ARLO = "arlo_update" + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): + cv.time_period, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up an Arlo component.""" + conf = config[DOMAIN] + username = conf.get(CONF_USERNAME) + password = conf.get(CONF_PASSWORD) + scan_interval = conf.get(CONF_SCAN_INTERVAL) + + try: + from pyarlo import PyArlo + + arlo = PyArlo(username, password, preload=False) + if not arlo.is_connected: + return False + + # assign refresh period to base station thread + arlo_base_station = next(( + station for station in arlo.base_stations), None) + + if arlo_base_station is not None: + arlo_base_station.refresh_rate = scan_interval.total_seconds() + elif not arlo.cameras: + _LOGGER.error("No Arlo camera or base station available.") + return False + + hass.data[DATA_ARLO] = arlo + + except (ConnectTimeout, HTTPError) as ex: + _LOGGER.error("Unable to connect to Netgear Arlo: %s", str(ex)) + hass.components.persistent_notification.create( + 'Error: {}
' + 'You will need to restart hass after fixing.' + ''.format(ex), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + return False + + def hub_refresh(event_time): + """Call ArloHub to refresh information.""" + _LOGGER.debug("Updating Arlo Hub component") + hass.data[DATA_ARLO].update(update_cameras=True, + update_base_station=True) + dispatcher_send(hass, SIGNAL_UPDATE_ARLO) + + # register service + hass.services.register(DOMAIN, 'update', hub_refresh) + + # register scan interval for ArloHub + track_time_interval(hass, hub_refresh, scan_interval) + return True diff --git a/homeassistant/components/arlo/alarm_control_panel.py b/homeassistant/components/arlo/alarm_control_panel.py new file mode 100644 index 0000000000000..8c21a448a23cb --- /dev/null +++ b/homeassistant/components/arlo/alarm_control_panel.py @@ -0,0 +1,136 @@ +"""Support for Arlo Alarm Control Panels.""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.components.alarm_control_panel import ( + AlarmControlPanel, PLATFORM_SCHEMA) +from homeassistant.components.arlo import ( + DATA_ARLO, CONF_ATTRIBUTION, SIGNAL_UPDATE_ARLO) +from homeassistant.const import ( + ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, + STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT) + +_LOGGER = logging.getLogger(__name__) + +ARMED = 'armed' + +CONF_HOME_MODE_NAME = 'home_mode_name' +CONF_AWAY_MODE_NAME = 'away_mode_name' +CONF_NIGHT_MODE_NAME = 'night_mode_name' + +DEPENDENCIES = ['arlo'] + +DISARMED = 'disarmed' + +ICON = 'mdi:security' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_HOME_MODE_NAME, default=ARMED): cv.string, + vol.Optional(CONF_AWAY_MODE_NAME, default=ARMED): cv.string, + vol.Optional(CONF_NIGHT_MODE_NAME, default=ARMED): cv.string, +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Arlo Alarm Control Panels.""" + arlo = hass.data[DATA_ARLO] + + if not arlo.base_stations: + return + + home_mode_name = config.get(CONF_HOME_MODE_NAME) + away_mode_name = config.get(CONF_AWAY_MODE_NAME) + night_mode_name = config.get(CONF_NIGHT_MODE_NAME) + base_stations = [] + for base_station in arlo.base_stations: + base_stations.append(ArloBaseStation(base_station, home_mode_name, + away_mode_name, night_mode_name)) + add_entities(base_stations, True) + + +class ArloBaseStation(AlarmControlPanel): + """Representation of an Arlo Alarm Control Panel.""" + + def __init__(self, data, home_mode_name, away_mode_name, night_mode_name): + """Initialize the alarm control panel.""" + self._base_station = data + self._home_mode_name = home_mode_name + self._away_mode_name = away_mode_name + self._night_mode_name = night_mode_name + self._state = None + + @property + def icon(self): + """Return icon.""" + return ICON + + async def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_ARLO, self._update_callback) + + @callback + def _update_callback(self): + """Call update method.""" + self.async_schedule_update_ha_state(True) + + @property + def state(self): + """Return the state of the device.""" + return self._state + + def update(self): + """Update the state of the device.""" + _LOGGER.debug("Updating Arlo Alarm Control Panel %s", self.name) + mode = self._base_station.mode + if mode: + self._state = self._get_state_from_mode(mode) + else: + self._state = None + + async def async_alarm_disarm(self, code=None): + """Send disarm command.""" + self._base_station.mode = DISARMED + + async def async_alarm_arm_away(self, code=None): + """Send arm away command. Uses custom mode.""" + self._base_station.mode = self._away_mode_name + + async def async_alarm_arm_home(self, code=None): + """Send arm home command. Uses custom mode.""" + self._base_station.mode = self._home_mode_name + + async def async_alarm_arm_night(self, code=None): + """Send arm night command. Uses custom mode.""" + self._base_station.mode = self._night_mode_name + + @property + def name(self): + """Return the name of the base station.""" + return self._base_station.name + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + 'device_id': self._base_station.device_id + } + + def _get_state_from_mode(self, mode): + """Convert Arlo mode to Home Assistant state.""" + if mode == ARMED: + return STATE_ALARM_ARMED_AWAY + if mode == DISARMED: + return STATE_ALARM_DISARMED + if mode == self._home_mode_name: + return STATE_ALARM_ARMED_HOME + if mode == self._away_mode_name: + return STATE_ALARM_ARMED_AWAY + if mode == self._night_mode_name: + return STATE_ALARM_ARMED_NIGHT + return mode diff --git a/homeassistant/components/arlo/camera.py b/homeassistant/components/arlo/camera.py new file mode 100644 index 0000000000000..6f20ecdadcd3f --- /dev/null +++ b/homeassistant/components/arlo/camera.py @@ -0,0 +1,164 @@ +"""Support for Netgear Arlo IP cameras.""" +import logging + +import voluptuous as vol + +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.components.arlo import ( + DEFAULT_BRAND, DATA_ARLO, SIGNAL_UPDATE_ARLO) +from homeassistant.components.camera import Camera, PLATFORM_SCHEMA +from homeassistant.components.ffmpeg import DATA_FFMPEG +from homeassistant.const import ATTR_BATTERY_LEVEL +from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +_LOGGER = logging.getLogger(__name__) + +ARLO_MODE_ARMED = 'armed' +ARLO_MODE_DISARMED = 'disarmed' + +ATTR_BRIGHTNESS = 'brightness' +ATTR_FLIPPED = 'flipped' +ATTR_MIRRORED = 'mirrored' +ATTR_MOTION = 'motion_detection_sensitivity' +ATTR_POWERSAVE = 'power_save_mode' +ATTR_SIGNAL_STRENGTH = 'signal_strength' +ATTR_UNSEEN_VIDEOS = 'unseen_videos' +ATTR_LAST_REFRESH = 'last_refresh' + +CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments' + +DEPENDENCIES = ['arlo', 'ffmpeg'] + +POWERSAVE_MODE_MAPPING = { + 1: 'best_battery_life', + 2: 'optimized', + 3: 'best_video' +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string, +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up an Arlo IP Camera.""" + arlo = hass.data[DATA_ARLO] + + cameras = [] + for camera in arlo.cameras: + cameras.append(ArloCam(hass, camera, config)) + + add_entities(cameras) + + +class ArloCam(Camera): + """An implementation of a Netgear Arlo IP camera.""" + + def __init__(self, hass, camera, device_info): + """Initialize an Arlo camera.""" + super().__init__() + self._camera = camera + self._name = self._camera.name + self._motion_status = False + self._ffmpeg = hass.data[DATA_FFMPEG] + self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS) + self._last_refresh = None + self.attrs = {} + + def camera_image(self): + """Return a still image response from the camera.""" + return self._camera.last_image_from_cache + + async def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_ARLO, self._update_callback) + + @callback + def _update_callback(self): + """Call update method.""" + self.async_schedule_update_ha_state() + + async def handle_async_mjpeg_stream(self, request): + """Generate an HTTP MJPEG stream from the camera.""" + from haffmpeg import CameraMjpeg + video = self._camera.last_video + if not video: + error_msg = \ + 'Video not found for {0}. Is it older than {1} days?'.format( + self.name, self._camera.min_days_vdo_cache) + _LOGGER.error(error_msg) + return + + stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop) + await stream.open_camera( + video.video_url, extra_cmd=self._ffmpeg_arguments) + + try: + return await async_aiohttp_proxy_stream( + self.hass, request, stream, + self._ffmpeg.ffmpeg_stream_content_type) + finally: + await stream.close() + + @property + def name(self): + """Return the name of this camera.""" + return self._name + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + name: value for name, value in ( + (ATTR_BATTERY_LEVEL, self._camera.battery_level), + (ATTR_BRIGHTNESS, self._camera.brightness), + (ATTR_FLIPPED, self._camera.flip_state), + (ATTR_MIRRORED, self._camera.mirror_state), + (ATTR_MOTION, self._camera.motion_detection_sensitivity), + (ATTR_POWERSAVE, POWERSAVE_MODE_MAPPING.get( + self._camera.powersave_mode)), + (ATTR_SIGNAL_STRENGTH, self._camera.signal_strength), + (ATTR_UNSEEN_VIDEOS, self._camera.unseen_videos), + ) if value is not None + } + + @property + def model(self): + """Return the camera model.""" + return self._camera.model_id + + @property + def brand(self): + """Return the camera brand.""" + return DEFAULT_BRAND + + @property + def motion_detection_enabled(self): + """Return the camera motion detection status.""" + return self._motion_status + + def set_base_station_mode(self, mode): + """Set the mode in the base station.""" + # Get the list of base stations identified by library + base_stations = self.hass.data[DATA_ARLO].base_stations + + # Some Arlo cameras does not have base station + # So check if there is base station detected first + # if yes, then choose the primary base station + # Set the mode on the chosen base station + if base_stations: + primary_base_station = base_stations[0] + primary_base_station.mode = mode + + def enable_motion_detection(self): + """Enable the Motion detection in base station (Arm).""" + self._motion_status = True + self.set_base_station_mode(ARLO_MODE_ARMED) + + def disable_motion_detection(self): + """Disable the motion detection in base station (Disarm).""" + self._motion_status = False + self.set_base_station_mode(ARLO_MODE_DISARMED) diff --git a/homeassistant/components/arlo/sensor.py b/homeassistant/components/arlo/sensor.py new file mode 100644 index 0000000000000..3ad7b70a9479a --- /dev/null +++ b/homeassistant/components/arlo/sensor.py @@ -0,0 +1,186 @@ +"""Sensor support for Netgear Arlo IP cameras.""" +import logging + +import voluptuous as vol + +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.components.arlo import ( + CONF_ATTRIBUTION, DEFAULT_BRAND, DATA_ARLO, SIGNAL_UPDATE_ARLO) +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS, TEMP_CELSIUS, + DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY) + +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.icon import icon_for_battery_level + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['arlo'] + +# sensor_type [ description, unit, icon ] +SENSOR_TYPES = { + 'last_capture': ['Last', None, 'run-fast'], + 'total_cameras': ['Arlo Cameras', None, 'video'], + 'captured_today': ['Captured Today', None, 'file-video'], + 'battery_level': ['Battery Level', '%', 'battery-50'], + 'signal_strength': ['Signal Strength', None, 'signal'], + 'temperature': ['Temperature', TEMP_CELSIUS, 'thermometer'], + 'humidity': ['Humidity', '%', 'water-percent'], + 'air_quality': ['Air Quality', 'ppm', 'biohazard'] +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up an Arlo IP sensor.""" + arlo = hass.data.get(DATA_ARLO) + if not arlo: + return + + sensors = [] + for sensor_type in config.get(CONF_MONITORED_CONDITIONS): + if sensor_type == 'total_cameras': + sensors.append(ArloSensor( + SENSOR_TYPES[sensor_type][0], arlo, sensor_type)) + else: + for camera in arlo.cameras: + if sensor_type in ('temperature', 'humidity', 'air_quality'): + continue + + name = '{0} {1}'.format( + SENSOR_TYPES[sensor_type][0], camera.name) + sensors.append(ArloSensor(name, camera, sensor_type)) + + for base_station in arlo.base_stations: + if sensor_type in ('temperature', 'humidity', 'air_quality') \ + and base_station.model_id == 'ABC1000': + name = '{0} {1}'.format( + SENSOR_TYPES[sensor_type][0], base_station.name) + sensors.append(ArloSensor(name, base_station, sensor_type)) + + add_entities(sensors, True) + + +class ArloSensor(Entity): + """An implementation of a Netgear Arlo IP sensor.""" + + def __init__(self, name, device, sensor_type): + """Initialize an Arlo sensor.""" + _LOGGER.debug('ArloSensor created for %s', name) + self._name = name + self._data = device + self._sensor_type = sensor_type + self._state = None + self._icon = 'mdi:{}'.format(SENSOR_TYPES.get(self._sensor_type)[2]) + + @property + def name(self): + """Return the name of this camera.""" + return self._name + + async def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_ARLO, self._update_callback) + + @callback + def _update_callback(self): + """Call update method.""" + self.async_schedule_update_ha_state(True) + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + if self._sensor_type == 'battery_level' and self._state is not None: + return icon_for_battery_level(battery_level=int(self._state), + charging=False) + return self._icon + + @property + def unit_of_measurement(self): + """Return the units of measurement.""" + return SENSOR_TYPES.get(self._sensor_type)[1] + + @property + def device_class(self): + """Return the device class of the sensor.""" + if self._sensor_type == 'temperature': + return DEVICE_CLASS_TEMPERATURE + if self._sensor_type == 'humidity': + return DEVICE_CLASS_HUMIDITY + return None + + def update(self): + """Get the latest data and updates the state.""" + _LOGGER.debug("Updating Arlo sensor %s", self.name) + if self._sensor_type == 'total_cameras': + self._state = len(self._data.cameras) + + elif self._sensor_type == 'captured_today': + self._state = len(self._data.captured_today) + + elif self._sensor_type == 'last_capture': + try: + video = self._data.last_video + self._state = video.created_at_pretty("%m-%d-%Y %H:%M:%S") + except (AttributeError, IndexError): + error_msg = \ + 'Video not found for {0}. Older than {1} days?'.format( + self.name, self._data.min_days_vdo_cache) + _LOGGER.debug(error_msg) + self._state = None + + elif self._sensor_type == 'battery_level': + try: + self._state = self._data.battery_level + except TypeError: + self._state = None + + elif self._sensor_type == 'signal_strength': + try: + self._state = self._data.signal_strength + except TypeError: + self._state = None + + elif self._sensor_type == 'temperature': + try: + self._state = self._data.ambient_temperature + except TypeError: + self._state = None + + elif self._sensor_type == 'humidity': + try: + self._state = self._data.ambient_humidity + except TypeError: + self._state = None + + elif self._sensor_type == 'air_quality': + try: + self._state = self._data.ambient_air_quality + except TypeError: + self._state = None + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + attrs = {} + + attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION + attrs['brand'] = DEFAULT_BRAND + + if self._sensor_type != 'total_cameras': + attrs['model'] = self._data.model_id + + return attrs diff --git a/homeassistant/components/asterisk_mbox.py b/homeassistant/components/asterisk_mbox.py deleted file mode 100644 index 406774d5fadbe..0000000000000 --- a/homeassistant/components/asterisk_mbox.py +++ /dev/null @@ -1,122 +0,0 @@ -""" -Support for Asterisk Voicemail interface. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/asterisk_mbox/ -""" -import logging - -import voluptuous as vol - -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT -from homeassistant.core import callback -from homeassistant.helpers import discovery -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_send, dispatcher_connect) - -REQUIREMENTS = ['asterisk_mbox==0.5.0'] - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'asterisk_mbox' - -SIGNAL_DISCOVER_PLATFORM = "asterisk_mbox.discover_platform" -SIGNAL_MESSAGE_REQUEST = 'asterisk_mbox.message_request' -SIGNAL_MESSAGE_UPDATE = 'asterisk_mbox.message_updated' -SIGNAL_CDR_UPDATE = 'asterisk_mbox.message_updated' -SIGNAL_CDR_REQUEST = 'asterisk_mbox.message_request' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_PORT): cv.port, - }), -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up for the Asterisk Voicemail box.""" - conf = config.get(DOMAIN) - - host = conf.get(CONF_HOST) - port = conf.get(CONF_PORT) - password = conf.get(CONF_PASSWORD) - - hass.data[DOMAIN] = AsteriskData(hass, host, port, password, config) - - return True - - -class AsteriskData: - """Store Asterisk mailbox data.""" - - def __init__(self, hass, host, port, password, config): - """Init the Asterisk data object.""" - from asterisk_mbox import Client as asteriskClient - self.hass = hass - self.config = config - self.messages = None - self.cdr = None - - dispatcher_connect( - self.hass, SIGNAL_MESSAGE_REQUEST, self._request_messages) - dispatcher_connect( - self.hass, SIGNAL_CDR_REQUEST, self._request_cdr) - dispatcher_connect( - self.hass, SIGNAL_DISCOVER_PLATFORM, self._discover_platform) - # Only connect after signal connection to ensure we don't miss any - self.client = asteriskClient(host, port, password, self.handle_data) - - @callback - def _discover_platform(self, component): - _LOGGER.debug("Adding mailbox %s", component) - self.hass.async_create_task(discovery.async_load_platform( - self.hass, "mailbox", component, {}, self.config)) - - @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) - - if command == CMD_MESSAGE_LIST: - _LOGGER.debug("AsteriskVM sent updated message list: Len %d", - len(msg)) - old_messages = self.messages - 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_MESSAGE_UPDATE, - self.messages) - elif command == CMD_MESSAGE_CDR: - _LOGGER.debug("AsteriskVM sent updated CDR list: Len %d", - len(msg.get('entries', []))) - self.cdr = msg['entries'] - async_dispatcher_send(self.hass, SIGNAL_CDR_UPDATE, self.cdr) - elif command == CMD_MESSAGE_CDR_AVAILABLE: - if not isinstance(self.cdr, list): - _LOGGER.debug("AsteriskVM adding CDR platform") - self.cdr = [] - async_dispatcher_send(self.hass, SIGNAL_DISCOVER_PLATFORM, - "asterisk_cdr") - async_dispatcher_send(self.hass, SIGNAL_CDR_REQUEST) - else: - _LOGGER.debug("AsteriskVM sent unknown message '%d' len: %d", - command, len(msg)) - - @callback - def _request_messages(self): - """Handle changes to the mailbox.""" - _LOGGER.debug("Requesting message list") - self.client.messages() - - @callback - def _request_cdr(self): - """Handle changes to the CDR.""" - _LOGGER.debug("Requesting CDR list") - self.client.get_cdr() diff --git a/homeassistant/components/asterisk_mbox/__init__.py b/homeassistant/components/asterisk_mbox/__init__.py new file mode 100644 index 0000000000000..d8d3b194cd7f1 --- /dev/null +++ b/homeassistant/components/asterisk_mbox/__init__.py @@ -0,0 +1,116 @@ +"""Support for Asterisk Voicemail interface.""" +import logging + +import voluptuous as vol + +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT +from homeassistant.core import callback +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_send, dispatcher_connect) + +REQUIREMENTS = ['asterisk_mbox==0.5.0'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'asterisk_mbox' + +SIGNAL_DISCOVER_PLATFORM = "asterisk_mbox.discover_platform" +SIGNAL_MESSAGE_REQUEST = 'asterisk_mbox.message_request' +SIGNAL_MESSAGE_UPDATE = 'asterisk_mbox.message_updated' +SIGNAL_CDR_UPDATE = 'asterisk_mbox.message_updated' +SIGNAL_CDR_REQUEST = 'asterisk_mbox.message_request' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_PORT): cv.port, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up for the Asterisk Voicemail box.""" + conf = config.get(DOMAIN) + + host = conf.get(CONF_HOST) + port = conf.get(CONF_PORT) + password = conf.get(CONF_PASSWORD) + + hass.data[DOMAIN] = AsteriskData(hass, host, port, password, config) + + return True + + +class AsteriskData: + """Store Asterisk mailbox data.""" + + def __init__(self, hass, host, port, password, config): + """Init the Asterisk data object.""" + from asterisk_mbox import Client as asteriskClient + self.hass = hass + self.config = config + self.messages = None + self.cdr = None + + dispatcher_connect( + self.hass, SIGNAL_MESSAGE_REQUEST, self._request_messages) + dispatcher_connect( + self.hass, SIGNAL_CDR_REQUEST, self._request_cdr) + dispatcher_connect( + self.hass, SIGNAL_DISCOVER_PLATFORM, self._discover_platform) + # Only connect after signal connection to ensure we don't miss any + self.client = asteriskClient(host, port, password, self.handle_data) + + @callback + def _discover_platform(self, component): + _LOGGER.debug("Adding mailbox %s", component) + self.hass.async_create_task(discovery.async_load_platform( + self.hass, "mailbox", component, {}, self.config)) + + @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) + + if command == CMD_MESSAGE_LIST: + _LOGGER.debug("AsteriskVM sent updated message list: Len %d", + len(msg)) + old_messages = self.messages + 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_MESSAGE_UPDATE, + self.messages) + elif command == CMD_MESSAGE_CDR: + _LOGGER.debug("AsteriskVM sent updated CDR list: Len %d", + len(msg.get('entries', []))) + self.cdr = msg['entries'] + async_dispatcher_send(self.hass, SIGNAL_CDR_UPDATE, self.cdr) + elif command == CMD_MESSAGE_CDR_AVAILABLE: + if not isinstance(self.cdr, list): + _LOGGER.debug("AsteriskVM adding CDR platform") + self.cdr = [] + async_dispatcher_send(self.hass, SIGNAL_DISCOVER_PLATFORM, + "asterisk_cdr") + async_dispatcher_send(self.hass, SIGNAL_CDR_REQUEST) + else: + _LOGGER.debug("AsteriskVM sent unknown message '%d' len: %d", + command, len(msg)) + + @callback + def _request_messages(self): + """Handle changes to the mailbox.""" + _LOGGER.debug("Requesting message list") + self.client.messages() + + @callback + def _request_cdr(self): + """Handle changes to the CDR.""" + _LOGGER.debug("Requesting CDR list") + self.client.get_cdr() diff --git a/homeassistant/components/asterisk_mbox/mailbox.py b/homeassistant/components/asterisk_mbox/mailbox.py new file mode 100644 index 0000000000000..9da4bd1a21a62 --- /dev/null +++ b/homeassistant/components/asterisk_mbox/mailbox.py @@ -0,0 +1,70 @@ +"""Support for the Asterisk Voicemail interface.""" +import logging + +from homeassistant.components.asterisk_mbox import DOMAIN as ASTERISK_DOMAIN +from homeassistant.components.mailbox import ( + CONTENT_TYPE_MPEG, Mailbox, StreamError) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['asterisk_mbox'] + +SIGNAL_MESSAGE_REQUEST = 'asterisk_mbox.message_request' +SIGNAL_MESSAGE_UPDATE = 'asterisk_mbox.message_updated' + + +async def async_get_handler(hass, config, discovery_info=None): + """Set up the Asterix VM platform.""" + return AsteriskMailbox(hass, ASTERISK_DOMAIN) + + +class AsteriskMailbox(Mailbox): + """Asterisk VM Sensor.""" + + def __init__(self, hass, name): + """Initialize Asterisk mailbox.""" + super().__init__(hass, name) + async_dispatcher_connect( + self.hass, SIGNAL_MESSAGE_UPDATE, self._update_callback) + + @callback + def _update_callback(self, msg): + """Update the message count in HA, if needed.""" + self.async_update() + + @property + def media_type(self): + """Return the supported media type.""" + return CONTENT_TYPE_MPEG + + @property + def can_delete(self): + """Return if messages can be deleted.""" + return True + + @property + def has_media(self): + """Return if messages have attached media files.""" + return True + + async def async_get_media(self, msgid): + """Return the media blob for the msgid.""" + from asterisk_mbox import ServerError + client = self.hass.data[ASTERISK_DOMAIN].client + try: + return client.mp3(msgid, sync=True) + except ServerError as err: + raise StreamError(err) + + async def async_get_messages(self): + """Return a list of the current messages.""" + return self.hass.data[ASTERISK_DOMAIN].messages + + def async_delete(self, msgid): + """Delete the specified messages.""" + client = self.hass.data[ASTERISK_DOMAIN].client + _LOGGER.info("Deleting: %s", msgid) + client.delete(msgid) + return True diff --git a/homeassistant/components/asuswrt.py b/homeassistant/components/asuswrt.py deleted file mode 100644 index 0069b3c0d73e9..0000000000000 --- a/homeassistant/components/asuswrt.py +++ /dev/null @@ -1,73 +0,0 @@ -""" -Support for ASUSWRT devices. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/asuswrt/ -""" -import logging - -import voluptuous as vol - -from homeassistant.const import ( - CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT, CONF_MODE, - CONF_PROTOCOL) -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.discovery import async_load_platform - -REQUIREMENTS = ['aioasuswrt==1.1.20'] - -_LOGGER = logging.getLogger(__name__) - -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({ - DOMAIN: vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_PROTOCOL, default='ssh'): vol.In(['ssh', 'telnet']), - vol.Optional(CONF_MODE, default='router'): vol.In(['router', 'ap']), - vol.Optional(CONF_PORT, default=DEFAULT_SSH_PORT): cv.port, - vol.Optional(CONF_REQUIRE_IP, default=True): cv.boolean, - vol.Exclusive(CONF_PASSWORD, SECRET_GROUP): cv.string, - vol.Exclusive(CONF_SSH_KEY, SECRET_GROUP): cv.isfile, - vol.Exclusive(CONF_PUB_KEY, SECRET_GROUP): cv.isfile, - vol.Optional(CONF_SENSORS): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)]), - }), -}, extra=vol.ALLOW_EXTRA) - - -async def async_setup(hass, config): - """Set up the asuswrt component.""" - from aioasuswrt.asuswrt import AsusWrt - conf = config[DOMAIN] - - api = AsusWrt(conf[CONF_HOST], conf.get(CONF_PORT), - conf.get(CONF_PROTOCOL) == 'telnet', - conf[CONF_USERNAME], - conf.get(CONF_PASSWORD, ''), - conf.get('ssh_key', conf.get('pub_key', '')), - conf.get(CONF_MODE), conf.get(CONF_REQUIRE_IP)) - - await api.connection.async_connect() - if not api.is_connected: - _LOGGER.error("Unable to setup asuswrt component") - return False - - hass.data[DATA_ASUSWRT] = api - - hass.async_create_task(async_load_platform( - hass, 'sensor', DOMAIN, config[DOMAIN].get(CONF_SENSORS), config)) - hass.async_create_task(async_load_platform( - hass, 'device_tracker', DOMAIN, {}, config)) - - return True diff --git a/homeassistant/components/asuswrt/__init__.py b/homeassistant/components/asuswrt/__init__.py new file mode 100644 index 0000000000000..3fc0e9d6476ca --- /dev/null +++ b/homeassistant/components/asuswrt/__init__.py @@ -0,0 +1,69 @@ +"""Support for ASUSWRT devices.""" +import logging + +import voluptuous as vol + +from homeassistant.const import ( + CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT, CONF_MODE, + CONF_PROTOCOL) +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.discovery import async_load_platform + +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 +DEFAULT_SSH_PORT = 22 + +SECRET_GROUP = 'Password or SSH Key' +SENSOR_TYPES = ['upload_speed', 'download_speed', 'download', 'upload'] + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_PROTOCOL, default='ssh'): vol.In(['ssh', 'telnet']), + vol.Optional(CONF_MODE, default='router'): vol.In(['router', 'ap']), + vol.Optional(CONF_PORT, default=DEFAULT_SSH_PORT): cv.port, + vol.Optional(CONF_REQUIRE_IP, default=True): cv.boolean, + vol.Exclusive(CONF_PASSWORD, SECRET_GROUP): cv.string, + vol.Exclusive(CONF_SSH_KEY, SECRET_GROUP): cv.isfile, + vol.Exclusive(CONF_PUB_KEY, SECRET_GROUP): cv.isfile, + vol.Optional(CONF_SENSORS): vol.All( + cv.ensure_list, [vol.In(SENSOR_TYPES)]), + }), +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the asuswrt component.""" + from aioasuswrt.asuswrt import AsusWrt + conf = config[DOMAIN] + + api = AsusWrt(conf[CONF_HOST], conf.get(CONF_PORT), + conf.get(CONF_PROTOCOL) == 'telnet', + conf[CONF_USERNAME], + conf.get(CONF_PASSWORD, ''), + conf.get('ssh_key', conf.get('pub_key', '')), + conf.get(CONF_MODE), conf.get(CONF_REQUIRE_IP)) + + await api.connection.async_connect() + if not api.is_connected: + _LOGGER.error("Unable to setup asuswrt component") + return False + + hass.data[DATA_ASUSWRT] = api + + hass.async_create_task(async_load_platform( + hass, 'sensor', DOMAIN, config[DOMAIN].get(CONF_SENSORS), config)) + hass.async_create_task(async_load_platform( + hass, 'device_tracker', DOMAIN, {}, config)) + + return True diff --git a/homeassistant/components/august.py b/homeassistant/components/august.py deleted file mode 100644 index 2073f680e00bd..0000000000000 --- a/homeassistant/components/august.py +++ /dev/null @@ -1,355 +0,0 @@ -""" -Support for August devices. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/august/ -""" -import logging -from datetime import timedelta - -import voluptuous as vol -from requests import RequestException - -import homeassistant.helpers.config_validation as cv -from homeassistant.const import ( - CONF_PASSWORD, CONF_USERNAME, CONF_TIMEOUT, EVENT_HOMEASSISTANT_STOP) -from homeassistant.helpers import discovery -from homeassistant.util import Throttle - -_LOGGER = logging.getLogger(__name__) - -_CONFIGURING = {} - -REQUIREMENTS = ['py-august==0.7.0'] - -DEFAULT_TIMEOUT = 10 -ACTIVITY_FETCH_LIMIT = 10 -ACTIVITY_INITIAL_FETCH_LIMIT = 20 - -CONF_LOGIN_METHOD = 'login_method' -CONF_INSTALL_ID = 'install_id' - -NOTIFICATION_ID = 'august_notification' -NOTIFICATION_TITLE = "August Setup" - -AUGUST_CONFIG_FILE = '.august.conf' - -DATA_AUGUST = 'august' -DOMAIN = 'august' -DEFAULT_ENTITY_NAMESPACE = 'august' -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) -DEFAULT_SCAN_INTERVAL = timedelta(seconds=5) -LOGIN_METHODS = ['phone', 'email'] - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_LOGIN_METHOD): vol.In(LOGIN_METHODS), - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_INSTALL_ID): cv.string, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - }) -}, extra=vol.ALLOW_EXTRA) - -AUGUST_COMPONENTS = [ - 'camera', 'binary_sensor', 'lock' -] - - -def request_configuration(hass, config, api, authenticator): - """Request configuration steps from the user.""" - configurator = hass.components.configurator - - def august_configuration_callback(data): - """Run when the configuration callback is called.""" - from august.authenticator import ValidationResult - - result = authenticator.validate_verification_code( - data.get('verification_code')) - - if result == ValidationResult.INVALID_VERIFICATION_CODE: - configurator.notify_errors(_CONFIGURING[DOMAIN], - "Invalid verification code") - elif result == ValidationResult.VALIDATED: - setup_august(hass, config, api, authenticator) - - if DOMAIN not in _CONFIGURING: - authenticator.send_verification_code() - - conf = config[DOMAIN] - username = conf.get(CONF_USERNAME) - login_method = conf.get(CONF_LOGIN_METHOD) - - _CONFIGURING[DOMAIN] = configurator.request_config( - NOTIFICATION_TITLE, - august_configuration_callback, - description="Please check your {} ({}) and enter the verification " - "code below".format(login_method, username), - submit_caption='Verify', - fields=[{ - 'id': 'verification_code', - 'name': "Verification code", - 'type': 'string'}] - ) - - -def setup_august(hass, config, api, authenticator): - """Set up the August component.""" - from august.authenticator import AuthenticationState - - authentication = None - try: - authentication = authenticator.authenticate() - except RequestException as ex: - _LOGGER.error("Unable to connect to August service: %s", str(ex)) - - hass.components.persistent_notification.create( - "Error: {}
" - "You will need to restart hass after fixing." - "".format(ex), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) - - state = authentication.state - - if state == AuthenticationState.AUTHENTICATED: - if DOMAIN in _CONFIGURING: - hass.components.configurator.request_done(_CONFIGURING.pop(DOMAIN)) - - hass.data[DATA_AUGUST] = AugustData( - hass, api, authentication.access_token) - - for component in AUGUST_COMPONENTS: - discovery.load_platform(hass, component, DOMAIN, {}, config) - - return True - if state == AuthenticationState.BAD_PASSWORD: - _LOGGER.error("Invalid password provided") - return False - if state == AuthenticationState.REQUIRES_VALIDATION: - request_configuration(hass, config, api, authenticator) - return True - - return False - - -def setup(hass, config): - """Set up the August component.""" - from august.api import Api - from august.authenticator import Authenticator - from requests import Session - - conf = config[DOMAIN] - api_http_session = None - try: - api_http_session = Session() - except RequestException as ex: - _LOGGER.warning("Creating HTTP session failed with: %s", str(ex)) - - api = Api(timeout=conf.get(CONF_TIMEOUT), http_session=api_http_session) - - authenticator = Authenticator( - api, - conf.get(CONF_LOGIN_METHOD), - conf.get(CONF_USERNAME), - conf.get(CONF_PASSWORD), - install_id=conf.get(CONF_INSTALL_ID), - access_token_cache_file=hass.config.path(AUGUST_CONFIG_FILE)) - - def close_http_session(event): - """Close API sessions used to connect to August.""" - _LOGGER.debug("Closing August HTTP sessions") - if api_http_session: - try: - api_http_session.close() - except RequestException: - pass - - _LOGGER.debug("August HTTP session closed.") - - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, close_http_session) - _LOGGER.debug("Registered for HASS stop event") - - return setup_august(hass, config, api, authenticator) - - -class AugustData: - """August data object.""" - - def __init__(self, hass, api, access_token): - """Init August data object.""" - self._hass = hass - self._api = api - self._access_token = access_token - self._doorbells = self._api.get_doorbells(self._access_token) or [] - self._locks = self._api.get_operable_locks(self._access_token) or [] - self._house_ids = [d.house_id for d in self._doorbells + self._locks] - - self._doorbell_detail_by_id = {} - self._lock_status_by_id = {} - self._lock_detail_by_id = {} - self._door_state_by_id = {} - self._activities_by_id = {} - - @property - def house_ids(self): - """Return a list of house_ids.""" - return self._house_ids - - @property - def doorbells(self): - """Return a list of doorbells.""" - return self._doorbells - - @property - def locks(self): - """Return a list of locks.""" - return self._locks - - def get_device_activities(self, device_id, *activity_types): - """Return a list of activities.""" - _LOGGER.debug("Getting device activities") - self._update_device_activities() - - activities = self._activities_by_id.get(device_id, []) - if activity_types: - return [a for a in activities if a.activity_type in activity_types] - return activities - - def get_latest_device_activity(self, device_id, *activity_types): - """Return latest activity.""" - activities = self.get_device_activities(device_id, *activity_types) - return next(iter(activities or []), None) - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def _update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT): - """Update data object with latest from August API.""" - _LOGGER.debug("Start retrieving device activities") - for house_id in self.house_ids: - _LOGGER.debug("Updating device activity for house id %s", - house_id) - - activities = self._api.get_house_activities(self._access_token, - house_id, - limit=limit) - - device_ids = {a.device_id for a in activities} - for device_id in device_ids: - self._activities_by_id[device_id] = [a for a in activities if - a.device_id == device_id] - _LOGGER.debug("Completed retrieving device activities") - - def get_doorbell_detail(self, doorbell_id): - """Return doorbell detail.""" - self._update_doorbells() - return self._doorbell_detail_by_id.get(doorbell_id) - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def _update_doorbells(self): - detail_by_id = {} - - _LOGGER.debug("Start retrieving doorbell details") - for doorbell in self._doorbells: - _LOGGER.debug("Updating doorbell status for %s", - doorbell.device_name) - try: - detail_by_id[doorbell.device_id] =\ - self._api.get_doorbell_detail( - self._access_token, doorbell.device_id) - except RequestException as ex: - _LOGGER.error("Request error trying to retrieve doorbell" - " status for %s. %s", doorbell.device_name, ex) - detail_by_id[doorbell.device_id] = None - except Exception: - detail_by_id[doorbell.device_id] = None - raise - - _LOGGER.debug("Completed retrieving doorbell details") - self._doorbell_detail_by_id = detail_by_id - - def get_lock_status(self, lock_id): - """Return status if the door is locked or unlocked. - - This is status for the lock itself. - """ - self._update_locks() - return self._lock_status_by_id.get(lock_id) - - def get_lock_detail(self, lock_id): - """Return lock detail.""" - self._update_locks() - return self._lock_detail_by_id.get(lock_id) - - def get_door_state(self, lock_id): - """Return status if the door is open or closed. - - This is the status from the door sensor. - """ - self._update_doors() - return self._door_state_by_id.get(lock_id) - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def _update_doors(self): - state_by_id = {} - - _LOGGER.debug("Start retrieving door status") - for lock in self._locks: - _LOGGER.debug("Updating door status for %s", - lock.device_name) - - try: - state_by_id[lock.device_id] = self._api.get_lock_door_status( - self._access_token, lock.device_id) - except RequestException as ex: - _LOGGER.error("Request error trying to retrieve door" - " status for %s. %s", lock.device_name, ex) - state_by_id[lock.device_id] = None - except Exception: - state_by_id[lock.device_id] = None - raise - - _LOGGER.debug("Completed retrieving door status") - self._door_state_by_id = state_by_id - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def _update_locks(self): - status_by_id = {} - detail_by_id = {} - - _LOGGER.debug("Start retrieving locks status") - for lock in self._locks: - _LOGGER.debug("Updating lock status for %s", - lock.device_name) - try: - status_by_id[lock.device_id] = self._api.get_lock_status( - self._access_token, lock.device_id) - except RequestException as ex: - _LOGGER.error("Request error trying to retrieve door" - " status for %s. %s", lock.device_name, ex) - status_by_id[lock.device_id] = None - except Exception: - status_by_id[lock.device_id] = None - raise - - try: - detail_by_id[lock.device_id] = self._api.get_lock_detail( - self._access_token, lock.device_id) - except RequestException as ex: - _LOGGER.error("Request error trying to retrieve door" - " details for %s. %s", lock.device_name, ex) - detail_by_id[lock.device_id] = None - except Exception: - detail_by_id[lock.device_id] = None - raise - - _LOGGER.debug("Completed retrieving locks status") - self._lock_status_by_id = status_by_id - self._lock_detail_by_id = detail_by_id - - def lock(self, device_id): - """Lock the device.""" - return self._api.lock(self._access_token, device_id) - - def unlock(self, device_id): - """Unlock the device.""" - return self._api.unlock(self._access_token, device_id) diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py new file mode 100644 index 0000000000000..8e749dca46e5f --- /dev/null +++ b/homeassistant/components/august/__init__.py @@ -0,0 +1,350 @@ +"""Support for August devices.""" +import logging +from datetime import timedelta + +import voluptuous as vol +from requests import RequestException + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import ( + CONF_PASSWORD, CONF_USERNAME, CONF_TIMEOUT, EVENT_HOMEASSISTANT_STOP) +from homeassistant.helpers import discovery +from homeassistant.util import Throttle + +_LOGGER = logging.getLogger(__name__) + +_CONFIGURING = {} + +REQUIREMENTS = ['py-august==0.7.0'] + +DEFAULT_TIMEOUT = 10 +ACTIVITY_FETCH_LIMIT = 10 +ACTIVITY_INITIAL_FETCH_LIMIT = 20 + +CONF_LOGIN_METHOD = 'login_method' +CONF_INSTALL_ID = 'install_id' + +NOTIFICATION_ID = 'august_notification' +NOTIFICATION_TITLE = "August Setup" + +AUGUST_CONFIG_FILE = '.august.conf' + +DATA_AUGUST = 'august' +DOMAIN = 'august' +DEFAULT_ENTITY_NAMESPACE = 'august' +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) +DEFAULT_SCAN_INTERVAL = timedelta(seconds=5) +LOGIN_METHODS = ['phone', 'email'] + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_LOGIN_METHOD): vol.In(LOGIN_METHODS), + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_INSTALL_ID): cv.string, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + }) +}, extra=vol.ALLOW_EXTRA) + +AUGUST_COMPONENTS = [ + 'camera', 'binary_sensor', 'lock' +] + + +def request_configuration(hass, config, api, authenticator): + """Request configuration steps from the user.""" + configurator = hass.components.configurator + + def august_configuration_callback(data): + """Run when the configuration callback is called.""" + from august.authenticator import ValidationResult + + result = authenticator.validate_verification_code( + data.get('verification_code')) + + if result == ValidationResult.INVALID_VERIFICATION_CODE: + configurator.notify_errors(_CONFIGURING[DOMAIN], + "Invalid verification code") + elif result == ValidationResult.VALIDATED: + setup_august(hass, config, api, authenticator) + + if DOMAIN not in _CONFIGURING: + authenticator.send_verification_code() + + conf = config[DOMAIN] + username = conf.get(CONF_USERNAME) + login_method = conf.get(CONF_LOGIN_METHOD) + + _CONFIGURING[DOMAIN] = configurator.request_config( + NOTIFICATION_TITLE, + august_configuration_callback, + description="Please check your {} ({}) and enter the verification " + "code below".format(login_method, username), + submit_caption='Verify', + fields=[{ + 'id': 'verification_code', + 'name': "Verification code", + 'type': 'string'}] + ) + + +def setup_august(hass, config, api, authenticator): + """Set up the August component.""" + from august.authenticator import AuthenticationState + + authentication = None + try: + authentication = authenticator.authenticate() + except RequestException as ex: + _LOGGER.error("Unable to connect to August service: %s", str(ex)) + + hass.components.persistent_notification.create( + "Error: {}
" + "You will need to restart hass after fixing." + "".format(ex), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + + state = authentication.state + + if state == AuthenticationState.AUTHENTICATED: + if DOMAIN in _CONFIGURING: + hass.components.configurator.request_done(_CONFIGURING.pop(DOMAIN)) + + hass.data[DATA_AUGUST] = AugustData( + hass, api, authentication.access_token) + + for component in AUGUST_COMPONENTS: + discovery.load_platform(hass, component, DOMAIN, {}, config) + + return True + if state == AuthenticationState.BAD_PASSWORD: + _LOGGER.error("Invalid password provided") + return False + if state == AuthenticationState.REQUIRES_VALIDATION: + request_configuration(hass, config, api, authenticator) + return True + + return False + + +def setup(hass, config): + """Set up the August component.""" + from august.api import Api + from august.authenticator import Authenticator + from requests import Session + + conf = config[DOMAIN] + api_http_session = None + try: + api_http_session = Session() + except RequestException as ex: + _LOGGER.warning("Creating HTTP session failed with: %s", str(ex)) + + api = Api(timeout=conf.get(CONF_TIMEOUT), http_session=api_http_session) + + authenticator = Authenticator( + api, + conf.get(CONF_LOGIN_METHOD), + conf.get(CONF_USERNAME), + conf.get(CONF_PASSWORD), + install_id=conf.get(CONF_INSTALL_ID), + access_token_cache_file=hass.config.path(AUGUST_CONFIG_FILE)) + + def close_http_session(event): + """Close API sessions used to connect to August.""" + _LOGGER.debug("Closing August HTTP sessions") + if api_http_session: + try: + api_http_session.close() + except RequestException: + pass + + _LOGGER.debug("August HTTP session closed.") + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, close_http_session) + _LOGGER.debug("Registered for HASS stop event") + + return setup_august(hass, config, api, authenticator) + + +class AugustData: + """August data object.""" + + def __init__(self, hass, api, access_token): + """Init August data object.""" + self._hass = hass + self._api = api + self._access_token = access_token + self._doorbells = self._api.get_doorbells(self._access_token) or [] + self._locks = self._api.get_operable_locks(self._access_token) or [] + self._house_ids = [d.house_id for d in self._doorbells + self._locks] + + self._doorbell_detail_by_id = {} + self._lock_status_by_id = {} + self._lock_detail_by_id = {} + self._door_state_by_id = {} + self._activities_by_id = {} + + @property + def house_ids(self): + """Return a list of house_ids.""" + return self._house_ids + + @property + def doorbells(self): + """Return a list of doorbells.""" + return self._doorbells + + @property + def locks(self): + """Return a list of locks.""" + return self._locks + + def get_device_activities(self, device_id, *activity_types): + """Return a list of activities.""" + _LOGGER.debug("Getting device activities") + self._update_device_activities() + + activities = self._activities_by_id.get(device_id, []) + if activity_types: + return [a for a in activities if a.activity_type in activity_types] + return activities + + def get_latest_device_activity(self, device_id, *activity_types): + """Return latest activity.""" + activities = self.get_device_activities(device_id, *activity_types) + return next(iter(activities or []), None) + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def _update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT): + """Update data object with latest from August API.""" + _LOGGER.debug("Start retrieving device activities") + for house_id in self.house_ids: + _LOGGER.debug("Updating device activity for house id %s", + house_id) + + activities = self._api.get_house_activities(self._access_token, + house_id, + limit=limit) + + device_ids = {a.device_id for a in activities} + for device_id in device_ids: + self._activities_by_id[device_id] = [a for a in activities if + a.device_id == device_id] + _LOGGER.debug("Completed retrieving device activities") + + def get_doorbell_detail(self, doorbell_id): + """Return doorbell detail.""" + self._update_doorbells() + return self._doorbell_detail_by_id.get(doorbell_id) + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def _update_doorbells(self): + detail_by_id = {} + + _LOGGER.debug("Start retrieving doorbell details") + for doorbell in self._doorbells: + _LOGGER.debug("Updating doorbell status for %s", + doorbell.device_name) + try: + detail_by_id[doorbell.device_id] =\ + self._api.get_doorbell_detail( + self._access_token, doorbell.device_id) + except RequestException as ex: + _LOGGER.error("Request error trying to retrieve doorbell" + " status for %s. %s", doorbell.device_name, ex) + detail_by_id[doorbell.device_id] = None + except Exception: + detail_by_id[doorbell.device_id] = None + raise + + _LOGGER.debug("Completed retrieving doorbell details") + self._doorbell_detail_by_id = detail_by_id + + def get_lock_status(self, lock_id): + """Return status if the door is locked or unlocked. + + This is status for the lock itself. + """ + self._update_locks() + return self._lock_status_by_id.get(lock_id) + + def get_lock_detail(self, lock_id): + """Return lock detail.""" + self._update_locks() + return self._lock_detail_by_id.get(lock_id) + + def get_door_state(self, lock_id): + """Return status if the door is open or closed. + + This is the status from the door sensor. + """ + self._update_doors() + return self._door_state_by_id.get(lock_id) + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def _update_doors(self): + state_by_id = {} + + _LOGGER.debug("Start retrieving door status") + for lock in self._locks: + _LOGGER.debug("Updating door status for %s", + lock.device_name) + + try: + state_by_id[lock.device_id] = self._api.get_lock_door_status( + self._access_token, lock.device_id) + except RequestException as ex: + _LOGGER.error("Request error trying to retrieve door" + " status for %s. %s", lock.device_name, ex) + state_by_id[lock.device_id] = None + except Exception: + state_by_id[lock.device_id] = None + raise + + _LOGGER.debug("Completed retrieving door status") + self._door_state_by_id = state_by_id + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def _update_locks(self): + status_by_id = {} + detail_by_id = {} + + _LOGGER.debug("Start retrieving locks status") + for lock in self._locks: + _LOGGER.debug("Updating lock status for %s", + lock.device_name) + try: + status_by_id[lock.device_id] = self._api.get_lock_status( + self._access_token, lock.device_id) + except RequestException as ex: + _LOGGER.error("Request error trying to retrieve door" + " status for %s. %s", lock.device_name, ex) + status_by_id[lock.device_id] = None + except Exception: + status_by_id[lock.device_id] = None + raise + + try: + detail_by_id[lock.device_id] = self._api.get_lock_detail( + self._access_token, lock.device_id) + except RequestException as ex: + _LOGGER.error("Request error trying to retrieve door" + " details for %s. %s", lock.device_name, ex) + detail_by_id[lock.device_id] = None + except Exception: + detail_by_id[lock.device_id] = None + raise + + _LOGGER.debug("Completed retrieving locks status") + self._lock_status_by_id = status_by_id + self._lock_detail_by_id = detail_by_id + + def lock(self, device_id): + """Lock the device.""" + return self._api.lock(self._access_token, device_id) + + def unlock(self, device_id): + """Unlock the device.""" + return self._api.unlock(self._access_token, device_id) diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py new file mode 100644 index 0000000000000..1ad2d80cea8c0 --- /dev/null +++ b/homeassistant/components/august/binary_sensor.py @@ -0,0 +1,193 @@ +"""Support for August binary sensors.""" +import logging +from datetime import timedelta, datetime + +from homeassistant.components.august import DATA_AUGUST +from homeassistant.components.binary_sensor import (BinarySensorDevice) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['august'] + +SCAN_INTERVAL = timedelta(seconds=5) + + +def _retrieve_door_state(data, lock): + """Get the latest state of the DoorSense sensor.""" + return data.get_door_state(lock.device_id) + + +def _retrieve_online_state(data, doorbell): + """Get the latest state of the sensor.""" + detail = data.get_doorbell_detail(doorbell.device_id) + if detail is None: + return None + + return detail.is_online + + +def _retrieve_motion_state(data, doorbell): + from august.activity import ActivityType + return _activity_time_based_state(data, doorbell, + [ActivityType.DOORBELL_MOTION, + ActivityType.DOORBELL_DING]) + + +def _retrieve_ding_state(data, doorbell): + from august.activity import ActivityType + return _activity_time_based_state(data, doorbell, + [ActivityType.DOORBELL_DING]) + + +def _activity_time_based_state(data, doorbell, activity_types): + """Get the latest state of the sensor.""" + latest = data.get_latest_device_activity(doorbell.device_id, + *activity_types) + + if latest is not None: + start = latest.activity_start_time + end = latest.activity_end_time + timedelta(seconds=30) + return start <= datetime.now() <= end + return None + + +# Sensor types: Name, device_class, state_provider +SENSOR_TYPES_DOOR = { + 'door_open': ['Open', 'door', _retrieve_door_state], +} + +SENSOR_TYPES_DOORBELL = { + 'doorbell_ding': ['Ding', 'occupancy', _retrieve_ding_state], + 'doorbell_motion': ['Motion', 'motion', _retrieve_motion_state], + 'doorbell_online': ['Online', 'connectivity', _retrieve_online_state], +} + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the August binary sensors.""" + data = hass.data[DATA_AUGUST] + devices = [] + + from august.lock import LockDoorStatus + for door in data.locks: + for sensor_type in SENSOR_TYPES_DOOR: + state_provider = SENSOR_TYPES_DOOR[sensor_type][2] + if state_provider(data, door) is LockDoorStatus.UNKNOWN: + _LOGGER.debug( + "Not adding sensor class %s for lock %s ", + SENSOR_TYPES_DOOR[sensor_type][1], door.device_name + ) + continue + + _LOGGER.debug( + "Adding sensor class %s for %s", + SENSOR_TYPES_DOOR[sensor_type][1], door.device_name + ) + devices.append(AugustDoorBinarySensor(data, sensor_type, door)) + + for doorbell in data.doorbells: + for sensor_type in SENSOR_TYPES_DOORBELL: + _LOGGER.debug("Adding doorbell sensor class %s for %s", + SENSOR_TYPES_DOORBELL[sensor_type][1], + doorbell.device_name) + devices.append( + AugustDoorbellBinarySensor(data, sensor_type, + doorbell) + ) + + add_entities(devices, True) + + +class AugustDoorBinarySensor(BinarySensorDevice): + """Representation of an August Door binary sensor.""" + + def __init__(self, data, sensor_type, door): + """Initialize the sensor.""" + self._data = data + self._sensor_type = sensor_type + self._door = door + self._state = None + self._available = False + + @property + def available(self): + """Return the availability of this sensor.""" + return self._available + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self._state + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return SENSOR_TYPES_DOOR[self._sensor_type][1] + + @property + def name(self): + """Return the name of the binary sensor.""" + return "{} {}".format(self._door.device_name, + SENSOR_TYPES_DOOR[self._sensor_type][0]) + + def update(self): + """Get the latest state of the sensor.""" + state_provider = SENSOR_TYPES_DOOR[self._sensor_type][2] + self._state = state_provider(self._data, self._door) + self._available = self._state is not None + + 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.""" + + def __init__(self, data, sensor_type, doorbell): + """Initialize the sensor.""" + self._data = data + self._sensor_type = sensor_type + self._doorbell = doorbell + self._state = None + self._available = False + + @property + def available(self): + """Return the availability of this sensor.""" + return self._available + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self._state + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return SENSOR_TYPES_DOORBELL[self._sensor_type][1] + + @property + def name(self): + """Return the name of the binary sensor.""" + return "{} {}".format(self._doorbell.device_name, + SENSOR_TYPES_DOORBELL[self._sensor_type][0]) + + def update(self): + """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._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/august/camera.py b/homeassistant/components/august/camera.py new file mode 100644 index 0000000000000..7420bb040674d --- /dev/null +++ b/homeassistant/components/august/camera.py @@ -0,0 +1,76 @@ +"""Support for August camera.""" +from datetime import timedelta + +import requests + +from homeassistant.components.august import DATA_AUGUST, DEFAULT_TIMEOUT +from homeassistant.components.camera import Camera + +DEPENDENCIES = ['august'] + +SCAN_INTERVAL = timedelta(seconds=5) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up August cameras.""" + data = hass.data[DATA_AUGUST] + devices = [] + + for doorbell in data.doorbells: + devices.append(AugustCamera(data, doorbell, DEFAULT_TIMEOUT)) + + add_entities(devices, True) + + +class AugustCamera(Camera): + """An implementation of a Canary security camera.""" + + def __init__(self, data, doorbell, timeout): + """Initialize a Canary security camera.""" + super().__init__() + self._data = data + self._doorbell = doorbell + self._timeout = timeout + self._image_url = None + self._image_content = None + + @property + def name(self): + """Return the name of this device.""" + return self._doorbell.device_name + + @property + def is_recording(self): + """Return true if the device is recording.""" + return self._doorbell.has_subscription + + @property + def motion_detection_enabled(self): + """Return the camera motion detection status.""" + return True + + @property + def brand(self): + """Return the camera brand.""" + return 'August' + + @property + def model(self): + """Return the camera model.""" + return 'Doorbell' + + def camera_image(self): + """Return bytes of camera image.""" + latest = self._data.get_doorbell_detail(self._doorbell.device_id) + + if self._image_url is not latest.image_url: + self._image_url = latest.image_url + self._image_content = requests.get(self._image_url, + 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/august/lock.py b/homeassistant/components/august/lock.py new file mode 100644 index 0000000000000..ff355bbf87bf3 --- /dev/null +++ b/homeassistant/components/august/lock.py @@ -0,0 +1,97 @@ +"""Support for August lock.""" +import logging +from datetime import timedelta + +from homeassistant.components.august import DATA_AUGUST +from homeassistant.components.lock import LockDevice +from homeassistant.const import ATTR_BATTERY_LEVEL + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['august'] + +SCAN_INTERVAL = timedelta(seconds=5) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up August locks.""" + data = hass.data[DATA_AUGUST] + devices = [] + + for lock in data.locks: + _LOGGER.debug("Adding lock for %s", lock.device_name) + devices.append(AugustLock(data, lock)) + + add_entities(devices, True) + + +class AugustLock(LockDevice): + """Representation of an August lock.""" + + def __init__(self, data, lock): + """Initialize the lock.""" + self._data = data + self._lock = lock + self._lock_status = None + self._lock_detail = None + self._changed_by = None + self._available = False + + def lock(self, **kwargs): + """Lock the device.""" + self._data.lock(self._lock.device_id) + + def unlock(self, **kwargs): + """Unlock the device.""" + self._data.unlock(self._lock.device_id) + + def update(self): + """Get the latest state of the sensor.""" + self._lock_status = self._data.get_lock_status(self._lock.device_id) + self._available = self._lock_status is not None + + self._lock_detail = self._data.get_lock_detail(self._lock.device_id) + + from august.activity import ActivityType + activity = self._data.get_latest_device_activity( + self._lock.device_id, + ActivityType.LOCK_OPERATION) + + if activity is not None: + self._changed_by = activity.operated_by + + @property + def name(self): + """Return the name of this device.""" + return self._lock.device_name + + @property + def available(self): + """Return the availability of this sensor.""" + return self._available + + @property + def is_locked(self): + """Return true if device is on.""" + from august.lock import LockStatus + return self._lock_status is LockStatus.LOCKED + + @property + def changed_by(self): + """Last change triggered by.""" + return self._changed_by + + @property + def device_state_attributes(self): + """Return the device specific state attributes.""" + if self._lock_detail is None: + return None + + return { + ATTR_BATTERY_LEVEL: self._lock_detail.battery_level, + } + + @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 0000000000000..f461f376d166c --- /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 7efc50d534cb2..c5278c63f798c 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 836901cde303e..35cf695f1e3a6 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 ec47479eac849..6cc7e3dae7df3 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 33ef00da38068..8f838ea6d6b6c 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 6d7a44291c99a..1b022316676fb 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 70e01174078a7..20c689d74cf43 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 1fa0d54061057..5f52da745ee14 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 aa51e6310269f..bf45abb88f0e0 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 4e47026d8d16d..f4d7f69c07a73 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 509195689a15a..07fbf716e1c2d 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 347b3f94e7da3..ed86d52584f87 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.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 d57e190490fe9..ce6d6eb444689 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 8b6e907f7b8d1..da8bc9f8629ce 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 f4afc8a601af2..f65b5cf885c65 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 0c3c0941a9e31..e2d79eede8d72 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 fd2e603445c38..df723272a7acd 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/axis/binary_sensor.py b/homeassistant/components/axis/binary_sensor.py new file mode 100644 index 0000000000000..11014dc4bc97c --- /dev/null +++ b/homeassistant/components/axis/binary_sensor.py @@ -0,0 +1,86 @@ +"""Support for Axis binary sensors.""" +from datetime import timedelta +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.const import ( + ATTR_LOCATION, CONF_EVENT, CONF_NAME, CONF_TRIGGER_TIME) +from homeassistant.core import callback +from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.util.dt import utcnow + +DEPENDENCIES = ['axis'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Axis binary devices.""" + add_entities([AxisBinarySensor(discovery_info)], True) + + +class AxisBinarySensor(BinarySensorDevice): + """Representation of a binary Axis event.""" + + def __init__(self, event_config): + """Initialize the Axis binary sensor.""" + self.axis_event = event_config[CONF_EVENT] + self.device_name = event_config[CONF_NAME] + self.location = event_config[ATTR_LOCATION] + self.delay = event_config[CONF_TRIGGER_TIME] + self.remove_timer = None + + async def async_added_to_hass(self): + """Subscribe sensors events.""" + self.axis_event.callback = self._update_callback + + def _update_callback(self): + """Update the sensor's state, if needed.""" + if self.remove_timer is not None: + self.remove_timer() + self.remove_timer = None + + if self.delay == 0 or self.is_on: + self.schedule_update_ha_state() + else: # Run timer to delay updating the state + @callback + def _delay_update(now): + """Timer callback for sensor update.""" + _LOGGER.debug("%s called delayed (%s sec) update", + self.name, self.delay) + self.async_schedule_update_ha_state() + self.remove_timer = None + + self.remove_timer = async_track_point_in_utc_time( + self.hass, _delay_update, + utcnow() + timedelta(seconds=self.delay)) + + @property + def is_on(self): + """Return true if event is active.""" + return self.axis_event.is_tripped + + @property + def name(self): + """Return the name of the event.""" + return '{}_{}_{}'.format( + self.device_name, self.axis_event.event_type, self.axis_event.id) + + @property + def device_class(self): + """Return the class of the event.""" + return self.axis_event.event_class + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def device_state_attributes(self): + """Return the state attributes of the event.""" + attr = {} + + attr[ATTR_LOCATION] = self.location + + return attr diff --git a/homeassistant/components/axis/camera.py b/homeassistant/components/axis/camera.py new file mode 100644 index 0000000000000..b9e969efec101 --- /dev/null +++ b/homeassistant/components/axis/camera.py @@ -0,0 +1,58 @@ +"""Support for Axis camera streaming.""" +import logging + +from homeassistant.components.camera.mjpeg import ( + CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera, filter_urllib3_logging) +from homeassistant.const import ( + CONF_AUTHENTICATION, CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, + CONF_USERNAME, HTTP_DIGEST_AUTHENTICATION) +from homeassistant.helpers.dispatcher import dispatcher_connect + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'axis' +DEPENDENCIES = [DOMAIN] + + +def _get_image_url(host, port, mode): + """Set the URL to get the image.""" + if mode == 'mjpeg': + return 'http://{}:{}/axis-cgi/mjpg/video.cgi'.format(host, port) + if mode == 'single': + return 'http://{}:{}/axis-cgi/jpg/image.cgi'.format(host, port) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Axis camera.""" + filter_urllib3_logging() + + camera_config = { + CONF_NAME: discovery_info[CONF_NAME], + CONF_USERNAME: discovery_info[CONF_USERNAME], + CONF_PASSWORD: discovery_info[CONF_PASSWORD], + CONF_MJPEG_URL: _get_image_url( + discovery_info[CONF_HOST], str(discovery_info[CONF_PORT]), + 'mjpeg'), + CONF_STILL_IMAGE_URL: _get_image_url( + discovery_info[CONF_HOST], str(discovery_info[CONF_PORT]), + 'single'), + CONF_AUTHENTICATION: HTTP_DIGEST_AUTHENTICATION, + } + add_entities([AxisCamera( + hass, camera_config, str(discovery_info[CONF_PORT]))]) + + +class AxisCamera(MjpegCamera): + """Representation of a Axis camera.""" + + def __init__(self, hass, config, port): + """Initialize Axis Communications camera component.""" + super().__init__(config) + self.port = port + dispatcher_connect( + hass, DOMAIN + '_' + config[CONF_NAME] + '_new_ip', self._new_ip) + + def _new_ip(self, host): + """Set new IP for video stream.""" + self._mjpeg_url = _get_image_url(host, self.port, 'mjpeg') + self._still_image_url = _get_image_url(host, self.port, 'single') diff --git a/homeassistant/components/bbb_gpio.py b/homeassistant/components/bbb_gpio.py deleted file mode 100644 index e3f327f1d5cf1..0000000000000 --- a/homeassistant/components/bbb_gpio.py +++ /dev/null @@ -1,71 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) - -REQUIREMENTS = ['Adafruit_BBIO==1.0.0'] - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'bbb_gpio' - - -def setup(hass, config): - """Set up the BeagleBone Black GPIO component.""" - # pylint: disable=import-error - from Adafruit_BBIO import GPIO - - def cleanup_gpio(event): - """Stuff to do before stopping.""" - GPIO.cleanup() - - def prepare_gpio(event): - """Stuff to do when home assistant starts.""" - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_gpio) - - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, prepare_gpio) - return True - - -def setup_output(pin): - """Set up a GPIO as output.""" - # pylint: disable=import-error - from Adafruit_BBIO import GPIO - GPIO.setup(pin, GPIO.OUT) - - -def setup_input(pin, pull_mode): - """Set up a GPIO as input.""" - # pylint: disable=import-error - from Adafruit_BBIO import GPIO - GPIO.setup(pin, GPIO.IN, - GPIO.PUD_DOWN if pull_mode == 'DOWN' - else GPIO.PUD_UP) - - -def write_output(pin, value): - """Write a value to a GPIO.""" - # pylint: disable=import-error - from Adafruit_BBIO import GPIO - GPIO.output(pin, value) - - -def read_input(pin): - """Read a value from a GPIO.""" - # pylint: disable=import-error - from Adafruit_BBIO import GPIO - return GPIO.input(pin) is GPIO.HIGH - - -def edge_detect(pin, event_callback, bounce): - """Add detection for RISING and FALLING events.""" - # pylint: disable=import-error - from Adafruit_BBIO import GPIO - GPIO.add_event_detect( - pin, GPIO.BOTH, callback=event_callback, bouncetime=bounce) diff --git a/homeassistant/components/bbb_gpio/__init__.py b/homeassistant/components/bbb_gpio/__init__.py new file mode 100644 index 0000000000000..7749af8f335c4 --- /dev/null +++ b/homeassistant/components/bbb_gpio/__init__.py @@ -0,0 +1,66 @@ +"""Support for controlling GPIO pins of a Beaglebone Black.""" +import logging + +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) + +REQUIREMENTS = ['Adafruit_BBIO==1.0.0'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'bbb_gpio' + + +def setup(hass, config): + """Set up the BeagleBone Black GPIO component.""" + # pylint: disable=import-error + from Adafruit_BBIO import GPIO + + def cleanup_gpio(event): + """Stuff to do before stopping.""" + GPIO.cleanup() + + def prepare_gpio(event): + """Stuff to do when home assistant starts.""" + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_gpio) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, prepare_gpio) + return True + + +def setup_output(pin): + """Set up a GPIO as output.""" + # pylint: disable=import-error + from Adafruit_BBIO import GPIO + GPIO.setup(pin, GPIO.OUT) + + +def setup_input(pin, pull_mode): + """Set up a GPIO as input.""" + # pylint: disable=import-error + from Adafruit_BBIO import GPIO + GPIO.setup(pin, GPIO.IN, + GPIO.PUD_DOWN if pull_mode == 'DOWN' + else GPIO.PUD_UP) + + +def write_output(pin, value): + """Write a value to a GPIO.""" + # pylint: disable=import-error + from Adafruit_BBIO import GPIO + GPIO.output(pin, value) + + +def read_input(pin): + """Read a value from a GPIO.""" + # pylint: disable=import-error + from Adafruit_BBIO import GPIO + return GPIO.input(pin) is GPIO.HIGH + + +def edge_detect(pin, event_callback, bounce): + """Add detection for RISING and FALLING events.""" + # pylint: disable=import-error + from Adafruit_BBIO import GPIO + GPIO.add_event_detect( + pin, GPIO.BOTH, callback=event_callback, bouncetime=bounce) diff --git a/homeassistant/components/bbb_gpio/binary_sensor.py b/homeassistant/components/bbb_gpio/binary_sensor.py new file mode 100644 index 0000000000000..1ee371dcc2a6a --- /dev/null +++ b/homeassistant/components/bbb_gpio/binary_sensor.py @@ -0,0 +1,84 @@ +"""Support for binary sensor using Beaglebone Black GPIO.""" +import logging + +import voluptuous as vol + +from homeassistant.components import bbb_gpio +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA) +from homeassistant.const import (DEVICE_DEFAULT_NAME, CONF_NAME) +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['bbb_gpio'] + +CONF_PINS = 'pins' +CONF_BOUNCETIME = 'bouncetime' +CONF_INVERT_LOGIC = 'invert_logic' +CONF_PULL_MODE = 'pull_mode' + +DEFAULT_BOUNCETIME = 50 +DEFAULT_INVERT_LOGIC = False +DEFAULT_PULL_MODE = 'UP' + +PIN_SCHEMA = vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_BOUNCETIME, default=DEFAULT_BOUNCETIME): cv.positive_int, + vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, + vol.Optional(CONF_PULL_MODE, default=DEFAULT_PULL_MODE): + vol.In(['UP', 'DOWN']) +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_PINS, default={}): + vol.Schema({cv.string: PIN_SCHEMA}), +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Beaglebone Black GPIO devices.""" + pins = config.get(CONF_PINS) + + binary_sensors = [] + + for pin, params in pins.items(): + binary_sensors.append(BBBGPIOBinarySensor(pin, params)) + add_entities(binary_sensors) + + +class BBBGPIOBinarySensor(BinarySensorDevice): + """Representation of a binary sensor that uses Beaglebone Black GPIO.""" + + def __init__(self, pin, params): + """Initialize the Beaglebone Black binary sensor.""" + self._pin = pin + self._name = params.get(CONF_NAME) or DEVICE_DEFAULT_NAME + self._bouncetime = params.get(CONF_BOUNCETIME) + self._pull_mode = params.get(CONF_PULL_MODE) + self._invert_logic = params.get(CONF_INVERT_LOGIC) + + bbb_gpio.setup_input(self._pin, self._pull_mode) + self._state = bbb_gpio.read_input(self._pin) + + def read_gpio(pin): + """Read state from GPIO.""" + self._state = bbb_gpio.read_input(self._pin) + self.schedule_update_ha_state() + + bbb_gpio.edge_detect(self._pin, read_gpio, self._bouncetime) + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def is_on(self): + """Return the state of the entity.""" + return self._state != self._invert_logic diff --git a/homeassistant/components/bbb_gpio/switch.py b/homeassistant/components/bbb_gpio/switch.py new file mode 100644 index 0000000000000..3ad46fd61aedf --- /dev/null +++ b/homeassistant/components/bbb_gpio/switch.py @@ -0,0 +1,83 @@ +"""Allows to configure a switch using BeagleBone Black GPIO.""" +import logging + +import voluptuous as vol + +from homeassistant.components.switch import PLATFORM_SCHEMA +from homeassistant.components import bbb_gpio +from homeassistant.const import (DEVICE_DEFAULT_NAME, CONF_NAME) +from homeassistant.helpers.entity import ToggleEntity +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['bbb_gpio'] + +CONF_PINS = 'pins' +CONF_INITIAL = 'initial' +CONF_INVERT_LOGIC = 'invert_logic' + +PIN_SCHEMA = vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_INITIAL, default=False): cv.boolean, + vol.Optional(CONF_INVERT_LOGIC, default=False): cv.boolean, +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_PINS, default={}): vol.Schema({cv.string: PIN_SCHEMA}), +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the BeagleBone Black GPIO devices.""" + pins = config.get(CONF_PINS) + + switches = [] + for pin, params in pins.items(): + switches.append(BBBGPIOSwitch(pin, params)) + add_entities(switches) + + +class BBBGPIOSwitch(ToggleEntity): + """Representation of a BeagleBone Black GPIO.""" + + def __init__(self, pin, params): + """Initialize the pin.""" + self._pin = pin + self._name = params.get(CONF_NAME) or DEVICE_DEFAULT_NAME + self._state = params.get(CONF_INITIAL) + self._invert_logic = params.get(CONF_INVERT_LOGIC) + + bbb_gpio.setup_output(self._pin) + + if self._state is False: + bbb_gpio.write_output(self._pin, 1 if self._invert_logic else 0) + else: + bbb_gpio.write_output(self._pin, 0 if self._invert_logic else 1) + + @property + def name(self): + """Return the name of the switch.""" + return self._name + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def is_on(self): + """Return true if device is on.""" + return self._state + + def turn_on(self, **kwargs): + """Turn the device on.""" + bbb_gpio.write_output(self._pin, 0 if self._invert_logic else 1) + self._state = True + self.schedule_update_ha_state() + + def turn_off(self, **kwargs): + """Turn the device off.""" + bbb_gpio.write_output(self._pin, 1 if self._invert_logic else 0) + self._state = False + self.schedule_update_ha_state() diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index 7b2da21ff6a68..9972e4dca3b4c 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -13,7 +13,8 @@ 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/abode.py b/homeassistant/components/binary_sensor/abode.py deleted file mode 100644 index a821abf445b67..0000000000000 --- a/homeassistant/components/binary_sensor/abode.py +++ /dev/null @@ -1,74 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.components.abode import (AbodeDevice, AbodeAutomation, - DOMAIN as ABODE_DOMAIN) -from homeassistant.components.binary_sensor import BinarySensorDevice - - -DEPENDENCIES = ['abode'] - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up a sensor for an Abode device.""" - import abodepy.helpers.constants as CONST - import abodepy.helpers.timeline as TIMELINE - - data = hass.data[ABODE_DOMAIN] - - device_types = [CONST.TYPE_CONNECTIVITY, CONST.TYPE_MOISTURE, - CONST.TYPE_MOTION, CONST.TYPE_OCCUPANCY, - CONST.TYPE_OPENING] - - devices = [] - for device in data.abode.get_devices(generic_type=device_types): - if data.is_excluded(device): - continue - - devices.append(AbodeBinarySensor(data, device)) - - for automation in data.abode.get_automations( - generic_type=CONST.TYPE_QUICK_ACTION): - if data.is_automation_excluded(automation): - continue - - devices.append(AbodeQuickActionBinarySensor( - data, automation, TIMELINE.AUTOMATION_EDIT_GROUP)) - - data.devices.extend(devices) - - add_entities(devices) - - -class AbodeBinarySensor(AbodeDevice, BinarySensorDevice): - """A binary sensor implementation for Abode device.""" - - @property - def is_on(self): - """Return True if the binary sensor is on.""" - return self._device.is_on - - @property - def device_class(self): - """Return the class of the binary sensor.""" - return self._device.generic_type - - -class AbodeQuickActionBinarySensor(AbodeAutomation, BinarySensorDevice): - """A binary sensor implementation for Abode quick action automations.""" - - def trigger(self): - """Trigger a quick automation.""" - self._automation.trigger() - - @property - def is_on(self): - """Return True if the binary sensor is on.""" - return self._automation.is_active diff --git a/homeassistant/components/binary_sensor/ads.py b/homeassistant/components/binary_sensor/ads.py deleted file mode 100644 index 1ee56cac9d3f4..0000000000000 --- a/homeassistant/components/binary_sensor/ads.py +++ /dev/null @@ -1,82 +0,0 @@ -""" -Support for ADS binary sensors. - -For more details about this platform, please refer to the documentation. -https://home-assistant.io/components/binary_sensor.ads/ -""" -import logging - -import voluptuous as vol - -from homeassistant.components.ads import CONF_ADS_VAR, DATA_ADS -from homeassistant.components.binary_sensor import ( - DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice) -from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = 'ADS binary sensor' -DEPENDENCIES = ['ads'] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_ADS_VAR): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Binary Sensor platform for ADS.""" - ads_hub = hass.data.get(DATA_ADS) - - ads_var = config.get(CONF_ADS_VAR) - name = config.get(CONF_NAME) - device_class = config.get(CONF_DEVICE_CLASS) - - ads_sensor = AdsBinarySensor(ads_hub, name, ads_var, device_class) - add_entities([ads_sensor]) - - -class AdsBinarySensor(BinarySensorDevice): - """Representation of ADS binary sensors.""" - - def __init__(self, ads_hub, name, ads_var, device_class): - """Initialize ADS binary sensor.""" - self._name = name - self._state = False - self._device_class = device_class or 'moving' - self._ads_hub = ads_hub - self.ads_var = ads_var - - async def async_added_to_hass(self): - """Register device notification.""" - def update(name, value): - """Handle device notifications.""" - _LOGGER.debug('Variable %s changed its value to %d', name, value) - self._state = value - self.schedule_update_ha_state() - - self.hass.async_add_job( - self._ads_hub.add_device_notification, - self.ads_var, self._ads_hub.PLCTYPE_BOOL, update) - - @property - def name(self): - """Return the default name of the binary sensor.""" - return self._name - - @property - def device_class(self): - """Return the device class.""" - return self._device_class - - @property - def is_on(self): - """Return if the binary sensor is on.""" - return self._state - - @property - def should_poll(self): - """Return False because entity pushes its state to HA.""" - return False diff --git a/homeassistant/components/binary_sensor/alarmdecoder.py b/homeassistant/components/binary_sensor/alarmdecoder.py deleted file mode 100644 index d8fddeaa540b2..0000000000000 --- a/homeassistant/components/binary_sensor/alarmdecoder.py +++ /dev/null @@ -1,145 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.alarmdecoder import ( - ZONE_SCHEMA, CONF_ZONES, CONF_ZONE_NAME, CONF_ZONE_TYPE, - CONF_ZONE_RFID, CONF_ZONE_LOOP, SIGNAL_ZONE_FAULT, SIGNAL_ZONE_RESTORE, - SIGNAL_RFX_MESSAGE, SIGNAL_REL_MESSAGE, CONF_RELAY_ADDR, - CONF_RELAY_CHAN) - -DEPENDENCIES = ['alarmdecoder'] - -_LOGGER = logging.getLogger(__name__) - -ATTR_RF_BIT0 = 'rf_bit0' -ATTR_RF_LOW_BAT = 'rf_low_battery' -ATTR_RF_SUPERVISED = 'rf_supervised' -ATTR_RF_BIT3 = 'rf_bit3' -ATTR_RF_LOOP3 = 'rf_loop3' -ATTR_RF_LOOP2 = 'rf_loop2' -ATTR_RF_LOOP4 = 'rf_loop4' -ATTR_RF_LOOP1 = 'rf_loop1' - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the AlarmDecoder binary sensor devices.""" - configured_zones = discovery_info[CONF_ZONES] - - devices = [] - for zone_num in configured_zones: - device_config_data = ZONE_SCHEMA(configured_zones[zone_num]) - zone_type = device_config_data[CONF_ZONE_TYPE] - zone_name = device_config_data[CONF_ZONE_NAME] - zone_rfid = device_config_data.get(CONF_ZONE_RFID) - zone_loop = device_config_data.get(CONF_ZONE_LOOP) - relay_addr = device_config_data.get(CONF_RELAY_ADDR) - relay_chan = device_config_data.get(CONF_RELAY_CHAN) - device = AlarmDecoderBinarySensor( - zone_num, zone_name, zone_type, zone_rfid, zone_loop, relay_addr, - relay_chan) - devices.append(device) - - add_entities(devices) - - return True - - -class AlarmDecoderBinarySensor(BinarySensorDevice): - """Representation of an AlarmDecoder binary sensor.""" - - def __init__(self, zone_number, zone_name, zone_type, zone_rfid, zone_loop, - relay_addr, relay_chan): - """Initialize the binary_sensor.""" - self._zone_number = zone_number - self._zone_type = zone_type - self._state = None - self._name = zone_name - self._rfid = zone_rfid - self._loop = zone_loop - self._rfstate = None - self._relay_addr = relay_addr - self._relay_chan = relay_chan - - async def async_added_to_hass(self): - """Register callbacks.""" - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_ZONE_FAULT, self._fault_callback) - - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_ZONE_RESTORE, self._restore_callback) - - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_RFX_MESSAGE, self._rfx_message_callback) - - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_REL_MESSAGE, self._rel_message_callback) - - @property - def name(self): - """Return the name of the entity.""" - return self._name - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def device_state_attributes(self): - """Return the state attributes.""" - attr = {} - if self._rfid and self._rfstate is not None: - attr[ATTR_RF_BIT0] = bool(self._rfstate & 0x01) - attr[ATTR_RF_LOW_BAT] = bool(self._rfstate & 0x02) - attr[ATTR_RF_SUPERVISED] = bool(self._rfstate & 0x04) - attr[ATTR_RF_BIT3] = bool(self._rfstate & 0x08) - attr[ATTR_RF_LOOP3] = bool(self._rfstate & 0x10) - attr[ATTR_RF_LOOP2] = bool(self._rfstate & 0x20) - attr[ATTR_RF_LOOP4] = bool(self._rfstate & 0x40) - attr[ATTR_RF_LOOP1] = bool(self._rfstate & 0x80) - return attr - - @property - def is_on(self): - """Return true if sensor is on.""" - return self._state == 1 - - @property - def device_class(self): - """Return the class of this sensor, from DEVICE_CLASSES.""" - return self._zone_type - - def _fault_callback(self, zone): - """Update the zone's state, if needed.""" - if zone is None or int(zone) == self._zone_number: - self._state = 1 - self.schedule_update_ha_state() - - def _restore_callback(self, zone): - """Update the zone's state, if needed.""" - if zone is None or int(zone) == self._zone_number: - self._state = 0 - self.schedule_update_ha_state() - - def _rfx_message_callback(self, message): - """Update RF state.""" - if self._rfid and message and message.serial_number == self._rfid: - self._rfstate = message.value - if self._loop: - self._state = 1 if message.loop[self._loop - 1] else 0 - self.schedule_update_ha_state() - - def _rel_message_callback(self, message): - """Update relay state.""" - if (self._relay_addr == message.address and - self._relay_chan == message.channel): - _LOGGER.debug("Relay %d:%d value:%d", message.address, - message.channel, message.value) - self._state = message.value - self.schedule_update_ha_state() diff --git a/homeassistant/components/binary_sensor/android_ip_webcam.py b/homeassistant/components/binary_sensor/android_ip_webcam.py deleted file mode 100644 index 085bafd3ae3f6..0000000000000 --- a/homeassistant/components/binary_sensor/android_ip_webcam.py +++ /dev/null @@ -1,59 +0,0 @@ -""" -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/ -""" -from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.android_ip_webcam import ( - KEY_MAP, DATA_IP_WEBCAM, AndroidIPCamEntity, CONF_HOST, CONF_NAME) - -DEPENDENCIES = ['android_ip_webcam'] - - -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 - - host = discovery_info[CONF_HOST] - name = discovery_info[CONF_NAME] - ipcam = hass.data[DATA_IP_WEBCAM][host] - - async_add_entities( - [IPWebcamBinarySensor(name, host, ipcam, 'motion_active')], True) - - -class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorDevice): - """Representation of an IP Webcam binary sensor.""" - - def __init__(self, name, host, ipcam, sensor): - """Initialize the binary sensor.""" - super().__init__(host, ipcam) - - self._sensor = sensor - self._mapped_name = KEY_MAP.get(self._sensor, self._sensor) - self._name = '{} {}'.format(name, self._mapped_name) - self._state = None - self._unit = None - - @property - def name(self): - """Return the name of the binary sensor, if any.""" - return self._name - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self._state - - async def async_update(self): - """Retrieve latest state.""" - state, _ = self._ipcam.export_sensor(self._sensor) - self._state = state == 1.0 - - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return 'motion' diff --git a/homeassistant/components/binary_sensor/apcupsd.py b/homeassistant/components/binary_sensor/apcupsd.py deleted file mode 100644 index f876b8cc34b1c..0000000000000 --- a/homeassistant/components/binary_sensor/apcupsd.py +++ /dev/null @@ -1,49 +0,0 @@ -""" -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/ -""" -import voluptuous as vol - -from homeassistant.components.binary_sensor import ( - BinarySensorDevice, PLATFORM_SCHEMA) -from homeassistant.const import CONF_NAME -import homeassistant.helpers.config_validation as cv -from homeassistant.components import apcupsd - -DEFAULT_NAME = 'UPS Online Status' -DEPENDENCIES = [apcupsd.DOMAIN] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up an APCUPSd Online Status binary sensor.""" - add_entities([OnlineStatus(config, apcupsd.DATA)], True) - - -class OnlineStatus(BinarySensorDevice): - """Representation of an UPS online status.""" - - def __init__(self, config, data): - """Initialize the APCUPSd binary device.""" - self._config = config - self._data = data - self._state = None - - @property - def name(self): - """Return the name of the UPS online status sensor.""" - return self._config.get(CONF_NAME) - - @property - def is_on(self): - """Return true if the UPS is online, else false.""" - return self._state == apcupsd.VALUE_ONLINE - - def update(self): - """Get the status report from APCUPSd and set this entity's state.""" - self._state = self._data.status[apcupsd.KEY_STATUS] diff --git a/homeassistant/components/binary_sensor/august.py b/homeassistant/components/binary_sensor/august.py deleted file mode 100644 index 4116a791b01cd..0000000000000 --- a/homeassistant/components/binary_sensor/august.py +++ /dev/null @@ -1,184 +0,0 @@ -""" -Support for August binary sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.august/ -""" -import logging -from datetime import timedelta, datetime - -from homeassistant.components.august import DATA_AUGUST -from homeassistant.components.binary_sensor import (BinarySensorDevice) - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['august'] - -SCAN_INTERVAL = timedelta(seconds=5) - - -def _retrieve_door_state(data, lock): - """Get the latest state of the DoorSense sensor.""" - return data.get_door_state(lock.device_id) - - -def _retrieve_online_state(data, doorbell): - """Get the latest state of the sensor.""" - detail = data.get_doorbell_detail(doorbell.device_id) - if detail is None: - return None - - return detail.is_online - - -def _retrieve_motion_state(data, doorbell): - from august.activity import ActivityType - return _activity_time_based_state(data, doorbell, - [ActivityType.DOORBELL_MOTION, - ActivityType.DOORBELL_DING]) - - -def _retrieve_ding_state(data, doorbell): - from august.activity import ActivityType - return _activity_time_based_state(data, doorbell, - [ActivityType.DOORBELL_DING]) - - -def _activity_time_based_state(data, doorbell, activity_types): - """Get the latest state of the sensor.""" - latest = data.get_latest_device_activity(doorbell.device_id, - *activity_types) - - if latest is not None: - start = latest.activity_start_time - end = latest.activity_end_time + timedelta(seconds=30) - return start <= datetime.now() <= end - return None - - -# Sensor types: Name, device_class, state_provider -SENSOR_TYPES_DOOR = { - 'door_open': ['Open', 'door', _retrieve_door_state], -} - -SENSOR_TYPES_DOORBELL = { - 'doorbell_ding': ['Ding', 'occupancy', _retrieve_ding_state], - 'doorbell_motion': ['Motion', 'motion', _retrieve_motion_state], - 'doorbell_online': ['Online', 'connectivity', _retrieve_online_state], -} - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the August binary sensors.""" - data = hass.data[DATA_AUGUST] - devices = [] - - from august.lock import LockDoorStatus - for door in data.locks: - for sensor_type in SENSOR_TYPES_DOOR: - state_provider = SENSOR_TYPES_DOOR[sensor_type][2] - if state_provider(data, door) is LockDoorStatus.UNKNOWN: - _LOGGER.debug( - "Not adding sensor class %s for lock %s ", - SENSOR_TYPES_DOOR[sensor_type][1], door.device_name - ) - continue - - _LOGGER.debug( - "Adding sensor class %s for %s", - SENSOR_TYPES_DOOR[sensor_type][1], door.device_name - ) - devices.append(AugustDoorBinarySensor(data, sensor_type, door)) - - for doorbell in data.doorbells: - for sensor_type in SENSOR_TYPES_DOORBELL: - _LOGGER.debug("Adding doorbell sensor class %s for %s", - SENSOR_TYPES_DOORBELL[sensor_type][1], - doorbell.device_name) - devices.append( - AugustDoorbellBinarySensor(data, sensor_type, - doorbell) - ) - - add_entities(devices, True) - - -class AugustDoorBinarySensor(BinarySensorDevice): - """Representation of an August Door binary sensor.""" - - def __init__(self, data, sensor_type, door): - """Initialize the sensor.""" - self._data = data - self._sensor_type = sensor_type - self._door = door - self._state = None - self._available = False - - @property - def available(self): - """Return the availability of this sensor.""" - return self._available - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self._state - - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return SENSOR_TYPES_DOOR[self._sensor_type][1] - - @property - def name(self): - """Return the name of the binary sensor.""" - return "{} {}".format(self._door.device_name, - SENSOR_TYPES_DOOR[self._sensor_type][0]) - - def update(self): - """Get the latest state of the sensor.""" - state_provider = SENSOR_TYPES_DOOR[self._sensor_type][2] - self._state = state_provider(self._data, self._door) - self._available = self._state is not None - - from august.lock import LockDoorStatus - self._state = self._state == LockDoorStatus.OPEN - - -class AugustDoorbellBinarySensor(BinarySensorDevice): - """Representation of an August binary sensor.""" - - def __init__(self, data, sensor_type, doorbell): - """Initialize the sensor.""" - self._data = data - self._sensor_type = sensor_type - self._doorbell = doorbell - self._state = None - self._available = False - - @property - def available(self): - """Return the availability of this sensor.""" - return self._available - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self._state - - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return SENSOR_TYPES_DOORBELL[self._sensor_type][1] - - @property - def name(self): - """Return the name of the binary sensor.""" - return "{} {}".format(self._doorbell.device_name, - SENSOR_TYPES_DOORBELL[self._sensor_type][0]) - - def update(self): - """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 diff --git a/homeassistant/components/binary_sensor/axis.py b/homeassistant/components/binary_sensor/axis.py deleted file mode 100644 index 671bbc730d0fa..0000000000000 --- a/homeassistant/components/binary_sensor/axis.py +++ /dev/null @@ -1,91 +0,0 @@ -""" -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/ -""" -from datetime import timedelta -import logging - -from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.const import ( - ATTR_LOCATION, CONF_EVENT, CONF_NAME, CONF_TRIGGER_TIME) -from homeassistant.core import callback -from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.util.dt import utcnow - -DEPENDENCIES = ['axis'] - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Axis binary devices.""" - add_entities([AxisBinarySensor(discovery_info)], True) - - -class AxisBinarySensor(BinarySensorDevice): - """Representation of a binary Axis event.""" - - def __init__(self, event_config): - """Initialize the Axis binary sensor.""" - self.axis_event = event_config[CONF_EVENT] - self.device_name = event_config[CONF_NAME] - self.location = event_config[ATTR_LOCATION] - self.delay = event_config[CONF_TRIGGER_TIME] - self.remove_timer = None - - async def async_added_to_hass(self): - """Subscribe sensors events.""" - self.axis_event.callback = self._update_callback - - def _update_callback(self): - """Update the sensor's state, if needed.""" - if self.remove_timer is not None: - self.remove_timer() - self.remove_timer = None - - if self.delay == 0 or self.is_on: - self.schedule_update_ha_state() - else: # Run timer to delay updating the state - @callback - def _delay_update(now): - """Timer callback for sensor update.""" - _LOGGER.debug("%s called delayed (%s sec) update", - self.name, self.delay) - self.async_schedule_update_ha_state() - self.remove_timer = None - - self.remove_timer = async_track_point_in_utc_time( - self.hass, _delay_update, - utcnow() + timedelta(seconds=self.delay)) - - @property - def is_on(self): - """Return true if event is active.""" - return self.axis_event.is_tripped - - @property - def name(self): - """Return the name of the event.""" - return '{}_{}_{}'.format( - self.device_name, self.axis_event.event_type, self.axis_event.id) - - @property - def device_class(self): - """Return the class of the event.""" - return self.axis_event.event_class - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def device_state_attributes(self): - """Return the state attributes of the event.""" - attr = {} - - attr[ATTR_LOCATION] = self.location - - return attr diff --git a/homeassistant/components/binary_sensor/bayesian.py b/homeassistant/components/binary_sensor/bayesian.py index f7802f0f29de8..97889ea749723 100644 --- a/homeassistant/components/binary_sensor/bayesian.py +++ b/homeassistant/components/binary_sensor/bayesian.py @@ -4,29 +4,27 @@ 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 @@ 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 @@ def __init__(self, name, prior, observations, probability_threshold, 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 @@ def async_threshold_sensor_state_listener(entity, old_state, 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 @@ def _process_state(self, entity_observation): 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/binary_sensor/bbb_gpio.py b/homeassistant/components/binary_sensor/bbb_gpio.py deleted file mode 100644 index 8968b68036983..0000000000000 --- a/homeassistant/components/binary_sensor/bbb_gpio.py +++ /dev/null @@ -1,89 +0,0 @@ -""" -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/ -""" -import logging - -import voluptuous as vol - -from homeassistant.components import bbb_gpio -from homeassistant.components.binary_sensor import ( - BinarySensorDevice, PLATFORM_SCHEMA) -from homeassistant.const import (DEVICE_DEFAULT_NAME, CONF_NAME) -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['bbb_gpio'] - -CONF_PINS = 'pins' -CONF_BOUNCETIME = 'bouncetime' -CONF_INVERT_LOGIC = 'invert_logic' -CONF_PULL_MODE = 'pull_mode' - -DEFAULT_BOUNCETIME = 50 -DEFAULT_INVERT_LOGIC = False -DEFAULT_PULL_MODE = 'UP' - -PIN_SCHEMA = vol.Schema({ - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_BOUNCETIME, default=DEFAULT_BOUNCETIME): cv.positive_int, - vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, - vol.Optional(CONF_PULL_MODE, default=DEFAULT_PULL_MODE): - vol.In(['UP', 'DOWN']) -}) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_PINS, default={}): - vol.Schema({cv.string: PIN_SCHEMA}), -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Beaglebone Black GPIO devices.""" - pins = config.get(CONF_PINS) - - binary_sensors = [] - - for pin, params in pins.items(): - binary_sensors.append(BBBGPIOBinarySensor(pin, params)) - add_entities(binary_sensors) - - -class BBBGPIOBinarySensor(BinarySensorDevice): - """Representation of a binary sensor that uses Beaglebone Black GPIO.""" - - def __init__(self, pin, params): - """Initialize the Beaglebone Black binary sensor.""" - self._pin = pin - self._name = params.get(CONF_NAME) or DEVICE_DEFAULT_NAME - self._bouncetime = params.get(CONF_BOUNCETIME) - self._pull_mode = params.get(CONF_PULL_MODE) - self._invert_logic = params.get(CONF_INVERT_LOGIC) - - bbb_gpio.setup_input(self._pin, self._pull_mode) - self._state = bbb_gpio.read_input(self._pin) - - def read_gpio(pin): - """Read state from GPIO.""" - self._state = bbb_gpio.read_input(self._pin) - self.schedule_update_ha_state() - - bbb_gpio.edge_detect(self._pin, read_gpio, self._bouncetime) - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def is_on(self): - """Return the state of the entity.""" - return self._state != self._invert_logic diff --git a/homeassistant/components/binary_sensor/blink.py b/homeassistant/components/binary_sensor/blink.py deleted file mode 100644 index cd558f0368487..0000000000000 --- a/homeassistant/components/binary_sensor/blink.py +++ /dev/null @@ -1,54 +0,0 @@ -""" -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. -""" -from homeassistant.components.blink import BLINK_DATA, BINARY_SENSORS -from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.const import CONF_MONITORED_CONDITIONS - -DEPENDENCIES = ['blink'] - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the blink binary sensors.""" - if discovery_info is None: - return - data = hass.data[BLINK_DATA] - - devs = [] - for camera in data.cameras: - for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]: - devs.append(BlinkBinarySensor(data, camera, sensor_type)) - add_entities(devs, True) - - -class BlinkBinarySensor(BinarySensorDevice): - """Representation of a Blink binary sensor.""" - - def __init__(self, data, camera, sensor_type): - """Initialize the sensor.""" - self.data = data - self._type = sensor_type - name, icon = BINARY_SENSORS[sensor_type] - self._name = "{} {} {}".format(BLINK_DATA, camera, name) - self._icon = icon - self._camera = data.cameras[camera] - self._state = None - self._unique_id = "{}-{}".format(self._camera.serial, self._type) - - @property - def name(self): - """Return the name of the blink sensor.""" - return self._name - - @property - def is_on(self): - """Return the status of the sensor.""" - return self._state - - def update(self): - """Update sensor state.""" - self.data.refresh() - self._state = self._camera.attributes[self._type] diff --git a/homeassistant/components/binary_sensor/bloomsky.py b/homeassistant/components/binary_sensor/bloomsky.py deleted file mode 100644 index 971941f4dd66e..0000000000000 --- a/homeassistant/components/binary_sensor/bloomsky.py +++ /dev/null @@ -1,80 +0,0 @@ -""" -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/ -""" -import logging - -import voluptuous as vol - -from homeassistant.components.binary_sensor import ( - BinarySensorDevice, PLATFORM_SCHEMA) -from homeassistant.const import CONF_MONITORED_CONDITIONS -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['bloomsky'] - -SENSOR_TYPES = { - 'Rain': 'moisture', - 'Night': None, -} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the available BloomSky weather binary sensors.""" - bloomsky = hass.components.bloomsky - # Default needed in case of discovery - sensors = config.get(CONF_MONITORED_CONDITIONS, SENSOR_TYPES) - - for device in bloomsky.BLOOMSKY.devices.values(): - for variable in sensors: - add_entities( - [BloomSkySensor(bloomsky.BLOOMSKY, device, variable)], True) - - -class BloomSkySensor(BinarySensorDevice): - """Representation of a single binary sensor in a BloomSky device.""" - - def __init__(self, bs, device, sensor_name): - """Initialize a BloomSky binary sensor.""" - self._bloomsky = bs - self._device_id = device['DeviceID'] - self._sensor_name = sensor_name - self._name = '{} {}'.format(device['DeviceName'], sensor_name) - self._state = None - self._unique_id = '{}-{}'.format(self._device_id, self._sensor_name) - - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id - - @property - def name(self): - """Return the name of the BloomSky device and this sensor.""" - return self._name - - @property - def device_class(self): - """Return the class of this sensor, from DEVICE_CLASSES.""" - return SENSOR_TYPES.get(self._sensor_name) - - @property - def is_on(self): - """Return true if binary sensor is on.""" - return self._state - - def update(self): - """Request an update from the BloomSky API.""" - self._bloomsky.refresh_devices() - - self._state = \ - self._bloomsky.devices[self._device_id]['Data'][self._sensor_name] diff --git a/homeassistant/components/binary_sensor/bmw_connected_drive.py b/homeassistant/components/binary_sensor/bmw_connected_drive.py deleted file mode 100644 index f8855b2e28b63..0000000000000 --- a/homeassistant/components/binary_sensor/bmw_connected_drive.py +++ /dev/null @@ -1,203 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN -from homeassistant.const import LENGTH_KILOMETERS - -DEPENDENCIES = ['bmw_connected_drive'] - -_LOGGER = logging.getLogger(__name__) - -SENSOR_TYPES = { - 'lids': ['Doors', 'opening'], - 'windows': ['Windows', 'opening'], - 'door_lock_state': ['Door lock state', 'safety'], - 'lights_parking': ['Parking lights', 'light'], - 'condition_based_services': ['Condition based services', 'problem'], - 'check_control_messages': ['Control messages', 'problem'] -} - -SENSOR_TYPES_ELEC = { - 'charging_status': ['Charging status', 'power'], - 'connection_status': ['Connection status', 'plug'] -} - -SENSOR_TYPES_ELEC.update(SENSOR_TYPES) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the BMW sensors.""" - accounts = hass.data[BMW_DOMAIN] - _LOGGER.debug('Found BMW accounts: %s', - ', '.join([a.name for a in accounts])) - devices = [] - for account in accounts: - for vehicle in account.account.vehicles: - 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]) - 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]) - devices.append(device) - add_entities(devices, True) - - -class BMWConnectedDriveSensor(BinarySensorDevice): - """Representation of a BMW vehicle binary sensor.""" - - def __init__(self, account, vehicle, attribute: str, sensor_name, - device_class): - """Constructor.""" - self._account = account - self._vehicle = vehicle - self._attribute = attribute - self._name = '{} {}'.format(self._vehicle.name, self._attribute) - self._unique_id = '{}-{}'.format(self._vehicle.vin, self._attribute) - self._sensor_name = sensor_name - self._device_class = device_class - self._state = None - - @property - def should_poll(self) -> bool: - """Return False. - - Data update is triggered from BMWConnectedDriveEntity. - """ - return False - - @property - def unique_id(self): - """Return the unique ID of the binary sensor.""" - return self._unique_id - - @property - def name(self): - """Return the name of the binary sensor.""" - return self._name - - @property - def device_class(self): - """Return the class of the binary sensor.""" - return self._device_class - - @property - def is_on(self): - """Return the state of the binary sensor.""" - return self._state - - @property - def device_state_attributes(self): - """Return the state attributes of the binary sensor.""" - vehicle_state = self._vehicle.state - result = { - 'car': self._vehicle.name - } - - if self._attribute == 'lids': - for lid in vehicle_state.lids: - result[lid.name] = lid.state.value - elif self._attribute == 'windows': - for window in vehicle_state.windows: - result[window.name] = window.state.value - elif self._attribute == 'door_lock_state': - result['door_lock_state'] = vehicle_state.door_lock_state.value - result['last_update_reason'] = vehicle_state.last_update_reason - elif self._attribute == 'lights_parking': - result['lights_parking'] = vehicle_state.parking_lights.value - elif self._attribute == 'condition_based_services': - for report in vehicle_state.condition_based_services: - result.update( - self._format_cbs_report(report)) - elif self._attribute == 'check_control_messages': - check_control_messages = vehicle_state.check_control_messages - if not check_control_messages: - result['check_control_messages'] = 'OK' - else: - cbs_list = [] - for message in check_control_messages: - cbs_list.append(message['ccmDescriptionShort']) - result['check_control_messages'] = cbs_list - elif self._attribute == 'charging_status': - result['charging_status'] = vehicle_state.charging_status.value - # pylint: disable=protected-access - result['last_charging_end_result'] = \ - vehicle_state._attributes['lastChargingEndResult'] - if self._attribute == 'connection_status': - # pylint: disable=protected-access - result['connection_status'] = \ - vehicle_state._attributes['connectionStatus'] - - return sorted(result.items()) - - def update(self): - """Read new state data from the library.""" - from bimmer_connected.state import LockState - from bimmer_connected.state import ChargingState - vehicle_state = self._vehicle.state - - # device class opening: On means open, Off means closed - if self._attribute == 'lids': - _LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed) - self._state = not vehicle_state.all_lids_closed - if self._attribute == 'windows': - self._state = not vehicle_state.all_windows_closed - # device class safety: On means unsafe, Off means safe - if self._attribute == 'door_lock_state': - # Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED - self._state = vehicle_state.door_lock_state not in \ - [LockState.LOCKED, LockState.SECURED] - # device class light: On means light detected, Off means no light - if self._attribute == 'lights_parking': - self._state = vehicle_state.are_parking_lights_on - # device class problem: On means problem detected, Off means no problem - if self._attribute == 'condition_based_services': - self._state = not vehicle_state.are_all_cbs_ok - if self._attribute == 'check_control_messages': - self._state = vehicle_state.has_check_control_messages - # device class power: On means power detected, Off means no power - if self._attribute == 'charging_status': - self._state = vehicle_state.charging_status in \ - [ChargingState.CHARGING] - # device class plug: On means device is plugged in, - # Off means device is unplugged - if self._attribute == 'connection_status': - # pylint: disable=protected-access - self._state = (vehicle_state._attributes['connectionStatus'] == - 'CONNECTED') - - def _format_cbs_report(self, report): - result = {} - service_type = report.service_type.lower().replace('_', ' ') - result['{} status'.format(service_type)] = report.state.value - if report.due_date is not None: - result['{} date'.format(service_type)] = \ - report.due_date.strftime('%Y-%m-%d') - if report.due_distance is not None: - distance = round(self.hass.config.units.length( - report.due_distance, LENGTH_KILOMETERS)) - result['{} distance'.format(service_type)] = '{} {}'.format( - distance, self.hass.config.units.length_unit) - return result - - def update_callback(self): - """Schedule a state update.""" - self.schedule_update_ha_state(True) - - async def async_added_to_hass(self): - """Add callback after being added to hass. - - Show latest data after startup. - """ - self._account.add_update_listener(self.update_callback) diff --git a/homeassistant/components/binary_sensor/digital_ocean.py b/homeassistant/components/binary_sensor/digital_ocean.py deleted file mode 100644 index 0f604c525e016..0000000000000 --- a/homeassistant/components/binary_sensor/digital_ocean.py +++ /dev/null @@ -1,97 +0,0 @@ -""" -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/ -""" -import logging - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components.binary_sensor import ( - BinarySensorDevice, PLATFORM_SCHEMA) -from homeassistant.components.digital_ocean import ( - CONF_DROPLETS, ATTR_CREATED_AT, ATTR_DROPLET_ID, ATTR_DROPLET_NAME, - ATTR_FEATURES, ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY, - ATTR_REGION, ATTR_VCPUS, CONF_ATTRIBUTION, DATA_DIGITAL_OCEAN) -from homeassistant.const import ATTR_ATTRIBUTION - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = 'Droplet' -DEFAULT_DEVICE_CLASS = 'moving' -DEPENDENCIES = ['digital_ocean'] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_DROPLETS): vol.All(cv.ensure_list, [cv.string]), -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Digital Ocean droplet sensor.""" - digital = hass.data.get(DATA_DIGITAL_OCEAN) - if not digital: - return False - - droplets = config.get(CONF_DROPLETS) - - dev = [] - for droplet in droplets: - droplet_id = digital.get_droplet_id(droplet) - if droplet_id is None: - _LOGGER.error("Droplet %s is not available", droplet) - return False - dev.append(DigitalOceanBinarySensor(digital, droplet_id)) - - add_entities(dev, True) - - -class DigitalOceanBinarySensor(BinarySensorDevice): - """Representation of a Digital Ocean droplet sensor.""" - - def __init__(self, do, droplet_id): - """Initialize a new Digital Ocean sensor.""" - self._digital_ocean = do - self._droplet_id = droplet_id - self._state = None - self.data = None - - @property - def name(self): - """Return the name of the sensor.""" - return self.data.name - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self.data.status == 'active' - - @property - def device_class(self): - """Return the class of this sensor.""" - return DEFAULT_DEVICE_CLASS - - @property - def device_state_attributes(self): - """Return the state attributes of the Digital Ocean droplet.""" - return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - ATTR_CREATED_AT: self.data.created_at, - ATTR_DROPLET_ID: self.data.id, - ATTR_DROPLET_NAME: self.data.name, - ATTR_FEATURES: self.data.features, - ATTR_IPV4_ADDRESS: self.data.ip_address, - ATTR_IPV6_ADDRESS: self.data.ip_v6_address, - ATTR_MEMORY: self.data.memory, - ATTR_REGION: self.data.region['name'], - ATTR_VCPUS: self.data.vcpus, - } - - def update(self): - """Update state of sensor.""" - self._digital_ocean.update() - - for droplet in self._digital_ocean.data: - if droplet.id == self._droplet_id: - self.data = droplet diff --git a/homeassistant/components/binary_sensor/ecobee.py b/homeassistant/components/binary_sensor/ecobee.py deleted file mode 100644 index 37f25476bd012..0000000000000 --- a/homeassistant/components/binary_sensor/ecobee.py +++ /dev/null @@ -1,66 +0,0 @@ -""" -Support for Ecobee sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.ecobee/ -""" -from homeassistant.components import ecobee -from homeassistant.components.binary_sensor import BinarySensorDevice - -DEPENDENCIES = ['ecobee'] - -ECOBEE_CONFIG_FILE = 'ecobee.conf' - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Ecobee sensors.""" - if discovery_info is None: - return - data = ecobee.NETWORK - dev = list() - for index in range(len(data.ecobee.thermostats)): - for sensor in data.ecobee.get_remote_sensors(index): - for item in sensor['capability']: - if item['type'] != 'occupancy': - continue - - dev.append(EcobeeBinarySensor(sensor['name'], index)) - - add_entities(dev, True) - - -class EcobeeBinarySensor(BinarySensorDevice): - """Representation of an Ecobee sensor.""" - - def __init__(self, sensor_name, sensor_index): - """Initialize the sensor.""" - self._name = sensor_name + ' Occupancy' - self.sensor_name = sensor_name - self.index = sensor_index - self._state = None - self._device_class = 'occupancy' - - @property - def name(self): - """Return the name of the Ecobee sensor.""" - return self._name.rstrip() - - @property - def is_on(self): - """Return the status of the sensor.""" - return self._state == 'true' - - @property - def device_class(self): - """Return the class of this sensor, from DEVICE_CLASSES.""" - return self._device_class - - def update(self): - """Get the latest state of the sensor.""" - data = ecobee.NETWORK - data.update() - for sensor in data.ecobee.get_remote_sensors(self.index): - for item in sensor['capability']: - if (item['type'] == 'occupancy' and - self.sensor_name == sensor['name']): - self._state = item['value'] diff --git a/homeassistant/components/binary_sensor/egardia.py b/homeassistant/components/binary_sensor/egardia.py deleted file mode 100644 index 56d7dda17badb..0000000000000 --- a/homeassistant/components/binary_sensor/egardia.py +++ /dev/null @@ -1,77 +0,0 @@ -""" -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/ -""" -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) -_LOGGER = logging.getLogger(__name__) -DEPENDENCIES = ['egardia'] -EGARDIA_TYPE_TO_DEVICE_CLASS = {'IR Sensor': 'motion', - 'Door Contact': 'opening', - 'IR': 'motion'} - - -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Initialize the platform.""" - if (discovery_info is None or - discovery_info[ATTR_DISCOVER_DEVICES] is None): - return - - disc_info = discovery_info[ATTR_DISCOVER_DEVICES] - # multiple devices here! - async_add_entities( - ( - EgardiaBinarySensor( - sensor_id=disc_info[sensor]['id'], - name=disc_info[sensor]['name'], - egardia_system=hass.data[EGARDIA_DEVICE], - device_class=EGARDIA_TYPE_TO_DEVICE_CLASS.get( - disc_info[sensor]['type'], None) - ) - for sensor in disc_info - ), True) - - -class EgardiaBinarySensor(BinarySensorDevice): - """Represents a sensor based on an Egardia sensor (IR, Door Contact).""" - - def __init__(self, sensor_id, name, egardia_system, device_class): - """Initialize the sensor device.""" - self._id = sensor_id - self._name = name - self._state = None - self._device_class = device_class - self._egardia_system = egardia_system - - def update(self): - """Update the status.""" - egardia_input = self._egardia_system.getsensorstate(self._id) - self._state = STATE_ON if egardia_input else STATE_OFF - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def is_on(self): - """Whether the device is switched on.""" - return self._state == STATE_ON - - @property - def hidden(self): - """Whether the device is hidden by default.""" - # these type of sensors are probably mainly used for automations - return True - - @property - def device_class(self): - """Return the device class.""" - return self._device_class diff --git a/homeassistant/components/binary_sensor/eight_sleep.py b/homeassistant/components/binary_sensor/eight_sleep.py deleted file mode 100644 index 34d3a7a13ca35..0000000000000 --- a/homeassistant/components/binary_sensor/eight_sleep.py +++ /dev/null @@ -1,67 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.eight_sleep import ( - DATA_EIGHT, EightSleepHeatEntity, CONF_BINARY_SENSORS, NAME_MAP) - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['eight_sleep'] - - -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up the eight sleep binary sensor.""" - if discovery_info is None: - return - - name = 'Eight' - sensors = discovery_info[CONF_BINARY_SENSORS] - eight = hass.data[DATA_EIGHT] - - all_sensors = [] - - for sensor in sensors: - all_sensors.append(EightHeatSensor(name, eight, sensor)) - - async_add_entities(all_sensors, True) - - -class EightHeatSensor(EightSleepHeatEntity, BinarySensorDevice): - """Representation of a Eight Sleep heat-based sensor.""" - - def __init__(self, name, eight, sensor): - """Initialize the sensor.""" - super().__init__(eight) - - self._sensor = sensor - self._mapped_name = NAME_MAP.get(self._sensor, self._sensor) - self._name = '{} {}'.format(name, self._mapped_name) - self._state = None - - self._side = self._sensor.split('_')[0] - self._userid = self._eight.fetch_userid(self._side) - self._usrobj = self._eight.users[self._userid] - - _LOGGER.debug("Presence Sensor: %s, Side: %s, User: %s", - self._sensor, self._side, self._userid) - - @property - def name(self): - """Return the name of the sensor, if any.""" - return self._name - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self._state - - async def async_update(self): - """Retrieve latest state.""" - self._state = self._usrobj.bed_presence diff --git a/homeassistant/components/binary_sensor/enocean.py b/homeassistant/components/binary_sensor/enocean.py deleted file mode 100644 index c883897c2eac3..0000000000000 --- a/homeassistant/components/binary_sensor/enocean.py +++ /dev/null @@ -1,90 +0,0 @@ -""" -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/ -""" -import logging - -import voluptuous as vol - -from homeassistant.components.binary_sensor import ( - BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA) -from homeassistant.components import enocean -from homeassistant.const import ( - CONF_NAME, CONF_ID, CONF_DEVICE_CLASS) -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['enocean'] -DEFAULT_NAME = 'EnOcean binary sensor' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Binary Sensor platform for EnOcean.""" - dev_id = config.get(CONF_ID) - devname = config.get(CONF_NAME) - device_class = config.get(CONF_DEVICE_CLASS) - - add_entities([EnOceanBinarySensor(dev_id, devname, device_class)]) - - -class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice): - """Representation of EnOcean binary sensors such as wall switches.""" - - def __init__(self, dev_id, devname, device_class): - """Initialize the EnOcean binary sensor.""" - enocean.EnOceanDevice.__init__(self) - self.stype = 'listener' - self.dev_id = dev_id - self.which = -1 - self.onoff = -1 - self.devname = devname - self._device_class = device_class - - @property - def name(self): - """Return the default name for the binary sensor.""" - return self.devname - - @property - def device_class(self): - """Return the class of this sensor.""" - return self._device_class - - def value_changed(self, value, value2): - """Fire an event with the data that have changed. - - This method is called when there is an incoming packet associated - with this platform. - """ - self.schedule_update_ha_state() - if value2 == 0x70: - self.which = 0 - self.onoff = 0 - elif value2 == 0x50: - self.which = 0 - self.onoff = 1 - elif value2 == 0x30: - self.which = 1 - self.onoff = 0 - elif value2 == 0x10: - self.which = 1 - self.onoff = 1 - elif value2 == 0x37: - self.which = 10 - self.onoff = 0 - elif value2 == 0x15: - self.which = 10 - self.onoff = 1 - self.hass.bus.fire('button_pressed', {'id': self.dev_id, - 'pushed': value, - 'which': self.which, - 'onoff': self.onoff}) diff --git a/homeassistant/components/binary_sensor/envisalink.py b/homeassistant/components/binary_sensor/envisalink.py deleted file mode 100644 index 276ace8dd5117..0000000000000 --- a/homeassistant/components/binary_sensor/envisalink.py +++ /dev/null @@ -1,101 +0,0 @@ -""" -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/ -""" -import logging -import datetime - -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.envisalink import ( - DATA_EVL, ZONE_SCHEMA, CONF_ZONENAME, CONF_ZONETYPE, EnvisalinkDevice, - SIGNAL_ZONE_UPDATE) -from homeassistant.const import ATTR_LAST_TRIP_TIME -from homeassistant.util import dt as dt_util - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['envisalink'] - - -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up the Envisalink binary sensor devices.""" - configured_zones = discovery_info['zones'] - - devices = [] - for zone_num in configured_zones: - device_config_data = ZONE_SCHEMA(configured_zones[zone_num]) - device = EnvisalinkBinarySensor( - hass, - zone_num, - device_config_data[CONF_ZONENAME], - device_config_data[CONF_ZONETYPE], - hass.data[DATA_EVL].alarm_state['zone'][zone_num], - hass.data[DATA_EVL] - ) - devices.append(device) - - async_add_entities(devices) - - -class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice): - """Representation of an Envisalink binary sensor.""" - - def __init__(self, hass, zone_number, zone_name, zone_type, info, - controller): - """Initialize the binary_sensor.""" - self._zone_type = zone_type - self._zone_number = zone_number - - _LOGGER.debug('Setting up zone: %s', zone_name) - super().__init__(zone_name, info, controller) - - async def async_added_to_hass(self): - """Register callbacks.""" - async_dispatcher_connect( - self.hass, SIGNAL_ZONE_UPDATE, self._update_callback) - - @property - def device_state_attributes(self): - """Return the state attributes.""" - attr = {} - - # The Envisalink library returns a "last_fault" value that's the - # number of seconds since the last fault, up to a maximum of 327680 - # seconds (65536 5-second ticks). - # - # We don't want the HA event log to fill up with a bunch of no-op - # "state changes" that are just that number ticking up once per poll - # interval, so we subtract it from the current second-accurate time - # unless it is already at the maximum value, in which case we set it - # to None since we can't determine the actual value. - seconds_ago = self._info['last_fault'] - if seconds_ago < 65536 * 5: - now = dt_util.now().replace(microsecond=0) - delta = datetime.timedelta(seconds=seconds_ago) - last_trip_time = (now - delta).isoformat() - else: - last_trip_time = None - - attr[ATTR_LAST_TRIP_TIME] = last_trip_time - return attr - - @property - def is_on(self): - """Return true if sensor is on.""" - return self._info['status']['open'] - - @property - def device_class(self): - """Return the class of this sensor, from DEVICE_CLASSES.""" - return self._zone_type - - @callback - def _update_callback(self, zone): - """Update the zone's state, if needed.""" - if zone is None or int(zone) == self._zone_number: - self.async_schedule_update_ha_state() diff --git a/homeassistant/components/binary_sensor/fibaro.py b/homeassistant/components/binary_sensor/fibaro.py deleted file mode 100644 index 1934580c58ea3..0000000000000 --- a/homeassistant/components/binary_sensor/fibaro.py +++ /dev/null @@ -1,82 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.components.binary_sensor import ( - BinarySensorDevice, ENTITY_ID_FORMAT) -from homeassistant.components.fibaro import ( - FIBARO_DEVICES, FibaroDevice) -from homeassistant.const import (CONF_DEVICE_CLASS, CONF_ICON) - -DEPENDENCIES = ['fibaro'] - -_LOGGER = logging.getLogger(__name__) - -SENSOR_TYPES = { - 'com.fibaro.floodSensor': ['Flood', 'mdi:water', 'flood'], - 'com.fibaro.motionSensor': ['Motion', 'mdi:run', 'motion'], - 'com.fibaro.doorSensor': ['Door', 'mdi:window-open', 'door'], - 'com.fibaro.windowSensor': ['Window', 'mdi:window-open', 'window'], - 'com.fibaro.smokeSensor': ['Smoke', 'mdi:smoking', 'smoke'], - 'com.fibaro.FGMS001': ['Motion', 'mdi:run', 'motion'], - 'com.fibaro.heatDetector': ['Heat', 'mdi:fire', 'heat'], -} - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Perform the setup for Fibaro controller devices.""" - if discovery_info is None: - return - - add_entities( - [FibaroBinarySensor(device) - for device in hass.data[FIBARO_DEVICES]['binary_sensor']], True) - - -class FibaroBinarySensor(FibaroDevice, BinarySensorDevice): - """Representation of a Fibaro Binary Sensor.""" - - def __init__(self, fibaro_device): - """Initialize the binary_sensor.""" - self._state = None - super().__init__(fibaro_device) - self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) - stype = None - devconf = fibaro_device.device_config - if fibaro_device.type in SENSOR_TYPES: - stype = fibaro_device.type - elif fibaro_device.baseType in SENSOR_TYPES: - stype = fibaro_device.baseType - if stype: - self._device_class = SENSOR_TYPES[stype][2] - self._icon = SENSOR_TYPES[stype][1] - else: - self._device_class = None - self._icon = None - # device_config overrides: - self._device_class = devconf.get(CONF_DEVICE_CLASS, - self._device_class) - self._icon = devconf.get(CONF_ICON, self._icon) - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return self._icon - - @property - def device_class(self): - """Return the device class of the sensor.""" - return self._device_class - - @property - def is_on(self): - """Return true if sensor is on.""" - return self._state - - def update(self): - """Get the latest data and update the state.""" - self._state = self.current_binary_state diff --git a/homeassistant/components/binary_sensor/fritzbox.py b/homeassistant/components/binary_sensor/fritzbox.py deleted file mode 100644 index ab58e6e84bc62..0000000000000 --- a/homeassistant/components/binary_sensor/fritzbox.py +++ /dev/null @@ -1,64 +0,0 @@ -""" -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/ -""" -import logging - -import requests - -from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.fritzbox import DOMAIN as FRITZBOX_DOMAIN - -DEPENDENCIES = ['fritzbox'] - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Fritzbox binary sensor platform.""" - devices = [] - fritz_list = hass.data[FRITZBOX_DOMAIN] - - for fritz in fritz_list: - device_list = fritz.get_devices() - for device in device_list: - if device.has_alarm: - devices.append(FritzboxBinarySensor(device, fritz)) - - add_entities(devices, True) - - -class FritzboxBinarySensor(BinarySensorDevice): - """Representation of a binary Fritzbox device.""" - - def __init__(self, device, fritz): - """Initialize the Fritzbox binary sensor.""" - self._device = device - self._fritz = fritz - - @property - def name(self): - """Return the name of the entity.""" - return self._device.name - - @property - def device_class(self): - """Return the class of this sensor.""" - return 'window' - - @property - def is_on(self): - """Return true if sensor is on.""" - if not self._device.present: - return False - return self._device.alert_state - - def update(self): - """Get latest data from the Fritzbox.""" - try: - self._device.update() - except requests.exceptions.HTTPError as ex: - _LOGGER.warning("Connection error: %s", ex) - self._fritz.login() diff --git a/homeassistant/components/binary_sensor/hydrawise.py b/homeassistant/components/binary_sensor/hydrawise.py deleted file mode 100644 index 38b660c506fd5..0000000000000 --- a/homeassistant/components/binary_sensor/hydrawise.py +++ /dev/null @@ -1,81 +0,0 @@ -""" -Support for Hydrawise sprinkler. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.hydrawise/ -""" -import logging - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components.hydrawise import ( - BINARY_SENSORS, DATA_HYDRAWISE, HydrawiseEntity, DEVICE_MAP, - DEVICE_MAP_INDEX) -from homeassistant.components.binary_sensor import ( - BinarySensorDevice, PLATFORM_SCHEMA) -from homeassistant.const import CONF_MONITORED_CONDITIONS - -DEPENDENCIES = ['hydrawise'] - -_LOGGER = logging.getLogger(__name__) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_MONITORED_CONDITIONS, default=BINARY_SENSORS): - vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)]), -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up a sensor for a Hydrawise device.""" - hydrawise = hass.data[DATA_HYDRAWISE].data - - sensors = [] - for sensor_type in config.get(CONF_MONITORED_CONDITIONS): - if sensor_type in ['status', 'rain_sensor']: - sensors.append( - HydrawiseBinarySensor( - hydrawise.controller_status, sensor_type)) - - else: - # create a sensor for each zone - for zone in hydrawise.relays: - zone_data = zone - zone_data['running'] = \ - hydrawise.controller_status.get('running', False) - sensors.append(HydrawiseBinarySensor(zone_data, sensor_type)) - - add_entities(sensors, True) - - -class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorDevice): - """A sensor implementation for Hydrawise device.""" - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self._state - - def update(self): - """Get the latest data and updates the state.""" - _LOGGER.debug("Updating Hydrawise binary sensor: %s", self._name) - mydata = self.hass.data[DATA_HYDRAWISE].data - if self._sensor_type == 'status': - self._state = mydata.status == 'All good!' - elif self._sensor_type == 'rain_sensor': - for sensor in mydata.sensors: - if sensor['name'] == 'Rain': - self._state = sensor['active'] == 1 - elif self._sensor_type == 'is_watering': - if not mydata.running: - self._state = False - elif int(mydata.running[0]['relay']) == self.data['relay']: - self._state = True - else: - self._state = False - - @property - def device_class(self): - """Return the device class of the sensor type.""" - return DEVICE_MAP[self._sensor_type][ - DEVICE_MAP_INDEX.index('DEVICE_CLASS_INDEX')] diff --git a/homeassistant/components/binary_sensor/knx.py b/homeassistant/components/binary_sensor/knx.py deleted file mode 100644 index a89d5d1c94579..0000000000000 --- a/homeassistant/components/binary_sensor/knx.py +++ /dev/null @@ -1,146 +0,0 @@ -""" -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/ -""" - -import voluptuous as vol - -from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, BinarySensorDevice) -from homeassistant.components.knx import ( - ATTR_DISCOVER_DEVICES, DATA_KNX, KNXAutomation) -from homeassistant.const import CONF_NAME -from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv - -CONF_ADDRESS = 'address' -CONF_DEVICE_CLASS = 'device_class' -CONF_SIGNIFICANT_BIT = 'significant_bit' -CONF_DEFAULT_SIGNIFICANT_BIT = 1 -CONF_AUTOMATION = 'automation' -CONF_HOOK = 'hook' -CONF_DEFAULT_HOOK = 'on' -CONF_COUNTER = 'counter' -CONF_DEFAULT_COUNTER = 1 -CONF_ACTION = 'action' -CONF_RESET_AFTER = 'reset_after' - -CONF__ACTION = 'turn_off_action' - -DEFAULT_NAME = 'KNX Binary Sensor' -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 -}) - -AUTOMATIONS_SCHEMA = vol.All( - cv.ensure_list, - [AUTOMATION_SCHEMA] -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_ADDRESS): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_DEVICE_CLASS): cv.string, - vol.Optional(CONF_SIGNIFICANT_BIT, default=CONF_DEFAULT_SIGNIFICANT_BIT): - cv.positive_int, - vol.Optional(CONF_RESET_AFTER): cv.positive_int, - vol.Optional(CONF_AUTOMATION): AUTOMATIONS_SCHEMA, -}) - - -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up binary sensor(s) 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 binary sensors 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(KNXBinarySensor(device)) - async_add_entities(entities) - - -@callback -def async_add_entities_config(hass, config, async_add_entities): - """Set up binary senor for KNX platform configured within platform.""" - name = config.get(CONF_NAME) - import xknx - binary_sensor = xknx.devices.BinarySensor( - hass.data[DATA_KNX].xknx, - name=name, - group_address=config.get(CONF_ADDRESS), - device_class=config.get(CONF_DEVICE_CLASS), - significant_bit=config.get(CONF_SIGNIFICANT_BIT), - reset_after=config.get(CONF_RESET_AFTER)) - hass.data[DATA_KNX].xknx.devices.add(binary_sensor) - - entity = KNXBinarySensor(binary_sensor) - automations = config.get(CONF_AUTOMATION) - if automations is not None: - for automation in automations: - counter = automation.get(CONF_COUNTER) - hook = automation.get(CONF_HOOK) - action = automation.get(CONF_ACTION) - entity.automations.append(KNXAutomation( - hass=hass, device=binary_sensor, hook=hook, - action=action, counter=counter)) - async_add_entities([entity]) - - -class KNXBinarySensor(BinarySensorDevice): - """Representation of a KNX binary sensor.""" - - def __init__(self, device): - """Initialize of KNX binary sensor.""" - self.device = device - self.automations = [] - - @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 device_class(self): - """Return the class of this sensor.""" - return self.device.device_class - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self.device.is_on() diff --git a/homeassistant/components/binary_sensor/konnected.py b/homeassistant/components/binary_sensor/konnected.py deleted file mode 100644 index e91d3f6136ad0..0000000000000 --- a/homeassistant/components/binary_sensor/konnected.py +++ /dev/null @@ -1,82 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.konnected import ( - DOMAIN as KONNECTED_DOMAIN, PIN_TO_ZONE, SIGNAL_SENSOR_UPDATE) -from homeassistant.const import ( - CONF_DEVICES, CONF_TYPE, CONF_NAME, CONF_BINARY_SENSORS, ATTR_ENTITY_ID, - ATTR_STATE) -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['konnected'] - - -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 - - data = hass.data[KONNECTED_DOMAIN] - device_id = discovery_info['device_id'] - sensors = [KonnectedBinarySensor(device_id, pin_num, pin_data) - for pin_num, pin_data in - data[CONF_DEVICES][device_id][CONF_BINARY_SENSORS].items()] - async_add_entities(sensors) - - -class KonnectedBinarySensor(BinarySensorDevice): - """Representation of a Konnected binary sensor.""" - - def __init__(self, device_id, pin_num, data): - """Initialize the binary sensor.""" - self._data = data - self._device_id = device_id - self._pin_num = pin_num - self._state = self._data.get(ATTR_STATE) - 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) - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def is_on(self): - """Return the state of the sensor.""" - return self._state - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def device_class(self): - """Return the device class.""" - return self._device_class - - async def async_added_to_hass(self): - """Store entity_id and register state change callback.""" - self._data[ATTR_ENTITY_ID] = self.entity_id - async_dispatcher_connect( - self.hass, SIGNAL_SENSOR_UPDATE.format(self.entity_id), - self.async_set_state) - - @callback - def async_set_state(self, state): - """Update the sensor's state.""" - self._state = state - self.async_schedule_update_ha_state() diff --git a/homeassistant/components/binary_sensor/linode.py b/homeassistant/components/binary_sensor/linode.py deleted file mode 100644 index 24abc3dd8be40..0000000000000 --- a/homeassistant/components/binary_sensor/linode.py +++ /dev/null @@ -1,97 +0,0 @@ -""" -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/ -""" -import logging - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components.binary_sensor import ( - BinarySensorDevice, 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) - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = 'Node' -DEFAULT_DEVICE_CLASS = 'moving' -DEPENDENCIES = ['linode'] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_NODES): vol.All(cv.ensure_list, [cv.string]), -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Linode droplet sensor.""" - linode = hass.data.get(DATA_LINODE) - nodes = config.get(CONF_NODES) - - dev = [] - for node in nodes: - node_id = linode.get_node_id(node) - if node_id is None: - _LOGGER.error("Node %s is not available", node) - return - dev.append(LinodeBinarySensor(linode, node_id)) - - add_entities(dev, True) - - -class LinodeBinarySensor(BinarySensorDevice): - """Representation of a Linode droplet sensor.""" - - def __init__(self, li, node_id): - """Initialize a new Linode sensor.""" - self._linode = li - self._node_id = node_id - self._state = None - self.data = None - self._attrs = {} - self._name = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self._state - - @property - def device_class(self): - """Return the class of this sensor.""" - return DEFAULT_DEVICE_CLASS - - @property - def device_state_attributes(self): - """Return the state attributes of the Linode Node.""" - return self._attrs - - def update(self): - """Update state of sensor.""" - self._linode.update() - if self._linode.data is not None: - for node in self._linode.data: - if node.id == self._node_id: - self.data = node - if self.data is not None: - self._state = self.data.status == 'running' - self._attrs = { - ATTR_CREATED: self.data.created, - ATTR_NODE_ID: self.data.id, - ATTR_NODE_NAME: self.data.label, - ATTR_IPV4_ADDRESS: self.data.ipv4, - ATTR_IPV6_ADDRESS: self.data.ipv6, - ATTR_MEMORY: self.data.specs.memory, - ATTR_REGION: self.data.region.country, - ATTR_VCPUS: self.data.specs.vcpus, - } - self._name = self.data.label diff --git a/homeassistant/components/binary_sensor/lupusec.py b/homeassistant/components/binary_sensor/lupusec.py deleted file mode 100644 index df8210df02615..0000000000000 --- a/homeassistant/components/binary_sensor/lupusec.py +++ /dev/null @@ -1,53 +0,0 @@ -""" -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/ -""" -import logging -from datetime import timedelta - -from homeassistant.components.lupusec import (LupusecDevice, - DOMAIN as LUPUSEC_DOMAIN) -from homeassistant.components.binary_sensor import (BinarySensorDevice, - DEVICE_CLASSES) - -DEPENDENCIES = ['lupusec'] - -SCAN_INTERVAL = timedelta(seconds=2) - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up a sensor for an Lupusec device.""" - if discovery_info is None: - return - - import lupupy.constants as CONST - - data = hass.data[LUPUSEC_DOMAIN] - - device_types = [CONST.TYPE_OPENING] - - devices = [] - for device in data.lupusec.get_devices(generic_type=device_types): - devices.append(LupusecBinarySensor(data, device)) - - add_entities(devices) - - -class LupusecBinarySensor(LupusecDevice, BinarySensorDevice): - """A binary sensor implementation for Lupusec device.""" - - @property - def is_on(self): - """Return True if the binary sensor is on.""" - return self._device.is_on - - @property - def device_class(self): - """Return the class of the binary sensor.""" - if self._device.generic_type not in DEVICE_CLASSES: - return None - return self._device.generic_type diff --git a/homeassistant/components/binary_sensor/maxcube.py b/homeassistant/components/binary_sensor/maxcube.py deleted file mode 100644 index 850a416acc540..0000000000000 --- a/homeassistant/components/binary_sensor/maxcube.py +++ /dev/null @@ -1,68 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.maxcube import DATA_KEY - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Iterate through all MAX! Devices and add window shutters.""" - devices = [] - for handler in hass.data[DATA_KEY].values(): - cube = handler.cube - for device in cube.devices: - name = "{} {}".format( - cube.room_by_id(device.room_id).name, device.name) - - # Only add Window Shutters - if cube.is_windowshutter(device): - devices.append( - MaxCubeShutter(handler, name, device.rf_address)) - - if devices: - add_entities(devices) - - -class MaxCubeShutter(BinarySensorDevice): - """Representation of a MAX! Cube Binary Sensor device.""" - - def __init__(self, handler, name, rf_address): - """Initialize MAX! Cube BinarySensorDevice.""" - self._name = name - self._sensor_type = 'window' - self._rf_address = rf_address - self._cubehandle = handler - self._state = None - - @property - def should_poll(self): - """Return the polling state.""" - return True - - @property - def name(self): - """Return the name of the BinarySensorDevice.""" - return self._name - - @property - def device_class(self): - """Return the class of this sensor.""" - return self._sensor_type - - @property - def is_on(self): - """Return true if the binary sensor is on/open.""" - return self._state - - def update(self): - """Get latest data from MAX! Cube.""" - self._cubehandle.update() - device = self._cubehandle.cube.device_by_rf(self._rf_address) - self._state = device.is_open diff --git a/homeassistant/components/binary_sensor/modbus.py b/homeassistant/components/binary_sensor/modbus.py deleted file mode 100644 index f9f2597593e64..0000000000000 --- a/homeassistant/components/binary_sensor/modbus.py +++ /dev/null @@ -1,71 +0,0 @@ -""" -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/ -""" -import logging -import voluptuous as vol - -from homeassistant.components import modbus -from homeassistant.const import CONF_NAME, CONF_SLAVE -from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.helpers import config_validation as cv -from homeassistant.components.sensor import PLATFORM_SCHEMA - -_LOGGER = logging.getLogger(__name__) -DEPENDENCIES = ['modbus'] - -CONF_COIL = 'coil' -CONF_COILS = 'coils' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_COILS): [{ - vol.Required(CONF_COIL): cv.positive_int, - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_SLAVE): cv.positive_int - }] -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Modbus binary sensors.""" - sensors = [] - for coil in config.get(CONF_COILS): - sensors.append(ModbusCoilSensor( - coil.get(CONF_NAME), - coil.get(CONF_SLAVE), - coil.get(CONF_COIL))) - add_entities(sensors) - - -class ModbusCoilSensor(BinarySensorDevice): - """Modbus coil sensor.""" - - def __init__(self, name, slave, coil): - """Initialize the modbus coil sensor.""" - self._name = name - self._slave = int(slave) if slave else None - self._coil = int(coil) - self._value = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def is_on(self): - """Return the state of the sensor.""" - return self._value - - def update(self): - """Update the state of the sensor.""" - result = modbus.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) diff --git a/homeassistant/components/binary_sensor/mychevy.py b/homeassistant/components/binary_sensor/mychevy.py deleted file mode 100644 index c1e3b6f0aacbf..0000000000000 --- a/homeassistant/components/binary_sensor/mychevy.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Support for MyChevy sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.mychevy/ -""" -import logging - -from homeassistant.components.mychevy import ( - EVBinarySensorConfig, DOMAIN as MYCHEVY_DOMAIN, UPDATE_TOPIC -) -from homeassistant.components.binary_sensor import ( - ENTITY_ID_FORMAT, BinarySensorDevice) -from homeassistant.core import callback -from homeassistant.util import slugify - -_LOGGER = logging.getLogger(__name__) - -SENSORS = [ - EVBinarySensorConfig("Plugged In", "plugged_in", "plug") -] - - -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up the MyChevy sensors.""" - if discovery_info is None: - return - - sensors = [] - hub = hass.data[MYCHEVY_DOMAIN] - for sconfig in SENSORS: - for car in hub.cars: - sensors.append(EVBinarySensor(hub, sconfig, car.vid)) - - async_add_entities(sensors) - - -class EVBinarySensor(BinarySensorDevice): - """Base EVSensor class. - - 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): - """Initialize sensor with car connection.""" - self._conn = connection - self._name = config.name - self._attr = config.attr - self._type = config.device_class - 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))) - - @property - def name(self): - """Return the name.""" - return self._name - - @property - def is_on(self): - """Return if on.""" - return self._is_on - - @property - def _car(self): - """Return the car.""" - return self._conn.get_car(self._car_vid) - - async def async_added_to_hass(self): - """Register callbacks.""" - self.hass.helpers.dispatcher.async_dispatcher_connect( - UPDATE_TOPIC, self.async_update_callback) - - @callback - def async_update_callback(self): - """Update state.""" - if self._car is not None: - self._is_on = getattr(self._car, self._attr, None) - self.async_schedule_update_ha_state() - - @property - def should_poll(self): - """Return the polling state.""" - return False diff --git a/homeassistant/components/binary_sensor/mysensors.py b/homeassistant/components/binary_sensor/mysensors.py deleted file mode 100644 index f0b7832cf2581..0000000000000 --- a/homeassistant/components/binary_sensor/mysensors.py +++ /dev/null @@ -1,48 +0,0 @@ -""" -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/ -""" -from homeassistant.components import mysensors -from homeassistant.components.binary_sensor import ( - DEVICE_CLASSES, DOMAIN, BinarySensorDevice) -from homeassistant.const import STATE_ON - -SENSORS = { - 'S_DOOR': 'door', - 'S_MOTION': 'motion', - 'S_SMOKE': 'smoke', - 'S_SPRINKLER': 'safety', - 'S_WATER_LEAK': 'safety', - 'S_SOUND': 'sound', - 'S_VIBRATION': 'vibration', - 'S_MOISTURE': 'moisture', -} - - -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): - """Set up the mysensors platform for binary sensors.""" - mysensors.setup_mysensors_platform( - hass, DOMAIN, discovery_info, MySensorsBinarySensor, - async_add_entities=async_add_entities) - - -class MySensorsBinarySensor( - mysensors.device.MySensorsEntity, BinarySensorDevice): - """Representation of a MySensors Binary Sensor child node.""" - - @property - def is_on(self): - """Return True if the binary sensor is on.""" - return self._values.get(self.value_type) == STATE_ON - - @property - def device_class(self): - """Return the class of this sensor, from DEVICE_CLASSES.""" - pres = self.gateway.const.Presentation - device_class = SENSORS.get(pres(self.child_type).name) - if device_class in DEVICE_CLASSES: - return device_class - return None diff --git a/homeassistant/components/binary_sensor/nest.py b/homeassistant/components/binary_sensor/nest.py deleted file mode 100644 index 7f7278d9789a1..0000000000000 --- a/homeassistant/components/binary_sensor/nest.py +++ /dev/null @@ -1,162 +0,0 @@ -""" -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/ -""" -from itertools import chain -import logging - -from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.nest import ( - DATA_NEST, DATA_NEST_CONFIG, CONF_BINARY_SENSORS, NestSensorDevice) -from homeassistant.const import CONF_MONITORED_CONDITIONS - -DEPENDENCIES = ['nest'] - -BINARY_TYPES = {'online': 'connectivity'} - -CLIMATE_BINARY_TYPES = { - 'fan': None, - 'is_using_emergency_heat': 'heat', - 'is_locked': None, - 'has_leaf': None, -} - -CAMERA_BINARY_TYPES = { - 'motion_detected': 'motion', - 'sound_detected': 'sound', - 'person_detected': 'occupancy', -} - -STRUCTURE_BINARY_TYPES = { - 'away': None, -} - -STRUCTURE_BINARY_STATE_MAP = { - 'away': {'away': True, 'home': False}, -} - -_BINARY_TYPES_DEPRECATED = [ - 'hvac_ac_state', - 'hvac_aux_heater_state', - 'hvac_heater_state', - 'hvac_heat_x2_state', - 'hvac_heat_x3_state', - 'hvac_alt_heat_state', - 'hvac_alt_heat_x2_state', - 'hvac_emer_heat_state', -] - -_VALID_BINARY_SENSOR_TYPES = {**BINARY_TYPES, **CLIMATE_BINARY_TYPES, - **CAMERA_BINARY_TYPES, **STRUCTURE_BINARY_TYPES} - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Nest binary sensors. - - No longer used. - """ - - -async def async_setup_entry(hass, entry, async_add_entities): - """Set up a Nest binary sensor based on a config entry.""" - nest = hass.data[DATA_NEST] - - discovery_info = \ - hass.data.get(DATA_NEST_CONFIG, {}).get(CONF_BINARY_SENSORS, {}) - - # Add all available binary sensors if no Nest binary sensor config is set - if discovery_info == {}: - conditions = _VALID_BINARY_SENSOR_TYPES - else: - conditions = discovery_info.get(CONF_MONITORED_CONDITIONS, {}) - - for variable in conditions: - if variable in _BINARY_TYPES_DEPRECATED: - wstr = (variable + " is no a longer supported " - "monitored_conditions. See " - "https://home-assistant.io/components/binary_sensor.nest/ " - "for valid options.") - _LOGGER.error(wstr) - - def get_binary_sensors(): - """Get the Nest binary sensors.""" - sensors = [] - for structure in nest.structures(): - 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()) - for structure, device in device_chain: - sensors += [NestBinarySensor(structure, device, variable) - for variable in conditions - if variable in BINARY_TYPES] - sensors += [NestBinarySensor(structure, device, variable) - for variable in conditions - if variable in CLIMATE_BINARY_TYPES - and device.is_thermostat] - - if device.is_camera: - sensors += [NestBinarySensor(structure, device, variable) - for variable in conditions - if variable in CAMERA_BINARY_TYPES] - for activity_zone in device.activity_zones: - sensors += [NestActivityZoneSensor(structure, - device, - activity_zone)] - - return sensors - - async_add_entities(await hass.async_add_job(get_binary_sensors), True) - - -class NestBinarySensor(NestSensorDevice, BinarySensorDevice): - """Represents a Nest binary sensor.""" - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self._state - - @property - def device_class(self): - """Return the device class of the binary sensor.""" - return _VALID_BINARY_SENSOR_TYPES.get(self.variable) - - def update(self): - """Retrieve latest state.""" - value = getattr(self.device, self.variable) - if self.variable in STRUCTURE_BINARY_TYPES: - self._state = bool(STRUCTURE_BINARY_STATE_MAP - [self.variable].get(value)) - else: - self._state = bool(value) - - -class NestActivityZoneSensor(NestBinarySensor): - """Represents a Nest binary sensor for activity in a zone.""" - - def __init__(self, structure, device, zone): - """Initialize the sensor.""" - super(NestActivityZoneSensor, self).__init__(structure, device, "") - self.zone = zone - self._name = "{} {} activity".format(self._name, self.zone.name) - - @property - def unique_id(self): - """Return unique id based on camera serial and zone id.""" - return "{}-{}".format(self.device.serial, self.zone.zone_id) - - @property - def device_class(self): - """Return the device class of the binary sensor.""" - return 'motion' - - def update(self): - """Retrieve latest state.""" - self._state = self.device.has_ongoing_motion_in_zone(self.zone.zone_id) diff --git a/homeassistant/components/binary_sensor/netatmo.py b/homeassistant/components/binary_sensor/netatmo.py deleted file mode 100644 index 2cafacf401c59..0000000000000 --- a/homeassistant/components/binary_sensor/netatmo.py +++ /dev/null @@ -1,198 +0,0 @@ -""" -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/. -""" -import logging - -import voluptuous as vol - -from homeassistant.components.binary_sensor import ( - BinarySensorDevice, PLATFORM_SCHEMA) -from homeassistant.components.netatmo import CameraData -from homeassistant.const import CONF_TIMEOUT -from homeassistant.helpers import config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['netatmo'] - -# These are the available sensors mapped to binary_sensor class -WELCOME_SENSOR_TYPES = { - "Someone known": "motion", - "Someone unknown": "motion", - "Motion": "motion", -} -PRESENCE_SENSOR_TYPES = { - "Outdoor motion": "motion", - "Outdoor human": "motion", - "Outdoor animal": "motion", - "Outdoor vehicle": "motion" -} -TAG_SENSOR_TYPES = { - "Tag Vibration": "vibration", - "Tag Open": "opening" -} - -CONF_HOME = 'home' -CONF_CAMERAS = 'cameras' -CONF_WELCOME_SENSORS = 'welcome_sensors' -CONF_PRESENCE_SENSORS = 'presence_sensors' -CONF_TAG_SENSORS = 'tag_sensors' - -DEFAULT_TIMEOUT = 90 - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_CAMERAS, default=[]): - vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_HOME): cv.string, - vol.Optional(CONF_PRESENCE_SENSORS, default=list(PRESENCE_SENSOR_TYPES)): - vol.All(cv.ensure_list, [vol.In(PRESENCE_SENSOR_TYPES)]), - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - vol.Optional(CONF_WELCOME_SENSORS, default=list(WELCOME_SENSOR_TYPES)): - vol.All(cv.ensure_list, [vol.In(WELCOME_SENSOR_TYPES)]), -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the access to Netatmo binary sensor.""" - netatmo = hass.components.netatmo - home = config.get(CONF_HOME) - timeout = config.get(CONF_TIMEOUT) - if timeout is None: - timeout = DEFAULT_TIMEOUT - - module_name = None - - import pyatmo - try: - data = CameraData(netatmo.NETATMO_AUTH, home) - if not data.get_camera_names(): - return None - except pyatmo.NoDevice: - return None - - welcome_sensors = config.get( - CONF_WELCOME_SENSORS, WELCOME_SENSOR_TYPES) - presence_sensors = config.get( - CONF_PRESENCE_SENSORS, PRESENCE_SENSOR_TYPES) - tag_sensors = config.get(CONF_TAG_SENSORS, TAG_SENSOR_TYPES) - - for camera_name in data.get_camera_names(): - camera_type = data.get_camera_type(camera=camera_name, home=home) - if camera_type == 'NACamera': - if CONF_CAMERAS in config: - if config[CONF_CAMERAS] != [] and \ - camera_name not in config[CONF_CAMERAS]: - continue - for variable in welcome_sensors: - add_entities([NetatmoBinarySensor( - data, camera_name, module_name, home, timeout, - camera_type, variable)], True) - if camera_type == 'NOC': - if CONF_CAMERAS in config: - if config[CONF_CAMERAS] != [] and \ - camera_name not in config[CONF_CAMERAS]: - continue - for variable in presence_sensors: - add_entities([NetatmoBinarySensor( - data, camera_name, module_name, home, timeout, - camera_type, variable)], True) - - for module_name in data.get_module_names(camera_name): - for variable in tag_sensors: - camera_type = None - add_entities([NetatmoBinarySensor( - data, camera_name, module_name, home, timeout, - camera_type, variable)], True) - - -class NetatmoBinarySensor(BinarySensorDevice): - """Represent a single binary sensor in a Netatmo Camera device.""" - - def __init__(self, data, camera_name, module_name, home, - timeout, camera_type, sensor): - """Set up for access to the Netatmo camera events.""" - self._data = data - self._camera_name = camera_name - self._module_name = module_name - self._home = home - self._timeout = timeout - if home: - self._name = '{} / {}'.format(home, camera_name) - else: - self._name = camera_name - if module_name: - self._name += ' / ' + module_name - self._sensor_name = sensor - self._name += ' ' + sensor - self._cameratype = camera_type - self._state = None - - @property - def name(self): - """Return the name of the Netatmo device and this sensor.""" - return self._name - - @property - def device_class(self): - """Return the class of this sensor, from DEVICE_CLASSES.""" - if self._cameratype == 'NACamera': - return WELCOME_SENSOR_TYPES.get(self._sensor_name) - if self._cameratype == 'NOC': - return PRESENCE_SENSOR_TYPES.get(self._sensor_name) - return TAG_SENSOR_TYPES.get(self._sensor_name) - - @property - def is_on(self): - """Return true if binary sensor is on.""" - return self._state - - def update(self): - """Request an update from the Netatmo API.""" - self._data.update() - self._data.update_event() - - if self._cameratype == 'NACamera': - if self._sensor_name == "Someone known": - self._state =\ - self._data.camera_data.someoneKnownSeen( - self._home, self._camera_name, self._timeout) - elif self._sensor_name == "Someone unknown": - self._state =\ - self._data.camera_data.someoneUnknownSeen( - self._home, self._camera_name, self._timeout) - elif self._sensor_name == "Motion": - self._state =\ - self._data.camera_data.motionDetected( - self._home, self._camera_name, self._timeout) - elif self._cameratype == 'NOC': - if self._sensor_name == "Outdoor motion": - self._state =\ - self._data.camera_data.outdoormotionDetected( - self._home, self._camera_name, self._timeout) - elif self._sensor_name == "Outdoor human": - self._state =\ - self._data.camera_data.humanDetected( - self._home, self._camera_name, self._timeout) - elif self._sensor_name == "Outdoor animal": - self._state =\ - self._data.camera_data.animalDetected( - self._home, self._camera_name, self._timeout) - elif self._sensor_name == "Outdoor vehicle": - self._state =\ - self._data.camera_data.carDetected( - self._home, self._camera_name, self._timeout) - if self._sensor_name == "Tag Vibration": - self._state =\ - self._data.camera_data.moduleMotionDetected( - self._home, self._module_name, self._camera_name, - self._timeout) - elif self._sensor_name == "Tag Open": - self._state =\ - self._data.camera_data.moduleOpened( - self._home, self._module_name, self._camera_name, - self._timeout) diff --git a/homeassistant/components/binary_sensor/octoprint.py b/homeassistant/components/binary_sensor/octoprint.py deleted file mode 100644 index 285495c03a026..0000000000000 --- a/homeassistant/components/binary_sensor/octoprint.py +++ /dev/null @@ -1,84 +0,0 @@ -""" -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/ -""" -import logging - -import requests - -from homeassistant.components.octoprint import (BINARY_SENSOR_TYPES, - DOMAIN as COMPONENT_DOMAIN) -from homeassistant.components.binary_sensor import BinarySensorDevice - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['octoprint'] - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the available OctoPrint binary sensors.""" - if discovery_info is None: - return - - name = discovery_info['name'] - base_url = discovery_info['base_url'] - monitored_conditions = discovery_info['sensors'] - octoprint_api = hass.data[COMPONENT_DOMAIN][base_url] - - devices = [] - for octo_type in monitored_conditions: - new_sensor = OctoPrintBinarySensor( - octoprint_api, octo_type, BINARY_SENSOR_TYPES[octo_type][2], - name, BINARY_SENSOR_TYPES[octo_type][3], - BINARY_SENSOR_TYPES[octo_type][0], - BINARY_SENSOR_TYPES[octo_type][1], 'flags') - devices.append(new_sensor) - add_entities(devices, True) - - -class OctoPrintBinarySensor(BinarySensorDevice): - """Representation an OctoPrint binary sensor.""" - - def __init__(self, api, condition, sensor_type, sensor_name, unit, - endpoint, group, tool=None): - """Initialize a new OctoPrint sensor.""" - self.sensor_name = sensor_name - if tool is None: - self._name = '{} {}'.format(sensor_name, condition) - else: - self._name = '{} {}'.format(sensor_name, condition) - self.sensor_type = sensor_type - self.api = api - self._state = False - self._unit_of_measurement = unit - self.api_endpoint = endpoint - self.api_group = group - self.api_tool = tool - _LOGGER.debug("Created OctoPrint binary sensor %r", self) - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def is_on(self): - """Return true if binary sensor is on.""" - return bool(self._state) - - @property - def device_class(self): - """Return the class of this sensor, from DEVICE_CLASSES.""" - return None - - def update(self): - """Update state of sensor.""" - try: - self._state = self.api.update( - self.sensor_type, self.api_endpoint, self.api_group, - self.api_tool) - except requests.exceptions.ConnectionError: - # Error calling the api, already logged in api.update() - return diff --git a/homeassistant/components/binary_sensor/opentherm_gw.py b/homeassistant/components/binary_sensor/opentherm_gw.py deleted file mode 100644 index 8c5ff8c44d178..0000000000000 --- a/homeassistant/components/binary_sensor/opentherm_gw.py +++ /dev/null @@ -1,145 +0,0 @@ -""" -Support for OpenTherm Gateway binary sensors. - -For more details about this platform, please refer to the documentation at -http://home-assistant.io/components/binary_sensor.opentherm_gw/ -""" -import logging - -from homeassistant.components.binary_sensor import ( - BinarySensorDevice, ENTITY_ID_FORMAT) -from homeassistant.components.opentherm_gw import ( - DATA_GW_VARS, DATA_OPENTHERM_GW, SIGNAL_OPENTHERM_GW_UPDATE) -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import async_generate_entity_id - -DEVICE_CLASS_COLD = 'cold' -DEVICE_CLASS_HEAT = 'heat' -DEVICE_CLASS_PROBLEM = 'problem' - -DEPENDENCIES = ['opentherm_gw'] - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up the OpenTherm Gateway binary sensors.""" - if discovery_info is None: - return - gw_vars = hass.data[DATA_OPENTHERM_GW][DATA_GW_VARS] - sensor_info = { - # [device_class, friendly_name] - gw_vars.DATA_MASTER_CH_ENABLED: [ - None, "Thermostat Central Heating Enabled"], - gw_vars.DATA_MASTER_DHW_ENABLED: [ - None, "Thermostat Hot Water Enabled"], - gw_vars.DATA_MASTER_COOLING_ENABLED: [ - None, "Thermostat Cooling Enabled"], - gw_vars.DATA_MASTER_OTC_ENABLED: [ - None, "Thermostat Outside Temperature Correction Enabled"], - gw_vars.DATA_MASTER_CH2_ENABLED: [ - None, "Thermostat Central Heating 2 Enabled"], - gw_vars.DATA_SLAVE_FAULT_IND: [ - DEVICE_CLASS_PROBLEM, "Boiler Fault Indication"], - gw_vars.DATA_SLAVE_CH_ACTIVE: [ - DEVICE_CLASS_HEAT, "Boiler Central Heating Status"], - gw_vars.DATA_SLAVE_DHW_ACTIVE: [ - DEVICE_CLASS_HEAT, "Boiler Hot Water Status"], - gw_vars.DATA_SLAVE_FLAME_ON: [ - DEVICE_CLASS_HEAT, "Boiler Flame Status"], - gw_vars.DATA_SLAVE_COOLING_ACTIVE: [ - DEVICE_CLASS_COLD, "Boiler Cooling Status"], - gw_vars.DATA_SLAVE_CH2_ACTIVE: [ - DEVICE_CLASS_HEAT, "Boiler Central Heating 2 Status"], - gw_vars.DATA_SLAVE_DIAG_IND: [ - DEVICE_CLASS_PROBLEM, "Boiler Diagnostics Indication"], - gw_vars.DATA_SLAVE_DHW_PRESENT: [None, "Boiler Hot Water Present"], - gw_vars.DATA_SLAVE_CONTROL_TYPE: [None, "Boiler Control Type"], - gw_vars.DATA_SLAVE_COOLING_SUPPORTED: [None, "Boiler Cooling Support"], - gw_vars.DATA_SLAVE_DHW_CONFIG: [ - None, "Boiler Hot Water Configuration"], - gw_vars.DATA_SLAVE_MASTER_LOW_OFF_PUMP: [ - None, "Boiler Pump Commands Support"], - gw_vars.DATA_SLAVE_CH2_PRESENT: [ - None, "Boiler Central Heating 2 Present"], - gw_vars.DATA_SLAVE_SERVICE_REQ: [ - DEVICE_CLASS_PROBLEM, "Boiler Service Required"], - gw_vars.DATA_SLAVE_REMOTE_RESET: [None, "Boiler Remote Reset Support"], - gw_vars.DATA_SLAVE_LOW_WATER_PRESS: [ - DEVICE_CLASS_PROBLEM, "Boiler Low Water Pressure"], - gw_vars.DATA_SLAVE_GAS_FAULT: [ - DEVICE_CLASS_PROBLEM, "Boiler Gas Fault"], - gw_vars.DATA_SLAVE_AIR_PRESS_FAULT: [ - DEVICE_CLASS_PROBLEM, "Boiler Air Pressure Fault"], - gw_vars.DATA_SLAVE_WATER_OVERTEMP: [ - DEVICE_CLASS_PROBLEM, "Boiler Water Overtemperature"], - gw_vars.DATA_REMOTE_TRANSFER_DHW: [ - None, "Remote Hot Water Setpoint Transfer Support"], - gw_vars.DATA_REMOTE_TRANSFER_MAX_CH: [ - None, "Remote Maximum Central Heating Setpoint Write Support"], - gw_vars.DATA_REMOTE_RW_DHW: [ - None, "Remote Hot Water Setpoint Write Support"], - gw_vars.DATA_REMOTE_RW_MAX_CH: [ - None, "Remote Central Heating Setpoint Write Support"], - gw_vars.DATA_ROVRD_MAN_PRIO: [ - None, "Remote Override Manual Change Priority"], - gw_vars.DATA_ROVRD_AUTO_PRIO: [ - None, "Remote Override Program Change Priority"], - gw_vars.OTGW_GPIO_A_STATE: [None, "Gateway GPIO A State"], - gw_vars.OTGW_GPIO_B_STATE: [None, "Gateway GPIO B State"], - gw_vars.OTGW_IGNORE_TRANSITIONS: [None, "Gateway Ignore Transitions"], - gw_vars.OTGW_OVRD_HB: [None, "Gateway Override High Byte"], - } - sensors = [] - for var in discovery_info: - device_class = sensor_info[var][0] - friendly_name = sensor_info[var][1] - entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, var, hass=hass) - sensors.append(OpenThermBinarySensor(entity_id, var, device_class, - friendly_name)) - async_add_entities(sensors) - - -class OpenThermBinarySensor(BinarySensorDevice): - """Represent an OpenTherm Gateway binary sensor.""" - - def __init__(self, entity_id, var, device_class, friendly_name): - """Initialize the binary sensor.""" - self.entity_id = entity_id - self._var = var - self._state = None - self._device_class = device_class - self._friendly_name = friendly_name - - async def async_added_to_hass(self): - """Subscribe to updates from the component.""" - _LOGGER.debug( - "Added OpenTherm Gateway binary sensor %s", self._friendly_name) - async_dispatcher_connect(self.hass, SIGNAL_OPENTHERM_GW_UPDATE, - self.receive_report) - - async def receive_report(self, status): - """Handle status updates from the component.""" - self._state = bool(status.get(self._var)) - self.async_schedule_update_ha_state() - - @property - def name(self): - """Return the friendly name.""" - return self._friendly_name - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self._state - - @property - def device_class(self): - """Return the class of this device.""" - return self._device_class - - @property - def should_poll(self): - """Return False because entity pushes its state.""" - return False diff --git a/homeassistant/components/binary_sensor/point.py b/homeassistant/components/binary_sensor/point.py deleted file mode 100644 index 1bd97ce274787..0000000000000 --- a/homeassistant/components/binary_sensor/point.py +++ /dev/null @@ -1,110 +0,0 @@ -""" -Support for Minut Point. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.point/ -""" - -import logging - -from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice -from homeassistant.components.point import MinutPointEntity -from homeassistant.components.point.const import ( - DOMAIN as POINT_DOMAIN, POINT_DISCOVERY_NEW, SIGNAL_WEBHOOK) -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect - -_LOGGER = logging.getLogger(__name__) - -EVENTS = { - 'battery': # On means low, Off means normal - ('battery_low', ''), - 'button_press': # On means the button was pressed, Off means normal - ('short_button_press', ''), - 'cold': # On means cold, Off means normal - ('temperature_low', 'temperature_risen_normal'), - 'connectivity': # On means connected, Off means disconnected - ('device_online', 'device_offline'), - 'dry': # On means too dry, Off means normal - ('humidity_low', 'humidity_risen_normal'), - 'heat': # On means hot, Off means normal - ('temperature_high', 'temperature_dropped_normal'), - 'moisture': # On means wet, Off means dry - ('humidity_high', 'humidity_dropped_normal'), - 'sound': # On means sound detected, Off means no sound (clear) - ('avg_sound_high', 'sound_level_dropped_normal'), - 'tamper': # On means the point was removed or attached - ('tamper', ''), -} - - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up a Point's binary sensors based on a config entry.""" - async def async_discover_sensor(device_id): - """Discover and add a discovered sensor.""" - client = hass.data[POINT_DOMAIN][config_entry.entry_id] - async_add_entities( - (MinutPointBinarySensor(client, device_id, device_class) - for device_class in EVENTS), True) - - async_dispatcher_connect( - hass, POINT_DISCOVERY_NEW.format(DOMAIN, POINT_DOMAIN), - async_discover_sensor) - - -class MinutPointBinarySensor(MinutPointEntity, BinarySensorDevice): - """The platform class required by Home Assistant.""" - - def __init__(self, point_client, device_id, device_class): - """Initialize the entity.""" - super().__init__(point_client, device_id, device_class) - - self._async_unsub_hook_dispatcher_connect = None - self._events = EVENTS[device_class] - self._is_on = None - - async def async_added_to_hass(self): - """Call when entity is added to hass.""" - await super().async_added_to_hass() - self._async_unsub_hook_dispatcher_connect = async_dispatcher_connect( - self.hass, SIGNAL_WEBHOOK, self._webhook_event) - - async def async_will_remove_from_hass(self): - """Disconnect dispatcher listener when removed.""" - await super().async_will_remove_from_hass() - if self._async_unsub_hook_dispatcher_connect: - self._async_unsub_hook_dispatcher_connect() - - @callback - def _update_callback(self): - """Update the value of the sensor.""" - if not self.is_updated: - return - if self._events[0] in self.device.ongoing_events: - self._is_on = True - else: - self._is_on = None - self.async_schedule_update_ha_state() - - @callback - def _webhook_event(self, data, webhook): - """Process new event from the webhook.""" - if self.device.webhook != webhook: - return - _type = data.get('event', {}).get('type') - if _type not in self._events: - return - _LOGGER.debug("Recieved webhook: %s", _type) - if _type == self._events[0]: - self._is_on = True - if _type == self._events[1]: - self._is_on = None - self.async_schedule_update_ha_state() - - @property - def is_on(self): - """Return the state of the binary sensor.""" - if self.device_class == 'connectivity': - # connectivity is the other way around. - return not self._is_on - return self._is_on diff --git a/homeassistant/components/binary_sensor/raspihats.py b/homeassistant/components/binary_sensor/raspihats.py deleted file mode 100644 index feef5396d8893..0000000000000 --- a/homeassistant/components/binary_sensor/raspihats.py +++ /dev/null @@ -1,119 +0,0 @@ -""" -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/ -""" -import logging - -import voluptuous as vol - -from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, BinarySensorDevice) -from homeassistant.components.raspihats import ( - CONF_ADDRESS, CONF_BOARD, CONF_CHANNELS, CONF_I2C_HATS, CONF_INDEX, - CONF_INVERT_LOGIC, I2C_HAT_NAMES, I2C_HATS_MANAGER, I2CHatsException) -from homeassistant.const import ( - CONF_DEVICE_CLASS, CONF_NAME, DEVICE_DEFAULT_NAME) -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['raspihats'] - -DEFAULT_INVERT_LOGIC = False -DEFAULT_DEVICE_CLASS = None - -_CHANNELS_SCHEMA = vol.Schema([{ - vol.Required(CONF_INDEX): cv.positive_int, - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, - vol.Optional(CONF_DEVICE_CLASS, default=DEFAULT_DEVICE_CLASS): cv.string, -}]) - -_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 -}]) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_I2C_HATS): _I2C_HATS_SCHEMA, -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the raspihats binary_sensor devices.""" - I2CHatBinarySensor.I2C_HATS_MANAGER = hass.data[I2C_HATS_MANAGER] - binary_sensors = [] - i2c_hat_configs = config.get(CONF_I2C_HATS) - for i2c_hat_config in i2c_hat_configs: - address = i2c_hat_config[CONF_ADDRESS] - board = i2c_hat_config[CONF_BOARD] - try: - I2CHatBinarySensor.I2C_HATS_MANAGER.register_board(board, address) - for channel_config in i2c_hat_config[CONF_CHANNELS]: - binary_sensors.append( - I2CHatBinarySensor( - address, - channel_config[CONF_INDEX], - channel_config[CONF_NAME], - channel_config[CONF_INVERT_LOGIC], - channel_config[CONF_DEVICE_CLASS] - ) - ) - except I2CHatsException as ex: - _LOGGER.error("Failed to register %s I2CHat@%s %s", - board, hex(address), str(ex)) - add_entities(binary_sensors) - - -class I2CHatBinarySensor(BinarySensorDevice): - """Representation of a binary sensor that uses a I2C-HAT digital input.""" - - I2C_HATS_MANAGER = None - - def __init__(self, address, channel, name, invert_logic, device_class): - """Initialize the raspihats sensor.""" - self._address = address - self._channel = channel - self._name = name or DEVICE_DEFAULT_NAME - self._invert_logic = invert_logic - self._device_class = device_class - self._state = self.I2C_HATS_MANAGER.read_di( - self._address, self._channel) - - def online_callback(): - """Call fired when board is online.""" - self.schedule_update_ha_state() - - self.I2C_HATS_MANAGER.register_online_callback( - self._address, self._channel, online_callback) - - def edge_callback(state): - """Read digital input state.""" - self._state = state - self.schedule_update_ha_state() - - self.I2C_HATS_MANAGER.register_di_callback( - self._address, self._channel, edge_callback) - - @property - def device_class(self): - """Return the class of this sensor.""" - return self._device_class - - @property - def name(self): - """Return the name of this sensor.""" - return self._name - - @property - def should_poll(self): - """No polling needed for this sensor.""" - return False - - @property - def is_on(self): - """Return the state of this sensor.""" - return self._state != self._invert_logic diff --git a/homeassistant/components/binary_sensor/rfxtrx.py b/homeassistant/components/binary_sensor/rfxtrx.py deleted file mode 100644 index 1e88c72e19d5a..0000000000000 --- a/homeassistant/components/binary_sensor/rfxtrx.py +++ /dev/null @@ -1,228 +0,0 @@ -""" -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/ -""" -import logging - -import voluptuous as vol - -from homeassistant.components import rfxtrx -from homeassistant.components.binary_sensor import ( - DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice) -from homeassistant.components.rfxtrx import ( - ATTR_NAME, CONF_AUTOMATIC_ADD, CONF_DATA_BITS, CONF_DEVICES, - CONF_FIRE_EVENT, CONF_OFF_DELAY) -from homeassistant.const import ( - CONF_COMMAND_OFF, CONF_COMMAND_ON, CONF_DEVICE_CLASS, CONF_NAME) -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import event as evt -from homeassistant.util import dt as dt_util -from homeassistant.util import slugify - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['rfxtrx'] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_DEVICES, default={}): { - cv.string: vol.Schema({ - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean, - vol.Optional(CONF_OFF_DELAY): - 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_AUTOMATIC_ADD, default=False): cv.boolean, -}, extra=vol.ALLOW_EXTRA) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Binary Sensor platform to RFXtrx.""" - import RFXtrx as rfxtrxmod - sensors = [] - - for packet_id, entity in config[CONF_DEVICES].items(): - event = rfxtrx.get_rfx_object(packet_id) - device_id = slugify(event.device.id_string.lower()) - - if device_id in rfxtrx.RFX_DEVICES: - continue - - if entity.get(CONF_DATA_BITS) is not None: - _LOGGER.debug( - "Masked device id: %s", rfxtrx.get_pt2262_deviceid( - device_id, entity.get(CONF_DATA_BITS))) - - _LOGGER.debug("Add %s rfxtrx.binary_sensor (class %s)", - entity[ATTR_NAME], entity.get(CONF_DEVICE_CLASS)) - - device = RfxtrxBinarySensor( - event, entity.get(CONF_NAME), entity.get(CONF_DEVICE_CLASS), - entity[CONF_FIRE_EVENT], entity.get(CONF_OFF_DELAY), - entity.get(CONF_DATA_BITS), entity.get(CONF_COMMAND_ON), - entity.get(CONF_COMMAND_OFF)) - device.hass = hass - sensors.append(device) - rfxtrx.RFX_DEVICES[device_id] = device - - add_entities(sensors) - - def binary_sensor_update(event): - """Call for control updates from the RFXtrx gateway.""" - if not isinstance(event, rfxtrxmod.ControlEvent): - return - - device_id = slugify(event.device.id_string.lower()) - - if device_id in rfxtrx.RFX_DEVICES: - sensor = rfxtrx.RFX_DEVICES[device_id] - else: - sensor = rfxtrx.get_pt2262_device(device_id) - - if sensor is None: - # Add the entity if not exists and automatic_add is True - if not config[CONF_AUTOMATIC_ADD]: - return - - if event.device.packettype == 0x13: - poss_dev = rfxtrx.find_possible_pt2262_device(device_id) - if poss_dev is not None: - poss_id = slugify(poss_dev.event.device.id_string.lower()) - _LOGGER.debug( - "Found possible matching device ID: %s", poss_id) - - pkt_id = "".join("{0:02x}".format(x) for x in event.data) - sensor = RfxtrxBinarySensor(event, pkt_id) - sensor.hass = hass - rfxtrx.RFX_DEVICES[device_id] = sensor - add_entities([sensor]) - _LOGGER.info( - "Added binary sensor %s (Device ID: %s Class: %s Sub: %s)", - pkt_id, slugify(event.device.id_string.lower()), - event.device.__class__.__name__, event.device.subtype) - - elif not isinstance(sensor, RfxtrxBinarySensor): - return - else: - _LOGGER.debug( - "Binary sensor update (Device ID: %s Class: %s Sub: %s)", - slugify(event.device.id_string.lower()), - event.device.__class__.__name__, event.device.subtype) - - if sensor.is_lighting4: - if sensor.data_bits is not None: - cmd = rfxtrx.get_pt2262_cmd(device_id, sensor.data_bits) - sensor.apply_cmd(int(cmd, 16)) - else: - sensor.update_state(True) - else: - rfxtrx.apply_received_command(event) - - if (sensor.is_on and sensor.off_delay is not None and - sensor.delay_listener is None): - - def off_delay_listener(now): - """Switch device off after a delay.""" - sensor.delay_listener = None - sensor.update_state(False) - - sensor.delay_listener = evt.track_point_in_time( - hass, off_delay_listener, dt_util.utcnow() + sensor.off_delay) - - # Subscribe to main RFXtrx events - if binary_sensor_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS: - rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(binary_sensor_update) - - -class RfxtrxBinarySensor(BinarySensorDevice): - """A representation of a RFXtrx binary sensor.""" - - def __init__(self, event, name, device_class=None, - should_fire=False, off_delay=None, data_bits=None, - cmd_on=None, cmd_off=None): - """Initialize the RFXtrx sensor.""" - self.event = event - self._name = name - self._should_fire_event = should_fire - self._device_class = device_class - self._off_delay = off_delay - self._state = False - self.is_lighting4 = (event.device.packettype == 0x13) - self.delay_listener = None - self._data_bits = data_bits - self._cmd_on = cmd_on - self._cmd_off = cmd_off - - if data_bits is not None: - self._masked_id = rfxtrx.get_pt2262_deviceid( - event.device.id_string.lower(), data_bits) - else: - self._masked_id = None - - @property - def name(self): - """Return the device name.""" - return self._name - - @property - def masked_id(self): - """Return the masked device id (isolated address bits).""" - return self._masked_id - - @property - def data_bits(self): - """Return the number of data bits.""" - return self._data_bits - - @property - def cmd_on(self): - """Return the value of the 'On' command.""" - return self._cmd_on - - @property - def cmd_off(self): - """Return the value of the 'Off' command.""" - return self._cmd_off - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def should_fire_event(self): - """Return is the device must fire event.""" - return self._should_fire_event - - @property - def device_class(self): - """Return the sensor class.""" - return self._device_class - - @property - def off_delay(self): - """Return the off_delay attribute value.""" - return self._off_delay - - @property - def is_on(self): - """Return true if the sensor state is True.""" - return self._state - - def apply_cmd(self, cmd): - """Apply a command for updating the state.""" - if cmd == self.cmd_on: - self.update_state(True) - elif cmd == self.cmd_off: - self.update_state(False) - - def update_state(self, state): - """Update the state of the device.""" - self._state = state - self.schedule_update_ha_state() diff --git a/homeassistant/components/binary_sensor/rpi_gpio.py b/homeassistant/components/binary_sensor/rpi_gpio.py deleted file mode 100644 index 2fe4e0766ed7e..0000000000000 --- a/homeassistant/components/binary_sensor/rpi_gpio.py +++ /dev/null @@ -1,94 +0,0 @@ -""" -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/ -""" -import logging - -import voluptuous as vol - -from homeassistant.components import rpi_gpio -from homeassistant.components.binary_sensor import ( - BinarySensorDevice, PLATFORM_SCHEMA) -from homeassistant.const import DEVICE_DEFAULT_NAME -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -CONF_BOUNCETIME = 'bouncetime' -CONF_INVERT_LOGIC = 'invert_logic' -CONF_PORTS = 'ports' -CONF_PULL_MODE = 'pull_mode' - -DEFAULT_BOUNCETIME = 50 -DEFAULT_INVERT_LOGIC = False -DEFAULT_PULL_MODE = 'UP' - -DEPENDENCIES = ['rpi_gpio'] - -_SENSORS_SCHEMA = vol.Schema({ - cv.positive_int: cv.string, -}) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_PORTS): _SENSORS_SCHEMA, - vol.Optional(CONF_BOUNCETIME, default=DEFAULT_BOUNCETIME): cv.positive_int, - vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, - vol.Optional(CONF_PULL_MODE, default=DEFAULT_PULL_MODE): cv.string, -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Raspberry PI GPIO devices.""" - pull_mode = config.get(CONF_PULL_MODE) - bouncetime = config.get(CONF_BOUNCETIME) - invert_logic = config.get(CONF_INVERT_LOGIC) - - binary_sensors = [] - ports = config.get('ports') - for port_num, port_name in ports.items(): - binary_sensors.append(RPiGPIOBinarySensor( - port_name, port_num, pull_mode, bouncetime, invert_logic)) - add_entities(binary_sensors, True) - - -class RPiGPIOBinarySensor(BinarySensorDevice): - """Represent a binary sensor that uses Raspberry Pi GPIO.""" - - def __init__(self, name, port, pull_mode, bouncetime, invert_logic): - """Initialize the RPi binary sensor.""" - self._name = name or DEVICE_DEFAULT_NAME - self._port = port - self._pull_mode = pull_mode - self._bouncetime = bouncetime - self._invert_logic = invert_logic - self._state = None - - rpi_gpio.setup_input(self._port, self._pull_mode) - - def read_gpio(port): - """Read state from GPIO.""" - self._state = rpi_gpio.read_input(self._port) - self.schedule_update_ha_state() - - rpi_gpio.edge_detect(self._port, read_gpio, self._bouncetime) - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def is_on(self): - """Return the state of the entity.""" - return self._state != self._invert_logic - - def update(self): - """Update the GPIO state.""" - self._state = rpi_gpio.read_input(self._port) diff --git a/homeassistant/components/binary_sensor/rpi_pfio.py b/homeassistant/components/binary_sensor/rpi_pfio.py deleted file mode 100644 index 61d1f8ac285f6..0000000000000 --- a/homeassistant/components/binary_sensor/rpi_pfio.py +++ /dev/null @@ -1,92 +0,0 @@ -""" -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/ -""" -import logging - -import voluptuous as vol - -from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, BinarySensorDevice) -from homeassistant.components import rpi_pfio -from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -CONF_INVERT_LOGIC = 'invert_logic' -CONF_PORTS = 'ports' -CONF_SETTLE_TIME = 'settle_time' - -DEFAULT_INVERT_LOGIC = False -DEFAULT_SETTLE_TIME = 20 - -DEPENDENCIES = ['rpi_pfio'] - -PORT_SCHEMA = vol.Schema({ - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_SETTLE_TIME, default=DEFAULT_SETTLE_TIME): - cv.positive_int, - vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, -}) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_PORTS, default={}): vol.Schema({ - cv.positive_int: PORT_SCHEMA, - }) -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the PiFace Digital Input devices.""" - binary_sensors = [] - ports = config.get(CONF_PORTS) - for port, port_entity in ports.items(): - name = port_entity.get(CONF_NAME) - settle_time = port_entity[CONF_SETTLE_TIME] / 1000 - invert_logic = port_entity[CONF_INVERT_LOGIC] - - binary_sensors.append(RPiPFIOBinarySensor( - hass, port, name, settle_time, invert_logic)) - add_entities(binary_sensors, True) - - rpi_pfio.activate_listener(hass) - - -class RPiPFIOBinarySensor(BinarySensorDevice): - """Represent a binary sensor that a PiFace Digital Input.""" - - def __init__(self, hass, port, name, settle_time, invert_logic): - """Initialize the RPi binary sensor.""" - self._port = port - self._name = name or DEVICE_DEFAULT_NAME - self._invert_logic = invert_logic - self._state = None - - def read_pfio(port): - """Read state from PFIO.""" - self._state = rpi_pfio.read_input(self._port) - self.schedule_update_ha_state() - - rpi_pfio.edge_detect(hass, self._port, read_pfio, settle_time) - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def is_on(self): - """Return the state of the entity.""" - return self._state != self._invert_logic - - def update(self): - """Update the PFIO state.""" - self._state = rpi_pfio.read_input(self._port) diff --git a/homeassistant/components/binary_sensor/satel_integra.py b/homeassistant/components/binary_sensor/satel_integra.py deleted file mode 100644 index b4541cf2c4457..0000000000000 --- a/homeassistant/components/binary_sensor/satel_integra.py +++ /dev/null @@ -1,101 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.satel_integra import (CONF_ZONES, - CONF_OUTPUTS, - CONF_ZONE_NAME, - CONF_ZONE_TYPE, - SIGNAL_ZONES_UPDATED, - SIGNAL_OUTPUTS_UPDATED) -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect - -DEPENDENCIES = ['satel_integra'] - -_LOGGER = logging.getLogger(__name__) - - -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 - - configured_zones = discovery_info[CONF_ZONES] - - devices = [] - - 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) - devices.append(device) - - configured_outputs = discovery_info[CONF_OUTPUTS] - - 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) - devices.append(device) - - async_add_entities(devices) - - -class SatelIntegraBinarySensor(BinarySensorDevice): - """Representation of an Satel Integra binary sensor.""" - - def __init__(self, device_number, device_name, zone_type, react_to_signal): - """Initialize the binary_sensor.""" - self._device_number = device_number - self._name = device_name - self._zone_type = zone_type - self._state = 0 - self._react_to_signal = react_to_signal - - async def async_added_to_hass(self): - """Register callbacks.""" - async_dispatcher_connect( - self.hass, self._react_to_signal, self._devices_updated) - - @property - def name(self): - """Return the name of the entity.""" - return self._name - - @property - def icon(self): - """Icon for device by its type.""" - if self._zone_type == 'smoke': - return "mdi:fire" - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def is_on(self): - """Return true if sensor is on.""" - return self._state == 1 - - @property - def device_class(self): - """Return the class of this sensor, from DEVICE_CLASSES.""" - return self._zone_type - - @callback - def _devices_updated(self, zones): - """Update the zone's state, if needed.""" - if self._device_number in zones \ - and self._state != zones[self._device_number]: - self._state = zones[self._device_number] - self.async_schedule_update_ha_state() diff --git a/homeassistant/components/binary_sensor/sense.py b/homeassistant/components/binary_sensor/sense.py deleted file mode 100644 index a85a0c889d17a..0000000000000 --- a/homeassistant/components/binary_sensor/sense.py +++ /dev/null @@ -1,119 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.sense import SENSE_DATA - -DEPENDENCIES = ['sense'] - -_LOGGER = logging.getLogger(__name__) - -BIN_SENSOR_CLASS = 'power' -MDI_ICONS = { - 'ac': 'air-conditioner', - 'aquarium': 'fish', - 'car': 'car-electric', - 'computer': 'desktop-classic', - 'cup': 'coffee', - 'dehumidifier': 'water-off', - 'dishes': 'dishwasher', - 'drill': 'toolbox', - 'fan': 'fan', - 'freezer': 'fridge-top', - 'fridge': 'fridge-bottom', - 'game': 'gamepad-variant', - 'garage': 'garage', - 'grill': 'stove', - 'heat': 'fire', - 'heater': 'radiatior', - 'humidifier': 'water', - 'kettle': 'kettle', - 'leafblower': 'leaf', - 'lightbulb': 'lightbulb', - 'media_console': 'set-top-box', - 'modem': 'router-wireless', - 'outlet': 'power-socket-us', - 'papershredder': 'shredder', - 'printer': 'printer', - 'pump': 'water-pump', - 'settings': 'settings', - 'skillet': 'pot', - 'smartcamera': 'webcam', - 'socket': 'power-plug', - 'sound': 'speaker', - 'stove': 'stove', - 'trash': 'trash-can', - 'tv': 'television', - 'vacuum': 'robot-vacuum', - 'washer': 'washing-machine', -} - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Sense binary sensor.""" - if discovery_info is None: - return - - data = hass.data[SENSE_DATA] - - sense_devices = data.get_discovered_device_data() - devices = [SenseDevice(data, device) for device in sense_devices - if device['tags']['DeviceListAllowed'] == 'true'] - add_entities(devices) - - -def sense_to_mdi(sense_icon): - """Convert sense icon to mdi icon.""" - return 'mdi:{}'.format(MDI_ICONS.get(sense_icon, 'power-plug')) - - -class SenseDevice(BinarySensorDevice): - """Implementation of a Sense energy device binary sensor.""" - - def __init__(self, data, device): - """Initialize the Sense binary sensor.""" - self._name = device['name'] - self._id = device['id'] - self._icon = sense_to_mdi(device['icon']) - self._data = data - self._state = False - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self._state - - @property - def name(self): - """Return the name of the binary sensor.""" - return self._name - - @property - def unique_id(self): - """Return the id of the binary sensor.""" - return self._id - - @property - def icon(self): - """Return the icon of the binary sensor.""" - return self._icon - - @property - def device_class(self): - """Return the device class of the binary sensor.""" - return BIN_SENSOR_CLASS - - def update(self): - """Retrieve latest state.""" - from sense_energy.sense_api import SenseAPITimeoutException - try: - self._data.get_realtime() - except SenseAPITimeoutException: - _LOGGER.error("Timeout retrieving data") - return - self._state = self._name in self._data.active_devices diff --git a/homeassistant/components/binary_sensor/skybell.py b/homeassistant/components/binary_sensor/skybell.py deleted file mode 100644 index 7d8b3a84a2a2b..0000000000000 --- a/homeassistant/components/binary_sensor/skybell.py +++ /dev/null @@ -1,97 +0,0 @@ -""" -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/ -""" -from datetime import timedelta -import logging - -import voluptuous as vol - -from homeassistant.components.binary_sensor import ( - BinarySensorDevice, PLATFORM_SCHEMA) -from homeassistant.components.skybell import ( - DEFAULT_ENTITY_NAMESPACE, DOMAIN as SKYBELL_DOMAIN, SkybellDevice) -from homeassistant.const import ( - CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS) -import homeassistant.helpers.config_validation as cv - -DEPENDENCIES = ['skybell'] - -_LOGGER = logging.getLogger(__name__) - -SCAN_INTERVAL = timedelta(seconds=5) - -# Sensor types: Name, device_class, event -SENSOR_TYPES = { - 'button': ['Button', 'occupancy', 'device:sensor:button'], - 'motion': ['Motion', 'motion', 'device:sensor:motion'], -} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_ENTITY_NAMESPACE, default=DEFAULT_ENTITY_NAMESPACE): - cv.string, - vol.Required(CONF_MONITORED_CONDITIONS, default=[]): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the platform for a Skybell device.""" - skybell = hass.data.get(SKYBELL_DOMAIN) - - sensors = [] - for sensor_type in config.get(CONF_MONITORED_CONDITIONS): - for device in skybell.get_devices(): - sensors.append(SkybellBinarySensor(device, sensor_type)) - - add_entities(sensors, True) - - -class SkybellBinarySensor(SkybellDevice, BinarySensorDevice): - """A binary sensor implementation for Skybell devices.""" - - def __init__(self, device, sensor_type): - """Initialize a binary sensor for a Skybell device.""" - super().__init__(device) - self._sensor_type = sensor_type - self._name = "{0} {1}".format(self._device.name, - SENSOR_TYPES[self._sensor_type][0]) - self._device_class = SENSOR_TYPES[self._sensor_type][1] - self._event = {} - self._state = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def is_on(self): - """Return True if the binary sensor is on.""" - return self._state - - @property - def device_class(self): - """Return the class of the binary sensor.""" - return self._device_class - - @property - def device_state_attributes(self): - """Return the state attributes.""" - attrs = super().device_state_attributes - - attrs['event_date'] = self._event.get('createdAt') - - return attrs - - def update(self): - """Get the latest data and updates the state.""" - super().update() - - event = self._device.latest(SENSOR_TYPES[self._sensor_type][2]) - - self._state = bool(event and event.get('id') != self._event.get('id')) - - self._event = event or {} diff --git a/homeassistant/components/binary_sensor/tahoma.py b/homeassistant/components/binary_sensor/tahoma.py deleted file mode 100644 index 73035a2da0d78..0000000000000 --- a/homeassistant/components/binary_sensor/tahoma.py +++ /dev/null @@ -1,106 +0,0 @@ -""" -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/ -""" - -import logging -from datetime import timedelta - -from homeassistant.components.binary_sensor import ( - BinarySensorDevice) -from homeassistant.components.tahoma import ( - DOMAIN as TAHOMA_DOMAIN, TahomaDevice) -from homeassistant.const import (STATE_OFF, STATE_ON, ATTR_BATTERY_LEVEL) - -DEPENDENCIES = ['tahoma'] - -_LOGGER = logging.getLogger(__name__) - -SCAN_INTERVAL = timedelta(seconds=120) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Tahoma controller devices.""" - _LOGGER.debug("Setup Tahoma Binary sensor platform") - controller = hass.data[TAHOMA_DOMAIN]['controller'] - devices = [] - for device in hass.data[TAHOMA_DOMAIN]['devices']['smoke']: - devices.append(TahomaBinarySensor(device, controller)) - add_entities(devices, True) - - -class TahomaBinarySensor(TahomaDevice, BinarySensorDevice): - """Representation of a Tahoma Binary Sensor.""" - - def __init__(self, tahoma_device, controller): - """Initialize the sensor.""" - super().__init__(tahoma_device, controller) - - self._state = None - self._icon = None - self._battery = None - self._available = False - - @property - def is_on(self): - """Return the state of the sensor.""" - return bool(self._state == STATE_ON) - - @property - def device_class(self): - """Return the class of the device.""" - if self.tahoma_device.type == 'rtds:RTDSSmokeSensor': - return 'smoke' - return None - - @property - def icon(self): - """Icon for device by its type.""" - return self._icon - - @property - def device_state_attributes(self): - """Return the device state attributes.""" - attr = {} - super_attr = super().device_state_attributes - if super_attr is not None: - attr.update(super_attr) - - if self._battery is not None: - attr[ATTR_BATTERY_LEVEL] = self._battery - return attr - - @property - def available(self): - """Return True if entity is available.""" - return self._available - - def update(self): - """Update the state.""" - self.controller.get_states([self.tahoma_device]) - if self.tahoma_device.type == 'rtds:RTDSSmokeSensor': - if self.tahoma_device.active_states['core:SmokeState']\ - == 'notDetected': - self._state = STATE_OFF - else: - self._state = STATE_ON - - if 'core:SensorDefectState' in self.tahoma_device.active_states: - # 'lowBattery' for low battery warning. 'dead' for not available. - self._battery = self.tahoma_device.active_states[ - 'core:SensorDefectState'] - self._available = bool(self._battery != 'dead') - else: - self._battery = None - self._available = True - - if self._state == STATE_ON: - self._icon = "mdi:fire" - elif self._battery == 'lowBattery': - self._icon = "mdi:battery-alert" - else: - self._icon = None - - _LOGGER.debug("Update %s, state: %s", self._name, self._state) diff --git a/homeassistant/components/binary_sensor/tellduslive.py b/homeassistant/components/binary_sensor/tellduslive.py deleted file mode 100644 index f6ed85db132bd..0000000000000 --- a/homeassistant/components/binary_sensor/tellduslive.py +++ /dev/null @@ -1,49 +0,0 @@ -""" -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/ - -""" -import logging - -from homeassistant.components import binary_sensor, tellduslive -from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.tellduslive.entry import TelldusLiveEntity -from homeassistant.helpers.dispatcher import async_dispatcher_connect - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Old way of setting up TelldusLive. - - Can only be called when a user accidentally mentions the platform in their - config. But even in that case it would have been ignored. - """ - pass - - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up tellduslive sensors dynamically.""" - async def async_discover_binary_sensor(device_id): - """Discover and add a discovered sensor.""" - client = hass.data[tellduslive.DOMAIN] - async_add_entities([TelldusLiveSensor(client, device_id)]) - - async_dispatcher_connect( - hass, - tellduslive.TELLDUS_DISCOVERY_NEW.format(binary_sensor.DOMAIN, - tellduslive.DOMAIN), - async_discover_binary_sensor) - - -class TelldusLiveSensor(TelldusLiveEntity, BinarySensorDevice): - """Representation of a Tellstick sensor.""" - - @property - def is_on(self): - """Return true if switch is on.""" - return self.device.is_on diff --git a/homeassistant/components/binary_sensor/tesla.py b/homeassistant/components/binary_sensor/tesla.py deleted file mode 100644 index f7613d74dfbba..0000000000000 --- a/homeassistant/components/binary_sensor/tesla.py +++ /dev/null @@ -1,56 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.components.binary_sensor import ( - BinarySensorDevice, ENTITY_ID_FORMAT) -from homeassistant.components.tesla import DOMAIN as TESLA_DOMAIN, TeslaDevice - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['tesla'] - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Tesla binary sensor.""" - devices = [ - TeslaBinarySensor( - device, hass.data[TESLA_DOMAIN]['controller'], 'connectivity') - for device in hass.data[TESLA_DOMAIN]['devices']['binary_sensor']] - add_entities(devices, True) - - -class TeslaBinarySensor(TeslaDevice, BinarySensorDevice): - """Implement an Tesla binary sensor for parking and charger.""" - - def __init__(self, tesla_device, controller, sensor_type): - """Initialise of a Tesla binary sensor.""" - super().__init__(tesla_device, controller) - self._state = False - self.entity_id = ENTITY_ID_FORMAT.format(self.tesla_id) - self._sensor_type = sensor_type - - @property - def device_class(self): - """Return the class of this binary sensor.""" - return self._sensor_type - - @property - def name(self): - """Return the name of the binary sensor.""" - return self._name - - @property - def is_on(self): - """Return the state of the binary sensor.""" - return self._state - - def update(self): - """Update the state of the device.""" - _LOGGER.debug("Updating sensor: %s", self._name) - self.tesla_device.update() - self._state = self.tesla_device.get_value() diff --git a/homeassistant/components/binary_sensor/upcloud.py b/homeassistant/components/binary_sensor/upcloud.py deleted file mode 100644 index c7b8a284dc97f..0000000000000 --- a/homeassistant/components/binary_sensor/upcloud.py +++ /dev/null @@ -1,38 +0,0 @@ -""" -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/ -""" -import logging - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components.binary_sensor import ( - BinarySensorDevice, PLATFORM_SCHEMA) -from homeassistant.components.upcloud import ( - UpCloudServerEntity, CONF_SERVERS, DATA_UPCLOUD) - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['upcloud'] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_SERVERS): vol.All(cv.ensure_list, [cv.string]), -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the UpCloud server binary sensor.""" - upcloud = hass.data[DATA_UPCLOUD] - - servers = config.get(CONF_SERVERS) - - devices = [UpCloudBinarySensor(upcloud, uuid) for uuid in servers] - - add_entities(devices, True) - - -class UpCloudBinarySensor(UpCloudServerEntity, BinarySensorDevice): - """Representation of an UpCloud server sensor.""" diff --git a/homeassistant/components/binary_sensor/velbus.py b/homeassistant/components/binary_sensor/velbus.py deleted file mode 100644 index b123b958560fd..0000000000000 --- a/homeassistant/components/binary_sensor/velbus.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.velbus import ( - DOMAIN as VELBUS_DOMAIN, VelbusEntity) - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['velbus'] - - -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up Velbus binary sensors.""" - if discovery_info is None: - return - sensors = [] - for sensor in discovery_info: - module = hass.data[VELBUS_DOMAIN].get_module(sensor[0]) - channel = sensor[1] - sensors.append(VelbusBinarySensor(module, channel)) - async_add_entities(sensors) - - -class VelbusBinarySensor(VelbusEntity, BinarySensorDevice): - """Representation of a Velbus Binary Sensor.""" - - @property - def is_on(self): - """Return true if the sensor is on.""" - return self._module.is_closed(self._channel) diff --git a/homeassistant/components/binary_sensor/vera.py b/homeassistant/components/binary_sensor/vera.py deleted file mode 100644 index bb1e7331de826..0000000000000 --- a/homeassistant/components/binary_sensor/vera.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.components.binary_sensor import ( - BinarySensorDevice, ENTITY_ID_FORMAT) -from homeassistant.components.vera import ( - VERA_CONTROLLER, VERA_DEVICES, VeraDevice) - -DEPENDENCIES = ['vera'] - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Perform the setup for Vera controller devices.""" - add_entities( - [VeraBinarySensor(device, hass.data[VERA_CONTROLLER]) - for device in hass.data[VERA_DEVICES]['binary_sensor']], True) - - -class VeraBinarySensor(VeraDevice, BinarySensorDevice): - """Representation of a Vera Binary Sensor.""" - - def __init__(self, vera_device, controller): - """Initialize the binary_sensor.""" - self._state = False - VeraDevice.__init__(self, vera_device, controller) - self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) - - @property - def is_on(self): - """Return true if sensor is on.""" - return self._state - - def update(self): - """Get the latest data and update the state.""" - self._state = self.vera_device.is_tripped diff --git a/homeassistant/components/binary_sensor/verisure.py b/homeassistant/components/binary_sensor/verisure.py deleted file mode 100644 index e040da959eaa1..0000000000000 --- a/homeassistant/components/binary_sensor/verisure.py +++ /dev/null @@ -1,60 +0,0 @@ -""" -Interfaces with Verisure sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.verisure/ -""" -import logging - -from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.verisure import CONF_DOOR_WINDOW -from homeassistant.components.verisure import HUB as hub - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Verisure binary sensors.""" - sensors = [] - hub.update_overview() - - if int(hub.config.get(CONF_DOOR_WINDOW, 1)): - sensors.extend([ - VerisureDoorWindowSensor(device_label) - for device_label in hub.get( - "$.doorWindow.doorWindowDevice[*].deviceLabel")]) - add_entities(sensors) - - -class VerisureDoorWindowSensor(BinarySensorDevice): - """Representation of a Verisure door window sensor.""" - - def __init__(self, device_label): - """Initialize the Verisure door window sensor.""" - self._device_label = device_label - - @property - def name(self): - """Return the name of the binary sensor.""" - return hub.get_first( - "$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')].area", - self._device_label) - - @property - def is_on(self): - """Return the state of the sensor.""" - return hub.get_first( - "$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')].state", - self._device_label) == "OPEN" - - @property - def available(self): - """Return True if entity is available.""" - return hub.get_first( - "$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')]", - self._device_label) is not None - - # pylint: disable=no-self-use - def update(self): - """Update the state of the sensor.""" - hub.update_overview() diff --git a/homeassistant/components/binary_sensor/volvooncall.py b/homeassistant/components/binary_sensor/volvooncall.py deleted file mode 100644 index e7092ff16d527..0000000000000 --- a/homeassistant/components/binary_sensor/volvooncall.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -Support for VOC. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.volvooncall/ -""" -import logging - -from homeassistant.components.volvooncall import VolvoEntity, DATA_KEY -from homeassistant.components.binary_sensor import ( - BinarySensorDevice, DEVICE_CLASSES) - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up the Volvo sensors.""" - if discovery_info is None: - return - async_add_entities([VolvoSensor(hass.data[DATA_KEY], *discovery_info)]) - - -class VolvoSensor(VolvoEntity, BinarySensorDevice): - """Representation of a Volvo sensor.""" - - @property - def is_on(self): - """Return True if the binary sensor is on.""" - return self.instrument.is_on - - @property - def device_class(self): - """Return the class of this sensor, from DEVICE_CLASSES.""" - if self.instrument.device_class in DEVICE_CLASSES: - return self.instrument.device_class - return None diff --git a/homeassistant/components/binary_sensor/w800rf32.py b/homeassistant/components/binary_sensor/w800rf32.py deleted file mode 100644 index 48ac6f41a1273..0000000000000 --- a/homeassistant/components/binary_sensor/w800rf32.py +++ /dev/null @@ -1,132 +0,0 @@ -""" -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/ - -""" -import logging - -import voluptuous as vol - -from homeassistant.components.binary_sensor import ( - DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice) -from homeassistant.components.w800rf32 import (W800RF32_DEVICE) -from homeassistant.const import (CONF_DEVICE_CLASS, CONF_NAME, CONF_DEVICES) -from homeassistant.core import callback -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import event as evt -from homeassistant.util import dt as dt_util -from homeassistant.helpers.dispatcher import (async_dispatcher_connect) - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['w800rf32'] -CONF_OFF_DELAY = 'off_delay' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_DEVICES): { - cv.string: vol.Schema({ - 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) - }) - }, -}, extra=vol.ALLOW_EXTRA) - - -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) - # --> name, device_class etc - for device_id, entity in config[CONF_DEVICES].items(): - - _LOGGER.debug("Add %s w800rf32.binary_sensor (class %s)", - entity[CONF_NAME], entity.get(CONF_DEVICE_CLASS)) - - device = W800rf32BinarySensor( - device_id, entity.get(CONF_NAME), entity.get(CONF_DEVICE_CLASS), - entity.get(CONF_OFF_DELAY)) - - binary_sensors.append(device) - - add_entities(binary_sensors) - - -class W800rf32BinarySensor(BinarySensorDevice): - """A representation of a w800rf32 binary sensor.""" - - def __init__(self, device_id, name, device_class=None, off_delay=None): - """Initialize the w800rf32 sensor.""" - self._signal = W800RF32_DEVICE.format(device_id) - self._name = name - self._device_class = device_class - self._off_delay = off_delay - self._state = False - self._delay_listener = None - - @callback - def _off_delay_listener(self, now): - """Switch device off after a delay.""" - self._delay_listener = None - self.update_state(False) - - @property - def name(self): - """Return the device name.""" - return self._name - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def device_class(self): - """Return the sensor class.""" - return self._device_class - - @property - def is_on(self): - """Return true if the sensor state is True.""" - return self._state - - @callback - def binary_sensor_update(self, event): - """Call for control updates from the w800rf32 gateway.""" - import W800rf32 as w800rf32mod - - if not isinstance(event, w800rf32mod.W800rf32Event): - return - - dev_id = event.device - command = event.command - - _LOGGER.debug( - "BinarySensor update (Device ID: %s Command %s ...)", - dev_id, command) - - # Update the w800rf32 device state - if command in ('On', 'Off'): - is_on = command == 'On' - self.update_state(is_on) - - if (self.is_on and self._off_delay is not None and - self._delay_listener is None): - - self._delay_listener = evt.async_track_point_in_time( - self.hass, self._off_delay_listener, - dt_util.utcnow() + self._off_delay) - - def update_state(self, state): - """Update the state of the device.""" - self._state = state - self.async_schedule_update_ha_state() - - async def async_added_to_hass(self): - """Register update callback.""" - async_dispatcher_connect(self.hass, self._signal, - self.binary_sensor_update) diff --git a/homeassistant/components/binary_sensor/wemo.py b/homeassistant/components/binary_sensor/wemo.py deleted file mode 100644 index e44cbb31e6681..0000000000000 --- a/homeassistant/components/binary_sensor/wemo.py +++ /dev/null @@ -1,133 +0,0 @@ -""" -Support for WeMo sensors. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.wemo/ -""" -import asyncio -import logging - -import async_timeout -import requests - -from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.exceptions import PlatformNotReady - -DEPENDENCIES = ['wemo'] - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Register discovered WeMo binary sensors.""" - from pywemo import discovery - - if discovery_info is not None: - location = discovery_info['ssdp_description'] - mac = discovery_info['mac_address'] - - try: - 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) - raise PlatformNotReady - - if device: - add_entities([WemoBinarySensor(hass, device)]) - - -class WemoBinarySensor(BinarySensorDevice): - """Representation a WeMo binary sensor.""" - - def __init__(self, hass, device): - """Initialize the WeMo sensor.""" - self.wemo = device - self._state = None - self._available = True - self._update_lock = None - self._model_name = self.wemo.model_name - self._name = self.wemo.name - self._serialnumber = self.wemo.serialnumber - - def _subscription_callback(self, _device, _type, _params): - """Update the state by the Wemo sensor.""" - _LOGGER.debug("Subscription update for %s", self.name) - updated = self.wemo.subscription_update(_type, _params) - self.hass.add_job( - self._async_locked_subscription_callback(not updated)) - - async def _async_locked_subscription_callback(self, force_update): - """Handle an update from a subscription.""" - # If an update is in progress, we don't do anything - if self._update_lock.locked(): - return - - await self._async_locked_update(force_update) - self.async_schedule_update_ha_state() - - async def async_added_to_hass(self): - """Wemo sensor added to HASS.""" - # Define inside async context so we know our event loop - self._update_lock = asyncio.Lock() - - registry = self.hass.components.wemo.SUBSCRIPTION_REGISTRY - await self.hass.async_add_executor_job(registry.register, self.wemo) - registry.on(self.wemo, None, self._subscription_callback) - - async def async_update(self): - """Update WeMo state. - - Wemo has an aggressive retry logic that sometimes can take over a - minute to return. If we don't get a state after 5 seconds, assume the - Wemo sensor is unreachable. If update goes through, it will be made - available again. - """ - # If an update is in progress, we don't do anything - if self._update_lock.locked(): - return - - try: - with async_timeout.timeout(5): - await asyncio.shield(self._async_locked_update(True)) - except asyncio.TimeoutError: - _LOGGER.warning('Lost connection to %s', self.name) - self._available = False - - async def _async_locked_update(self, force_update): - """Try updating within an async lock.""" - async with self._update_lock: - await self.hass.async_add_executor_job(self._update, force_update) - - def _update(self, force_update=True): - """Update the sensor state.""" - try: - self._state = self.wemo.get_state(force_update) - - if not self._available: - _LOGGER.info('Reconnected to %s', self.name) - self._available = True - except AttributeError as err: - _LOGGER.warning("Could not update status for %s (%s)", - self.name, err) - self._available = False - - @property - def unique_id(self): - """Return the id of this WeMo sensor.""" - return self._serialnumber - - @property - def name(self): - """Return the name of the service if any.""" - return self._name - - @property - def is_on(self): - """Return true if sensor is on.""" - return self._state - - @property - def available(self): - """Return true if sensor is available.""" - return self._available diff --git a/homeassistant/components/binary_sensor/wink.py b/homeassistant/components/binary_sensor/wink.py deleted file mode 100644 index a950289789e78..0000000000000 --- a/homeassistant/components/binary_sensor/wink.py +++ /dev/null @@ -1,191 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.wink import DOMAIN, WinkDevice - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['wink'] - -# These are the available sensors mapped to binary_sensor class -SENSOR_TYPES = { - 'brightness': 'light', - 'capturing_audio': 'sound', - 'capturing_video': None, - 'co_detected': 'gas', - 'liquid_detected': 'moisture', - 'loudness': 'sound', - 'motion': 'motion', - 'noise': 'sound', - 'opened': 'opening', - 'presence': 'occupancy', - 'smoke_detected': 'smoke', - 'vibration': 'vibration', -} - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Wink binary sensor platform.""" - import pywink - - for sensor in pywink.get_sensors(): - _id = sensor.object_id() + sensor.name() - if _id not in hass.data[DOMAIN]['unique_ids']: - if sensor.capability() in SENSOR_TYPES: - add_entities([WinkBinarySensorDevice(sensor, hass)]) - - for key in pywink.get_keys(): - _id = key.object_id() + key.name() - if _id not in hass.data[DOMAIN]['unique_ids']: - add_entities([WinkBinarySensorDevice(key, hass)]) - - for sensor in pywink.get_smoke_and_co_detectors(): - _id = sensor.object_id() + sensor.name() - if _id not in hass.data[DOMAIN]['unique_ids']: - add_entities([WinkSmokeDetector(sensor, hass)]) - - for hub in pywink.get_hubs(): - _id = hub.object_id() + hub.name() - if _id not in hass.data[DOMAIN]['unique_ids']: - add_entities([WinkHub(hub, hass)]) - - for remote in pywink.get_remotes(): - _id = remote.object_id() + remote.name() - if _id not in hass.data[DOMAIN]['unique_ids']: - add_entities([WinkRemote(remote, hass)]) - - for button in pywink.get_buttons(): - _id = button.object_id() + button.name() - if _id not in hass.data[DOMAIN]['unique_ids']: - add_entities([WinkButton(button, hass)]) - - for gang in pywink.get_gangs(): - _id = gang.object_id() + gang.name() - if _id not in hass.data[DOMAIN]['unique_ids']: - add_entities([WinkGang(gang, hass)]) - - for door_bell_sensor in pywink.get_door_bells(): - _id = door_bell_sensor.object_id() + door_bell_sensor.name() - if _id not in hass.data[DOMAIN]['unique_ids']: - add_entities([WinkBinarySensorDevice(door_bell_sensor, hass)]) - - for camera_sensor in pywink.get_cameras(): - _id = camera_sensor.object_id() + camera_sensor.name() - if _id not in hass.data[DOMAIN]['unique_ids']: - try: - if camera_sensor.capability() in SENSOR_TYPES: - add_entities([WinkBinarySensorDevice(camera_sensor, hass)]) - except AttributeError: - _LOGGER.info("Device isn't a sensor, skipping") - - -class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice): - """Representation of a Wink binary sensor.""" - - def __init__(self, wink, hass): - """Initialize the Wink binary sensor.""" - super().__init__(wink, hass) - if hasattr(self.wink, 'unit'): - self._unit_of_measurement = self.wink.unit() - else: - self._unit_of_measurement = None - if hasattr(self.wink, 'capability'): - self.capability = self.wink.capability() - else: - self.capability = None - - async def async_added_to_hass(self): - """Call when entity is added to hass.""" - self.hass.data[DOMAIN]['entities']['binary_sensor'].append(self) - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self.wink.state() - - @property - def device_class(self): - """Return the class of this sensor, from DEVICE_CLASSES.""" - return SENSOR_TYPES.get(self.capability) - - @property - def device_state_attributes(self): - """Return the device state attributes.""" - return super().device_state_attributes - - -class WinkSmokeDetector(WinkBinarySensorDevice): - """Representation of a Wink Smoke detector.""" - - @property - def device_state_attributes(self): - """Return the device state attributes.""" - _attributes = super().device_state_attributes - _attributes['test_activated'] = self.wink.test_activated() - return _attributes - - -class WinkHub(WinkBinarySensorDevice): - """Representation of a Wink Hub.""" - - @property - def device_state_attributes(self): - """Return the device state attributes.""" - _attributes = super().device_state_attributes - _attributes['update_needed'] = self.wink.update_needed() - _attributes['firmware_version'] = self.wink.firmware_version() - _attributes['pairing_mode'] = self.wink.pairing_mode() - _kidde_code = self.wink.kidde_radio_code() - if _kidde_code is not None: - # The service call to set the Kidde code - # takes a string of 1s and 0s so it makes - # sense to display it to the user that way - _formatted_kidde_code = "{:b}".format(_kidde_code).zfill(8) - _attributes['kidde_radio_code'] = _formatted_kidde_code - return _attributes - - -class WinkRemote(WinkBinarySensorDevice): - """Representation of a Wink Lutron Connected bulb remote.""" - - @property - def device_state_attributes(self): - """Return the state attributes.""" - _attributes = super().device_state_attributes - _attributes['button_on_pressed'] = self.wink.button_on_pressed() - _attributes['button_off_pressed'] = self.wink.button_off_pressed() - _attributes['button_up_pressed'] = self.wink.button_up_pressed() - _attributes['button_down_pressed'] = self.wink.button_down_pressed() - return _attributes - - @property - def device_class(self): - """Return the class of this sensor, from DEVICE_CLASSES.""" - return None - - -class WinkButton(WinkBinarySensorDevice): - """Representation of a Wink Relay button.""" - - @property - def device_state_attributes(self): - """Return the device state attributes.""" - _attributes = super().device_state_attributes - _attributes['pressed'] = self.wink.pressed() - _attributes['long_pressed'] = self.wink.long_pressed() - return _attributes - - -class WinkGang(WinkBinarySensorDevice): - """Representation of a Wink Relay gang.""" - - @property - def is_on(self): - """Return true if the gang is connected.""" - return self.wink.state() diff --git a/homeassistant/components/binary_sensor/wirelesstag.py b/homeassistant/components/binary_sensor/wirelesstag.py deleted file mode 100644 index 190b408abf38d..0000000000000 --- a/homeassistant/components/binary_sensor/wirelesstag.py +++ /dev/null @@ -1,148 +0,0 @@ -""" -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/ -""" -import logging - -import voluptuous as vol - -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.components.binary_sensor import ( - BinarySensorDevice, PLATFORM_SCHEMA) -from homeassistant.components.wirelesstag import ( - DOMAIN as WIRELESSTAG_DOMAIN, - SIGNAL_BINARY_EVENT_UPDATE, - WirelessTagBaseSensor) -from homeassistant.const import ( - CONF_MONITORED_CONDITIONS, STATE_ON, STATE_OFF) -import homeassistant.helpers.config_validation as cv - -DEPENDENCIES = ['wirelesstag'] - -_LOGGER = logging.getLogger(__name__) - -# On means in range, Off means out of range -SENSOR_PRESENCE = 'presence' - -# On means motion detected, Off means clear -SENSOR_MOTION = 'motion' - -# On means open, Off means closed -SENSOR_DOOR = 'door' - -# On means temperature become too cold, Off means normal -SENSOR_COLD = 'cold' - -# On means hot, Off means normal -SENSOR_HEAT = 'heat' - -# On means too dry (humidity), Off means normal -SENSOR_DRY = 'dry' - -# On means too wet (humidity), Off means normal -SENSOR_WET = 'wet' - -# On means light detected, Off means no light -SENSOR_LIGHT = 'light' - -# On means moisture detected (wet), Off means no moisture (dry) -SENSOR_MOISTURE = 'moisture' - -# On means tag battery is low, Off means normal -SENSOR_BATTERY = 'battery' - -# Sensor types: Name, device_class, push notification type representing 'on', -# attr to check -SENSOR_TYPES = { - SENSOR_PRESENCE: 'Presence', - SENSOR_MOTION: 'Motion', - SENSOR_DOOR: 'Door', - SENSOR_COLD: 'Cold', - SENSOR_HEAT: 'Heat', - SENSOR_DRY: 'Too dry', - SENSOR_WET: 'Too wet', - SENSOR_LIGHT: 'Light', - SENSOR_MOISTURE: 'Leak', - SENSOR_BATTERY: 'Low Battery' -} - - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_MONITORED_CONDITIONS, default=[]): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the platform for a WirelessTags.""" - platform = hass.data.get(WIRELESSTAG_DOMAIN) - - sensors = [] - tags = platform.tags - for tag in tags.values(): - allowed_sensor_types = tag.supported_binary_events_types - for sensor_type in config.get(CONF_MONITORED_CONDITIONS): - if sensor_type in allowed_sensor_types: - sensors.append(WirelessTagBinarySensor(platform, tag, - sensor_type)) - - add_entities(sensors, True) - hass.add_job(platform.install_push_notifications, sensors) - - -class WirelessTagBinarySensor(WirelessTagBaseSensor, BinarySensorDevice): - """A binary sensor implementation for WirelessTags.""" - - def __init__(self, api, tag, sensor_type): - """Initialize a binary sensor for a Wireless Sensor Tags.""" - super().__init__(api, tag) - self._sensor_type = sensor_type - self._name = '{0} {1}'.format(self._tag.name, - self.event.human_readable_name) - - async def async_added_to_hass(self): - """Register callbacks.""" - tag_id = self.tag_id - event_type = self.device_class - mac = self.tag_manager_mac - async_dispatcher_connect( - self.hass, - SIGNAL_BINARY_EVENT_UPDATE.format(tag_id, event_type, mac), - self._on_binary_event_callback) - - @property - def is_on(self): - """Return True if the binary sensor is on.""" - return self._state == STATE_ON - - @property - def device_class(self): - """Return the class of the binary sensor.""" - return self._sensor_type - - @property - def event(self): - """Binary event of tag.""" - return self._tag.event[self._sensor_type] - - @property - def principal_value(self): - """Return value of tag. - - Subclasses need override based on type of sensor. - """ - return STATE_ON if self.event.is_state_on else STATE_OFF - - def updated_state_value(self): - """Use raw princial value.""" - return self.principal_value - - @callback - def _on_binary_event_callback(self, event): - """Update state from arrived push notification.""" - # state should be 'on' or 'off' - self._state = event.data.get('state') - self.async_schedule_update_ha_state() diff --git a/homeassistant/components/binary_sensor/zigbee.py b/homeassistant/components/binary_sensor/zigbee.py deleted file mode 100644 index 67c05f470948e..0000000000000 --- a/homeassistant/components/binary_sensor/zigbee.py +++ /dev/null @@ -1,34 +0,0 @@ -""" -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/ -""" -import voluptuous as vol - -from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.zigbee import ( - ZigBeeDigitalIn, ZigBeeDigitalInConfig, PLATFORM_SCHEMA) - -CONF_ON_STATE = 'on_state' - -DEFAULT_ON_STATE = 'high' -DEPENDENCIES = ['zigbee'] - -STATES = ['high', 'low'] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_ON_STATE): vol.In(STATES), -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Zigbee binary sensor platform.""" - add_entities( - [ZigBeeBinarySensor(hass, ZigBeeDigitalInConfig(config))], True) - - -class ZigBeeBinarySensor(ZigBeeDigitalIn, BinarySensorDevice): - """Use ZigBeeDigitalIn as binary sensor.""" - - pass diff --git a/homeassistant/components/binary_sensor/zwave.py b/homeassistant/components/binary_sensor/zwave.py deleted file mode 100644 index ca07986976d0d..0000000000000 --- a/homeassistant/components/binary_sensor/zwave.py +++ /dev/null @@ -1,111 +0,0 @@ -""" -Interfaces with Z-Wave sensors. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/binary_sensor.zwave/ -""" -import logging -import datetime -import homeassistant.util.dt as dt_util -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.event import track_point_in_time -from homeassistant.components import zwave -from homeassistant.components.zwave import workaround -from homeassistant.components.binary_sensor import ( - DOMAIN, - BinarySensorDevice) - -_LOGGER = logging.getLogger(__name__) -DEPENDENCIES = [] - - -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Old method of setting up Z-Wave binary sensors.""" - pass - - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up Z-Wave binary sensors from Config Entry.""" - @callback - def async_add_binary_sensor(binary_sensor): - """Add Z-Wave binary sensor.""" - async_add_entities([binary_sensor]) - - async_dispatcher_connect(hass, 'zwave_new_binary_sensor', - async_add_binary_sensor) - - -def get_device(values, **kwargs): - """Create Z-Wave entity device.""" - device_mapping = workaround.get_device_mapping(values.primary) - if device_mapping == workaround.WORKAROUND_NO_OFF_EVENT: - return ZWaveTriggerSensor(values, "motion") - - if workaround.get_device_component_mapping(values.primary) == DOMAIN: - return ZWaveBinarySensor(values, None) - - if values.primary.command_class == zwave.const.COMMAND_CLASS_SENSOR_BINARY: - return ZWaveBinarySensor(values, None) - return None - - -class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity): - """Representation of a binary sensor within Z-Wave.""" - - def __init__(self, values, device_class): - """Initialize the sensor.""" - zwave.ZWaveDeviceEntity.__init__(self, values, DOMAIN) - self._sensor_type = device_class - self._state = self.values.primary.data - - def update_properties(self): - """Handle data changes for node values.""" - self._state = self.values.primary.data - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self._state - - @property - def device_class(self): - """Return the class of this sensor, from DEVICE_CLASSES.""" - return self._sensor_type - - -class ZWaveTriggerSensor(ZWaveBinarySensor): - """Representation of a stateless sensor within Z-Wave.""" - - def __init__(self, values, device_class): - """Initialize the sensor.""" - super(ZWaveTriggerSensor, self).__init__(values, device_class) - # Set default off delay to 60 sec - self.re_arm_sec = 60 - self.invalidate_after = None - - def update_properties(self): - """Handle value changes for this entity's node.""" - self._state = self.values.primary.data - _LOGGER.debug('off_delay=%s', self.values.off_delay) - # Set re_arm_sec if off_delay is provided from the sensor - if self.values.off_delay: - _LOGGER.debug('off_delay.data=%s', self.values.off_delay.data) - self.re_arm_sec = self.values.off_delay.data * 8 - # only allow this value to be true for re_arm secs - if not self.hass: - return - - self.invalidate_after = dt_util.utcnow() + datetime.timedelta( - seconds=self.re_arm_sec) - track_point_in_time( - self.hass, self.async_update_ha_state, - self.invalidate_after) - - @property - def is_on(self): - """Return true if movement has happened within the rearm time.""" - return self._state and \ - (self.invalidate_after is None or - self.invalidate_after > dt_util.utcnow()) diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py index 82815d11a6e86..8e95f967396e9 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_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/blink/alarm_control_panel.py b/homeassistant/components/blink/alarm_control_panel.py new file mode 100644 index 0000000000000..b9bf4a5250fc5 --- /dev/null +++ b/homeassistant/components/blink/alarm_control_panel.py @@ -0,0 +1,86 @@ +"""Support for Blink Alarm Control Panel.""" +import logging + +from homeassistant.components.alarm_control_panel import AlarmControlPanel +from homeassistant.components.blink import ( + BLINK_DATA, DEFAULT_ATTRIBUTION) +from homeassistant.const import ( + ATTR_ATTRIBUTION, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['blink'] + +ICON = 'mdi:security' + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Arlo Alarm Control Panels.""" + if discovery_info is None: + return + data = hass.data[BLINK_DATA] + + sync_modules = [] + for sync_name, sync_module in data.sync.items(): + sync_modules.append(BlinkSyncModule(data, sync_name, sync_module)) + add_entities(sync_modules, True) + + +class BlinkSyncModule(AlarmControlPanel): + """Representation of a Blink Alarm Control Panel.""" + + def __init__(self, data, name, sync): + """Initialize the alarm control panel.""" + self.data = data + self.sync = sync + self._name = name + self._state = None + + @property + def unique_id(self): + """Return the unique id for the sync module.""" + return self.sync.serial + + @property + def icon(self): + """Return icon.""" + return ICON + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def name(self): + """Return the name of the panel.""" + return "{} {}".format(BLINK_DATA, self._name) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + attr = self.sync.attributes + attr['network_info'] = self.data.networks + attr['associated_cameras'] = list(self.sync.cameras.keys()) + attr[ATTR_ATTRIBUTION] = DEFAULT_ATTRIBUTION + return attr + + def update(self): + """Update the state of the device.""" + _LOGGER.debug("Updating Blink Alarm Control Panel %s", self._name) + self.data.refresh() + mode = self.sync.arm + if mode: + self._state = STATE_ALARM_ARMED_AWAY + else: + self._state = STATE_ALARM_DISARMED + + def alarm_disarm(self, code=None): + """Send disarm command.""" + self.sync.arm = False + self.sync.refresh() + + def alarm_arm_away(self, code=None): + """Send arm command.""" + self.sync.arm = True + self.sync.refresh() diff --git a/homeassistant/components/blink/binary_sensor.py b/homeassistant/components/blink/binary_sensor.py new file mode 100644 index 0000000000000..fe0b95b1517f5 --- /dev/null +++ b/homeassistant/components/blink/binary_sensor.py @@ -0,0 +1,49 @@ +"""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 + +DEPENDENCIES = ['blink'] + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the blink binary sensors.""" + if discovery_info is None: + return + data = hass.data[BLINK_DATA] + + devs = [] + for camera in data.cameras: + for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]: + devs.append(BlinkBinarySensor(data, camera, sensor_type)) + add_entities(devs, True) + + +class BlinkBinarySensor(BinarySensorDevice): + """Representation of a Blink binary sensor.""" + + def __init__(self, data, camera, sensor_type): + """Initialize the sensor.""" + self.data = data + self._type = sensor_type + name, icon = BINARY_SENSORS[sensor_type] + self._name = "{} {} {}".format(BLINK_DATA, camera, name) + self._icon = icon + self._camera = data.cameras[camera] + self._state = None + self._unique_id = "{}-{}".format(self._camera.serial, self._type) + + @property + def name(self): + """Return the name of the blink sensor.""" + return self._name + + @property + def is_on(self): + """Return the status of the sensor.""" + return self._state + + def update(self): + """Update sensor state.""" + self.data.refresh() + self._state = self._camera.attributes[self._type] diff --git a/homeassistant/components/blink/camera.py b/homeassistant/components/blink/camera.py new file mode 100644 index 0000000000000..2e5c024d6e509 --- /dev/null +++ b/homeassistant/components/blink/camera.py @@ -0,0 +1,77 @@ +"""Support for Blink system camera.""" +import logging + +from homeassistant.components.blink import BLINK_DATA, DEFAULT_BRAND +from homeassistant.components.camera import Camera + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['blink'] + +ATTR_VIDEO_CLIP = 'video' +ATTR_IMAGE = 'image' + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up a Blink Camera.""" + if discovery_info is None: + return + data = hass.data[BLINK_DATA] + devs = [] + for name, camera in data.cameras.items(): + devs.append(BlinkCamera(data, name, camera)) + + add_entities(devs) + + +class BlinkCamera(Camera): + """An implementation of a Blink Camera.""" + + def __init__(self, data, name, camera): + """Initialize a camera.""" + super().__init__() + self.data = data + self._name = "{} {}".format(BLINK_DATA, name) + self._camera = camera + self._unique_id = "{}-camera".format(camera.serial) + self.response = None + self.current_image = None + self.last_image = None + _LOGGER.debug("Initialized blink camera %s", self._name) + + @property + def name(self): + """Return the camera name.""" + return self._name + + @property + def unique_id(self): + """Return the unique camera id.""" + return self._unique_id + + @property + def device_state_attributes(self): + """Return the camera attributes.""" + return self._camera.attributes + + def enable_motion_detection(self): + """Enable motion detection for the camera.""" + self._camera.set_motion_detect(True) + + def disable_motion_detection(self): + """Disable motion detection for the camera.""" + self._camera.set_motion_detect(False) + + @property + def motion_detection_enabled(self): + """Return the state of the camera.""" + return self._camera.motion_enabled + + @property + def brand(self): + """Return the camera brand.""" + return DEFAULT_BRAND + + def camera_image(self): + """Return a still image response from the camera.""" + return self._camera.image_from_cache.content diff --git a/homeassistant/components/blink/sensor.py b/homeassistant/components/blink/sensor.py new file mode 100644 index 0000000000000..c1fdf9252ddf7 --- /dev/null +++ b/homeassistant/components/blink/sensor.py @@ -0,0 +1,80 @@ +"""Support for Blink system camera sensors.""" +import logging + +from homeassistant.components.blink import BLINK_DATA, SENSORS +from homeassistant.helpers.entity import Entity +from homeassistant.const import CONF_MONITORED_CONDITIONS + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['blink'] + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up a Blink sensor.""" + if discovery_info is None: + return + data = hass.data[BLINK_DATA] + devs = [] + for camera in data.cameras: + for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]: + devs.append(BlinkSensor(data, camera, sensor_type)) + + add_entities(devs, True) + + +class BlinkSensor(Entity): + """A Blink camera sensor.""" + + def __init__(self, data, camera, sensor_type): + """Initialize sensors from Blink camera.""" + name, units, icon = SENSORS[sensor_type] + self._name = "{} {} {}".format( + BLINK_DATA, camera, name) + self._camera_name = name + self._type = sensor_type + self.data = data + self._camera = data.cameras[camera] + self._state = None + self._unit_of_measurement = units + self._icon = icon + self._unique_id = "{}-{}".format(self._camera.serial, self._type) + self._sensor_key = self._type + if self._type == 'temperature': + self._sensor_key = 'temperature_calibrated' + + @property + def name(self): + """Return the name of the camera.""" + return self._name + + @property + def unique_id(self): + """Return the unique id for the camera sensor.""" + return self._unique_id + + @property + def icon(self): + """Return the icon of the sensor.""" + return self._icon + + @property + def state(self): + """Return the camera's current state.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit_of_measurement + + def update(self): + """Retrieve sensor data from the camera.""" + self.data.refresh() + try: + self._state = self._camera.attributes[self._sensor_key] + except KeyError: + self._state = None + _LOGGER.error( + "%s not a valid camera attribute. Did the API change?", + self._sensor_key) diff --git a/homeassistant/components/bloomsky.py b/homeassistant/components/bloomsky.py deleted file mode 100644 index 55fb15c686d85..0000000000000 --- a/homeassistant/components/bloomsky.py +++ /dev/null @@ -1,80 +0,0 @@ -""" -Support for BloomSky weather station. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/bloomsky/ -""" -from datetime import timedelta -import logging - -from aiohttp.hdrs import AUTHORIZATION -import requests -import voluptuous as vol - -from homeassistant.const import CONF_API_KEY -from homeassistant.helpers import discovery -import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle - -_LOGGER = logging.getLogger(__name__) - -BLOOMSKY = None -BLOOMSKY_TYPE = ['camera', 'binary_sensor', 'sensor'] - -DOMAIN = 'bloomsky' - -# The BloomSky only updates every 5-8 minutes as per the API spec so there's -# no point in polling the API more frequently -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_API_KEY): cv.string, - }), -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the BloomSky component.""" - api_key = config[DOMAIN][CONF_API_KEY] - - global BLOOMSKY - try: - BLOOMSKY = BloomSky(api_key) - except RuntimeError: - return False - - for component in BLOOMSKY_TYPE: - discovery.load_platform(hass, component, DOMAIN, {}, config) - - return True - - -class BloomSky: - """Handle all communication with the BloomSky API.""" - - # API documentation at http://weatherlution.com/bloomsky-api/ - API_URL = 'http://api.bloomsky.com/api/skydata' - - def __init__(self, api_key): - """Initialize the BookSky.""" - self._api_key = api_key - self.devices = {} - _LOGGER.debug("Initial BloomSky device load...") - self.refresh_devices() - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def refresh_devices(self): - """Use the API to retrieve a list of devices.""" - _LOGGER.debug("Fetching BloomSky update") - response = requests.get( - self.API_URL, headers={AUTHORIZATION: self._api_key}, timeout=10) - if response.status_code == 401: - raise RuntimeError("Invalid API_KEY") - elif response.status_code != 200: - _LOGGER.error("Invalid HTTP response: %s", response.status_code) - return - # Create dictionary keyed off of the device unique id - self.devices.update({ - device['DeviceID']: device for device in response.json() - }) diff --git a/homeassistant/components/bloomsky/__init__.py b/homeassistant/components/bloomsky/__init__.py new file mode 100644 index 0000000000000..a42eb34004be9 --- /dev/null +++ b/homeassistant/components/bloomsky/__init__.py @@ -0,0 +1,75 @@ +"""Support for BloomSky weather station.""" +from datetime import timedelta +import logging + +from aiohttp.hdrs import AUTHORIZATION +import requests +import voluptuous as vol + +from homeassistant.const import CONF_API_KEY +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle + +_LOGGER = logging.getLogger(__name__) + +BLOOMSKY = None +BLOOMSKY_TYPE = ['camera', 'binary_sensor', 'sensor'] + +DOMAIN = 'bloomsky' + +# The BloomSky only updates every 5-8 minutes as per the API spec so there's +# no point in polling the API more frequently +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_API_KEY): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the BloomSky component.""" + api_key = config[DOMAIN][CONF_API_KEY] + + global BLOOMSKY + try: + BLOOMSKY = BloomSky(api_key) + except RuntimeError: + return False + + for component in BLOOMSKY_TYPE: + discovery.load_platform(hass, component, DOMAIN, {}, config) + + return True + + +class BloomSky: + """Handle all communication with the BloomSky API.""" + + # API documentation at http://weatherlution.com/bloomsky-api/ + API_URL = 'http://api.bloomsky.com/api/skydata' + + def __init__(self, api_key): + """Initialize the BookSky.""" + self._api_key = api_key + self.devices = {} + _LOGGER.debug("Initial BloomSky device load...") + self.refresh_devices() + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def refresh_devices(self): + """Use the API to retrieve a list of devices.""" + _LOGGER.debug("Fetching BloomSky update") + response = requests.get( + self.API_URL, headers={AUTHORIZATION: self._api_key}, timeout=10) + if response.status_code == 401: + raise RuntimeError("Invalid API_KEY") + elif response.status_code != 200: + _LOGGER.error("Invalid HTTP response: %s", response.status_code) + return + # Create dictionary keyed off of the device unique id + self.devices.update({ + device['DeviceID']: device for device in response.json() + }) diff --git a/homeassistant/components/bloomsky/binary_sensor.py b/homeassistant/components/bloomsky/binary_sensor.py new file mode 100644 index 0000000000000..c8763524de763 --- /dev/null +++ b/homeassistant/components/bloomsky/binary_sensor.py @@ -0,0 +1,75 @@ +"""Support the binary sensors of a BloomSky weather station.""" +import logging + +import voluptuous as vol + +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA) +from homeassistant.const import CONF_MONITORED_CONDITIONS +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['bloomsky'] + +SENSOR_TYPES = { + 'Rain': 'moisture', + 'Night': None, +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the available BloomSky weather binary sensors.""" + bloomsky = hass.components.bloomsky + # Default needed in case of discovery + sensors = config.get(CONF_MONITORED_CONDITIONS, SENSOR_TYPES) + + for device in bloomsky.BLOOMSKY.devices.values(): + for variable in sensors: + add_entities( + [BloomSkySensor(bloomsky.BLOOMSKY, device, variable)], True) + + +class BloomSkySensor(BinarySensorDevice): + """Representation of a single binary sensor in a BloomSky device.""" + + def __init__(self, bs, device, sensor_name): + """Initialize a BloomSky binary sensor.""" + self._bloomsky = bs + self._device_id = device['DeviceID'] + self._sensor_name = sensor_name + self._name = '{} {}'.format(device['DeviceName'], sensor_name) + self._state = None + self._unique_id = '{}-{}'.format(self._device_id, self._sensor_name) + + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id + + @property + def name(self): + """Return the name of the BloomSky device and this sensor.""" + return self._name + + @property + def device_class(self): + """Return the class of this sensor, from DEVICE_CLASSES.""" + return SENSOR_TYPES.get(self._sensor_name) + + @property + def is_on(self): + """Return true if binary sensor is on.""" + return self._state + + def update(self): + """Request an update from the BloomSky API.""" + self._bloomsky.refresh_devices() + + self._state = \ + self._bloomsky.devices[self._device_id]['Data'][self._sensor_name] diff --git a/homeassistant/components/bloomsky/camera.py b/homeassistant/components/bloomsky/camera.py new file mode 100644 index 0000000000000..5cb2e1adfe16b --- /dev/null +++ b/homeassistant/components/bloomsky/camera.py @@ -0,0 +1,59 @@ +"""Support for a camera of a BloomSky weather station.""" +import logging + +import requests + +from homeassistant.components.camera import Camera + +DEPENDENCIES = ['bloomsky'] + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up access to BloomSky cameras.""" + bloomsky = hass.components.bloomsky + for device in bloomsky.BLOOMSKY.devices.values(): + add_entities([BloomSkyCamera(bloomsky.BLOOMSKY, device)]) + + +class BloomSkyCamera(Camera): + """Representation of the images published from the BloomSky's camera.""" + + def __init__(self, bs, device): + """Initialize access to the BloomSky camera images.""" + super(BloomSkyCamera, self).__init__() + self._name = device['DeviceName'] + self._id = device['DeviceID'] + self._bloomsky = bs + self._url = "" + self._last_url = "" + # last_image will store images as they are downloaded so that the + # frequent updates in home-assistant don't keep poking the server + # to download the same image over and over. + self._last_image = "" + self._logger = logging.getLogger(__name__) + + def camera_image(self): + """Update the camera's image if it has changed.""" + try: + self._url = self._bloomsky.devices[self._id]['Data']['ImageURL'] + self._bloomsky.refresh_devices() + # If the URL hasn't changed then the image hasn't changed. + if self._url != self._last_url: + response = requests.get(self._url, timeout=10) + self._last_url = self._url + self._last_image = response.content + except requests.exceptions.RequestException as error: + self._logger.error("Error getting bloomsky image: %s", error) + return None + + return self._last_image + + @property + def unique_id(self): + """Return a unique ID.""" + return self._id + + @property + def name(self): + """Return the name of this BloomSky device.""" + return self._name diff --git a/homeassistant/components/bloomsky/sensor.py b/homeassistant/components/bloomsky/sensor.py new file mode 100644 index 0000000000000..7e6847f0e7ec2 --- /dev/null +++ b/homeassistant/components/bloomsky/sensor.py @@ -0,0 +1,93 @@ +"""Support the sensor of a BloomSky weather station.""" +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import (TEMP_FAHRENHEIT, CONF_MONITORED_CONDITIONS) +from homeassistant.helpers.entity import Entity +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['bloomsky'] + +# These are the available sensors +SENSOR_TYPES = ['Temperature', + 'Humidity', + 'Pressure', + 'Luminance', + 'UVIndex', + 'Voltage'] + +# Sensor units - these do not currently align with the API documentation +SENSOR_UNITS = {'Temperature': TEMP_FAHRENHEIT, + 'Humidity': '%', + 'Pressure': 'inHg', + 'Luminance': 'cd/m²', + 'Voltage': 'mV'} + +# Which sensors to format numerically +FORMAT_NUMBERS = ['Temperature', 'Pressure', 'Voltage'] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_TYPES): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the available BloomSky weather sensors.""" + bloomsky = hass.components.bloomsky + # Default needed in case of discovery + sensors = config.get(CONF_MONITORED_CONDITIONS, SENSOR_TYPES) + + for device in bloomsky.BLOOMSKY.devices.values(): + for variable in sensors: + add_entities( + [BloomSkySensor(bloomsky.BLOOMSKY, device, variable)], True) + + +class BloomSkySensor(Entity): + """Representation of a single sensor in a BloomSky device.""" + + def __init__(self, bs, device, sensor_name): + """Initialize a BloomSky sensor.""" + self._bloomsky = bs + self._device_id = device['DeviceID'] + self._sensor_name = sensor_name + self._name = '{} {}'.format(device['DeviceName'], sensor_name) + self._state = None + self._unique_id = '{}-{}'.format(self._device_id, self._sensor_name) + + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id + + @property + def name(self): + """Return the name of the BloomSky device and this sensor.""" + return self._name + + @property + def state(self): + """Return the current state, eg. value, of this sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the sensor units.""" + return SENSOR_UNITS.get(self._sensor_name, None) + + def update(self): + """Request an update from the BloomSky API.""" + self._bloomsky.refresh_devices() + + state = \ + self._bloomsky.devices[self._device_id]['Data'][self._sensor_name] + + if self._sensor_name in FORMAT_NUMBERS: + self._state = '{0:.2f}'.format(state) + else: + self._state = state diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 40f2b91045afd..e1ac30120d211 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 execute_service(call): 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 execute_service(call): # 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 @@ def update(self, *_): 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/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py new file mode 100644 index 0000000000000..1843f647df8d8 --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -0,0 +1,198 @@ +"""Reads vehicle status from BMW connected drive portal.""" +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN +from homeassistant.const import LENGTH_KILOMETERS + +DEPENDENCIES = ['bmw_connected_drive'] + +_LOGGER = logging.getLogger(__name__) + +SENSOR_TYPES = { + 'lids': ['Doors', 'opening'], + 'windows': ['Windows', 'opening'], + 'door_lock_state': ['Door lock state', 'safety'], + 'lights_parking': ['Parking lights', 'light'], + 'condition_based_services': ['Condition based services', 'problem'], + 'check_control_messages': ['Control messages', 'problem'] +} + +SENSOR_TYPES_ELEC = { + 'charging_status': ['Charging status', 'power'], + 'connection_status': ['Connection status', 'plug'] +} + +SENSOR_TYPES_ELEC.update(SENSOR_TYPES) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the BMW sensors.""" + accounts = hass.data[BMW_DOMAIN] + _LOGGER.debug('Found BMW accounts: %s', + ', '.join([a.name for a in accounts])) + devices = [] + for account in accounts: + for vehicle in account.account.vehicles: + 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]) + 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]) + devices.append(device) + add_entities(devices, True) + + +class BMWConnectedDriveSensor(BinarySensorDevice): + """Representation of a BMW vehicle binary sensor.""" + + def __init__(self, account, vehicle, attribute: str, sensor_name, + device_class): + """Constructor.""" + self._account = account + self._vehicle = vehicle + self._attribute = attribute + self._name = '{} {}'.format(self._vehicle.name, self._attribute) + self._unique_id = '{}-{}'.format(self._vehicle.vin, self._attribute) + self._sensor_name = sensor_name + self._device_class = device_class + self._state = None + + @property + def should_poll(self) -> bool: + """Return False. + + Data update is triggered from BMWConnectedDriveEntity. + """ + return False + + @property + def unique_id(self): + """Return the unique ID of the binary sensor.""" + return self._unique_id + + @property + def name(self): + """Return the name of the binary sensor.""" + return self._name + + @property + def device_class(self): + """Return the class of the binary sensor.""" + return self._device_class + + @property + def is_on(self): + """Return the state of the binary sensor.""" + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes of the binary sensor.""" + vehicle_state = self._vehicle.state + result = { + 'car': self._vehicle.name + } + + if self._attribute == 'lids': + for lid in vehicle_state.lids: + result[lid.name] = lid.state.value + elif self._attribute == 'windows': + for window in vehicle_state.windows: + result[window.name] = window.state.value + elif self._attribute == 'door_lock_state': + result['door_lock_state'] = vehicle_state.door_lock_state.value + result['last_update_reason'] = vehicle_state.last_update_reason + elif self._attribute == 'lights_parking': + result['lights_parking'] = vehicle_state.parking_lights.value + elif self._attribute == 'condition_based_services': + for report in vehicle_state.condition_based_services: + result.update( + self._format_cbs_report(report)) + elif self._attribute == 'check_control_messages': + check_control_messages = vehicle_state.check_control_messages + if not check_control_messages: + result['check_control_messages'] = 'OK' + else: + cbs_list = [] + for message in check_control_messages: + cbs_list.append(message['ccmDescriptionShort']) + result['check_control_messages'] = cbs_list + elif self._attribute == 'charging_status': + result['charging_status'] = vehicle_state.charging_status.value + # pylint: disable=protected-access + result['last_charging_end_result'] = \ + vehicle_state._attributes['lastChargingEndResult'] + if self._attribute == 'connection_status': + # pylint: disable=protected-access + result['connection_status'] = \ + vehicle_state._attributes['connectionStatus'] + + return sorted(result.items()) + + def update(self): + """Read new state data from the library.""" + from bimmer_connected.state import LockState + from bimmer_connected.state import ChargingState + vehicle_state = self._vehicle.state + + # device class opening: On means open, Off means closed + if self._attribute == 'lids': + _LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed) + self._state = not vehicle_state.all_lids_closed + if self._attribute == 'windows': + self._state = not vehicle_state.all_windows_closed + # device class safety: On means unsafe, Off means safe + if self._attribute == 'door_lock_state': + # Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED + self._state = vehicle_state.door_lock_state not in \ + [LockState.LOCKED, LockState.SECURED] + # device class light: On means light detected, Off means no light + if self._attribute == 'lights_parking': + self._state = vehicle_state.are_parking_lights_on + # device class problem: On means problem detected, Off means no problem + if self._attribute == 'condition_based_services': + self._state = not vehicle_state.are_all_cbs_ok + if self._attribute == 'check_control_messages': + self._state = vehicle_state.has_check_control_messages + # device class power: On means power detected, Off means no power + if self._attribute == 'charging_status': + self._state = vehicle_state.charging_status in \ + [ChargingState.CHARGING] + # device class plug: On means device is plugged in, + # Off means device is unplugged + if self._attribute == 'connection_status': + # pylint: disable=protected-access + self._state = (vehicle_state._attributes['connectionStatus'] == + 'CONNECTED') + + def _format_cbs_report(self, report): + result = {} + service_type = report.service_type.lower().replace('_', ' ') + result['{} status'.format(service_type)] = report.state.value + if report.due_date is not None: + result['{} date'.format(service_type)] = \ + report.due_date.strftime('%Y-%m-%d') + if report.due_distance is not None: + distance = round(self.hass.config.units.length( + report.due_distance, LENGTH_KILOMETERS)) + result['{} distance'.format(service_type)] = '{} {}'.format( + distance, self.hass.config.units.length_unit) + return result + + def update_callback(self): + """Schedule a state update.""" + self.schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Add callback after being added to hass. + + Show latest data after startup. + """ + self._account.add_update_listener(self.update_callback) diff --git a/homeassistant/components/bmw_connected_drive/device_tracker.py b/homeassistant/components/bmw_connected_drive/device_tracker.py new file mode 100644 index 0000000000000..21121b069af2c --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/device_tracker.py @@ -0,0 +1,54 @@ +"""Device tracker for BMW Connected Drive vehicles.""" +import logging + +from homeassistant.components.bmw_connected_drive import DOMAIN \ + as BMW_DOMAIN +from homeassistant.util import slugify + +DEPENDENCIES = ['bmw_connected_drive'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_scanner(hass, config, see, discovery_info=None): + """Set up the BMW tracker.""" + accounts = hass.data[BMW_DOMAIN] + _LOGGER.debug('Found BMW accounts: %s', + ', '.join([a.name for a in accounts])) + for account in accounts: + for vehicle in account.account.vehicles: + tracker = BMWDeviceTracker(see, vehicle) + account.add_update_listener(tracker.update) + tracker.update() + return True + + +class BMWDeviceTracker: + """BMW Connected Drive device tracker.""" + + def __init__(self, see, vehicle): + """Initialize the Tracker.""" + self._see = see + self.vehicle = vehicle + + def update(self) -> None: + """Update the device info. + + Only update the state in home assistant if tracking in + the car is enabled. + """ + dev_id = slugify(self.vehicle.name) + + if not self.vehicle.state.is_vehicle_tracking_enabled: + _LOGGER.debug('Tracking is disabled for vehicle %s', dev_id) + return + + _LOGGER.debug('Updating %s', dev_id) + attrs = { + 'vin': self.vehicle.vin, + } + self._see( + dev_id=dev_id, host_name=self.vehicle.name, + gps=self.vehicle.state.gps_position, attributes=attrs, + icon='mdi:car' + ) diff --git a/homeassistant/components/bmw_connected_drive/lock.py b/homeassistant/components/bmw_connected_drive/lock.py new file mode 100644 index 0000000000000..8a5eddaa86a11 --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/lock.py @@ -0,0 +1,113 @@ +"""Support for BMW car locks with BMW ConnectedDrive.""" +import logging + +from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN +from homeassistant.components.lock import LockDevice +from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED + +DEPENDENCIES = ['bmw_connected_drive'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the BMW Connected Drive lock.""" + accounts = hass.data[BMW_DOMAIN] + _LOGGER.debug('Found BMW accounts: %s', + ', '.join([a.name for a in accounts])) + devices = [] + for account in accounts: + if not account.read_only: + for vehicle in account.account.vehicles: + device = BMWLock(account, vehicle, 'lock', 'BMW lock') + devices.append(device) + add_entities(devices, True) + + +class BMWLock(LockDevice): + """Representation of a BMW vehicle lock.""" + + def __init__(self, account, vehicle, attribute: str, sensor_name): + """Initialize the lock.""" + self._account = account + self._vehicle = vehicle + self._attribute = attribute + self._name = '{} {}'.format(self._vehicle.name, self._attribute) + self._unique_id = '{}-{}'.format(self._vehicle.vin, self._attribute) + self._sensor_name = sensor_name + self._state = None + + @property + def should_poll(self): + """Do not poll this class. + + Updates are triggered from BMWConnectedDriveAccount. + """ + return False + + @property + def unique_id(self): + """Return the unique ID of the lock.""" + return self._unique_id + + @property + def name(self): + """Return the name of the lock.""" + return self._name + + @property + def device_state_attributes(self): + """Return the state attributes of the lock.""" + vehicle_state = self._vehicle.state + return { + 'car': self._vehicle.name, + 'door_lock_state': vehicle_state.door_lock_state.value + } + + @property + def is_locked(self): + """Return true if lock is locked.""" + return self._state == STATE_LOCKED + + def lock(self, **kwargs): + """Lock the car.""" + _LOGGER.debug("%s: locking doors", self._vehicle.name) + # Optimistic state set here because it takes some time before the + # update callback response + self._state = STATE_LOCKED + self.schedule_update_ha_state() + self._vehicle.remote_services.trigger_remote_door_lock() + + def unlock(self, **kwargs): + """Unlock the car.""" + _LOGGER.debug("%s: unlocking doors", self._vehicle.name) + # Optimistic state set here because it takes some time before the + # update callback response + self._state = STATE_UNLOCKED + self.schedule_update_ha_state() + self._vehicle.remote_services.trigger_remote_door_unlock() + + def update(self): + """Update state of the lock.""" + from bimmer_connected.state import LockState + + _LOGGER.debug("%s: updating data for %s", self._vehicle.name, + self._attribute) + vehicle_state = self._vehicle.state + + # Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED + self._state = STATE_LOCKED \ + if vehicle_state.door_lock_state \ + in [LockState.LOCKED, LockState.SECURED] \ + else STATE_UNLOCKED + + def update_callback(self): + """Schedule a state update.""" + self.schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Add callback after being added to hass. + + Show latest data after startup. + """ + self._account.add_update_listener(self.update_callback) diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py new file mode 100644 index 0000000000000..a01142c53ed22 --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -0,0 +1,157 @@ +"""Support for reading vehicle status from BMW connected drive portal.""" +import logging + +from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.icon import icon_for_battery_level +from homeassistant.const import (CONF_UNIT_SYSTEM_IMPERIAL, VOLUME_LITERS, + VOLUME_GALLONS, LENGTH_KILOMETERS, + LENGTH_MILES) + +DEPENDENCIES = ['bmw_connected_drive'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_TO_HA_METRIC = { + 'mileage': ['mdi:speedometer', LENGTH_KILOMETERS], + 'remaining_range_total': ['mdi:ruler', LENGTH_KILOMETERS], + 'remaining_range_electric': ['mdi:ruler', LENGTH_KILOMETERS], + 'remaining_range_fuel': ['mdi:ruler', LENGTH_KILOMETERS], + 'max_range_electric': ['mdi:ruler', LENGTH_KILOMETERS], + 'remaining_fuel': ['mdi:gas-station', VOLUME_LITERS], + 'charging_time_remaining': ['mdi:update', 'h'], + 'charging_status': ['mdi:battery-charging', None], +} + +ATTR_TO_HA_IMPERIAL = { + 'mileage': ['mdi:speedometer', LENGTH_MILES], + 'remaining_range_total': ['mdi:ruler', LENGTH_MILES], + 'remaining_range_electric': ['mdi:ruler', LENGTH_MILES], + 'remaining_range_fuel': ['mdi:ruler', LENGTH_MILES], + 'max_range_electric': ['mdi:ruler', LENGTH_MILES], + 'remaining_fuel': ['mdi:gas-station', VOLUME_GALLONS], + 'charging_time_remaining': ['mdi:update', 'h'], + 'charging_status': ['mdi:battery-charging', None], +} + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the BMW sensors.""" + if hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: + attribute_info = ATTR_TO_HA_IMPERIAL + else: + attribute_info = ATTR_TO_HA_METRIC + + accounts = hass.data[BMW_DOMAIN] + _LOGGER.debug('Found BMW accounts: %s', + ', '.join([a.name for a in accounts])) + devices = [] + 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) + devices.append(device) + device = BMWConnectedDriveSensor( + account, vehicle, 'mileage', attribute_info) + devices.append(device) + add_entities(devices, True) + + +class BMWConnectedDriveSensor(Entity): + """Representation of a BMW vehicle sensor.""" + + def __init__(self, account, vehicle, attribute: str, attribute_info): + """Constructor.""" + self._vehicle = vehicle + self._account = account + self._attribute = attribute + self._state = None + self._name = '{} {}'.format(self._vehicle.name, self._attribute) + self._unique_id = '{}-{}'.format(self._vehicle.vin, self._attribute) + self._attribute_info = attribute_info + + @property + def should_poll(self) -> bool: + """Return False. + + Data update is triggered from BMWConnectedDriveEntity. + """ + return False + + @property + def unique_id(self): + """Return the unique ID of the sensor.""" + return self._unique_id + + @property + def name(self) -> str: + """Return the name of the sensor.""" + return self._name + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + from bimmer_connected.state import ChargingState + vehicle_state = self._vehicle.state + charging_state = vehicle_state.charging_status in [ + ChargingState.CHARGING] + + if self._attribute == 'charging_level_hv': + return icon_for_battery_level( + battery_level=vehicle_state.charging_level_hv, + charging=charging_state) + icon, _ = self._attribute_info.get(self._attribute, [None, None]) + return icon + + @property + def state(self): + """Return the state of the sensor. + + The return type of this call depends on the attribute that + is configured. + """ + return self._state + + @property + def unit_of_measurement(self) -> str: + """Get the unit of measurement.""" + _, unit = self._attribute_info.get(self._attribute, [None, None]) + return unit + + @property + def device_state_attributes(self): + """Return the state attributes of the sensor.""" + return { + 'car': self._vehicle.name + } + + def update(self) -> None: + """Read new state data from the library.""" + _LOGGER.debug('Updating %s', self._vehicle.name) + vehicle_state = self._vehicle.state + if self._attribute == 'charging_status': + self._state = getattr(vehicle_state, self._attribute).value + elif self.unit_of_measurement == VOLUME_GALLONS: + value = getattr(vehicle_state, self._attribute) + value_converted = self.hass.config.units.volume( + value, VOLUME_LITERS) + self._state = round(value_converted) + elif self.unit_of_measurement == LENGTH_MILES: + value = getattr(vehicle_state, self._attribute) + value_converted = self.hass.config.units.length( + value, LENGTH_KILOMETERS) + self._state = round(value_converted) + else: + self._state = getattr(vehicle_state, self._attribute) + + def update_callback(self): + """Schedule a state update.""" + self.schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Add callback after being added to hass. + + Show latest data after startup. + """ + self._account.add_update_listener(self.update_callback) diff --git a/homeassistant/components/browser.py b/homeassistant/components/browser.py deleted file mode 100644 index 041a0f9cdc647..0000000000000 --- a/homeassistant/components/browser.py +++ /dev/null @@ -1,30 +0,0 @@ -""" -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/ -""" -import voluptuous as vol - -DOMAIN = "browser" -SERVICE_BROWSE_URL = "browse_url" - -ATTR_URL = 'url' -ATTR_URL_DEFAULT = 'https://www.google.com' - -SERVICE_BROWSE_URL_SCHEMA = vol.Schema({ - # pylint: disable=no-value-for-parameter - vol.Required(ATTR_URL, default=ATTR_URL_DEFAULT): vol.Url(), -}) - - -def setup(hass, config): - """Listen for browse_url events.""" - import webbrowser - - hass.services.register(DOMAIN, SERVICE_BROWSE_URL, - lambda service: - webbrowser.open(service.data[ATTR_URL]), - schema=SERVICE_BROWSE_URL_SCHEMA) - - return True diff --git a/homeassistant/components/browser/__init__.py b/homeassistant/components/browser/__init__.py new file mode 100644 index 0000000000000..1c002f21f5fb4 --- /dev/null +++ b/homeassistant/components/browser/__init__.py @@ -0,0 +1,26 @@ +"""Support for launching a web browser on the host machine.""" +import voluptuous as vol + +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(), +}) + + +def setup(hass, config): + """Listen for browse_url events.""" + import webbrowser + + hass.services.register(DOMAIN, SERVICE_BROWSE_URL, + lambda service: + webbrowser.open(service.data[ATTR_URL]), + schema=SERVICE_BROWSE_URL_SCHEMA) + + return True diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 9d105fb02d0ff..aa9e3153fe5bb 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 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 653d0315ad4e3..474f9594610c9 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -25,7 +25,8 @@ 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/camera/abode.py b/homeassistant/components/camera/abode.py deleted file mode 100644 index 39681760d4d27..0000000000000 --- a/homeassistant/components/camera/abode.py +++ /dev/null @@ -1,99 +0,0 @@ -""" -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/ -""" -import logging - -from datetime import timedelta -import requests - -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) - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Abode camera devices.""" - import abodepy.helpers.constants as CONST - import abodepy.helpers.timeline as TIMELINE - - data = hass.data[ABODE_DOMAIN] - - devices = [] - for device in data.abode.get_devices(generic_type=CONST.TYPE_CAMERA): - if data.is_excluded(device): - continue - - devices.append(AbodeCamera(data, device, TIMELINE.CAPTURE_IMAGE)) - - data.devices.extend(devices) - - add_entities(devices) - - -class AbodeCamera(AbodeDevice, Camera): - """Representation of an Abode camera.""" - - def __init__(self, data, device, event): - """Initialize the Abode device.""" - AbodeDevice.__init__(self, data, device) - Camera.__init__(self) - self._event = event - self._response = None - - async def async_added_to_hass(self): - """Subscribe Abode events.""" - await super().async_added_to_hass() - - self.hass.async_add_job( - self._data.abode.events.add_timeline_callback, - self._event, self._capture_callback - ) - - def capture(self): - """Request a new image capture.""" - return self._device.capture() - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def refresh_image(self): - """Find a new image on the timeline.""" - if self._device.refresh_image(): - self.get_image() - - def get_image(self): - """Attempt to download the most recent capture.""" - if self._device.image_url: - try: - self._response = requests.get( - self._device.image_url, stream=True) - - self._response.raise_for_status() - except requests.HTTPError as err: - _LOGGER.warning("Failed to get camera image: %s", err) - self._response = None - else: - self._response = None - - def camera_image(self): - """Get a camera image.""" - self.refresh_image() - - if self._response: - return self._response.content - - return None - - def _capture_callback(self, capture): - """Update the image with the device then refresh device.""" - self._device.update_image_location(capture) - self.get_image() - self.schedule_update_ha_state() diff --git a/homeassistant/components/camera/amcrest.py b/homeassistant/components/camera/amcrest.py deleted file mode 100644 index 3b3368c2f5c4b..0000000000000 --- a/homeassistant/components/camera/amcrest.py +++ /dev/null @@ -1,92 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.components.amcrest import ( - DATA_AMCREST, STREAM_SOURCE_LIST, TIMEOUT) -from homeassistant.components.camera import Camera -from homeassistant.components.ffmpeg import DATA_FFMPEG -from homeassistant.const import CONF_NAME -from homeassistant.helpers.aiohttp_client import ( - async_get_clientsession, async_aiohttp_proxy_web, - async_aiohttp_proxy_stream) - -DEPENDENCIES = ['amcrest', 'ffmpeg'] - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up an Amcrest IP Camera.""" - if discovery_info is None: - return - - device_name = discovery_info[CONF_NAME] - amcrest = hass.data[DATA_AMCREST][device_name] - - async_add_entities([AmcrestCam(hass, amcrest)], True) - - return True - - -class AmcrestCam(Camera): - """An implementation of an Amcrest IP camera.""" - - def __init__(self, hass, amcrest): - """Initialize an Amcrest camera.""" - super(AmcrestCam, self).__init__() - self._name = amcrest.name - self._camera = amcrest.device - self._base_url = self._camera.get_base_url() - self._ffmpeg = hass.data[DATA_FFMPEG] - self._ffmpeg_arguments = amcrest.ffmpeg_arguments - self._stream_source = amcrest.stream_source - self._resolution = amcrest.resolution - self._token = self._auth = amcrest.authentication - - def camera_image(self): - """Return a still image response from the camera.""" - # Send the request to snap a picture and return raw jpg data - response = self._camera.snapshot(channel=self._resolution) - return response.data - - async def handle_async_mjpeg_stream(self, request): - """Return an MJPEG stream.""" - # The snapshot implementation is handled by the parent class - if self._stream_source == STREAM_SOURCE_LIST['snapshot']: - return await super().handle_async_mjpeg_stream(request) - - if self._stream_source == STREAM_SOURCE_LIST['mjpeg']: - # stream an MJPEG image stream directly from the camera - websession = async_get_clientsession(self.hass) - streaming_url = self._camera.mjpeg_url(typeno=self._resolution) - stream_coro = websession.get( - streaming_url, auth=self._token, timeout=TIMEOUT) - - return await async_aiohttp_proxy_web( - self.hass, request, stream_coro) - - # streaming via ffmpeg - from haffmpeg import CameraMjpeg - - streaming_url = self._camera.rtsp_url(typeno=self._resolution) - stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop) - await stream.open_camera( - streaming_url, extra_cmd=self._ffmpeg_arguments) - - try: - return await async_aiohttp_proxy_stream( - self.hass, request, stream, - self._ffmpeg.ffmpeg_stream_content_type) - finally: - await stream.close() - - @property - def name(self): - """Return the name of this camera.""" - return self._name diff --git a/homeassistant/components/camera/arlo.py b/homeassistant/components/camera/arlo.py deleted file mode 100644 index 7857995b4af3a..0000000000000 --- a/homeassistant/components/camera/arlo.py +++ /dev/null @@ -1,169 +0,0 @@ -""" -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/ -""" -import logging - -import voluptuous as vol - -from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv -from homeassistant.components.arlo import ( - DEFAULT_BRAND, DATA_ARLO, SIGNAL_UPDATE_ARLO) -from homeassistant.components.camera import Camera, PLATFORM_SCHEMA -from homeassistant.components.ffmpeg import DATA_FFMPEG -from homeassistant.const import ATTR_BATTERY_LEVEL -from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream -from homeassistant.helpers.dispatcher import async_dispatcher_connect - -_LOGGER = logging.getLogger(__name__) - -ARLO_MODE_ARMED = 'armed' -ARLO_MODE_DISARMED = 'disarmed' - -ATTR_BRIGHTNESS = 'brightness' -ATTR_FLIPPED = 'flipped' -ATTR_MIRRORED = 'mirrored' -ATTR_MOTION = 'motion_detection_sensitivity' -ATTR_POWERSAVE = 'power_save_mode' -ATTR_SIGNAL_STRENGTH = 'signal_strength' -ATTR_UNSEEN_VIDEOS = 'unseen_videos' -ATTR_LAST_REFRESH = 'last_refresh' - -CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments' - -DEPENDENCIES = ['arlo', 'ffmpeg'] - -POWERSAVE_MODE_MAPPING = { - 1: 'best_battery_life', - 2: 'optimized', - 3: 'best_video' -} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string, -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up an Arlo IP Camera.""" - arlo = hass.data[DATA_ARLO] - - cameras = [] - for camera in arlo.cameras: - cameras.append(ArloCam(hass, camera, config)) - - add_entities(cameras) - - -class ArloCam(Camera): - """An implementation of a Netgear Arlo IP camera.""" - - def __init__(self, hass, camera, device_info): - """Initialize an Arlo camera.""" - super().__init__() - self._camera = camera - self._name = self._camera.name - self._motion_status = False - self._ffmpeg = hass.data[DATA_FFMPEG] - self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS) - self._last_refresh = None - self.attrs = {} - - def camera_image(self): - """Return a still image response from the camera.""" - return self._camera.last_image_from_cache - - async def async_added_to_hass(self): - """Register callbacks.""" - async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_ARLO, self._update_callback) - - @callback - def _update_callback(self): - """Call update method.""" - self.async_schedule_update_ha_state() - - async def handle_async_mjpeg_stream(self, request): - """Generate an HTTP MJPEG stream from the camera.""" - from haffmpeg import CameraMjpeg - video = self._camera.last_video - if not video: - error_msg = \ - 'Video not found for {0}. Is it older than {1} days?'.format( - self.name, self._camera.min_days_vdo_cache) - _LOGGER.error(error_msg) - return - - stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop) - await stream.open_camera( - video.video_url, extra_cmd=self._ffmpeg_arguments) - - try: - return await async_aiohttp_proxy_stream( - self.hass, request, stream, - self._ffmpeg.ffmpeg_stream_content_type) - finally: - await stream.close() - - @property - def name(self): - """Return the name of this camera.""" - return self._name - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return { - name: value for name, value in ( - (ATTR_BATTERY_LEVEL, self._camera.battery_level), - (ATTR_BRIGHTNESS, self._camera.brightness), - (ATTR_FLIPPED, self._camera.flip_state), - (ATTR_MIRRORED, self._camera.mirror_state), - (ATTR_MOTION, self._camera.motion_detection_sensitivity), - (ATTR_POWERSAVE, POWERSAVE_MODE_MAPPING.get( - self._camera.powersave_mode)), - (ATTR_SIGNAL_STRENGTH, self._camera.signal_strength), - (ATTR_UNSEEN_VIDEOS, self._camera.unseen_videos), - ) if value is not None - } - - @property - def model(self): - """Return the camera model.""" - return self._camera.model_id - - @property - def brand(self): - """Return the camera brand.""" - return DEFAULT_BRAND - - @property - def motion_detection_enabled(self): - """Return the camera motion detection status.""" - return self._motion_status - - def set_base_station_mode(self, mode): - """Set the mode in the base station.""" - # Get the list of base stations identified by library - base_stations = self.hass.data[DATA_ARLO].base_stations - - # Some Arlo cameras does not have base station - # So check if there is base station detected first - # if yes, then choose the primary base station - # Set the mode on the chosen base station - if base_stations: - primary_base_station = base_stations[0] - primary_base_station.mode = mode - - def enable_motion_detection(self): - """Enable the Motion detection in base station (Arm).""" - self._motion_status = True - self.set_base_station_mode(ARLO_MODE_ARMED) - - def disable_motion_detection(self): - """Disable the motion detection in base station (Disarm).""" - self._motion_status = False - self.set_base_station_mode(ARLO_MODE_DISARMED) diff --git a/homeassistant/components/camera/august.py b/homeassistant/components/camera/august.py deleted file mode 100644 index dcce5e13588c1..0000000000000 --- a/homeassistant/components/camera/august.py +++ /dev/null @@ -1,76 +0,0 @@ -""" -Support for August camera. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/camera.august/ -""" -from datetime import timedelta - -import requests - -from homeassistant.components.august import DATA_AUGUST, DEFAULT_TIMEOUT -from homeassistant.components.camera import Camera - -DEPENDENCIES = ['august'] - -SCAN_INTERVAL = timedelta(seconds=5) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up August cameras.""" - data = hass.data[DATA_AUGUST] - devices = [] - - for doorbell in data.doorbells: - devices.append(AugustCamera(data, doorbell, DEFAULT_TIMEOUT)) - - add_entities(devices, True) - - -class AugustCamera(Camera): - """An implementation of a Canary security camera.""" - - def __init__(self, data, doorbell, timeout): - """Initialize a Canary security camera.""" - super().__init__() - self._data = data - self._doorbell = doorbell - self._timeout = timeout - self._image_url = None - self._image_content = None - - @property - def name(self): - """Return the name of this device.""" - return self._doorbell.device_name - - @property - def is_recording(self): - """Return true if the device is recording.""" - return self._doorbell.has_subscription - - @property - def motion_detection_enabled(self): - """Return the camera motion detection status.""" - return True - - @property - def brand(self): - """Return the camera brand.""" - return 'August' - - @property - def model(self): - """Return the camera model.""" - return 'Doorbell' - - def camera_image(self): - """Return bytes of camera image.""" - latest = self._data.get_doorbell_detail(self._doorbell.device_id) - - if self._image_url is not latest.image_url: - self._image_url = latest.image_url - self._image_content = requests.get(self._image_url, - timeout=self._timeout).content - - return self._image_content diff --git a/homeassistant/components/camera/axis.py b/homeassistant/components/camera/axis.py deleted file mode 100644 index 9846ae85fb2d1..0000000000000 --- a/homeassistant/components/camera/axis.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -Support for Axis camera streaming. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/camera.axis/ -""" -import logging - -from homeassistant.components.camera.mjpeg import ( - CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera, filter_urllib3_logging) -from homeassistant.const import ( - CONF_AUTHENTICATION, CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, - CONF_USERNAME, HTTP_DIGEST_AUTHENTICATION) -from homeassistant.helpers.dispatcher import dispatcher_connect - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'axis' -DEPENDENCIES = [DOMAIN] - - -def _get_image_url(host, port, mode): - """Set the URL to get the image.""" - if mode == 'mjpeg': - return 'http://{}:{}/axis-cgi/mjpg/video.cgi'.format(host, port) - if mode == 'single': - return 'http://{}:{}/axis-cgi/jpg/image.cgi'.format(host, port) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Axis camera.""" - filter_urllib3_logging() - - camera_config = { - CONF_NAME: discovery_info[CONF_NAME], - CONF_USERNAME: discovery_info[CONF_USERNAME], - CONF_PASSWORD: discovery_info[CONF_PASSWORD], - CONF_MJPEG_URL: _get_image_url( - discovery_info[CONF_HOST], str(discovery_info[CONF_PORT]), - 'mjpeg'), - CONF_STILL_IMAGE_URL: _get_image_url( - discovery_info[CONF_HOST], str(discovery_info[CONF_PORT]), - 'single'), - CONF_AUTHENTICATION: HTTP_DIGEST_AUTHENTICATION, - } - add_entities([AxisCamera( - hass, camera_config, str(discovery_info[CONF_PORT]))]) - - -class AxisCamera(MjpegCamera): - """Representation of a Axis camera.""" - - def __init__(self, hass, config, port): - """Initialize Axis Communications camera component.""" - super().__init__(config) - self.port = port - dispatcher_connect( - hass, DOMAIN + '_' + config[CONF_NAME] + '_new_ip', self._new_ip) - - def _new_ip(self, host): - """Set new IP for video stream.""" - self._mjpeg_url = _get_image_url(host, self.port, 'mjpeg') - self._still_image_url = _get_image_url(host, self.port, 'single') diff --git a/homeassistant/components/camera/blink.py b/homeassistant/components/camera/blink.py deleted file mode 100644 index e904791445630..0000000000000 --- a/homeassistant/components/camera/blink.py +++ /dev/null @@ -1,82 +0,0 @@ -""" -Support for Blink system camera. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/camera.blink/ -""" -import logging - -from homeassistant.components.blink import BLINK_DATA, DEFAULT_BRAND -from homeassistant.components.camera import Camera - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['blink'] - -ATTR_VIDEO_CLIP = 'video' -ATTR_IMAGE = 'image' - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up a Blink Camera.""" - if discovery_info is None: - return - data = hass.data[BLINK_DATA] - devs = [] - for name, camera in data.cameras.items(): - devs.append(BlinkCamera(data, name, camera)) - - add_entities(devs) - - -class BlinkCamera(Camera): - """An implementation of a Blink Camera.""" - - def __init__(self, data, name, camera): - """Initialize a camera.""" - super().__init__() - self.data = data - self._name = "{} {}".format(BLINK_DATA, name) - self._camera = camera - self._unique_id = "{}-camera".format(camera.serial) - self.response = None - self.current_image = None - self.last_image = None - _LOGGER.debug("Initialized blink camera %s", self._name) - - @property - def name(self): - """Return the camera name.""" - return self._name - - @property - def unique_id(self): - """Return the unique camera id.""" - return self._unique_id - - @property - def device_state_attributes(self): - """Return the camera attributes.""" - return self._camera.attributes - - def enable_motion_detection(self): - """Enable motion detection for the camera.""" - self._camera.set_motion_detect(True) - - def disable_motion_detection(self): - """Disable motion detection for the camera.""" - self._camera.set_motion_detect(False) - - @property - def motion_detection_enabled(self): - """Return the state of the camera.""" - return self._camera.motion_enabled - - @property - def brand(self): - """Return the camera brand.""" - return DEFAULT_BRAND - - def camera_image(self): - """Return a still image response from the camera.""" - return self._camera.image_from_cache.content diff --git a/homeassistant/components/camera/bloomsky.py b/homeassistant/components/camera/bloomsky.py deleted file mode 100644 index 1c9266ca3a724..0000000000000 --- a/homeassistant/components/camera/bloomsky.py +++ /dev/null @@ -1,64 +0,0 @@ -""" -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/ -""" -import logging - -import requests - -from homeassistant.components.camera import Camera - -DEPENDENCIES = ['bloomsky'] - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up access to BloomSky cameras.""" - bloomsky = hass.components.bloomsky - for device in bloomsky.BLOOMSKY.devices.values(): - add_entities([BloomSkyCamera(bloomsky.BLOOMSKY, device)]) - - -class BloomSkyCamera(Camera): - """Representation of the images published from the BloomSky's camera.""" - - def __init__(self, bs, device): - """Initialize access to the BloomSky camera images.""" - super(BloomSkyCamera, self).__init__() - self._name = device['DeviceName'] - self._id = device['DeviceID'] - self._bloomsky = bs - self._url = "" - self._last_url = "" - # last_image will store images as they are downloaded so that the - # frequent updates in home-assistant don't keep poking the server - # to download the same image over and over. - self._last_image = "" - self._logger = logging.getLogger(__name__) - - def camera_image(self): - """Update the camera's image if it has changed.""" - try: - self._url = self._bloomsky.devices[self._id]['Data']['ImageURL'] - self._bloomsky.refresh_devices() - # If the URL hasn't changed then the image hasn't changed. - if self._url != self._last_url: - response = requests.get(self._url, timeout=10) - self._last_url = self._url - self._last_image = response.content - except requests.exceptions.RequestException as error: - self._logger.error("Error getting bloomsky image: %s", error) - return None - - return self._last_image - - @property - def unique_id(self): - """Return a unique ID.""" - return self._id - - @property - def name(self): - """Return the name of this BloomSky device.""" - return self._name diff --git a/homeassistant/components/camera/doorbird.py b/homeassistant/components/camera/doorbird.py deleted file mode 100644 index 8982e6d08473e..0000000000000 --- a/homeassistant/components/camera/doorbird.py +++ /dev/null @@ -1,88 +0,0 @@ -""" -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/ -""" -import asyncio -import datetime -import logging - -import aiohttp -import async_timeout - -from homeassistant.components.camera import Camera -from homeassistant.components.doorbird import DOMAIN as DOORBIRD_DOMAIN -from homeassistant.helpers.aiohttp_client import async_get_clientsession - -DEPENDENCIES = ['doorbird'] - -_CAMERA_LAST_VISITOR = "{} Last Ring" -_CAMERA_LAST_MOTION = "{} Last Motion" -_CAMERA_LIVE = "{} Live" -_LAST_VISITOR_INTERVAL = datetime.timedelta(minutes=1) -_LAST_MOTION_INTERVAL = datetime.timedelta(minutes=1) -_LIVE_INTERVAL = datetime.timedelta(seconds=1) -_LOGGER = logging.getLogger(__name__) -_TIMEOUT = 10 # seconds - - -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up the DoorBird camera platform.""" - for doorstation in hass.data[DOORBIRD_DOMAIN]: - device = doorstation.device - async_add_entities([ - DoorBirdCamera( - device.live_image_url, - _CAMERA_LIVE.format(doorstation.name), - _LIVE_INTERVAL), - DoorBirdCamera( - device.history_image_url(1, 'doorbell'), - _CAMERA_LAST_VISITOR.format(doorstation.name), - _LAST_VISITOR_INTERVAL), - DoorBirdCamera( - device.history_image_url(1, 'motionsensor'), - _CAMERA_LAST_MOTION.format(doorstation.name), - _LAST_MOTION_INTERVAL), - ]) - - -class DoorBirdCamera(Camera): - """The camera on a DoorBird device.""" - - def __init__(self, url, name, interval=None): - """Initialize the camera on a DoorBird device.""" - self._url = url - self._name = name - self._last_image = None - self._interval = interval or datetime.timedelta - self._last_update = datetime.datetime.min - super().__init__() - - @property - def name(self): - """Get the name of the camera.""" - return self._name - - async def async_camera_image(self): - """Pull a still image from the camera.""" - now = datetime.datetime.now() - - if self._last_image and now - self._last_update < self._interval: - return self._last_image - - try: - websession = async_get_clientsession(self.hass) - with async_timeout.timeout(_TIMEOUT, loop=self.hass.loop): - response = await websession.get(self._url) - - self._last_image = await response.read() - self._last_update = now - return self._last_image - except asyncio.TimeoutError: - _LOGGER.error("Camera image timed out") - return self._last_image - except aiohttp.ClientError as error: - _LOGGER.error("Error getting camera image: %s", error) - return self._last_image diff --git a/homeassistant/components/camera/logi_circle.py b/homeassistant/components/camera/logi_circle.py deleted file mode 100644 index 1dae58ad0f7af..0000000000000 --- a/homeassistant/components/camera/logi_circle.py +++ /dev/null @@ -1,210 +0,0 @@ -""" -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/ -""" -import logging -import asyncio -from datetime import timedelta - -import voluptuous as vol - -from homeassistant.helpers import config_validation as cv -from homeassistant.components.logi_circle import ( - DOMAIN as LOGI_CIRCLE_DOMAIN, CONF_ATTRIBUTION) -from homeassistant.components.camera import ( - Camera, PLATFORM_SCHEMA, CAMERA_SERVICE_SCHEMA, SUPPORT_ON_OFF, - ATTR_ENTITY_ID, ATTR_FILENAME, DOMAIN) -from homeassistant.const import ( - ATTR_ATTRIBUTION, ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, - CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF) - -DEPENDENCIES = ['logi_circle'] - -_LOGGER = logging.getLogger(__name__) - -SCAN_INTERVAL = timedelta(seconds=60) - -SERVICE_SET_CONFIG = 'logi_circle_set_config' -SERVICE_LIVESTREAM_SNAPSHOT = 'logi_circle_livestream_snapshot' -SERVICE_LIVESTREAM_RECORD = 'logi_circle_livestream_record' -DATA_KEY = 'camera.logi_circle' - -BATTERY_SAVING_MODE_KEY = 'BATTERY_SAVING' -PRIVACY_MODE_KEY = 'PRIVACY_MODE' -LED_MODE_KEY = 'LED' - -ATTR_MODE = 'mode' -ATTR_VALUE = 'value' -ATTR_DURATION = 'duration' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): - cv.time_period, -}) - -LOGI_CIRCLE_SERVICE_SET_CONFIG = CAMERA_SERVICE_SCHEMA.extend({ - vol.Required(ATTR_MODE): vol.In([BATTERY_SAVING_MODE_KEY, LED_MODE_KEY, - PRIVACY_MODE_KEY]), - vol.Required(ATTR_VALUE): cv.boolean -}) - -LOGI_CIRCLE_SERVICE_SNAPSHOT = CAMERA_SERVICE_SCHEMA.extend({ - vol.Required(ATTR_FILENAME): cv.template -}) - -LOGI_CIRCLE_SERVICE_RECORD = CAMERA_SERVICE_SCHEMA.extend({ - vol.Required(ATTR_FILENAME): cv.template, - vol.Required(ATTR_DURATION): cv.positive_int -}) - - -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): - """Set up a Logi Circle Camera.""" - devices = hass.data[LOGI_CIRCLE_DOMAIN] - - cameras = [] - for device in devices: - cameras.append(LogiCam(device, config)) - - async_add_entities(cameras, True) - - async def service_handler(service): - """Dispatch service calls to target entities.""" - params = {key: value for key, value in service.data.items() - if key != ATTR_ENTITY_ID} - entity_ids = service.data.get(ATTR_ENTITY_ID) - if entity_ids: - target_devices = [dev for dev in cameras - if dev.entity_id in entity_ids] - else: - target_devices = cameras - - for target_device in target_devices: - if service.service == SERVICE_SET_CONFIG: - await target_device.set_config(**params) - if service.service == SERVICE_LIVESTREAM_SNAPSHOT: - await target_device.livestream_snapshot(**params) - if service.service == SERVICE_LIVESTREAM_RECORD: - await target_device.download_livestream(**params) - - hass.services.async_register( - DOMAIN, SERVICE_SET_CONFIG, service_handler, - schema=LOGI_CIRCLE_SERVICE_SET_CONFIG) - - hass.services.async_register( - DOMAIN, SERVICE_LIVESTREAM_SNAPSHOT, service_handler, - schema=LOGI_CIRCLE_SERVICE_SNAPSHOT) - - hass.services.async_register( - DOMAIN, SERVICE_LIVESTREAM_RECORD, service_handler, - schema=LOGI_CIRCLE_SERVICE_RECORD) - - -class LogiCam(Camera): - """An implementation of a Logi Circle camera.""" - - def __init__(self, camera, device_info): - """Initialize Logi Circle camera.""" - super().__init__() - self._camera = camera - self._name = self._camera.name - self._id = self._camera.mac_address - self._has_battery = self._camera.supports_feature('battery_level') - - @property - def unique_id(self): - """Return a unique ID.""" - return self._id - - @property - def name(self): - """Return the name of this camera.""" - return self._name - - @property - def supported_features(self): - """Logi Circle camera's support turning on and off ("soft" switch).""" - return SUPPORT_ON_OFF - - @property - def device_state_attributes(self): - """Return the state attributes.""" - state = { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - 'battery_saving_mode': ( - STATE_ON if self._camera.battery_saving else STATE_OFF), - 'ip_address': self._camera.ip_address, - 'microphone_gain': self._camera.microphone_gain - } - - # Add battery attributes if camera is battery-powered - if self._has_battery: - state[ATTR_BATTERY_CHARGING] = self._camera.is_charging - state[ATTR_BATTERY_LEVEL] = self._camera.battery_level - - return state - - async def async_camera_image(self): - """Return a still image from the camera.""" - return await self._camera.get_snapshot_image() - - async def async_turn_off(self): - """Disable streaming mode for this camera.""" - await self._camera.set_streaming_mode(False) - - async def async_turn_on(self): - """Enable streaming mode for this camera.""" - await self._camera.set_streaming_mode(True) - - @property - def should_poll(self): - """Update the image periodically.""" - return True - - async def set_config(self, mode, value): - """Set an configuration property for the target camera.""" - if mode == LED_MODE_KEY: - await self._camera.set_led(value) - if mode == PRIVACY_MODE_KEY: - await self._camera.set_privacy_mode(value) - if mode == BATTERY_SAVING_MODE_KEY: - await self._camera.set_battery_saving_mode(value) - - async def download_livestream(self, filename, duration): - """Download a recording from the camera's livestream.""" - # Render filename from template. - filename.hass = self.hass - stream_file = filename.async_render( - variables={ATTR_ENTITY_ID: self.entity_id}) - - # Respect configured path whitelist. - if not self.hass.config.is_allowed_path(stream_file): - _LOGGER.error( - "Can't write %s, no access to path!", stream_file) - return - - asyncio.shield(self._camera.record_livestream( - stream_file, timedelta(seconds=duration)), loop=self.hass.loop) - - async def livestream_snapshot(self, filename): - """Download a still frame from the camera's livestream.""" - # Render filename from template. - filename.hass = self.hass - snapshot_file = filename.async_render( - variables={ATTR_ENTITY_ID: self.entity_id}) - - # Respect configured path whitelist. - if not self.hass.config.is_allowed_path(snapshot_file): - _LOGGER.error( - "Can't write %s, no access to path!", snapshot_file) - return - - asyncio.shield(self._camera.get_livestream_image( - snapshot_file), loop=self.hass.loop) - - async def async_update(self): - """Update camera entity and refresh attributes.""" - await self._camera.update() diff --git a/homeassistant/components/camera/neato.py b/homeassistant/components/camera/neato.py deleted file mode 100644 index 4df423344bb31..0000000000000 --- a/homeassistant/components/camera/neato.py +++ /dev/null @@ -1,70 +0,0 @@ -""" -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/ -""" -import logging - -from datetime import timedelta -from homeassistant.components.camera import Camera -from homeassistant.components.neato import ( - NEATO_MAP_DATA, NEATO_ROBOTS, NEATO_LOGIN) - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['neato'] - -SCAN_INTERVAL = timedelta(minutes=10) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Neato Camera.""" - dev = [] - for robot in hass.data[NEATO_ROBOTS]: - if 'maps' in robot.traits: - dev.append(NeatoCleaningMap(hass, robot)) - _LOGGER.debug("Adding robots for cleaning maps %s", dev) - add_entities(dev, True) - - -class NeatoCleaningMap(Camera): - """Neato cleaning map for last clean.""" - - def __init__(self, hass, robot): - """Initialize Neato cleaning map.""" - super().__init__() - self.robot = robot - self._robot_name = '{} {}'.format(self.robot.name, 'Cleaning Map') - self._robot_serial = self.robot.serial - self.neato = hass.data[NEATO_LOGIN] - self._image_url = None - self._image = None - - def camera_image(self): - """Return image response.""" - self.update() - return self._image - - def update(self): - """Check the contents of the map list.""" - self.neato.update_robots() - image_url = None - map_data = self.hass.data[NEATO_MAP_DATA] - image_url = map_data[self._robot_serial]['maps'][0]['url'] - if image_url == self._image_url: - _LOGGER.debug("The map image_url is the same as old") - return - image = self.neato.download_map(image_url) - self._image = image.read() - self._image_url = image_url - - @property - def name(self): - """Return the name of this camera.""" - return self._robot_name - - @property - def unique_id(self): - """Return unique ID.""" - return self._robot_serial diff --git a/homeassistant/components/camera/nest.py b/homeassistant/components/camera/nest.py deleted file mode 100644 index 158123989c021..0000000000000 --- a/homeassistant/components/camera/nest.py +++ /dev/null @@ -1,161 +0,0 @@ -""" -Support for Nest Cameras. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/camera.nest/ -""" -import logging -from datetime import timedelta - -import requests - -from homeassistant.components import nest -from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera, - SUPPORT_ON_OFF) -from homeassistant.util.dt import utcnow - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['nest'] - -NEST_BRAND = 'Nest' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up a Nest Cam. - - No longer in use. - """ - - -async def async_setup_entry(hass, entry, async_add_entities): - """Set up a Nest sensor based on a config entry.""" - camera_devices = \ - await hass.async_add_job(hass.data[nest.DATA_NEST].cameras) - cameras = [NestCamera(structure, device) - for structure, device in camera_devices] - async_add_entities(cameras, True) - - -class NestCamera(Camera): - """Representation of a Nest Camera.""" - - def __init__(self, structure, device): - """Initialize a Nest Camera.""" - super(NestCamera, self).__init__() - self.structure = structure - self.device = device - self._location = None - self._name = None - self._online = None - self._is_streaming = None - self._is_video_history_enabled = False - # Default to non-NestAware subscribed, but will be fixed during update - self._time_between_snapshots = timedelta(seconds=30) - self._last_image = None - self._next_snapshot_at = None - - @property - def name(self): - """Return the name of the nest, if any.""" - return self._name - - @property - def unique_id(self): - """Return the serial number.""" - return self.device.device_id - - @property - def device_info(self): - """Return information about the device.""" - return { - 'identifiers': { - (nest.DOMAIN, self.device.device_id) - }, - 'name': self.device.name_long, - 'manufacturer': 'Nest Labs', - 'model': "Camera", - } - - @property - def should_poll(self): - """Nest camera should poll periodically.""" - return True - - @property - def is_recording(self): - """Return true if the device is recording.""" - return self._is_streaming - - @property - def brand(self): - """Return the brand of the camera.""" - return NEST_BRAND - - @property - def supported_features(self): - """Nest Cam support turn on and off.""" - return SUPPORT_ON_OFF - - @property - def is_on(self): - """Return true if on.""" - return self._online and self._is_streaming - - def turn_off(self): - """Turn off camera.""" - _LOGGER.debug('Turn off camera %s', self._name) - # Calling Nest API in is_streaming setter. - # device.is_streaming would not immediately change until the process - # finished in Nest Cam. - self.device.is_streaming = False - - def turn_on(self): - """Turn on camera.""" - if not self._online: - _LOGGER.error('Camera %s is offline.', self._name) - return - - _LOGGER.debug('Turn on camera %s', self._name) - # Calling Nest API in is_streaming setter. - # device.is_streaming would not immediately change until the process - # finished in Nest Cam. - self.device.is_streaming = True - - def update(self): - """Cache value from Python-nest.""" - self._location = self.device.where - self._name = self.device.name - self._online = self.device.online - self._is_streaming = self.device.is_streaming - self._is_video_history_enabled = self.device.is_video_history_enabled - - if self._is_video_history_enabled: - # NestAware allowed 10/min - self._time_between_snapshots = timedelta(seconds=6) - else: - # Otherwise, 2/min - self._time_between_snapshots = timedelta(seconds=30) - - def _ready_for_snapshot(self, now): - return (self._next_snapshot_at is None or - now > self._next_snapshot_at) - - def camera_image(self): - """Return a still image response from the camera.""" - now = utcnow() - if self._ready_for_snapshot(now): - url = self.device.snapshot_url - - try: - response = requests.get(url) - except requests.exceptions.RequestException as error: - _LOGGER.error("Error getting camera image: %s", error) - return None - - self._next_snapshot_at = now + self._time_between_snapshots - self._last_image = response.content - - return self._last_image diff --git a/homeassistant/components/camera/netatmo.py b/homeassistant/components/camera/netatmo.py deleted file mode 100644 index 93ad2cd055b7c..0000000000000 --- a/homeassistant/components/camera/netatmo.py +++ /dev/null @@ -1,110 +0,0 @@ -""" -Support for the Netatmo cameras. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/camera.netatmo/. -""" -import logging - -import requests -import voluptuous as vol - -from homeassistant.const import CONF_VERIFY_SSL -from homeassistant.components.netatmo import CameraData -from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA) -from homeassistant.helpers import config_validation as cv - -DEPENDENCIES = ['netatmo'] - -_LOGGER = logging.getLogger(__name__) - -CONF_HOME = 'home' -CONF_CAMERAS = 'cameras' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, - vol.Optional(CONF_HOME): cv.string, - vol.Optional(CONF_CAMERAS, default=[]): - vol.All(cv.ensure_list, [cv.string]), -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up access to Netatmo cameras.""" - netatmo = hass.components.netatmo - home = config.get(CONF_HOME) - verify_ssl = config.get(CONF_VERIFY_SSL, True) - import pyatmo - try: - data = CameraData(netatmo.NETATMO_AUTH, home) - for camera_name in data.get_camera_names(): - camera_type = data.get_camera_type(camera=camera_name, home=home) - if CONF_CAMERAS in config: - if config[CONF_CAMERAS] != [] and \ - camera_name not in config[CONF_CAMERAS]: - continue - add_entities([NetatmoCamera(data, camera_name, home, - camera_type, verify_ssl)]) - except pyatmo.NoDevice: - return None - - -class NetatmoCamera(Camera): - """Representation of the images published from a Netatmo camera.""" - - def __init__(self, data, camera_name, home, camera_type, verify_ssl): - """Set up for access to the Netatmo camera images.""" - super(NetatmoCamera, self).__init__() - self._data = data - self._camera_name = camera_name - self._verify_ssl = verify_ssl - if home: - self._name = home + ' / ' + camera_name - else: - self._name = camera_name - self._vpnurl, self._localurl = self._data.camera_data.cameraUrls( - camera=camera_name - ) - self._cameratype = camera_type - - def camera_image(self): - """Return a still image response from the camera.""" - try: - if self._localurl: - response = requests.get('{0}/live/snapshot_720.jpg'.format( - self._localurl), timeout=10) - elif self._vpnurl: - response = requests.get('{0}/live/snapshot_720.jpg'.format( - self._vpnurl), timeout=10, verify=self._verify_ssl) - else: - _LOGGER.error("Welcome VPN URL is None") - self._data.update() - (self._vpnurl, self._localurl) = \ - self._data.camera_data.cameraUrls(camera=self._camera_name) - return None - except requests.exceptions.RequestException as error: - _LOGGER.error("Welcome URL changed: %s", error) - self._data.update() - (self._vpnurl, self._localurl) = \ - self._data.camera_data.cameraUrls(camera=self._camera_name) - return None - return response.content - - @property - def name(self): - """Return the name of this Netatmo camera device.""" - return self._name - - @property - def brand(self): - """Return the camera brand.""" - return "Netatmo" - - @property - def model(self): - """Return the camera model.""" - if self._cameratype == "NOC": - return "Presence" - if self._cameratype == "NACamera": - return "Welcome" - return None diff --git a/homeassistant/components/camera/skybell.py b/homeassistant/components/camera/skybell.py deleted file mode 100644 index 3ad95e40d62f9..0000000000000 --- a/homeassistant/components/camera/skybell.py +++ /dev/null @@ -1,102 +0,0 @@ -""" -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/ -""" -from datetime import timedelta -import logging - -import requests -import voluptuous as vol - -from homeassistant.components.camera import PLATFORM_SCHEMA -from homeassistant.const import CONF_MONITORED_CONDITIONS -import homeassistant.helpers.config_validation as cv - -from homeassistant.components.camera import Camera -from homeassistant.components.skybell import ( - DOMAIN as SKYBELL_DOMAIN, SkybellDevice) - -DEPENDENCIES = ['skybell'] - -_LOGGER = logging.getLogger(__name__) - -SCAN_INTERVAL = timedelta(seconds=90) - -IMAGE_AVATAR = 'avatar' -IMAGE_ACTIVITY = 'activity' - -CONF_ACTIVITY_NAME = 'activity_name' -CONF_AVATAR_NAME = 'avatar_name' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_MONITORED_CONDITIONS, default=[IMAGE_AVATAR]): - vol.All(cv.ensure_list, [vol.In([IMAGE_AVATAR, IMAGE_ACTIVITY])]), - vol.Optional(CONF_ACTIVITY_NAME): cv.string, - vol.Optional(CONF_AVATAR_NAME): cv.string, -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the platform for a Skybell device.""" - cond = config[CONF_MONITORED_CONDITIONS] - names = {} - names[IMAGE_ACTIVITY] = config.get(CONF_ACTIVITY_NAME) - names[IMAGE_AVATAR] = config.get(CONF_AVATAR_NAME) - skybell = hass.data.get(SKYBELL_DOMAIN) - - sensors = [] - for device in skybell.get_devices(): - for camera_type in cond: - sensors.append(SkybellCamera(device, camera_type, - names.get(camera_type))) - - add_entities(sensors, True) - - -class SkybellCamera(SkybellDevice, Camera): - """A camera implementation for Skybell devices.""" - - def __init__(self, device, camera_type, name=None): - """Initialize a camera for a Skybell device.""" - self._type = camera_type - SkybellDevice.__init__(self, device) - Camera.__init__(self) - if name is not None: - self._name = "{} {}".format(self._device.name, name) - else: - self._name = self._device.name - self._url = None - self._response = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def image_url(self): - """Get the camera image url based on type.""" - if self._type == IMAGE_ACTIVITY: - return self._device.activity_image - return self._device.image - - def camera_image(self): - """Get the latest camera image.""" - super().update() - - if self._url != self.image_url: - self._url = self.image_url - - try: - self._response = requests.get( - self._url, stream=True, timeout=10) - except requests.HTTPError as err: - _LOGGER.warning("Failed to get camera image: %s", err) - self._response = None - - if not self._response: - return None - - return self._response.content diff --git a/homeassistant/components/camera/usps.py b/homeassistant/components/camera/usps.py deleted file mode 100644 index d23359d8c57ab..0000000000000 --- a/homeassistant/components/camera/usps.py +++ /dev/null @@ -1,94 +0,0 @@ -""" -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/ -""" -from datetime import timedelta -import logging - -from homeassistant.components.camera import Camera -from homeassistant.components.usps import DATA_USPS - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['usps'] - -SCAN_INTERVAL = timedelta(seconds=10) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up USPS mail camera.""" - if discovery_info is None: - return - - usps = hass.data[DATA_USPS] - add_entities([USPSCamera(usps)]) - - -class USPSCamera(Camera): - """Representation of the images available from USPS.""" - - def __init__(self, usps): - """Initialize the USPS camera images.""" - super().__init__() - - self._usps = usps - self._name = self._usps.name - self._session = self._usps.session - - self._mail_img = [] - self._last_mail = None - self._mail_index = 0 - self._mail_count = 0 - - self._timer = None - - def camera_image(self): - """Update the camera's image if it has changed.""" - self._usps.update() - try: - self._mail_count = len(self._usps.mail) - except TypeError: - # No mail - return None - - if self._usps.mail != self._last_mail: - # Mail items must have changed - self._mail_img = [] - if len(self._usps.mail) >= 1: - self._last_mail = self._usps.mail - for article in self._usps.mail: - _LOGGER.debug("Fetching article image: %s", article) - img = self._session.get(article['image']).content - self._mail_img.append(img) - - try: - return self._mail_img[self._mail_index] - except IndexError: - return None - - @property - def name(self): - """Return the name of this camera.""" - return '{} mail'.format(self._name) - - @property - def model(self): - """Return date of mail as model.""" - try: - return 'Date: {}'.format(str(self._usps.mail[0]['date'])) - except IndexError: - return None - - @property - def should_poll(self): - """Update the mail image index periodically.""" - return True - - def update(self): - """Update mail image index.""" - if self._mail_index < (self._mail_count - 1): - self._mail_index += 1 - else: - self._mail_index = 0 diff --git a/homeassistant/components/camera/verisure.py b/homeassistant/components/camera/verisure.py deleted file mode 100644 index 01e4e82f3bcc1..0000000000000 --- a/homeassistant/components/camera/verisure.py +++ /dev/null @@ -1,99 +0,0 @@ -""" -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/ -""" -import errno -import logging -import os - -from homeassistant.components.camera import Camera -from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.components.verisure import HUB as hub -from homeassistant.components.verisure import CONF_SMARTCAM - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Verisure Camera.""" - if not int(hub.config.get(CONF_SMARTCAM, 1)): - return False - directory_path = hass.config.config_dir - if not os.access(directory_path, os.R_OK): - _LOGGER.error("file path %s is not readable", directory_path) - return False - hub.update_overview() - smartcams = [] - smartcams.extend([ - VerisureSmartcam(hass, device_label, directory_path) - for device_label in hub.get( - "$.customerImageCameras[*].deviceLabel")]) - add_entities(smartcams) - - -class VerisureSmartcam(Camera): - """Representation of a Verisure camera.""" - - def __init__(self, hass, device_label, directory_path): - """Initialize Verisure File Camera component.""" - super().__init__() - - self._device_label = device_label - self._directory_path = directory_path - self._image = None - self._image_id = None - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, - self.delete_image) - - def camera_image(self): - """Return image response.""" - self.check_imagelist() - if not self._image: - _LOGGER.debug("No image to display") - return - _LOGGER.debug("Trying to open %s", self._image) - with open(self._image, 'rb') as file: - return file.read() - - def check_imagelist(self): - """Check the contents of the image list.""" - hub.update_smartcam_imageseries() - image_ids = hub.get_image_info( - "$.imageSeries[?(@.deviceLabel=='%s')].image[0].imageId", - self._device_label) - if not image_ids: - return - new_image_id = image_ids[0] - if new_image_id in ('-1', self._image_id): - _LOGGER.debug("The image is the same, or loading image_id") - return - _LOGGER.debug("Download new image %s", new_image_id) - new_image_path = os.path.join( - self._directory_path, '{}{}'.format(new_image_id, '.jpg')) - hub.session.download_image( - self._device_label, new_image_id, new_image_path) - _LOGGER.debug("Old image_id=%s", self._image_id) - self.delete_image(self) - - self._image_id = new_image_id - self._image = new_image_path - - def delete_image(self, event): - """Delete an old image.""" - remove_image = os.path.join( - self._directory_path, '{}{}'.format(self._image_id, '.jpg')) - try: - os.remove(remove_image) - _LOGGER.debug("Deleting old image %s", remove_image) - except OSError as error: - if error.errno != errno.ENOENT: - raise - - @property - def name(self): - """Return the name of this camera.""" - return hub.get_first( - "$.customerImageCameras[?(@.deviceLabel=='%s')].area", - self._device_label) diff --git a/homeassistant/components/canary.py b/homeassistant/components/canary.py deleted file mode 100644 index 04c33d83f3db8..0000000000000 --- a/homeassistant/components/canary.py +++ /dev/null @@ -1,128 +0,0 @@ -""" -Support for Canary. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/canary/ -""" -import logging -from datetime import timedelta - -import voluptuous as vol -from requests import ConnectTimeout, HTTPError - -import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT -from homeassistant.helpers import discovery -from homeassistant.util import Throttle - -REQUIREMENTS = ['py-canary==0.5.0'] - -_LOGGER = logging.getLogger(__name__) - -NOTIFICATION_ID = 'canary_notification' -NOTIFICATION_TITLE = 'Canary Setup' - -DOMAIN = 'canary' -DATA_CANARY = 'canary' -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) -DEFAULT_TIMEOUT = 10 - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - }), -}, extra=vol.ALLOW_EXTRA) - -CANARY_COMPONENTS = [ - 'alarm_control_panel', 'camera', 'sensor' -] - - -def setup(hass, config): - """Set up the Canary component.""" - conf = config[DOMAIN] - username = conf.get(CONF_USERNAME) - password = conf.get(CONF_PASSWORD) - timeout = conf.get(CONF_TIMEOUT) - - try: - hass.data[DATA_CANARY] = CanaryData(username, password, timeout) - except (ConnectTimeout, HTTPError) as ex: - _LOGGER.error("Unable to connect to Canary service: %s", str(ex)) - hass.components.persistent_notification.create( - 'Error: {}
' - 'You will need to restart hass after fixing.' - ''.format(ex), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) - return False - - for component in CANARY_COMPONENTS: - discovery.load_platform(hass, component, DOMAIN, {}, config) - - return True - - -class CanaryData: - """Get the latest data and update the states.""" - - def __init__(self, username, password, timeout): - """Init the Canary data object.""" - from canary.api import Api - self._api = Api(username, password, timeout) - - self._locations_by_id = {} - self._readings_by_device_id = {} - self._entries_by_location_id = {} - - self.update() - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self, **kwargs): - """Get the latest data from py-canary.""" - for location in self._api.get_locations(): - location_id = location.location_id - - self._locations_by_id[location_id] = location - self._entries_by_location_id[location_id] = self._api.get_entries( - location_id, entry_type="motion", limit=1) - - for device in location.devices: - if device.is_online: - self._readings_by_device_id[device.device_id] = \ - self._api.get_latest_readings(device.device_id) - - @property - def locations(self): - """Return a list of locations.""" - return self._locations_by_id.values() - - def get_motion_entries(self, location_id): - """Return a list of motion entries based on location_id.""" - return self._entries_by_location_id.get(location_id, []) - - def get_location(self, location_id): - """Return a location based on location_id.""" - return self._locations_by_id.get(location_id, []) - - def get_readings(self, device_id): - """Return a list of readings based on device_id.""" - return self._readings_by_device_id.get(device_id, []) - - def get_reading(self, device_id, sensor_type): - """Return reading for device_id and sensor type.""" - readings = self._readings_by_device_id.get(device_id, []) - return next(( - reading.value for reading in readings - if reading.sensor_type == sensor_type), None) - - def set_location_mode(self, location_id, mode_name, is_private=False): - """Set location mode.""" - self._api.set_location_mode(location_id, mode_name, is_private) - self.update(no_throttle=True) - - def get_live_stream_session(self, device): - """Return live stream session.""" - return self._api.get_live_stream_session(device) diff --git a/homeassistant/components/canary/__init__.py b/homeassistant/components/canary/__init__.py new file mode 100644 index 0000000000000..e53c7e22d2d6d --- /dev/null +++ b/homeassistant/components/canary/__init__.py @@ -0,0 +1,123 @@ +"""Support for Canary devices.""" +import logging +from datetime import timedelta + +import voluptuous as vol +from requests import ConnectTimeout, HTTPError + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT +from homeassistant.helpers import discovery +from homeassistant.util import Throttle + +REQUIREMENTS = ['py-canary==0.5.0'] + +_LOGGER = logging.getLogger(__name__) + +NOTIFICATION_ID = 'canary_notification' +NOTIFICATION_TITLE = 'Canary Setup' + +DOMAIN = 'canary' +DATA_CANARY = 'canary' +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) +DEFAULT_TIMEOUT = 10 + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + }), +}, extra=vol.ALLOW_EXTRA) + +CANARY_COMPONENTS = [ + 'alarm_control_panel', 'camera', 'sensor' +] + + +def setup(hass, config): + """Set up the Canary component.""" + conf = config[DOMAIN] + username = conf.get(CONF_USERNAME) + password = conf.get(CONF_PASSWORD) + timeout = conf.get(CONF_TIMEOUT) + + try: + hass.data[DATA_CANARY] = CanaryData(username, password, timeout) + except (ConnectTimeout, HTTPError) as ex: + _LOGGER.error("Unable to connect to Canary service: %s", str(ex)) + hass.components.persistent_notification.create( + 'Error: {}
' + 'You will need to restart hass after fixing.' + ''.format(ex), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + return False + + for component in CANARY_COMPONENTS: + discovery.load_platform(hass, component, DOMAIN, {}, config) + + return True + + +class CanaryData: + """Get the latest data and update the states.""" + + def __init__(self, username, password, timeout): + """Init the Canary data object.""" + from canary.api import Api + self._api = Api(username, password, timeout) + + self._locations_by_id = {} + self._readings_by_device_id = {} + self._entries_by_location_id = {} + + self.update() + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self, **kwargs): + """Get the latest data from py-canary.""" + for location in self._api.get_locations(): + location_id = location.location_id + + self._locations_by_id[location_id] = location + self._entries_by_location_id[location_id] = self._api.get_entries( + location_id, entry_type="motion", limit=1) + + for device in location.devices: + if device.is_online: + self._readings_by_device_id[device.device_id] = \ + self._api.get_latest_readings(device.device_id) + + @property + def locations(self): + """Return a list of locations.""" + return self._locations_by_id.values() + + def get_motion_entries(self, location_id): + """Return a list of motion entries based on location_id.""" + return self._entries_by_location_id.get(location_id, []) + + def get_location(self, location_id): + """Return a location based on location_id.""" + return self._locations_by_id.get(location_id, []) + + def get_readings(self, device_id): + """Return a list of readings based on device_id.""" + return self._readings_by_device_id.get(device_id, []) + + def get_reading(self, device_id, sensor_type): + """Return reading for device_id and sensor type.""" + readings = self._readings_by_device_id.get(device_id, []) + return next(( + reading.value for reading in readings + if reading.sensor_type == sensor_type), None) + + def set_location_mode(self, location_id, mode_name, is_private=False): + """Set location mode.""" + self._api.set_location_mode(location_id, mode_name, is_private) + self.update(no_throttle=True) + + def get_live_stream_session(self, device): + """Return live stream session.""" + return self._api.get_live_stream_session(device) diff --git a/homeassistant/components/cast/.translations/uk.json b/homeassistant/components/cast/.translations/uk.json new file mode 100644 index 0000000000000..783defdca258a --- /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 53f5e70401900..1b3da2005406a 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/cast/media_player.py b/homeassistant/components/cast/media_player.py new file mode 100644 index 0000000000000..432290482f158 --- /dev/null +++ b/homeassistant/components/cast/media_player.py @@ -0,0 +1,792 @@ +"""Provide functionality to interact with Cast devices on the network.""" +import asyncio +import logging +import threading +from typing import Optional, Tuple + +import attr +import voluptuous as vol + +from homeassistant.components.cast import DOMAIN as CAST_DOMAIN +from homeassistant.components.media_player import ( + 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) +from homeassistant.const import ( + CONF_HOST, EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_OFF, STATE_PAUSED, + STATE_PLAYING) +from homeassistant.core import callback +from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, dispatcher_send) +from homeassistant.helpers.typing import ConfigType, HomeAssistantType +import homeassistant.util.dt as dt_util + +DEPENDENCIES = ('cast',) + +_LOGGER = logging.getLogger(__name__) + +CONF_IGNORE_CEC = 'ignore_cec' +CAST_SPLASH = 'https://home-assistant.io/images/cast/splash.png' + +DEFAULT_PORT = 8009 + +SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ + SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \ + SUPPORT_NEXT_TRACK | SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_PLAY + +# Stores a threading.Lock that is held by the internal pychromecast discovery. +INTERNAL_DISCOVERY_RUNNING_KEY = 'cast_discovery_running' +# Stores all ChromecastInfo we encountered through discovery or config as a set +# If we find a chromecast with a new host, the old one will be removed again. +KNOWN_CHROMECAST_INFO_KEY = 'cast_known_chromecasts' +# Stores UUIDs of cast devices that were added as entities. Doesn't store +# None UUIDs. +ADDED_CAST_DEVICES_KEY = 'cast_added_cast_devices' + +# Dispatcher signal fired with a ChromecastInfo every time we discover a new +# 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=[]): + vol.All(cv.ensure_list, [cv.string]), +}) + + +@attr.s(slots=True, frozen=True) +class ChromecastInfo: + """Class to hold all data about a chromecast for creating connections. + + This also has the same attributes as the mDNS fields by zeroconf. + """ + + 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='') + model_name = attr.ib(type=str, default='') + friendly_name = attr.ib(type=Optional[str], default=None) + + @property + def is_audio_group(self) -> bool: + """Return if this is an audio group.""" + return self.port != DEFAULT_PORT + + @property + def is_information_complete(self) -> bool: + """Return if all information is filled out.""" + return all(attr.astuple(self)) + + @property + def host_port(self) -> Tuple[str, int]: + """Return the host+port tuple.""" + return self.host, self.port + + +def _fill_out_missing_chromecast_info(info: ChromecastInfo) -> ChromecastInfo: + """Fill out missing attributes of ChromecastInfo using blocking HTTP.""" + if info.is_information_complete or info.is_audio_group: + # We have all information, no need to check HTTP API. Or this is an + # audio group, so checking via HTTP won't give us any new information. + return info + + # Fill out missing information via HTTP dial. + from pychromecast import dial + + 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( + 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), + model_name=(info.model_name or http_device_status.model_name) + ) + + +def _discover_chromecast(hass: HomeAssistantType, info: ChromecastInfo): + if info in hass.data[KNOWN_CHROMECAST_INFO_KEY]: + _LOGGER.debug("Discovered previous chromecast %s", info) + + # Either discovered completely new chromecast or a "moved" one. + info = _fill_out_missing_chromecast_info(info) + _LOGGER.debug("Discovered chromecast %s", info) + + if info.uuid is not None: + # Remove previous cast infos with same uuid from known chromecasts. + same_uuid = set(x for x in hass.data[KNOWN_CHROMECAST_INFO_KEY] + if info.uuid == x.uuid) + hass.data[KNOWN_CHROMECAST_INFO_KEY] -= same_uuid + + hass.data[KNOWN_CHROMECAST_INFO_KEY].add(info) + 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: + hass.data[INTERNAL_DISCOVERY_RUNNING_KEY] = threading.Lock() + + if not hass.data[INTERNAL_DISCOVERY_RUNNING_KEY].acquire(blocking=False): + # Internal discovery is already running + return + + import pychromecast + + 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], + model_name=mdns[3], + friendly_name=mdns[4], + )) + + _LOGGER.debug("Starting internal pychromecast discovery.") + 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.""" + _LOGGER.debug("Stopping internal pychromecast discovery.") + pychromecast.stop_discovery(browser) + hass.data[INTERNAL_DISCOVERY_RUNNING_KEY].release() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_discovery) + + +@callback +def _async_create_cast_device(hass: HomeAssistantType, + info: ChromecastInfo): + """Create a CastDevice Entity from the chromecast object. + + Returns None if the cast device has already been added. + """ + if info.uuid is None: + # Found a cast without UUID, we don't store it because we won't be able + # to update it anyway. + return CastDevice(info) + + # Found a cast with UUID + added_casts = hass.data[ADDED_CAST_DEVICES_KEY] + if info.uuid in added_casts: + # Already added this one, the entity will take care of moved hosts + # itself + return None + # -> New cast device + added_casts.add(info.uuid) + return CastDevice(info) + + +async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_entities, discovery_info=None): + """Set up thet Cast platform. + + Deprecated. + """ + _LOGGER.warning( + 'Setting configuration for Cast via platform is deprecated. ' + 'Configure via Cast component instead.') + await _async_setup_platform( + hass, config, async_add_entities, discovery_info) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Cast from a config entry.""" + config = hass.data[CAST_DOMAIN].get('media_player', {}) + if not isinstance(config, list): + config = [config] + + # no pending task + done, _ = await asyncio.wait([ + _async_setup_platform(hass, cfg, async_add_entities, None) + for cfg in config]) + if any([task.exception() for task in done]): + raise PlatformNotReady + + +async def _async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_entities, discovery_info): + """Set up the cast platform.""" + import pychromecast + + # Import CEC IGNORE attributes + pychromecast.IGNORE_CEC += config.get(CONF_IGNORE_CEC, []) + hass.data.setdefault(ADDED_CAST_DEVICES_KEY, set()) + hass.data.setdefault(KNOWN_CHROMECAST_INFO_KEY, set()) + + info = None + if discovery_info is not None: + info = ChromecastInfo(host=discovery_info['host'], + port=discovery_info['port']) + elif CONF_HOST in config: + info = ChromecastInfo(host=config[CONF_HOST], + port=DEFAULT_PORT) + + @callback + def async_cast_discovered(discover: ChromecastInfo) -> None: + """Handle discovery of a new chromecast.""" + if info is not None and info.host_port != discover.host_port: + # Not our requested cast device. + return + + cast_device = _async_create_cast_device(hass, discover) + if cast_device is not None: + async_add_entities([cast_device]) + + remove_handler = async_dispatcher_connect( + hass, SIGNAL_CAST_DISCOVERED, async_cast_discovered) + # Re-play the callback for all past chromecasts, store the objects in + # a list to avoid concurrent modification resulting in exception. + for chromecast in list(hass.data[KNOWN_CHROMECAST_INFO_KEY]): + async_cast_discovered(chromecast) + + if info is None or info.is_audio_group: + # If we were a) explicitly told to enable discovery or + # b) have an audio group cast device, we need internal discovery. + hass.async_add_job(_setup_internal_discovery, hass) + else: + info = await hass.async_add_job(_fill_out_missing_chromecast_info, + info) + if info.friendly_name is None: + _LOGGER.debug("Cannot retrieve detail information for chromecast" + " %s, the device may not be online", info) + remove_handler() + raise PlatformNotReady + + hass.async_add_job(_discover_chromecast, hass, info) + + +class CastStatusListener: + """Helper class to handle pychromecast status callbacks. + + Necessary because a CastDevice entity can create a new socket client + and therefore callbacks from multiple chromecast connections can + potentially arrive. This class allows invalidating past chromecast objects. + """ + + def __init__(self, cast_device, chromecast): + """Initialize the status listener.""" + self._cast_device = cast_device + self._valid = True + + chromecast.register_status_listener(self) + chromecast.socket_client.media_controller.register_status_listener( + self) + chromecast.register_connection_listener(self) + + def new_cast_status(self, cast_status): + """Handle reception of a new CastStatus.""" + if self._valid: + self._cast_device.new_cast_status(cast_status) + + def new_media_status(self, media_status): + """Handle reception of a new MediaStatus.""" + if self._valid: + self._cast_device.new_media_status(media_status) + + def new_connection_status(self, connection_status): + """Handle reception of a new ConnectionStatus.""" + if self._valid: + self._cast_device.new_connection_status(connection_status) + + def invalidate(self): + """Invalidate this status listener. + + All following callbacks won't be forwarded. + """ + self._valid = False + + +class CastDevice(MediaPlayerDevice): + """Representation of a Cast device on the network. + + This class is the holder of the pychromecast.Chromecast object and its + socket client. It therefore handles all reconnects and audio group changing + "elected leader" itself. + """ + + 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.""" + @callback + def async_cast_discovered(discover: ChromecastInfo): + """Handle discovery of new Chromecast.""" + if self._cast_info.uuid is None: + # We can't handle empty UUIDs + return + 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() + + 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)) + + async def async_will_remove_from_hass(self) -> None: + """Disconnect Chromecast object when removed.""" + await self._async_disconnect() + if self._cast_info.uuid is not None: + # 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 + 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: + # Only setup the chromecast once, added elements to services + # will automatically be picked up. + return + + # pylint: disable=protected-access + 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 + # register the connection listener *after* the initial connection + # attempt. If the initial connection failed, we would never reach + # this code anyway. + self._available = True + self.cast_status = chromecast.status + self.media_status = chromecast.media_controller.status + _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("[%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() + + await self.hass.async_add_job(self._chromecast.disconnect) + + self._invalidate() + + self.async_schedule_update_ha_state() + + def _invalidate(self): + """Invalidate some attributes.""" + self._chromecast = None + self.cast_status = None + self.media_status = None + self.media_status_received = None + if self._status_listener is not None: + self._status_listener.invalidate() + self._status_listener = None + + # ========== Callbacks ========== + def new_cast_status(self, cast_status): + """Handle updates of the cast status.""" + self.cast_status = cast_status + self.schedule_update_ha_state() + + def new_media_status(self, media_status): + """Handle updates of the media status.""" + self.media_status = media_status + self.media_status_received = dt_util.utcnow() + self.schedule_update_ha_state() + + def new_connection_status(self, connection_status): + """Handle updates of connection status.""" + from pychromecast.socket_client import CONNECTION_STATUS_CONNECTED, \ + CONNECTION_STATUS_DISCONNECTED + + _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() + self.schedule_update_ha_state() + return + + new_available = connection_status.status == CONNECTION_STATUS_CONNECTED + if new_available != self._available: + # Connection status callbacks happen often when disconnected. + # Only update state when availability changed to put less pressure + # on state machine. + _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() + + # ========== Service Calls ========== + def turn_on(self): + """Turn on the cast device.""" + import pychromecast + + if not self._chromecast.is_idle: + # Already turned on + return + + if self._chromecast.app_id is not None: + # Quit the previous app before starting splash screen + self._chromecast.quit_app() + + # The only way we can turn the Chromecast is on is by launching an app + self._chromecast.play_media(CAST_SPLASH, + pychromecast.STREAM_TYPE_BUFFERED) + + def turn_off(self): + """Turn off the cast device.""" + self._chromecast.quit_app() + + def mute_volume(self, mute): + """Mute the volume.""" + self._chromecast.set_volume_muted(mute) + + def set_volume_level(self, volume): + """Set volume level, range 0..1.""" + self._chromecast.set_volume(volume) + + def media_play(self): + """Send play command.""" + self._chromecast.media_controller.play() + + def media_pause(self): + """Send pause command.""" + self._chromecast.media_controller.pause() + + def media_stop(self): + """Send stop command.""" + self._chromecast.media_controller.stop() + + def media_previous_track(self): + """Send previous track command.""" + self._chromecast.media_controller.rewind() + + def media_next_track(self): + """Send next track command.""" + self._chromecast.media_controller.skip() + + def media_seek(self, position): + """Seek the media to a specific location.""" + self._chromecast.media_controller.seek(position) + + def play_media(self, media_type, media_id, **kwargs): + """Play media from a URL.""" + self._chromecast.media_controller.play_media(media_id, media_type) + + # ========== Properties ========== + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return the name of the device.""" + return self._cast_info.friendly_name + + @property + def device_info(self): + """Return information about the device.""" + cast_info = self._cast_info + + if cast_info.model_name == "Google Cast Group": + return None + + return { + 'name': cast_info.friendly_name, + 'identifiers': { + (CAST_DOMAIN, cast_info.uuid.replace('-', '')) + }, + 'model': cast_info.model_name, + 'manufacturer': cast_info.manufacturer, + } + + @property + def state(self): + """Return the state of the player.""" + if self.media_status is None: + return None + if self.media_status.player_is_playing: + return STATE_PLAYING + if self.media_status.player_is_paused: + return STATE_PAUSED + if self.media_status.player_is_idle: + return STATE_IDLE + if self._chromecast is not None and self._chromecast.is_idle: + return STATE_OFF + return None + + @property + def available(self): + """Return True if the cast device is connected.""" + return self._available + + @property + def volume_level(self): + """Volume level of the media player (0..1).""" + return self.cast_status.volume_level if self.cast_status else None + + @property + def is_volume_muted(self): + """Boolean if volume is currently muted.""" + return self.cast_status.volume_muted if self.cast_status else None + + @property + def media_content_id(self): + """Content ID of current playing media.""" + return self.media_status.content_id if self.media_status else None + + @property + def media_content_type(self): + """Content type of current playing media.""" + if self.media_status is None: + return None + if self.media_status.media_is_tvshow: + return MEDIA_TYPE_TVSHOW + if self.media_status.media_is_movie: + return MEDIA_TYPE_MOVIE + if self.media_status.media_is_musictrack: + return MEDIA_TYPE_MUSIC + return None + + @property + def media_duration(self): + """Duration of current playing media in seconds.""" + return self.media_status.duration if self.media_status else None + + @property + def media_image_url(self): + """Image url of current playing media.""" + if self.media_status is None: + return None + + images = self.media_status.images + + return images[0].url if images and images[0].url else None + + @property + def media_title(self): + """Title of current playing media.""" + return self.media_status.title if self.media_status else None + + @property + def media_artist(self): + """Artist of current playing media (Music track only).""" + return self.media_status.artist if self.media_status else None + + @property + def media_album_name(self): + """Album of current playing media (Music track only).""" + return self.media_status.album_name if self.media_status else None + + @property + def media_album_artist(self): + """Album artist of current playing media (Music track only).""" + return self.media_status.album_artist if self.media_status else None + + @property + def media_track(self): + """Track number of current playing media (Music track only).""" + return self.media_status.track if self.media_status else None + + @property + def media_series_title(self): + """Return the title of the series of current playing media.""" + return self.media_status.series_title if self.media_status else None + + @property + def media_season(self): + """Season of current playing media (TV Show only).""" + return self.media_status.season if self.media_status else None + + @property + def media_episode(self): + """Episode of current playing media (TV Show only).""" + return self.media_status.episode if self.media_status else None + + @property + def app_id(self): + """Return the ID of the current running app.""" + return self._chromecast.app_id if self._chromecast else None + + @property + def app_name(self): + """Name of the current running app.""" + return self._chromecast.app_display_name if self._chromecast else None + + @property + def supported_features(self): + """Flag media player features that are supported.""" + return SUPPORT_CAST + + @property + def media_position(self): + """Position of current playing media in seconds.""" + if self.media_status is None or \ + not (self.media_status.player_is_playing or + self.media_status.player_is_paused or + self.media_status.player_is_idle): + return None + return self.media_status.current_time + + @property + def media_position_updated_at(self): + """When was the position of the current playing media valid. + + Returns value from homeassistant.util.dt.utcnow(). + """ + return self.media_status_received + + @property + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return self._cast_info.uuid diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 6d7f9432e39a2..e1d3093995c4c 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -14,32 +14,54 @@ 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_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 0000000000000..2f84ee27bbdcc --- /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 0000000000000..32c77b93eeabf --- /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/daikin.py b/homeassistant/components/climate/daikin.py deleted file mode 100644 index 45589c128644e..0000000000000 --- a/homeassistant/components/climate/daikin.py +++ /dev/null @@ -1,272 +0,0 @@ -""" -Support for the Daikin HVAC. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/climate.daikin/ -""" -import logging -import re - -import voluptuous as vol - -from homeassistant.components.climate import ( - ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, ATTR_OPERATION_MODE, - ATTR_SWING_MODE, PLATFORM_SCHEMA, STATE_AUTO, STATE_COOL, STATE_DRY, - STATE_FAN_ONLY, STATE_HEAT, STATE_OFF, SUPPORT_FAN_MODE, - SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, - ClimateDevice) -from homeassistant.components.daikin import DOMAIN as DAIKIN_DOMAIN -from homeassistant.components.daikin.const import ( - ATTR_INSIDE_TEMPERATURE, ATTR_OUTSIDE_TEMPERATURE, ATTR_TARGET_TEMPERATURE) -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({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME): cv.string, -}) - -HA_STATE_TO_DAIKIN = { - STATE_FAN_ONLY: 'fan', - STATE_DRY: 'dry', - STATE_COOL: 'cool', - STATE_HEAT: 'hot', - STATE_AUTO: 'auto', - STATE_OFF: 'off', -} - -DAIKIN_TO_HA_STATE = { - 'fan': STATE_FAN_ONLY, - 'dry': STATE_DRY, - 'cool': STATE_COOL, - 'hot': STATE_HEAT, - 'auto': STATE_AUTO, - 'off': STATE_OFF, -} - -HA_ATTR_TO_DAIKIN = { - ATTR_OPERATION_MODE: 'mode', - ATTR_FAN_MODE: 'f_rate', - ATTR_SWING_MODE: 'f_dir', - ATTR_INSIDE_TEMPERATURE: 'htemp', - ATTR_OUTSIDE_TEMPERATURE: 'otemp', - ATTR_TARGET_TEMPERATURE: 'stemp' -} - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Old way of setting up the Daikin HVAC platform. - - Can only be called when a user accidentally mentions the platform in their - config. But even in that case it would have been ignored. - """ - pass - - -async def async_setup_entry(hass, entry, async_add_entities): - """Set up Daikin climate based on config_entry.""" - daikin_api = hass.data[DAIKIN_DOMAIN].get(entry.entry_id) - async_add_entities([DaikinClimate(daikin_api)]) - - -class DaikinClimate(ClimateDevice): - """Representation of a Daikin HVAC.""" - - def __init__(self, api): - """Initialize the climate device.""" - from pydaikin import appliance - - self._api = api - self._list = { - ATTR_OPERATION_MODE: list(HA_STATE_TO_DAIKIN), - ATTR_FAN_MODE: list( - map( - str.title, - appliance.daikin_values(HA_ATTR_TO_DAIKIN[ATTR_FAN_MODE]) - ) - ), - ATTR_SWING_MODE: list( - map( - str.title, - appliance.daikin_values(HA_ATTR_TO_DAIKIN[ATTR_SWING_MODE]) - ) - ), - } - - self._supported_features = SUPPORT_TARGET_TEMPERATURE \ - | SUPPORT_OPERATION_MODE - - if self._api.device.support_fan_mode: - self._supported_features |= SUPPORT_FAN_MODE - - if self._api.device.support_swing_mode: - self._supported_features |= SUPPORT_SWING_MODE - - def get(self, key): - """Retrieve device settings from API library cache.""" - value = None - cast_to_float = False - - if key in [ATTR_TEMPERATURE, ATTR_INSIDE_TEMPERATURE, - ATTR_CURRENT_TEMPERATURE]: - key = ATTR_INSIDE_TEMPERATURE - - daikin_attr = HA_ATTR_TO_DAIKIN.get(key) - - if key == ATTR_INSIDE_TEMPERATURE: - value = self._api.device.values.get(daikin_attr) - cast_to_float = True - elif key == ATTR_TARGET_TEMPERATURE: - value = self._api.device.values.get(daikin_attr) - cast_to_float = True - elif key == ATTR_OUTSIDE_TEMPERATURE: - value = self._api.device.values.get(daikin_attr) - cast_to_float = True - elif key == ATTR_FAN_MODE: - value = self._api.device.represent(daikin_attr)[1].title() - elif key == ATTR_SWING_MODE: - value = self._api.device.represent(daikin_attr)[1].title() - elif key == ATTR_OPERATION_MODE: - # Daikin can return also internal states auto-1 or auto-7 - # and we need to translate them as AUTO - daikin_mode = re.sub( - '[^a-z]', '', - self._api.device.represent(daikin_attr)[1]) - ha_mode = DAIKIN_TO_HA_STATE.get(daikin_mode) - value = ha_mode - - if value is None: - _LOGGER.error("Invalid value requested for key %s", key) - else: - if value in ("-", "--"): - value = None - elif cast_to_float: - try: - value = float(value) - except ValueError: - value = None - - return value - - def set(self, settings): - """Set device settings using API.""" - values = {} - - for attr in [ATTR_TEMPERATURE, ATTR_FAN_MODE, ATTR_SWING_MODE, - ATTR_OPERATION_MODE]: - value = settings.get(attr) - if value is None: - continue - - daikin_attr = HA_ATTR_TO_DAIKIN.get(attr) - if daikin_attr is not None: - if attr == ATTR_OPERATION_MODE: - values[daikin_attr] = HA_STATE_TO_DAIKIN[value] - elif value in self._list[attr]: - values[daikin_attr] = value.lower() - else: - _LOGGER.error("Invalid value %s for %s", attr, value) - - # temperature - elif attr == ATTR_TEMPERATURE: - try: - values['stemp'] = str(int(value)) - except ValueError: - _LOGGER.error("Invalid temperature %s", value) - - if values: - self._api.device.set(values) - - @property - def supported_features(self): - """Return the list of supported features.""" - return self._supported_features - - @property - def name(self): - """Return the name of the thermostat, if any.""" - return self._api.name - - @property - def unique_id(self): - """Return a unique ID.""" - return self._api.mac - - @property - def temperature_unit(self): - """Return the unit of measurement which this thermostat uses.""" - return TEMP_CELSIUS - - @property - def current_temperature(self): - """Return the current temperature.""" - return self.get(ATTR_CURRENT_TEMPERATURE) - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self.get(ATTR_TARGET_TEMPERATURE) - - @property - def target_temperature_step(self): - """Return the supported step of target temperature.""" - return 1 - - def set_temperature(self, **kwargs): - """Set new target temperature.""" - self.set(kwargs) - - @property - def current_operation(self): - """Return current operation ie. heat, cool, idle.""" - return self.get(ATTR_OPERATION_MODE) - - @property - def operation_list(self): - """Return the list of available operation modes.""" - return self._list.get(ATTR_OPERATION_MODE) - - def set_operation_mode(self, operation_mode): - """Set HVAC mode.""" - self.set({ATTR_OPERATION_MODE: operation_mode}) - - @property - def current_fan_mode(self): - """Return the fan setting.""" - return self.get(ATTR_FAN_MODE) - - def set_fan_mode(self, fan_mode): - """Set fan mode.""" - self.set({ATTR_FAN_MODE: fan_mode}) - - @property - def fan_list(self): - """List of available fan modes.""" - return self._list.get(ATTR_FAN_MODE) - - @property - def current_swing_mode(self): - """Return the fan setting.""" - return self.get(ATTR_SWING_MODE) - - def set_swing_mode(self, swing_mode): - """Set new target temperature.""" - self.set({ATTR_SWING_MODE: swing_mode}) - - @property - def swing_list(self): - """List of available swing modes.""" - return self._list.get(ATTR_SWING_MODE) - - def update(self): - """Retrieve latest state.""" - self._api.update() - - @property - def device_info(self): - """Return a device description for device registry.""" - return self._api.device_info diff --git a/homeassistant/components/climate/demo.py b/homeassistant/components/climate/demo.py index bc0b9bd52ee5b..14c22cefbe902 100644 --- a/homeassistant/components/climate/demo.py +++ b/homeassistant/components/climate/demo.py @@ -186,22 +186,22 @@ def set_temperature(self, **kwargs): 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/ecobee.py b/homeassistant/components/climate/ecobee.py deleted file mode 100644 index 46fc5c297526b..0000000000000 --- a/homeassistant/components/climate/ecobee.py +++ /dev/null @@ -1,446 +0,0 @@ -""" -Platform for Ecobee Thermostats. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/climate.ecobee/ -""" -import logging - -import voluptuous as vol - -from homeassistant.components import ecobee -from homeassistant.components.climate import ( - DOMAIN, STATE_COOL, STATE_HEAT, STATE_AUTO, STATE_IDLE, ClimateDevice, - ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, SUPPORT_TARGET_TEMPERATURE, - SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE, - SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_TARGET_HUMIDITY_HIGH, - SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_FAN_MODE, - SUPPORT_TARGET_TEMPERATURE_LOW, STATE_OFF) -from homeassistant.const import ( - ATTR_ENTITY_ID, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT) -import homeassistant.helpers.config_validation as cv - -_CONFIGURING = {} -_LOGGER = logging.getLogger(__name__) - -ATTR_FAN_MIN_ON_TIME = 'fan_min_on_time' -ATTR_RESUME_ALL = 'resume_all' - -DEFAULT_RESUME_ALL = False -TEMPERATURE_HOLD = 'temp' -VACATION_HOLD = 'vacation' -AWAY_MODE = 'awayMode' - -DEPENDENCIES = ['ecobee'] - -SERVICE_SET_FAN_MIN_ON_TIME = 'ecobee_set_fan_min_on_time' -SERVICE_RESUME_PROGRAM = 'ecobee_resume_program' - -SET_FAN_MIN_ON_TIME_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_FAN_MIN_ON_TIME): vol.Coerce(int), -}) - -RESUME_PROGRAM_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Optional(ATTR_RESUME_ALL, default=DEFAULT_RESUME_ALL): cv.boolean, -}) - -SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE | - SUPPORT_HOLD_MODE | SUPPORT_OPERATION_MODE | - SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH | - SUPPORT_AUX_HEAT | SUPPORT_TARGET_TEMPERATURE_HIGH | - SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_FAN_MODE) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Ecobee Thermostat Platform.""" - if discovery_info is None: - return - data = ecobee.NETWORK - hold_temp = discovery_info['hold_temp'] - _LOGGER.info( - "Loading ecobee thermostat component with hold_temp set to %s", - hold_temp) - devices = [Thermostat(data, index, hold_temp) - for index in range(len(data.ecobee.thermostats))] - add_entities(devices) - - def fan_min_on_time_set_service(service): - """Set the minimum fan on time on the target thermostats.""" - entity_id = service.data.get(ATTR_ENTITY_ID) - fan_min_on_time = service.data[ATTR_FAN_MIN_ON_TIME] - - if entity_id: - target_thermostats = [device for device in devices - if device.entity_id in entity_id] - else: - target_thermostats = devices - - for thermostat in target_thermostats: - thermostat.set_fan_min_on_time(str(fan_min_on_time)) - - thermostat.schedule_update_ha_state(True) - - def resume_program_set_service(service): - """Resume the program on the target thermostats.""" - entity_id = service.data.get(ATTR_ENTITY_ID) - resume_all = service.data.get(ATTR_RESUME_ALL) - - if entity_id: - target_thermostats = [device for device in devices - if device.entity_id in entity_id] - else: - target_thermostats = devices - - for thermostat in target_thermostats: - thermostat.resume_program(resume_all) - - thermostat.schedule_update_ha_state(True) - - hass.services.register( - DOMAIN, SERVICE_SET_FAN_MIN_ON_TIME, fan_min_on_time_set_service, - schema=SET_FAN_MIN_ON_TIME_SCHEMA) - - hass.services.register( - DOMAIN, SERVICE_RESUME_PROGRAM, resume_program_set_service, - schema=RESUME_PROGRAM_SCHEMA) - - -class Thermostat(ClimateDevice): - """A thermostat class for Ecobee.""" - - def __init__(self, data, thermostat_index, hold_temp): - """Initialize the thermostat.""" - self.data = data - self.thermostat_index = thermostat_index - self.thermostat = self.data.ecobee.get_thermostat( - self.thermostat_index) - self._name = self.thermostat['name'] - self.hold_temp = hold_temp - self.vacation = None - self._climate_list = self.climate_list - self._operation_list = ['auto', 'auxHeatOnly', 'cool', - 'heat', 'off'] - self._fan_list = ['auto', 'on'] - self.update_without_throttle = False - - def update(self): - """Get the latest state from the thermostat.""" - if self.update_without_throttle: - self.data.update(no_throttle=True) - self.update_without_throttle = False - else: - self.data.update() - - self.thermostat = self.data.ecobee.get_thermostat( - self.thermostat_index) - - @property - def supported_features(self): - """Return the list of supported features.""" - return SUPPORT_FLAGS - - @property - def name(self): - """Return the name of the Ecobee Thermostat.""" - return self.thermostat['name'] - - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_FAHRENHEIT - - @property - def current_temperature(self): - """Return the current temperature.""" - return self.thermostat['runtime']['actualTemperature'] / 10.0 - - @property - def target_temperature_low(self): - """Return the lower bound temperature we try to reach.""" - if self.current_operation == STATE_AUTO: - return self.thermostat['runtime']['desiredHeat'] / 10.0 - return None - - @property - def target_temperature_high(self): - """Return the upper bound temperature we try to reach.""" - if self.current_operation == STATE_AUTO: - return self.thermostat['runtime']['desiredCool'] / 10.0 - return None - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - if self.current_operation == STATE_AUTO: - return None - if self.current_operation == STATE_HEAT: - return self.thermostat['runtime']['desiredHeat'] / 10.0 - if self.current_operation == STATE_COOL: - return self.thermostat['runtime']['desiredCool'] / 10.0 - return None - - @property - def fan(self): - """Return the current fan status.""" - if 'fan' in self.thermostat['equipmentStatus']: - return STATE_ON - return STATE_OFF - - @property - def current_fan_mode(self): - """Return the fan setting.""" - return self.thermostat['runtime']['desiredFanMode'] - - @property - def current_hold_mode(self): - """Return current hold mode.""" - mode = self._current_hold_mode - return None if mode == AWAY_MODE else mode - - @property - def fan_list(self): - """Return the available fan modes.""" - return self._fan_list - - @property - def _current_hold_mode(self): - events = self.thermostat['events'] - for event in events: - if event['running']: - if event['type'] == 'hold': - if event['holdClimateRef'] == 'away': - if int(event['endDate'][0:4]) - \ - int(event['startDate'][0:4]) <= 1: - # A temporary hold from away climate is a hold - return 'away' - # A permanent hold from away climate - return AWAY_MODE - if event['holdClimateRef'] != "": - # Any other hold based on climate - return event['holdClimateRef'] - # Any hold not based on a climate is a temp hold - return TEMPERATURE_HOLD - if event['type'].startswith('auto'): - # All auto modes are treated as holds - return event['type'][4:].lower() - if event['type'] == 'vacation': - self.vacation = event['name'] - return VACATION_HOLD - return None - - @property - def current_operation(self): - """Return current operation.""" - if self.operation_mode == 'auxHeatOnly' or \ - self.operation_mode == 'heatPump': - return STATE_HEAT - return self.operation_mode - - @property - def operation_list(self): - """Return the operation modes list.""" - return self._operation_list - - @property - def operation_mode(self): - """Return current operation ie. heat, cool, idle.""" - return self.thermostat['settings']['hvacMode'] - - @property - def mode(self): - """Return current mode, as the user-visible name.""" - cur = self.thermostat['program']['currentClimateRef'] - climates = self.thermostat['program']['climates'] - current = list(filter(lambda x: x['climateRef'] == cur, climates)) - return current[0]['name'] - - @property - def fan_min_on_time(self): - """Return current fan minimum on time.""" - return self.thermostat['settings']['fanMinOnTime'] - - @property - def device_state_attributes(self): - """Return device specific state attributes.""" - # Move these to Thermostat Device and make them global - status = self.thermostat['equipmentStatus'] - operation = None - if status == '': - operation = STATE_IDLE - elif 'Cool' in status: - operation = STATE_COOL - elif 'auxHeat' in status: - operation = STATE_HEAT - elif 'heatPump' in status: - operation = STATE_HEAT - else: - operation = status - - return { - "actual_humidity": self.thermostat['runtime']['actualHumidity'], - "fan": self.fan, - "climate_mode": self.mode, - "operation": operation, - "climate_list": self.climate_list, - "fan_min_on_time": self.fan_min_on_time - } - - @property - def is_away_mode_on(self): - """Return true if away mode is on.""" - return self._current_hold_mode == AWAY_MODE - - @property - def is_aux_heat_on(self): - """Return true if aux heater.""" - return 'auxHeat' in self.thermostat['equipmentStatus'] - - def turn_away_mode_on(self): - """Turn away mode on by setting it on away hold indefinitely.""" - if self._current_hold_mode != AWAY_MODE: - self.data.ecobee.set_climate_hold(self.thermostat_index, 'away', - 'indefinite') - self.update_without_throttle = True - - def turn_away_mode_off(self): - """Turn away off.""" - if self._current_hold_mode == AWAY_MODE: - self.data.ecobee.resume_program(self.thermostat_index) - self.update_without_throttle = True - - def set_hold_mode(self, hold_mode): - """Set hold mode (away, home, temp, sleep, etc.).""" - hold = self.current_hold_mode - - if hold == hold_mode: - # no change, so no action required - return - if hold_mode == 'None' or hold_mode is None: - if hold == VACATION_HOLD: - self.data.ecobee.delete_vacation( - self.thermostat_index, self.vacation) - else: - self.data.ecobee.resume_program(self.thermostat_index) - else: - if hold_mode == TEMPERATURE_HOLD: - self.set_temp_hold(self.current_temperature) - else: - self.data.ecobee.set_climate_hold( - self.thermostat_index, hold_mode, self.hold_preference()) - self.update_without_throttle = True - - def set_auto_temp_hold(self, heat_temp, cool_temp): - """Set temperature hold in auto mode.""" - if cool_temp is not None: - cool_temp_setpoint = cool_temp - else: - cool_temp_setpoint = ( - self.thermostat['runtime']['desiredCool'] / 10.0) - - if heat_temp is not None: - heat_temp_setpoint = heat_temp - else: - heat_temp_setpoint = ( - self.thermostat['runtime']['desiredCool'] / 10.0) - - self.data.ecobee.set_hold_temp(self.thermostat_index, - cool_temp_setpoint, heat_temp_setpoint, - self.hold_preference()) - _LOGGER.debug("Setting ecobee hold_temp to: heat=%s, is=%s, " - "cool=%s, is=%s", heat_temp, - isinstance(heat_temp, (int, float)), cool_temp, - isinstance(cool_temp, (int, float))) - - self.update_without_throttle = True - - def set_fan_mode(self, fan_mode): - """Set the fan mode. Valid values are "on" or "auto".""" - if (fan_mode.lower() != STATE_ON) and (fan_mode.lower() != STATE_AUTO): - error = "Invalid fan_mode value: Valid values are 'on' or 'auto'" - _LOGGER.error(error) - return - - cool_temp = self.thermostat['runtime']['desiredCool'] / 10.0 - heat_temp = self.thermostat['runtime']['desiredHeat'] / 10.0 - self.data.ecobee.set_fan_mode(self.thermostat_index, fan_mode, - cool_temp, heat_temp, - self.hold_preference()) - - _LOGGER.info("Setting fan mode to: %s", fan_mode) - - def set_temp_hold(self, temp): - """Set temperature hold in modes other than auto. - - Ecobee API: It is good practice to set the heat and cool hold - temperatures to be the same, if the thermostat is in either heat, cool, - auxHeatOnly, or off mode. If the thermostat is in auto mode, an - additional rule is required. The cool hold temperature must be greater - than the heat hold temperature by at least the amount in the - heatCoolMinDelta property. - https://www.ecobee.com/home/developer/api/examples/ex5.shtml - """ - if self.current_operation == STATE_HEAT or self.current_operation == \ - STATE_COOL: - heat_temp = temp - cool_temp = temp - else: - delta = self.thermostat['settings']['heatCoolMinDelta'] / 10 - heat_temp = temp - delta - cool_temp = temp + delta - self.set_auto_temp_hold(heat_temp, cool_temp) - - def set_temperature(self, **kwargs): - """Set new target temperature.""" - low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW) - high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) - temp = kwargs.get(ATTR_TEMPERATURE) - - if self.current_operation == STATE_AUTO and \ - (low_temp is not None or high_temp is not None): - self.set_auto_temp_hold(low_temp, high_temp) - elif temp is not None: - self.set_temp_hold(temp) - else: - _LOGGER.error( - "Missing valid arguments for set_temperature in %s", kwargs) - - def set_humidity(self, humidity): - """Set the humidity level.""" - self.data.ecobee.set_humidity(self.thermostat_index, humidity) - - def set_operation_mode(self, operation_mode): - """Set HVAC mode (auto, auxHeatOnly, cool, heat, off).""" - self.data.ecobee.set_hvac_mode(self.thermostat_index, operation_mode) - self.update_without_throttle = True - - def set_fan_min_on_time(self, fan_min_on_time): - """Set the minimum fan on time.""" - self.data.ecobee.set_fan_min_on_time( - self.thermostat_index, fan_min_on_time) - self.update_without_throttle = True - - def resume_program(self, resume_all): - """Resume the thermostat schedule program.""" - self.data.ecobee.resume_program( - self.thermostat_index, 'true' if resume_all else 'false') - self.update_without_throttle = True - - def hold_preference(self): - """Return user preference setting for hold time.""" - # Values returned from thermostat are 'useEndTime4hour', - # 'useEndTime2hour', 'nextTransition', 'indefinite', 'askMe' - default = self.thermostat['settings']['holdAction'] - if default == 'nextTransition': - return default - # add further conditions if other hold durations should be - # supported; note that this should not include 'indefinite' - # as an indefinite away hold is interpreted as away_mode - return 'nextTransition' - - @property - def climate_list(self): - """Return the list of climates currently available.""" - climates = self.thermostat['program']['climates'] - return list(map((lambda x: x['name']), climates)) diff --git a/homeassistant/components/climate/elkm1.py b/homeassistant/components/climate/elkm1.py deleted file mode 100644 index 6bd33b382dc9c..0000000000000 --- a/homeassistant/components/climate/elkm1.py +++ /dev/null @@ -1,193 +0,0 @@ -""" -Support for control of Elk-M1 connected thermostats. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/climate.elkm1/ -""" -from homeassistant.components.climate import ( - ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, PRECISION_WHOLE, STATE_AUTO, - STATE_COOL, STATE_FAN_ONLY, STATE_HEAT, STATE_IDLE, SUPPORT_AUX_HEAT, - SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE_HIGH, - SUPPORT_TARGET_TEMPERATURE_LOW, ClimateDevice) -from homeassistant.components.elkm1 import ( - DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities) -from homeassistant.const import STATE_ON - -DEPENDENCIES = [ELK_DOMAIN] - - -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Create the Elk-M1 thermostat platform.""" - if discovery_info is None: - return - - elk = hass.data[ELK_DOMAIN]['elk'] - async_add_entities(create_elk_entities( - hass, elk.thermostats, 'thermostat', ElkThermostat, []), True) - - -class ElkThermostat(ElkEntity, ClimateDevice): - """Representation of an Elk-M1 Thermostat.""" - - def __init__(self, element, elk, elk_data): - """Initialize climate entity.""" - super().__init__(element, elk, elk_data) - self._state = None - - @property - def supported_features(self): - """Return the list of supported features.""" - return (SUPPORT_OPERATION_MODE | SUPPORT_FAN_MODE | SUPPORT_AUX_HEAT - | SUPPORT_TARGET_TEMPERATURE_HIGH - | SUPPORT_TARGET_TEMPERATURE_LOW) - - @property - def temperature_unit(self): - """Return the temperature unit.""" - return self._temperature_unit - - @property - def current_temperature(self): - """Return the current temperature.""" - return self._element.current_temp - - @property - def target_temperature(self): - """Return the temperature we are trying to reach.""" - from elkm1_lib.const import ThermostatMode - if (self._element.mode == ThermostatMode.HEAT.value) or ( - self._element.mode == ThermostatMode.EMERGENCY_HEAT.value): - return self._element.heat_setpoint - if self._element.mode == ThermostatMode.COOL.value: - return self._element.cool_setpoint - return None - - @property - def target_temperature_high(self): - """Return the high target temperature.""" - return self._element.cool_setpoint - - @property - def target_temperature_low(self): - """Return the low target temperature.""" - return self._element.heat_setpoint - - @property - def target_temperature_step(self): - """Return the supported step of target temperature.""" - return 1 - - @property - def current_humidity(self): - """Return the current humidity.""" - return self._element.humidity - - @property - def current_operation(self): - """Return current operation ie. heat, cool, idle.""" - return self._state - - @property - def operation_list(self): - """Return the list of available operation modes.""" - return [STATE_IDLE, STATE_HEAT, STATE_COOL, STATE_AUTO, STATE_FAN_ONLY] - - @property - def precision(self): - """Return the precision of the system.""" - return PRECISION_WHOLE - - @property - def is_aux_heat_on(self): - """Return if aux heater is on.""" - from elkm1_lib.const import ThermostatMode - return self._element.mode == ThermostatMode.EMERGENCY_HEAT.value - - @property - def min_temp(self): - """Return the minimum temperature supported.""" - return 1 - - @property - def max_temp(self): - """Return the maximum temperature supported.""" - return 99 - - @property - def current_fan_mode(self): - """Return the fan setting.""" - from elkm1_lib.const import ThermostatFan - if self._element.fan == ThermostatFan.AUTO.value: - return STATE_AUTO - if self._element.fan == ThermostatFan.ON.value: - return STATE_ON - return None - - def _elk_set(self, mode, fan): - from elkm1_lib.const import ThermostatSetting - if mode is not None: - self._element.set(ThermostatSetting.MODE.value, mode) - if fan is not None: - self._element.set(ThermostatSetting.FAN.value, fan) - - async def async_set_operation_mode(self, operation_mode): - """Set thermostat operation mode.""" - from elkm1_lib.const import ThermostatFan, ThermostatMode - settings = { - STATE_IDLE: (ThermostatMode.OFF.value, ThermostatFan.AUTO.value), - STATE_HEAT: (ThermostatMode.HEAT.value, None), - STATE_COOL: (ThermostatMode.COOL.value, None), - STATE_AUTO: (ThermostatMode.AUTO.value, None), - STATE_FAN_ONLY: (ThermostatMode.OFF.value, ThermostatFan.ON.value) - } - self._elk_set(settings[operation_mode][0], settings[operation_mode][1]) - - async def async_turn_aux_heat_on(self): - """Turn auxiliary heater on.""" - from elkm1_lib.const import ThermostatMode - self._elk_set(ThermostatMode.EMERGENCY_HEAT.value, None) - - async def async_turn_aux_heat_off(self): - """Turn auxiliary heater off.""" - from elkm1_lib.const import ThermostatMode - self._elk_set(ThermostatMode.HEAT.value, None) - - @property - def fan_list(self): - """Return the list of available fan modes.""" - return [STATE_AUTO, STATE_ON] - - async def async_set_fan_mode(self, fan_mode): - """Set new target fan mode.""" - from elkm1_lib.const import ThermostatFan - if fan_mode == STATE_AUTO: - self._elk_set(None, ThermostatFan.AUTO.value) - elif fan_mode == STATE_ON: - self._elk_set(None, ThermostatFan.ON.value) - - async def async_set_temperature(self, **kwargs): - """Set new target temperature.""" - from elkm1_lib.const import ThermostatSetting - low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW) - high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) - if low_temp is not None: - self._element.set( - ThermostatSetting.HEAT_SETPOINT.value, round(low_temp)) - if high_temp is not None: - self._element.set( - ThermostatSetting.COOL_SETPOINT.value, round(high_temp)) - - def _element_changed(self, element, changeset): - from elkm1_lib.const import ThermostatFan, ThermostatMode - mode_to_state = { - ThermostatMode.OFF.value: STATE_IDLE, - ThermostatMode.COOL.value: STATE_COOL, - ThermostatMode.HEAT.value: STATE_HEAT, - ThermostatMode.EMERGENCY_HEAT.value: STATE_HEAT, - ThermostatMode.AUTO.value: STATE_AUTO, - } - self._state = mode_to_state.get(self._element.mode) - if self._state == STATE_IDLE and \ - self._element.fan == ThermostatFan.ON.value: - self._state = STATE_FAN_ONLY diff --git a/homeassistant/components/climate/evohome.py b/homeassistant/components/climate/evohome.py deleted file mode 100644 index fd58e6c01e868..0000000000000 --- a/homeassistant/components/climate/evohome.py +++ /dev/null @@ -1,580 +0,0 @@ -"""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/ -""" - -from datetime import datetime, timedelta -import logging - -from requests.exceptions import HTTPError - -from homeassistant.components.climate import ( - STATE_AUTO, STATE_ECO, STATE_MANUAL, STATE_OFF, - SUPPORT_AWAY_MODE, - SUPPORT_ON_OFF, - SUPPORT_OPERATION_MODE, - SUPPORT_TARGET_TEMPERATURE, - ClimateDevice -) -from homeassistant.components.evohome import ( - DATA_EVOHOME, DISPATCHER_EVOHOME, - CONF_LOCATION_IDX, SCAN_INTERVAL_DEFAULT, - EVO_PARENT, EVO_CHILD, - GWS, TCS, -) -from homeassistant.const import ( - CONF_SCAN_INTERVAL, - HTTP_TOO_MANY_REQUESTS, - PRECISION_HALVES, - TEMP_CELSIUS -) -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import ( - dispatcher_send, - async_dispatcher_connect -) - -_LOGGER = logging.getLogger(__name__) - -# the Controller's opmode/state and the zone's (inherited) state -EVO_RESET = 'AutoWithReset' -EVO_AUTO = 'Auto' -EVO_AUTOECO = 'AutoWithEco' -EVO_AWAY = 'Away' -EVO_DAYOFF = 'DayOff' -EVO_CUSTOM = 'Custom' -EVO_HEATOFF = 'HeatingOff' - -# 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, -# where HA considers it to 'override' the exising operating mode -TCS_STATE_TO_HA = { - EVO_RESET: STATE_AUTO, - EVO_AUTO: STATE_AUTO, - EVO_AUTOECO: STATE_ECO, - EVO_AWAY: STATE_AUTO, - EVO_DAYOFF: STATE_AUTO, - EVO_CUSTOM: STATE_AUTO, - EVO_HEATOFF: STATE_OFF -} -HA_STATE_TO_TCS = { - STATE_AUTO: EVO_AUTO, - STATE_ECO: EVO_AUTOECO, - STATE_OFF: EVO_HEATOFF -} -TCS_OP_LIST = list(HA_STATE_TO_TCS) - -# the Zones' opmode; their state is usually 'inherited' from the TCS -EVO_FOLLOW = 'FollowSchedule' -EVO_TEMPOVER = 'TemporaryOverride' -EVO_PERMOVER = 'PermanentOverride' - -# for the Zones... -ZONE_STATE_TO_HA = { - EVO_FOLLOW: STATE_AUTO, - EVO_TEMPOVER: STATE_MANUAL, - EVO_PERMOVER: STATE_MANUAL -} -HA_STATE_TO_ZONE = { - STATE_AUTO: EVO_FOLLOW, - STATE_MANUAL: EVO_PERMOVER -} -ZONE_OP_LIST = list(HA_STATE_TO_ZONE) - - -async def async_setup_platform(hass, hass_config, async_add_entities, - discovery_info=None): - """Create the evohome Controller, and its Zones, if any.""" - evo_data = hass.data[DATA_EVOHOME] - - client = evo_data['client'] - loc_idx = evo_data['params'][CONF_LOCATION_IDX] - - # 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 - - _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 - ) - - controller = EvoController(evo_data, client, tcs_obj_ref) - zones = [] - - 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 - ) - zones.append(EvoZone(evo_data, client, zone_obj_ref)) - - entities = [controller] + zones - - async_add_entities(entities, update_before_add=False) - - -class EvoClimateDevice(ClimateDevice): - """Base for a Honeywell evohome Climate device.""" - - # pylint: disable=no-member - - def __init__(self, evo_data, client, obj_ref): - """Initialize the evohome entity.""" - self._client = client - self._obj = obj_ref - - self._params = evo_data['params'] - self._timers = evo_data['timers'] - self._status = {} - - self._available = False # should become True after first update() - - async def async_added_to_hass(self): - """Run when entity about to be added.""" - async_dispatcher_connect(self.hass, DISPATCHER_EVOHOME, self._connect) - - @callback - def _connect(self, packet): - if packet['to'] & self._type and packet['signal'] == 'refresh': - self.async_schedule_update_ha_state(force_refresh=True) - - def _handle_requests_exceptions(self, err): - if err.response.status_code == HTTP_TOO_MANY_REQUESTS: - # execute a backoff: pause, and also reduce rate - old_interval = self._params[CONF_SCAN_INTERVAL] - new_interval = min(old_interval, SCAN_INTERVAL_DEFAULT) * 2 - self._params[CONF_SCAN_INTERVAL] = new_interval - - _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, - ) - - self._timers['statusUpdated'] = datetime.now() + new_interval * 3 - - else: - raise err # we dont handle any other HTTPErrors - - @property - def name(self) -> str: - """Return the name to use in the frontend UI.""" - return self._name - - @property - def icon(self): - """Return the icon to use in the frontend UI.""" - return self._icon - - @property - def device_state_attributes(self): - """Return the device state attributes of the evohome Climate device. - - This is state data that is not available otherwise, due to the - restrictions placed upon ClimateDevice properties, etc. by HA. - """ - return {'status': self._status} - - @property - def available(self) -> bool: - """Return True if the device is currently available.""" - return self._available - - @property - def supported_features(self): - """Get the list of supported features of the device.""" - return self._supported_features - - @property - def operation_list(self): - """Return the list of available operations.""" - return self._operation_list - - @property - def temperature_unit(self): - """Return the temperature unit to use in the frontend UI.""" - return TEMP_CELSIUS - - @property - def precision(self): - """Return the temperature precision to use in the frontend UI.""" - return PRECISION_HALVES - - -class EvoZone(EvoClimateDevice): - """Base for a Honeywell evohome Zone device.""" - - def __init__(self, evo_data, client, obj_ref): - """Initialize the evohome Zone.""" - super().__init__(evo_data, client, obj_ref) - - self._id = obj_ref.zoneId - self._name = obj_ref.name - self._icon = "mdi:radiator" - self._type = EVO_CHILD - - for _zone in evo_data['config'][GWS][0][TCS][0]['zones']: - if _zone['zoneId'] == self._id: - self._config = _zone - break - self._status = {} - - self._operation_list = ZONE_OP_LIST - self._supported_features = \ - SUPPORT_OPERATION_MODE | \ - SUPPORT_TARGET_TEMPERATURE | \ - SUPPORT_ON_OFF - - @property - def min_temp(self): - """Return the minimum target temperature of a evohome Zone. - - The default is 5 (in Celsius), but it is configurable within 5-35. - """ - return self._config['setpointCapabilities']['minHeatSetpoint'] - - @property - def max_temp(self): - """Return the minimum target temperature of a evohome Zone. - - The default is 35 (in Celsius), but it is configurable within 5-35. - """ - return self._config['setpointCapabilities']['maxHeatSetpoint'] - - @property - def target_temperature(self): - """Return the target temperature of the evohome Zone.""" - return self._status['setpointStatus']['targetHeatTemperature'] - - @property - def current_temperature(self): - """Return the current temperature of the evohome Zone.""" - return self._status['temperatureStatus']['temperature'] - - @property - def current_operation(self): - """Return the current operating mode of the evohome Zone. - - The evohome Zones that are in 'FollowSchedule' mode inherit their - actual operating mode from the Controller. - """ - evo_data = self.hass.data[DATA_EVOHOME] - - system_mode = evo_data['status']['systemModeStatus']['mode'] - setpoint_mode = self._status['setpointStatus']['setpointMode'] - - if setpoint_mode == EVO_FOLLOW: - # then inherit state from the controller - if system_mode == EVO_RESET: - current_operation = TCS_STATE_TO_HA.get(EVO_AUTO) - else: - current_operation = TCS_STATE_TO_HA.get(system_mode) - else: - current_operation = ZONE_STATE_TO_HA.get(setpoint_mode) - - return current_operation - - @property - def is_on(self) -> bool: - """Return True if the evohome Zone is off. - - A Zone is considered off if its target temp is set to its minimum, and - it is not following its schedule (i.e. not in 'FollowSchedule' mode). - """ - is_off = \ - self.target_temperature == self.min_temp and \ - self._status['setpointStatus']['setpointMode'] == EVO_PERMOVER - return not is_off - - def _set_temperature(self, temperature, until=None): - """Set the new target temperature of a Zone. - - temperature is required, until can be: - - strftime('%Y-%m-%dT%H:%M:%SZ') for TemporaryOverride, or - - None for PermanentOverride (i.e. indefinitely) - """ - try: - self._obj.set_temperature(temperature, until) - except HTTPError as err: - self._handle_exception("HTTPError", str(err)) # noqa: E501; pylint: disable=no-member - - def set_temperature(self, **kwargs): - """Set new target temperature, indefinitely.""" - self._set_temperature(kwargs['temperature'], until=None) - - def turn_on(self): - """Turn the evohome Zone on. - - This is achieved by setting the Zone to its 'FollowSchedule' mode. - """ - self._set_operation_mode(EVO_FOLLOW) - - def turn_off(self): - """Turn the evohome Zone off. - - This is achieved by setting the Zone to its minimum temperature, - indefinitely (i.e. 'PermanentOverride' mode). - """ - self._set_temperature(self.min_temp, until=None) - - def set_operation_mode(self, operation_mode): - """Set an operating mode for a Zone. - - Currently limited to 'Auto' & 'Manual'. If 'Off' is needed, it can be - enabled via turn_off method. - - NB: evohome Zones do not have an operating mode as understood by HA. - Instead they usually 'inherit' an operating mode from their controller. - - More correctly, these Zones are in a follow mode, 'FollowSchedule', - where their setpoint temperatures are a function of their schedule, and - the Controller's operating_mode, e.g. Economy mode is their scheduled - setpoint less (usually) 3C. - - Thus, you cannot set a Zone to Away mode, but the location (i.e. the - Controller) is set to Away and each Zones's setpoints are adjusted - accordingly to some lower temperature. - - However, Zones can override these setpoints, either for a specified - period of time, 'TemporaryOverride', after which they will revert back - to 'FollowSchedule' mode, or indefinitely, 'PermanentOverride'. - """ - self._set_operation_mode(HA_STATE_TO_ZONE.get(operation_mode)) - - def _set_operation_mode(self, operation_mode): - if operation_mode == EVO_FOLLOW: - try: - self._obj.cancel_temp_override(self._obj) - except HTTPError as err: - self._handle_exception("HTTPError", str(err)) # noqa: E501; pylint: disable=no-member - - elif operation_mode == EVO_TEMPOVER: - _LOGGER.error( - "_set_operation_mode(op_mode=%s): mode not yet implemented", - operation_mode - ) - - elif operation_mode == EVO_PERMOVER: - self._set_temperature(self.target_temperature, until=None) - - else: - _LOGGER.error( - "_set_operation_mode(op_mode=%s): mode not valid", - operation_mode - ) - - @property - def should_poll(self) -> bool: - """Return False as evohome child devices should never be polled. - - The evohome Controller will inform its children when to update(). - """ - return False - - def update(self): - """Process the evohome Zone's state data.""" - evo_data = self.hass.data[DATA_EVOHOME] - - for _zone in evo_data['status']['zones']: - if _zone['zoneId'] == self._id: - self._status = _zone - break - - self._available = True - - -class EvoController(EvoClimateDevice): - """Base for a Honeywell evohome hub/Controller device. - - The Controller (aka TCS, temperature control system) is the parent of all - the child (CH/DHW) devices. It is also a Climate device. - """ - - def __init__(self, evo_data, client, obj_ref): - """Initialize the evohome Controller (hub).""" - super().__init__(evo_data, client, obj_ref) - - self._id = obj_ref.systemId - self._name = '_{}'.format(obj_ref.location.name) - self._icon = "mdi:thermostat" - self._type = EVO_PARENT - - self._config = evo_data['config'][GWS][0][TCS][0] - self._status = evo_data['status'] - self._timers['statusUpdated'] = datetime.min - - self._operation_list = TCS_OP_LIST - self._supported_features = \ - SUPPORT_OPERATION_MODE | \ - SUPPORT_AWAY_MODE - - @property - def device_state_attributes(self): - """Return the device state attributes of the evohome Controller. - - This is state data that is not available otherwise, due to the - restrictions placed upon ClimateDevice properties, etc. by HA. - """ - status = dict(self._status) - - if 'zones' in status: - del status['zones'] - if 'dhw' in status: - del status['dhw'] - - return {'status': status} - - @property - def current_operation(self): - """Return the current operating mode of the evohome Controller.""" - return TCS_STATE_TO_HA.get(self._status['systemModeStatus']['mode']) - - @property - def min_temp(self): - """Return the minimum target temperature of a evohome Controller. - - Although evohome Controllers do not have a minimum target temp, one is - expected by the HA schema; the default for an evohome HR92 is used. - """ - return 5 - - @property - def max_temp(self): - """Return the minimum target temperature of a evohome Controller. - - Although evohome Controllers do not have a maximum target temp, one is - expected by the HA schema; the default for an evohome HR92 is used. - """ - return 35 - - @property - def target_temperature(self): - """Return the average target temperature of the Heating/DHW zones. - - Although evohome Controllers do not have a target temp, one is - expected by the HA schema. - """ - temps = [zone['setpointStatus']['targetHeatTemperature'] - for zone in self._status['zones']] - - avg_temp = round(sum(temps) / len(temps), 1) if temps else None - return avg_temp - - @property - def current_temperature(self): - """Return the average current temperature of the Heating/DHW zones. - - Although evohome Controllers do not have a target temp, one is - expected by the HA schema. - """ - tmp_list = [x for x in self._status['zones'] - if x['temperatureStatus']['isAvailable'] is True] - temps = [zone['temperatureStatus']['temperature'] for zone in tmp_list] - - avg_temp = round(sum(temps) / len(temps), 1) if temps else None - return avg_temp - - @property - def is_on(self) -> bool: - """Return True as evohome Controllers are always on. - - For example, evohome Controllers have a 'HeatingOff' mode, but even - then the DHW would remain on. - """ - return True - - @property - def is_away_mode_on(self) -> bool: - """Return True if away mode is on.""" - return self._status['systemModeStatus']['mode'] == EVO_AWAY - - def turn_away_mode_on(self): - """Turn away mode on. - - The evohome Controller will not remember is previous operating mode. - """ - self._set_operation_mode(EVO_AWAY) - - def turn_away_mode_off(self): - """Turn away mode off. - - The evohome Controller can not recall its previous operating mode (as - intimated by the HA schema), so this method is achieved by setting the - Controller's mode back to Auto. - """ - self._set_operation_mode(EVO_AUTO) - - def _set_operation_mode(self, operation_mode): - try: - self._obj._set_status(operation_mode) # noqa: E501; pylint: disable=protected-access - except HTTPError as err: - self._handle_requests_exceptions(err) - - def set_operation_mode(self, operation_mode): - """Set new target operation mode for the TCS. - - Currently limited to 'Auto', 'AutoWithEco' & 'HeatingOff'. If 'Away' - mode is needed, it can be enabled via turn_away_mode_on method. - """ - self._set_operation_mode(HA_STATE_TO_TCS.get(operation_mode)) - - @property - def should_poll(self) -> bool: - """Return True as the evohome Controller should always be polled.""" - return True - - def update(self): - """Get the latest state data of the entire evohome Location. - - This includes state data for the Controller and all its child devices, - such as the operating mode of the Controller and the current temp of - its children (e.g. Zones, DHW controller). - """ - # should the latest evohome state data be retreived this cycle? - timeout = datetime.now() + timedelta(seconds=55) - expired = timeout > self._timers['statusUpdated'] + \ - self._params[CONF_SCAN_INTERVAL] - - if not expired: - return - - # Retreive the latest state data via the client api - loc_idx = self._params[CONF_LOCATION_IDX] - - try: - self._status.update( - self._client.locations[loc_idx].status()[GWS][0][TCS][0]) - except HTTPError as err: # check if we've exceeded the api rate limit - self._handle_requests_exceptions(err) - else: - self._timers['statusUpdated'] = datetime.now() - self._available = True - - _LOGGER.debug( - "_update_state_data(): self._status = %s", - self._status - ) - - # inform the child devices that state data has been updated - pkt = {'sender': 'controller', 'signal': 'refresh', 'to': EVO_CHILD} - dispatcher_send(self.hass, DISPATCHER_EVOHOME, pkt) diff --git a/homeassistant/components/climate/flexit.py b/homeassistant/components/climate/flexit.py index de74d2facb57b..e0453b8bf90fa 100644 --- a/homeassistant/components/climate/flexit.py +++ b/homeassistant/components/climate/flexit.py @@ -20,13 +20,15 @@ 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 @@ def __init__(self, modbus_slave, name): 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/fritzbox.py b/homeassistant/components/climate/fritzbox.py deleted file mode 100644 index f2d13ee92f6d0..0000000000000 --- a/homeassistant/components/climate/fritzbox.py +++ /dev/null @@ -1,183 +0,0 @@ -""" -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/ -""" -import logging - -import requests - -from homeassistant.components.fritzbox import DOMAIN as FRITZBOX_DOMAIN -from homeassistant.components.fritzbox import ( - ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_BATTERY_LOW, ATTR_STATE_HOLIDAY_MODE, - ATTR_STATE_LOCKED, ATTR_STATE_SUMMER_MODE, - ATTR_STATE_WINDOW_OPEN) -from homeassistant.components.climate import ( - ATTR_OPERATION_MODE, ClimateDevice, STATE_ECO, STATE_HEAT, STATE_MANUAL, - STATE_OFF, STATE_ON, SUPPORT_OPERATION_MODE, - SUPPORT_TARGET_TEMPERATURE) -from homeassistant.const import ( - ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, PRECISION_HALVES, TEMP_CELSIUS) -DEPENDENCIES = ['fritzbox'] - -_LOGGER = logging.getLogger(__name__) - -SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE) - -OPERATION_LIST = [STATE_HEAT, STATE_ECO, STATE_OFF, STATE_ON] - -MIN_TEMPERATURE = 8 -MAX_TEMPERATURE = 28 - -# special temperatures for on/off in Fritz!Box API (modified by pyfritzhome) -ON_API_TEMPERATURE = 127.0 -OFF_API_TEMPERATURE = 126.5 -ON_REPORT_SET_TEMPERATURE = 30.0 -OFF_REPORT_SET_TEMPERATURE = 0.0 - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Fritzbox smarthome thermostat platform.""" - devices = [] - fritz_list = hass.data[FRITZBOX_DOMAIN] - - for fritz in fritz_list: - device_list = fritz.get_devices() - for device in device_list: - if device.has_thermostat: - devices.append(FritzboxThermostat(device, fritz)) - - add_entities(devices) - - -class FritzboxThermostat(ClimateDevice): - """The thermostat class for Fritzbox smarthome thermostates.""" - - def __init__(self, device, fritz): - """Initialize the thermostat.""" - self._device = device - self._fritz = fritz - self._current_temperature = self._device.actual_temperature - self._target_temperature = self._device.target_temperature - self._comfort_temperature = self._device.comfort_temperature - self._eco_temperature = self._device.eco_temperature - - @property - def supported_features(self): - """Return the list of supported features.""" - return SUPPORT_FLAGS - - @property - def available(self): - """Return if thermostat is available.""" - return self._device.present - - @property - def name(self): - """Return the name of the device.""" - return self._device.name - - @property - def temperature_unit(self): - """Return the unit of measurement that is used.""" - return TEMP_CELSIUS - - @property - def precision(self): - """Return precision 0.5.""" - return PRECISION_HALVES - - @property - def current_temperature(self): - """Return the current temperature.""" - return self._current_temperature - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - if self._target_temperature in (ON_API_TEMPERATURE, - OFF_API_TEMPERATURE): - return None - return self._target_temperature - - def set_temperature(self, **kwargs): - """Set new target temperature.""" - if ATTR_OPERATION_MODE in kwargs: - operation_mode = kwargs.get(ATTR_OPERATION_MODE) - self.set_operation_mode(operation_mode) - elif ATTR_TEMPERATURE in kwargs: - temperature = kwargs.get(ATTR_TEMPERATURE) - self._device.set_target_temperature(temperature) - - @property - def current_operation(self): - """Return the current operation mode.""" - if self._target_temperature == ON_API_TEMPERATURE: - return STATE_ON - if self._target_temperature == OFF_API_TEMPERATURE: - return STATE_OFF - if self._target_temperature == self._comfort_temperature: - return STATE_HEAT - if self._target_temperature == self._eco_temperature: - return STATE_ECO - return STATE_MANUAL - - @property - def operation_list(self): - """Return the list of available operation modes.""" - return OPERATION_LIST - - def set_operation_mode(self, operation_mode): - """Set new operation mode.""" - if operation_mode == STATE_HEAT: - self.set_temperature(temperature=self._comfort_temperature) - elif operation_mode == STATE_ECO: - self.set_temperature(temperature=self._eco_temperature) - elif operation_mode == STATE_OFF: - self.set_temperature(temperature=OFF_REPORT_SET_TEMPERATURE) - elif operation_mode == STATE_ON: - self.set_temperature(temperature=ON_REPORT_SET_TEMPERATURE) - - @property - def min_temp(self): - """Return the minimum temperature.""" - return MIN_TEMPERATURE - - @property - def max_temp(self): - """Return the maximum temperature.""" - return MAX_TEMPERATURE - - @property - def device_state_attributes(self): - """Return the device specific state attributes.""" - attrs = { - ATTR_STATE_BATTERY_LOW: self._device.battery_low, - ATTR_STATE_DEVICE_LOCKED: self._device.device_lock, - ATTR_STATE_LOCKED: self._device.lock, - } - - # the following attributes are available since fritzos 7 - if self._device.battery_level is not None: - attrs[ATTR_BATTERY_LEVEL] = self._device.battery_level - if self._device.holiday_active is not None: - attrs[ATTR_STATE_HOLIDAY_MODE] = self._device.holiday_active - if self._device.summer_active is not None: - attrs[ATTR_STATE_SUMMER_MODE] = self._device.summer_active - if ATTR_STATE_WINDOW_OPEN is not None: - attrs[ATTR_STATE_WINDOW_OPEN] = self._device.window_open - - return attrs - - def update(self): - """Update the data from the thermostat.""" - try: - self._device.update() - self._current_temperature = self._device.actual_temperature - self._target_temperature = self._device.target_temperature - self._comfort_temperature = self._device.comfort_temperature - self._eco_temperature = self._device.eco_temperature - except requests.exceptions.HTTPError as ex: - _LOGGER.warning("Fritzbox connection error: %s", ex) - self._fritz.login() diff --git a/homeassistant/components/climate/knx.py b/homeassistant/components/climate/knx.py deleted file mode 100644 index 77d995af2a19f..0000000000000 --- a/homeassistant/components/climate/knx.py +++ /dev/null @@ -1,282 +0,0 @@ -""" -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/ -""" - -import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.climate import ( - PLATFORM_SCHEMA, SUPPORT_ON_OFF, SUPPORT_OPERATION_MODE, - SUPPORT_TARGET_TEMPERATURE, STATE_HEAT, - STATE_IDLE, STATE_MANUAL, STATE_DRY, - STATE_FAN_ONLY, STATE_ECO, ClimateDevice) -from homeassistant.const import ( - ATTR_TEMPERATURE, CONF_NAME, TEMP_CELSIUS) -from homeassistant.core import callback - -from homeassistant.components.knx import DATA_KNX, ATTR_DISCOVER_DEVICES - -CONF_SETPOINT_SHIFT_ADDRESS = 'setpoint_shift_address' -CONF_SETPOINT_SHIFT_STATE_ADDRESS = 'setpoint_shift_state_address' -CONF_SETPOINT_SHIFT_STEP = 'setpoint_shift_step' -CONF_SETPOINT_SHIFT_MAX = 'setpoint_shift_max' -CONF_SETPOINT_SHIFT_MIN = 'setpoint_shift_min' -CONF_TEMPERATURE_ADDRESS = 'temperature_address' -CONF_TARGET_TEMPERATURE_ADDRESS = 'target_temperature_address' -CONF_OPERATION_MODE_ADDRESS = 'operation_mode_address' -CONF_OPERATION_MODE_STATE_ADDRESS = 'operation_mode_state_address' -CONF_CONTROLLER_STATUS_ADDRESS = 'controller_status_address' -CONF_CONTROLLER_STATUS_STATE_ADDRESS = 'controller_status_state_address' -CONF_CONTROLLER_MODE_ADDRESS = 'controller_mode_address' -CONF_CONTROLLER_MODE_STATE_ADDRESS = 'controller_mode_state_address' -CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS = \ - 'operation_mode_frost_protection_address' -CONF_OPERATION_MODE_NIGHT_ADDRESS = 'operation_mode_night_address' -CONF_OPERATION_MODE_COMFORT_ADDRESS = 'operation_mode_comfort_address' -CONF_OPERATION_MODES = 'operation_modes' -CONF_ON_OFF_ADDRESS = 'on_off_address' -CONF_ON_OFF_STATE_ADDRESS = 'on_off_state_address' -CONF_MIN_TEMP = 'min_temp' -CONF_MAX_TEMP = 'max_temp' - -DEFAULT_NAME = 'KNX Climate' -DEFAULT_SETPOINT_SHIFT_STEP = 0.5 -DEFAULT_SETPOINT_SHIFT_MAX = 6 -DEFAULT_SETPOINT_SHIFT_MIN = -6 -DEPENDENCIES = ['knx'] - -# Map KNX operation modes to HA modes. This list might not be full. -OPERATION_MODES = { - # Map DPT 201.100 HVAC operating modes - "Frost Protection": STATE_MANUAL, - "Night": STATE_IDLE, - "Standby": STATE_ECO, - "Comfort": STATE_HEAT, - # Map DPT 201.104 HVAC control modes - "Fan only": STATE_FAN_ONLY, - "Dehumidification": STATE_DRY -} - -OPERATION_MODES_INV = dict(( - reversed(item) for item in OPERATION_MODES.items())) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_TEMPERATURE_ADDRESS): cv.string, - vol.Required(CONF_TARGET_TEMPERATURE_ADDRESS): cv.string, - vol.Optional(CONF_SETPOINT_SHIFT_ADDRESS): cv.string, - vol.Optional(CONF_SETPOINT_SHIFT_STATE_ADDRESS): cv.string, - vol.Optional(CONF_SETPOINT_SHIFT_STEP, - default=DEFAULT_SETPOINT_SHIFT_STEP): vol.All( - float, vol.Range(min=0, max=2)), - vol.Optional(CONF_SETPOINT_SHIFT_MAX, default=DEFAULT_SETPOINT_SHIFT_MAX): - vol.All(int, vol.Range(min=0, max=32)), - vol.Optional(CONF_SETPOINT_SHIFT_MIN, default=DEFAULT_SETPOINT_SHIFT_MIN): - vol.All(int, vol.Range(min=-32, max=0)), - vol.Optional(CONF_OPERATION_MODE_ADDRESS): cv.string, - vol.Optional(CONF_OPERATION_MODE_STATE_ADDRESS): cv.string, - vol.Optional(CONF_CONTROLLER_STATUS_ADDRESS): cv.string, - vol.Optional(CONF_CONTROLLER_STATUS_STATE_ADDRESS): cv.string, - vol.Optional(CONF_CONTROLLER_MODE_ADDRESS): cv.string, - vol.Optional(CONF_CONTROLLER_MODE_STATE_ADDRESS): cv.string, - vol.Optional(CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS): cv.string, - vol.Optional(CONF_OPERATION_MODE_NIGHT_ADDRESS): cv.string, - vol.Optional(CONF_OPERATION_MODE_COMFORT_ADDRESS): cv.string, - vol.Optional(CONF_ON_OFF_ADDRESS): cv.string, - vol.Optional(CONF_ON_OFF_STATE_ADDRESS): cv.string, - vol.Optional(CONF_OPERATION_MODES): vol.All(cv.ensure_list, - [vol.In(OPERATION_MODES)]), - vol.Optional(CONF_MIN_TEMP): vol.Coerce(float), - vol.Optional(CONF_MAX_TEMP): vol.Coerce(float), -}) - - -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up climate(s) 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 climates for KNX platform configured within platform.""" - entities = [] - for device_name in discovery_info[ATTR_DISCOVER_DEVICES]: - device = hass.data[DATA_KNX].xknx.devices[device_name] - entities.append(KNXClimate(device)) - async_add_entities(entities) - - -@callback -def async_add_entities_config(hass, config, async_add_entities): - """Set up climate for KNX platform configured within platform.""" - import xknx - - climate_mode = xknx.devices.ClimateMode( - hass.data[DATA_KNX].xknx, - name=config.get(CONF_NAME) + " Mode", - group_address_operation_mode=config.get(CONF_OPERATION_MODE_ADDRESS), - group_address_operation_mode_state=config.get( - CONF_OPERATION_MODE_STATE_ADDRESS), - group_address_controller_status=config.get( - CONF_CONTROLLER_STATUS_ADDRESS), - group_address_controller_status_state=config.get( - CONF_CONTROLLER_STATUS_STATE_ADDRESS), - group_address_controller_mode=config.get( - CONF_CONTROLLER_MODE_ADDRESS), - group_address_controller_mode_state=config.get( - CONF_CONTROLLER_MODE_STATE_ADDRESS), - group_address_operation_mode_protection=config.get( - CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS), - group_address_operation_mode_night=config.get( - CONF_OPERATION_MODE_NIGHT_ADDRESS), - group_address_operation_mode_comfort=config.get( - CONF_OPERATION_MODE_COMFORT_ADDRESS), - operation_modes=config.get( - CONF_OPERATION_MODES)) - hass.data[DATA_KNX].xknx.devices.add(climate_mode) - - climate = xknx.devices.Climate( - hass.data[DATA_KNX].xknx, - name=config.get(CONF_NAME), - group_address_temperature=config.get(CONF_TEMPERATURE_ADDRESS), - group_address_target_temperature=config.get( - CONF_TARGET_TEMPERATURE_ADDRESS), - group_address_setpoint_shift=config.get(CONF_SETPOINT_SHIFT_ADDRESS), - group_address_setpoint_shift_state=config.get( - CONF_SETPOINT_SHIFT_STATE_ADDRESS), - setpoint_shift_step=config.get(CONF_SETPOINT_SHIFT_STEP), - setpoint_shift_max=config.get(CONF_SETPOINT_SHIFT_MAX), - setpoint_shift_min=config.get(CONF_SETPOINT_SHIFT_MIN), - group_address_on_off=config.get( - CONF_ON_OFF_ADDRESS), - group_address_on_off_state=config.get( - CONF_ON_OFF_STATE_ADDRESS), - min_temp=config.get(CONF_MIN_TEMP), - max_temp=config.get(CONF_MAX_TEMP), - mode=climate_mode) - hass.data[DATA_KNX].xknx.devices.add(climate) - - async_add_entities([KNXClimate(climate)]) - - -class KNXClimate(ClimateDevice): - """Representation of a KNX climate device.""" - - def __init__(self, device): - """Initialize of a KNX climate device.""" - self.device = device - self._unit_of_measurement = TEMP_CELSIUS - - @property - def supported_features(self): - """Return the list of supported features.""" - support = SUPPORT_TARGET_TEMPERATURE - if self.device.mode.supports_operation_mode: - support |= SUPPORT_OPERATION_MODE - if self.device.supports_on_off: - support |= SUPPORT_ON_OFF - return support - - async def async_added_to_hass(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) - - @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 temperature_unit(self): - """Return the unit of measurement.""" - return self._unit_of_measurement - - @property - def current_temperature(self): - """Return the current temperature.""" - return self.device.temperature.value - - @property - def target_temperature_step(self): - """Return the supported step of target temperature.""" - return self.device.setpoint_shift_step - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self.device.target_temperature.value - - @property - def min_temp(self): - """Return the minimum temperature.""" - return self.device.target_temperature_min - - @property - def max_temp(self): - """Return the maximum temperature.""" - return self.device.target_temperature_max - - async def async_set_temperature(self, **kwargs): - """Set new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is None: - return - await self.device.set_target_temperature(temperature) - await self.async_update_ha_state() - - @property - def current_operation(self): - """Return current operation ie. heat, cool, idle.""" - if self.device.mode.supports_operation_mode: - return OPERATION_MODES.get(self.device.mode.operation_mode.value) - return None - - @property - def operation_list(self): - """Return the list of available operation modes.""" - return [OPERATION_MODES.get(operation_mode.value) for - operation_mode in - self.device.mode.operation_modes] - - async def async_set_operation_mode(self, operation_mode): - """Set operation mode.""" - if self.device.mode.supports_operation_mode: - from xknx.knx import HVACOperationMode - knx_operation_mode = HVACOperationMode( - OPERATION_MODES_INV.get(operation_mode)) - await self.device.mode.set_operation_mode(knx_operation_mode) - await self.async_update_ha_state() - - @property - def is_on(self): - """Return true if the device is on.""" - if self.device.supports_on_off: - return self.device.is_on - return None - - async def async_turn_on(self): - """Turn on.""" - await self.device.turn_on() - - async def async_turn_off(self): - """Turn off.""" - await self.device.turn_off() diff --git a/homeassistant/components/climate/maxcube.py b/homeassistant/components/climate/maxcube.py deleted file mode 100644 index 328cdabde626c..0000000000000 --- a/homeassistant/components/climate/maxcube.py +++ /dev/null @@ -1,196 +0,0 @@ -""" -Support for MAX! Thermostats via MAX! Cube. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/maxcube/ -""" -import socket -import logging - -from homeassistant.components.climate import ( - ClimateDevice, STATE_AUTO, SUPPORT_TARGET_TEMPERATURE, - SUPPORT_OPERATION_MODE) -from homeassistant.components.maxcube import DATA_KEY -from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE - -_LOGGER = logging.getLogger(__name__) - -STATE_MANUAL = 'manual' -STATE_BOOST = 'boost' -STATE_VACATION = 'vacation' - -SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Iterate through all MAX! Devices and add thermostats.""" - devices = [] - for handler in hass.data[DATA_KEY].values(): - cube = handler.cube - for device in cube.devices: - name = '{} {}'.format( - cube.room_by_id(device.room_id).name, device.name) - - if cube.is_thermostat(device) or cube.is_wallthermostat(device): - devices.append( - MaxCubeClimate(handler, name, device.rf_address)) - - if devices: - add_entities(devices) - - -class MaxCubeClimate(ClimateDevice): - """MAX! Cube ClimateDevice.""" - - def __init__(self, handler, name, rf_address): - """Initialize MAX! Cube ClimateDevice.""" - self._name = name - self._operation_list = [STATE_AUTO, STATE_MANUAL, STATE_BOOST, - STATE_VACATION] - self._rf_address = rf_address - self._cubehandle = handler - - @property - def supported_features(self): - """Return the list of supported features.""" - return SUPPORT_FLAGS - - @property - def should_poll(self): - """Return the polling state.""" - return True - - @property - def name(self): - """Return the name of the climate device.""" - return self._name - - @property - def min_temp(self): - """Return the minimum temperature.""" - device = self._cubehandle.cube.device_by_rf(self._rf_address) - return self.map_temperature_max_hass(device.min_temperature) - - @property - def max_temp(self): - """Return the maximum temperature.""" - device = self._cubehandle.cube.device_by_rf(self._rf_address) - return self.map_temperature_max_hass(device.max_temperature) - - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - - @property - def current_temperature(self): - """Return the current temperature.""" - device = self._cubehandle.cube.device_by_rf(self._rf_address) - - # Map and return current temperature - return self.map_temperature_max_hass(device.actual_temperature) - - @property - def current_operation(self): - """Return current operation (auto, manual, boost, vacation).""" - device = self._cubehandle.cube.device_by_rf(self._rf_address) - return self.map_mode_max_hass(device.mode) - - @property - def operation_list(self): - """Return the list of available operation modes.""" - return self._operation_list - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - device = self._cubehandle.cube.device_by_rf(self._rf_address) - return self.map_temperature_max_hass(device.target_temperature) - - def set_temperature(self, **kwargs): - """Set new target temperatures.""" - if kwargs.get(ATTR_TEMPERATURE) is None: - return False - - target_temperature = kwargs.get(ATTR_TEMPERATURE) - device = self._cubehandle.cube.device_by_rf(self._rf_address) - - cube = self._cubehandle.cube - - with self._cubehandle.mutex: - try: - cube.set_target_temperature(device, target_temperature) - except (socket.timeout, socket.error): - _LOGGER.error("Setting target temperature failed") - return False - - def set_operation_mode(self, operation_mode): - """Set new operation mode.""" - device = self._cubehandle.cube.device_by_rf(self._rf_address) - mode = self.map_mode_hass_max(operation_mode) - - if mode is None: - return False - - with self._cubehandle.mutex: - try: - self._cubehandle.cube.set_mode(device, mode) - except (socket.timeout, socket.error): - _LOGGER.error("Setting operation mode failed") - return False - - def update(self): - """Get latest data from MAX! Cube.""" - self._cubehandle.update() - - @staticmethod - def map_temperature_max_hass(temperature): - """Map Temperature from MAX! to HASS.""" - if temperature is None: - return 0.0 - - return temperature - - @staticmethod - def map_mode_hass_max(operation_mode): - """Map Home Assistant Operation Modes to MAX! Operation Modes.""" - from maxcube.device import \ - MAX_DEVICE_MODE_AUTOMATIC, \ - MAX_DEVICE_MODE_MANUAL, \ - MAX_DEVICE_MODE_VACATION, \ - MAX_DEVICE_MODE_BOOST - - if operation_mode == STATE_AUTO: - mode = MAX_DEVICE_MODE_AUTOMATIC - elif operation_mode == STATE_MANUAL: - mode = MAX_DEVICE_MODE_MANUAL - elif operation_mode == STATE_VACATION: - mode = MAX_DEVICE_MODE_VACATION - elif operation_mode == STATE_BOOST: - mode = MAX_DEVICE_MODE_BOOST - else: - mode = None - - return mode - - @staticmethod - def map_mode_max_hass(mode): - """Map MAX! Operation Modes to Home Assistant Operation Modes.""" - from maxcube.device import \ - MAX_DEVICE_MODE_AUTOMATIC, \ - MAX_DEVICE_MODE_MANUAL, \ - MAX_DEVICE_MODE_VACATION, \ - MAX_DEVICE_MODE_BOOST - - if mode == MAX_DEVICE_MODE_AUTOMATIC: - operation_mode = STATE_AUTO - elif mode == MAX_DEVICE_MODE_MANUAL: - operation_mode = STATE_MANUAL - elif mode == MAX_DEVICE_MODE_VACATION: - operation_mode = STATE_VACATION - elif mode == MAX_DEVICE_MODE_BOOST: - operation_mode = STATE_BOOST - else: - operation_mode = None - - return operation_mode diff --git a/homeassistant/components/climate/modbus.py b/homeassistant/components/climate/modbus.py deleted file mode 100644 index 1c5c03e4502a1..0000000000000 --- a/homeassistant/components/climate/modbus.py +++ /dev/null @@ -1,148 +0,0 @@ -""" -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/ -""" -import logging -import struct - -import voluptuous as vol - -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 -import homeassistant.helpers.config_validation as cv - -DEPENDENCIES = ['modbus'] - -# Parameters not defined by homeassistant.const -CONF_TARGET_TEMP = 'target_temp_register' -CONF_CURRENT_TEMP = 'current_temp_register' -CONF_DATA_TYPE = 'data_type' -CONF_COUNT = 'data_count' -CONF_PRECISION = 'precision' - -DATA_TYPE_INT = 'int' -DATA_TYPE_UINT = 'uint' -DATA_TYPE_FLOAT = 'float' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_SLAVE): cv.positive_int, - vol.Required(CONF_TARGET_TEMP): cv.positive_int, - vol.Required(CONF_CURRENT_TEMP): cv.positive_int, - vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_FLOAT): - vol.In([DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT]), - vol.Optional(CONF_COUNT, default=2): cv.positive_int, - vol.Optional(CONF_PRECISION, default=1): cv.positive_int -}) - -_LOGGER = logging.getLogger(__name__) - -SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Modbus Thermostat Platform.""" - name = config.get(CONF_NAME) - modbus_slave = config.get(CONF_SLAVE) - target_temp_register = config.get(CONF_TARGET_TEMP) - current_temp_register = config.get(CONF_CURRENT_TEMP) - data_type = config.get(CONF_DATA_TYPE) - count = config.get(CONF_COUNT) - precision = config.get(CONF_PRECISION) - - add_entities([ModbusThermostat(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, - current_temp_register, data_type, count, precision): - """Initialize the unit.""" - self._name = name - self._slave = modbus_slave - self._target_temperature_register = target_temp_register - self._current_temperature_register = current_temp_register - self._target_temperature = None - self._current_temperature = None - self._data_type = data_type - self._count = int(count) - self._precision = precision - self._structure = '>f' - - data_types = {DATA_TYPE_INT: {1: 'h', 2: 'i', 4: 'q'}, - DATA_TYPE_UINT: {1: 'H', 2: 'I', 4: 'Q'}, - DATA_TYPE_FLOAT: {1: 'e', 2: 'f', 4: 'd'}} - - self._structure = '>{}'.format(data_types[self._data_type] - [self._count]) - - @property - def supported_features(self): - """Return the list of supported features.""" - return SUPPORT_FLAGS - - def update(self): - """Update Target & Current Temperature.""" - self._target_temperature = self.read_register( - self._target_temperature_register) - self._current_temperature = self.read_register( - self._current_temperature_register) - - @property - def name(self): - """Return the name of the climate device.""" - return self._name - - @property - def current_temperature(self): - """Return the current temperature.""" - return self._current_temperature - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self._target_temperature - - def set_temperature(self, **kwargs): - """Set new target temperature.""" - target_temperature = kwargs.get(ATTR_TEMPERATURE) - if target_temperature is None: - return - byte_string = struct.pack(self._structure, target_temperature) - register_value = struct.unpack('>h', byte_string[0:2])[0] - - try: - self.write_register(self._target_temperature_register, - register_value) - except AttributeError as ex: - _LOGGER.error(ex) - - 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) - except AttributeError as ex: - _LOGGER.error(ex) - byte_string = b''.join( - [x.to_bytes(2, byteorder='big') for x in result.registers]) - val = struct.unpack(self._structure, byte_string)[0] - register_value = format(val, '.{}f'.format(self._precision)) - return register_value - - def write_register(self, register, value): - """Write register using the modbus hub slave.""" - modbus.HUB.write_registers(self._slave, register, [value, 0]) diff --git a/homeassistant/components/climate/mysensors.py b/homeassistant/components/climate/mysensors.py deleted file mode 100644 index 66c634d8cd9b1..0000000000000 --- a/homeassistant/components/climate/mysensors.py +++ /dev/null @@ -1,182 +0,0 @@ -""" -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/ -""" -from homeassistant.components import mysensors -from homeassistant.components.climate import ( - ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DOMAIN, STATE_AUTO, - STATE_COOL, STATE_HEAT, STATE_OFF, SUPPORT_FAN_MODE, - SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, - SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW, - ClimateDevice) -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT - -DICT_HA_TO_MYS = { - STATE_AUTO: 'AutoChangeOver', - STATE_COOL: 'CoolOn', - STATE_HEAT: 'HeatOn', - STATE_OFF: 'Off', -} -DICT_MYS_TO_HA = { - 'AutoChangeOver': STATE_AUTO, - 'CoolOn': STATE_COOL, - 'HeatOn': STATE_HEAT, - 'Off': STATE_OFF, -} - -FAN_LIST = ['Auto', 'Min', 'Normal', 'Max'] -OPERATION_LIST = [STATE_OFF, STATE_AUTO, STATE_COOL, STATE_HEAT] - - -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): - """Set up the mysensors climate.""" - mysensors.setup_mysensors_platform( - hass, DOMAIN, discovery_info, MySensorsHVAC, - async_add_entities=async_add_entities) - - -class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateDevice): - """Representation of a MySensors HVAC.""" - - @property - def supported_features(self): - """Return the list of supported features.""" - features = SUPPORT_OPERATION_MODE - set_req = self.gateway.const.SetReq - if set_req.V_HVAC_SPEED in self._values: - features = features | SUPPORT_FAN_MODE - if (set_req.V_HVAC_SETPOINT_COOL in self._values and - set_req.V_HVAC_SETPOINT_HEAT in self._values): - features = ( - features | SUPPORT_TARGET_TEMPERATURE_HIGH | - SUPPORT_TARGET_TEMPERATURE_LOW) - else: - features = features | SUPPORT_TARGET_TEMPERATURE - return features - - @property - def assumed_state(self): - """Return True if unable to access real state of entity.""" - return self.gateway.optimistic - - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS if self.gateway.metric else TEMP_FAHRENHEIT - - @property - def current_temperature(self): - """Return the current temperature.""" - value = self._values.get(self.gateway.const.SetReq.V_TEMP) - - if value is not None: - value = float(value) - - return value - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - set_req = self.gateway.const.SetReq - if set_req.V_HVAC_SETPOINT_COOL in self._values and \ - set_req.V_HVAC_SETPOINT_HEAT in self._values: - return None - temp = self._values.get(set_req.V_HVAC_SETPOINT_COOL) - if temp is None: - temp = self._values.get(set_req.V_HVAC_SETPOINT_HEAT) - return float(temp) if temp is not None else None - - @property - def target_temperature_high(self): - """Return the highbound target temperature we try to reach.""" - set_req = self.gateway.const.SetReq - if set_req.V_HVAC_SETPOINT_HEAT in self._values: - temp = self._values.get(set_req.V_HVAC_SETPOINT_COOL) - return float(temp) if temp is not None else None - - @property - def target_temperature_low(self): - """Return the lowbound target temperature we try to reach.""" - set_req = self.gateway.const.SetReq - if set_req.V_HVAC_SETPOINT_COOL in self._values: - temp = self._values.get(set_req.V_HVAC_SETPOINT_HEAT) - return float(temp) if temp is not None else None - - @property - def current_operation(self): - """Return current operation ie. heat, cool, idle.""" - return self._values.get(self.value_type) - - @property - def operation_list(self): - """List of available operation modes.""" - return OPERATION_LIST - - @property - def current_fan_mode(self): - """Return the fan setting.""" - return self._values.get(self.gateway.const.SetReq.V_HVAC_SPEED) - - @property - def fan_list(self): - """List of available fan modes.""" - return FAN_LIST - - async def async_set_temperature(self, **kwargs): - """Set new target temperature.""" - set_req = self.gateway.const.SetReq - temp = kwargs.get(ATTR_TEMPERATURE) - low = kwargs.get(ATTR_TARGET_TEMP_LOW) - high = kwargs.get(ATTR_TARGET_TEMP_HIGH) - heat = self._values.get(set_req.V_HVAC_SETPOINT_HEAT) - cool = self._values.get(set_req.V_HVAC_SETPOINT_COOL) - updates = [] - if temp is not None: - if heat is not None: - # Set HEAT Target temperature - value_type = set_req.V_HVAC_SETPOINT_HEAT - elif cool is not None: - # Set COOL Target temperature - value_type = set_req.V_HVAC_SETPOINT_COOL - if heat is not None or cool is not None: - updates = [(value_type, temp)] - elif all(val is not None for val in (low, high, heat, cool)): - updates = [ - (set_req.V_HVAC_SETPOINT_HEAT, low), - (set_req.V_HVAC_SETPOINT_COOL, high)] - for value_type, value in updates: - self.gateway.set_child_value( - self.node_id, self.child_id, value_type, value) - if self.gateway.optimistic: - # Optimistically assume that device has changed state - self._values[value_type] = value - self.async_schedule_update_ha_state() - - async def async_set_fan_mode(self, fan_mode): - """Set new target temperature.""" - set_req = self.gateway.const.SetReq - self.gateway.set_child_value( - self.node_id, self.child_id, set_req.V_HVAC_SPEED, fan_mode) - if self.gateway.optimistic: - # Optimistically assume that device has changed state - self._values[set_req.V_HVAC_SPEED] = fan_mode - self.async_schedule_update_ha_state() - - async def async_set_operation_mode(self, operation_mode): - """Set new target temperature.""" - self.gateway.set_child_value( - self.node_id, self.child_id, self.value_type, - DICT_HA_TO_MYS[operation_mode]) - if self.gateway.optimistic: - # Optimistically assume that device has changed state - self._values[self.value_type] = operation_mode - self.async_schedule_update_ha_state() - - async def async_update(self): - """Update the controller with the latest value from a sensor.""" - await super().async_update() - self._values[self.value_type] = DICT_MYS_TO_HA[ - self._values[self.value_type]] diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py deleted file mode 100644 index bd6bb2991ccfe..0000000000000 --- a/homeassistant/components/climate/nest.py +++ /dev/null @@ -1,295 +0,0 @@ -""" -Support for Nest thermostats. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/climate.nest/ -""" -import logging - -import voluptuous as vol - -from homeassistant.components.nest import ( - DATA_NEST, SIGNAL_NEST_UPDATE, DOMAIN as NEST_DOMAIN) -from homeassistant.components.climate import ( - STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_ECO, ClimateDevice, - PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - ATTR_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE, - SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW, - SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE, SUPPORT_FAN_MODE) -from homeassistant.const import ( - TEMP_CELSIUS, TEMP_FAHRENHEIT, - CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF) -from homeassistant.helpers.dispatcher import async_dispatcher_connect - -DEPENDENCIES = ['nest'] -_LOGGER = logging.getLogger(__name__) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_SCAN_INTERVAL): - vol.All(vol.Coerce(int), vol.Range(min=1)), -}) - -NEST_MODE_HEAT_COOL = 'heat-cool' - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Nest thermostat. - - No longer in use. - """ - - -async def async_setup_entry(hass, entry, async_add_entities): - """Set up the Nest climate device based on a config entry.""" - temp_unit = hass.config.units.temperature_unit - - thermostats = await hass.async_add_job(hass.data[DATA_NEST].thermostats) - - all_devices = [NestThermostat(structure, device, temp_unit) - for structure, device in thermostats] - - async_add_entities(all_devices, True) - - -class NestThermostat(ClimateDevice): - """Representation of a Nest thermostat.""" - - def __init__(self, structure, device, temp_unit): - """Initialize the thermostat.""" - self._unit = temp_unit - self.structure = structure - self.device = device - self._fan_list = [STATE_ON, STATE_AUTO] - - # Set the default supported features - self._support_flags = (SUPPORT_TARGET_TEMPERATURE | - SUPPORT_OPERATION_MODE | SUPPORT_AWAY_MODE) - - # Not all nest devices support cooling and heating remove unused - self._operation_list = [STATE_OFF] - - # Add supported nest thermostat features - if self.device.can_heat: - self._operation_list.append(STATE_HEAT) - - if self.device.can_cool: - self._operation_list.append(STATE_COOL) - - if self.device.can_heat and self.device.can_cool: - self._operation_list.append(STATE_AUTO) - self._support_flags = (self._support_flags | - SUPPORT_TARGET_TEMPERATURE_HIGH | - SUPPORT_TARGET_TEMPERATURE_LOW) - - self._operation_list.append(STATE_ECO) - - # feature of device - self._has_fan = self.device.has_fan - if self._has_fan: - self._support_flags = (self._support_flags | SUPPORT_FAN_MODE) - - # data attributes - self._away = None - self._location = None - self._name = None - self._humidity = None - self._target_temperature = None - self._temperature = None - self._temperature_scale = None - self._mode = None - self._fan = None - self._eco_temperature = None - self._is_locked = None - self._locked_temperature = None - self._min_temperature = None - self._max_temperature = None - - @property - def should_poll(self): - """Do not need poll thanks using Nest streaming API.""" - return False - - async def async_added_to_hass(self): - """Register update signal handler.""" - async def async_update_state(): - """Update device state.""" - await self.async_update_ha_state(True) - - async_dispatcher_connect(self.hass, SIGNAL_NEST_UPDATE, - async_update_state) - - @property - def supported_features(self): - """Return the list of supported features.""" - return self._support_flags - - @property - def unique_id(self): - """Return unique ID for this device.""" - return self.device.serial - - @property - def device_info(self): - """Return information about the device.""" - return { - 'identifiers': { - (NEST_DOMAIN, self.device.device_id), - }, - 'name': self.device.name_long, - 'manufacturer': 'Nest Labs', - 'model': "Thermostat", - 'sw_version': self.device.software_version, - } - - @property - def name(self): - """Return the name of the nest, if any.""" - return self._name - - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return self._temperature_scale - - @property - def current_temperature(self): - """Return the current temperature.""" - return self._temperature - - @property - def current_operation(self): - """Return current operation ie. heat, cool, idle.""" - if self._mode in [STATE_HEAT, STATE_COOL, STATE_OFF, STATE_ECO]: - return self._mode - if self._mode == NEST_MODE_HEAT_COOL: - return STATE_AUTO - return None - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - if self._mode not in (NEST_MODE_HEAT_COOL, STATE_ECO): - return self._target_temperature - return None - - @property - def target_temperature_low(self): - """Return the lower bound temperature we try to reach.""" - if self._mode == STATE_ECO: - return self._eco_temperature[0] - if self._mode == NEST_MODE_HEAT_COOL: - return self._target_temperature[0] - return None - - @property - def target_temperature_high(self): - """Return the upper bound temperature we try to reach.""" - if self._mode == STATE_ECO: - return self._eco_temperature[1] - if self._mode == NEST_MODE_HEAT_COOL: - return self._target_temperature[1] - return None - - @property - def is_away_mode_on(self): - """Return if away mode is on.""" - return self._away - - def set_temperature(self, **kwargs): - """Set new target temperature.""" - import nest - temp = None - target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) - target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) - if self._mode == NEST_MODE_HEAT_COOL: - if target_temp_low is not None and target_temp_high is not None: - temp = (target_temp_low, target_temp_high) - _LOGGER.debug("Nest set_temperature-output-value=%s", temp) - else: - temp = kwargs.get(ATTR_TEMPERATURE) - _LOGGER.debug("Nest set_temperature-output-value=%s", temp) - try: - if temp is not None: - self.device.target = temp - except nest.nest.APIError as api_error: - _LOGGER.error("An error occurred while setting temperature: %s", - api_error) - # restore target temperature - self.schedule_update_ha_state(True) - - def set_operation_mode(self, operation_mode): - """Set operation mode.""" - if operation_mode in [STATE_HEAT, STATE_COOL, STATE_OFF, STATE_ECO]: - device_mode = operation_mode - elif operation_mode == STATE_AUTO: - device_mode = NEST_MODE_HEAT_COOL - else: - device_mode = STATE_OFF - _LOGGER.error( - "An error occurred while setting device mode. " - "Invalid operation mode: %s", operation_mode) - self.device.mode = device_mode - - @property - def operation_list(self): - """List of available operation modes.""" - return self._operation_list - - def turn_away_mode_on(self): - """Turn away on.""" - self.structure.away = True - - def turn_away_mode_off(self): - """Turn away off.""" - self.structure.away = False - - @property - def current_fan_mode(self): - """Return whether the fan is on.""" - if self._has_fan: - # Return whether the fan is on - return STATE_ON if self._fan else STATE_AUTO - # No Fan available so disable slider - return None - - @property - def fan_list(self): - """List of available fan modes.""" - if self._has_fan: - return self._fan_list - return None - - def set_fan_mode(self, fan_mode): - """Turn fan on/off.""" - if self._has_fan: - self.device.fan = fan_mode.lower() - - @property - def min_temp(self): - """Identify min_temp in Nest API or defaults if not available.""" - return self._min_temperature - - @property - def max_temp(self): - """Identify max_temp in Nest API or defaults if not available.""" - return self._max_temperature - - def update(self): - """Cache value from Python-nest.""" - self._location = self.device.where - self._name = self.device.name - self._humidity = self.device.humidity - self._temperature = self.device.temperature - self._mode = self.device.mode - self._target_temperature = self.device.target - self._fan = self.device.fan - self._away = self.structure.away == 'away' - self._eco_temperature = self.device.eco_temperature - self._locked_temperature = self.device.locked_temperature - self._min_temperature = self.device.min_temperature - self._max_temperature = self.device.max_temperature - self._is_locked = self.device.is_locked - if self.device.temperature_scale == 'C': - self._temperature_scale = TEMP_CELSIUS - else: - self._temperature_scale = TEMP_FAHRENHEIT diff --git a/homeassistant/components/climate/netatmo.py b/homeassistant/components/climate/netatmo.py deleted file mode 100644 index 8849ada5ccc78..0000000000000 --- a/homeassistant/components/climate/netatmo.py +++ /dev/null @@ -1,175 +0,0 @@ -""" -Support for Netatmo Smart Thermostat. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/climate.netatmo/ -""" -import logging -from datetime import timedelta -import voluptuous as vol - -from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE -from homeassistant.components.climate import ( - STATE_HEAT, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE) -from homeassistant.util import Throttle -import homeassistant.helpers.config_validation as cv - -DEPENDENCIES = ['netatmo'] - -_LOGGER = logging.getLogger(__name__) - -CONF_RELAY = 'relay' -CONF_THERMOSTAT = 'thermostat' - -DEFAULT_AWAY_TEMPERATURE = 14 -# # The default offset is 2 hours (when you use the thermostat itself) -DEFAULT_TIME_OFFSET = 7200 -# # Return cached results if last scan was less then this time ago -# # NetAtmo Data is uploaded to server every hour -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_RELAY): cv.string, - vol.Optional(CONF_THERMOSTAT, default=[]): - vol.All(cv.ensure_list, [cv.string]), -}) - -SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | - SUPPORT_AWAY_MODE) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the NetAtmo Thermostat.""" - netatmo = hass.components.netatmo - device = config.get(CONF_RELAY) - - import pyatmo - try: - data = ThermostatData(netatmo.NETATMO_AUTH, device) - for module_name in data.get_module_names(): - if CONF_THERMOSTAT in config: - if config[CONF_THERMOSTAT] != [] and \ - module_name not in config[CONF_THERMOSTAT]: - continue - add_entities([NetatmoThermostat(data, module_name)], True) - except pyatmo.NoDevice: - return None - - -class NetatmoThermostat(ClimateDevice): - """Representation a Netatmo thermostat.""" - - def __init__(self, data, module_name, away_temp=None): - """Initialize the sensor.""" - self._data = data - self._state = None - self._name = module_name - self._target_temperature = None - self._away = None - - @property - def supported_features(self): - """Return the list of supported features.""" - return SUPPORT_FLAGS - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - - @property - def current_temperature(self): - """Return the current temperature.""" - return self._data.current_temperature - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self._target_temperature - - @property - def current_operation(self): - """Return the current state of the thermostat.""" - state = self._data.thermostatdata.relay_cmd - if state == 0: - return STATE_IDLE - if state == 100: - return STATE_HEAT - - @property - def is_away_mode_on(self): - """Return true if away mode is on.""" - return self._away - - def turn_away_mode_on(self): - """Turn away on.""" - mode = "away" - temp = None - self._data.thermostatdata.setthermpoint(mode, temp, endTimeOffset=None) - self._away = True - - def turn_away_mode_off(self): - """Turn away off.""" - mode = "program" - temp = None - self._data.thermostatdata.setthermpoint(mode, temp, endTimeOffset=None) - self._away = False - - def set_temperature(self, **kwargs): - """Set new target temperature for 2 hours.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is None: - return - mode = "manual" - self._data.thermostatdata.setthermpoint( - mode, temperature, DEFAULT_TIME_OFFSET) - self._target_temperature = temperature - self._away = False - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest data from NetAtmo API and updates the states.""" - self._data.update() - self._target_temperature = self._data.thermostatdata.setpoint_temp - self._away = self._data.setpoint_mode == 'away' - - -class ThermostatData: - """Get the latest data from Netatmo.""" - - def __init__(self, auth, device=None): - """Initialize the data object.""" - self.auth = auth - self.thermostatdata = None - self.module_names = [] - self.device = device - self.current_temperature = None - self.target_temperature = None - self.setpoint_mode = None - - def get_module_names(self): - """Return all module available on the API as a list.""" - self.update() - if not self.device: - for device in self.thermostatdata.modules: - for module in self.thermostatdata.modules[device].values(): - self.module_names.append(module['module_name']) - else: - for module in self.thermostatdata.modules[self.device].values(): - self.module_names.append(module['module_name']) - return self.module_names - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Call the NetAtmo API to update the data.""" - import pyatmo - self.thermostatdata = pyatmo.ThermostatData(self.auth) - self.target_temperature = self.thermostatdata.setpoint_temp - self.setpoint_mode = self.thermostatdata.setpoint_mode - self.current_temperature = self.thermostatdata.temp diff --git a/homeassistant/components/climate/opentherm_gw.py b/homeassistant/components/climate/opentherm_gw.py deleted file mode 100644 index 6dc52e6acc7f1..0000000000000 --- a/homeassistant/components/climate/opentherm_gw.py +++ /dev/null @@ -1,175 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.components.climate import (ClimateDevice, STATE_IDLE, - STATE_HEAT, STATE_COOL, - SUPPORT_TARGET_TEMPERATURE) -from homeassistant.components.opentherm_gw import ( - CONF_FLOOR_TEMP, CONF_PRECISION, DATA_DEVICE, DATA_GW_VARS, - DATA_OPENTHERM_GW, SIGNAL_OPENTHERM_GW_UPDATE) -from homeassistant.const import (ATTR_TEMPERATURE, CONF_NAME, PRECISION_HALVES, - PRECISION_TENTHS, PRECISION_WHOLE, - TEMP_CELSIUS) -from homeassistant.helpers.dispatcher import async_dispatcher_connect - -DEPENDENCIES = ['opentherm_gw'] - -SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE) -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up the opentherm_gw device.""" - gateway = OpenThermGateway(hass, discovery_info) - async_add_entities([gateway]) - - -class OpenThermGateway(ClimateDevice): - """Representation of a climate device.""" - - def __init__(self, hass, config): - """Initialize the device.""" - self._gateway = hass.data[DATA_OPENTHERM_GW][DATA_DEVICE] - self._gw_vars = hass.data[DATA_OPENTHERM_GW][DATA_GW_VARS] - self.friendly_name = config.get(CONF_NAME) - self.floor_temp = config.get(CONF_FLOOR_TEMP) - self.temp_precision = config.get(CONF_PRECISION) - self._current_operation = STATE_IDLE - self._current_temperature = 0.0 - self._target_temperature = 0.0 - self._away_mode_a = None - self._away_mode_b = None - self._away_state_a = False - self._away_state_b = False - - async def async_added_to_hass(self): - """Connect to the OpenTherm Gateway device.""" - _LOGGER.debug("Added device %s", self.friendly_name) - async_dispatcher_connect(self.hass, SIGNAL_OPENTHERM_GW_UPDATE, - self.receive_report) - - async def receive_report(self, status): - """Receive and handle a new report from the Gateway.""" - ch_active = status.get(self._gw_vars.DATA_SLAVE_CH_ACTIVE) - flame_on = status.get(self._gw_vars.DATA_SLAVE_FLAME_ON) - cooling_active = status.get(self._gw_vars.DATA_SLAVE_COOLING_ACTIVE) - if ch_active and flame_on: - self._current_operation = STATE_HEAT - elif cooling_active: - self._current_operation = STATE_COOL - else: - self._current_operation = STATE_IDLE - self._current_temperature = status.get(self._gw_vars.DATA_ROOM_TEMP) - - temp = status.get(self._gw_vars.DATA_ROOM_SETPOINT_OVRD) - if temp is None: - temp = status.get(self._gw_vars.DATA_ROOM_SETPOINT) - self._target_temperature = temp - - # GPIO mode 5: 0 == Away - # GPIO mode 6: 1 == Away - gpio_a_state = status.get(self._gw_vars.OTGW_GPIO_A) - if gpio_a_state == 5: - self._away_mode_a = 0 - elif gpio_a_state == 6: - self._away_mode_a = 1 - else: - self._away_mode_a = None - gpio_b_state = status.get(self._gw_vars.OTGW_GPIO_B) - if gpio_b_state == 5: - self._away_mode_b = 0 - elif gpio_b_state == 6: - self._away_mode_b = 1 - else: - self._away_mode_b = None - if self._away_mode_a is not None: - self._away_state_a = (status.get( - self._gw_vars.OTGW_GPIO_A_STATE) == self._away_mode_a) - if self._away_mode_b is not None: - self._away_state_b = (status.get( - self._gw_vars.OTGW_GPIO_B_STATE) == self._away_mode_b) - self.async_schedule_update_ha_state() - - @property - def name(self): - """Return the friendly name.""" - return self.friendly_name - - @property - def precision(self): - """Return the precision of the system.""" - if self.temp_precision is not None: - return self.temp_precision - if self.hass.config.units.temperature_unit == TEMP_CELSIUS: - return PRECISION_HALVES - return PRECISION_WHOLE - - @property - def should_poll(self): - """Disable polling for this entity.""" - return False - - @property - def temperature_unit(self): - """Return the unit of measurement used by the platform.""" - return TEMP_CELSIUS - - @property - def current_operation(self): - """Return current operation ie. heat, cool, idle.""" - return self._current_operation - - @property - def current_temperature(self): - """Return the current temperature.""" - if self.floor_temp is True: - if self.temp_precision == PRECISION_HALVES: - return int(2 * self._current_temperature) / 2 - if self.temp_precision == PRECISION_TENTHS: - return int(10 * self._current_temperature) / 10 - return int(self._current_temperature) - return self._current_temperature - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self._target_temperature - - @property - def target_temperature_step(self): - """Return the supported step of target temperature.""" - return self.temp_precision - - @property - def is_away_mode_on(self): - """Return true if away mode is on.""" - return self._away_state_a or self._away_state_b - - async def async_set_temperature(self, **kwargs): - """Set new target temperature.""" - if ATTR_TEMPERATURE in kwargs: - temp = float(kwargs[ATTR_TEMPERATURE]) - self._target_temperature = await self._gateway.set_target_temp( - temp) - self.async_schedule_update_ha_state() - - @property - def supported_features(self): - """Return the list of supported features.""" - return SUPPORT_FLAGS - - @property - def min_temp(self): - """Return the minimum temperature.""" - return 1 - - @property - def max_temp(self): - """Return the maximum temperature.""" - return 30 diff --git a/homeassistant/components/climate/reproduce_state.py b/homeassistant/components/climate/reproduce_state.py new file mode 100644 index 0000000000000..3259e4084cf68 --- /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/climate/spider.py b/homeassistant/components/climate/spider.py deleted file mode 100644 index a9d966bd499c3..0000000000000 --- a/homeassistant/components/climate/spider.py +++ /dev/null @@ -1,127 +0,0 @@ -""" -Support for Spider thermostats. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/climate.spider/ -""" - -import logging - -from homeassistant.components.climate import ( - ATTR_TEMPERATURE, STATE_COOL, STATE_HEAT, STATE_IDLE, - SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, ClimateDevice) -from homeassistant.components.spider import DOMAIN as SPIDER_DOMAIN -from homeassistant.const import TEMP_CELSIUS - -DEPENDENCIES = ['spider'] - -OPERATION_LIST = [ - STATE_HEAT, - STATE_COOL, -] - -HA_STATE_TO_SPIDER = { - STATE_COOL: 'Cool', - STATE_HEAT: 'Heat', - STATE_IDLE: 'Idle' -} - -SPIDER_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_SPIDER.items()} - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Spider thermostat.""" - if discovery_info is None: - return - - devices = [SpiderThermostat(hass.data[SPIDER_DOMAIN]['controller'], device) - for device in hass.data[SPIDER_DOMAIN]['thermostats']] - add_entities(devices, True) - - -class SpiderThermostat(ClimateDevice): - """Representation of a thermostat.""" - - def __init__(self, api, thermostat): - """Initialize the thermostat.""" - self.api = api - self.thermostat = thermostat - - @property - def supported_features(self): - """Return the list of supported features.""" - supports = SUPPORT_TARGET_TEMPERATURE - - if self.thermostat.has_operation_mode: - supports = supports | SUPPORT_OPERATION_MODE - - return supports - - @property - def unique_id(self): - """Return the id of the thermostat, if any.""" - return self.thermostat.id - - @property - def name(self): - """Return the name of the thermostat, if any.""" - return self.thermostat.name - - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - - @property - def current_temperature(self): - """Return the current temperature.""" - return self.thermostat.current_temperature - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self.thermostat.target_temperature - - @property - def target_temperature_step(self): - """Return the supported step of target temperature.""" - return self.thermostat.temperature_steps - - @property - def min_temp(self): - """Return the minimum temperature.""" - return self.thermostat.minimum_temperature - - @property - def max_temp(self): - """Return the maximum temperature.""" - return self.thermostat.maximum_temperature - - @property - def current_operation(self): - """Return current operation ie. heat, cool, idle.""" - return SPIDER_STATE_TO_HA[self.thermostat.operation_mode] - - @property - def operation_list(self): - """Return the list of available operation modes.""" - return OPERATION_LIST - - def set_temperature(self, **kwargs): - """Set new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is None: - return - - self.thermostat.set_temperature(temperature) - - def set_operation_mode(self, operation_mode): - """Set new target operation mode.""" - self.thermostat.set_operation_mode( - HA_STATE_TO_SPIDER.get(operation_mode)) - - def update(self): - """Get the latest data.""" - self.thermostat = self.api.get_thermostat(self.unique_id) diff --git a/homeassistant/components/climate/tado.py b/homeassistant/components/climate/tado.py deleted file mode 100644 index 1e52c1636244b..0000000000000 --- a/homeassistant/components/climate/tado.py +++ /dev/null @@ -1,356 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.const import (PRECISION_TENTHS, TEMP_CELSIUS) -from homeassistant.components.climate import ( - ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE) -from homeassistant.util.temperature import convert as convert_temperature -from homeassistant.const import ATTR_TEMPERATURE -from homeassistant.components.tado import DATA_TADO - -_LOGGER = logging.getLogger(__name__) - -CONST_MODE_SMART_SCHEDULE = 'SMART_SCHEDULE' # Default mytado mode -CONST_MODE_OFF = 'OFF' # Switch off heating in a zone - -# When we change the temperature setting, we need an overlay mode -# wait until tado changes the mode automatic -CONST_OVERLAY_TADO_MODE = 'TADO_MODE' -# the user has change the temperature or mode manually -CONST_OVERLAY_MANUAL = 'MANUAL' -# the temperature will be reset after a timespan -CONST_OVERLAY_TIMER = 'TIMER' - -CONST_MODE_FAN_HIGH = 'HIGH' -CONST_MODE_FAN_MIDDLE = 'MIDDLE' -CONST_MODE_FAN_LOW = 'LOW' - -FAN_MODES_LIST = { - CONST_MODE_FAN_HIGH: 'High', - CONST_MODE_FAN_MIDDLE: 'Middle', - CONST_MODE_FAN_LOW: 'Low', - CONST_MODE_OFF: 'Off', -} - -OPERATION_LIST = { - CONST_OVERLAY_MANUAL: 'Manual', - CONST_OVERLAY_TIMER: 'Timer', - CONST_OVERLAY_TADO_MODE: 'Tado mode', - CONST_MODE_SMART_SCHEDULE: 'Smart schedule', - CONST_MODE_OFF: 'Off', -} - -SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Tado climate platform.""" - tado = hass.data[DATA_TADO] - - try: - zones = tado.get_zones() - except RuntimeError: - _LOGGER.error("Unable to get zone info from mytado") - return - - climate_devices = [] - for zone in zones: - device = create_climate_device( - tado, hass, zone, zone['name'], zone['id']) - if not device: - continue - climate_devices.append(device) - - if climate_devices: - add_entities(climate_devices, True) - - -def create_climate_device(tado, hass, zone, name, zone_id): - """Create a Tado climate device.""" - capabilities = tado.get_capabilities(zone_id) - - unit = TEMP_CELSIUS - ac_mode = capabilities['type'] == 'AIR_CONDITIONING' - - if ac_mode: - temperatures = capabilities['HEAT']['temperatures'] - elif 'temperatures' in capabilities: - temperatures = capabilities['temperatures'] - else: - _LOGGER.debug("Received zone %s has no temperature; not adding", name) - return - - min_temp = float(temperatures['celsius']['min']) - max_temp = float(temperatures['celsius']['max']) - - data_id = 'zone {} {}'.format(name, zone_id) - device = TadoClimate(tado, - name, zone_id, data_id, - hass.config.units.temperature(min_temp, unit), - hass.config.units.temperature(max_temp, unit), - ac_mode) - - tado.add_sensor(data_id, { - 'id': zone_id, - 'zone': zone, - 'name': name, - 'climate': device - }) - - return device - - -class TadoClimate(ClimateDevice): - """Representation of a tado climate device.""" - - def __init__(self, store, zone_name, zone_id, data_id, - min_temp, max_temp, ac_mode, - tolerance=0.3): - """Initialize of Tado climate device.""" - self._store = store - self._data_id = data_id - - self.zone_name = zone_name - self.zone_id = zone_id - - self.ac_mode = ac_mode - - self._active = False - self._device_is_active = False - - self._unit = TEMP_CELSIUS - self._cur_temp = None - self._cur_humidity = None - self._is_away = False - self._min_temp = min_temp - self._max_temp = max_temp - self._target_temp = None - self._tolerance = tolerance - self._cooling = False - - self._current_fan = CONST_MODE_OFF - self._current_operation = CONST_MODE_SMART_SCHEDULE - self._overlay_mode = CONST_MODE_SMART_SCHEDULE - - @property - def supported_features(self): - """Return the list of supported features.""" - return SUPPORT_FLAGS - - @property - def name(self): - """Return the name of the device.""" - return self.zone_name - - @property - def current_humidity(self): - """Return the current humidity.""" - return self._cur_humidity - - @property - def current_temperature(self): - """Return the sensor temperature.""" - return self._cur_temp - - @property - def current_operation(self): - """Return current readable operation mode.""" - if self._cooling: - return "Cooling" - return OPERATION_LIST.get(self._current_operation) - - @property - def operation_list(self): - """Return the list of available operation modes (readable).""" - return list(OPERATION_LIST.values()) - - @property - def current_fan_mode(self): - """Return the fan setting.""" - if self.ac_mode: - return FAN_MODES_LIST.get(self._current_fan) - return None - - @property - def fan_list(self): - """List of available fan modes.""" - if self.ac_mode: - return list(FAN_MODES_LIST.values()) - return None - - @property - def temperature_unit(self): - """Return the unit of measurement used by the platform.""" - return self._unit - - @property - def is_away_mode_on(self): - """Return true if away mode is on.""" - return self._is_away - - @property - def target_temperature_step(self): - """Return the supported step of target temperature.""" - return PRECISION_TENTHS - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self._target_temp - - def set_temperature(self, **kwargs): - """Set new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is None: - return - - self._current_operation = CONST_OVERLAY_TADO_MODE - self._overlay_mode = None - self._target_temp = temperature - self._control_heating() - - # pylint: disable=arguments-differ - def set_operation_mode(self, readable_operation_mode): - """Set new operation mode.""" - operation_mode = CONST_MODE_SMART_SCHEDULE - - for mode, readable in OPERATION_LIST.items(): - if readable == readable_operation_mode: - operation_mode = mode - break - - self._current_operation = operation_mode - self._overlay_mode = None - self._control_heating() - - @property - def min_temp(self): - """Return the minimum temperature.""" - return convert_temperature(self._min_temp, self._unit, - self.hass.config.units.temperature_unit) - - @property - def max_temp(self): - """Return the maximum temperature.""" - return convert_temperature(self._max_temp, self._unit, - self.hass.config.units.temperature_unit) - - def update(self): - """Update the state of this climate device.""" - self._store.update() - - data = self._store.get_data(self._data_id) - - if data is None: - _LOGGER.debug("Received no data for zone %s", self.zone_name) - return - - if 'sensorDataPoints' in data: - sensor_data = data['sensorDataPoints'] - - unit = TEMP_CELSIUS - - if 'insideTemperature' in sensor_data: - temperature = float( - sensor_data['insideTemperature']['celsius']) - self._cur_temp = self.hass.config.units.temperature( - temperature, unit) - - if 'humidity' in sensor_data: - humidity = float( - sensor_data['humidity']['percentage']) - self._cur_humidity = humidity - - # temperature setting will not exist when device is off - if 'temperature' in data['setting'] and \ - data['setting']['temperature'] is not None: - setting = float( - data['setting']['temperature']['celsius']) - self._target_temp = self.hass.config.units.temperature( - setting, unit) - - if 'tadoMode' in data: - mode = data['tadoMode'] - self._is_away = mode == 'AWAY' - - if 'setting' in data: - power = data['setting']['power'] - if power == 'OFF': - self._current_operation = CONST_MODE_OFF - self._current_fan = CONST_MODE_OFF - # There is no overlay, the mode will always be - # "SMART_SCHEDULE" - self._overlay_mode = CONST_MODE_SMART_SCHEDULE - self._device_is_active = False - else: - self._device_is_active = True - - overlay = False - overlay_data = None - termination = CONST_MODE_SMART_SCHEDULE - cooling = False - fan_speed = CONST_MODE_OFF - - if 'overlay' in data: - overlay_data = data['overlay'] - overlay = overlay_data is not None - - if overlay: - termination = overlay_data['termination']['type'] - - if 'setting' in overlay_data: - setting_data = overlay_data['setting'] - setting = setting_data is not None - - if setting: - if 'mode' in setting_data: - cooling = setting_data['mode'] == 'COOL' - - if 'fanSpeed' in setting_data: - fan_speed = setting_data['fanSpeed'] - - if self._device_is_active: - # If you set mode manually to off, there will be an overlay - # and a termination, but we want to see the mode "OFF" - self._overlay_mode = termination - self._current_operation = termination - - self._cooling = cooling - self._current_fan = fan_speed - - def _control_heating(self): - """Send new target temperature to mytado.""" - if not self._active and None not in ( - self._cur_temp, self._target_temp): - self._active = True - _LOGGER.info("Obtained current and target temperature. " - "Tado thermostat active") - - if not self._active or self._current_operation == self._overlay_mode: - return - - if self._current_operation == CONST_MODE_SMART_SCHEDULE: - _LOGGER.info("Switching mytado.com to SCHEDULE (default) " - "for zone %s", self.zone_name) - self._store.reset_zone_overlay(self.zone_id) - self._overlay_mode = self._current_operation - return - - if self._current_operation == CONST_MODE_OFF: - _LOGGER.info("Switching mytado.com to OFF for zone %s", - self.zone_name) - self._store.set_zone_overlay(self.zone_id, CONST_OVERLAY_MANUAL) - self._overlay_mode = self._current_operation - return - - _LOGGER.info("Switching mytado.com to %s mode for zone %s", - self._current_operation, self.zone_name) - self._store.set_zone_overlay( - self.zone_id, self._current_operation, self._target_temp) - - self._overlay_mode = self._current_operation diff --git a/homeassistant/components/climate/tesla.py b/homeassistant/components/climate/tesla.py deleted file mode 100644 index ef5f2227c11c2..0000000000000 --- a/homeassistant/components/climate/tesla.py +++ /dev/null @@ -1,100 +0,0 @@ -""" -Support for Tesla HVAC system. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/climate.tesla/ -""" -import logging - -from homeassistant.components.climate import ( - ENTITY_ID_FORMAT, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, - ClimateDevice) -from homeassistant.components.tesla import DOMAIN as TESLA_DOMAIN -from homeassistant.components.tesla import TeslaDevice -from homeassistant.const import ( - ATTR_TEMPERATURE, STATE_OFF, STATE_ON, TEMP_CELSIUS, TEMP_FAHRENHEIT) - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['tesla'] - -OPERATION_LIST = [STATE_ON, STATE_OFF] - -SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Tesla climate platform.""" - devices = [TeslaThermostat(device, hass.data[TESLA_DOMAIN]['controller']) - for device in hass.data[TESLA_DOMAIN]['devices']['climate']] - add_entities(devices, True) - - -class TeslaThermostat(TeslaDevice, ClimateDevice): - """Representation of a Tesla climate.""" - - def __init__(self, tesla_device, controller): - """Initialize the Tesla device.""" - super().__init__(tesla_device, controller) - self.entity_id = ENTITY_ID_FORMAT.format(self.tesla_id) - self._target_temperature = None - self._temperature = None - - @property - def supported_features(self): - """Return the list of supported features.""" - return SUPPORT_FLAGS - - @property - def current_operation(self): - """Return current operation ie. On or Off.""" - mode = self.tesla_device.is_hvac_enabled() - if mode: - return OPERATION_LIST[0] # On - return OPERATION_LIST[1] # Off - - @property - def operation_list(self): - """List of available operation modes.""" - return OPERATION_LIST - - def update(self): - """Call by the Tesla device callback to update state.""" - _LOGGER.debug("Updating: %s", self._name) - self.tesla_device.update() - self._target_temperature = self.tesla_device.get_goal_temp() - self._temperature = self.tesla_device.get_current_temp() - - @property - def temperature_unit(self): - """Return the unit of measurement.""" - tesla_temp_units = self.tesla_device.measurement - - if tesla_temp_units == 'F': - return TEMP_FAHRENHEIT - return TEMP_CELSIUS - - @property - def current_temperature(self): - """Return the current temperature.""" - return self._temperature - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self._target_temperature - - def set_temperature(self, **kwargs): - """Set new target temperatures.""" - _LOGGER.debug("Setting temperature for: %s", self._name) - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature: - self.tesla_device.set_temperature(temperature) - - def set_operation_mode(self, operation_mode): - """Set HVAC mode (auto, cool, heat, off).""" - _LOGGER.debug("Setting mode for: %s", self._name) - if operation_mode == OPERATION_LIST[1]: # off - self.tesla_device.set_status(False) - elif operation_mode == OPERATION_LIST[0]: # heat - self.tesla_device.set_status(True) diff --git a/homeassistant/components/climate/toon.py b/homeassistant/components/climate/toon.py deleted file mode 100644 index 022a509ce06bd..0000000000000 --- a/homeassistant/components/climate/toon.py +++ /dev/null @@ -1,97 +0,0 @@ -""" -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/ -""" -from homeassistant.components.climate import ( - ATTR_TEMPERATURE, STATE_COOL, STATE_ECO, STATE_HEAT, STATE_AUTO, - SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, ClimateDevice) -import homeassistant.components.toon as toon_main -from homeassistant.const import TEMP_CELSIUS - -SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE - -HA_TOON = { - STATE_AUTO: 'Comfort', - STATE_HEAT: 'Home', - STATE_ECO: 'Away', - STATE_COOL: 'Sleep', -} -TOON_HA = {value: key for key, value in HA_TOON.items()} - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Toon climate device.""" - add_entities([ThermostatDevice(hass)], True) - - -class ThermostatDevice(ClimateDevice): - """Representation of a Toon climate device.""" - - def __init__(self, hass): - """Initialize the Toon climate device.""" - self._name = 'Toon van Eneco' - self.hass = hass - self.thermos = hass.data[toon_main.TOON_HANDLE] - - self._state = None - self._temperature = None - self._setpoint = None - self._operation_list = [ - STATE_AUTO, - STATE_HEAT, - STATE_ECO, - STATE_COOL, - ] - - @property - def supported_features(self): - """Return the list of supported features.""" - return SUPPORT_FLAGS - - @property - def name(self): - """Return the name of this thermostat.""" - return self._name - - @property - def temperature_unit(self): - """Return the unit of measurement used by the platform.""" - return TEMP_CELSIUS - - @property - def current_operation(self): - """Return current operation i.e. comfort, home, away.""" - return TOON_HA.get(self.thermos.get_data('state')) - - @property - def operation_list(self): - """Return a list of available operation modes.""" - return self._operation_list - - @property - def current_temperature(self): - """Return the current temperature.""" - return self.thermos.get_data('temp') - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self.thermos.get_data('setpoint') - - def set_temperature(self, **kwargs): - """Change the setpoint of the thermostat.""" - temp = kwargs.get(ATTR_TEMPERATURE) - self.thermos.set_temp(temp) - - def set_operation_mode(self, operation_mode): - """Set new operation mode.""" - self.thermos.set_state(HA_TOON[operation_mode]) - - def update(self): - """Update local state.""" - self.thermos.update() diff --git a/homeassistant/components/climate/tuya.py b/homeassistant/components/climate/tuya.py deleted file mode 100644 index 4548867a45ea9..0000000000000 --- a/homeassistant/components/climate/tuya.py +++ /dev/null @@ -1,167 +0,0 @@ -""" -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/ -""" - -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, - SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, ClimateDevice) -from homeassistant.components.fan import SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH -from homeassistant.components.tuya import DATA_TUYA, TuyaDevice - -from homeassistant.const import ( - PRECISION_WHOLE, TEMP_CELSIUS, TEMP_FAHRENHEIT) - -DEPENDENCIES = ['tuya'] -DEVICE_TYPE = 'climate' - -HA_STATE_TO_TUYA = { - STATE_AUTO: 'auto', - STATE_COOL: 'cold', - STATE_ECO: 'eco', - STATE_FAN_ONLY: 'wind', - STATE_HEAT: 'hot', -} - -TUYA_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_TUYA.items()} - -FAN_MODES = {SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH} - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Tuya Climate devices.""" - if discovery_info is None: - return - tuya = hass.data[DATA_TUYA] - dev_ids = discovery_info.get('dev_ids') - devices = [] - for dev_id in dev_ids: - device = tuya.get_device_by_id(dev_id) - if device is None: - continue - devices.append(TuyaClimateDevice(device)) - add_entities(devices) - - -class TuyaClimateDevice(TuyaDevice, ClimateDevice): - """Tuya climate devices,include air conditioner,heater.""" - - def __init__(self, tuya): - """Init climate device.""" - super().__init__(tuya) - self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id()) - self.operations = [] - - async def async_added_to_hass(self): - """Create operation list when add to hass.""" - await super().async_added_to_hass() - modes = self.tuya.operation_list() - if modes is None: - return - for mode in modes: - if mode in TUYA_STATE_TO_HA: - self.operations.append(TUYA_STATE_TO_HA[mode]) - - @property - def is_on(self): - """Return true if climate is on.""" - return self.tuya.state() - - @property - def precision(self): - """Return the precision of the system.""" - return PRECISION_WHOLE - - @property - def temperature_unit(self): - """Return the unit of measurement used by the platform.""" - unit = self.tuya.temperature_unit() - if unit == 'CELSIUS': - return TEMP_CELSIUS - if unit == 'FAHRENHEIT': - return TEMP_FAHRENHEIT - return TEMP_CELSIUS - - @property - def current_operation(self): - """Return current operation ie. heat, cool, idle.""" - mode = self.tuya.current_operation() - if mode is None: - return None - return TUYA_STATE_TO_HA.get(mode) - - @property - def operation_list(self): - """Return the list of available operation modes.""" - return self.operations - - @property - def current_temperature(self): - """Return the current temperature.""" - return self.tuya.current_temperature() - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self.tuya.target_temperature() - - @property - def target_temperature_step(self): - """Return the supported step of target temperature.""" - return self.tuya.target_temperature_step() - - @property - def current_fan_mode(self): - """Return the fan setting.""" - return self.tuya.current_fan_mode() - - @property - def fan_list(self): - """Return the list of available fan modes.""" - return self.tuya.fan_list() - - def set_temperature(self, **kwargs): - """Set new target temperature.""" - if ATTR_TEMPERATURE in kwargs: - self.tuya.set_temperature(kwargs[ATTR_TEMPERATURE]) - - def set_fan_mode(self, fan_mode): - """Set new target fan mode.""" - self.tuya.set_fan_mode(fan_mode) - - def set_operation_mode(self, operation_mode): - """Set new target operation mode.""" - self.tuya.set_operation_mode(HA_STATE_TO_TUYA.get(operation_mode)) - - def turn_on(self): - """Turn device on.""" - self.tuya.turn_on() - - def turn_off(self): - """Turn device off.""" - self.tuya.turn_off() - - @property - def supported_features(self): - """Return the list of supported features.""" - supports = SUPPORT_ON_OFF - if self.tuya.support_target_temperature(): - supports = supports | SUPPORT_TARGET_TEMPERATURE - if self.tuya.support_mode(): - supports = supports | SUPPORT_OPERATION_MODE - if self.tuya.support_wind_speed(): - supports = supports | SUPPORT_FAN_MODE - return supports - - @property - def min_temp(self): - """Return the minimum temperature.""" - return self.tuya.min_temp() - - @property - def max_temp(self): - """Return the maximum temperature.""" - return self.tuya.max_temp() diff --git a/homeassistant/components/climate/velbus.py b/homeassistant/components/climate/velbus.py deleted file mode 100644 index 0b0205acefb48..0000000000000 --- a/homeassistant/components/climate/velbus.py +++ /dev/null @@ -1,74 +0,0 @@ -""" -Support for Velbus thermostat. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/climate.velbus/ -""" -import logging - -from homeassistant.components.climate import ( - STATE_HEAT, SUPPORT_TARGET_TEMPERATURE, ClimateDevice) -from homeassistant.components.velbus import ( - DOMAIN as VELBUS_DOMAIN, VelbusEntity) -from homeassistant.const import ( - TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['velbus'] - -SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE) - - -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): - """Set up the Velbus thermostat platform.""" - if discovery_info is None: - return - - sensors = [] - for sensor in discovery_info: - module = hass.data[VELBUS_DOMAIN].get_module(sensor[0]) - channel = sensor[1] - sensors.append(VelbusClimate(module, channel)) - - async_add_entities(sensors) - - -class VelbusClimate(VelbusEntity, ClimateDevice): - """Representation of a Velbus thermostat.""" - - @property - def supported_features(self): - """Return the list off supported features.""" - return SUPPORT_FLAGS - - @property - def temperature_unit(self): - """Return the unit this state is expressed in.""" - if self._module.get_unit(self._channel) == '°C': - return TEMP_CELSIUS - return TEMP_FAHRENHEIT - - @property - def current_temperature(self): - """Return the current temperature.""" - return self._module.get_state(self._channel) - - @property - def current_operation(self): - """Return current operation.""" - return STATE_HEAT - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self._module.get_climate_target() - - def set_temperature(self, **kwargs): - """Set new target temperatures.""" - temp = kwargs.get(ATTR_TEMPERATURE) - if temp is None: - return - self._module.set_temp(temp) - self.schedule_update_ha_state() diff --git a/homeassistant/components/climate/vera.py b/homeassistant/components/climate/vera.py deleted file mode 100644 index 5e016b8666be6..0000000000000 --- a/homeassistant/components/climate/vera.py +++ /dev/null @@ -1,151 +0,0 @@ -""" -Support for Vera thermostats. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.vera/ -""" -import logging - -from homeassistant.util import convert -from homeassistant.components.climate import ( - ClimateDevice, STATE_AUTO, STATE_COOL, - STATE_HEAT, ENTITY_ID_FORMAT, SUPPORT_TARGET_TEMPERATURE, - SUPPORT_OPERATION_MODE, SUPPORT_FAN_MODE) -from homeassistant.const import ( - STATE_ON, - STATE_OFF, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, - ATTR_TEMPERATURE) - -from homeassistant.components.vera import ( - VERA_CONTROLLER, VERA_DEVICES, VeraDevice) - -DEPENDENCIES = ['vera'] - -_LOGGER = logging.getLogger(__name__) - -OPERATION_LIST = [STATE_HEAT, STATE_COOL, STATE_AUTO, STATE_OFF] -FAN_OPERATION_LIST = [STATE_ON, STATE_AUTO] - -SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | - SUPPORT_FAN_MODE) - - -def setup_platform(hass, config, add_entities_callback, discovery_info=None): - """Set up of Vera thermostats.""" - add_entities_callback( - [VeraThermostat(device, hass.data[VERA_CONTROLLER]) for - device in hass.data[VERA_DEVICES]['climate']], True) - - -class VeraThermostat(VeraDevice, ClimateDevice): - """Representation of a Vera Thermostat.""" - - def __init__(self, vera_device, controller): - """Initialize the Vera device.""" - VeraDevice.__init__(self, vera_device, controller) - self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) - - @property - def supported_features(self): - """Return the list of supported features.""" - return SUPPORT_FLAGS - - @property - def current_operation(self): - """Return current operation ie. heat, cool, idle.""" - mode = self.vera_device.get_hvac_mode() - if mode == 'HeatOn': - return OPERATION_LIST[0] # Heat - if mode == 'CoolOn': - return OPERATION_LIST[1] # Cool - if mode == 'AutoChangeOver': - return OPERATION_LIST[2] # Auto - if mode == 'Off': - return OPERATION_LIST[3] # Off - return 'Off' - - @property - def operation_list(self): - """Return the list of available operation modes.""" - return OPERATION_LIST - - @property - def current_fan_mode(self): - """Return the fan setting.""" - mode = self.vera_device.get_fan_mode() - if mode == "ContinuousOn": - return FAN_OPERATION_LIST[0] # on - if mode == "Auto": - return FAN_OPERATION_LIST[1] # auto - return "Auto" - - @property - def fan_list(self): - """Return a list of available fan modes.""" - return FAN_OPERATION_LIST - - def set_fan_mode(self, fan_mode): - """Set new target temperature.""" - if fan_mode == FAN_OPERATION_LIST[0]: - self.vera_device.fan_on() - else: - self.vera_device.fan_auto() - - @property - def current_power_w(self): - """Return the current power usage in W.""" - power = self.vera_device.power - if power: - return convert(power, float, 0.0) - - @property - def temperature_unit(self): - """Return the unit of measurement.""" - vera_temp_units = ( - self.vera_device.vera_controller.temperature_units) - - if vera_temp_units == 'F': - return TEMP_FAHRENHEIT - - return TEMP_CELSIUS - - @property - def current_temperature(self): - """Return the current temperature.""" - return self.vera_device.get_current_temperature() - - @property - def operation(self): - """Return current operation ie. heat, cool, idle.""" - return self.vera_device.get_hvac_state() - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self.vera_device.get_current_goal_temperature() - - def set_temperature(self, **kwargs): - """Set new target temperatures.""" - if kwargs.get(ATTR_TEMPERATURE) is not None: - self.vera_device.set_temperature(kwargs.get(ATTR_TEMPERATURE)) - - def set_operation_mode(self, operation_mode): - """Set HVAC mode (auto, cool, heat, off).""" - if operation_mode == OPERATION_LIST[3]: # off - self.vera_device.turn_off() - elif operation_mode == OPERATION_LIST[2]: # auto - self.vera_device.turn_auto_on() - elif operation_mode == OPERATION_LIST[1]: # cool - self.vera_device.turn_cool_on() - elif operation_mode == OPERATION_LIST[0]: # heat - self.vera_device.turn_heat_on() - - def turn_fan_on(self): - """Turn fan on.""" - self.vera_device.fan_on() - - def turn_fan_off(self): - """Turn fan off.""" - self.vera_device.fan_auto() diff --git a/homeassistant/components/climate/wink.py b/homeassistant/components/climate/wink.py deleted file mode 100644 index 7e5230ba3c702..0000000000000 --- a/homeassistant/components/climate/wink.py +++ /dev/null @@ -1,492 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.components.climate import ( - ATTR_CURRENT_HUMIDITY, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - ATTR_TEMPERATURE, STATE_AUTO, STATE_COOL, STATE_ECO, - STATE_FAN_ONLY, STATE_HEAT, SUPPORT_AUX_HEAT, - SUPPORT_AWAY_MODE, SUPPORT_FAN_MODE, - SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, - SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW, - ClimateDevice) -from homeassistant.components.wink import DOMAIN, WinkDevice -from homeassistant.const import ( - PRECISION_TENTHS, STATE_OFF, STATE_ON, STATE_UNKNOWN, TEMP_CELSIUS) -from homeassistant.helpers.temperature import display_temp as show_temp - -_LOGGER = logging.getLogger(__name__) - -ATTR_ECO_TARGET = 'eco_target' -ATTR_EXTERNAL_TEMPERATURE = 'external_temperature' -ATTR_OCCUPIED = 'occupied' -ATTR_SCHEDULE_ENABLED = 'schedule_enabled' -ATTR_SMART_TEMPERATURE = 'smart_temperature' -ATTR_TOTAL_CONSUMPTION = 'total_consumption' -ATTR_HEAT_ON = 'heat_on' -ATTR_COOL_ON = 'cool_on' - -DEPENDENCIES = ['wink'] - -SPEED_LOW = 'low' -SPEED_MEDIUM = 'medium' -SPEED_HIGH = 'high' - -HA_STATE_TO_WINK = { - STATE_AUTO: 'auto', - STATE_COOL: 'cool_only', - STATE_ECO: 'eco', - STATE_FAN_ONLY: 'fan_only', - STATE_HEAT: 'heat_only', - STATE_OFF: 'off', -} - -WINK_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_WINK.items()} - -SUPPORT_FLAGS_THERMOSTAT = ( - SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_HIGH | - SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_OPERATION_MODE | - SUPPORT_AWAY_MODE | SUPPORT_FAN_MODE | SUPPORT_AUX_HEAT) - -SUPPORT_FLAGS_AC = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | - SUPPORT_FAN_MODE) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Wink climate devices.""" - import pywink - for climate in pywink.get_thermostats(): - _id = climate.object_id() + climate.name() - if _id not in hass.data[DOMAIN]['unique_ids']: - add_entities([WinkThermostat(climate, hass)]) - for climate in pywink.get_air_conditioners(): - _id = climate.object_id() + climate.name() - if _id not in hass.data[DOMAIN]['unique_ids']: - add_entities([WinkAC(climate, hass)]) - - -class WinkThermostat(WinkDevice, ClimateDevice): - """Representation of a Wink thermostat.""" - - @property - def supported_features(self): - """Return the list of supported features.""" - return SUPPORT_FLAGS_THERMOSTAT - - async def async_added_to_hass(self): - """Call when entity is added to hass.""" - self.hass.data[DOMAIN]['entities']['climate'].append(self) - - @property - def temperature_unit(self): - """Return the unit of measurement.""" - # The Wink API always returns temp in Celsius - return TEMP_CELSIUS - - @property - def device_state_attributes(self): - """Return the optional device state attributes.""" - data = {} - target_temp_high = self.target_temperature_high - target_temp_low = self.target_temperature_low - if target_temp_high is not None: - data[ATTR_TARGET_TEMP_HIGH] = show_temp( - self.hass, self.target_temperature_high, self.temperature_unit, - PRECISION_TENTHS) - if target_temp_low is not None: - data[ATTR_TARGET_TEMP_LOW] = show_temp( - self.hass, self.target_temperature_low, self.temperature_unit, - PRECISION_TENTHS) - - if self.external_temperature is not None: - data[ATTR_EXTERNAL_TEMPERATURE] = show_temp( - self.hass, self.external_temperature, self.temperature_unit, - PRECISION_TENTHS) - - if self.smart_temperature: - data[ATTR_SMART_TEMPERATURE] = self.smart_temperature - - if self.occupied is not None: - data[ATTR_OCCUPIED] = self.occupied - - if self.eco_target is not None: - data[ATTR_ECO_TARGET] = self.eco_target - - if self.heat_on is not None: - data[ATTR_HEAT_ON] = self.heat_on - - if self.cool_on is not None: - data[ATTR_COOL_ON] = self.cool_on - - current_humidity = self.current_humidity - if current_humidity is not None: - data[ATTR_CURRENT_HUMIDITY] = current_humidity - - return data - - @property - def current_temperature(self): - """Return the current temperature.""" - return self.wink.current_temperature() - - @property - def current_humidity(self): - """Return the current humidity.""" - if self.wink.current_humidity() is not None: - # The API states humidity will be a float 0-1 - # the only example API response with humidity listed show an int - # This will address both possibilities - if self.wink.current_humidity() < 1: - return self.wink.current_humidity() * 100 - return self.wink.current_humidity() - return None - - @property - def external_temperature(self): - """Return the current external temperature.""" - return self.wink.current_external_temperature() - - @property - def smart_temperature(self): - """Return the current average temp of all remote sensor.""" - return self.wink.current_smart_temperature() - - @property - def eco_target(self): - """Return status of eco target (Is the thermostat in eco mode).""" - return self.wink.eco_target() - - @property - def occupied(self): - """Return status of if the thermostat has detected occupancy.""" - return self.wink.occupied() - - @property - def heat_on(self): - """Return whether or not the heat is actually heating.""" - return self.wink.heat_on() - - @property - def cool_on(self): - """Return whether or not the heat is actually heating.""" - return self.wink.cool_on() - - @property - def current_operation(self): - """Return current operation ie. heat, cool, idle.""" - if not self.wink.is_on(): - current_op = STATE_OFF - else: - current_op = WINK_STATE_TO_HA.get(self.wink.current_hvac_mode()) - if current_op == 'aux': - return STATE_HEAT - if current_op is None: - current_op = STATE_UNKNOWN - return current_op - - @property - def target_humidity(self): - """Return the humidity we try to reach.""" - target_hum = None - if self.wink.current_humidifier_mode() == 'on': - if self.wink.current_humidifier_set_point() is not None: - target_hum = self.wink.current_humidifier_set_point() * 100 - elif self.wink.current_dehumidifier_mode() == 'on': - if self.wink.current_dehumidifier_set_point() is not None: - target_hum = self.wink.current_dehumidifier_set_point() * 100 - else: - target_hum = None - return target_hum - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - if self.current_operation != STATE_AUTO and not self.is_away_mode_on: - if self.current_operation == STATE_COOL: - return self.wink.current_max_set_point() - if self.current_operation == STATE_HEAT: - return self.wink.current_min_set_point() - return None - - @property - def target_temperature_low(self): - """Return the lower bound temperature we try to reach.""" - if self.current_operation == STATE_AUTO: - return self.wink.current_min_set_point() - return None - - @property - def target_temperature_high(self): - """Return the higher bound temperature we try to reach.""" - if self.current_operation == STATE_AUTO: - return self.wink.current_max_set_point() - return None - - @property - def is_away_mode_on(self): - """Return if away mode is on.""" - return self.wink.away() - - @property - def is_aux_heat_on(self): - """Return true if aux heater.""" - if 'aux' not in self.wink.hvac_modes(): - return None - - if self.wink.current_hvac_mode() == 'aux': - return True - return False - - def set_temperature(self, **kwargs): - """Set new target temperature.""" - target_temp = kwargs.get(ATTR_TEMPERATURE) - target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) - target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) - if target_temp is not None: - if self.current_operation == STATE_COOL: - target_temp_high = target_temp - if self.current_operation == STATE_HEAT: - target_temp_low = target_temp - if target_temp_low is not None: - target_temp_low = target_temp_low - if target_temp_high is not None: - target_temp_high = target_temp_high - self.wink.set_temperature(target_temp_low, target_temp_high) - - def set_operation_mode(self, operation_mode): - """Set operation mode.""" - op_mode_to_set = HA_STATE_TO_WINK.get(operation_mode) - # The only way to disable aux heat is with the toggle - if self.is_aux_heat_on and op_mode_to_set == STATE_HEAT: - return - self.wink.set_operation_mode(op_mode_to_set) - - @property - def operation_list(self): - """List of available operation modes.""" - op_list = ['off'] - modes = self.wink.hvac_modes() - for mode in modes: - if mode == 'aux': - continue - ha_mode = WINK_STATE_TO_HA.get(mode) - if ha_mode is not None: - op_list.append(ha_mode) - else: - error = "Invalid operation mode mapping. " + mode + \ - " doesn't map. Please report this." - _LOGGER.error(error) - return op_list - - def turn_away_mode_on(self): - """Turn away on.""" - self.wink.set_away_mode() - - def turn_away_mode_off(self): - """Turn away off.""" - self.wink.set_away_mode(False) - - @property - def current_fan_mode(self): - """Return whether the fan is on.""" - if self.wink.current_fan_mode() == 'on': - return STATE_ON - if self.wink.current_fan_mode() == 'auto': - return STATE_AUTO - # No Fan available so disable slider - return None - - @property - def fan_list(self): - """List of available fan modes.""" - if self.wink.has_fan(): - return self.wink.fan_modes() - return None - - def set_fan_mode(self, fan_mode): - """Turn fan on/off.""" - self.wink.set_fan_mode(fan_mode.lower()) - - def turn_aux_heat_on(self): - """Turn auxiliary heater on.""" - self.wink.set_operation_mode('aux') - - def turn_aux_heat_off(self): - """Turn auxiliary heater off.""" - self.set_operation_mode(STATE_HEAT) - - @property - def min_temp(self): - """Return the minimum temperature.""" - minimum = 7 # Default minimum - min_min = self.wink.min_min_set_point() - min_max = self.wink.min_max_set_point() - if self.current_operation == STATE_HEAT: - if min_min: - return_value = min_min - else: - return_value = minimum - elif self.current_operation == STATE_COOL: - if min_max: - return_value = min_max - else: - return_value = minimum - elif self.current_operation == STATE_AUTO: - if min_min and min_max: - return_value = min(min_min, min_max) - else: - return_value = minimum - else: - return_value = minimum - return return_value - - @property - def max_temp(self): - """Return the maximum temperature.""" - maximum = 35 # Default maximum - max_min = self.wink.max_min_set_point() - max_max = self.wink.max_max_set_point() - if self.current_operation == STATE_HEAT: - if max_min: - return_value = max_min - else: - return_value = maximum - elif self.current_operation == STATE_COOL: - if max_max: - return_value = max_max - else: - return_value = maximum - elif self.current_operation == STATE_AUTO: - if max_min and max_max: - return_value = min(max_min, max_max) - else: - return_value = maximum - else: - return_value = maximum - return return_value - - -class WinkAC(WinkDevice, ClimateDevice): - """Representation of a Wink air conditioner.""" - - @property - def supported_features(self): - """Return the list of supported features.""" - return SUPPORT_FLAGS_AC - - @property - def temperature_unit(self): - """Return the unit of measurement.""" - # The Wink API always returns temp in Celsius - return TEMP_CELSIUS - - @property - def device_state_attributes(self): - """Return the optional device state attributes.""" - data = {} - target_temp_high = self.target_temperature_high - target_temp_low = self.target_temperature_low - if target_temp_high is not None: - data[ATTR_TARGET_TEMP_HIGH] = show_temp( - self.hass, self.target_temperature_high, self.temperature_unit, - PRECISION_TENTHS) - if target_temp_low is not None: - data[ATTR_TARGET_TEMP_LOW] = show_temp( - self.hass, self.target_temperature_low, self.temperature_unit, - PRECISION_TENTHS) - data[ATTR_TOTAL_CONSUMPTION] = self.wink.total_consumption() - data[ATTR_SCHEDULE_ENABLED] = self.wink.schedule_enabled() - - return data - - @property - def current_temperature(self): - """Return the current temperature.""" - return self.wink.current_temperature() - - @property - def current_operation(self): - """Return current operation ie. auto_eco, cool_only, fan_only.""" - if not self.wink.is_on(): - current_op = STATE_OFF - else: - wink_mode = self.wink.current_mode() - if wink_mode == "auto_eco": - wink_mode = "eco" - current_op = WINK_STATE_TO_HA.get(wink_mode) - if current_op is None: - current_op = STATE_UNKNOWN - return current_op - - @property - def operation_list(self): - """List of available operation modes.""" - op_list = ['off'] - modes = self.wink.modes() - for mode in modes: - if mode == "auto_eco": - mode = "eco" - ha_mode = WINK_STATE_TO_HA.get(mode) - if ha_mode is not None: - op_list.append(ha_mode) - else: - error = "Invalid operation mode mapping. " + mode + \ - " doesn't map. Please report this." - _LOGGER.error(error) - return op_list - - def set_temperature(self, **kwargs): - """Set new target temperature.""" - target_temp = kwargs.get(ATTR_TEMPERATURE) - self.wink.set_temperature(target_temp) - - def set_operation_mode(self, operation_mode): - """Set operation mode.""" - op_mode_to_set = HA_STATE_TO_WINK.get(operation_mode) - if op_mode_to_set == 'eco': - op_mode_to_set = 'auto_eco' - self.wink.set_operation_mode(op_mode_to_set) - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self.wink.current_max_set_point() - - @property - def current_fan_mode(self): - """ - Return the current fan mode. - - The official Wink app only supports 3 modes [low, medium, high] - which are equal to [0.33, 0.66, 1.0] respectively. - """ - speed = self.wink.current_fan_speed() - if speed <= 0.33: - return SPEED_LOW - if speed <= 0.66: - return SPEED_MEDIUM - return SPEED_HIGH - - @property - def fan_list(self): - """Return a list of available fan modes.""" - return [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] - - def set_fan_mode(self, fan_mode): - """ - Set fan speed. - - The official Wink app only supports 3 modes [low, medium, high] - which are equal to [0.33, 0.66, 1.0] respectively. - """ - if fan_mode == SPEED_LOW: - speed = 0.33 - elif fan_mode == SPEED_MEDIUM: - speed = 0.66 - elif fan_mode == SPEED_HIGH: - speed = 1.0 - self.wink.set_ac_fan_speed(speed) diff --git a/homeassistant/components/climate/zwave.py b/homeassistant/components/climate/zwave.py deleted file mode 100644 index 561af9c9f57b6..0000000000000 --- a/homeassistant/components/climate/zwave.py +++ /dev/null @@ -1,264 +0,0 @@ -""" -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/ -""" -# Because we do not compile openzwave on CI -import logging -from homeassistant.core import callback -from homeassistant.components.climate import ( - DOMAIN, ClimateDevice, STATE_AUTO, STATE_COOL, STATE_HEAT, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, - SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE) -from homeassistant.components.zwave import ZWaveDeviceEntity -from homeassistant.const import ( - STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) -from homeassistant.helpers.dispatcher import async_dispatcher_connect - -_LOGGER = logging.getLogger(__name__) - -CONF_NAME = 'name' -DEFAULT_NAME = 'Z-Wave Climate' - -REMOTEC = 0x5254 -REMOTEC_ZXT_120 = 0x8377 -REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120) -ATTR_OPERATING_STATE = 'operating_state' -ATTR_FAN_STATE = 'fan_state' - -WORKAROUND_ZXT_120 = 'zxt_120' - -DEVICE_MAPPINGS = { - REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120 -} - -STATE_MAPPINGS = { - 'Off': STATE_OFF, - 'Heat': STATE_HEAT, - 'Heat Mode': STATE_HEAT, - 'Heat (Default)': STATE_HEAT, - 'Cool': STATE_COOL, - 'Auto': STATE_AUTO, -} - - -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Old method of setting up Z-Wave climate devices.""" - pass - - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up Z-Wave Climate device from Config Entry.""" - @callback - def async_add_climate(climate): - """Add Z-Wave Climate Device.""" - async_add_entities([climate]) - - async_dispatcher_connect(hass, 'zwave_new_climate', async_add_climate) - - -def get_device(hass, values, **kwargs): - """Create Z-Wave entity device.""" - temp_unit = hass.config.units.temperature_unit - return ZWaveClimate(values, temp_unit) - - -class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): - """Representation of a Z-Wave Climate device.""" - - def __init__(self, values, temp_unit): - """Initialize the Z-Wave climate device.""" - ZWaveDeviceEntity.__init__(self, values, DOMAIN) - self._target_temperature = None - self._current_temperature = None - self._current_operation = None - self._operation_list = None - self._operation_mapping = None - self._operating_state = None - self._current_fan_mode = None - self._fan_list = None - self._fan_state = None - self._current_swing_mode = None - self._swing_list = None - self._unit = temp_unit - _LOGGER.debug("temp_unit is %s", self._unit) - self._zxt_120 = None - # Make sure that we have values for the key before converting to int - if (self.node.manufacturer_id.strip() and - self.node.product_id.strip()): - specific_sensor_key = ( - int(self.node.manufacturer_id, 16), - int(self.node.product_id, 16)) - if specific_sensor_key in DEVICE_MAPPINGS: - if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZXT_120: - _LOGGER.debug( - "Remotec ZXT-120 Zwave Thermostat workaround") - self._zxt_120 = 1 - self.update_properties() - - @property - def supported_features(self): - """Return the list of supported features.""" - support = SUPPORT_TARGET_TEMPERATURE - if self.values.fan_mode: - support |= SUPPORT_FAN_MODE - if self.values.mode: - support |= SUPPORT_OPERATION_MODE - if self._zxt_120 == 1 and self.values.zxt_120_swing_mode: - support |= SUPPORT_SWING_MODE - return support - - def update_properties(self): - """Handle the data changes for node values.""" - # Operation Mode - if self.values.mode: - self._operation_list = [] - self._operation_mapping = {} - operation_list = self.values.mode.data_items - if operation_list: - for mode in operation_list: - ha_mode = STATE_MAPPINGS.get(mode) - if ha_mode and ha_mode not in self._operation_mapping: - self._operation_mapping[ha_mode] = mode - self._operation_list.append(ha_mode) - continue - self._operation_list.append(mode) - current_mode = self.values.mode.data - self._current_operation = next( - (key for key, value in self._operation_mapping.items() - if value == current_mode), current_mode) - _LOGGER.debug("self._operation_list=%s", self._operation_list) - _LOGGER.debug("self._current_operation=%s", self._current_operation) - - # Current Temp - if self.values.temperature: - self._current_temperature = self.values.temperature.data - device_unit = self.values.temperature.units - if device_unit is not None: - self._unit = device_unit - - # Fan Mode - if self.values.fan_mode: - self._current_fan_mode = self.values.fan_mode.data - fan_list = self.values.fan_mode.data_items - if fan_list: - self._fan_list = list(fan_list) - _LOGGER.debug("self._fan_list=%s", self._fan_list) - _LOGGER.debug("self._current_fan_mode=%s", - self._current_fan_mode) - # Swing mode - if self._zxt_120 == 1: - if self.values.zxt_120_swing_mode: - self._current_swing_mode = self.values.zxt_120_swing_mode.data - swing_list = self.values.zxt_120_swing_mode.data_items - if swing_list: - self._swing_list = list(swing_list) - _LOGGER.debug("self._swing_list=%s", self._swing_list) - _LOGGER.debug("self._current_swing_mode=%s", - self._current_swing_mode) - # Set point - if self.values.primary.data == 0: - _LOGGER.debug("Setpoint is 0, setting default to " - "current_temperature=%s", - self._current_temperature) - if self._current_temperature is not None: - self._target_temperature = ( - round((float(self._current_temperature)), 1)) - else: - self._target_temperature = round( - (float(self.values.primary.data)), 1) - - # Operating state - if self.values.operating_state: - self._operating_state = self.values.operating_state.data - - # Fan operating state - if self.values.fan_state: - self._fan_state = self.values.fan_state.data - - @property - def current_fan_mode(self): - """Return the fan speed set.""" - return self._current_fan_mode - - @property - def fan_list(self): - """Return a list of available fan modes.""" - return self._fan_list - - @property - def current_swing_mode(self): - """Return the swing mode set.""" - return self._current_swing_mode - - @property - def swing_list(self): - """Return a list of available swing modes.""" - return self._swing_list - - @property - def temperature_unit(self): - """Return the unit of measurement.""" - if self._unit == 'C': - return TEMP_CELSIUS - if self._unit == 'F': - return TEMP_FAHRENHEIT - return self._unit - - @property - def current_temperature(self): - """Return the current temperature.""" - return self._current_temperature - - @property - def current_operation(self): - """Return the current operation mode.""" - return self._current_operation - - @property - def operation_list(self): - """Return a list of available operation modes.""" - return self._operation_list - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self._target_temperature - - def set_temperature(self, **kwargs): - """Set new target temperature.""" - if kwargs.get(ATTR_TEMPERATURE) is not None: - temperature = kwargs.get(ATTR_TEMPERATURE) - else: - return - - self.values.primary.data = temperature - - def set_fan_mode(self, fan_mode): - """Set new target fan mode.""" - if self.values.fan_mode: - self.values.fan_mode.data = fan_mode - - def set_operation_mode(self, operation_mode): - """Set new target operation mode.""" - if self.values.mode: - self.values.mode.data = self._operation_mapping.get( - operation_mode, operation_mode) - - def set_swing_mode(self, swing_mode): - """Set new target swing mode.""" - if self._zxt_120 == 1: - if self.values.zxt_120_swing_mode: - self.values.zxt_120_swing_mode.data = swing_mode - - @property - def device_state_attributes(self): - """Return the device specific state attributes.""" - data = super().device_state_attributes - if self._operating_state: - data[ATTR_OPERATING_STATE] = self._operating_state - if self._fan_state: - data[ATTR_FAN_STATE] = self._fan_state - return data diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 98e649e1742ee..c427657c76d47 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.py deleted file mode 100644 index ae400ca638569..0000000000000 --- a/homeassistant/components/cloudflare.py +++ /dev/null @@ -1,77 +0,0 @@ -""" -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/ -""" -from datetime import timedelta -import logging - -import voluptuous as vol - -from homeassistant.const import CONF_API_KEY, CONF_EMAIL, CONF_ZONE -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import track_time_interval - -REQUIREMENTS = ['pycfdns==0.0.1'] - -_LOGGER = logging.getLogger(__name__) - -CONF_RECORDS = 'records' - -DOMAIN = 'cloudflare' - -INTERVAL = timedelta(minutes=60) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_EMAIL): cv.string, - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_ZONE): cv.string, - vol.Required(CONF_RECORDS): vol.All(cv.ensure_list, [cv.string]), - }) -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the Cloudflare component.""" - from pycfdns import CloudflareUpdater - - cfupdate = CloudflareUpdater() - email = config[DOMAIN][CONF_EMAIL] - key = config[DOMAIN][CONF_API_KEY] - zone = config[DOMAIN][CONF_ZONE] - records = config[DOMAIN][CONF_RECORDS] - - def update_records_interval(now): - """Set up recurring update.""" - _update_cloudflare(cfupdate, email, key, zone, records) - - def update_records_service(now): - """Set up service for manual trigger.""" - _update_cloudflare(cfupdate, email, key, zone, records) - - track_time_interval(hass, update_records_interval, INTERVAL) - hass.services.register( - DOMAIN, 'update_records', update_records_service) - return True - - -def _update_cloudflare(cfupdate, email, key, zone, records): - """Update DNS records for a given zone.""" - _LOGGER.debug("Starting update for zone %s", zone) - - headers = cfupdate.set_header(email, key) - _LOGGER.debug("Header data defined as: %s", headers) - - zoneid = cfupdate.get_zoneID(headers, zone) - _LOGGER.debug("Zone ID is set to: %s", zoneid) - - update_records = cfupdate.get_recordInfo(headers, zoneid, zone, records) - _LOGGER.debug("Records: %s", update_records) - - result = cfupdate.update_records(headers, zoneid, update_records) - _LOGGER.debug("Update for zone %s is complete", zone) - - if result is not True: - _LOGGER.warning(result) diff --git a/homeassistant/components/cloudflare/__init__.py b/homeassistant/components/cloudflare/__init__.py new file mode 100644 index 0000000000000..363e7c5eeb11d --- /dev/null +++ b/homeassistant/components/cloudflare/__init__.py @@ -0,0 +1,72 @@ +"""Update the IP addresses of your Cloudflare DNS records.""" +from datetime import timedelta +import logging + +import voluptuous as vol + +from homeassistant.const import CONF_API_KEY, CONF_EMAIL, CONF_ZONE +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import track_time_interval + +REQUIREMENTS = ['pycfdns==0.0.1'] + +_LOGGER = logging.getLogger(__name__) + +CONF_RECORDS = 'records' + +DOMAIN = 'cloudflare' + +INTERVAL = timedelta(minutes=60) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_EMAIL): cv.string, + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_ZONE): cv.string, + vol.Required(CONF_RECORDS): vol.All(cv.ensure_list, [cv.string]), + }) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Cloudflare component.""" + from pycfdns import CloudflareUpdater + + cfupdate = CloudflareUpdater() + email = config[DOMAIN][CONF_EMAIL] + key = config[DOMAIN][CONF_API_KEY] + zone = config[DOMAIN][CONF_ZONE] + records = config[DOMAIN][CONF_RECORDS] + + def update_records_interval(now): + """Set up recurring update.""" + _update_cloudflare(cfupdate, email, key, zone, records) + + def update_records_service(now): + """Set up service for manual trigger.""" + _update_cloudflare(cfupdate, email, key, zone, records) + + track_time_interval(hass, update_records_interval, INTERVAL) + hass.services.register( + DOMAIN, 'update_records', update_records_service) + return True + + +def _update_cloudflare(cfupdate, email, key, zone, records): + """Update DNS records for a given zone.""" + _LOGGER.debug("Starting update for zone %s", zone) + + headers = cfupdate.set_header(email, key) + _LOGGER.debug("Header data defined as: %s", headers) + + zoneid = cfupdate.get_zoneID(headers, zone) + _LOGGER.debug("Zone ID is set to: %s", zoneid) + + update_records = cfupdate.get_recordInfo(headers, zoneid, zone, records) + _LOGGER.debug("Records: %s", update_records) + + result = cfupdate.update_records(headers, zoneid, update_records) + _LOGGER.debug("Update for zone %s is complete", zone) + + if result is not True: + _LOGGER.warning(result) diff --git a/homeassistant/components/coinbase.py b/homeassistant/components/coinbase.py deleted file mode 100644 index 98c321b9f5a43..0000000000000 --- a/homeassistant/components/coinbase.py +++ /dev/null @@ -1,100 +0,0 @@ -""" -Support for Coinbase. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/coinbase/ -""" -from datetime import timedelta -import logging - -import voluptuous as vol - -from homeassistant.const import CONF_API_KEY -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import load_platform -from homeassistant.util import Throttle - -REQUIREMENTS = ['coinbase==2.1.0'] - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'coinbase' - -CONF_API_SECRET = 'api_secret' -CONF_ACCOUNT_CURRENCIES = 'account_balance_currencies' -CONF_EXCHANGE_CURRENCIES = 'exchange_rate_currencies' - -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) - -DATA_COINBASE = 'coinbase_cache' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_API_SECRET): cv.string, - vol.Optional(CONF_ACCOUNT_CURRENCIES): - vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_EXCHANGE_CURRENCIES, default=[]): - vol.All(cv.ensure_list, [cv.string]) - }) -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the Coinbase component. - - Will automatically setup sensors to support - wallets discovered on the network. - """ - api_key = config[DOMAIN].get(CONF_API_KEY) - api_secret = config[DOMAIN].get(CONF_API_SECRET) - account_currencies = config[DOMAIN].get(CONF_ACCOUNT_CURRENCIES) - exchange_currencies = config[DOMAIN].get(CONF_EXCHANGE_CURRENCIES) - - hass.data[DATA_COINBASE] = coinbase_data = CoinbaseData( - api_key, api_secret) - - if not hasattr(coinbase_data, 'accounts'): - return False - for account in coinbase_data.accounts.data: - if (account_currencies is None or - account.currency in account_currencies): - load_platform(hass, - 'sensor', - DOMAIN, - {'account': account}, - config) - for currency in exchange_currencies: - if currency not in coinbase_data.exchange_rates.rates: - _LOGGER.warning("Currency %s not found", currency) - continue - native = coinbase_data.exchange_rates.currency - load_platform(hass, - 'sensor', - DOMAIN, - {'native_currency': native, - 'exchange_currency': currency}, - config) - - return True - - -class CoinbaseData: - """Get the latest data and update the states.""" - - def __init__(self, api_key, api_secret): - """Init the coinbase data object.""" - from coinbase.wallet.client import Client - self.client = Client(api_key, api_secret) - self.update() - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest data from coinbase.""" - from coinbase.wallet.error import AuthenticationError - try: - self.accounts = self.client.get_accounts() - self.exchange_rates = self.client.get_exchange_rates() - except AuthenticationError as coinbase_error: - _LOGGER.error("Authentication error connecting" - " to coinbase: %s", coinbase_error) diff --git a/homeassistant/components/coinbase/__init__.py b/homeassistant/components/coinbase/__init__.py new file mode 100644 index 0000000000000..40d04eadb3a79 --- /dev/null +++ b/homeassistant/components/coinbase/__init__.py @@ -0,0 +1,95 @@ +"""Support for Coinbase.""" +from datetime import timedelta +import logging + +import voluptuous as vol + +from homeassistant.const import CONF_API_KEY +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform +from homeassistant.util import Throttle + +REQUIREMENTS = ['coinbase==2.1.0'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'coinbase' + +CONF_API_SECRET = 'api_secret' +CONF_ACCOUNT_CURRENCIES = 'account_balance_currencies' +CONF_EXCHANGE_CURRENCIES = 'exchange_rate_currencies' + +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) + +DATA_COINBASE = 'coinbase_cache' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_API_SECRET): cv.string, + vol.Optional(CONF_ACCOUNT_CURRENCIES): + vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_EXCHANGE_CURRENCIES, default=[]): + vol.All(cv.ensure_list, [cv.string]) + }) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Coinbase component. + + Will automatically setup sensors to support + wallets discovered on the network. + """ + api_key = config[DOMAIN].get(CONF_API_KEY) + api_secret = config[DOMAIN].get(CONF_API_SECRET) + account_currencies = config[DOMAIN].get(CONF_ACCOUNT_CURRENCIES) + exchange_currencies = config[DOMAIN].get(CONF_EXCHANGE_CURRENCIES) + + hass.data[DATA_COINBASE] = coinbase_data = CoinbaseData( + api_key, api_secret) + + if not hasattr(coinbase_data, 'accounts'): + return False + for account in coinbase_data.accounts.data: + if (account_currencies is None or + account.currency in account_currencies): + load_platform(hass, + 'sensor', + DOMAIN, + {'account': account}, + config) + for currency in exchange_currencies: + if currency not in coinbase_data.exchange_rates.rates: + _LOGGER.warning("Currency %s not found", currency) + continue + native = coinbase_data.exchange_rates.currency + load_platform(hass, + 'sensor', + DOMAIN, + {'native_currency': native, + 'exchange_currency': currency}, + config) + + return True + + +class CoinbaseData: + """Get the latest data and update the states.""" + + def __init__(self, api_key, api_secret): + """Init the coinbase data object.""" + from coinbase.wallet.client import Client + self.client = Client(api_key, api_secret) + self.update() + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the latest data from coinbase.""" + from coinbase.wallet.error import AuthenticationError + try: + self.accounts = self.client.get_accounts() + self.exchange_rates = self.client.get_exchange_rates() + except AuthenticationError as coinbase_error: + _LOGGER.error("Authentication error connecting" + " to coinbase: %s", coinbase_error) diff --git a/homeassistant/components/comfoconnect.py b/homeassistant/components/comfoconnect.py deleted file mode 100644 index 69d88274f2965..0000000000000 --- a/homeassistant/components/comfoconnect.py +++ /dev/null @@ -1,134 +0,0 @@ -""" -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/ -""" -import logging - -import voluptuous as vol - -from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_PIN, CONF_TOKEN, EVENT_HOMEASSISTANT_STOP) -from homeassistant.helpers import discovery -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import dispatcher_send - -REQUIREMENTS = ['pycomfoconnect==0.3'] - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'comfoconnect' - -SIGNAL_COMFOCONNECT_UPDATE_RECEIVED = 'comfoconnect_update_received' - -ATTR_CURRENT_TEMPERATURE = 'current_temperature' -ATTR_CURRENT_HUMIDITY = 'current_humidity' -ATTR_OUTSIDE_TEMPERATURE = 'outside_temperature' -ATTR_OUTSIDE_HUMIDITY = 'outside_humidity' -ATTR_AIR_FLOW_SUPPLY = 'air_flow_supply' -ATTR_AIR_FLOW_EXHAUST = 'air_flow_exhaust' - -CONF_USER_AGENT = 'user_agent' - -DEFAULT_NAME = 'ComfoAirQ' -DEFAULT_PIN = 0 -DEFAULT_TOKEN = '00000000000000000000000000000001' -DEFAULT_USER_AGENT = 'Home Assistant' - -DEVICE = None - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_TOKEN, default=DEFAULT_TOKEN): - vol.Length(min=32, max=32, msg='invalid token'), - vol.Optional(CONF_USER_AGENT, default=DEFAULT_USER_AGENT): cv.string, - vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.positive_int, - }), -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the ComfoConnect bridge.""" - from pycomfoconnect import (Bridge) - - conf = config[DOMAIN] - host = conf.get(CONF_HOST) - name = conf.get(CONF_NAME) - token = conf.get(CONF_TOKEN) - user_agent = conf.get(CONF_USER_AGENT) - pin = conf.get(CONF_PIN) - - # Run discovery on the configured ip - bridges = Bridge.discover(host) - if not bridges: - _LOGGER.error("Could not connect to ComfoConnect bridge on %s", host) - return False - bridge = bridges[0] - _LOGGER.info("Bridge found: %s (%s)", bridge.uuid.hex(), bridge.host) - - # Setup ComfoConnect Bridge - ccb = ComfoConnectBridge(hass, bridge, name, token, user_agent, pin) - hass.data[DOMAIN] = ccb - - # Start connection with bridge - ccb.connect() - - # Schedule disconnect on shutdown - def _shutdown(_event): - ccb.disconnect() - - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown) - - # Load platforms - discovery.load_platform(hass, 'fan', DOMAIN, {}, config) - - return True - - -class ComfoConnectBridge: - """Representation of a ComfoConnect bridge.""" - - def __init__(self, hass, bridge, name, token, friendly_name, pin): - """Initialize the ComfoConnect bridge.""" - from pycomfoconnect import (ComfoConnect) - - self.data = {} - self.name = name - self.hass = hass - - self.comfoconnect = ComfoConnect( - bridge=bridge, local_uuid=bytes.fromhex(token), - local_devicename=friendly_name, pin=pin) - self.comfoconnect.callback_sensor = self.sensor_callback - - def connect(self): - """Connect with the bridge.""" - _LOGGER.debug("Connecting with bridge") - self.comfoconnect.connect(True) - - def disconnect(self): - """Disconnect from the bridge.""" - _LOGGER.debug("Disconnecting from bridge") - self.comfoconnect.disconnect() - - def sensor_callback(self, var, value): - """Call function for sensor updates.""" - _LOGGER.debug("Got value from bridge: %d = %d", var, value) - - from pycomfoconnect import ( - SENSOR_TEMPERATURE_EXTRACT, SENSOR_TEMPERATURE_OUTDOOR) - - if var in [SENSOR_TEMPERATURE_EXTRACT, SENSOR_TEMPERATURE_OUTDOOR]: - self.data[var] = value / 10 - else: - self.data[var] = value - - # Notify listeners that we have received an update - dispatcher_send(self.hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, var) - - def subscribe_sensor(self, sensor_id): - """Subscribe for the specified sensor.""" - self.comfoconnect.register_sensor(sensor_id) diff --git a/homeassistant/components/comfoconnect/__init__.py b/homeassistant/components/comfoconnect/__init__.py new file mode 100644 index 0000000000000..64ebec1854590 --- /dev/null +++ b/homeassistant/components/comfoconnect/__init__.py @@ -0,0 +1,129 @@ +"""Support to control a Zehnder ComfoAir Q350/450/600 ventilation unit.""" +import logging + +import voluptuous as vol + +from homeassistant.const import ( + CONF_HOST, CONF_NAME, CONF_PIN, CONF_TOKEN, EVENT_HOMEASSISTANT_STOP) +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import dispatcher_send + +REQUIREMENTS = ['pycomfoconnect==0.3'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'comfoconnect' + +SIGNAL_COMFOCONNECT_UPDATE_RECEIVED = 'comfoconnect_update_received' + +ATTR_CURRENT_TEMPERATURE = 'current_temperature' +ATTR_CURRENT_HUMIDITY = 'current_humidity' +ATTR_OUTSIDE_TEMPERATURE = 'outside_temperature' +ATTR_OUTSIDE_HUMIDITY = 'outside_humidity' +ATTR_AIR_FLOW_SUPPLY = 'air_flow_supply' +ATTR_AIR_FLOW_EXHAUST = 'air_flow_exhaust' + +CONF_USER_AGENT = 'user_agent' + +DEFAULT_NAME = 'ComfoAirQ' +DEFAULT_PIN = 0 +DEFAULT_TOKEN = '00000000000000000000000000000001' +DEFAULT_USER_AGENT = 'Home Assistant' + +DEVICE = None + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_TOKEN, default=DEFAULT_TOKEN): + vol.Length(min=32, max=32, msg='invalid token'), + vol.Optional(CONF_USER_AGENT, default=DEFAULT_USER_AGENT): cv.string, + vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.positive_int, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the ComfoConnect bridge.""" + from pycomfoconnect import (Bridge) + + conf = config[DOMAIN] + host = conf.get(CONF_HOST) + name = conf.get(CONF_NAME) + token = conf.get(CONF_TOKEN) + user_agent = conf.get(CONF_USER_AGENT) + pin = conf.get(CONF_PIN) + + # Run discovery on the configured ip + bridges = Bridge.discover(host) + if not bridges: + _LOGGER.error("Could not connect to ComfoConnect bridge on %s", host) + return False + bridge = bridges[0] + _LOGGER.info("Bridge found: %s (%s)", bridge.uuid.hex(), bridge.host) + + # Setup ComfoConnect Bridge + ccb = ComfoConnectBridge(hass, bridge, name, token, user_agent, pin) + hass.data[DOMAIN] = ccb + + # Start connection with bridge + ccb.connect() + + # Schedule disconnect on shutdown + def _shutdown(_event): + ccb.disconnect() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown) + + # Load platforms + discovery.load_platform(hass, 'fan', DOMAIN, {}, config) + + return True + + +class ComfoConnectBridge: + """Representation of a ComfoConnect bridge.""" + + def __init__(self, hass, bridge, name, token, friendly_name, pin): + """Initialize the ComfoConnect bridge.""" + from pycomfoconnect import (ComfoConnect) + + self.data = {} + self.name = name + self.hass = hass + + self.comfoconnect = ComfoConnect( + bridge=bridge, local_uuid=bytes.fromhex(token), + local_devicename=friendly_name, pin=pin) + self.comfoconnect.callback_sensor = self.sensor_callback + + def connect(self): + """Connect with the bridge.""" + _LOGGER.debug("Connecting with bridge") + self.comfoconnect.connect(True) + + def disconnect(self): + """Disconnect from the bridge.""" + _LOGGER.debug("Disconnecting from bridge") + self.comfoconnect.disconnect() + + def sensor_callback(self, var, value): + """Call function for sensor updates.""" + _LOGGER.debug("Got value from bridge: %d = %d", var, value) + + from pycomfoconnect import ( + SENSOR_TEMPERATURE_EXTRACT, SENSOR_TEMPERATURE_OUTDOOR) + + if var in [SENSOR_TEMPERATURE_EXTRACT, SENSOR_TEMPERATURE_OUTDOOR]: + self.data[var] = value / 10 + else: + self.data[var] = value + + # Notify listeners that we have received an update + dispatcher_send(self.hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, var) + + def subscribe_sensor(self, sensor_id): + """Subscribe for the specified sensor.""" + self.comfoconnect.register_sensor(sensor_id) diff --git a/homeassistant/components/comfoconnect/fan.py b/homeassistant/components/comfoconnect/fan.py new file mode 100644 index 0000000000000..a396dd276a544 --- /dev/null +++ b/homeassistant/components/comfoconnect/fan.py @@ -0,0 +1,111 @@ +"""Platform to control a Zehnder ComfoAir Q350/450/600 ventilation unit.""" +import logging + +from homeassistant.components.comfoconnect import ( + DOMAIN, ComfoConnectBridge, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED) +from homeassistant.components.fan import ( + FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, + SUPPORT_SET_SPEED) +from homeassistant.helpers.dispatcher import (dispatcher_connect) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['comfoconnect'] + +SPEED_MAPPING = { + 0: SPEED_OFF, + 1: SPEED_LOW, + 2: SPEED_MEDIUM, + 3: SPEED_HIGH +} + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the ComfoConnect fan platform.""" + ccb = hass.data[DOMAIN] + + add_entities([ComfoConnectFan(hass, name=ccb.name, ccb=ccb)], True) + + +class ComfoConnectFan(FanEntity): + """Representation of the ComfoConnect fan platform.""" + + def __init__(self, hass, name, ccb: ComfoConnectBridge) -> None: + """Initialize the ComfoConnect fan.""" + from pycomfoconnect import SENSOR_FAN_SPEED_MODE + + self._ccb = ccb + self._name = name + + # Ask the bridge to keep us updated + self._ccb.comfoconnect.register_sensor(SENSOR_FAN_SPEED_MODE) + + def _handle_update(var): + if var == SENSOR_FAN_SPEED_MODE: + _LOGGER.debug("Dispatcher update for %s", var) + self.schedule_update_ha_state() + + # Register for dispatcher updates + dispatcher_connect( + hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, _handle_update) + + @property + def name(self): + """Return the name of the fan.""" + return self._name + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return 'mdi:air-conditioner' + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return SUPPORT_SET_SPEED + + @property + def speed(self): + """Return the current fan mode.""" + from pycomfoconnect import (SENSOR_FAN_SPEED_MODE) + + try: + speed = self._ccb.data[SENSOR_FAN_SPEED_MODE] + return SPEED_MAPPING[speed] + except KeyError: + return None + + @property + def speed_list(self): + """List of available fan modes.""" + return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + + def turn_on(self, speed: str = None, **kwargs) -> None: + """Turn on the fan.""" + if speed is None: + speed = SPEED_LOW + self.set_speed(speed) + + def turn_off(self, **kwargs) -> None: + """Turn off the fan (to away).""" + self.set_speed(SPEED_OFF) + + def set_speed(self, speed: str): + """Set fan speed.""" + _LOGGER.debug('Changing fan speed to %s.', speed) + + from pycomfoconnect import ( + CMD_FAN_MODE_AWAY, CMD_FAN_MODE_LOW, CMD_FAN_MODE_MEDIUM, + CMD_FAN_MODE_HIGH) + + if speed == SPEED_OFF: + self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_AWAY) + elif speed == SPEED_LOW: + self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_LOW) + elif speed == SPEED_MEDIUM: + self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_MEDIUM) + elif speed == SPEED_HIGH: + self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_HIGH) + + # Update current mode + self.schedule_update_ha_state() diff --git a/homeassistant/components/comfoconnect/sensor.py b/homeassistant/components/comfoconnect/sensor.py new file mode 100644 index 0000000000000..ac5de866cfb71 --- /dev/null +++ b/homeassistant/components/comfoconnect/sensor.py @@ -0,0 +1,134 @@ +"""Platform to control a Zehnder ComfoAir Q350/450/600 ventilation unit.""" +import logging + +from homeassistant.components.comfoconnect import ( + DOMAIN, ComfoConnectBridge, ATTR_CURRENT_TEMPERATURE, + ATTR_CURRENT_HUMIDITY, ATTR_OUTSIDE_TEMPERATURE, + ATTR_OUTSIDE_HUMIDITY, ATTR_AIR_FLOW_SUPPLY, + ATTR_AIR_FLOW_EXHAUST, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED) +from homeassistant.const import CONF_RESOURCES, TEMP_CELSIUS +from homeassistant.helpers.dispatcher import dispatcher_connect +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['comfoconnect'] + +SENSOR_TYPES = {} + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the ComfoConnect fan platform.""" + from pycomfoconnect import ( + SENSOR_TEMPERATURE_EXTRACT, SENSOR_HUMIDITY_EXTRACT, + SENSOR_TEMPERATURE_OUTDOOR, SENSOR_HUMIDITY_OUTDOOR, + SENSOR_FAN_SUPPLY_FLOW, SENSOR_FAN_EXHAUST_FLOW) + + global SENSOR_TYPES + SENSOR_TYPES = { + ATTR_CURRENT_TEMPERATURE: [ + 'Inside Temperature', + TEMP_CELSIUS, + 'mdi:thermometer', + SENSOR_TEMPERATURE_EXTRACT + ], + ATTR_CURRENT_HUMIDITY: [ + 'Inside Humidity', + '%', + 'mdi:water-percent', + SENSOR_HUMIDITY_EXTRACT + ], + ATTR_OUTSIDE_TEMPERATURE: [ + 'Outside Temperature', + TEMP_CELSIUS, + 'mdi:thermometer', + SENSOR_TEMPERATURE_OUTDOOR + ], + ATTR_OUTSIDE_HUMIDITY: [ + 'Outside Humidity', + '%', + 'mdi:water-percent', + SENSOR_HUMIDITY_OUTDOOR + ], + ATTR_AIR_FLOW_SUPPLY: [ + 'Supply airflow', + 'm³/h', + 'mdi:air-conditioner', + SENSOR_FAN_SUPPLY_FLOW + ], + ATTR_AIR_FLOW_EXHAUST: [ + 'Exhaust airflow', + 'm³/h', + 'mdi:air-conditioner', + SENSOR_FAN_EXHAUST_FLOW + ], + } + + ccb = hass.data[DOMAIN] + + sensors = [] + for resource in config[CONF_RESOURCES]: + sensor_type = resource.lower() + + if sensor_type not in SENSOR_TYPES: + _LOGGER.warning("Sensor type: %s is not a valid sensor.", + sensor_type) + continue + + sensors.append( + ComfoConnectSensor( + hass, + name="%s %s" % (ccb.name, SENSOR_TYPES[sensor_type][0]), + ccb=ccb, + sensor_type=sensor_type + ) + ) + + add_entities(sensors, True) + + +class ComfoConnectSensor(Entity): + """Representation of a ComfoConnect sensor.""" + + def __init__(self, hass, name, ccb: ComfoConnectBridge, + sensor_type) -> None: + """Initialize the ComfoConnect sensor.""" + self._ccb = ccb + self._sensor_type = sensor_type + self._sensor_id = SENSOR_TYPES[self._sensor_type][3] + self._name = name + + # Register the requested sensor + self._ccb.comfoconnect.register_sensor(self._sensor_id) + + def _handle_update(var): + if var == self._sensor_id: + _LOGGER.debug('Dispatcher update for %s.', var) + self.schedule_update_ha_state() + + # Register for dispatcher updates + dispatcher_connect( + hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, _handle_update) + + @property + def state(self): + """Return the state of the entity.""" + try: + return self._ccb.data[self._sensor_id] + except KeyError: + return None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return SENSOR_TYPES[self._sensor_type][2] + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return SENSOR_TYPES[self._sensor_type][1] diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index 7836cba6cf9da..47ac9d3a4b226 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -7,7 +7,6 @@ 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 b7a8c9c070ab6..ec9d9d0ff27cd 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 3adc6f1423388..640e267d06613 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 57123ee12de38..f29dde4594c2a 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 d8d386f5ca048..7082cb367264f 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 aeef2818f6368..ab7ada618fed8 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 b5b2a91b09789..bd003f1ad6703 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -13,7 +13,8 @@ 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/cover/abode.py b/homeassistant/components/cover/abode.py deleted file mode 100644 index 3ba3fb118f321..0000000000000 --- a/homeassistant/components/cover/abode.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN -from homeassistant.components.cover import CoverDevice - - -DEPENDENCIES = ['abode'] - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Abode cover devices.""" - import abodepy.helpers.constants as CONST - - data = hass.data[ABODE_DOMAIN] - - devices = [] - for device in data.abode.get_devices(generic_type=CONST.TYPE_COVER): - if data.is_excluded(device): - continue - - devices.append(AbodeCover(data, device)) - - data.devices.extend(devices) - - add_entities(devices) - - -class AbodeCover(AbodeDevice, CoverDevice): - """Representation of an Abode cover.""" - - @property - def is_closed(self): - """Return true if cover is closed, else False.""" - return not self._device.is_open - - def close_cover(self, **kwargs): - """Issue close command to cover.""" - self._device.close_cover() - - def open_cover(self, **kwargs): - """Issue open command to cover.""" - self._device.open_cover() diff --git a/homeassistant/components/cover/fibaro.py b/homeassistant/components/cover/fibaro.py deleted file mode 100644 index d47dbb2031541..0000000000000 --- a/homeassistant/components/cover/fibaro.py +++ /dev/null @@ -1,92 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.components.cover import ( - CoverDevice, ENTITY_ID_FORMAT, ATTR_POSITION, ATTR_TILT_POSITION) -from homeassistant.components.fibaro import ( - FIBARO_DEVICES, FibaroDevice) - -DEPENDENCIES = ['fibaro'] - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Fibaro covers.""" - if discovery_info is None: - return - - add_entities( - [FibaroCover(device) for - device in hass.data[FIBARO_DEVICES]['cover']], True) - - -class FibaroCover(FibaroDevice, CoverDevice): - """Representation a Fibaro Cover.""" - - def __init__(self, fibaro_device): - """Initialize the Vera device.""" - super().__init__(fibaro_device) - self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) - - @staticmethod - def bound(position): - """Normalize the position.""" - if position is None: - return None - position = int(position) - if position <= 5: - return 0 - if position >= 95: - return 100 - return position - - @property - def current_cover_position(self): - """Return current position of cover. 0 is closed, 100 is open.""" - return self.bound(self.level) - - @property - def current_cover_tilt_position(self): - """Return the current tilt position for venetian blinds.""" - return self.bound(self.level2) - - def set_cover_position(self, **kwargs): - """Move the cover to a specific position.""" - self.set_level(kwargs.get(ATTR_POSITION)) - - def set_cover_tilt_position(self, **kwargs): - """Move the cover to a specific position.""" - self.set_level2(kwargs.get(ATTR_TILT_POSITION)) - - @property - def is_closed(self): - """Return if the cover is closed.""" - if self.current_cover_position is None: - return None - return self.current_cover_position == 0 - - def open_cover(self, **kwargs): - """Open the cover.""" - self.action("open") - - def close_cover(self, **kwargs): - """Close the cover.""" - self.action("close") - - def open_cover_tilt(self, **kwargs): - """Open the cover tilt.""" - self.set_level2(100) - - def close_cover_tilt(self, **kwargs): - """Close the cover.""" - self.set_level2(0) - - def stop_cover(self, **kwargs): - """Stop the cover.""" - self.action("stop") diff --git a/homeassistant/components/cover/homematicip_cloud.py b/homeassistant/components/cover/homematicip_cloud.py deleted file mode 100644 index 27f26805e81db..0000000000000 --- a/homeassistant/components/cover/homematicip_cloud.py +++ /dev/null @@ -1,70 +0,0 @@ -""" -Support for HomematicIP Cloud cover devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/cover.homematicip_cloud/ -""" -import logging - -from homeassistant.components.cover import ( - ATTR_POSITION, CoverDevice) -from homeassistant.components.homematicip_cloud import ( - HMIPC_HAPID, HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN) - -DEPENDENCIES = ['homematicip_cloud'] - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): - """Set up the HomematicIP Cloud cover devices.""" - pass - - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up the HomematicIP cover from a config entry.""" - from homematicip.aio.device import AsyncFullFlushShutter - - home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home - devices = [] - for device in home.devices: - if isinstance(device, AsyncFullFlushShutter): - devices.append(HomematicipCoverShutter(home, device)) - - if devices: - async_add_entities(devices) - - -class HomematicipCoverShutter(HomematicipGenericDevice, CoverDevice): - """Representation of a HomematicIP Cloud cover device.""" - - @property - def current_cover_position(self): - """Return current position of cover.""" - return int(self._device.shutterLevel * 100) - - async def async_set_cover_position(self, **kwargs): - """Move the cover to a specific position.""" - position = kwargs[ATTR_POSITION] - level = 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 None - - async def async_open_cover(self, **kwargs): - """Open the cover.""" - await self._device.set_shutter_level(1) - - async def async_close_cover(self, **kwargs): - """Close the cover.""" - await self._device.set_shutter_level(0) - - async def async_stop_cover(self, **kwargs): - """Stop the device if in motion.""" - await self._device.set_shutter_stop() diff --git a/homeassistant/components/cover/knx.py b/homeassistant/components/cover/knx.py deleted file mode 100644 index 4173db5f45049..0000000000000 --- a/homeassistant/components/cover/knx.py +++ /dev/null @@ -1,204 +0,0 @@ -""" -Support for KNX/IP covers. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/cover.knx/ -""" - -import voluptuous as vol - -from homeassistant.components.cover import ( - ATTR_POSITION, ATTR_TILT_POSITION, PLATFORM_SCHEMA, SUPPORT_CLOSE, - SUPPORT_OPEN, SUPPORT_SET_POSITION, SUPPORT_SET_TILT_POSITION, - SUPPORT_STOP, CoverDevice) -from homeassistant.components.knx import ATTR_DISCOVER_DEVICES, DATA_KNX -from homeassistant.const import CONF_NAME -from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import async_track_utc_time_change - -CONF_MOVE_LONG_ADDRESS = 'move_long_address' -CONF_MOVE_SHORT_ADDRESS = 'move_short_address' -CONF_POSITION_ADDRESS = 'position_address' -CONF_POSITION_STATE_ADDRESS = 'position_state_address' -CONF_ANGLE_ADDRESS = 'angle_address' -CONF_ANGLE_STATE_ADDRESS = 'angle_state_address' -CONF_TRAVELLING_TIME_DOWN = 'travelling_time_down' -CONF_TRAVELLING_TIME_UP = 'travelling_time_up' -CONF_INVERT_POSITION = 'invert_position' -CONF_INVERT_ANGLE = 'invert_angle' - -DEFAULT_TRAVEL_TIME = 25 -DEFAULT_NAME = 'KNX Cover' -DEPENDENCIES = ['knx'] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_MOVE_LONG_ADDRESS): cv.string, - vol.Optional(CONF_MOVE_SHORT_ADDRESS): cv.string, - vol.Optional(CONF_POSITION_ADDRESS): cv.string, - vol.Optional(CONF_POSITION_STATE_ADDRESS): cv.string, - vol.Optional(CONF_ANGLE_ADDRESS): cv.string, - vol.Optional(CONF_ANGLE_STATE_ADDRESS): cv.string, - vol.Optional(CONF_TRAVELLING_TIME_DOWN, default=DEFAULT_TRAVEL_TIME): - cv.positive_int, - vol.Optional(CONF_TRAVELLING_TIME_UP, default=DEFAULT_TRAVEL_TIME): - cv.positive_int, - vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean, - vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean, -}) - - -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) - else: - async_add_entities_config(hass, config, async_add_entities) - - -@callback -def async_add_entities_discovery(hass, discovery_info, async_add_entities): - """Set up covers 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(KNXCover(device)) - async_add_entities(entities) - - -@callback -def async_add_entities_config(hass, config, async_add_entities): - """Set up cover for KNX platform configured within platform.""" - import xknx - cover = xknx.devices.Cover( - hass.data[DATA_KNX].xknx, - name=config.get(CONF_NAME), - group_address_long=config.get(CONF_MOVE_LONG_ADDRESS), - group_address_short=config.get(CONF_MOVE_SHORT_ADDRESS), - group_address_position_state=config.get( - CONF_POSITION_STATE_ADDRESS), - group_address_angle=config.get(CONF_ANGLE_ADDRESS), - group_address_angle_state=config.get(CONF_ANGLE_STATE_ADDRESS), - group_address_position=config.get(CONF_POSITION_ADDRESS), - travel_time_down=config.get(CONF_TRAVELLING_TIME_DOWN), - travel_time_up=config.get(CONF_TRAVELLING_TIME_UP), - invert_position=config.get(CONF_INVERT_POSITION), - invert_angle=config.get(CONF_INVERT_ANGLE)) - - hass.data[DATA_KNX].xknx.devices.add(cover) - async_add_entities([KNXCover(cover)]) - - -class KNXCover(CoverDevice): - """Representation of a KNX cover.""" - - def __init__(self, device): - """Initialize the cover.""" - self.device = device - self._unsubscribe_auto_updater = None - - @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 supported_features(self): - """Flag supported features.""" - supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | \ - SUPPORT_SET_POSITION | SUPPORT_STOP - if self.device.supports_angle: - supported_features |= SUPPORT_SET_TILT_POSITION - return supported_features - - @property - def current_cover_position(self): - """Return the current position of the cover.""" - return self.device.current_position() - - @property - def is_closed(self): - """Return if the cover is closed.""" - return self.device.is_closed() - - async def async_close_cover(self, **kwargs): - """Close the cover.""" - if not self.device.is_closed(): - await self.device.set_down() - self.start_auto_updater() - - async def async_open_cover(self, **kwargs): - """Open the cover.""" - if not self.device.is_open(): - await self.device.set_up() - self.start_auto_updater() - - async def async_set_cover_position(self, **kwargs): - """Move the cover to a specific position.""" - if ATTR_POSITION in kwargs: - position = kwargs[ATTR_POSITION] - await self.device.set_position(position) - self.start_auto_updater() - - async def async_stop_cover(self, **kwargs): - """Stop the cover.""" - await self.device.stop() - self.stop_auto_updater() - - @property - def current_cover_tilt_position(self): - """Return current tilt position of cover.""" - if not self.device.supports_angle: - return None - return self.device.current_angle() - - async def async_set_cover_tilt_position(self, **kwargs): - """Move the cover tilt to a specific position.""" - if ATTR_TILT_POSITION in kwargs: - tilt_position = kwargs[ATTR_TILT_POSITION] - await self.device.set_angle(tilt_position) - - def start_auto_updater(self): - """Start the autoupdater to update HASS while cover is moving.""" - if self._unsubscribe_auto_updater is None: - self._unsubscribe_auto_updater = async_track_utc_time_change( - self.hass, self.auto_updater_hook) - - def stop_auto_updater(self): - """Stop the autoupdater.""" - if self._unsubscribe_auto_updater is not None: - self._unsubscribe_auto_updater() - self._unsubscribe_auto_updater = None - - @callback - def auto_updater_hook(self, now): - """Call for the autoupdater.""" - self.async_schedule_update_ha_state() - if self.device.position_reached(): - self.stop_auto_updater() - - self.hass.add_job(self.device.auto_stop_if_necessary()) diff --git a/homeassistant/components/cover/lutron.py b/homeassistant/components/cover/lutron.py deleted file mode 100644 index 7ea7abf882d2c..0000000000000 --- a/homeassistant/components/cover/lutron.py +++ /dev/null @@ -1,75 +0,0 @@ -""" -Support for Lutron shades. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/cover.lutron/ -""" -import logging - -from homeassistant.components.cover import ( - CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_SET_POSITION, - ATTR_POSITION) -from homeassistant.components.lutron import ( - LutronDevice, LUTRON_DEVICES, LUTRON_CONTROLLER) - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['lutron'] - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Lutron shades.""" - devs = [] - for (area_name, device) in hass.data[LUTRON_DEVICES]['cover']: - dev = LutronCover(area_name, device, hass.data[LUTRON_CONTROLLER]) - devs.append(dev) - - add_entities(devs, True) - return True - - -class LutronCover(LutronDevice, CoverDevice): - """Representation of a Lutron shade.""" - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION - - @property - def is_closed(self): - """Return if the cover is closed.""" - return self._lutron_device.last_level() < 1 - - @property - def current_cover_position(self): - """Return the current position of cover.""" - return self._lutron_device.last_level() - - def close_cover(self, **kwargs): - """Close the cover.""" - self._lutron_device.level = 0 - - def open_cover(self, **kwargs): - """Open the cover.""" - self._lutron_device.level = 100 - - def set_cover_position(self, **kwargs): - """Move the shade to a specific position.""" - if ATTR_POSITION in kwargs: - position = kwargs[ATTR_POSITION] - self._lutron_device.level = position - - def update(self): - """Call when forcing a refresh of the device.""" - # Reading the property (rather than last_level()) fetches value - level = self._lutron_device.level - _LOGGER.debug("Lutron ID: %d updated to %f", - self._lutron_device.id, level) - - @property - def device_state_attributes(self): - """Return the state attributes.""" - attr = {} - attr['Lutron Integration ID'] = self._lutron_device.id - return attr diff --git a/homeassistant/components/cover/lutron_caseta.py b/homeassistant/components/cover/lutron_caseta.py deleted file mode 100644 index 37b7c1be42c4e..0000000000000 --- a/homeassistant/components/cover/lutron_caseta.py +++ /dev/null @@ -1,68 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.components.cover import ( - CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_SET_POSITION, - ATTR_POSITION, DOMAIN) -from homeassistant.components.lutron_caseta import ( - LUTRON_CASETA_SMARTBRIDGE, LutronCasetaDevice) - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['lutron_caseta'] - - -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] - cover_devices = bridge.get_devices_by_domain(DOMAIN) - for cover_device in cover_devices: - dev = LutronCasetaCover(cover_device, bridge) - devs.append(dev) - - async_add_entities(devs, True) - - -class LutronCasetaCover(LutronCasetaDevice, CoverDevice): - """Representation of a Lutron shade.""" - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION - - @property - def is_closed(self): - """Return if the cover is closed.""" - return self._state['current_state'] < 1 - - @property - def current_cover_position(self): - """Return the current position of cover.""" - return self._state['current_state'] - - async def async_close_cover(self, **kwargs): - """Close the cover.""" - self._smartbridge.set_value(self._device_id, 0) - - async def async_open_cover(self, **kwargs): - """Open the cover.""" - self._smartbridge.set_value(self._device_id, 100) - - async def async_set_cover_position(self, **kwargs): - """Move the shade to a specific position.""" - if ATTR_POSITION in kwargs: - position = kwargs[ATTR_POSITION] - self._smartbridge.set_value(self._device_id, position) - - async def async_update(self): - """Call when forcing a refresh of the device.""" - self._state = self._smartbridge.get_device_by_id(self._device_id) - _LOGGER.debug(self._state) diff --git a/homeassistant/components/cover/mysensors.py b/homeassistant/components/cover/mysensors.py deleted file mode 100644 index 60ff7aeef1d6c..0000000000000 --- a/homeassistant/components/cover/mysensors.py +++ /dev/null @@ -1,86 +0,0 @@ -""" -Support for MySensors covers. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/cover.mysensors/ -""" -from homeassistant.components import mysensors -from homeassistant.components.cover import ATTR_POSITION, DOMAIN, CoverDevice -from homeassistant.const import STATE_OFF, STATE_ON - - -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): - """Set up the mysensors platform for covers.""" - mysensors.setup_mysensors_platform( - hass, DOMAIN, discovery_info, MySensorsCover, - async_add_entities=async_add_entities) - - -class MySensorsCover(mysensors.device.MySensorsEntity, CoverDevice): - """Representation of the value of a MySensors Cover child node.""" - - @property - def assumed_state(self): - """Return True if unable to access real state of entity.""" - return self.gateway.optimistic - - @property - def is_closed(self): - """Return True if cover is closed.""" - set_req = self.gateway.const.SetReq - if set_req.V_DIMMER in self._values: - return self._values.get(set_req.V_DIMMER) == 0 - return self._values.get(set_req.V_LIGHT) == STATE_OFF - - @property - def current_cover_position(self): - """Return current position of cover. - - None is unknown, 0 is closed, 100 is fully open. - """ - set_req = self.gateway.const.SetReq - return self._values.get(set_req.V_DIMMER) - - async def async_open_cover(self, **kwargs): - """Move the cover up.""" - set_req = self.gateway.const.SetReq - self.gateway.set_child_value( - self.node_id, self.child_id, set_req.V_UP, 1) - if self.gateway.optimistic: - # Optimistically assume that cover has changed state. - if set_req.V_DIMMER in self._values: - self._values[set_req.V_DIMMER] = 100 - else: - self._values[set_req.V_LIGHT] = STATE_ON - self.async_schedule_update_ha_state() - - async def async_close_cover(self, **kwargs): - """Move the cover down.""" - set_req = self.gateway.const.SetReq - self.gateway.set_child_value( - self.node_id, self.child_id, set_req.V_DOWN, 1) - if self.gateway.optimistic: - # Optimistically assume that cover has changed state. - if set_req.V_DIMMER in self._values: - self._values[set_req.V_DIMMER] = 0 - else: - self._values[set_req.V_LIGHT] = STATE_OFF - self.async_schedule_update_ha_state() - - async def async_set_cover_position(self, **kwargs): - """Move the cover to a specific position.""" - position = kwargs.get(ATTR_POSITION) - set_req = self.gateway.const.SetReq - self.gateway.set_child_value( - self.node_id, self.child_id, set_req.V_DIMMER, position) - if self.gateway.optimistic: - # Optimistically assume that cover has changed state. - self._values[set_req.V_DIMMER] = position - self.async_schedule_update_ha_state() - - async def async_stop_cover(self, **kwargs): - """Stop the device.""" - set_req = self.gateway.const.SetReq - self.gateway.set_child_value( - self.node_id, self.child_id, set_req.V_STOP, 1) diff --git a/homeassistant/components/cover/rfxtrx.py b/homeassistant/components/cover/rfxtrx.py deleted file mode 100644 index d486b60197779..0000000000000 --- a/homeassistant/components/cover/rfxtrx.py +++ /dev/null @@ -1,80 +0,0 @@ -""" -Support for RFXtrx cover components. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/cover.rfxtrx/ -""" -import voluptuous as vol - -from homeassistant.components import rfxtrx -from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME -from homeassistant.components.rfxtrx import ( - CONF_AUTOMATIC_ADD, CONF_FIRE_EVENT, DEFAULT_SIGNAL_REPETITIONS, - CONF_SIGNAL_REPETITIONS, CONF_DEVICES) -from homeassistant.helpers import config_validation as cv - -DEPENDENCIES = ['rfxtrx'] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_DEVICES, default={}): { - cv.string: vol.Schema({ - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean - }) - }, - vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean, - vol.Optional(CONF_SIGNAL_REPETITIONS, default=DEFAULT_SIGNAL_REPETITIONS): - vol.Coerce(int), -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the RFXtrx cover.""" - import RFXtrx as rfxtrxmod - - covers = rfxtrx.get_devices_from_config(config, RfxtrxCover) - add_entities(covers) - - def cover_update(event): - """Handle cover updates from the RFXtrx gateway.""" - if not isinstance(event.device, rfxtrxmod.LightingDevice) or \ - event.device.known_to_be_dimmable or \ - not event.device.known_to_be_rollershutter: - return - - new_device = rfxtrx.get_new_device(event, config, RfxtrxCover) - if new_device: - add_entities([new_device]) - - rfxtrx.apply_received_command(event) - - # Subscribe to main RFXtrx events - if cover_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS: - rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(cover_update) - - -class RfxtrxCover(rfxtrx.RfxtrxDevice, CoverDevice): - """Representation of a RFXtrx cover.""" - - @property - def should_poll(self): - """Return the polling state. No polling available in RFXtrx cover.""" - return False - - @property - def is_closed(self): - """Return if the cover is closed.""" - return None - - def open_cover(self, **kwargs): - """Move the cover up.""" - self._send_command("roll_up") - - def close_cover(self, **kwargs): - """Move the cover down.""" - self._send_command("roll_down") - - def stop_cover(self, **kwargs): - """Stop the cover.""" - self._send_command("stop_roll") diff --git a/homeassistant/components/cover/rpi_gpio.py b/homeassistant/components/cover/rpi_gpio.py deleted file mode 100644 index 828f5e8e0fee0..0000000000000 --- a/homeassistant/components/cover/rpi_gpio.py +++ /dev/null @@ -1,119 +0,0 @@ -""" -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/ -""" -import logging -from time import sleep - -import voluptuous as vol - -from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME -from homeassistant.components import rpi_gpio -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -CONF_COVERS = 'covers' -CONF_RELAY_PIN = 'relay_pin' -CONF_RELAY_TIME = 'relay_time' -CONF_STATE_PIN = 'state_pin' -CONF_STATE_PULL_MODE = 'state_pull_mode' -CONF_INVERT_STATE = 'invert_state' -CONF_INVERT_RELAY = 'invert_relay' - -DEFAULT_RELAY_TIME = .2 -DEFAULT_STATE_PULL_MODE = 'UP' -DEFAULT_INVERT_STATE = False -DEFAULT_INVERT_RELAY = False -DEPENDENCIES = ['rpi_gpio'] - -_COVERS_SCHEMA = vol.All( - cv.ensure_list, - [ - vol.Schema({ - CONF_NAME: cv.string, - CONF_RELAY_PIN: cv.positive_int, - CONF_STATE_PIN: cv.positive_int, - }) - ] -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_COVERS): _COVERS_SCHEMA, - vol.Optional(CONF_STATE_PULL_MODE, default=DEFAULT_STATE_PULL_MODE): - cv.string, - vol.Optional(CONF_RELAY_TIME, default=DEFAULT_RELAY_TIME): cv.positive_int, - vol.Optional(CONF_INVERT_STATE, default=DEFAULT_INVERT_STATE): cv.boolean, - vol.Optional(CONF_INVERT_RELAY, default=DEFAULT_INVERT_RELAY): cv.boolean, -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the RPi cover platform.""" - relay_time = config.get(CONF_RELAY_TIME) - state_pull_mode = config.get(CONF_STATE_PULL_MODE) - invert_state = config.get(CONF_INVERT_STATE) - invert_relay = config.get(CONF_INVERT_RELAY) - covers = [] - covers_conf = config.get(CONF_COVERS) - - for cover in covers_conf: - covers.append(RPiGPIOCover( - cover[CONF_NAME], cover[CONF_RELAY_PIN], cover[CONF_STATE_PIN], - state_pull_mode, relay_time, invert_state, invert_relay)) - add_entities(covers) - - -class RPiGPIOCover(CoverDevice): - """Representation of a Raspberry GPIO cover.""" - - def __init__(self, name, relay_pin, state_pin, state_pull_mode, - relay_time, invert_state, invert_relay): - """Initialize the cover.""" - self._name = name - self._state = False - self._relay_pin = relay_pin - self._state_pin = state_pin - self._state_pull_mode = state_pull_mode - self._relay_time = relay_time - self._invert_state = invert_state - self._invert_relay = invert_relay - rpi_gpio.setup_output(self._relay_pin) - rpi_gpio.setup_input(self._state_pin, self._state_pull_mode) - rpi_gpio.write_output(self._relay_pin, 0 if self._invert_relay else 1) - - @property - def name(self): - """Return the name of the cover if any.""" - return self._name - - def update(self): - """Update the state of the cover.""" - self._state = rpi_gpio.read_input(self._state_pin) - - @property - def is_closed(self): - """Return true if cover is closed.""" - return self._state != self._invert_state - - def _trigger(self): - """Trigger the cover.""" - rpi_gpio.write_output(self._relay_pin, 1 if self._invert_relay else 0) - sleep(self._relay_time) - rpi_gpio.write_output(self._relay_pin, 0 if self._invert_relay else 1) - - def close_cover(self, **kwargs): - """Close the cover.""" - if not self.is_closed: - self._trigger() - - def open_cover(self, **kwargs): - """Open the cover.""" - if self.is_closed: - self._trigger() diff --git a/homeassistant/components/cover/scsgate.py b/homeassistant/components/cover/scsgate.py deleted file mode 100644 index 2d85c1fe3c3ca..0000000000000 --- a/homeassistant/components/cover/scsgate.py +++ /dev/null @@ -1,101 +0,0 @@ -""" -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/ -""" -import logging - -import voluptuous as vol - -from homeassistant.components import scsgate -from homeassistant.components.cover import (CoverDevice, PLATFORM_SCHEMA) -from homeassistant.const import (CONF_DEVICES, CONF_NAME) -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['scsgate'] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_DEVICES): - cv.schema_with_slug_keys(scsgate.SCSGATE_SCHEMA), -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the SCSGate cover.""" - devices = config.get(CONF_DEVICES) - covers = [] - logger = logging.getLogger(__name__) - - if devices: - for _, entity_info in devices.items(): - if entity_info[scsgate.CONF_SCS_ID] in scsgate.SCSGATE.devices: - continue - - name = entity_info[CONF_NAME] - scs_id = entity_info[scsgate.CONF_SCS_ID] - - logger.info("Adding %s scsgate.cover", name) - - cover = SCSGateCover(name=name, scs_id=scs_id, logger=logger) - scsgate.SCSGATE.add_device(cover) - covers.append(cover) - - add_entities(covers) - - -class SCSGateCover(CoverDevice): - """Representation of SCSGate cover.""" - - def __init__(self, scs_id, name, logger): - """Initialize the cover.""" - self._scs_id = scs_id - self._name = name - self._logger = logger - - @property - def scs_id(self): - """Return the SCSGate ID.""" - return self._scs_id - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return the name of the cover.""" - return self._name - - @property - def is_closed(self): - """Return if the cover is closed.""" - return None - - def open_cover(self, **kwargs): - """Move the cover.""" - from scsgate.tasks import RaiseRollerShutterTask - - scsgate.SCSGATE.append_task( - RaiseRollerShutterTask(target=self._scs_id)) - - def close_cover(self, **kwargs): - """Move the cover down.""" - from scsgate.tasks import LowerRollerShutterTask - - scsgate.SCSGATE.append_task( - LowerRollerShutterTask(target=self._scs_id)) - - def stop_cover(self, **kwargs): - """Stop the cover.""" - from scsgate.tasks import HaltRollerShutterTask - - scsgate.SCSGATE.append_task(HaltRollerShutterTask(target=self._scs_id)) - - def process_event(self, message): - """Handle a SCSGate message related with this cover.""" - self._logger.debug("Cover %s, got message %s", - self._scs_id, message.toggled) diff --git a/homeassistant/components/cover/tahoma.py b/homeassistant/components/cover/tahoma.py deleted file mode 100644 index baf32073c444d..0000000000000 --- a/homeassistant/components/cover/tahoma.py +++ /dev/null @@ -1,218 +0,0 @@ -""" -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/ -""" -from datetime import timedelta -import logging - -from homeassistant.util.dt import utcnow -from homeassistant.components.cover import CoverDevice, ATTR_POSITION -from homeassistant.components.tahoma import ( - DOMAIN as TAHOMA_DOMAIN, TahomaDevice) - -DEPENDENCIES = ['tahoma'] - -_LOGGER = logging.getLogger(__name__) - -ATTR_MEM_POS = 'memorized_position' -ATTR_RSSI_LEVEL = 'rssi_level' -ATTR_LOCK_START_TS = 'lock_start_ts' -ATTR_LOCK_END_TS = 'lock_end_ts' -ATTR_LOCK_LEVEL = 'lock_level' -ATTR_LOCK_ORIG = 'lock_originator' - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Tahoma covers.""" - controller = hass.data[TAHOMA_DOMAIN]['controller'] - devices = [] - for device in hass.data[TAHOMA_DOMAIN]['devices']['cover']: - devices.append(TahomaCover(device, controller)) - add_entities(devices, True) - - -class TahomaCover(TahomaDevice, CoverDevice): - """Representation a Tahoma Cover.""" - - def __init__(self, tahoma_device, controller): - """Initialize the device.""" - super().__init__(tahoma_device, controller) - - self._closure = 0 - # 100 equals open - self._position = 100 - self._closed = False - self._rssi_level = None - self._icon = None - # Can be 0 and bigger - self._lock_timer = 0 - self._lock_start_ts = None - self._lock_end_ts = None - # Can be 'comfortLevel1', 'comfortLevel2', 'comfortLevel3', - # 'comfortLevel4', 'environmentProtection', 'humanProtection', - # 'userLevel1', 'userLevel2' - self._lock_level = None - # Can be 'LSC', 'SAAC', 'SFC', 'UPS', 'externalGateway', 'localUser', - # 'myself', 'rain', 'security', 'temperature', 'timer', 'user', 'wind' - self._lock_originator = None - - def update(self): - """Update method.""" - self.controller.get_states([self.tahoma_device]) - - # For vertical covers - self._closure = self.tahoma_device.active_states.get( - 'core:ClosureState') - # For horizontal covers - if self._closure is None: - self._closure = self.tahoma_device.active_states.get( - 'core:DeploymentState') - - # For all, if available - if 'core:PriorityLockTimerState' in self.tahoma_device.active_states: - old_lock_timer = self._lock_timer - self._lock_timer = \ - self.tahoma_device.active_states['core:PriorityLockTimerState'] - # Derive timestamps from _lock_timer, only if not already set or - # something has changed - if self._lock_timer > 0: - _LOGGER.debug("Update %s, lock_timer: %d", self._name, - self._lock_timer) - if self._lock_start_ts is None: - self._lock_start_ts = utcnow() - if self._lock_end_ts is None or \ - old_lock_timer != self._lock_timer: - self._lock_end_ts = utcnow() +\ - timedelta(seconds=self._lock_timer) - else: - self._lock_start_ts = None - self._lock_end_ts = None - else: - self._lock_timer = 0 - self._lock_start_ts = None - self._lock_end_ts = None - - self._lock_level = self.tahoma_device.active_states.get( - 'io:PriorityLockLevelState') - - self._lock_originator = self.tahoma_device.active_states.get( - 'io:PriorityLockOriginatorState') - - self._rssi_level = self.tahoma_device.active_states.get( - 'core:RSSILevelState') - - # Define which icon to use - if self._lock_timer > 0: - if self._lock_originator == 'wind': - self._icon = 'mdi:weather-windy' - else: - self._icon = 'mdi:lock-alert' - else: - self._icon = None - - # Define current position. - # _position: 0 is closed, 100 is fully open. - # 'core:ClosureState': 100 is closed, 0 is fully open. - if self._closure is not None: - self._position = 100 - self._closure - if self._position <= 5: - self._position = 0 - if self._position >= 95: - self._position = 100 - self._closed = self._position == 0 - else: - self._position = None - if 'core:OpenClosedState' in self.tahoma_device.active_states: - self._closed = \ - self.tahoma_device.active_states['core:OpenClosedState']\ - == 'closed' - else: - self._closed = False - - _LOGGER.debug("Update %s, position: %d", self._name, self._position) - - @property - def current_cover_position(self): - """Return current position of cover.""" - return self._position - - def set_cover_position(self, **kwargs): - """Move the cover to a specific position.""" - self.apply_action('setPosition', 100 - kwargs.get(ATTR_POSITION)) - - @property - def is_closed(self): - """Return if the cover is closed.""" - return self._closed - - @property - def device_class(self): - """Return the class of the device.""" - if self.tahoma_device.type == 'io:WindowOpenerVeluxIOComponent': - return 'window' - return None - - @property - def device_state_attributes(self): - """Return the device state attributes.""" - attr = {} - super_attr = super().device_state_attributes - if super_attr is not None: - attr.update(super_attr) - - if 'core:Memorized1PositionState' in self.tahoma_device.active_states: - attr[ATTR_MEM_POS] = self.tahoma_device.active_states[ - 'core:Memorized1PositionState'] - if self._rssi_level is not None: - attr[ATTR_RSSI_LEVEL] = self._rssi_level - if self._lock_start_ts is not None: - attr[ATTR_LOCK_START_TS] = self._lock_start_ts.isoformat() - if self._lock_end_ts is not None: - attr[ATTR_LOCK_END_TS] = self._lock_end_ts.isoformat() - if self._lock_level is not None: - attr[ATTR_LOCK_LEVEL] = self._lock_level - if self._lock_originator is not None: - attr[ATTR_LOCK_ORIG] = self._lock_originator - return attr - - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return self._icon - - def open_cover(self, **kwargs): - """Open the cover.""" - if self.tahoma_device.type == 'io:HorizontalAwningIOComponent': - self.apply_action('close') - else: - self.apply_action('open') - - def close_cover(self, **kwargs): - """Close the cover.""" - if self.tahoma_device.type == 'io:HorizontalAwningIOComponent': - self.apply_action('open') - else: - self.apply_action('close') - - def stop_cover(self, **kwargs): - """Stop the cover.""" - if self.tahoma_device.type == \ - 'io:RollerShutterWithLowSpeedManagementIOComponent': - self.apply_action('setPosition', 'secured') - elif self.tahoma_device.type in \ - ('rts:BlindRTSComponent', - 'io:ExteriorVenetianBlindIOComponent', - 'rts:VenetianBlindRTSComponent', - 'rts:DualCurtainRTSComponent', - 'rts:ExteriorVenetianBlindRTSComponent', - 'rts:BlindRTSComponent'): - self.apply_action('my') - elif self.tahoma_device.type in \ - ('io:HorizontalAwningIOComponent', - 'io:RollerShutterGenericIOComponent', - 'io:VerticalExteriorAwningIOComponent'): - self.apply_action('stop') - else: - self.apply_action('stopIdentify') diff --git a/homeassistant/components/cover/tellduslive.py b/homeassistant/components/cover/tellduslive.py deleted file mode 100644 index 1879c88c83c1f..0000000000000 --- a/homeassistant/components/cover/tellduslive.py +++ /dev/null @@ -1,61 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.components import cover, tellduslive -from homeassistant.components.cover import CoverDevice -from homeassistant.components.tellduslive.entry import TelldusLiveEntity -from homeassistant.helpers.dispatcher import async_dispatcher_connect - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Old way of setting up TelldusLive. - - Can only be called when a user accidentally mentions the platform in their - config. But even in that case it would have been ignored. - """ - pass - - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up tellduslive sensors dynamically.""" - async def async_discover_cover(device_id): - """Discover and add a discovered sensor.""" - client = hass.data[tellduslive.DOMAIN] - async_add_entities([TelldusLiveCover(client, device_id)]) - - async_dispatcher_connect( - hass, - tellduslive.TELLDUS_DISCOVERY_NEW.format(cover.DOMAIN, - tellduslive.DOMAIN), - async_discover_cover, - ) - - -class TelldusLiveCover(TelldusLiveEntity, CoverDevice): - """Representation of a cover.""" - - @property - def is_closed(self): - """Return the current position of the cover.""" - return self.device.is_down - - def close_cover(self, **kwargs): - """Close the cover.""" - self.device.down() - - def open_cover(self, **kwargs): - """Open the cover.""" - self.device.up() - - def stop_cover(self, **kwargs): - """Stop the cover.""" - self.device.stop() diff --git a/homeassistant/components/cover/tellstick.py b/homeassistant/components/cover/tellstick.py deleted file mode 100644 index 88608ac42e9a2..0000000000000 --- a/homeassistant/components/cover/tellstick.py +++ /dev/null @@ -1,65 +0,0 @@ -""" -Support for Tellstick covers. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/cover.tellstick/ -""" - - -from homeassistant.components.cover import CoverDevice -from homeassistant.components.tellstick import ( - DEFAULT_SIGNAL_REPETITIONS, ATTR_DISCOVER_DEVICES, ATTR_DISCOVER_CONFIG, - DATA_TELLSTICK, TellstickDevice) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Tellstick covers.""" - if (discovery_info is None or - discovery_info[ATTR_DISCOVER_DEVICES] is None): - return - - signal_repetitions = discovery_info.get( - ATTR_DISCOVER_CONFIG, DEFAULT_SIGNAL_REPETITIONS) - - add_entities([TellstickCover(hass.data[DATA_TELLSTICK][tellcore_id], - signal_repetitions) - for tellcore_id in discovery_info[ATTR_DISCOVER_DEVICES]], - True) - - -class TellstickCover(TellstickDevice, CoverDevice): - """Representation of a Tellstick cover.""" - - @property - def is_closed(self): - """Return the current position of the cover is not possible.""" - return None - - @property - def assumed_state(self): - """Return True if unable to access real state of the entity.""" - return True - - def close_cover(self, **kwargs): - """Close the cover.""" - self._tellcore_device.down() - - def open_cover(self, **kwargs): - """Open the cover.""" - self._tellcore_device.up() - - def stop_cover(self, **kwargs): - """Stop the cover.""" - self._tellcore_device.stop() - - def _parse_tellcore_data(self, tellcore_data): - """Turn the value received from tellcore into something useful.""" - pass - - def _parse_ha_data(self, kwargs): - """Turn the value from HA into something useful.""" - pass - - def _update_model(self, new_state, data): - """Update the device entity state to match the arguments.""" - pass diff --git a/homeassistant/components/cover/tuya.py b/homeassistant/components/cover/tuya.py deleted file mode 100644 index a3a3db972e9f9..0000000000000 --- a/homeassistant/components/cover/tuya.py +++ /dev/null @@ -1,65 +0,0 @@ -""" -Support for Tuya cover. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/cover.tuya/ -""" -from homeassistant.components.cover import ( - CoverDevice, ENTITY_ID_FORMAT, SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_STOP) -from homeassistant.components.tuya import DATA_TUYA, TuyaDevice - -DEPENDENCIES = ['tuya'] - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Tuya cover devices.""" - if discovery_info is None: - return - tuya = hass.data[DATA_TUYA] - dev_ids = discovery_info.get('dev_ids') - devices = [] - for dev_id in dev_ids: - device = tuya.get_device_by_id(dev_id) - if device is None: - continue - devices.append(TuyaCover(device)) - add_entities(devices) - - -class TuyaCover(TuyaDevice, CoverDevice): - """Tuya cover devices.""" - - def __init__(self, tuya): - """Init tuya cover device.""" - super().__init__(tuya) - self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id()) - - @property - def supported_features(self): - """Flag supported features.""" - supported_features = SUPPORT_OPEN | SUPPORT_CLOSE - if self.tuya.support_stop(): - supported_features |= SUPPORT_STOP - return supported_features - - @property - def is_closed(self): - """Return if the cover is closed or not.""" - state = self.tuya.state() - if state == 1: - return False - if state == 2: - return True - return None - - def open_cover(self, **kwargs): - """Open the cover.""" - self.tuya.open_cover() - - def close_cover(self, **kwargs): - """Close cover.""" - self.tuya.close_cover() - - def stop_cover(self, **kwargs): - """Stop the cover.""" - self.tuya.stop_cover() diff --git a/homeassistant/components/cover/velbus.py b/homeassistant/components/cover/velbus.py deleted file mode 100644 index 7e5099cecf805..0000000000000 --- a/homeassistant/components/cover/velbus.py +++ /dev/null @@ -1,158 +0,0 @@ -""" -Support for Velbus covers. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/cover.velbus/ -""" -import logging -import time - -import voluptuous as vol - -from homeassistant.components.cover import ( - CoverDevice, PLATFORM_SCHEMA, SUPPORT_OPEN, SUPPORT_CLOSE, - SUPPORT_STOP) -from homeassistant.components.velbus import DOMAIN -from homeassistant.const import (CONF_COVERS, CONF_NAME) -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -COVER_SCHEMA = vol.Schema({ - vol.Required('module'): cv.positive_int, - vol.Required('open_channel'): cv.positive_int, - vol.Required('close_channel'): cv.positive_int, - vol.Required(CONF_NAME): cv.string -}) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_SCHEMA), -}) - -DEPENDENCIES = ['velbus'] - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up cover controlled by Velbus.""" - devices = config.get(CONF_COVERS, {}) - covers = [] - - velbus = hass.data[DOMAIN] - for device_name, device_config in devices.items(): - covers.append( - VelbusCover( - velbus, - device_config.get(CONF_NAME, device_name), - device_config.get('module'), - device_config.get('open_channel'), - device_config.get('close_channel') - ) - ) - - if not covers: - _LOGGER.error("No covers added") - return False - - add_entities(covers) - - -class VelbusCover(CoverDevice): - """Representation a Velbus cover.""" - - def __init__(self, velbus, name, module, open_channel, close_channel): - """Initialize the cover.""" - self._velbus = velbus - self._name = name - self._close_channel_state = None - self._open_channel_state = None - self._module = module - self._open_channel = open_channel - self._close_channel = close_channel - - async def async_added_to_hass(self): - """Add listener for Velbus messages on bus.""" - def _init_velbus(): - """Initialize Velbus on startup.""" - self._velbus.subscribe(self._on_message) - self.get_status() - - await self.hass.async_add_job(_init_velbus) - - def _on_message(self, message): - import velbus - if isinstance(message, velbus.RelayStatusMessage): - if message.address == self._module: - if message.channel == self._close_channel: - self._close_channel_state = message.is_on() - self.schedule_update_ha_state() - if message.channel == self._open_channel: - self._open_channel_state = message.is_on() - self.schedule_update_ha_state() - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP - - @property - def should_poll(self): - """Disable polling.""" - return False - - @property - def name(self): - """Return the name of the cover.""" - return self._name - - @property - def is_closed(self): - """Return if the cover is closed.""" - return self._close_channel_state - - @property - def current_cover_position(self): - """Return current position of cover. - - None is unknown. - """ - return None - - def _relay_off(self, channel): - import velbus - message = velbus.SwitchRelayOffMessage() - message.set_defaults(self._module) - message.relay_channels = [channel] - self._velbus.send(message) - - def _relay_on(self, channel): - import velbus - message = velbus.SwitchRelayOnMessage() - message.set_defaults(self._module) - message.relay_channels = [channel] - self._velbus.send(message) - - def open_cover(self, **kwargs): - """Open the cover.""" - self._relay_off(self._close_channel) - time.sleep(0.3) - self._relay_on(self._open_channel) - - def close_cover(self, **kwargs): - """Close the cover.""" - self._relay_off(self._open_channel) - time.sleep(0.3) - self._relay_on(self._close_channel) - - def stop_cover(self, **kwargs): - """Stop the cover.""" - self._relay_off(self._open_channel) - time.sleep(0.3) - self._relay_off(self._close_channel) - - def get_status(self): - """Retrieve current status.""" - import velbus - message = velbus.ModuleStatusRequestMessage() - message.set_defaults(self._module) - message.channels = [self._open_channel, self._close_channel] - self._velbus.send(message) diff --git a/homeassistant/components/cover/vera.py b/homeassistant/components/cover/vera.py deleted file mode 100644 index 279e4a4307d21..0000000000000 --- a/homeassistant/components/cover/vera.py +++ /dev/null @@ -1,72 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.components.cover import CoverDevice, ENTITY_ID_FORMAT, \ - ATTR_POSITION -from homeassistant.components.vera import ( - VERA_CONTROLLER, VERA_DEVICES, VeraDevice) - -DEPENDENCIES = ['vera'] - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Vera covers.""" - add_entities( - [VeraCover(device, hass.data[VERA_CONTROLLER]) for - device in hass.data[VERA_DEVICES]['cover']], True) - - -class VeraCover(VeraDevice, CoverDevice): - """Representation a Vera Cover.""" - - def __init__(self, vera_device, controller): - """Initialize the Vera device.""" - VeraDevice.__init__(self, vera_device, controller) - self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) - - @property - def current_cover_position(self): - """ - Return current position of cover. - - 0 is closed, 100 is fully open. - """ - position = self.vera_device.get_level() - if position <= 5: - return 0 - if position >= 95: - return 100 - return position - - def set_cover_position(self, **kwargs): - """Move the cover to a specific position.""" - self.vera_device.set_level(kwargs.get(ATTR_POSITION)) - self.schedule_update_ha_state() - - @property - def is_closed(self): - """Return if the cover is closed.""" - if self.current_cover_position is not None: - return self.current_cover_position == 0 - - def open_cover(self, **kwargs): - """Open the cover.""" - self.vera_device.open() - self.schedule_update_ha_state() - - def close_cover(self, **kwargs): - """Close the cover.""" - self.vera_device.close() - self.schedule_update_ha_state() - - def stop_cover(self, **kwargs): - """Stop the cover.""" - self.vera_device.stop() - self.schedule_update_ha_state() diff --git a/homeassistant/components/cover/wink.py b/homeassistant/components/cover/wink.py deleted file mode 100644 index 3cf9c753e3a27..0000000000000 --- a/homeassistant/components/cover/wink.py +++ /dev/null @@ -1,62 +0,0 @@ -""" -Support for Wink Covers. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/cover.wink/ -""" -from homeassistant.components.cover import CoverDevice, ATTR_POSITION -from homeassistant.components.wink import WinkDevice, DOMAIN - -DEPENDENCIES = ['wink'] - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Wink cover platform.""" - import pywink - - for shade in pywink.get_shades(): - _id = shade.object_id() + shade.name() - if _id not in hass.data[DOMAIN]['unique_ids']: - add_entities([WinkCoverDevice(shade, hass)]) - for shade in pywink.get_shade_groups(): - _id = shade.object_id() + shade.name() - if _id not in hass.data[DOMAIN]['unique_ids']: - add_entities([WinkCoverDevice(shade, hass)]) - for door in pywink.get_garage_doors(): - _id = door.object_id() + door.name() - if _id not in hass.data[DOMAIN]['unique_ids']: - add_entities([WinkCoverDevice(door, hass)]) - - -class WinkCoverDevice(WinkDevice, CoverDevice): - """Representation of a Wink cover device.""" - - async def async_added_to_hass(self): - """Call when entity is added to hass.""" - self.hass.data[DOMAIN]['entities']['cover'].append(self) - - def close_cover(self, **kwargs): - """Close the cover.""" - self.wink.set_state(0) - - def open_cover(self, **kwargs): - """Open the cover.""" - self.wink.set_state(1) - - def set_cover_position(self, **kwargs): - """Move the cover shutter to a specific position.""" - position = kwargs.get(ATTR_POSITION) - self.wink.set_state(position/100) - - @property - def current_cover_position(self): - """Return the current position of cover shutter.""" - if self.wink.state() is not None: - return int(self.wink.state()*100) - return None - - @property - def is_closed(self): - """Return if the cover is closed.""" - state = self.wink.state() - return bool(state == 0) diff --git a/homeassistant/components/cover/zwave.py b/homeassistant/components/cover/zwave.py deleted file mode 100644 index 835305449e825..0000000000000 --- a/homeassistant/components/cover/zwave.py +++ /dev/null @@ -1,189 +0,0 @@ -""" -Support for Z-Wave cover components. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/cover.zwave/ -""" -import logging -from homeassistant.core import callback -from homeassistant.components.cover import ( - DOMAIN, SUPPORT_OPEN, SUPPORT_CLOSE, ATTR_POSITION) -from homeassistant.components import zwave -from homeassistant.components.zwave import ( - ZWaveDeviceEntity, workaround) -from homeassistant.components.cover import CoverDevice -from homeassistant.helpers.dispatcher import async_dispatcher_connect - -_LOGGER = logging.getLogger(__name__) - -SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE - - -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Old method of setting up Z-Wave covers.""" - pass - - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up Z-Wave Cover from Config Entry.""" - @callback - def async_add_cover(cover): - """Add Z-Wave Cover.""" - async_add_entities([cover]) - - async_dispatcher_connect(hass, 'zwave_new_cover', async_add_cover) - - -def get_device(hass, values, node_config, **kwargs): - """Create Z-Wave entity device.""" - invert_buttons = node_config.get(zwave.CONF_INVERT_OPENCLOSE_BUTTONS) - if (values.primary.command_class == - zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL - and values.primary.index == 0): - return ZwaveRollershutter(hass, values, invert_buttons) - if values.primary.command_class == zwave.const.COMMAND_CLASS_SWITCH_BINARY: - return ZwaveGarageDoorSwitch(values) - if values.primary.command_class == \ - zwave.const.COMMAND_CLASS_BARRIER_OPERATOR: - return ZwaveGarageDoorBarrier(values) - return None - - -class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice): - """Representation of an Z-Wave cover.""" - - def __init__(self, hass, values, invert_buttons): - """Initialize the Z-Wave rollershutter.""" - ZWaveDeviceEntity.__init__(self, values, DOMAIN) - self._network = hass.data[zwave.const.DATA_NETWORK] - self._open_id = None - self._close_id = None - self._current_position = None - self._invert_buttons = invert_buttons - - self._workaround = workaround.get_device_mapping(values.primary) - if self._workaround: - _LOGGER.debug("Using workaround %s", self._workaround) - self.update_properties() - - def update_properties(self): - """Handle data changes for node values.""" - # Position value - self._current_position = self.values.primary.data - - if self.values.open and self.values.close and \ - self._open_id is None and self._close_id is None: - if self._invert_buttons: - self._open_id = self.values.close.value_id - self._close_id = self.values.open.value_id - else: - self._open_id = self.values.open.value_id - self._close_id = self.values.close.value_id - - @property - def is_closed(self): - """Return if the cover is closed.""" - if self.current_cover_position is None: - return None - if self.current_cover_position > 0: - return False - return True - - @property - def current_cover_position(self): - """Return the current position of Zwave roller shutter.""" - if self._workaround == workaround.WORKAROUND_NO_POSITION: - return None - if self._current_position is not None: - if self._current_position <= 5: - return 0 - if self._current_position >= 95: - return 100 - return self._current_position - - def open_cover(self, **kwargs): - """Move the roller shutter up.""" - self._network.manager.pressButton(self._open_id) - - def close_cover(self, **kwargs): - """Move the roller shutter down.""" - self._network.manager.pressButton(self._close_id) - - def set_cover_position(self, **kwargs): - """Move the roller shutter to a specific position.""" - self.node.set_dimmer(self.values.primary.value_id, - kwargs.get(ATTR_POSITION)) - - def stop_cover(self, **kwargs): - """Stop the roller shutter.""" - self._network.manager.releaseButton(self._open_id) - - -class ZwaveGarageDoorBase(zwave.ZWaveDeviceEntity, CoverDevice): - """Base class for a Zwave garage door device.""" - - def __init__(self, values): - """Initialize the zwave garage door.""" - ZWaveDeviceEntity.__init__(self, values, DOMAIN) - self._state = None - self.update_properties() - - def update_properties(self): - """Handle data changes for node values.""" - self._state = self.values.primary.data - _LOGGER.debug("self._state=%s", self._state) - - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return 'garage' - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_GARAGE - - -class ZwaveGarageDoorSwitch(ZwaveGarageDoorBase): - """Representation of a switch based Zwave garage door device.""" - - @property - def is_closed(self): - """Return the current position of Zwave garage door.""" - return not self._state - - def close_cover(self, **kwargs): - """Close the garage door.""" - self.values.primary.data = False - - def open_cover(self, **kwargs): - """Open the garage door.""" - self.values.primary.data = True - - -class ZwaveGarageDoorBarrier(ZwaveGarageDoorBase): - """Representation of a barrier operator Zwave garage door device.""" - - @property - def is_opening(self): - """Return true if cover is in an opening state.""" - return self._state == "Opening" - - @property - def is_closing(self): - """Return true if cover is in a closing state.""" - return self._state == "Closing" - - @property - def is_closed(self): - """Return the current position of Zwave garage door.""" - return self._state == "Closed" - - def close_cover(self, **kwargs): - """Close the garage door.""" - self.values.primary.data = "Closed" - - def open_cover(self, **kwargs): - """Open the garage door.""" - self.values.primary.data = "Opened" diff --git a/homeassistant/components/daikin/.translations/da.json b/homeassistant/components/daikin/.translations/da.json new file mode 100644 index 0000000000000..856bb1445c71b --- /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 f16d6a87d55c6..ce4a58162c7f3 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/daikin/climate.py b/homeassistant/components/daikin/climate.py new file mode 100644 index 0000000000000..d97b506e2732c --- /dev/null +++ b/homeassistant/components/daikin/climate.py @@ -0,0 +1,266 @@ +"""Support for the Daikin HVAC.""" +import logging +import re + +import voluptuous as vol + +from homeassistant.components.climate import ( + ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, ATTR_OPERATION_MODE, + ATTR_SWING_MODE, PLATFORM_SCHEMA, STATE_AUTO, STATE_COOL, STATE_DRY, + STATE_FAN_ONLY, STATE_HEAT, STATE_OFF, SUPPORT_FAN_MODE, + SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, + ClimateDevice) +from homeassistant.components.daikin import DOMAIN as DAIKIN_DOMAIN +from homeassistant.components.daikin.const import ( + ATTR_INSIDE_TEMPERATURE, ATTR_OUTSIDE_TEMPERATURE, ATTR_TARGET_TEMPERATURE) +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({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME): cv.string, +}) + +HA_STATE_TO_DAIKIN = { + STATE_FAN_ONLY: 'fan', + STATE_DRY: 'dry', + STATE_COOL: 'cool', + STATE_HEAT: 'hot', + STATE_AUTO: 'auto', + STATE_OFF: 'off', +} + +DAIKIN_TO_HA_STATE = { + 'fan': STATE_FAN_ONLY, + 'dry': STATE_DRY, + 'cool': STATE_COOL, + 'hot': STATE_HEAT, + 'auto': STATE_AUTO, + 'off': STATE_OFF, +} + +HA_ATTR_TO_DAIKIN = { + ATTR_OPERATION_MODE: 'mode', + ATTR_FAN_MODE: 'f_rate', + ATTR_SWING_MODE: 'f_dir', + ATTR_INSIDE_TEMPERATURE: 'htemp', + ATTR_OUTSIDE_TEMPERATURE: 'otemp', + ATTR_TARGET_TEMPERATURE: 'stemp' +} + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Old way of setting up the Daikin HVAC platform. + + Can only be called when a user accidentally mentions the platform in their + config. But even in that case it would have been ignored. + """ + pass + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Daikin climate based on config_entry.""" + daikin_api = hass.data[DAIKIN_DOMAIN].get(entry.entry_id) + async_add_entities([DaikinClimate(daikin_api)]) + + +class DaikinClimate(ClimateDevice): + """Representation of a Daikin HVAC.""" + + def __init__(self, api): + """Initialize the climate device.""" + from pydaikin import appliance + + self._api = api + self._list = { + ATTR_OPERATION_MODE: list(HA_STATE_TO_DAIKIN), + ATTR_FAN_MODE: list( + map( + str.title, + appliance.daikin_values(HA_ATTR_TO_DAIKIN[ATTR_FAN_MODE]) + ) + ), + ATTR_SWING_MODE: list( + map( + str.title, + appliance.daikin_values(HA_ATTR_TO_DAIKIN[ATTR_SWING_MODE]) + ) + ), + } + + self._supported_features = SUPPORT_TARGET_TEMPERATURE \ + | SUPPORT_OPERATION_MODE + + if self._api.device.support_fan_mode: + self._supported_features |= SUPPORT_FAN_MODE + + if self._api.device.support_swing_mode: + self._supported_features |= SUPPORT_SWING_MODE + + def get(self, key): + """Retrieve device settings from API library cache.""" + value = None + cast_to_float = False + + if key in [ATTR_TEMPERATURE, ATTR_INSIDE_TEMPERATURE, + ATTR_CURRENT_TEMPERATURE]: + key = ATTR_INSIDE_TEMPERATURE + + daikin_attr = HA_ATTR_TO_DAIKIN.get(key) + + if key == ATTR_INSIDE_TEMPERATURE: + value = self._api.device.values.get(daikin_attr) + cast_to_float = True + elif key == ATTR_TARGET_TEMPERATURE: + value = self._api.device.values.get(daikin_attr) + cast_to_float = True + elif key == ATTR_OUTSIDE_TEMPERATURE: + value = self._api.device.values.get(daikin_attr) + cast_to_float = True + elif key == ATTR_FAN_MODE: + value = self._api.device.represent(daikin_attr)[1].title() + elif key == ATTR_SWING_MODE: + value = self._api.device.represent(daikin_attr)[1].title() + elif key == ATTR_OPERATION_MODE: + # Daikin can return also internal states auto-1 or auto-7 + # and we need to translate them as AUTO + daikin_mode = re.sub( + '[^a-z]', '', + self._api.device.represent(daikin_attr)[1]) + ha_mode = DAIKIN_TO_HA_STATE.get(daikin_mode) + value = ha_mode + + if value is None: + _LOGGER.error("Invalid value requested for key %s", key) + else: + if value in ("-", "--"): + value = None + elif cast_to_float: + try: + value = float(value) + except ValueError: + value = None + + return value + + def set(self, settings): + """Set device settings using API.""" + values = {} + + for attr in [ATTR_TEMPERATURE, ATTR_FAN_MODE, ATTR_SWING_MODE, + ATTR_OPERATION_MODE]: + value = settings.get(attr) + if value is None: + continue + + daikin_attr = HA_ATTR_TO_DAIKIN.get(attr) + if daikin_attr is not None: + if attr == ATTR_OPERATION_MODE: + values[daikin_attr] = HA_STATE_TO_DAIKIN[value] + elif value in self._list[attr]: + values[daikin_attr] = value.lower() + else: + _LOGGER.error("Invalid value %s for %s", attr, value) + + # temperature + elif attr == ATTR_TEMPERATURE: + try: + values['stemp'] = str(int(value)) + except ValueError: + _LOGGER.error("Invalid temperature %s", value) + + if values: + self._api.device.set(values) + + @property + def supported_features(self): + """Return the list of supported features.""" + return self._supported_features + + @property + def name(self): + """Return the name of the thermostat, if any.""" + return self._api.name + + @property + def unique_id(self): + """Return a unique ID.""" + return self._api.mac + + @property + def temperature_unit(self): + """Return the unit of measurement which this thermostat uses.""" + return TEMP_CELSIUS + + @property + def current_temperature(self): + """Return the current temperature.""" + return self.get(ATTR_CURRENT_TEMPERATURE) + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self.get(ATTR_TARGET_TEMPERATURE) + + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + return 1 + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + self.set(kwargs) + + @property + def current_operation(self): + """Return current operation ie. heat, cool, idle.""" + return self.get(ATTR_OPERATION_MODE) + + @property + def operation_list(self): + """Return the list of available operation modes.""" + return self._list.get(ATTR_OPERATION_MODE) + + def set_operation_mode(self, operation_mode): + """Set HVAC mode.""" + self.set({ATTR_OPERATION_MODE: operation_mode}) + + @property + def current_fan_mode(self): + """Return the fan setting.""" + return self.get(ATTR_FAN_MODE) + + def set_fan_mode(self, fan_mode): + """Set fan mode.""" + self.set({ATTR_FAN_MODE: fan_mode}) + + @property + def fan_list(self): + """List of available fan modes.""" + return self._list.get(ATTR_FAN_MODE) + + @property + def current_swing_mode(self): + """Return the fan setting.""" + return self.get(ATTR_SWING_MODE) + + def set_swing_mode(self, swing_mode): + """Set new target temperature.""" + self.set({ATTR_SWING_MODE: swing_mode}) + + @property + def swing_list(self): + """List of available swing modes.""" + return self._list.get(ATTR_SWING_MODE) + + def update(self): + """Retrieve latest state.""" + self._api.update() + + @property + def device_info(self): + """Return a device description for device registry.""" + return self._api.device_info diff --git a/homeassistant/components/daikin/sensor.py b/homeassistant/components/daikin/sensor.py new file mode 100644 index 0000000000000..6065a18227422 --- /dev/null +++ b/homeassistant/components/daikin/sensor.py @@ -0,0 +1,106 @@ +"""Support for Daikin AC sensors.""" +import logging + +from homeassistant.components.daikin import DOMAIN as DAIKIN_DOMAIN +from homeassistant.components.daikin.const import ( + ATTR_INSIDE_TEMPERATURE, ATTR_OUTSIDE_TEMPERATURE, SENSOR_TYPE_TEMPERATURE, + SENSOR_TYPES) +from homeassistant.const import CONF_ICON, CONF_NAME, CONF_TYPE +from homeassistant.helpers.entity import Entity +from homeassistant.util.unit_system import UnitSystem + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Old way of setting up the Daikin sensors. + + Can only be called when a user accidentally mentions the platform in their + config. But even in that case it would have been ignored. + """ + pass + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Daikin climate based on config_entry.""" + daikin_api = hass.data[DAIKIN_DOMAIN].get(entry.entry_id) + async_add_entities([ + DaikinClimateSensor(daikin_api, sensor, hass.config.units) + for sensor in SENSOR_TYPES + ]) + + +class DaikinClimateSensor(Entity): + """Representation of a Sensor.""" + + def __init__(self, api, monitored_state, units: UnitSystem, + name=None) -> None: + """Initialize the sensor.""" + self._api = api + self._sensor = SENSOR_TYPES.get(monitored_state) + if name is None: + name = "{} {}".format(self._sensor[CONF_NAME], api.name) + + self._name = "{} {}".format(name, monitored_state.replace("_", " ")) + self._device_attribute = monitored_state + + if self._sensor[CONF_TYPE] == SENSOR_TYPE_TEMPERATURE: + self._unit_of_measurement = units.temperature_unit + + @property + def unique_id(self): + """Return a unique ID.""" + return "{}-{}".format(self._api.mac, self._device_attribute) + + def get(self, key): + """Retrieve device settings from API library cache.""" + value = None + cast_to_float = False + + if key == ATTR_INSIDE_TEMPERATURE: + value = self._api.device.values.get('htemp') + cast_to_float = True + elif key == ATTR_OUTSIDE_TEMPERATURE: + value = self._api.device.values.get('otemp') + + if value is None: + _LOGGER.warning("Invalid value requested for key %s", key) + else: + if value in ("-", "--"): + value = None + elif cast_to_float: + try: + value = float(value) + except ValueError: + value = None + + return value + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self._sensor[CONF_ICON] + + @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.get(self._device_attribute) + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit_of_measurement + + def update(self): + """Retrieve latest state.""" + self._api.update() + + @property + def device_info(self): + """Return a device description for device registry.""" + return self._api.device_info diff --git a/homeassistant/components/danfoss_air/__init__.py b/homeassistant/components/danfoss_air/__init__.py index 80c36b6f0c63f..d6123a25f235f 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.py deleted file mode 100644 index 58503d7187b37..0000000000000 --- a/homeassistant/components/datadog.py +++ /dev/null @@ -1,104 +0,0 @@ -""" -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/ -""" -import logging - -import voluptuous as vol - -from homeassistant.const import ( - CONF_HOST, CONF_PORT, CONF_PREFIX, EVENT_LOGBOOK_ENTRY, - EVENT_STATE_CHANGED, STATE_UNKNOWN) -from homeassistant.helpers import state as state_helper -import homeassistant.helpers.config_validation as cv - -REQUIREMENTS = ['datadog==0.15.0'] - -_LOGGER = logging.getLogger(__name__) - -CONF_RATE = 'rate' -DEFAULT_HOST = 'localhost' -DEFAULT_PORT = 8125 -DEFAULT_PREFIX = 'hass' -DEFAULT_RATE = 1 -DOMAIN = 'datadog' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_PREFIX, default=DEFAULT_PREFIX): cv.string, - vol.Optional(CONF_RATE, default=DEFAULT_RATE): - vol.All(vol.Coerce(int), vol.Range(min=1)), - }), -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the Datadog component.""" - from datadog import initialize, statsd - - conf = config[DOMAIN] - host = conf.get(CONF_HOST) - port = conf.get(CONF_PORT) - sample_rate = conf.get(CONF_RATE) - prefix = conf.get(CONF_PREFIX) - - initialize(statsd_host=host, statsd_port=port) - - def logbook_entry_listener(event): - """Listen for logbook entries and send them as events.""" - name = event.data.get('name') - message = event.data.get('message') - - statsd.event( - title="Home Assistant", - text="%%% \n **{}** {} \n %%%".format(name, message), - tags=[ - "entity:{}".format(event.data.get('entity_id')), - "domain:{}".format(event.data.get('domain')) - ] - ) - - _LOGGER.debug('Sent event %s', event.data.get('entity_id')) - - def state_changed_listener(event): - """Listen for new messages on the bus and sends them to Datadog.""" - state = event.data.get('new_state') - - if state is None or state.state == STATE_UNKNOWN: - return - - if state.attributes.get('hidden') is True: - return - - states = dict(state.attributes) - metric = "{}.{}".format(prefix, state.domain) - tags = ["entity:{}".format(state.entity_id)] - - for key, value in states.items(): - if isinstance(value, (float, int)): - attribute = "{}.{}".format(metric, key.replace(' ', '_')) - statsd.gauge( - attribute, value, sample_rate=sample_rate, tags=tags) - - _LOGGER.debug( - "Sent metric %s: %s (tags: %s)", attribute, value, tags) - - try: - value = state_helper.state_as_number(state) - except ValueError: - _LOGGER.debug( - "Error sending %s: %s (tags: %s)", metric, state.state, tags) - return - - statsd.gauge(metric, value, sample_rate=sample_rate, tags=tags) - - _LOGGER.debug('Sent metric %s: %s (tags: %s)', metric, value, tags) - - hass.bus.listen(EVENT_LOGBOOK_ENTRY, logbook_entry_listener) - hass.bus.listen(EVENT_STATE_CHANGED, state_changed_listener) - - return True diff --git a/homeassistant/components/datadog/__init__.py b/homeassistant/components/datadog/__init__.py new file mode 100644 index 0000000000000..3b519514d17b4 --- /dev/null +++ b/homeassistant/components/datadog/__init__.py @@ -0,0 +1,99 @@ +"""Support for sending data to Datadog.""" +import logging + +import voluptuous as vol + +from homeassistant.const import ( + CONF_HOST, CONF_PORT, CONF_PREFIX, EVENT_LOGBOOK_ENTRY, + EVENT_STATE_CHANGED, STATE_UNKNOWN) +from homeassistant.helpers import state as state_helper +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['datadog==0.15.0'] + +_LOGGER = logging.getLogger(__name__) + +CONF_RATE = 'rate' +DEFAULT_HOST = 'localhost' +DEFAULT_PORT = 8125 +DEFAULT_PREFIX = 'hass' +DEFAULT_RATE = 1 +DOMAIN = 'datadog' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_PREFIX, default=DEFAULT_PREFIX): cv.string, + vol.Optional(CONF_RATE, default=DEFAULT_RATE): + vol.All(vol.Coerce(int), vol.Range(min=1)), + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Datadog component.""" + from datadog import initialize, statsd + + conf = config[DOMAIN] + host = conf.get(CONF_HOST) + port = conf.get(CONF_PORT) + sample_rate = conf.get(CONF_RATE) + prefix = conf.get(CONF_PREFIX) + + initialize(statsd_host=host, statsd_port=port) + + def logbook_entry_listener(event): + """Listen for logbook entries and send them as events.""" + name = event.data.get('name') + message = event.data.get('message') + + statsd.event( + title="Home Assistant", + text="%%% \n **{}** {} \n %%%".format(name, message), + tags=[ + "entity:{}".format(event.data.get('entity_id')), + "domain:{}".format(event.data.get('domain')) + ] + ) + + _LOGGER.debug('Sent event %s', event.data.get('entity_id')) + + def state_changed_listener(event): + """Listen for new messages on the bus and sends them to Datadog.""" + state = event.data.get('new_state') + + if state is None or state.state == STATE_UNKNOWN: + return + + if state.attributes.get('hidden') is True: + return + + states = dict(state.attributes) + metric = "{}.{}".format(prefix, state.domain) + tags = ["entity:{}".format(state.entity_id)] + + for key, value in states.items(): + if isinstance(value, (float, int)): + attribute = "{}.{}".format(metric, key.replace(' ', '_')) + statsd.gauge( + attribute, value, sample_rate=sample_rate, tags=tags) + + _LOGGER.debug( + "Sent metric %s: %s (tags: %s)", attribute, value, tags) + + try: + value = state_helper.state_as_number(state) + except ValueError: + _LOGGER.debug( + "Error sending %s: %s (tags: %s)", metric, state.state, tags) + return + + statsd.gauge(metric, value, sample_rate=sample_rate, tags=tags) + + _LOGGER.debug('Sent metric %s: %s (tags: %s)', metric, value, tags) + + hass.bus.listen(EVENT_LOGBOOK_ENTRY, logbook_entry_listener) + hass.bus.listen(EVENT_STATE_CHANGED, state_changed_listener) + + return True diff --git a/homeassistant/components/deconz/.translations/da.json b/homeassistant/components/deconz/.translations/da.json index 7f9aad83160d9..e4e5f098a4d88 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 a501951540b3e..6a527ab0a0b11 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 3ff60254a6a50..c92f1562157a6 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 4d3e2cbc6a926..8015324be13fd 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 286b310c1a99a..77d01c5c40be9 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 @@ 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 f7bc71a23987e..8f90f303fcaad 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 99bdd20a2954b..48f06a894bb0c 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 @@ 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 8d33e011b9438..fe9fc4b77523e 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.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 @@ def __init__(self, hass, config_entry): 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 @@ async def async_setup(self, tries=0): 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 @@ async def async_reset(self): 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 f7c777b810077..50e22c84d6fe3 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 @@ 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 05845a022886a..d3a6df810bae8 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 @@ 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 1913e3d50879a..3083f0c673256 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_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 9ab7b56c0ca6e..1bf7235713afb 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 64d933896705d..c48c7205e01d1 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 .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 0000000000000..d56cf9a4ee896 --- /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.py deleted file mode 100644 index 2b9854fbcc74c..0000000000000 --- a/homeassistant/components/demo.py +++ /dev/null @@ -1,231 +0,0 @@ -""" -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/ -""" -import asyncio -import time - -from homeassistant import bootstrap -import homeassistant.core as ha -from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM - -DEPENDENCIES = ['conversation', 'introduction', 'zone'] -DOMAIN = 'demo' - -COMPONENTS_WITH_DEMO_PLATFORM = [ - 'air_quality', - 'alarm_control_panel', - 'binary_sensor', - 'calendar', - 'camera', - 'climate', - 'cover', - 'device_tracker', - 'fan', - 'image_processing', - 'light', - 'lock', - 'media_player', - 'notify', - 'sensor', - 'switch', - 'tts', - 'mailbox', -] - - -async def async_setup(hass, config): - """Set up the demo environment.""" - group = hass.components.group - configurator = hass.components.configurator - persistent_notification = hass.components.persistent_notification - - config.setdefault(ha.DOMAIN, {}) - config.setdefault(DOMAIN, {}) - - if config[DOMAIN].get('hide_demo_state') != 1: - hass.states.async_set('a.Demo_Mode', 'Enabled') - - # Setup sun - if not hass.config.latitude: - hass.config.latitude = 32.87336 - - if not hass.config.longitude: - hass.config.longitude = 117.22743 - - tasks = [ - bootstrap.async_setup_component(hass, 'sun') - ] - - # Set up demo platforms - demo_config = config.copy() - for component in COMPONENTS_WITH_DEMO_PLATFORM: - demo_config[component] = {CONF_PLATFORM: 'demo'} - tasks.append( - bootstrap.async_setup_component(hass, component, demo_config)) - - # Set up input select - tasks.append(bootstrap.async_setup_component( - hass, 'input_select', - {'input_select': - {'living_room_preset': {'options': ['Visitors', - 'Visitors with kids', - 'Home Alone']}, - 'who_cooks': {'icon': 'mdi:panda', - 'initial': 'Anne Therese', - 'name': 'Cook today', - 'options': ['Paulus', 'Anne Therese']}}})) - # Set up input boolean - tasks.append(bootstrap.async_setup_component( - hass, 'input_boolean', - {'input_boolean': {'notify': { - 'icon': 'mdi:car', - 'initial': False, - 'name': 'Notify Anne Therese is home'}}})) - - # Set up input boolean - tasks.append(bootstrap.async_setup_component( - hass, 'input_number', - {'input_number': { - 'noise_allowance': {'icon': 'mdi:bell-ring', - 'min': 0, - 'max': 10, - 'name': 'Allowed Noise', - 'unit_of_measurement': 'dB'}}})) - - # Set up weblink - tasks.append(bootstrap.async_setup_component( - hass, 'weblink', - {'weblink': {'entities': [{'name': 'Router', - 'url': 'http://192.168.1.1'}]}})) - - results = await asyncio.gather(*tasks, loop=hass.loop) - - if any(not result for result in results): - return False - - # Set up example persistent notification - persistent_notification.async_create( - 'This is an example of a persistent notification.', - title='Example Notification') - - # Set up room groups - lights = sorted(hass.states.async_entity_ids('light')) - switches = sorted(hass.states.async_entity_ids('switch')) - media_players = sorted(hass.states.async_entity_ids('media_player')) - - tasks2 = [] - - # Set up history graph - tasks2.append(bootstrap.async_setup_component( - hass, 'history_graph', - {'history_graph': {'switches': { - 'name': 'Recent Switches', - 'entities': switches, - 'hours_to_show': 1, - 'refresh': 60 - }}} - )) - - # Set up scripts - tasks2.append(bootstrap.async_setup_component( - hass, 'script', - {'script': { - 'demo': { - 'alias': 'Toggle {}'.format(lights[0].split('.')[1]), - 'sequence': [{ - 'service': 'light.turn_off', - 'data': {ATTR_ENTITY_ID: lights[0]} - }, { - 'delay': {'seconds': 5} - }, { - 'service': 'light.turn_on', - 'data': {ATTR_ENTITY_ID: lights[0]} - }, { - 'delay': {'seconds': 5} - }, { - 'service': 'light.turn_off', - 'data': {ATTR_ENTITY_ID: lights[0]} - }] - }}})) - - # Set up scenes - tasks2.append(bootstrap.async_setup_component( - hass, 'scene', - {'scene': [ - {'name': 'Romantic lights', - 'entities': { - lights[0]: True, - lights[1]: {'state': 'on', 'xy_color': [0.33, 0.66], - 'brightness': 200}, - }}, - {'name': 'Switch on and off', - 'entities': { - switches[0]: True, - switches[1]: False, - }}, - ]})) - - tasks2.append(group.Group.async_create_group(hass, 'Living Room', [ - lights[1], switches[0], 'input_select.living_room_preset', - 'cover.living_room_window', media_players[1], - 'scene.romantic_lights'])) - tasks2.append(group.Group.async_create_group(hass, 'Bedroom', [ - lights[0], switches[1], media_players[0], - 'input_number.noise_allowance'])) - tasks2.append(group.Group.async_create_group(hass, 'Kitchen', [ - lights[2], 'cover.kitchen_window', 'lock.kitchen_door'])) - tasks2.append(group.Group.async_create_group(hass, 'Doors', [ - 'lock.front_door', 'lock.kitchen_door', - 'garage_door.right_garage_door', 'garage_door.left_garage_door'])) - tasks2.append(group.Group.async_create_group(hass, 'Automations', [ - 'input_select.who_cooks', 'input_boolean.notify', ])) - tasks2.append(group.Group.async_create_group(hass, 'People', [ - 'device_tracker.demo_anne_therese', 'device_tracker.demo_home_boy', - 'device_tracker.demo_paulus'])) - tasks2.append(group.Group.async_create_group(hass, 'Downstairs', [ - 'group.living_room', 'group.kitchen', - 'scene.romantic_lights', 'cover.kitchen_window', - 'cover.living_room_window', 'group.doors', - 'climate.ecobee', - ], view=True)) - - results = await asyncio.gather(*tasks2, loop=hass.loop) - - if any(not result for result in results): - return False - - # Set up configurator - configurator_ids = [] - - def hue_configuration_callback(data): - """Fake callback, mark config as done.""" - time.sleep(2) - - # First time it is called, pretend it failed. - if len(configurator_ids) == 1: - configurator.notify_errors( - configurator_ids[0], - "Failed to register, please try again.") - - configurator_ids.append(0) - else: - configurator.request_done(configurator_ids[0]) - - def setup_configurator(): - """Set up a configurator.""" - request_id = configurator.request_config( - "Philips Hue", hue_configuration_callback, - description=("Press the button on the bridge to register Philips " - "Hue with Home Assistant."), - description_image="/static/images/config_philips_hue.jpg", - fields=[{'id': 'username', 'name': 'Username'}], - submit_caption="I have pressed the button" - ) - configurator_ids.append(request_id) - - hass.async_add_job(setup_configurator) - - return True diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py new file mode 100644 index 0000000000000..2699ade0b1d00 --- /dev/null +++ b/homeassistant/components/demo/__init__.py @@ -0,0 +1,226 @@ +"""Set up the demo environment that mimics interaction with devices.""" +import asyncio +import time + +from homeassistant import bootstrap +import homeassistant.core as ha +from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM + +DEPENDENCIES = ['conversation', 'introduction', 'zone'] +DOMAIN = 'demo' + +COMPONENTS_WITH_DEMO_PLATFORM = [ + 'air_quality', + 'alarm_control_panel', + 'binary_sensor', + 'calendar', + 'camera', + 'climate', + 'cover', + 'device_tracker', + 'fan', + 'image_processing', + 'light', + 'lock', + 'media_player', + 'notify', + 'sensor', + 'switch', + 'tts', + 'mailbox', +] + + +async def async_setup(hass, config): + """Set up the demo environment.""" + group = hass.components.group + configurator = hass.components.configurator + persistent_notification = hass.components.persistent_notification + + config.setdefault(ha.DOMAIN, {}) + config.setdefault(DOMAIN, {}) + + if config[DOMAIN].get('hide_demo_state') != 1: + hass.states.async_set('a.Demo_Mode', 'Enabled') + + # Setup sun + if not hass.config.latitude: + hass.config.latitude = 32.87336 + + if not hass.config.longitude: + hass.config.longitude = 117.22743 + + tasks = [ + bootstrap.async_setup_component(hass, 'sun') + ] + + # Set up demo platforms + demo_config = config.copy() + for component in COMPONENTS_WITH_DEMO_PLATFORM: + demo_config[component] = {CONF_PLATFORM: 'demo'} + tasks.append( + bootstrap.async_setup_component(hass, component, demo_config)) + + # Set up input select + tasks.append(bootstrap.async_setup_component( + hass, 'input_select', + {'input_select': + {'living_room_preset': {'options': ['Visitors', + 'Visitors with kids', + 'Home Alone']}, + 'who_cooks': {'icon': 'mdi:panda', + 'initial': 'Anne Therese', + 'name': 'Cook today', + 'options': ['Paulus', 'Anne Therese']}}})) + # Set up input boolean + tasks.append(bootstrap.async_setup_component( + hass, 'input_boolean', + {'input_boolean': {'notify': { + 'icon': 'mdi:car', + 'initial': False, + 'name': 'Notify Anne Therese is home'}}})) + + # Set up input boolean + tasks.append(bootstrap.async_setup_component( + hass, 'input_number', + {'input_number': { + 'noise_allowance': {'icon': 'mdi:bell-ring', + 'min': 0, + 'max': 10, + 'name': 'Allowed Noise', + 'unit_of_measurement': 'dB'}}})) + + # Set up weblink + tasks.append(bootstrap.async_setup_component( + hass, 'weblink', + {'weblink': {'entities': [{'name': 'Router', + 'url': 'http://192.168.1.1'}]}})) + + results = await asyncio.gather(*tasks, loop=hass.loop) + + if any(not result for result in results): + return False + + # Set up example persistent notification + persistent_notification.async_create( + 'This is an example of a persistent notification.', + title='Example Notification') + + # Set up room groups + lights = sorted(hass.states.async_entity_ids('light')) + switches = sorted(hass.states.async_entity_ids('switch')) + media_players = sorted(hass.states.async_entity_ids('media_player')) + + tasks2 = [] + + # Set up history graph + tasks2.append(bootstrap.async_setup_component( + hass, 'history_graph', + {'history_graph': {'switches': { + 'name': 'Recent Switches', + 'entities': switches, + 'hours_to_show': 1, + 'refresh': 60 + }}} + )) + + # Set up scripts + tasks2.append(bootstrap.async_setup_component( + hass, 'script', + {'script': { + 'demo': { + 'alias': 'Toggle {}'.format(lights[0].split('.')[1]), + 'sequence': [{ + 'service': 'light.turn_off', + 'data': {ATTR_ENTITY_ID: lights[0]} + }, { + 'delay': {'seconds': 5} + }, { + 'service': 'light.turn_on', + 'data': {ATTR_ENTITY_ID: lights[0]} + }, { + 'delay': {'seconds': 5} + }, { + 'service': 'light.turn_off', + 'data': {ATTR_ENTITY_ID: lights[0]} + }] + }}})) + + # Set up scenes + tasks2.append(bootstrap.async_setup_component( + hass, 'scene', + {'scene': [ + {'name': 'Romantic lights', + 'entities': { + lights[0]: True, + lights[1]: {'state': 'on', 'xy_color': [0.33, 0.66], + 'brightness': 200}, + }}, + {'name': 'Switch on and off', + 'entities': { + switches[0]: True, + switches[1]: False, + }}, + ]})) + + tasks2.append(group.Group.async_create_group(hass, 'Living Room', [ + lights[1], switches[0], 'input_select.living_room_preset', + 'cover.living_room_window', media_players[1], + 'scene.romantic_lights'])) + tasks2.append(group.Group.async_create_group(hass, 'Bedroom', [ + lights[0], switches[1], media_players[0], + 'input_number.noise_allowance'])) + tasks2.append(group.Group.async_create_group(hass, 'Kitchen', [ + lights[2], 'cover.kitchen_window', 'lock.kitchen_door'])) + tasks2.append(group.Group.async_create_group(hass, 'Doors', [ + 'lock.front_door', 'lock.kitchen_door', + 'garage_door.right_garage_door', 'garage_door.left_garage_door'])) + tasks2.append(group.Group.async_create_group(hass, 'Automations', [ + 'input_select.who_cooks', 'input_boolean.notify', ])) + tasks2.append(group.Group.async_create_group(hass, 'People', [ + 'device_tracker.demo_anne_therese', 'device_tracker.demo_home_boy', + 'device_tracker.demo_paulus'])) + tasks2.append(group.Group.async_create_group(hass, 'Downstairs', [ + 'group.living_room', 'group.kitchen', + 'scene.romantic_lights', 'cover.kitchen_window', + 'cover.living_room_window', 'group.doors', + 'climate.ecobee', + ], view=True)) + + results = await asyncio.gather(*tasks2, loop=hass.loop) + + if any(not result for result in results): + return False + + # Set up configurator + configurator_ids = [] + + def hue_configuration_callback(data): + """Fake callback, mark config as done.""" + time.sleep(2) + + # First time it is called, pretend it failed. + if len(configurator_ids) == 1: + configurator.notify_errors( + configurator_ids[0], + "Failed to register, please try again.") + + configurator_ids.append(0) + else: + configurator.request_done(configurator_ids[0]) + + def setup_configurator(): + """Set up a configurator.""" + request_id = configurator.request_config( + "Philips Hue", hue_configuration_callback, + description=("Press the button on the bridge to register Philips " + "Hue with Home Assistant."), + description_image="/static/images/config_philips_hue.jpg", + fields=[{'id': 'username', 'name': 'Username'}], + submit_caption="I have pressed the button" + ) + configurator_ids.append(request_id) + + hass.async_add_job(setup_configurator) + + return True 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 202883713c740..af33453c9d55f 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -22,10 +22,10 @@ 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 @@ 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 297e98e548a7c..f59c922577b01 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/bmw_connected_drive.py b/homeassistant/components/device_tracker/bmw_connected_drive.py deleted file mode 100644 index 02a1265318070..0000000000000 --- a/homeassistant/components/device_tracker/bmw_connected_drive.py +++ /dev/null @@ -1,58 +0,0 @@ -"""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/ -""" -import logging - -from homeassistant.components.bmw_connected_drive import DOMAIN \ - as BMW_DOMAIN -from homeassistant.util import slugify - -DEPENDENCIES = ['bmw_connected_drive'] - -_LOGGER = logging.getLogger(__name__) - - -def setup_scanner(hass, config, see, discovery_info=None): - """Set up the BMW tracker.""" - accounts = hass.data[BMW_DOMAIN] - _LOGGER.debug('Found BMW accounts: %s', - ', '.join([a.name for a in accounts])) - for account in accounts: - for vehicle in account.account.vehicles: - tracker = BMWDeviceTracker(see, vehicle) - account.add_update_listener(tracker.update) - tracker.update() - return True - - -class BMWDeviceTracker: - """BMW Connected Drive device tracker.""" - - def __init__(self, see, vehicle): - """Initialize the Tracker.""" - self._see = see - self.vehicle = vehicle - - def update(self) -> None: - """Update the device info. - - Only update the state in home assistant if tracking in - the car is enabled. - """ - dev_id = slugify(self.vehicle.name) - - if not self.vehicle.state.is_vehicle_tracking_enabled: - _LOGGER.debug('Tracking is disabled for vehicle %s', dev_id) - return - - _LOGGER.debug('Updating %s', dev_id) - attrs = { - 'vin': self.vehicle.vin, - } - self._see( - dev_id=dev_id, host_name=self.vehicle.name, - gps=self.vehicle.state.gps_position, attributes=attrs, - icon='mdi:car' - ) diff --git a/homeassistant/components/device_tracker/freebox.py b/homeassistant/components/device_tracker/freebox.py deleted file mode 100644 index f4e1ce5bd8a03..0000000000000 --- a/homeassistant/components/device_tracker/freebox.py +++ /dev/null @@ -1,71 +0,0 @@ -""" -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/ -""" -from collections import namedtuple -import logging - -from homeassistant.components.device_tracker import DeviceScanner -from homeassistant.components.freebox import DATA_FREEBOX - -DEPENDENCIES = ['freebox'] - -_LOGGER = logging.getLogger(__name__) - - -async def async_get_scanner(hass, config): - """Validate the configuration and return a Freebox scanner.""" - scanner = FreeboxDeviceScanner(hass.data[DATA_FREEBOX]) - await scanner.async_connect() - return scanner if scanner.success_init else None - -Device = namedtuple('Device', ['id', 'name', 'ip']) - - -def _build_device(device_dict): - return Device( - device_dict['l2ident']['id'], - device_dict['primary_name'], - device_dict['l3connectivities'][0]['addr']) - - -class FreeboxDeviceScanner(DeviceScanner): - """Queries the Freebox device.""" - - def __init__(self, fbx): - """Initialize the scanner.""" - self.last_results = {} - self.success_init = False - self.connection = fbx - - async def async_connect(self): - """Initialize connection to the router.""" - # Test the router is accessible. - data = await self.connection.lan.get_hosts_list() - 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 [device.id for device in self.last_results] - - async def get_device_name(self, device): - """Return the name of the given device or None if we don't know.""" - name = next(( - result.name for result in self.last_results - if result.id == device), None) - return name - - async def async_update_info(self): - """Ensure the information from the Freebox router is up to date.""" - _LOGGER.debug('Checking Devices') - - hosts = await self.connection.lan.get_hosts_list() - - last_results = [_build_device(device) - for device in hosts - if device['active']] - - self.last_results = last_results diff --git a/homeassistant/components/device_tracker/googlehome.py b/homeassistant/components/device_tracker/googlehome.py deleted file mode 100644 index daa36d1d2c71d..0000000000000 --- 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 cddcd1f26eefb..c4635f8dc431a 100644 --- a/homeassistant/components/device_tracker/mikrotik.py +++ b/homeassistant/components/device_tracker/mikrotik.py @@ -174,7 +174,7 @@ def _update_info(self): 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/mysensors.py b/homeassistant/components/device_tracker/mysensors.py deleted file mode 100644 index 705dc9968c959..0000000000000 --- a/homeassistant/components/device_tracker/mysensors.py +++ /dev/null @@ -1,60 +0,0 @@ -""" -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/ -""" -from homeassistant.components import mysensors -from homeassistant.components.device_tracker import DOMAIN -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.util import slugify - - -async def async_setup_scanner(hass, config, async_see, discovery_info=None): - """Set up the MySensors device scanner.""" - new_devices = mysensors.setup_mysensors_platform( - hass, DOMAIN, discovery_info, MySensorsDeviceScanner, - device_args=(hass, async_see)) - if not new_devices: - return False - - for device in new_devices: - gateway_id = id(device.gateway) - dev_id = ( - gateway_id, device.node_id, device.child_id, - device.value_type) - async_dispatcher_connect( - hass, mysensors.const.CHILD_CALLBACK.format(*dev_id), - device.async_update_callback) - async_dispatcher_connect( - hass, - mysensors.const.NODE_CALLBACK.format(gateway_id, device.node_id), - device.async_update_callback) - - return True - - -class MySensorsDeviceScanner(mysensors.device.MySensorsDevice): - """Represent a MySensors scanner.""" - - def __init__(self, hass, async_see, *args): - """Set up instance.""" - super().__init__(*args) - self.async_see = async_see - self.hass = hass - - async def _async_update_callback(self): - """Update the device.""" - await self.async_update() - node = self.gateway.sensors[self.node_id] - child = node.children[self.child_id] - position = child.values[self.value_type] - latitude, longitude, _ = position.split(',') - - await self.async_see( - dev_id=slugify(self.name), - host_name=self.name, - gps=(latitude, longitude), - battery=node.battery_level, - attributes=self.device_state_attributes - ) diff --git a/homeassistant/components/device_tracker/synology_srm.py b/homeassistant/components/device_tracker/synology_srm.py index cc931b797d410..5c7ac9a5d00df 100644 --- a/homeassistant/components/device_tracker/synology_srm.py +++ b/homeassistant/components/device_tracker/synology_srm.py @@ -13,7 +13,7 @@ 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/device_tracker/tado.py b/homeassistant/components/device_tracker/tado.py deleted file mode 100644 index ef816338ce970..0000000000000 --- a/homeassistant/components/device_tracker/tado.py +++ /dev/null @@ -1,143 +0,0 @@ -""" -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/ -""" -import logging -from datetime import timedelta -from collections import namedtuple - -import asyncio -import aiohttp -import async_timeout -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD -from homeassistant.util import Throttle -from homeassistant.components.device_tracker import ( - DOMAIN, PLATFORM_SCHEMA, DeviceScanner) -from homeassistant.helpers.aiohttp_client import async_create_clientsession - -_LOGGER = logging.getLogger(__name__) - -CONF_HOME_ID = 'home_id' - -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 -}) - - -def get_scanner(hass, config): - """Return a Tado scanner.""" - scanner = TadoDeviceScanner(hass, config[DOMAIN]) - return scanner if scanner.success_init else None - - -Device = namedtuple("Device", ["mac", "name"]) - - -class TadoDeviceScanner(DeviceScanner): - """This class gets geofenced devices from Tado.""" - - def __init__(self, hass, config): - """Initialize the scanner.""" - self.last_results = [] - - self.username = config[CONF_USERNAME] - self.password = config[CONF_PASSWORD] - - # The Tado device tracker can work with or without a home_id - self.home_id = config[CONF_HOME_ID] if CONF_HOME_ID in config else None - - # If there's a home_id, we need a different API URL - if self.home_id is None: - self.tadoapiurl = 'https://my.tado.com/api/v2/me' - else: - self.tadoapiurl = 'https://my.tado.com/api/v2' \ - '/homes/{home_id}/mobileDevices' - - # The API URL always needs a username and password - self.tadoapiurl += '?username={username}&password={password}' - - self.websession = async_create_clientsession( - hass, cookie_jar=aiohttp.CookieJar(unsafe=True, loop=hass.loop)) - - self.success_init = asyncio.run_coroutine_threadsafe( - self._async_update_info(), hass.loop - ).result() - - _LOGGER.info("Scanner initialized") - - async def async_scan_devices(self): - """Scan for devices and return a list containing found device ids.""" - await self._async_update_info() - return [device.mac for device in self.last_results] - - async def async_get_device_name(self, device): - """Return the name of the given device or None if we don't know.""" - filter_named = [result.name for result in self.last_results - if result.mac == device] - - if filter_named: - return filter_named[0] - return None - - @Throttle(MIN_TIME_BETWEEN_SCANS) - async def _async_update_info(self): - """ - Query Tado for device marked as at home. - - Returns boolean if scanning successful. - """ - _LOGGER.debug("Requesting Tado") - - last_results = [] - - try: - with async_timeout.timeout(10): - # Format the URL here, so we can log the template URL if - # anything goes wrong without exposing username and password. - url = self.tadoapiurl.format( - home_id=self.home_id, username=self.username, - password=self.password) - - response = await self.websession.get(url) - - if response.status != 200: - _LOGGER.warning( - "Error %d on %s.", response.status, self.tadoapiurl) - return False - - tado_json = await response.json() - - except (asyncio.TimeoutError, aiohttp.ClientError): - _LOGGER.error("Cannot load Tado data") - return False - - # Without a home_id, we fetched an URL where the mobile devices can be - # found under the mobileDevices key. - if 'mobileDevices' in tado_json: - tado_json = tado_json['mobileDevices'] - - # Find devices that have geofencing enabled, and are currently at home. - for mobile_device in tado_json: - if mobile_device.get('location'): - if mobile_device['location']['atHome']: - device_id = mobile_device['id'] - device_name = mobile_device['name'] - last_results.append(Device(device_id, device_name)) - - self.last_results = last_results - - _LOGGER.debug( - "Tado presence query successful, %d device(s) at home", - len(self.last_results) - ) - - return True diff --git a/homeassistant/components/device_tracker/tesla.py b/homeassistant/components/device_tracker/tesla.py deleted file mode 100644 index c08ddb4047b6a..0000000000000 --- a/homeassistant/components/device_tracker/tesla.py +++ /dev/null @@ -1,58 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.components.tesla import DOMAIN as TESLA_DOMAIN -from homeassistant.helpers.event import track_utc_time_change -from homeassistant.util import slugify - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['tesla'] - - -def setup_scanner(hass, config, see, discovery_info=None): - """Set up the Tesla tracker.""" - TeslaDeviceTracker( - hass, config, see, - hass.data[TESLA_DOMAIN]['devices']['devices_tracker']) - return True - - -class TeslaDeviceTracker: - """A class representing a Tesla device.""" - - def __init__(self, hass, config, see, tesla_devices): - """Initialize the Tesla device scanner.""" - self.hass = hass - self.see = see - self.devices = tesla_devices - self._update_info() - - track_utc_time_change( - self.hass, self._update_info, second=range(0, 60, 30)) - - def _update_info(self, now=None): - """Update the device info.""" - for device in self.devices: - device.update() - name = device.name - _LOGGER.debug("Updating device position: %s", name) - dev_id = slugify(device.uniq_name) - location = device.get_location() - if location: - lat = location['latitude'] - lon = location['longitude'] - attrs = { - 'trackr_id': dev_id, - 'id': dev_id, - 'name': name - } - self.see( - dev_id=dev_id, host_name=name, - gps=(lat, lon), attributes=attrs - ) diff --git a/homeassistant/components/device_tracker/volvooncall.py b/homeassistant/components/device_tracker/volvooncall.py deleted file mode 100644 index 395b539a065cb..0000000000000 --- a/homeassistant/components/device_tracker/volvooncall.py +++ /dev/null @@ -1,38 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.util import slugify -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.components.device_tracker import SOURCE_TYPE_GPS -from homeassistant.components.volvooncall import DATA_KEY, SIGNAL_STATE_UPDATED - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_scanner(hass, config, async_see, discovery_info=None): - """Set up the Volvo tracker.""" - if discovery_info is None: - return - - vin, component, attr = discovery_info - data = hass.data[DATA_KEY] - instrument = data.instrument(vin, component, attr) - - async def see_vehicle(): - """Handle the reporting of the vehicle position.""" - host_name = instrument.vehicle_name - dev_id = 'volvo_{}'.format(slugify(host_name)) - await async_see(dev_id=dev_id, - host_name=host_name, - source_type=SOURCE_TYPE_GPS, - gps=instrument.state, - icon='mdi:car') - - async_dispatcher_connect(hass, SIGNAL_STATE_UPDATED, see_vehicle) - - return True diff --git a/homeassistant/components/device_tracker/xiaomi_miio.py b/homeassistant/components/device_tracker/xiaomi_miio.py deleted file mode 100644 index c5c6ebcbc35ec..0000000000000 --- a/homeassistant/components/device_tracker/xiaomi_miio.py +++ /dev/null @@ -1,81 +0,0 @@ -""" -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/ -""" -import logging - -import voluptuous as vol - -from homeassistant.components.device_tracker import ( - DOMAIN, PLATFORM_SCHEMA, DeviceScanner) -from homeassistant.const import CONF_HOST, CONF_TOKEN -import homeassistant.helpers.config_validation as cv - -REQUIREMENTS = ['python-miio==0.4.4', 'construct==2.9.45'] - -_LOGGER = logging.getLogger(__name__) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)), -}) - - -def get_scanner(hass, config): - """Return a Xiaomi MiIO device scanner.""" - from miio import WifiRepeater, DeviceException - - scanner = None - host = config[DOMAIN].get(CONF_HOST) - token = config[DOMAIN].get(CONF_TOKEN) - - _LOGGER.info( - "Initializing with host %s (token %s...)", host, token[:5]) - - try: - device = WifiRepeater(host, token) - device_info = device.info() - _LOGGER.info("%s %s %s detected", - device_info.model, - device_info.firmware_version, - device_info.hardware_version) - scanner = XiaomiMiioDeviceScanner(device) - except DeviceException as ex: - _LOGGER.error("Device unavailable or token incorrect: %s", ex) - - return scanner - - -class XiaomiMiioDeviceScanner(DeviceScanner): - """This class queries a Xiaomi Mi WiFi Repeater.""" - - def __init__(self, device): - """Initialize the scanner.""" - self.device = device - - async def async_scan_devices(self): - """Scan for devices and return a list containing found device IDs.""" - from miio import DeviceException - - devices = [] - try: - station_info = \ - await self.hass.async_add_executor_job(self.device.status) - _LOGGER.debug("Got new station info: %s", station_info) - - for device in station_info.associated_stations: - devices.append(device['mac']) - - except DeviceException as ex: - _LOGGER.error("Unable to fetch the state: %s", ex) - - return devices - - async def async_get_device_name(self, device): - """Return None. - - The repeater doesn't provide the name of the associated device. - """ - return None diff --git a/homeassistant/components/dialogflow/.translations/da.json b/homeassistant/components/dialogflow/.translations/da.json new file mode 100644 index 0000000000000..2fb203450a5eb --- /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 cf53f81bdb8e9..33c465bf0e74e 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 3f3fbe7c14e2b..210aebe80d5cb 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 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.py deleted file mode 100644 index c0c9d95586c4a..0000000000000 --- a/homeassistant/components/digital_ocean.py +++ /dev/null @@ -1,93 +0,0 @@ -""" -Support for Digital Ocean. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/digital_ocean/ -""" -import logging -from datetime import timedelta - -import voluptuous as vol - -from homeassistant.const import CONF_ACCESS_TOKEN -from homeassistant.util import Throttle -import homeassistant.helpers.config_validation as cv - -REQUIREMENTS = ['python-digitalocean==1.13.2'] - -_LOGGER = logging.getLogger(__name__) - -ATTR_CREATED_AT = 'created_at' -ATTR_DROPLET_ID = 'droplet_id' -ATTR_DROPLET_NAME = 'droplet_name' -ATTR_FEATURES = 'features' -ATTR_IPV4_ADDRESS = 'ipv4_address' -ATTR_IPV6_ADDRESS = 'ipv6_address' -ATTR_MEMORY = 'memory' -ATTR_REGION = 'region' -ATTR_VCPUS = 'vcpus' - -CONF_ATTRIBUTION = 'Data provided by Digital Ocean' -CONF_DROPLETS = 'droplets' - -DATA_DIGITAL_OCEAN = 'data_do' -DIGITAL_OCEAN_PLATFORMS = ['switch', 'binary_sensor'] -DOMAIN = 'digital_ocean' - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_ACCESS_TOKEN): cv.string, - }), -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the Digital Ocean component.""" - import digitalocean - - conf = config[DOMAIN] - access_token = conf.get(CONF_ACCESS_TOKEN) - - digital = DigitalOcean(access_token) - - try: - if not digital.manager.get_account(): - _LOGGER.error("No account found for the given API token") - return False - except digitalocean.baseapi.DataReadError: - _LOGGER.error("API token not valid for authentication") - return False - - hass.data[DATA_DIGITAL_OCEAN] = digital - - return True - - -class DigitalOcean: - """Handle all communication with the Digital Ocean API.""" - - def __init__(self, access_token): - """Initialize the Digital Ocean connection.""" - import digitalocean - - self._access_token = access_token - self.data = None - self.manager = digitalocean.Manager(token=self._access_token) - - def get_droplet_id(self, droplet_name): - """Get the status of a Digital Ocean droplet.""" - droplet_id = None - - all_droplets = self.manager.get_all_droplets() - for droplet in all_droplets: - if droplet_name == droplet.name: - droplet_id = droplet.id - - return droplet_id - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Use the data from Digital Ocean API.""" - self.data = self.manager.get_all_droplets() diff --git a/homeassistant/components/digital_ocean/__init__.py b/homeassistant/components/digital_ocean/__init__.py new file mode 100644 index 0000000000000..d061dad67261c --- /dev/null +++ b/homeassistant/components/digital_ocean/__init__.py @@ -0,0 +1,88 @@ +"""Support for Digital Ocean.""" +import logging +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.util import Throttle +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['python-digitalocean==1.13.2'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_CREATED_AT = 'created_at' +ATTR_DROPLET_ID = 'droplet_id' +ATTR_DROPLET_NAME = 'droplet_name' +ATTR_FEATURES = 'features' +ATTR_IPV4_ADDRESS = 'ipv4_address' +ATTR_IPV6_ADDRESS = 'ipv6_address' +ATTR_MEMORY = 'memory' +ATTR_REGION = 'region' +ATTR_VCPUS = 'vcpus' + +CONF_ATTRIBUTION = 'Data provided by Digital Ocean' +CONF_DROPLETS = 'droplets' + +DATA_DIGITAL_OCEAN = 'data_do' +DIGITAL_OCEAN_PLATFORMS = ['switch', 'binary_sensor'] +DOMAIN = 'digital_ocean' + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_ACCESS_TOKEN): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Digital Ocean component.""" + import digitalocean + + conf = config[DOMAIN] + access_token = conf.get(CONF_ACCESS_TOKEN) + + digital = DigitalOcean(access_token) + + try: + if not digital.manager.get_account(): + _LOGGER.error("No account found for the given API token") + return False + except digitalocean.baseapi.DataReadError: + _LOGGER.error("API token not valid for authentication") + return False + + hass.data[DATA_DIGITAL_OCEAN] = digital + + return True + + +class DigitalOcean: + """Handle all communication with the Digital Ocean API.""" + + def __init__(self, access_token): + """Initialize the Digital Ocean connection.""" + import digitalocean + + self._access_token = access_token + self.data = None + self.manager = digitalocean.Manager(token=self._access_token) + + def get_droplet_id(self, droplet_name): + """Get the status of a Digital Ocean droplet.""" + droplet_id = None + + all_droplets = self.manager.get_all_droplets() + for droplet in all_droplets: + if droplet_name == droplet.name: + droplet_id = droplet.id + + return droplet_id + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Use the data from Digital Ocean API.""" + self.data = self.manager.get_all_droplets() diff --git a/homeassistant/components/digital_ocean/binary_sensor.py b/homeassistant/components/digital_ocean/binary_sensor.py new file mode 100644 index 0000000000000..255f43b67bab3 --- /dev/null +++ b/homeassistant/components/digital_ocean/binary_sensor.py @@ -0,0 +1,92 @@ +"""Support for monitoring the state of Digital Ocean droplets.""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA) +from homeassistant.components.digital_ocean import ( + CONF_DROPLETS, ATTR_CREATED_AT, ATTR_DROPLET_ID, ATTR_DROPLET_NAME, + ATTR_FEATURES, ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY, + ATTR_REGION, ATTR_VCPUS, CONF_ATTRIBUTION, DATA_DIGITAL_OCEAN) +from homeassistant.const import ATTR_ATTRIBUTION + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'Droplet' +DEFAULT_DEVICE_CLASS = 'moving' +DEPENDENCIES = ['digital_ocean'] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_DROPLETS): vol.All(cv.ensure_list, [cv.string]), +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Digital Ocean droplet sensor.""" + digital = hass.data.get(DATA_DIGITAL_OCEAN) + if not digital: + return False + + droplets = config.get(CONF_DROPLETS) + + dev = [] + for droplet in droplets: + droplet_id = digital.get_droplet_id(droplet) + if droplet_id is None: + _LOGGER.error("Droplet %s is not available", droplet) + return False + dev.append(DigitalOceanBinarySensor(digital, droplet_id)) + + add_entities(dev, True) + + +class DigitalOceanBinarySensor(BinarySensorDevice): + """Representation of a Digital Ocean droplet sensor.""" + + def __init__(self, do, droplet_id): + """Initialize a new Digital Ocean sensor.""" + self._digital_ocean = do + self._droplet_id = droplet_id + self._state = None + self.data = None + + @property + def name(self): + """Return the name of the sensor.""" + return self.data.name + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self.data.status == 'active' + + @property + def device_class(self): + """Return the class of this sensor.""" + return DEFAULT_DEVICE_CLASS + + @property + def device_state_attributes(self): + """Return the state attributes of the Digital Ocean droplet.""" + return { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_CREATED_AT: self.data.created_at, + ATTR_DROPLET_ID: self.data.id, + ATTR_DROPLET_NAME: self.data.name, + ATTR_FEATURES: self.data.features, + ATTR_IPV4_ADDRESS: self.data.ip_address, + ATTR_IPV6_ADDRESS: self.data.ip_v6_address, + ATTR_MEMORY: self.data.memory, + ATTR_REGION: self.data.region['name'], + ATTR_VCPUS: self.data.vcpus, + } + + def update(self): + """Update state of sensor.""" + self._digital_ocean.update() + + for droplet in self._digital_ocean.data: + if droplet.id == self._droplet_id: + self.data = droplet diff --git a/homeassistant/components/digital_ocean/switch.py b/homeassistant/components/digital_ocean/switch.py new file mode 100644 index 0000000000000..a10c961b8e43a --- /dev/null +++ b/homeassistant/components/digital_ocean/switch.py @@ -0,0 +1,96 @@ +"""Support for interacting with Digital Ocean droplets.""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) +from homeassistant.components.digital_ocean import ( + CONF_DROPLETS, ATTR_CREATED_AT, ATTR_DROPLET_ID, ATTR_DROPLET_NAME, + ATTR_FEATURES, ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY, + ATTR_REGION, ATTR_VCPUS, CONF_ATTRIBUTION, DATA_DIGITAL_OCEAN) +from homeassistant.const import ATTR_ATTRIBUTION + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['digital_ocean'] + +DEFAULT_NAME = 'Droplet' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_DROPLETS): vol.All(cv.ensure_list, [cv.string]), +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Digital Ocean droplet switch.""" + digital = hass.data.get(DATA_DIGITAL_OCEAN) + if not digital: + return False + + droplets = config.get(CONF_DROPLETS) + + dev = [] + for droplet in droplets: + droplet_id = digital.get_droplet_id(droplet) + if droplet_id is None: + _LOGGER.error("Droplet %s is not available", droplet) + return False + dev.append(DigitalOceanSwitch(digital, droplet_id)) + + add_entities(dev, True) + + +class DigitalOceanSwitch(SwitchDevice): + """Representation of a Digital Ocean droplet switch.""" + + def __init__(self, do, droplet_id): + """Initialize a new Digital Ocean sensor.""" + self._digital_ocean = do + self._droplet_id = droplet_id + self.data = None + self._state = None + + @property + def name(self): + """Return the name of the switch.""" + return self.data.name + + @property + def is_on(self): + """Return true if switch is on.""" + return self.data.status == 'active' + + @property + def device_state_attributes(self): + """Return the state attributes of the Digital Ocean droplet.""" + return { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_CREATED_AT: self.data.created_at, + ATTR_DROPLET_ID: self.data.id, + ATTR_DROPLET_NAME: self.data.name, + ATTR_FEATURES: self.data.features, + ATTR_IPV4_ADDRESS: self.data.ip_address, + ATTR_IPV6_ADDRESS: self.data.ip_v6_address, + ATTR_MEMORY: self.data.memory, + ATTR_REGION: self.data.region['name'], + ATTR_VCPUS: self.data.vcpus, + } + + def turn_on(self, **kwargs): + """Boot-up the droplet.""" + if self.data.status != 'active': + self.data.power_on() + + def turn_off(self, **kwargs): + """Shutdown the droplet.""" + if self.data.status == 'active': + self.data.power_off() + + def update(self): + """Get the latest data from the device and update the data.""" + self._digital_ocean.update() + + for droplet in self._digital_ocean.data: + if droplet.id == self._droplet_id: + self.data = droplet diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py deleted file mode 100644 index d8198ba303362..0000000000000 --- a/homeassistant/components/discovery.py +++ /dev/null @@ -1,216 +0,0 @@ -""" -Starts a service to scan in intervals for new devices. - -Will emit EVENT_PLATFORM_DISCOVERED whenever a new service has been discovered. - -Knows which components handle certain types, will make sure they are -loaded before the EVENT_PLATFORM_DISCOVERED is fired. -""" -import json -from datetime import timedelta -import logging -import os - -import voluptuous as vol - -from homeassistant import config_entries -from homeassistant.core import callback -from homeassistant.const import EVENT_HOMEASSISTANT_START -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.helpers.discovery import async_load_platform, async_discover -import homeassistant.util.dt as dt_util - -REQUIREMENTS = ['netdisco==2.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_DAIKIN = 'daikin' -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' - -CONFIG_ENTRY_HANDLERS = { - SERVICE_DAIKIN: 'daikin', - SERVICE_DECONZ: 'deconz', - 'esphome': 'esphome', - 'google_cast': 'cast', - SERVICE_HUE: 'hue', - SERVICE_TELLDUSLIVE: 'tellduslive', - SERVICE_IKEA_TRADFRI: 'tradfri', - 'sonos': 'sonos', - SERVICE_IGD: 'upnp', -} - -SERVICE_HANDLERS = { - SERVICE_HASS_IOS_APP: ('ios', None), - SERVICE_NETGEAR: ('device_tracker', None), - SERVICE_WEMO: ('wemo', None), - SERVICE_HASSIO: ('hassio', None), - SERVICE_AXIS: ('axis', None), - SERVICE_APPLE_TV: ('apple_tv', None), - SERVICE_ROKU: ('roku', None), - SERVICE_WINK: ('wink', None), - SERVICE_XIAOMI_GW: ('xiaomi_aqara', None), - SERVICE_SABNZBD: ('sabnzbd', None), - SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'), - SERVICE_KONNECTED: ('konnected', None), - SERVICE_OCTOPRINT: ('octoprint', None), - SERVICE_FREEBOX: ('freebox', None), - 'panasonic_viera': ('media_player', 'panasonic_viera'), - 'plex_mediaserver': ('media_player', 'plex'), - 'yamaha': ('media_player', 'yamaha'), - 'logitech_mediaserver': ('media_player', 'squeezebox'), - 'directv': ('media_player', 'directv'), - 'denonavr': ('media_player', 'denonavr'), - 'samsung_tv': ('media_player', 'samsungtv'), - 'yeelight': ('light', 'yeelight'), - 'frontier_silicon': ('media_player', 'frontier_silicon'), - 'openhome': ('media_player', 'openhome'), - 'harmony': ('remote', 'harmony'), - 'bose_soundtouch': ('media_player', 'soundtouch'), - 'bluesound': ('media_player', 'bluesound'), - 'songpal': ('media_player', 'songpal'), - 'kodi': ('media_player', 'kodi'), - 'volumio': ('media_player', 'volumio'), - 'lg_smart_device': ('media_player', 'lg_soundbar'), - 'nanoleaf_aurora': ('light', 'nanoleaf_aurora'), -} - -OPTIONAL_SERVICE_HANDLERS = { - SERVICE_HOMEKIT: ('homekit_controller', None), - SERVICE_DLNA_DMR: ('media_player', 'dlna_dmr'), -} - -CONF_IGNORE = 'ignore' -CONF_ENABLE = 'enable' - -CONFIG_SCHEMA = vol.Schema({ - vol.Required(DOMAIN): vol.Schema({ - vol.Optional(CONF_IGNORE, default=[]): - vol.All(cv.ensure_list, [ - vol.In(list(CONFIG_ENTRY_HANDLERS) + list(SERVICE_HANDLERS))]), - vol.Optional(CONF_ENABLE, default=[]): - vol.All(cv.ensure_list, [vol.In(OPTIONAL_SERVICE_HANDLERS)]) - }), -}, extra=vol.ALLOW_EXTRA) - - -async def async_setup(hass, config): - """Start a discovery service.""" - from netdisco.discovery import NetworkDiscovery - - logger = logging.getLogger(__name__) - netdisco = NetworkDiscovery() - already_discovered = set() - - # Disable zeroconf logging, it spams - logging.getLogger('zeroconf').setLevel(logging.CRITICAL) - - # Platforms ignore by config - ignored_platforms = config[DOMAIN][CONF_IGNORE] - - # Optional platforms enabled by config - enabled_platforms = config[DOMAIN][CONF_ENABLE] - - async def new_service_found(service, info): - """Handle a new service if one is found.""" - if service in ignored_platforms: - logger.info("Ignoring service: %s %s", service, info) - return - - discovery_hash = json.dumps([service, info], sort_keys=True) - if discovery_hash in already_discovered: - logger.debug("Already discovered service %s %s.", service, info) - return - - already_discovered.add(discovery_hash) - - if service in CONFIG_ENTRY_HANDLERS: - await hass.config_entries.flow.async_init( - CONFIG_ENTRY_HANDLERS[service], - context={'source': config_entries.SOURCE_DISCOVERY}, - data=info - ) - return - - comp_plat = SERVICE_HANDLERS.get(service) - - if not comp_plat and service in enabled_platforms: - comp_plat = OPTIONAL_SERVICE_HANDLERS[service] - - # We do not know how to handle this service. - if not comp_plat: - logger.info("Unknown service discovered: %s %s", service, info) - return - - logger.info("Found new service: %s %s", service, info) - - component, platform = comp_plat - - if platform is None: - await async_discover(hass, service, info, component, config) - else: - await async_load_platform( - hass, component, platform, info, config) - - async def scan_devices(now): - """Scan for devices.""" - try: - results = await hass.async_add_job(_discover, netdisco) - - for result in results: - hass.async_create_task(new_service_found(*result)) - except OSError: - logger.error("Network is unreachable") - - async_track_point_in_utc_time( - hass, scan_devices, dt_util.utcnow() + SCAN_INTERVAL) - - @callback - def schedule_first(event): - """Schedule the first discovery when Home Assistant starts up.""" - async_track_point_in_utc_time(hass, scan_devices, dt_util.utcnow()) - - # Discovery for local services - if 'HASSIO' in os.environ: - hass.async_create_task(new_service_found(SERVICE_HASSIO, {})) - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, schedule_first) - - return True - - -def _discover(netdisco): - """Discover devices.""" - results = [] - try: - netdisco.scan() - - for disc in netdisco.discover(): - for service in netdisco.get_info(disc): - results.append((disc, service)) - - finally: - netdisco.stop() - - return results diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py new file mode 100644 index 0000000000000..85e3164d08bdc --- /dev/null +++ b/homeassistant/components/discovery/__init__.py @@ -0,0 +1,220 @@ +""" +Starts a service to scan in intervals for new devices. + +Will emit EVENT_PLATFORM_DISCOVERED whenever a new service has been discovered. + +Knows which components handle certain types, will make sure they are +loaded before the EVENT_PLATFORM_DISCOVERED is fired. +""" +import json +from datetime import timedelta +import logging +import os + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.core import callback +from homeassistant.const import EVENT_HOMEASSISTANT_START +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.helpers.discovery import async_load_platform, async_discover +import homeassistant.util.dt as dt_util + +REQUIREMENTS = ['netdisco==2.3.0'] + +DOMAIN = 'discovery' + +SCAN_INTERVAL = timedelta(seconds=300) +SERVICE_APPLE_TV = 'apple_tv' +SERVICE_AXIS = 'axis' +SERVICE_DAIKIN = 'daikin' +SERVICE_DECONZ = 'deconz' +SERVICE_DLNA_DMR = 'dlna_dmr' +SERVICE_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_TELLDUSLIVE = 'tellstick' +SERVICE_WEMO = 'belkin_wemo' +SERVICE_WINK = 'wink' +SERVICE_XIAOMI_GW = 'xiaomi_gw' + +CONFIG_ENTRY_HANDLERS = { + SERVICE_DAIKIN: 'daikin', + SERVICE_DECONZ: 'deconz', + 'esphome': 'esphome', + 'google_cast': 'cast', + SERVICE_HUE: 'hue', + SERVICE_TELLDUSLIVE: 'tellduslive', + SERVICE_IKEA_TRADFRI: 'tradfri', + 'sonos': 'sonos', + SERVICE_IGD: 'upnp', +} + +SERVICE_HANDLERS = { + SERVICE_HASS_IOS_APP: ('ios', None), + SERVICE_NETGEAR: ('device_tracker', None), + SERVICE_WEMO: ('wemo', None), + SERVICE_HASSIO: ('hassio', None), + SERVICE_AXIS: ('axis', None), + SERVICE_APPLE_TV: ('apple_tv', None), + SERVICE_ROKU: ('roku', None), + SERVICE_WINK: ('wink', None), + SERVICE_XIAOMI_GW: ('xiaomi_aqara', None), + SERVICE_SABNZBD: ('sabnzbd', None), + SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'), + SERVICE_KONNECTED: ('konnected', None), + SERVICE_OCTOPRINT: ('octoprint', None), + SERVICE_FREEBOX: ('freebox', None), + 'panasonic_viera': ('media_player', 'panasonic_viera'), + 'plex_mediaserver': ('media_player', 'plex'), + 'yamaha': ('media_player', 'yamaha'), + 'logitech_mediaserver': ('media_player', 'squeezebox'), + 'directv': ('media_player', 'directv'), + 'denonavr': ('media_player', 'denonavr'), + 'samsung_tv': ('media_player', 'samsungtv'), + 'yeelight': ('light', 'yeelight'), + 'frontier_silicon': ('media_player', 'frontier_silicon'), + 'openhome': ('media_player', 'openhome'), + 'harmony': ('remote', 'harmony'), + 'bose_soundtouch': ('media_player', 'soundtouch'), + 'bluesound': ('media_player', 'bluesound'), + 'songpal': ('media_player', 'songpal'), + 'kodi': ('media_player', 'kodi'), + 'volumio': ('media_player', 'volumio'), + 'lg_smart_device': ('media_player', 'lg_soundbar'), + 'nanoleaf_aurora': ('light', 'nanoleaf_aurora'), +} + +OPTIONAL_SERVICE_HANDLERS = { + SERVICE_HOMEKIT: ('homekit_controller', None), + SERVICE_DLNA_DMR: ('media_player', 'dlna_dmr'), +} + +CONF_IGNORE = 'ignore' +CONF_ENABLE = 'enable' + +CONFIG_SCHEMA = 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))]), + vol.Optional(CONF_ENABLE, default=[]): + vol.All(cv.ensure_list, [vol.In(OPTIONAL_SERVICE_HANDLERS)]) + }), +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Start a discovery service.""" + from netdisco.discovery import NetworkDiscovery + + logger = logging.getLogger(__name__) + netdisco = NetworkDiscovery() + already_discovered = set() + + # Disable zeroconf logging, it spams + logging.getLogger('zeroconf').setLevel(logging.CRITICAL) + + 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] + else: + ignored_platforms = [] + enabled_platforms = [] + + async def new_service_found(service, info): + """Handle a new service if one is found.""" + if service in ignored_platforms: + logger.info("Ignoring service: %s %s", service, info) + return + + discovery_hash = json.dumps([service, info], sort_keys=True) + if discovery_hash in already_discovered: + logger.debug("Already discovered service %s %s.", service, info) + return + + already_discovered.add(discovery_hash) + + if service in CONFIG_ENTRY_HANDLERS: + await hass.config_entries.flow.async_init( + CONFIG_ENTRY_HANDLERS[service], + context={'source': config_entries.SOURCE_DISCOVERY}, + data=info + ) + return + + comp_plat = SERVICE_HANDLERS.get(service) + + if not comp_plat and service in enabled_platforms: + comp_plat = OPTIONAL_SERVICE_HANDLERS[service] + + # We do not know how to handle this service. + if not comp_plat: + logger.info("Unknown service discovered: %s %s", service, info) + return + + logger.info("Found new service: %s %s", service, info) + + component, platform = comp_plat + + if platform is None: + await async_discover(hass, service, info, component, config) + else: + await async_load_platform( + hass, component, platform, info, config) + + async def scan_devices(now): + """Scan for devices.""" + try: + results = await hass.async_add_job(_discover, netdisco) + + for result in results: + hass.async_create_task(new_service_found(*result)) + except OSError: + logger.error("Network is unreachable") + + async_track_point_in_utc_time( + hass, scan_devices, dt_util.utcnow() + SCAN_INTERVAL) + + @callback + def schedule_first(event): + """Schedule the first discovery when Home Assistant starts up.""" + async_track_point_in_utc_time(hass, scan_devices, dt_util.utcnow()) + + # Discovery for local services + if 'HASSIO' in os.environ: + hass.async_create_task(new_service_found(SERVICE_HASSIO, {})) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, schedule_first) + + return True + + +def _discover(netdisco): + """Discover devices.""" + results = [] + try: + netdisco.scan() + + for disc in netdisco.discover(): + for service in netdisco.get_info(disc): + results.append((disc, service)) + + finally: + netdisco.stop() + + return results diff --git a/homeassistant/components/dominos.py b/homeassistant/components/dominos.py deleted file mode 100644 index 2c9f763aaa843..0000000000000 --- a/homeassistant/components/dominos.py +++ /dev/null @@ -1,252 +0,0 @@ -""" -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 -from datetime import timedelta - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components import http -from homeassistant.core import callback -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.util import Throttle - -_LOGGER = logging.getLogger(__name__) - -# The domain of your component. Should be equal to the name of your component. -DOMAIN = 'dominos' -ENTITY_ID_FORMAT = DOMAIN + '.{}' - -ATTR_COUNTRY = 'country_code' -ATTR_FIRST_NAME = 'first_name' -ATTR_LAST_NAME = 'last_name' -ATTR_EMAIL = 'email' -ATTR_PHONE = 'phone' -ATTR_ADDRESS = 'address' -ATTR_ORDERS = 'orders' -ATTR_SHOW_MENU = 'show_menu' -ATTR_ORDER_ENTITY = 'order_entity_id' -ATTR_ORDER_NAME = 'name' -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({ - vol.Required(ATTR_ORDER_NAME): cv.string, - vol.Required(ATTR_ORDER_CODES): vol.All(cv.ensure_list, [cv.string]), -}) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(ATTR_COUNTRY): cv.string, - vol.Required(ATTR_FIRST_NAME): cv.string, - vol.Required(ATTR_LAST_NAME): cv.string, - vol.Required(ATTR_EMAIL): cv.string, - vol.Required(ATTR_PHONE): cv.string, - vol.Required(ATTR_ADDRESS): cv.string, - vol.Optional(ATTR_SHOW_MENU): cv.boolean, - vol.Optional(ATTR_ORDERS, default=[]): vol.All( - cv.ensure_list, [_ORDERS_SCHEMA]), - }), -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up is called when Home Assistant is loading our component.""" - dominos = Dominos(hass, config) - - component = EntityComponent(_LOGGER, DOMAIN, hass) - hass.data[DOMAIN] = {} - entities = [] - conf = config[DOMAIN] - - hass.services.register(DOMAIN, 'order', dominos.handle_order) - - if conf.get(ATTR_SHOW_MENU): - hass.http.register_view(DominosProductListView(dominos)) - - for order_info in conf.get(ATTR_ORDERS): - order = DominosOrder(order_info, dominos) - entities.append(order) - - if entities: - component.add_entities(entities) - - # Return boolean to indicate that initialization was successfully. - return True - - -class Dominos(): - """Main Dominos service.""" - - def __init__(self, hass, config): - """Set up main service.""" - conf = config[DOMAIN] - from pizzapi import Address, Customer - from pizzapi.address import StoreException - self.hass = hass - self.customer = Customer( - conf.get(ATTR_FIRST_NAME), - conf.get(ATTR_LAST_NAME), - conf.get(ATTR_EMAIL), - conf.get(ATTR_PHONE), - conf.get(ATTR_ADDRESS)) - self.address = Address( - *self.customer.address.split(','), - country=conf.get(ATTR_COUNTRY)) - self.country = conf.get(ATTR_COUNTRY) - try: - self.closest_store = self.address.closest_store() - except StoreException: - self.closest_store = None - - def handle_order(self, call): - """Handle ordering pizza.""" - entity_ids = call.data.get(ATTR_ORDER_ENTITY, None) - - target_orders = [order for order in self.hass.data[DOMAIN]['entities'] - if order.entity_id in entity_ids] - - for order in target_orders: - order.place() - - @Throttle(MIN_TIME_BETWEEN_STORE_UPDATES) - def update_closest_store(self): - """Update the shared closest store (if open).""" - from pizzapi.address import StoreException - try: - self.closest_store = self.address.closest_store() - return True - except StoreException: - self.closest_store = None - return False - - def get_menu(self): - """Return the products from the closest stores menu.""" - self.update_closest_store() - if self.closest_store is None: - _LOGGER.warning('Cannot get menu. Store may be closed') - return [] - menu = self.closest_store.get_menu() - product_entries = [] - - for product in menu.products: - item = {} - if isinstance(product.menu_data['Variants'], list): - variants = ', '.join(product.menu_data['Variants']) - else: - variants = product.menu_data['Variants'] - item['name'] = product.name - item['variants'] = variants - product_entries.append(item) - - return product_entries - - -class DominosProductListView(http.HomeAssistantView): - """View to retrieve product list content.""" - - url = '/api/dominos' - name = "api:dominos" - - def __init__(self, dominos): - """Initialize suite view.""" - self.dominos = dominos - - @callback - def get(self, request): - """Retrieve if API is running.""" - return self.json(self.dominos.get_menu()) - - -class DominosOrder(Entity): - """Represents a Dominos order entity.""" - - def __init__(self, order_info, dominos): - """Set up the entity.""" - self._name = order_info['name'] - self._product_codes = order_info['codes'] - self._orderable = False - self.dominos = dominos - - @property - def name(self): - """Return the orders name.""" - return self._name - - @property - def product_codes(self): - """Return the orders product codes.""" - return self._product_codes - - @property - def orderable(self): - """Return the true if orderable.""" - return self._orderable - - @property - def state(self): - """Return the state either closed, orderable or unorderable.""" - if self.dominos.closest_store is None: - return 'closed' - return 'orderable' if self._orderable else 'unorderable' - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Update the order state and refreshes the store.""" - from pizzapi.address import StoreException - try: - self.dominos.update_closest_store() - except StoreException: - self._orderable = False - return - - try: - order = self.order() - order.pay_with() - self._orderable = True - except StoreException: - self._orderable = False - - def order(self): - """Create the order object.""" - from pizzapi import Order - from pizzapi.address import StoreException - - if self.dominos.closest_store is None: - raise StoreException - - order = Order( - self.dominos.closest_store, - self.dominos.customer, - self.dominos.address, - self.dominos.country) - - for code in self._product_codes: - order.add_item(code) - - return order - - def place(self): - """Place the order.""" - from pizzapi.address import StoreException - try: - order = self.order() - order.place() - except StoreException: - self._orderable = False - _LOGGER.warning( - 'Attempted to order Dominos - Order invalid or store closed') diff --git a/homeassistant/components/dominos/__init__.py b/homeassistant/components/dominos/__init__.py new file mode 100644 index 0000000000000..1c8966f3b4b5d --- /dev/null +++ b/homeassistant/components/dominos/__init__.py @@ -0,0 +1,244 @@ +"""Support for Dominos Pizza ordering.""" +from datetime import timedelta +import logging + +import voluptuous as vol + +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. +DOMAIN = 'dominos' +ENTITY_ID_FORMAT = DOMAIN + '.{}' + +ATTR_COUNTRY = 'country_code' +ATTR_FIRST_NAME = 'first_name' +ATTR_LAST_NAME = 'last_name' +ATTR_EMAIL = 'email' +ATTR_PHONE = 'phone' +ATTR_ADDRESS = 'address' +ATTR_ORDERS = 'orders' +ATTR_SHOW_MENU = 'show_menu' +ATTR_ORDER_ENTITY = 'order_entity_id' +ATTR_ORDER_NAME = 'name' +ATTR_ORDER_CODES = 'codes' + +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) +MIN_TIME_BETWEEN_STORE_UPDATES = timedelta(minutes=3330) + +DEPENDENCIES = ['http'] + +_ORDERS_SCHEMA = vol.Schema({ + vol.Required(ATTR_ORDER_NAME): cv.string, + vol.Required(ATTR_ORDER_CODES): vol.All(cv.ensure_list, [cv.string]), +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(ATTR_COUNTRY): cv.string, + vol.Required(ATTR_FIRST_NAME): cv.string, + vol.Required(ATTR_LAST_NAME): cv.string, + vol.Required(ATTR_EMAIL): cv.string, + vol.Required(ATTR_PHONE): cv.string, + vol.Required(ATTR_ADDRESS): cv.string, + vol.Optional(ATTR_SHOW_MENU): cv.boolean, + vol.Optional(ATTR_ORDERS, default=[]): vol.All( + cv.ensure_list, [_ORDERS_SCHEMA]), + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up is called when Home Assistant is loading our component.""" + dominos = Dominos(hass, config) + + component = EntityComponent(_LOGGER, DOMAIN, hass) + hass.data[DOMAIN] = {} + entities = [] + conf = config[DOMAIN] + + hass.services.register(DOMAIN, 'order', dominos.handle_order) + + if conf.get(ATTR_SHOW_MENU): + hass.http.register_view(DominosProductListView(dominos)) + + for order_info in conf.get(ATTR_ORDERS): + order = DominosOrder(order_info, dominos) + entities.append(order) + + if entities: + component.add_entities(entities) + + # Return boolean to indicate that initialization was successfully. + return True + + +class Dominos(): + """Main Dominos service.""" + + def __init__(self, hass, config): + """Set up main service.""" + conf = config[DOMAIN] + from pizzapi import Address, Customer + from pizzapi.address import StoreException + self.hass = hass + self.customer = Customer( + conf.get(ATTR_FIRST_NAME), + conf.get(ATTR_LAST_NAME), + conf.get(ATTR_EMAIL), + conf.get(ATTR_PHONE), + conf.get(ATTR_ADDRESS)) + self.address = Address( + *self.customer.address.split(','), + country=conf.get(ATTR_COUNTRY)) + self.country = conf.get(ATTR_COUNTRY) + try: + self.closest_store = self.address.closest_store() + except StoreException: + self.closest_store = None + + def handle_order(self, call): + """Handle ordering pizza.""" + entity_ids = call.data.get(ATTR_ORDER_ENTITY, None) + + target_orders = [order for order in self.hass.data[DOMAIN]['entities'] + if order.entity_id in entity_ids] + + for order in target_orders: + order.place() + + @Throttle(MIN_TIME_BETWEEN_STORE_UPDATES) + def update_closest_store(self): + """Update the shared closest store (if open).""" + from pizzapi.address import StoreException + try: + self.closest_store = self.address.closest_store() + return True + except StoreException: + self.closest_store = None + return False + + def get_menu(self): + """Return the products from the closest stores menu.""" + self.update_closest_store() + if self.closest_store is None: + _LOGGER.warning('Cannot get menu. Store may be closed') + return [] + menu = self.closest_store.get_menu() + product_entries = [] + + for product in menu.products: + item = {} + if isinstance(product.menu_data['Variants'], list): + variants = ', '.join(product.menu_data['Variants']) + else: + variants = product.menu_data['Variants'] + item['name'] = product.name + item['variants'] = variants + product_entries.append(item) + + return product_entries + + +class DominosProductListView(http.HomeAssistantView): + """View to retrieve product list content.""" + + url = '/api/dominos' + name = "api:dominos" + + def __init__(self, dominos): + """Initialize suite view.""" + self.dominos = dominos + + @callback + def get(self, request): + """Retrieve if API is running.""" + return self.json(self.dominos.get_menu()) + + +class DominosOrder(Entity): + """Represents a Dominos order entity.""" + + def __init__(self, order_info, dominos): + """Set up the entity.""" + self._name = order_info['name'] + self._product_codes = order_info['codes'] + self._orderable = False + self.dominos = dominos + + @property + def name(self): + """Return the orders name.""" + return self._name + + @property + def product_codes(self): + """Return the orders product codes.""" + return self._product_codes + + @property + def orderable(self): + """Return the true if orderable.""" + return self._orderable + + @property + def state(self): + """Return the state either closed, orderable or unorderable.""" + if self.dominos.closest_store is None: + return 'closed' + return 'orderable' if self._orderable else 'unorderable' + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Update the order state and refreshes the store.""" + from pizzapi.address import StoreException + try: + self.dominos.update_closest_store() + except StoreException: + self._orderable = False + return + + try: + order = self.order() + order.pay_with() + self._orderable = True + except StoreException: + self._orderable = False + + def order(self): + """Create the order object.""" + from pizzapi import Order + from pizzapi.address import StoreException + + if self.dominos.closest_store is None: + raise StoreException + + order = Order( + self.dominos.closest_store, + self.dominos.customer, + self.dominos.address, + self.dominos.country) + + for code in self._product_codes: + order.add_item(code) + + return order + + def place(self): + """Place the order.""" + from pizzapi.address import StoreException + try: + order = self.order() + order.place() + except StoreException: + self._orderable = False + _LOGGER.warning( + 'Attempted to order Dominos - Order invalid or store closed') diff --git a/homeassistant/components/doorbird.py b/homeassistant/components/doorbird.py deleted file mode 100644 index 28747bbe8bea3..0000000000000 --- a/homeassistant/components/doorbird.py +++ /dev/null @@ -1,418 +0,0 @@ -""" -Support for DoorBird device. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/doorbird/ -""" -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 -import homeassistant.helpers.config_validation as cv -from homeassistant.util import slugify, dt as dt_util - -REQUIREMENTS = ['doorbirdpy==2.0.6'] - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'doorbird' - -API_URL = '/api/{}'.format(DOMAIN) - -CONF_CUSTOM_URL = 'hass_url_override' -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': { - 'name': 'Button', - 'device_class': 'occupancy', - }, - 'motion': { - 'name': 'Motion', - 'device_class': 'motion', - }, - 'relay': { - 'name': 'Relay', - 'device_class': 'relay', - } -} - -RESET_DEVICE_FAVORITES = 'doorbird_reset_favorites' - -DEVICE_SCHEMA = vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_DOORBELL_NUMS, default=[1]): vol.All( - cv.ensure_list, [cv.positive_int]), - vol.Optional(CONF_RELAY_NUMS, default=[1]): vol.All( - cv.ensure_list, [cv.positive_int]), - vol.Optional(CONF_CUSTOM_URL): cv.string, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_MONITORED_CONDITIONS, default=[]): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), -}) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_TOKEN): cv.string, - vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [DEVICE_SCHEMA]) - }), -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the DoorBird component.""" - from doorbirdpy import DoorBird - - token = config[DOMAIN].get(CONF_TOKEN) - - # Provide an endpoint for the doorstations to call to trigger events - hass.http.register_view(DoorBirdRequestView(token)) - - # Provide an endpoint for the user to call to clear device changes - hass.http.register_view(DoorBirdCleanupView(token)) - - doorstations = [] - - for index, doorstation_config in enumerate(config[DOMAIN][CONF_DEVICES]): - device_ip = doorstation_config.get(CONF_HOST) - username = doorstation_config.get(CONF_USERNAME) - password = doorstation_config.get(CONF_PASSWORD) - doorbell_nums = doorstation_config.get(CONF_DOORBELL_NUMS) - relay_nums = doorstation_config.get(CONF_RELAY_NUMS) - custom_url = doorstation_config.get(CONF_CUSTOM_URL) - events = doorstation_config.get(CONF_MONITORED_CONDITIONS) - name = (doorstation_config.get(CONF_NAME) - or 'DoorBird {}'.format(index + 1)) - - device = DoorBird(device_ip, username, password) - status = device.ready() - - if status[0]: - doorstation = ConfiguredDoorBird(device, name, events, custom_url, - doorbell_nums, relay_nums, token) - doorstations.append(doorstation) - _LOGGER.info('Connected to DoorBird "%s" as %s@%s', - doorstation.name, username, device_ip) - elif status[1] == 401: - _LOGGER.error("Authorization rejected by DoorBird for %s@%s", - username, device_ip) - return False - else: - _LOGGER.error("Could not connect to DoorBird as %s@%s: Error %s", - username, device_ip, str(status[1])) - return False - - # Subscribe to doorbell or motion events - if events: - try: - doorstation.update_schedule(hass) - except HTTPError: - hass.components.persistent_notification.create( - 'Doorbird configuration failed. Please verify that API ' - 'Operator permission is enabled for the Doorbird user. ' - 'A restart will be required once permissions have been ' - 'verified.', - title='Doorbird Configuration Failure', - notification_id='doorbird_schedule_error') - - return False - - hass.data[DOMAIN] = doorstations - - def _reset_device_favorites_handler(event): - """Handle clearing favorites on device.""" - slug = event.data.get('slug') - - if slug is None: - return - - doorstation = get_doorstation_by_slug(hass, slug) - - if doorstation is None: - _LOGGER.error('Device not found %s', format(slug)) - - # Clear webhooks - favorites = doorstation.device.favorites() - - for favorite_type in favorites: - for favorite_id in favorites[favorite_type]: - doorstation.device.delete_favorite(favorite_type, favorite_id) - - hass.bus.listen(RESET_DEVICE_FAVORITES, _reset_device_favorites_handler) - - return True - - -def get_doorstation_by_slug(hass, slug): - """Get doorstation by slug.""" - for doorstation in hass.data[DOMAIN]: - if slugify(doorstation.name) in slug: - return doorstation - - -def handle_event(event): - """Handle dummy events.""" - return None - - -class ConfiguredDoorBird(): - """Attach additional information to pass along with configured device.""" - - def __init__(self, device, name, events, custom_url, doorbell_nums, - relay_nums, token): - """Initialize configured device.""" - self._name = name - self._device = device - self._custom_url = custom_url - self._monitored_events = events - self._doorbell_nums = doorbell_nums - self._relay_nums = relay_nums - self._token = token - - @property - def name(self): - """Get custom device name.""" - return self._name - - @property - def device(self): - """Get the configured device.""" - return self._device - - @property - def custom_url(self): - """Get custom url for device.""" - return self._custom_url - - def update_schedule(self, hass): - """Register monitored sensors and deregister others.""" - from doorbirdpy import DoorBirdScheduleEntrySchedule - - # Create a new schedule (24/7) - schedule = DoorBirdScheduleEntrySchedule() - schedule.add_weekday(0, 604800) # seconds in a week - - # Get the URL of this server - hass_url = hass.config.api.base_url - - # Override url if another is specified in the configuration - if self.custom_url is not None: - hass_url = self.custom_url - - # For all sensor types (enabled + disabled) - for sensor_type in SENSOR_TYPES: - name = '{} {}'.format(self.name, SENSOR_TYPES[sensor_type]['name']) - slug = slugify(name) - - url = '{}{}/{}?token={}'.format(hass_url, API_URL, slug, - self._token) - if sensor_type in self._monitored_events: - # Enabled -> register - self._register_event(url, sensor_type, schedule) - _LOGGER.info('Registered for %s pushes from DoorBird "%s". ' - 'Use the "%s_%s" event for automations.', - sensor_type, self.name, DOMAIN, slug) - - # Register a dummy listener so event is listed in GUI - hass.bus.listen('{}_{}'.format(DOMAIN, slug), handle_event) - else: - # Disabled -> deregister - self._deregister_event(url, sensor_type) - _LOGGER.info('Deregistered %s pushes from DoorBird "%s". ' - 'If any old favorites or schedules remain, ' - 'follow the instructions in the component ' - 'documentation to clear device registrations.', - sensor_type, self.name) - - def _register_event(self, hass_url, event, schedule): - """Add a schedule entry in the device for a sensor.""" - from doorbirdpy import DoorBirdScheduleEntryOutput - - # Register HA URL as webhook if not already, then get the ID - if not self.webhook_is_registered(hass_url): - self.device.change_favorite('http', 'Home Assistant ({} events)' - .format(event), hass_url) - - fav_id = self.get_webhook_id(hass_url) - - if not fav_id: - _LOGGER.warning('Could not find favorite for URL "%s". ' - 'Skipping sensor "%s".', hass_url, event) - return - - # Add event handling to device schedule - output = DoorBirdScheduleEntryOutput(event='http', - param=fav_id, - schedule=schedule) - - if event == 'doorbell': - # Repeat edit for each monitored doorbell number - for doorbell in self._doorbell_nums: - entry = self.device.get_schedule_entry(event, str(doorbell)) - entry.output.append(output) - self.device.change_schedule(entry) - elif event == 'relay': - # Repeat edit for each monitored doorbell number - for relay in self._relay_nums: - entry = self.device.get_schedule_entry(event, str(relay)) - entry.output.append(output) - else: - entry = self.device.get_schedule_entry(event) - entry.output.append(output) - self.device.change_schedule(entry) - - def _deregister_event(self, hass_url, event): - """Remove the schedule entry in the device for a sensor.""" - # Find the right favorite and delete it - fav_id = self.get_webhook_id(hass_url) - if not fav_id: - return - - self._device.delete_favorite('http', fav_id) - - if event == 'doorbell': - # Delete the matching schedule for each doorbell number - for doorbell in self._doorbell_nums: - self._delete_schedule_action(event, fav_id, str(doorbell)) - else: - self._delete_schedule_action(event, fav_id) - - def _delete_schedule_action(self, sensor, fav_id, param=""): - """Remove the HA output from a schedule.""" - entries = self._device.schedule() - for entry in entries: - if entry.input != sensor or entry.param != param: - continue - - for action in entry.output: - if action.event == 'http' and action.param == fav_id: - entry.output.remove(action) - - self._device.change_schedule(entry) - - def webhook_is_registered(self, ha_url, favs=None) -> bool: - """Return whether the given URL is registered as a device favorite.""" - favs = favs if favs else self.device.favorites() - - if 'http' not in favs: - return False - - for fav in favs['http'].values(): - if fav['value'] == ha_url: - return True - - return False - - def get_webhook_id(self, ha_url, favs=None) -> str or None: - """ - Return the device favorite ID for the given URL. - - The favorite must exist or there will be problems. - """ - favs = favs if favs else self.device.favorites() - - if 'http' not in favs: - return None - - for fav_id in favs['http']: - if favs['http'][fav_id]['value'] == ha_url: - return fav_id - - return None - - def get_event_data(self): - """Get data to pass along with HA event.""" - return { - 'timestamp': dt_util.utcnow().isoformat(), - 'live_video_url': self._device.live_video_url, - 'live_image_url': self._device.live_image_url, - 'rtsp_live_video_url': self._device.rtsp_live_video_url, - 'html5_viewer_url': self._device.html5_viewer_url - } - - -class DoorBirdRequestView(HomeAssistantView): - """Provide a page for the device to call.""" - - requires_auth = False - url = API_URL - name = API_URL[1:].replace('/', ':') - extra_urls = [API_URL + '/{sensor}'] - - def __init__(self, token): - """Initialize view.""" - HomeAssistantView.__init__(self) - self._token = token - - # pylint: disable=no-self-use - async def get(self, request, sensor): - """Respond to requests from the device.""" - from aiohttp import web - hass = request.app['hass'] - - request_token = request.query.get('token') - - authenticated = request_token == self._token - - if request_token == '' or not authenticated: - return web.Response(status=401, text='Unauthorized') - - doorstation = get_doorstation_by_slug(hass, sensor) - - if doorstation: - event_data = doorstation.get_event_data() - else: - event_data = {} - - hass.bus.async_fire('{}_{}'.format(DOMAIN, sensor), event_data) - - return web.Response(status=200, text='OK') - - -class DoorBirdCleanupView(HomeAssistantView): - """Provide a URL to call to delete ALL webhooks/schedules.""" - - requires_auth = False - url = API_URL + '/clear/{slug}' - name = 'DoorBird Cleanup' - - def __init__(self, token): - """Initialize view.""" - HomeAssistantView.__init__(self) - self._token = token - - # pylint: disable=no-self-use - async def get(self, request, slug): - """Act on requests.""" - from aiohttp import web - hass = request.app['hass'] - - request_token = request.query.get('token') - - authenticated = request_token == self._token - - if request_token == '' or not authenticated: - return web.Response(status=401, text='Unauthorized') - - device = get_doorstation_by_slug(hass, slug) - - # No matching device - if device is None: - return web.Response(status=404, - text='Device slug {} not found'.format(slug)) - - hass.bus.async_fire(RESET_DEVICE_FAVORITES, - {'slug': slug}) - - message = 'Clearing schedule for {}'.format(slug) - return web.Response(status=200, text=message) diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py new file mode 100644 index 0000000000000..d477836425d5e --- /dev/null +++ b/homeassistant/components/doorbird/__init__.py @@ -0,0 +1,413 @@ +"""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_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 dt as dt_util, slugify + +REQUIREMENTS = ['doorbirdpy==2.0.6'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'doorbird' + +API_URL = '/api/{}'.format(DOMAIN) + +CONF_CUSTOM_URL = 'hass_url_override' +CONF_DOORBELL_EVENTS = 'doorbell_events' +CONF_DOORBELL_NUMS = 'doorbell_numbers' +CONF_RELAY_NUMS = 'relay_numbers' +CONF_MOTION_EVENTS = 'motion_events' + +SENSOR_TYPES = { + 'doorbell': { + 'name': 'Button', + 'device_class': 'occupancy', + }, + 'motion': { + 'name': 'Motion', + 'device_class': 'motion', + }, + 'relay': { + 'name': 'Relay', + 'device_class': 'relay', + } +} + +RESET_DEVICE_FAVORITES = 'doorbird_reset_favorites' + +DEVICE_SCHEMA = vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_DOORBELL_NUMS, default=[1]): vol.All( + cv.ensure_list, [cv.positive_int]), + vol.Optional(CONF_RELAY_NUMS, default=[1]): vol.All( + cv.ensure_list, [cv.positive_int]), + vol.Optional(CONF_CUSTOM_URL): cv.string, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_MONITORED_CONDITIONS, default=[]): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_TOKEN): cv.string, + vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [DEVICE_SCHEMA]) + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the DoorBird component.""" + from doorbirdpy import DoorBird + + token = config[DOMAIN].get(CONF_TOKEN) + + # Provide an endpoint for the doorstations to call to trigger events + hass.http.register_view(DoorBirdRequestView(token)) + + # Provide an endpoint for the user to call to clear device changes + hass.http.register_view(DoorBirdCleanupView(token)) + + doorstations = [] + + for index, doorstation_config in enumerate(config[DOMAIN][CONF_DEVICES]): + device_ip = doorstation_config.get(CONF_HOST) + username = doorstation_config.get(CONF_USERNAME) + password = doorstation_config.get(CONF_PASSWORD) + doorbell_nums = doorstation_config.get(CONF_DOORBELL_NUMS) + relay_nums = doorstation_config.get(CONF_RELAY_NUMS) + custom_url = doorstation_config.get(CONF_CUSTOM_URL) + events = doorstation_config.get(CONF_MONITORED_CONDITIONS) + name = (doorstation_config.get(CONF_NAME) + or 'DoorBird {}'.format(index + 1)) + + device = DoorBird(device_ip, username, password) + status = device.ready() + + if status[0]: + doorstation = ConfiguredDoorBird(device, name, events, custom_url, + doorbell_nums, relay_nums, token) + doorstations.append(doorstation) + _LOGGER.info('Connected to DoorBird "%s" as %s@%s', + doorstation.name, username, device_ip) + elif status[1] == 401: + _LOGGER.error("Authorization rejected by DoorBird for %s@%s", + username, device_ip) + return False + else: + _LOGGER.error("Could not connect to DoorBird as %s@%s: Error %s", + username, device_ip, str(status[1])) + return False + + # Subscribe to doorbell or motion events + if events: + try: + doorstation.update_schedule(hass) + except HTTPError: + hass.components.persistent_notification.create( + 'Doorbird configuration failed. Please verify that API ' + 'Operator permission is enabled for the Doorbird user. ' + 'A restart will be required once permissions have been ' + 'verified.', + title='Doorbird Configuration Failure', + notification_id='doorbird_schedule_error') + + return False + + hass.data[DOMAIN] = doorstations + + def _reset_device_favorites_handler(event): + """Handle clearing favorites on device.""" + slug = event.data.get('slug') + + if slug is None: + return + + doorstation = get_doorstation_by_slug(hass, slug) + + if doorstation is None: + _LOGGER.error('Device not found %s', format(slug)) + + # Clear webhooks + favorites = doorstation.device.favorites() + + for favorite_type in favorites: + for favorite_id in favorites[favorite_type]: + doorstation.device.delete_favorite(favorite_type, favorite_id) + + hass.bus.listen(RESET_DEVICE_FAVORITES, _reset_device_favorites_handler) + + return True + + +def get_doorstation_by_slug(hass, slug): + """Get doorstation by slug.""" + for doorstation in hass.data[DOMAIN]: + if slugify(doorstation.name) in slug: + return doorstation + + +def handle_event(event): + """Handle dummy events.""" + return None + + +class ConfiguredDoorBird(): + """Attach additional information to pass along with configured device.""" + + def __init__(self, device, name, events, custom_url, doorbell_nums, + relay_nums, token): + """Initialize configured device.""" + self._name = name + self._device = device + self._custom_url = custom_url + self._monitored_events = events + self._doorbell_nums = doorbell_nums + self._relay_nums = relay_nums + self._token = token + + @property + def name(self): + """Get custom device name.""" + return self._name + + @property + def device(self): + """Get the configured device.""" + return self._device + + @property + def custom_url(self): + """Get custom url for device.""" + return self._custom_url + + def update_schedule(self, hass): + """Register monitored sensors and deregister others.""" + from doorbirdpy import DoorBirdScheduleEntrySchedule + + # Create a new schedule (24/7) + schedule = DoorBirdScheduleEntrySchedule() + schedule.add_weekday(0, 604800) # seconds in a week + + # Get the URL of this server + hass_url = hass.config.api.base_url + + # Override url if another is specified in the configuration + if self.custom_url is not None: + hass_url = self.custom_url + + # For all sensor types (enabled + disabled) + for sensor_type in SENSOR_TYPES: + name = '{} {}'.format(self.name, SENSOR_TYPES[sensor_type]['name']) + slug = slugify(name) + + url = '{}{}/{}?token={}'.format(hass_url, API_URL, slug, + self._token) + if sensor_type in self._monitored_events: + # Enabled -> register + self._register_event(url, sensor_type, schedule) + _LOGGER.info('Registered for %s pushes from DoorBird "%s". ' + 'Use the "%s_%s" event for automations.', + sensor_type, self.name, DOMAIN, slug) + + # Register a dummy listener so event is listed in GUI + hass.bus.listen('{}_{}'.format(DOMAIN, slug), handle_event) + else: + # Disabled -> deregister + self._deregister_event(url, sensor_type) + _LOGGER.info('Deregistered %s pushes from DoorBird "%s". ' + 'If any old favorites or schedules remain, ' + 'follow the instructions in the component ' + 'documentation to clear device registrations.', + sensor_type, self.name) + + def _register_event(self, hass_url, event, schedule): + """Add a schedule entry in the device for a sensor.""" + from doorbirdpy import DoorBirdScheduleEntryOutput + + # Register HA URL as webhook if not already, then get the ID + if not self.webhook_is_registered(hass_url): + self.device.change_favorite('http', 'Home Assistant ({} events)' + .format(event), hass_url) + + fav_id = self.get_webhook_id(hass_url) + + if not fav_id: + _LOGGER.warning('Could not find favorite for URL "%s". ' + 'Skipping sensor "%s".', hass_url, event) + return + + # Add event handling to device schedule + output = DoorBirdScheduleEntryOutput(event='http', + param=fav_id, + schedule=schedule) + + if event == 'doorbell': + # Repeat edit for each monitored doorbell number + for doorbell in self._doorbell_nums: + entry = self.device.get_schedule_entry(event, str(doorbell)) + entry.output.append(output) + self.device.change_schedule(entry) + elif event == 'relay': + # Repeat edit for each monitored doorbell number + for relay in self._relay_nums: + entry = self.device.get_schedule_entry(event, str(relay)) + entry.output.append(output) + else: + entry = self.device.get_schedule_entry(event) + entry.output.append(output) + self.device.change_schedule(entry) + + def _deregister_event(self, hass_url, event): + """Remove the schedule entry in the device for a sensor.""" + # Find the right favorite and delete it + fav_id = self.get_webhook_id(hass_url) + if not fav_id: + return + + self._device.delete_favorite('http', fav_id) + + if event == 'doorbell': + # Delete the matching schedule for each doorbell number + for doorbell in self._doorbell_nums: + self._delete_schedule_action(event, fav_id, str(doorbell)) + else: + self._delete_schedule_action(event, fav_id) + + def _delete_schedule_action(self, sensor, fav_id, param=""): + """Remove the HA output from a schedule.""" + entries = self._device.schedule() + for entry in entries: + if entry.input != sensor or entry.param != param: + continue + + for action in entry.output: + if action.event == 'http' and action.param == fav_id: + entry.output.remove(action) + + self._device.change_schedule(entry) + + def webhook_is_registered(self, ha_url, favs=None) -> bool: + """Return whether the given URL is registered as a device favorite.""" + favs = favs if favs else self.device.favorites() + + if 'http' not in favs: + return False + + for fav in favs['http'].values(): + if fav['value'] == ha_url: + return True + + return False + + def get_webhook_id(self, ha_url, favs=None) -> str or None: + """ + Return the device favorite ID for the given URL. + + The favorite must exist or there will be problems. + """ + favs = favs if favs else self.device.favorites() + + if 'http' not in favs: + return None + + for fav_id in favs['http']: + if favs['http'][fav_id]['value'] == ha_url: + return fav_id + + return None + + def get_event_data(self): + """Get data to pass along with HA event.""" + return { + 'timestamp': dt_util.utcnow().isoformat(), + 'live_video_url': self._device.live_video_url, + 'live_image_url': self._device.live_image_url, + 'rtsp_live_video_url': self._device.rtsp_live_video_url, + 'html5_viewer_url': self._device.html5_viewer_url + } + + +class DoorBirdRequestView(HomeAssistantView): + """Provide a page for the device to call.""" + + requires_auth = False + url = API_URL + name = API_URL[1:].replace('/', ':') + extra_urls = [API_URL + '/{sensor}'] + + def __init__(self, token): + """Initialize view.""" + HomeAssistantView.__init__(self) + self._token = token + + # pylint: disable=no-self-use + async def get(self, request, sensor): + """Respond to requests from the device.""" + from aiohttp import web + hass = request.app['hass'] + + request_token = request.query.get('token') + + authenticated = request_token == self._token + + if request_token == '' or not authenticated: + return web.Response(status=401, text='Unauthorized') + + doorstation = get_doorstation_by_slug(hass, sensor) + + if doorstation: + event_data = doorstation.get_event_data() + else: + event_data = {} + + hass.bus.async_fire('{}_{}'.format(DOMAIN, sensor), event_data) + + return web.Response(status=200, text='OK') + + +class DoorBirdCleanupView(HomeAssistantView): + """Provide a URL to call to delete ALL webhooks/schedules.""" + + requires_auth = False + url = API_URL + '/clear/{slug}' + name = 'DoorBird Cleanup' + + def __init__(self, token): + """Initialize view.""" + HomeAssistantView.__init__(self) + self._token = token + + # pylint: disable=no-self-use + async def get(self, request, slug): + """Act on requests.""" + from aiohttp import web + hass = request.app['hass'] + + request_token = request.query.get('token') + + authenticated = request_token == self._token + + if request_token == '' or not authenticated: + return web.Response(status=401, text='Unauthorized') + + device = get_doorstation_by_slug(hass, slug) + + # No matching device + if device is None: + return web.Response(status=404, + text='Device slug {} not found'.format(slug)) + + hass.bus.async_fire(RESET_DEVICE_FAVORITES, + {'slug': slug}) + + message = 'Clearing schedule for {}'.format(slug) + return web.Response(status=200, text=message) diff --git a/homeassistant/components/doorbird/camera.py b/homeassistant/components/doorbird/camera.py new file mode 100644 index 0000000000000..e201837aaf626 --- /dev/null +++ b/homeassistant/components/doorbird/camera.py @@ -0,0 +1,83 @@ +"""Support for viewing the camera feed from a DoorBird video doorbell.""" +import asyncio +import datetime +import logging + +import aiohttp +import async_timeout + +from homeassistant.components.camera import Camera +from homeassistant.components.doorbird import DOMAIN as DOORBIRD_DOMAIN +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +DEPENDENCIES = ['doorbird'] + +_CAMERA_LAST_VISITOR = "{} Last Ring" +_CAMERA_LAST_MOTION = "{} Last Motion" +_CAMERA_LIVE = "{} Live" +_LAST_VISITOR_INTERVAL = datetime.timedelta(minutes=1) +_LAST_MOTION_INTERVAL = datetime.timedelta(minutes=1) +_LIVE_INTERVAL = datetime.timedelta(seconds=1) +_LOGGER = logging.getLogger(__name__) +_TIMEOUT = 10 # seconds + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the DoorBird camera platform.""" + for doorstation in hass.data[DOORBIRD_DOMAIN]: + device = doorstation.device + async_add_entities([ + DoorBirdCamera( + device.live_image_url, + _CAMERA_LIVE.format(doorstation.name), + _LIVE_INTERVAL), + DoorBirdCamera( + device.history_image_url(1, 'doorbell'), + _CAMERA_LAST_VISITOR.format(doorstation.name), + _LAST_VISITOR_INTERVAL), + DoorBirdCamera( + device.history_image_url(1, 'motionsensor'), + _CAMERA_LAST_MOTION.format(doorstation.name), + _LAST_MOTION_INTERVAL), + ]) + + +class DoorBirdCamera(Camera): + """The camera on a DoorBird device.""" + + def __init__(self, url, name, interval=None): + """Initialize the camera on a DoorBird device.""" + self._url = url + self._name = name + self._last_image = None + self._interval = interval or datetime.timedelta + self._last_update = datetime.datetime.min + super().__init__() + + @property + def name(self): + """Get the name of the camera.""" + return self._name + + async def async_camera_image(self): + """Pull a still image from the camera.""" + now = datetime.datetime.now() + + if self._last_image and now - self._last_update < self._interval: + return self._last_image + + try: + websession = async_get_clientsession(self.hass) + with async_timeout.timeout(_TIMEOUT, loop=self.hass.loop): + response = await websession.get(self._url) + + self._last_image = await response.read() + self._last_update = now + return self._last_image + except asyncio.TimeoutError: + _LOGGER.error("Camera image timed out") + return self._last_image + except aiohttp.ClientError as error: + _LOGGER.error("Error getting camera image: %s", error) + return self._last_image 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 7a50ac815b1be..df2eed3011a87 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 00036378a7848..ea6ba2b455fa5 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 b89275b179558..eb0016ed29816 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 @@ 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 @@ def __init__(self, data, sensor): 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.py deleted file mode 100644 index 0d57740a83d0e..0000000000000 --- a/homeassistant/components/downloader.py +++ /dev/null @@ -1,161 +0,0 @@ -""" -Support for functionality to download files. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/downloader/ -""" -import logging -import os -import re -import threading - -import requests -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.util import sanitize_filename - -_LOGGER = logging.getLogger(__name__) - -ATTR_FILENAME = 'filename' -ATTR_SUBDIR = 'subdir' -ATTR_URL = 'url' -ATTR_OVERWRITE = 'overwrite' - -CONF_DOWNLOAD_DIR = 'download_dir' - -DOMAIN = 'downloader' -DOWNLOAD_FAILED_EVENT = 'download_failed' -DOWNLOAD_COMPLETED_EVENT = 'download_completed' - -SERVICE_DOWNLOAD_FILE = 'download_file' - -SERVICE_DOWNLOAD_FILE_SCHEMA = vol.Schema({ - vol.Required(ATTR_URL): cv.url, - vol.Optional(ATTR_SUBDIR): cv.string, - vol.Optional(ATTR_FILENAME): cv.string, - vol.Optional(ATTR_OVERWRITE, default=False): cv.boolean, -}) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_DOWNLOAD_DIR): cv.string, - }), -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Listen for download events to download files.""" - download_path = config[DOMAIN][CONF_DOWNLOAD_DIR] - - # If path is relative, we assume relative to HASS config dir - if not os.path.isabs(download_path): - download_path = hass.config.path(download_path) - - if not os.path.isdir(download_path): - _LOGGER.error( - "Download path %s does not exist. File Downloader not active", - download_path) - - return False - - def download_file(service): - """Start thread to download file specified in the URL.""" - def do_download(): - """Download the file.""" - try: - url = service.data[ATTR_URL] - - subdir = service.data.get(ATTR_SUBDIR) - - filename = service.data.get(ATTR_FILENAME) - - overwrite = service.data.get(ATTR_OVERWRITE) - - if subdir: - subdir = sanitize_filename(subdir) - - final_path = None - - req = requests.get(url, stream=True, timeout=10) - - if req.status_code != 200: - _LOGGER.warning( - "downloading '%s' failed, status_code=%d", - url, - req.status_code) - - else: - if filename is None and \ - 'content-disposition' in req.headers: - match = re.findall(r"filename=(\S+)", - req.headers['content-disposition']) - - if match: - filename = match[0].strip("'\" ") - - if not filename: - filename = os.path.basename(url).strip() - - if not filename: - filename = 'ha_download' - - # Remove stuff to ruin paths - filename = sanitize_filename(filename) - - # Do we want to download to subdir, create if needed - if subdir: - subdir_path = os.path.join(download_path, subdir) - - # Ensure subdir exist - if not os.path.isdir(subdir_path): - os.makedirs(subdir_path) - - final_path = os.path.join(subdir_path, filename) - - else: - final_path = os.path.join(download_path, filename) - - path, ext = os.path.splitext(final_path) - - # If file exist append a number. - # We test filename, filename_2.. - if not overwrite: - tries = 1 - final_path = path + ext - while os.path.isfile(final_path): - tries += 1 - - final_path = "{}_{}.{}".format(path, tries, ext) - - _LOGGER.debug("%s -> %s", url, final_path) - - with open(final_path, 'wb') as fil: - for chunk in req.iter_content(1024): - fil.write(chunk) - - _LOGGER.debug("Downloading of %s done", url) - hass.bus.fire( - "{}_{}".format(DOMAIN, DOWNLOAD_COMPLETED_EVENT), { - 'url': url, - 'filename': filename - }) - - except requests.exceptions.ConnectionError: - _LOGGER.exception("ConnectionError occurred for %s", url) - hass.bus.fire( - "{}_{}".format(DOMAIN, DOWNLOAD_FAILED_EVENT), { - 'url': url, - 'filename': filename - }) - - # Remove file if we started downloading but failed - if final_path and os.path.isfile(final_path): - os.remove(final_path) - - threading.Thread(target=do_download).start() - - hass.services.register(DOMAIN, SERVICE_DOWNLOAD_FILE, download_file, - schema=SERVICE_DOWNLOAD_FILE_SCHEMA) - - return True diff --git a/homeassistant/components/downloader/__init__.py b/homeassistant/components/downloader/__init__.py new file mode 100644 index 0000000000000..5af367ef92d45 --- /dev/null +++ b/homeassistant/components/downloader/__init__.py @@ -0,0 +1,156 @@ +"""Support for functionality to download files.""" +import logging +import os +import re +import threading + +import requests +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.util import sanitize_filename + +_LOGGER = logging.getLogger(__name__) + +ATTR_FILENAME = 'filename' +ATTR_SUBDIR = 'subdir' +ATTR_URL = 'url' +ATTR_OVERWRITE = 'overwrite' + +CONF_DOWNLOAD_DIR = 'download_dir' + +DOMAIN = 'downloader' +DOWNLOAD_FAILED_EVENT = 'download_failed' +DOWNLOAD_COMPLETED_EVENT = 'download_completed' + +SERVICE_DOWNLOAD_FILE = 'download_file' + +SERVICE_DOWNLOAD_FILE_SCHEMA = vol.Schema({ + vol.Required(ATTR_URL): cv.url, + vol.Optional(ATTR_SUBDIR): cv.string, + vol.Optional(ATTR_FILENAME): cv.string, + vol.Optional(ATTR_OVERWRITE, default=False): cv.boolean, +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_DOWNLOAD_DIR): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Listen for download events to download files.""" + download_path = config[DOMAIN][CONF_DOWNLOAD_DIR] + + # If path is relative, we assume relative to HASS config dir + if not os.path.isabs(download_path): + download_path = hass.config.path(download_path) + + if not os.path.isdir(download_path): + _LOGGER.error( + "Download path %s does not exist. File Downloader not active", + download_path) + + return False + + def download_file(service): + """Start thread to download file specified in the URL.""" + def do_download(): + """Download the file.""" + try: + url = service.data[ATTR_URL] + + subdir = service.data.get(ATTR_SUBDIR) + + filename = service.data.get(ATTR_FILENAME) + + overwrite = service.data.get(ATTR_OVERWRITE) + + if subdir: + subdir = sanitize_filename(subdir) + + final_path = None + + req = requests.get(url, stream=True, timeout=10) + + if req.status_code != 200: + _LOGGER.warning( + "downloading '%s' failed, status_code=%d", + url, + req.status_code) + + else: + if filename is None and \ + 'content-disposition' in req.headers: + match = re.findall(r"filename=(\S+)", + req.headers['content-disposition']) + + if match: + filename = match[0].strip("'\" ") + + if not filename: + filename = os.path.basename(url).strip() + + if not filename: + filename = 'ha_download' + + # Remove stuff to ruin paths + filename = sanitize_filename(filename) + + # Do we want to download to subdir, create if needed + if subdir: + subdir_path = os.path.join(download_path, subdir) + + # Ensure subdir exist + if not os.path.isdir(subdir_path): + os.makedirs(subdir_path) + + final_path = os.path.join(subdir_path, filename) + + else: + final_path = os.path.join(download_path, filename) + + path, ext = os.path.splitext(final_path) + + # If file exist append a number. + # We test filename, filename_2.. + if not overwrite: + tries = 1 + final_path = path + ext + while os.path.isfile(final_path): + tries += 1 + + final_path = "{}_{}.{}".format(path, tries, ext) + + _LOGGER.debug("%s -> %s", url, final_path) + + with open(final_path, 'wb') as fil: + for chunk in req.iter_content(1024): + fil.write(chunk) + + _LOGGER.debug("Downloading of %s done", url) + hass.bus.fire( + "{}_{}".format(DOMAIN, DOWNLOAD_COMPLETED_EVENT), { + 'url': url, + 'filename': filename + }) + + except requests.exceptions.ConnectionError: + _LOGGER.exception("ConnectionError occurred for %s", url) + hass.bus.fire( + "{}_{}".format(DOMAIN, DOWNLOAD_FAILED_EVENT), { + 'url': url, + 'filename': filename + }) + + # Remove file if we started downloading but failed + if final_path and os.path.isfile(final_path): + os.remove(final_path) + + threading.Thread(target=do_download).start() + + hass.services.register(DOMAIN, SERVICE_DOWNLOAD_FILE, download_file, + schema=SERVICE_DOWNLOAD_FILE_SCHEMA) + + return True diff --git a/homeassistant/components/duckdns.py b/homeassistant/components/duckdns.py deleted file mode 100644 index 3420bbed1bcc0..0000000000000 --- a/homeassistant/components/duckdns.py +++ /dev/null @@ -1,98 +0,0 @@ -""" -Integrate with DuckDNS. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/duckdns/ -""" -from datetime import timedelta -import logging - -import voluptuous as vol - -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.aiohttp_client import async_get_clientsession - -_LOGGER = logging.getLogger(__name__) - -ATTR_TXT = 'txt' - -DOMAIN = 'duckdns' - -INTERVAL = timedelta(minutes=5) - -SERVICE_SET_TXT = 'set_txt' - -UPDATE_URL = 'https://www.duckdns.org/update' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_DOMAIN): cv.string, - vol.Required(CONF_ACCESS_TOKEN): cv.string, - }) -}, extra=vol.ALLOW_EXTRA) - -SERVICE_TXT_SCHEMA = vol.Schema({ - vol.Required(ATTR_TXT): vol.Any(None, cv.string) -}) - - -async def async_setup(hass, config): - """Initialize the DuckDNS component.""" - domain = config[DOMAIN][CONF_DOMAIN] - token = config[DOMAIN][CONF_ACCESS_TOKEN] - session = async_get_clientsession(hass) - - result = await _update_duckdns(session, domain, token) - - if not result: - return False - - async def update_domain_interval(now): - """Update the DuckDNS entry.""" - await _update_duckdns(session, domain, token) - - async def update_domain_service(call): - """Update the DuckDNS entry.""" - await _update_duckdns( - session, domain, token, txt=call.data[ATTR_TXT]) - - async_track_time_interval(hass, update_domain_interval, INTERVAL) - hass.services.async_register( - DOMAIN, SERVICE_SET_TXT, update_domain_service, - schema=SERVICE_TXT_SCHEMA) - - return result - - -_SENTINEL = object() - - -async def _update_duckdns(session, domain, token, *, txt=_SENTINEL, - clear=False): - """Update DuckDNS.""" - params = { - 'domains': domain, - 'token': token, - } - - if txt is not _SENTINEL: - if txt is None: - # Pass in empty txt value to indicate it's clearing txt record - params['txt'] = '' - clear = True - else: - params['txt'] = txt - - if clear: - params['clear'] = 'true' - - resp = await session.get(UPDATE_URL, params=params) - body = await resp.text() - - if body != 'OK': - _LOGGER.warning("Updating DuckDNS domain failed: %s", domain) - return False - - return True diff --git a/homeassistant/components/duckdns/__init__.py b/homeassistant/components/duckdns/__init__.py new file mode 100644 index 0000000000000..9899a0af98ec1 --- /dev/null +++ b/homeassistant/components/duckdns/__init__.py @@ -0,0 +1,93 @@ +"""Integrate with DuckDNS.""" +from datetime import timedelta +import logging + +import voluptuous as vol + +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +_LOGGER = logging.getLogger(__name__) + +ATTR_TXT = 'txt' + +DOMAIN = 'duckdns' + +INTERVAL = timedelta(minutes=5) + +SERVICE_SET_TXT = 'set_txt' + +UPDATE_URL = 'https://www.duckdns.org/update' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_DOMAIN): cv.string, + vol.Required(CONF_ACCESS_TOKEN): cv.string, + }) +}, extra=vol.ALLOW_EXTRA) + +SERVICE_TXT_SCHEMA = vol.Schema({ + vol.Required(ATTR_TXT): vol.Any(None, cv.string) +}) + + +async def async_setup(hass, config): + """Initialize the DuckDNS component.""" + domain = config[DOMAIN][CONF_DOMAIN] + token = config[DOMAIN][CONF_ACCESS_TOKEN] + session = async_get_clientsession(hass) + + result = await _update_duckdns(session, domain, token) + + if not result: + return False + + async def update_domain_interval(now): + """Update the DuckDNS entry.""" + await _update_duckdns(session, domain, token) + + async def update_domain_service(call): + """Update the DuckDNS entry.""" + await _update_duckdns( + session, domain, token, txt=call.data[ATTR_TXT]) + + async_track_time_interval(hass, update_domain_interval, INTERVAL) + hass.services.async_register( + DOMAIN, SERVICE_SET_TXT, update_domain_service, + schema=SERVICE_TXT_SCHEMA) + + return result + + +_SENTINEL = object() + + +async def _update_duckdns(session, domain, token, *, txt=_SENTINEL, + clear=False): + """Update DuckDNS.""" + params = { + 'domains': domain, + 'token': token, + } + + if txt is not _SENTINEL: + if txt is None: + # Pass in empty txt value to indicate it's clearing txt record + params['txt'] = '' + clear = True + else: + params['txt'] = txt + + if clear: + params['clear'] = 'true' + + resp = await session.get(UPDATE_URL, params=params) + body = await resp.text() + + if body != 'OK': + _LOGGER.warning("Updating DuckDNS domain failed: %s", domain) + return False + + return True diff --git a/homeassistant/components/dweet.py b/homeassistant/components/dweet.py deleted file mode 100644 index d5f94bb2c7bde..0000000000000 --- a/homeassistant/components/dweet.py +++ /dev/null @@ -1,70 +0,0 @@ -""" -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/ -""" -import logging -from datetime import timedelta - -import voluptuous as vol - -from homeassistant.const import ( - CONF_NAME, CONF_WHITELIST, EVENT_STATE_CHANGED, STATE_UNKNOWN) -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers import state as state_helper -from homeassistant.util import Throttle - -REQUIREMENTS = ['dweepy==0.3.0'] - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'dweet' - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=1) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_WHITELIST, default=[]): - vol.All(cv.ensure_list, [cv.entity_id]), - }), -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the Dweet.io component.""" - conf = config[DOMAIN] - name = conf.get(CONF_NAME) - whitelist = conf.get(CONF_WHITELIST) - json_body = {} - - def dweet_event_listener(event): - """Listen for new messages on the bus and sends them to Dweet.io.""" - state = event.data.get('new_state') - if state is None or state.state in (STATE_UNKNOWN, '') \ - or state.entity_id not in whitelist: - return - - try: - _state = state_helper.state_as_number(state) - except ValueError: - _state = state.state - - json_body[state.attributes.get('friendly_name')] = _state - - send_data(name, json_body) - - hass.bus.listen(EVENT_STATE_CHANGED, dweet_event_listener) - - return True - - -@Throttle(MIN_TIME_BETWEEN_UPDATES) -def send_data(name, msg): - """Send the collected data to Dweet.io.""" - import dweepy - try: - dweepy.dweet_for(name, msg) - except dweepy.DweepyError: - _LOGGER.error("Error saving data to Dweet.io: %s", msg) diff --git a/homeassistant/components/dweet/__init__.py b/homeassistant/components/dweet/__init__.py new file mode 100644 index 0000000000000..f8e5b1811632e --- /dev/null +++ b/homeassistant/components/dweet/__init__.py @@ -0,0 +1,65 @@ +"""Support for sending data to Dweet.io.""" +import logging +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.const import ( + CONF_NAME, CONF_WHITELIST, EVENT_STATE_CHANGED, STATE_UNKNOWN) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import state as state_helper +from homeassistant.util import Throttle + +REQUIREMENTS = ['dweepy==0.3.0'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'dweet' + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=1) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_WHITELIST, default=[]): + vol.All(cv.ensure_list, [cv.entity_id]), + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Dweet.io component.""" + conf = config[DOMAIN] + name = conf.get(CONF_NAME) + whitelist = conf.get(CONF_WHITELIST) + json_body = {} + + def dweet_event_listener(event): + """Listen for new messages on the bus and sends them to Dweet.io.""" + state = event.data.get('new_state') + if state is None or state.state in (STATE_UNKNOWN, '') \ + or state.entity_id not in whitelist: + return + + try: + _state = state_helper.state_as_number(state) + except ValueError: + _state = state.state + + json_body[state.attributes.get('friendly_name')] = _state + + send_data(name, json_body) + + hass.bus.listen(EVENT_STATE_CHANGED, dweet_event_listener) + + return True + + +@Throttle(MIN_TIME_BETWEEN_UPDATES) +def send_data(name, msg): + """Send the collected data to Dweet.io.""" + import dweepy + try: + dweepy.dweet_for(name, msg) + except dweepy.DweepyError: + _LOGGER.error("Error saving data to Dweet.io: %s", msg) diff --git a/homeassistant/components/dweet/sensor.py b/homeassistant/components/dweet/sensor.py new file mode 100644 index 0000000000000..d1a64201e6dc4 --- /dev/null +++ b/homeassistant/components/dweet/sensor.py @@ -0,0 +1,111 @@ +"""Support for showing values from Dweet.io.""" +import json +import logging +from datetime import timedelta + +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_DEVICE) +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['dweepy==0.3.0'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'Dweet.io Sensor' + +SCAN_INTERVAL = timedelta(minutes=1) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_DEVICE): cv.string, + vol.Required(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Dweet sensor.""" + import dweepy + + name = config.get(CONF_NAME) + device = config.get(CONF_DEVICE) + value_template = config.get(CONF_VALUE_TEMPLATE) + unit = config.get(CONF_UNIT_OF_MEASUREMENT) + if value_template is not None: + value_template.hass = hass + + try: + 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 + + if value_template.render_with_possible_json_value(content) == '': + _LOGGER.error("%s was not found", value_template) + return + + dweet = DweetData(device) + + add_entities([DweetSensor(hass, dweet, name, value_template, unit)], True) + + +class DweetSensor(Entity): + """Representation of a Dweet sensor.""" + + def __init__(self, hass, dweet, name, value_template, unit_of_measurement): + """Initialize the sensor.""" + self.hass = hass + self.dweet = dweet + self._name = name + self._value_template = value_template + self._state = None + self._unit_of_measurement = unit_of_measurement + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit_of_measurement + + @property + def state(self): + """Return the state.""" + return self._state + + def update(self): + """Get the latest data from REST API.""" + self.dweet.update() + + if self.dweet.data is None: + self._state = None + else: + values = json.dumps(self.dweet.data[0]['content']) + self._state = self._value_template.render_with_possible_json_value( + values, None) + + +class DweetData: + """The class for handling the data retrieval.""" + + def __init__(self, device): + """Initialize the sensor.""" + self._device = device + self.data = None + + def update(self): + """Get the latest data from Dweet.io.""" + import dweepy + + try: + self.data = dweepy.get_latest_dweet_for(self._device) + except dweepy.DweepyError: + _LOGGER.warning("Device %s doesn't contain any data", self._device) + self.data = None diff --git a/homeassistant/components/dyson.py b/homeassistant/components/dyson.py deleted file mode 100644 index 791f990d9ad4c..0000000000000 --- a/homeassistant/components/dyson.py +++ /dev/null @@ -1,107 +0,0 @@ -"""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/ -""" - -import logging - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers import discovery -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT, \ - CONF_DEVICES - -REQUIREMENTS = ['libpurecoollink==0.4.2'] - -_LOGGER = logging.getLogger(__name__) - -CONF_LANGUAGE = "language" -CONF_RETRY = "retry" - -DEFAULT_TIMEOUT = 5 -DEFAULT_RETRY = 10 - -DOMAIN = "dyson" - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_LANGUAGE): cv.string, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - vol.Optional(CONF_RETRY, default=DEFAULT_RETRY): cv.positive_int, - vol.Optional(CONF_DEVICES, default=[]): - vol.All(cv.ensure_list, [dict]), - }) -}, extra=vol.ALLOW_EXTRA) - -DYSON_DEVICES = "dyson_devices" - - -def setup(hass, config): - """Set up the Dyson parent component.""" - _LOGGER.info("Creating new Dyson component") - - if DYSON_DEVICES not in hass.data: - hass.data[DYSON_DEVICES] = [] - - from libpurecoollink.dyson import DysonAccount - dyson_account = DysonAccount(config[DOMAIN].get(CONF_USERNAME), - config[DOMAIN].get(CONF_PASSWORD), - config[DOMAIN].get(CONF_LANGUAGE)) - - logged = dyson_account.login() - - timeout = config[DOMAIN].get(CONF_TIMEOUT) - retry = config[DOMAIN].get(CONF_RETRY) - - if not logged: - _LOGGER.error("Not connected to Dyson account. Unable to add devices") - return False - - _LOGGER.info("Connected to Dyson account") - dyson_devices = dyson_account.devices() - if CONF_DEVICES in config[DOMAIN] and config[DOMAIN].get(CONF_DEVICES): - configured_devices = config[DOMAIN].get(CONF_DEVICES) - for device in configured_devices: - dyson_device = next((d for d in dyson_devices if - d.serial == device["device_id"]), None) - if dyson_device: - try: - connected = dyson_device.connect(device["device_ip"]) - if connected: - _LOGGER.info("Connected to device %s", dyson_device) - hass.data[DYSON_DEVICES].append(dyson_device) - else: - _LOGGER.warning("Unable to connect to device %s", - dyson_device) - except OSError as ose: - _LOGGER.error("Unable to connect to device %s: %s", - str(dyson_device.network_device), str(ose)) - else: - _LOGGER.warning( - "Unable to find device %s in Dyson account", - device["device_id"]) - else: - # Not yet reliable - for device in dyson_devices: - _LOGGER.info("Trying to connect to device %s with timeout=%i " - "and retry=%i", device, timeout, retry) - connected = device.auto_connect(timeout, retry) - if connected: - _LOGGER.info("Connected to device %s", device) - hass.data[DYSON_DEVICES].append(device) - else: - _LOGGER.warning("Unable to connect to device %s", device) - - # Start fan/sensors components - if hass.data[DYSON_DEVICES]: - _LOGGER.debug("Starting sensor/fan components") - discovery.load_platform(hass, "sensor", DOMAIN, {}, config) - discovery.load_platform(hass, "fan", DOMAIN, {}, config) - discovery.load_platform(hass, "vacuum", DOMAIN, {}, config) - discovery.load_platform(hass, "climate", DOMAIN, {}, config) - - return True diff --git a/homeassistant/components/dyson/__init__.py b/homeassistant/components/dyson/__init__.py new file mode 100644 index 0000000000000..c2e56436bd8b7 --- /dev/null +++ b/homeassistant/components/dyson/__init__.py @@ -0,0 +1,101 @@ +"""Support for Dyson Pure Cool Link devices.""" +import logging + +import voluptuous as vol + +from homeassistant.const import ( + CONF_DEVICES, CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME) +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['libpurecoollink==0.4.2'] + +_LOGGER = logging.getLogger(__name__) + +CONF_LANGUAGE = 'language' +CONF_RETRY = 'retry' + +DEFAULT_TIMEOUT = 5 +DEFAULT_RETRY = 10 +DYSON_DEVICES = 'dyson_devices' + +DOMAIN = 'dyson' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_LANGUAGE): cv.string, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + vol.Optional(CONF_RETRY, default=DEFAULT_RETRY): cv.positive_int, + vol.Optional(CONF_DEVICES, default=[]): + vol.All(cv.ensure_list, [dict]), + }) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Dyson parent component.""" + _LOGGER.info("Creating new Dyson component") + + if DYSON_DEVICES not in hass.data: + hass.data[DYSON_DEVICES] = [] + + from libpurecoollink.dyson import DysonAccount + dyson_account = DysonAccount(config[DOMAIN].get(CONF_USERNAME), + config[DOMAIN].get(CONF_PASSWORD), + config[DOMAIN].get(CONF_LANGUAGE)) + + logged = dyson_account.login() + + timeout = config[DOMAIN].get(CONF_TIMEOUT) + retry = config[DOMAIN].get(CONF_RETRY) + + if not logged: + _LOGGER.error("Not connected to Dyson account. Unable to add devices") + return False + + _LOGGER.info("Connected to Dyson account") + dyson_devices = dyson_account.devices() + if CONF_DEVICES in config[DOMAIN] and config[DOMAIN].get(CONF_DEVICES): + configured_devices = config[DOMAIN].get(CONF_DEVICES) + for device in configured_devices: + dyson_device = next((d for d in dyson_devices if + d.serial == device["device_id"]), None) + if dyson_device: + try: + connected = dyson_device.connect(device["device_ip"]) + if connected: + _LOGGER.info("Connected to device %s", dyson_device) + hass.data[DYSON_DEVICES].append(dyson_device) + else: + _LOGGER.warning("Unable to connect to device %s", + dyson_device) + except OSError as ose: + _LOGGER.error("Unable to connect to device %s: %s", + str(dyson_device.network_device), str(ose)) + else: + _LOGGER.warning( + "Unable to find device %s in Dyson account", + device["device_id"]) + else: + # Not yet reliable + for device in dyson_devices: + _LOGGER.info("Trying to connect to device %s with timeout=%i " + "and retry=%i", device, timeout, retry) + connected = device.auto_connect(timeout, retry) + if connected: + _LOGGER.info("Connected to device %s", device) + hass.data[DYSON_DEVICES].append(device) + else: + _LOGGER.warning("Unable to connect to device %s", device) + + # Start fan/sensors components + if hass.data[DYSON_DEVICES]: + _LOGGER.debug("Starting sensor/fan components") + discovery.load_platform(hass, "sensor", DOMAIN, {}, config) + discovery.load_platform(hass, "fan", DOMAIN, {}, config) + discovery.load_platform(hass, "vacuum", DOMAIN, {}, config) + discovery.load_platform(hass, "climate", DOMAIN, {}, config) + + return True diff --git a/homeassistant/components/ebusd/.translations/ca.json b/homeassistant/components/ebusd/.translations/ca.json new file mode 100644 index 0000000000000..88b76539deb80 --- /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 0000000000000..00b499e2a39d2 --- /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 0000000000000..347c6e6eeb56a --- /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 0000000000000..16ab79fc58263 --- /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 0000000000000..d0b95daaafa99 --- /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 0000000000000..abe5fff8fec88 --- /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 0000000000000..dd70cfd2c6e2b --- /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 0000000000000..5a302af79e1fc --- /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 0000000000000..624744de470d3 --- /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 0000000000000..0c926a0335cd1 --- /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 0000000000000..7b013a4d7bcd1 --- /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 0000000000000..1d2851acb6b6f --- /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 0000000000000..bc1b3aa9595f9 --- /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 0000000000000..c36981c5278f5 --- /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 0000000000000..942ba107509a9 --- /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 0000000000000..0f64533f7f156 --- /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 0000000000000..ee62df8ddad5f --- /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.py deleted file mode 100644 index bd08024e64a88..0000000000000 --- a/homeassistant/components/ecoal_boiler.py +++ /dev/null @@ -1,98 +0,0 @@ -""" -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/ -""" -import logging - -import voluptuous as vol - -from homeassistant.const import (CONF_HOST, CONF_PASSWORD, CONF_USERNAME, - CONF_MONITORED_CONDITIONS, CONF_SENSORS, - CONF_SWITCHES) -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import load_platform - -REQUIREMENTS = ['ecoaliface==0.4.0'] - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "ecoal_boiler" -DATA_ECOAL_BOILER = 'data_' + DOMAIN - -DEFAULT_USERNAME = "admin" -DEFAULT_PASSWORD = "admin" - - -# Available pump ids with assigned HA names -# Available as switches -AVAILABLE_PUMPS = { - "central_heating_pump": "Central heating pump", - "central_heating_pump2": "Central heating pump2", - "domestic_hot_water_pump": "Domestic hot water pump", -} - -# Available temp sensor ids with assigned HA names -# Available as sensors -AVAILABLE_SENSORS = { - "outdoor_temp": 'Outdoor temperature', - "indoor_temp": 'Indoor temperature', - "indoor2_temp": 'Indoor temperature 2', - "domestic_hot_water_temp": 'Domestic hot water temperature', - "target_domestic_hot_water_temp": 'Target hot water temperature', - "feedwater_in_temp": 'Feedwater input temperature', - "feedwater_out_temp": 'Feedwater output temperature', - "target_feedwater_temp": 'Target feedwater temperature', - "fuel_feeder_temp": 'Fuel feeder temperature', - "exhaust_temp": 'Exhaust temperature', -} - -SWITCH_SCHEMA = vol.Schema({ - vol.Optional(CONF_MONITORED_CONDITIONS, default=list(AVAILABLE_PUMPS)): - vol.All(cv.ensure_list, [vol.In(AVAILABLE_PUMPS)]) -}) - -SENSOR_SCHEMA = vol.Schema({ - vol.Optional(CONF_MONITORED_CONDITIONS, default=list(AVAILABLE_SENSORS)): - vol.All(cv.ensure_list, [vol.In(AVAILABLE_SENSORS)]) -}) - -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_SENSORS, default={}): SENSOR_SCHEMA, - }) -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, hass_config): - """Set up global ECoalController instance same for sensors and switches.""" - from ecoaliface.simple import ECoalController - - conf = hass_config[DOMAIN] - host = conf[CONF_HOST] - username = conf[CONF_USERNAME] - passwd = conf[CONF_PASSWORD] - # Creating ECoalController instance makes HTTP request to controller. - ecoal_contr = ECoalController(host, username, passwd) - if ecoal_contr.version is None: - # Wrong credentials nor network config - _LOGGER.error("Unable to read controller status from %s@%s" - " (wrong host/credentials)", username, host, ) - return False - _LOGGER.debug("Detected controller version: %r @%s", - ecoal_contr.version, host, ) - hass.data[DATA_ECOAL_BOILER] = ecoal_contr - # Setup switches - switches = conf[CONF_SWITCHES][CONF_MONITORED_CONDITIONS] - load_platform(hass, 'switch', DOMAIN, switches, hass_config) - # Setup temp sensors - sensors = conf[CONF_SENSORS][CONF_MONITORED_CONDITIONS] - load_platform(hass, 'sensor', DOMAIN, sensors, hass_config) - return True diff --git a/homeassistant/components/ecoal_boiler/__init__.py b/homeassistant/components/ecoal_boiler/__init__.py new file mode 100644 index 0000000000000..6ab9fc3181cb9 --- /dev/null +++ b/homeassistant/components/ecoal_boiler/__init__.py @@ -0,0 +1,91 @@ +"""Support to control ecoal/esterownik.pl coal/wood boiler controller.""" +import logging + +import voluptuous as vol + +from homeassistant.const import (CONF_HOST, CONF_PASSWORD, CONF_USERNAME, + CONF_MONITORED_CONDITIONS, CONF_SENSORS, + CONF_SWITCHES) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform + +REQUIREMENTS = ['ecoaliface==0.4.0'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "ecoal_boiler" +DATA_ECOAL_BOILER = 'data_' + DOMAIN + +DEFAULT_USERNAME = "admin" +DEFAULT_PASSWORD = "admin" + + +# Available pump ids with assigned HA names +# Available as switches +AVAILABLE_PUMPS = { + "central_heating_pump": "Central heating pump", + "central_heating_pump2": "Central heating pump2", + "domestic_hot_water_pump": "Domestic hot water pump", +} + +# Available temp sensor ids with assigned HA names +# Available as sensors +AVAILABLE_SENSORS = { + "outdoor_temp": 'Outdoor temperature', + "indoor_temp": 'Indoor temperature', + "indoor2_temp": 'Indoor temperature 2', + "domestic_hot_water_temp": 'Domestic hot water temperature', + "target_domestic_hot_water_temp": 'Target hot water temperature', + "feedwater_in_temp": 'Feedwater input temperature', + "feedwater_out_temp": 'Feedwater output temperature', + "target_feedwater_temp": 'Target feedwater temperature', + "fuel_feeder_temp": 'Fuel feeder temperature', + "exhaust_temp": 'Exhaust temperature', +} + +SWITCH_SCHEMA = vol.Schema({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(AVAILABLE_PUMPS)): + vol.All(cv.ensure_list, [vol.In(AVAILABLE_PUMPS)]) +}) + +SENSOR_SCHEMA = vol.Schema({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(AVAILABLE_SENSORS)): + vol.All(cv.ensure_list, [vol.In(AVAILABLE_SENSORS)]) +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOST): cv.string, + 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) + + +def setup(hass, hass_config): + """Set up global ECoalController instance same for sensors and switches.""" + from ecoaliface.simple import ECoalController + + conf = hass_config[DOMAIN] + host = conf[CONF_HOST] + username = conf[CONF_USERNAME] + passwd = conf[CONF_PASSWORD] + # Creating ECoalController instance makes HTTP request to controller. + ecoal_contr = ECoalController(host, username, passwd) + if ecoal_contr.version is None: + # Wrong credentials nor network config + _LOGGER.error("Unable to read controller status from %s@%s" + " (wrong host/credentials)", username, host, ) + return False + _LOGGER.debug("Detected controller version: %r @%s", + ecoal_contr.version, host, ) + hass.data[DATA_ECOAL_BOILER] = ecoal_contr + # Setup switches + switches = conf[CONF_SWITCHES][CONF_MONITORED_CONDITIONS] + load_platform(hass, 'switch', DOMAIN, switches, hass_config) + # Setup temp sensors + sensors = conf[CONF_SENSORS][CONF_MONITORED_CONDITIONS] + load_platform(hass, 'sensor', DOMAIN, sensors, hass_config) + return True diff --git a/homeassistant/components/ecoal_boiler/sensor.py b/homeassistant/components/ecoal_boiler/sensor.py new file mode 100644 index 0000000000000..47ed2d6ebdf6d --- /dev/null +++ b/homeassistant/components/ecoal_boiler/sensor.py @@ -0,0 +1,58 @@ +"""Allows reading temperatures from ecoal/esterownik.pl controller.""" +import logging + +from homeassistant.components.ecoal_boiler import ( + DATA_ECOAL_BOILER, AVAILABLE_SENSORS, ) +from homeassistant.const import TEMP_CELSIUS +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['ecoal_boiler'] + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the ecoal sensors.""" + if discovery_info is None: + return + devices = [] + ecoal_contr = hass.data[DATA_ECOAL_BOILER] + for sensor_id in discovery_info: + name = AVAILABLE_SENSORS[sensor_id] + devices.append(EcoalTempSensor(ecoal_contr, name, sensor_id)) + add_entities(devices, True) + + +class EcoalTempSensor(Entity): + """Representation of a temperature sensor using ecoal status data.""" + + def __init__(self, ecoal_contr, name, status_attr): + """Initialize the sensor.""" + self._ecoal_contr = ecoal_contr + self._name = name + self._status_attr = status_attr + 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 unit_of_measurement(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + def update(self): + """Fetch new state data for the sensor. + + This is the only method that should fetch new data for Home Assistant. + """ + # Old values read 0.5 back can still be used + status = self._ecoal_contr.get_cached_status() + self._state = getattr(status, self._status_attr) diff --git a/homeassistant/components/ecoal_boiler/switch.py b/homeassistant/components/ecoal_boiler/switch.py new file mode 100644 index 0000000000000..f113125194a17 --- /dev/null +++ b/homeassistant/components/ecoal_boiler/switch.py @@ -0,0 +1,80 @@ +"""Allows to configuration ecoal (esterownik.pl) pumps as switches.""" +import logging +from typing import Optional + +from homeassistant.components.switch import SwitchDevice +from homeassistant.components.ecoal_boiler import ( + DATA_ECOAL_BOILER, AVAILABLE_PUMPS, ) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['ecoal_boiler'] + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up switches based on ecoal interface.""" + if discovery_info is None: + return + ecoal_contr = hass.data[DATA_ECOAL_BOILER] + switches = [] + for pump_id in discovery_info: + name = AVAILABLE_PUMPS[pump_id] + switches.append(EcoalSwitch(ecoal_contr, name, pump_id)) + add_entities(switches, True) + + +class EcoalSwitch(SwitchDevice): + """Representation of Ecoal switch.""" + + def __init__(self, ecoal_contr, name, state_attr): + """ + Initialize switch. + + Sets HA switch to state as read from controller. + """ + self._ecoal_contr = ecoal_contr + self._name = name + self._state_attr = state_attr + # Ecoalcotroller holds convention that same postfix is used + # to set attribute + # set_() + # as attribute name in status instance: + # status. + self._contr_set_fun = getattr(self._ecoal_contr, "set_" + state_attr) + # No value set, will be read from controller instead + self._state = None + + @property + def name(self) -> Optional[str]: + """Return the name of the switch.""" + return self._name + + def update(self): + """Fetch new state data for the sensor. + + This is the only method that should fetch new data for Home Assistant. + """ + status = self._ecoal_contr.get_cached_status() + self._state = getattr(status, self._state_attr) + + def invalidate_ecoal_cache(self): + """Invalidate ecoal interface cache. + + Forces that next read from ecaol interface to not use cache. + """ + self._ecoal_contr.status = None + + @property + def is_on(self) -> bool: + """Return true if device is on.""" + return self._state + + def turn_on(self, **kwargs) -> None: + """Turn the device on.""" + self._contr_set_fun(1) + self.invalidate_ecoal_cache() + + def turn_off(self, **kwargs) -> None: + """Turn the device off.""" + self._contr_set_fun(0) + self.invalidate_ecoal_cache() diff --git a/homeassistant/components/ecobee.py b/homeassistant/components/ecobee.py deleted file mode 100644 index 3829c2caebd46..0000000000000 --- a/homeassistant/components/ecobee.py +++ /dev/null @@ -1,122 +0,0 @@ -""" -Support for Ecobee. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/ecobee/ -""" -import logging -import os -from datetime import timedelta - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers import discovery -from homeassistant.const import CONF_API_KEY -from homeassistant.util import Throttle -from homeassistant.util.json import save_json - -REQUIREMENTS = ['python-ecobee-api==0.0.18'] - -_CONFIGURING = {} -_LOGGER = logging.getLogger(__name__) - -CONF_HOLD_TEMP = 'hold_temp' - -DOMAIN = 'ecobee' - -ECOBEE_CONFIG_FILE = 'ecobee.conf' - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=180) - -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 - }) -}, extra=vol.ALLOW_EXTRA) - - -def request_configuration(network, hass, config): - """Request configuration steps from the user.""" - configurator = hass.components.configurator - if 'ecobee' in _CONFIGURING: - configurator.notify_errors( - _CONFIGURING['ecobee'], "Failed to register, please try again.") - - return - - def ecobee_configuration_callback(callback_data): - """Handle configuration callbacks.""" - network.request_tokens() - network.update() - setup_ecobee(hass, network, config) - - _CONFIGURING['ecobee'] = configurator.request_config( - "Ecobee", ecobee_configuration_callback, - description=( - 'Please authorize this app at https://www.ecobee.com/consumer' - 'portal/index.html with pin code: ' + network.pin), - description_image="/static/images/config_ecobee_thermostat.png", - submit_caption="I have authorized the app." - ) - - -def setup_ecobee(hass, network, config): - """Set up the Ecobee thermostat.""" - # If ecobee has a PIN then it needs to be configured. - if network.pin is not None: - request_configuration(network, hass, config) - return - - if 'ecobee' in _CONFIGURING: - configurator = hass.components.configurator - configurator.request_done(_CONFIGURING.pop('ecobee')) - - hold_temp = config[DOMAIN].get(CONF_HOLD_TEMP) - - discovery.load_platform( - hass, 'climate', DOMAIN, {'hold_temp': hold_temp}, config) - discovery.load_platform(hass, 'sensor', DOMAIN, {}, config) - discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config) - discovery.load_platform(hass, 'weather', DOMAIN, {}, config) - - -class EcobeeData: - """Get the latest data and update the states.""" - - def __init__(self, config_file): - """Init the Ecobee data object.""" - from pyecobee import Ecobee - self.ecobee = Ecobee(config_file) - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest data from pyecobee.""" - self.ecobee.update() - _LOGGER.info("Ecobee data updated successfully") - - -def setup(hass, config): - """Set up the Ecobee. - - Will automatically load thermostat and sensor components to support - devices discovered on the network. - """ - global NETWORK - - if 'ecobee' in _CONFIGURING: - return - - # Create ecobee.conf if it doesn't exist - if not os.path.isfile(hass.config.path(ECOBEE_CONFIG_FILE)): - jsonconfig = {"API_KEY": config[DOMAIN].get(CONF_API_KEY)} - save_json(hass.config.path(ECOBEE_CONFIG_FILE), jsonconfig) - - NETWORK = EcobeeData(hass.config.path(ECOBEE_CONFIG_FILE)) - - setup_ecobee(hass, NETWORK.ecobee, config) - - return True diff --git a/homeassistant/components/ecobee/__init__.py b/homeassistant/components/ecobee/__init__.py new file mode 100644 index 0000000000000..167132a5f41f1 --- /dev/null +++ b/homeassistant/components/ecobee/__init__.py @@ -0,0 +1,117 @@ +"""Support for Ecobee devices.""" +import logging +import os +from datetime import timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import discovery +from homeassistant.const import CONF_API_KEY +from homeassistant.util import Throttle +from homeassistant.util.json import save_json + +REQUIREMENTS = ['python-ecobee-api==0.0.18'] + +_CONFIGURING = {} +_LOGGER = logging.getLogger(__name__) + +CONF_HOLD_TEMP = 'hold_temp' + +DOMAIN = 'ecobee' + +ECOBEE_CONFIG_FILE = 'ecobee.conf' + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=180) + +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, + }) +}, extra=vol.ALLOW_EXTRA) + + +def request_configuration(network, hass, config): + """Request configuration steps from the user.""" + configurator = hass.components.configurator + if 'ecobee' in _CONFIGURING: + configurator.notify_errors( + _CONFIGURING['ecobee'], "Failed to register, please try again.") + + return + + def ecobee_configuration_callback(callback_data): + """Handle configuration callbacks.""" + network.request_tokens() + network.update() + setup_ecobee(hass, network, config) + + _CONFIGURING['ecobee'] = configurator.request_config( + "Ecobee", ecobee_configuration_callback, + description=( + 'Please authorize this app at https://www.ecobee.com/consumer' + 'portal/index.html with pin code: ' + network.pin), + description_image="/static/images/config_ecobee_thermostat.png", + submit_caption="I have authorized the app." + ) + + +def setup_ecobee(hass, network, config): + """Set up the Ecobee thermostat.""" + # If ecobee has a PIN then it needs to be configured. + if network.pin is not None: + request_configuration(network, hass, config) + return + + if 'ecobee' in _CONFIGURING: + configurator = hass.components.configurator + configurator.request_done(_CONFIGURING.pop('ecobee')) + + hold_temp = config[DOMAIN].get(CONF_HOLD_TEMP) + + discovery.load_platform( + hass, 'climate', DOMAIN, {'hold_temp': hold_temp}, config) + discovery.load_platform(hass, 'sensor', DOMAIN, {}, config) + discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config) + discovery.load_platform(hass, 'weather', DOMAIN, {}, config) + + +class EcobeeData: + """Get the latest data and update the states.""" + + def __init__(self, config_file): + """Init the Ecobee data object.""" + from pyecobee import Ecobee + self.ecobee = Ecobee(config_file) + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the latest data from pyecobee.""" + self.ecobee.update() + _LOGGER.info("Ecobee data updated successfully") + + +def setup(hass, config): + """Set up the Ecobee. + + Will automatically load thermostat and sensor components to support + devices discovered on the network. + """ + global NETWORK + + if 'ecobee' in _CONFIGURING: + return + + # Create ecobee.conf if it doesn't exist + if not os.path.isfile(hass.config.path(ECOBEE_CONFIG_FILE)): + jsonconfig = {"API_KEY": config[DOMAIN].get(CONF_API_KEY)} + save_json(hass.config.path(ECOBEE_CONFIG_FILE), jsonconfig) + + NETWORK = EcobeeData(hass.config.path(ECOBEE_CONFIG_FILE)) + + setup_ecobee(hass, NETWORK.ecobee, config) + + return True diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py new file mode 100644 index 0000000000000..ca8e551bf5e6f --- /dev/null +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -0,0 +1,61 @@ +"""Support for Ecobee binary sensors.""" +from homeassistant.components import ecobee +from homeassistant.components.binary_sensor import BinarySensorDevice + +DEPENDENCIES = ['ecobee'] + +ECOBEE_CONFIG_FILE = 'ecobee.conf' + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Ecobee sensors.""" + if discovery_info is None: + return + data = ecobee.NETWORK + dev = list() + for index in range(len(data.ecobee.thermostats)): + for sensor in data.ecobee.get_remote_sensors(index): + for item in sensor['capability']: + if item['type'] != 'occupancy': + continue + + dev.append(EcobeeBinarySensor(sensor['name'], index)) + + add_entities(dev, True) + + +class EcobeeBinarySensor(BinarySensorDevice): + """Representation of an Ecobee sensor.""" + + def __init__(self, sensor_name, sensor_index): + """Initialize the Ecobee sensor.""" + self._name = sensor_name + ' Occupancy' + self.sensor_name = sensor_name + self.index = sensor_index + self._state = None + self._device_class = 'occupancy' + + @property + def name(self): + """Return the name of the Ecobee sensor.""" + return self._name.rstrip() + + @property + def is_on(self): + """Return the status of the sensor.""" + return self._state == 'true' + + @property + def device_class(self): + """Return the class of this sensor, from DEVICE_CLASSES.""" + return self._device_class + + def update(self): + """Get the latest state of the sensor.""" + data = ecobee.NETWORK + data.update() + for sensor in data.ecobee.get_remote_sensors(self.index): + for item in sensor['capability']: + if (item['type'] == 'occupancy' and + self.sensor_name == sensor['name']): + self._state = item['value'] diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py new file mode 100644 index 0000000000000..aa6440894e1e7 --- /dev/null +++ b/homeassistant/components/ecobee/climate.py @@ -0,0 +1,441 @@ +"""Support for Ecobee Thermostats.""" +import logging + +import voluptuous as vol + +from homeassistant.components import ecobee +from homeassistant.components.climate import ( + DOMAIN, STATE_COOL, STATE_HEAT, STATE_AUTO, STATE_IDLE, ClimateDevice, + ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE, + SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_TARGET_HUMIDITY_HIGH, + SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_FAN_MODE, + SUPPORT_TARGET_TEMPERATURE_LOW, STATE_OFF) +from homeassistant.const import ( + ATTR_ENTITY_ID, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT) +import homeassistant.helpers.config_validation as cv + +_CONFIGURING = {} +_LOGGER = logging.getLogger(__name__) + +ATTR_FAN_MIN_ON_TIME = 'fan_min_on_time' +ATTR_RESUME_ALL = 'resume_all' + +DEFAULT_RESUME_ALL = False +TEMPERATURE_HOLD = 'temp' +VACATION_HOLD = 'vacation' +AWAY_MODE = 'awayMode' + +DEPENDENCIES = ['ecobee'] + +SERVICE_SET_FAN_MIN_ON_TIME = 'ecobee_set_fan_min_on_time' +SERVICE_RESUME_PROGRAM = 'ecobee_resume_program' + +SET_FAN_MIN_ON_TIME_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_FAN_MIN_ON_TIME): vol.Coerce(int), +}) + +RESUME_PROGRAM_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_RESUME_ALL, default=DEFAULT_RESUME_ALL): cv.boolean, +}) + +SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE | + SUPPORT_HOLD_MODE | SUPPORT_OPERATION_MODE | + SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH | + SUPPORT_AUX_HEAT | SUPPORT_TARGET_TEMPERATURE_HIGH | + SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_FAN_MODE) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Ecobee Thermostat Platform.""" + if discovery_info is None: + return + data = ecobee.NETWORK + hold_temp = discovery_info['hold_temp'] + _LOGGER.info( + "Loading ecobee thermostat component with hold_temp set to %s", + hold_temp) + devices = [Thermostat(data, index, hold_temp) + for index in range(len(data.ecobee.thermostats))] + add_entities(devices) + + def fan_min_on_time_set_service(service): + """Set the minimum fan on time on the target thermostats.""" + entity_id = service.data.get(ATTR_ENTITY_ID) + fan_min_on_time = service.data[ATTR_FAN_MIN_ON_TIME] + + if entity_id: + target_thermostats = [device for device in devices + if device.entity_id in entity_id] + else: + target_thermostats = devices + + for thermostat in target_thermostats: + thermostat.set_fan_min_on_time(str(fan_min_on_time)) + + thermostat.schedule_update_ha_state(True) + + def resume_program_set_service(service): + """Resume the program on the target thermostats.""" + entity_id = service.data.get(ATTR_ENTITY_ID) + resume_all = service.data.get(ATTR_RESUME_ALL) + + if entity_id: + target_thermostats = [device for device in devices + if device.entity_id in entity_id] + else: + target_thermostats = devices + + for thermostat in target_thermostats: + thermostat.resume_program(resume_all) + + thermostat.schedule_update_ha_state(True) + + hass.services.register( + DOMAIN, SERVICE_SET_FAN_MIN_ON_TIME, fan_min_on_time_set_service, + schema=SET_FAN_MIN_ON_TIME_SCHEMA) + + hass.services.register( + DOMAIN, SERVICE_RESUME_PROGRAM, resume_program_set_service, + schema=RESUME_PROGRAM_SCHEMA) + + +class Thermostat(ClimateDevice): + """A thermostat class for Ecobee.""" + + def __init__(self, data, thermostat_index, hold_temp): + """Initialize the thermostat.""" + self.data = data + self.thermostat_index = thermostat_index + self.thermostat = self.data.ecobee.get_thermostat( + self.thermostat_index) + self._name = self.thermostat['name'] + self.hold_temp = hold_temp + self.vacation = None + self._climate_list = self.climate_list + self._operation_list = ['auto', 'auxHeatOnly', 'cool', + 'heat', 'off'] + self._fan_list = ['auto', 'on'] + self.update_without_throttle = False + + def update(self): + """Get the latest state from the thermostat.""" + if self.update_without_throttle: + self.data.update(no_throttle=True) + self.update_without_throttle = False + else: + self.data.update() + + self.thermostat = self.data.ecobee.get_thermostat( + self.thermostat_index) + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS + + @property + def name(self): + """Return the name of the Ecobee Thermostat.""" + return self.thermostat['name'] + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_FAHRENHEIT + + @property + def current_temperature(self): + """Return the current temperature.""" + return self.thermostat['runtime']['actualTemperature'] / 10.0 + + @property + def target_temperature_low(self): + """Return the lower bound temperature we try to reach.""" + if self.current_operation == STATE_AUTO: + return self.thermostat['runtime']['desiredHeat'] / 10.0 + return None + + @property + def target_temperature_high(self): + """Return the upper bound temperature we try to reach.""" + if self.current_operation == STATE_AUTO: + return self.thermostat['runtime']['desiredCool'] / 10.0 + return None + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + if self.current_operation == STATE_AUTO: + return None + if self.current_operation == STATE_HEAT: + return self.thermostat['runtime']['desiredHeat'] / 10.0 + if self.current_operation == STATE_COOL: + return self.thermostat['runtime']['desiredCool'] / 10.0 + return None + + @property + def fan(self): + """Return the current fan status.""" + if 'fan' in self.thermostat['equipmentStatus']: + return STATE_ON + return STATE_OFF + + @property + def current_fan_mode(self): + """Return the fan setting.""" + return self.thermostat['runtime']['desiredFanMode'] + + @property + def current_hold_mode(self): + """Return current hold mode.""" + mode = self._current_hold_mode + return None if mode == AWAY_MODE else mode + + @property + def fan_list(self): + """Return the available fan modes.""" + return self._fan_list + + @property + def _current_hold_mode(self): + events = self.thermostat['events'] + for event in events: + if event['running']: + if event['type'] == 'hold': + if event['holdClimateRef'] == 'away': + if int(event['endDate'][0:4]) - \ + int(event['startDate'][0:4]) <= 1: + # A temporary hold from away climate is a hold + return 'away' + # A permanent hold from away climate + return AWAY_MODE + if event['holdClimateRef'] != "": + # Any other hold based on climate + return event['holdClimateRef'] + # Any hold not based on a climate is a temp hold + return TEMPERATURE_HOLD + if event['type'].startswith('auto'): + # All auto modes are treated as holds + return event['type'][4:].lower() + if event['type'] == 'vacation': + self.vacation = event['name'] + return VACATION_HOLD + return None + + @property + def current_operation(self): + """Return current operation.""" + if self.operation_mode == 'auxHeatOnly' or \ + self.operation_mode == 'heatPump': + return STATE_HEAT + return self.operation_mode + + @property + def operation_list(self): + """Return the operation modes list.""" + return self._operation_list + + @property + def operation_mode(self): + """Return current operation ie. heat, cool, idle.""" + return self.thermostat['settings']['hvacMode'] + + @property + def mode(self): + """Return current mode, as the user-visible name.""" + cur = self.thermostat['program']['currentClimateRef'] + climates = self.thermostat['program']['climates'] + current = list(filter(lambda x: x['climateRef'] == cur, climates)) + return current[0]['name'] + + @property + def fan_min_on_time(self): + """Return current fan minimum on time.""" + return self.thermostat['settings']['fanMinOnTime'] + + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + # Move these to Thermostat Device and make them global + status = self.thermostat['equipmentStatus'] + operation = None + if status == '': + operation = STATE_IDLE + elif 'Cool' in status: + operation = STATE_COOL + elif 'auxHeat' in status: + operation = STATE_HEAT + elif 'heatPump' in status: + operation = STATE_HEAT + else: + operation = status + + return { + "actual_humidity": self.thermostat['runtime']['actualHumidity'], + "fan": self.fan, + "climate_mode": self.mode, + "operation": operation, + "climate_list": self.climate_list, + "fan_min_on_time": self.fan_min_on_time + } + + @property + def is_away_mode_on(self): + """Return true if away mode is on.""" + return self._current_hold_mode == AWAY_MODE + + @property + def is_aux_heat_on(self): + """Return true if aux heater.""" + return 'auxHeat' in self.thermostat['equipmentStatus'] + + def turn_away_mode_on(self): + """Turn away mode on by setting it on away hold indefinitely.""" + if self._current_hold_mode != AWAY_MODE: + self.data.ecobee.set_climate_hold(self.thermostat_index, 'away', + 'indefinite') + self.update_without_throttle = True + + def turn_away_mode_off(self): + """Turn away off.""" + if self._current_hold_mode == AWAY_MODE: + self.data.ecobee.resume_program(self.thermostat_index) + self.update_without_throttle = True + + def set_hold_mode(self, hold_mode): + """Set hold mode (away, home, temp, sleep, etc.).""" + hold = self.current_hold_mode + + if hold == hold_mode: + # no change, so no action required + return + if hold_mode == 'None' or hold_mode is None: + if hold == VACATION_HOLD: + self.data.ecobee.delete_vacation( + self.thermostat_index, self.vacation) + else: + self.data.ecobee.resume_program(self.thermostat_index) + else: + if hold_mode == TEMPERATURE_HOLD: + self.set_temp_hold(self.current_temperature) + else: + self.data.ecobee.set_climate_hold( + self.thermostat_index, hold_mode, self.hold_preference()) + self.update_without_throttle = True + + def set_auto_temp_hold(self, heat_temp, cool_temp): + """Set temperature hold in auto mode.""" + if cool_temp is not None: + cool_temp_setpoint = cool_temp + else: + cool_temp_setpoint = ( + self.thermostat['runtime']['desiredCool'] / 10.0) + + if heat_temp is not None: + heat_temp_setpoint = heat_temp + else: + heat_temp_setpoint = ( + self.thermostat['runtime']['desiredCool'] / 10.0) + + self.data.ecobee.set_hold_temp(self.thermostat_index, + cool_temp_setpoint, heat_temp_setpoint, + self.hold_preference()) + _LOGGER.debug("Setting ecobee hold_temp to: heat=%s, is=%s, " + "cool=%s, is=%s", heat_temp, + isinstance(heat_temp, (int, float)), cool_temp, + isinstance(cool_temp, (int, float))) + + self.update_without_throttle = True + + def set_fan_mode(self, fan_mode): + """Set the fan mode. Valid values are "on" or "auto".""" + if (fan_mode.lower() != STATE_ON) and (fan_mode.lower() != STATE_AUTO): + error = "Invalid fan_mode value: Valid values are 'on' or 'auto'" + _LOGGER.error(error) + return + + cool_temp = self.thermostat['runtime']['desiredCool'] / 10.0 + heat_temp = self.thermostat['runtime']['desiredHeat'] / 10.0 + self.data.ecobee.set_fan_mode(self.thermostat_index, fan_mode, + cool_temp, heat_temp, + self.hold_preference()) + + _LOGGER.info("Setting fan mode to: %s", fan_mode) + + def set_temp_hold(self, temp): + """Set temperature hold in modes other than auto. + + Ecobee API: It is good practice to set the heat and cool hold + temperatures to be the same, if the thermostat is in either heat, cool, + auxHeatOnly, or off mode. If the thermostat is in auto mode, an + additional rule is required. The cool hold temperature must be greater + than the heat hold temperature by at least the amount in the + heatCoolMinDelta property. + https://www.ecobee.com/home/developer/api/examples/ex5.shtml + """ + if self.current_operation == STATE_HEAT or self.current_operation == \ + STATE_COOL: + heat_temp = temp + cool_temp = temp + else: + delta = self.thermostat['settings']['heatCoolMinDelta'] / 10 + heat_temp = temp - delta + cool_temp = temp + delta + self.set_auto_temp_hold(heat_temp, cool_temp) + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW) + high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) + temp = kwargs.get(ATTR_TEMPERATURE) + + if self.current_operation == STATE_AUTO and \ + (low_temp is not None or high_temp is not None): + self.set_auto_temp_hold(low_temp, high_temp) + elif temp is not None: + self.set_temp_hold(temp) + else: + _LOGGER.error( + "Missing valid arguments for set_temperature in %s", kwargs) + + def set_humidity(self, humidity): + """Set the humidity level.""" + self.data.ecobee.set_humidity(self.thermostat_index, humidity) + + def set_operation_mode(self, operation_mode): + """Set HVAC mode (auto, auxHeatOnly, cool, heat, off).""" + self.data.ecobee.set_hvac_mode(self.thermostat_index, operation_mode) + self.update_without_throttle = True + + def set_fan_min_on_time(self, fan_min_on_time): + """Set the minimum fan on time.""" + self.data.ecobee.set_fan_min_on_time( + self.thermostat_index, fan_min_on_time) + self.update_without_throttle = True + + def resume_program(self, resume_all): + """Resume the thermostat schedule program.""" + self.data.ecobee.resume_program( + self.thermostat_index, 'true' if resume_all else 'false') + self.update_without_throttle = True + + def hold_preference(self): + """Return user preference setting for hold time.""" + # Values returned from thermostat are 'useEndTime4hour', + # 'useEndTime2hour', 'nextTransition', 'indefinite', 'askMe' + default = self.thermostat['settings']['holdAction'] + if default == 'nextTransition': + return default + # add further conditions if other hold durations should be + # supported; note that this should not include 'indefinite' + # as an indefinite away hold is interpreted as away_mode + return 'nextTransition' + + @property + def climate_list(self): + """Return the list of climates currently available.""" + climates = self.thermostat['program']['climates'] + return list(map((lambda x: x['name']), climates)) diff --git a/homeassistant/components/ecobee/notify.py b/homeassistant/components/ecobee/notify.py new file mode 100644 index 0000000000000..9824d20b85e98 --- /dev/null +++ b/homeassistant/components/ecobee/notify.py @@ -0,0 +1,37 @@ +"""Support for Ecobee Send Message service.""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components import ecobee +from homeassistant.components.notify import ( + BaseNotificationService, PLATFORM_SCHEMA) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['ecobee'] + +CONF_INDEX = 'index' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_INDEX, default=0): cv.positive_int, +}) + + +def get_service(hass, config, discovery_info=None): + """Get the Ecobee notification service.""" + index = config.get(CONF_INDEX) + return EcobeeNotificationService(index) + + +class EcobeeNotificationService(BaseNotificationService): + """Implement the notification service for the Ecobee thermostat.""" + + def __init__(self, thermostat_index): + """Initialize the service.""" + self.thermostat_index = thermostat_index + + def send_message(self, message="", **kwargs): + """Send a message to a command line.""" + ecobee.NETWORK.ecobee.send_message(self.thermostat_index, message) diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py new file mode 100644 index 0000000000000..1f9fd5cbde854 --- /dev/null +++ b/homeassistant/components/ecobee/sensor.py @@ -0,0 +1,80 @@ +"""Support for Ecobee sensors.""" +from homeassistant.components import ecobee +from homeassistant.const import ( + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, TEMP_FAHRENHEIT) +from homeassistant.helpers.entity import Entity + +DEPENDENCIES = ['ecobee'] + +ECOBEE_CONFIG_FILE = 'ecobee.conf' + +SENSOR_TYPES = { + 'temperature': ['Temperature', TEMP_FAHRENHEIT], + 'humidity': ['Humidity', '%'] +} + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Ecobee sensors.""" + if discovery_info is None: + return + data = ecobee.NETWORK + dev = list() + for index in range(len(data.ecobee.thermostats)): + for sensor in data.ecobee.get_remote_sensors(index): + for item in sensor['capability']: + if item['type'] not in ('temperature', 'humidity'): + continue + + dev.append(EcobeeSensor(sensor['name'], item['type'], index)) + + add_entities(dev, True) + + +class EcobeeSensor(Entity): + """Representation of an Ecobee sensor.""" + + def __init__(self, sensor_name, sensor_type, sensor_index): + """Initialize the sensor.""" + self._name = '{} {}'.format(sensor_name, SENSOR_TYPES[sensor_type][0]) + self.sensor_name = sensor_name + self.type = sensor_type + self.index = sensor_index + self._state = None + self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + + @property + def name(self): + """Return the name of the Ecobee sensor.""" + return self._name + + @property + def device_class(self): + """Return the device class of the sensor.""" + if self.type in (DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE): + return self.type + return None + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement this sensor expresses itself in.""" + return self._unit_of_measurement + + def update(self): + """Get the latest state of the sensor.""" + data = ecobee.NETWORK + data.update() + for sensor in data.ecobee.get_remote_sensors(self.index): + for item in sensor['capability']: + if (item['type'] == self.type and + self.sensor_name == sensor['name']): + if (self.type == 'temperature' and + item['value'] != 'unknown'): + self._state = float(item['value']) / 10 + else: + self._state = item['value'] diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py new file mode 100644 index 0000000000000..2ba5f362b7d82 --- /dev/null +++ b/homeassistant/components/ecobee/weather.py @@ -0,0 +1,163 @@ +"""Support for displaying weather info from Ecobee API.""" +from datetime import datetime + +from homeassistant.components import ecobee +from homeassistant.components.weather import ( + ATTR_FORECAST_CONDITION, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_SPEED, WeatherEntity) +from homeassistant.const import TEMP_FAHRENHEIT + +DEPENDENCIES = ['ecobee'] + +ATTR_FORECAST_TEMP_HIGH = 'temphigh' +ATTR_FORECAST_PRESSURE = 'pressure' +ATTR_FORECAST_VISIBILITY = 'visibility' +ATTR_FORECAST_HUMIDITY = 'humidity' + +MISSING_DATA = -5002 + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Ecobee weather platform.""" + if discovery_info is None: + return + dev = list() + data = ecobee.NETWORK + for index in range(len(data.ecobee.thermostats)): + thermostat = data.ecobee.get_thermostat(index) + if 'weather' in thermostat: + dev.append(EcobeeWeather(thermostat['name'], index)) + + add_entities(dev, True) + + +class EcobeeWeather(WeatherEntity): + """Representation of Ecobee weather data.""" + + def __init__(self, name, index): + """Initialize the Ecobee weather platform.""" + self._name = name + self._index = index + self.weather = None + + def get_forecast(self, index, param): + """Retrieve forecast parameter.""" + try: + forecast = self.weather['forecasts'][index] + return forecast[param] + except (ValueError, IndexError, KeyError): + raise ValueError + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def condition(self): + """Return the current condition.""" + try: + return self.get_forecast(0, 'condition') + except ValueError: + return None + + @property + def temperature(self): + """Return the temperature.""" + try: + return float(self.get_forecast(0, 'temperature')) / 10 + except ValueError: + return None + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_FAHRENHEIT + + @property + def pressure(self): + """Return the pressure.""" + try: + return int(self.get_forecast(0, 'pressure')) + except ValueError: + return None + + @property + def humidity(self): + """Return the humidity.""" + try: + return int(self.get_forecast(0, 'relativeHumidity')) + except ValueError: + return None + + @property + def visibility(self): + """Return the visibility.""" + try: + return int(self.get_forecast(0, 'visibility')) + except ValueError: + return None + + @property + def wind_speed(self): + """Return the wind speed.""" + try: + return int(self.get_forecast(0, 'windSpeed')) + except ValueError: + return None + + @property + def wind_bearing(self): + """Return the wind direction.""" + try: + return int(self.get_forecast(0, 'windBearing')) + except ValueError: + return None + + @property + def attribution(self): + """Return the attribution.""" + if self.weather: + station = self.weather.get('weatherStation', "UNKNOWN") + time = self.weather.get('timestamp', "UNKNOWN") + return "Ecobee weather provided by {} at {}".format(station, time) + return None + + @property + def forecast(self): + """Return the forecast array.""" + try: + forecasts = [] + for day in self.weather['forecasts']: + date_time = datetime.strptime(day['dateTime'], + '%Y-%m-%d %H:%M:%S').isoformat() + forecast = { + ATTR_FORECAST_TIME: date_time, + ATTR_FORECAST_CONDITION: day['condition'], + ATTR_FORECAST_TEMP: float(day['tempHigh']) / 10, + } + if day['tempHigh'] == MISSING_DATA: + break + if day['tempLow'] != MISSING_DATA: + forecast[ATTR_FORECAST_TEMP_LOW] = \ + float(day['tempLow']) / 10 + if day['pressure'] != MISSING_DATA: + forecast[ATTR_FORECAST_PRESSURE] = int(day['pressure']) + if day['windSpeed'] != MISSING_DATA: + forecast[ATTR_FORECAST_WIND_SPEED] = int(day['windSpeed']) + if day['visibility'] != MISSING_DATA: + forecast[ATTR_FORECAST_WIND_SPEED] = int(day['visibility']) + if day['relativeHumidity'] != MISSING_DATA: + forecast[ATTR_FORECAST_HUMIDITY] = \ + int(day['relativeHumidity']) + forecasts.append(forecast) + return forecasts + except (ValueError, IndexError, KeyError): + return None + + def update(self): + """Get the latest state of the sensor.""" + data = ecobee.NETWORK + data.update() + thermostat = data.ecobee.get_thermostat(self._index) + self.weather = thermostat.get('weather', None) diff --git a/homeassistant/components/ecovacs.py b/homeassistant/components/ecovacs.py deleted file mode 100644 index 8cbe95ee685b5..0000000000000 --- a/homeassistant/components/ecovacs.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Parent component for Ecovacs Deebot vacuums. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/ecovacs/ -""" - -import logging -import random -import string - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers import discovery -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, \ - EVENT_HOMEASSISTANT_STOP - -REQUIREMENTS = ['sucks==0.9.3'] - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "ecovacs" - -CONF_COUNTRY = "country" -CONF_CONTINENT = "continent" - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_COUNTRY): vol.All(vol.Lower, cv.string), - vol.Required(CONF_CONTINENT): vol.All(vol.Lower, cv.string), - }) -}, extra=vol.ALLOW_EXTRA) - -ECOVACS_DEVICES = "ecovacs_devices" - -# Generate a random device ID on each bootup -ECOVACS_API_DEVICEID = ''.join( - random.choice(string.ascii_uppercase + string.digits) for _ in range(8) -) - - -def setup(hass, config): - """Set up the Ecovacs component.""" - _LOGGER.debug("Creating new Ecovacs component") - - hass.data[ECOVACS_DEVICES] = [] - - from sucks import EcoVacsAPI, VacBot - - ecovacs_api = EcoVacsAPI(ECOVACS_API_DEVICEID, - config[DOMAIN].get(CONF_USERNAME), - EcoVacsAPI.md5(config[DOMAIN].get(CONF_PASSWORD)), - config[DOMAIN].get(CONF_COUNTRY), - config[DOMAIN].get(CONF_CONTINENT)) - - devices = ecovacs_api.devices() - _LOGGER.debug("Ecobot devices: %s", devices) - - for device in devices: - _LOGGER.info( - "Discovered Ecovacs device on account: %s with nickname %s", - device['did'], device['nick']) - vacbot = VacBot(ecovacs_api.uid, - ecovacs_api.REALM, - ecovacs_api.resource, - ecovacs_api.user_access_token, - device, - config[DOMAIN].get(CONF_CONTINENT).lower(), - monitor=True) - hass.data[ECOVACS_DEVICES].append(vacbot) - - def stop(event: object) -> None: - """Shut down open connections to Ecovacs XMPP server.""" - for device in hass.data[ECOVACS_DEVICES]: - _LOGGER.info("Shutting down connection to Ecovacs device %s", - device.vacuum['did']) - device.disconnect() - - # Listen for HA stop to disconnect. - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop) - - if hass.data[ECOVACS_DEVICES]: - _LOGGER.debug("Starting vacuum components") - discovery.load_platform(hass, "vacuum", DOMAIN, {}, config) - - return True diff --git a/homeassistant/components/ecovacs/__init__.py b/homeassistant/components/ecovacs/__init__.py new file mode 100644 index 0000000000000..124cae3ca4719 --- /dev/null +++ b/homeassistant/components/ecovacs/__init__.py @@ -0,0 +1,83 @@ +"""Support for Ecovacs Deebot vacuums.""" +import logging +import random +import string + +import voluptuous as vol + +from homeassistant.const import ( + CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP) +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['sucks==0.9.3'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "ecovacs" + +CONF_COUNTRY = "country" +CONF_CONTINENT = "continent" + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_COUNTRY): vol.All(vol.Lower, cv.string), + vol.Required(CONF_CONTINENT): vol.All(vol.Lower, cv.string), + }) +}, extra=vol.ALLOW_EXTRA) + +ECOVACS_DEVICES = "ecovacs_devices" + +# Generate a random device ID on each bootup +ECOVACS_API_DEVICEID = ''.join( + random.choice(string.ascii_uppercase + string.digits) for _ in range(8) +) + + +def setup(hass, config): + """Set up the Ecovacs component.""" + _LOGGER.debug("Creating new Ecovacs component") + + hass.data[ECOVACS_DEVICES] = [] + + from sucks import EcoVacsAPI, VacBot + + ecovacs_api = EcoVacsAPI(ECOVACS_API_DEVICEID, + config[DOMAIN].get(CONF_USERNAME), + EcoVacsAPI.md5(config[DOMAIN].get(CONF_PASSWORD)), + config[DOMAIN].get(CONF_COUNTRY), + config[DOMAIN].get(CONF_CONTINENT)) + + devices = ecovacs_api.devices() + _LOGGER.debug("Ecobot devices: %s", devices) + + for device in devices: + _LOGGER.info( + "Discovered Ecovacs device on account: %s with nickname %s", + device['did'], device['nick']) + vacbot = VacBot(ecovacs_api.uid, + ecovacs_api.REALM, + ecovacs_api.resource, + ecovacs_api.user_access_token, + device, + config[DOMAIN].get(CONF_CONTINENT).lower(), + monitor=True) + hass.data[ECOVACS_DEVICES].append(vacbot) + + def stop(event: object) -> None: + """Shut down open connections to Ecovacs XMPP server.""" + for device in hass.data[ECOVACS_DEVICES]: + _LOGGER.info("Shutting down connection to Ecovacs device %s", + device.vacuum['did']) + device.disconnect() + + # Listen for HA stop to disconnect. + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop) + + if hass.data[ECOVACS_DEVICES]: + _LOGGER.debug("Starting vacuum components") + discovery.load_platform(hass, "vacuum", DOMAIN, {}, config) + + return True diff --git a/homeassistant/components/ecovacs/vacuum.py b/homeassistant/components/ecovacs/vacuum.py new file mode 100644 index 0000000000000..9d2af73031570 --- /dev/null +++ b/homeassistant/components/ecovacs/vacuum.py @@ -0,0 +1,189 @@ +"""Support for Ecovacs Ecovacs Vaccums.""" +import logging + +from homeassistant.components.vacuum import ( + VacuumDevice, SUPPORT_BATTERY, SUPPORT_RETURN_HOME, SUPPORT_CLEAN_SPOT, + SUPPORT_STATUS, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + SUPPORT_LOCATE, SUPPORT_FAN_SPEED, SUPPORT_SEND_COMMAND, ) +from homeassistant.components.ecovacs import ( + ECOVACS_DEVICES) +from homeassistant.helpers.icon import icon_for_battery_level + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['ecovacs'] + +SUPPORT_ECOVACS = ( + SUPPORT_BATTERY | SUPPORT_RETURN_HOME | SUPPORT_CLEAN_SPOT | + SUPPORT_STOP | SUPPORT_TURN_OFF | SUPPORT_TURN_ON | SUPPORT_LOCATE | + SUPPORT_STATUS | SUPPORT_SEND_COMMAND | SUPPORT_FAN_SPEED) + +ATTR_ERROR = 'error' +ATTR_COMPONENT_PREFIX = 'component_' + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Ecovacs vacuums.""" + vacuums = [] + for device in hass.data[ECOVACS_DEVICES]: + vacuums.append(EcovacsVacuum(device)) + _LOGGER.debug("Adding Ecovacs Vacuums to Hass: %s", vacuums) + add_entities(vacuums, True) + + +class EcovacsVacuum(VacuumDevice): + """Ecovacs Vacuums such as Deebot.""" + + def __init__(self, device): + """Initialize the Ecovacs Vacuum.""" + self.device = device + self.device.connect_and_wait_until_ready() + if self.device.vacuum.get('nick', None) is not None: + self._name = '{}'.format(self.device.vacuum['nick']) + else: + # In case there is no nickname defined, use the device id + self._name = '{}'.format(self.device.vacuum['did']) + + self._fan_speed = None + self._error = None + _LOGGER.debug("Vacuum initialized: %s", self.name) + + async def async_added_to_hass(self) -> None: + """Set up the event listeners now that hass is ready.""" + self.device.statusEvents.subscribe(lambda _: + self.schedule_update_ha_state()) + self.device.batteryEvents.subscribe(lambda _: + self.schedule_update_ha_state()) + self.device.lifespanEvents.subscribe(lambda _: + self.schedule_update_ha_state()) + self.device.errorEvents.subscribe(self.on_error) + + def on_error(self, error): + """Handle an error event from the robot. + + This will not change the entity's state. If the error caused the state + to change, that will come through as a separate on_status event + """ + if error == 'no_error': + self._error = None + else: + self._error = error + + self.hass.bus.fire('ecovacs_error', { + 'entity_id': self.entity_id, + 'error': error + }) + self.schedule_update_ha_state() + + @property + def should_poll(self) -> bool: + """Return True if entity has to be polled for state.""" + return False + + @property + def unique_id(self) -> str: + """Return an unique ID.""" + return self.device.vacuum.get('did', None) + + @property + def is_on(self): + """Return true if vacuum is currently cleaning.""" + return self.device.is_cleaning + + @property + def is_charging(self): + """Return true if vacuum is currently charging.""" + return self.device.is_charging + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def supported_features(self): + """Flag vacuum cleaner robot features that are supported.""" + return SUPPORT_ECOVACS + + @property + def status(self): + """Return the status of the vacuum cleaner.""" + return self.device.vacuum_status + + def return_to_base(self, **kwargs): + """Set the vacuum cleaner to return to the dock.""" + from sucks import Charge + self.device.run(Charge()) + + @property + def battery_icon(self): + """Return the battery icon for the vacuum cleaner.""" + return icon_for_battery_level( + battery_level=self.battery_level, charging=self.is_charging) + + @property + def battery_level(self): + """Return the battery level of the vacuum cleaner.""" + if self.device.battery_status is not None: + return self.device.battery_status * 100 + + return super().battery_level + + @property + def fan_speed(self): + """Return the fan speed of the vacuum cleaner.""" + return self.device.fan_speed + + @property + def fan_speed_list(self): + """Get the list of available fan speed steps of the vacuum cleaner.""" + from sucks import FAN_SPEED_NORMAL, FAN_SPEED_HIGH + return [FAN_SPEED_NORMAL, FAN_SPEED_HIGH] + + def turn_on(self, **kwargs): + """Turn the vacuum on and start cleaning.""" + from sucks import Clean + self.device.run(Clean()) + + def turn_off(self, **kwargs): + """Turn the vacuum off stopping the cleaning and returning home.""" + self.return_to_base() + + def stop(self, **kwargs): + """Stop the vacuum cleaner.""" + from sucks import Stop + self.device.run(Stop()) + + def clean_spot(self, **kwargs): + """Perform a spot clean-up.""" + from sucks import Spot + self.device.run(Spot()) + + def locate(self, **kwargs): + """Locate the vacuum cleaner.""" + from sucks import PlaySound + self.device.run(PlaySound()) + + def set_fan_speed(self, fan_speed, **kwargs): + """Set fan speed.""" + if self.is_on: + from sucks import Clean + self.device.run(Clean( + mode=self.device.clean_status, speed=fan_speed)) + + def send_command(self, command, params=None, **kwargs): + """Send a command to a vacuum cleaner.""" + from sucks import VacBotCommand + self.device.run(VacBotCommand(command, params)) + + @property + def device_state_attributes(self): + """Return the device-specific state attributes of this vacuum.""" + data = {} + data[ATTR_ERROR] = self._error + + for key, val in self.device.components.items(): + attr_name = ATTR_COMPONENT_PREFIX + key + data[attr_name] = int(val * 100) + + return data diff --git a/homeassistant/components/edp_redy.py b/homeassistant/components/edp_redy.py deleted file mode 100644 index 1078010361336..0000000000000 --- a/homeassistant/components/edp_redy.py +++ /dev/null @@ -1,135 +0,0 @@ -""" -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 -from datetime import timedelta - -import voluptuous as vol - -from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, - EVENT_HOMEASSISTANT_START) -from homeassistant.core import callback -from homeassistant.helpers import discovery, dispatcher, aiohttp_client -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_point_in_time -from homeassistant.util import dt as dt_util - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'edp_redy' -EDP_REDY = 'edp_redy' -DATA_UPDATE_TOPIC = '{0}_data_update'.format(DOMAIN) -UPDATE_INTERVAL = 60 - -REQUIREMENTS = ['edp_redy==0.0.3'] - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string - }) -}, extra=vol.ALLOW_EXTRA) - - -async def async_setup(hass, config): - """Set up the EDP re:dy component.""" - from edp_redy import EdpRedySession - - session = EdpRedySession(config[DOMAIN][CONF_USERNAME], - config[DOMAIN][CONF_PASSWORD], - aiohttp_client.async_get_clientsession(hass), - hass.loop) - hass.data[EDP_REDY] = session - platform_loaded = False - - async def async_update_and_sched(time): - update_success = await session.async_update() - - if update_success: - nonlocal platform_loaded - # pylint: disable=used-before-assignment - if not platform_loaded: - for component in ['sensor', 'switch']: - await discovery.async_load_platform(hass, component, - DOMAIN, {}, config) - platform_loaded = True - - dispatcher.async_dispatcher_send(hass, DATA_UPDATE_TOPIC) - - # schedule next update - async_track_point_in_time(hass, async_update_and_sched, - time + timedelta(seconds=UPDATE_INTERVAL)) - - async def start_component(event): - _LOGGER.debug("Starting updates") - await async_update_and_sched(dt_util.utcnow()) - - # only start fetching data after HA boots to prevent delaying the boot - # process - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_component) - - return True - - -class EdpRedyDevice(Entity): - """Representation a base re:dy device.""" - - def __init__(self, session, device_id, name): - """Initialize the device.""" - self._session = session - self._state = None - self._is_available = True - self._device_state_attributes = {} - self._id = device_id - self._unique_id = device_id - self._name = name if name else device_id - - async def async_added_to_hass(self): - """Subscribe to the data updates topic.""" - dispatcher.async_dispatcher_connect( - self.hass, DATA_UPDATE_TOPIC, self._data_updated) - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return self._unique_id - - @property - def available(self): - """Return True if entity is available.""" - return self._is_available - - @property - def should_poll(self): - """Return the polling state. No polling needed.""" - return False - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return self._device_state_attributes - - @callback - def _data_updated(self): - """Update state, trigger updates.""" - self.async_schedule_update_ha_state(True) - - def _parse_data(self, data): - """Parse data received from the server.""" - if "OutOfOrder" in data: - try: - self._is_available = not data['OutOfOrder'] - except ValueError: - _LOGGER.error( - "Could not parse OutOfOrder for %s", self._id) - self._is_available = False diff --git a/homeassistant/components/edp_redy/__init__.py b/homeassistant/components/edp_redy/__init__.py new file mode 100644 index 0000000000000..9b8bfaa437a12 --- /dev/null +++ b/homeassistant/components/edp_redy/__init__.py @@ -0,0 +1,129 @@ +"""Support for EDP re:dy.""" +from datetime import timedelta +import logging + +import voluptuous as vol + +from homeassistant.const import ( + CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_START) +from homeassistant.core import callback +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 +from homeassistant.util import dt as dt_util + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'edp_redy' +EDP_REDY = 'edp_redy' +DATA_UPDATE_TOPIC = '{0}_data_update'.format(DOMAIN) +UPDATE_INTERVAL = 60 + +REQUIREMENTS = ['edp_redy==0.0.3'] + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string + }) +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the EDP re:dy component.""" + from edp_redy import EdpRedySession + + session = EdpRedySession(config[DOMAIN][CONF_USERNAME], + config[DOMAIN][CONF_PASSWORD], + aiohttp_client.async_get_clientsession(hass), + hass.loop) + hass.data[EDP_REDY] = session + platform_loaded = False + + async def async_update_and_sched(time): + update_success = await session.async_update() + + if update_success: + nonlocal platform_loaded + # pylint: disable=used-before-assignment + if not platform_loaded: + for component in ['sensor', 'switch']: + await discovery.async_load_platform(hass, component, + DOMAIN, {}, config) + platform_loaded = True + + dispatcher.async_dispatcher_send(hass, DATA_UPDATE_TOPIC) + + # schedule next update + async_track_point_in_time(hass, async_update_and_sched, + time + timedelta(seconds=UPDATE_INTERVAL)) + + async def start_component(event): + _LOGGER.debug("Starting updates") + await async_update_and_sched(dt_util.utcnow()) + + # only start fetching data after HA boots to prevent delaying the boot + # process + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_component) + + return True + + +class EdpRedyDevice(Entity): + """Representation a base re:dy device.""" + + def __init__(self, session, device_id, name): + """Initialize the device.""" + self._session = session + self._state = None + self._is_available = True + self._device_state_attributes = {} + self._id = device_id + self._unique_id = device_id + self._name = name if name else device_id + + async def async_added_to_hass(self): + """Subscribe to the data updates topic.""" + dispatcher.async_dispatcher_connect( + self.hass, DATA_UPDATE_TOPIC, self._data_updated) + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._unique_id + + @property + def available(self): + """Return True if entity is available.""" + return self._is_available + + @property + def should_poll(self): + """Return the polling state. No polling needed.""" + return False + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._device_state_attributes + + @callback + def _data_updated(self): + """Update state, trigger updates.""" + self.async_schedule_update_ha_state(True) + + def _parse_data(self, data): + """Parse data received from the server.""" + if "OutOfOrder" in data: + try: + self._is_available = not data['OutOfOrder'] + except ValueError: + _LOGGER.error( + "Could not parse OutOfOrder for %s", self._id) + self._is_available = False diff --git a/homeassistant/components/edp_redy/sensor.py b/homeassistant/components/edp_redy/sensor.py new file mode 100644 index 0000000000000..926a073832c5b --- /dev/null +++ b/homeassistant/components/edp_redy/sensor.py @@ -0,0 +1,115 @@ +"""Support for EDP re:dy sensors.""" +import logging + +from homeassistant.helpers.entity import Entity + +from homeassistant.components.edp_redy import EdpRedyDevice, EDP_REDY + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['edp_redy'] + +# Load power in watts (W) +ATTR_ACTIVE_POWER = 'active_power' + + +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 + + session = hass.data[EDP_REDY] + devices = [] + + # Create sensors for modules + for device_json in session.modules_dict.values(): + if 'HA_POWER_METER' not in device_json['Capabilities']: + continue + devices.append(EdpRedyModuleSensor(session, device_json)) + + # Create a sensor for global active power + devices.append(EdpRedySensor(session, ACTIVE_POWER_ID, "Power Home", + 'mdi:flash', 'W')) + + async_add_entities(devices, True) + + +class EdpRedySensor(EdpRedyDevice, Entity): + """Representation of a EDP re:dy generic sensor.""" + + def __init__(self, session, sensor_id, name, icon, unit): + """Initialize the sensor.""" + super().__init__(session, sensor_id, name) + + self._icon = icon + self._unit = unit + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return self._icon + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this sensor.""" + return self._unit + + async def async_update(self): + """Parse the data for this sensor.""" + if self._id in self._session.values_dict: + self._state = self._session.values_dict[self._id] + self._is_available = True + else: + self._is_available = False + + +class EdpRedyModuleSensor(EdpRedyDevice, Entity): + """Representation of a EDP re:dy module sensor.""" + + def __init__(self, session, device_json): + """Initialize the sensor.""" + super().__init__(session, device_json['PKID'], + "Power {0}".format(device_json['Name'])) + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return 'mdi:flash' + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this sensor.""" + return 'W' + + async def async_update(self): + """Parse the data for this sensor.""" + if self._id in self._session.modules_dict: + device_json = self._session.modules_dict[self._id] + self._parse_data(device_json) + else: + self._is_available = False + + def _parse_data(self, data): + """Parse data received from the server.""" + super()._parse_data(data) + + _LOGGER.debug("Sensor data: %s", str(data)) + + for state_var in data['StateVars']: + if state_var['Name'] == 'ActivePower': + try: + self._state = float(state_var['Value']) * 1000 + except ValueError: + _LOGGER.error("Could not parse power for %s", self._id) + self._state = 0 + self._is_available = False diff --git a/homeassistant/components/edp_redy/switch.py b/homeassistant/components/edp_redy/switch.py new file mode 100644 index 0000000000000..ad4ce8fe72867 --- /dev/null +++ b/homeassistant/components/edp_redy/switch.py @@ -0,0 +1,94 @@ +"""Support for EDP re:dy plugs/switches.""" +import logging + +from homeassistant.components.edp_redy import EdpRedyDevice, EDP_REDY +from homeassistant.components.switch import SwitchDevice + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['edp_redy'] + +# Load power in watts (W) +ATTR_ACTIVE_POWER = 'active_power' + + +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 = [] + for device_json in session.modules_dict.values(): + if 'HA_SWITCH' not in device_json['Capabilities']: + continue + devices.append(EdpRedySwitch(session, device_json)) + + async_add_entities(devices, True) + + +class EdpRedySwitch(EdpRedyDevice, SwitchDevice): + """Representation of a Edp re:dy switch (plugs, switches, etc).""" + + def __init__(self, session, device_json): + """Initialize the switch.""" + super().__init__(session, device_json['PKID'], device_json['Name']) + + self._active_power = None + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return 'mdi:power-plug' + + @property + def is_on(self): + """Return true if it is on.""" + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self._active_power is not None: + attrs = {ATTR_ACTIVE_POWER: self._active_power} + else: + attrs = {} + attrs.update(super().device_state_attributes) + return attrs + + async def async_turn_on(self, **kwargs): + """Turn the switch on.""" + if await self._async_send_state_cmd(True): + self._state = True + self.async_schedule_update_ha_state() + + async def async_turn_off(self, **kwargs): + """Turn the switch off.""" + if await self._async_send_state_cmd(False): + self._state = False + self.async_schedule_update_ha_state() + + async def _async_send_state_cmd(self, state): + state_json = {'devModuleId': self._id, 'key': 'RelayState', + 'value': state} + return await self._session.async_set_state_var(state_json) + + async def async_update(self): + """Parse the data for this switch.""" + if self._id in self._session.modules_dict: + device_json = self._session.modules_dict[self._id] + self._parse_data(device_json) + else: + self._is_available = False + + def _parse_data(self, data): + """Parse data received from the server.""" + super()._parse_data(data) + + for state_var in data['StateVars']: + if state_var['Name'] == 'RelayState': + self._state = state_var['Value'] == 'true' + elif state_var['Name'] == 'ActivePower': + try: + self._active_power = float(state_var['Value']) * 1000 + except ValueError: + _LOGGER.error("Could not parse power for %s", self._id) + self._active_power = None diff --git a/homeassistant/components/egardia.py b/homeassistant/components/egardia.py deleted file mode 100644 index 3547f4fc76e62..0000000000000 --- a/homeassistant/components/egardia.py +++ /dev/null @@ -1,123 +0,0 @@ -""" -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/ -""" -import logging - -import requests -import voluptuous as vol - -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__) - -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' -DEFAULT_PORT = 80 -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' -NOTIFICATION_ID = 'egardia_notification' -NOTIFICATION_TITLE = 'Egardia' -ATTR_DISCOVER_DEVICES = 'egardia_sensor' - -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]) -}) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_REPORT_SERVER_CODES, default={}): SERVER_CODE_SCHEMA, - vol.Optional(CONF_REPORT_SERVER_ENABLED, - default=DEFAULT_REPORT_SERVER_ENABLED): cv.boolean, - vol.Optional(CONF_REPORT_SERVER_PORT, - default=DEFAULT_REPORT_SERVER_PORT): cv.port, - }), -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the Egardia platform.""" - from pythonegardia import egardiadevice - from pythonegardia import egardiaserver - conf = config[DOMAIN] - username = conf.get(CONF_USERNAME) - password = conf.get(CONF_PASSWORD) - host = conf.get(CONF_HOST) - port = conf.get(CONF_PORT) - version = conf.get(CONF_VERSION) - rs_enabled = conf.get(CONF_REPORT_SERVER_ENABLED) - rs_port = conf.get(CONF_REPORT_SERVER_PORT) - try: - device = hass.data[EGARDIA_DEVICE] = egardiadevice.EgardiaDevice( - host, port, username, password, '', version) - except requests.exceptions.RequestException: - _LOGGER.error("An error occurred accessing your Egardia device. " - "Please check config.") - return False - except egardiadevice.UnauthorizedError: - _LOGGER.error("Unable to authorize. Wrong password or username.") - return False - # Set up the egardia server if enabled - if rs_enabled: - _LOGGER.debug("Setting up EgardiaServer") - try: - if EGARDIA_SERVER not in hass.data: - server = egardiaserver.EgardiaServer('', rs_port) - bound = server.bind() - if not bound: - raise IOError("Binding error occurred while " + - "starting EgardiaServer.") - hass.data[EGARDIA_SERVER] = server - server.start() - - def handle_stop_event(event): - """Handle HA 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.") - return False - - discovery.load_platform(hass, 'alarm_control_panel', DOMAIN, - discovered=conf, hass_config=config) - - # get the sensors from the device and add those - sensors = device.getsensors() - discovery.load_platform(hass, 'binary_sensor', DOMAIN, - {ATTR_DISCOVER_DEVICES: sensors}, config) - - return True diff --git a/homeassistant/components/egardia/__init__.py b/homeassistant/components/egardia/__init__.py new file mode 100644 index 0000000000000..fe613824c9512 --- /dev/null +++ b/homeassistant/components/egardia/__init__.py @@ -0,0 +1,122 @@ +"""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 + +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' +CONF_VERSION = 'version' + +DEFAULT_NAME = 'Egardia' +DEFAULT_PORT = 80 +DEFAULT_REPORT_SERVER_ENABLED = False +DEFAULT_REPORT_SERVER_PORT = 52010 +DEFAULT_VERSION = 'GATE-01' +DOMAIN = 'egardia' + +EGARDIA_DEVICE = 'egardiadevice' +EGARDIA_NAME = 'egardianame' +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' + +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]), +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_REPORT_SERVER_CODES, default={}): SERVER_CODE_SCHEMA, + vol.Optional(CONF_REPORT_SERVER_ENABLED, + default=DEFAULT_REPORT_SERVER_ENABLED): cv.boolean, + vol.Optional(CONF_REPORT_SERVER_PORT, + default=DEFAULT_REPORT_SERVER_PORT): cv.port, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Egardia platform.""" + from pythonegardia import egardiadevice + from pythonegardia import egardiaserver + conf = config[DOMAIN] + username = conf.get(CONF_USERNAME) + password = conf.get(CONF_PASSWORD) + host = conf.get(CONF_HOST) + port = conf.get(CONF_PORT) + version = conf.get(CONF_VERSION) + rs_enabled = conf.get(CONF_REPORT_SERVER_ENABLED) + rs_port = conf.get(CONF_REPORT_SERVER_PORT) + try: + device = hass.data[EGARDIA_DEVICE] = egardiadevice.EgardiaDevice( + host, port, username, password, '', version) + except requests.exceptions.RequestException: + _LOGGER.error("An error occurred accessing your Egardia device. " + "Please check configuration") + return False + except egardiadevice.UnauthorizedError: + _LOGGER.error("Unable to authorize. Wrong password or username") + return False + # Set up the egardia server if enabled + if rs_enabled: + _LOGGER.debug("Setting up EgardiaServer") + try: + if EGARDIA_SERVER not in hass.data: + server = egardiaserver.EgardiaServer('', rs_port) + bound = server.bind() + if not bound: + raise IOError("Binding error occurred while " + + "starting EgardiaServer.") + hass.data[EGARDIA_SERVER] = server + server.start() + + def handle_stop_event(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") + return False + + discovery.load_platform(hass, 'alarm_control_panel', DOMAIN, + discovered=conf, hass_config=config) + + # Get the sensors from the device and add those + sensors = device.getsensors() + discovery.load_platform(hass, 'binary_sensor', DOMAIN, + {ATTR_DISCOVER_DEVICES: sensors}, config) + + return True diff --git a/homeassistant/components/egardia/alarm_control_panel.py b/homeassistant/components/egardia/alarm_control_panel.py new file mode 100644 index 0000000000000..e202a46f9f15d --- /dev/null +++ b/homeassistant/components/egardia/alarm_control_panel.py @@ -0,0 +1,136 @@ +"""Interfaces with Egardia/Woonveilig alarm control panel.""" +import logging + +import requests + +import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.egardia import ( + 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__) + +STATES = { + 'ARM': STATE_ALARM_ARMED_AWAY, + 'DAY HOME': STATE_ALARM_ARMED_HOME, + 'DISARM': STATE_ALARM_DISARMED, + 'ARMHOME': STATE_ALARM_ARMED_HOME, + 'HOME': STATE_ALARM_ARMED_HOME, + 'NIGHT HOME': STATE_ALARM_ARMED_NIGHT, + 'TRIGGERED': STATE_ALARM_TRIGGERED +} + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Egardia Alarm Control Panael platform.""" + if discovery_info is None: + return + device = EgardiaAlarm( + discovery_info['name'], + hass.data[EGARDIA_DEVICE], + discovery_info[CONF_REPORT_SERVER_ENABLED], + discovery_info.get(CONF_REPORT_SERVER_CODES), + discovery_info[CONF_REPORT_SERVER_PORT]) + + add_entities([device], True) + + +class EgardiaAlarm(alarm.AlarmControlPanel): + """Representation of a Egardia alarm.""" + + def __init__(self, name, egardiasystem, + rs_enabled=False, rs_codes=None, rs_port=52010): + """Initialize the Egardia alarm.""" + self._name = name + self._egardiasystem = egardiasystem + self._status = None + self._rs_enabled = rs_enabled + self._rs_codes = rs_codes + self._rs_port = rs_port + + async def async_added_to_hass(self): + """Add Egardiaserver callback if enabled.""" + if self._rs_enabled: + _LOGGER.debug("Registering callback to Egardiaserver") + self.hass.data[EGARDIA_SERVER].register_callback( + self.handle_status_event) + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._status + + @property + def should_poll(self): + """Poll if no report server is enabled.""" + if not self._rs_enabled: + return True + return False + + def handle_status_event(self, event): + """Handle the Egardia system status event.""" + statuscode = event.get('status') + if statuscode is not None: + status = self.lookupstatusfromcode(statuscode) + self.parsestatus(status) + self.schedule_update_ha_state() + + def lookupstatusfromcode(self, statuscode): + """Look at the rs_codes and returns the status from the code.""" + status = next(( + status_group.upper() for status_group, codes + in self._rs_codes.items() for code in codes + if statuscode == code), 'UNKNOWN') + return status + + def parsestatus(self, status): + """Parse the status.""" + _LOGGER.debug("Parsing status %s", status) + # Ignore the statuscode if it is IGNORE + if status.lower().strip() != REPORT_SERVER_CODES_IGNORE: + _LOGGER.debug("Not ignoring status %s", status) + newstatus = STATES.get(status.upper()) + _LOGGER.debug("newstatus %s", newstatus) + self._status = newstatus + else: + _LOGGER.error("Ignoring status") + + def update(self): + """Update the alarm status.""" + status = self._egardiasystem.getstate() + self.parsestatus(status) + + def alarm_disarm(self, code=None): + """Send disarm command.""" + try: + self._egardiasystem.alarm_disarm() + except requests.exceptions.RequestException as err: + _LOGGER.error("Egardia device exception occurred when " + "sending disarm command: %s", err) + + def alarm_arm_home(self, code=None): + """Send arm home command.""" + try: + self._egardiasystem.alarm_arm_home() + except requests.exceptions.RequestException as err: + _LOGGER.error("Egardia device exception occurred when " + "sending arm home command: %s", err) + + def alarm_arm_away(self, code=None): + """Send arm away command.""" + try: + self._egardiasystem.alarm_arm_away() + except requests.exceptions.RequestException as err: + _LOGGER.error("Egardia device exception occurred when " + "sending arm away command: %s", err) diff --git a/homeassistant/components/egardia/binary_sensor.py b/homeassistant/components/egardia/binary_sensor.py new file mode 100644 index 0000000000000..74a048a86c0f6 --- /dev/null +++ b/homeassistant/components/egardia/binary_sensor.py @@ -0,0 +1,77 @@ +"""Interfaces with Egardia/Woonveilig alarm control panel.""" +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.egardia import ( + 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', +} + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Initialize the platform.""" + if (discovery_info is None or + discovery_info[ATTR_DISCOVER_DEVICES] is None): + return + + disc_info = discovery_info[ATTR_DISCOVER_DEVICES] + + async_add_entities( + ( + EgardiaBinarySensor( + sensor_id=disc_info[sensor]['id'], + name=disc_info[sensor]['name'], + egardia_system=hass.data[EGARDIA_DEVICE], + device_class=EGARDIA_TYPE_TO_DEVICE_CLASS.get( + disc_info[sensor]['type'], None) + ) + for sensor in disc_info + ), True) + + +class EgardiaBinarySensor(BinarySensorDevice): + """Represents a sensor based on an Egardia sensor (IR, Door Contact).""" + + def __init__(self, sensor_id, name, egardia_system, device_class): + """Initialize the sensor device.""" + self._id = sensor_id + self._name = name + self._state = None + self._device_class = device_class + self._egardia_system = egardia_system + + def update(self): + """Update the status.""" + egardia_input = self._egardia_system.getsensorstate(self._id) + self._state = STATE_ON if egardia_input else STATE_OFF + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def is_on(self): + """Whether the device is switched on.""" + return self._state == STATE_ON + + @property + def hidden(self): + """Whether the device is hidden by default.""" + # these type of sensors are probably mainly used for automations + return True + + @property + def device_class(self): + """Return the device class.""" + return self._device_class diff --git a/homeassistant/components/eight_sleep.py b/homeassistant/components/eight_sleep.py deleted file mode 100644 index 8c36830817e16..0000000000000 --- a/homeassistant/components/eight_sleep.py +++ /dev/null @@ -1,229 +0,0 @@ -""" -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/ -""" -import logging -from datetime import timedelta - -import voluptuous as vol - -from homeassistant.core import callback -from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, CONF_SENSORS, CONF_BINARY_SENSORS, - ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP) -from homeassistant.helpers import discovery -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_send, async_dispatcher_connect) -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'] - -_LOGGER = logging.getLogger(__name__) - -CONF_PARTNER = 'partner' - -DATA_EIGHT = 'eight_sleep' -DEFAULT_PARTNER = False -DOMAIN = 'eight_sleep' - -HEAT_ENTITY = 'heat' -USER_ENTITY = 'user' - -HEAT_SCAN_INTERVAL = timedelta(seconds=60) -USER_SCAN_INTERVAL = timedelta(seconds=300) - -SIGNAL_UPDATE_HEAT = 'eight_heat_update' -SIGNAL_UPDATE_USER = 'eight_user_update' - -NAME_MAP = { - 'left_current_sleep': 'Left Sleep Session', - 'left_last_sleep': 'Left Previous Sleep Session', - 'left_bed_state': 'Left Bed State', - 'left_presence': 'Left Bed Presence', - 'left_bed_temp': 'Left Bed Temperature', - 'left_sleep_stage': 'Left Sleep Stage', - 'right_current_sleep': 'Right Sleep Session', - 'right_last_sleep': 'Right Previous Sleep Session', - 'right_bed_state': 'Right Bed State', - 'right_presence': 'Right Bed Presence', - 'right_bed_temp': 'Right Bed Temperature', - 'right_sleep_stage': 'Right Sleep Stage', - 'room_temp': 'Room Temperature', -} - -SENSORS = ['current_sleep', - 'last_sleep', - 'bed_state', - 'bed_temp', - 'sleep_stage'] - -SERVICE_HEAT_SET = 'heat_set' - -ATTR_TARGET_HEAT = 'target' -ATTR_HEAT_DURATION = 'duration' - -VALID_TARGET_HEAT = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=100)) -VALID_DURATION = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=28800)) - -SERVICE_EIGHT_SCHEMA = vol.Schema({ - ATTR_ENTITY_ID: cv.entity_ids, - ATTR_TARGET_HEAT: VALID_TARGET_HEAT, - ATTR_HEAT_DURATION: VALID_DURATION, - }) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PARTNER, default=DEFAULT_PARTNER): cv.boolean, - }), -}, extra=vol.ALLOW_EXTRA) - - -async def async_setup(hass, config): - """Set up the Eight Sleep component.""" - from pyeight.eight import EightSleep - - conf = config.get(DOMAIN) - user = conf.get(CONF_USERNAME) - password = conf.get(CONF_PASSWORD) - partner = conf.get(CONF_PARTNER) - - if hass.config.time_zone is None: - _LOGGER.error('Timezone is not set in Home Assistant.') - return False - - timezone = hass.config.time_zone - - eight = EightSleep(user, password, timezone, partner, None, hass.loop) - - hass.data[DATA_EIGHT] = eight - - # Authenticate, build sensors - success = await eight.start() - if not success: - # Authentication failed, cannot continue - return False - - async def async_update_heat_data(now): - """Update heat data from eight in HEAT_SCAN_INTERVAL.""" - await eight.update_device_data() - async_dispatcher_send(hass, SIGNAL_UPDATE_HEAT) - - async_track_point_in_utc_time( - hass, async_update_heat_data, utcnow() + HEAT_SCAN_INTERVAL) - - async def async_update_user_data(now): - """Update user data from eight in USER_SCAN_INTERVAL.""" - await eight.update_user_data() - async_dispatcher_send(hass, SIGNAL_UPDATE_USER) - - async_track_point_in_utc_time( - hass, async_update_user_data, utcnow() + USER_SCAN_INTERVAL) - - await async_update_heat_data(None) - await async_update_user_data(None) - - # Load sub components - sensors = [] - binary_sensors = [] - if eight.users: - for user in eight.users: - obj = eight.users[user] - for sensor in SENSORS: - sensors.append('{}_{}'.format(obj.side, sensor)) - binary_sensors.append('{}_presence'.format(obj.side)) - sensors.append('room_temp') - else: - # No users, cannot continue - return False - - hass.async_create_task(discovery.async_load_platform( - hass, 'sensor', DOMAIN, { - CONF_SENSORS: sensors, - }, config)) - - hass.async_create_task(discovery.async_load_platform( - hass, 'binary_sensor', DOMAIN, { - CONF_BINARY_SENSORS: binary_sensors, - }, config)) - - async def async_service_handler(service): - """Handle eight sleep service calls.""" - params = service.data.copy() - - sensor = params.pop(ATTR_ENTITY_ID, None) - target = params.pop(ATTR_TARGET_HEAT, None) - duration = params.pop(ATTR_HEAT_DURATION, 0) - - for sens in sensor: - side = sens.split('_')[1] - userid = eight.fetch_userid(side) - usrobj = eight.users[userid] - await usrobj.set_heating_level(target, duration) - - async_dispatcher_send(hass, SIGNAL_UPDATE_HEAT) - - # Register services - hass.services.async_register( - DOMAIN, SERVICE_HEAT_SET, async_service_handler, - schema=SERVICE_EIGHT_SCHEMA) - - async def stop_eight(event): - """Handle stopping eight api session.""" - await eight.stop() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_eight) - - return True - - -class EightSleepUserEntity(Entity): - """The Eight Sleep device entity.""" - - def __init__(self, eight): - """Initialize the data object.""" - self._eight = eight - - async def async_added_to_hass(self): - """Register update dispatcher.""" - @callback - def async_eight_user_update(): - """Update callback.""" - self.async_schedule_update_ha_state(True) - - async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_USER, async_eight_user_update) - - @property - def should_poll(self): - """Return True if entity has to be polled for state.""" - return False - - -class EightSleepHeatEntity(Entity): - """The Eight Sleep device entity.""" - - def __init__(self, eight): - """Initialize the data object.""" - self._eight = eight - - async def async_added_to_hass(self): - """Register update dispatcher.""" - @callback - def async_eight_heat_update(): - """Update callback.""" - self.async_schedule_update_ha_state(True) - - async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_HEAT, async_eight_heat_update) - - @property - def should_poll(self): - """Return True if entity has to be polled for state.""" - return False diff --git a/homeassistant/components/eight_sleep/__init__.py b/homeassistant/components/eight_sleep/__init__.py new file mode 100644 index 0000000000000..ca6c8a5a5c607 --- /dev/null +++ b/homeassistant/components/eight_sleep/__init__.py @@ -0,0 +1,224 @@ +"""Support for Eight smart mattress covers and mattresses.""" +import logging +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.const import ( + CONF_USERNAME, CONF_PASSWORD, CONF_SENSORS, CONF_BINARY_SENSORS, + ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP) +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_send, async_dispatcher_connect) +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.1'] + +_LOGGER = logging.getLogger(__name__) + +CONF_PARTNER = 'partner' + +DATA_EIGHT = 'eight_sleep' +DEFAULT_PARTNER = False +DOMAIN = 'eight_sleep' + +HEAT_ENTITY = 'heat' +USER_ENTITY = 'user' + +HEAT_SCAN_INTERVAL = timedelta(seconds=60) +USER_SCAN_INTERVAL = timedelta(seconds=300) + +SIGNAL_UPDATE_HEAT = 'eight_heat_update' +SIGNAL_UPDATE_USER = 'eight_user_update' + +NAME_MAP = { + 'left_current_sleep': 'Left Sleep Session', + 'left_last_sleep': 'Left Previous Sleep Session', + 'left_bed_state': 'Left Bed State', + 'left_presence': 'Left Bed Presence', + 'left_bed_temp': 'Left Bed Temperature', + 'left_sleep_stage': 'Left Sleep Stage', + 'right_current_sleep': 'Right Sleep Session', + 'right_last_sleep': 'Right Previous Sleep Session', + 'right_bed_state': 'Right Bed State', + 'right_presence': 'Right Bed Presence', + 'right_bed_temp': 'Right Bed Temperature', + 'right_sleep_stage': 'Right Sleep Stage', + 'room_temp': 'Room Temperature', +} + +SENSORS = ['current_sleep', + 'last_sleep', + 'bed_state', + 'bed_temp', + 'sleep_stage'] + +SERVICE_HEAT_SET = 'heat_set' + +ATTR_TARGET_HEAT = 'target' +ATTR_HEAT_DURATION = 'duration' + +VALID_TARGET_HEAT = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=100)) +VALID_DURATION = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=28800)) + +SERVICE_EIGHT_SCHEMA = vol.Schema({ + ATTR_ENTITY_ID: cv.entity_ids, + ATTR_TARGET_HEAT: VALID_TARGET_HEAT, + ATTR_HEAT_DURATION: VALID_DURATION, + }) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_PARTNER, default=DEFAULT_PARTNER): cv.boolean, + }), +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the Eight Sleep component.""" + from pyeight.eight import EightSleep + + conf = config.get(DOMAIN) + user = conf.get(CONF_USERNAME) + password = conf.get(CONF_PASSWORD) + partner = conf.get(CONF_PARTNER) + + if hass.config.time_zone is None: + _LOGGER.error('Timezone is not set in Home Assistant.') + return False + + timezone = hass.config.time_zone + + eight = EightSleep(user, password, timezone, partner, None, hass.loop) + + hass.data[DATA_EIGHT] = eight + + # Authenticate, build sensors + success = await eight.start() + if not success: + # Authentication failed, cannot continue + return False + + async def async_update_heat_data(now): + """Update heat data from eight in HEAT_SCAN_INTERVAL.""" + await eight.update_device_data() + async_dispatcher_send(hass, SIGNAL_UPDATE_HEAT) + + async_track_point_in_utc_time( + hass, async_update_heat_data, utcnow() + HEAT_SCAN_INTERVAL) + + async def async_update_user_data(now): + """Update user data from eight in USER_SCAN_INTERVAL.""" + await eight.update_user_data() + async_dispatcher_send(hass, SIGNAL_UPDATE_USER) + + async_track_point_in_utc_time( + hass, async_update_user_data, utcnow() + USER_SCAN_INTERVAL) + + await async_update_heat_data(None) + await async_update_user_data(None) + + # Load sub components + sensors = [] + binary_sensors = [] + if eight.users: + for user in eight.users: + obj = eight.users[user] + for sensor in SENSORS: + sensors.append('{}_{}'.format(obj.side, sensor)) + binary_sensors.append('{}_presence'.format(obj.side)) + sensors.append('room_temp') + else: + # No users, cannot continue + return False + + hass.async_create_task(discovery.async_load_platform( + hass, 'sensor', DOMAIN, { + CONF_SENSORS: sensors, + }, config)) + + hass.async_create_task(discovery.async_load_platform( + hass, 'binary_sensor', DOMAIN, { + CONF_BINARY_SENSORS: binary_sensors, + }, config)) + + async def async_service_handler(service): + """Handle eight sleep service calls.""" + params = service.data.copy() + + sensor = params.pop(ATTR_ENTITY_ID, None) + target = params.pop(ATTR_TARGET_HEAT, None) + duration = params.pop(ATTR_HEAT_DURATION, 0) + + for sens in sensor: + side = sens.split('_')[1] + userid = eight.fetch_userid(side) + usrobj = eight.users[userid] + await usrobj.set_heating_level(target, duration) + + async_dispatcher_send(hass, SIGNAL_UPDATE_HEAT) + + # Register services + hass.services.async_register( + DOMAIN, SERVICE_HEAT_SET, async_service_handler, + schema=SERVICE_EIGHT_SCHEMA) + + async def stop_eight(event): + """Handle stopping eight api session.""" + await eight.stop() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_eight) + + return True + + +class EightSleepUserEntity(Entity): + """The Eight Sleep device entity.""" + + def __init__(self, eight): + """Initialize the data object.""" + self._eight = eight + + async def async_added_to_hass(self): + """Register update dispatcher.""" + @callback + def async_eight_user_update(): + """Update callback.""" + self.async_schedule_update_ha_state(True) + + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_USER, async_eight_user_update) + + @property + def should_poll(self): + """Return True if entity has to be polled for state.""" + return False + + +class EightSleepHeatEntity(Entity): + """The Eight Sleep device entity.""" + + def __init__(self, eight): + """Initialize the data object.""" + self._eight = eight + + async def async_added_to_hass(self): + """Register update dispatcher.""" + @callback + def async_eight_heat_update(): + """Update callback.""" + self.async_schedule_update_ha_state(True) + + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_HEAT, async_eight_heat_update) + + @property + def should_poll(self): + """Return True if entity has to be polled for state.""" + return False diff --git a/homeassistant/components/eight_sleep/binary_sensor.py b/homeassistant/components/eight_sleep/binary_sensor.py new file mode 100644 index 0000000000000..2a9cb19a327a3 --- /dev/null +++ b/homeassistant/components/eight_sleep/binary_sensor.py @@ -0,0 +1,62 @@ +"""Support for Eight Sleep binary sensors.""" +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.eight_sleep import ( + DATA_EIGHT, EightSleepHeatEntity, CONF_BINARY_SENSORS, NAME_MAP) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['eight_sleep'] + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the eight sleep binary sensor.""" + if discovery_info is None: + return + + name = 'Eight' + sensors = discovery_info[CONF_BINARY_SENSORS] + eight = hass.data[DATA_EIGHT] + + all_sensors = [] + + for sensor in sensors: + all_sensors.append(EightHeatSensor(name, eight, sensor)) + + async_add_entities(all_sensors, True) + + +class EightHeatSensor(EightSleepHeatEntity, BinarySensorDevice): + """Representation of a Eight Sleep heat-based sensor.""" + + def __init__(self, name, eight, sensor): + """Initialize the sensor.""" + super().__init__(eight) + + self._sensor = sensor + self._mapped_name = NAME_MAP.get(self._sensor, self._sensor) + self._name = '{} {}'.format(name, self._mapped_name) + self._state = None + + self._side = self._sensor.split('_')[0] + self._userid = self._eight.fetch_userid(self._side) + self._usrobj = self._eight.users[self._userid] + + _LOGGER.debug("Presence Sensor: %s, Side: %s, User: %s", + self._sensor, self._side, self._userid) + + @property + def name(self): + """Return the name of the sensor, if any.""" + return self._name + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self._state + + async def async_update(self): + """Retrieve latest state.""" + self._state = self._usrobj.bed_presence diff --git a/homeassistant/components/eight_sleep/sensor.py b/homeassistant/components/eight_sleep/sensor.py new file mode 100644 index 0000000000000..2bb03c8d4f209 --- /dev/null +++ b/homeassistant/components/eight_sleep/sensor.py @@ -0,0 +1,289 @@ +"""Support for Eight Sleep sensors.""" +import logging + +from homeassistant.components.eight_sleep import ( + DATA_EIGHT, EightSleepHeatEntity, EightSleepUserEntity, + CONF_SENSORS, NAME_MAP) + +DEPENDENCIES = ['eight_sleep'] + +ATTR_ROOM_TEMP = 'Room Temperature' +ATTR_AVG_ROOM_TEMP = 'Average Room Temperature' +ATTR_BED_TEMP = 'Bed Temperature' +ATTR_AVG_BED_TEMP = 'Average Bed Temperature' +ATTR_RESP_RATE = 'Respiratory Rate' +ATTR_AVG_RESP_RATE = 'Average Respiratory Rate' +ATTR_HEART_RATE = 'Heart Rate' +ATTR_AVG_HEART_RATE = 'Average Heart Rate' +ATTR_SLEEP_DUR = 'Time Slept' +ATTR_LIGHT_PERC = 'Light Sleep %' +ATTR_DEEP_PERC = 'Deep Sleep %' +ATTR_REM_PERC = 'REM Sleep %' +ATTR_TNT = 'Tosses & Turns' +ATTR_SLEEP_STAGE = 'Sleep Stage' +ATTR_TARGET_HEAT = 'Target Heating Level' +ATTR_ACTIVE_HEAT = 'Heating Active' +ATTR_DURATION_HEAT = 'Heating Time Remaining' +ATTR_PROCESSING = 'Processing' +ATTR_SESSION_START = 'Session Start' + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the eight sleep sensors.""" + if discovery_info is None: + return + + name = 'Eight' + sensors = discovery_info[CONF_SENSORS] + eight = hass.data[DATA_EIGHT] + + if hass.config.units.is_metric: + units = 'si' + else: + units = 'us' + + all_sensors = [] + + for sensor in sensors: + if 'bed_state' in sensor: + all_sensors.append(EightHeatSensor(name, eight, sensor)) + elif 'room_temp' in sensor: + all_sensors.append(EightRoomSensor(name, eight, sensor, units)) + else: + all_sensors.append(EightUserSensor(name, eight, sensor, units)) + + async_add_entities(all_sensors, True) + + +class EightHeatSensor(EightSleepHeatEntity): + """Representation of an eight sleep heat-based sensor.""" + + def __init__(self, name, eight, sensor): + """Initialize the sensor.""" + super().__init__(eight) + + self._sensor = sensor + self._mapped_name = NAME_MAP.get(self._sensor, self._sensor) + self._name = '{} {}'.format(name, self._mapped_name) + self._state = None + + self._side = self._sensor.split('_')[0] + self._userid = self._eight.fetch_userid(self._side) + self._usrobj = self._eight.users[self._userid] + + _LOGGER.debug("Heat Sensor: %s, Side: %s, User: %s", + self._sensor, self._side, self._userid) + + @property + def name(self): + """Return the name of the sensor, if any.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return '%' + + async def async_update(self): + """Retrieve latest state.""" + _LOGGER.debug("Updating Heat sensor: %s", self._sensor) + self._state = self._usrobj.heating_level + + @property + def device_state_attributes(self): + """Return device state attributes.""" + state_attr = {ATTR_TARGET_HEAT: self._usrobj.target_heating_level} + state_attr[ATTR_ACTIVE_HEAT] = self._usrobj.now_heating + state_attr[ATTR_DURATION_HEAT] = self._usrobj.heating_remaining + + return state_attr + + +class EightUserSensor(EightSleepUserEntity): + """Representation of an eight sleep user-based sensor.""" + + def __init__(self, name, eight, sensor, units): + """Initialize the sensor.""" + super().__init__(eight) + + self._sensor = sensor + self._sensor_root = self._sensor.split('_', 1)[1] + self._mapped_name = NAME_MAP.get(self._sensor, self._sensor) + self._name = '{} {}'.format(name, self._mapped_name) + self._state = None + self._attr = None + self._units = units + + self._side = self._sensor.split('_', 1)[0] + self._userid = self._eight.fetch_userid(self._side) + self._usrobj = self._eight.users[self._userid] + + _LOGGER.debug("User Sensor: %s, Side: %s, User: %s", + self._sensor, self._side, self._userid) + + @property + def name(self): + """Return the name of the sensor, if any.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + if 'current_sleep' in self._sensor or 'last_sleep' in self._sensor: + return 'Score' + if 'bed_temp' in self._sensor: + if self._units == 'si': + return '°C' + return '°F' + return None + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + if 'bed_temp' in self._sensor: + return 'mdi:thermometer' + + async def async_update(self): + """Retrieve latest state.""" + _LOGGER.debug("Updating User sensor: %s", self._sensor) + if 'current' in self._sensor: + self._state = self._usrobj.current_sleep_score + self._attr = self._usrobj.current_values + elif 'last' in self._sensor: + self._state = self._usrobj.last_sleep_score + self._attr = self._usrobj.last_values + elif 'bed_temp' in self._sensor: + temp = self._usrobj.current_values['bed_temp'] + try: + if self._units == 'si': + self._state = round(temp, 2) + else: + self._state = round((temp*1.8)+32, 2) + except TypeError: + self._state = None + elif 'sleep_stage' in self._sensor: + self._state = self._usrobj.current_values['stage'] + + @property + def device_state_attributes(self): + """Return device state attributes.""" + if self._attr is None: + # Skip attributes if sensor type doesn't support + return None + + state_attr = {ATTR_SESSION_START: self._attr['date']} + state_attr[ATTR_TNT] = self._attr['tnt'] + state_attr[ATTR_PROCESSING] = self._attr['processing'] + + sleep_time = sum(self._attr['breakdown'].values()) - \ + self._attr['breakdown']['awake'] + state_attr[ATTR_SLEEP_DUR] = sleep_time + try: + state_attr[ATTR_LIGHT_PERC] = round(( + self._attr['breakdown']['light'] / sleep_time) * 100, 2) + except ZeroDivisionError: + state_attr[ATTR_LIGHT_PERC] = 0 + try: + state_attr[ATTR_DEEP_PERC] = round(( + self._attr['breakdown']['deep'] / sleep_time) * 100, 2) + except ZeroDivisionError: + state_attr[ATTR_DEEP_PERC] = 0 + + try: + state_attr[ATTR_REM_PERC] = round(( + self._attr['breakdown']['rem'] / sleep_time) * 100, 2) + except ZeroDivisionError: + state_attr[ATTR_REM_PERC] = 0 + + try: + if self._units == 'si': + room_temp = round(self._attr['room_temp'], 2) + else: + room_temp = round((self._attr['room_temp']*1.8)+32, 2) + except TypeError: + room_temp = None + + try: + if self._units == 'si': + bed_temp = round(self._attr['bed_temp'], 2) + else: + bed_temp = round((self._attr['bed_temp']*1.8)+32, 2) + except TypeError: + bed_temp = None + + if 'current' in self._sensor_root: + state_attr[ATTR_RESP_RATE] = round(self._attr['resp_rate'], 2) + state_attr[ATTR_HEART_RATE] = round(self._attr['heart_rate'], 2) + state_attr[ATTR_SLEEP_STAGE] = self._attr['stage'] + state_attr[ATTR_ROOM_TEMP] = room_temp + state_attr[ATTR_BED_TEMP] = bed_temp + elif 'last' in self._sensor_root: + state_attr[ATTR_AVG_RESP_RATE] = round(self._attr['resp_rate'], 2) + state_attr[ATTR_AVG_HEART_RATE] = round( + self._attr['heart_rate'], 2) + state_attr[ATTR_AVG_ROOM_TEMP] = room_temp + state_attr[ATTR_AVG_BED_TEMP] = bed_temp + + return state_attr + + +class EightRoomSensor(EightSleepUserEntity): + """Representation of an eight sleep room sensor.""" + + def __init__(self, name, eight, sensor, units): + """Initialize the sensor.""" + super().__init__(eight) + + self._sensor = sensor + self._mapped_name = NAME_MAP.get(self._sensor, self._sensor) + self._name = '{} {}'.format(name, self._mapped_name) + self._state = None + self._attr = None + self._units = units + + @property + def name(self): + """Return the name of the sensor, if any.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + async def async_update(self): + """Retrieve latest state.""" + _LOGGER.debug("Updating Room sensor: %s", self._sensor) + temp = self._eight.room_temperature() + try: + if self._units == 'si': + self._state = round(temp, 2) + else: + self._state = round((temp*1.8)+32, 2) + except TypeError: + self._state = None + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + if self._units == 'si': + return '°C' + return '°F' + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return 'mdi:thermometer' diff --git a/homeassistant/components/eight_sleep/services.yaml b/homeassistant/components/eight_sleep/services.yaml new file mode 100644 index 0000000000000..db7690730dd91 --- /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 8ac3cec641131..a0c08bf54299f 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.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/elkm1/alarm_control_panel.py b/homeassistant/components/elkm1/alarm_control_panel.py new file mode 100644 index 0000000000000..63b38c1d321fa --- /dev/null +++ b/homeassistant/components/elkm1/alarm_control_panel.py @@ -0,0 +1,198 @@ +"""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 ( + ATTR_CODE, ATTR_ENTITY_ID, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMING, STATE_ALARM_DISARMED, + STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED) +from homeassistant.components.elkm1 import ( + DOMAIN as ELK_DOMAIN, create_elk_entities, ElkEntity) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, async_dispatcher_send) + +DEPENDENCIES = [ELK_DOMAIN] + +SIGNAL_ARM_ENTITY = 'elkm1_arm' +SIGNAL_DISPLAY_MESSAGE = 'elkm1_display_message' + +ELK_ALARM_SERVICE_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID, default=[]): cv.entity_ids, + vol.Required(ATTR_CODE): vol.All(vol.Coerce(int), vol.Range(0, 999999)), +}) + +DISPLAY_MESSAGE_SERVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID, default=[]): cv.entity_ids, + vol.Optional('clear', default=2): vol.In([0, 1, 2]), + vol.Optional('beep', default=False): cv.boolean, + vol.Optional('timeout', default=0): vol.Range(min=0, max=65535), + vol.Optional('line1', default=''): cv.string, + vol.Optional('line2', default=''): cv.string, +}) + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the ElkM1 alarm platform.""" + if discovery_info is None: + return + + elk = hass.data[ELK_DOMAIN]['elk'] + entities = create_elk_entities(hass, elk.areas, 'area', ElkArea, []) + async_add_entities(entities, True) + + def _dispatch(signal, entity_ids, *args): + for entity_id in entity_ids: + async_dispatcher_send( + hass, '{}_{}'.format(signal, entity_id), *args) + + def _arm_service(service): + entity_ids = service.data.get(ATTR_ENTITY_ID, []) + arm_level = _arm_services().get(service.service) + args = (arm_level, service.data.get(ATTR_CODE)) + _dispatch(SIGNAL_ARM_ENTITY, entity_ids, *args) + + for service in _arm_services(): + hass.services.async_register( + alarm.DOMAIN, service, _arm_service, ELK_ALARM_SERVICE_SCHEMA) + + def _display_message_service(service): + entity_ids = service.data.get(ATTR_ENTITY_ID, []) + data = service.data + args = (data['clear'], data['beep'], data['timeout'], + data['line1'], data['line2']) + _dispatch(SIGNAL_DISPLAY_MESSAGE, entity_ids, *args) + + hass.services.async_register( + alarm.DOMAIN, 'elkm1_alarm_display_message', + _display_message_service, DISPLAY_MESSAGE_SERVICE_SCHEMA) + + +def _arm_services(): + from elkm1_lib.const import ArmLevel + + return { + 'elkm1_alarm_arm_vacation': ArmLevel.ARMED_VACATION.value, + 'elkm1_alarm_arm_home_instant': ArmLevel.ARMED_STAY_INSTANT.value, + 'elkm1_alarm_arm_night_instant': ArmLevel.ARMED_NIGHT_INSTANT.value, + } + + +class ElkArea(ElkEntity, alarm.AlarmControlPanel): + """Representation of an Area / Partition within the ElkM1 alarm panel.""" + + def __init__(self, element, elk, elk_data): + """Initialize Area as Alarm Control Panel.""" + super().__init__(element, elk, elk_data) + self._changed_by_entity_id = '' + self._state = None + + async def async_added_to_hass(self): + """Register callback for ElkM1 changes.""" + await super().async_added_to_hass() + for keypad in self._elk.keypads: + keypad.add_callback(self._watch_keypad) + async_dispatcher_connect( + self.hass, '{}_{}'.format(SIGNAL_ARM_ENTITY, self.entity_id), + self._arm_service) + async_dispatcher_connect( + self.hass, '{}_{}'.format(SIGNAL_DISPLAY_MESSAGE, self.entity_id), + self._display_message) + + def _watch_keypad(self, keypad, changeset): + if keypad.area != self._element.index: + return + if changeset.get('last_user') is not None: + self._changed_by_entity_id = self.hass.data[ + ELK_DOMAIN]['keypads'].get(keypad.index, '') + self.async_schedule_update_ha_state(True) + + @property + def code_format(self): + """Return the alarm code format.""" + return alarm.FORMAT_NUMBER + + @property + def state(self): + """Return the state of the element.""" + return self._state + + @property + def device_state_attributes(self): + """Attributes of the area.""" + from elkm1_lib.const import AlarmState, ArmedStatus, ArmUpState + + attrs = self.initial_attrs() + elmt = self._element + attrs['is_exit'] = elmt.is_exit + attrs['timer1'] = elmt.timer1 + attrs['timer2'] = elmt.timer2 + if elmt.armed_status is not None: + attrs['armed_status'] = \ + ArmedStatus(elmt.armed_status).name.lower() + if elmt.arm_up_state is not None: + attrs['arm_up_state'] = ArmUpState(elmt.arm_up_state).name.lower() + if elmt.alarm_state is not None: + attrs['alarm_state'] = AlarmState(elmt.alarm_state).name.lower() + attrs['changed_by_entity_id'] = self._changed_by_entity_id + return attrs + + def _element_changed(self, element, changeset): + from elkm1_lib.const import ArmedStatus + + elk_state_to_hass_state = { + ArmedStatus.DISARMED.value: STATE_ALARM_DISARMED, + ArmedStatus.ARMED_AWAY.value: STATE_ALARM_ARMED_AWAY, + ArmedStatus.ARMED_STAY.value: STATE_ALARM_ARMED_HOME, + ArmedStatus.ARMED_STAY_INSTANT.value: STATE_ALARM_ARMED_HOME, + ArmedStatus.ARMED_TO_NIGHT.value: STATE_ALARM_ARMED_NIGHT, + ArmedStatus.ARMED_TO_NIGHT_INSTANT.value: STATE_ALARM_ARMED_NIGHT, + ArmedStatus.ARMED_TO_VACATION.value: STATE_ALARM_ARMED_AWAY, + } + + if self._element.alarm_state is None: + self._state = None + elif self._area_is_in_alarm_state(): + self._state = STATE_ALARM_TRIGGERED + elif self._entry_exit_timer_is_running(): + self._state = STATE_ALARM_ARMING \ + if self._element.is_exit else STATE_ALARM_PENDING + else: + self._state = elk_state_to_hass_state[self._element.armed_status] + + def _entry_exit_timer_is_running(self): + return self._element.timer1 > 0 or self._element.timer2 > 0 + + def _area_is_in_alarm_state(self): + from elkm1_lib.const import AlarmState + + return self._element.alarm_state >= AlarmState.FIRE_ALARM.value + + async def async_alarm_disarm(self, code=None): + """Send disarm command.""" + self._element.disarm(int(code)) + + async def async_alarm_arm_home(self, code=None): + """Send arm home command.""" + from elkm1_lib.const import ArmLevel + + self._element.arm(ArmLevel.ARMED_STAY.value, int(code)) + + async def async_alarm_arm_away(self, code=None): + """Send arm away command.""" + from elkm1_lib.const import ArmLevel + + self._element.arm(ArmLevel.ARMED_AWAY.value, int(code)) + + async def async_alarm_arm_night(self, code=None): + """Send arm night command.""" + from elkm1_lib.const import ArmLevel + + self._element.arm(ArmLevel.ARMED_NIGHT.value, int(code)) + + async def _arm_service(self, arm_level, code): + self._element.arm(arm_level, code) + + async def _display_message(self, clear, beep, timeout, line1, line2): + """Display a message on all keypads for the area.""" + self._element.display_message(clear, beep, timeout, line1, line2) diff --git a/homeassistant/components/elkm1/climate.py b/homeassistant/components/elkm1/climate.py new file mode 100644 index 0000000000000..467d542ee6d49 --- /dev/null +++ b/homeassistant/components/elkm1/climate.py @@ -0,0 +1,188 @@ +"""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, + SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE_HIGH, + SUPPORT_TARGET_TEMPERATURE_LOW, ClimateDevice) +from homeassistant.components.elkm1 import ( + DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities) +from homeassistant.const import STATE_ON + +DEPENDENCIES = [ELK_DOMAIN] + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Create the Elk-M1 thermostat platform.""" + if discovery_info is None: + return + + elk = hass.data[ELK_DOMAIN]['elk'] + async_add_entities(create_elk_entities( + hass, elk.thermostats, 'thermostat', ElkThermostat, []), True) + + +class ElkThermostat(ElkEntity, ClimateDevice): + """Representation of an Elk-M1 Thermostat.""" + + def __init__(self, element, elk, elk_data): + """Initialize climate entity.""" + super().__init__(element, elk, elk_data) + self._state = None + + @property + def supported_features(self): + """Return the list of supported features.""" + return (SUPPORT_OPERATION_MODE | SUPPORT_FAN_MODE | SUPPORT_AUX_HEAT + | SUPPORT_TARGET_TEMPERATURE_HIGH + | SUPPORT_TARGET_TEMPERATURE_LOW) + + @property + def temperature_unit(self): + """Return the temperature unit.""" + return self._temperature_unit + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._element.current_temp + + @property + def target_temperature(self): + """Return the temperature we are trying to reach.""" + from elkm1_lib.const import ThermostatMode + if (self._element.mode == ThermostatMode.HEAT.value) or ( + self._element.mode == ThermostatMode.EMERGENCY_HEAT.value): + return self._element.heat_setpoint + if self._element.mode == ThermostatMode.COOL.value: + return self._element.cool_setpoint + return None + + @property + def target_temperature_high(self): + """Return the high target temperature.""" + return self._element.cool_setpoint + + @property + def target_temperature_low(self): + """Return the low target temperature.""" + return self._element.heat_setpoint + + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + return 1 + + @property + def current_humidity(self): + """Return the current humidity.""" + return self._element.humidity + + @property + def current_operation(self): + """Return current operation ie. heat, cool, idle.""" + return self._state + + @property + def operation_list(self): + """Return the list of available operation modes.""" + return [STATE_IDLE, STATE_HEAT, STATE_COOL, STATE_AUTO, STATE_FAN_ONLY] + + @property + def precision(self): + """Return the precision of the system.""" + return PRECISION_WHOLE + + @property + def is_aux_heat_on(self): + """Return if aux heater is on.""" + from elkm1_lib.const import ThermostatMode + return self._element.mode == ThermostatMode.EMERGENCY_HEAT.value + + @property + def min_temp(self): + """Return the minimum temperature supported.""" + return 1 + + @property + def max_temp(self): + """Return the maximum temperature supported.""" + return 99 + + @property + def current_fan_mode(self): + """Return the fan setting.""" + from elkm1_lib.const import ThermostatFan + if self._element.fan == ThermostatFan.AUTO.value: + return STATE_AUTO + if self._element.fan == ThermostatFan.ON.value: + return STATE_ON + return None + + def _elk_set(self, mode, fan): + from elkm1_lib.const import ThermostatSetting + if mode is not None: + self._element.set(ThermostatSetting.MODE.value, mode) + if fan is not None: + self._element.set(ThermostatSetting.FAN.value, fan) + + async def async_set_operation_mode(self, operation_mode): + """Set thermostat operation mode.""" + from elkm1_lib.const import ThermostatFan, ThermostatMode + settings = { + STATE_IDLE: (ThermostatMode.OFF.value, ThermostatFan.AUTO.value), + STATE_HEAT: (ThermostatMode.HEAT.value, None), + STATE_COOL: (ThermostatMode.COOL.value, None), + STATE_AUTO: (ThermostatMode.AUTO.value, None), + STATE_FAN_ONLY: (ThermostatMode.OFF.value, ThermostatFan.ON.value) + } + self._elk_set(settings[operation_mode][0], settings[operation_mode][1]) + + async def async_turn_aux_heat_on(self): + """Turn auxiliary heater on.""" + from elkm1_lib.const import ThermostatMode + self._elk_set(ThermostatMode.EMERGENCY_HEAT.value, None) + + async def async_turn_aux_heat_off(self): + """Turn auxiliary heater off.""" + from elkm1_lib.const import ThermostatMode + self._elk_set(ThermostatMode.HEAT.value, None) + + @property + def fan_list(self): + """Return the list of available fan modes.""" + return [STATE_AUTO, STATE_ON] + + async def async_set_fan_mode(self, fan_mode): + """Set new target fan mode.""" + from elkm1_lib.const import ThermostatFan + if fan_mode == STATE_AUTO: + self._elk_set(None, ThermostatFan.AUTO.value) + elif fan_mode == STATE_ON: + self._elk_set(None, ThermostatFan.ON.value) + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + from elkm1_lib.const import ThermostatSetting + low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW) + high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) + if low_temp is not None: + self._element.set( + ThermostatSetting.HEAT_SETPOINT.value, round(low_temp)) + if high_temp is not None: + self._element.set( + ThermostatSetting.COOL_SETPOINT.value, round(high_temp)) + + def _element_changed(self, element, changeset): + from elkm1_lib.const import ThermostatFan, ThermostatMode + mode_to_state = { + ThermostatMode.OFF.value: STATE_IDLE, + ThermostatMode.COOL.value: STATE_COOL, + ThermostatMode.HEAT.value: STATE_HEAT, + ThermostatMode.EMERGENCY_HEAT.value: STATE_HEAT, + ThermostatMode.AUTO.value: STATE_AUTO, + } + self._state = mode_to_state.get(self._element.mode) + if self._state == STATE_IDLE and \ + self._element.fan == ThermostatFan.ON.value: + self._state = STATE_FAN_ONLY diff --git a/homeassistant/components/elkm1/light.py b/homeassistant/components/elkm1/light.py new file mode 100644 index 0000000000000..3a282595d5854 --- /dev/null +++ b/homeassistant/components/elkm1/light.py @@ -0,0 +1,53 @@ +"""Support for control of ElkM1 lighting (X10, UPB, etc).""" +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) +from homeassistant.components.elkm1 import ( + DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities) + +DEPENDENCIES = [ELK_DOMAIN] + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Set up the Elk light platform.""" + if discovery_info is None: + return + elk = hass.data[ELK_DOMAIN]['elk'] + async_add_entities( + create_elk_entities(hass, elk.lights, 'plc', ElkLight, []), True) + + +class ElkLight(ElkEntity, Light): + """Representation of an Elk lighting device.""" + + def __init__(self, element, elk, elk_data): + """Initialize the Elk light.""" + super().__init__(element, elk, elk_data) + self._brightness = self._element.status + + @property + def brightness(self): + """Get the brightness.""" + return self._brightness + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS + + @property + def is_on(self) -> bool: + """Get the current brightness.""" + return self._brightness != 0 + + def _element_changed(self, element, changeset): + status = self._element.status if self._element.status != 1 else 100 + self._brightness = round(status * 2.55) + + async def async_turn_on(self, **kwargs): + """Turn on the light.""" + self._element.level(round(kwargs.get(ATTR_BRIGHTNESS, 255) / 2.55)) + + async def async_turn_off(self, **kwargs): + """Turn off the light.""" + self._element.level(0) diff --git a/homeassistant/components/elkm1/scene.py b/homeassistant/components/elkm1/scene.py new file mode 100644 index 0000000000000..c8583b1d8bfec --- /dev/null +++ b/homeassistant/components/elkm1/scene.py @@ -0,0 +1,24 @@ +"""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 + +DEPENDENCIES = [ELK_DOMAIN] + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Create the Elk-M1 scene platform.""" + if discovery_info is None: + return + elk = hass.data[ELK_DOMAIN]['elk'] + entities = create_elk_entities(hass, elk.tasks, 'task', ElkTask, []) + async_add_entities(entities, True) + + +class ElkTask(ElkEntity, Scene): + """Elk-M1 task as scene.""" + + async def async_activate(self): + """Activate the task.""" + self._element.activate() diff --git a/homeassistant/components/elkm1/sensor.py b/homeassistant/components/elkm1/sensor.py new file mode 100644 index 0000000000000..63da6ea537658 --- /dev/null +++ b/homeassistant/components/elkm1/sensor.py @@ -0,0 +1,223 @@ +"""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): + """Create the Elk-M1 sensor platform.""" + if discovery_info is None: + return + + elk = hass.data[ELK_DOMAIN]['elk'] + entities = create_elk_entities( + hass, elk.counters, 'counter', ElkCounter, []) + entities = create_elk_entities( + hass, elk.keypads, 'keypad', ElkKeypad, entities) + entities = create_elk_entities( + hass, [elk.panel], 'panel', ElkPanel, entities) + entities = create_elk_entities( + hass, elk.settings, 'setting', ElkSetting, entities) + entities = create_elk_entities( + hass, elk.zones, 'zone', ElkZone, entities) + async_add_entities(entities, True) + + +def temperature_to_state(temperature, undefined_temperature): + """Convert temperature to a state.""" + return temperature if temperature > undefined_temperature else None + + +class ElkSensor(ElkEntity): + """Base representation of Elk-M1 sensor.""" + + def __init__(self, element, elk, elk_data): + """Initialize the base of all Elk sensors.""" + super().__init__(element, elk, elk_data) + self._state = None + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + +class ElkCounter(ElkSensor): + """Representation of an Elk-M1 Counter.""" + + @property + def icon(self): + """Icon to use in the frontend.""" + return 'mdi:numeric' + + def _element_changed(self, element, changeset): + self._state = self._element.value + + +class ElkKeypad(ElkSensor): + """Representation of an Elk-M1 Keypad.""" + + @property + def temperature_unit(self): + """Return the temperature unit.""" + return self._temperature_unit + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._temperature_unit + + @property + def icon(self): + """Icon to use in the frontend.""" + return 'mdi:thermometer-lines' + + @property + def device_state_attributes(self): + """Attributes of the sensor.""" + from elkm1_lib.util import username + + attrs = self.initial_attrs() + attrs['area'] = self._element.area + 1 + attrs['temperature'] = self._element.temperature + attrs['last_user_time'] = self._element.last_user_time.isoformat() + attrs['last_user'] = self._element.last_user + 1 + attrs['code'] = self._element.code + attrs['last_user_name'] = username(self._elk, self._element.last_user) + attrs['last_keypress'] = self._element.last_keypress + return attrs + + def _element_changed(self, element, changeset): + self._state = temperature_to_state(self._element.temperature, -40) + + async def async_added_to_hass(self): + """Register callback for ElkM1 changes and update entity state.""" + await super().async_added_to_hass() + self.hass.data[ELK_DOMAIN]['keypads'][ + self._element.index] = self.entity_id + + +class ElkPanel(ElkSensor): + """Representation of an Elk-M1 Panel.""" + + @property + def icon(self): + """Icon to use in the frontend.""" + return "mdi:home" + + @property + def device_state_attributes(self): + """Attributes of the sensor.""" + attrs = self.initial_attrs() + attrs['system_trouble_status'] = self._element.system_trouble_status + return attrs + + def _element_changed(self, element, changeset): + if self._elk.is_connected(): + self._state = 'Paused' if self._element.remote_programming_status \ + else 'Connected' + else: + self._state = 'Disconnected' + + +class ElkSetting(ElkSensor): + """Representation of an Elk-M1 Setting.""" + + @property + def icon(self): + """Icon to use in the frontend.""" + return 'mdi:numeric' + + def _element_changed(self, element, changeset): + self._state = self._element.value + + @property + def device_state_attributes(self): + """Attributes of the sensor.""" + from elkm1_lib.const import SettingFormat + attrs = self.initial_attrs() + attrs['value_format'] = SettingFormat( + self._element.value_format).name.lower() + return attrs + + +class ElkZone(ElkSensor): + """Representation of an Elk-M1 Zone.""" + + @property + def icon(self): + """Icon to use in the frontend.""" + from elkm1_lib.const import ZoneType + zone_icons = { + ZoneType.FIRE_ALARM.value: 'fire', + ZoneType.FIRE_VERIFIED.value: 'fire', + ZoneType.FIRE_SUPERVISORY.value: 'fire', + ZoneType.KEYFOB.value: 'key', + ZoneType.NON_ALARM.value: 'alarm-off', + ZoneType.MEDICAL_ALARM.value: 'medical-bag', + ZoneType.POLICE_ALARM.value: 'alarm-light', + ZoneType.POLICE_NO_INDICATION.value: 'alarm-light', + ZoneType.KEY_MOMENTARY_ARM_DISARM.value: 'power', + ZoneType.KEY_MOMENTARY_ARM_AWAY.value: 'power', + ZoneType.KEY_MOMENTARY_ARM_STAY.value: 'power', + ZoneType.KEY_MOMENTARY_DISARM.value: 'power', + ZoneType.KEY_ON_OFF.value: 'toggle-switch', + ZoneType.MUTE_AUDIBLES.value: 'volume-mute', + ZoneType.POWER_SUPERVISORY.value: 'power-plug', + ZoneType.TEMPERATURE.value: 'thermometer-lines', + ZoneType.ANALOG_ZONE.value: 'speedometer', + ZoneType.PHONE_KEY.value: 'phone-classic', + ZoneType.INTERCOM_KEY.value: 'deskphone' + } + return 'mdi:{}'.format( + zone_icons.get(self._element.definition, 'alarm-bell')) + + @property + def device_state_attributes(self): + """Attributes of the sensor.""" + from elkm1_lib.const import ( + ZoneLogicalStatus, ZonePhysicalStatus, ZoneType) + + attrs = self.initial_attrs() + attrs['physical_status'] = ZonePhysicalStatus( + self._element.physical_status).name.lower() + attrs['logical_status'] = ZoneLogicalStatus( + self._element.logical_status).name.lower() + attrs['definition'] = ZoneType( + self._element.definition).name.lower() + attrs['area'] = self._element.area + 1 + attrs['bypassed'] = self._element.bypassed + attrs['triggered_alarm'] = self._element.triggered_alarm + return attrs + + @property + def temperature_unit(self): + """Return the temperature unit.""" + from elkm1_lib.const import ZoneType + if self._element.definition == ZoneType.TEMPERATURE.value: + return self._temperature_unit + return None + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + from elkm1_lib.const import ZoneType + if self._element.definition == ZoneType.TEMPERATURE.value: + return self._temperature_unit + if self._element.definition == ZoneType.ANALOG_ZONE.value: + return 'V' + return None + + def _element_changed(self, element, changeset): + from elkm1_lib.const import ZoneLogicalStatus, ZoneType + from elkm1_lib.util import pretty_const + + if self._element.definition == ZoneType.TEMPERATURE.value: + self._state = temperature_to_state(self._element.temperature, -60) + elif self._element.definition == ZoneType.ANALOG_ZONE.value: + self._state = self._element.voltage + else: + self._state = pretty_const(ZoneLogicalStatus( + self._element.logical_status).name) diff --git a/homeassistant/components/elkm1/switch.py b/homeassistant/components/elkm1/switch.py new file mode 100644 index 0000000000000..7badd6ee5dc2f --- /dev/null +++ b/homeassistant/components/elkm1/switch.py @@ -0,0 +1,33 @@ +"""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 + +DEPENDENCIES = [ELK_DOMAIN] + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Create the Elk-M1 switch platform.""" + if discovery_info is None: + return + elk = hass.data[ELK_DOMAIN]['elk'] + entities = create_elk_entities(hass, elk.outputs, 'output', ElkOutput, []) + async_add_entities(entities, True) + + +class ElkOutput(ElkEntity, SwitchDevice): + """Elk output as switch.""" + + @property + def is_on(self) -> bool: + """Get the current output status.""" + return self._element.output_on + + async def async_turn_on(self, **kwargs): + """Turn on the output.""" + self._element.turn_on(0) + + async def async_turn_off(self, **kwargs): + """Turn off the output.""" + self._element.turn_off() diff --git a/homeassistant/components/emoncms_history.py b/homeassistant/components/emoncms_history.py deleted file mode 100644 index 6a92ab6404443..0000000000000 --- a/homeassistant/components/emoncms_history.py +++ /dev/null @@ -1,89 +0,0 @@ -""" -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/ -""" -import logging -from datetime import timedelta - -import requests -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.const import ( - CONF_API_KEY, CONF_WHITELIST, CONF_URL, STATE_UNKNOWN, STATE_UNAVAILABLE, - CONF_SCAN_INTERVAL) -from homeassistant.helpers import state as state_helper -from homeassistant.helpers.event import track_point_in_time -from homeassistant.util import dt as dt_util - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'emoncms_history' -CONF_INPUTNODE = 'inputnode' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_URL): cv.string, - vol.Required(CONF_INPUTNODE): cv.positive_int, - vol.Required(CONF_WHITELIST): cv.entity_ids, - vol.Optional(CONF_SCAN_INTERVAL, default=30): cv.positive_int, - }), -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the Emoncms history component.""" - conf = config[DOMAIN] - whitelist = conf.get(CONF_WHITELIST) - - def send_data(url, apikey, node, payload): - """Send payload data to Emoncms.""" - try: - fullurl = '{}/input/post.json'.format(url) - data = {"apikey": apikey, "data": payload} - parameters = {"node": node} - req = requests.post( - fullurl, params=parameters, data=data, allow_redirects=True, - timeout=5) - - except requests.exceptions.RequestException: - _LOGGER.error("Error saving data '%s' to '%s'", payload, fullurl) - - else: - if req.status_code != 200: - _LOGGER.error( - "Error saving data %s to %s (http status code = %d)", - payload, fullurl, req.status_code) - - def update_emoncms(time): - """Send whitelisted entities states regularly to Emoncms.""" - payload_dict = {} - - for entity_id in whitelist: - state = hass.states.get(entity_id) - - if state is None or state.state in ( - STATE_UNKNOWN, '', STATE_UNAVAILABLE): - continue - - try: - payload_dict[entity_id] = state_helper.state_as_number(state) - except ValueError: - continue - - if payload_dict: - payload = "{%s}" % ",".join("{}:{}".format(key, val) - for key, val in - payload_dict.items()) - - send_data(conf.get(CONF_URL), conf.get(CONF_API_KEY), - str(conf.get(CONF_INPUTNODE)), payload) - - track_point_in_time(hass, update_emoncms, time + - timedelta(seconds=conf.get(CONF_SCAN_INTERVAL))) - - update_emoncms(dt_util.utcnow()) - return True diff --git a/homeassistant/components/emoncms_history/__init__.py b/homeassistant/components/emoncms_history/__init__.py new file mode 100644 index 0000000000000..45fb358cecc63 --- /dev/null +++ b/homeassistant/components/emoncms_history/__init__.py @@ -0,0 +1,84 @@ +"""Support for sending data to Emoncms.""" +import logging +from datetime import timedelta + +import requests +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import ( + CONF_API_KEY, CONF_WHITELIST, CONF_URL, STATE_UNKNOWN, STATE_UNAVAILABLE, + CONF_SCAN_INTERVAL) +from homeassistant.helpers import state as state_helper +from homeassistant.helpers.event import track_point_in_time +from homeassistant.util import dt as dt_util + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'emoncms_history' +CONF_INPUTNODE = 'inputnode' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_URL): cv.string, + vol.Required(CONF_INPUTNODE): cv.positive_int, + vol.Required(CONF_WHITELIST): cv.entity_ids, + vol.Optional(CONF_SCAN_INTERVAL, default=30): cv.positive_int, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Emoncms history component.""" + conf = config[DOMAIN] + whitelist = conf.get(CONF_WHITELIST) + + def send_data(url, apikey, node, payload): + """Send payload data to Emoncms.""" + try: + fullurl = '{}/input/post.json'.format(url) + data = {"apikey": apikey, "data": payload} + parameters = {"node": node} + req = requests.post( + fullurl, params=parameters, data=data, allow_redirects=True, + timeout=5) + + except requests.exceptions.RequestException: + _LOGGER.error("Error saving data '%s' to '%s'", payload, fullurl) + + else: + if req.status_code != 200: + _LOGGER.error( + "Error saving data %s to %s (http status code = %d)", + payload, fullurl, req.status_code) + + def update_emoncms(time): + """Send whitelisted entities states regularly to Emoncms.""" + payload_dict = {} + + for entity_id in whitelist: + state = hass.states.get(entity_id) + + if state is None or state.state in ( + STATE_UNKNOWN, '', STATE_UNAVAILABLE): + continue + + try: + payload_dict[entity_id] = state_helper.state_as_number(state) + except ValueError: + continue + + if payload_dict: + payload = "{%s}" % ",".join("{}:{}".format(key, val) + for key, val in + payload_dict.items()) + + send_data(conf.get(CONF_URL), conf.get(CONF_API_KEY), + str(conf.get(CONF_INPUTNODE)), payload) + + track_point_in_time(hass, update_emoncms, time + + timedelta(seconds=conf.get(CONF_SCAN_INTERVAL))) + + update_emoncms(dt_util.utcnow()) + return True diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index 07ecb9d265a98..c8ed263a2dc20 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 @@ 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 815e28b4fa463..95b3c470d9e00 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.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 @@ async def put(self, request, username, entity_number): # 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 @@ async def put(self, request, username, entity_number): 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 @@ async def put(self, request, username, entity_number): 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 548b6f3d77197..a163d4b2e91f4 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 0000000000000..0479dee437d6e --- /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 0000000000000..5da2d437a3586 --- /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 0000000000000..fe26cda31e264 --- /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 0000000000000..286cd58dd8966 --- /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 611b56472330c..c7b85c195929d 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 4dec1d5602a6f..ef87e14ec434b 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 f2d56f846813a..d08ad09f1c0b9 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 f4a034e31accc..25ea3adaa84e5 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.py deleted file mode 100644 index 75e456f62bde4..0000000000000 --- a/homeassistant/components/enocean.py +++ /dev/null @@ -1,132 +0,0 @@ -""" -EnOcean Component. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/EnOcean/ -""" -import logging - -import voluptuous as vol - -from homeassistant.const import CONF_DEVICE -import homeassistant.helpers.config_validation as cv - -REQUIREMENTS = ['enocean==0.40'] - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'enocean' - -ENOCEAN_DONGLE = None - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_DEVICE): cv.string, - }), -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the EnOcean component.""" - global ENOCEAN_DONGLE - - serial_dev = config[DOMAIN].get(CONF_DEVICE) - - ENOCEAN_DONGLE = EnOceanDongle(hass, serial_dev) - - return True - - -class EnOceanDongle: - """Representation of an EnOcean dongle.""" - - def __init__(self, hass, ser): - """Initialize the EnOcean dongle.""" - from enocean.communicators.serialcommunicator import SerialCommunicator - self.__communicator = SerialCommunicator( - port=ser, callback=self.callback) - self.__communicator.start() - self.__devices = [] - - def register_device(self, dev): - """Register another device.""" - self.__devices.append(dev) - - def send_command(self, command): - """Send a command from the EnOcean dongle.""" - self.__communicator.send(command) - - # pylint: disable=no-self-use - def _combine_hex(self, data): - """Combine list of integer values to one big integer.""" - output = 0x00 - for i, j in enumerate(reversed(data)): - output |= (j << i * 8) - return output - - def callback(self, temp): - """Handle EnOcean device's callback. - - This is the callback function called by python-enocan whenever there - is an incoming packet. - """ - from enocean.protocol.packet import RadioPacket - if isinstance(temp, RadioPacket): - _LOGGER.debug("Received radio packet: %s", temp) - rxtype = None - value = None - channel = 0 - if temp.data[6] == 0x30: - rxtype = "wallswitch" - value = 1 - elif temp.data[6] == 0x20: - rxtype = "wallswitch" - value = 0 - elif temp.data[4] == 0x0c: - rxtype = "power" - value = temp.data[3] + (temp.data[2] << 8) - elif temp.data[2] & 0x60 == 0x60: - rxtype = "switch_status" - channel = temp.data[2] & 0x1F - if temp.data[3] == 0xe4: - value = 1 - elif temp.data[3] == 0x80: - value = 0 - elif temp.data[0] == 0xa5 and temp.data[1] == 0x02: - rxtype = "dimmerstatus" - value = temp.data[2] - for device in self.__devices: - if rxtype == "wallswitch" and device.stype == "listener": - if temp.sender_int == self._combine_hex(device.dev_id): - device.value_changed(value, temp.data[1]) - if rxtype == "power" and device.stype == "powersensor": - if temp.sender_int == self._combine_hex(device.dev_id): - device.value_changed(value) - if rxtype == "power" and device.stype == "switch": - if temp.sender_int == self._combine_hex(device.dev_id): - if value > 10: - device.value_changed(1) - if rxtype == "switch_status" and device.stype == "switch" and \ - channel == device.channel: - if temp.sender_int == self._combine_hex(device.dev_id): - device.value_changed(value) - if rxtype == "dimmerstatus" and device.stype == "dimmer": - if temp.sender_int == self._combine_hex(device.dev_id): - device.value_changed(value) - - -class EnOceanDevice(): - """Parent class for all devices associated with the EnOcean component.""" - - def __init__(self): - """Initialize the device.""" - ENOCEAN_DONGLE.register_device(self) - self.stype = "" - self.sensorid = [0x00, 0x00, 0x00, 0x00] - - # pylint: disable=no-self-use - def send_command(self, data, optional, packet_type): - """Send a command via the EnOcean dongle.""" - from enocean.protocol.packet import Packet - packet = Packet(packet_type, data=data, optional=optional) - ENOCEAN_DONGLE.send_command(packet) diff --git a/homeassistant/components/enocean/__init__.py b/homeassistant/components/enocean/__init__.py new file mode 100644 index 0000000000000..8b3c27025cd26 --- /dev/null +++ b/homeassistant/components/enocean/__init__.py @@ -0,0 +1,127 @@ +"""Support for EnOcean devices.""" +import logging + +import voluptuous as vol + +from homeassistant.const import CONF_DEVICE +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['enocean==0.40'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'enocean' + +ENOCEAN_DONGLE = None + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_DEVICE): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the EnOcean component.""" + global ENOCEAN_DONGLE + + serial_dev = config[DOMAIN].get(CONF_DEVICE) + + ENOCEAN_DONGLE = EnOceanDongle(hass, serial_dev) + + return True + + +class EnOceanDongle: + """Representation of an EnOcean dongle.""" + + def __init__(self, hass, ser): + """Initialize the EnOcean dongle.""" + from enocean.communicators.serialcommunicator import SerialCommunicator + self.__communicator = SerialCommunicator( + port=ser, callback=self.callback) + self.__communicator.start() + self.__devices = [] + + def register_device(self, dev): + """Register another device.""" + self.__devices.append(dev) + + def send_command(self, command): + """Send a command from the EnOcean dongle.""" + self.__communicator.send(command) + + # pylint: disable=no-self-use + def _combine_hex(self, data): + """Combine list of integer values to one big integer.""" + output = 0x00 + for i, j in enumerate(reversed(data)): + output |= (j << i * 8) + return output + + def callback(self, temp): + """Handle EnOcean device's callback. + + This is the callback function called by python-enocan whenever there + is an incoming packet. + """ + from enocean.protocol.packet import RadioPacket + if isinstance(temp, RadioPacket): + _LOGGER.debug("Received radio packet: %s", temp) + rxtype = None + value = None + channel = 0 + if temp.data[6] == 0x30: + rxtype = "wallswitch" + value = 1 + elif temp.data[6] == 0x20: + rxtype = "wallswitch" + value = 0 + elif temp.data[4] == 0x0c: + rxtype = "power" + value = temp.data[3] + (temp.data[2] << 8) + elif temp.data[2] & 0x60 == 0x60: + rxtype = "switch_status" + channel = temp.data[2] & 0x1F + if temp.data[3] == 0xe4: + value = 1 + elif temp.data[3] == 0x80: + value = 0 + elif temp.data[0] == 0xa5 and temp.data[1] == 0x02: + rxtype = "dimmerstatus" + value = temp.data[2] + for device in self.__devices: + if rxtype == "wallswitch" and device.stype == "listener": + if temp.sender_int == self._combine_hex(device.dev_id): + device.value_changed(value, temp.data[1]) + if rxtype == "power" and device.stype == "powersensor": + if temp.sender_int == self._combine_hex(device.dev_id): + device.value_changed(value) + if rxtype == "power" and device.stype == "switch": + if temp.sender_int == self._combine_hex(device.dev_id): + if value > 10: + device.value_changed(1) + if rxtype == "switch_status" and device.stype == "switch" and \ + channel == device.channel: + if temp.sender_int == self._combine_hex(device.dev_id): + device.value_changed(value) + if rxtype == "dimmerstatus" and device.stype == "dimmer": + if temp.sender_int == self._combine_hex(device.dev_id): + device.value_changed(value) + + +class EnOceanDevice(): + """Parent class for all devices associated with the EnOcean component.""" + + def __init__(self): + """Initialize the device.""" + ENOCEAN_DONGLE.register_device(self) + self.stype = "" + self.sensorid = [0x00, 0x00, 0x00, 0x00] + + # pylint: disable=no-self-use + def send_command(self, data, optional, packet_type): + """Send a command via the EnOcean dongle.""" + from enocean.protocol.packet import Packet + packet = Packet(packet_type, data=data, optional=optional) + ENOCEAN_DONGLE.send_command(packet) diff --git a/homeassistant/components/enocean/binary_sensor.py b/homeassistant/components/enocean/binary_sensor.py new file mode 100644 index 0000000000000..1fde8c79e401d --- /dev/null +++ b/homeassistant/components/enocean/binary_sensor.py @@ -0,0 +1,85 @@ +"""Support for EnOcean binary sensors.""" +import logging + +import voluptuous as vol + +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA) +from homeassistant.components import enocean +from homeassistant.const import ( + CONF_NAME, CONF_ID, CONF_DEVICE_CLASS) +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['enocean'] +DEFAULT_NAME = 'EnOcean binary sensor' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Binary Sensor platform for EnOcean.""" + dev_id = config.get(CONF_ID) + devname = config.get(CONF_NAME) + device_class = config.get(CONF_DEVICE_CLASS) + + add_entities([EnOceanBinarySensor(dev_id, devname, device_class)]) + + +class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice): + """Representation of EnOcean binary sensors such as wall switches.""" + + def __init__(self, dev_id, devname, device_class): + """Initialize the EnOcean binary sensor.""" + enocean.EnOceanDevice.__init__(self) + self.stype = 'listener' + self.dev_id = dev_id + self.which = -1 + self.onoff = -1 + self.devname = devname + self._device_class = device_class + + @property + def name(self): + """Return the default name for the binary sensor.""" + return self.devname + + @property + def device_class(self): + """Return the class of this sensor.""" + return self._device_class + + def value_changed(self, value, value2): + """Fire an event with the data that have changed. + + This method is called when there is an incoming packet associated + with this platform. + """ + self.schedule_update_ha_state() + if value2 == 0x70: + self.which = 0 + self.onoff = 0 + elif value2 == 0x50: + self.which = 0 + self.onoff = 1 + elif value2 == 0x30: + self.which = 1 + self.onoff = 0 + elif value2 == 0x10: + self.which = 1 + self.onoff = 1 + elif value2 == 0x37: + self.which = 10 + self.onoff = 0 + elif value2 == 0x15: + self.which = 10 + self.onoff = 1 + self.hass.bus.fire('button_pressed', {'id': self.dev_id, + 'pushed': value, + 'which': self.which, + 'onoff': self.onoff}) diff --git a/homeassistant/components/enocean/light.py b/homeassistant/components/enocean/light.py new file mode 100644 index 0000000000000..f574f89f951f7 --- /dev/null +++ b/homeassistant/components/enocean/light.py @@ -0,0 +1,103 @@ +"""Support for EnOcean light sources.""" +import logging +import math + +import voluptuous as vol + +from homeassistant.components.light import ( + Light, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, PLATFORM_SCHEMA) +from homeassistant.const import (CONF_NAME, CONF_ID) +from homeassistant.components import enocean +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +CONF_SENDER_ID = 'sender_id' + +DEFAULT_NAME = 'EnOcean Light' +DEPENDENCIES = ['enocean'] + +SUPPORT_ENOCEAN = SUPPORT_BRIGHTNESS + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_ID, default=[]): + vol.All(cv.ensure_list, [vol.Coerce(int)]), + vol.Required(CONF_SENDER_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the EnOcean light platform.""" + sender_id = config.get(CONF_SENDER_ID) + devname = config.get(CONF_NAME) + dev_id = config.get(CONF_ID) + + add_entities([EnOceanLight(sender_id, devname, dev_id)]) + + +class EnOceanLight(enocean.EnOceanDevice, Light): + """Representation of an EnOcean light source.""" + + def __init__(self, sender_id, devname, dev_id): + """Initialize the EnOcean light source.""" + enocean.EnOceanDevice.__init__(self) + self._on_state = False + self._brightness = 50 + self._sender_id = sender_id + self.dev_id = dev_id + self._devname = devname + self.stype = 'dimmer' + + @property + def name(self): + """Return the name of the device if any.""" + return self._devname + + @property + def brightness(self): + """Brightness of the light. + + This method is optional. Removing it indicates to Home Assistant + that brightness is not supported for this light. + """ + return self._brightness + + @property + def is_on(self): + """If light is on.""" + return self._on_state + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_ENOCEAN + + def turn_on(self, **kwargs): + """Turn the light source on or sets a specific dimmer value.""" + brightness = kwargs.get(ATTR_BRIGHTNESS) + if brightness is not None: + self._brightness = brightness + + bval = math.floor(self._brightness / 256.0 * 100.0) + if bval == 0: + bval = 1 + command = [0xa5, 0x02, bval, 0x01, 0x09] + command.extend(self._sender_id) + command.extend([0x00]) + self.send_command(command, [], 0x01) + self._on_state = True + + def turn_off(self, **kwargs): + """Turn the light source off.""" + command = [0xa5, 0x02, 0x00, 0x01, 0x09] + command.extend(self._sender_id) + command.extend([0x00]) + self.send_command(command, [], 0x01) + self._on_state = False + + def value_changed(self, val): + """Update the internal state of this device.""" + self._brightness = math.floor(val / 100.0 * 256.0) + self._on_state = bool(val != 0) + self.schedule_update_ha_state() diff --git a/homeassistant/components/enocean/sensor.py b/homeassistant/components/enocean/sensor.py new file mode 100644 index 0000000000000..d2e88ed3825e7 --- /dev/null +++ b/homeassistant/components/enocean/sensor.py @@ -0,0 +1,62 @@ +"""Support for EnOcean sensors.""" +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import (CONF_NAME, CONF_ID) +from homeassistant.helpers.entity import Entity +import homeassistant.helpers.config_validation as cv +from homeassistant.components import enocean + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'EnOcean sensor' +DEPENDENCIES = ['enocean'] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up an EnOcean sensor device.""" + dev_id = config.get(CONF_ID) + devname = config.get(CONF_NAME) + + add_entities([EnOceanSensor(dev_id, devname)]) + + +class EnOceanSensor(enocean.EnOceanDevice, Entity): + """Representation of an EnOcean sensor device such as a power meter.""" + + def __init__(self, dev_id, devname): + """Initialize the EnOcean sensor device.""" + enocean.EnOceanDevice.__init__(self) + self.stype = "powersensor" + self.power = None + self.dev_id = dev_id + self.which = -1 + self.onoff = -1 + self.devname = devname + + @property + def name(self): + """Return the name of the device.""" + return 'Power %s' % self.devname + + def value_changed(self, value): + """Update the internal state of the device.""" + self.power = value + self.schedule_update_ha_state() + + @property + def state(self): + """Return the state of the device.""" + return self.power + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return 'W' diff --git a/homeassistant/components/enocean/switch.py b/homeassistant/components/enocean/switch.py new file mode 100644 index 0000000000000..4dfbafd36b16f --- /dev/null +++ b/homeassistant/components/enocean/switch.py @@ -0,0 +1,81 @@ +"""Support for EnOcean switches.""" +import logging + +import voluptuous as vol + +from homeassistant.components.switch import PLATFORM_SCHEMA +from homeassistant.const import (CONF_NAME, CONF_ID) +from homeassistant.components import enocean +from homeassistant.helpers.entity import ToggleEntity +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'EnOcean Switch' +DEPENDENCIES = ['enocean'] +CONF_CHANNEL = 'channel' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_CHANNEL, default=0): cv.positive_int, +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the EnOcean switch platform.""" + dev_id = config.get(CONF_ID) + devname = config.get(CONF_NAME) + channel = config.get(CONF_CHANNEL) + + add_entities([EnOceanSwitch(dev_id, devname, channel)]) + + +class EnOceanSwitch(enocean.EnOceanDevice, ToggleEntity): + """Representation of an EnOcean switch device.""" + + def __init__(self, dev_id, devname, channel): + """Initialize the EnOcean switch device.""" + enocean.EnOceanDevice.__init__(self) + self.dev_id = dev_id + self._devname = devname + self._light = None + self._on_state = False + self._on_state2 = False + self.channel = channel + self.stype = "switch" + + @property + def is_on(self): + """Return whether the switch is on or off.""" + return self._on_state + + @property + def name(self): + """Return the device name.""" + return self._devname + + def turn_on(self, **kwargs): + """Turn on the switch.""" + optional = [0x03, ] + optional.extend(self.dev_id) + optional.extend([0xff, 0x00]) + self.send_command(data=[0xD2, 0x01, self.channel & 0xFF, 0x64, 0x00, + 0x00, 0x00, 0x00, 0x00], optional=optional, + packet_type=0x01) + self._on_state = True + + def turn_off(self, **kwargs): + """Turn off the switch.""" + optional = [0x03, ] + optional.extend(self.dev_id) + optional.extend([0xff, 0x00]) + self.send_command(data=[0xD2, 0x01, self.channel & 0xFF, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00], optional=optional, + packet_type=0x01) + self._on_state = False + + def value_changed(self, val): + """Update the internal state of the switch.""" + self._on_state = val + self.schedule_update_ha_state() diff --git a/homeassistant/components/envisalink/__init__.py b/homeassistant/components/envisalink/__init__.py index 8b89b307db9e8..b7590341f788c 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/envisalink/alarm_control_panel.py b/homeassistant/components/envisalink/alarm_control_panel.py new file mode 100644 index 0000000000000..a4cc5864fc4c5 --- /dev/null +++ b/homeassistant/components/envisalink/alarm_control_panel.py @@ -0,0 +1,154 @@ +"""Support for Envisalink-based alarm control panels (Honeywell/DSC).""" +import logging + +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +import homeassistant.components.alarm_control_panel as alarm +import homeassistant.helpers.config_validation as cv +from homeassistant.components.envisalink import ( + DATA_EVL, EnvisalinkDevice, PARTITION_SCHEMA, CONF_CODE, CONF_PANIC, + CONF_PARTITIONNAME, SIGNAL_KEYPAD_UPDATE, SIGNAL_PARTITION_UPDATE) +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, + STATE_UNKNOWN, STATE_ALARM_TRIGGERED, STATE_ALARM_PENDING, ATTR_ENTITY_ID) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['envisalink'] + +SERVICE_ALARM_KEYPRESS = 'envisalink_alarm_keypress' +ATTR_KEYPRESS = 'keypress' +ALARM_KEYPRESS_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_KEYPRESS): cv.string +}) + + +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] + panic_type = discovery_info[CONF_PANIC] + + devices = [] + 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]) + devices.append(device) + + async_add_entities(devices) + + @callback + def alarm_keypress_handler(service): + """Map services to methods on Alarm.""" + entity_ids = service.data.get(ATTR_ENTITY_ID) + keypress = service.data.get(ATTR_KEYPRESS) + + target_devices = [device for device in devices + if device.entity_id in entity_ids] + + for device in target_devices: + device.async_alarm_keypress(keypress) + + hass.services.async_register( + alarm.DOMAIN, SERVICE_ALARM_KEYPRESS, alarm_keypress_handler, + schema=ALARM_KEYPRESS_SCHEMA) + + return True + + +class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): + """Representation of an Envisalink-based alarm panel.""" + + def __init__(self, hass, partition_number, alarm_name, code, panic_type, + info, controller): + """Initialize the alarm panel.""" + self._partition_number = partition_number + self._code = code + self._panic_type = panic_type + + _LOGGER.debug("Setting up alarm: %s", alarm_name) + super().__init__(alarm_name, info, controller) + + async def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect( + self.hass, SIGNAL_KEYPAD_UPDATE, self._update_callback) + async_dispatcher_connect( + self.hass, SIGNAL_PARTITION_UPDATE, self._update_callback) + + @callback + def _update_callback(self, partition): + """Update Home Assistant state, if needed.""" + if partition is None or int(partition) == self._partition_number: + self.async_schedule_update_ha_state() + + @property + def code_format(self): + """Regex for code format or None if no code is required.""" + if self._code: + return None + return alarm.FORMAT_NUMBER + + @property + def state(self): + """Return the state of the device.""" + state = STATE_UNKNOWN + + if self._info['status']['alarm']: + state = STATE_ALARM_TRIGGERED + elif self._info['status']['armed_away']: + state = STATE_ALARM_ARMED_AWAY + elif self._info['status']['armed_stay']: + state = STATE_ALARM_ARMED_HOME + elif self._info['status']['exit_delay']: + state = STATE_ALARM_PENDING + elif self._info['status']['entry_delay']: + state = STATE_ALARM_PENDING + elif self._info['status']['alpha']: + state = STATE_ALARM_DISARMED + return state + + async def async_alarm_disarm(self, code=None): + """Send disarm command.""" + if code: + self.hass.data[DATA_EVL].disarm_partition( + str(code), self._partition_number) + else: + self.hass.data[DATA_EVL].disarm_partition( + str(self._code), self._partition_number) + + async def async_alarm_arm_home(self, code=None): + """Send arm home command.""" + if code: + self.hass.data[DATA_EVL].arm_stay_partition( + str(code), self._partition_number) + else: + self.hass.data[DATA_EVL].arm_stay_partition( + str(self._code), self._partition_number) + + async def async_alarm_arm_away(self, code=None): + """Send arm away command.""" + if code: + self.hass.data[DATA_EVL].arm_away_partition( + str(code), self._partition_number) + else: + self.hass.data[DATA_EVL].arm_away_partition( + str(self._code), self._partition_number) + + async def async_alarm_trigger(self, code=None): + """Alarm trigger command. Will be used to trigger a panic alarm.""" + self.hass.data[DATA_EVL].panic_alarm(self._panic_type) + + @callback + def async_alarm_keypress(self, keypress=None): + """Send custom keypress.""" + if keypress: + self.hass.data[DATA_EVL].keypresses_to_partition( + self._partition_number, keypress) diff --git a/homeassistant/components/envisalink/binary_sensor.py b/homeassistant/components/envisalink/binary_sensor.py new file mode 100644 index 0000000000000..26b54e16cc8c2 --- /dev/null +++ b/homeassistant/components/envisalink/binary_sensor.py @@ -0,0 +1,96 @@ +"""Support for Envisalink zone states- represented as binary sensors.""" +import logging +import datetime + +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.envisalink import ( + DATA_EVL, ZONE_SCHEMA, CONF_ZONENAME, CONF_ZONETYPE, EnvisalinkDevice, + SIGNAL_ZONE_UPDATE) +from homeassistant.const import ATTR_LAST_TRIP_TIME +from homeassistant.util import dt as dt_util + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['envisalink'] + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the Envisalink binary sensor devices.""" + configured_zones = discovery_info['zones'] + + devices = [] + for zone_num in configured_zones: + device_config_data = ZONE_SCHEMA(configured_zones[zone_num]) + device = EnvisalinkBinarySensor( + hass, + zone_num, + device_config_data[CONF_ZONENAME], + device_config_data[CONF_ZONETYPE], + hass.data[DATA_EVL].alarm_state['zone'][zone_num], + hass.data[DATA_EVL] + ) + devices.append(device) + + async_add_entities(devices) + + +class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice): + """Representation of an Envisalink binary sensor.""" + + def __init__(self, hass, zone_number, zone_name, zone_type, info, + controller): + """Initialize the binary_sensor.""" + self._zone_type = zone_type + self._zone_number = zone_number + + _LOGGER.debug('Setting up zone: %s', zone_name) + super().__init__(zone_name, info, controller) + + async def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect( + self.hass, SIGNAL_ZONE_UPDATE, self._update_callback) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + attr = {} + + # The Envisalink library returns a "last_fault" value that's the + # number of seconds since the last fault, up to a maximum of 327680 + # seconds (65536 5-second ticks). + # + # We don't want the HA event log to fill up with a bunch of no-op + # "state changes" that are just that number ticking up once per poll + # interval, so we subtract it from the current second-accurate time + # unless it is already at the maximum value, in which case we set it + # to None since we can't determine the actual value. + seconds_ago = self._info['last_fault'] + if seconds_ago < 65536 * 5: + now = dt_util.now().replace(microsecond=0) + delta = datetime.timedelta(seconds=seconds_ago) + last_trip_time = (now - delta).isoformat() + else: + last_trip_time = None + + attr[ATTR_LAST_TRIP_TIME] = last_trip_time + return attr + + @property + def is_on(self): + """Return true if sensor is on.""" + return self._info['status']['open'] + + @property + def device_class(self): + """Return the class of this sensor, from DEVICE_CLASSES.""" + return self._zone_type + + @callback + def _update_callback(self, zone): + """Update the zone's state, if needed.""" + if zone is None or int(zone) == self._zone_number: + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/envisalink/sensor.py b/homeassistant/components/envisalink/sensor.py new file mode 100644 index 0000000000000..cc6a8b8723298 --- /dev/null +++ b/homeassistant/components/envisalink/sensor.py @@ -0,0 +1,72 @@ +"""Support for Envisalink sensors (shows panel info).""" +import logging + +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.components.envisalink import ( + DATA_EVL, PARTITION_SCHEMA, CONF_PARTITIONNAME, EnvisalinkDevice, + SIGNAL_KEYPAD_UPDATE, SIGNAL_PARTITION_UPDATE) +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['envisalink'] + + +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'] + + devices = [] + 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.data[DATA_EVL].alarm_state['partition'][part_num], + hass.data[DATA_EVL]) + + devices.append(device) + + async_add_entities(devices) + + +class EnvisalinkSensor(EnvisalinkDevice, Entity): + """Representation of an Envisalink keypad.""" + + def __init__(self, hass, partition_name, partition_number, info, + controller): + """Initialize the sensor.""" + self._icon = 'mdi:alarm' + self._partition_number = partition_number + + _LOGGER.debug("Setting up sensor for partition: %s", partition_name) + super().__init__(partition_name + ' Keypad', info, controller) + + async def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect( + self.hass, SIGNAL_KEYPAD_UPDATE, self._update_callback) + async_dispatcher_connect( + self.hass, SIGNAL_PARTITION_UPDATE, self._update_callback) + + @property + def icon(self): + """Return the icon if any.""" + return self._icon + + @property + def state(self): + """Return the overall state.""" + return self._info['status']['alpha'] + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._info['status'] + + @callback + def _update_callback(self, partition): + """Update the partition state in HA, if needed.""" + if partition is None or int(partition) == self._partition_number: + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/esphome/.translations/da.json b/homeassistant/components/esphome/.translations/da.json new file mode 100644 index 0000000000000..20224ec0d15bc --- /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 a021f1fe9f408..cebe6848f1bd5 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 70e21e1466680..ea1e25c3024df 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 0000000000000..94dafeb3c2e5c --- /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 1ff2c10c82887..004162341b110 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -30,9 +30,11 @@ 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 @@ '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 try_connect(tries: int = 0, is_disconnect: bool = True) -> None: # 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 1f71d8d66b5fd..e509455c12e43 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -55,11 +55,10 @@ async def async_step_user(self, user_input: Optional[ConfigType] = None, 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 @@ async def async_step_discovery(self, user_input: ConfigType): # 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.py deleted file mode 100644 index c1166f8cf7b97..0000000000000 --- a/homeassistant/components/eufy.py +++ /dev/null @@ -1,77 +0,0 @@ -""" -Support for Eufy devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/eufy/ -""" -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.helpers import discovery - -import homeassistant.helpers.config_validation as cv - - -REQUIREMENTS = ['lakeside==0.11'] - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'eufy' - -DEVICE_SCHEMA = vol.Schema({ - vol.Required(CONF_ADDRESS): cv.string, - vol.Required(CONF_ACCESS_TOKEN): cv.string, - vol.Required(CONF_TYPE): cv.string, - vol.Optional(CONF_NAME): cv.string -}) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.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, - }), -}, extra=vol.ALLOW_EXTRA) - -EUFY_DISPATCH = { - 'T1011': 'light', - 'T1012': 'light', - 'T1013': 'light', - 'T1201': 'switch', - 'T1202': 'switch', - 'T1203': 'switch', - 'T1211': 'switch' -} - - -def setup(hass, config): - """Set up Eufy devices.""" - import lakeside - - if CONF_USERNAME in config[DOMAIN] and CONF_PASSWORD in config[DOMAIN]: - data = lakeside.get_devices(config[DOMAIN][CONF_USERNAME], - config[DOMAIN][CONF_PASSWORD]) - for device in data: - kind = device['type'] - if kind not in EUFY_DISPATCH: - continue - discovery.load_platform(hass, EUFY_DISPATCH[kind], DOMAIN, device, - config) - - for device_info in config[DOMAIN][CONF_DEVICES]: - kind = device_info['type'] - if kind not in EUFY_DISPATCH: - continue - device = {} - device['address'] = device_info['address'] - device['code'] = device_info['access_token'] - device['type'] = device_info['type'] - device['name'] = device_info['name'] - discovery.load_platform(hass, EUFY_DISPATCH[kind], DOMAIN, device, - config) - - return True diff --git a/homeassistant/components/eufy/__init__.py b/homeassistant/components/eufy/__init__.py new file mode 100644 index 0000000000000..d5a0938bf66f3 --- /dev/null +++ b/homeassistant/components/eufy/__init__.py @@ -0,0 +1,71 @@ +"""Support for Eufy devices.""" +import logging + +import voluptuous as vol + +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__) + +DOMAIN = 'eufy' + +DEVICE_SCHEMA = vol.Schema({ + vol.Required(CONF_ADDRESS): cv.string, + vol.Required(CONF_ACCESS_TOKEN): cv.string, + vol.Required(CONF_TYPE): cv.string, + vol.Optional(CONF_NAME): cv.string +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.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, + }), +}, extra=vol.ALLOW_EXTRA) + +EUFY_DISPATCH = { + 'T1011': 'light', + 'T1012': 'light', + 'T1013': 'light', + 'T1201': 'switch', + 'T1202': 'switch', + 'T1203': 'switch', + 'T1211': 'switch' +} + + +def setup(hass, config): + """Set up Eufy devices.""" + import lakeside + + if CONF_USERNAME in config[DOMAIN] and CONF_PASSWORD in config[DOMAIN]: + data = lakeside.get_devices(config[DOMAIN][CONF_USERNAME], + config[DOMAIN][CONF_PASSWORD]) + for device in data: + kind = device['type'] + if kind not in EUFY_DISPATCH: + continue + discovery.load_platform(hass, EUFY_DISPATCH[kind], DOMAIN, device, + config) + + for device_info in config[DOMAIN][CONF_DEVICES]: + kind = device_info['type'] + if kind not in EUFY_DISPATCH: + continue + device = {} + device['address'] = device_info['address'] + device['code'] = device_info['access_token'] + device['type'] = device_info['type'] + device['name'] = device_info['name'] + discovery.load_platform(hass, EUFY_DISPATCH[kind], DOMAIN, device, + config) + + return True diff --git a/homeassistant/components/eufy/light.py b/homeassistant/components/eufy/light.py new file mode 100644 index 0000000000000..62bc058f1555a --- /dev/null +++ b/homeassistant/components/eufy/light.py @@ -0,0 +1,165 @@ +"""Support for Eufy lights.""" +import logging + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR_TEMP, SUPPORT_COLOR, Light) + +import homeassistant.util.color as color_util + +from homeassistant.util.color import ( + color_temperature_mired_to_kelvin as mired_to_kelvin, + color_temperature_kelvin_to_mired as kelvin_to_mired) + +DEPENDENCIES = ['eufy'] + +_LOGGER = logging.getLogger(__name__) + +EUFY_MAX_KELVIN = 6500 +EUFY_MIN_KELVIN = 2700 + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up Eufy bulbs.""" + if discovery_info is None: + return + add_entities([EufyLight(discovery_info)], True) + + +class EufyLight(Light): + """Representation of a Eufy light.""" + + def __init__(self, device): + """Initialize the light.""" + import lakeside + + self._temp = None + self._brightness = None + self._hs = None + self._state = None + self._name = device['name'] + self._address = device['address'] + self._code = device['code'] + self._type = device['type'] + self._bulb = lakeside.bulb(self._address, self._code, self._type) + self._colormode = False + if self._type == "T1011": + self._features = SUPPORT_BRIGHTNESS + elif self._type == "T1012": + self._features = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP + elif self._type == "T1013": + self._features = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | \ + SUPPORT_COLOR + self._bulb.connect() + + def update(self): + """Synchronise state from the bulb.""" + self._bulb.update() + if self._bulb.power: + self._brightness = self._bulb.brightness + self._temp = self._bulb.temperature + if self._bulb.colors: + self._colormode = True + self._hs = color_util.color_RGB_to_hs(*self._bulb.colors) + else: + self._colormode = False + self._state = self._bulb.power + + @property + def unique_id(self): + """Return the ID of this light.""" + return self._address + + @property + def name(self): + """Return the name of the device if any.""" + return self._name + + @property + def is_on(self): + """Return true if device is on.""" + return self._state + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + return int(self._brightness * 255 / 100) + + @property + def min_mireds(self): + """Return minimum supported color temperature.""" + return kelvin_to_mired(EUFY_MAX_KELVIN) + + @property + def max_mireds(self): + """Return maximu supported color temperature.""" + return kelvin_to_mired(EUFY_MIN_KELVIN) + + @property + def color_temp(self): + """Return the color temperature of this light.""" + temp_in_k = int(EUFY_MIN_KELVIN + (self._temp * + (EUFY_MAX_KELVIN - EUFY_MIN_KELVIN) + / 100)) + return kelvin_to_mired(temp_in_k) + + @property + def hs_color(self): + """Return the color of this light.""" + if not self._colormode: + return None + return self._hs + + @property + def supported_features(self): + """Flag supported features.""" + return self._features + + def turn_on(self, **kwargs): + """Turn the specified light on.""" + brightness = kwargs.get(ATTR_BRIGHTNESS) + colortemp = kwargs.get(ATTR_COLOR_TEMP) + # pylint: disable=invalid-name + hs = kwargs.get(ATTR_HS_COLOR) + + if brightness is not None: + brightness = int(brightness * 100 / 255) + else: + if self._brightness is None: + self._brightness = 100 + brightness = self._brightness + + if colortemp is not None: + self._colormode = False + temp_in_k = mired_to_kelvin(colortemp) + relative_temp = temp_in_k - EUFY_MIN_KELVIN + temp = int(relative_temp * 100 / + (EUFY_MAX_KELVIN - EUFY_MIN_KELVIN)) + else: + temp = None + + if hs is not None: + rgb = color_util.color_hsv_to_RGB( + hs[0], hs[1], brightness / 255 * 100) + self._colormode = True + elif self._colormode: + rgb = color_util.color_hsv_to_RGB( + self._hs[0], self._hs[1], brightness / 255 * 100) + else: + rgb = None + + try: + self._bulb.set_state(power=True, brightness=brightness, + temperature=temp, colors=rgb) + except BrokenPipeError: + self._bulb.connect() + self._bulb.set_state(power=True, brightness=brightness, + temperature=temp, colors=rgb) + + def turn_off(self, **kwargs): + """Turn the specified light off.""" + try: + self._bulb.set_state(power=False) + except BrokenPipeError: + self._bulb.connect() + self._bulb.set_state(power=False) diff --git a/homeassistant/components/eufy/switch.py b/homeassistant/components/eufy/switch.py new file mode 100644 index 0000000000000..96d6819410721 --- /dev/null +++ b/homeassistant/components/eufy/switch.py @@ -0,0 +1,67 @@ +"""Support for Eufy switches.""" +import logging + +from homeassistant.components.switch import SwitchDevice + +DEPENDENCIES = ['eufy'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up Eufy switches.""" + if discovery_info is None: + return + add_entities([EufySwitch(discovery_info)], True) + + +class EufySwitch(SwitchDevice): + """Representation of a Eufy switch.""" + + def __init__(self, device): + """Initialize the light.""" + import lakeside + + self._state = None + self._name = device['name'] + self._address = device['address'] + self._code = device['code'] + self._type = device['type'] + self._switch = lakeside.switch(self._address, self._code, self._type) + self._switch.connect() + + def update(self): + """Synchronise state from the switch.""" + self._switch.update() + self._state = self._switch.power + + @property + def unique_id(self): + """Return the ID of this light.""" + return self._address + + @property + def name(self): + """Return the name of the device if any.""" + return self._name + + @property + def is_on(self): + """Return true if device is on.""" + return self._state + + def turn_on(self, **kwargs): + """Turn the specified switch on.""" + try: + self._switch.set_state(True) + except BrokenPipeError: + self._switch.connect() + self._switch.set_state(power=True) + + def turn_off(self, **kwargs): + """Turn the specified switch off.""" + try: + self._switch.set_state(False) + except BrokenPipeError: + self._switch.connect() + self._switch.set_state(False) diff --git a/homeassistant/components/evohome.py b/homeassistant/components/evohome.py deleted file mode 100644 index 40ba5b9b70ff0..0000000000000 --- a/homeassistant/components/evohome.py +++ /dev/null @@ -1,163 +0,0 @@ -"""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/ -""" - -# 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 - -from requests.exceptions import HTTPError -import voluptuous as vol - -from homeassistant.const import ( - CONF_SCAN_INTERVAL, CONF_USERNAME, CONF_PASSWORD, - EVENT_HOMEASSISTANT_START, - HTTP_BAD_REQUEST, HTTP_SERVICE_UNAVAILABLE, HTTP_TOO_MANY_REQUESTS -) -from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import load_platform -from homeassistant.helpers.dispatcher import async_dispatcher_send - -REQUIREMENTS = ['evohomeclient==0.2.8'] - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'evohome' -DATA_EVOHOME = 'data_' + DOMAIN -DISPATCHER_EVOHOME = 'dispatcher_' + DOMAIN - -CONF_LOCATION_IDX = 'location_idx' -SCAN_INTERVAL_DEFAULT = timedelta(seconds=300) -SCAN_INTERVAL_MINIMUM = timedelta(seconds=180) - -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_SCAN_INTERVAL, default=SCAN_INTERVAL_DEFAULT): - vol.All(cv.time_period, vol.Range(min=SCAN_INTERVAL_MINIMUM)), - }), -}, extra=vol.ALLOW_EXTRA) - -# These are used to help prevent E501 (line too long) violations. -GWS = 'gateways' -TCS = 'temperatureControlSystems' - -# bit masks for dispatcher packets -EVO_PARENT = 0x01 -EVO_CHILD = 0x02 - - -def setup(hass, hass_config): - """Create a (EMEA/EU-based) Honeywell evohome system. - - Currently, only the Controller and the Zones are implemented here. - """ - evo_data = hass.data[DATA_EVOHOME] = {} - evo_data['timers'] = {} - - # use a copy, since scan_interval is rounded up to nearest 60s - evo_data['params'] = dict(hass_config[DOMAIN]) - scan_interval = evo_data['params'][CONF_SCAN_INTERVAL] - scan_interval = timedelta( - minutes=(scan_interval.total_seconds() + 59) // 60) - - from evohomeclient2 import EvohomeClient - - try: - client = EvohomeClient( - evo_data['params'][CONF_USERNAME], - evo_data['params'][CONF_PASSWORD], - debug=False - ) - - except HTTPError as err: - if err.response.status_code == HTTP_BAD_REQUEST: - _LOGGER.error( - "setup(): Failed to connect with the vendor's web servers. " - "Check your username (%s), and password are correct." - "Unable to continue. Resolve any errors and restart HA.", - evo_data['params'][CONF_USERNAME] - ) - - elif err.response.status_code == HTTP_SERVICE_UNAVAILABLE: - _LOGGER.error( - "setup(): Failed to connect with the vendor's web servers. " - "The server is not contactable. Unable to continue. " - "Resolve any errors and restart HA." - ) - - elif err.response.status_code == HTTP_TOO_MANY_REQUESTS: - _LOGGER.error( - "setup(): Failed to connect with the vendor's web servers. " - "You have exceeded the api rate limit. Unable to continue. " - "Wait a while (say 10 minutes) and restart HA." - ) - - else: - raise # we dont expect/handle any other HTTPErrors - - return False # unable to continue - - finally: # Redact username, password as no longer needed - evo_data['params'][CONF_USERNAME] = 'REDACTED' - evo_data['params'][CONF_PASSWORD] = 'REDACTED' - - evo_data['client'] = client - evo_data['status'] = {} - - # Redact any installation data we'll never need - for loc in client.installation_info: - loc['locationInfo']['locationId'] = 'REDACTED' - loc['locationInfo']['locationOwner'] = 'REDACTED' - loc['locationInfo']['streetAddress'] = 'REDACTED' - loc['locationInfo']['city'] = 'REDACTED' - loc[GWS][0]['gatewayInfo'] = 'REDACTED' - - # Pull down the installation configuration - loc_idx = evo_data['params'][CONF_LOCATION_IDX] - - try: - evo_data['config'] = client.installation_info[loc_idx] - 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 - - if _LOGGER.isEnabledFor(logging.DEBUG): - tmp_loc = dict(evo_data['config']) - tmp_loc['locationInfo']['postcode'] = 'REDACTED' - if 'dhw' in tmp_loc[GWS][0][TCS][0]: # if this location has DHW... - tmp_loc[GWS][0][TCS][0]['dhw'] = '...' - - _LOGGER.debug("setup(): evo_data['config']=%s", tmp_loc) - - load_platform(hass, 'climate', DOMAIN, {}, hass_config) - - @callback - def _first_update(event): - # When HA has started, the hub knows to retreive it's first update - pkt = {'sender': 'setup()', 'signal': 'refresh', 'to': EVO_PARENT} - async_dispatcher_send(hass, DISPATCHER_EVOHOME, pkt) - - hass.bus.listen(EVENT_HOMEASSISTANT_START, _first_update) - - return True diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py new file mode 100644 index 0000000000000..52bb77516e671 --- /dev/null +++ b/homeassistant/components/evohome/__init__.py @@ -0,0 +1,150 @@ +"""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 + +from requests.exceptions import HTTPError +import voluptuous as vol + +from homeassistant.const import ( + CONF_SCAN_INTERVAL, CONF_USERNAME, CONF_PASSWORD, + EVENT_HOMEASSISTANT_START, + HTTP_BAD_REQUEST, HTTP_SERVICE_UNAVAILABLE, HTTP_TOO_MANY_REQUESTS +) +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform +from homeassistant.helpers.dispatcher import async_dispatcher_send + +REQUIREMENTS = ['evohomeclient==0.2.8'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'evohome' +DATA_EVOHOME = 'data_' + DOMAIN +DISPATCHER_EVOHOME = 'dispatcher_' + DOMAIN + +CONF_LOCATION_IDX = 'location_idx' +SCAN_INTERVAL_DEFAULT = timedelta(seconds=300) +SCAN_INTERVAL_MINIMUM = timedelta(seconds=180) + +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_SCAN_INTERVAL, default=SCAN_INTERVAL_DEFAULT): + vol.All(cv.time_period, vol.Range(min=SCAN_INTERVAL_MINIMUM)), + }), +}, extra=vol.ALLOW_EXTRA) + +# These are used to help prevent E501 (line too long) violations. +GWS = 'gateways' +TCS = 'temperatureControlSystems' + +# bit masks for dispatcher packets +EVO_PARENT = 0x01 +EVO_CHILD = 0x02 + + +def setup(hass, hass_config): + """Create a (EMEA/EU-based) Honeywell evohome system. + + Currently, only the Controller and the Zones are implemented here. + """ + evo_data = hass.data[DATA_EVOHOME] = {} + evo_data['timers'] = {} + + # use a copy, since scan_interval is rounded up to nearest 60s + evo_data['params'] = dict(hass_config[DOMAIN]) + scan_interval = evo_data['params'][CONF_SCAN_INTERVAL] + scan_interval = timedelta( + minutes=(scan_interval.total_seconds() + 59) // 60) + + from evohomeclient2 import EvohomeClient + + try: + client = EvohomeClient( + evo_data['params'][CONF_USERNAME], + evo_data['params'][CONF_PASSWORD], + debug=False + ) + + except HTTPError as err: + if err.response.status_code == HTTP_BAD_REQUEST: + _LOGGER.error( + "setup(): Failed to connect with the vendor's web servers. " + "Check your username (%s), and password are correct." + "Unable to continue. Resolve any errors and restart HA.", + evo_data['params'][CONF_USERNAME] + ) + + elif err.response.status_code == HTTP_SERVICE_UNAVAILABLE: + _LOGGER.error( + "setup(): Failed to connect with the vendor's web servers. " + "The server is not contactable. Unable to continue. " + "Resolve any errors and restart HA." + ) + + elif err.response.status_code == HTTP_TOO_MANY_REQUESTS: + _LOGGER.error( + "setup(): Failed to connect with the vendor's web servers. " + "You have exceeded the api rate limit. Unable to continue. " + "Wait a while (say 10 minutes) and restart HA." + ) + + else: + raise # We don't expect/handle any other HTTPErrors + + return False + + finally: # Redact username, password as no longer needed + evo_data['params'][CONF_USERNAME] = 'REDACTED' + evo_data['params'][CONF_PASSWORD] = 'REDACTED' + + evo_data['client'] = client + evo_data['status'] = {} + + # Redact any installation data we'll never need + for loc in client.installation_info: + loc['locationInfo']['locationId'] = 'REDACTED' + loc['locationInfo']['locationOwner'] = 'REDACTED' + loc['locationInfo']['streetAddress'] = 'REDACTED' + loc['locationInfo']['city'] = 'REDACTED' + loc[GWS][0]['gatewayInfo'] = 'REDACTED' + + # Pull down the installation configuration + loc_idx = evo_data['params'][CONF_LOCATION_IDX] + + try: + evo_data['config'] = client.installation_info[loc_idx] + 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 + + if _LOGGER.isEnabledFor(logging.DEBUG): + tmp_loc = dict(evo_data['config']) + tmp_loc['locationInfo']['postcode'] = 'REDACTED' + if 'dhw' in tmp_loc[GWS][0][TCS][0]: # if this location has DHW... + tmp_loc[GWS][0][TCS][0]['dhw'] = '...' + + _LOGGER.debug("setup(): evo_data['config']=%s", tmp_loc) + + load_platform(hass, 'climate', DOMAIN, {}, hass_config) + + @callback + def _first_update(event): + """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) + + hass.bus.listen(EVENT_HOMEASSISTANT_START, _first_update) + + return True diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py new file mode 100644 index 0000000000000..ef82a3dc81cf1 --- /dev/null +++ b/homeassistant/components/evohome/climate.py @@ -0,0 +1,558 @@ +"""Support for Climate devices of (EMEA/EU-based) Honeywell evohome systems.""" +from datetime import datetime, timedelta +import logging + +from requests.exceptions import HTTPError + +from homeassistant.components.climate import ( + STATE_AUTO, STATE_ECO, STATE_MANUAL, STATE_OFF, + SUPPORT_AWAY_MODE, + SUPPORT_ON_OFF, + SUPPORT_OPERATION_MODE, + SUPPORT_TARGET_TEMPERATURE, + ClimateDevice +) +from homeassistant.components.evohome import ( + DATA_EVOHOME, DISPATCHER_EVOHOME, + CONF_LOCATION_IDX, SCAN_INTERVAL_DEFAULT, + EVO_PARENT, EVO_CHILD, + GWS, TCS, +) +from homeassistant.const import ( + CONF_SCAN_INTERVAL, + HTTP_TOO_MANY_REQUESTS, + PRECISION_HALVES, + TEMP_CELSIUS +) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import ( + dispatcher_send, + async_dispatcher_connect +) + +_LOGGER = logging.getLogger(__name__) + +# The Controller's opmode/state and the zone's (inherited) state +EVO_RESET = 'AutoWithReset' +EVO_AUTO = 'Auto' +EVO_AUTOECO = 'AutoWithEco' +EVO_AWAY = 'Away' +EVO_DAYOFF = 'DayOff' +EVO_CUSTOM = 'Custom' +EVO_HEATOFF = 'HeatingOff' + +# 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, +# where HA considers it to 'override' the exising operating mode +TCS_STATE_TO_HA = { + EVO_RESET: STATE_AUTO, + EVO_AUTO: STATE_AUTO, + EVO_AUTOECO: STATE_ECO, + EVO_AWAY: STATE_AUTO, + EVO_DAYOFF: STATE_AUTO, + EVO_CUSTOM: STATE_AUTO, + EVO_HEATOFF: STATE_OFF +} +HA_STATE_TO_TCS = { + STATE_AUTO: EVO_AUTO, + STATE_ECO: EVO_AUTOECO, + STATE_OFF: EVO_HEATOFF +} +TCS_OP_LIST = list(HA_STATE_TO_TCS) + +# the Zones' opmode; their state is usually 'inherited' from the TCS +EVO_FOLLOW = 'FollowSchedule' +EVO_TEMPOVER = 'TemporaryOverride' +EVO_PERMOVER = 'PermanentOverride' + +# for the Zones... +ZONE_STATE_TO_HA = { + EVO_FOLLOW: STATE_AUTO, + EVO_TEMPOVER: STATE_MANUAL, + EVO_PERMOVER: STATE_MANUAL +} +HA_STATE_TO_ZONE = { + STATE_AUTO: EVO_FOLLOW, + STATE_MANUAL: EVO_PERMOVER +} +ZONE_OP_LIST = list(HA_STATE_TO_ZONE) + + +async def async_setup_platform(hass, hass_config, async_add_entities, + discovery_info=None): + """Create the evohome Controller, and its Zones, if any.""" + evo_data = hass.data[DATA_EVOHOME] + + client = evo_data['client'] + loc_idx = evo_data['params'][CONF_LOCATION_IDX] + + # 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 + + _LOGGER.debug( + "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 = [] + + for zone_idx in tcs_obj_ref.zones: + zone_obj_ref = tcs_obj_ref.zones[zone_idx] + _LOGGER.debug( + "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 + + async_add_entities(entities, update_before_add=False) + + +class EvoClimateDevice(ClimateDevice): + """Base for a Honeywell evohome Climate device.""" + + # pylint: disable=no-member + + def __init__(self, evo_data, client, obj_ref): + """Initialize the evohome entity.""" + self._client = client + self._obj = obj_ref + + self._params = evo_data['params'] + self._timers = evo_data['timers'] + self._status = {} + + self._available = False # should become True after first update() + + async def async_added_to_hass(self): + """Run when entity about to be added.""" + async_dispatcher_connect(self.hass, DISPATCHER_EVOHOME, self._connect) + + @callback + def _connect(self, packet): + if packet['to'] & self._type and packet['signal'] == 'refresh': + self.async_schedule_update_ha_state(force_refresh=True) + + def _handle_requests_exceptions(self, err): + if err.response.status_code == HTTP_TOO_MANY_REQUESTS: + # execute a backoff: pause, and also reduce rate + old_interval = self._params[CONF_SCAN_INTERVAL] + new_interval = min(old_interval, SCAN_INTERVAL_DEFAULT) * 2 + self._params[CONF_SCAN_INTERVAL] = new_interval + + _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) + + self._timers['statusUpdated'] = datetime.now() + new_interval * 3 + + else: + raise err # we dont handle any other HTTPErrors + + @property + def name(self) -> str: + """Return the name to use in the frontend UI.""" + return self._name + + @property + def icon(self): + """Return the icon to use in the frontend UI.""" + return self._icon + + @property + def device_state_attributes(self): + """Return the device state attributes of the evohome Climate device. + + This is state data that is not available otherwise, due to the + restrictions placed upon ClimateDevice properties, etc. by HA. + """ + return {'status': self._status} + + @property + def available(self) -> bool: + """Return True if the device is currently available.""" + return self._available + + @property + def supported_features(self): + """Get the list of supported features of the device.""" + return self._supported_features + + @property + def operation_list(self): + """Return the list of available operations.""" + return self._operation_list + + @property + def temperature_unit(self): + """Return the temperature unit to use in the frontend UI.""" + return TEMP_CELSIUS + + @property + def precision(self): + """Return the temperature precision to use in the frontend UI.""" + return PRECISION_HALVES + + +class EvoZone(EvoClimateDevice): + """Base for a Honeywell evohome Zone device.""" + + def __init__(self, evo_data, client, obj_ref): + """Initialize the evohome Zone.""" + super().__init__(evo_data, client, obj_ref) + + self._id = obj_ref.zoneId + self._name = obj_ref.name + self._icon = "mdi:radiator" + self._type = EVO_CHILD + + for _zone in evo_data['config'][GWS][0][TCS][0]['zones']: + if _zone['zoneId'] == self._id: + self._config = _zone + break + self._status = {} + + self._operation_list = ZONE_OP_LIST + self._supported_features = \ + SUPPORT_OPERATION_MODE | \ + SUPPORT_TARGET_TEMPERATURE | \ + SUPPORT_ON_OFF + + @property + def min_temp(self): + """Return the minimum target temperature of a evohome Zone. + + The default is 5 (in Celsius), but it is configurable within 5-35. + """ + return self._config['setpointCapabilities']['minHeatSetpoint'] + + @property + def max_temp(self): + """Return the minimum target temperature of a evohome Zone. + + The default is 35 (in Celsius), but it is configurable within 5-35. + """ + return self._config['setpointCapabilities']['maxHeatSetpoint'] + + @property + def target_temperature(self): + """Return the target temperature of the evohome Zone.""" + return self._status['setpointStatus']['targetHeatTemperature'] + + @property + def current_temperature(self): + """Return the current temperature of the evohome Zone.""" + return self._status['temperatureStatus']['temperature'] + + @property + def current_operation(self): + """Return the current operating mode of the evohome Zone. + + The evohome Zones that are in 'FollowSchedule' mode inherit their + actual operating mode from the Controller. + """ + evo_data = self.hass.data[DATA_EVOHOME] + + system_mode = evo_data['status']['systemModeStatus']['mode'] + setpoint_mode = self._status['setpointStatus']['setpointMode'] + + if setpoint_mode == EVO_FOLLOW: + # then inherit state from the controller + if system_mode == EVO_RESET: + current_operation = TCS_STATE_TO_HA.get(EVO_AUTO) + else: + current_operation = TCS_STATE_TO_HA.get(system_mode) + else: + current_operation = ZONE_STATE_TO_HA.get(setpoint_mode) + + return current_operation + + @property + def is_on(self) -> bool: + """Return True if the evohome Zone is off. + + A Zone is considered off if its target temp is set to its minimum, and + it is not following its schedule (i.e. not in 'FollowSchedule' mode). + """ + is_off = \ + self.target_temperature == self.min_temp and \ + self._status['setpointStatus']['setpointMode'] == EVO_PERMOVER + return not is_off + + def _set_temperature(self, temperature, until=None): + """Set the new target temperature of a Zone. + + temperature is required, until can be: + - strftime('%Y-%m-%dT%H:%M:%SZ') for TemporaryOverride, or + - None for PermanentOverride (i.e. indefinitely) + """ + try: + self._obj.set_temperature(temperature, until) + except HTTPError as err: + self._handle_exception("HTTPError", str(err)) # noqa: E501; pylint: disable=no-member + + def set_temperature(self, **kwargs): + """Set new target temperature, indefinitely.""" + self._set_temperature(kwargs['temperature'], until=None) + + def turn_on(self): + """Turn the evohome Zone on. + + This is achieved by setting the Zone to its 'FollowSchedule' mode. + """ + self._set_operation_mode(EVO_FOLLOW) + + def turn_off(self): + """Turn the evohome Zone off. + + This is achieved by setting the Zone to its minimum temperature, + indefinitely (i.e. 'PermanentOverride' mode). + """ + self._set_temperature(self.min_temp, until=None) + + def set_operation_mode(self, operation_mode): + """Set an operating mode for a Zone. + + Currently limited to 'Auto' & 'Manual'. If 'Off' is needed, it can be + enabled via turn_off method. + + NB: evohome Zones do not have an operating mode as understood by HA. + Instead they usually 'inherit' an operating mode from their controller. + + More correctly, these Zones are in a follow mode, 'FollowSchedule', + where their setpoint temperatures are a function of their schedule, and + the Controller's operating_mode, e.g. Economy mode is their scheduled + setpoint less (usually) 3C. + + Thus, you cannot set a Zone to Away mode, but the location (i.e. the + Controller) is set to Away and each Zones's setpoints are adjusted + accordingly to some lower temperature. + + However, Zones can override these setpoints, either for a specified + period of time, 'TemporaryOverride', after which they will revert back + to 'FollowSchedule' mode, or indefinitely, 'PermanentOverride'. + """ + self._set_operation_mode(HA_STATE_TO_ZONE.get(operation_mode)) + + def _set_operation_mode(self, operation_mode): + if operation_mode == EVO_FOLLOW: + try: + self._obj.cancel_temp_override(self._obj) + except HTTPError as err: + self._handle_exception("HTTPError", str(err)) # noqa: E501; pylint: disable=no-member + + elif operation_mode == EVO_TEMPOVER: + _LOGGER.error( + "_set_operation_mode(op_mode=%s): mode not yet implemented", + operation_mode + ) + + elif operation_mode == EVO_PERMOVER: + self._set_temperature(self.target_temperature, until=None) + + else: + _LOGGER.error( + "_set_operation_mode(op_mode=%s): mode not valid", + operation_mode + ) + + @property + def should_poll(self) -> bool: + """Return False as evohome child devices should never be polled. + + The evohome Controller will inform its children when to update(). + """ + return False + + def update(self): + """Process the evohome Zone's state data.""" + evo_data = self.hass.data[DATA_EVOHOME] + + for _zone in evo_data['status']['zones']: + if _zone['zoneId'] == self._id: + self._status = _zone + break + + self._available = True + + +class EvoController(EvoClimateDevice): + """Base for a Honeywell evohome hub/Controller device. + + The Controller (aka TCS, temperature control system) is the parent of all + the child (CH/DHW) devices. It is also a Climate device. + """ + + def __init__(self, evo_data, client, obj_ref): + """Initialize the evohome Controller (hub).""" + super().__init__(evo_data, client, obj_ref) + + self._id = obj_ref.systemId + self._name = '_{}'.format(obj_ref.location.name) + self._icon = "mdi:thermostat" + self._type = EVO_PARENT + + self._config = evo_data['config'][GWS][0][TCS][0] + self._status = evo_data['status'] + self._timers['statusUpdated'] = datetime.min + + self._operation_list = TCS_OP_LIST + self._supported_features = \ + SUPPORT_OPERATION_MODE | \ + SUPPORT_AWAY_MODE + + @property + def device_state_attributes(self): + """Return the device state attributes of the evohome Controller. + + This is state data that is not available otherwise, due to the + restrictions placed upon ClimateDevice properties, etc. by HA. + """ + status = dict(self._status) + + if 'zones' in status: + del status['zones'] + if 'dhw' in status: + del status['dhw'] + + return {'status': status} + + @property + def current_operation(self): + """Return the current operating mode of the evohome Controller.""" + return TCS_STATE_TO_HA.get(self._status['systemModeStatus']['mode']) + + @property + def min_temp(self): + """Return the minimum target temperature of a evohome Controller. + + Although evohome Controllers do not have a minimum target temp, one is + expected by the HA schema; the default for an evohome HR92 is used. + """ + return 5 + + @property + def max_temp(self): + """Return the minimum target temperature of a evohome Controller. + + Although evohome Controllers do not have a maximum target temp, one is + expected by the HA schema; the default for an evohome HR92 is used. + """ + return 35 + + @property + def target_temperature(self): + """Return the average target temperature of the Heating/DHW zones. + + Although evohome Controllers do not have a target temp, one is + expected by the HA schema. + """ + temps = [zone['setpointStatus']['targetHeatTemperature'] + for zone in self._status['zones']] + + avg_temp = round(sum(temps) / len(temps), 1) if temps else None + return avg_temp + + @property + def current_temperature(self): + """Return the average current temperature of the Heating/DHW zones. + + Although evohome Controllers do not have a target temp, one is + expected by the HA schema. + """ + tmp_list = [x for x in self._status['zones'] + if x['temperatureStatus']['isAvailable'] is True] + temps = [zone['temperatureStatus']['temperature'] for zone in tmp_list] + + avg_temp = round(sum(temps) / len(temps), 1) if temps else None + return avg_temp + + @property + def is_on(self) -> bool: + """Return True as evohome Controllers are always on. + + For example, evohome Controllers have a 'HeatingOff' mode, but even + then the DHW would remain on. + """ + return True + + @property + def is_away_mode_on(self) -> bool: + """Return True if away mode is on.""" + return self._status['systemModeStatus']['mode'] == EVO_AWAY + + def turn_away_mode_on(self): + """Turn away mode on. + + The evohome Controller will not remember is previous operating mode. + """ + self._set_operation_mode(EVO_AWAY) + + def turn_away_mode_off(self): + """Turn away mode off. + + The evohome Controller can not recall its previous operating mode (as + intimated by the HA schema), so this method is achieved by setting the + Controller's mode back to Auto. + """ + self._set_operation_mode(EVO_AUTO) + + def _set_operation_mode(self, operation_mode): + try: + self._obj._set_status(operation_mode) # noqa: E501; pylint: disable=protected-access + except HTTPError as err: + self._handle_requests_exceptions(err) + + def set_operation_mode(self, operation_mode): + """Set new target operation mode for the TCS. + + Currently limited to 'Auto', 'AutoWithEco' & 'HeatingOff'. If 'Away' + mode is needed, it can be enabled via turn_away_mode_on method. + """ + self._set_operation_mode(HA_STATE_TO_TCS.get(operation_mode)) + + @property + def should_poll(self) -> bool: + """Return True as the evohome Controller should always be polled.""" + return True + + def update(self): + """Get the latest state data of the entire evohome Location. + + This includes state data for the Controller and all its child devices, + such as the operating mode of the Controller and the current temp of + its children (e.g. Zones, DHW controller). + """ + # should the latest evohome state data be retreived this cycle? + timeout = datetime.now() + timedelta(seconds=55) + expired = timeout > self._timers['statusUpdated'] + \ + self._params[CONF_SCAN_INTERVAL] + + if not expired: + return + + # Retrieve the latest state data via the client API + loc_idx = self._params[CONF_LOCATION_IDX] + + try: + self._status.update( + self._client.locations[loc_idx].status()[GWS][0][TCS][0]) + except HTTPError as err: # check if we've exceeded the api rate limit + self._handle_requests_exceptions(err) + else: + self._timers['statusUpdated'] = datetime.now() + self._available = True + + _LOGGER.debug("Status = %s", self._status) + + # inform the child devices that state data has been updated + pkt = {'sender': 'controller', 'signal': 'refresh', 'to': EVO_CHILD} + dispatcher_send(self.hass, DISPATCHER_EVOHOME, pkt) diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 3525b95c007ad..50d6802c4d241 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -16,7 +16,8 @@ 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/fan/comfoconnect.py b/homeassistant/components/fan/comfoconnect.py deleted file mode 100644 index 0e0ac8c80b64b..0000000000000 --- a/homeassistant/components/fan/comfoconnect.py +++ /dev/null @@ -1,116 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.components.comfoconnect import ( - DOMAIN, ComfoConnectBridge, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED) -from homeassistant.components.fan import ( - FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, - SUPPORT_SET_SPEED) -from homeassistant.helpers.dispatcher import (dispatcher_connect) - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['comfoconnect'] - -SPEED_MAPPING = { - 0: SPEED_OFF, - 1: SPEED_LOW, - 2: SPEED_MEDIUM, - 3: SPEED_HIGH -} - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the ComfoConnect fan platform.""" - ccb = hass.data[DOMAIN] - - add_entities([ComfoConnectFan(hass, name=ccb.name, ccb=ccb)], True) - - -class ComfoConnectFan(FanEntity): - """Representation of the ComfoConnect fan platform.""" - - def __init__(self, hass, name, ccb: ComfoConnectBridge) -> None: - """Initialize the ComfoConnect fan.""" - from pycomfoconnect import SENSOR_FAN_SPEED_MODE - - self._ccb = ccb - self._name = name - - # Ask the bridge to keep us updated - self._ccb.comfoconnect.register_sensor(SENSOR_FAN_SPEED_MODE) - - def _handle_update(var): - if var == SENSOR_FAN_SPEED_MODE: - _LOGGER.debug("Dispatcher update for %s", var) - self.schedule_update_ha_state() - - # Register for dispatcher updates - dispatcher_connect( - hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, _handle_update) - - @property - def name(self): - """Return the name of the fan.""" - return self._name - - @property - def icon(self): - """Return the icon to use in the frontend.""" - return 'mdi:air-conditioner' - - @property - def supported_features(self) -> int: - """Flag supported features.""" - return SUPPORT_SET_SPEED - - @property - def speed(self): - """Return the current fan mode.""" - from pycomfoconnect import (SENSOR_FAN_SPEED_MODE) - - try: - speed = self._ccb.data[SENSOR_FAN_SPEED_MODE] - return SPEED_MAPPING[speed] - except KeyError: - return None - - @property - def speed_list(self): - """List of available fan modes.""" - return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] - - def turn_on(self, speed: str = None, **kwargs) -> None: - """Turn on the fan.""" - if speed is None: - speed = SPEED_LOW - self.set_speed(speed) - - def turn_off(self, **kwargs) -> None: - """Turn off the fan (to away).""" - self.set_speed(SPEED_OFF) - - def set_speed(self, speed: str): - """Set fan speed.""" - _LOGGER.debug('Changing fan speed to %s.', speed) - - from pycomfoconnect import ( - CMD_FAN_MODE_AWAY, CMD_FAN_MODE_LOW, CMD_FAN_MODE_MEDIUM, - CMD_FAN_MODE_HIGH) - - if speed == SPEED_OFF: - self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_AWAY) - elif speed == SPEED_LOW: - self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_LOW) - elif speed == SPEED_MEDIUM: - self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_MEDIUM) - elif speed == SPEED_HIGH: - self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_HIGH) - - # Update current mode - self.schedule_update_ha_state() diff --git a/homeassistant/components/fan/tuya.py b/homeassistant/components/fan/tuya.py deleted file mode 100644 index 9cb7cdc3f2c8e..0000000000000 --- a/homeassistant/components/fan/tuya.py +++ /dev/null @@ -1,99 +0,0 @@ -""" -Support for Tuya fans. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/fan.tuya/ -""" - -from homeassistant.components.fan import ( - ENTITY_ID_FORMAT, FanEntity, SUPPORT_OSCILLATE, SUPPORT_SET_SPEED) -from homeassistant.components.tuya import DATA_TUYA, TuyaDevice -from homeassistant.const import STATE_OFF - -DEPENDENCIES = ['tuya'] - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Tuya fan platform.""" - if discovery_info is None: - return - tuya = hass.data[DATA_TUYA] - dev_ids = discovery_info.get('dev_ids') - devices = [] - for dev_id in dev_ids: - device = tuya.get_device_by_id(dev_id) - if device is None: - continue - devices.append(TuyaFanDevice(device)) - add_entities(devices) - - -class TuyaFanDevice(TuyaDevice, FanEntity): - """Tuya fan devices.""" - - def __init__(self, tuya): - """Init Tuya fan device.""" - super().__init__(tuya) - self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id()) - self.speeds = [STATE_OFF] - - async def async_added_to_hass(self): - """Create fan list when add to hass.""" - await super().async_added_to_hass() - self.speeds.extend(self.tuya.speed_list()) - - def set_speed(self, speed: str) -> None: - """Set the speed of the fan.""" - if speed == STATE_OFF: - self.turn_off() - else: - self.tuya.set_speed(speed) - - def turn_on(self, speed: str = None, **kwargs) -> None: - """Turn on the fan.""" - if speed is not None: - self.set_speed(speed) - else: - self.tuya.turn_on() - - def turn_off(self, **kwargs) -> None: - """Turn the entity off.""" - self.tuya.turn_off() - - def oscillate(self, oscillating) -> None: - """Oscillate the fan.""" - self.tuya.oscillate(oscillating) - - @property - def oscillating(self): - """Return current oscillating status.""" - if self.supported_features & SUPPORT_OSCILLATE == 0: - return None - if self.speed == STATE_OFF: - return False - return self.tuya.oscillating() - - @property - def is_on(self): - """Return true if the entity is on.""" - return self.tuya.state() - - @property - def speed(self) -> str: - """Return the current speed.""" - if self.is_on: - return self.tuya.speed() - return STATE_OFF - - @property - def speed_list(self) -> list: - """Get the list of available speeds.""" - return self.speeds - - @property - def supported_features(self) -> int: - """Flag supported features.""" - supports = SUPPORT_SET_SPEED - if self.tuya.support_oscillate(): - supports = supports | SUPPORT_OSCILLATE - return supports diff --git a/homeassistant/components/fan/wemo.py b/homeassistant/components/fan/wemo.py deleted file mode 100644 index fbf72185ac241..0000000000000 --- a/homeassistant/components/fan/wemo.py +++ /dev/null @@ -1,318 +0,0 @@ -""" -Support for WeMo humidifier. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/fan.wemo/ -""" -import asyncio -import logging -from datetime import timedelta - -import requests -import async_timeout -import voluptuous as vol -import homeassistant.helpers.config_validation as cv - -from homeassistant.components.fan import ( - DOMAIN, SUPPORT_SET_SPEED, FanEntity, - SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH) -from homeassistant.exceptions import PlatformNotReady -from homeassistant.const import ATTR_ENTITY_ID - -DEPENDENCIES = ['wemo'] -SCAN_INTERVAL = timedelta(seconds=10) -DATA_KEY = 'fan.wemo' - -_LOGGER = logging.getLogger(__name__) - -ATTR_CURRENT_HUMIDITY = 'current_humidity' -ATTR_TARGET_HUMIDITY = 'target_humidity' -ATTR_FAN_MODE = 'fan_mode' -ATTR_FILTER_LIFE = 'filter_life' -ATTR_FILTER_EXPIRED = 'filter_expired' -ATTR_WATER_LEVEL = 'water_level' - -# The WEMO_ constants below come from pywemo itself -WEMO_ON = 1 -WEMO_OFF = 0 - -WEMO_HUMIDITY_45 = 0 -WEMO_HUMIDITY_50 = 1 -WEMO_HUMIDITY_55 = 2 -WEMO_HUMIDITY_60 = 3 -WEMO_HUMIDITY_100 = 4 - -WEMO_FAN_OFF = 0 -WEMO_FAN_MINIMUM = 1 -WEMO_FAN_LOW = 2 # Not used due to limitations of the base fan implementation -WEMO_FAN_MEDIUM = 3 -WEMO_FAN_HIGH = 4 # Not used due to limitations of the base fan implementation -WEMO_FAN_MAXIMUM = 5 - -WEMO_WATER_EMPTY = 0 -WEMO_WATER_LOW = 1 -WEMO_WATER_GOOD = 2 - -SUPPORTED_SPEEDS = [ - SPEED_OFF, SPEED_LOW, - SPEED_MEDIUM, SPEED_HIGH] - -SUPPORTED_FEATURES = SUPPORT_SET_SPEED - -# Since the base fan object supports a set list of fan speeds, -# we have to reuse some of them when mapping to the 5 WeMo speeds -WEMO_FAN_SPEED_TO_HASS = { - WEMO_FAN_OFF: SPEED_OFF, - WEMO_FAN_MINIMUM: SPEED_LOW, - WEMO_FAN_LOW: SPEED_LOW, # Reusing SPEED_LOW - WEMO_FAN_MEDIUM: SPEED_MEDIUM, - WEMO_FAN_HIGH: SPEED_HIGH, # Reusing SPEED_HIGH - WEMO_FAN_MAXIMUM: SPEED_HIGH -} - -# Because we reused mappings in the previous dict, we have to filter them -# back out in this dict, or else we would have duplicate keys -HASS_FAN_SPEED_TO_WEMO = {v: k for (k, v) in WEMO_FAN_SPEED_TO_HASS.items() - if k not in [WEMO_FAN_LOW, WEMO_FAN_HIGH]} - -SERVICE_SET_HUMIDITY = 'wemo_set_humidity' - -SET_HUMIDITY_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_TARGET_HUMIDITY): - vol.All(vol.Coerce(float), vol.Range(min=0, max=100)) -}) - -SERVICE_RESET_FILTER_LIFE = 'wemo_reset_filter_life' - -RESET_FILTER_LIFE_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_ids -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up discovered WeMo humidifiers.""" - from pywemo import discovery - - if DATA_KEY not in hass.data: - hass.data[DATA_KEY] = {} - - if discovery_info is None: - return - - location = discovery_info['ssdp_description'] - mac = discovery_info['mac_address'] - - try: - device = WemoHumidifier( - discovery.device_from_description(location, mac)) - except (requests.exceptions.ConnectionError, - requests.exceptions.Timeout) as err: - _LOGGER.error('Unable to access %s (%s)', location, err) - raise PlatformNotReady - - hass.data[DATA_KEY][device.entity_id] = device - add_entities([device]) - - def service_handle(service): - """Handle the WeMo humidifier services.""" - entity_ids = service.data.get(ATTR_ENTITY_ID) - - humidifiers = [device for device in - hass.data[DATA_KEY].values() if - device.entity_id in entity_ids] - - if service.service == SERVICE_SET_HUMIDITY: - target_humidity = service.data.get(ATTR_TARGET_HUMIDITY) - - for humidifier in humidifiers: - humidifier.set_humidity(target_humidity) - elif service.service == SERVICE_RESET_FILTER_LIFE: - for humidifier in humidifiers: - humidifier.reset_filter_life() - - # Register service(s) - hass.services.register( - DOMAIN, SERVICE_SET_HUMIDITY, service_handle, - schema=SET_HUMIDITY_SCHEMA) - - hass.services.register( - DOMAIN, SERVICE_RESET_FILTER_LIFE, service_handle, - schema=RESET_FILTER_LIFE_SCHEMA) - - -class WemoHumidifier(FanEntity): - """Representation of a WeMo humidifier.""" - - def __init__(self, device): - """Initialize the WeMo switch.""" - self.wemo = device - self._state = None - self._available = True - self._update_lock = None - self._fan_mode = None - self._target_humidity = None - self._current_humidity = None - self._water_level = None - self._filter_life = None - self._filter_expired = None - self._last_fan_on_mode = WEMO_FAN_MEDIUM - self._model_name = self.wemo.model_name - self._name = self.wemo.name - self._serialnumber = self.wemo.serialnumber - - def _subscription_callback(self, _device, _type, _params): - """Update the state by the Wemo device.""" - _LOGGER.info("Subscription update for %s", self.name) - updated = self.wemo.subscription_update(_type, _params) - self.hass.add_job( - self._async_locked_subscription_callback(not updated)) - - async def _async_locked_subscription_callback(self, force_update): - """Handle an update from a subscription.""" - # If an update is in progress, we don't do anything - if self._update_lock.locked(): - return - - await self._async_locked_update(force_update) - self.async_schedule_update_ha_state() - - @property - def unique_id(self): - """Return the ID of this WeMo humidifier.""" - return self._serialnumber - - @property - def name(self): - """Return the name of the humidifier if any.""" - return self._name - - @property - def is_on(self): - """Return true if switch is on. Standby is on.""" - return self._state - - @property - def available(self): - """Return true if switch is available.""" - return self._available - - @property - def icon(self): - """Return the icon of device based on its type.""" - return 'mdi:water-percent' - - @property - def device_state_attributes(self): - """Return device specific state attributes.""" - return { - ATTR_CURRENT_HUMIDITY: self._current_humidity, - ATTR_TARGET_HUMIDITY: self._target_humidity, - ATTR_FAN_MODE: self._fan_mode, - ATTR_WATER_LEVEL: self._water_level, - ATTR_FILTER_LIFE: self._filter_life, - ATTR_FILTER_EXPIRED: self._filter_expired - } - - @property - def speed(self) -> str: - """Return the current speed.""" - return WEMO_FAN_SPEED_TO_HASS.get(self._fan_mode) - - @property - def speed_list(self) -> list: - """Get the list of available speeds.""" - return SUPPORTED_SPEEDS - - @property - def supported_features(self) -> int: - """Flag supported features.""" - return SUPPORTED_FEATURES - - async def async_added_to_hass(self): - """Wemo humidifier added to HASS.""" - # Define inside async context so we know our event loop - self._update_lock = asyncio.Lock() - - registry = self.hass.components.wemo.SUBSCRIPTION_REGISTRY - await self.hass.async_add_executor_job(registry.register, self.wemo) - registry.on(self.wemo, None, self._subscription_callback) - - async def async_update(self): - """Update WeMo state. - - Wemo has an aggressive retry logic that sometimes can take over a - minute to return. If we don't get a state after 5 seconds, assume the - Wemo humidifier is unreachable. If update goes through, it will be made - available again. - """ - # If an update is in progress, we don't do anything - if self._update_lock.locked(): - return - - try: - with async_timeout.timeout(5): - await asyncio.shield(self._async_locked_update(True)) - except asyncio.TimeoutError: - _LOGGER.warning('Lost connection to %s', self.name) - self._available = False - - async def _async_locked_update(self, force_update): - """Try updating within an async lock.""" - async with self._update_lock: - await self.hass.async_add_executor_job(self._update, force_update) - - def _update(self, force_update=True): - """Update the device state.""" - try: - self._state = self.wemo.get_state(force_update) - - self._fan_mode = self.wemo.fan_mode_string - self._target_humidity = self.wemo.desired_humidity_percent - self._current_humidity = self.wemo.current_humidity_percent - self._water_level = self.wemo.water_level_string - self._filter_life = self.wemo.filter_life_percent - self._filter_expired = self.wemo.filter_expired - - if self.wemo.fan_mode != WEMO_FAN_OFF: - self._last_fan_on_mode = self.wemo.fan_mode - - if not self._available: - _LOGGER.info('Reconnected to %s', self.name) - self._available = True - except AttributeError as err: - _LOGGER.warning("Could not update status for %s (%s)", - self.name, err) - self._available = False - - def turn_on(self, speed: str = None, **kwargs) -> None: - """Turn the switch on.""" - if speed is None: - self.wemo.set_state(self._last_fan_on_mode) - else: - self.set_speed(speed) - - def turn_off(self, **kwargs) -> None: - """Turn the switch off.""" - self.wemo.set_state(WEMO_FAN_OFF) - - def set_speed(self, speed: str) -> None: - """Set the fan_mode of the Humidifier.""" - self.wemo.set_state(HASS_FAN_SPEED_TO_WEMO.get(speed)) - - def set_humidity(self, humidity: float) -> None: - """Set the target humidity level for the Humidifier.""" - if humidity < 50: - self.wemo.set_humidity(WEMO_HUMIDITY_45) - elif 50 <= humidity < 55: - self.wemo.set_humidity(WEMO_HUMIDITY_50) - elif 55 <= humidity < 60: - self.wemo.set_humidity(WEMO_HUMIDITY_55) - elif 60 <= humidity < 100: - self.wemo.set_humidity(WEMO_HUMIDITY_60) - elif humidity >= 100: - self.wemo.set_humidity(WEMO_HUMIDITY_100) - - def reset_filter_life(self) -> None: - """Reset the filter life to 100%.""" - self.wemo.reset_filter_life() diff --git a/homeassistant/components/fan/wink.py b/homeassistant/components/fan/wink.py deleted file mode 100644 index eca985a8d1e9f..0000000000000 --- a/homeassistant/components/fan/wink.py +++ /dev/null @@ -1,101 +0,0 @@ -""" -Support for Wink fans. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/fan.wink/ -""" -import logging - -from homeassistant.components.fan import ( - SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, SUPPORT_DIRECTION, - SUPPORT_SET_SPEED, FanEntity) -from homeassistant.components.wink import DOMAIN, WinkDevice - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['wink'] - -SPEED_AUTO = 'auto' -SPEED_LOWEST = 'lowest' -SUPPORTED_FEATURES = SUPPORT_DIRECTION + SUPPORT_SET_SPEED - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Wink platform.""" - import pywink - - for fan in pywink.get_fans(): - if fan.object_id() + fan.name() not in hass.data[DOMAIN]['unique_ids']: - add_entities([WinkFanDevice(fan, hass)]) - - -class WinkFanDevice(WinkDevice, FanEntity): - """Representation of a Wink fan.""" - - async def async_added_to_hass(self): - """Call when entity is added to hass.""" - self.hass.data[DOMAIN]['entities']['fan'].append(self) - - def set_direction(self, direction: str) -> None: - """Set the direction of the fan.""" - self.wink.set_fan_direction(direction) - - def set_speed(self, speed: str) -> None: - """Set the speed of the fan.""" - self.wink.set_state(True, speed) - - def turn_on(self, speed: str = None, **kwargs) -> None: - """Turn on the fan.""" - self.wink.set_state(True, speed) - - def turn_off(self, **kwargs) -> None: - """Turn off the fan.""" - self.wink.set_state(False) - - @property - def is_on(self): - """Return true if the entity is on.""" - return self.wink.state() - - @property - def speed(self) -> str: - """Return the current speed.""" - current_wink_speed = self.wink.current_fan_speed() - if SPEED_AUTO == current_wink_speed: - return SPEED_AUTO - if SPEED_LOWEST == current_wink_speed: - return SPEED_LOWEST - if SPEED_LOW == current_wink_speed: - return SPEED_LOW - if SPEED_MEDIUM == current_wink_speed: - return SPEED_MEDIUM - if SPEED_HIGH == current_wink_speed: - return SPEED_HIGH - return None - - @property - def current_direction(self): - """Return direction of the fan [forward, reverse].""" - return self.wink.current_fan_direction() - - @property - def speed_list(self) -> list: - """Get the list of available speeds.""" - wink_supported_speeds = self.wink.fan_speeds() - supported_speeds = [] - if SPEED_AUTO in wink_supported_speeds: - supported_speeds.append(SPEED_AUTO) - if SPEED_LOWEST in wink_supported_speeds: - supported_speeds.append(SPEED_LOWEST) - if SPEED_LOW in wink_supported_speeds: - supported_speeds.append(SPEED_LOW) - if SPEED_MEDIUM in wink_supported_speeds: - supported_speeds.append(SPEED_MEDIUM) - if SPEED_HIGH in wink_supported_speeds: - supported_speeds.append(SPEED_HIGH) - return supported_speeds - - @property - def supported_features(self) -> int: - """Flag supported features.""" - return SUPPORTED_FEATURES diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py deleted file mode 100644 index 2e0b1657d234d..0000000000000 --- a/homeassistant/components/fan/xiaomi_miio.py +++ /dev/null @@ -1,1030 +0,0 @@ -""" -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/ -""" -import asyncio -from enum import Enum -from functools import partial -import logging - -import voluptuous as vol - -from homeassistant.components.fan import (FanEntity, PLATFORM_SCHEMA, - SUPPORT_SET_SPEED, DOMAIN, ) -from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN, - ATTR_ENTITY_ID, ) -from homeassistant.exceptions import PlatformNotReady -import homeassistant.helpers.config_validation as cv - -REQUIREMENTS = ['python-miio==0.4.4', 'construct==2.9.45'] - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = 'Xiaomi Miio Device' -DATA_KEY = 'fan.xiaomi_miio' - -CONF_MODEL = 'model' -MODEL_AIRPURIFIER_V1 = 'zhimi.airpurifier.v1' -MODEL_AIRPURIFIER_V2 = 'zhimi.airpurifier.v2' -MODEL_AIRPURIFIER_V3 = 'zhimi.airpurifier.v3' -MODEL_AIRPURIFIER_V5 = 'zhimi.airpurifier.v5' -MODEL_AIRPURIFIER_PRO = 'zhimi.airpurifier.v6' -MODEL_AIRPURIFIER_PRO_V7 = 'zhimi.airpurifier.v7' -MODEL_AIRPURIFIER_M1 = 'zhimi.airpurifier.m1' -MODEL_AIRPURIFIER_M2 = 'zhimi.airpurifier.m2' -MODEL_AIRPURIFIER_MA1 = 'zhimi.airpurifier.ma1' -MODEL_AIRPURIFIER_MA2 = 'zhimi.airpurifier.ma2' -MODEL_AIRPURIFIER_SA1 = 'zhimi.airpurifier.sa1' -MODEL_AIRPURIFIER_SA2 = 'zhimi.airpurifier.sa2' -MODEL_AIRPURIFIER_2S = 'zhimi.airpurifier.mc1' - -MODEL_AIRHUMIDIFIER_V1 = 'zhimi.humidifier.v1' -MODEL_AIRHUMIDIFIER_CA = 'zhimi.humidifier.ca1' - -MODEL_AIRFRESH_VA2 = 'zhimi.airfresh.va2' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_MODEL): vol.In( - [MODEL_AIRPURIFIER_V1, - MODEL_AIRPURIFIER_V2, - MODEL_AIRPURIFIER_V3, - MODEL_AIRPURIFIER_V5, - MODEL_AIRPURIFIER_PRO, - MODEL_AIRPURIFIER_PRO_V7, - MODEL_AIRPURIFIER_M1, - MODEL_AIRPURIFIER_M2, - MODEL_AIRPURIFIER_MA1, - MODEL_AIRPURIFIER_MA2, - MODEL_AIRPURIFIER_SA1, - MODEL_AIRPURIFIER_SA2, - MODEL_AIRPURIFIER_2S, - MODEL_AIRHUMIDIFIER_V1, - MODEL_AIRHUMIDIFIER_CA, - MODEL_AIRFRESH_VA2, - ]), -}) - -ATTR_MODEL = 'model' - -# Air Purifier -ATTR_TEMPERATURE = 'temperature' -ATTR_HUMIDITY = 'humidity' -ATTR_AIR_QUALITY_INDEX = 'aqi' -ATTR_MODE = 'mode' -ATTR_FILTER_HOURS_USED = 'filter_hours_used' -ATTR_FILTER_LIFE = 'filter_life_remaining' -ATTR_FAVORITE_LEVEL = 'favorite_level' -ATTR_BUZZER = 'buzzer' -ATTR_CHILD_LOCK = 'child_lock' -ATTR_LED = 'led' -ATTR_LED_BRIGHTNESS = 'led_brightness' -ATTR_MOTOR_SPEED = 'motor_speed' -ATTR_AVERAGE_AIR_QUALITY_INDEX = 'average_aqi' -ATTR_PURIFY_VOLUME = 'purify_volume' -ATTR_BRIGHTNESS = 'brightness' -ATTR_LEVEL = 'level' -ATTR_MOTOR2_SPEED = 'motor2_speed' -ATTR_ILLUMINANCE = 'illuminance' -ATTR_FILTER_RFID_PRODUCT_ID = 'filter_rfid_product_id' -ATTR_FILTER_RFID_TAG = 'filter_rfid_tag' -ATTR_FILTER_TYPE = 'filter_type' -ATTR_LEARN_MODE = 'learn_mode' -ATTR_SLEEP_TIME = 'sleep_time' -ATTR_SLEEP_LEARN_COUNT = 'sleep_mode_learn_count' -ATTR_EXTRA_FEATURES = 'extra_features' -ATTR_FEATURES = 'features' -ATTR_TURBO_MODE_SUPPORTED = 'turbo_mode_supported' -ATTR_AUTO_DETECT = 'auto_detect' -ATTR_SLEEP_MODE = 'sleep_mode' -ATTR_VOLUME = 'volume' -ATTR_USE_TIME = 'use_time' -ATTR_BUTTON_PRESSED = 'button_pressed' - -# Air Humidifier -ATTR_TARGET_HUMIDITY = 'target_humidity' -ATTR_TRANS_LEVEL = 'trans_level' -ATTR_HARDWARE_VERSION = 'hardware_version' - -# Air Humidifier CA -ATTR_MOTOR_SPEED = 'motor_speed' -ATTR_DEPTH = 'depth' -ATTR_DRY = 'dry' - -# Air Fresh -ATTR_CO2 = 'co2' - -# Map attributes to properties of the state object -AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON = { - ATTR_TEMPERATURE: 'temperature', - ATTR_HUMIDITY: 'humidity', - ATTR_AIR_QUALITY_INDEX: 'aqi', - ATTR_MODE: 'mode', - ATTR_FILTER_HOURS_USED: 'filter_hours_used', - ATTR_FILTER_LIFE: 'filter_life_remaining', - ATTR_FAVORITE_LEVEL: 'favorite_level', - ATTR_CHILD_LOCK: 'child_lock', - ATTR_LED: 'led', - ATTR_MOTOR_SPEED: 'motor_speed', - ATTR_AVERAGE_AIR_QUALITY_INDEX: 'average_aqi', - ATTR_LEARN_MODE: 'learn_mode', - ATTR_EXTRA_FEATURES: 'extra_features', - ATTR_TURBO_MODE_SUPPORTED: 'turbo_mode_supported', - ATTR_BUTTON_PRESSED: 'button_pressed', -} - -AVAILABLE_ATTRIBUTES_AIRPURIFIER = { - **AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON, - ATTR_PURIFY_VOLUME: 'purify_volume', - ATTR_SLEEP_TIME: 'sleep_time', - ATTR_SLEEP_LEARN_COUNT: 'sleep_mode_learn_count', - ATTR_AUTO_DETECT: 'auto_detect', - ATTR_USE_TIME: 'use_time', - ATTR_BUZZER: 'buzzer', - ATTR_LED_BRIGHTNESS: 'led_brightness', - ATTR_SLEEP_MODE: 'sleep_mode', -} - -AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO = { - **AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON, - ATTR_PURIFY_VOLUME: 'purify_volume', - ATTR_USE_TIME: 'use_time', - ATTR_FILTER_RFID_PRODUCT_ID: 'filter_rfid_product_id', - ATTR_FILTER_RFID_TAG: 'filter_rfid_tag', - ATTR_FILTER_TYPE: 'filter_type', - ATTR_ILLUMINANCE: 'illuminance', - ATTR_MOTOR2_SPEED: 'motor2_speed', - ATTR_VOLUME: 'volume', - # perhaps supported but unconfirmed - ATTR_AUTO_DETECT: 'auto_detect', - ATTR_SLEEP_TIME: 'sleep_time', - ATTR_SLEEP_LEARN_COUNT: 'sleep_mode_learn_count', -} - -AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO_V7 = { - **AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON, - ATTR_FILTER_RFID_PRODUCT_ID: 'filter_rfid_product_id', - ATTR_FILTER_RFID_TAG: 'filter_rfid_tag', - ATTR_FILTER_TYPE: 'filter_type', - ATTR_ILLUMINANCE: 'illuminance', - ATTR_MOTOR2_SPEED: 'motor2_speed', - ATTR_VOLUME: 'volume', -} - -AVAILABLE_ATTRIBUTES_AIRPURIFIER_2S = { - **AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON, - ATTR_BUZZER: 'buzzer', - ATTR_FILTER_RFID_PRODUCT_ID: 'filter_rfid_product_id', - ATTR_FILTER_RFID_TAG: 'filter_rfid_tag', - ATTR_FILTER_TYPE: 'filter_type', - ATTR_ILLUMINANCE: 'illuminance', -} - -AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3 = { - # Common set isn't used here. It's a very basic version of the device. - ATTR_AIR_QUALITY_INDEX: 'aqi', - ATTR_MODE: 'mode', - ATTR_LED: 'led', - ATTR_BUZZER: 'buzzer', - ATTR_CHILD_LOCK: 'child_lock', - ATTR_ILLUMINANCE: 'illuminance', - ATTR_FILTER_HOURS_USED: 'filter_hours_used', - ATTR_FILTER_LIFE: 'filter_life_remaining', - ATTR_MOTOR_SPEED: 'motor_speed', - # perhaps supported but unconfirmed - ATTR_AVERAGE_AIR_QUALITY_INDEX: 'average_aqi', - ATTR_VOLUME: 'volume', - ATTR_MOTOR2_SPEED: 'motor2_speed', - ATTR_FILTER_RFID_PRODUCT_ID: 'filter_rfid_product_id', - ATTR_FILTER_RFID_TAG: 'filter_rfid_tag', - ATTR_FILTER_TYPE: 'filter_type', - ATTR_PURIFY_VOLUME: 'purify_volume', - ATTR_LEARN_MODE: 'learn_mode', - ATTR_SLEEP_TIME: 'sleep_time', - ATTR_SLEEP_LEARN_COUNT: 'sleep_mode_learn_count', - ATTR_EXTRA_FEATURES: 'extra_features', - ATTR_AUTO_DETECT: 'auto_detect', - ATTR_USE_TIME: 'use_time', - ATTR_BUTTON_PRESSED: 'button_pressed', -} - -AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_COMMON = { - ATTR_TEMPERATURE: 'temperature', - ATTR_HUMIDITY: 'humidity', - ATTR_MODE: 'mode', - ATTR_BUZZER: 'buzzer', - ATTR_CHILD_LOCK: 'child_lock', - ATTR_TARGET_HUMIDITY: 'target_humidity', - ATTR_LED_BRIGHTNESS: 'led_brightness', - ATTR_USE_TIME: 'use_time', - ATTR_HARDWARE_VERSION: 'hardware_version', -} - -AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER = { - **AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_COMMON, - ATTR_TRANS_LEVEL: 'trans_level', - ATTR_BUTTON_PRESSED: 'button_pressed', -} - -AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA = { - **AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_COMMON, - ATTR_MOTOR_SPEED: 'speed', - ATTR_DEPTH: 'depth', - ATTR_DRY: 'dry', -} - -AVAILABLE_ATTRIBUTES_AIRFRESH = { - ATTR_TEMPERATURE: 'temperature', - ATTR_AIR_QUALITY_INDEX: 'aqi', - ATTR_AVERAGE_AIR_QUALITY_INDEX: 'average_aqi', - ATTR_CO2: 'co2', - ATTR_HUMIDITY: 'humidity', - ATTR_MODE: 'mode', - ATTR_LED: 'led', - ATTR_LED_BRIGHTNESS: 'led_brightness', - ATTR_BUZZER: 'buzzer', - ATTR_CHILD_LOCK: 'child_lock', - ATTR_FILTER_LIFE: 'filter_life_remaining', - ATTR_FILTER_HOURS_USED: 'filter_hours_used', - ATTR_USE_TIME: 'use_time', - ATTR_MOTOR_SPEED: 'motor_speed', - ATTR_EXTRA_FEATURES: 'extra_features', -} - -OPERATION_MODES_AIRPURIFIER = ['Auto', 'Silent', 'Favorite', 'Idle'] -OPERATION_MODES_AIRPURIFIER_PRO = ['Auto', 'Silent', 'Favorite'] -OPERATION_MODES_AIRPURIFIER_PRO_V7 = OPERATION_MODES_AIRPURIFIER_PRO -OPERATION_MODES_AIRPURIFIER_2S = ['Auto', 'Silent', 'Favorite'] -OPERATION_MODES_AIRPURIFIER_V3 = ['Auto', 'Silent', 'Favorite', 'Idle', - 'Medium', 'High', 'Strong'] -OPERATION_MODES_AIRFRESH = ['Auto', 'Silent', 'Interval', 'Low', - 'Middle', 'Strong'] - -SUCCESS = ['ok'] - -FEATURE_SET_BUZZER = 1 -FEATURE_SET_LED = 2 -FEATURE_SET_CHILD_LOCK = 4 -FEATURE_SET_LED_BRIGHTNESS = 8 -FEATURE_SET_FAVORITE_LEVEL = 16 -FEATURE_SET_AUTO_DETECT = 32 -FEATURE_SET_LEARN_MODE = 64 -FEATURE_SET_VOLUME = 128 -FEATURE_RESET_FILTER = 256 -FEATURE_SET_EXTRA_FEATURES = 512 -FEATURE_SET_TARGET_HUMIDITY = 1024 -FEATURE_SET_DRY = 2048 - -FEATURE_FLAGS_AIRPURIFIER = (FEATURE_SET_BUZZER | - FEATURE_SET_CHILD_LOCK | - FEATURE_SET_LED | - FEATURE_SET_LED_BRIGHTNESS | - FEATURE_SET_FAVORITE_LEVEL | - FEATURE_SET_LEARN_MODE | - FEATURE_RESET_FILTER | - FEATURE_SET_EXTRA_FEATURES) - -FEATURE_FLAGS_AIRPURIFIER_PRO = (FEATURE_SET_CHILD_LOCK | - FEATURE_SET_LED | - FEATURE_SET_FAVORITE_LEVEL | - FEATURE_SET_AUTO_DETECT | - FEATURE_SET_VOLUME) - -FEATURE_FLAGS_AIRPURIFIER_PRO_V7 = (FEATURE_SET_CHILD_LOCK | - FEATURE_SET_LED | - FEATURE_SET_FAVORITE_LEVEL | - FEATURE_SET_VOLUME) - -FEATURE_FLAGS_AIRPURIFIER_2S = (FEATURE_SET_BUZZER | - FEATURE_SET_CHILD_LOCK | - FEATURE_SET_LED | - FEATURE_SET_FAVORITE_LEVEL) - -FEATURE_FLAGS_AIRPURIFIER_V3 = (FEATURE_SET_BUZZER | - FEATURE_SET_CHILD_LOCK | - FEATURE_SET_LED) - -FEATURE_FLAGS_AIRHUMIDIFIER = (FEATURE_SET_BUZZER | - FEATURE_SET_CHILD_LOCK | - FEATURE_SET_LED_BRIGHTNESS | - FEATURE_SET_TARGET_HUMIDITY) - -FEATURE_FLAGS_AIRHUMIDIFIER_CA = (FEATURE_FLAGS_AIRHUMIDIFIER | - FEATURE_SET_DRY) - -FEATURE_FLAGS_AIRFRESH = (FEATURE_SET_BUZZER | - FEATURE_SET_CHILD_LOCK | - FEATURE_SET_LED | - FEATURE_SET_LED_BRIGHTNESS | - FEATURE_RESET_FILTER | - FEATURE_SET_EXTRA_FEATURES) - -SERVICE_SET_BUZZER_ON = 'xiaomi_miio_set_buzzer_on' -SERVICE_SET_BUZZER_OFF = 'xiaomi_miio_set_buzzer_off' -SERVICE_SET_LED_ON = 'xiaomi_miio_set_led_on' -SERVICE_SET_LED_OFF = 'xiaomi_miio_set_led_off' -SERVICE_SET_CHILD_LOCK_ON = 'xiaomi_miio_set_child_lock_on' -SERVICE_SET_CHILD_LOCK_OFF = 'xiaomi_miio_set_child_lock_off' -SERVICE_SET_LED_BRIGHTNESS = 'xiaomi_miio_set_led_brightness' -SERVICE_SET_FAVORITE_LEVEL = 'xiaomi_miio_set_favorite_level' -SERVICE_SET_AUTO_DETECT_ON = 'xiaomi_miio_set_auto_detect_on' -SERVICE_SET_AUTO_DETECT_OFF = 'xiaomi_miio_set_auto_detect_off' -SERVICE_SET_LEARN_MODE_ON = 'xiaomi_miio_set_learn_mode_on' -SERVICE_SET_LEARN_MODE_OFF = 'xiaomi_miio_set_learn_mode_off' -SERVICE_SET_VOLUME = 'xiaomi_miio_set_volume' -SERVICE_RESET_FILTER = 'xiaomi_miio_reset_filter' -SERVICE_SET_EXTRA_FEATURES = 'xiaomi_miio_set_extra_features' -SERVICE_SET_TARGET_HUMIDITY = 'xiaomi_miio_set_target_humidity' -SERVICE_SET_DRY_ON = 'xiaomi_miio_set_dry_on' -SERVICE_SET_DRY_OFF = 'xiaomi_miio_set_dry_off' - -AIRPURIFIER_SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, -}) - -SERVICE_SCHEMA_LED_BRIGHTNESS = AIRPURIFIER_SERVICE_SCHEMA.extend({ - vol.Required(ATTR_BRIGHTNESS): - vol.All(vol.Coerce(int), vol.Clamp(min=0, max=2)) -}) - -SERVICE_SCHEMA_FAVORITE_LEVEL = AIRPURIFIER_SERVICE_SCHEMA.extend({ - vol.Required(ATTR_LEVEL): - vol.All(vol.Coerce(int), vol.Clamp(min=0, max=16)) -}) - -SERVICE_SCHEMA_VOLUME = AIRPURIFIER_SERVICE_SCHEMA.extend({ - vol.Required(ATTR_VOLUME): - vol.All(vol.Coerce(int), vol.Clamp(min=0, max=100)) -}) - -SERVICE_SCHEMA_EXTRA_FEATURES = AIRPURIFIER_SERVICE_SCHEMA.extend({ - vol.Required(ATTR_FEATURES): - vol.All(vol.Coerce(int), vol.Range(min=0)) -}) - -SERVICE_SCHEMA_TARGET_HUMIDITY = AIRPURIFIER_SERVICE_SCHEMA.extend({ - vol.Required(ATTR_HUMIDITY): - vol.All(vol.Coerce(int), vol.In([30, 40, 50, 60, 70, 80])) -}) - -SERVICE_TO_METHOD = { - SERVICE_SET_BUZZER_ON: {'method': 'async_set_buzzer_on'}, - SERVICE_SET_BUZZER_OFF: {'method': 'async_set_buzzer_off'}, - SERVICE_SET_LED_ON: {'method': 'async_set_led_on'}, - SERVICE_SET_LED_OFF: {'method': 'async_set_led_off'}, - SERVICE_SET_CHILD_LOCK_ON: {'method': 'async_set_child_lock_on'}, - SERVICE_SET_CHILD_LOCK_OFF: {'method': 'async_set_child_lock_off'}, - SERVICE_SET_AUTO_DETECT_ON: {'method': 'async_set_auto_detect_on'}, - SERVICE_SET_AUTO_DETECT_OFF: {'method': 'async_set_auto_detect_off'}, - SERVICE_SET_LEARN_MODE_ON: {'method': 'async_set_learn_mode_on'}, - SERVICE_SET_LEARN_MODE_OFF: {'method': 'async_set_learn_mode_off'}, - SERVICE_RESET_FILTER: {'method': 'async_reset_filter'}, - SERVICE_SET_LED_BRIGHTNESS: { - 'method': 'async_set_led_brightness', - 'schema': SERVICE_SCHEMA_LED_BRIGHTNESS}, - SERVICE_SET_FAVORITE_LEVEL: { - 'method': 'async_set_favorite_level', - 'schema': SERVICE_SCHEMA_FAVORITE_LEVEL}, - SERVICE_SET_VOLUME: { - 'method': 'async_set_volume', - 'schema': SERVICE_SCHEMA_VOLUME}, - SERVICE_SET_EXTRA_FEATURES: { - 'method': 'async_set_extra_features', - 'schema': SERVICE_SCHEMA_EXTRA_FEATURES}, - SERVICE_SET_TARGET_HUMIDITY: { - 'method': 'async_set_target_humidity', - 'schema': SERVICE_SCHEMA_TARGET_HUMIDITY}, - SERVICE_SET_DRY_ON: {'method': 'async_set_dry_on'}, - SERVICE_SET_DRY_OFF: {'method': 'async_set_dry_off'}, -} - - -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up the miio fan device from config.""" - from miio import Device, DeviceException - if DATA_KEY not in hass.data: - hass.data[DATA_KEY] = {} - - host = config.get(CONF_HOST) - name = config.get(CONF_NAME) - token = config.get(CONF_TOKEN) - model = config.get(CONF_MODEL) - - _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) - unique_id = None - - if model is None: - try: - miio_device = Device(host, token) - device_info = miio_device.info() - model = device_info.model - unique_id = "{}-{}".format(model, device_info.mac_address) - _LOGGER.info("%s %s %s detected", - model, - device_info.firmware_version, - device_info.hardware_version) - except DeviceException: - raise PlatformNotReady - - if model.startswith('zhimi.airpurifier.'): - from miio import AirPurifier - air_purifier = AirPurifier(host, token) - device = XiaomiAirPurifier(name, air_purifier, model, unique_id) - elif model.startswith('zhimi.humidifier.'): - from miio import AirHumidifier - air_humidifier = AirHumidifier(host, token, model=model) - device = XiaomiAirHumidifier(name, air_humidifier, model, unique_id) - elif model.startswith('zhimi.airfresh.'): - from miio import AirFresh - air_fresh = AirFresh(host, token) - device = XiaomiAirFresh(name, air_fresh, model, unique_id) - else: - _LOGGER.error( - 'Unsupported device found! Please create an issue at ' - 'https://github.com/syssi/xiaomi_airpurifier/issues ' - 'and provide the following data: %s', model) - return False - - hass.data[DATA_KEY][host] = device - async_add_entities([device], update_before_add=True) - - async def async_service_handler(service): - """Map services to methods on XiaomiAirPurifier.""" - method = SERVICE_TO_METHOD.get(service.service) - params = {key: value for key, value in service.data.items() - if key != ATTR_ENTITY_ID} - entity_ids = service.data.get(ATTR_ENTITY_ID) - if entity_ids: - devices = [device for device in hass.data[DATA_KEY].values() if - device.entity_id in entity_ids] - else: - devices = hass.data[DATA_KEY].values() - - update_tasks = [] - for device in devices: - if not hasattr(device, method['method']): - continue - await getattr(device, method['method'])(**params) - update_tasks.append(device.async_update_ha_state(True)) - - if update_tasks: - await asyncio.wait(update_tasks, loop=hass.loop) - - for air_purifier_service in SERVICE_TO_METHOD: - schema = SERVICE_TO_METHOD[air_purifier_service].get( - 'schema', AIRPURIFIER_SERVICE_SCHEMA) - hass.services.async_register( - DOMAIN, air_purifier_service, async_service_handler, schema=schema) - - -class XiaomiGenericDevice(FanEntity): - """Representation of a generic Xiaomi device.""" - - def __init__(self, name, device, model, unique_id): - """Initialize the generic Xiaomi device.""" - self._name = name - self._device = device - self._model = model - self._unique_id = unique_id - - self._available = False - self._state = None - self._state_attrs = { - ATTR_MODEL: self._model, - } - self._device_features = FEATURE_SET_CHILD_LOCK - self._skip_update = False - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_SET_SPEED - - @property - def should_poll(self): - """Poll the device.""" - return True - - @property - def unique_id(self): - """Return an unique ID.""" - return self._unique_id - - @property - def name(self): - """Return the name of the device if any.""" - return self._name - - @property - def available(self): - """Return true when state is known.""" - return self._available - - @property - def device_state_attributes(self): - """Return the state attributes of the device.""" - return self._state_attrs - - @property - def is_on(self): - """Return true if device is on.""" - return self._state - - @staticmethod - def _extract_value_from_attribute(state, attribute): - value = getattr(state, attribute) - if isinstance(value, Enum): - return value.value - - return value - - async def _try_command(self, mask_error, func, *args, **kwargs): - """Call a miio device command handling error messages.""" - from miio import DeviceException - try: - result = await self.hass.async_add_executor_job( - partial(func, *args, **kwargs)) - - _LOGGER.debug("Response received from miio device: %s", result) - - return result == SUCCESS - except DeviceException as exc: - _LOGGER.error(mask_error, exc) - self._available = False - return False - - async def async_turn_on(self, speed: str = None, - **kwargs) -> None: - """Turn the device on.""" - if speed: - # If operation mode was set the device must not be turned on. - result = await self.async_set_speed(speed) - else: - result = await self._try_command( - "Turning the miio device on failed.", self._device.on) - - if result: - self._state = True - self._skip_update = True - - async def async_turn_off(self, **kwargs) -> None: - """Turn the device off.""" - result = await self._try_command( - "Turning the miio device off failed.", self._device.off) - - if result: - self._state = False - self._skip_update = True - - async def async_set_buzzer_on(self): - """Turn the buzzer on.""" - if self._device_features & FEATURE_SET_BUZZER == 0: - return - - await self._try_command( - "Turning the buzzer of the miio device on failed.", - self._device.set_buzzer, True) - - async def async_set_buzzer_off(self): - """Turn the buzzer off.""" - if self._device_features & FEATURE_SET_BUZZER == 0: - return - - await self._try_command( - "Turning the buzzer of the miio device off failed.", - self._device.set_buzzer, False) - - async def async_set_child_lock_on(self): - """Turn the child lock on.""" - if self._device_features & FEATURE_SET_CHILD_LOCK == 0: - return - - await self._try_command( - "Turning the child lock of the miio device on failed.", - self._device.set_child_lock, True) - - async def async_set_child_lock_off(self): - """Turn the child lock off.""" - if self._device_features & FEATURE_SET_CHILD_LOCK == 0: - return - - await self._try_command( - "Turning the child lock of the miio device off failed.", - self._device.set_child_lock, False) - - -class XiaomiAirPurifier(XiaomiGenericDevice): - """Representation of a Xiaomi Air Purifier.""" - - def __init__(self, name, device, model, unique_id): - """Initialize the plug switch.""" - super().__init__(name, device, model, unique_id) - - if self._model == MODEL_AIRPURIFIER_PRO: - self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO - self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO - self._speed_list = OPERATION_MODES_AIRPURIFIER_PRO - elif self._model == MODEL_AIRPURIFIER_PRO_V7: - self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO_V7 - self._available_attributes = \ - AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO_V7 - self._speed_list = OPERATION_MODES_AIRPURIFIER_PRO_V7 - elif self._model == MODEL_AIRPURIFIER_2S: - self._device_features = FEATURE_FLAGS_AIRPURIFIER_2S - self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_2S - self._speed_list = OPERATION_MODES_AIRPURIFIER_2S - elif self._model == MODEL_AIRPURIFIER_V3: - self._device_features = FEATURE_FLAGS_AIRPURIFIER_V3 - self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3 - self._speed_list = OPERATION_MODES_AIRPURIFIER_V3 - else: - self._device_features = FEATURE_FLAGS_AIRPURIFIER - self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER - self._speed_list = OPERATION_MODES_AIRPURIFIER - - self._state_attrs.update( - {attribute: None for attribute in self._available_attributes}) - - async def async_update(self): - """Fetch state from the device.""" - from miio import DeviceException - - # On state change the device doesn't provide the new state immediately. - if self._skip_update: - self._skip_update = False - return - - try: - state = await self.hass.async_add_executor_job( - self._device.status) - _LOGGER.debug("Got new state: %s", state) - - self._available = True - self._state = state.is_on - self._state_attrs.update( - {key: self._extract_value_from_attribute(state, value) for - key, value in self._available_attributes.items()}) - - except DeviceException as ex: - self._available = False - _LOGGER.error("Got exception while fetching the state: %s", ex) - - @property - def speed_list(self) -> list: - """Get the list of available speeds.""" - return self._speed_list - - @property - def speed(self): - """Return the current speed.""" - if self._state: - from miio.airpurifier import OperationMode - - return OperationMode(self._state_attrs[ATTR_MODE]).name - - return None - - async def async_set_speed(self, speed: str) -> None: - """Set the speed of the fan.""" - if self.supported_features & SUPPORT_SET_SPEED == 0: - return - - from miio.airpurifier import OperationMode - - _LOGGER.debug("Setting the operation mode to: %s", speed) - - await self._try_command( - "Setting operation mode of the miio device failed.", - self._device.set_mode, OperationMode[speed.title()]) - - async def async_set_led_on(self): - """Turn the led on.""" - if self._device_features & FEATURE_SET_LED == 0: - return - - await self._try_command( - "Turning the led of the miio device off failed.", - self._device.set_led, True) - - async def async_set_led_off(self): - """Turn the led off.""" - if self._device_features & FEATURE_SET_LED == 0: - return - - await self._try_command( - "Turning the led of the miio device off failed.", - self._device.set_led, False) - - async def async_set_led_brightness(self, brightness: int = 2): - """Set the led brightness.""" - if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0: - return - - from miio.airpurifier import LedBrightness - - await self._try_command( - "Setting the led brightness of the miio device failed.", - self._device.set_led_brightness, LedBrightness(brightness)) - - async def async_set_favorite_level(self, level: int = 1): - """Set the favorite level.""" - if self._device_features & FEATURE_SET_FAVORITE_LEVEL == 0: - return - - await self._try_command( - "Setting the favorite level of the miio device failed.", - self._device.set_favorite_level, level) - - async def async_set_auto_detect_on(self): - """Turn the auto detect on.""" - if self._device_features & FEATURE_SET_AUTO_DETECT == 0: - return - - await self._try_command( - "Turning the auto detect of the miio device on failed.", - self._device.set_auto_detect, True) - - async def async_set_auto_detect_off(self): - """Turn the auto detect off.""" - if self._device_features & FEATURE_SET_AUTO_DETECT == 0: - return - - await self._try_command( - "Turning the auto detect of the miio device off failed.", - self._device.set_auto_detect, False) - - async def async_set_learn_mode_on(self): - """Turn the learn mode on.""" - if self._device_features & FEATURE_SET_LEARN_MODE == 0: - return - - await self._try_command( - "Turning the learn mode of the miio device on failed.", - self._device.set_learn_mode, True) - - async def async_set_learn_mode_off(self): - """Turn the learn mode off.""" - if self._device_features & FEATURE_SET_LEARN_MODE == 0: - return - - await self._try_command( - "Turning the learn mode of the miio device off failed.", - self._device.set_learn_mode, False) - - async def async_set_volume(self, volume: int = 50): - """Set the sound volume.""" - if self._device_features & FEATURE_SET_VOLUME == 0: - return - - await self._try_command( - "Setting the sound volume of the miio device failed.", - self._device.set_volume, volume) - - async def async_set_extra_features(self, features: int = 1): - """Set the extra features.""" - if self._device_features & FEATURE_SET_EXTRA_FEATURES == 0: - return - - await self._try_command( - "Setting the extra features of the miio device failed.", - self._device.set_extra_features, features) - - async def async_reset_filter(self): - """Reset the filter lifetime and usage.""" - if self._device_features & FEATURE_RESET_FILTER == 0: - return - - await self._try_command( - "Resetting the filter lifetime of the miio device failed.", - self._device.reset_filter) - - -class XiaomiAirHumidifier(XiaomiGenericDevice): - """Representation of a Xiaomi Air Humidifier.""" - - def __init__(self, name, device, model, unique_id): - """Initialize the plug switch.""" - from miio.airhumidifier import OperationMode - - super().__init__(name, device, model, unique_id) - - if self._model == MODEL_AIRHUMIDIFIER_CA: - self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA - self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA - self._speed_list = [mode.name for mode in OperationMode if - mode is not OperationMode.Strong] - else: - self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER - self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER - self._speed_list = [mode.name for mode in OperationMode if - mode is not OperationMode.Auto] - - self._state_attrs.update( - {attribute: None for attribute in self._available_attributes}) - - async def async_update(self): - """Fetch state from the device.""" - from miio import DeviceException - - # On state change the device doesn't provide the new state immediately. - if self._skip_update: - self._skip_update = False - return - - try: - state = await self.hass.async_add_executor_job(self._device.status) - _LOGGER.debug("Got new state: %s", state) - - self._available = True - self._state = state.is_on - self._state_attrs.update( - {key: self._extract_value_from_attribute(state, value) for - key, value in self._available_attributes.items()}) - - except DeviceException as ex: - self._available = False - _LOGGER.error("Got exception while fetching the state: %s", ex) - - @property - def speed_list(self) -> list: - """Get the list of available speeds.""" - return self._speed_list - - @property - def speed(self): - """Return the current speed.""" - if self._state: - from miio.airhumidifier import OperationMode - - return OperationMode(self._state_attrs[ATTR_MODE]).name - - return None - - async def async_set_speed(self, speed: str) -> None: - """Set the speed of the fan.""" - if self.supported_features & SUPPORT_SET_SPEED == 0: - return - - from miio.airhumidifier import OperationMode - - _LOGGER.debug("Setting the operation mode to: %s", speed) - - await self._try_command( - "Setting operation mode of the miio device failed.", - self._device.set_mode, OperationMode[speed.title()]) - - async def async_set_led_brightness(self, brightness: int = 2): - """Set the led brightness.""" - if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0: - return - - from miio.airhumidifier import LedBrightness - - await self._try_command( - "Setting the led brightness of the miio device failed.", - self._device.set_led_brightness, LedBrightness(brightness)) - - async def async_set_target_humidity(self, humidity: int = 40): - """Set the target humidity.""" - if self._device_features & FEATURE_SET_TARGET_HUMIDITY == 0: - return - - await self._try_command( - "Setting the target humidity of the miio device failed.", - self._device.set_target_humidity, humidity) - - async def async_set_dry_on(self): - """Turn the dry mode on.""" - if self._device_features & FEATURE_SET_DRY == 0: - return - - await self._try_command( - "Turning the dry mode of the miio device off failed.", - self._device.set_dry, True) - - async def async_set_dry_off(self): - """Turn the dry mode off.""" - if self._device_features & FEATURE_SET_DRY == 0: - return - - await self._try_command( - "Turning the dry mode of the miio device off failed.", - self._device.set_dry, False) - - -class XiaomiAirFresh(XiaomiGenericDevice): - """Representation of a Xiaomi Air Fresh.""" - - def __init__(self, name, device, model, unique_id): - """Initialize the miio device.""" - super().__init__(name, device, model, unique_id) - - self._device_features = FEATURE_FLAGS_AIRFRESH - self._available_attributes = AVAILABLE_ATTRIBUTES_AIRFRESH - self._speed_list = OPERATION_MODES_AIRFRESH - self._state_attrs.update( - {attribute: None for attribute in self._available_attributes}) - - async def async_update(self): - """Fetch state from the device.""" - from miio import DeviceException - - # On state change the device doesn't provide the new state immediately. - if self._skip_update: - self._skip_update = False - return - - try: - state = await self.hass.async_add_executor_job( - self._device.status) - _LOGGER.debug("Got new state: %s", state) - - self._available = True - self._state = state.is_on - self._state_attrs.update( - {key: self._extract_value_from_attribute(state, value) for - key, value in self._available_attributes.items()}) - - except DeviceException as ex: - self._available = False - _LOGGER.error("Got exception while fetching the state: %s", ex) - - @property - def speed_list(self) -> list: - """Get the list of available speeds.""" - return self._speed_list - - @property - def speed(self): - """Return the current speed.""" - if self._state: - from miio.airfresh import OperationMode - - return OperationMode(self._state_attrs[ATTR_MODE]).name - - return None - - async def async_set_speed(self, speed: str) -> None: - """Set the speed of the fan.""" - if self.supported_features & SUPPORT_SET_SPEED == 0: - return - - from miio.airfresh import OperationMode - - _LOGGER.debug("Setting the operation mode to: %s", speed) - - await self._try_command( - "Setting operation mode of the miio device failed.", - self._device.set_mode, OperationMode[speed.title()]) - - async def async_set_led_on(self): - """Turn the led on.""" - if self._device_features & FEATURE_SET_LED == 0: - return - - await self._try_command( - "Turning the led of the miio device off failed.", - self._device.set_led, True) - - async def async_set_led_off(self): - """Turn the led off.""" - if self._device_features & FEATURE_SET_LED == 0: - return - - await self._try_command( - "Turning the led of the miio device off failed.", - self._device.set_led, False) - - async def async_set_led_brightness(self, brightness: int = 2): - """Set the led brightness.""" - if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0: - return - - from miio.airfresh import LedBrightness - - await self._try_command( - "Setting the led brightness of the miio device failed.", - self._device.set_led_brightness, LedBrightness(brightness)) - - async def async_set_extra_features(self, features: int = 1): - """Set the extra features.""" - if self._device_features & FEATURE_SET_EXTRA_FEATURES == 0: - return - - await self._try_command( - "Setting the extra features of the miio device failed.", - self._device.set_extra_features, features) - - async def async_reset_filter(self): - """Reset the filter lifetime and usage.""" - if self._device_features & FEATURE_RESET_FILTER == 0: - return - - await self._try_command( - "Resetting the filter lifetime of the miio device failed.", - self._device.reset_filter) diff --git a/homeassistant/components/fan/zwave.py b/homeassistant/components/fan/zwave.py deleted file mode 100644 index 4b4204aa454f1..0000000000000 --- a/homeassistant/components/fan/zwave.py +++ /dev/null @@ -1,103 +0,0 @@ -""" -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/ -""" -import logging -import math - -from homeassistant.core import callback -from homeassistant.components.fan import ( - DOMAIN, FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, - SUPPORT_SET_SPEED) -from homeassistant.components import zwave -from homeassistant.helpers.dispatcher import async_dispatcher_connect - -_LOGGER = logging.getLogger(__name__) - -SPEED_LIST = [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] - -SUPPORTED_FEATURES = SUPPORT_SET_SPEED - -# Value will first be divided to an integer -VALUE_TO_SPEED = { - 0: SPEED_OFF, - 1: SPEED_LOW, - 2: SPEED_MEDIUM, - 3: SPEED_HIGH, -} - -SPEED_TO_VALUE = { - SPEED_OFF: 0, - SPEED_LOW: 1, - SPEED_MEDIUM: 50, - SPEED_HIGH: 99, -} - - -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Old method of setting up Z-Wave fans.""" - pass - - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up Z-Wave Fan from Config Entry.""" - @callback - def async_add_fan(fan): - """Add Z-Wave Fan.""" - async_add_entities([fan]) - - async_dispatcher_connect(hass, 'zwave_new_fan', async_add_fan) - - -def get_device(values, **kwargs): - """Create Z-Wave entity device.""" - return ZwaveFan(values) - - -class ZwaveFan(zwave.ZWaveDeviceEntity, FanEntity): - """Representation of a Z-Wave fan.""" - - def __init__(self, values): - """Initialize the Z-Wave fan device.""" - zwave.ZWaveDeviceEntity.__init__(self, values, DOMAIN) - self.update_properties() - - def update_properties(self): - """Handle data changes for node values.""" - value = math.ceil(self.values.primary.data * 3 / 100) - self._state = VALUE_TO_SPEED[value] - - def set_speed(self, speed): - """Set the speed of the fan.""" - self.node.set_dimmer( - self.values.primary.value_id, SPEED_TO_VALUE[speed]) - - def turn_on(self, speed=None, **kwargs): - """Turn the device on.""" - if speed is None: - # Value 255 tells device to return to previous value - self.node.set_dimmer(self.values.primary.value_id, 255) - else: - self.set_speed(speed) - - def turn_off(self, **kwargs): - """Turn the device off.""" - self.node.set_dimmer(self.values.primary.value_id, 0) - - @property - def speed(self): - """Return the current speed.""" - return self._state - - @property - def speed_list(self): - """Get the list of available speeds.""" - return SPEED_LIST - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORTED_FEATURES diff --git a/homeassistant/components/fastdotcom/__init__.py b/homeassistant/components/fastdotcom/__init__.py new file mode 100644 index 0000000000000..2e092e527c571 --- /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 0000000000000..0f17179f9185b --- /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 0000000000000..fe6cb1ac12dba --- /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.py deleted file mode 100644 index 7882cdc5a1510..0000000000000 --- a/homeassistant/components/feedreader.py +++ /dev/null @@ -1,213 +0,0 @@ -""" -Support for RSS/Atom feeds. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/feedreader/ -""" -from datetime import datetime, timedelta -from logging import getLogger -from os.path import exists -from threading import Lock -import pickle - -import voluptuous as vol - -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'] - -_LOGGER = getLogger(__name__) - -CONF_URLS = 'urls' -CONF_MAX_ENTRIES = 'max_entries' - -DEFAULT_MAX_ENTRIES = 20 -DEFAULT_SCAN_INTERVAL = timedelta(hours=1) - -DOMAIN = 'feedreader' - -EVENT_FEEDREADER = 'feedreader' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: { - vol.Required(CONF_URLS): vol.All(cv.ensure_list, [cv.url]), - vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): - cv.time_period, - vol.Optional(CONF_MAX_ENTRIES, default=DEFAULT_MAX_ENTRIES): - cv.positive_int - } -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the Feedreader component.""" - urls = config.get(DOMAIN)[CONF_URLS] - scan_interval = config.get(DOMAIN).get(CONF_SCAN_INTERVAL) - max_entries = config.get(DOMAIN).get(CONF_MAX_ENTRIES) - data_file = hass.config.path("{}.pickle".format(DOMAIN)) - storage = StoredData(data_file) - feeds = [FeedManager(url, scan_interval, max_entries, hass, storage) for - url in urls] - return len(feeds) > 0 - - -class FeedManager: - """Abstraction over Feedparser module.""" - - def __init__(self, url, scan_interval, max_entries, hass, storage): - """Initialize the FeedManager object, poll as per scan interval.""" - self._url = url - self._scan_interval = scan_interval - self._max_entries = max_entries - self._feed = None - self._hass = hass - self._firstrun = True - self._storage = storage - self._last_entry_timestamp = None - self._last_update_successful = False - self._has_published_parsed = False - self._event_type = EVENT_FEEDREADER - self._feed_id = url - hass.bus.listen_once( - EVENT_HOMEASSISTANT_START, lambda _: self._update()) - self._init_regular_updates(hass) - - def _log_no_entries(self): - """Send no entries log at debug level.""" - _LOGGER.debug("No new entries to be published in feed %s", self._url) - - def _init_regular_updates(self, hass): - """Schedule regular updates at the top of the clock.""" - track_time_interval(hass, lambda now: self._update(), - self._scan_interval) - - @property - def last_update_successful(self): - """Return True if the last feed update was successful.""" - return self._last_update_successful - - def _update(self): - """Update the feed and publish new entries to the event bus.""" - import feedparser - _LOGGER.info("Fetching new data from feed %s", self._url) - self._feed = feedparser.parse(self._url, - etag=None if not self._feed - else self._feed.get('etag'), - modified=None if not self._feed - else self._feed.get('modified')) - if not self._feed: - _LOGGER.error("Error fetching feed data from %s", self._url) - self._last_update_successful = False - else: - # The 'bozo' flag really only indicates that there was an issue - # during the initial parsing of the XML, but it doesn't indicate - # whether this is an unrecoverable error. In this case the - # feedparser lib is trying a less strict parsing approach. - # If an error is detected here, log error message but continue - # processing the feed entries if present. - if self._feed.bozo != 0: - _LOGGER.error("Error parsing feed %s: %s", self._url, - self._feed.bozo_exception) - # Using etag and modified, if there's no new data available, - # the entries list will be empty - if self._feed.entries: - _LOGGER.debug("%s entri(es) available in feed %s", - len(self._feed.entries), self._url) - self._filter_entries() - self._publish_new_entries() - if self._has_published_parsed: - self._storage.put_timestamp( - self._feed_id, self._last_entry_timestamp) - else: - self._log_no_entries() - self._last_update_successful = True - _LOGGER.info("Fetch from feed %s completed", self._url) - - def _filter_entries(self): - """Filter the entries provided and return the ones to keep.""" - if len(self._feed.entries) > self._max_entries: - _LOGGER.debug("Processing only the first %s entries " - "in feed %s", self._max_entries, self._url) - self._feed.entries = self._feed.entries[0:self._max_entries] - - def _update_and_fire_entry(self, entry): - """Update last_entry_timestamp and fire entry.""" - # We are lucky, `published_parsed` data available, let's make use of - # it to publish only new available entries since the last run - if 'published_parsed' in entry.keys(): - self._has_published_parsed = True - self._last_entry_timestamp = max( - entry.published_parsed, self._last_entry_timestamp) - else: - self._has_published_parsed = False - _LOGGER.debug("No published_parsed info available for entry %s", - entry) - entry.update({'feed_url': self._url}) - self._hass.bus.fire(self._event_type, entry) - - def _publish_new_entries(self): - """Publish new entries to the event bus.""" - new_entries = False - self._last_entry_timestamp = self._storage.get_timestamp(self._feed_id) - if self._last_entry_timestamp: - self._firstrun = False - else: - # Set last entry timestamp as epoch time if not available - self._last_entry_timestamp = \ - datetime.utcfromtimestamp(0).timetuple() - for entry in self._feed.entries: - if self._firstrun or ( - 'published_parsed' in entry.keys() and - entry.published_parsed > self._last_entry_timestamp): - self._update_and_fire_entry(entry) - new_entries = True - else: - _LOGGER.debug("Entry %s already processed", entry) - if not new_entries: - self._log_no_entries() - self._firstrun = False - - -class StoredData: - """Abstraction over pickle data storage.""" - - def __init__(self, data_file): - """Initialize pickle data storage.""" - self._data_file = data_file - self._lock = Lock() - self._cache_outdated = True - self._data = {} - self._fetch_data() - - def _fetch_data(self): - """Fetch data stored into pickle file.""" - if self._cache_outdated and exists(self._data_file): - try: - _LOGGER.debug("Fetching data from file %s", self._data_file) - with self._lock, open(self._data_file, 'rb') as myfile: - self._data = pickle.load(myfile) or {} - self._cache_outdated = False - except: # noqa: E722 pylint: disable=bare-except - _LOGGER.error("Error loading data from pickled file %s", - self._data_file) - - def get_timestamp(self, feed_id): - """Return stored timestamp for given feed id (usually the url).""" - self._fetch_data() - return self._data.get(feed_id) - - def put_timestamp(self, feed_id, timestamp): - """Update timestamp for given feed id (usually the url).""" - self._fetch_data() - with self._lock, open(self._data_file, 'wb') as myfile: - self._data.update({feed_id: timestamp}) - _LOGGER.debug("Overwriting feed %s timestamp in storage file %s", - feed_id, self._data_file) - try: - pickle.dump(self._data, myfile) - except: # noqa: E722 pylint: disable=bare-except - _LOGGER.error( - "Error saving pickled data to %s", self._data_file) - self._cache_outdated = True diff --git a/homeassistant/components/feedreader/__init__.py b/homeassistant/components/feedreader/__init__.py new file mode 100644 index 0000000000000..86744bfd39c77 --- /dev/null +++ b/homeassistant/components/feedreader/__init__.py @@ -0,0 +1,208 @@ +"""Support for RSS/Atom feeds.""" +from datetime import datetime, timedelta +from logging import getLogger +from os.path import exists +from threading import Lock +import pickle + +import voluptuous as vol + +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-homeassistant==5.2.2.dev1'] + +_LOGGER = getLogger(__name__) + +CONF_URLS = 'urls' +CONF_MAX_ENTRIES = 'max_entries' + +DEFAULT_MAX_ENTRIES = 20 +DEFAULT_SCAN_INTERVAL = timedelta(hours=1) + +DOMAIN = 'feedreader' + +EVENT_FEEDREADER = 'feedreader' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: { + vol.Required(CONF_URLS): vol.All(cv.ensure_list, [cv.url]), + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): + cv.time_period, + vol.Optional(CONF_MAX_ENTRIES, default=DEFAULT_MAX_ENTRIES): + cv.positive_int + } +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Feedreader component.""" + urls = config.get(DOMAIN)[CONF_URLS] + scan_interval = config.get(DOMAIN).get(CONF_SCAN_INTERVAL) + max_entries = config.get(DOMAIN).get(CONF_MAX_ENTRIES) + data_file = hass.config.path("{}.pickle".format(DOMAIN)) + storage = StoredData(data_file) + feeds = [FeedManager(url, scan_interval, max_entries, hass, storage) for + url in urls] + return len(feeds) > 0 + + +class FeedManager: + """Abstraction over Feedparser module.""" + + def __init__(self, url, scan_interval, max_entries, hass, storage): + """Initialize the FeedManager object, poll as per scan interval.""" + self._url = url + self._scan_interval = scan_interval + self._max_entries = max_entries + self._feed = None + self._hass = hass + self._firstrun = True + self._storage = storage + self._last_entry_timestamp = None + self._last_update_successful = False + self._has_published_parsed = False + self._event_type = EVENT_FEEDREADER + self._feed_id = url + hass.bus.listen_once( + EVENT_HOMEASSISTANT_START, lambda _: self._update()) + self._init_regular_updates(hass) + + def _log_no_entries(self): + """Send no entries log at debug level.""" + _LOGGER.debug("No new entries to be published in feed %s", self._url) + + def _init_regular_updates(self, hass): + """Schedule regular updates at the top of the clock.""" + track_time_interval(hass, lambda now: self._update(), + self._scan_interval) + + @property + def last_update_successful(self): + """Return True if the last feed update was successful.""" + return self._last_update_successful + + def _update(self): + """Update the feed and publish new entries to the event bus.""" + import feedparser + _LOGGER.info("Fetching new data from feed %s", self._url) + self._feed = feedparser.parse(self._url, + etag=None if not self._feed + else self._feed.get('etag'), + modified=None if not self._feed + else self._feed.get('modified')) + if not self._feed: + _LOGGER.error("Error fetching feed data from %s", self._url) + self._last_update_successful = False + else: + # The 'bozo' flag really only indicates that there was an issue + # during the initial parsing of the XML, but it doesn't indicate + # whether this is an unrecoverable error. In this case the + # feedparser lib is trying a less strict parsing approach. + # If an error is detected here, log error message but continue + # processing the feed entries if present. + if self._feed.bozo != 0: + _LOGGER.error("Error parsing feed %s: %s", self._url, + self._feed.bozo_exception) + # Using etag and modified, if there's no new data available, + # the entries list will be empty + if self._feed.entries: + _LOGGER.debug("%s entri(es) available in feed %s", + len(self._feed.entries), self._url) + self._filter_entries() + self._publish_new_entries() + if self._has_published_parsed: + self._storage.put_timestamp( + self._feed_id, self._last_entry_timestamp) + else: + self._log_no_entries() + self._last_update_successful = True + _LOGGER.info("Fetch from feed %s completed", self._url) + + def _filter_entries(self): + """Filter the entries provided and return the ones to keep.""" + if len(self._feed.entries) > self._max_entries: + _LOGGER.debug("Processing only the first %s entries " + "in feed %s", self._max_entries, self._url) + self._feed.entries = self._feed.entries[0:self._max_entries] + + def _update_and_fire_entry(self, entry): + """Update last_entry_timestamp and fire entry.""" + # We are lucky, `published_parsed` data available, let's make use of + # it to publish only new available entries since the last run + if 'published_parsed' in entry.keys(): + self._has_published_parsed = True + self._last_entry_timestamp = max( + entry.published_parsed, self._last_entry_timestamp) + else: + self._has_published_parsed = False + _LOGGER.debug("No published_parsed info available for entry %s", + entry) + entry.update({'feed_url': self._url}) + self._hass.bus.fire(self._event_type, entry) + + def _publish_new_entries(self): + """Publish new entries to the event bus.""" + new_entries = False + self._last_entry_timestamp = self._storage.get_timestamp(self._feed_id) + if self._last_entry_timestamp: + self._firstrun = False + else: + # Set last entry timestamp as epoch time if not available + self._last_entry_timestamp = \ + datetime.utcfromtimestamp(0).timetuple() + for entry in self._feed.entries: + if self._firstrun or ( + 'published_parsed' in entry.keys() and + entry.published_parsed > self._last_entry_timestamp): + self._update_and_fire_entry(entry) + new_entries = True + else: + _LOGGER.debug("Entry %s already processed", entry) + if not new_entries: + self._log_no_entries() + self._firstrun = False + + +class StoredData: + """Abstraction over pickle data storage.""" + + def __init__(self, data_file): + """Initialize pickle data storage.""" + self._data_file = data_file + self._lock = Lock() + self._cache_outdated = True + self._data = {} + self._fetch_data() + + def _fetch_data(self): + """Fetch data stored into pickle file.""" + if self._cache_outdated and exists(self._data_file): + try: + _LOGGER.debug("Fetching data from file %s", self._data_file) + with self._lock, open(self._data_file, 'rb') as myfile: + self._data = pickle.load(myfile) or {} + self._cache_outdated = False + except: # noqa: E722 pylint: disable=bare-except + _LOGGER.error("Error loading data from pickled file %s", + self._data_file) + + def get_timestamp(self, feed_id): + """Return stored timestamp for given feed id (usually the url).""" + self._fetch_data() + return self._data.get(feed_id) + + def put_timestamp(self, feed_id, timestamp): + """Update timestamp for given feed id (usually the url).""" + self._fetch_data() + with self._lock, open(self._data_file, 'wb') as myfile: + self._data.update({feed_id: timestamp}) + _LOGGER.debug("Overwriting feed %s timestamp in storage file %s", + feed_id, self._data_file) + try: + pickle.dump(self._data, myfile) + except: # noqa: E722 pylint: disable=bare-except + _LOGGER.error( + "Error saving pickled data to %s", self._data_file) + self._cache_outdated = True diff --git a/homeassistant/components/ffmpeg.py b/homeassistant/components/ffmpeg.py deleted file mode 100644 index 3184b5a5d54ab..0000000000000 --- a/homeassistant/components/ffmpeg.py +++ /dev/null @@ -1,211 +0,0 @@ -""" -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/ -""" -import logging -import re - -import voluptuous as vol - -from homeassistant.core import callback -from homeassistant.const import ( - ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) -from homeassistant.helpers.dispatcher import ( - async_dispatcher_send, async_dispatcher_connect) -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity - -REQUIREMENTS = ['ha-ffmpeg==1.11'] - -DOMAIN = 'ffmpeg' - -_LOGGER = logging.getLogger(__name__) - -SERVICE_START = 'start' -SERVICE_STOP = 'stop' -SERVICE_RESTART = 'restart' - -SIGNAL_FFMPEG_START = 'ffmpeg.start' -SIGNAL_FFMPEG_STOP = 'ffmpeg.stop' -SIGNAL_FFMPEG_RESTART = 'ffmpeg.restart' - -DATA_FFMPEG = 'ffmpeg' - -CONF_INITIAL_STATE = 'initial_state' -CONF_INPUT = 'input' -CONF_FFMPEG_BIN = 'ffmpeg_bin' -CONF_EXTRA_ARGUMENTS = 'extra_arguments' -CONF_OUTPUT = 'output' - -DEFAULT_BINARY = 'ffmpeg' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(CONF_FFMPEG_BIN, default=DEFAULT_BINARY): cv.string, - }), -}, extra=vol.ALLOW_EXTRA) - -SERVICE_FFMPEG_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, -}) - - -async def async_setup(hass, config): - """Set up the FFmpeg component.""" - conf = config.get(DOMAIN, {}) - - manager = FFmpegManager( - hass, - conf.get(CONF_FFMPEG_BIN, DEFAULT_BINARY) - ) - - await manager.async_get_version() - - # Register service - async def async_service_handle(service): - """Handle service ffmpeg process.""" - entity_ids = service.data.get(ATTR_ENTITY_ID) - - if service.service == SERVICE_START: - async_dispatcher_send(hass, SIGNAL_FFMPEG_START, entity_ids) - elif service.service == SERVICE_STOP: - async_dispatcher_send(hass, SIGNAL_FFMPEG_STOP, entity_ids) - else: - async_dispatcher_send(hass, SIGNAL_FFMPEG_RESTART, entity_ids) - - hass.services.async_register( - DOMAIN, SERVICE_START, async_service_handle, - schema=SERVICE_FFMPEG_SCHEMA) - - hass.services.async_register( - DOMAIN, SERVICE_STOP, async_service_handle, - schema=SERVICE_FFMPEG_SCHEMA) - - hass.services.async_register( - DOMAIN, SERVICE_RESTART, async_service_handle, - schema=SERVICE_FFMPEG_SCHEMA) - - hass.data[DATA_FFMPEG] = manager - return True - - -class FFmpegManager: - """Helper for ha-ffmpeg.""" - - def __init__(self, hass, ffmpeg_bin): - """Initialize helper.""" - self.hass = hass - self._cache = {} - self._bin = ffmpeg_bin - self._version = None - self._major_version = None - - @property - def binary(self): - """Return ffmpeg binary from config.""" - return self._bin - - async def async_get_version(self): - """Return ffmpeg version.""" - from haffmpeg.tools import FFVersion - - ffversion = FFVersion(self._bin, self.hass.loop) - self._version = await ffversion.get_version() - - self._major_version = None - if self._version is not None: - result = re.search(r"(\d+)\.", self._version) - if result is not None: - self._major_version = int(result.group(1)) - - return self._version, self._major_version - - @property - def ffmpeg_stream_content_type(self): - """Return HTTP content type for ffmpeg stream.""" - if self._major_version is not None and self._major_version > 3: - return 'multipart/x-mixed-replace;boundary=ffmpeg' - - return 'multipart/x-mixed-replace;boundary=ffserver' - - -class FFmpegBase(Entity): - """Interface object for FFmpeg.""" - - def __init__(self, initial_state=True): - """Initialize ffmpeg base object.""" - self.ffmpeg = None - self.initial_state = initial_state - - async def async_added_to_hass(self): - """Register dispatcher & events. - - This method is a coroutine. - """ - async_dispatcher_connect( - self.hass, SIGNAL_FFMPEG_START, self._async_start_ffmpeg) - async_dispatcher_connect( - self.hass, SIGNAL_FFMPEG_STOP, self._async_stop_ffmpeg) - async_dispatcher_connect( - self.hass, SIGNAL_FFMPEG_RESTART, self._async_restart_ffmpeg) - - # register start/stop - self._async_register_events() - - @property - def available(self): - """Return True if entity is available.""" - return self.ffmpeg.is_running - - @property - def should_poll(self): - """Return True if entity has to be polled for state.""" - return False - - async def _async_start_ffmpeg(self, entity_ids): - """Start a FFmpeg process. - - This method is a coroutine. - """ - raise NotImplementedError() - - async def _async_stop_ffmpeg(self, entity_ids): - """Stop a FFmpeg process. - - This method is a coroutine. - """ - if entity_ids is None or self.entity_id in entity_ids: - await self.ffmpeg.close() - - async def _async_restart_ffmpeg(self, entity_ids): - """Stop a FFmpeg process. - - This method is a coroutine. - """ - if entity_ids is None or self.entity_id in entity_ids: - await self._async_stop_ffmpeg(None) - await self._async_start_ffmpeg(None) - - @callback - def _async_register_events(self): - """Register a FFmpeg process/device.""" - async def async_shutdown_handle(event): - """Stop FFmpeg process.""" - await self._async_stop_ffmpeg(None) - - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, async_shutdown_handle) - - # start on startup - if not self.initial_state: - return - - async def async_start_handle(event): - """Start FFmpeg process.""" - await self._async_start_ffmpeg(None) - self.async_schedule_update_ha_state() - - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, async_start_handle) diff --git a/homeassistant/components/ffmpeg/__init__.py b/homeassistant/components/ffmpeg/__init__.py new file mode 100644 index 0000000000000..7b7e3a8129408 --- /dev/null +++ b/homeassistant/components/ffmpeg/__init__.py @@ -0,0 +1,206 @@ +"""Support for FFmpeg.""" +import logging +import re + +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.const import ( + ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) +from homeassistant.helpers.dispatcher import ( + async_dispatcher_send, async_dispatcher_connect) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['ha-ffmpeg==1.11'] + +DOMAIN = 'ffmpeg' + +_LOGGER = logging.getLogger(__name__) + +SERVICE_START = 'start' +SERVICE_STOP = 'stop' +SERVICE_RESTART = 'restart' + +SIGNAL_FFMPEG_START = 'ffmpeg.start' +SIGNAL_FFMPEG_STOP = 'ffmpeg.stop' +SIGNAL_FFMPEG_RESTART = 'ffmpeg.restart' + +DATA_FFMPEG = 'ffmpeg' + +CONF_INITIAL_STATE = 'initial_state' +CONF_INPUT = 'input' +CONF_FFMPEG_BIN = 'ffmpeg_bin' +CONF_EXTRA_ARGUMENTS = 'extra_arguments' +CONF_OUTPUT = 'output' + +DEFAULT_BINARY = 'ffmpeg' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Optional(CONF_FFMPEG_BIN, default=DEFAULT_BINARY): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + +SERVICE_FFMPEG_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, +}) + + +async def async_setup(hass, config): + """Set up the FFmpeg component.""" + conf = config.get(DOMAIN, {}) + + manager = FFmpegManager( + hass, + conf.get(CONF_FFMPEG_BIN, DEFAULT_BINARY) + ) + + await manager.async_get_version() + + # Register service + async def async_service_handle(service): + """Handle service ffmpeg process.""" + entity_ids = service.data.get(ATTR_ENTITY_ID) + + if service.service == SERVICE_START: + async_dispatcher_send(hass, SIGNAL_FFMPEG_START, entity_ids) + elif service.service == SERVICE_STOP: + async_dispatcher_send(hass, SIGNAL_FFMPEG_STOP, entity_ids) + else: + async_dispatcher_send(hass, SIGNAL_FFMPEG_RESTART, entity_ids) + + hass.services.async_register( + DOMAIN, SERVICE_START, async_service_handle, + schema=SERVICE_FFMPEG_SCHEMA) + + hass.services.async_register( + DOMAIN, SERVICE_STOP, async_service_handle, + schema=SERVICE_FFMPEG_SCHEMA) + + hass.services.async_register( + DOMAIN, SERVICE_RESTART, async_service_handle, + schema=SERVICE_FFMPEG_SCHEMA) + + hass.data[DATA_FFMPEG] = manager + return True + + +class FFmpegManager: + """Helper for ha-ffmpeg.""" + + def __init__(self, hass, ffmpeg_bin): + """Initialize helper.""" + self.hass = hass + self._cache = {} + self._bin = ffmpeg_bin + self._version = None + self._major_version = None + + @property + def binary(self): + """Return ffmpeg binary from config.""" + return self._bin + + async def async_get_version(self): + """Return ffmpeg version.""" + from haffmpeg.tools import FFVersion + + ffversion = FFVersion(self._bin, self.hass.loop) + self._version = await ffversion.get_version() + + self._major_version = None + if self._version is not None: + result = re.search(r"(\d+)\.", self._version) + if result is not None: + self._major_version = int(result.group(1)) + + return self._version, self._major_version + + @property + def ffmpeg_stream_content_type(self): + """Return HTTP content type for ffmpeg stream.""" + if self._major_version is not None and self._major_version > 3: + return 'multipart/x-mixed-replace;boundary=ffmpeg' + + return 'multipart/x-mixed-replace;boundary=ffserver' + + +class FFmpegBase(Entity): + """Interface object for FFmpeg.""" + + def __init__(self, initial_state=True): + """Initialize ffmpeg base object.""" + self.ffmpeg = None + self.initial_state = initial_state + + async def async_added_to_hass(self): + """Register dispatcher & events. + + This method is a coroutine. + """ + async_dispatcher_connect( + self.hass, SIGNAL_FFMPEG_START, self._async_start_ffmpeg) + async_dispatcher_connect( + self.hass, SIGNAL_FFMPEG_STOP, self._async_stop_ffmpeg) + async_dispatcher_connect( + self.hass, SIGNAL_FFMPEG_RESTART, self._async_restart_ffmpeg) + + # register start/stop + self._async_register_events() + + @property + def available(self): + """Return True if entity is available.""" + return self.ffmpeg.is_running + + @property + def should_poll(self): + """Return True if entity has to be polled for state.""" + return False + + async def _async_start_ffmpeg(self, entity_ids): + """Start a FFmpeg process. + + This method is a coroutine. + """ + raise NotImplementedError() + + async def _async_stop_ffmpeg(self, entity_ids): + """Stop a FFmpeg process. + + This method is a coroutine. + """ + if entity_ids is None or self.entity_id in entity_ids: + await self.ffmpeg.close() + + async def _async_restart_ffmpeg(self, entity_ids): + """Stop a FFmpeg process. + + This method is a coroutine. + """ + if entity_ids is None or self.entity_id in entity_ids: + await self._async_stop_ffmpeg(None) + await self._async_start_ffmpeg(None) + + @callback + def _async_register_events(self): + """Register a FFmpeg process/device.""" + async def async_shutdown_handle(event): + """Stop FFmpeg process.""" + await self._async_stop_ffmpeg(None) + + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, async_shutdown_handle) + + # start on startup + if not self.initial_state: + return + + async def async_start_handle(event): + """Start FFmpeg process.""" + await self._async_start_ffmpeg(None) + self.async_schedule_update_ha_state() + + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, async_start_handle) diff --git a/homeassistant/components/ffmpeg/services.yaml b/homeassistant/components/ffmpeg/services.yaml new file mode 100644 index 0000000000000..05c9c4fb18060 --- /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 715cc036265f0..9a6ccccb5e3fb 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 @@ REQUIREMENTS = ['fiblary3==0.1.7'] _LOGGER = logging.getLogger(__name__) + +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_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_DEVICES = 'fibaro_devices' -FIBARO_COMPONENTS = ['binary_sensor', 'cover', 'light', - 'scene', 'sensor', 'switch'] +FIBARO_COMPONENTS = [ + 'binary_sensor', + 'cover', + 'light', + 'scene', + 'sensor', + 'switch', +] FIBARO_TYPEMAP = { 'com.fibaro.multilevelSensor': "sensor", @@ -78,8 +82,7 @@ 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 @@ def __init__(self, config): """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 @@ def connect(self): 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/fibaro/binary_sensor.py b/homeassistant/components/fibaro/binary_sensor.py new file mode 100644 index 0000000000000..2c2d9c30a794a --- /dev/null +++ b/homeassistant/components/fibaro/binary_sensor.py @@ -0,0 +1,77 @@ +"""Support for Fibaro binary sensors.""" +import logging + +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, ENTITY_ID_FORMAT) +from homeassistant.components.fibaro import ( + FIBARO_DEVICES, FibaroDevice) +from homeassistant.const import (CONF_DEVICE_CLASS, CONF_ICON) + +DEPENDENCIES = ['fibaro'] + +_LOGGER = logging.getLogger(__name__) + +SENSOR_TYPES = { + 'com.fibaro.floodSensor': ['Flood', 'mdi:water', 'flood'], + 'com.fibaro.motionSensor': ['Motion', 'mdi:run', 'motion'], + 'com.fibaro.doorSensor': ['Door', 'mdi:window-open', 'door'], + 'com.fibaro.windowSensor': ['Window', 'mdi:window-open', 'window'], + 'com.fibaro.smokeSensor': ['Smoke', 'mdi:smoking', 'smoke'], + 'com.fibaro.FGMS001': ['Motion', 'mdi:run', 'motion'], + 'com.fibaro.heatDetector': ['Heat', 'mdi:fire', 'heat'], +} + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Perform the setup for Fibaro controller devices.""" + if discovery_info is None: + return + + add_entities( + [FibaroBinarySensor(device) + for device in hass.data[FIBARO_DEVICES]['binary_sensor']], True) + + +class FibaroBinarySensor(FibaroDevice, BinarySensorDevice): + """Representation of a Fibaro Binary Sensor.""" + + def __init__(self, fibaro_device): + """Initialize the binary_sensor.""" + self._state = None + super().__init__(fibaro_device) + self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) + stype = None + devconf = fibaro_device.device_config + if fibaro_device.type in SENSOR_TYPES: + stype = fibaro_device.type + elif fibaro_device.baseType in SENSOR_TYPES: + stype = fibaro_device.baseType + if stype: + self._device_class = SENSOR_TYPES[stype][2] + self._icon = SENSOR_TYPES[stype][1] + else: + self._device_class = None + self._icon = None + # device_config overrides: + self._device_class = devconf.get(CONF_DEVICE_CLASS, + self._device_class) + self._icon = devconf.get(CONF_ICON, self._icon) + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self._icon + + @property + def device_class(self): + """Return the device class of the sensor.""" + return self._device_class + + @property + def is_on(self): + """Return true if sensor is on.""" + return self._state + + def update(self): + """Get the latest data and update the state.""" + self._state = self.current_binary_state diff --git a/homeassistant/components/fibaro/cover.py b/homeassistant/components/fibaro/cover.py new file mode 100644 index 0000000000000..aa34fcc36a965 --- /dev/null +++ b/homeassistant/components/fibaro/cover.py @@ -0,0 +1,87 @@ +"""Support for Fibaro cover - curtains, rollershutters etc.""" +import logging + +from homeassistant.components.cover import ( + CoverDevice, ENTITY_ID_FORMAT, ATTR_POSITION, ATTR_TILT_POSITION) +from homeassistant.components.fibaro import ( + FIBARO_DEVICES, FibaroDevice) + +DEPENDENCIES = ['fibaro'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Fibaro covers.""" + if discovery_info is None: + return + + add_entities( + [FibaroCover(device) for + device in hass.data[FIBARO_DEVICES]['cover']], True) + + +class FibaroCover(FibaroDevice, CoverDevice): + """Representation a Fibaro Cover.""" + + def __init__(self, fibaro_device): + """Initialize the Vera device.""" + super().__init__(fibaro_device) + self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) + + @staticmethod + def bound(position): + """Normalize the position.""" + if position is None: + return None + position = int(position) + if position <= 5: + return 0 + if position >= 95: + return 100 + return position + + @property + def current_cover_position(self): + """Return current position of cover. 0 is closed, 100 is open.""" + return self.bound(self.level) + + @property + def current_cover_tilt_position(self): + """Return the current tilt position for venetian blinds.""" + return self.bound(self.level2) + + def set_cover_position(self, **kwargs): + """Move the cover to a specific position.""" + self.set_level(kwargs.get(ATTR_POSITION)) + + def set_cover_tilt_position(self, **kwargs): + """Move the cover to a specific position.""" + self.set_level2(kwargs.get(ATTR_TILT_POSITION)) + + @property + def is_closed(self): + """Return if the cover is closed.""" + if self.current_cover_position is None: + return None + return self.current_cover_position == 0 + + def open_cover(self, **kwargs): + """Open the cover.""" + self.action("open") + + def close_cover(self, **kwargs): + """Close the cover.""" + self.action("close") + + def open_cover_tilt(self, **kwargs): + """Open the cover tilt.""" + self.set_level2(100) + + def close_cover_tilt(self, **kwargs): + """Close the cover.""" + self.set_level2(0) + + def stop_cover(self, **kwargs): + """Stop the cover.""" + self.action("stop") diff --git a/homeassistant/components/fibaro/light.py b/homeassistant/components/fibaro/light.py new file mode 100644 index 0000000000000..5ee3e83b95fd7 --- /dev/null +++ b/homeassistant/components/fibaro/light.py @@ -0,0 +1,205 @@ +"""Support for Fibaro lights.""" +import logging +import asyncio +from functools import partial + +from homeassistant.const import ( + CONF_WHITE_VALUE) +from homeassistant.components.fibaro import ( + FIBARO_DEVICES, FibaroDevice, + CONF_DIMMING, CONF_COLOR, CONF_RESET_COLOR) +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_WHITE_VALUE, ENTITY_ID_FORMAT, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_WHITE_VALUE, Light) +import homeassistant.util.color as color_util + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['fibaro'] + + +def scaleto255(value): + """Scale the input value from 0-100 to 0-255.""" + # Fibaro has a funny way of storing brightness either 0-100 or 0-99 + # depending on device type (e.g. dimmer vs led) + if value > 98: + value = 100 + return max(0, min(255, ((value * 255.0) / 100.0))) + + +def scaleto100(value): + """Scale the input value from 0-255 to 0-100.""" + # Make sure a low but non-zero value is not rounded down to zero + if 0 < value < 3: + return 1 + return max(0, min(100, ((value * 100.0) / 255.0))) + + +async def async_setup_platform(hass, + config, + async_add_entities, + discovery_info=None): + """Perform the setup for Fibaro controller devices.""" + if discovery_info is None: + return + + async_add_entities( + [FibaroLight(device) + for device in hass.data[FIBARO_DEVICES]['light']], True) + + +class FibaroLight(FibaroDevice, Light): + """Representation of a Fibaro Light, including dimmable.""" + + def __init__(self, fibaro_device): + """Initialize the light.""" + self._brightness = None + self._color = (0, 0) + self._last_brightness = 0 + self._supported_flags = 0 + self._update_lock = asyncio.Lock() + self._white = 0 + + devconf = fibaro_device.device_config + self._reset_color = devconf.get(CONF_RESET_COLOR, False) + supports_color = 'color' in fibaro_device.properties and \ + 'setColor' in fibaro_device.actions + supports_dimming = 'levelChange' in fibaro_device.interfaces + supports_white_v = 'setW' in fibaro_device.actions + + # Configuration can overrride default capability detection + if devconf.get(CONF_DIMMING, supports_dimming): + self._supported_flags |= SUPPORT_BRIGHTNESS + if devconf.get(CONF_COLOR, supports_color): + self._supported_flags |= SUPPORT_COLOR + if devconf.get(CONF_WHITE_VALUE, supports_white_v): + self._supported_flags |= SUPPORT_WHITE_VALUE + + super().__init__(fibaro_device) + self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) + + @property + def brightness(self): + """Return the brightness of the light.""" + return scaleto255(self._brightness) + + @property + def hs_color(self): + """Return the color of the light.""" + return self._color + + @property + def white_value(self): + """Return the white value of this light between 0..255.""" + return self._white + + @property + def supported_features(self): + """Flag supported features.""" + return self._supported_flags + + async def async_turn_on(self, **kwargs): + """Turn the light on.""" + async with self._update_lock: + await self.hass.async_add_executor_job( + partial(self._turn_on, **kwargs)) + + def _turn_on(self, **kwargs): + """Really turn the light on.""" + if self._supported_flags & SUPPORT_BRIGHTNESS: + target_brightness = kwargs.get(ATTR_BRIGHTNESS) + + # No brightness specified, so we either restore it to + # last brightness or switch it on at maximum level + if target_brightness is None: + if self._brightness == 0: + if self._last_brightness: + self._brightness = self._last_brightness + else: + self._brightness = 100 + else: + # We set it to the target brightness and turn it on + self._brightness = scaleto100(target_brightness) + + if self._supported_flags & SUPPORT_COLOR: + if self._reset_color and \ + kwargs.get(ATTR_WHITE_VALUE) is None and \ + kwargs.get(ATTR_HS_COLOR) is None and \ + kwargs.get(ATTR_BRIGHTNESS) is None: + self._color = (100, 0) + + # Update based on parameters + self._white = kwargs.get(ATTR_WHITE_VALUE, self._white) + self._color = kwargs.get(ATTR_HS_COLOR, self._color) + rgb = color_util.color_hs_to_RGB(*self._color) + self.call_set_color( + round(rgb[0] * self._brightness / 100.0), + round(rgb[1] * self._brightness / 100.0), + round(rgb[2] * self._brightness / 100.0), + round(self._white * self._brightness / 100.0)) + + if self.state == 'off': + self.set_level(int(self._brightness)) + return + + if self._reset_color: + bri255 = scaleto255(self._brightness) + self.call_set_color(bri255, bri255, bri255, bri255) + + if self._supported_flags & SUPPORT_BRIGHTNESS: + self.set_level(int(self._brightness)) + return + + # The simplest case is left for last. No dimming, just switch on + self.call_turn_on() + + async def async_turn_off(self, **kwargs): + """Turn the light off.""" + async with self._update_lock: + await self.hass.async_add_executor_job( + partial(self._turn_off, **kwargs)) + + def _turn_off(self, **kwargs): + """Really turn the light off.""" + # Let's save the last brightness level before we switch it off + if (self._supported_flags & SUPPORT_BRIGHTNESS) and \ + self._brightness and self._brightness > 0: + self._last_brightness = self._brightness + self._brightness = 0 + self.call_turn_off() + + @property + def is_on(self): + """Return true if device is on.""" + return self.current_binary_state + + async def async_update(self): + """Update the state.""" + async with self._update_lock: + await self.hass.async_add_executor_job(self._update) + + def _update(self): + """Really update the state.""" + # Brightness handling + if self._supported_flags & SUPPORT_BRIGHTNESS: + self._brightness = float(self.fibaro_device.properties.value) + # Fibaro might report 0-99 or 0-100 for brightness, + # based on device type, so we round up here + if self._brightness > 99: + self._brightness = 100 + # Color handling + if self._supported_flags & SUPPORT_COLOR and \ + 'color' in self.fibaro_device.properties and \ + ',' in self.fibaro_device.properties.color: + # Fibaro communicates the color as an 'R, G, B, W' string + rgbw_s = self.fibaro_device.properties.color + if rgbw_s == '0,0,0,0' and\ + 'lastColorSet' in self.fibaro_device.properties: + rgbw_s = self.fibaro_device.properties.lastColorSet + rgbw_list = [int(i) for i in rgbw_s.split(",")][:4] + if rgbw_list[0] or rgbw_list[1] or rgbw_list[2]: + self._color = color_util.color_RGB_to_hs(*rgbw_list[:3]) + if (self._supported_flags & SUPPORT_WHITE_VALUE) and \ + self.brightness != 0: + self._white = min(255, max(0, rgbw_list[3]*100.0 / + self._brightness)) diff --git a/homeassistant/components/fibaro/scene.py b/homeassistant/components/fibaro/scene.py new file mode 100644 index 0000000000000..620f095b733a4 --- /dev/null +++ b/homeassistant/components/fibaro/scene.py @@ -0,0 +1,30 @@ +"""Support for Fibaro scenes.""" +import logging + +from homeassistant.components.scene import ( + Scene) +from homeassistant.components.fibaro import ( + FIBARO_DEVICES, FibaroDevice) + +DEPENDENCIES = ['fibaro'] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Perform the setup for Fibaro scenes.""" + if discovery_info is None: + return + + async_add_entities( + [FibaroScene(scene) + for scene in hass.data[FIBARO_DEVICES]['scene']], True) + + +class FibaroScene(FibaroDevice, Scene): + """Representation of a Fibaro scene entity.""" + + def activate(self): + """Activate the scene.""" + self.fibaro_device.start() diff --git a/homeassistant/components/fibaro/sensor.py b/homeassistant/components/fibaro/sensor.py new file mode 100644 index 0000000000000..01452d8b394a7 --- /dev/null +++ b/homeassistant/components/fibaro/sensor.py @@ -0,0 +1,94 @@ +"""Support for Fibaro sensors.""" +import logging + +from homeassistant.const import ( + DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, TEMP_CELSIUS, TEMP_FAHRENHEIT) +from homeassistant.helpers.entity import Entity +from homeassistant.components.sensor import ENTITY_ID_FORMAT +from homeassistant.components.fibaro import ( + FIBARO_DEVICES, FibaroDevice) + +SENSOR_TYPES = { + 'com.fibaro.temperatureSensor': + ['Temperature', None, None, DEVICE_CLASS_TEMPERATURE], + 'com.fibaro.smokeSensor': + ['Smoke', 'ppm', 'mdi:fire', None], + 'CO2': + ['CO2', 'ppm', 'mdi:cloud', None], + 'com.fibaro.humiditySensor': + ['Humidity', '%', None, DEVICE_CLASS_HUMIDITY], + 'com.fibaro.lightSensor': + ['Light', 'lx', None, DEVICE_CLASS_ILLUMINANCE] +} + +DEPENDENCIES = ['fibaro'] +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Fibaro controller devices.""" + if discovery_info is None: + return + + add_entities( + [FibaroSensor(device) + for device in hass.data[FIBARO_DEVICES]['sensor']], True) + + +class FibaroSensor(FibaroDevice, Entity): + """Representation of a Fibaro Sensor.""" + + def __init__(self, fibaro_device): + """Initialize the sensor.""" + self.current_value = None + self.last_changed_time = None + super().__init__(fibaro_device) + self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) + if fibaro_device.type in SENSOR_TYPES: + self._unit = SENSOR_TYPES[fibaro_device.type][1] + self._icon = SENSOR_TYPES[fibaro_device.type][2] + self._device_class = SENSOR_TYPES[fibaro_device.type][3] + else: + self._unit = None + self._icon = None + self._device_class = None + try: + if not self._unit: + if self.fibaro_device.properties.unit == 'lux': + self._unit = 'lx' + elif self.fibaro_device.properties.unit == 'C': + self._unit = TEMP_CELSIUS + elif self.fibaro_device.properties.unit == 'F': + self._unit = TEMP_FAHRENHEIT + else: + self._unit = self.fibaro_device.properties.unit + except (KeyError, ValueError): + pass + + @property + def state(self): + """Return the state of the sensor.""" + return self.current_value + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self._unit + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self._icon + + @property + def device_class(self): + """Return the device class of the sensor.""" + return self._device_class + + def update(self): + """Update the state.""" + try: + self.current_value = float(self.fibaro_device.properties.value) + except (KeyError, ValueError): + pass diff --git a/homeassistant/components/fibaro/switch.py b/homeassistant/components/fibaro/switch.py new file mode 100644 index 0000000000000..04b8aba1cf43a --- /dev/null +++ b/homeassistant/components/fibaro/switch.py @@ -0,0 +1,63 @@ +"""Support for Fibaro switches.""" +import logging + +from homeassistant.util import convert +from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchDevice +from homeassistant.components.fibaro import ( + FIBARO_DEVICES, FibaroDevice) + +DEPENDENCIES = ['fibaro'] +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Fibaro switches.""" + if discovery_info is None: + return + + add_entities( + [FibaroSwitch(device) for + device in hass.data[FIBARO_DEVICES]['switch']], True) + + +class FibaroSwitch(FibaroDevice, SwitchDevice): + """Representation of a Fibaro Switch.""" + + def __init__(self, fibaro_device): + """Initialize the Fibaro device.""" + self._state = False + super().__init__(fibaro_device) + self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) + + def turn_on(self, **kwargs): + """Turn device on.""" + self.call_turn_on() + self._state = True + + def turn_off(self, **kwargs): + """Turn device off.""" + self.call_turn_off() + self._state = False + + @property + def current_power_w(self): + """Return the current power usage in W.""" + if 'power' in self.fibaro_device.interfaces: + return convert(self.fibaro_device.properties.power, float, 0.0) + return None + + @property + def today_energy_kwh(self): + """Return the today total energy usage in kWh.""" + if 'energy' in self.fibaro_device.interfaces: + return convert(self.fibaro_device.properties.energy, float, 0.0) + return None + + @property + def is_on(self): + """Return true if device is on.""" + return self._state + + def update(self): + """Update device state.""" + self._state = self.current_binary_state diff --git a/homeassistant/components/folder_watcher.py b/homeassistant/components/folder_watcher.py deleted file mode 100644 index 098b34ac948cf..0000000000000 --- a/homeassistant/components/folder_watcher.py +++ /dev/null @@ -1,110 +0,0 @@ -""" -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 -import logging -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' -CONF_PATTERNS = 'patterns' -DEFAULT_PATTERN = '*' -DOMAIN = "folder_watcher" - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ - vol.Required(CONF_FOLDER): cv.isdir, - vol.Optional(CONF_PATTERNS, default=[DEFAULT_PATTERN]): - vol.All(cv.ensure_list, [cv.string]), - })]) -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the folder watcher.""" - conf = config[DOMAIN] - for watcher in conf: - path = watcher[CONF_FOLDER] - patterns = watcher[CONF_PATTERNS] - if not hass.config.is_allowed_path(path): - _LOGGER.error("folder %s is not valid or allowed", path) - return False - Watcher(path, patterns, hass) - - return True - - -def create_event_handler(patterns, hass): - """Return the Watchdog EventHandler object.""" - from watchdog.events import PatternMatchingEventHandler - - class EventHandler(PatternMatchingEventHandler): - """Class for handling Watcher events.""" - - def __init__(self, patterns, hass): - """Initialise the EventHandler.""" - super().__init__(patterns) - self.hass = hass - - def process(self, event): - """On Watcher event, fire HA event.""" - _LOGGER.debug("process(%s)", event) - if not event.is_directory: - folder, file_name = os.path.split(event.src_path) - self.hass.bus.fire( - DOMAIN, { - "event_type": event.event_type, - 'path': event.src_path, - 'file': file_name, - 'folder': folder, - }) - - def on_modified(self, event): - """File modified.""" - self.process(event) - - def on_moved(self, event): - """File moved.""" - self.process(event) - - def on_created(self, event): - """File created.""" - self.process(event) - - def on_deleted(self, event): - """File deleted.""" - self.process(event) - - return EventHandler(patterns, hass) - - -class Watcher(): - """Class for starting Watchdog.""" - - def __init__(self, path, patterns, hass): - """Initialise the watchdog observer.""" - from watchdog.observers import Observer - self._observer = Observer() - self._observer.schedule( - create_event_handler(patterns, hass), - path, - recursive=True) - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, self.startup) - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.shutdown) - - def startup(self, event): - """Start the watcher.""" - self._observer.start() - - def shutdown(self, event): - """Shutdown the watcher.""" - self._observer.stop() - self._observer.join() diff --git a/homeassistant/components/folder_watcher/__init__.py b/homeassistant/components/folder_watcher/__init__.py new file mode 100644 index 0000000000000..babfbd9e9aa61 --- /dev/null +++ b/homeassistant/components/folder_watcher/__init__.py @@ -0,0 +1,108 @@ +"""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' +CONF_PATTERNS = 'patterns' +DEFAULT_PATTERN = '*' +DOMAIN = "folder_watcher" + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ + vol.Required(CONF_FOLDER): cv.isdir, + vol.Optional(CONF_PATTERNS, default=[DEFAULT_PATTERN]): + vol.All(cv.ensure_list, [cv.string]), + })]) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the folder watcher.""" + conf = config[DOMAIN] + for watcher in conf: + path = watcher[CONF_FOLDER] + patterns = watcher[CONF_PATTERNS] + if not hass.config.is_allowed_path(path): + _LOGGER.error("folder %s is not valid or allowed", path) + return False + Watcher(path, patterns, hass) + + return True + + +def create_event_handler(patterns, hass): + """Return the Watchdog EventHandler object.""" + from watchdog.events import PatternMatchingEventHandler + + class EventHandler(PatternMatchingEventHandler): + """Class for handling Watcher events.""" + + def __init__(self, patterns, hass): + """Initialise the EventHandler.""" + super().__init__(patterns) + self.hass = hass + + def process(self, event): + """On Watcher event, fire HA event.""" + _LOGGER.debug("process(%s)", event) + if not event.is_directory: + folder, file_name = os.path.split(event.src_path) + self.hass.bus.fire( + DOMAIN, { + "event_type": event.event_type, + 'path': event.src_path, + 'file': file_name, + 'folder': folder, + }) + + def on_modified(self, event): + """File modified.""" + self.process(event) + + def on_moved(self, event): + """File moved.""" + self.process(event) + + def on_created(self, event): + """File created.""" + self.process(event) + + def on_deleted(self, event): + """File deleted.""" + self.process(event) + + return EventHandler(patterns, hass) + + +class Watcher(): + """Class for starting Watchdog.""" + + def __init__(self, path, patterns, hass): + """Initialise the watchdog observer.""" + from watchdog.observers import Observer + self._observer = Observer() + self._observer.schedule( + create_event_handler(patterns, hass), + path, + recursive=True) + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, self.startup) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.shutdown) + + def startup(self, event): + """Start the watcher.""" + self._observer.start() + + def shutdown(self, event): + """Shutdown the watcher.""" + self._observer.stop() + self._observer.join() diff --git a/homeassistant/components/foursquare.py b/homeassistant/components/foursquare.py deleted file mode 100644 index a4a7395adc432..0000000000000 --- a/homeassistant/components/foursquare.py +++ /dev/null @@ -1,103 +0,0 @@ -""" -Support for the Foursquare (Swarm) API. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/foursquare/ -""" -import logging - -import requests -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_ACCESS_TOKEN, HTTP_BAD_REQUEST -from homeassistant.components.http import HomeAssistantView - -_LOGGER = logging.getLogger(__name__) - -CONF_PUSH_SECRET = 'push_secret' - -DEPENDENCIES = ['http'] -DOMAIN = 'foursquare' - -EVENT_CHECKIN = 'foursquare.checkin' -EVENT_PUSH = 'foursquare.push' - -SERVICE_CHECKIN = 'checkin' - -CHECKIN_SERVICE_SCHEMA = vol.Schema({ - vol.Optional('alt'): cv.string, - vol.Optional('altAcc'): cv.string, - vol.Optional('broadcast'): cv.string, - vol.Optional('eventId'): cv.string, - vol.Optional('ll'): cv.string, - vol.Optional('llAcc'): cv.string, - vol.Optional('mentions'): cv.string, - vol.Optional('shout'): cv.string, - vol.Required('venueId'): cv.string, -}) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_ACCESS_TOKEN): cv.string, - vol.Required(CONF_PUSH_SECRET): cv.string, - }), -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the Foursquare component.""" - config = config[DOMAIN] - - def checkin_user(call): - """Check a user in on Swarm.""" - url = ("https://api.foursquare.com/v2/checkins/add" - "?oauth_token={}" - "&v=20160802" - "&m=swarm").format(config[CONF_ACCESS_TOKEN]) - response = requests.post(url, data=call.data, timeout=10) - - if response.status_code not in (200, 201): - _LOGGER.exception( - "Error checking in user. Response %d: %s:", - response.status_code, response.reason) - - hass.bus.fire(EVENT_CHECKIN, response.text) - - # Register our service with Home Assistant. - hass.services.register(DOMAIN, 'checkin', checkin_user, - schema=CHECKIN_SERVICE_SCHEMA) - - hass.http.register_view(FoursquarePushReceiver(config[CONF_PUSH_SECRET])) - - return True - - -class FoursquarePushReceiver(HomeAssistantView): - """Handle pushes from the Foursquare API.""" - - requires_auth = False - url = "/api/foursquare" - name = "foursquare" - - def __init__(self, push_secret): - """Initialize the OAuth callback view.""" - self.push_secret = push_secret - - async def post(self, request): - """Accept the POST from Foursquare.""" - try: - data = await request.json() - except ValueError: - return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) - - secret = data.pop('secret', None) - - _LOGGER.debug("Received Foursquare push: %s", data) - - if self.push_secret != secret: - _LOGGER.error("Received Foursquare push with invalid" - "push secret: %s", secret) - return self.json_message('Incorrect secret', HTTP_BAD_REQUEST) - - request.app['hass'].bus.async_fire(EVENT_PUSH, data) diff --git a/homeassistant/components/foursquare/__init__.py b/homeassistant/components/foursquare/__init__.py new file mode 100644 index 0000000000000..0c5a48049ecc9 --- /dev/null +++ b/homeassistant/components/foursquare/__init__.py @@ -0,0 +1,98 @@ +"""Support for the Foursquare (Swarm) API.""" +import logging + +import requests +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_ACCESS_TOKEN, HTTP_BAD_REQUEST +from homeassistant.components.http import HomeAssistantView + +_LOGGER = logging.getLogger(__name__) + +CONF_PUSH_SECRET = 'push_secret' + +DEPENDENCIES = ['http'] +DOMAIN = 'foursquare' + +EVENT_CHECKIN = 'foursquare.checkin' +EVENT_PUSH = 'foursquare.push' + +SERVICE_CHECKIN = 'checkin' + +CHECKIN_SERVICE_SCHEMA = vol.Schema({ + vol.Optional('alt'): cv.string, + vol.Optional('altAcc'): cv.string, + vol.Optional('broadcast'): cv.string, + vol.Optional('eventId'): cv.string, + vol.Optional('ll'): cv.string, + vol.Optional('llAcc'): cv.string, + vol.Optional('mentions'): cv.string, + vol.Optional('shout'): cv.string, + vol.Required('venueId'): cv.string, +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_ACCESS_TOKEN): cv.string, + vol.Required(CONF_PUSH_SECRET): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Foursquare component.""" + config = config[DOMAIN] + + def checkin_user(call): + """Check a user in on Swarm.""" + url = ("https://api.foursquare.com/v2/checkins/add" + "?oauth_token={}" + "&v=20160802" + "&m=swarm").format(config[CONF_ACCESS_TOKEN]) + response = requests.post(url, data=call.data, timeout=10) + + if response.status_code not in (200, 201): + _LOGGER.exception( + "Error checking in user. Response %d: %s:", + response.status_code, response.reason) + + hass.bus.fire(EVENT_CHECKIN, response.text) + + # Register our service with Home Assistant. + hass.services.register(DOMAIN, 'checkin', checkin_user, + schema=CHECKIN_SERVICE_SCHEMA) + + hass.http.register_view(FoursquarePushReceiver(config[CONF_PUSH_SECRET])) + + return True + + +class FoursquarePushReceiver(HomeAssistantView): + """Handle pushes from the Foursquare API.""" + + requires_auth = False + url = "/api/foursquare" + name = "foursquare" + + def __init__(self, push_secret): + """Initialize the OAuth callback view.""" + self.push_secret = push_secret + + async def post(self, request): + """Accept the POST from Foursquare.""" + try: + data = await request.json() + except ValueError: + return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) + + secret = data.pop('secret', None) + + _LOGGER.debug("Received Foursquare push: %s", data) + + if self.push_secret != secret: + _LOGGER.error("Received Foursquare push with invalid" + "push secret: %s", secret) + return self.json_message('Incorrect secret', HTTP_BAD_REQUEST) + + request.app['hass'].bus.async_fire(EVENT_PUSH, data) diff --git a/homeassistant/components/foursquare/services.yaml b/homeassistant/components/foursquare/services.yaml new file mode 100644 index 0000000000000..3d15a9583f64f --- /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.py deleted file mode 100644 index 99efbae09843d..0000000000000 --- a/homeassistant/components/freebox.py +++ /dev/null @@ -1,91 +0,0 @@ -""" -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/ -""" -import logging -import socket - -import voluptuous as vol - -from homeassistant.components.discovery import SERVICE_FREEBOX -from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP -from homeassistant.helpers import config_validation as cv, discovery -from homeassistant.helpers.discovery import async_load_platform - -REQUIREMENTS = ['aiofreepybox==0.0.6'] - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "freebox" -DATA_FREEBOX = DOMAIN - -FREEBOX_CONFIG_FILE = 'freebox.conf' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PORT): cv.port - }) -}, extra=vol.ALLOW_EXTRA) - - -async def async_setup(hass, config): - """Set up the Freebox component.""" - conf = config.get(DOMAIN) - - async def discovery_dispatch(service, discovery_info): - if conf is None: - host = discovery_info.get('properties', {}).get('api_domain') - port = discovery_info.get('properties', {}).get('https_port') - _LOGGER.info("Discovered Freebox server: %s:%s", host, port) - await async_setup_freebox(hass, config, host, port) - - discovery.async_listen(hass, SERVICE_FREEBOX, discovery_dispatch) - - if conf is not None: - host = conf.get(CONF_HOST) - port = conf.get(CONF_PORT) - await async_setup_freebox(hass, config, host, port) - - return True - - -async def async_setup_freebox(hass, config, host, port): - """Start up the Freebox component platforms.""" - from aiofreepybox import Freepybox - from aiofreepybox.exceptions import HttpRequestError - - app_desc = { - 'app_id': 'hass', - 'app_name': 'Home Assistant', - 'app_version': '0.65', - 'device_name': socket.gethostname() - } - - token_file = hass.config.path(FREEBOX_CONFIG_FILE) - api_version = 'v1' - - fbx = Freepybox( - app_desc=app_desc, - token_file=token_file, - api_version=api_version) - - try: - await fbx.open(host, port) - except HttpRequestError: - _LOGGER.exception('Failed to connect to Freebox') - else: - hass.data[DATA_FREEBOX] = fbx - - hass.async_create_task(async_load_platform( - hass, 'sensor', DOMAIN, {}, config)) - hass.async_create_task(async_load_platform( - hass, 'device_tracker', DOMAIN, {}, config)) - - async def close_fbx(event): - """Close Freebox connection on HA Stop.""" - await fbx.close() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, close_fbx) diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py new file mode 100644 index 0000000000000..41e60d884ceb9 --- /dev/null +++ b/homeassistant/components/freebox/__init__.py @@ -0,0 +1,86 @@ +"""Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" +import logging +import socket + +import voluptuous as vol + +from homeassistant.components.discovery import SERVICE_FREEBOX +from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.helpers.discovery import async_load_platform + +REQUIREMENTS = ['aiofreepybox==0.0.6'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "freebox" +DATA_FREEBOX = DOMAIN + +FREEBOX_CONFIG_FILE = 'freebox.conf' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PORT): cv.port, + }) +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the Freebox component.""" + conf = config.get(DOMAIN) + + async def discovery_dispatch(service, discovery_info): + if conf is None: + host = discovery_info.get('properties', {}).get('api_domain') + port = discovery_info.get('properties', {}).get('https_port') + _LOGGER.info("Discovered Freebox server: %s:%s", host, port) + await async_setup_freebox(hass, config, host, port) + + discovery.async_listen(hass, SERVICE_FREEBOX, discovery_dispatch) + + if conf is not None: + host = conf.get(CONF_HOST) + port = conf.get(CONF_PORT) + await async_setup_freebox(hass, config, host, port) + + return True + + +async def async_setup_freebox(hass, config, host, port): + """Start up the Freebox component platforms.""" + from aiofreepybox import Freepybox + from aiofreepybox.exceptions import HttpRequestError + + app_desc = { + 'app_id': 'hass', + 'app_name': 'Home Assistant', + 'app_version': '0.65', + 'device_name': socket.gethostname() + } + + token_file = hass.config.path(FREEBOX_CONFIG_FILE) + api_version = 'v1' + + fbx = Freepybox( + app_desc=app_desc, + token_file=token_file, + api_version=api_version) + + try: + await fbx.open(host, port) + except HttpRequestError: + _LOGGER.exception('Failed to connect to Freebox') + else: + hass.data[DATA_FREEBOX] = fbx + + hass.async_create_task(async_load_platform( + hass, 'sensor', DOMAIN, {}, config)) + hass.async_create_task(async_load_platform( + hass, 'device_tracker', DOMAIN, {}, config)) + + async def close_fbx(event): + """Close Freebox connection on HA Stop.""" + await fbx.close() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, close_fbx) diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py new file mode 100644 index 0000000000000..fb94f7f56f5ff --- /dev/null +++ b/homeassistant/components/freebox/device_tracker.py @@ -0,0 +1,66 @@ +"""Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" +from collections import namedtuple +import logging + +from homeassistant.components.device_tracker import DeviceScanner +from homeassistant.components.freebox import DATA_FREEBOX + +DEPENDENCIES = ['freebox'] + +_LOGGER = logging.getLogger(__name__) + + +async def async_get_scanner(hass, config): + """Validate the configuration and return a Freebox scanner.""" + scanner = FreeboxDeviceScanner(hass.data[DATA_FREEBOX]) + await scanner.async_connect() + return scanner if scanner.success_init else None + +Device = namedtuple('Device', ['id', 'name', 'ip']) + + +def _build_device(device_dict): + return Device( + device_dict['l2ident']['id'], + device_dict['primary_name'], + device_dict['l3connectivities'][0]['addr']) + + +class FreeboxDeviceScanner(DeviceScanner): + """Queries the Freebox device.""" + + def __init__(self, fbx): + """Initialize the scanner.""" + self.last_results = {} + self.success_init = False + self.connection = fbx + + async def async_connect(self): + """Initialize connection to the router.""" + # Test the router is accessible. + data = await self.connection.lan.get_hosts_list() + 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 [device.id for device in self.last_results] + + async def get_device_name(self, device): + """Return the name of the given device or None if we don't know.""" + name = next(( + result.name for result in self.last_results + if result.id == device), None) + return name + + async def async_update_info(self): + """Ensure the information from the Freebox router is up to date.""" + _LOGGER.debug('Checking Devices') + + hosts = await self.connection.lan.get_hosts_list() + + last_results = [_build_device(device) + for device in hosts + if device['active']] + + self.last_results = last_results diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py new file mode 100644 index 0000000000000..49e68dc2c414d --- /dev/null +++ b/homeassistant/components/freebox/sensor.py @@ -0,0 +1,78 @@ +"""Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" +import logging + +from homeassistant.components.freebox import DATA_FREEBOX +from homeassistant.helpers.entity import Entity + +DEPENDENCIES = ['freebox'] + +_LOGGER = logging.getLogger(__name__) + + +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) + + +class FbxSensor(Entity): + """Representation of a freebox sensor.""" + + _name = 'generic' + + def __init__(self, fbx): + """Initialize the sensor.""" + self._fbx = fbx + self._state = None + self._datas = 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 + + async def async_update(self): + """Fetch status from freebox.""" + self._datas = await self._fbx.connection.get_status() + + +class FbxRXSensor(FbxSensor): + """Update the Freebox RxSensor.""" + + _name = 'Freebox download speed' + _unit = 'KB/s' + + @property + def unit_of_measurement(self): + """Define the unit.""" + return self._unit + + async def async_update(self): + """Get the value from fetched datas.""" + await super().async_update() + if self._datas is not None: + self._state = round(self._datas['rate_down'] / 1000, 2) + + +class FbxTXSensor(FbxSensor): + """Update the Freebox TxSensor.""" + + _name = 'Freebox upload speed' + _unit = 'KB/s' + + @property + def unit_of_measurement(self): + """Define the unit.""" + return self._unit + + async def async_update(self): + """Get the value from fetched datas.""" + await super().async_update() + if self._datas is not None: + self._state = round(self._datas['rate_up'] / 1000, 2) diff --git a/homeassistant/components/freedns.py b/homeassistant/components/freedns.py deleted file mode 100644 index ec38bb59cc782..0000000000000 --- a/homeassistant/components/freedns.py +++ /dev/null @@ -1,99 +0,0 @@ -""" -Integrate with FreeDNS Dynamic DNS service at freedns.afraid.org. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/freedns/ -""" -import asyncio -from datetime import timedelta -import logging - -import aiohttp -import async_timeout -import voluptuous as vol - -from homeassistant.const import (CONF_URL, CONF_ACCESS_TOKEN, - CONF_UPDATE_INTERVAL) -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'freedns' - -DEFAULT_INTERVAL = timedelta(minutes=10) - -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), - - }) -}, 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) - - session = hass.helpers.aiohttp_client.async_get_clientsession() - - result = await _update_freedns( - hass, session, url, auth_token) - - if result is False: - return False - - async def update_domain_callback(now): - """Update the FreeDNS entry.""" - await _update_freedns(hass, session, url, auth_token) - - hass.helpers.event.async_track_time_interval( - update_domain_callback, update_interval) - - return True - - -async def _update_freedns(hass, session, url, auth_token): - """Update FreeDNS.""" - params = None - - if url is None: - url = UPDATE_URL - - if auth_token is not None: - params = {} - params[auth_token] = "" - - try: - with async_timeout.timeout(TIMEOUT, loop=hass.loop): - resp = await session.get(url, params=params) - body = await resp.text() - - if "has not changed" in body: - # IP has not changed. - _LOGGER.debug("FreeDNS update skipped: IP has not changed") - return True - - if "ERROR" not in body: - _LOGGER.debug("Updating FreeDNS was successful: %s", body) - return True - - if "Invalid update URL" in body: - _LOGGER.error("FreeDNS update token is invalid") - else: - _LOGGER.warning("Updating FreeDNS failed: %s", body) - - except aiohttp.ClientError: - _LOGGER.warning("Can't connect to FreeDNS API") - - except asyncio.TimeoutError: - _LOGGER.warning("Timeout from FreeDNS API at %s", url) - - return False diff --git a/homeassistant/components/freedns/__init__.py b/homeassistant/components/freedns/__init__.py new file mode 100644 index 0000000000000..edb3a57c28ca5 --- /dev/null +++ b/homeassistant/components/freedns/__init__.py @@ -0,0 +1,110 @@ +""" +Integrate with FreeDNS Dynamic DNS service at freedns.afraid.org. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/freedns/ +""" +import asyncio +from datetime import timedelta +import logging + +import aiohttp +import async_timeout +import voluptuous as vol + +from homeassistant.const import (CONF_URL, CONF_ACCESS_TOKEN, + CONF_UPDATE_INTERVAL, CONF_SCAN_INTERVAL, + CONF_UPDATE_INTERVAL_INVALIDATION_VERSION) +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'freedns' + +DEFAULT_INTERVAL = timedelta(minutes=10) + +TIMEOUT = 10 +UPDATE_URL = 'https://freedns.afraid.org/dynamic/update.php' + +CONFIG_SCHEMA = vol.Schema({ + 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.""" + 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() + + result = await _update_freedns( + hass, session, url, auth_token) + + if result is False: + return False + + async def update_domain_callback(now): + """Update the FreeDNS entry.""" + await _update_freedns(hass, session, url, auth_token) + + hass.helpers.event.async_track_time_interval( + update_domain_callback, update_interval) + + return True + + +async def _update_freedns(hass, session, url, auth_token): + """Update FreeDNS.""" + params = None + + if url is None: + url = UPDATE_URL + + if auth_token is not None: + params = {} + params[auth_token] = "" + + try: + with async_timeout.timeout(TIMEOUT, loop=hass.loop): + resp = await session.get(url, params=params) + body = await resp.text() + + if "has not changed" in body: + # IP has not changed. + _LOGGER.debug("FreeDNS update skipped: IP has not changed") + return True + + if "ERROR" not in body: + _LOGGER.debug("Updating FreeDNS was successful: %s", body) + return True + + if "Invalid update URL" in body: + _LOGGER.error("FreeDNS update token is invalid") + else: + _LOGGER.warning("Updating FreeDNS failed: %s", body) + + except aiohttp.ClientError: + _LOGGER.warning("Can't connect to FreeDNS API") + + except asyncio.TimeoutError: + _LOGGER.warning("Timeout from FreeDNS API at %s", url) + + return False diff --git a/homeassistant/components/fritzbox.py b/homeassistant/components/fritzbox.py deleted file mode 100644 index ad3c7bc192979..0000000000000 --- a/homeassistant/components/fritzbox.py +++ /dev/null @@ -1,86 +0,0 @@ -""" -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/ -""" -import logging - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.const import ( - CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, - EVENT_HOMEASSISTANT_STOP) -from homeassistant.helpers import discovery - -_LOGGER = logging.getLogger(__name__) - -REQUIREMENTS = ['pyfritzhome==0.4.0'] - -SUPPORTED_DOMAINS = ['binary_sensor', 'climate', 'switch', 'sensor'] - -DOMAIN = 'fritzbox' - -ATTR_STATE_BATTERY_LOW = 'battery_low' -ATTR_STATE_DEVICE_LOCKED = 'device_locked' -ATTR_STATE_HOLIDAY_MODE = 'holiday_mode' -ATTR_STATE_LOCKED = 'locked' -ATTR_STATE_SUMMER_MODE = 'summer_mode' -ATTR_STATE_WINDOW_OPEN = 'window_open' - - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_DEVICES): - vol.All(cv.ensure_list, [ - vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - }), - ]), - }) -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the fritzbox component.""" - from pyfritzhome import Fritzhome, LoginError - - fritz_list = [] - - configured_devices = config[DOMAIN].get(CONF_DEVICES) - for device in configured_devices: - host = device.get(CONF_HOST) - username = device.get(CONF_USERNAME) - password = device.get(CONF_PASSWORD) - fritzbox = Fritzhome(host=host, user=username, - password=password) - try: - fritzbox.login() - _LOGGER.info("Connected to device %s", device) - except LoginError: - _LOGGER.warning("Login to Fritz!Box %s as %s failed", - host, username) - continue - - fritz_list.append(fritzbox) - - if not fritz_list: - _LOGGER.info("No fritzboxes configured") - return False - - hass.data[DOMAIN] = fritz_list - - def logout_fritzboxes(event): - """Close all connections to the fritzboxes.""" - for fritz in fritz_list: - fritz.logout() - - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, logout_fritzboxes) - - for domain in SUPPORTED_DOMAINS: - discovery.load_platform(hass, domain, DOMAIN, {}, config) - - return True diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py new file mode 100644 index 0000000000000..81ba019acbc0b --- /dev/null +++ b/homeassistant/components/fritzbox/__init__.py @@ -0,0 +1,81 @@ +"""Support for AVM Fritz!Box smarthome devices.""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import ( + CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, + EVENT_HOMEASSISTANT_STOP) +from homeassistant.helpers import discovery + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['pyfritzhome==0.4.0'] + +SUPPORTED_DOMAINS = ['binary_sensor', 'climate', 'switch', 'sensor'] + +DOMAIN = 'fritzbox' + +ATTR_STATE_BATTERY_LOW = 'battery_low' +ATTR_STATE_DEVICE_LOCKED = 'device_locked' +ATTR_STATE_HOLIDAY_MODE = 'holiday_mode' +ATTR_STATE_LOCKED = 'locked' +ATTR_STATE_SUMMER_MODE = 'summer_mode' +ATTR_STATE_WINDOW_OPEN = 'window_open' + + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_DEVICES): + vol.All(cv.ensure_list, [ + vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + }), + ]), + }) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the fritzbox component.""" + from pyfritzhome import Fritzhome, LoginError + + fritz_list = [] + + configured_devices = config[DOMAIN].get(CONF_DEVICES) + for device in configured_devices: + host = device.get(CONF_HOST) + username = device.get(CONF_USERNAME) + password = device.get(CONF_PASSWORD) + fritzbox = Fritzhome(host=host, user=username, + password=password) + try: + fritzbox.login() + _LOGGER.info("Connected to device %s", device) + except LoginError: + _LOGGER.warning("Login to Fritz!Box %s as %s failed", + host, username) + continue + + fritz_list.append(fritzbox) + + if not fritz_list: + _LOGGER.info("No fritzboxes configured") + return False + + hass.data[DOMAIN] = fritz_list + + def logout_fritzboxes(event): + """Close all connections to the fritzboxes.""" + for fritz in fritz_list: + fritz.logout() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, logout_fritzboxes) + + for domain in SUPPORTED_DOMAINS: + discovery.load_platform(hass, domain, DOMAIN, {}, config) + + return True diff --git a/homeassistant/components/fritzbox/binary_sensor.py b/homeassistant/components/fritzbox/binary_sensor.py new file mode 100644 index 0000000000000..c68c79f1e774b --- /dev/null +++ b/homeassistant/components/fritzbox/binary_sensor.py @@ -0,0 +1,59 @@ +"""Support for Fritzbox binary sensors.""" +import logging + +import requests + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.fritzbox import DOMAIN as FRITZBOX_DOMAIN + +DEPENDENCIES = ['fritzbox'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Fritzbox binary sensor platform.""" + devices = [] + fritz_list = hass.data[FRITZBOX_DOMAIN] + + for fritz in fritz_list: + device_list = fritz.get_devices() + for device in device_list: + if device.has_alarm: + devices.append(FritzboxBinarySensor(device, fritz)) + + add_entities(devices, True) + + +class FritzboxBinarySensor(BinarySensorDevice): + """Representation of a binary Fritzbox device.""" + + def __init__(self, device, fritz): + """Initialize the Fritzbox binary sensor.""" + self._device = device + self._fritz = fritz + + @property + def name(self): + """Return the name of the entity.""" + return self._device.name + + @property + def device_class(self): + """Return the class of this sensor.""" + return 'window' + + @property + def is_on(self): + """Return true if sensor is on.""" + if not self._device.present: + return False + return self._device.alert_state + + def update(self): + """Get latest data from the Fritzbox.""" + try: + self._device.update() + except requests.exceptions.HTTPError as ex: + _LOGGER.warning("Connection error: %s", ex) + self._fritz.login() diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py new file mode 100644 index 0000000000000..64d99ebf133fd --- /dev/null +++ b/homeassistant/components/fritzbox/climate.py @@ -0,0 +1,179 @@ +"""Support for AVM Fritz!Box smarthome thermostate devices.""" +import logging + +import requests + +from homeassistant.components.fritzbox import DOMAIN as FRITZBOX_DOMAIN +from homeassistant.components.fritzbox import ( + ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_BATTERY_LOW, ATTR_STATE_HOLIDAY_MODE, + ATTR_STATE_LOCKED, ATTR_STATE_SUMMER_MODE, + ATTR_STATE_WINDOW_OPEN) +from homeassistant.components.climate import ( + ATTR_OPERATION_MODE, ClimateDevice, STATE_ECO, STATE_HEAT, STATE_MANUAL, + STATE_OFF, STATE_ON, SUPPORT_OPERATION_MODE, + SUPPORT_TARGET_TEMPERATURE) +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, PRECISION_HALVES, TEMP_CELSIUS) + +DEPENDENCIES = ['fritzbox'] + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE) + +OPERATION_LIST = [STATE_HEAT, STATE_ECO, STATE_OFF, STATE_ON] + +MIN_TEMPERATURE = 8 +MAX_TEMPERATURE = 28 + +# special temperatures for on/off in Fritz!Box API (modified by pyfritzhome) +ON_API_TEMPERATURE = 127.0 +OFF_API_TEMPERATURE = 126.5 +ON_REPORT_SET_TEMPERATURE = 30.0 +OFF_REPORT_SET_TEMPERATURE = 0.0 + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Fritzbox smarthome thermostat platform.""" + devices = [] + fritz_list = hass.data[FRITZBOX_DOMAIN] + + for fritz in fritz_list: + device_list = fritz.get_devices() + for device in device_list: + if device.has_thermostat: + devices.append(FritzboxThermostat(device, fritz)) + + add_entities(devices) + + +class FritzboxThermostat(ClimateDevice): + """The thermostat class for Fritzbox smarthome thermostates.""" + + def __init__(self, device, fritz): + """Initialize the thermostat.""" + self._device = device + self._fritz = fritz + self._current_temperature = self._device.actual_temperature + self._target_temperature = self._device.target_temperature + self._comfort_temperature = self._device.comfort_temperature + self._eco_temperature = self._device.eco_temperature + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS + + @property + def available(self): + """Return if thermostat is available.""" + return self._device.present + + @property + def name(self): + """Return the name of the device.""" + return self._device.name + + @property + def temperature_unit(self): + """Return the unit of measurement that is used.""" + return TEMP_CELSIUS + + @property + def precision(self): + """Return precision 0.5.""" + return PRECISION_HALVES + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._current_temperature + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + if self._target_temperature in (ON_API_TEMPERATURE, + OFF_API_TEMPERATURE): + return None + return self._target_temperature + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + if ATTR_OPERATION_MODE in kwargs: + operation_mode = kwargs.get(ATTR_OPERATION_MODE) + self.set_operation_mode(operation_mode) + elif ATTR_TEMPERATURE in kwargs: + temperature = kwargs.get(ATTR_TEMPERATURE) + self._device.set_target_temperature(temperature) + + @property + def current_operation(self): + """Return the current operation mode.""" + if self._target_temperature == ON_API_TEMPERATURE: + return STATE_ON + if self._target_temperature == OFF_API_TEMPERATURE: + return STATE_OFF + if self._target_temperature == self._comfort_temperature: + return STATE_HEAT + if self._target_temperature == self._eco_temperature: + return STATE_ECO + return STATE_MANUAL + + @property + def operation_list(self): + """Return the list of available operation modes.""" + return OPERATION_LIST + + def set_operation_mode(self, operation_mode): + """Set new operation mode.""" + if operation_mode == STATE_HEAT: + self.set_temperature(temperature=self._comfort_temperature) + elif operation_mode == STATE_ECO: + self.set_temperature(temperature=self._eco_temperature) + elif operation_mode == STATE_OFF: + self.set_temperature(temperature=OFF_REPORT_SET_TEMPERATURE) + elif operation_mode == STATE_ON: + self.set_temperature(temperature=ON_REPORT_SET_TEMPERATURE) + + @property + def min_temp(self): + """Return the minimum temperature.""" + return MIN_TEMPERATURE + + @property + def max_temp(self): + """Return the maximum temperature.""" + return MAX_TEMPERATURE + + @property + def device_state_attributes(self): + """Return the device specific state attributes.""" + attrs = { + ATTR_STATE_BATTERY_LOW: self._device.battery_low, + ATTR_STATE_DEVICE_LOCKED: self._device.device_lock, + ATTR_STATE_LOCKED: self._device.lock, + } + + # the following attributes are available since fritzos 7 + if self._device.battery_level is not None: + attrs[ATTR_BATTERY_LEVEL] = self._device.battery_level + if self._device.holiday_active is not None: + attrs[ATTR_STATE_HOLIDAY_MODE] = self._device.holiday_active + if self._device.summer_active is not None: + attrs[ATTR_STATE_SUMMER_MODE] = self._device.summer_active + if ATTR_STATE_WINDOW_OPEN is not None: + attrs[ATTR_STATE_WINDOW_OPEN] = self._device.window_open + + return attrs + + def update(self): + """Update the data from the thermostat.""" + try: + self._device.update() + self._current_temperature = self._device.actual_temperature + self._target_temperature = self._device.target_temperature + self._comfort_temperature = self._device.comfort_temperature + self._eco_temperature = self._device.eco_temperature + except requests.exceptions.HTTPError as ex: + _LOGGER.warning("Fritzbox connection error: %s", ex) + self._fritz.login() diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py new file mode 100644 index 0000000000000..a1736fb985752 --- /dev/null +++ b/homeassistant/components/fritzbox/sensor.py @@ -0,0 +1,72 @@ +"""Support for AVM Fritz!Box smarthome temperature sensor only devices.""" +import logging + +import requests + +from homeassistant.components.fritzbox import DOMAIN as FRITZBOX_DOMAIN +from homeassistant.components.fritzbox import ( + ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_LOCKED) +from homeassistant.helpers.entity import Entity +from homeassistant.const import TEMP_CELSIUS + +DEPENDENCIES = ['fritzbox'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Fritzbox smarthome sensor platform.""" + _LOGGER.debug("Initializing fritzbox temperature sensors") + devices = [] + fritz_list = hass.data[FRITZBOX_DOMAIN] + + for fritz in fritz_list: + device_list = fritz.get_devices() + for device in device_list: + if (device.has_temperature_sensor + and not device.has_switch + and not device.has_thermostat): + devices.append(FritzBoxTempSensor(device, fritz)) + + add_entities(devices) + + +class FritzBoxTempSensor(Entity): + """The entity class for Fritzbox temperature sensors.""" + + def __init__(self, device, fritz): + """Initialize the switch.""" + self._device = device + self._fritz = fritz + + @property + def name(self): + """Return the name of the device.""" + return self._device.name + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.temperature + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + def update(self): + """Get latest data and states from the device.""" + try: + self._device.update() + except requests.exceptions.HTTPError as ex: + _LOGGER.warning("Fritzhome connection error: %s", ex) + self._fritz.login() + + @property + def device_state_attributes(self): + """Return the state attributes of the device.""" + attrs = { + ATTR_STATE_DEVICE_LOCKED: self._device.device_lock, + ATTR_STATE_LOCKED: self._device.lock, + } + return attrs diff --git a/homeassistant/components/fritzbox/switch.py b/homeassistant/components/fritzbox/switch.py new file mode 100644 index 0000000000000..be9212793ab3a --- /dev/null +++ b/homeassistant/components/fritzbox/switch.py @@ -0,0 +1,99 @@ +"""Support for AVM Fritz!Box smarthome switch devices.""" +import logging + +import requests + +from homeassistant.components.fritzbox import DOMAIN as FRITZBOX_DOMAIN +from homeassistant.components.fritzbox import ( + ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_LOCKED) +from homeassistant.components.switch import SwitchDevice +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS + +DEPENDENCIES = ['fritzbox'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_TOTAL_CONSUMPTION = 'total_consumption' +ATTR_TOTAL_CONSUMPTION_UNIT = 'total_consumption_unit' +ATTR_TOTAL_CONSUMPTION_UNIT_VALUE = 'kWh' + +ATTR_TEMPERATURE_UNIT = 'temperature_unit' + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Fritzbox smarthome switch platform.""" + devices = [] + fritz_list = hass.data[FRITZBOX_DOMAIN] + + for fritz in fritz_list: + device_list = fritz.get_devices() + for device in device_list: + if device.has_switch: + devices.append(FritzboxSwitch(device, fritz)) + + add_entities(devices) + + +class FritzboxSwitch(SwitchDevice): + """The switch class for Fritzbox switches.""" + + def __init__(self, device, fritz): + """Initialize the switch.""" + self._device = device + self._fritz = fritz + + @property + def available(self): + """Return if switch is available.""" + return self._device.present + + @property + def name(self): + """Return the name of the device.""" + return self._device.name + + @property + def is_on(self): + """Return true if the switch is on.""" + return self._device.switch_state + + def turn_on(self, **kwargs): + """Turn the switch on.""" + self._device.set_switch_state_on() + + def turn_off(self, **kwargs): + """Turn the switch off.""" + self._device.set_switch_state_off() + + def update(self): + """Get latest data and states from the device.""" + try: + self._device.update() + except requests.exceptions.HTTPError as ex: + _LOGGER.warning("Fritzhome connection error: %s", ex) + self._fritz.login() + + @property + def device_state_attributes(self): + """Return the state attributes of the device.""" + attrs = {} + attrs[ATTR_STATE_DEVICE_LOCKED] = self._device.device_lock + attrs[ATTR_STATE_LOCKED] = self._device.lock + + if self._device.has_powermeter: + attrs[ATTR_TOTAL_CONSUMPTION] = "{:.3f}".format( + (self._device.energy or 0.0) / 1000) + attrs[ATTR_TOTAL_CONSUMPTION_UNIT] = \ + ATTR_TOTAL_CONSUMPTION_UNIT_VALUE + if self._device.has_temperature_sensor: + attrs[ATTR_TEMPERATURE] = \ + str(self.hass.config.units.temperature( + self._device.temperature, TEMP_CELSIUS)) + attrs[ATTR_TEMPERATURE_UNIT] = \ + self.hass.config.units.temperature_unit + return attrs + + @property + def current_power_w(self): + """Return the current power usage in W.""" + return self._device.power / 1000 diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 46652b4d7b014..caf6bbccb5c3f 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.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 0000000000000..f01abc79e8e98 --- /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.py deleted file mode 100644 index 0d4b19da03009..0000000000000 --- a/homeassistant/components/gc100.py +++ /dev/null @@ -1,74 +0,0 @@ -""" -Support for controlling Global Cache gc100. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/gc100/ -""" -import logging - -import voluptuous as vol - -from homeassistant.const import ( - EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_PORT) -import homeassistant.helpers.config_validation as cv - -REQUIREMENTS = ['python-gc100==1.0.3a'] - -_LOGGER = logging.getLogger(__name__) - -CONF_PORTS = 'ports' - -DEFAULT_PORT = 4998 -DOMAIN = 'gc100' - -DATA_GC100 = 'gc100' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - }), -}, extra=vol.ALLOW_EXTRA) - - -# pylint: disable=no-member -def setup(hass, base_config): - """Set up the gc100 component.""" - import gc100 - - config = base_config[DOMAIN] - host = config[CONF_HOST] - port = config[CONF_PORT] - - gc_device = gc100.GC100SocketClient(host, port) - - def cleanup_gc100(event): - """Stuff to do before stopping.""" - gc_device.quit() - - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_gc100) - - hass.data[DATA_GC100] = GC100Device(hass, gc_device) - - return True - - -class GC100Device: - """The GC100 component.""" - - def __init__(self, hass, gc_device): - """Init a gc100 device.""" - self.hass = hass - self.gc_device = gc_device - - def read_sensor(self, port_addr, callback): - """Read a value from a digital input.""" - self.gc_device.read_sensor(port_addr, callback) - - def write_switch(self, port_addr, state, callback): - """Write a value to a relay.""" - self.gc_device.write_switch(port_addr, state, callback) - - def subscribe(self, port_addr, callback): - """Add detection for RISING and FALLING events.""" - self.gc_device.subscribe_notify(port_addr, callback) diff --git a/homeassistant/components/gc100/__init__.py b/homeassistant/components/gc100/__init__.py new file mode 100644 index 0000000000000..36e9c61b1ba1e --- /dev/null +++ b/homeassistant/components/gc100/__init__.py @@ -0,0 +1,69 @@ +"""Support for controlling Global Cache gc100.""" +import logging + +import voluptuous as vol + +from homeassistant.const import ( + EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_PORT) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['python-gc100==1.0.3a'] + +_LOGGER = logging.getLogger(__name__) + +CONF_PORTS = 'ports' + +DEFAULT_PORT = 4998 +DOMAIN = 'gc100' + +DATA_GC100 = 'gc100' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + }), +}, extra=vol.ALLOW_EXTRA) + + +# pylint: disable=no-member +def setup(hass, base_config): + """Set up the gc100 component.""" + import gc100 + + config = base_config[DOMAIN] + host = config[CONF_HOST] + port = config[CONF_PORT] + + gc_device = gc100.GC100SocketClient(host, port) + + def cleanup_gc100(event): + """Stuff to do before stopping.""" + gc_device.quit() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_gc100) + + hass.data[DATA_GC100] = GC100Device(hass, gc_device) + + return True + + +class GC100Device: + """The GC100 component.""" + + def __init__(self, hass, gc_device): + """Init a gc100 device.""" + self.hass = hass + self.gc_device = gc_device + + def read_sensor(self, port_addr, callback): + """Read a value from a digital input.""" + self.gc_device.read_sensor(port_addr, callback) + + def write_switch(self, port_addr, state, callback): + """Write a value to a relay.""" + self.gc_device.write_switch(port_addr, state, callback) + + def subscribe(self, port_addr, callback): + """Add detection for RISING and FALLING events.""" + self.gc_device.subscribe_notify(port_addr, callback) 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 4597a56c61aa4..9095ce617aab1 100644 --- a/homeassistant/components/geo_location/__init__.py +++ b/homeassistant/components/geo_location/__init__.py @@ -9,7 +9,8 @@ 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 0000000000000..1390dfb504a61 --- /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 0000000000000..142f40754b9dd --- /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 8a857acbdc6c3..db60ec18fe195 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 0000000000000..04aec33b5d686 --- /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 34290b35f4286..2460e28393ac5 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 0000000000000..af8bb60f8b1dc --- /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 0000000000000..f2d5ad09350f3 --- /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 0000000000000..c4b490ab316b2 --- /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 0000000000000..90b9cda80bbfa --- /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 0000000000000..6d5c2185718a3 --- /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 0000000000000..ae2b217771216 --- /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 a65e51d7cae5c..2c8881034ff5f 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 0000000000000..d0dece65a0f18 --- /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 34b7e9072887f..ac9c1c2d43ebe 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 b6dcd65fc2c89..d1cd88a8438d6 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -23,6 +23,8 @@ 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 0000000000000..1cf1793e6f6f2 --- /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 0000000000000..079b57722e213 --- /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 af0e76829e542..b1bcf5725bef6 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 0000000000000..25a33929c1a80 --- /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/harmony/remote.py b/homeassistant/components/harmony/remote.py new file mode 100644 index 0000000000000..489fe9144f2b5 --- /dev/null +++ b/homeassistant/components/harmony/remote.py @@ -0,0 +1,422 @@ +""" +Support for Harmony Hub devices. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/remote.harmony/ +""" +import asyncio +import json +import logging + +import voluptuous as vol + +from homeassistant.components import remote +from homeassistant.components.remote import ( + ATTR_ACTIVITY, ATTR_DELAY_SECS, ATTR_DEVICE, ATTR_NUM_REPEATS, + DEFAULT_DELAY_SECS, DOMAIN, PLATFORM_SCHEMA +) +from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_STOP +) +import homeassistant.helpers.config_validation as cv +from homeassistant.exceptions import PlatformNotReady +from homeassistant.util import slugify + +REQUIREMENTS = ['aioharmony==0.1.8'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_CHANNEL = 'channel' +ATTR_CURRENT_ACTIVITY = 'current_activity' + +DEFAULT_PORT = 8088 +DEVICES = [] +CONF_DEVICE_CACHE = 'harmony_device_cache' + +SERVICE_SYNC = 'harmony_sync' +SERVICE_CHANGE_CHANNEL = 'harmony_change_channel' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(ATTR_ACTIVITY): cv.string, + vol.Required(CONF_NAME): cv.string, + vol.Optional(ATTR_DELAY_SECS, default=DEFAULT_DELAY_SECS): + vol.Coerce(float), + vol.Optional(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, +}) + +HARMONY_SYNC_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, +}) + +HARMONY_CHANGE_CHANNEL_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_CHANNEL): cv.positive_int +}) + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the Harmony platform.""" + activity = None + + if CONF_DEVICE_CACHE not in hass.data: + hass.data[CONF_DEVICE_CACHE] = [] + + if discovery_info: + # Find the discovered device in the list of user configurations + override = next((c for c in hass.data[CONF_DEVICE_CACHE] + if c.get(CONF_NAME) == discovery_info.get(CONF_NAME)), + None) + + port = DEFAULT_PORT + delay_secs = DEFAULT_DELAY_SECS + if override is not None: + activity = override.get(ATTR_ACTIVITY) + delay_secs = override.get(ATTR_DELAY_SECS) + port = override.get(CONF_PORT, DEFAULT_PORT) + + host = ( + discovery_info.get(CONF_NAME), + discovery_info.get(CONF_HOST), + port) + + # Ignore hub name when checking if this hub is known - ip and port only + if host[1:] in ((h.host, h.port) for h in DEVICES): + _LOGGER.debug("Discovered host already known: %s", host) + return + elif CONF_HOST in config: + host = ( + config.get(CONF_NAME), + config.get(CONF_HOST), + config.get(CONF_PORT), + ) + activity = config.get(ATTR_ACTIVITY) + delay_secs = config.get(ATTR_DELAY_SECS) + else: + hass.data[CONF_DEVICE_CACHE].append(config) + return + + name, address, port = host + _LOGGER.info("Loading Harmony Platform: %s at %s:%s, startup activity: %s", + name, address, port, activity) + + harmony_conf_file = hass.config.path( + '{}{}{}'.format('harmony_', slugify(name), '.conf')) + try: + device = HarmonyRemote( + name, address, port, activity, harmony_conf_file, delay_secs) + if not await device.connect(): + raise PlatformNotReady + + DEVICES.append(device) + async_add_entities([device]) + register_services(hass) + except (ValueError, AttributeError): + raise PlatformNotReady + + +def register_services(hass): + """Register all services for harmony devices.""" + hass.services.async_register( + DOMAIN, SERVICE_SYNC, _sync_service, + schema=HARMONY_SYNC_SCHEMA) + + hass.services.async_register( + DOMAIN, SERVICE_CHANGE_CHANNEL, _change_channel_service, + schema=HARMONY_CHANGE_CHANNEL_SCHEMA) + + +async def _apply_service(service, service_func, *service_func_args): + """Handle services to apply.""" + entity_ids = service.data.get('entity_id') + + if entity_ids: + _devices = [device for device in DEVICES + if device.entity_id in entity_ids] + else: + _devices = DEVICES + + for device in _devices: + await service_func(device, *service_func_args) + + +async def _sync_service(service): + await _apply_service(service, HarmonyRemote.sync) + + +async def _change_channel_service(service): + channel = service.data.get(ATTR_CHANNEL) + await _apply_service(service, HarmonyRemote.change_channel, channel) + + +class HarmonyRemote(remote.RemoteDevice): + """Remote representation used to control a Harmony device.""" + + def __init__(self, name, host, port, activity, out_path, delay_secs): + """Initialize HarmonyRemote class.""" + from aioharmony.harmonyapi import HarmonyAPI as HarmonyClient + + self._name = name + self.host = host + self.port = port + self._state = None + self._current_activity = None + self._default_activity = activity + self._client = HarmonyClient(ip_address=host) + self._config_path = out_path + self._delay_secs = delay_secs + self._available = False + + async def async_added_to_hass(self): + """Complete the initialization.""" + from aioharmony.harmonyapi import ClientCallbackType + + _LOGGER.debug("%s: Harmony Hub added", self._name) + # Register the callbacks + self._client.callbacks = ClientCallbackType( + new_activity=self.new_activity, + config_updated=self.new_config, + connect=self.got_connected, + disconnect=self.got_disconnected + ) + + # Store Harmony HUB config, this will also update our current + # activity + await self.new_config() + + import aioharmony.exceptions as aioexc + + async def shutdown(_): + """Close connection on shutdown.""" + _LOGGER.debug("%s: Closing Harmony Hub", self._name) + try: + await self._client.close() + except aioexc.TimeOut: + _LOGGER.warning("%s: Disconnect timed-out", self._name) + + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) + + @property + def name(self): + """Return the Harmony device's name.""" + return self._name + + @property + def should_poll(self): + """Return the fact that we should not be polled.""" + return False + + @property + def device_state_attributes(self): + """Add platform specific attributes.""" + return {ATTR_CURRENT_ACTIVITY: self._current_activity} + + @property + def is_on(self): + """Return False if PowerOff is the current activity, otherwise True.""" + return self._current_activity not in [None, 'PowerOff'] + + @property + def available(self): + """Return True if connected to Hub, otherwise False.""" + return self._available + + async def connect(self): + """Connect to the Harmony HUB.""" + import aioharmony.exceptions as aioexc + + _LOGGER.debug("%s: Connecting", self._name) + try: + if not await self._client.connect(): + _LOGGER.warning("%s: Unable to connect to HUB.", self._name) + await self._client.close() + return False + except aioexc.TimeOut: + _LOGGER.warning("%s: Connection timed-out", self._name) + return False + + return True + + def new_activity(self, activity_info: tuple) -> None: + """Call for updating the current activity.""" + activity_id, activity_name = activity_info + _LOGGER.debug("%s: activity reported as: %s", self._name, + activity_name) + self._current_activity = activity_name + self._state = bool(activity_id != -1) + self._available = True + self.async_schedule_update_ha_state() + + async def new_config(self, _=None): + """Call for updating the current activity.""" + _LOGGER.debug("%s: configuration has been updated", self._name) + self.new_activity(self._client.current_activity) + await self.hass.async_add_executor_job(self.write_config_file) + + async def got_connected(self, _=None): + """Notification that we're connected to the HUB.""" + _LOGGER.debug("%s: connected to the HUB.", self._name) + if not self._available: + # We were disconnected before. + await self.new_config() + + async def got_disconnected(self, _=None): + """Notification that we're disconnected from the HUB.""" + _LOGGER.debug("%s: disconnected from the HUB.", self._name) + self._available = False + # We're going to wait for 10 seconds before announcing we're + # unavailable, this to allow a reconnection to happen. + await asyncio.sleep(10) + + if not self._available: + # Still disconnected. Let the state engine know. + self.async_schedule_update_ha_state() + + async def async_turn_on(self, **kwargs): + """Start an activity from the Harmony device.""" + import aioharmony.exceptions as aioexc + + _LOGGER.debug("%s: Turn On", self.name) + + activity = kwargs.get(ATTR_ACTIVITY, self._default_activity) + + if activity: + activity_id = None + if activity.isdigit() or activity == '-1': + _LOGGER.debug("%s: Activity is numeric", self.name) + if self._client.get_activity_name(int(activity)): + activity_id = activity + + if activity_id is None: + _LOGGER.debug("%s: Find activity ID based on name", self.name) + activity_id = self._client.get_activity_id( + str(activity).strip()) + + if activity_id is None: + _LOGGER.error("%s: Activity %s is invalid", + self.name, activity) + return + + try: + await self._client.start_activity(activity_id) + except aioexc.TimeOut: + _LOGGER.error("%s: Starting activity %s timed-out", + self.name, + activity) + else: + _LOGGER.error("%s: No activity specified with turn_on service", + self.name) + + async def async_turn_off(self, **kwargs): + """Start the PowerOff activity.""" + import aioharmony.exceptions as aioexc + _LOGGER.debug("%s: Turn Off", self.name) + try: + await self._client.power_off() + except aioexc.TimeOut: + _LOGGER.error("%s: Powering off timed-out", self.name) + + # pylint: disable=arguments-differ + async def async_send_command(self, command, **kwargs): + """Send a list of commands to one device.""" + from aioharmony.harmonyapi import SendCommandDevice + import aioharmony.exceptions as aioexc + + _LOGGER.debug("%s: Send Command", self.name) + device = kwargs.get(ATTR_DEVICE) + if device is None: + _LOGGER.error("%s: Missing required argument: device", self.name) + return + + device_id = None + if device.isdigit(): + _LOGGER.debug("%s: Device %s is numeric", + self.name, device) + if self._client.get_device_name(int(device)): + device_id = device + + if device_id is None: + _LOGGER.debug("%s: Find device ID %s based on device name", + self.name, device) + device_id = self._client.get_device_id(str(device).strip()) + + if device_id is None: + _LOGGER.error("%s: Device %s is invalid", self.name, device) + return + + num_repeats = kwargs.get(ATTR_NUM_REPEATS) + delay_secs = kwargs.get(ATTR_DELAY_SECS, self._delay_secs) + + # Creating list of commands to send. + snd_cmnd_list = [] + for _ in range(num_repeats): + for single_command in command: + send_command = SendCommandDevice( + device=device_id, + command=single_command, + delay=0 + ) + snd_cmnd_list.append(send_command) + if delay_secs > 0: + snd_cmnd_list.append(float(delay_secs)) + + _LOGGER.debug("%s: Sending commands", self.name) + try: + result_list = await self._client.send_commands(snd_cmnd_list) + except aioexc.TimeOut: + _LOGGER.error("%s: Sending commands timed-out", self.name) + return + + for result in result_list: + _LOGGER.error("Sending command %s to device %s failed with code " + "%s: %s", + result.command.command, + result.command.device, + result.code, + result.msg + ) + + async def change_channel(self, channel): + """Change the channel using Harmony remote.""" + import aioharmony.exceptions as aioexc + + _LOGGER.debug("%s: Changing channel to %s", + self.name, channel) + try: + await self._client.change_channel(channel) + except aioexc.TimeOut: + _LOGGER.error("%s: Changing channel to %s timed-out", + self.name, + channel) + + async def sync(self): + """Sync the Harmony device with the web service.""" + import aioharmony.exceptions as aioexc + + _LOGGER.debug("%s: Syncing hub with Harmony cloud", self.name) + try: + await self._client.sync() + except aioexc.TimeOut: + _LOGGER.error("%s: Syncing hub with Harmony cloud timed-out", + self.name) + else: + await self.hass.async_add_executor_job(self.write_config_file) + + def write_config_file(self): + """Write Harmony configuration file.""" + _LOGGER.debug("%s: Writing hub config to file: %s", + self.name, + self._config_path) + if self._client.config is None: + _LOGGER.warning("%s: No configuration received from hub", + self.name) + return + + try: + with open(self._config_path, 'w+', encoding='utf-8') as file_out: + json.dump(self._client.json_config, file_out, + sort_keys=True, indent=4) + except IOError as exc: + _LOGGER.error("%s: Unable to write HUB configuration to %s: %s", + self.name, self._config_path, exc) diff --git a/homeassistant/components/hassio/services.yaml b/homeassistant/components/hassio/services.yaml new file mode 100644 index 0000000000000..33574c5dd713b --- /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/hdmi_cec/media_player.py b/homeassistant/components/hdmi_cec/media_player.py new file mode 100644 index 0000000000000..6e691cad94fc7 --- /dev/null +++ b/homeassistant/components/hdmi_cec/media_player.py @@ -0,0 +1,177 @@ +""" +Support for HDMI CEC devices as media players. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/hdmi_cec/ +""" +import logging + +from homeassistant.components.hdmi_cec import ATTR_NEW, CecDevice +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) +from homeassistant.const import ( + STATE_IDLE, STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING) + +DEPENDENCIES = ['hdmi_cec'] + +_LOGGER = logging.getLogger(__name__) + +ENTITY_ID_FORMAT = DOMAIN + '.{}' + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Find and return HDMI devices as +switches.""" + if ATTR_NEW in discovery_info: + _LOGGER.info("Setting up HDMI devices %s", discovery_info[ATTR_NEW]) + entities = [] + for device in discovery_info[ATTR_NEW]: + hdmi_device = hass.data.get(device) + entities.append(CecPlayerDevice( + hdmi_device, hdmi_device.logical_address, + )) + add_entities(entities, True) + + +class CecPlayerDevice(CecDevice, MediaPlayerDevice): + """Representation of a HDMI device as a Media player.""" + + def __init__(self, device, logical) -> None: + """Initialize the HDMI device.""" + CecDevice.__init__(self, device, logical) + self.entity_id = "%s.%s_%s" % ( + DOMAIN, 'hdmi', hex(self._logical_address)[2:]) + + def send_keypress(self, key): + """Send keypress to CEC adapter.""" + from pycec.commands import KeyPressCommand, KeyReleaseCommand + _LOGGER.debug("Sending keypress %s to device %s", hex(key), + hex(self._logical_address)) + self._device.send_command( + KeyPressCommand(key, dst=self._logical_address)) + self._device.send_command( + KeyReleaseCommand(dst=self._logical_address)) + + def send_playback(self, key): + """Send playback status to CEC adapter.""" + from pycec.commands import CecCommand + self._device.async_send_command( + CecCommand(key, dst=self._logical_address)) + + def mute_volume(self, mute): + """Mute volume.""" + from pycec.const import KEY_MUTE_TOGGLE + self.send_keypress(KEY_MUTE_TOGGLE) + + def media_previous_track(self): + """Go to previous track.""" + from pycec.const import KEY_BACKWARD + self.send_keypress(KEY_BACKWARD) + + def turn_on(self): + """Turn device on.""" + self._device.turn_on() + self._state = STATE_ON + + def clear_playlist(self): + """Clear players playlist.""" + raise NotImplementedError() + + def turn_off(self): + """Turn device off.""" + self._device.turn_off() + self._state = STATE_OFF + + def media_stop(self): + """Stop playback.""" + from pycec.const import KEY_STOP + self.send_keypress(KEY_STOP) + self._state = STATE_IDLE + + def play_media(self, media_type, media_id, **kwargs): + """Not supported.""" + raise NotImplementedError() + + def media_next_track(self): + """Skip to next track.""" + from pycec.const import KEY_FORWARD + self.send_keypress(KEY_FORWARD) + + def media_seek(self, position): + """Not supported.""" + raise NotImplementedError() + + def set_volume_level(self, volume): + """Set volume level, range 0..1.""" + raise NotImplementedError() + + def media_pause(self): + """Pause playback.""" + from pycec.const import KEY_PAUSE + self.send_keypress(KEY_PAUSE) + self._state = STATE_PAUSED + + def select_source(self, source): + """Not supported.""" + raise NotImplementedError() + + def media_play(self): + """Start playback.""" + from pycec.const import KEY_PLAY + self.send_keypress(KEY_PLAY) + self._state = STATE_PLAYING + + def volume_up(self): + """Increase volume.""" + from pycec.const import KEY_VOLUME_UP + _LOGGER.debug("%s: volume up", self._logical_address) + self.send_keypress(KEY_VOLUME_UP) + + def volume_down(self): + """Decrease volume.""" + from pycec.const import KEY_VOLUME_DOWN + _LOGGER.debug("%s: volume down", self._logical_address) + self.send_keypress(KEY_VOLUME_DOWN) + + @property + def state(self) -> str: + """Cache state of device.""" + return self._state + + def update(self): + """Update device status.""" + device = self._device + from pycec.const import STATUS_PLAY, STATUS_STOP, STATUS_STILL, \ + POWER_OFF, POWER_ON + if device.power_status in [POWER_OFF, 3]: + self._state = STATE_OFF + elif not self.support_pause: + if device.power_status in [POWER_ON, 4]: + self._state = STATE_ON + elif device.status == STATUS_PLAY: + self._state = STATE_PLAYING + elif device.status == STATUS_STOP: + self._state = STATE_IDLE + elif device.status == STATUS_STILL: + self._state = STATE_PAUSED + else: + _LOGGER.warning("Unknown state: %s", device.status) + + @property + def supported_features(self): + """Flag media player features that are supported.""" + from pycec.const import TYPE_RECORDER, TYPE_PLAYBACK, TYPE_TUNER, \ + TYPE_AUDIO + if self.type_id == TYPE_RECORDER or self.type == TYPE_PLAYBACK: + return (SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PLAY_MEDIA | + SUPPORT_PAUSE | SUPPORT_STOP | SUPPORT_PREVIOUS_TRACK | + SUPPORT_NEXT_TRACK) + if self.type == TYPE_TUNER: + return (SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PLAY_MEDIA | + SUPPORT_PAUSE | SUPPORT_STOP) + if self.type_id == TYPE_AUDIO: + return (SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_VOLUME_STEP | + SUPPORT_VOLUME_MUTE) + return SUPPORT_TURN_ON | SUPPORT_TURN_OFF diff --git a/homeassistant/components/hdmi_cec/services.yaml b/homeassistant/components/hdmi_cec/services.yaml new file mode 100644 index 0000000000000..bb0f5f932aeaa --- /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 5baed0294b852..ca1b560e336c4 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -124,6 +124,8 @@ def update_battery(self, new_state): """ 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 d0e3d52b363e0..1b2a4dbf05d2a 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -113,6 +113,7 @@ 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 2b4e55c4c8dc5..dcc93b7cf9e49 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -4,17 +4,20 @@ 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 @@ def __init__(self, *args): 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 @@ def __init__(self, *args): 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 @@ def set_oscillating(self, value): 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 @@ def update_state(self, new_state): 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 10fdc07e7b425..f1327f8b5270f 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 9a496d914fc57..3439a23adb3bc 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -90,7 +90,7 @@ '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 7473b4a7b8674..4b8371fc748ac 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 b0ea1a3b348f1..f048a50d1d0e4 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -19,7 +19,7 @@ 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/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py new file mode 100644 index 0000000000000..80fc8f7b43065 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -0,0 +1,74 @@ +""" +Support for HomematicIP Cloud cover devices. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/cover.homematicip_cloud/ +""" +import logging + +from homeassistant.components.cover import ( + ATTR_POSITION, CoverDevice) +from homeassistant.components.homematicip_cloud import ( + HMIPC_HAPID, HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN) + +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): + """Set up the HomematicIP Cloud cover devices.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the HomematicIP cover from a config entry.""" + from homematicip.aio.device import AsyncFullFlushShutter + + home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home + devices = [] + for device in home.devices: + if isinstance(device, AsyncFullFlushShutter): + devices.append(HomematicipCoverShutter(home, device)) + + if devices: + async_add_entities(devices) + + +class HomematicipCoverShutter(HomematicipGenericDevice, CoverDevice): + """Representation of a HomematicIP Cloud cover device.""" + + @property + def current_cover_position(self): + """Return current position of cover.""" + return int(self._device.shutterLevel * 100) + + async def async_set_cover_position(self, **kwargs): + """Move the cover to a specific position.""" + position = kwargs[ATTR_POSITION] + # 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 == HMIP_COVER_CLOSED + return None + + async def async_open_cover(self, **kwargs): + """Open the cover.""" + 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(HMIP_COVER_CLOSED) + + async def async_stop_cover(self, **kwargs): + """Stop the device if in motion.""" + await self._device.set_shutter_stop() diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 37507a1fca83d..9af6669652d9c 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 @@ def __init__(self, hass, config_entry): 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 @@ async def async_setup(self, tries=0): 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 @@ async def async_connect(self): 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 0000000000000..5d604d2c66568 --- /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/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py new file mode 100644 index 0000000000000..911c00e45bc13 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -0,0 +1,220 @@ +""" +Support for HomematicIP Cloud sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.homematicip_cloud/ +""" +import logging + +from homeassistant.components.homematicip_cloud import ( + DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice) +from homeassistant.const import ( + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['homematicip_cloud'] + +ATTR_VALVE_STATE = 'valve_state' +ATTR_VALVE_POSITION = 'valve_position' +ATTR_TEMPERATURE = 'temperature' +ATTR_TEMPERATURE_OFFSET = 'temperature_offset' +ATTR_HUMIDITY = 'humidity' + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Set up the HomematicIP Cloud sensors devices.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the HomematicIP Cloud sensors from a config entry.""" + from homematicip.aio.device import ( + AsyncHeatingThermostat, AsyncTemperatureHumiditySensorWithoutDisplay, + AsyncTemperatureHumiditySensorDisplay, AsyncMotionDetectorIndoor, + AsyncTemperatureHumiditySensorOutdoor, + AsyncMotionDetectorPushButton, AsyncLightSensor, + AsyncPlugableSwitchMeasuring, AsyncBrandSwitchMeasuring, + AsyncFullFlushSwitchMeasuring) + + home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home + devices = [HomematicipAccesspointStatus(home)] + for device in home.devices: + if isinstance(device, AsyncHeatingThermostat): + devices.append(HomematicipHeatingThermostat(home, device)) + if isinstance(device, (AsyncTemperatureHumiditySensorDisplay, + AsyncTemperatureHumiditySensorWithoutDisplay, + AsyncTemperatureHumiditySensorOutdoor)): + devices.append(HomematicipTemperatureSensor(home, device)) + devices.append(HomematicipHumiditySensor(home, device)) + 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) + + +class HomematicipAccesspointStatus(HomematicipGenericDevice): + """Representation of an HomeMaticIP Cloud access point.""" + + def __init__(self, home): + """Initialize access point device.""" + super().__init__(home, home) + + @property + def icon(self): + """Return the icon of the access point device.""" + return 'mdi:access-point-network' + + @property + def state(self): + """Return the state of the access point.""" + return self._home.dutyCycle + + @property + def available(self): + """Device available.""" + return self._home.connected + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return '%' + + +class HomematicipHeatingThermostat(HomematicipGenericDevice): + """Represenation of a HomematicIP heating thermostat device.""" + + def __init__(self, home, device): + """Initialize heating thermostat device.""" + super().__init__(home, device, 'Heating') + + @property + def icon(self): + """Return the icon.""" + from homematicip.base.enums import ValveState + + if super().icon: + return super().icon + if self._device.valveState != ValveState.ADAPTION_DONE: + return 'mdi:alert' + return 'mdi:radiator' + + @property + def state(self): + """Return the state of the radiator valve.""" + from homematicip.base.enums import ValveState + + if self._device.valveState != ValveState.ADAPTION_DONE: + return self._device.valveState + return round(self._device.valvePosition*100) + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return '%' + + +class HomematicipHumiditySensor(HomematicipGenericDevice): + """Represenation of a HomematicIP Cloud humidity device.""" + + def __init__(self, home, device): + """Initialize the thermometer device.""" + super().__init__(home, device, 'Humidity') + + @property + def device_class(self): + """Return the device class of the sensor.""" + return DEVICE_CLASS_HUMIDITY + + @property + def state(self): + """Return the state.""" + return self._device.humidity + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return '%' + + +class HomematicipTemperatureSensor(HomematicipGenericDevice): + """Representation of a HomematicIP Cloud thermometer device.""" + + def __init__(self, home, device): + """Initialize the thermometer device.""" + super().__init__(home, device, 'Temperature') + + @property + def device_class(self): + """Return the device class of the sensor.""" + return DEVICE_CLASS_TEMPERATURE + + @property + def state(self): + """Return the state.""" + return self._device.actualTemperature + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return TEMP_CELSIUS + + +class HomematicipIlluminanceSensor(HomematicipGenericDevice): + """Represenation of a HomematicIP Illuminance device.""" + + def __init__(self, home, device): + """Initialize the device.""" + super().__init__(home, device, 'Illuminance') + + @property + def device_class(self): + """Return the device class of the sensor.""" + return DEVICE_CLASS_ILLUMINANCE + + @property + def state(self): + """Return the state.""" + return self._device.illumination + + @property + 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 0000000000000..a406a7ec2d827 --- /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 19e60b073d380..08bad3e91ea5a 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 7618e702d04d5..0871d961a933e 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -28,6 +28,8 @@ 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 93241622f0bab..6e3d818db6826 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -5,6 +5,7 @@ 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 @@ def __init__(self, hass, config_entry, allow_unreachable, allow_groups): self.allow_groups = allow_groups self.available = True self.api = None - self._cancel_retry_setup = None @property def host(self): @@ -59,20 +59,7 @@ async def async_setup(self, tries=0): 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 @@ async def async_reset(self): # 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.py deleted file mode 100644 index 5a045a083b30c..0000000000000 --- a/homeassistant/components/hydrawise.py +++ /dev/null @@ -1,151 +0,0 @@ -""" -Support for Hydrawise cloud. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/hydrawise/ -""" -from datetime import timedelta -import logging - -from requests.exceptions import ConnectTimeout, HTTPError -import voluptuous as vol - -from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_ACCESS_TOKEN, CONF_SCAN_INTERVAL) -import homeassistant.helpers.config_validation as cv -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, dispatcher_send) -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import track_time_interval - -REQUIREMENTS = ['hydrawiser==0.1.1'] - -_LOGGER = logging.getLogger(__name__) - -ALLOWED_WATERING_TIME = [5, 10, 15, 30, 45, 60] - -CONF_ATTRIBUTION = "Data provided by hydrawise.com" -CONF_WATERING_TIME = 'watering_minutes' - -NOTIFICATION_ID = 'hydrawise_notification' -NOTIFICATION_TITLE = 'Hydrawise Setup' - -DATA_HYDRAWISE = 'hydrawise' -DOMAIN = 'hydrawise' -DEFAULT_WATERING_TIME = 15 - -DEVICE_MAP_INDEX = ['KEY_INDEX', 'ICON_INDEX', 'DEVICE_CLASS_INDEX', - 'UNIT_OF_MEASURE_INDEX'] -DEVICE_MAP = { - 'auto_watering': ['Automatic Watering', 'mdi:autorenew', '', ''], - 'is_watering': ['Watering', '', 'moisture', ''], - 'manual_watering': ['Manual Watering', 'mdi:water-pump', '', ''], - 'next_cycle': ['Next Cycle', 'mdi:calendar-clock', '', ''], - 'status': ['Status', '', 'connectivity', ''], - 'watering_time': ['Watering Time', 'mdi:water-pump', '', 'min'], - 'rain_sensor': ['Rain Sensor', '', 'moisture', ''] -} - -BINARY_SENSORS = ['is_watering', 'status', 'rain_sensor'] - -SENSORS = ['next_cycle', 'watering_time'] - -SWITCHES = ['auto_watering', 'manual_watering'] - -SCAN_INTERVAL = timedelta(seconds=30) - -SIGNAL_UPDATE_HYDRAWISE = "hydrawise_update" - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_ACCESS_TOKEN): cv.string, - vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): - cv.time_period, - }), -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the Hunter Hydrawise component.""" - conf = config[DOMAIN] - access_token = conf[CONF_ACCESS_TOKEN] - scan_interval = conf.get(CONF_SCAN_INTERVAL) - - try: - from hydrawiser.core import Hydrawiser - - hydrawise = Hydrawiser(user_token=access_token) - hass.data[DATA_HYDRAWISE] = HydrawiseHub(hydrawise) - except (ConnectTimeout, HTTPError) as ex: - _LOGGER.error( - "Unable to connect to Hydrawise cloud service: %s", str(ex)) - hass.components.persistent_notification.create( - 'Error: {}
' - 'You will need to restart hass after fixing.' - ''.format(ex), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) - return False - - def hub_refresh(event_time): - """Call Hydrawise hub to refresh information.""" - _LOGGER.debug("Updating Hydrawise Hub component") - hass.data[DATA_HYDRAWISE].data.update_controller_info() - dispatcher_send(hass, SIGNAL_UPDATE_HYDRAWISE) - - # Call the Hydrawise API to refresh updates - track_time_interval(hass, hub_refresh, scan_interval) - - return True - - -class HydrawiseHub: - """Representation of a base Hydrawise device.""" - - def __init__(self, data): - """Initialize the entity.""" - self.data = data - - -class HydrawiseEntity(Entity): - """Entity class for Hydrawise devices.""" - - def __init__(self, data, sensor_type): - """Initialize the Hydrawise entity.""" - self.data = data - self._sensor_type = sensor_type - self._name = "{0} {1}".format( - self.data['name'], - DEVICE_MAP[self._sensor_type][ - DEVICE_MAP_INDEX.index('KEY_INDEX')]) - self._state = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - async def async_added_to_hass(self): - """Register callbacks.""" - async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_HYDRAWISE, self._update_callback) - - @callback - def _update_callback(self): - """Call update method.""" - self.async_schedule_update_ha_state(True) - - @property - def unit_of_measurement(self): - """Return the units of measurement.""" - return DEVICE_MAP[self._sensor_type][ - DEVICE_MAP_INDEX.index('UNIT_OF_MEASURE_INDEX')] - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - 'identifier': self.data.get('relay'), - } diff --git a/homeassistant/components/hydrawise/__init__.py b/homeassistant/components/hydrawise/__init__.py new file mode 100644 index 0000000000000..800d19d7efe21 --- /dev/null +++ b/homeassistant/components/hydrawise/__init__.py @@ -0,0 +1,146 @@ +"""Support for Hydrawise cloud.""" +from datetime import timedelta +import logging + +from requests.exceptions import ConnectTimeout, HTTPError +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_ACCESS_TOKEN, CONF_SCAN_INTERVAL) +import homeassistant.helpers.config_validation as cv +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, dispatcher_send) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import track_time_interval + +REQUIREMENTS = ['hydrawiser==0.1.1'] + +_LOGGER = logging.getLogger(__name__) + +ALLOWED_WATERING_TIME = [5, 10, 15, 30, 45, 60] + +CONF_ATTRIBUTION = "Data provided by hydrawise.com" +CONF_WATERING_TIME = 'watering_minutes' + +NOTIFICATION_ID = 'hydrawise_notification' +NOTIFICATION_TITLE = 'Hydrawise Setup' + +DATA_HYDRAWISE = 'hydrawise' +DOMAIN = 'hydrawise' +DEFAULT_WATERING_TIME = 15 + +DEVICE_MAP_INDEX = ['KEY_INDEX', 'ICON_INDEX', 'DEVICE_CLASS_INDEX', + 'UNIT_OF_MEASURE_INDEX'] +DEVICE_MAP = { + 'auto_watering': ['Automatic Watering', 'mdi:autorenew', '', ''], + 'is_watering': ['Watering', '', 'moisture', ''], + 'manual_watering': ['Manual Watering', 'mdi:water-pump', '', ''], + 'next_cycle': ['Next Cycle', 'mdi:calendar-clock', '', ''], + 'status': ['Status', '', 'connectivity', ''], + 'watering_time': ['Watering Time', 'mdi:water-pump', '', 'min'], + 'rain_sensor': ['Rain Sensor', '', 'moisture', ''] +} + +BINARY_SENSORS = ['is_watering', 'status', 'rain_sensor'] + +SENSORS = ['next_cycle', 'watering_time'] + +SWITCHES = ['auto_watering', 'manual_watering'] + +SCAN_INTERVAL = timedelta(seconds=30) + +SIGNAL_UPDATE_HYDRAWISE = "hydrawise_update" + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_ACCESS_TOKEN): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): + cv.time_period, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Hunter Hydrawise component.""" + conf = config[DOMAIN] + access_token = conf[CONF_ACCESS_TOKEN] + scan_interval = conf.get(CONF_SCAN_INTERVAL) + + try: + from hydrawiser.core import Hydrawiser + + hydrawise = Hydrawiser(user_token=access_token) + hass.data[DATA_HYDRAWISE] = HydrawiseHub(hydrawise) + except (ConnectTimeout, HTTPError) as ex: + _LOGGER.error( + "Unable to connect to Hydrawise cloud service: %s", str(ex)) + hass.components.persistent_notification.create( + 'Error: {}
' + 'You will need to restart hass after fixing.' + ''.format(ex), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + return False + + def hub_refresh(event_time): + """Call Hydrawise hub to refresh information.""" + _LOGGER.debug("Updating Hydrawise Hub component") + hass.data[DATA_HYDRAWISE].data.update_controller_info() + dispatcher_send(hass, SIGNAL_UPDATE_HYDRAWISE) + + # Call the Hydrawise API to refresh updates + track_time_interval(hass, hub_refresh, scan_interval) + + return True + + +class HydrawiseHub: + """Representation of a base Hydrawise device.""" + + def __init__(self, data): + """Initialize the entity.""" + self.data = data + + +class HydrawiseEntity(Entity): + """Entity class for Hydrawise devices.""" + + def __init__(self, data, sensor_type): + """Initialize the Hydrawise entity.""" + self.data = data + self._sensor_type = sensor_type + self._name = "{0} {1}".format( + self.data['name'], + DEVICE_MAP[self._sensor_type][ + DEVICE_MAP_INDEX.index('KEY_INDEX')]) + self._state = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + async def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_HYDRAWISE, self._update_callback) + + @callback + def _update_callback(self): + """Call update method.""" + self.async_schedule_update_ha_state(True) + + @property + def unit_of_measurement(self): + """Return the units of measurement.""" + return DEVICE_MAP[self._sensor_type][ + DEVICE_MAP_INDEX.index('UNIT_OF_MEASURE_INDEX')] + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + 'identifier': self.data.get('relay'), + } diff --git a/homeassistant/components/hydrawise/binary_sensor.py b/homeassistant/components/hydrawise/binary_sensor.py new file mode 100644 index 0000000000000..bfe7cbd5531ed --- /dev/null +++ b/homeassistant/components/hydrawise/binary_sensor.py @@ -0,0 +1,76 @@ +"""Support for Hydrawise sprinkler binary sensors.""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.hydrawise import ( + BINARY_SENSORS, DATA_HYDRAWISE, HydrawiseEntity, DEVICE_MAP, + DEVICE_MAP_INDEX) +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA) +from homeassistant.const import CONF_MONITORED_CONDITIONS + +DEPENDENCIES = ['hydrawise'] + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=BINARY_SENSORS): + vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)]), +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up a sensor for a Hydrawise device.""" + hydrawise = hass.data[DATA_HYDRAWISE].data + + sensors = [] + for sensor_type in config.get(CONF_MONITORED_CONDITIONS): + if sensor_type in ['status', 'rain_sensor']: + sensors.append( + HydrawiseBinarySensor( + hydrawise.controller_status, sensor_type)) + + else: + # create a sensor for each zone + for zone in hydrawise.relays: + zone_data = zone + zone_data['running'] = \ + hydrawise.controller_status.get('running', False) + sensors.append(HydrawiseBinarySensor(zone_data, sensor_type)) + + add_entities(sensors, True) + + +class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorDevice): + """A sensor implementation for Hydrawise device.""" + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self._state + + def update(self): + """Get the latest data and updates the state.""" + _LOGGER.debug("Updating Hydrawise binary sensor: %s", self._name) + mydata = self.hass.data[DATA_HYDRAWISE].data + if self._sensor_type == 'status': + self._state = mydata.status == 'All good!' + elif self._sensor_type == 'rain_sensor': + for sensor in mydata.sensors: + if sensor['name'] == 'Rain': + self._state = sensor['active'] == 1 + elif self._sensor_type == 'is_watering': + if not mydata.running: + self._state = False + elif int(mydata.running[0]['relay']) == self.data['relay']: + self._state = True + else: + self._state = False + + @property + def device_class(self): + """Return the device class of the sensor type.""" + return DEVICE_MAP[self._sensor_type][ + DEVICE_MAP_INDEX.index('DEVICE_CLASS_INDEX')] diff --git a/homeassistant/components/hydrawise/sensor.py b/homeassistant/components/hydrawise/sensor.py new file mode 100644 index 0000000000000..575686b92cdd5 --- /dev/null +++ b/homeassistant/components/hydrawise/sensor.py @@ -0,0 +1,67 @@ +"""Support for Hydrawise sprinkler sensors.""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.hydrawise import ( + DATA_HYDRAWISE, HydrawiseEntity, DEVICE_MAP, DEVICE_MAP_INDEX, SENSORS) +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_MONITORED_CONDITIONS + +DEPENDENCIES = ['hydrawise'] + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSORS): + vol.All(cv.ensure_list, [vol.In(SENSORS)]), +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up a sensor for a Hydrawise device.""" + hydrawise = hass.data[DATA_HYDRAWISE].data + + sensors = [] + for sensor_type in config.get(CONF_MONITORED_CONDITIONS): + for zone in hydrawise.relays: + sensors.append(HydrawiseSensor(zone, sensor_type)) + + add_entities(sensors, True) + + +class HydrawiseSensor(HydrawiseEntity): + """A sensor implementation for Hydrawise device.""" + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + def update(self): + """Get the latest data and updates the states.""" + mydata = self.hass.data[DATA_HYDRAWISE].data + _LOGGER.debug("Updating Hydrawise sensor: %s", self._name) + if self._sensor_type == 'watering_time': + if not mydata.running: + self._state = 0 + else: + if int(mydata.running[0]['relay']) == self.data['relay']: + self._state = int(mydata.running[0]['time_left']/60) + else: + self._state = 0 + else: # _sensor_type == 'next_cycle' + for relay in mydata.relays: + if relay['relay'] == self.data['relay']: + if relay['nicetime'] == 'Not scheduled': + self._state = 'not_scheduled' + else: + self._state = relay['nicetime'].split(',')[0] + \ + ' ' + relay['nicetime'].split(' ')[3] + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return DEVICE_MAP[self._sensor_type][ + DEVICE_MAP_INDEX.index('ICON_INDEX')] diff --git a/homeassistant/components/hydrawise/switch.py b/homeassistant/components/hydrawise/switch.py new file mode 100644 index 0000000000000..a6a8b9c54cff5 --- /dev/null +++ b/homeassistant/components/hydrawise/switch.py @@ -0,0 +1,96 @@ +"""Support for Hydrawise cloud switches.""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.hydrawise import ( + ALLOWED_WATERING_TIME, CONF_WATERING_TIME, + DATA_HYDRAWISE, DEFAULT_WATERING_TIME, HydrawiseEntity, SWITCHES, + DEVICE_MAP, DEVICE_MAP_INDEX) +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import CONF_MONITORED_CONDITIONS + +DEPENDENCIES = ['hydrawise'] + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=SWITCHES): + vol.All(cv.ensure_list, [vol.In(SWITCHES)]), + vol.Optional(CONF_WATERING_TIME, default=DEFAULT_WATERING_TIME): + vol.All(vol.In(ALLOWED_WATERING_TIME)), +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up a sensor for a Hydrawise device.""" + hydrawise = hass.data[DATA_HYDRAWISE].data + + default_watering_timer = config.get(CONF_WATERING_TIME) + + sensors = [] + for sensor_type in config.get(CONF_MONITORED_CONDITIONS): + # Create a switch for each zone + for zone in hydrawise.relays: + sensors.append( + HydrawiseSwitch(default_watering_timer, zone, sensor_type)) + + add_entities(sensors, True) + + +class HydrawiseSwitch(HydrawiseEntity, SwitchDevice): + """A switch implementation for Hydrawise device.""" + + def __init__(self, default_watering_timer, *args): + """Initialize a switch for Hydrawise device.""" + super().__init__(*args) + self._default_watering_timer = default_watering_timer + + @property + def is_on(self): + """Return true if device is on.""" + return self._state + + def turn_on(self, **kwargs): + """Turn the device on.""" + if self._sensor_type == 'manual_watering': + self.hass.data[DATA_HYDRAWISE].data.run_zone( + self._default_watering_timer, (self.data['relay']-1)) + elif self._sensor_type == 'auto_watering': + self.hass.data[DATA_HYDRAWISE].data.suspend_zone( + 0, (self.data['relay']-1)) + + def turn_off(self, **kwargs): + """Turn the device off.""" + if self._sensor_type == 'manual_watering': + self.hass.data[DATA_HYDRAWISE].data.run_zone( + 0, (self.data['relay']-1)) + elif self._sensor_type == 'auto_watering': + self.hass.data[DATA_HYDRAWISE].data.suspend_zone( + 365, (self.data['relay']-1)) + + def update(self): + """Update device state.""" + mydata = self.hass.data[DATA_HYDRAWISE].data + _LOGGER.debug("Updating Hydrawise switch: %s", self._name) + if self._sensor_type == 'manual_watering': + if not mydata.running: + self._state = False + else: + self._state = int( + mydata.running[0]['relay']) == self.data['relay'] + elif self._sensor_type == 'auto_watering': + for relay in mydata.relays: + if relay['relay'] == self.data['relay']: + if relay.get('suspended') is not None: + self._state = False + else: + self._state = True + break + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return DEVICE_MAP[self._sensor_type][ + DEVICE_MAP_INDEX.index('ICON_INDEX')] diff --git a/homeassistant/components/idteck_prox.py b/homeassistant/components/idteck_prox.py deleted file mode 100644 index 90c07c487b6b9..0000000000000 --- a/homeassistant/components/idteck_prox.py +++ /dev/null @@ -1,75 +0,0 @@ -"""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/ -""" -import logging - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.const import ( - CONF_HOST, CONF_PORT, CONF_NAME, EVENT_HOMEASSISTANT_STOP) - -REQUIREMENTS = ['rfk101py==0.0.1'] - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "idteck_prox" - -EVENT_IDTECK_PROX_KEYCARD = 'idteck_prox_keycard' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PORT): cv.port, - vol.Required(CONF_NAME): cv.string, - })]) -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the IDTECK proximity card component.""" - conf = config[DOMAIN] - for unit in conf: - host = unit[CONF_HOST] - port = unit[CONF_PORT] - name = unit[CONF_NAME] - - try: - reader = IdteckReader(hass, host, port, name) - reader.connect() - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, reader.stop) - except OSError as error: - _LOGGER.error('Error creating "%s". %s', name, error) - return False - - return True - - -class IdteckReader(): - """Representation of an IDTECK proximity card reader.""" - - def __init__(self, hass, host, port, name): - """Initialize the reader.""" - self.hass = hass - self._host = host - self._port = port - self._name = name - self._connection = None - - def connect(self): - """Connect to the reader.""" - from rfk101py.rfk101py import rfk101py - self._connection = rfk101py(self._host, self._port, self._callback) - - def _callback(self, card): - """Send a keycard event message into HASS whenever a card is read.""" - self.hass.bus.fire( - EVENT_IDTECK_PROX_KEYCARD, {'card': card, 'name': self._name}) - - def stop(self): - """Close resources.""" - if self._connection: - self._connection.close() - self._connection = None diff --git a/homeassistant/components/idteck_prox/__init__.py b/homeassistant/components/idteck_prox/__init__.py new file mode 100644 index 0000000000000..8ec6f49b95d3c --- /dev/null +++ b/homeassistant/components/idteck_prox/__init__.py @@ -0,0 +1,71 @@ +"""Component for interfacing RFK101 proximity card readers.""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import ( + CONF_HOST, CONF_PORT, CONF_NAME, EVENT_HOMEASSISTANT_STOP) + +REQUIREMENTS = ['rfk101py==0.0.1'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "idteck_prox" + +EVENT_IDTECK_PROX_KEYCARD = 'idteck_prox_keycard' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PORT): cv.port, + vol.Required(CONF_NAME): cv.string, + })]) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the IDTECK proximity card component.""" + conf = config[DOMAIN] + for unit in conf: + host = unit[CONF_HOST] + port = unit[CONF_PORT] + name = unit[CONF_NAME] + + try: + reader = IdteckReader(hass, host, port, name) + reader.connect() + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, reader.stop) + except OSError as error: + _LOGGER.error('Error creating "%s". %s', name, error) + return False + + return True + + +class IdteckReader(): + """Representation of an IDTECK proximity card reader.""" + + def __init__(self, hass, host, port, name): + """Initialize the reader.""" + self.hass = hass + self._host = host + self._port = port + self._name = name + self._connection = None + + def connect(self): + """Connect to the reader.""" + from rfk101py.rfk101py import rfk101py + self._connection = rfk101py(self._host, self._port, self._callback) + + def _callback(self, card): + """Send a keycard event message into HASS whenever a card is read.""" + self.hass.bus.fire( + EVENT_IDTECK_PROX_KEYCARD, {'card': card, 'name': self._name}) + + def stop(self): + """Close resources.""" + if self._connection: + self._connection.close() + self._connection = None diff --git a/homeassistant/components/ifttt/.translations/da.json b/homeassistant/components/ifttt/.translations/da.json new file mode 100644 index 0000000000000..25c502ed05efa --- /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 bb54f7ef6cba1..75bdd0d99c8ec 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 209bbcef607ac..7dee93b22608a 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/ifttt/alarm_control_panel.py b/homeassistant/components/ifttt/alarm_control_panel.py new file mode 100644 index 0000000000000..bbb9a02c8a130 --- /dev/null +++ b/homeassistant/components/ifttt/alarm_control_panel.py @@ -0,0 +1,170 @@ +"""Support for alarm control panels that can be controlled through IFTTT.""" +import logging +import re + +import voluptuous as vol + +import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel import ( + DOMAIN, PLATFORM_SCHEMA) +from homeassistant.components.ifttt import ( + ATTR_EVENT, DOMAIN as IFTTT_DOMAIN, SERVICE_TRIGGER) +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_STATE, CONF_NAME, CONF_CODE, + CONF_OPTIMISTIC, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY) +import homeassistant.helpers.config_validation as cv + +DEPENDENCIES = ['ifttt'] + +_LOGGER = logging.getLogger(__name__) + +ALLOWED_STATES = [ + STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME] + +DATA_IFTTT_ALARM = 'ifttt_alarm' +DEFAULT_NAME = "Home" + +CONF_EVENT_AWAY = "event_arm_away" +CONF_EVENT_HOME = "event_arm_home" +CONF_EVENT_NIGHT = "event_arm_night" +CONF_EVENT_DISARM = "event_disarm" + +DEFAULT_EVENT_AWAY = "alarm_arm_away" +DEFAULT_EVENT_HOME = "alarm_arm_home" +DEFAULT_EVENT_NIGHT = "alarm_arm_night" +DEFAULT_EVENT_DISARM = "alarm_disarm" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_CODE): cv.string, + vol.Optional(CONF_EVENT_AWAY, default=DEFAULT_EVENT_AWAY): cv.string, + vol.Optional(CONF_EVENT_HOME, default=DEFAULT_EVENT_HOME): cv.string, + vol.Optional(CONF_EVENT_NIGHT, default=DEFAULT_EVENT_NIGHT): cv.string, + vol.Optional(CONF_EVENT_DISARM, default=DEFAULT_EVENT_DISARM): cv.string, + vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, +}) + +SERVICE_PUSH_ALARM_STATE = "ifttt_push_alarm_state" + +PUSH_ALARM_STATE_SERVICE_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_STATE): cv.string, +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up a control panel managed through IFTTT.""" + if DATA_IFTTT_ALARM not in hass.data: + hass.data[DATA_IFTTT_ALARM] = [] + + name = config.get(CONF_NAME) + code = config.get(CONF_CODE) + event_away = config.get(CONF_EVENT_AWAY) + event_home = config.get(CONF_EVENT_HOME) + event_night = config.get(CONF_EVENT_NIGHT) + event_disarm = config.get(CONF_EVENT_DISARM) + optimistic = config.get(CONF_OPTIMISTIC) + + alarmpanel = IFTTTAlarmPanel(name, code, event_away, event_home, + event_night, event_disarm, optimistic) + hass.data[DATA_IFTTT_ALARM].append(alarmpanel) + add_entities([alarmpanel]) + + async def push_state_update(service): + """Set the service state as device state attribute.""" + entity_ids = service.data.get(ATTR_ENTITY_ID) + state = service.data.get(ATTR_STATE) + devices = hass.data[DATA_IFTTT_ALARM] + if entity_ids: + devices = [d for d in devices if d.entity_id in entity_ids] + + for device in devices: + device.push_alarm_state(state) + device.async_schedule_update_ha_state() + + hass.services.register(DOMAIN, SERVICE_PUSH_ALARM_STATE, push_state_update, + schema=PUSH_ALARM_STATE_SERVICE_SCHEMA) + + +class IFTTTAlarmPanel(alarm.AlarmControlPanel): + """Representation of an alarm control panel controlled through IFTTT.""" + + def __init__(self, name, code, event_away, event_home, event_night, + event_disarm, optimistic): + """Initialize the alarm control panel.""" + self._name = name + self._code = code + self._event_away = event_away + self._event_home = event_home + self._event_night = event_night + self._event_disarm = event_disarm + self._optimistic = optimistic + self._state = None + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def assumed_state(self): + """Notify that this platform return an assumed state.""" + return True + + @property + def code_format(self): + """Return one or more digits/characters.""" + if self._code is None: + return None + if isinstance(self._code, str) and re.search('^\\d+$', self._code): + return alarm.FORMAT_NUMBER + return alarm.FORMAT_TEXT + + def alarm_disarm(self, code=None): + """Send disarm command.""" + if not self._check_code(code): + return + self.set_alarm_state(self._event_disarm, STATE_ALARM_DISARMED) + + def alarm_arm_away(self, code=None): + """Send arm away command.""" + if not self._check_code(code): + return + self.set_alarm_state(self._event_away, STATE_ALARM_ARMED_AWAY) + + def alarm_arm_home(self, code=None): + """Send arm home command.""" + if not self._check_code(code): + return + self.set_alarm_state(self._event_home, STATE_ALARM_ARMED_HOME) + + def alarm_arm_night(self, code=None): + """Send arm night command.""" + if not self._check_code(code): + return + self.set_alarm_state(self._event_night, STATE_ALARM_ARMED_NIGHT) + + def set_alarm_state(self, event, state): + """Call the IFTTT trigger service to change the alarm state.""" + data = {ATTR_EVENT: event} + + self.hass.services.call(IFTTT_DOMAIN, SERVICE_TRIGGER, data) + _LOGGER.debug("Called IFTTT component to trigger event %s", event) + if self._optimistic: + self._state = state + + def push_alarm_state(self, value): + """Push the alarm state to the given value.""" + if value in ALLOWED_STATES: + _LOGGER.debug("Pushed the alarm state to %s", value) + self._state = value + + def _check_code(self, code): + return self._code is None or self._code == code 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 b2cbb2b2391af..f854384bb0307 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -60,6 +60,7 @@ 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.py deleted file mode 100644 index 2b9a5d9e19307..0000000000000 --- a/homeassistant/components/influxdb.py +++ /dev/null @@ -1,344 +0,0 @@ -""" -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/ -""" -import logging -import re -import queue -import threading -import time -import math - -import requests.exceptions -import voluptuous as vol - -from homeassistant.const import ( - CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_HOST, CONF_INCLUDE, - CONF_PASSWORD, CONF_PORT, CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL, - EVENT_STATE_CHANGED, EVENT_HOMEASSISTANT_STOP, STATE_UNAVAILABLE, - STATE_UNKNOWN) -from homeassistant.helpers import state as state_helper -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_values import EntityValues - -REQUIREMENTS = ['influxdb==5.2.0'] - -_LOGGER = logging.getLogger(__name__) - -CONF_DB_NAME = 'database' -CONF_TAGS = 'tags' -CONF_DEFAULT_MEASUREMENT = 'default_measurement' -CONF_OVERRIDE_MEASUREMENT = 'override_measurement' -CONF_TAGS_ATTRIBUTES = 'tags_attributes' -CONF_COMPONENT_CONFIG = 'component_config' -CONF_COMPONENT_CONFIG_GLOB = 'component_config_glob' -CONF_COMPONENT_CONFIG_DOMAIN = 'component_config_domain' -CONF_RETRY_COUNT = 'max_retries' - -DEFAULT_DATABASE = 'home_assistant' -DEFAULT_VERIFY_SSL = True -DOMAIN = 'influxdb' - -TIMEOUT = 5 -RETRY_DELAY = 20 -QUEUE_BACKLOG_SECONDS = 30 - -BATCH_TIMEOUT = 1 -BATCH_BUFFER_SIZE = 100 - -COMPONENT_CONFIG_SCHEMA_ENTRY = vol.Schema({ - vol.Optional(CONF_OVERRIDE_MEASUREMENT): cv.string, -}) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.All(vol.Schema({ - vol.Optional(CONF_HOST): cv.string, - vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string, - vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string, - 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.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.Optional(CONF_DB_NAME, default=DEFAULT_DATABASE): cv.string, - vol.Optional(CONF_PORT): cv.port, - vol.Optional(CONF_SSL): cv.boolean, - vol.Optional(CONF_RETRY_COUNT, default=0): cv.positive_int, - vol.Optional(CONF_DEFAULT_MEASUREMENT): cv.string, - vol.Optional(CONF_OVERRIDE_MEASUREMENT): cv.string, - vol.Optional(CONF_TAGS, default={}): - vol.Schema({cv.string: cv.string}), - vol.Optional(CONF_TAGS_ATTRIBUTES, default=[]): - vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, - vol.Optional(CONF_COMPONENT_CONFIG, default={}): - vol.Schema({cv.entity_id: COMPONENT_CONFIG_SCHEMA_ENTRY}), - vol.Optional(CONF_COMPONENT_CONFIG_GLOB, default={}): - vol.Schema({cv.string: COMPONENT_CONFIG_SCHEMA_ENTRY}), - vol.Optional(CONF_COMPONENT_CONFIG_DOMAIN, default={}): - vol.Schema({cv.string: COMPONENT_CONFIG_SCHEMA_ENTRY}), - })), -}, extra=vol.ALLOW_EXTRA) - -RE_DIGIT_TAIL = re.compile(r'^[^\.]*\d+\.?\d+[^\.]*$') -RE_DECIMAL = re.compile(r'[^\d.]+') - - -def setup(hass, config): - """Set up the InfluxDB component.""" - from influxdb import InfluxDBClient, exceptions - - conf = config[DOMAIN] - - kwargs = { - 'database': conf[CONF_DB_NAME], - 'verify_ssl': conf[CONF_VERIFY_SSL], - 'timeout': TIMEOUT - } - - if CONF_HOST in conf: - kwargs['host'] = conf[CONF_HOST] - - if CONF_PORT in conf: - kwargs['port'] = conf[CONF_PORT] - - if CONF_USERNAME in conf: - kwargs['username'] = conf[CONF_USERNAME] - - if CONF_PASSWORD in conf: - kwargs['password'] = conf[CONF_PASSWORD] - - if CONF_SSL in conf: - kwargs['ssl'] = conf[CONF_SSL] - - include = conf.get(CONF_INCLUDE, {}) - exclude = conf.get(CONF_EXCLUDE, {}) - whitelist_e = set(include.get(CONF_ENTITIES, [])) - whitelist_d = set(include.get(CONF_DOMAINS, [])) - blacklist_e = set(exclude.get(CONF_ENTITIES, [])) - blacklist_d = set(exclude.get(CONF_DOMAINS, [])) - tags = conf.get(CONF_TAGS) - tags_attributes = conf.get(CONF_TAGS_ATTRIBUTES) - default_measurement = conf.get(CONF_DEFAULT_MEASUREMENT) - override_measurement = conf.get(CONF_OVERRIDE_MEASUREMENT) - component_config = EntityValues( - conf[CONF_COMPONENT_CONFIG], - conf[CONF_COMPONENT_CONFIG_DOMAIN], - conf[CONF_COMPONENT_CONFIG_GLOB]) - max_tries = conf.get(CONF_RETRY_COUNT) - - try: - influx = InfluxDBClient(**kwargs) - influx.write_points([]) - except (exceptions.InfluxDBClientError, - requests.exceptions.ConnectionError) as exc: - _LOGGER.error("Database host is not accessible due to '%s', please " - "check your entries in the configuration file (host, " - "port, etc.) and verify that the database exists and is " - "READ/WRITE", exc) - return False - - def event_to_json(event): - """Add an event to the outgoing Influx list.""" - state = event.data.get('new_state') - if state is None or state.state in ( - STATE_UNKNOWN, '', STATE_UNAVAILABLE) or \ - state.entity_id in blacklist_e or state.domain in blacklist_d: - return - - try: - if ((whitelist_e or whitelist_d) - and state.entity_id not in whitelist_e - and state.domain not in whitelist_d): - return - - _include_state = _include_value = False - - _state_as_value = float(state.state) - _include_value = True - except ValueError: - try: - _state_as_value = float(state_helper.state_as_number(state)) - _include_state = _include_value = True - except ValueError: - _include_state = True - - include_uom = True - measurement = component_config.get(state.entity_id).get( - CONF_OVERRIDE_MEASUREMENT) - if measurement in (None, ''): - if override_measurement: - measurement = override_measurement - else: - measurement = state.attributes.get('unit_of_measurement') - if measurement in (None, ''): - if default_measurement: - measurement = default_measurement - else: - measurement = state.entity_id - else: - include_uom = False - - json = { - 'measurement': measurement, - 'tags': { - 'domain': state.domain, - 'entity_id': state.object_id, - }, - 'time': event.time_fired, - 'fields': {} - } - if _include_state: - json['fields']['state'] = state.state - if _include_value: - json['fields']['value'] = _state_as_value - - for key, value in state.attributes.items(): - if key in tags_attributes: - json['tags'][key] = value - elif key != 'unit_of_measurement' or include_uom: - # If the key is already in fields - if key in json['fields']: - key = key + "_" - # Prevent column data errors in influxDB. - # For each value we try to cast it as float - # But if we can not do it we store the value - # as string add "_str" postfix to the field key - try: - json['fields'][key] = float(value) - except (ValueError, TypeError): - new_key = "{}_str".format(key) - new_value = str(value) - json['fields'][new_key] = new_value - - if RE_DIGIT_TAIL.match(new_value): - json['fields'][key] = float( - RE_DECIMAL.sub('', new_value)) - - # Infinity and NaN are not valid floats in InfluxDB - try: - if not math.isfinite(json['fields'][key]): - del json['fields'][key] - except (KeyError, TypeError): - pass - - json['tags'].update(tags) - - return json - - instance = hass.data[DOMAIN] = InfluxThread( - hass, influx, event_to_json, max_tries) - instance.start() - - def shutdown(event): - """Shut down the thread.""" - instance.queue.put(None) - instance.join() - influx.close() - - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) - - return True - - -class InfluxThread(threading.Thread): - """A threaded event handler class.""" - - def __init__(self, hass, influx, event_to_json, max_tries): - """Initialize the listener.""" - threading.Thread.__init__(self, name='InfluxDB') - self.queue = queue.Queue() - self.influx = influx - self.event_to_json = event_to_json - self.max_tries = max_tries - self.write_errors = 0 - self.shutdown = False - hass.bus.listen(EVENT_STATE_CHANGED, self._event_listener) - - def _event_listener(self, event): - """Listen for new messages on the bus and queue them for Influx.""" - item = (time.monotonic(), event) - self.queue.put(item) - - @staticmethod - def batch_timeout(): - """Return number of seconds to wait for more events.""" - return BATCH_TIMEOUT - - def get_events_json(self): - """Return a batch of events formatted for writing.""" - queue_seconds = QUEUE_BACKLOG_SECONDS + self.max_tries*RETRY_DELAY - - count = 0 - json = [] - - dropped = 0 - - try: - while len(json) < BATCH_BUFFER_SIZE and not self.shutdown: - timeout = None if count == 0 else self.batch_timeout() - item = self.queue.get(timeout=timeout) - count += 1 - - if item is None: - self.shutdown = True - else: - timestamp, event = item - age = time.monotonic() - timestamp - - if age < queue_seconds: - event_json = self.event_to_json(event) - if event_json: - json.append(event_json) - else: - dropped += 1 - - except queue.Empty: - pass - - if dropped: - _LOGGER.warning("Catching up, dropped %d old events", dropped) - - return count, json - - def write_to_influxdb(self, json): - """Write preprocessed events to influxdb, with retry.""" - from influxdb import exceptions - - for retry in range(self.max_tries+1): - try: - self.influx.write_points(json) - - if self.write_errors: - _LOGGER.error("Resumed, lost %d events", self.write_errors) - self.write_errors = 0 - - _LOGGER.debug("Wrote %d events", len(json)) - break - except (exceptions.InfluxDBClientError, IOError): - if retry < self.max_tries: - time.sleep(RETRY_DELAY) - else: - if not self.write_errors: - _LOGGER.exception("Write error") - self.write_errors += len(json) - - def run(self): - """Process incoming events.""" - while not self.shutdown: - count, json = self.get_events_json() - if json: - self.write_to_influxdb(json) - for _ in range(count): - self.queue.task_done() - - def block_till_done(self): - """Block till all events processed.""" - self.queue.join() diff --git a/homeassistant/components/influxdb/__init__.py b/homeassistant/components/influxdb/__init__.py new file mode 100644 index 0000000000000..b421960b51fbc --- /dev/null +++ b/homeassistant/components/influxdb/__init__.py @@ -0,0 +1,339 @@ +"""Support for sending data to an Influx database.""" +import logging +import re +import queue +import threading +import time +import math + +import requests.exceptions +import voluptuous as vol + +from homeassistant.const import ( + CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_HOST, CONF_INCLUDE, + CONF_PASSWORD, CONF_PORT, CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL, + EVENT_STATE_CHANGED, EVENT_HOMEASSISTANT_STOP, STATE_UNAVAILABLE, + STATE_UNKNOWN) +from homeassistant.helpers import state as state_helper +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_values import EntityValues + +REQUIREMENTS = ['influxdb==5.2.0'] + +_LOGGER = logging.getLogger(__name__) + +CONF_DB_NAME = 'database' +CONF_TAGS = 'tags' +CONF_DEFAULT_MEASUREMENT = 'default_measurement' +CONF_OVERRIDE_MEASUREMENT = 'override_measurement' +CONF_TAGS_ATTRIBUTES = 'tags_attributes' +CONF_COMPONENT_CONFIG = 'component_config' +CONF_COMPONENT_CONFIG_GLOB = 'component_config_glob' +CONF_COMPONENT_CONFIG_DOMAIN = 'component_config_domain' +CONF_RETRY_COUNT = 'max_retries' + +DEFAULT_DATABASE = 'home_assistant' +DEFAULT_VERIFY_SSL = True +DOMAIN = 'influxdb' + +TIMEOUT = 5 +RETRY_DELAY = 20 +QUEUE_BACKLOG_SECONDS = 30 + +BATCH_TIMEOUT = 1 +BATCH_BUFFER_SIZE = 100 + +COMPONENT_CONFIG_SCHEMA_ENTRY = vol.Schema({ + vol.Optional(CONF_OVERRIDE_MEASUREMENT): cv.string, +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All(vol.Schema({ + vol.Optional(CONF_HOST): cv.string, + vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string, + vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string, + 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.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.Optional(CONF_DB_NAME, default=DEFAULT_DATABASE): cv.string, + vol.Optional(CONF_PORT): cv.port, + vol.Optional(CONF_SSL): cv.boolean, + vol.Optional(CONF_RETRY_COUNT, default=0): cv.positive_int, + vol.Optional(CONF_DEFAULT_MEASUREMENT): cv.string, + vol.Optional(CONF_OVERRIDE_MEASUREMENT): cv.string, + vol.Optional(CONF_TAGS, default={}): + vol.Schema({cv.string: cv.string}), + vol.Optional(CONF_TAGS_ATTRIBUTES, default=[]): + vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, + vol.Optional(CONF_COMPONENT_CONFIG, default={}): + vol.Schema({cv.entity_id: COMPONENT_CONFIG_SCHEMA_ENTRY}), + vol.Optional(CONF_COMPONENT_CONFIG_GLOB, default={}): + vol.Schema({cv.string: COMPONENT_CONFIG_SCHEMA_ENTRY}), + vol.Optional(CONF_COMPONENT_CONFIG_DOMAIN, default={}): + vol.Schema({cv.string: COMPONENT_CONFIG_SCHEMA_ENTRY}), + })), +}, extra=vol.ALLOW_EXTRA) + +RE_DIGIT_TAIL = re.compile(r'^[^\.]*\d+\.?\d+[^\.]*$') +RE_DECIMAL = re.compile(r'[^\d.]+') + + +def setup(hass, config): + """Set up the InfluxDB component.""" + from influxdb import InfluxDBClient, exceptions + + conf = config[DOMAIN] + + kwargs = { + 'database': conf[CONF_DB_NAME], + 'verify_ssl': conf[CONF_VERIFY_SSL], + 'timeout': TIMEOUT + } + + if CONF_HOST in conf: + kwargs['host'] = conf[CONF_HOST] + + if CONF_PORT in conf: + kwargs['port'] = conf[CONF_PORT] + + if CONF_USERNAME in conf: + kwargs['username'] = conf[CONF_USERNAME] + + if CONF_PASSWORD in conf: + kwargs['password'] = conf[CONF_PASSWORD] + + if CONF_SSL in conf: + kwargs['ssl'] = conf[CONF_SSL] + + include = conf.get(CONF_INCLUDE, {}) + exclude = conf.get(CONF_EXCLUDE, {}) + whitelist_e = set(include.get(CONF_ENTITIES, [])) + whitelist_d = set(include.get(CONF_DOMAINS, [])) + blacklist_e = set(exclude.get(CONF_ENTITIES, [])) + blacklist_d = set(exclude.get(CONF_DOMAINS, [])) + tags = conf.get(CONF_TAGS) + tags_attributes = conf.get(CONF_TAGS_ATTRIBUTES) + default_measurement = conf.get(CONF_DEFAULT_MEASUREMENT) + override_measurement = conf.get(CONF_OVERRIDE_MEASUREMENT) + component_config = EntityValues( + conf[CONF_COMPONENT_CONFIG], + conf[CONF_COMPONENT_CONFIG_DOMAIN], + conf[CONF_COMPONENT_CONFIG_GLOB]) + max_tries = conf.get(CONF_RETRY_COUNT) + + try: + influx = InfluxDBClient(**kwargs) + influx.write_points([]) + except (exceptions.InfluxDBClientError, + requests.exceptions.ConnectionError) as exc: + _LOGGER.error("Database host is not accessible due to '%s', please " + "check your entries in the configuration file (host, " + "port, etc.) and verify that the database exists and is " + "READ/WRITE", exc) + return False + + def event_to_json(event): + """Add an event to the outgoing Influx list.""" + state = event.data.get('new_state') + if state is None or state.state in ( + STATE_UNKNOWN, '', STATE_UNAVAILABLE) or \ + state.entity_id in blacklist_e or state.domain in blacklist_d: + return + + try: + if ((whitelist_e or whitelist_d) + and state.entity_id not in whitelist_e + and state.domain not in whitelist_d): + return + + _include_state = _include_value = False + + _state_as_value = float(state.state) + _include_value = True + except ValueError: + try: + _state_as_value = float(state_helper.state_as_number(state)) + _include_state = _include_value = True + except ValueError: + _include_state = True + + include_uom = True + measurement = component_config.get(state.entity_id).get( + CONF_OVERRIDE_MEASUREMENT) + if measurement in (None, ''): + if override_measurement: + measurement = override_measurement + else: + measurement = state.attributes.get('unit_of_measurement') + if measurement in (None, ''): + if default_measurement: + measurement = default_measurement + else: + measurement = state.entity_id + else: + include_uom = False + + json = { + 'measurement': measurement, + 'tags': { + 'domain': state.domain, + 'entity_id': state.object_id, + }, + 'time': event.time_fired, + 'fields': {} + } + if _include_state: + json['fields']['state'] = state.state + if _include_value: + json['fields']['value'] = _state_as_value + + for key, value in state.attributes.items(): + if key in tags_attributes: + json['tags'][key] = value + elif key != 'unit_of_measurement' or include_uom: + # If the key is already in fields + if key in json['fields']: + key = key + "_" + # Prevent column data errors in influxDB. + # For each value we try to cast it as float + # But if we can not do it we store the value + # as string add "_str" postfix to the field key + try: + json['fields'][key] = float(value) + except (ValueError, TypeError): + new_key = "{}_str".format(key) + new_value = str(value) + json['fields'][new_key] = new_value + + if RE_DIGIT_TAIL.match(new_value): + json['fields'][key] = float( + RE_DECIMAL.sub('', new_value)) + + # Infinity and NaN are not valid floats in InfluxDB + try: + if not math.isfinite(json['fields'][key]): + del json['fields'][key] + except (KeyError, TypeError): + pass + + json['tags'].update(tags) + + return json + + instance = hass.data[DOMAIN] = InfluxThread( + hass, influx, event_to_json, max_tries) + instance.start() + + def shutdown(event): + """Shut down the thread.""" + instance.queue.put(None) + instance.join() + influx.close() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) + + return True + + +class InfluxThread(threading.Thread): + """A threaded event handler class.""" + + def __init__(self, hass, influx, event_to_json, max_tries): + """Initialize the listener.""" + threading.Thread.__init__(self, name='InfluxDB') + self.queue = queue.Queue() + self.influx = influx + self.event_to_json = event_to_json + self.max_tries = max_tries + self.write_errors = 0 + self.shutdown = False + hass.bus.listen(EVENT_STATE_CHANGED, self._event_listener) + + def _event_listener(self, event): + """Listen for new messages on the bus and queue them for Influx.""" + item = (time.monotonic(), event) + self.queue.put(item) + + @staticmethod + def batch_timeout(): + """Return number of seconds to wait for more events.""" + return BATCH_TIMEOUT + + def get_events_json(self): + """Return a batch of events formatted for writing.""" + queue_seconds = QUEUE_BACKLOG_SECONDS + self.max_tries*RETRY_DELAY + + count = 0 + json = [] + + dropped = 0 + + try: + while len(json) < BATCH_BUFFER_SIZE and not self.shutdown: + timeout = None if count == 0 else self.batch_timeout() + item = self.queue.get(timeout=timeout) + count += 1 + + if item is None: + self.shutdown = True + else: + timestamp, event = item + age = time.monotonic() - timestamp + + if age < queue_seconds: + event_json = self.event_to_json(event) + if event_json: + json.append(event_json) + else: + dropped += 1 + + except queue.Empty: + pass + + if dropped: + _LOGGER.warning("Catching up, dropped %d old events", dropped) + + return count, json + + def write_to_influxdb(self, json): + """Write preprocessed events to influxdb, with retry.""" + from influxdb import exceptions + + for retry in range(self.max_tries+1): + try: + self.influx.write_points(json) + + if self.write_errors: + _LOGGER.error("Resumed, lost %d events", self.write_errors) + self.write_errors = 0 + + _LOGGER.debug("Wrote %d events", len(json)) + break + except (exceptions.InfluxDBClientError, IOError): + if retry < self.max_tries: + time.sleep(RETRY_DELAY) + else: + if not self.write_errors: + _LOGGER.exception("Write error") + self.write_errors += len(json) + + def run(self): + """Process incoming events.""" + while not self.shutdown: + count, json = self.get_events_json() + if json: + self.write_to_influxdb(json) + for _ in range(count): + self.queue.task_done() + + def block_till_done(self): + """Block till all events processed.""" + self.queue.join() diff --git a/homeassistant/components/input_boolean.py b/homeassistant/components/input_boolean.py deleted file mode 100644 index 896de61130cb6..0000000000000 --- a/homeassistant/components/input_boolean.py +++ /dev/null @@ -1,135 +0,0 @@ -""" -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/ -""" -import logging - -import voluptuous as vol - -from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, SERVICE_TURN_OFF, SERVICE_TURN_ON, - SERVICE_TOGGLE, STATE_ON) -from homeassistant.loader import bind_hass -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import ToggleEntity -from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.restore_state import RestoreEntity - -DOMAIN = 'input_boolean' - -ENTITY_ID_FORMAT = DOMAIN + '.{}' - -_LOGGER = logging.getLogger(__name__) - -CONF_INITIAL = 'initial' - -SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, -}) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: cv.schema_with_slug_keys( - vol.Any({ - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_INITIAL): cv.boolean, - vol.Optional(CONF_ICON): cv.icon, - }, None) - ) -}, extra=vol.ALLOW_EXTRA) - - -@bind_hass -def is_on(hass, entity_id): - """Test if input_boolean is True.""" - return hass.states.is_state(entity_id, STATE_ON) - - -async def async_setup(hass, config): - """Set up an input boolean.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) - - entities = [] - - for object_id, cfg in config[DOMAIN].items(): - if not cfg: - cfg = {} - - name = cfg.get(CONF_NAME) - initial = cfg.get(CONF_INITIAL) - icon = cfg.get(CONF_ICON) - - entities.append(InputBoolean(object_id, name, initial, icon)) - - if not entities: - return False - - component.async_register_entity_service( - SERVICE_TURN_ON, SERVICE_SCHEMA, - 'async_turn_on' - ) - - component.async_register_entity_service( - SERVICE_TURN_OFF, SERVICE_SCHEMA, - 'async_turn_off' - ) - - component.async_register_entity_service( - SERVICE_TOGGLE, SERVICE_SCHEMA, - 'async_toggle' - ) - - await component.async_add_entities(entities) - return True - - -class InputBoolean(ToggleEntity, RestoreEntity): - """Representation of a boolean input.""" - - def __init__(self, object_id, name, initial, icon): - """Initialize a boolean input.""" - self.entity_id = ENTITY_ID_FORMAT.format(object_id) - self._name = name - self._state = initial - self._icon = icon - - @property - def should_poll(self): - """If entity should be polled.""" - return False - - @property - def name(self): - """Return name of the boolean input.""" - return self._name - - @property - def icon(self): - """Return the icon to be used for this entity.""" - return self._icon - - @property - def is_on(self): - """Return true if entity is on.""" - return self._state - - async def async_added_to_hass(self): - """Call when entity about to be added to hass.""" - # If not None, we got an initial value. - await super().async_added_to_hass() - if self._state is not None: - return - - state = await self.async_get_last_state() - self._state = state and state.state == STATE_ON - - async def async_turn_on(self, **kwargs): - """Turn the entity on.""" - self._state = True - await self.async_update_ha_state() - - async def async_turn_off(self, **kwargs): - """Turn the entity off.""" - self._state = False - await self.async_update_ha_state() diff --git a/homeassistant/components/input_boolean/__init__.py b/homeassistant/components/input_boolean/__init__.py new file mode 100644 index 0000000000000..246af2613a786 --- /dev/null +++ b/homeassistant/components/input_boolean/__init__.py @@ -0,0 +1,130 @@ +"""Support to keep track of user controlled booleans for within automation.""" +import logging + +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, SERVICE_TURN_OFF, SERVICE_TURN_ON, + SERVICE_TOGGLE, STATE_ON) +from homeassistant.loader import bind_hass +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.restore_state import RestoreEntity + +DOMAIN = 'input_boolean' + +ENTITY_ID_FORMAT = DOMAIN + '.{}' + +_LOGGER = logging.getLogger(__name__) + +CONF_INITIAL = 'initial' + +SERVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: cv.schema_with_slug_keys( + vol.Any({ + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_INITIAL): cv.boolean, + vol.Optional(CONF_ICON): cv.icon, + }, None) + ) +}, extra=vol.ALLOW_EXTRA) + + +@bind_hass +def is_on(hass, entity_id): + """Test if input_boolean is True.""" + return hass.states.is_state(entity_id, STATE_ON) + + +async def async_setup(hass, config): + """Set up an input boolean.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + + entities = [] + + for object_id, cfg in config[DOMAIN].items(): + if not cfg: + cfg = {} + + name = cfg.get(CONF_NAME) + initial = cfg.get(CONF_INITIAL) + icon = cfg.get(CONF_ICON) + + entities.append(InputBoolean(object_id, name, initial, icon)) + + if not entities: + return False + + component.async_register_entity_service( + SERVICE_TURN_ON, SERVICE_SCHEMA, + 'async_turn_on' + ) + + component.async_register_entity_service( + SERVICE_TURN_OFF, SERVICE_SCHEMA, + 'async_turn_off' + ) + + component.async_register_entity_service( + SERVICE_TOGGLE, SERVICE_SCHEMA, + 'async_toggle' + ) + + await component.async_add_entities(entities) + return True + + +class InputBoolean(ToggleEntity, RestoreEntity): + """Representation of a boolean input.""" + + def __init__(self, object_id, name, initial, icon): + """Initialize a boolean input.""" + self.entity_id = ENTITY_ID_FORMAT.format(object_id) + self._name = name + self._state = initial + self._icon = icon + + @property + def should_poll(self): + """If entity should be polled.""" + return False + + @property + def name(self): + """Return name of the boolean input.""" + return self._name + + @property + def icon(self): + """Return the icon to be used for this entity.""" + return self._icon + + @property + def is_on(self): + """Return true if entity is on.""" + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + if self._state is not None: + return + + state = await self.async_get_last_state() + self._state = state and state.state == STATE_ON + + async def async_turn_on(self, **kwargs): + """Turn the entity on.""" + self._state = True + await self.async_update_ha_state() + + async def async_turn_off(self, **kwargs): + """Turn the entity off.""" + self._state = False + await self.async_update_ha_state() diff --git a/homeassistant/components/input_boolean/services.yaml b/homeassistant/components/input_boolean/services.yaml new file mode 100644 index 0000000000000..e49d46c9b8604 --- /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.py deleted file mode 100644 index 63dcc364c9c94..0000000000000 --- a/homeassistant/components/input_datetime.py +++ /dev/null @@ -1,200 +0,0 @@ -""" -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/ -""" -import logging -import datetime - -import voluptuous as vol - -from homeassistant.const import ATTR_ENTITY_ID, CONF_ICON, CONF_NAME -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.util import dt as dt_util - - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'input_datetime' -ENTITY_ID_FORMAT = DOMAIN + '.{}' - -CONF_HAS_DATE = 'has_date' -CONF_HAS_TIME = 'has_time' -CONF_INITIAL = 'initial' - -ATTR_DATE = 'date' -ATTR_TIME = 'time' - -SERVICE_SET_DATETIME = 'set_datetime' - -SERVICE_SET_DATETIME_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Optional(ATTR_DATE): cv.date, - vol.Optional(ATTR_TIME): cv.time, -}) - - -def has_date_or_time(conf): - """Check at least date or time is true.""" - if conf[CONF_HAS_DATE] or conf[CONF_HAS_TIME]: - return conf - - raise vol.Invalid('Entity needs at least a date or a time') - - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: cv.schema_with_slug_keys( - vol.All({ - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_HAS_DATE, default=False): cv.boolean, - vol.Optional(CONF_HAS_TIME, default=False): cv.boolean, - vol.Optional(CONF_ICON): cv.icon, - vol.Optional(CONF_INITIAL): cv.string, - }, has_date_or_time) - ) -}, extra=vol.ALLOW_EXTRA) - - -async def async_setup(hass, config): - """Set up an input datetime.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) - - entities = [] - - for object_id, cfg in config[DOMAIN].items(): - name = cfg.get(CONF_NAME) - has_time = cfg.get(CONF_HAS_TIME) - has_date = cfg.get(CONF_HAS_DATE) - icon = cfg.get(CONF_ICON) - initial = cfg.get(CONF_INITIAL) - entities.append(InputDatetime(object_id, name, - has_date, has_time, icon, initial)) - - if not entities: - return False - - async def async_set_datetime_service(entity, call): - """Handle a call to the input datetime 'set datetime' service.""" - time = call.data.get(ATTR_TIME) - date = call.data.get(ATTR_DATE) - if (entity.has_date and not date) or (entity.has_time and not time): - _LOGGER.error("Invalid service data for %s " - "input_datetime.set_datetime: %s", - entity.entity_id, str(call.data)) - return - - entity.async_set_datetime(date, time) - - component.async_register_entity_service( - SERVICE_SET_DATETIME, SERVICE_SET_DATETIME_SCHEMA, - async_set_datetime_service - ) - - await component.async_add_entities(entities) - return True - - -class InputDatetime(RestoreEntity): - """Representation of a datetime input.""" - - def __init__(self, object_id, name, has_date, has_time, icon, initial): - """Initialize a select input.""" - self.entity_id = ENTITY_ID_FORMAT.format(object_id) - self._name = name - self.has_date = has_date - self.has_time = has_time - self._icon = icon - self._initial = initial - self._current_datetime = None - - async def async_added_to_hass(self): - """Run when entity about to be added.""" - await super().async_added_to_hass() - restore_val = None - - # Priority 1: Initial State - if self._initial is not None: - restore_val = self._initial - - # Priority 2: Old state - if restore_val is None: - old_state = await self.async_get_last_state() - if old_state is not None: - restore_val = old_state.state - - if restore_val is not None: - if not self.has_date: - self._current_datetime = dt_util.parse_time(restore_val) - elif not self.has_time: - self._current_datetime = dt_util.parse_date(restore_val) - else: - self._current_datetime = dt_util.parse_datetime(restore_val) - - @property - def should_poll(self): - """If entity should be polled.""" - return False - - @property - def name(self): - """Return the name of the select input.""" - return self._name - - @property - def icon(self): - """Return the icon to be used for this entity.""" - return self._icon - - @property - def state(self): - """Return the state of the component.""" - return self._current_datetime - - @property - def state_attributes(self): - """Return the state attributes.""" - attrs = { - 'has_date': self.has_date, - 'has_time': self.has_time, - } - - if self._current_datetime is None: - return attrs - - if self.has_date and self._current_datetime is not None: - attrs['year'] = self._current_datetime.year - attrs['month'] = self._current_datetime.month - attrs['day'] = self._current_datetime.day - - if self.has_time and self._current_datetime is not None: - attrs['hour'] = self._current_datetime.hour - attrs['minute'] = self._current_datetime.minute - attrs['second'] = self._current_datetime.second - - if not self.has_date: - attrs['timestamp'] = self._current_datetime.hour * 3600 + \ - self._current_datetime.minute * 60 + \ - self._current_datetime.second - elif not self.has_time: - extended = datetime.datetime.combine(self._current_datetime, - datetime.time(0, 0)) - attrs['timestamp'] = extended.timestamp() - else: - attrs['timestamp'] = self._current_datetime.timestamp() - - return attrs - - def async_set_datetime(self, date_val, time_val): - """Set a new date / time.""" - if self.has_date and self.has_time and date_val and time_val: - self._current_datetime = datetime.datetime.combine(date_val, - time_val) - elif self.has_date and not self.has_time and date_val: - self._current_datetime = date_val - if self.has_time and not self.has_date and time_val: - self._current_datetime = time_val - - self.async_schedule_update_ha_state() diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py new file mode 100644 index 0000000000000..34faffd202821 --- /dev/null +++ b/homeassistant/components/input_datetime/__init__.py @@ -0,0 +1,195 @@ +"""Support to select a date and/or a time.""" +import logging +import datetime + +import voluptuous as vol + +from homeassistant.const import ATTR_ENTITY_ID, CONF_ICON, CONF_NAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.util import dt as dt_util + + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'input_datetime' +ENTITY_ID_FORMAT = DOMAIN + '.{}' + +CONF_HAS_DATE = 'has_date' +CONF_HAS_TIME = 'has_time' +CONF_INITIAL = 'initial' + +ATTR_DATE = 'date' +ATTR_TIME = 'time' + +SERVICE_SET_DATETIME = 'set_datetime' + +SERVICE_SET_DATETIME_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_DATE): cv.date, + vol.Optional(ATTR_TIME): cv.time, +}) + + +def has_date_or_time(conf): + """Check at least date or time is true.""" + if conf[CONF_HAS_DATE] or conf[CONF_HAS_TIME]: + return conf + + raise vol.Invalid('Entity needs at least a date or a time') + + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: cv.schema_with_slug_keys( + vol.All({ + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_HAS_DATE, default=False): cv.boolean, + vol.Optional(CONF_HAS_TIME, default=False): cv.boolean, + vol.Optional(CONF_ICON): cv.icon, + vol.Optional(CONF_INITIAL): cv.string, + }, has_date_or_time) + ) +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up an input datetime.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + + entities = [] + + for object_id, cfg in config[DOMAIN].items(): + name = cfg.get(CONF_NAME) + has_time = cfg.get(CONF_HAS_TIME) + has_date = cfg.get(CONF_HAS_DATE) + icon = cfg.get(CONF_ICON) + initial = cfg.get(CONF_INITIAL) + entities.append(InputDatetime(object_id, name, + has_date, has_time, icon, initial)) + + if not entities: + return False + + async def async_set_datetime_service(entity, call): + """Handle a call to the input datetime 'set datetime' service.""" + time = call.data.get(ATTR_TIME) + date = call.data.get(ATTR_DATE) + if (entity.has_date and not date) or (entity.has_time and not time): + _LOGGER.error("Invalid service data for %s " + "input_datetime.set_datetime: %s", + entity.entity_id, str(call.data)) + return + + entity.async_set_datetime(date, time) + + component.async_register_entity_service( + SERVICE_SET_DATETIME, SERVICE_SET_DATETIME_SCHEMA, + async_set_datetime_service + ) + + await component.async_add_entities(entities) + return True + + +class InputDatetime(RestoreEntity): + """Representation of a datetime input.""" + + def __init__(self, object_id, name, has_date, has_time, icon, initial): + """Initialize a select input.""" + self.entity_id = ENTITY_ID_FORMAT.format(object_id) + self._name = name + self.has_date = has_date + self.has_time = has_time + self._icon = icon + self._initial = initial + self._current_datetime = None + + async def async_added_to_hass(self): + """Run when entity about to be added.""" + await super().async_added_to_hass() + restore_val = None + + # Priority 1: Initial State + if self._initial is not None: + restore_val = self._initial + + # Priority 2: Old state + if restore_val is None: + old_state = await self.async_get_last_state() + if old_state is not None: + restore_val = old_state.state + + if restore_val is not None: + if not self.has_date: + self._current_datetime = dt_util.parse_time(restore_val) + elif not self.has_time: + self._current_datetime = dt_util.parse_date(restore_val) + else: + self._current_datetime = dt_util.parse_datetime(restore_val) + + @property + def should_poll(self): + """If entity should be polled.""" + return False + + @property + def name(self): + """Return the name of the select input.""" + return self._name + + @property + def icon(self): + """Return the icon to be used for this entity.""" + return self._icon + + @property + def state(self): + """Return the state of the component.""" + return self._current_datetime + + @property + def state_attributes(self): + """Return the state attributes.""" + attrs = { + 'has_date': self.has_date, + 'has_time': self.has_time, + } + + if self._current_datetime is None: + return attrs + + if self.has_date and self._current_datetime is not None: + attrs['year'] = self._current_datetime.year + attrs['month'] = self._current_datetime.month + attrs['day'] = self._current_datetime.day + + if self.has_time and self._current_datetime is not None: + attrs['hour'] = self._current_datetime.hour + attrs['minute'] = self._current_datetime.minute + attrs['second'] = self._current_datetime.second + + if not self.has_date: + attrs['timestamp'] = self._current_datetime.hour * 3600 + \ + self._current_datetime.minute * 60 + \ + self._current_datetime.second + elif not self.has_time: + extended = datetime.datetime.combine(self._current_datetime, + datetime.time(0, 0)) + attrs['timestamp'] = extended.timestamp() + else: + attrs['timestamp'] = self._current_datetime.timestamp() + + return attrs + + def async_set_datetime(self, date_val, time_val): + """Set a new date / time.""" + if self.has_date and self.has_time and date_val and time_val: + self._current_datetime = datetime.datetime.combine(date_val, + time_val) + elif self.has_date and not self.has_time and date_val: + self._current_datetime = date_val + if self.has_time and not self.has_date and time_val: + self._current_datetime = time_val + + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/input_number.py b/homeassistant/components/input_number.py deleted file mode 100644 index 8cfa7abaf202c..0000000000000 --- a/homeassistant/components/input_number.py +++ /dev/null @@ -1,221 +0,0 @@ -""" -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/ -""" -import logging - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_NAME, CONF_MODE) -from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.restore_state import RestoreEntity - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'input_number' -ENTITY_ID_FORMAT = DOMAIN + '.{}' - -CONF_INITIAL = 'initial' -CONF_MIN = 'min' -CONF_MAX = 'max' -CONF_STEP = 'step' - -MODE_SLIDER = 'slider' -MODE_BOX = 'box' - -ATTR_INITIAL = 'initial' -ATTR_VALUE = 'value' -ATTR_MIN = 'min' -ATTR_MAX = 'max' -ATTR_STEP = 'step' -ATTR_MODE = 'mode' - -SERVICE_SET_VALUE = 'set_value' -SERVICE_INCREMENT = 'increment' -SERVICE_DECREMENT = 'decrement' - -SERVICE_DEFAULT_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids -}) - -SERVICE_SET_VALUE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_VALUE): vol.Coerce(float), -}) - - -def _cv_input_number(cfg): - """Configure validation helper for input number (voluptuous).""" - minimum = cfg.get(CONF_MIN) - maximum = cfg.get(CONF_MAX) - if minimum >= maximum: - raise vol.Invalid('Maximum ({}) is not greater than minimum ({})' - .format(minimum, maximum)) - state = cfg.get(CONF_INITIAL) - if state is not None and (state < minimum or state > maximum): - raise vol.Invalid('Initial value {} not in range {}-{}' - .format(state, minimum, maximum)) - return cfg - - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: cv.schema_with_slug_keys( - vol.All({ - vol.Optional(CONF_NAME): cv.string, - vol.Required(CONF_MIN): vol.Coerce(float), - vol.Required(CONF_MAX): vol.Coerce(float), - vol.Optional(CONF_INITIAL): vol.Coerce(float), - vol.Optional(CONF_STEP, default=1): - vol.All(vol.Coerce(float), vol.Range(min=1e-3)), - vol.Optional(CONF_ICON): cv.icon, - vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string, - vol.Optional(CONF_MODE, default=MODE_SLIDER): - vol.In([MODE_BOX, MODE_SLIDER]), - }, _cv_input_number) - ) -}, required=True, extra=vol.ALLOW_EXTRA) - - -async def async_setup(hass, config): - """Set up an input slider.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) - - entities = [] - - for object_id, cfg in config[DOMAIN].items(): - name = cfg.get(CONF_NAME) - minimum = cfg.get(CONF_MIN) - maximum = cfg.get(CONF_MAX) - initial = cfg.get(CONF_INITIAL) - step = cfg.get(CONF_STEP) - icon = cfg.get(CONF_ICON) - unit = cfg.get(ATTR_UNIT_OF_MEASUREMENT) - mode = cfg.get(CONF_MODE) - - entities.append(InputNumber( - object_id, name, initial, minimum, maximum, step, icon, unit, - mode)) - - if not entities: - return False - - component.async_register_entity_service( - SERVICE_SET_VALUE, SERVICE_SET_VALUE_SCHEMA, - 'async_set_value' - ) - - component.async_register_entity_service( - SERVICE_INCREMENT, SERVICE_DEFAULT_SCHEMA, - 'async_increment' - ) - - component.async_register_entity_service( - SERVICE_DECREMENT, SERVICE_DEFAULT_SCHEMA, - 'async_decrement' - ) - - await component.async_add_entities(entities) - return True - - -class InputNumber(RestoreEntity): - """Representation of a slider.""" - - def __init__(self, object_id, name, initial, minimum, maximum, step, icon, - unit, mode): - """Initialize an input number.""" - self.entity_id = ENTITY_ID_FORMAT.format(object_id) - self._name = name - self._current_value = initial - self._initial = initial - self._minimum = minimum - self._maximum = maximum - self._step = step - self._icon = icon - self._unit = unit - self._mode = mode - - @property - def should_poll(self): - """If entity should be polled.""" - return False - - @property - def name(self): - """Return the name of the input slider.""" - return self._name - - @property - def icon(self): - """Return the icon to be used for this entity.""" - return self._icon - - @property - def state(self): - """Return the state of the component.""" - return self._current_value - - @property - def unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._unit - - @property - def state_attributes(self): - """Return the state attributes.""" - return { - ATTR_INITIAL: self._initial, - ATTR_MIN: self._minimum, - ATTR_MAX: self._maximum, - ATTR_STEP: self._step, - ATTR_MODE: self._mode, - } - - async def async_added_to_hass(self): - """Run when entity about to be added to hass.""" - await super().async_added_to_hass() - if self._current_value is not None: - return - - state = await self.async_get_last_state() - value = state and float(state.state) - - # Check against None because value can be 0 - if value is not None and self._minimum <= value <= self._maximum: - self._current_value = value - else: - self._current_value = self._minimum - - async def async_set_value(self, value): - """Set new value.""" - num_value = float(value) - if num_value < self._minimum or num_value > self._maximum: - _LOGGER.warning("Invalid value: %s (range %s - %s)", - num_value, self._minimum, self._maximum) - return - self._current_value = num_value - await self.async_update_ha_state() - - async def async_increment(self): - """Increment value.""" - new_value = self._current_value + self._step - if new_value > self._maximum: - _LOGGER.warning("Invalid value: %s (range %s - %s)", - new_value, self._minimum, self._maximum) - return - self._current_value = new_value - await self.async_update_ha_state() - - async def async_decrement(self): - """Decrement value.""" - new_value = self._current_value - self._step - if new_value < self._minimum: - _LOGGER.warning("Invalid value: %s (range %s - %s)", - new_value, self._minimum, self._maximum) - return - self._current_value = new_value - await self.async_update_ha_state() diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py new file mode 100644 index 0000000000000..d9d3ac8bbc067 --- /dev/null +++ b/homeassistant/components/input_number/__init__.py @@ -0,0 +1,216 @@ +"""Support to set a numeric value from a slider or text box.""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_NAME, CONF_MODE) +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.restore_state import RestoreEntity + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'input_number' +ENTITY_ID_FORMAT = DOMAIN + '.{}' + +CONF_INITIAL = 'initial' +CONF_MIN = 'min' +CONF_MAX = 'max' +CONF_STEP = 'step' + +MODE_SLIDER = 'slider' +MODE_BOX = 'box' + +ATTR_INITIAL = 'initial' +ATTR_VALUE = 'value' +ATTR_MIN = 'min' +ATTR_MAX = 'max' +ATTR_STEP = 'step' +ATTR_MODE = 'mode' + +SERVICE_SET_VALUE = 'set_value' +SERVICE_INCREMENT = 'increment' +SERVICE_DECREMENT = 'decrement' + +SERVICE_DEFAULT_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids +}) + +SERVICE_SET_VALUE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_VALUE): vol.Coerce(float), +}) + + +def _cv_input_number(cfg): + """Configure validation helper for input number (voluptuous).""" + minimum = cfg.get(CONF_MIN) + maximum = cfg.get(CONF_MAX) + if minimum >= maximum: + raise vol.Invalid('Maximum ({}) is not greater than minimum ({})' + .format(minimum, maximum)) + state = cfg.get(CONF_INITIAL) + if state is not None and (state < minimum or state > maximum): + raise vol.Invalid('Initial value {} not in range {}-{}' + .format(state, minimum, maximum)) + return cfg + + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: cv.schema_with_slug_keys( + vol.All({ + vol.Optional(CONF_NAME): cv.string, + vol.Required(CONF_MIN): vol.Coerce(float), + vol.Required(CONF_MAX): vol.Coerce(float), + vol.Optional(CONF_INITIAL): vol.Coerce(float), + vol.Optional(CONF_STEP, default=1): + vol.All(vol.Coerce(float), vol.Range(min=1e-3)), + vol.Optional(CONF_ICON): cv.icon, + vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_MODE, default=MODE_SLIDER): + vol.In([MODE_BOX, MODE_SLIDER]), + }, _cv_input_number) + ) +}, required=True, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up an input slider.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + + entities = [] + + for object_id, cfg in config[DOMAIN].items(): + name = cfg.get(CONF_NAME) + minimum = cfg.get(CONF_MIN) + maximum = cfg.get(CONF_MAX) + initial = cfg.get(CONF_INITIAL) + step = cfg.get(CONF_STEP) + icon = cfg.get(CONF_ICON) + unit = cfg.get(ATTR_UNIT_OF_MEASUREMENT) + mode = cfg.get(CONF_MODE) + + entities.append(InputNumber( + object_id, name, initial, minimum, maximum, step, icon, unit, + mode)) + + if not entities: + return False + + component.async_register_entity_service( + SERVICE_SET_VALUE, SERVICE_SET_VALUE_SCHEMA, + 'async_set_value' + ) + + component.async_register_entity_service( + SERVICE_INCREMENT, SERVICE_DEFAULT_SCHEMA, + 'async_increment' + ) + + component.async_register_entity_service( + SERVICE_DECREMENT, SERVICE_DEFAULT_SCHEMA, + 'async_decrement' + ) + + await component.async_add_entities(entities) + return True + + +class InputNumber(RestoreEntity): + """Representation of a slider.""" + + def __init__(self, object_id, name, initial, minimum, maximum, step, icon, + unit, mode): + """Initialize an input number.""" + self.entity_id = ENTITY_ID_FORMAT.format(object_id) + self._name = name + self._current_value = initial + self._initial = initial + self._minimum = minimum + self._maximum = maximum + self._step = step + self._icon = icon + self._unit = unit + self._mode = mode + + @property + def should_poll(self): + """If entity should be polled.""" + return False + + @property + def name(self): + """Return the name of the input slider.""" + return self._name + + @property + def icon(self): + """Return the icon to be used for this entity.""" + return self._icon + + @property + def state(self): + """Return the state of the component.""" + return self._current_value + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit + + @property + def state_attributes(self): + """Return the state attributes.""" + return { + ATTR_INITIAL: self._initial, + ATTR_MIN: self._minimum, + ATTR_MAX: self._maximum, + ATTR_STEP: self._step, + ATTR_MODE: self._mode, + } + + async def async_added_to_hass(self): + """Run when entity about to be added to hass.""" + await super().async_added_to_hass() + if self._current_value is not None: + return + + state = await self.async_get_last_state() + value = state and float(state.state) + + # Check against None because value can be 0 + if value is not None and self._minimum <= value <= self._maximum: + self._current_value = value + else: + self._current_value = self._minimum + + async def async_set_value(self, value): + """Set new value.""" + num_value = float(value) + if num_value < self._minimum or num_value > self._maximum: + _LOGGER.warning("Invalid value: %s (range %s - %s)", + num_value, self._minimum, self._maximum) + return + self._current_value = num_value + await self.async_update_ha_state() + + async def async_increment(self): + """Increment value.""" + new_value = self._current_value + self._step + if new_value > self._maximum: + _LOGGER.warning("Invalid value: %s (range %s - %s)", + new_value, self._minimum, self._maximum) + return + self._current_value = new_value + await self.async_update_ha_state() + + async def async_decrement(self): + """Decrement value.""" + new_value = self._current_value - self._step + if new_value < self._minimum: + _LOGGER.warning("Invalid value: %s (range %s - %s)", + new_value, self._minimum, self._maximum) + return + self._current_value = new_value + await self.async_update_ha_state() diff --git a/homeassistant/components/input_number/services.yaml b/homeassistant/components/input_number/services.yaml new file mode 100644 index 0000000000000..650abc056a97c --- /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.py deleted file mode 100644 index fc858e7539759..0000000000000 --- a/homeassistant/components/input_select.py +++ /dev/null @@ -1,189 +0,0 @@ -""" -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/ -""" -import logging - -import voluptuous as vol - -from homeassistant.const import ATTR_ENTITY_ID, CONF_ICON, CONF_NAME -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.restore_state import RestoreEntity - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'input_select' -ENTITY_ID_FORMAT = DOMAIN + '.{}' - -CONF_INITIAL = 'initial' -CONF_OPTIONS = 'options' - -ATTR_OPTION = 'option' -ATTR_OPTIONS = 'options' - -SERVICE_SELECT_OPTION = 'select_option' - -SERVICE_SELECT_OPTION_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_OPTION): cv.string, -}) - -SERVICE_SELECT_NEXT = 'select_next' - -SERVICE_SELECT_NEXT_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, -}) - -SERVICE_SELECT_PREVIOUS = 'select_previous' - -SERVICE_SELECT_PREVIOUS_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, -}) - - -SERVICE_SET_OPTIONS = 'set_options' - -SERVICE_SET_OPTIONS_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_OPTIONS): - vol.All(cv.ensure_list, vol.Length(min=1), [cv.string]), -}) - - -def _cv_input_select(cfg): - """Configure validation helper for input select (voluptuous).""" - options = cfg[CONF_OPTIONS] - initial = cfg.get(CONF_INITIAL) - if initial is not None and initial not in options: - raise vol.Invalid('initial state "{}" is not part of the options: {}' - .format(initial, ','.join(options))) - return cfg - - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: cv.schema_with_slug_keys( - vol.All({ - vol.Optional(CONF_NAME): cv.string, - vol.Required(CONF_OPTIONS): - vol.All(cv.ensure_list, vol.Length(min=1), [cv.string]), - vol.Optional(CONF_INITIAL): cv.string, - vol.Optional(CONF_ICON): cv.icon, - }, _cv_input_select) - ) -}, required=True, extra=vol.ALLOW_EXTRA) - - -async def async_setup(hass, config): - """Set up an input select.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) - - entities = [] - - for object_id, cfg in config[DOMAIN].items(): - name = cfg.get(CONF_NAME) - options = cfg.get(CONF_OPTIONS) - initial = cfg.get(CONF_INITIAL) - icon = cfg.get(CONF_ICON) - entities.append(InputSelect(object_id, name, initial, options, icon)) - - if not entities: - return False - - component.async_register_entity_service( - SERVICE_SELECT_OPTION, SERVICE_SELECT_OPTION_SCHEMA, - 'async_select_option' - ) - - component.async_register_entity_service( - SERVICE_SELECT_NEXT, SERVICE_SELECT_NEXT_SCHEMA, - lambda entity, call: entity.async_offset_index(1) - ) - - component.async_register_entity_service( - SERVICE_SELECT_PREVIOUS, SERVICE_SELECT_PREVIOUS_SCHEMA, - lambda entity, call: entity.async_offset_index(-1) - ) - - component.async_register_entity_service( - SERVICE_SET_OPTIONS, SERVICE_SET_OPTIONS_SCHEMA, - 'async_set_options' - ) - - await component.async_add_entities(entities) - return True - - -class InputSelect(RestoreEntity): - """Representation of a select input.""" - - def __init__(self, object_id, name, initial, options, icon): - """Initialize a select input.""" - self.entity_id = ENTITY_ID_FORMAT.format(object_id) - self._name = name - self._current_option = initial - self._options = options - self._icon = icon - - async def async_added_to_hass(self): - """Run when entity about to be added.""" - await super().async_added_to_hass() - if self._current_option is not None: - return - - state = await self.async_get_last_state() - if not state or state.state not in self._options: - self._current_option = self._options[0] - else: - self._current_option = state.state - - @property - def should_poll(self): - """If entity should be polled.""" - return False - - @property - def name(self): - """Return the name of the select input.""" - return self._name - - @property - def icon(self): - """Return the icon to be used for this entity.""" - return self._icon - - @property - def state(self): - """Return the state of the component.""" - return self._current_option - - @property - def state_attributes(self): - """Return the state attributes.""" - return { - ATTR_OPTIONS: self._options, - } - - async def async_select_option(self, option): - """Select new option.""" - if option not in self._options: - _LOGGER.warning('Invalid option: %s (possible options: %s)', - option, ', '.join(self._options)) - return - self._current_option = option - await self.async_update_ha_state() - - async def async_offset_index(self, offset): - """Offset current index.""" - current_index = self._options.index(self._current_option) - new_index = (current_index + offset) % len(self._options) - self._current_option = self._options[new_index] - await self.async_update_ha_state() - - async def async_set_options(self, options): - """Set options.""" - self._current_option = options[0] - self._options = options - await self.async_update_ha_state() diff --git a/homeassistant/components/input_select/__init__.py b/homeassistant/components/input_select/__init__.py new file mode 100644 index 0000000000000..fd3e4335c337b --- /dev/null +++ b/homeassistant/components/input_select/__init__.py @@ -0,0 +1,184 @@ +"""Support to select an option from a list.""" +import logging + +import voluptuous as vol + +from homeassistant.const import ATTR_ENTITY_ID, CONF_ICON, CONF_NAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.restore_state import RestoreEntity + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'input_select' +ENTITY_ID_FORMAT = DOMAIN + '.{}' + +CONF_INITIAL = 'initial' +CONF_OPTIONS = 'options' + +ATTR_OPTION = 'option' +ATTR_OPTIONS = 'options' + +SERVICE_SELECT_OPTION = 'select_option' + +SERVICE_SELECT_OPTION_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_OPTION): cv.string, +}) + +SERVICE_SELECT_NEXT = 'select_next' + +SERVICE_SELECT_NEXT_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, +}) + +SERVICE_SELECT_PREVIOUS = 'select_previous' + +SERVICE_SELECT_PREVIOUS_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, +}) + + +SERVICE_SET_OPTIONS = 'set_options' + +SERVICE_SET_OPTIONS_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_OPTIONS): + vol.All(cv.ensure_list, vol.Length(min=1), [cv.string]), +}) + + +def _cv_input_select(cfg): + """Configure validation helper for input select (voluptuous).""" + options = cfg[CONF_OPTIONS] + initial = cfg.get(CONF_INITIAL) + if initial is not None and initial not in options: + raise vol.Invalid('initial state "{}" is not part of the options: {}' + .format(initial, ','.join(options))) + return cfg + + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: cv.schema_with_slug_keys( + vol.All({ + vol.Optional(CONF_NAME): cv.string, + vol.Required(CONF_OPTIONS): + vol.All(cv.ensure_list, vol.Length(min=1), [cv.string]), + vol.Optional(CONF_INITIAL): cv.string, + vol.Optional(CONF_ICON): cv.icon, + }, _cv_input_select) + ) +}, required=True, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up an input select.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + + entities = [] + + for object_id, cfg in config[DOMAIN].items(): + name = cfg.get(CONF_NAME) + options = cfg.get(CONF_OPTIONS) + initial = cfg.get(CONF_INITIAL) + icon = cfg.get(CONF_ICON) + entities.append(InputSelect(object_id, name, initial, options, icon)) + + if not entities: + return False + + component.async_register_entity_service( + SERVICE_SELECT_OPTION, SERVICE_SELECT_OPTION_SCHEMA, + 'async_select_option' + ) + + component.async_register_entity_service( + SERVICE_SELECT_NEXT, SERVICE_SELECT_NEXT_SCHEMA, + lambda entity, call: entity.async_offset_index(1) + ) + + component.async_register_entity_service( + SERVICE_SELECT_PREVIOUS, SERVICE_SELECT_PREVIOUS_SCHEMA, + lambda entity, call: entity.async_offset_index(-1) + ) + + component.async_register_entity_service( + SERVICE_SET_OPTIONS, SERVICE_SET_OPTIONS_SCHEMA, + 'async_set_options' + ) + + await component.async_add_entities(entities) + return True + + +class InputSelect(RestoreEntity): + """Representation of a select input.""" + + def __init__(self, object_id, name, initial, options, icon): + """Initialize a select input.""" + self.entity_id = ENTITY_ID_FORMAT.format(object_id) + self._name = name + self._current_option = initial + self._options = options + self._icon = icon + + async def async_added_to_hass(self): + """Run when entity about to be added.""" + await super().async_added_to_hass() + if self._current_option is not None: + return + + state = await self.async_get_last_state() + if not state or state.state not in self._options: + self._current_option = self._options[0] + else: + self._current_option = state.state + + @property + def should_poll(self): + """If entity should be polled.""" + return False + + @property + def name(self): + """Return the name of the select input.""" + return self._name + + @property + def icon(self): + """Return the icon to be used for this entity.""" + return self._icon + + @property + def state(self): + """Return the state of the component.""" + return self._current_option + + @property + def state_attributes(self): + """Return the state attributes.""" + return { + ATTR_OPTIONS: self._options, + } + + async def async_select_option(self, option): + """Select new option.""" + if option not in self._options: + _LOGGER.warning('Invalid option: %s (possible options: %s)', + option, ', '.join(self._options)) + return + self._current_option = option + await self.async_update_ha_state() + + async def async_offset_index(self, offset): + """Offset current index.""" + current_index = self._options.index(self._current_option) + new_index = (current_index + offset) % len(self._options) + self._current_option = self._options[new_index] + await self.async_update_ha_state() + + async def async_set_options(self, options): + """Set options.""" + self._current_option = options[0] + self._options = options + await self.async_update_ha_state() diff --git a/homeassistant/components/input_select/services.yaml b/homeassistant/components/input_select/services.yaml new file mode 100644 index 0000000000000..8084e56b731e7 --- /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.py deleted file mode 100644 index 580337a3af35d..0000000000000 --- a/homeassistant/components/input_text.py +++ /dev/null @@ -1,177 +0,0 @@ -""" -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/ -""" -import logging - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_NAME, CONF_MODE) -from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.restore_state import RestoreEntity - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'input_text' -ENTITY_ID_FORMAT = DOMAIN + '.{}' - -CONF_INITIAL = 'initial' -CONF_MIN = 'min' -CONF_MAX = 'max' - -MODE_TEXT = 'text' -MODE_PASSWORD = 'password' - -ATTR_VALUE = 'value' -ATTR_MIN = 'min' -ATTR_MAX = 'max' -ATTR_PATTERN = 'pattern' -ATTR_MODE = 'mode' - -SERVICE_SET_VALUE = 'set_value' - -SERVICE_SET_VALUE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_VALUE): cv.string, -}) - - -def _cv_input_text(cfg): - """Configure validation helper for input box (voluptuous).""" - minimum = cfg.get(CONF_MIN) - maximum = cfg.get(CONF_MAX) - if minimum > maximum: - raise vol.Invalid('Max len ({}) is not greater than min len ({})' - .format(minimum, maximum)) - state = cfg.get(CONF_INITIAL) - if state is not None and (len(state) < minimum or len(state) > maximum): - raise vol.Invalid('Initial value {} length not in range {}-{}' - .format(state, minimum, maximum)) - return cfg - - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: cv.schema_with_slug_keys( - vol.All({ - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_MIN, default=0): vol.Coerce(int), - vol.Optional(CONF_MAX, default=100): vol.Coerce(int), - vol.Optional(CONF_INITIAL, ''): cv.string, - vol.Optional(CONF_ICON): cv.icon, - vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string, - vol.Optional(ATTR_PATTERN): cv.string, - vol.Optional(CONF_MODE, default=MODE_TEXT): - vol.In([MODE_TEXT, MODE_PASSWORD]), - }, _cv_input_text) - ) -}, required=True, extra=vol.ALLOW_EXTRA) - - -async def async_setup(hass, config): - """Set up an input text box.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) - - entities = [] - - for object_id, cfg in config[DOMAIN].items(): - name = cfg.get(CONF_NAME) - minimum = cfg.get(CONF_MIN) - maximum = cfg.get(CONF_MAX) - initial = cfg.get(CONF_INITIAL) - icon = cfg.get(CONF_ICON) - unit = cfg.get(ATTR_UNIT_OF_MEASUREMENT) - pattern = cfg.get(ATTR_PATTERN) - mode = cfg.get(CONF_MODE) - - entities.append(InputText( - object_id, name, initial, minimum, maximum, icon, unit, - pattern, mode)) - - if not entities: - return False - - component.async_register_entity_service( - SERVICE_SET_VALUE, SERVICE_SET_VALUE_SCHEMA, - 'async_set_value' - ) - - await component.async_add_entities(entities) - return True - - -class InputText(RestoreEntity): - """Represent a text box.""" - - def __init__(self, object_id, name, initial, minimum, maximum, icon, - unit, pattern, mode): - """Initialize a text input.""" - self.entity_id = ENTITY_ID_FORMAT.format(object_id) - self._name = name - self._current_value = initial - self._minimum = minimum - self._maximum = maximum - self._icon = icon - self._unit = unit - self._pattern = pattern - self._mode = mode - - @property - def should_poll(self): - """If entity should be polled.""" - return False - - @property - def name(self): - """Return the name of the text input entity.""" - return self._name - - @property - def icon(self): - """Return the icon to be used for this entity.""" - return self._icon - - @property - def state(self): - """Return the state of the component.""" - return self._current_value - - @property - def unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._unit - - @property - def state_attributes(self): - """Return the state attributes.""" - return { - ATTR_MIN: self._minimum, - ATTR_MAX: self._maximum, - ATTR_PATTERN: self._pattern, - ATTR_MODE: self._mode, - } - - async def async_added_to_hass(self): - """Run when entity about to be added to hass.""" - await super().async_added_to_hass() - if self._current_value is not None: - return - - state = await self.async_get_last_state() - value = state and state.state - - # Check against None because value can be 0 - if value is not None and self._minimum <= len(value) <= self._maximum: - self._current_value = value - - async def async_set_value(self, value): - """Select new value.""" - if len(value) < self._minimum or len(value) > self._maximum: - _LOGGER.warning("Invalid value: %s (length range %s - %s)", - value, self._minimum, self._maximum) - return - self._current_value = value - await self.async_update_ha_state() diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py new file mode 100644 index 0000000000000..48a467b54a2de --- /dev/null +++ b/homeassistant/components/input_text/__init__.py @@ -0,0 +1,172 @@ +"""Support to enter a value into a text box.""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_NAME, CONF_MODE) +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.restore_state import RestoreEntity + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'input_text' +ENTITY_ID_FORMAT = DOMAIN + '.{}' + +CONF_INITIAL = 'initial' +CONF_MIN = 'min' +CONF_MAX = 'max' + +MODE_TEXT = 'text' +MODE_PASSWORD = 'password' + +ATTR_VALUE = 'value' +ATTR_MIN = 'min' +ATTR_MAX = 'max' +ATTR_PATTERN = 'pattern' +ATTR_MODE = 'mode' + +SERVICE_SET_VALUE = 'set_value' + +SERVICE_SET_VALUE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_VALUE): cv.string, +}) + + +def _cv_input_text(cfg): + """Configure validation helper for input box (voluptuous).""" + minimum = cfg.get(CONF_MIN) + maximum = cfg.get(CONF_MAX) + if minimum > maximum: + raise vol.Invalid('Max len ({}) is not greater than min len ({})' + .format(minimum, maximum)) + state = cfg.get(CONF_INITIAL) + if state is not None and (len(state) < minimum or len(state) > maximum): + raise vol.Invalid('Initial value {} length not in range {}-{}' + .format(state, minimum, maximum)) + return cfg + + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: cv.schema_with_slug_keys( + vol.All({ + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_MIN, default=0): vol.Coerce(int), + vol.Optional(CONF_MAX, default=100): vol.Coerce(int), + vol.Optional(CONF_INITIAL, ''): cv.string, + vol.Optional(CONF_ICON): cv.icon, + vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(ATTR_PATTERN): cv.string, + vol.Optional(CONF_MODE, default=MODE_TEXT): + vol.In([MODE_TEXT, MODE_PASSWORD]), + }, _cv_input_text) + ) +}, required=True, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up an input text box.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + + entities = [] + + for object_id, cfg in config[DOMAIN].items(): + name = cfg.get(CONF_NAME) + minimum = cfg.get(CONF_MIN) + maximum = cfg.get(CONF_MAX) + initial = cfg.get(CONF_INITIAL) + icon = cfg.get(CONF_ICON) + unit = cfg.get(ATTR_UNIT_OF_MEASUREMENT) + pattern = cfg.get(ATTR_PATTERN) + mode = cfg.get(CONF_MODE) + + entities.append(InputText( + object_id, name, initial, minimum, maximum, icon, unit, + pattern, mode)) + + if not entities: + return False + + component.async_register_entity_service( + SERVICE_SET_VALUE, SERVICE_SET_VALUE_SCHEMA, + 'async_set_value' + ) + + await component.async_add_entities(entities) + return True + + +class InputText(RestoreEntity): + """Represent a text box.""" + + def __init__(self, object_id, name, initial, minimum, maximum, icon, + unit, pattern, mode): + """Initialize a text input.""" + self.entity_id = ENTITY_ID_FORMAT.format(object_id) + self._name = name + self._current_value = initial + self._minimum = minimum + self._maximum = maximum + self._icon = icon + self._unit = unit + self._pattern = pattern + self._mode = mode + + @property + def should_poll(self): + """If entity should be polled.""" + return False + + @property + def name(self): + """Return the name of the text input entity.""" + return self._name + + @property + def icon(self): + """Return the icon to be used for this entity.""" + return self._icon + + @property + def state(self): + """Return the state of the component.""" + return self._current_value + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit + + @property + def state_attributes(self): + """Return the state attributes.""" + return { + ATTR_MIN: self._minimum, + ATTR_MAX: self._maximum, + ATTR_PATTERN: self._pattern, + ATTR_MODE: self._mode, + } + + async def async_added_to_hass(self): + """Run when entity about to be added to hass.""" + await super().async_added_to_hass() + if self._current_value is not None: + return + + state = await self.async_get_last_state() + value = state and state.state + + # Check against None because value can be 0 + if value is not None and self._minimum <= len(value) <= self._maximum: + self._current_value = value + + async def async_set_value(self, value): + """Select new value.""" + if len(value) < self._minimum or len(value) > self._maximum: + _LOGGER.warning("Invalid value: %s (length range %s - %s)", + value, self._minimum, self._maximum) + return + self._current_value = value + await self.async_update_ha_state() diff --git a/homeassistant/components/input_text/services.yaml b/homeassistant/components/input_text/services.yaml new file mode 100644 index 0000000000000..219eecf2fd6d8 --- /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.py deleted file mode 100644 index 003714d0f944e..0000000000000 --- a/homeassistant/components/insteon_local.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -Local support for Insteon. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/insteon_local/ -""" -import logging - -_LOGGER = logging.getLogger(__name__) - - -def setup(hass, config): - """Set up the insteon_local component. - - This component is deprecated as of release 0.77 and should be removed in - release 0.90. - """ - _LOGGER.warning('The insteon_local component has been replaced by ' - 'the insteon component') - _LOGGER.warning('Please see https://home-assistant.io/components/insteon') - - hass.components.persistent_notification.create( - 'insteon_local has been replaced by the insteon component.
' - 'Please see https://home-assistant.io/components/insteon', - title='insteon_local Component Deactivated', - notification_id='insteon_local') - - return False diff --git a/homeassistant/components/insteon_local/__init__.py b/homeassistant/components/insteon_local/__init__.py new file mode 100644 index 0000000000000..f73c46746f02c --- /dev/null +++ b/homeassistant/components/insteon_local/__init__.py @@ -0,0 +1,23 @@ +"""Local support for Insteon.""" +import logging + +_LOGGER = logging.getLogger(__name__) + + +def setup(hass, config): + """Set up the insteon_local component. + + This component is deprecated as of release 0.77 and should be removed in + release 0.90. + """ + _LOGGER.warning('The insteon_local component has been replaced by ' + 'the insteon component') + _LOGGER.warning('Please see https://home-assistant.io/components/insteon') + + hass.components.persistent_notification.create( + 'insteon_local has been replaced by the insteon component.
' + 'Please see https://home-assistant.io/components/insteon', + title='insteon_local Component Deactivated', + notification_id='insteon_local') + + return False diff --git a/homeassistant/components/insteon_plm.py b/homeassistant/components/insteon_plm.py deleted file mode 100644 index b3011e9d7bdd9..0000000000000 --- a/homeassistant/components/insteon_plm.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -Support for INSTEON PowerLinc Modem. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/insteon_plm/ -""" -import logging - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup(hass, config): - """Set up the insteon_plm component. - - This component is deprecated as of release 0.77 and should be removed in - release 0.90. - """ - _LOGGER.warning('The insteon_plm component has been replaced by ' - 'the insteon component') - _LOGGER.warning('Please see https://home-assistant.io/components/insteon') - - hass.components.persistent_notification.create( - 'insteon_plm has been replaced by the insteon component.
' - 'Please see https://home-assistant.io/components/insteon', - title='insteon_plm Component Deactivated', - notification_id='insteon_plm') - - return False diff --git a/homeassistant/components/insteon_plm/__init__.py b/homeassistant/components/insteon_plm/__init__.py new file mode 100644 index 0000000000000..5ff492b6f6c39 --- /dev/null +++ b/homeassistant/components/insteon_plm/__init__.py @@ -0,0 +1,23 @@ +"""Support for INSTEON PowerLinc Modem.""" +import logging + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass, config): + """Set up the insteon_plm component. + + This component is deprecated as of release 0.77 and should be removed in + release 0.90. + """ + _LOGGER.warning('The insteon_plm component has been replaced by ' + 'the insteon component') + _LOGGER.warning('Please see https://home-assistant.io/components/insteon') + + hass.components.persistent_notification.create( + 'insteon_plm has been replaced by the insteon component.
' + 'Please see https://home-assistant.io/components/insteon', + title='insteon_plm Component Deactivated', + notification_id='insteon_plm') + + return False 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.py deleted file mode 100644 index 17de7fcd6ca26..0000000000000 --- a/homeassistant/components/introduction.py +++ /dev/null @@ -1,61 +0,0 @@ -""" -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/ -""" -import logging - -import voluptuous as vol - -DOMAIN = 'introduction' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({}), -}, extra=vol.ALLOW_EXTRA) - - -async def async_setup(hass, config=None): - """Set up the introduction component.""" - log = logging.getLogger(__name__) - log.info(""" - - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - Hello, and welcome to Home Assistant! - - We'll hope that we can make all your dreams come true. - - Here are some resources to get started: - - - Configuring Home Assistant: - https://home-assistant.io/getting-started/configuration/ - - - Available components: - https://home-assistant.io/components/ - - - Troubleshooting your configuration: - https://home-assistant.io/getting-started/troubleshooting-configuration/ - - - Getting help: - https://home-assistant.io/help/ - - This message is generated by the introduction component. You can - disable it in configuration.yaml. - - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - """) - - hass.components.persistent_notification.async_create(""" -Here are some resources to get started: - - - [Configuring Home Assistant](https://home-assistant.io/getting-started/configuration/) - - [Available components](https://home-assistant.io/components/) - - [Troubleshooting your configuration](https://home-assistant.io/docs/configuration/troubleshooting/) - - [Getting help](https://home-assistant.io/help/) - -To not see this card popup in the future, edit your config in -`configuration.yaml` and disable the `introduction` component. -""", 'Welcome Home!') # noqa - - return True diff --git a/homeassistant/components/introduction/__init__.py b/homeassistant/components/introduction/__init__.py new file mode 100644 index 0000000000000..8a2d72ebbddaf --- /dev/null +++ b/homeassistant/components/introduction/__init__.py @@ -0,0 +1,56 @@ +"""Component that will help guide the user taking its first steps.""" +import logging + +import voluptuous as vol + +DOMAIN = 'introduction' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({}), +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config=None): + """Set up the introduction component.""" + log = logging.getLogger(__name__) + log.info(""" + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Hello, and welcome to Home Assistant! + + We'll hope that we can make all your dreams come true. + + Here are some resources to get started: + + - Configuring Home Assistant: + https://home-assistant.io/getting-started/configuration/ + + - Available components: + https://home-assistant.io/components/ + + - Troubleshooting your configuration: + https://home-assistant.io/getting-started/troubleshooting-configuration/ + + - Getting help: + https://home-assistant.io/help/ + + This message is generated by the introduction component. You can + disable it in configuration.yaml. + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + """) + + hass.components.persistent_notification.async_create(""" +Here are some resources to get started: + + - [Configuring Home Assistant](https://home-assistant.io/getting-started/configuration/) + - [Available components](https://home-assistant.io/components/) + - [Troubleshooting your configuration](https://home-assistant.io/docs/configuration/troubleshooting/) + - [Getting help](https://home-assistant.io/help/) + +To not see this card popup in the future, edit your config in +`configuration.yaml` and disable the `introduction` component. +""", 'Welcome Home!') # noqa + + return True diff --git a/homeassistant/components/ios/.translations/da.json b/homeassistant/components/ios/.translations/da.json new file mode 100644 index 0000000000000..4a900097b148e --- /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 fdcc964a0e6ba..282715ebb3b35 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.py deleted file mode 100644 index 717213da9acca..0000000000000 --- a/homeassistant/components/iota.py +++ /dev/null @@ -1,81 +0,0 @@ -""" -Support for IOTA wallets. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/iota/ -""" -import logging -from datetime import timedelta - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import load_platform -from homeassistant.helpers.entity import Entity - -REQUIREMENTS = ['pyota==2.0.5'] - -_LOGGER = logging.getLogger(__name__) - -CONF_IRI = 'iri' -CONF_TESTNET = 'testnet' -CONF_WALLET_NAME = 'name' -CONF_WALLET_SEED = 'seed' -CONF_WALLETS = 'wallets' - -DOMAIN = 'iota' - -IOTA_PLATFORMS = ['sensor'] - -SCAN_INTERVAL = timedelta(minutes=10) - -WALLET_CONFIG = vol.Schema({ - vol.Required(CONF_WALLET_NAME): cv.string, - vol.Required(CONF_WALLET_SEED): cv.string, -}) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_IRI): cv.string, - vol.Optional(CONF_TESTNET, default=False): cv.boolean, - vol.Required(CONF_WALLETS): vol.All(cv.ensure_list, [WALLET_CONFIG]), - }) -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the IOTA component.""" - iota_config = config[DOMAIN] - - for platform in IOTA_PLATFORMS: - load_platform(hass, platform, DOMAIN, iota_config, config) - - return True - - -class IotaDevice(Entity): - """Representation of a IOTA device.""" - - def __init__(self, name, seed, iri, is_testnet=False): - """Initialise the IOTA device.""" - self._name = name - self._seed = seed - self.iri = iri - self.is_testnet = is_testnet - - @property - def name(self): - """Return the default name of the device.""" - return self._name - - @property - def device_state_attributes(self): - """Return the state attributes of the device.""" - attr = {CONF_WALLET_NAME: self._name} - return attr - - @property - def api(self): - """Construct API object for interaction with the IRI node.""" - from iota import Iota - return Iota(adapter=self.iri, seed=self._seed) diff --git a/homeassistant/components/iota/__init__.py b/homeassistant/components/iota/__init__.py new file mode 100644 index 0000000000000..e28de61aad017 --- /dev/null +++ b/homeassistant/components/iota/__init__.py @@ -0,0 +1,76 @@ +"""Support for IOTA wallets.""" +import logging +from datetime import timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['pyota==2.0.5'] + +_LOGGER = logging.getLogger(__name__) + +CONF_IRI = 'iri' +CONF_TESTNET = 'testnet' +CONF_WALLET_NAME = 'name' +CONF_WALLET_SEED = 'seed' +CONF_WALLETS = 'wallets' + +DOMAIN = 'iota' + +IOTA_PLATFORMS = ['sensor'] + +SCAN_INTERVAL = timedelta(minutes=10) + +WALLET_CONFIG = vol.Schema({ + vol.Required(CONF_WALLET_NAME): cv.string, + vol.Required(CONF_WALLET_SEED): cv.string, +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_IRI): cv.string, + vol.Optional(CONF_TESTNET, default=False): cv.boolean, + vol.Required(CONF_WALLETS): vol.All(cv.ensure_list, [WALLET_CONFIG]), + }) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the IOTA component.""" + iota_config = config[DOMAIN] + + for platform in IOTA_PLATFORMS: + load_platform(hass, platform, DOMAIN, iota_config, config) + + return True + + +class IotaDevice(Entity): + """Representation of a IOTA device.""" + + def __init__(self, name, seed, iri, is_testnet=False): + """Initialise the IOTA device.""" + self._name = name + self._seed = seed + self.iri = iri + self.is_testnet = is_testnet + + @property + def name(self): + """Return the default name of the device.""" + return self._name + + @property + def device_state_attributes(self): + """Return the state attributes of the device.""" + attr = {CONF_WALLET_NAME: self._name} + return attr + + @property + def api(self): + """Construct API object for interaction with the IRI node.""" + from iota import Iota + return Iota(adapter=self.iri, seed=self._seed) diff --git a/homeassistant/components/iota/sensor.py b/homeassistant/components/iota/sensor.py new file mode 100644 index 0000000000000..5cd5db6169be0 --- /dev/null +++ b/homeassistant/components/iota/sensor.py @@ -0,0 +1,95 @@ +"""Support for IOTA wallet sensors.""" +import logging +from datetime import timedelta + +from homeassistant.components.iota import IotaDevice, CONF_WALLETS +from homeassistant.const import CONF_NAME + +_LOGGER = logging.getLogger(__name__) + +ATTR_TESTNET = 'testnet' +ATTR_URL = 'url' + +CONF_IRI = 'iri' +CONF_SEED = 'seed' +CONF_TESTNET = 'testnet' + +DEPENDENCIES = ['iota'] + +SCAN_INTERVAL = timedelta(minutes=3) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the IOTA sensor.""" + iota_config = discovery_info + sensors = [IotaBalanceSensor(wallet, iota_config) + for wallet in iota_config[CONF_WALLETS]] + + sensors.append(IotaNodeSensor(iota_config=iota_config)) + + add_entities(sensors) + + +class IotaBalanceSensor(IotaDevice): + """Implement an IOTA sensor for displaying wallets balance.""" + + def __init__(self, wallet_config, iota_config): + """Initialize the sensor.""" + super().__init__( + name=wallet_config[CONF_NAME], seed=wallet_config[CONF_SEED], + iri=iota_config[CONF_IRI], is_testnet=iota_config[CONF_TESTNET]) + self._state = None + + @property + def name(self): + """Return the name of the sensor.""" + return '{} Balance'.format(self._name) + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return 'IOTA' + + def update(self): + """Fetch new balance from IRI.""" + self._state = self.api.get_inputs()['totalBalance'] + + +class IotaNodeSensor(IotaDevice): + """Implement an IOTA sensor for displaying attributes of node.""" + + def __init__(self, iota_config): + """Initialize the sensor.""" + super().__init__( + name='Node Info', seed=None, iri=iota_config[CONF_IRI], + is_testnet=iota_config[CONF_TESTNET]) + self._state = None + self._attr = {ATTR_URL: self.iri, ATTR_TESTNET: self.is_testnet} + + @property + def name(self): + """Return the name of the sensor.""" + return 'IOTA Node' + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes of the device.""" + return self._attr + + def update(self): + """Fetch new attributes IRI node.""" + node_info = self.api.get_node_info() + self._state = node_info.get('appVersion') + + # convert values to raw string formats + self._attr.update({k: str(v) for k, v in node_info.items()}) diff --git a/homeassistant/components/ipma/.translations/ca.json b/homeassistant/components/ipma/.translations/ca.json new file mode 100644 index 0000000000000..29dbaa4f58dac --- /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 0000000000000..080c41429ba21 --- /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 0000000000000..9e717b77843ae --- /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 0000000000000..15459b91f2a40 --- /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 0000000000000..828733c9195ae --- /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 0000000000000..c9eb3a01941dc --- /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 0000000000000..bc10eb3573ecd --- /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 0000000000000..735f5a4a12628 --- /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 0000000000000..2ddeb9a4b3341 --- /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 0000000000000..f49852d5c0c0b --- /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 0000000000000..25c832e51c652 --- /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 0000000000000..87f62371b5557 --- /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 0000000000000..bb42a00742e43 --- /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 0000000000000..dbb192945419b --- /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 0000000000000..f22d1b62fe44c --- /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/ipma/weather.py b/homeassistant/components/ipma/weather.py new file mode 100644 index 0000000000000..ec9b6fec2e8b5 --- /dev/null +++ b/homeassistant/components/ipma/weather.py @@ -0,0 +1,224 @@ +""" +Support for IPMA weather service. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/weather.ipma/ +""" +import logging +from datetime import timedelta + +import async_timeout +import voluptuous as vol + +from homeassistant.components.weather import ( + WeatherEntity, PLATFORM_SCHEMA, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME) +from homeassistant.const import \ + CONF_NAME, TEMP_CELSIUS, CONF_LATITUDE, CONF_LONGITUDE +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.2.1'] + +_LOGGER = logging.getLogger(__name__) + +ATTRIBUTION = 'Instituto Português do Mar e Atmosfera' + +ATTR_WEATHER_DESCRIPTION = "description" + +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30) + +CONDITION_CLASSES = { + 'cloudy': [4, 5, 24, 25, 27], + 'fog': [16, 17, 26], + 'hail': [21, 22], + 'lightning': [19], + 'lightning-rainy': [20, 23], + 'partlycloudy': [2, 3], + 'pouring': [8, 11], + 'rainy': [6, 7, 9, 10, 12, 13, 14, 15], + 'snowy': [18], + 'snowy-rainy': [], + 'sunny': [1], + 'windy': [], + 'windy-variant': [], + 'exceptional': [], +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, +}) + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """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) + + if None in (latitude, longitude): + _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) + with async_timeout.timeout(10, loop=hass.loop): + station = await Station.get(websession, float(latitude), + float(longitude)) + + _LOGGER.debug("Initializing for coordinates %s, %s -> station %s", + latitude, longitude, station.local) + + return station + + +class IPMAWeather(WeatherEntity): + """Representation of a weather condition.""" + + def __init__(self, station, config): + """Initialise the platform with a data instance and station name.""" + self._station_name = config.get(CONF_NAME, station.local) + self._station = station + self._condition = None + self._forecast = None + self._description = None + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + async def async_update(self): + """Update Condition and Forecast.""" + with async_timeout.timeout(10, loop=self.hass.loop): + _new_condition = await self._station.observation() + if _new_condition is None: + _LOGGER.warning("Could not update weather conditions") + return + self._condition = _new_condition + + _LOGGER.debug("Updating station %s, condition %s", + self._station.local, self._condition) + 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.""" + return ATTRIBUTION + + @property + def name(self): + """Return the name of the station.""" + return self._station_name + + @property + def condition(self): + """Return the current condition.""" + if not self._forecast: + return + + return next((k for k, v in CONDITION_CLASSES.items() + if self._forecast[0].idWeatherType in v), None) + + @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 + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def forecast(self): + """Return the forecast array.""" + if self._forecast: + fcdata_out = [] + for data_in in self._forecast: + data_out = {} + data_out[ATTR_FORECAST_TIME] = data_in.forecastDate + data_out[ATTR_FORECAST_CONDITION] =\ + next((k for k, v in CONDITION_CLASSES.items() + if int(data_in.idWeatherType) in v), None) + data_out[ATTR_FORECAST_TEMP_LOW] = data_in.tMin + data_out[ATTR_FORECAST_TEMP] = data_in.tMax + data_out[ATTR_FORECAST_PRECIPITATION] = data_in.precipitaProb + + fcdata_out.append(data_out) + + return fcdata_out + + @property + def device_state_attributes(self): + """Return the state attributes.""" + data = dict() + + if self._description: + data[ATTR_WEATHER_DESCRIPTION] = self._description + + return data diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py deleted file mode 100644 index a9916ed54fe33..0000000000000 --- a/homeassistant/components/isy994.py +++ /dev/null @@ -1,482 +0,0 @@ -""" -Support the ISY-994 controllers. - -For configuration details please visit the documentation for this component at -https://home-assistant.io/components/isy994/ -""" -from collections import namedtuple -import logging -from urllib.parse import urlparse - -import voluptuous as vol - -from homeassistant.core import HomeAssistant -from homeassistant.const import ( - CONF_HOST, CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP) -from homeassistant.helpers import discovery, config_validation as cv -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import ConfigType, Dict - -REQUIREMENTS = ['PyISY==1.1.1'] - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'isy994' - -CONF_IGNORE_STRING = 'ignore_string' -CONF_SENSOR_STRING = 'sensor_string' -CONF_ENABLE_CLIMATE = 'enable_climate' -CONF_TLS_VER = 'tls' - -DEFAULT_IGNORE_STRING = '{IGNORE ME}' -DEFAULT_SENSOR_STRING = 'sensor' - -KEY_ACTIONS = 'actions' -KEY_FOLDER = 'folder' -KEY_MY_PROGRAMS = 'My Programs' -KEY_STATUS = 'status' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_HOST): cv.url, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_TLS_VER): vol.Coerce(float), - vol.Optional(CONF_IGNORE_STRING, - default=DEFAULT_IGNORE_STRING): cv.string, - vol.Optional(CONF_SENSOR_STRING, - default=DEFAULT_SENSOR_STRING): cv.string, - vol.Optional(CONF_ENABLE_CLIMATE, - default=True): cv.boolean - }) -}, extra=vol.ALLOW_EXTRA) - -# Do not use the Hass consts for the states here - we're matching exact API -# responses, not using them for Hass states -NODE_FILTERS = { - 'binary_sensor': { - 'uom': [], - 'states': [], - 'node_def_id': ['BinaryAlarm'], - 'insteon_type': ['16.'] # Does a startswith() match; include the dot - }, - 'sensor': { - # This is just a more-readable way of including MOST uoms between 1-100 - # (Remember that range() is non-inclusive of the stop value) - 'uom': (['1'] + - list(map(str, range(3, 11))) + - list(map(str, range(12, 51))) + - list(map(str, range(52, 66))) + - list(map(str, range(69, 78))) + - ['79'] + - list(map(str, range(82, 97)))), - 'states': [], - 'node_def_id': ['IMETER_SOLO'], - 'insteon_type': ['9.0.', '9.7.'] - }, - 'lock': { - 'uom': ['11'], - 'states': ['locked', 'unlocked'], - 'node_def_id': ['DoorLock'], - 'insteon_type': ['15.'] - }, - 'fan': { - 'uom': [], - 'states': ['off', 'low', 'med', 'high'], - 'node_def_id': ['FanLincMotor'], - 'insteon_type': ['1.46.'] - }, - 'cover': { - 'uom': ['97'], - 'states': ['open', 'closed', 'closing', 'opening', 'stopped'], - 'node_def_id': [], - 'insteon_type': [] - }, - 'light': { - 'uom': ['51'], - 'states': ['on', 'off', '%'], - 'node_def_id': ['DimmerLampSwitch', 'DimmerLampSwitch_ADV', - 'DimmerSwitchOnly', 'DimmerSwitchOnly_ADV', - 'DimmerLampOnly', 'BallastRelayLampSwitch', - 'BallastRelayLampSwitch_ADV', - 'RemoteLinc2', 'RemoteLinc2_ADV'], - 'insteon_type': ['1.'] - }, - 'switch': { - 'uom': ['2', '78'], - 'states': ['on', 'off'], - 'node_def_id': ['OnOffControl', 'RelayLampSwitch', - 'RelayLampSwitch_ADV', 'RelaySwitchOnlyPlusQuery', - 'RelaySwitchOnlyPlusQuery_ADV', 'RelayLampOnly', - 'RelayLampOnly_ADV', 'KeypadButton', - 'KeypadButton_ADV', 'EZRAIN_Input', 'EZRAIN_Output', - 'EZIO2x4_Input', 'EZIO2x4_Input_ADV', 'BinaryControl', - 'BinaryControl_ADV', 'AlertModuleSiren', - 'AlertModuleSiren_ADV', 'AlertModuleArmed', 'Siren', - 'Siren_ADV'], - 'insteon_type': ['2.', '9.10.', '9.11.'] - } -} - -SUPPORTED_DOMAINS = ['binary_sensor', 'sensor', 'lock', 'fan', 'cover', - 'light', 'switch'] -SUPPORTED_PROGRAM_DOMAINS = ['binary_sensor', 'lock', 'fan', 'cover', 'switch'] - -# ISY Scenes are more like Switches than Hass Scenes -# (they can turn off, and report their state) -SCENE_DOMAIN = 'switch' - -ISY994_NODES = "isy994_nodes" -ISY994_WEATHER = "isy994_weather" -ISY994_PROGRAMS = "isy994_programs" - -WeatherNode = namedtuple('WeatherNode', ('status', 'name', 'uom')) - - -def _check_for_node_def(hass: HomeAssistant, node, - single_domain: str = None) -> bool: - """Check if the node matches the node_def_id for any domains. - - This is only present on the 5.0 ISY firmware, and is the most reliable - way to determine a device's type. - """ - if not hasattr(node, 'node_def_id') or node.node_def_id is None: - # Node doesn't have a node_def (pre 5.0 firmware most likely) - return False - - node_def_id = node.node_def_id - - domains = SUPPORTED_DOMAINS if not single_domain else [single_domain] - for domain in domains: - if node_def_id in NODE_FILTERS[domain]['node_def_id']: - hass.data[ISY994_NODES][domain].append(node) - return True - - return False - - -def _check_for_insteon_type(hass: HomeAssistant, node, - single_domain: str = None) -> bool: - """Check if the node matches the Insteon type for any domains. - - This is for (presumably) every version of the ISY firmware, but only - works for Insteon device. "Node Server" (v5+) and Z-Wave and others will - not have a type. - """ - if not hasattr(node, 'type') or node.type is None: - # Node doesn't have a type (non-Insteon device most likely) - return False - - device_type = node.type - domains = SUPPORTED_DOMAINS if not single_domain else [single_domain] - for domain in domains: - if any([device_type.startswith(t) for t in - set(NODE_FILTERS[domain]['insteon_type'])]): - - # Hacky special-case just for FanLinc, which has a light module - # as one of its nodes. Note that this special-case is not necessary - # on ISY 5.x firmware as it uses the superior NodeDefs method - if domain == 'fan' and int(node.nid[-1]) == 1: - hass.data[ISY994_NODES]['light'].append(node) - return True - - hass.data[ISY994_NODES][domain].append(node) - return True - - return False - - -def _check_for_uom_id(hass: HomeAssistant, node, - single_domain: str = None, - uom_list: list = None) -> bool: - """Check if a node's uom matches any of the domains uom filter. - - This is used for versions of the ISY firmware that report uoms as a single - ID. We can often infer what type of device it is by that ID. - """ - if not hasattr(node, 'uom') or node.uom is None: - # Node doesn't have a uom (Scenes for example) - return False - - node_uom = set(map(str.lower, node.uom)) - - if uom_list: - if node_uom.intersection(uom_list): - hass.data[ISY994_NODES][single_domain].append(node) - return True - else: - domains = SUPPORTED_DOMAINS if not single_domain else [single_domain] - for domain in domains: - if node_uom.intersection(NODE_FILTERS[domain]['uom']): - hass.data[ISY994_NODES][domain].append(node) - return True - - return False - - -def _check_for_states_in_uom(hass: HomeAssistant, node, - single_domain: str = None, - states_list: list = None) -> bool: - """Check if a list of uoms matches two possible filters. - - This is for versions of the ISY firmware that report uoms as a list of all - possible "human readable" states. This filter passes if all of the possible - states fit inside the given filter. - """ - if not hasattr(node, 'uom') or node.uom is None: - # Node doesn't have a uom (Scenes for example) - return False - - node_uom = set(map(str.lower, node.uom)) - - if states_list: - if node_uom == set(states_list): - hass.data[ISY994_NODES][single_domain].append(node) - return True - else: - domains = SUPPORTED_DOMAINS if not single_domain else [single_domain] - for domain in domains: - if node_uom == set(NODE_FILTERS[domain]['states']): - hass.data[ISY994_NODES][domain].append(node) - return True - - return False - - -def _is_sensor_a_binary_sensor(hass: HomeAssistant, node) -> bool: - """Determine if the given sensor node should be a binary_sensor.""" - if _check_for_node_def(hass, node, single_domain='binary_sensor'): - return True - if _check_for_insteon_type(hass, node, single_domain='binary_sensor'): - return True - - # For the next two checks, we're providing our own set of uoms that - # represent on/off devices. This is because we can only depend on these - # checks in the context of already knowing that this is definitely a - # sensor device. - if _check_for_uom_id(hass, node, single_domain='binary_sensor', - uom_list=['2', '78']): - return True - if _check_for_states_in_uom(hass, node, single_domain='binary_sensor', - states_list=['on', 'off']): - return True - - return False - - -def _categorize_nodes(hass: HomeAssistant, nodes, ignore_identifier: str, - 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 - if ignored: - # Don't import this node as a device at all - continue - - from PyISY.Nodes import Group - if isinstance(node, Group): - hass.data[ISY994_NODES][SCENE_DOMAIN].append(node) - continue - - if sensor_identifier in path or sensor_identifier in node.name: - # User has specified to treat this as a sensor. First we need to - # determine if it should be a binary_sensor. - if _is_sensor_a_binary_sensor(hass, node): - continue - else: - hass.data[ISY994_NODES]['sensor'].append(node) - continue - - # We have a bunch of different methods for determining the device type, - # each of which works with different ISY firmware versions or device - # family. The order here is important, from most reliable to least. - if _check_for_node_def(hass, node): - continue - if _check_for_insteon_type(hass, node): - continue - if _check_for_uom_id(hass, node): - continue - if _check_for_states_in_uom(hass, node): - continue - - -def _categorize_programs(hass: HomeAssistant, programs: dict) -> None: - """Categorize the ISY994 programs.""" - for domain in SUPPORTED_PROGRAM_DOMAINS: - try: - folder = programs[KEY_MY_PROGRAMS]['HA.{}'.format(domain)] - except KeyError: - pass - else: - for dtype, _, node_id in folder.children: - if dtype != KEY_FOLDER: - continue - entity_folder = folder[node_id] - try: - status = entity_folder[KEY_STATUS] - assert status.dtype == 'program', 'Not a program' - if domain != 'binary_sensor': - actions = entity_folder[KEY_ACTIONS] - assert actions.dtype == 'program', 'Not a program' - else: - actions = None - except (AttributeError, KeyError, AssertionError): - _LOGGER.warning("Program entity '%s' not loaded due " - "to invalid folder structure.", - entity_folder.name) - continue - - entity = (entity_folder.name, status, actions) - hass.data[ISY994_PROGRAMS][domain].append(entity) - - -def _categorize_weather(hass: HomeAssistant, climate) -> None: - """Categorize the ISY994 weather data.""" - climate_attrs = dir(climate) - weather_nodes = [WeatherNode(getattr(climate, attr), - attr.replace('_', ' '), - getattr(climate, '{}_units'.format(attr))) - for attr in climate_attrs - if '{}_units'.format(attr) in climate_attrs] - hass.data[ISY994_WEATHER].extend(weather_nodes) - - -def setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the ISY 994 platform.""" - hass.data[ISY994_NODES] = {} - for domain in SUPPORTED_DOMAINS: - hass.data[ISY994_NODES][domain] = [] - - hass.data[ISY994_WEATHER] = [] - - hass.data[ISY994_PROGRAMS] = {} - for domain in SUPPORTED_DOMAINS: - hass.data[ISY994_PROGRAMS][domain] = [] - - isy_config = config.get(DOMAIN) - - user = isy_config.get(CONF_USERNAME) - password = isy_config.get(CONF_PASSWORD) - tls_version = isy_config.get(CONF_TLS_VER) - host = urlparse(isy_config.get(CONF_HOST)) - ignore_identifier = isy_config.get(CONF_IGNORE_STRING) - sensor_identifier = isy_config.get(CONF_SENSOR_STRING) - enable_climate = isy_config.get(CONF_ENABLE_CLIMATE) - - if host.scheme == 'http': - https = False - port = host.port or 80 - elif host.scheme == 'https': - https = True - port = host.port or 443 - else: - _LOGGER.error("isy994 host value in configuration is invalid") - return False - - import PyISY - # Connect to ISY controller. - isy = PyISY.ISY(host.hostname, port, username=user, password=password, - use_https=https, tls_ver=tls_version, log=_LOGGER) - if not isy.connected: - return False - - _categorize_nodes(hass, isy.nodes, ignore_identifier, sensor_identifier) - _categorize_programs(hass, isy.programs) - - if enable_climate and isy.configuration.get('Weather Information'): - _categorize_weather(hass, isy.climate) - - def stop(event: object) -> None: - """Stop ISY auto updates.""" - isy.auto_update = False - - # Listen for HA stop to disconnect. - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop) - - # Load platforms for the devices in the ISY controller that we support. - for component in SUPPORTED_DOMAINS: - discovery.load_platform(hass, component, DOMAIN, {}, config) - - isy.auto_update = True - return True - - -class ISYDevice(Entity): - """Representation of an ISY994 device.""" - - _attrs = {} - _name = None # type: str - - def __init__(self, node) -> None: - """Initialize the insteon device.""" - self._node = node - self._change_handler = None - self._control_handler = None - - async def async_added_to_hass(self) -> None: - """Subscribe to the node change events.""" - self._change_handler = self._node.status.subscribe( - 'changed', self.on_update) - - if hasattr(self._node, 'controlEvents'): - self._control_handler = self._node.controlEvents.subscribe( - self.on_control) - - def on_update(self, event: object) -> None: - """Handle the update event from the ISY994 Node.""" - self.schedule_update_ha_state() - - def on_control(self, event: object) -> None: - """Handle a control event from the ISY994 Node.""" - self.hass.bus.fire('isy994_control', { - 'entity_id': self.entity_id, - 'control': event - }) - - @property - def unique_id(self) -> str: - """Get the unique identifier of the device.""" - # pylint: disable=protected-access - if hasattr(self._node, '_id'): - return self._node._id - - return None - - @property - def name(self) -> str: - """Get the name of the device.""" - return self._name or str(self._node.name) - - @property - def should_poll(self) -> bool: - """No polling required since we're using the subscription.""" - return False - - @property - def value(self) -> int: - """Get the current value of the device.""" - # pylint: disable=protected-access - return self._node.status._val - - def is_unknown(self) -> bool: - """Get whether or not the value of this Entity's node is unknown. - - PyISY reports unknown values as -inf - """ - return self.value == -1 * float('inf') - - @property - def state(self): - """Return the state of the ISY device.""" - if self.is_unknown(): - return None - return super().state - - @property - def device_state_attributes(self) -> Dict: - """Get the state attributes for the device.""" - attr = {} - if hasattr(self._node, 'aux_properties'): - for name, val in self._node.aux_properties.items(): - attr[name] = '{} {}'.format(val.get('value'), val.get('uom')) - return attr diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py new file mode 100644 index 0000000000000..2b5f8fcb13f16 --- /dev/null +++ b/homeassistant/components/isy994/__init__.py @@ -0,0 +1,482 @@ +""" +Support the ISY-994 controllers. + +For configuration details please visit the documentation for this component at +https://home-assistant.io/components/isy994/ +""" +from collections import namedtuple +import logging +from urllib.parse import urlparse + +import voluptuous as vol + +from homeassistant.core import HomeAssistant +from homeassistant.const import ( + CONF_HOST, CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP) +from homeassistant.helpers import discovery, config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import ConfigType, Dict + +REQUIREMENTS = ['PyISY==1.1.1'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'isy994' + +CONF_IGNORE_STRING = 'ignore_string' +CONF_SENSOR_STRING = 'sensor_string' +CONF_ENABLE_CLIMATE = 'enable_climate' +CONF_TLS_VER = 'tls' + +DEFAULT_IGNORE_STRING = '{IGNORE ME}' +DEFAULT_SENSOR_STRING = 'sensor' + +KEY_ACTIONS = 'actions' +KEY_FOLDER = 'folder' +KEY_MY_PROGRAMS = 'My Programs' +KEY_STATUS = 'status' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOST): cv.url, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_TLS_VER): vol.Coerce(float), + vol.Optional(CONF_IGNORE_STRING, + default=DEFAULT_IGNORE_STRING): cv.string, + vol.Optional(CONF_SENSOR_STRING, + default=DEFAULT_SENSOR_STRING): cv.string, + vol.Optional(CONF_ENABLE_CLIMATE, + default=True): cv.boolean + }) +}, extra=vol.ALLOW_EXTRA) + +# Do not use the Hass consts for the states here - we're matching exact API +# responses, not using them for Hass states +NODE_FILTERS = { + 'binary_sensor': { + 'uom': [], + 'states': [], + 'node_def_id': ['BinaryAlarm'], + 'insteon_type': ['16.'] # Does a startswith() match; include the dot + }, + 'sensor': { + # This is just a more-readable way of including MOST uoms between 1-100 + # (Remember that range() is non-inclusive of the stop value) + 'uom': (['1'] + + list(map(str, range(3, 11))) + + list(map(str, range(12, 51))) + + list(map(str, range(52, 66))) + + list(map(str, range(69, 78))) + + ['79'] + + list(map(str, range(82, 97)))), + 'states': [], + 'node_def_id': ['IMETER_SOLO'], + 'insteon_type': ['9.0.', '9.7.'] + }, + 'lock': { + 'uom': ['11'], + 'states': ['locked', 'unlocked'], + 'node_def_id': ['DoorLock'], + 'insteon_type': ['15.'] + }, + 'fan': { + 'uom': [], + 'states': ['off', 'low', 'med', 'high'], + 'node_def_id': ['FanLincMotor'], + 'insteon_type': ['1.46.'] + }, + 'cover': { + 'uom': ['97'], + 'states': ['open', 'closed', 'closing', 'opening', 'stopped'], + 'node_def_id': [], + 'insteon_type': [] + }, + 'light': { + 'uom': ['51'], + 'states': ['on', 'off', '%'], + 'node_def_id': ['DimmerLampSwitch', 'DimmerLampSwitch_ADV', + 'DimmerSwitchOnly', 'DimmerSwitchOnly_ADV', + 'DimmerLampOnly', 'BallastRelayLampSwitch', + 'BallastRelayLampSwitch_ADV', + 'RemoteLinc2', 'RemoteLinc2_ADV'], + 'insteon_type': ['1.'] + }, + 'switch': { + 'uom': ['2', '78'], + 'states': ['on', 'off'], + 'node_def_id': ['OnOffControl', 'RelayLampSwitch', + 'RelayLampSwitch_ADV', 'RelaySwitchOnlyPlusQuery', + 'RelaySwitchOnlyPlusQuery_ADV', 'RelayLampOnly', + 'RelayLampOnly_ADV', 'KeypadButton', + 'KeypadButton_ADV', 'EZRAIN_Input', 'EZRAIN_Output', + 'EZIO2x4_Input', 'EZIO2x4_Input_ADV', 'BinaryControl', + 'BinaryControl_ADV', 'AlertModuleSiren', + 'AlertModuleSiren_ADV', 'AlertModuleArmed', 'Siren', + 'Siren_ADV'], + 'insteon_type': ['2.', '9.10.', '9.11.'] + } +} + +SUPPORTED_DOMAINS = ['binary_sensor', 'sensor', 'lock', 'fan', 'cover', + 'light', 'switch'] +SUPPORTED_PROGRAM_DOMAINS = ['binary_sensor', 'lock', 'fan', 'cover', 'switch'] + +# ISY Scenes are more like Switches than Hass Scenes +# (they can turn off, and report their state) +SCENE_DOMAIN = 'switch' + +ISY994_NODES = "isy994_nodes" +ISY994_WEATHER = "isy994_weather" +ISY994_PROGRAMS = "isy994_programs" + +WeatherNode = namedtuple('WeatherNode', ('status', 'name', 'uom')) + + +def _check_for_node_def(hass: HomeAssistant, node, + single_domain: str = None) -> bool: + """Check if the node matches the node_def_id for any domains. + + This is only present on the 5.0 ISY firmware, and is the most reliable + way to determine a device's type. + """ + if not hasattr(node, 'node_def_id') or node.node_def_id is None: + # Node doesn't have a node_def (pre 5.0 firmware most likely) + return False + + node_def_id = node.node_def_id + + domains = SUPPORTED_DOMAINS if not single_domain else [single_domain] + for domain in domains: + if node_def_id in NODE_FILTERS[domain]['node_def_id']: + hass.data[ISY994_NODES][domain].append(node) + return True + + return False + + +def _check_for_insteon_type(hass: HomeAssistant, node, + single_domain: str = None) -> bool: + """Check if the node matches the Insteon type for any domains. + + This is for (presumably) every version of the ISY firmware, but only + works for Insteon device. "Node Server" (v5+) and Z-Wave and others will + not have a type. + """ + if not hasattr(node, 'type') or node.type is None: + # Node doesn't have a type (non-Insteon device most likely) + return False + + device_type = node.type + domains = SUPPORTED_DOMAINS if not single_domain else [single_domain] + for domain in domains: + if any([device_type.startswith(t) for t in + set(NODE_FILTERS[domain]['insteon_type'])]): + + # Hacky special-case just for FanLinc, which has a light module + # as one of its nodes. Note that this special-case is not necessary + # on ISY 5.x firmware as it uses the superior NodeDefs method + if domain == 'fan' and int(node.nid[-1]) == 1: + hass.data[ISY994_NODES]['light'].append(node) + return True + + hass.data[ISY994_NODES][domain].append(node) + return True + + return False + + +def _check_for_uom_id(hass: HomeAssistant, node, + single_domain: str = None, + uom_list: list = None) -> bool: + """Check if a node's uom matches any of the domains uom filter. + + This is used for versions of the ISY firmware that report uoms as a single + ID. We can often infer what type of device it is by that ID. + """ + if not hasattr(node, 'uom') or node.uom is None: + # Node doesn't have a uom (Scenes for example) + return False + + node_uom = set(map(str.lower, node.uom)) + + if uom_list: + if node_uom.intersection(uom_list): + hass.data[ISY994_NODES][single_domain].append(node) + return True + else: + domains = SUPPORTED_DOMAINS if not single_domain else [single_domain] + for domain in domains: + if node_uom.intersection(NODE_FILTERS[domain]['uom']): + hass.data[ISY994_NODES][domain].append(node) + return True + + return False + + +def _check_for_states_in_uom(hass: HomeAssistant, node, + single_domain: str = None, + states_list: list = None) -> bool: + """Check if a list of uoms matches two possible filters. + + This is for versions of the ISY firmware that report uoms as a list of all + possible "human readable" states. This filter passes if all of the possible + states fit inside the given filter. + """ + if not hasattr(node, 'uom') or node.uom is None: + # Node doesn't have a uom (Scenes for example) + return False + + node_uom = set(map(str.lower, node.uom)) + + if states_list: + if node_uom == set(states_list): + hass.data[ISY994_NODES][single_domain].append(node) + return True + else: + domains = SUPPORTED_DOMAINS if not single_domain else [single_domain] + for domain in domains: + if node_uom == set(NODE_FILTERS[domain]['states']): + hass.data[ISY994_NODES][domain].append(node) + return True + + return False + + +def _is_sensor_a_binary_sensor(hass: HomeAssistant, node) -> bool: + """Determine if the given sensor node should be a binary_sensor.""" + if _check_for_node_def(hass, node, single_domain='binary_sensor'): + return True + if _check_for_insteon_type(hass, node, single_domain='binary_sensor'): + return True + + # For the next two checks, we're providing our own set of uoms that + # represent on/off devices. This is because we can only depend on these + # checks in the context of already knowing that this is definitely a + # sensor device. + if _check_for_uom_id(hass, node, single_domain='binary_sensor', + uom_list=['2', '78']): + return True + if _check_for_states_in_uom(hass, node, single_domain='binary_sensor', + states_list=['on', 'off']): + return True + + return False + + +def _categorize_nodes(hass: HomeAssistant, nodes, ignore_identifier: str, + 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 + if ignored: + # Don't import this node as a device at all + continue + + from PyISY.Nodes import Group + if isinstance(node, Group): + hass.data[ISY994_NODES][SCENE_DOMAIN].append(node) + continue + + if sensor_identifier in path or sensor_identifier in node.name: + # User has specified to treat this as a sensor. First we need to + # determine if it should be a binary_sensor. + if _is_sensor_a_binary_sensor(hass, node): + continue + else: + hass.data[ISY994_NODES]['sensor'].append(node) + continue + + # We have a bunch of different methods for determining the device type, + # each of which works with different ISY firmware versions or device + # family. The order here is important, from most reliable to least. + if _check_for_node_def(hass, node): + continue + if _check_for_insteon_type(hass, node): + continue + if _check_for_uom_id(hass, node): + continue + if _check_for_states_in_uom(hass, node): + continue + + +def _categorize_programs(hass: HomeAssistant, programs: dict) -> None: + """Categorize the ISY994 programs.""" + for domain in SUPPORTED_PROGRAM_DOMAINS: + try: + folder = programs[KEY_MY_PROGRAMS]['HA.{}'.format(domain)] + except KeyError: + pass + else: + for dtype, _, node_id in folder.children: + if dtype != KEY_FOLDER: + continue + entity_folder = folder[node_id] + try: + status = entity_folder[KEY_STATUS] + assert status.dtype == 'program', 'Not a program' + if domain != 'binary_sensor': + actions = entity_folder[KEY_ACTIONS] + assert actions.dtype == 'program', 'Not a program' + else: + actions = None + except (AttributeError, KeyError, AssertionError): + _LOGGER.warning("Program entity '%s' not loaded due " + "to invalid folder structure.", + entity_folder.name) + continue + + entity = (entity_folder.name, status, actions) + hass.data[ISY994_PROGRAMS][domain].append(entity) + + +def _categorize_weather(hass: HomeAssistant, climate) -> None: + """Categorize the ISY994 weather data.""" + climate_attrs = dir(climate) + weather_nodes = [WeatherNode(getattr(climate, attr), + attr.replace('_', ' '), + getattr(climate, '{}_units'.format(attr))) + for attr in climate_attrs + if '{}_units'.format(attr) in climate_attrs] + hass.data[ISY994_WEATHER].extend(weather_nodes) + + +def setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the ISY 994 platform.""" + hass.data[ISY994_NODES] = {} + for domain in SUPPORTED_DOMAINS: + hass.data[ISY994_NODES][domain] = [] + + hass.data[ISY994_WEATHER] = [] + + hass.data[ISY994_PROGRAMS] = {} + for domain in SUPPORTED_DOMAINS: + hass.data[ISY994_PROGRAMS][domain] = [] + + isy_config = config.get(DOMAIN) + + user = isy_config.get(CONF_USERNAME) + password = isy_config.get(CONF_PASSWORD) + tls_version = isy_config.get(CONF_TLS_VER) + host = urlparse(isy_config.get(CONF_HOST)) + ignore_identifier = isy_config.get(CONF_IGNORE_STRING) + sensor_identifier = isy_config.get(CONF_SENSOR_STRING) + enable_climate = isy_config.get(CONF_ENABLE_CLIMATE) + + if host.scheme == 'http': + https = False + port = host.port or 80 + elif host.scheme == 'https': + https = True + port = host.port or 443 + else: + _LOGGER.error("isy994 host value in configuration is invalid") + return False + + import PyISY + # Connect to ISY controller. + isy = PyISY.ISY(host.hostname, port, username=user, password=password, + use_https=https, tls_ver=tls_version, log=_LOGGER) + if not isy.connected: + return False + + _categorize_nodes(hass, isy.nodes, ignore_identifier, sensor_identifier) + _categorize_programs(hass, isy.programs) + + if enable_climate and isy.configuration.get('Weather Information'): + _categorize_weather(hass, isy.climate) + + def stop(event: object) -> None: + """Stop ISY auto updates.""" + isy.auto_update = False + + # Listen for HA stop to disconnect. + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop) + + # Load platforms for the devices in the ISY controller that we support. + for component in SUPPORTED_DOMAINS: + discovery.load_platform(hass, component, DOMAIN, {}, config) + + isy.auto_update = True + return True + + +class ISYDevice(Entity): + """Representation of an ISY994 device.""" + + _attrs = {} + _name = None # type: str + + def __init__(self, node) -> None: + """Initialize the insteon device.""" + self._node = node + self._change_handler = None + self._control_handler = None + + async def async_added_to_hass(self) -> None: + """Subscribe to the node change events.""" + self._change_handler = self._node.status.subscribe( + 'changed', self.on_update) + + if hasattr(self._node, 'controlEvents'): + self._control_handler = self._node.controlEvents.subscribe( + self.on_control) + + def on_update(self, event: object) -> None: + """Handle the update event from the ISY994 Node.""" + self.schedule_update_ha_state() + + def on_control(self, event: object) -> None: + """Handle a control event from the ISY994 Node.""" + self.hass.bus.fire('isy994_control', { + 'entity_id': self.entity_id, + 'control': event + }) + + @property + def unique_id(self) -> str: + """Get the unique identifier of the device.""" + # pylint: disable=protected-access + if hasattr(self._node, '_id'): + return self._node._id + + return None + + @property + def name(self) -> str: + """Get the name of the device.""" + return self._name or str(self._node.name) + + @property + def should_poll(self) -> bool: + """No polling required since we're using the subscription.""" + return False + + @property + def value(self) -> int: + """Get the current value of the device.""" + # pylint: disable=protected-access + return self._node.status._val + + def is_unknown(self) -> bool: + """Get whether or not the value of this Entity's node is unknown. + + PyISY reports unknown values as -inf + """ + return self.value == -1 * float('inf') + + @property + def state(self): + """Return the state of the ISY device.""" + if self.is_unknown(): + return None + return super().state + + @property + def device_state_attributes(self) -> Dict: + """Get the state attributes for the device.""" + attr = {} + if hasattr(self._node, 'aux_properties'): + for name, val in self._node.aux_properties.items(): + attr[name] = '{} {}'.format(val.get('value'), val.get('uom')) + return attr 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 0000000000000..de43b41fdb745 --- /dev/null +++ b/homeassistant/components/itach/__init__.py @@ -0,0 +1 @@ +"""Support for itach devices.""" diff --git a/homeassistant/components/itach/remote.py b/homeassistant/components/itach/remote.py new file mode 100644 index 0000000000000..beb773838fb17 --- /dev/null +++ b/homeassistant/components/itach/remote.py @@ -0,0 +1,110 @@ +"""Support for iTach IR devices.""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components import remote +from homeassistant.const import ( + DEVICE_DEFAULT_NAME, CONF_NAME, CONF_MAC, CONF_HOST, CONF_PORT, + CONF_DEVICES) +from homeassistant.components.remote import PLATFORM_SCHEMA + +REQUIREMENTS = ['pyitachip2ir==0.0.7'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_PORT = 4998 +CONNECT_TIMEOUT = 5000 + +CONF_MODADDR = 'modaddr' +CONF_CONNADDR = 'connaddr' +CONF_COMMANDS = 'commands' +CONF_DATA = 'data' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_MAC): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [{ + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_MODADDR): vol.Coerce(int), + 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, + }]) + }]) +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the ITach connection and devices.""" + import pyitachip2ir + itachip2ir = pyitachip2ir.ITachIP2IR( + config.get(CONF_MAC), config.get(CONF_HOST), + int(config.get(CONF_PORT))) + + if not itachip2ir.ready(CONNECT_TIMEOUT): + _LOGGER.error("Unable to find iTach") + return False + + devices = [] + for data in config.get(CONF_DEVICES): + name = data.get(CONF_NAME) + modaddr = int(data.get(CONF_MODADDR, 1)) + connaddr = int(data.get(CONF_CONNADDR, 1)) + cmddatas = "" + for cmd in data.get(CONF_COMMANDS): + cmdname = cmd[CONF_NAME].strip() + if not cmdname: + cmdname = '""' + cmddata = cmd[CONF_DATA].strip() + if not cmddata: + cmddata = '""' + cmddatas += "{}\n{}\n".format(cmdname, cmddata) + itachip2ir.addDevice(name, modaddr, connaddr, cmddatas) + devices.append(ITachIP2IRRemote(itachip2ir, name)) + add_entities(devices, True) + return True + + +class ITachIP2IRRemote(remote.RemoteDevice): + """Device that sends commands to an ITachIP2IR device.""" + + def __init__(self, itachip2ir, name): + """Initialize device.""" + self.itachip2ir = itachip2ir + self._power = False + self._name = name or DEVICE_DEFAULT_NAME + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def is_on(self): + """Return true if device is on.""" + return self._power + + def turn_on(self, **kwargs): + """Turn the device on.""" + self._power = True + self.itachip2ir.send(self._name, "ON", 1) + self.schedule_update_ha_state() + + def turn_off(self, **kwargs): + """Turn the device off.""" + self._power = False + self.itachip2ir.send(self._name, "OFF", 1) + self.schedule_update_ha_state() + + def send_command(self, command, **kwargs): + """Send a command to one device.""" + for single_command in command: + self.itachip2ir.send(self._name, single_command, 1) + + def update(self): + """Update the device.""" + self.itachip2ir.update() diff --git a/homeassistant/components/joaoapps_join.py b/homeassistant/components/joaoapps_join.py deleted file mode 100644 index b5bcb1e1a8a1b..0000000000000 --- a/homeassistant/components/joaoapps_join.py +++ /dev/null @@ -1,107 +0,0 @@ -""" -Component for Joaoapps Join services. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/join/ -""" -import logging - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_NAME, CONF_API_KEY - -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' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.All(cv.ensure_list, [{ - vol.Required(CONF_API_KEY): cv.string, - 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 - }]) -}, extra=vol.ALLOW_EXTRA) - - -def register_device(hass, api_key, name, device_id, device_ids, device_names): - """Register services for each join device listed.""" - from pyjoin import (ring_device, set_wallpaper, send_sms, - send_file, send_url, send_notification) - - def ring_service(service): - """Service to ring devices.""" - ring_device(api_key=api_key, device_id=device_id, - device_ids=device_ids, device_names=device_names) - - def set_wallpaper_service(service): - """Service to set wallpaper on devices.""" - set_wallpaper(api_key=api_key, device_id=device_id, - device_ids=device_ids, device_names=device_names, - url=service.data.get('url')) - - def send_file_service(service): - """Service to send files to devices.""" - send_file(api_key=api_key, device_id=device_id, - device_ids=device_ids, device_names=device_names, - url=service.data.get('url')) - - def send_url_service(service): - """Service to open url on devices.""" - send_url(api_key=api_key, device_id=device_id, - device_ids=device_ids, device_names=device_names, - url=service.data.get('url')) - - def send_tasker_service(service): - """Service to open url on devices.""" - send_notification(api_key=api_key, device_id=device_id, - device_ids=device_ids, device_names=device_names, - text=service.data.get('command')) - - def send_sms_service(service): - """Service to send sms from devices.""" - send_sms(device_id=device_id, - device_ids=device_ids, - device_names=device_names, - sms_number=service.data.get('number'), - sms_text=service.data.get('message'), - api_key=api_key) - - hass.services.register(DOMAIN, name + 'ring', ring_service) - hass.services.register(DOMAIN, name + 'set_wallpaper', - set_wallpaper_service) - hass.services.register(DOMAIN, name + 'send_sms', send_sms_service) - hass.services.register(DOMAIN, name + 'send_file', send_file_service) - hass.services.register(DOMAIN, name + 'send_url', send_url_service) - hass.services.register(DOMAIN, name + 'send_tasker', send_tasker_service) - - -def setup(hass, config): - """Set up the Join services.""" - from pyjoin import get_devices - for device in config[DOMAIN]: - api_key = device.get(CONF_API_KEY) - device_id = device.get(CONF_DEVICE_ID) - device_ids = device.get(CONF_DEVICE_IDS) - device_names = device.get(CONF_DEVICE_NAMES) - name = device.get(CONF_NAME) - name = name.lower().replace(" ", "_") + "_" if name else "" - if api_key: - if not get_devices(api_key): - _LOGGER.error("Error connecting to Join, check API key") - return False - if device_id is None and device_ids is None and device_names is None: - _LOGGER.error("No device was provided. Please specify device_id" - ", device_ids, or device_names") - return False - - register_device(hass, api_key, name, - device_id, device_ids, device_names) - return True diff --git a/homeassistant/components/joaoapps_join/__init__.py b/homeassistant/components/joaoapps_join/__init__.py new file mode 100644 index 0000000000000..adc856bdd3a24 --- /dev/null +++ b/homeassistant/components/joaoapps_join/__init__.py @@ -0,0 +1,103 @@ +"""Support for Joaoapps Join services.""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_NAME, CONF_API_KEY + +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' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All(cv.ensure_list, [{ + vol.Required(CONF_API_KEY): cv.string, + 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, + }]) +}, extra=vol.ALLOW_EXTRA) + + +def register_device(hass, api_key, name, device_id, device_ids, device_names): + """Register services for each join device listed.""" + from pyjoin import (ring_device, set_wallpaper, send_sms, + send_file, send_url, send_notification) + + def ring_service(service): + """Service to ring devices.""" + ring_device(api_key=api_key, device_id=device_id, + device_ids=device_ids, device_names=device_names) + + def set_wallpaper_service(service): + """Service to set wallpaper on devices.""" + set_wallpaper(api_key=api_key, device_id=device_id, + device_ids=device_ids, device_names=device_names, + url=service.data.get('url')) + + def send_file_service(service): + """Service to send files to devices.""" + send_file(api_key=api_key, device_id=device_id, + device_ids=device_ids, device_names=device_names, + url=service.data.get('url')) + + def send_url_service(service): + """Service to open url on devices.""" + send_url(api_key=api_key, device_id=device_id, + device_ids=device_ids, device_names=device_names, + url=service.data.get('url')) + + def send_tasker_service(service): + """Service to open url on devices.""" + send_notification(api_key=api_key, device_id=device_id, + device_ids=device_ids, device_names=device_names, + text=service.data.get('command')) + + def send_sms_service(service): + """Service to send sms from devices.""" + send_sms(device_id=device_id, + device_ids=device_ids, + device_names=device_names, + sms_number=service.data.get('number'), + sms_text=service.data.get('message'), + api_key=api_key) + + hass.services.register(DOMAIN, name + 'ring', ring_service) + hass.services.register(DOMAIN, name + 'set_wallpaper', + set_wallpaper_service) + hass.services.register(DOMAIN, name + 'send_sms', send_sms_service) + hass.services.register(DOMAIN, name + 'send_file', send_file_service) + hass.services.register(DOMAIN, name + 'send_url', send_url_service) + hass.services.register(DOMAIN, name + 'send_tasker', send_tasker_service) + + +def setup(hass, config): + """Set up the Join services.""" + from pyjoin import get_devices + for device in config[DOMAIN]: + api_key = device.get(CONF_API_KEY) + device_id = device.get(CONF_DEVICE_ID) + device_ids = device.get(CONF_DEVICE_IDS) + device_names = device.get(CONF_DEVICE_NAMES) + name = device.get(CONF_NAME) + name = name.lower().replace(" ", "_") + "_" if name else "" + if api_key: + if not get_devices(api_key): + _LOGGER.error("Error connecting to Join, check API key") + return False + if device_id is None and device_ids is None and device_names is None: + _LOGGER.error("No device was provided. Please specify device_id" + ", device_ids, or device_names") + return False + + register_device(hass, api_key, name, + device_id, device_ids, device_names) + return True diff --git a/homeassistant/components/joaoapps_join/notify.py b/homeassistant/components/joaoapps_join/notify.py new file mode 100644 index 0000000000000..c586147d632d4 --- /dev/null +++ b/homeassistant/components/joaoapps_join/notify.py @@ -0,0 +1,64 @@ +"""Support for Join notifications.""" +import logging +import voluptuous as vol +from homeassistant.components.notify import ( + ATTR_DATA, ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, + BaseNotificationService) +from homeassistant.const import CONF_API_KEY +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['python-join-api==0.0.2'] + +_LOGGER = logging.getLogger(__name__) + +CONF_DEVICE_ID = 'device_id' +CONF_DEVICE_IDS = 'device_ids' +CONF_DEVICE_NAMES = 'device_names' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_DEVICE_ID): cv.string, + vol.Optional(CONF_DEVICE_IDS): cv.string, + vol.Optional(CONF_DEVICE_NAMES): cv.string, +}) + + +def get_service(hass, config, discovery_info=None): + """Get the Join notification service.""" + api_key = config.get(CONF_API_KEY) + device_id = config.get(CONF_DEVICE_ID) + device_ids = config.get(CONF_DEVICE_IDS) + device_names = config.get(CONF_DEVICE_NAMES) + if api_key: + from pyjoin import get_devices + if not get_devices(api_key): + _LOGGER.error("Error connecting to Join. Check the API key") + return False + if device_id is None and device_ids is None and device_names is None: + _LOGGER.error("No device was provided. Please specify device_id" + ", device_ids, or device_names") + return False + return JoinNotificationService(api_key, device_id, + device_ids, device_names) + + +class JoinNotificationService(BaseNotificationService): + """Implement the notification service for Join.""" + + def __init__(self, api_key, device_id, device_ids, device_names): + """Initialize the service.""" + self._api_key = api_key + self._device_id = device_id + self._device_ids = device_ids + self._device_names = device_names + + def send_message(self, message="", **kwargs): + """Send a message to a user.""" + from pyjoin import send_notification + title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) + data = kwargs.get(ATTR_DATA) or {} + send_notification( + device_id=self._device_id, device_ids=self._device_ids, + device_names=self._device_names, text=message, title=title, + icon=data.get('icon'), smallicon=data.get('smallicon'), + vibration=data.get('vibration'), api_key=self._api_key) diff --git a/homeassistant/components/juicenet.py b/homeassistant/components/juicenet.py deleted file mode 100644 index 55567d4587901..0000000000000 --- a/homeassistant/components/juicenet.py +++ /dev/null @@ -1,74 +0,0 @@ -""" -Support for Juicenet cloud. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/juicenet -""" - -import logging - -import voluptuous as vol - -from homeassistant.helpers import discovery -from homeassistant.const import CONF_ACCESS_TOKEN -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.config_validation as cv - -REQUIREMENTS = ['python-juicenet==0.0.5'] - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'juicenet' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_ACCESS_TOKEN): cv.string - }) -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the Juicenet component.""" - import pyjuicenet - - hass.data[DOMAIN] = {} - - access_token = config[DOMAIN].get(CONF_ACCESS_TOKEN) - hass.data[DOMAIN]['api'] = pyjuicenet.Api(access_token) - - discovery.load_platform(hass, 'sensor', DOMAIN, {}, config) - return True - - -class JuicenetDevice(Entity): - """Represent a base Juicenet device.""" - - def __init__(self, device, sensor_type, hass): - """Initialise the sensor.""" - self.hass = hass - self.device = device - self.type = sensor_type - - @property - def name(self): - """Return the name of the device.""" - return self.device.name() - - def update(self): - """Update state of the device.""" - self.device.update_state() - - @property - def _manufacturer_device_id(self): - """Return the manufacturer device id.""" - return self.device.id() - - @property - def _token(self): - """Return the device API token.""" - return self.device.token() - - @property - def unique_id(self): - """Return a unique ID.""" - return "{}-{}".format(self.device.id(), self.type) diff --git a/homeassistant/components/juicenet/__init__.py b/homeassistant/components/juicenet/__init__.py new file mode 100644 index 0000000000000..f62331d1502ea --- /dev/null +++ b/homeassistant/components/juicenet/__init__.py @@ -0,0 +1,68 @@ +"""Support for Juicenet cloud.""" +import logging + +import voluptuous as vol + +from homeassistant.helpers import discovery +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.helpers.entity import Entity +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['python-juicenet==0.0.5'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'juicenet' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_ACCESS_TOKEN): cv.string, + }) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Juicenet component.""" + import pyjuicenet + + hass.data[DOMAIN] = {} + + access_token = config[DOMAIN].get(CONF_ACCESS_TOKEN) + hass.data[DOMAIN]['api'] = pyjuicenet.Api(access_token) + + discovery.load_platform(hass, 'sensor', DOMAIN, {}, config) + return True + + +class JuicenetDevice(Entity): + """Represent a base Juicenet device.""" + + def __init__(self, device, sensor_type, hass): + """Initialise the sensor.""" + self.hass = hass + self.device = device + self.type = sensor_type + + @property + def name(self): + """Return the name of the device.""" + return self.device.name() + + def update(self): + """Update state of the device.""" + self.device.update_state() + + @property + def _manufacturer_device_id(self): + """Return the manufacturer device id.""" + return self.device.id() + + @property + def _token(self): + """Return the device API token.""" + return self.device.token() + + @property + def unique_id(self): + """Return a unique ID.""" + return "{}-{}".format(self.device.id(), self.type) diff --git a/homeassistant/components/juicenet/sensor.py b/homeassistant/components/juicenet/sensor.py new file mode 100644 index 0000000000000..e378662707588 --- /dev/null +++ b/homeassistant/components/juicenet/sensor.py @@ -0,0 +1,110 @@ +"""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 + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['juicenet'] + +SENSOR_TYPES = { + 'status': ['Charging Status', None], + 'temperature': ['Temperature', TEMP_CELSIUS], + 'voltage': ['Voltage', 'V'], + 'amps': ['Amps', 'A'], + 'watts': ['Watts', 'W'], + 'charge_time': ['Charge time', 's'], + 'energy_added': ['Energy added', 'Wh'] +} + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Juicenet sensor.""" + api = hass.data[DOMAIN]['api'] + + dev = [] + for device in api.get_devices(): + for variable in SENSOR_TYPES: + dev.append(JuicenetSensorDevice(device, variable, hass)) + + add_entities(dev) + + +class JuicenetSensorDevice(JuicenetDevice, Entity): + """Implementation of a Juicenet sensor.""" + + def __init__(self, device, sensor_type, hass): + """Initialise the sensor.""" + super().__init__(device, sensor_type, hass) + self._name = SENSOR_TYPES[sensor_type][0] + self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + + @property + def name(self): + """Return the name of the device.""" + return '{} {}'.format(self.device.name(), self._name) + + @property + def icon(self): + """Return the icon of the sensor.""" + icon = None + if self.type == 'status': + status = self.device.getStatus() + if status == 'standby': + icon = 'mdi:power-plug-off' + elif status == 'plugged': + icon = 'mdi:power-plug' + elif status == 'charging': + icon = 'mdi:battery-positive' + elif self.type == 'temperature': + icon = 'mdi:thermometer' + elif self.type == 'voltage': + icon = 'mdi:flash' + elif self.type == 'amps': + icon = 'mdi:flash' + elif self.type == 'watts': + icon = 'mdi:flash' + elif self.type == 'charge_time': + icon = 'mdi:timer' + elif self.type == 'energy_added': + icon = 'mdi:flash' + return icon + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit_of_measurement + + @property + def state(self): + """Return the state.""" + state = None + if self.type == 'status': + state = self.device.getStatus() + elif self.type == 'temperature': + state = self.device.getTemperature() + elif self.type == 'voltage': + state = self.device.getVoltage() + elif self.type == 'amps': + state = self.device.getAmps() + elif self.type == 'watts': + state = self.device.getWatts() + elif self.type == 'charge_time': + state = self.device.getChargeTime() + elif self.type == 'energy_added': + state = self.device.getEnergyAdded() + else: + state = 'Unknown' + return state + + @property + def device_state_attributes(self): + """Return the state attributes.""" + attributes = {} + if self.type == 'status': + man_dev_id = self.device.id() + if man_dev_id: + attributes["manufacturer_device_id"] = man_dev_id + return attributes diff --git a/homeassistant/components/keyboard.py b/homeassistant/components/keyboard.py deleted file mode 100644 index 16253ba271a5b..0000000000000 --- a/homeassistant/components/keyboard.py +++ /dev/null @@ -1,57 +0,0 @@ -""" -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/ -""" -import voluptuous as vol - -from homeassistant.const import ( - SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PLAY_PAUSE, - SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE, - SERVICE_VOLUME_UP) - -REQUIREMENTS = ['pyuserinput==0.1.11'] - -DOMAIN = 'keyboard' - -TAP_KEY_SCHEMA = vol.Schema({}) - - -def setup(hass, config): - """Listen for keyboard events.""" - import pykeyboard # pylint: disable=import-error - - keyboard = pykeyboard.PyKeyboard() - keyboard.special_key_assignment() - - hass.services.register(DOMAIN, SERVICE_VOLUME_UP, - lambda service: - keyboard.tap_key(keyboard.volume_up_key), - schema=TAP_KEY_SCHEMA) - - hass.services.register(DOMAIN, SERVICE_VOLUME_DOWN, - lambda service: - keyboard.tap_key(keyboard.volume_down_key), - schema=TAP_KEY_SCHEMA) - - hass.services.register(DOMAIN, SERVICE_VOLUME_MUTE, - lambda service: - keyboard.tap_key(keyboard.volume_mute_key), - schema=TAP_KEY_SCHEMA) - - hass.services.register(DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, - lambda service: - keyboard.tap_key(keyboard.media_play_pause_key), - schema=TAP_KEY_SCHEMA) - - hass.services.register(DOMAIN, SERVICE_MEDIA_NEXT_TRACK, - lambda service: - keyboard.tap_key(keyboard.media_next_track_key), - schema=TAP_KEY_SCHEMA) - - hass.services.register(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, - lambda service: - keyboard.tap_key(keyboard.media_prev_track_key), - schema=TAP_KEY_SCHEMA) - return True diff --git a/homeassistant/components/keyboard/__init__.py b/homeassistant/components/keyboard/__init__.py new file mode 100644 index 0000000000000..44accca2f56a7 --- /dev/null +++ b/homeassistant/components/keyboard/__init__.py @@ -0,0 +1,52 @@ +"""Support to emulate keyboard presses on host machine.""" +import voluptuous as vol + +from homeassistant.const import ( + SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PLAY_PAUSE, + SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE, + SERVICE_VOLUME_UP) + +REQUIREMENTS = ['pyuserinput==0.1.11'] + +DOMAIN = 'keyboard' + +TAP_KEY_SCHEMA = vol.Schema({}) + + +def setup(hass, config): + """Listen for keyboard events.""" + import pykeyboard # pylint: disable=import-error + + keyboard = pykeyboard.PyKeyboard() + keyboard.special_key_assignment() + + hass.services.register(DOMAIN, SERVICE_VOLUME_UP, + lambda service: + keyboard.tap_key(keyboard.volume_up_key), + schema=TAP_KEY_SCHEMA) + + hass.services.register(DOMAIN, SERVICE_VOLUME_DOWN, + lambda service: + keyboard.tap_key(keyboard.volume_down_key), + schema=TAP_KEY_SCHEMA) + + hass.services.register(DOMAIN, SERVICE_VOLUME_MUTE, + lambda service: + keyboard.tap_key(keyboard.volume_mute_key), + schema=TAP_KEY_SCHEMA) + + hass.services.register(DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, + lambda service: + keyboard.tap_key(keyboard.media_play_pause_key), + schema=TAP_KEY_SCHEMA) + + hass.services.register(DOMAIN, SERVICE_MEDIA_NEXT_TRACK, + lambda service: + keyboard.tap_key(keyboard.media_next_track_key), + schema=TAP_KEY_SCHEMA) + + hass.services.register(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, + lambda service: + keyboard.tap_key(keyboard.media_prev_track_key), + schema=TAP_KEY_SCHEMA) + return True diff --git a/homeassistant/components/keyboard_remote.py b/homeassistant/components/keyboard_remote.py deleted file mode 100644 index e02c2ee5475d8..0000000000000 --- a/homeassistant/components/keyboard_remote.py +++ /dev/null @@ -1,203 +0,0 @@ -""" -Receive signals from a keyboard and use it as a remote control. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/keyboard_remote/ -""" -# pylint: disable=import-error -import threading -import logging -import os -import time - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) - -REQUIREMENTS = ['evdev==0.6.1'] - -_LOGGER = logging.getLogger(__name__) - -DEVICE_DESCRIPTOR = 'device_descriptor' -DEVICE_ID_GROUP = 'Device description' -DEVICE_NAME = 'device_name' -DOMAIN = 'keyboard_remote' - -ICON = 'mdi:remote' - -KEY_CODE = 'key_code' -KEY_VALUE = {'key_up': 0, 'key_down': 1, 'key_hold': 2} -KEYBOARD_REMOTE_COMMAND_RECEIVED = 'keyboard_remote_command_received' -KEYBOARD_REMOTE_CONNECTED = 'keyboard_remote_connected' -KEYBOARD_REMOTE_DISCONNECTED = 'keyboard_remote_disconnected' - -TYPE = 'type' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: - vol.All(cv.ensure_list, [vol.Schema({ - vol.Exclusive(DEVICE_DESCRIPTOR, DEVICE_ID_GROUP): cv.string, - vol.Exclusive(DEVICE_NAME, DEVICE_ID_GROUP): cv.string, - vol.Optional(TYPE, default='key_up'): - vol.All(cv.string, vol.Any('key_up', 'key_down', 'key_hold')) - })]) -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the keyboard_remote.""" - config = config.get(DOMAIN) - - keyboard_remote = KeyboardRemote(hass, config) - - def _start_keyboard_remote(_event): - keyboard_remote.run() - - def _stop_keyboard_remote(_event): - keyboard_remote.stop() - - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start_keyboard_remote) - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _stop_keyboard_remote) - - return True - - -class KeyboardRemoteThread(threading.Thread): - """This interfaces with the inputdevice using evdev.""" - - def __init__(self, hass, device_name, device_descriptor, key_value): - """Construct a thread listening for events on one device.""" - self.hass = hass - self.device_name = device_name - self.device_descriptor = device_descriptor - self.key_value = key_value - - if self.device_descriptor: - self.device_id = self.device_descriptor - else: - self.device_id = self.device_name - - self.dev = self._get_keyboard_device() - if self.dev is not None: - _LOGGER.debug("Keyboard connected, %s", self.device_id) - else: - _LOGGER.debug( - "Keyboard not connected, %s. " - "Check /dev/input/event* permissions", self.device_id) - - id_folder = '/dev/input/by-id/' - - if os.path.isdir(id_folder): - from evdev import InputDevice, list_devices - device_names = [InputDevice(file_name).name - for file_name in list_devices()] - _LOGGER.debug( - "Possible device names are: %s. " - "Possible device descriptors are %s: %s", - device_names, id_folder, os.listdir(id_folder)) - - threading.Thread.__init__(self) - self.stopped = threading.Event() - self.hass = hass - - def _get_keyboard_device(self): - """Get the keyboard device.""" - from evdev import InputDevice, list_devices - if self.device_name: - devices = [InputDevice(file_name) for file_name in list_devices()] - for device in devices: - if self.device_name == device.name: - return device - elif self.device_descriptor: - try: - device = InputDevice(self.device_descriptor) - except OSError: - pass - else: - return device - return None - - def run(self): - """Run the loop of the KeyboardRemote.""" - from evdev import categorize, ecodes - - if self.dev is not None: - self.dev.grab() - _LOGGER.debug("Interface started for %s", self.dev) - - while not self.stopped.isSet(): - # Sleeps to ease load on processor - time.sleep(.05) - - if self.dev is None: - self.dev = self._get_keyboard_device() - if self.dev is not None: - self.dev.grab() - self.hass.bus.fire( - KEYBOARD_REMOTE_CONNECTED, - { - DEVICE_DESCRIPTOR: self.device_descriptor, - DEVICE_NAME: self.device_name - } - ) - _LOGGER.debug("Keyboard re-connected, %s", self.device_id) - else: - continue - - try: - event = self.dev.read_one() - except IOError: # Keyboard Disconnected - self.dev = None - self.hass.bus.fire( - KEYBOARD_REMOTE_DISCONNECTED, - { - DEVICE_DESCRIPTOR: self.device_descriptor, - DEVICE_NAME: self.device_name - } - ) - _LOGGER.debug("Keyboard disconnected, %s", self.device_id) - continue - - if not event: - continue - - if event.type is ecodes.EV_KEY and event.value is self.key_value: - _LOGGER.debug(categorize(event)) - self.hass.bus.fire( - KEYBOARD_REMOTE_COMMAND_RECEIVED, - { - KEY_CODE: event.code, - DEVICE_DESCRIPTOR: self.device_descriptor, - DEVICE_NAME: self.device_name - } - ) - - -class KeyboardRemote: - """Sets up one thread per device.""" - - def __init__(self, hass, config): - """Construct a KeyboardRemote interface object.""" - self.threads = [] - for dev_block in config: - device_descriptor = dev_block.get(DEVICE_DESCRIPTOR) - device_name = dev_block.get(DEVICE_NAME) - key_value = KEY_VALUE.get(dev_block.get(TYPE, 'key_up')) - - if device_descriptor is not None\ - or device_name is not None: - thread = KeyboardRemoteThread( - hass, device_name, device_descriptor, key_value) - self.threads.append(thread) - - def run(self): - """Run all event listener threads.""" - for thread in self.threads: - thread.start() - - def stop(self): - """Stop all event listener threads.""" - for thread in self.threads: - thread.stopped.set() diff --git a/homeassistant/components/keyboard_remote/__init__.py b/homeassistant/components/keyboard_remote/__init__.py new file mode 100644 index 0000000000000..e786fe458a846 --- /dev/null +++ b/homeassistant/components/keyboard_remote/__init__.py @@ -0,0 +1,198 @@ +"""Receive signals from a keyboard and use it as a remote control.""" +# pylint: disable=import-error +import threading +import logging +import os +import time + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) + +REQUIREMENTS = ['evdev==0.6.1'] + +_LOGGER = logging.getLogger(__name__) + +DEVICE_DESCRIPTOR = 'device_descriptor' +DEVICE_ID_GROUP = 'Device description' +DEVICE_NAME = 'device_name' +DOMAIN = 'keyboard_remote' + +ICON = 'mdi:remote' + +KEY_CODE = 'key_code' +KEY_VALUE = {'key_up': 0, 'key_down': 1, 'key_hold': 2} +KEYBOARD_REMOTE_COMMAND_RECEIVED = 'keyboard_remote_command_received' +KEYBOARD_REMOTE_CONNECTED = 'keyboard_remote_connected' +KEYBOARD_REMOTE_DISCONNECTED = 'keyboard_remote_disconnected' + +TYPE = 'type' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: + vol.All(cv.ensure_list, [vol.Schema({ + vol.Exclusive(DEVICE_DESCRIPTOR, DEVICE_ID_GROUP): cv.string, + vol.Exclusive(DEVICE_NAME, DEVICE_ID_GROUP): cv.string, + vol.Optional(TYPE, default='key_up'): + vol.All(cv.string, vol.Any('key_up', 'key_down', 'key_hold')) + })]) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the keyboard_remote.""" + config = config.get(DOMAIN) + + keyboard_remote = KeyboardRemote(hass, config) + + def _start_keyboard_remote(_event): + keyboard_remote.run() + + def _stop_keyboard_remote(_event): + keyboard_remote.stop() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start_keyboard_remote) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _stop_keyboard_remote) + + return True + + +class KeyboardRemoteThread(threading.Thread): + """This interfaces with the inputdevice using evdev.""" + + def __init__(self, hass, device_name, device_descriptor, key_value): + """Construct a thread listening for events on one device.""" + self.hass = hass + self.device_name = device_name + self.device_descriptor = device_descriptor + self.key_value = key_value + + if self.device_descriptor: + self.device_id = self.device_descriptor + else: + self.device_id = self.device_name + + self.dev = self._get_keyboard_device() + if self.dev is not None: + _LOGGER.debug("Keyboard connected, %s", self.device_id) + else: + _LOGGER.debug( + "Keyboard not connected, %s. " + "Check /dev/input/event* permissions", self.device_id) + + id_folder = '/dev/input/by-id/' + + if os.path.isdir(id_folder): + from evdev import InputDevice, list_devices + device_names = [InputDevice(file_name).name + for file_name in list_devices()] + _LOGGER.debug( + "Possible device names are: %s. " + "Possible device descriptors are %s: %s", + device_names, id_folder, os.listdir(id_folder)) + + threading.Thread.__init__(self) + self.stopped = threading.Event() + self.hass = hass + + def _get_keyboard_device(self): + """Get the keyboard device.""" + from evdev import InputDevice, list_devices + if self.device_name: + devices = [InputDevice(file_name) for file_name in list_devices()] + for device in devices: + if self.device_name == device.name: + return device + elif self.device_descriptor: + try: + device = InputDevice(self.device_descriptor) + except OSError: + pass + else: + return device + return None + + def run(self): + """Run the loop of the KeyboardRemote.""" + from evdev import categorize, ecodes + + if self.dev is not None: + self.dev.grab() + _LOGGER.debug("Interface started for %s", self.dev) + + while not self.stopped.isSet(): + # Sleeps to ease load on processor + time.sleep(.05) + + if self.dev is None: + self.dev = self._get_keyboard_device() + if self.dev is not None: + self.dev.grab() + self.hass.bus.fire( + KEYBOARD_REMOTE_CONNECTED, + { + DEVICE_DESCRIPTOR: self.device_descriptor, + DEVICE_NAME: self.device_name + } + ) + _LOGGER.debug("Keyboard re-connected, %s", self.device_id) + else: + continue + + try: + event = self.dev.read_one() + except IOError: # Keyboard Disconnected + self.dev = None + self.hass.bus.fire( + KEYBOARD_REMOTE_DISCONNECTED, + { + DEVICE_DESCRIPTOR: self.device_descriptor, + DEVICE_NAME: self.device_name + } + ) + _LOGGER.debug("Keyboard disconnected, %s", self.device_id) + continue + + if not event: + continue + + if event.type is ecodes.EV_KEY and event.value is self.key_value: + _LOGGER.debug(categorize(event)) + self.hass.bus.fire( + KEYBOARD_REMOTE_COMMAND_RECEIVED, + { + KEY_CODE: event.code, + DEVICE_DESCRIPTOR: self.device_descriptor, + DEVICE_NAME: self.device_name + } + ) + + +class KeyboardRemote: + """Sets up one thread per device.""" + + def __init__(self, hass, config): + """Construct a KeyboardRemote interface object.""" + self.threads = [] + for dev_block in config: + device_descriptor = dev_block.get(DEVICE_DESCRIPTOR) + device_name = dev_block.get(DEVICE_NAME) + key_value = KEY_VALUE.get(dev_block.get(TYPE, 'key_up')) + + if device_descriptor is not None\ + or device_name is not None: + thread = KeyboardRemoteThread( + hass, device_name, device_descriptor, key_value) + self.threads.append(thread) + + def run(self): + """Run all event listener threads.""" + for thread in self.threads: + thread.start() + + def stop(self): + """Stop all event listener threads.""" + for thread in self.threads: + thread.stopped.set() diff --git a/homeassistant/components/kira.py b/homeassistant/components/kira.py deleted file mode 100644 index 3a5ee25f05ebd..0000000000000 --- a/homeassistant/components/kira.py +++ /dev/null @@ -1,140 +0,0 @@ -""" -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/ -""" -import logging -import os - -import voluptuous as vol -from voluptuous.error import Error as VoluptuousError -import yaml - -from homeassistant.const import ( - CONF_DEVICE, CONF_HOST, CONF_NAME, CONF_PORT, CONF_SENSORS, CONF_TYPE, - EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN) -from homeassistant.helpers import discovery -import homeassistant.helpers.config_validation as cv - -REQUIREMENTS = ['pykira==0.1.1'] - -DOMAIN = 'kira' - -_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" -CONF_REMOTE = "remote" - -CODES_YAML = '{}_codes.yaml'.format(DOMAIN) - -CODE_SCHEMA = vol.Schema({ - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_CODE): cv.string, - vol.Optional(CONF_TYPE): cv.string, - vol.Optional(CONF_DEVICE): cv.string, - vol.Optional(CONF_REPEAT): cv.positive_int, -}) - -SENSOR_SCHEMA = vol.Schema({ - vol.Optional(CONF_NAME, default=DOMAIN): - vol.Exclusive(cv.string, "sensors"), - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, -}) - -REMOTE_SCHEMA = vol.Schema({ - vol.Optional(CONF_NAME, default=DOMAIN): - vol.Exclusive(cv.string, "remotes"), - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, -}) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(CONF_SENSORS): [SENSOR_SCHEMA], - vol.Optional(CONF_REMOTES): [REMOTE_SCHEMA]}) -}, extra=vol.ALLOW_EXTRA) - - -def load_codes(path): - """Load KIRA codes from specified file.""" - codes = [] - if os.path.exists(path): - with open(path) as code_file: - data = yaml.load(code_file) or [] - for code in data: - try: - codes.append(CODE_SCHEMA(code)) - except VoluptuousError as exception: - # keep going - _LOGGER.warning("KIRA code invalid data: %s", exception) - else: - with open(path, 'w') as code_file: - code_file.write('') - return codes - - -def setup(hass, config): - """Set up the KIRA component.""" - import pykira - - sensors = config.get(DOMAIN, {}).get(CONF_SENSORS, []) - remotes = config.get(DOMAIN, {}).get(CONF_REMOTES, []) - # If no sensors or remotes were specified, add a sensor - if not(sensors or remotes): - sensors.append({}) - - codes = load_codes(hass.config.path(CODES_YAML)) - - hass.data[DOMAIN] = { - CONF_SENSOR: {}, - CONF_REMOTE: {}, - } - - def load_module(platform, idx, module_conf): - """Set up the KIRA module and load platform.""" - # note: module_name is not the HA device name. it's just a unique name - # to ensure the component and platform can share information - module_name = ("%s_%d" % (DOMAIN, idx)) if idx else DOMAIN - device_name = module_conf.get(CONF_NAME, DOMAIN) - port = module_conf.get(CONF_PORT, DEFAULT_PORT) - host = module_conf.get(CONF_HOST, DEFAULT_HOST) - - if platform == CONF_SENSOR: - module = pykira.KiraReceiver(host, port) - module.start() - else: - module = pykira.KiraModule(host, port) - - hass.data[DOMAIN][platform][module_name] = module - for code in codes: - code_tuple = (code.get(CONF_NAME), - code.get(CONF_DEVICE, STATE_UNKNOWN)) - module.registerCode(code_tuple, code.get(CONF_CODE)) - - discovery.load_platform(hass, platform, DOMAIN, - {'name': module_name, 'device': device_name}, - config) - - for idx, module_conf in enumerate(sensors): - load_module(CONF_SENSOR, idx, module_conf) - - for idx, module_conf in enumerate(remotes): - load_module(CONF_REMOTE, idx, module_conf) - - def _stop_kira(_event): - """Stop the KIRA receiver.""" - for receiver in hass.data[DOMAIN][CONF_SENSOR].values(): - receiver.stop() - _LOGGER.info("Terminated receivers") - - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _stop_kira) - - return True diff --git a/homeassistant/components/kira/__init__.py b/homeassistant/components/kira/__init__.py new file mode 100644 index 0000000000000..d60d8e0cfeb10 --- /dev/null +++ b/homeassistant/components/kira/__init__.py @@ -0,0 +1,134 @@ +"""KIRA interface to receive UDP packets from an IR-IP bridge.""" +import logging +import os + +import voluptuous as vol +from voluptuous.error import Error as VoluptuousError +import yaml + +from homeassistant.const import ( + CONF_DEVICE, CONF_HOST, CONF_NAME, CONF_PORT, CONF_SENSORS, CONF_TYPE, + EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN, CONF_CODE) +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['pykira==0.1.1'] + +DOMAIN = 'kira' + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_HOST = "0.0.0.0" +DEFAULT_PORT = 65432 + +CONF_REPEAT = "repeat" +CONF_REMOTES = "remotes" +CONF_SENSOR = "sensor" +CONF_REMOTE = "remote" + +CODES_YAML = '{}_codes.yaml'.format(DOMAIN) + +CODE_SCHEMA = vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_CODE): cv.string, + vol.Optional(CONF_TYPE): cv.string, + vol.Optional(CONF_DEVICE): cv.string, + vol.Optional(CONF_REPEAT): cv.positive_int, +}) + +SENSOR_SCHEMA = vol.Schema({ + vol.Optional(CONF_NAME, default=DOMAIN): + vol.Exclusive(cv.string, "sensors"), + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, +}) + +REMOTE_SCHEMA = vol.Schema({ + vol.Optional(CONF_NAME, default=DOMAIN): + vol.Exclusive(cv.string, "remotes"), + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Optional(CONF_SENSORS): [SENSOR_SCHEMA], + vol.Optional(CONF_REMOTES): [REMOTE_SCHEMA]}) +}, extra=vol.ALLOW_EXTRA) + + +def load_codes(path): + """Load KIRA codes from specified file.""" + codes = [] + if os.path.exists(path): + with open(path) as code_file: + data = yaml.load(code_file) or [] + for code in data: + try: + codes.append(CODE_SCHEMA(code)) + except VoluptuousError as exception: + # keep going + _LOGGER.warning("KIRA code invalid data: %s", exception) + else: + with open(path, 'w') as code_file: + code_file.write('') + return codes + + +def setup(hass, config): + """Set up the KIRA component.""" + import pykira + + sensors = config.get(DOMAIN, {}).get(CONF_SENSORS, []) + remotes = config.get(DOMAIN, {}).get(CONF_REMOTES, []) + # If no sensors or remotes were specified, add a sensor + if not(sensors or remotes): + sensors.append({}) + + codes = load_codes(hass.config.path(CODES_YAML)) + + hass.data[DOMAIN] = { + CONF_SENSOR: {}, + CONF_REMOTE: {}, + } + + def load_module(platform, idx, module_conf): + """Set up the KIRA module and load platform.""" + # note: module_name is not the HA device name. it's just a unique name + # to ensure the component and platform can share information + module_name = ("%s_%d" % (DOMAIN, idx)) if idx else DOMAIN + device_name = module_conf.get(CONF_NAME, DOMAIN) + port = module_conf.get(CONF_PORT, DEFAULT_PORT) + host = module_conf.get(CONF_HOST, DEFAULT_HOST) + + if platform == CONF_SENSOR: + module = pykira.KiraReceiver(host, port) + module.start() + else: + module = pykira.KiraModule(host, port) + + hass.data[DOMAIN][platform][module_name] = module + for code in codes: + code_tuple = (code.get(CONF_NAME), + code.get(CONF_DEVICE, STATE_UNKNOWN)) + module.registerCode(code_tuple, code.get(CONF_CODE)) + + discovery.load_platform(hass, platform, DOMAIN, + {'name': module_name, 'device': device_name}, + config) + + for idx, module_conf in enumerate(sensors): + load_module(CONF_SENSOR, idx, module_conf) + + for idx, module_conf in enumerate(remotes): + load_module(CONF_REMOTE, idx, module_conf) + + def _stop_kira(_event): + """Stop the KIRA receiver.""" + for receiver in hass.data[DOMAIN][CONF_SENSOR].values(): + receiver.stop() + _LOGGER.info("Terminated receivers") + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _stop_kira) + + return True diff --git a/homeassistant/components/kira/remote.py b/homeassistant/components/kira/remote.py new file mode 100644 index 0000000000000..8ddf0858e1617 --- /dev/null +++ b/homeassistant/components/kira/remote.py @@ -0,0 +1,58 @@ +"""Support for Keene Electronics IR-IP devices.""" +import functools as ft +import logging + +from homeassistant.components import remote +from homeassistant.const import CONF_DEVICE, CONF_NAME +from homeassistant.helpers.entity import Entity + +DOMAIN = 'kira' + +_LOGGER = logging.getLogger(__name__) + +CONF_REMOTE = 'remote' + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Kira platform.""" + if discovery_info: + name = discovery_info.get(CONF_NAME) + device = discovery_info.get(CONF_DEVICE) + + kira = hass.data[DOMAIN][CONF_REMOTE][name] + add_entities([KiraRemote(device, kira)]) + return True + + +class KiraRemote(Entity): + """Remote representation used to send commands to a Kira device.""" + + def __init__(self, name, kira): + """Initialize KiraRemote class.""" + _LOGGER.debug("KiraRemote device init started for: %s", name) + self._name = name + self._kira = kira + + @property + def name(self): + """Return the Kira device's name.""" + return self._name + + def update(self): + """No-op.""" + + def send_command(self, command, **kwargs): + """Send a command to one device.""" + for single_command in command: + code_tuple = (single_command, + kwargs.get(remote.ATTR_DEVICE)) + _LOGGER.info("Sending Command: %s to %s", *code_tuple) + self._kira.sendCode(code_tuple) + + def async_send_command(self, command, **kwargs): + """Send a command to a device. + + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.async_add_job(ft.partial( + self.send_command, command, **kwargs)) diff --git a/homeassistant/components/kira/sensor.py b/homeassistant/components/kira/sensor.py new file mode 100644 index 0000000000000..8885ebcbe2409 --- /dev/null +++ b/homeassistant/components/kira/sensor.py @@ -0,0 +1,72 @@ +"""KIRA interface to receive UDP packets from an IR-IP bridge.""" +import logging + +from homeassistant.const import CONF_DEVICE, CONF_NAME, STATE_UNKNOWN +from homeassistant.helpers.entity import Entity + +DOMAIN = 'kira' + +_LOGGER = logging.getLogger(__name__) + +ICON = 'mdi:remote' + +CONF_SENSOR = 'sensor' + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up a Kira sensor.""" + if discovery_info is not None: + name = discovery_info.get(CONF_NAME) + device = discovery_info.get(CONF_DEVICE) + kira = hass.data[DOMAIN][CONF_SENSOR][name] + + add_entities([KiraReceiver(device, kira)]) + + +class KiraReceiver(Entity): + """Implementation of a Kira Receiver.""" + + def __init__(self, name, kira): + """Initialize the sensor.""" + self._name = name + self._state = None + self._device = STATE_UNKNOWN + + kira.registerCallback(self._update_callback) + + def _update_callback(self, code): + code_name, device = code + _LOGGER.debug("Kira Code: %s", code_name) + self._state = code_name + self._device = device + self.schedule_update_ha_state() + + @property + def name(self): + """Return the name of the receiver.""" + return self._name + + @property + def icon(self): + """Return icon.""" + return ICON + + @property + def state(self): + """Return the state of the receiver.""" + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes of the device.""" + return {CONF_DEVICE: self._device} + + @property + def should_poll(self) -> bool: + """Entity should not be polled.""" + return False + + @property + def force_update(self) -> bool: + """Kira should force updates. Repeated states have meaning.""" + return True diff --git a/homeassistant/components/knx.py b/homeassistant/components/knx.py deleted file mode 100644 index 366ec4405cd76..0000000000000 --- a/homeassistant/components/knx.py +++ /dev/null @@ -1,340 +0,0 @@ -""" -Connects to KNX platform. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/knx/ -""" - -import logging - -import voluptuous as vol - -from homeassistant.const import ( - CONF_ENTITY_ID, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP) -from homeassistant.core import callback -from homeassistant.helpers import discovery -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'] - -DOMAIN = "knx" -DATA_KNX = "data_knx" -CONF_KNX_CONFIG = "config_file" - -CONF_KNX_ROUTING = "routing" -CONF_KNX_TUNNELING = "tunneling" -CONF_KNX_LOCAL_IP = "local_ip" -CONF_KNX_FIRE_EVENT = "fire_event" -CONF_KNX_FIRE_EVENT_FILTER = "fire_event_filter" -CONF_KNX_STATE_UPDATER = "state_updater" -CONF_KNX_EXPOSE = "expose" -CONF_KNX_EXPOSE_TYPE = "type" -CONF_KNX_EXPOSE_ADDRESS = "address" - -SERVICE_KNX_SEND = "send" -SERVICE_KNX_ATTR_ADDRESS = "address" -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, - vol.Optional(CONF_PORT): cv.port, -}) - -ROUTING_SCHEMA = vol.Schema({ - vol.Required(CONF_KNX_LOCAL_IP): cv.string, -}) - -EXPOSE_SCHEMA = vol.Schema({ - vol.Required(CONF_KNX_EXPOSE_TYPE): cv.string, - vol.Optional(CONF_ENTITY_ID): cv.entity_id, - vol.Required(CONF_KNX_EXPOSE_ADDRESS): cv.string, -}) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(CONF_KNX_CONFIG): cv.string, - vol.Exclusive(CONF_KNX_ROUTING, 'connection_type'): ROUTING_SCHEMA, - vol.Exclusive(CONF_KNX_TUNNELING, 'connection_type'): - TUNNELING_SCHEMA, - vol.Inclusive(CONF_KNX_FIRE_EVENT, 'fire_ev'): - cv.boolean, - vol.Inclusive(CONF_KNX_FIRE_EVENT_FILTER, 'fire_ev'): - vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_KNX_STATE_UPDATER, default=True): cv.boolean, - vol.Optional(CONF_KNX_EXPOSE): - vol.All( - cv.ensure_list, - [EXPOSE_SCHEMA]), - }) -}, extra=vol.ALLOW_EXTRA) - -SERVICE_KNX_SEND_SCHEMA = vol.Schema({ - vol.Required(SERVICE_KNX_ATTR_ADDRESS): cv.string, - vol.Required(SERVICE_KNX_ATTR_PAYLOAD): vol.Any( - cv.positive_int, [cv.positive_int]), -}) - - -async def async_setup(hass, config): - """Set up the KNX component.""" - from xknx.exceptions import XKNXException - try: - hass.data[DATA_KNX] = KNXModule(hass, config) - hass.data[DATA_KNX].async_create_exposures() - await hass.data[DATA_KNX].start() - - except XKNXException as ex: - _LOGGER.warning("Can't connect to KNX interface: %s", ex) - hass.components.persistent_notification.async_create( - "Can't connect to KNX interface:
" - "{0}".format(ex), - title="KNX") - - for component, discovery_type in ( - ('switch', 'Switch'), - ('climate', 'Climate'), - ('cover', 'Cover'), - ('light', 'Light'), - ('sensor', 'Sensor'), - ('binary_sensor', 'BinarySensor'), - ('scene', 'Scene'), - ('notify', 'Notification')): - found_devices = _get_devices(hass, discovery_type) - hass.async_create_task( - discovery.async_load_platform(hass, component, DOMAIN, { - ATTR_DISCOVER_DEVICES: found_devices - }, config)) - - hass.services.async_register( - DOMAIN, SERVICE_KNX_SEND, - hass.data[DATA_KNX].service_send_to_knx_bus, - schema=SERVICE_KNX_SEND_SCHEMA) - - return True - - -def _get_devices(hass, discovery_type): - """Get the KNX devices.""" - return list( - map(lambda device: device.name, - filter( - lambda device: type(device).__name__ == discovery_type, - hass.data[DATA_KNX].xknx.devices))) - - -class KNXModule: - """Representation of KNX Object.""" - - def __init__(self, hass, config): - """Initialize of KNX module.""" - self.hass = hass - self.config = config - self.connected = False - self.init_xknx() - self.register_callbacks() - self.exposures = [] - - def init_xknx(self): - """Initialize of KNX object.""" - from xknx import XKNX - self.xknx = XKNX(config=self.config_file(), loop=self.hass.loop) - - async def start(self): - """Start KNX object. Connect to tunneling or Routing device.""" - connection_config = self.connection_config() - await self.xknx.start( - state_updater=self.config[DOMAIN][CONF_KNX_STATE_UPDATER], - connection_config=connection_config) - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop) - self.connected = True - - async def stop(self, event): - """Stop KNX object. Disconnect from tunneling or Routing device.""" - await self.xknx.stop() - - def config_file(self): - """Resolve and return the full path of xknx.yaml if configured.""" - config_file = self.config[DOMAIN].get(CONF_KNX_CONFIG) - if not config_file: - return None - if not config_file.startswith("/"): - return self.hass.config.path(config_file) - return config_file - - def connection_config(self): - """Return the connection_config.""" - if CONF_KNX_TUNNELING in self.config[DOMAIN]: - return self.connection_config_tunneling() - if CONF_KNX_ROUTING in self.config[DOMAIN]: - return self.connection_config_routing() - return self.connection_config_auto() - - def connection_config_routing(self): - """Return the connection_config if routing is configured.""" - from xknx.io import ConnectionConfig, ConnectionType - local_ip = \ - self.config[DOMAIN][CONF_KNX_ROUTING].get(CONF_KNX_LOCAL_IP) - return ConnectionConfig( - connection_type=ConnectionType.ROUTING, - local_ip=local_ip) - - def connection_config_tunneling(self): - """Return the connection_config if tunneling is configured.""" - from xknx.io import ConnectionConfig, ConnectionType, \ - DEFAULT_MCAST_PORT - gateway_ip = \ - self.config[DOMAIN][CONF_KNX_TUNNELING].get(CONF_HOST) - gateway_port = \ - self.config[DOMAIN][CONF_KNX_TUNNELING].get(CONF_PORT) - local_ip = \ - self.config[DOMAIN][CONF_KNX_TUNNELING].get(CONF_KNX_LOCAL_IP) - if gateway_port is None: - gateway_port = DEFAULT_MCAST_PORT - return ConnectionConfig( - connection_type=ConnectionType.TUNNELING, gateway_ip=gateway_ip, - gateway_port=gateway_port, local_ip=local_ip) - - def connection_config_auto(self): - """Return the connection_config if auto is configured.""" - # pylint: disable=no-self-use - from xknx.io import ConnectionConfig - return ConnectionConfig() - - def register_callbacks(self): - """Register callbacks within XKNX object.""" - if CONF_KNX_FIRE_EVENT in self.config[DOMAIN] and \ - self.config[DOMAIN][CONF_KNX_FIRE_EVENT]: - from xknx.knx import AddressFilter - address_filters = list(map( - AddressFilter, - self.config[DOMAIN][CONF_KNX_FIRE_EVENT_FILTER])) - self.xknx.telegram_queue.register_telegram_received_cb( - self.telegram_received_cb, address_filters) - - @callback - def async_create_exposures(self): - """Create exposures.""" - if CONF_KNX_EXPOSE not in self.config[DOMAIN]: - return - for to_expose in self.config[DOMAIN][CONF_KNX_EXPOSE]: - expose_type = to_expose.get(CONF_KNX_EXPOSE_TYPE) - entity_id = to_expose.get(CONF_ENTITY_ID) - address = to_expose.get(CONF_KNX_EXPOSE_ADDRESS) - if expose_type in ['time', 'date', 'datetime']: - exposure = KNXExposeTime( - self.xknx, expose_type, address) - exposure.async_register() - self.exposures.append(exposure) - else: - exposure = KNXExposeSensor( - self.hass, self.xknx, expose_type, entity_id, address) - exposure.async_register() - self.exposures.append(exposure) - - async def telegram_received_cb(self, telegram): - """Call invoked after a KNX telegram was received.""" - self.hass.bus.async_fire('knx_event', { - 'address': str(telegram.group_address), - 'data': telegram.payload.value - }) - # False signals XKNX to proceed with processing telegrams. - return False - - async def service_send_to_knx_bus(self, call): - """Service for sending an arbitrary KNX message to the KNX bus.""" - from xknx.knx import Telegram, GroupAddress, DPTBinary, DPTArray - attr_payload = call.data.get(SERVICE_KNX_ATTR_PAYLOAD) - attr_address = call.data.get(SERVICE_KNX_ATTR_ADDRESS) - - def calculate_payload(attr_payload): - """Calculate payload depending on type of attribute.""" - if isinstance(attr_payload, int): - return DPTBinary(attr_payload) - return DPTArray(attr_payload) - payload = calculate_payload(attr_payload) - address = GroupAddress(attr_address) - - telegram = Telegram() - telegram.payload = payload - telegram.group_address = address - await self.xknx.telegrams.put(telegram) - - -class KNXAutomation(): - """Wrapper around xknx.devices.ActionCallback object..""" - - def __init__(self, hass, device, hook, action, counter=1): - """Initialize Automation class.""" - self.hass = hass - self.device = device - script_name = "{} turn ON script".format(device.get_name()) - self.script = Script(hass, action, script_name) - - import xknx - self.action = xknx.devices.ActionCallback( - hass.data[DATA_KNX].xknx, self.script.async_run, - hook=hook, counter=counter) - device.actions.append(self.action) - - -class KNXExposeTime: - """Object to Expose Time/Date object to KNX bus.""" - - def __init__(self, xknx, expose_type, address): - """Initialize of Expose class.""" - self.xknx = xknx - self.type = expose_type - self.address = address - self.device = None - - @callback - def async_register(self): - """Register listener.""" - from xknx.devices import DateTime, DateTimeBroadcastType - broadcast_type_string = self.type.upper() - broadcast_type = DateTimeBroadcastType[broadcast_type_string] - self.device = DateTime( - self.xknx, - 'Time', - broadcast_type=broadcast_type, - group_address=self.address) - self.xknx.devices.add(self.device) - - -class KNXExposeSensor: - """Object to Expose HASS entity to KNX bus.""" - - def __init__(self, hass, xknx, expose_type, entity_id, address): - """Initialize of Expose class.""" - self.hass = hass - self.xknx = xknx - self.type = expose_type - self.entity_id = entity_id - self.address = address - self.device = None - - @callback - def async_register(self): - """Register listener.""" - from xknx.devices import ExposeSensor - self.device = ExposeSensor( - self.xknx, - name=self.entity_id, - group_address=self.address, - value_type=self.type) - self.xknx.devices.add(self.device) - async_track_state_change( - self.hass, self.entity_id, self._async_entity_changed) - - async def _async_entity_changed(self, entity_id, old_state, new_state): - """Handle entity change.""" - if new_state is None: - return - await self.device.set(float(new_state.state)) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py new file mode 100644 index 0000000000000..fdaba5e570900 --- /dev/null +++ b/homeassistant/components/knx/__init__.py @@ -0,0 +1,334 @@ +"""Support KNX devices.""" +import logging + +import voluptuous as vol + +from homeassistant.const import ( + CONF_ENTITY_ID, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP) +from homeassistant.core import callback +from homeassistant.helpers import discovery +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.4'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "knx" +DATA_KNX = "data_knx" +CONF_KNX_CONFIG = "config_file" + +CONF_KNX_ROUTING = "routing" +CONF_KNX_TUNNELING = "tunneling" +CONF_KNX_LOCAL_IP = "local_ip" +CONF_KNX_FIRE_EVENT = "fire_event" +CONF_KNX_FIRE_EVENT_FILTER = "fire_event_filter" +CONF_KNX_STATE_UPDATER = "state_updater" +CONF_KNX_EXPOSE = "expose" +CONF_KNX_EXPOSE_TYPE = "type" +CONF_KNX_EXPOSE_ADDRESS = "address" + +SERVICE_KNX_SEND = "send" +SERVICE_KNX_ATTR_ADDRESS = "address" +SERVICE_KNX_ATTR_PAYLOAD = "payload" + +ATTR_DISCOVER_DEVICES = 'devices' + +TUNNELING_SCHEMA = vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_KNX_LOCAL_IP): cv.string, + vol.Optional(CONF_PORT): cv.port, +}) + +ROUTING_SCHEMA = vol.Schema({ + vol.Required(CONF_KNX_LOCAL_IP): cv.string, +}) + +EXPOSE_SCHEMA = vol.Schema({ + vol.Required(CONF_KNX_EXPOSE_TYPE): cv.string, + vol.Optional(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_KNX_EXPOSE_ADDRESS): cv.string, +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Optional(CONF_KNX_CONFIG): cv.string, + vol.Exclusive(CONF_KNX_ROUTING, 'connection_type'): ROUTING_SCHEMA, + vol.Exclusive(CONF_KNX_TUNNELING, 'connection_type'): + TUNNELING_SCHEMA, + vol.Inclusive(CONF_KNX_FIRE_EVENT, 'fire_ev'): + cv.boolean, + vol.Inclusive(CONF_KNX_FIRE_EVENT_FILTER, 'fire_ev'): + vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_KNX_STATE_UPDATER, default=True): cv.boolean, + vol.Optional(CONF_KNX_EXPOSE): + vol.All( + cv.ensure_list, + [EXPOSE_SCHEMA]), + }) +}, extra=vol.ALLOW_EXTRA) + +SERVICE_KNX_SEND_SCHEMA = vol.Schema({ + vol.Required(SERVICE_KNX_ATTR_ADDRESS): cv.string, + vol.Required(SERVICE_KNX_ATTR_PAYLOAD): vol.Any( + cv.positive_int, [cv.positive_int]), +}) + + +async def async_setup(hass, config): + """Set up the KNX component.""" + from xknx.exceptions import XKNXException + try: + hass.data[DATA_KNX] = KNXModule(hass, config) + hass.data[DATA_KNX].async_create_exposures() + await hass.data[DATA_KNX].start() + + except XKNXException as ex: + _LOGGER.warning("Can't connect to KNX interface: %s", ex) + hass.components.persistent_notification.async_create( + "Can't connect to KNX interface:
" + "{0}".format(ex), + title="KNX") + + for component, discovery_type in ( + ('switch', 'Switch'), + ('climate', 'Climate'), + ('cover', 'Cover'), + ('light', 'Light'), + ('sensor', 'Sensor'), + ('binary_sensor', 'BinarySensor'), + ('scene', 'Scene'), + ('notify', 'Notification')): + found_devices = _get_devices(hass, discovery_type) + hass.async_create_task( + discovery.async_load_platform(hass, component, DOMAIN, { + ATTR_DISCOVER_DEVICES: found_devices + }, config)) + + hass.services.async_register( + DOMAIN, SERVICE_KNX_SEND, + hass.data[DATA_KNX].service_send_to_knx_bus, + schema=SERVICE_KNX_SEND_SCHEMA) + + return True + + +def _get_devices(hass, discovery_type): + """Get the KNX devices.""" + return list( + map(lambda device: device.name, + filter( + lambda device: type(device).__name__ == discovery_type, + hass.data[DATA_KNX].xknx.devices))) + + +class KNXModule: + """Representation of KNX Object.""" + + def __init__(self, hass, config): + """Initialize of KNX module.""" + self.hass = hass + self.config = config + self.connected = False + self.init_xknx() + self.register_callbacks() + self.exposures = [] + + def init_xknx(self): + """Initialize of KNX object.""" + from xknx import XKNX + self.xknx = XKNX(config=self.config_file(), loop=self.hass.loop) + + async def start(self): + """Start KNX object. Connect to tunneling or Routing device.""" + connection_config = self.connection_config() + await self.xknx.start( + state_updater=self.config[DOMAIN][CONF_KNX_STATE_UPDATER], + connection_config=connection_config) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop) + self.connected = True + + async def stop(self, event): + """Stop KNX object. Disconnect from tunneling or Routing device.""" + await self.xknx.stop() + + def config_file(self): + """Resolve and return the full path of xknx.yaml if configured.""" + config_file = self.config[DOMAIN].get(CONF_KNX_CONFIG) + if not config_file: + return None + if not config_file.startswith("/"): + return self.hass.config.path(config_file) + return config_file + + def connection_config(self): + """Return the connection_config.""" + if CONF_KNX_TUNNELING in self.config[DOMAIN]: + return self.connection_config_tunneling() + if CONF_KNX_ROUTING in self.config[DOMAIN]: + return self.connection_config_routing() + return self.connection_config_auto() + + def connection_config_routing(self): + """Return the connection_config if routing is configured.""" + from xknx.io import ConnectionConfig, ConnectionType + local_ip = \ + self.config[DOMAIN][CONF_KNX_ROUTING].get(CONF_KNX_LOCAL_IP) + return ConnectionConfig( + connection_type=ConnectionType.ROUTING, + local_ip=local_ip) + + def connection_config_tunneling(self): + """Return the connection_config if tunneling is configured.""" + from xknx.io import ConnectionConfig, ConnectionType, \ + DEFAULT_MCAST_PORT + gateway_ip = \ + self.config[DOMAIN][CONF_KNX_TUNNELING].get(CONF_HOST) + gateway_port = \ + self.config[DOMAIN][CONF_KNX_TUNNELING].get(CONF_PORT) + local_ip = \ + self.config[DOMAIN][CONF_KNX_TUNNELING].get(CONF_KNX_LOCAL_IP) + if gateway_port is None: + gateway_port = DEFAULT_MCAST_PORT + return ConnectionConfig( + connection_type=ConnectionType.TUNNELING, gateway_ip=gateway_ip, + gateway_port=gateway_port, local_ip=local_ip) + + def connection_config_auto(self): + """Return the connection_config if auto is configured.""" + # pylint: disable=no-self-use + from xknx.io import ConnectionConfig + return ConnectionConfig() + + def register_callbacks(self): + """Register callbacks within XKNX object.""" + if CONF_KNX_FIRE_EVENT in self.config[DOMAIN] and \ + self.config[DOMAIN][CONF_KNX_FIRE_EVENT]: + from xknx.knx import AddressFilter + address_filters = list(map( + AddressFilter, + self.config[DOMAIN][CONF_KNX_FIRE_EVENT_FILTER])) + self.xknx.telegram_queue.register_telegram_received_cb( + self.telegram_received_cb, address_filters) + + @callback + def async_create_exposures(self): + """Create exposures.""" + if CONF_KNX_EXPOSE not in self.config[DOMAIN]: + return + for to_expose in self.config[DOMAIN][CONF_KNX_EXPOSE]: + expose_type = to_expose.get(CONF_KNX_EXPOSE_TYPE) + entity_id = to_expose.get(CONF_ENTITY_ID) + address = to_expose.get(CONF_KNX_EXPOSE_ADDRESS) + if expose_type in ['time', 'date', 'datetime']: + exposure = KNXExposeTime( + self.xknx, expose_type, address) + exposure.async_register() + self.exposures.append(exposure) + else: + exposure = KNXExposeSensor( + self.hass, self.xknx, expose_type, entity_id, address) + exposure.async_register() + self.exposures.append(exposure) + + async def telegram_received_cb(self, telegram): + """Call invoked after a KNX telegram was received.""" + self.hass.bus.async_fire('knx_event', { + 'address': str(telegram.group_address), + 'data': telegram.payload.value + }) + # False signals XKNX to proceed with processing telegrams. + return False + + async def service_send_to_knx_bus(self, call): + """Service for sending an arbitrary KNX message to the KNX bus.""" + from xknx.knx import Telegram, GroupAddress, DPTBinary, DPTArray + attr_payload = call.data.get(SERVICE_KNX_ATTR_PAYLOAD) + attr_address = call.data.get(SERVICE_KNX_ATTR_ADDRESS) + + def calculate_payload(attr_payload): + """Calculate payload depending on type of attribute.""" + if isinstance(attr_payload, int): + return DPTBinary(attr_payload) + return DPTArray(attr_payload) + payload = calculate_payload(attr_payload) + address = GroupAddress(attr_address) + + telegram = Telegram() + telegram.payload = payload + telegram.group_address = address + await self.xknx.telegrams.put(telegram) + + +class KNXAutomation(): + """Wrapper around xknx.devices.ActionCallback object..""" + + def __init__(self, hass, device, hook, action, counter=1): + """Initialize Automation class.""" + self.hass = hass + self.device = device + script_name = "{} turn ON script".format(device.get_name()) + self.script = Script(hass, action, script_name) + + import xknx + self.action = xknx.devices.ActionCallback( + hass.data[DATA_KNX].xknx, self.script.async_run, + hook=hook, counter=counter) + device.actions.append(self.action) + + +class KNXExposeTime: + """Object to Expose Time/Date object to KNX bus.""" + + def __init__(self, xknx, expose_type, address): + """Initialize of Expose class.""" + self.xknx = xknx + self.type = expose_type + self.address = address + self.device = None + + @callback + def async_register(self): + """Register listener.""" + from xknx.devices import DateTime, DateTimeBroadcastType + broadcast_type_string = self.type.upper() + broadcast_type = DateTimeBroadcastType[broadcast_type_string] + self.device = DateTime( + self.xknx, + 'Time', + broadcast_type=broadcast_type, + group_address=self.address) + self.xknx.devices.add(self.device) + + +class KNXExposeSensor: + """Object to Expose HASS entity to KNX bus.""" + + def __init__(self, hass, xknx, expose_type, entity_id, address): + """Initialize of Expose class.""" + self.hass = hass + self.xknx = xknx + self.type = expose_type + self.entity_id = entity_id + self.address = address + self.device = None + + @callback + def async_register(self): + """Register listener.""" + from xknx.devices import ExposeSensor + self.device = ExposeSensor( + self.xknx, + name=self.entity_id, + group_address=self.address, + value_type=self.type) + self.xknx.devices.add(self.device) + async_track_state_change( + self.hass, self.entity_id, self._async_entity_changed) + + async def _async_entity_changed(self, entity_id, old_state, new_state): + """Handle entity change.""" + if new_state is None: + return + await self.device.set(float(new_state.state)) diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py new file mode 100644 index 0000000000000..ca7037fe81d16 --- /dev/null +++ b/homeassistant/components/knx/binary_sensor.py @@ -0,0 +1,140 @@ +"""Support for KNX/IP binary sensors.""" +import voluptuous as vol + +from homeassistant.components.binary_sensor import ( + PLATFORM_SCHEMA, BinarySensorDevice) +from homeassistant.components.knx import ( + ATTR_DISCOVER_DEVICES, DATA_KNX, KNXAutomation) +from homeassistant.const import CONF_NAME +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv + +CONF_ADDRESS = 'address' +CONF_DEVICE_CLASS = 'device_class' +CONF_SIGNIFICANT_BIT = 'significant_bit' +CONF_DEFAULT_SIGNIFICANT_BIT = 1 +CONF_AUTOMATION = 'automation' +CONF_HOOK = 'hook' +CONF_DEFAULT_HOOK = 'on' +CONF_COUNTER = 'counter' +CONF_DEFAULT_COUNTER = 1 +CONF_ACTION = 'action' +CONF_RESET_AFTER = 'reset_after' + +CONF__ACTION = 'turn_off_action' + +DEFAULT_NAME = 'KNX Binary Sensor' +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, +}) + +AUTOMATIONS_SCHEMA = vol.All( + cv.ensure_list, + [AUTOMATION_SCHEMA] +) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_ADDRESS): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_DEVICE_CLASS): cv.string, + vol.Optional(CONF_SIGNIFICANT_BIT, default=CONF_DEFAULT_SIGNIFICANT_BIT): + cv.positive_int, + vol.Optional(CONF_RESET_AFTER): cv.positive_int, + vol.Optional(CONF_AUTOMATION): AUTOMATIONS_SCHEMA, +}) + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up binary sensor(s) 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 binary sensors 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(KNXBinarySensor(device)) + async_add_entities(entities) + + +@callback +def async_add_entities_config(hass, config, async_add_entities): + """Set up binary senor for KNX platform configured within platform.""" + name = config.get(CONF_NAME) + import xknx + binary_sensor = xknx.devices.BinarySensor( + hass.data[DATA_KNX].xknx, + name=name, + group_address=config.get(CONF_ADDRESS), + device_class=config.get(CONF_DEVICE_CLASS), + significant_bit=config.get(CONF_SIGNIFICANT_BIT), + reset_after=config.get(CONF_RESET_AFTER)) + hass.data[DATA_KNX].xknx.devices.add(binary_sensor) + + entity = KNXBinarySensor(binary_sensor) + automations = config.get(CONF_AUTOMATION) + if automations is not None: + for automation in automations: + counter = automation.get(CONF_COUNTER) + hook = automation.get(CONF_HOOK) + action = automation.get(CONF_ACTION) + entity.automations.append(KNXAutomation( + hass=hass, device=binary_sensor, hook=hook, + action=action, counter=counter)) + async_add_entities([entity]) + + +class KNXBinarySensor(BinarySensorDevice): + """Representation of a KNX binary sensor.""" + + def __init__(self, device): + """Initialize of KNX binary sensor.""" + self.device = device + self.automations = [] + + @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 device_class(self): + """Return the class of this sensor.""" + return self.device.device_class + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self.device.is_on() diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py new file mode 100644 index 0000000000000..82eaa52ae5a49 --- /dev/null +++ b/homeassistant/components/knx/climate.py @@ -0,0 +1,276 @@ +"""Support for KNX/IP climate devices.""" +import voluptuous as vol +import homeassistant.helpers.config_validation as cv +from homeassistant.components.climate import ( + PLATFORM_SCHEMA, SUPPORT_ON_OFF, SUPPORT_OPERATION_MODE, + SUPPORT_TARGET_TEMPERATURE, STATE_HEAT, + STATE_IDLE, STATE_MANUAL, STATE_DRY, + STATE_FAN_ONLY, STATE_ECO, ClimateDevice) +from homeassistant.const import ( + ATTR_TEMPERATURE, CONF_NAME, TEMP_CELSIUS) +from homeassistant.core import callback + +from homeassistant.components.knx import DATA_KNX, ATTR_DISCOVER_DEVICES + +CONF_SETPOINT_SHIFT_ADDRESS = 'setpoint_shift_address' +CONF_SETPOINT_SHIFT_STATE_ADDRESS = 'setpoint_shift_state_address' +CONF_SETPOINT_SHIFT_STEP = 'setpoint_shift_step' +CONF_SETPOINT_SHIFT_MAX = 'setpoint_shift_max' +CONF_SETPOINT_SHIFT_MIN = 'setpoint_shift_min' +CONF_TEMPERATURE_ADDRESS = 'temperature_address' +CONF_TARGET_TEMPERATURE_ADDRESS = 'target_temperature_address' +CONF_OPERATION_MODE_ADDRESS = 'operation_mode_address' +CONF_OPERATION_MODE_STATE_ADDRESS = 'operation_mode_state_address' +CONF_CONTROLLER_STATUS_ADDRESS = 'controller_status_address' +CONF_CONTROLLER_STATUS_STATE_ADDRESS = 'controller_status_state_address' +CONF_CONTROLLER_MODE_ADDRESS = 'controller_mode_address' +CONF_CONTROLLER_MODE_STATE_ADDRESS = 'controller_mode_state_address' +CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS = \ + 'operation_mode_frost_protection_address' +CONF_OPERATION_MODE_NIGHT_ADDRESS = 'operation_mode_night_address' +CONF_OPERATION_MODE_COMFORT_ADDRESS = 'operation_mode_comfort_address' +CONF_OPERATION_MODES = 'operation_modes' +CONF_ON_OFF_ADDRESS = 'on_off_address' +CONF_ON_OFF_STATE_ADDRESS = 'on_off_state_address' +CONF_MIN_TEMP = 'min_temp' +CONF_MAX_TEMP = 'max_temp' + +DEFAULT_NAME = 'KNX Climate' +DEFAULT_SETPOINT_SHIFT_STEP = 0.5 +DEFAULT_SETPOINT_SHIFT_MAX = 6 +DEFAULT_SETPOINT_SHIFT_MIN = -6 +DEPENDENCIES = ['knx'] + +# Map KNX operation modes to HA modes. This list might not be full. +OPERATION_MODES = { + # Map DPT 201.100 HVAC operating modes + "Frost Protection": STATE_MANUAL, + "Night": STATE_IDLE, + "Standby": STATE_ECO, + "Comfort": STATE_HEAT, + # Map DPT 201.104 HVAC control modes + "Fan only": STATE_FAN_ONLY, + "Dehumidification": STATE_DRY +} + +OPERATION_MODES_INV = dict(( + reversed(item) for item in OPERATION_MODES.items())) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_TEMPERATURE_ADDRESS): cv.string, + vol.Required(CONF_TARGET_TEMPERATURE_ADDRESS): cv.string, + vol.Optional(CONF_SETPOINT_SHIFT_ADDRESS): cv.string, + vol.Optional(CONF_SETPOINT_SHIFT_STATE_ADDRESS): cv.string, + vol.Optional(CONF_SETPOINT_SHIFT_STEP, + default=DEFAULT_SETPOINT_SHIFT_STEP): vol.All( + float, vol.Range(min=0, max=2)), + vol.Optional(CONF_SETPOINT_SHIFT_MAX, default=DEFAULT_SETPOINT_SHIFT_MAX): + vol.All(int, vol.Range(min=0, max=32)), + vol.Optional(CONF_SETPOINT_SHIFT_MIN, default=DEFAULT_SETPOINT_SHIFT_MIN): + vol.All(int, vol.Range(min=-32, max=0)), + vol.Optional(CONF_OPERATION_MODE_ADDRESS): cv.string, + vol.Optional(CONF_OPERATION_MODE_STATE_ADDRESS): cv.string, + vol.Optional(CONF_CONTROLLER_STATUS_ADDRESS): cv.string, + vol.Optional(CONF_CONTROLLER_STATUS_STATE_ADDRESS): cv.string, + vol.Optional(CONF_CONTROLLER_MODE_ADDRESS): cv.string, + vol.Optional(CONF_CONTROLLER_MODE_STATE_ADDRESS): cv.string, + vol.Optional(CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS): cv.string, + vol.Optional(CONF_OPERATION_MODE_NIGHT_ADDRESS): cv.string, + vol.Optional(CONF_OPERATION_MODE_COMFORT_ADDRESS): cv.string, + vol.Optional(CONF_ON_OFF_ADDRESS): cv.string, + vol.Optional(CONF_ON_OFF_STATE_ADDRESS): cv.string, + vol.Optional(CONF_OPERATION_MODES): vol.All(cv.ensure_list, + [vol.In(OPERATION_MODES)]), + vol.Optional(CONF_MIN_TEMP): vol.Coerce(float), + vol.Optional(CONF_MAX_TEMP): vol.Coerce(float), +}) + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up climate(s) 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 climates for KNX platform configured within platform.""" + entities = [] + for device_name in discovery_info[ATTR_DISCOVER_DEVICES]: + device = hass.data[DATA_KNX].xknx.devices[device_name] + entities.append(KNXClimate(device)) + async_add_entities(entities) + + +@callback +def async_add_entities_config(hass, config, async_add_entities): + """Set up climate for KNX platform configured within platform.""" + import xknx + + climate_mode = xknx.devices.ClimateMode( + hass.data[DATA_KNX].xknx, + name=config.get(CONF_NAME) + " Mode", + group_address_operation_mode=config.get(CONF_OPERATION_MODE_ADDRESS), + group_address_operation_mode_state=config.get( + CONF_OPERATION_MODE_STATE_ADDRESS), + group_address_controller_status=config.get( + CONF_CONTROLLER_STATUS_ADDRESS), + group_address_controller_status_state=config.get( + CONF_CONTROLLER_STATUS_STATE_ADDRESS), + group_address_controller_mode=config.get( + CONF_CONTROLLER_MODE_ADDRESS), + group_address_controller_mode_state=config.get( + CONF_CONTROLLER_MODE_STATE_ADDRESS), + group_address_operation_mode_protection=config.get( + CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS), + group_address_operation_mode_night=config.get( + CONF_OPERATION_MODE_NIGHT_ADDRESS), + group_address_operation_mode_comfort=config.get( + CONF_OPERATION_MODE_COMFORT_ADDRESS), + operation_modes=config.get( + CONF_OPERATION_MODES)) + hass.data[DATA_KNX].xknx.devices.add(climate_mode) + + climate = xknx.devices.Climate( + hass.data[DATA_KNX].xknx, + name=config.get(CONF_NAME), + group_address_temperature=config.get(CONF_TEMPERATURE_ADDRESS), + group_address_target_temperature=config.get( + CONF_TARGET_TEMPERATURE_ADDRESS), + group_address_setpoint_shift=config.get(CONF_SETPOINT_SHIFT_ADDRESS), + group_address_setpoint_shift_state=config.get( + CONF_SETPOINT_SHIFT_STATE_ADDRESS), + setpoint_shift_step=config.get(CONF_SETPOINT_SHIFT_STEP), + setpoint_shift_max=config.get(CONF_SETPOINT_SHIFT_MAX), + setpoint_shift_min=config.get(CONF_SETPOINT_SHIFT_MIN), + group_address_on_off=config.get( + CONF_ON_OFF_ADDRESS), + group_address_on_off_state=config.get( + CONF_ON_OFF_STATE_ADDRESS), + min_temp=config.get(CONF_MIN_TEMP), + max_temp=config.get(CONF_MAX_TEMP), + mode=climate_mode) + hass.data[DATA_KNX].xknx.devices.add(climate) + + async_add_entities([KNXClimate(climate)]) + + +class KNXClimate(ClimateDevice): + """Representation of a KNX climate device.""" + + def __init__(self, device): + """Initialize of a KNX climate device.""" + self.device = device + self._unit_of_measurement = TEMP_CELSIUS + + @property + def supported_features(self): + """Return the list of supported features.""" + support = SUPPORT_TARGET_TEMPERATURE + if self.device.mode.supports_operation_mode: + support |= SUPPORT_OPERATION_MODE + if self.device.supports_on_off: + support |= SUPPORT_ON_OFF + return support + + async def async_added_to_hass(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) + + @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 temperature_unit(self): + """Return the unit of measurement.""" + return self._unit_of_measurement + + @property + def current_temperature(self): + """Return the current temperature.""" + return self.device.temperature.value + + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + return self.device.setpoint_shift_step + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self.device.target_temperature.value + + @property + def min_temp(self): + """Return the minimum temperature.""" + return self.device.target_temperature_min + + @property + def max_temp(self): + """Return the maximum temperature.""" + return self.device.target_temperature_max + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature is None: + return + await self.device.set_target_temperature(temperature) + await self.async_update_ha_state() + + @property + def current_operation(self): + """Return current operation ie. heat, cool, idle.""" + if self.device.mode.supports_operation_mode: + return OPERATION_MODES.get(self.device.mode.operation_mode.value) + return None + + @property + def operation_list(self): + """Return the list of available operation modes.""" + return [OPERATION_MODES.get(operation_mode.value) for + operation_mode in + self.device.mode.operation_modes] + + async def async_set_operation_mode(self, operation_mode): + """Set operation mode.""" + if self.device.mode.supports_operation_mode: + from xknx.knx import HVACOperationMode + knx_operation_mode = HVACOperationMode( + OPERATION_MODES_INV.get(operation_mode)) + await self.device.mode.set_operation_mode(knx_operation_mode) + await self.async_update_ha_state() + + @property + def is_on(self): + """Return true if the device is on.""" + if self.device.supports_on_off: + return self.device.is_on + return None + + async def async_turn_on(self): + """Turn on.""" + await self.device.turn_on() + + async def async_turn_off(self): + """Turn off.""" + await self.device.turn_off() diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py new file mode 100644 index 0000000000000..9423983f9f758 --- /dev/null +++ b/homeassistant/components/knx/cover.py @@ -0,0 +1,198 @@ +"""Support for KNX/IP covers.""" +import voluptuous as vol + +from homeassistant.components.cover import ( + ATTR_POSITION, ATTR_TILT_POSITION, PLATFORM_SCHEMA, SUPPORT_CLOSE, + SUPPORT_OPEN, SUPPORT_SET_POSITION, SUPPORT_SET_TILT_POSITION, + SUPPORT_STOP, CoverDevice) +from homeassistant.components.knx import ATTR_DISCOVER_DEVICES, DATA_KNX +from homeassistant.const import CONF_NAME +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_track_utc_time_change + +CONF_MOVE_LONG_ADDRESS = 'move_long_address' +CONF_MOVE_SHORT_ADDRESS = 'move_short_address' +CONF_POSITION_ADDRESS = 'position_address' +CONF_POSITION_STATE_ADDRESS = 'position_state_address' +CONF_ANGLE_ADDRESS = 'angle_address' +CONF_ANGLE_STATE_ADDRESS = 'angle_state_address' +CONF_TRAVELLING_TIME_DOWN = 'travelling_time_down' +CONF_TRAVELLING_TIME_UP = 'travelling_time_up' +CONF_INVERT_POSITION = 'invert_position' +CONF_INVERT_ANGLE = 'invert_angle' + +DEFAULT_TRAVEL_TIME = 25 +DEFAULT_NAME = 'KNX Cover' +DEPENDENCIES = ['knx'] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_MOVE_LONG_ADDRESS): cv.string, + vol.Optional(CONF_MOVE_SHORT_ADDRESS): cv.string, + vol.Optional(CONF_POSITION_ADDRESS): cv.string, + vol.Optional(CONF_POSITION_STATE_ADDRESS): cv.string, + vol.Optional(CONF_ANGLE_ADDRESS): cv.string, + vol.Optional(CONF_ANGLE_STATE_ADDRESS): cv.string, + vol.Optional(CONF_TRAVELLING_TIME_DOWN, default=DEFAULT_TRAVEL_TIME): + cv.positive_int, + vol.Optional(CONF_TRAVELLING_TIME_UP, default=DEFAULT_TRAVEL_TIME): + cv.positive_int, + vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean, + vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean, +}) + + +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) + else: + async_add_entities_config(hass, config, async_add_entities) + + +@callback +def async_add_entities_discovery(hass, discovery_info, async_add_entities): + """Set up covers 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(KNXCover(device)) + async_add_entities(entities) + + +@callback +def async_add_entities_config(hass, config, async_add_entities): + """Set up cover for KNX platform configured within platform.""" + import xknx + cover = xknx.devices.Cover( + hass.data[DATA_KNX].xknx, + name=config.get(CONF_NAME), + group_address_long=config.get(CONF_MOVE_LONG_ADDRESS), + group_address_short=config.get(CONF_MOVE_SHORT_ADDRESS), + group_address_position_state=config.get( + CONF_POSITION_STATE_ADDRESS), + group_address_angle=config.get(CONF_ANGLE_ADDRESS), + group_address_angle_state=config.get(CONF_ANGLE_STATE_ADDRESS), + group_address_position=config.get(CONF_POSITION_ADDRESS), + travel_time_down=config.get(CONF_TRAVELLING_TIME_DOWN), + travel_time_up=config.get(CONF_TRAVELLING_TIME_UP), + invert_position=config.get(CONF_INVERT_POSITION), + invert_angle=config.get(CONF_INVERT_ANGLE)) + + hass.data[DATA_KNX].xknx.devices.add(cover) + async_add_entities([KNXCover(cover)]) + + +class KNXCover(CoverDevice): + """Representation of a KNX cover.""" + + def __init__(self, device): + """Initialize the cover.""" + self.device = device + self._unsubscribe_auto_updater = None + + @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 supported_features(self): + """Flag supported features.""" + supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | \ + SUPPORT_SET_POSITION | SUPPORT_STOP + if self.device.supports_angle: + supported_features |= SUPPORT_SET_TILT_POSITION + return supported_features + + @property + def current_cover_position(self): + """Return the current position of the cover.""" + return self.device.current_position() + + @property + def is_closed(self): + """Return if the cover is closed.""" + return self.device.is_closed() + + async def async_close_cover(self, **kwargs): + """Close the cover.""" + if not self.device.is_closed(): + await self.device.set_down() + self.start_auto_updater() + + async def async_open_cover(self, **kwargs): + """Open the cover.""" + if not self.device.is_open(): + await self.device.set_up() + self.start_auto_updater() + + async def async_set_cover_position(self, **kwargs): + """Move the cover to a specific position.""" + if ATTR_POSITION in kwargs: + position = kwargs[ATTR_POSITION] + await self.device.set_position(position) + self.start_auto_updater() + + async def async_stop_cover(self, **kwargs): + """Stop the cover.""" + await self.device.stop() + self.stop_auto_updater() + + @property + def current_cover_tilt_position(self): + """Return current tilt position of cover.""" + if not self.device.supports_angle: + return None + return self.device.current_angle() + + async def async_set_cover_tilt_position(self, **kwargs): + """Move the cover tilt to a specific position.""" + if ATTR_TILT_POSITION in kwargs: + tilt_position = kwargs[ATTR_TILT_POSITION] + await self.device.set_angle(tilt_position) + + def start_auto_updater(self): + """Start the autoupdater to update HASS while cover is moving.""" + if self._unsubscribe_auto_updater is None: + self._unsubscribe_auto_updater = async_track_utc_time_change( + self.hass, self.auto_updater_hook) + + def stop_auto_updater(self): + """Stop the autoupdater.""" + if self._unsubscribe_auto_updater is not None: + self._unsubscribe_auto_updater() + self._unsubscribe_auto_updater = None + + @callback + def auto_updater_hook(self, now): + """Call for the autoupdater.""" + self.async_schedule_update_ha_state() + if self.device.position_reached(): + self.stop_auto_updater() + + self.hass.add_job(self.device.auto_stop_if_necessary()) diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py new file mode 100644 index 0000000000000..f2a6f15e08b9e --- /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/knx/notify.py b/homeassistant/components/knx/notify.py new file mode 100644 index 0000000000000..2488114aa418b --- /dev/null +++ b/homeassistant/components/knx/notify.py @@ -0,0 +1,85 @@ +"""Support for KNX/IP notification services.""" +import voluptuous as vol + +from homeassistant.components.knx import DATA_KNX, ATTR_DISCOVER_DEVICES +from homeassistant.components.notify import PLATFORM_SCHEMA, \ + BaseNotificationService +from homeassistant.const import CONF_NAME +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv + +CONF_ADDRESS = 'address' +DEFAULT_NAME = 'KNX Notify' + +DEPENDENCIES = ['knx'] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_ADDRESS): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string +}) + + +async def async_get_service(hass, config, discovery_info=None): + """Get the KNX notification service.""" + return async_get_service_discovery(hass, discovery_info) \ + if discovery_info is not None else \ + async_get_service_config(hass, config) + + +@callback +def async_get_service_discovery(hass, discovery_info): + """Set up notifications for KNX platform configured via xknx.yaml.""" + notification_devices = [] + for device_name in discovery_info[ATTR_DISCOVER_DEVICES]: + device = hass.data[DATA_KNX].xknx.devices[device_name] + notification_devices.append(device) + return \ + KNXNotificationService(notification_devices) \ + if notification_devices else \ + None + + +@callback +def async_get_service_config(hass, config): + """Set up notification for KNX platform configured within platform.""" + import xknx + notification = xknx.devices.Notification( + hass.data[DATA_KNX].xknx, + name=config.get(CONF_NAME), + group_address=config.get(CONF_ADDRESS)) + hass.data[DATA_KNX].xknx.devices.add(notification) + return KNXNotificationService([notification, ]) + + +class KNXNotificationService(BaseNotificationService): + """Implement demo notification service.""" + + def __init__(self, devices): + """Initialize the service.""" + self.devices = devices + + @property + def targets(self): + """Return a dictionary of registered targets.""" + ret = {} + for device in self.devices: + ret[device.name] = device.name + return ret + + async def async_send_message(self, message="", **kwargs): + """Send a notification to knx bus.""" + if "target" in kwargs: + await self._async_send_to_device(message, kwargs["target"]) + else: + await self._async_send_to_all_devices(message) + + async def _async_send_to_all_devices(self, message): + """Send a notification to knx bus to all connected devices.""" + for device in self.devices: + await device.set(message) + + async def _async_send_to_device(self, message, names): + """Send a notification to knx bus to device with given names.""" + for device in self.devices: + if device.name in names: + await device.set(message) diff --git a/homeassistant/components/knx/scene.py b/homeassistant/components/knx/scene.py new file mode 100644 index 0000000000000..008e81508b921 --- /dev/null +++ b/homeassistant/components/knx/scene.py @@ -0,0 +1,70 @@ +"""Support for KNX scenes.""" +import voluptuous as vol + +from homeassistant.components.knx import ATTR_DISCOVER_DEVICES, DATA_KNX +from homeassistant.components.scene import CONF_PLATFORM, Scene +from homeassistant.const import CONF_NAME +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv + +CONF_ADDRESS = 'address' +CONF_SCENE_NUMBER = 'scene_number' + +DEFAULT_NAME = 'KNX SCENE' +DEPENDENCIES = ['knx'] + +PLATFORM_SCHEMA = vol.Schema({ + vol.Required(CONF_PLATFORM): 'knx', + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_ADDRESS): cv.string, + vol.Required(CONF_SCENE_NUMBER): cv.positive_int, +}) + + +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) + else: + async_add_entities_config(hass, config, async_add_entities) + + +@callback +def async_add_entities_discovery(hass, discovery_info, async_add_entities): + """Set up scenes 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(KNXScene(device)) + async_add_entities(entities) + + +@callback +def async_add_entities_config(hass, config, async_add_entities): + """Set up scene for KNX platform configured within platform.""" + import xknx + scene = xknx.devices.Scene( + hass.data[DATA_KNX].xknx, + name=config.get(CONF_NAME), + group_address=config.get(CONF_ADDRESS), + scene_number=config.get(CONF_SCENE_NUMBER)) + hass.data[DATA_KNX].xknx.devices.add(scene) + async_add_entities([KNXScene(scene)]) + + +class KNXScene(Scene): + """Representation of a KNX scene.""" + + def __init__(self, scene): + """Init KNX scene.""" + self.scene = scene + + @property + def name(self): + """Return the name of the scene.""" + return self.scene.name + + async def async_activate(self): + """Activate the scene.""" + await self.scene.run() diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py new file mode 100644 index 0000000000000..6a2d8144b1e81 --- /dev/null +++ b/homeassistant/components/knx/sensor.py @@ -0,0 +1,103 @@ +"""Support for KNX/IP sensors.""" +import voluptuous as vol + +from homeassistant.components.knx import ATTR_DISCOVER_DEVICES, DATA_KNX +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +CONF_ADDRESS = 'address' +CONF_TYPE = 'type' + +DEFAULT_NAME = 'KNX Sensor' +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_TYPE): cv.string, +}) + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up sensor(s) 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 sensors 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(KNXSensor(device)) + async_add_entities(entities) + + +@callback +def async_add_entities_config(hass, config, async_add_entities): + """Set up sensor for KNX platform configured within platform.""" + import xknx + sensor = xknx.devices.Sensor( + hass.data[DATA_KNX].xknx, + name=config.get(CONF_NAME), + group_address=config.get(CONF_ADDRESS), + value_type=config.get(CONF_TYPE)) + hass.data[DATA_KNX].xknx.devices.add(sensor) + async_add_entities([KNXSensor(sensor)]) + + +class KNXSensor(Entity): + """Representation of a KNX sensor.""" + + def __init__(self, device): + """Initialize of a KNX sensor.""" + 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 state(self): + """Return the state of the sensor.""" + return self.device.resolve_state() + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return self.device.unit_of_measurement() + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return None diff --git a/homeassistant/components/knx/services.yaml b/homeassistant/components/knx/services.yaml new file mode 100644 index 0000000000000..79b11c129af63 --- /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/knx/switch.py b/homeassistant/components/knx/switch.py new file mode 100644 index 0000000000000..305234e1eec8b --- /dev/null +++ b/homeassistant/components/knx/switch.py @@ -0,0 +1,100 @@ +"""Support for KNX/IP switches.""" +import voluptuous as vol + +from homeassistant.components.knx import ATTR_DISCOVER_DEVICES, DATA_KNX +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import CONF_NAME +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv + +CONF_ADDRESS = 'address' +CONF_STATE_ADDRESS = 'state_address' + +DEFAULT_NAME = 'KNX Switch' +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, +}) + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up switch(es) 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 switches 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(KNXSwitch(device)) + async_add_entities(entities) + + +@callback +def async_add_entities_config(hass, config, async_add_entities): + """Set up switch for KNX platform configured within platform.""" + import xknx + switch = xknx.devices.Switch( + hass.data[DATA_KNX].xknx, + name=config.get(CONF_NAME), + group_address=config.get(CONF_ADDRESS), + group_address_state=config.get(CONF_STATE_ADDRESS)) + hass.data[DATA_KNX].xknx.devices.add(switch) + async_add_entities([KNXSwitch(switch)]) + + +class KNXSwitch(SwitchDevice): + """Representation of a KNX switch.""" + + def __init__(self, device): + """Initialize of KNX switch.""" + 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): + """Return the polling state. Not needed within KNX.""" + return False + + @property + def is_on(self): + """Return true if device is on.""" + return self.device.state + + async def async_turn_on(self, **kwargs): + """Turn the device on.""" + await self.device.set_on() + + async def async_turn_off(self, **kwargs): + """Turn the device off.""" + await self.device.set_off() diff --git a/homeassistant/components/konnected.py b/homeassistant/components/konnected.py deleted file mode 100644 index 0cba855234640..0000000000000 --- a/homeassistant/components/konnected.py +++ /dev/null @@ -1,454 +0,0 @@ -""" -Support for Konnected devices. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/konnected/ -""" -import asyncio -import hmac -import json -import logging - -import voluptuous as vol - -from aiohttp.hdrs import AUTHORIZATION -from aiohttp.web import Request, Response - -from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA -from homeassistant.components.discovery import SERVICE_KONNECTED -from homeassistant.components.http import HomeAssistantView -from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, - HTTP_UNAUTHORIZED, CONF_DEVICES, CONF_BINARY_SENSORS, CONF_SWITCHES, - CONF_HOST, CONF_PORT, CONF_ID, CONF_NAME, CONF_TYPE, CONF_PIN, CONF_ZONE, - CONF_ACCESS_TOKEN, ATTR_ENTITY_ID, ATTR_STATE, STATE_ON) -from homeassistant.helpers.dispatcher import ( - async_dispatcher_send, dispatcher_send) -from homeassistant.helpers import discovery -from homeassistant.helpers import config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -REQUIREMENTS = ['konnected==0.1.4'] - -DOMAIN = 'konnected' - -CONF_ACTIVATION = 'activation' -CONF_API_HOST = 'api_host' -CONF_MOMENTARY = 'momentary' -CONF_PAUSE = 'pause' -CONF_REPEAT = 'repeat' -CONF_INVERSE = 'inverse' -CONF_BLINK = 'blink' -CONF_DISCOVERY = 'discovery' - -STATE_LOW = 'low' -STATE_HIGH = 'high' - -PIN_TO_ZONE = {1: 1, 2: 2, 5: 3, 6: 4, 7: 5, 8: 'out', 9: 6} -ZONE_TO_PIN = {zone: pin for pin, zone in PIN_TO_ZONE.items()} - -_BINARY_SENSOR_SCHEMA = vol.All( - vol.Schema({ - vol.Exclusive(CONF_PIN, 's_pin'): vol.Any(*PIN_TO_ZONE), - vol.Exclusive(CONF_ZONE, 's_pin'): vol.Any(*ZONE_TO_PIN), - vol.Required(CONF_TYPE): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_INVERSE, default=False): cv.boolean, - }), cv.has_at_least_one_key(CONF_PIN, CONF_ZONE) -) - -_SWITCH_SCHEMA = vol.All( - vol.Schema({ - vol.Exclusive(CONF_PIN, 'a_pin'): vol.Any(*PIN_TO_ZONE), - vol.Exclusive(CONF_ZONE, 'a_pin'): vol.Any(*ZONE_TO_PIN), - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_ACTIVATION, default=STATE_HIGH): - vol.All(vol.Lower, vol.Any(STATE_HIGH, STATE_LOW)), - vol.Optional(CONF_MOMENTARY): - vol.All(vol.Coerce(int), vol.Range(min=10)), - vol.Optional(CONF_PAUSE): - vol.All(vol.Coerce(int), vol.Range(min=10)), - vol.Optional(CONF_REPEAT): - vol.All(vol.Coerce(int), vol.Range(min=-1)), - }), cv.has_at_least_one_key(CONF_PIN, CONF_ZONE) -) - -# pylint: disable=no-value-for-parameter -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema({ - vol.Required(CONF_ACCESS_TOKEN): cv.string, - vol.Optional(CONF_API_HOST): vol.Url(), - vol.Required(CONF_DEVICES): [{ - vol.Required(CONF_ID): cv.matches_regex("[0-9a-f]{12}"), - vol.Optional(CONF_BINARY_SENSORS): vol.All( - cv.ensure_list, [_BINARY_SENSOR_SCHEMA]), - vol.Optional(CONF_SWITCHES): vol.All( - cv.ensure_list, [_SWITCH_SCHEMA]), - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_PORT): cv.port, - vol.Optional(CONF_BLINK, default=True): cv.boolean, - vol.Optional(CONF_DISCOVERY, default=True): cv.boolean, - }], - }), - }, - extra=vol.ALLOW_EXTRA, -) - -DEPENDENCIES = ['http'] - -ENDPOINT_ROOT = '/api/konnected' -UPDATE_ENDPOINT = (ENDPOINT_ROOT + r'/device/{device_id:[a-zA-Z0-9]+}') -SIGNAL_SENSOR_UPDATE = 'konnected.{}.update' - - -async def async_setup(hass, config): - """Set up the Konnected platform.""" - import konnected - - cfg = config.get(DOMAIN) - if cfg is None: - cfg = {} - - access_token = cfg.get(CONF_ACCESS_TOKEN) - if DOMAIN not in hass.data: - hass.data[DOMAIN] = { - CONF_ACCESS_TOKEN: access_token, - CONF_API_HOST: cfg.get(CONF_API_HOST) - } - - def setup_device(host, port): - """Set up a Konnected device at `host` listening on `port`.""" - discovered = DiscoveredDevice(hass, host, port) - if discovered.is_configured: - discovered.setup() - else: - _LOGGER.warning("Konnected device %s was discovered on the network" - " but not specified in configuration.yaml", - discovered.device_id) - - def device_discovered(service, info): - """Call when a Konnected device has been discovered.""" - host = info.get(CONF_HOST) - port = info.get(CONF_PORT) - setup_device(host, port) - - async def manual_discovery(event): - """Init devices on the network with manually assigned addresses.""" - specified = [dev for dev in cfg.get(CONF_DEVICES) if - dev.get(CONF_HOST) and dev.get(CONF_PORT)] - - while specified: - for dev in specified: - _LOGGER.debug("Discovering Konnected device %s at %s:%s", - dev.get(CONF_ID), - dev.get(CONF_HOST), - dev.get(CONF_PORT)) - try: - await hass.async_add_executor_job(setup_device, - dev.get(CONF_HOST), - dev.get(CONF_PORT)) - specified.remove(dev) - except konnected.Client.ClientError as err: - _LOGGER.error(err) - await asyncio.sleep(10) # try again in 10 seconds - - # Initialize devices specified in the configuration on boot - for device in cfg.get(CONF_DEVICES): - ConfiguredDevice(hass, device, config).save_data() - - discovery.async_listen( - hass, - SERVICE_KONNECTED, - device_discovered) - - hass.http.register_view(KonnectedView(access_token)) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, manual_discovery) - - return True - - -class ConfiguredDevice: - """A representation of a configured Konnected device.""" - - def __init__(self, hass, config, hass_config): - """Initialize the Konnected device.""" - self.hass = hass - self.config = config - self.hass_config = hass_config - - @property - def device_id(self): - """Device id is the MAC address as string with punctuation removed.""" - return self.config.get(CONF_ID) - - def save_data(self): - """Save the device configuration to `hass.data`.""" - sensors = {} - for entity in self.config.get(CONF_BINARY_SENSORS) or []: - if CONF_ZONE in entity: - pin = ZONE_TO_PIN[entity[CONF_ZONE]] - else: - pin = entity[CONF_PIN] - - sensors[pin] = { - CONF_TYPE: entity[CONF_TYPE], - CONF_NAME: entity.get(CONF_NAME, 'Konnected {} Zone {}'.format( - self.device_id[6:], PIN_TO_ZONE[pin])), - CONF_INVERSE: entity.get(CONF_INVERSE), - ATTR_STATE: None - } - _LOGGER.debug('Set up sensor %s (initial state: %s)', - sensors[pin].get('name'), - sensors[pin].get(ATTR_STATE)) - - actuators = [] - for entity in self.config.get(CONF_SWITCHES) or []: - if 'zone' in entity: - pin = ZONE_TO_PIN[entity['zone']] - else: - pin = entity['pin'] - - act = { - CONF_PIN: pin, - CONF_NAME: entity.get( - CONF_NAME, 'Konnected {} Actuator {}'.format( - self.device_id[6:], PIN_TO_ZONE[pin])), - ATTR_STATE: None, - CONF_ACTIVATION: entity[CONF_ACTIVATION], - CONF_MOMENTARY: entity.get(CONF_MOMENTARY), - CONF_PAUSE: entity.get(CONF_PAUSE), - CONF_REPEAT: entity.get(CONF_REPEAT)} - actuators.append(act) - _LOGGER.debug('Set up actuator %s', act) - - device_data = { - CONF_BINARY_SENSORS: sensors, - CONF_SWITCHES: actuators, - CONF_BLINK: self.config.get(CONF_BLINK), - CONF_DISCOVERY: self.config.get(CONF_DISCOVERY) - } - - if CONF_DEVICES not in self.hass.data[DOMAIN]: - self.hass.data[DOMAIN][CONF_DEVICES] = {} - - _LOGGER.debug('Storing data in hass.data[%s][%s][%s]: %s', - DOMAIN, CONF_DEVICES, self.device_id, device_data) - self.hass.data[DOMAIN][CONF_DEVICES][self.device_id] = device_data - - discovery.load_platform( - self.hass, 'binary_sensor', DOMAIN, - {'device_id': self.device_id}, self.hass_config) - discovery.load_platform( - self.hass, 'switch', DOMAIN, - {'device_id': self.device_id}, self.hass_config) - - -class DiscoveredDevice: - """A representation of a discovered Konnected device.""" - - def __init__(self, hass, host, port): - """Initialize the Konnected device.""" - self.hass = hass - self.host = host - self.port = port - - import konnected - self.client = konnected.Client(host, str(port)) - self.status = self.client.get_status() - - def setup(self): - """Set up a newly discovered Konnected device.""" - _LOGGER.info('Discovered Konnected device %s. Open http://%s:%s in a ' - 'web browser to view device status.', - self.device_id, self.host, self.port) - self.save_data() - self.update_initial_states() - self.sync_device_config() - - def save_data(self): - """Save the discovery information to `hass.data`.""" - self.stored_configuration['client'] = self.client - self.stored_configuration['host'] = self.host - self.stored_configuration['port'] = self.port - - @property - def device_id(self): - """Device id is the MAC address as string with punctuation removed.""" - return self.status['mac'].replace(':', '') - - @property - def is_configured(self): - """Return true if device_id is specified in the configuration.""" - return bool(self.hass.data[DOMAIN][CONF_DEVICES].get(self.device_id)) - - @property - def stored_configuration(self): - """Return the configuration stored in `hass.data` for this device.""" - return self.hass.data[DOMAIN][CONF_DEVICES].get(self.device_id) - - def sensor_configuration(self): - """Return the configuration map for syncing sensors.""" - return [{'pin': p} for p in - self.stored_configuration[CONF_BINARY_SENSORS]] - - def actuator_configuration(self): - """Return the configuration map for syncing actuators.""" - return [{'pin': data.get(CONF_PIN), - 'trigger': (0 if data.get(CONF_ACTIVATION) in [0, STATE_LOW] - else 1)} - for data in self.stored_configuration[CONF_SWITCHES]] - - def update_initial_states(self): - """Update the initial state of each sensor from status poll.""" - for sensor_data in self.status.get('sensors'): - sensor_config = self.stored_configuration[CONF_BINARY_SENSORS]. \ - get(sensor_data.get(CONF_PIN), {}) - entity_id = sensor_config.get(ATTR_ENTITY_ID) - - state = bool(sensor_data.get(ATTR_STATE)) - if sensor_config.get(CONF_INVERSE): - state = not state - - dispatcher_send( - self.hass, - SIGNAL_SENSOR_UPDATE.format(entity_id), - state) - - def sync_device_config(self): - """Sync the new pin configuration to the Konnected device.""" - desired_sensor_configuration = self.sensor_configuration() - current_sensor_configuration = [ - {'pin': s[CONF_PIN]} for s in self.status.get('sensors')] - _LOGGER.debug('%s: desired sensor config: %s', self.device_id, - desired_sensor_configuration) - _LOGGER.debug('%s: current sensor config: %s', self.device_id, - current_sensor_configuration) - - desired_actuator_config = self.actuator_configuration() - current_actuator_config = self.status.get('actuators') - _LOGGER.debug('%s: desired actuator config: %s', self.device_id, - desired_actuator_config) - _LOGGER.debug('%s: current actuator config: %s', self.device_id, - current_actuator_config) - - desired_api_host = \ - self.hass.data[DOMAIN].get(CONF_API_HOST) or \ - self.hass.config.api.base_url - desired_api_endpoint = desired_api_host + ENDPOINT_ROOT - current_api_endpoint = self.status.get('endpoint') - - _LOGGER.debug('%s: desired api endpoint: %s', self.device_id, - desired_api_endpoint) - _LOGGER.debug('%s: current api endpoint: %s', self.device_id, - current_api_endpoint) - - if (desired_sensor_configuration != current_sensor_configuration) or \ - (current_actuator_config != desired_actuator_config) or \ - (current_api_endpoint != desired_api_endpoint) or \ - (self.status.get(CONF_BLINK) != - self.stored_configuration.get(CONF_BLINK)) or \ - (self.status.get(CONF_DISCOVERY) != - self.stored_configuration.get(CONF_DISCOVERY)): - _LOGGER.info('pushing settings to device %s', self.device_id) - self.client.put_settings( - desired_sensor_configuration, - desired_actuator_config, - self.hass.data[DOMAIN].get(CONF_ACCESS_TOKEN), - desired_api_endpoint, - blink=self.stored_configuration.get(CONF_BLINK), - discovery=self.stored_configuration.get(CONF_DISCOVERY) - ) - - -class KonnectedView(HomeAssistantView): - """View creates an endpoint to receive push updates from the device.""" - - url = UPDATE_ENDPOINT - extra_urls = [UPDATE_ENDPOINT + '/{pin_num}/{state}'] - name = 'api:konnected' - requires_auth = False # Uses access token from configuration - - def __init__(self, auth_token): - """Initialize the view.""" - self.auth_token = auth_token - - @staticmethod - def binary_value(state, activation): - """Return binary value for GPIO based on state and activation.""" - if activation == STATE_HIGH: - return 1 if state == STATE_ON else 0 - return 0 if state == STATE_ON else 1 - - async def get(self, request: Request, device_id) -> Response: - """Return the current binary state of a switch.""" - hass = request.app['hass'] - pin_num = int(request.query.get('pin')) - data = hass.data[DOMAIN] - - device = data[CONF_DEVICES][device_id] - if not device: - return self.json_message( - 'Device ' + device_id + ' not configured', - status_code=HTTP_NOT_FOUND) - - try: - pin = next(filter( - lambda switch: switch[CONF_PIN] == pin_num, - device[CONF_SWITCHES])) - except StopIteration: - pin = None - - if not pin: - return self.json_message( - 'Switch on pin ' + pin_num + ' not configured', - status_code=HTTP_NOT_FOUND) - - return self.json( - {'pin': pin_num, - 'state': self.binary_value( - hass.states.get(pin[ATTR_ENTITY_ID]).state, - pin[CONF_ACTIVATION])}) - - async def put(self, request: Request, device_id, - pin_num=None, state=None) -> Response: - """Receive a sensor update via PUT request and async set state.""" - hass = request.app['hass'] - data = hass.data[DOMAIN] - - try: # Konnected 2.2.0 and above supports JSON payloads - payload = await request.json() - pin_num = payload['pin'] - state = payload['state'] - except json.decoder.JSONDecodeError: - _LOGGER.warning(("Your Konnected device software may be out of " - "date. Visit https://help.konnected.io for " - "updating instructions.")) - - auth = request.headers.get(AUTHORIZATION, None) - if not hmac.compare_digest('Bearer {}'.format(self.auth_token), auth): - return self.json_message( - "unauthorized", status_code=HTTP_UNAUTHORIZED) - pin_num = int(pin_num) - device = data[CONF_DEVICES].get(device_id) - if device is None: - return self.json_message('unregistered device', - status_code=HTTP_BAD_REQUEST) - pin_data = device[CONF_BINARY_SENSORS].get(pin_num) - - if pin_data is None: - return self.json_message('unregistered sensor/actuator', - status_code=HTTP_BAD_REQUEST) - - entity_id = pin_data.get(ATTR_ENTITY_ID) - if entity_id is None: - return self.json_message('uninitialized sensor/actuator', - status_code=HTTP_NOT_FOUND) - state = bool(int(state)) - if pin_data.get(CONF_INVERSE): - state = not state - - async_dispatcher_send( - hass, SIGNAL_SENSOR_UPDATE.format(entity_id), state) - return self.json_message('ok') diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py new file mode 100644 index 0000000000000..e3f9a46743d67 --- /dev/null +++ b/homeassistant/components/konnected/__init__.py @@ -0,0 +1,449 @@ +"""Support for Konnected devices.""" +import asyncio +import hmac +import json +import logging + +import voluptuous as vol + +from aiohttp.hdrs import AUTHORIZATION +from aiohttp.web import Request, Response + +from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA +from homeassistant.components.discovery import SERVICE_KONNECTED +from homeassistant.components.http import HomeAssistantView +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, + HTTP_UNAUTHORIZED, CONF_DEVICES, CONF_BINARY_SENSORS, CONF_SWITCHES, + CONF_HOST, CONF_PORT, CONF_ID, CONF_NAME, CONF_TYPE, CONF_PIN, CONF_ZONE, + CONF_ACCESS_TOKEN, ATTR_ENTITY_ID, ATTR_STATE, STATE_ON) +from homeassistant.helpers.dispatcher import ( + async_dispatcher_send, dispatcher_send) +from homeassistant.helpers import discovery +from homeassistant.helpers import config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['konnected==0.1.4'] + +DOMAIN = 'konnected' + +CONF_ACTIVATION = 'activation' +CONF_API_HOST = 'api_host' +CONF_MOMENTARY = 'momentary' +CONF_PAUSE = 'pause' +CONF_REPEAT = 'repeat' +CONF_INVERSE = 'inverse' +CONF_BLINK = 'blink' +CONF_DISCOVERY = 'discovery' + +STATE_LOW = 'low' +STATE_HIGH = 'high' + +PIN_TO_ZONE = {1: 1, 2: 2, 5: 3, 6: 4, 7: 5, 8: 'out', 9: 6} +ZONE_TO_PIN = {zone: pin for pin, zone in PIN_TO_ZONE.items()} + +_BINARY_SENSOR_SCHEMA = vol.All( + vol.Schema({ + vol.Exclusive(CONF_PIN, 's_pin'): vol.Any(*PIN_TO_ZONE), + vol.Exclusive(CONF_ZONE, 's_pin'): vol.Any(*ZONE_TO_PIN), + vol.Required(CONF_TYPE): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_INVERSE, default=False): cv.boolean, + }), cv.has_at_least_one_key(CONF_PIN, CONF_ZONE) +) + +_SWITCH_SCHEMA = vol.All( + vol.Schema({ + vol.Exclusive(CONF_PIN, 'a_pin'): vol.Any(*PIN_TO_ZONE), + vol.Exclusive(CONF_ZONE, 'a_pin'): vol.Any(*ZONE_TO_PIN), + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_ACTIVATION, default=STATE_HIGH): + vol.All(vol.Lower, vol.Any(STATE_HIGH, STATE_LOW)), + vol.Optional(CONF_MOMENTARY): + vol.All(vol.Coerce(int), vol.Range(min=10)), + vol.Optional(CONF_PAUSE): + vol.All(vol.Coerce(int), vol.Range(min=10)), + vol.Optional(CONF_REPEAT): + vol.All(vol.Coerce(int), vol.Range(min=-1)), + }), cv.has_at_least_one_key(CONF_PIN, CONF_ZONE) +) + +# pylint: disable=no-value-for-parameter +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema({ + vol.Required(CONF_ACCESS_TOKEN): cv.string, + vol.Optional(CONF_API_HOST): vol.Url(), + vol.Required(CONF_DEVICES): [{ + vol.Required(CONF_ID): cv.matches_regex("[0-9a-f]{12}"), + vol.Optional(CONF_BINARY_SENSORS): vol.All( + cv.ensure_list, [_BINARY_SENSOR_SCHEMA]), + vol.Optional(CONF_SWITCHES): vol.All( + cv.ensure_list, [_SWITCH_SCHEMA]), + vol.Optional(CONF_HOST): cv.string, + vol.Optional(CONF_PORT): cv.port, + vol.Optional(CONF_BLINK, default=True): cv.boolean, + vol.Optional(CONF_DISCOVERY, default=True): cv.boolean, + }], + }), + }, + extra=vol.ALLOW_EXTRA, +) + +DEPENDENCIES = ['http'] + +ENDPOINT_ROOT = '/api/konnected' +UPDATE_ENDPOINT = (ENDPOINT_ROOT + r'/device/{device_id:[a-zA-Z0-9]+}') +SIGNAL_SENSOR_UPDATE = 'konnected.{}.update' + + +async def async_setup(hass, config): + """Set up the Konnected platform.""" + import konnected + + cfg = config.get(DOMAIN) + if cfg is None: + cfg = {} + + access_token = cfg.get(CONF_ACCESS_TOKEN) + if DOMAIN not in hass.data: + hass.data[DOMAIN] = { + CONF_ACCESS_TOKEN: access_token, + CONF_API_HOST: cfg.get(CONF_API_HOST) + } + + def setup_device(host, port): + """Set up a Konnected device at `host` listening on `port`.""" + discovered = DiscoveredDevice(hass, host, port) + if discovered.is_configured: + discovered.setup() + else: + _LOGGER.warning("Konnected device %s was discovered on the network" + " but not specified in configuration.yaml", + discovered.device_id) + + def device_discovered(service, info): + """Call when a Konnected device has been discovered.""" + host = info.get(CONF_HOST) + port = info.get(CONF_PORT) + setup_device(host, port) + + async def manual_discovery(event): + """Init devices on the network with manually assigned addresses.""" + specified = [dev for dev in cfg.get(CONF_DEVICES) if + dev.get(CONF_HOST) and dev.get(CONF_PORT)] + + while specified: + for dev in specified: + _LOGGER.debug("Discovering Konnected device %s at %s:%s", + dev.get(CONF_ID), + dev.get(CONF_HOST), + dev.get(CONF_PORT)) + try: + await hass.async_add_executor_job(setup_device, + dev.get(CONF_HOST), + dev.get(CONF_PORT)) + specified.remove(dev) + except konnected.Client.ClientError as err: + _LOGGER.error(err) + await asyncio.sleep(10) # try again in 10 seconds + + # Initialize devices specified in the configuration on boot + for device in cfg.get(CONF_DEVICES): + ConfiguredDevice(hass, device, config).save_data() + + discovery.async_listen( + hass, + SERVICE_KONNECTED, + device_discovered) + + hass.http.register_view(KonnectedView(access_token)) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, manual_discovery) + + return True + + +class ConfiguredDevice: + """A representation of a configured Konnected device.""" + + def __init__(self, hass, config, hass_config): + """Initialize the Konnected device.""" + self.hass = hass + self.config = config + self.hass_config = hass_config + + @property + def device_id(self): + """Device id is the MAC address as string with punctuation removed.""" + return self.config.get(CONF_ID) + + def save_data(self): + """Save the device configuration to `hass.data`.""" + sensors = {} + for entity in self.config.get(CONF_BINARY_SENSORS) or []: + if CONF_ZONE in entity: + pin = ZONE_TO_PIN[entity[CONF_ZONE]] + else: + pin = entity[CONF_PIN] + + sensors[pin] = { + CONF_TYPE: entity[CONF_TYPE], + CONF_NAME: entity.get(CONF_NAME, 'Konnected {} Zone {}'.format( + self.device_id[6:], PIN_TO_ZONE[pin])), + CONF_INVERSE: entity.get(CONF_INVERSE), + ATTR_STATE: None + } + _LOGGER.debug('Set up sensor %s (initial state: %s)', + sensors[pin].get('name'), + sensors[pin].get(ATTR_STATE)) + + actuators = [] + for entity in self.config.get(CONF_SWITCHES) or []: + if 'zone' in entity: + pin = ZONE_TO_PIN[entity['zone']] + else: + pin = entity['pin'] + + act = { + CONF_PIN: pin, + CONF_NAME: entity.get( + CONF_NAME, 'Konnected {} Actuator {}'.format( + self.device_id[6:], PIN_TO_ZONE[pin])), + ATTR_STATE: None, + CONF_ACTIVATION: entity[CONF_ACTIVATION], + CONF_MOMENTARY: entity.get(CONF_MOMENTARY), + CONF_PAUSE: entity.get(CONF_PAUSE), + CONF_REPEAT: entity.get(CONF_REPEAT)} + actuators.append(act) + _LOGGER.debug('Set up actuator %s', act) + + device_data = { + CONF_BINARY_SENSORS: sensors, + CONF_SWITCHES: actuators, + CONF_BLINK: self.config.get(CONF_BLINK), + CONF_DISCOVERY: self.config.get(CONF_DISCOVERY) + } + + if CONF_DEVICES not in self.hass.data[DOMAIN]: + self.hass.data[DOMAIN][CONF_DEVICES] = {} + + _LOGGER.debug('Storing data in hass.data[%s][%s][%s]: %s', + DOMAIN, CONF_DEVICES, self.device_id, device_data) + self.hass.data[DOMAIN][CONF_DEVICES][self.device_id] = device_data + + discovery.load_platform( + self.hass, 'binary_sensor', DOMAIN, + {'device_id': self.device_id}, self.hass_config) + discovery.load_platform( + self.hass, 'switch', DOMAIN, + {'device_id': self.device_id}, self.hass_config) + + +class DiscoveredDevice: + """A representation of a discovered Konnected device.""" + + def __init__(self, hass, host, port): + """Initialize the Konnected device.""" + self.hass = hass + self.host = host + self.port = port + + import konnected + self.client = konnected.Client(host, str(port)) + self.status = self.client.get_status() + + def setup(self): + """Set up a newly discovered Konnected device.""" + _LOGGER.info('Discovered Konnected device %s. Open http://%s:%s in a ' + 'web browser to view device status.', + self.device_id, self.host, self.port) + self.save_data() + self.update_initial_states() + self.sync_device_config() + + def save_data(self): + """Save the discovery information to `hass.data`.""" + self.stored_configuration['client'] = self.client + self.stored_configuration['host'] = self.host + self.stored_configuration['port'] = self.port + + @property + def device_id(self): + """Device id is the MAC address as string with punctuation removed.""" + return self.status['mac'].replace(':', '') + + @property + def is_configured(self): + """Return true if device_id is specified in the configuration.""" + return bool(self.hass.data[DOMAIN][CONF_DEVICES].get(self.device_id)) + + @property + def stored_configuration(self): + """Return the configuration stored in `hass.data` for this device.""" + return self.hass.data[DOMAIN][CONF_DEVICES].get(self.device_id) + + def sensor_configuration(self): + """Return the configuration map for syncing sensors.""" + return [{'pin': p} for p in + self.stored_configuration[CONF_BINARY_SENSORS]] + + def actuator_configuration(self): + """Return the configuration map for syncing actuators.""" + return [{'pin': data.get(CONF_PIN), + 'trigger': (0 if data.get(CONF_ACTIVATION) in [0, STATE_LOW] + else 1)} + for data in self.stored_configuration[CONF_SWITCHES]] + + def update_initial_states(self): + """Update the initial state of each sensor from status poll.""" + for sensor_data in self.status.get('sensors'): + sensor_config = self.stored_configuration[CONF_BINARY_SENSORS]. \ + get(sensor_data.get(CONF_PIN), {}) + entity_id = sensor_config.get(ATTR_ENTITY_ID) + + state = bool(sensor_data.get(ATTR_STATE)) + if sensor_config.get(CONF_INVERSE): + state = not state + + dispatcher_send( + self.hass, + SIGNAL_SENSOR_UPDATE.format(entity_id), + state) + + def sync_device_config(self): + """Sync the new pin configuration to the Konnected device.""" + desired_sensor_configuration = self.sensor_configuration() + current_sensor_configuration = [ + {'pin': s[CONF_PIN]} for s in self.status.get('sensors')] + _LOGGER.debug('%s: desired sensor config: %s', self.device_id, + desired_sensor_configuration) + _LOGGER.debug('%s: current sensor config: %s', self.device_id, + current_sensor_configuration) + + desired_actuator_config = self.actuator_configuration() + current_actuator_config = self.status.get('actuators') + _LOGGER.debug('%s: desired actuator config: %s', self.device_id, + desired_actuator_config) + _LOGGER.debug('%s: current actuator config: %s', self.device_id, + current_actuator_config) + + desired_api_host = \ + self.hass.data[DOMAIN].get(CONF_API_HOST) or \ + self.hass.config.api.base_url + desired_api_endpoint = desired_api_host + ENDPOINT_ROOT + current_api_endpoint = self.status.get('endpoint') + + _LOGGER.debug('%s: desired api endpoint: %s', self.device_id, + desired_api_endpoint) + _LOGGER.debug('%s: current api endpoint: %s', self.device_id, + current_api_endpoint) + + if (desired_sensor_configuration != current_sensor_configuration) or \ + (current_actuator_config != desired_actuator_config) or \ + (current_api_endpoint != desired_api_endpoint) or \ + (self.status.get(CONF_BLINK) != + self.stored_configuration.get(CONF_BLINK)) or \ + (self.status.get(CONF_DISCOVERY) != + self.stored_configuration.get(CONF_DISCOVERY)): + _LOGGER.info('pushing settings to device %s', self.device_id) + self.client.put_settings( + desired_sensor_configuration, + desired_actuator_config, + self.hass.data[DOMAIN].get(CONF_ACCESS_TOKEN), + desired_api_endpoint, + blink=self.stored_configuration.get(CONF_BLINK), + discovery=self.stored_configuration.get(CONF_DISCOVERY) + ) + + +class KonnectedView(HomeAssistantView): + """View creates an endpoint to receive push updates from the device.""" + + url = UPDATE_ENDPOINT + extra_urls = [UPDATE_ENDPOINT + '/{pin_num}/{state}'] + name = 'api:konnected' + requires_auth = False # Uses access token from configuration + + def __init__(self, auth_token): + """Initialize the view.""" + self.auth_token = auth_token + + @staticmethod + def binary_value(state, activation): + """Return binary value for GPIO based on state and activation.""" + if activation == STATE_HIGH: + return 1 if state == STATE_ON else 0 + return 0 if state == STATE_ON else 1 + + async def get(self, request: Request, device_id) -> Response: + """Return the current binary state of a switch.""" + hass = request.app['hass'] + pin_num = int(request.query.get('pin')) + data = hass.data[DOMAIN] + + device = data[CONF_DEVICES][device_id] + if not device: + return self.json_message( + 'Device ' + device_id + ' not configured', + status_code=HTTP_NOT_FOUND) + + try: + pin = next(filter( + lambda switch: switch[CONF_PIN] == pin_num, + device[CONF_SWITCHES])) + except StopIteration: + pin = None + + if not pin: + return self.json_message( + 'Switch on pin ' + pin_num + ' not configured', + status_code=HTTP_NOT_FOUND) + + return self.json( + {'pin': pin_num, + 'state': self.binary_value( + hass.states.get(pin[ATTR_ENTITY_ID]).state, + pin[CONF_ACTIVATION])}) + + async def put(self, request: Request, device_id, + pin_num=None, state=None) -> Response: + """Receive a sensor update via PUT request and async set state.""" + hass = request.app['hass'] + data = hass.data[DOMAIN] + + try: # Konnected 2.2.0 and above supports JSON payloads + payload = await request.json() + pin_num = payload['pin'] + state = payload['state'] + except json.decoder.JSONDecodeError: + _LOGGER.warning(("Your Konnected device software may be out of " + "date. Visit https://help.konnected.io for " + "updating instructions.")) + + auth = request.headers.get(AUTHORIZATION, None) + if not hmac.compare_digest('Bearer {}'.format(self.auth_token), auth): + return self.json_message( + "unauthorized", status_code=HTTP_UNAUTHORIZED) + pin_num = int(pin_num) + device = data[CONF_DEVICES].get(device_id) + if device is None: + return self.json_message('unregistered device', + status_code=HTTP_BAD_REQUEST) + pin_data = device[CONF_BINARY_SENSORS].get(pin_num) + + if pin_data is None: + return self.json_message('unregistered sensor/actuator', + status_code=HTTP_BAD_REQUEST) + + entity_id = pin_data.get(ATTR_ENTITY_ID) + if entity_id is None: + return self.json_message('uninitialized sensor/actuator', + status_code=HTTP_NOT_FOUND) + state = bool(int(state)) + if pin_data.get(CONF_INVERSE): + state = not state + + async_dispatcher_send( + hass, SIGNAL_SENSOR_UPDATE.format(entity_id), state) + return self.json_message('ok') diff --git a/homeassistant/components/konnected/binary_sensor.py b/homeassistant/components/konnected/binary_sensor.py new file mode 100644 index 0000000000000..cb15e44e7985e --- /dev/null +++ b/homeassistant/components/konnected/binary_sensor.py @@ -0,0 +1,77 @@ +"""Support for wired binary sensors attached to a Konnected device.""" +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.konnected import ( + DOMAIN as KONNECTED_DOMAIN, PIN_TO_ZONE, SIGNAL_SENSOR_UPDATE) +from homeassistant.const import ( + CONF_DEVICES, CONF_TYPE, CONF_NAME, CONF_BINARY_SENSORS, ATTR_ENTITY_ID, + ATTR_STATE) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['konnected'] + + +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 + + data = hass.data[KONNECTED_DOMAIN] + device_id = discovery_info['device_id'] + sensors = [KonnectedBinarySensor(device_id, pin_num, pin_data) + for pin_num, pin_data in + data[CONF_DEVICES][device_id][CONF_BINARY_SENSORS].items()] + async_add_entities(sensors) + + +class KonnectedBinarySensor(BinarySensorDevice): + """Representation of a Konnected binary sensor.""" + + def __init__(self, device_id, pin_num, data): + """Initialize the Konnected binary sensor.""" + self._data = data + self._device_id = device_id + self._pin_num = pin_num + self._state = self._data.get(ATTR_STATE) + 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) + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def is_on(self): + """Return the state of the sensor.""" + return self._state + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def device_class(self): + """Return the device class.""" + return self._device_class + + async def async_added_to_hass(self): + """Store entity_id and register state change callback.""" + self._data[ATTR_ENTITY_ID] = self.entity_id + async_dispatcher_connect( + self.hass, SIGNAL_SENSOR_UPDATE.format(self.entity_id), + self.async_set_state) + + @callback + def async_set_state(self, state): + """Update the sensor's state.""" + self._state = state + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/konnected/switch.py b/homeassistant/components/konnected/switch.py new file mode 100644 index 0000000000000..897933e6d800f --- /dev/null +++ b/homeassistant/components/konnected/switch.py @@ -0,0 +1,106 @@ +"""Support for wired switches attached to a Konnected device.""" +import logging + +from homeassistant.components.konnected import ( + DOMAIN as KONNECTED_DOMAIN, PIN_TO_ZONE, CONF_ACTIVATION, CONF_MOMENTARY, + CONF_PAUSE, CONF_REPEAT, STATE_LOW, STATE_HIGH) +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.const import ( + CONF_DEVICES, CONF_SWITCHES, CONF_PIN, ATTR_STATE) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['konnected'] + + +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 + + data = hass.data[KONNECTED_DOMAIN] + device_id = discovery_info['device_id'] + switches = [ + KonnectedSwitch(device_id, pin_data.get(CONF_PIN), pin_data) + for pin_data in data[CONF_DEVICES][device_id][CONF_SWITCHES]] + async_add_entities(switches) + + +class KonnectedSwitch(ToggleEntity): + """Representation of a Konnected switch.""" + + def __init__(self, device_id, pin_num, data): + """Initialize the Konnected switch.""" + self._data = data + self._device_id = device_id + self._pin_num = pin_num + self._activation = self._data.get(CONF_ACTIVATION, STATE_HIGH) + self._momentary = self._data.get(CONF_MOMENTARY) + self._pause = self._data.get(CONF_PAUSE) + self._repeat = self._data.get(CONF_REPEAT) + self._state = self._boolean_state(self._data.get(ATTR_STATE)) + self._name = self._data.get( + 'name', 'Konnected {} Actuator {}'.format( + device_id, PIN_TO_ZONE[pin_num])) + _LOGGER.debug("Created new switch: %s", self._name) + + @property + def name(self): + """Return the name of the switch.""" + return self._name + + @property + def is_on(self): + """Return the status of the sensor.""" + return self._state + + @property + def client(self): + """Return the Konnected HTTP client.""" + return \ + self.hass.data[KONNECTED_DOMAIN][CONF_DEVICES][self._device_id].\ + get('client') + + def turn_on(self, **kwargs): + """Send a command to turn on the switch.""" + resp = self.client.put_device( + self._pin_num, + int(self._activation == STATE_HIGH), + self._momentary, + self._repeat, + self._pause + ) + + if resp.get(ATTR_STATE) is not None: + self._set_state(True) + + if self._momentary and resp.get(ATTR_STATE) != -1: + # Immediately set the state back off for momentary switches + self._set_state(False) + + def turn_off(self, **kwargs): + """Send a command to turn off the switch.""" + resp = self.client.put_device( + self._pin_num, int(self._activation == STATE_LOW)) + + if resp.get(ATTR_STATE) is not None: + self._set_state(self._boolean_state(resp.get(ATTR_STATE))) + + def _boolean_state(self, int_state): + if int_state is None: + return False + if int_state == 0: + return self._activation == STATE_LOW + if int_state == 1: + return self._activation == STATE_HIGH + + def _set_state(self, state): + self._state = state + self.schedule_update_ha_state() + _LOGGER.debug('Setting status of %s actuator pin %s to %s', + self._device_id, self.name, state) + + async def async_added_to_hass(self): + """Store entity_id.""" + self._data['entity_id'] = self.entity_id diff --git a/homeassistant/components/lametric.py b/homeassistant/components/lametric.py deleted file mode 100644 index 96ea3781566cd..0000000000000 --- a/homeassistant/components/lametric.py +++ /dev/null @@ -1,62 +0,0 @@ -""" -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/ -""" -import logging - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv - -REQUIREMENTS = ['lmnotify==0.0.4'] - -_LOGGER = logging.getLogger(__name__) - -CONF_CLIENT_ID = 'client_id' -CONF_CLIENT_SECRET = 'client_secret' - -DOMAIN = 'lametric' -LAMETRIC_DEVICES = 'LAMETRIC_DEVICES' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_CLIENT_SECRET): cv.string, - }), -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the LaMetricManager.""" - _LOGGER.debug("Setting up LaMetric platform") - conf = config[DOMAIN] - hlmn = HassLaMetricManager(client_id=conf[CONF_CLIENT_ID], - client_secret=conf[CONF_CLIENT_SECRET]) - devices = hlmn.manager.get_devices() - if not devices: - _LOGGER.error("No LaMetric devices found") - return False - - hass.data[DOMAIN] = hlmn - for dev in devices: - _LOGGER.debug("Discovered LaMetric device: %s", dev) - - return True - - -class HassLaMetricManager(): - """A class that encapsulated requests to the LaMetric manager.""" - - def __init__(self, client_id, client_secret): - """Initialize HassLaMetricManager and connect to LaMetric.""" - from lmnotify import LaMetricManager - - _LOGGER.debug("Connecting to LaMetric") - self.manager = LaMetricManager(client_id, client_secret) - self._client_id = client_id - self._client_secret = client_secret diff --git a/homeassistant/components/lametric/__init__.py b/homeassistant/components/lametric/__init__.py new file mode 100644 index 0000000000000..0c3c8b08dd732 --- /dev/null +++ b/homeassistant/components/lametric/__init__.py @@ -0,0 +1,54 @@ +"""Support for LaMetric time.""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['lmnotify==0.0.4'] + +_LOGGER = logging.getLogger(__name__) + +CONF_CLIENT_ID = 'client_id' +CONF_CLIENT_SECRET = 'client_secret' + +DOMAIN = 'lametric' +LAMETRIC_DEVICES = 'LAMETRIC_DEVICES' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the LaMetricManager.""" + _LOGGER.debug("Setting up LaMetric platform") + conf = config[DOMAIN] + hlmn = HassLaMetricManager(client_id=conf[CONF_CLIENT_ID], + client_secret=conf[CONF_CLIENT_SECRET]) + devices = hlmn.manager.get_devices() + if not devices: + _LOGGER.error("No LaMetric devices found") + return False + + hass.data[DOMAIN] = hlmn + for dev in devices: + _LOGGER.debug("Discovered LaMetric device: %s", dev) + + return True + + +class HassLaMetricManager(): + """A class that encapsulated requests to the LaMetric manager.""" + + def __init__(self, client_id, client_secret): + """Initialize HassLaMetricManager and connect to LaMetric.""" + from lmnotify import LaMetricManager + + _LOGGER.debug("Connecting to LaMetric") + self.manager = LaMetricManager(client_id, client_secret) + self._client_id = client_id + self._client_secret = client_secret diff --git a/homeassistant/components/lametric/notify.py b/homeassistant/components/lametric/notify.py new file mode 100644 index 0000000000000..e5e6a5bd52296 --- /dev/null +++ b/homeassistant/components/lametric/notify.py @@ -0,0 +1,117 @@ +"""Support for LaMetric notifications.""" +import logging + +from requests.exceptions import ConnectionError as RequestsConnectionError +import voluptuous as vol + +from homeassistant.components.notify import ( + ATTR_TARGET, ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService) +from homeassistant.const import CONF_ICON +import homeassistant.helpers.config_validation as cv + +from homeassistant.components.lametric import DOMAIN as LAMETRIC_DOMAIN + +REQUIREMENTS = ['lmnotify==0.0.4'] + +_LOGGER = logging.getLogger(__name__) + +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_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), +}) + + +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]) + + +class LaMetricNotificationService(BaseNotificationService): + """Implement the notification service for LaMetric.""" + + def __init__(self, hasslametricmanager, icon, lifetime, cycles, priority): + """Initialize the service.""" + self.hasslametricmanager = hasslametricmanager + self._icon = icon + self._lifetime = lifetime + self._cycles = cycles + self._priority = priority + self._devices = [] + + def send_message(self, message="", **kwargs): + """Send a message to some LaMetric device.""" + from lmnotify import SimpleFrame, Sound, Model + from oauthlib.oauth2 import TokenExpiredError + + targets = kwargs.get(ATTR_TARGET) + data = kwargs.get(ATTR_DATA) + _LOGGER.debug("Targets/Data: %s/%s", targets, data) + icon = self._icon + cycles = self._cycles + sound = None + priority = self._priority + + # Additional data? + if data is not None: + if "icon" in data: + icon = data["icon"] + if "sound" in data: + try: + sound = Sound(category="notifications", + sound_id=data["sound"]) + _LOGGER.debug("Adding notification sound %s", + data["sound"]) + except AssertionError: + _LOGGER.error("Sound ID %s unknown, ignoring", + data["sound"]) + if "cycles" in data: + cycles = int(data['cycles']) + if "priority" in data: + if data['priority'] in AVAILABLE_PRIORITIES: + priority = data['priority'] + else: + _LOGGER.warning("Priority %s invalid, using default %s", + data['priority'], priority) + + text_frame = SimpleFrame(icon, message) + _LOGGER.debug("Icon/Message/Cycles/Lifetime: %s, %s, %d, %d", + icon, message, self._cycles, self._lifetime) + + frames = [text_frame] + + model = Model(frames=frames, cycles=cycles, sound=sound) + lmn = self.hasslametricmanager.manager + try: + self._devices = lmn.get_devices() + except TokenExpiredError: + _LOGGER.debug("Token expired, fetching new token") + lmn.get_token() + self._devices = lmn.get_devices() + except RequestsConnectionError: + _LOGGER.warning("Problem connecting to LaMetric, " + "using cached devices instead") + for dev in self._devices: + if targets is None or dev["name"] in targets: + try: + lmn.set_device(dev) + lmn.send_notification(model, lifetime=self._lifetime, + priority=priority) + _LOGGER.debug("Sent notification to LaMetric %s", + dev["name"]) + except OSError: + _LOGGER.warning("Cannot connect to LaMetric %s", + dev["name"]) diff --git a/homeassistant/components/lcn.py b/homeassistant/components/lcn.py deleted file mode 100644 index 8efdcc997947f..0000000000000 --- a/homeassistant/components/lcn.py +++ /dev/null @@ -1,214 +0,0 @@ -""" -Connects to LCN platform. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/lcn/ -""" - -import logging -import re - -import voluptuous as vol - -from homeassistant.const import ( - CONF_ADDRESS, CONF_HOST, CONF_LIGHTS, CONF_NAME, CONF_PASSWORD, CONF_PORT, - CONF_SWITCHES, CONF_USERNAME) -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import async_load_platform -from homeassistant.helpers.entity import Entity - -REQUIREMENTS = ['pypck==0.5.9'] - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'lcn' -DATA_LCN = 'lcn' -DEFAULT_NAME = 'pchk' - -CONF_SK_NUM_TRIES = 'sk_num_tries' -CONF_DIM_MODE = 'dim_mode' -CONF_OUTPUT = 'output' -CONF_TRANSITION = 'transition' -CONF_DIMMABLE = 'dimmable' -CONF_CONNECTIONS = 'connections' - -DIM_MODES = ['STEPS50', 'STEPS200'] -OUTPUT_PORTS = ['OUTPUT1', 'OUTPUT2', 'OUTPUT3', 'OUTPUT4'] -RELAY_PORTS = ['RELAY1', 'RELAY2', 'RELAY3', 'RELAY4', - 'RELAY5', 'RELAY6', 'RELAY7', 'RELAY8'] - -# Regex for address validation -PATTERN_ADDRESS = re.compile('^((?P\\w+)\\.)?s?(?P\\d+)' - '\\.(?Pm|g)?(?P\\d+)$') - - -def has_unique_connection_names(connections): - """Validate that all connection names are unique. - - Use 'pchk' as default connection_name (or add a numeric suffix if - pchk' is already in use. - """ - for suffix, connection in enumerate(connections): - connection_name = connection.get(CONF_NAME) - if connection_name is None: - if suffix == 0: - connection[CONF_NAME] = DEFAULT_NAME - else: - connection[CONF_NAME] = '{}{:d}'.format(DEFAULT_NAME, suffix) - - schema = vol.Schema(vol.Unique()) - schema([connection.get(CONF_NAME) for connection in connections]) - return connections - - -def is_address(value): - """Validate the given address string. - - Examples for S000M005 at myhome: - myhome.s000.m005 - myhome.s0.m5 - myhome.0.5 ("m" is implicit if missing) - - Examples for s000g011 - myhome.0.g11 - myhome.s0.g11 - """ - matcher = PATTERN_ADDRESS.match(value) - if matcher: - is_group = (matcher.group('type') == 'g') - addr = (int(matcher.group('seg_id')), - int(matcher.group('id')), - is_group) - conn_id = matcher.group('conn_id') - return addr, conn_id - raise vol.error.Invalid('Not a valid address string.') - - -LIGHTS_SCHEMA = vol.Schema({ - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_ADDRESS): is_address, - vol.Required(CONF_OUTPUT): vol.All(vol.Upper, - vol.In(OUTPUT_PORTS + RELAY_PORTS)), - vol.Optional(CONF_DIMMABLE, default=False): vol.Coerce(bool), - vol.Optional(CONF_TRANSITION, default=0): - vol.All(vol.Coerce(float), vol.Range(min=0., max=486.), - lambda value: value * 1000), -}) - -SWITCHES_SCHEMA = vol.Schema({ - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_ADDRESS): is_address, - vol.Required(CONF_OUTPUT): vol.All(vol.Upper, - vol.In(OUTPUT_PORTS + RELAY_PORTS)) -}) - -CONNECTION_SCHEMA = vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PORT): cv.port, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_SK_NUM_TRIES, default=3): cv.positive_int, - vol.Optional(CONF_DIM_MODE, default='steps50'): vol.All(vol.Upper, - vol.In(DIM_MODES)), - vol.Optional(CONF_NAME): cv.string -}) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_CONNECTIONS): vol.All( - cv.ensure_list, has_unique_connection_names, [CONNECTION_SCHEMA]), - vol.Optional(CONF_LIGHTS): vol.All(cv.ensure_list, [LIGHTS_SCHEMA]), - vol.Optional(CONF_SWITCHES): vol.All(cv.ensure_list, [SWITCHES_SCHEMA]) - }) -}, extra=vol.ALLOW_EXTRA) - - -def get_connection(connections, connection_id=None): - """Return the connection object from list.""" - if connection_id is None: - connection = connections[0] - else: - for connection in connections: - if connection.connection_id == connection_id: - break - else: - raise ValueError('Unknown connection_id.') - return connection - - -async def async_setup(hass, config): - """Set up the LCN component.""" - import pypck - from pypck.connection import PchkConnectionManager - - hass.data[DATA_LCN] = {} - - conf_connections = config[DOMAIN][CONF_CONNECTIONS] - connections = [] - for conf_connection in conf_connections: - connection_name = conf_connection.get(CONF_NAME) - - settings = {'SK_NUM_TRIES': conf_connection[CONF_SK_NUM_TRIES], - 'DIM_MODE': pypck.lcn_defs.OutputPortDimMode[ - conf_connection[CONF_DIM_MODE]]} - - connection = PchkConnectionManager(hass.loop, - conf_connection[CONF_HOST], - conf_connection[CONF_PORT], - conf_connection[CONF_USERNAME], - conf_connection[CONF_PASSWORD], - settings=settings, - connection_id=connection_name) - - try: - # establish connection to PCHK server - await hass.async_create_task(connection.async_connect(timeout=15)) - connections.append(connection) - _LOGGER.info('LCN connected to "%s"', connection_name) - except TimeoutError: - _LOGGER.error('Connection to PCHK server "%s" failed.', - connection_name) - return False - - hass.data[DATA_LCN][CONF_CONNECTIONS] = connections - - hass.async_create_task( - async_load_platform(hass, 'light', DOMAIN, - config[DOMAIN][CONF_LIGHTS], config)) - - hass.async_create_task( - async_load_platform(hass, 'switch', DOMAIN, - config[DOMAIN][CONF_SWITCHES], config)) - - return True - - -class LcnDevice(Entity): - """Parent class for all devices associated with the LCN component.""" - - def __init__(self, config, address_connection): - """Initialize the LCN device.""" - import pypck - self.pypck = pypck - self.config = config - self.address_connection = address_connection - self._name = config[CONF_NAME] - - @property - def should_poll(self): - """Lcn device entity pushes its state to HA.""" - return False - - async def async_added_to_hass(self): - """Run when entity about to be added to hass.""" - self.address_connection.register_for_inputs( - self.input_received) - - @property - def name(self): - """Return the name of the device.""" - return self._name - - def input_received(self, input_obj): - """Set state/value when LCN input object (command) is received.""" - raise NotImplementedError('Pure virtual function.') diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py new file mode 100644 index 0000000000000..941160b63970f --- /dev/null +++ b/homeassistant/components/lcn/__init__.py @@ -0,0 +1,208 @@ +"""Support for LCN devices.""" +import logging +import re + +import voluptuous as vol + +from homeassistant.const import ( + CONF_ADDRESS, CONF_HOST, CONF_LIGHTS, CONF_NAME, CONF_PASSWORD, CONF_PORT, + CONF_SWITCHES, CONF_USERNAME) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['pypck==0.5.9'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'lcn' +DATA_LCN = 'lcn' +DEFAULT_NAME = 'pchk' + +CONF_SK_NUM_TRIES = 'sk_num_tries' +CONF_DIM_MODE = 'dim_mode' +CONF_OUTPUT = 'output' +CONF_TRANSITION = 'transition' +CONF_DIMMABLE = 'dimmable' +CONF_CONNECTIONS = 'connections' + +DIM_MODES = ['STEPS50', 'STEPS200'] +OUTPUT_PORTS = ['OUTPUT1', 'OUTPUT2', 'OUTPUT3', 'OUTPUT4'] +RELAY_PORTS = ['RELAY1', 'RELAY2', 'RELAY3', 'RELAY4', + 'RELAY5', 'RELAY6', 'RELAY7', 'RELAY8'] + +# Regex for address validation +PATTERN_ADDRESS = re.compile('^((?P\\w+)\\.)?s?(?P\\d+)' + '\\.(?Pm|g)?(?P\\d+)$') + + +def has_unique_connection_names(connections): + """Validate that all connection names are unique. + + Use 'pchk' as default connection_name (or add a numeric suffix if + pchk' is already in use. + """ + for suffix, connection in enumerate(connections): + connection_name = connection.get(CONF_NAME) + if connection_name is None: + if suffix == 0: + connection[CONF_NAME] = DEFAULT_NAME + else: + connection[CONF_NAME] = '{}{:d}'.format(DEFAULT_NAME, suffix) + + schema = vol.Schema(vol.Unique()) + schema([connection.get(CONF_NAME) for connection in connections]) + return connections + + +def is_address(value): + """Validate the given address string. + + Examples for S000M005 at myhome: + myhome.s000.m005 + myhome.s0.m5 + myhome.0.5 ("m" is implicit if missing) + + Examples for s000g011 + myhome.0.g11 + myhome.s0.g11 + """ + matcher = PATTERN_ADDRESS.match(value) + if matcher: + is_group = (matcher.group('type') == 'g') + addr = (int(matcher.group('seg_id')), + int(matcher.group('id')), + is_group) + conn_id = matcher.group('conn_id') + return addr, conn_id + raise vol.error.Invalid('Not a valid address string.') + + +LIGHTS_SCHEMA = vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_ADDRESS): is_address, + vol.Required(CONF_OUTPUT): vol.All(vol.Upper, + vol.In(OUTPUT_PORTS + RELAY_PORTS)), + vol.Optional(CONF_DIMMABLE, default=False): vol.Coerce(bool), + vol.Optional(CONF_TRANSITION, default=0): + vol.All(vol.Coerce(float), vol.Range(min=0., max=486.), + lambda value: value * 1000), +}) + +SWITCHES_SCHEMA = vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_ADDRESS): is_address, + vol.Required(CONF_OUTPUT): vol.All(vol.Upper, + vol.In(OUTPUT_PORTS + RELAY_PORTS)) +}) + +CONNECTION_SCHEMA = vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PORT): cv.port, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_SK_NUM_TRIES, default=3): cv.positive_int, + vol.Optional(CONF_DIM_MODE, default='steps50'): vol.All(vol.Upper, + vol.In(DIM_MODES)), + vol.Optional(CONF_NAME): cv.string +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_CONNECTIONS): vol.All( + cv.ensure_list, has_unique_connection_names, [CONNECTION_SCHEMA]), + vol.Optional(CONF_LIGHTS): vol.All(cv.ensure_list, [LIGHTS_SCHEMA]), + vol.Optional(CONF_SWITCHES): vol.All(cv.ensure_list, [SWITCHES_SCHEMA]) + }) +}, extra=vol.ALLOW_EXTRA) + + +def get_connection(connections, connection_id=None): + """Return the connection object from list.""" + if connection_id is None: + connection = connections[0] + else: + for connection in connections: + if connection.connection_id == connection_id: + break + else: + raise ValueError('Unknown connection_id.') + return connection + + +async def async_setup(hass, config): + """Set up the LCN component.""" + import pypck + from pypck.connection import PchkConnectionManager + + hass.data[DATA_LCN] = {} + + conf_connections = config[DOMAIN][CONF_CONNECTIONS] + connections = [] + for conf_connection in conf_connections: + connection_name = conf_connection.get(CONF_NAME) + + settings = {'SK_NUM_TRIES': conf_connection[CONF_SK_NUM_TRIES], + 'DIM_MODE': pypck.lcn_defs.OutputPortDimMode[ + conf_connection[CONF_DIM_MODE]]} + + connection = PchkConnectionManager(hass.loop, + conf_connection[CONF_HOST], + conf_connection[CONF_PORT], + conf_connection[CONF_USERNAME], + conf_connection[CONF_PASSWORD], + settings=settings, + connection_id=connection_name) + + try: + # establish connection to PCHK server + await hass.async_create_task(connection.async_connect(timeout=15)) + connections.append(connection) + _LOGGER.info('LCN connected to "%s"', connection_name) + except TimeoutError: + _LOGGER.error('Connection to PCHK server "%s" failed.', + connection_name) + return False + + hass.data[DATA_LCN][CONF_CONNECTIONS] = connections + + hass.async_create_task( + async_load_platform(hass, 'light', DOMAIN, + config[DOMAIN][CONF_LIGHTS], config)) + + hass.async_create_task( + async_load_platform(hass, 'switch', DOMAIN, + config[DOMAIN][CONF_SWITCHES], config)) + + return True + + +class LcnDevice(Entity): + """Parent class for all devices associated with the LCN component.""" + + def __init__(self, config, address_connection): + """Initialize the LCN device.""" + import pypck + self.pypck = pypck + self.config = config + self.address_connection = address_connection + self._name = config[CONF_NAME] + + @property + def should_poll(self): + """Lcn device entity pushes its state to HA.""" + return False + + async def async_added_to_hass(self): + """Run when entity about to be added to hass.""" + self.address_connection.register_for_inputs( + self.input_received) + + @property + def name(self): + """Return the name of the device.""" + return self._name + + def input_received(self, input_obj): + """Set state/value when LCN input object (command) is received.""" + raise NotImplementedError('Pure virtual function.') diff --git a/homeassistant/components/lcn/light.py b/homeassistant/components/lcn/light.py new file mode 100644 index 0000000000000..2b7f4ed4074c4 --- /dev/null +++ b/homeassistant/components/lcn/light.py @@ -0,0 +1,176 @@ +"""Support for LCN lights.""" +from homeassistant.components.lcn import ( + CONF_CONNECTIONS, CONF_DIMMABLE, CONF_OUTPUT, CONF_TRANSITION, DATA_LCN, + OUTPUT_PORTS, LcnDevice, get_connection) +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, + Light) +from homeassistant.const import CONF_ADDRESS + +DEPENDENCIES = ['lcn'] + + +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 + + import pypck + + devices = [] + for config in discovery_info: + address, connection_id = config[CONF_ADDRESS] + addr = pypck.lcn_addr.LcnAddr(*address) + connections = hass.data[DATA_LCN][CONF_CONNECTIONS] + connection = get_connection(connections, connection_id) + address_connection = connection.get_address_conn(addr) + + if config[CONF_OUTPUT] in OUTPUT_PORTS: + device = LcnOutputLight(config, address_connection) + else: # in RELAY_PORTS + device = LcnRelayLight(config, address_connection) + + devices.append(device) + + async_add_entities(devices) + + +class LcnOutputLight(LcnDevice, Light): + """Representation of a LCN light for output ports.""" + + def __init__(self, config, address_connection): + """Initialize the LCN light.""" + super().__init__(config, address_connection) + + self.output = self.pypck.lcn_defs.OutputPort[config[CONF_OUTPUT]] + + self._transition = self.pypck.lcn_defs.time_to_ramp_value( + config[CONF_TRANSITION]) + self.dimmable = config[CONF_DIMMABLE] + + self._brightness = 255 + self._is_on = None + self._is_dimming_to_zero = False + + async def async_added_to_hass(self): + """Run when entity about to be added to hass.""" + await super().async_added_to_hass() + self.hass.async_create_task( + self.address_connection.activate_status_request_handler( + self.output)) + + @property + def supported_features(self): + """Flag supported features.""" + features = SUPPORT_TRANSITION + if self.dimmable: + features |= SUPPORT_BRIGHTNESS + return features + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + return self._brightness + + @property + def is_on(self): + """Return True if entity is on.""" + return self._is_on + + async def async_turn_on(self, **kwargs): + """Turn the entity on.""" + self._is_on = True + self._is_dimming_to_zero = False + if ATTR_BRIGHTNESS in kwargs: + percent = int(kwargs[ATTR_BRIGHTNESS] / 255. * 100) + else: + percent = 100 + if ATTR_TRANSITION in kwargs: + transition = self.pypck.lcn_defs.time_to_ramp_value( + kwargs[ATTR_TRANSITION] * 1000) + else: + transition = self._transition + + self.address_connection.dim_output(self.output.value, percent, + transition) + await self.async_update_ha_state() + + async def async_turn_off(self, **kwargs): + """Turn the entity off.""" + self._is_on = False + if ATTR_TRANSITION in kwargs: + transition = self.pypck.lcn_defs.time_to_ramp_value( + kwargs[ATTR_TRANSITION] * 1000) + else: + transition = self._transition + + self._is_dimming_to_zero = bool(transition) + + self.address_connection.dim_output(self.output.value, 0, transition) + await self.async_update_ha_state() + + def input_received(self, input_obj): + """Set light state when LCN input object (command) is received.""" + if not isinstance(input_obj, self.pypck.inputs.ModStatusOutput) or \ + input_obj.get_output_id() != self.output.value: + return + + self._brightness = int(input_obj.get_percent() / 100.*255) + if self.brightness == 0: + self._is_dimming_to_zero = False + if not self._is_dimming_to_zero: + self._is_on = self.brightness > 0 + self.async_schedule_update_ha_state() + + +class LcnRelayLight(LcnDevice, Light): + """Representation of a LCN light for relay ports.""" + + def __init__(self, config, address_connection): + """Initialize the LCN light.""" + super().__init__(config, address_connection) + + self.output = self.pypck.lcn_defs.RelayPort[config[CONF_OUTPUT]] + + self._is_on = None + + async def async_added_to_hass(self): + """Run when entity about to be added to hass.""" + await super().async_added_to_hass() + self.hass.async_create_task( + self.address_connection.activate_status_request_handler( + self.output)) + + @property + def is_on(self): + """Return True if entity is on.""" + return self._is_on + + async def async_turn_on(self, **kwargs): + """Turn the entity on.""" + self._is_on = True + + states = [self.pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 + states[self.output.value] = self.pypck.lcn_defs.RelayStateModifier.ON + self.address_connection.control_relays(states) + + await self.async_update_ha_state() + + async def async_turn_off(self, **kwargs): + """Turn the entity off.""" + self._is_on = False + + states = [self.pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 + states[self.output.value] = self.pypck.lcn_defs.RelayStateModifier.OFF + self.address_connection.control_relays(states) + + await self.async_update_ha_state() + + def input_received(self, input_obj): + """Set light state when LCN input object (command) is received.""" + if not isinstance(input_obj, self.pypck.inputs.ModStatusRelays): + return + + self._is_on = input_obj.get_state(self.output.value) + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/lcn/switch.py b/homeassistant/components/lcn/switch.py new file mode 100755 index 0000000000000..60eda2ea77995 --- /dev/null +++ b/homeassistant/components/lcn/switch.py @@ -0,0 +1,129 @@ +"""Support for LCN switches.""" +from homeassistant.components.lcn import ( + CONF_CONNECTIONS, CONF_OUTPUT, DATA_LCN, OUTPUT_PORTS, LcnDevice, + get_connection) +from homeassistant.components.switch import SwitchDevice +from homeassistant.const import CONF_ADDRESS + +DEPENDENCIES = ['lcn'] + + +async def async_setup_platform(hass, hass_config, async_add_entities, + discovery_info=None): + """Set up the LCN switch platform.""" + if discovery_info is None: + return + + import pypck + + devices = [] + for config in discovery_info: + address, connection_id = config[CONF_ADDRESS] + addr = pypck.lcn_addr.LcnAddr(*address) + connections = hass.data[DATA_LCN][CONF_CONNECTIONS] + connection = get_connection(connections, connection_id) + address_connection = connection.get_address_conn(addr) + + if config[CONF_OUTPUT] in OUTPUT_PORTS: + device = LcnOutputSwitch(config, address_connection) + else: # in RELAY_PORTS + device = LcnRelaySwitch(config, address_connection) + + devices.append(device) + + async_add_entities(devices) + + +class LcnOutputSwitch(LcnDevice, SwitchDevice): + """Representation of a LCN switch for output ports.""" + + def __init__(self, config, address_connection): + """Initialize the LCN switch.""" + super().__init__(config, address_connection) + + self.output = self.pypck.lcn_defs.OutputPort[config[CONF_OUTPUT]] + + self._is_on = None + + async def async_added_to_hass(self): + """Run when entity about to be added to hass.""" + await super().async_added_to_hass() + self.hass.async_create_task( + self.address_connection.activate_status_request_handler( + self.output)) + + @property + def is_on(self): + """Return True if entity is on.""" + return self._is_on + + async def async_turn_on(self, **kwargs): + """Turn the entity on.""" + self._is_on = True + self.address_connection.dim_output(self.output.value, 100, 0) + await self.async_update_ha_state() + + async def async_turn_off(self, **kwargs): + """Turn the entity off.""" + self._is_on = False + self.address_connection.dim_output(self.output.value, 0, 0) + await self.async_update_ha_state() + + def input_received(self, input_obj): + """Set switch state when LCN input object (command) is received.""" + if not isinstance(input_obj, self.pypck.inputs.ModStatusOutput) or \ + input_obj.get_output_id() != self.output.value: + return + + self._is_on = input_obj.get_percent() > 0 + self.async_schedule_update_ha_state() + + +class LcnRelaySwitch(LcnDevice, SwitchDevice): + """Representation of a LCN switch for relay ports.""" + + def __init__(self, config, address_connection): + """Initialize the LCN switch.""" + super().__init__(config, address_connection) + + self.output = self.pypck.lcn_defs.RelayPort[config[CONF_OUTPUT]] + + self._is_on = None + + async def async_added_to_hass(self): + """Run when entity about to be added to hass.""" + await super().async_added_to_hass() + self.hass.async_create_task( + self.address_connection.activate_status_request_handler( + self.output)) + + @property + def is_on(self): + """Return True if entity is on.""" + return self._is_on + + async def async_turn_on(self, **kwargs): + """Turn the entity on.""" + self._is_on = True + + states = [self.pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 + states[self.output.value] = self.pypck.lcn_defs.RelayStateModifier.ON + self.address_connection.control_relays(states) + await self.async_update_ha_state() + + async def async_turn_off(self, **kwargs): + """Turn the entity off.""" + self._is_on = False + + states = [self.pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 + states[self.output.value] = self.pypck.lcn_defs.RelayStateModifier.OFF + self.address_connection.control_relays(states) + await self.async_update_ha_state() + + def input_received(self, input_obj): + """Set switch state when LCN input object (command) is received.""" + if not isinstance(input_obj, self.pypck.inputs.ModStatusRelays): + return + + self._is_on = input_obj.get_state(self.output.value) + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/lifx/.translations/da.json b/homeassistant/components/lifx/.translations/da.json new file mode 100644 index 0000000000000..ffd8e20ce427b --- /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 a2ae6266a8d62..82802bab4af75 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.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/lifx/light.py b/homeassistant/components/lifx/light.py new file mode 100644 index 0000000000000..c0b6158f18609 --- /dev/null +++ b/homeassistant/components/lifx/light.py @@ -0,0 +1,715 @@ +"""Support for LIFX lights.""" +import asyncio +from datetime import timedelta +from functools import partial +import logging +import math +import sys + +import voluptuous as vol + +from homeassistant import util +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME, ATTR_COLOR_TEMP, + ATTR_EFFECT, ATTR_HS_COLOR, ATTR_KELVIN, ATTR_RGB_COLOR, ATTR_TRANSITION, + ATTR_XY_COLOR, COLOR_GROUP, DOMAIN, LIGHT_TURN_ON_SCHEMA, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, + SUPPORT_TRANSITION, VALID_BRIGHTNESS, VALID_BRIGHTNESS_PCT, Light, + preprocess_turn_on_alternatives) +from homeassistant.components.lifx import ( + DOMAIN as LIFX_DOMAIN, DATA_LIFX_MANAGER, CONF_SERVER, CONF_PORT, + CONF_BROADCAST) +from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +import homeassistant.helpers.device_registry as dr +from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.helpers.service import extract_entity_ids +import homeassistant.util.color as color_util + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['lifx'] +REQUIREMENTS = ['aiolifx_effects==0.2.1'] + +SCAN_INTERVAL = timedelta(seconds=10) + +DISCOVERY_INTERVAL = 60 +MESSAGE_TIMEOUT = 1.0 +MESSAGE_RETRIES = 8 +UNAVAILABLE_GRACE = 90 + +SERVICE_LIFX_SET_STATE = 'lifx_set_state' + +ATTR_INFRARED = 'infrared' +ATTR_ZONES = 'zones' +ATTR_POWER = 'power' + +LIFX_SET_STATE_SCHEMA = LIGHT_TURN_ON_SCHEMA.extend({ + ATTR_INFRARED: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)), + ATTR_ZONES: vol.All(cv.ensure_list, [cv.positive_int]), + ATTR_POWER: cv.boolean, +}) + +SERVICE_EFFECT_PULSE = 'lifx_effect_pulse' +SERVICE_EFFECT_COLORLOOP = 'lifx_effect_colorloop' +SERVICE_EFFECT_STOP = 'lifx_effect_stop' + +ATTR_POWER_ON = 'power_on' +ATTR_MODE = 'mode' +ATTR_PERIOD = 'period' +ATTR_CYCLES = 'cycles' +ATTR_SPREAD = 'spread' +ATTR_CHANGE = 'change' + +PULSE_MODE_BLINK = 'blink' +PULSE_MODE_BREATHE = 'breathe' +PULSE_MODE_PING = 'ping' +PULSE_MODE_STROBE = 'strobe' +PULSE_MODE_SOLID = 'solid' + +PULSE_MODES = [PULSE_MODE_BLINK, PULSE_MODE_BREATHE, PULSE_MODE_PING, + PULSE_MODE_STROBE, PULSE_MODE_SOLID] + +LIFX_EFFECT_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_POWER_ON, default=True): cv.boolean, +}) + +LIFX_EFFECT_PULSE_SCHEMA = LIFX_EFFECT_SCHEMA.extend({ + ATTR_BRIGHTNESS: VALID_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, + vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string, + vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): + vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)), + vol.Coerce(tuple)), + vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): + vol.All(vol.ExactSequence((cv.small_float, cv.small_float)), + vol.Coerce(tuple)), + vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): + vol.All(vol.ExactSequence( + (vol.All(vol.Coerce(float), vol.Range(min=0, max=360)), + vol.All(vol.Coerce(float), vol.Range(min=0, max=100)))), + vol.Coerce(tuple)), + vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP): + vol.All(vol.Coerce(int), vol.Range(min=1)), + vol.Exclusive(ATTR_KELVIN, COLOR_GROUP): + vol.All(vol.Coerce(int), vol.Range(min=0)), + ATTR_PERIOD: vol.All(vol.Coerce(float), vol.Range(min=0.05)), + ATTR_CYCLES: vol.All(vol.Coerce(float), vol.Range(min=1)), + ATTR_MODE: vol.In(PULSE_MODES), +}) + +LIFX_EFFECT_COLORLOOP_SCHEMA = LIFX_EFFECT_SCHEMA.extend({ + ATTR_BRIGHTNESS: VALID_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, + ATTR_PERIOD: vol.All(vol.Coerce(float), vol.Clamp(min=0.05)), + ATTR_CHANGE: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=360)), + ATTR_SPREAD: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=360)), + ATTR_TRANSITION: vol.All(vol.Coerce(float), vol.Range(min=0)), +}) + +LIFX_EFFECT_STOP_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, +}) + + +def aiolifx(): + """Return the aiolifx module.""" + import aiolifx as aiolifx_module + return aiolifx_module + + +def aiolifx_effects(): + """Return the aiolifx_effects module.""" + import aiolifx_effects as aiolifx_effects_module + return aiolifx_effects_module + + +async def async_setup_platform(hass, + config, + async_add_entities, + discovery_info=None): + """Set up the LIFX light platform. Obsolete.""" + _LOGGER.warning('LIFX no longer works with light platform configuration.') + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up LIFX from a config entry.""" + if sys.platform == 'win32': + _LOGGER.warning("The lifx platform is known to not work on Windows. " + "Consider using the lifx_legacy platform instead") + + # Priority 1: manual config + interfaces = hass.data[LIFX_DOMAIN].get(DOMAIN) + if not interfaces: + # Priority 2: scanned interfaces + lifx_ip_addresses = await aiolifx().LifxScan(hass.loop).scan() + interfaces = [{CONF_SERVER: ip} for ip in lifx_ip_addresses] + if not interfaces: + # Priority 3: default interface + interfaces = [{}] + + lifx_manager = LIFXManager(hass, async_add_entities) + hass.data[DATA_LIFX_MANAGER] = lifx_manager + + for interface in interfaces: + lifx_manager.start_discovery(interface) + + return True + + +def lifx_features(bulb): + """Return a feature map for this bulb, or a default map if unknown.""" + return aiolifx().products.features_map.get(bulb.product) or \ + aiolifx().products.features_map.get(1) + + +def find_hsbk(**kwargs): + """Find the desired color from a number of possible inputs.""" + hue, saturation, brightness, kelvin = [None]*4 + + preprocess_turn_on_alternatives(kwargs) + + if ATTR_HS_COLOR in kwargs: + hue, saturation = kwargs[ATTR_HS_COLOR] + hue = int(hue / 360 * 65535) + saturation = int(saturation / 100 * 65535) + kelvin = 3500 + + if ATTR_COLOR_TEMP in kwargs: + kelvin = int(color_util.color_temperature_mired_to_kelvin( + kwargs[ATTR_COLOR_TEMP])) + saturation = 0 + + if ATTR_BRIGHTNESS in kwargs: + brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS]) + + hsbk = [hue, saturation, brightness, kelvin] + return None if hsbk == [None]*4 else hsbk + + +def merge_hsbk(base, change): + """Copy change on top of base, except when None.""" + if change is None: + return None + return [b if c is None else c for b, c in zip(base, change)] + + +class LIFXManager: + """Representation of all known LIFX entities.""" + + def __init__(self, hass, async_add_entities): + """Initialize the light.""" + self.entities = {} + self.hass = hass + self.async_add_entities = async_add_entities + self.effects_conductor = aiolifx_effects().Conductor(loop=hass.loop) + self.discoveries = [] + self.cleanup_unsub = self.hass.bus.async_listen( + EVENT_HOMEASSISTANT_STOP, + self.cleanup) + + self.register_set_state() + self.register_effects() + + def start_discovery(self, interface): + """Start discovery on a network interface.""" + kwargs = {'discovery_interval': DISCOVERY_INTERVAL} + broadcast_ip = interface.get(CONF_BROADCAST) + if broadcast_ip: + kwargs['broadcast_ip'] = broadcast_ip + lifx_discovery = aiolifx().LifxDiscovery( + self.hass.loop, self, **kwargs) + + kwargs = {} + listen_ip = interface.get(CONF_SERVER) + if listen_ip: + kwargs['listen_ip'] = listen_ip + listen_port = interface.get(CONF_PORT) + if listen_port: + kwargs['listen_port'] = listen_port + lifx_discovery.start(**kwargs) + + self.discoveries.append(lifx_discovery) + + @callback + def cleanup(self, event=None): + """Release resources.""" + self.cleanup_unsub() + + for discovery in self.discoveries: + discovery.cleanup() + + for service in [SERVICE_LIFX_SET_STATE, SERVICE_EFFECT_STOP, + SERVICE_EFFECT_PULSE, SERVICE_EFFECT_COLORLOOP]: + self.hass.services.async_remove(DOMAIN, service) + + def register_set_state(self): + """Register the LIFX set_state service call.""" + async def service_handler(service): + """Apply a service.""" + tasks = [] + for light in self.service_to_entities(service): + if service.service == SERVICE_LIFX_SET_STATE: + task = light.set_state(**service.data) + tasks.append(self.hass.async_create_task(task)) + if tasks: + await asyncio.wait(tasks, loop=self.hass.loop) + + self.hass.services.async_register( + DOMAIN, SERVICE_LIFX_SET_STATE, service_handler, + schema=LIFX_SET_STATE_SCHEMA) + + def register_effects(self): + """Register the LIFX effects as hass service calls.""" + async def service_handler(service): + """Apply a service, i.e. start an effect.""" + entities = self.service_to_entities(service) + if entities: + await self.start_effect( + entities, service.service, **service.data) + + self.hass.services.async_register( + DOMAIN, SERVICE_EFFECT_PULSE, service_handler, + schema=LIFX_EFFECT_PULSE_SCHEMA) + + self.hass.services.async_register( + DOMAIN, SERVICE_EFFECT_COLORLOOP, service_handler, + schema=LIFX_EFFECT_COLORLOOP_SCHEMA) + + self.hass.services.async_register( + DOMAIN, SERVICE_EFFECT_STOP, service_handler, + schema=LIFX_EFFECT_STOP_SCHEMA) + + async def start_effect(self, entities, service, **kwargs): + """Start a light effect on entities.""" + bulbs = [light.bulb for light in entities] + + if service == SERVICE_EFFECT_PULSE: + effect = aiolifx_effects().EffectPulse( + power_on=kwargs.get(ATTR_POWER_ON), + period=kwargs.get(ATTR_PERIOD), + cycles=kwargs.get(ATTR_CYCLES), + mode=kwargs.get(ATTR_MODE), + hsbk=find_hsbk(**kwargs), + ) + await self.effects_conductor.start(effect, bulbs) + elif service == SERVICE_EFFECT_COLORLOOP: + preprocess_turn_on_alternatives(kwargs) + + brightness = None + if ATTR_BRIGHTNESS in kwargs: + brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS]) + + effect = aiolifx_effects().EffectColorloop( + power_on=kwargs.get(ATTR_POWER_ON), + period=kwargs.get(ATTR_PERIOD), + change=kwargs.get(ATTR_CHANGE), + spread=kwargs.get(ATTR_SPREAD), + transition=kwargs.get(ATTR_TRANSITION), + brightness=brightness, + ) + await self.effects_conductor.start(effect, bulbs) + elif service == SERVICE_EFFECT_STOP: + await self.effects_conductor.stop(bulbs) + + def service_to_entities(self, service): + """Return the known entities that a service call mentions.""" + entity_ids = extract_entity_ids(self.hass, service) + if entity_ids: + entities = [entity for entity in self.entities.values() + if entity.entity_id in entity_ids] + else: + entities = list(self.entities.values()) + + return entities + + @callback + def register(self, bulb): + """Handle aiolifx detected bulb.""" + self.hass.async_create_task(self.register_new_bulb(bulb)) + + async def register_new_bulb(self, bulb): + """Handle newly detected bulb.""" + if bulb.mac_addr in self.entities: + entity = self.entities[bulb.mac_addr] + entity.registered = True + _LOGGER.debug("%s register AGAIN", entity.who) + await entity.update_hass() + else: + _LOGGER.debug("%s register NEW", bulb.ip_addr) + + # Read initial state + ack = AwaitAioLIFX().wait + color_resp = await ack(bulb.get_color) + if color_resp: + version_resp = await ack(bulb.get_version) + + if color_resp is None or version_resp is None: + _LOGGER.error("Failed to initialize %s", bulb.ip_addr) + bulb.registered = False + else: + bulb.timeout = MESSAGE_TIMEOUT + bulb.retry_count = MESSAGE_RETRIES + bulb.unregister_timeout = UNAVAILABLE_GRACE + + if lifx_features(bulb)["multizone"]: + entity = LIFXStrip(bulb, self.effects_conductor) + elif lifx_features(bulb)["color"]: + entity = LIFXColor(bulb, self.effects_conductor) + else: + entity = LIFXWhite(bulb, self.effects_conductor) + + _LOGGER.debug("%s register READY", entity.who) + self.entities[bulb.mac_addr] = entity + self.async_add_entities([entity], True) + + @callback + def unregister(self, bulb): + """Handle aiolifx disappearing bulbs.""" + if bulb.mac_addr in self.entities: + entity = self.entities[bulb.mac_addr] + _LOGGER.debug("%s unregister", entity.who) + entity.registered = False + self.hass.async_create_task(entity.async_update_ha_state()) + + +class AwaitAioLIFX: + """Wait for an aiolifx callback and return the message.""" + + def __init__(self): + """Initialize the wrapper.""" + self.message = None + self.event = asyncio.Event() + + @callback + def callback(self, bulb, message): + """Handle responses.""" + self.message = message + self.event.set() + + async def wait(self, method): + """Call an aiolifx method and wait for its response.""" + self.message = None + self.event.clear() + method(callb=self.callback) + + await self.event.wait() + return self.message + + +def convert_8_to_16(value): + """Scale an 8 bit level into 16 bits.""" + return (value << 8) | value + + +def convert_16_to_8(value): + """Scale a 16 bit level into 8 bits.""" + return value >> 8 + + +class LIFXLight(Light): + """Representation of a LIFX light.""" + + def __init__(self, bulb, effects_conductor): + """Initialize the light.""" + self.bulb = bulb + self.effects_conductor = effects_conductor + self.registered = True + self.postponed_update = None + self.lock = asyncio.Lock() + + @property + def device_info(self): + """Return information about the device.""" + info = { + 'identifiers': { + (LIFX_DOMAIN, self.unique_id) + }, + 'name': self.name, + 'connections': { + (dr.CONNECTION_NETWORK_MAC, self.bulb.mac_addr) + }, + 'manufacturer': 'LIFX', + } + + model = aiolifx().products.product_map.get(self.bulb.product) + if model is not None: + info['model'] = model + + return info + + @property + def available(self): + """Return the availability of the bulb.""" + return self.registered + + @property + def unique_id(self): + """Return a unique ID.""" + return self.bulb.mac_addr + + @property + def name(self): + """Return the name of the bulb.""" + return self.bulb.label + + @property + def who(self): + """Return a string identifying the bulb.""" + return "%s (%s)" % (self.bulb.ip_addr, self.name) + + @property + def min_mireds(self): + """Return the coldest color_temp that this light supports.""" + kelvin = lifx_features(self.bulb)['max_kelvin'] + return math.floor(color_util.color_temperature_kelvin_to_mired(kelvin)) + + @property + def max_mireds(self): + """Return the warmest color_temp that this light supports.""" + kelvin = lifx_features(self.bulb)['min_kelvin'] + return math.ceil(color_util.color_temperature_kelvin_to_mired(kelvin)) + + @property + def supported_features(self): + """Flag supported features.""" + support = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION | SUPPORT_EFFECT + + bulb_features = lifx_features(self.bulb) + if bulb_features['min_kelvin'] != bulb_features['max_kelvin']: + support |= SUPPORT_COLOR_TEMP + + return support + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + return convert_16_to_8(self.bulb.color[2]) + + @property + def color_temp(self): + """Return the color temperature.""" + _, sat, _, kelvin = self.bulb.color + if sat: + return None + return color_util.color_temperature_kelvin_to_mired(kelvin) + + @property + def is_on(self): + """Return true if light is on.""" + return self.bulb.power_level != 0 + + @property + def effect(self): + """Return the name of the currently running effect.""" + effect = self.effects_conductor.effect(self.bulb) + if effect: + return 'lifx_effect_' + effect.name + return None + + async def update_hass(self, now=None): + """Request new status and push it to hass.""" + self.postponed_update = None + await self.async_update() + await self.async_update_ha_state() + + async def update_during_transition(self, when): + """Update state at the start and end of a transition.""" + if self.postponed_update: + self.postponed_update() + + # Transition has started + await self.update_hass() + + # Transition has ended + if when > 0: + self.postponed_update = async_track_point_in_utc_time( + self.hass, self.update_hass, + util.dt.utcnow() + timedelta(milliseconds=when)) + + async def async_turn_on(self, **kwargs): + """Turn the light on.""" + kwargs[ATTR_POWER] = True + self.hass.async_create_task(self.set_state(**kwargs)) + + async def async_turn_off(self, **kwargs): + """Turn the light off.""" + kwargs[ATTR_POWER] = False + self.hass.async_create_task(self.set_state(**kwargs)) + + async def set_state(self, **kwargs): + """Set a color on the light and turn it on/off.""" + async with self.lock: + bulb = self.bulb + + await self.effects_conductor.stop([bulb]) + + if ATTR_EFFECT in kwargs: + await self.default_effect(**kwargs) + return + + if ATTR_INFRARED in kwargs: + bulb.set_infrared(convert_8_to_16(kwargs[ATTR_INFRARED])) + + if ATTR_TRANSITION in kwargs: + fade = int(kwargs[ATTR_TRANSITION] * 1000) + else: + fade = 0 + + # These are both False if ATTR_POWER is not set + power_on = kwargs.get(ATTR_POWER, False) + power_off = not kwargs.get(ATTR_POWER, True) + + hsbk = find_hsbk(**kwargs) + + # Send messages, waiting for ACK each time + ack = AwaitAioLIFX().wait + + if not self.is_on: + if power_off: + await self.set_power(ack, False) + if hsbk: + await self.set_color(ack, hsbk, kwargs) + if power_on: + await self.set_power(ack, True, duration=fade) + else: + if power_on: + await self.set_power(ack, True) + if hsbk: + await self.set_color(ack, hsbk, kwargs, duration=fade) + if power_off: + await self.set_power(ack, False, duration=fade) + + # Avoid state ping-pong by holding off updates as the state settles + await asyncio.sleep(0.3) + + # Update when the transition starts and ends + await self.update_during_transition(fade) + + async def set_power(self, ack, pwr, duration=0): + """Send a power change to the bulb.""" + await ack(partial(self.bulb.set_power, pwr, duration=duration)) + + async def set_color(self, ack, hsbk, kwargs, duration=0): + """Send a color change to the bulb.""" + hsbk = merge_hsbk(self.bulb.color, hsbk) + await ack(partial(self.bulb.set_color, hsbk, duration=duration)) + + async def default_effect(self, **kwargs): + """Start an effect with default parameters.""" + service = kwargs[ATTR_EFFECT] + data = { + ATTR_ENTITY_ID: self.entity_id, + } + await self.hass.services.async_call(DOMAIN, service, data) + + async def async_update(self): + """Update bulb status.""" + if self.available and not self.lock.locked(): + await AwaitAioLIFX().wait(self.bulb.get_color) + + +class LIFXWhite(LIFXLight): + """Representation of a white-only LIFX light.""" + + @property + def effect_list(self): + """Return the list of supported effects for this light.""" + return [ + SERVICE_EFFECT_PULSE, + SERVICE_EFFECT_STOP, + ] + + +class LIFXColor(LIFXLight): + """Representation of a color LIFX light.""" + + @property + def supported_features(self): + """Flag supported features.""" + support = super().supported_features + support |= SUPPORT_COLOR + return support + + @property + def effect_list(self): + """Return the list of supported effects for this light.""" + return [ + SERVICE_EFFECT_COLORLOOP, + SERVICE_EFFECT_PULSE, + SERVICE_EFFECT_STOP, + ] + + @property + def hs_color(self): + """Return the hs value.""" + hue, sat, _, _ = self.bulb.color + hue = hue / 65535 * 360 + sat = sat / 65535 * 100 + return (hue, sat) if sat else None + + +class LIFXStrip(LIFXColor): + """Representation of a LIFX light strip with multiple zones.""" + + async def set_color(self, ack, hsbk, kwargs, duration=0): + """Send a color change to the bulb.""" + bulb = self.bulb + num_zones = len(bulb.color_zones) + + zones = kwargs.get(ATTR_ZONES) + if zones is None: + # Fast track: setting all zones to the same brightness and color + # can be treated as a single-zone bulb. + if hsbk[2] is not None and hsbk[3] is not None: + await super().set_color(ack, hsbk, kwargs, duration) + return + + zones = list(range(0, num_zones)) + else: + zones = [x for x in set(zones) if x < num_zones] + + # Zone brightness is not reported when powered off + if not self.is_on and hsbk[2] is None: + await self.set_power(ack, True) + await asyncio.sleep(0.3) + await self.update_color_zones() + await self.set_power(ack, False) + await asyncio.sleep(0.3) + + # Send new color to each zone + for index, zone in enumerate(zones): + zone_hsbk = merge_hsbk(bulb.color_zones[zone], hsbk) + apply = 1 if (index == len(zones)-1) else 0 + set_zone = partial(bulb.set_color_zones, + start_index=zone, + end_index=zone, + color=zone_hsbk, + duration=duration, + apply=apply) + await ack(set_zone) + + async def async_update(self): + """Update strip status.""" + if self.available and not self.lock.locked(): + await super().async_update() + await self.update_color_zones() + + async def update_color_zones(self): + """Get updated color information for each zone.""" + zone = 0 + top = 1 + while self.available and zone < top: + # Each get_color_zones can update 8 zones at once + resp = await AwaitAioLIFX().wait(partial( + self.bulb.get_color_zones, + start_index=zone)) + if resp: + zone += 8 + top = resp.count + + # We only await multizone responses so don't ask for just one + if zone == top-1: + zone -= 1 diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index a16d1aaf87e88..816f93b58815e 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -20,7 +20,8 @@ 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/abode.py b/homeassistant/components/light/abode.py deleted file mode 100644 index 397d61f307382..0000000000000 --- a/homeassistant/components/light/abode.py +++ /dev/null @@ -1,103 +0,0 @@ -""" -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/ -""" -import logging -from math import ceil -from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_COLOR_TEMP, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, Light) -from homeassistant.util.color import ( - color_temperature_kelvin_to_mired, color_temperature_mired_to_kelvin) - - -DEPENDENCIES = ['abode'] - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Abode light devices.""" - import abodepy.helpers.constants as CONST - - data = hass.data[ABODE_DOMAIN] - - device_types = [CONST.TYPE_LIGHT, CONST.TYPE_SWITCH] - - devices = [] - - # Get all regular lights that are not excluded or switches marked as lights - for device in data.abode.get_devices(generic_type=device_types): - if data.is_excluded(device) or not data.is_light(device): - continue - - devices.append(AbodeLight(data, device)) - - data.devices.extend(devices) - - add_entities(devices) - - -class AbodeLight(AbodeDevice, Light): - """Representation of an Abode light.""" - - def turn_on(self, **kwargs): - """Turn on the light.""" - if ATTR_COLOR_TEMP in kwargs and self._device.is_color_capable: - self._device.set_color_temp( - int(color_temperature_mired_to_kelvin( - kwargs[ATTR_COLOR_TEMP]))) - - if ATTR_HS_COLOR in kwargs and self._device.is_color_capable: - self._device.set_color(kwargs[ATTR_HS_COLOR]) - - if ATTR_BRIGHTNESS in kwargs and self._device.is_dimmable: - # Convert HASS brightness (0-255) to Abode brightness (0-99) - # If 100 is sent to Abode, response is 99 causing an error - self._device.set_level(ceil(kwargs[ATTR_BRIGHTNESS] * 99 / 255.0)) - else: - self._device.switch_on() - - def turn_off(self, **kwargs): - """Turn off the light.""" - self._device.switch_off() - - @property - def is_on(self): - """Return true if device is on.""" - return self._device.is_on - - @property - def brightness(self): - """Return the brightness of the light.""" - if self._device.is_dimmable and self._device.has_brightness: - brightness = int(self._device.brightness) - # Abode returns 100 during device initialization and device refresh - if brightness == 100: - return 255 - # Convert Abode brightness (0-99) to HASS brightness (0-255) - return ceil(brightness * 255 / 99.0) - - @property - def color_temp(self): - """Return the color temp of the light.""" - if self._device.has_color: - return color_temperature_kelvin_to_mired(self._device.color_temp) - - @property - def hs_color(self): - """Return the color of the light.""" - if self._device.has_color: - return self._device.color - - @property - def supported_features(self): - """Flag supported features.""" - if self._device.is_dimmable and self._device.is_color_capable: - return SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_COLOR_TEMP - if self._device.is_dimmable: - return SUPPORT_BRIGHTNESS - return 0 diff --git a/homeassistant/components/light/ads.py b/homeassistant/components/light/ads.py deleted file mode 100644 index 10df4c0bf7287..0000000000000 --- a/homeassistant/components/light/ads.py +++ /dev/null @@ -1,118 +0,0 @@ -""" -Support for ADS light sources. - -For more details about this platform, please refer to the documentation. -https://home-assistant.io/components/light.ads/ - -""" -import logging -import voluptuous as vol -from homeassistant.components.light import Light, ATTR_BRIGHTNESS, \ - SUPPORT_BRIGHTNESS, PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME -from homeassistant.components.ads import DATA_ADS, CONF_ADS_VAR, \ - CONF_ADS_VAR_BRIGHTNESS -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) -DEPENDENCIES = ['ads'] -DEFAULT_NAME = 'ADS Light' -CONF_ADSVAR_BRIGHTNESS = 'adsvar_brightness' -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_ADS_VAR): cv.string, - vol.Optional(CONF_ADS_VAR_BRIGHTNESS): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the light platform for ADS.""" - ads_hub = hass.data.get(DATA_ADS) - - ads_var_enable = config.get(CONF_ADS_VAR) - ads_var_brightness = config.get(CONF_ADS_VAR_BRIGHTNESS) - name = config.get(CONF_NAME) - - add_entities([AdsLight(ads_hub, ads_var_enable, ads_var_brightness, - name)], True) - - -class AdsLight(Light): - """Representation of ADS light.""" - - def __init__(self, ads_hub, ads_var_enable, ads_var_brightness, name): - """Initialize AdsLight entity.""" - self._ads_hub = ads_hub - self._on_state = False - self._brightness = None - self._name = name - self.ads_var_enable = ads_var_enable - self.ads_var_brightness = ads_var_brightness - - async def async_added_to_hass(self): - """Register device notification.""" - def update_on_state(name, value): - """Handle device notifications for state.""" - _LOGGER.debug('Variable %s changed its value to %d', name, value) - self._on_state = value - self.schedule_update_ha_state() - - def update_brightness(name, value): - """Handle device notification for brightness.""" - _LOGGER.debug('Variable %s changed its value to %d', name, value) - self._brightness = value - self.schedule_update_ha_state() - - self.hass.async_add_executor_job( - self._ads_hub.add_device_notification, - self.ads_var_enable, self._ads_hub.PLCTYPE_BOOL, update_on_state - ) - if self.ads_var_brightness is not None: - self.hass.async_add_executor_job( - self._ads_hub.add_device_notification, - self.ads_var_brightness, self._ads_hub.PLCTYPE_INT, - update_brightness - ) - - @property - def name(self): - """Return the name of the device if any.""" - return self._name - - @property - def brightness(self): - """Return the brightness of the light (0..255).""" - return self._brightness - - @property - def is_on(self): - """Return if light is on.""" - return self._on_state - - @property - def should_poll(self): - """Return False because entity pushes its state to HA.""" - return False - - @property - def supported_features(self): - """Flag supported features.""" - support = 0 - if self.ads_var_brightness is not None: - support = SUPPORT_BRIGHTNESS - return support - - def turn_on(self, **kwargs): - """Turn the light on or set a specific dimmer value.""" - brightness = kwargs.get(ATTR_BRIGHTNESS) - self._ads_hub.write_by_name(self.ads_var_enable, True, - self._ads_hub.PLCTYPE_BOOL) - - if self.ads_var_brightness is not None and brightness is not None: - self._ads_hub.write_by_name(self.ads_var_brightness, brightness, - self._ads_hub.PLCTYPE_UINT) - - def turn_off(self, **kwargs): - """Turn the light off.""" - self._ads_hub.write_by_name(self.ads_var_enable, False, - self._ads_hub.PLCTYPE_BOOL) diff --git a/homeassistant/components/light/elkm1.py b/homeassistant/components/light/elkm1.py deleted file mode 100644 index 707aedbb16135..0000000000000 --- a/homeassistant/components/light/elkm1.py +++ /dev/null @@ -1,59 +0,0 @@ -""" -Support for control of ElkM1 lighting (X10, UPB, etc). - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.elkm1/ -""" - -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) -from homeassistant.components.elkm1 import ( - DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities) - -DEPENDENCIES = [ELK_DOMAIN] - - -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up the Elk light platform.""" - if discovery_info is None: - return - elk = hass.data[ELK_DOMAIN]['elk'] - async_add_entities( - create_elk_entities(hass, elk.lights, 'plc', ElkLight, []), True) - - -class ElkLight(ElkEntity, Light): - """Elk lighting device.""" - - def __init__(self, element, elk, elk_data): - """Initialize light.""" - super().__init__(element, elk, elk_data) - self._brightness = self._element.status - - @property - def brightness(self): - """Get the brightness.""" - return self._brightness - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_BRIGHTNESS - - @property - def is_on(self) -> bool: - """Get the current brightness.""" - return self._brightness != 0 - - def _element_changed(self, element, changeset): - status = self._element.status if self._element.status != 1 else 100 - self._brightness = round(status * 2.55) - - async def async_turn_on(self, **kwargs): - """Turn on the light.""" - self._element.level(round(kwargs.get(ATTR_BRIGHTNESS, 255) / 2.55)) - - async def async_turn_off(self, **kwargs): - """Turn off the light.""" - self._element.level(0) diff --git a/homeassistant/components/light/enocean.py b/homeassistant/components/light/enocean.py deleted file mode 100644 index ebe2c40979679..0000000000000 --- a/homeassistant/components/light/enocean.py +++ /dev/null @@ -1,108 +0,0 @@ -""" -Support for EnOcean light sources. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.enocean/ -""" -import logging -import math - -import voluptuous as vol - -from homeassistant.components.light import ( - Light, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, PLATFORM_SCHEMA) -from homeassistant.const import (CONF_NAME, CONF_ID) -from homeassistant.components import enocean -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -CONF_SENDER_ID = 'sender_id' - -DEFAULT_NAME = 'EnOcean Light' -DEPENDENCIES = ['enocean'] - -SUPPORT_ENOCEAN = SUPPORT_BRIGHTNESS - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_ID, default=[]): - vol.All(cv.ensure_list, [vol.Coerce(int)]), - vol.Required(CONF_SENDER_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the EnOcean light platform.""" - sender_id = config.get(CONF_SENDER_ID) - devname = config.get(CONF_NAME) - dev_id = config.get(CONF_ID) - - add_entities([EnOceanLight(sender_id, devname, dev_id)]) - - -class EnOceanLight(enocean.EnOceanDevice, Light): - """Representation of an EnOcean light source.""" - - def __init__(self, sender_id, devname, dev_id): - """Initialize the EnOcean light source.""" - enocean.EnOceanDevice.__init__(self) - self._on_state = False - self._brightness = 50 - self._sender_id = sender_id - self.dev_id = dev_id - self._devname = devname - self.stype = 'dimmer' - - @property - def name(self): - """Return the name of the device if any.""" - return self._devname - - @property - def brightness(self): - """Brightness of the light. - - This method is optional. Removing it indicates to Home Assistant - that brightness is not supported for this light. - """ - return self._brightness - - @property - def is_on(self): - """If light is on.""" - return self._on_state - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_ENOCEAN - - def turn_on(self, **kwargs): - """Turn the light source on or sets a specific dimmer value.""" - brightness = kwargs.get(ATTR_BRIGHTNESS) - if brightness is not None: - self._brightness = brightness - - bval = math.floor(self._brightness / 256.0 * 100.0) - if bval == 0: - bval = 1 - command = [0xa5, 0x02, bval, 0x01, 0x09] - command.extend(self._sender_id) - command.extend([0x00]) - self.send_command(command, [], 0x01) - self._on_state = True - - def turn_off(self, **kwargs): - """Turn the light source off.""" - command = [0xa5, 0x02, 0x00, 0x01, 0x09] - command.extend(self._sender_id) - command.extend([0x00]) - self.send_command(command, [], 0x01) - self._on_state = False - - def value_changed(self, val): - """Update the internal state of this device.""" - self._brightness = math.floor(val / 100.0 * 256.0) - self._on_state = bool(val != 0) - self.schedule_update_ha_state() diff --git a/homeassistant/components/light/eufy.py b/homeassistant/components/light/eufy.py deleted file mode 100644 index 7a44a58cd81d0..0000000000000 --- a/homeassistant/components/light/eufy.py +++ /dev/null @@ -1,170 +0,0 @@ -""" -Support for Eufy lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.eufy/ -""" -import logging - -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_COLOR, Light) - -import homeassistant.util.color as color_util - -from homeassistant.util.color import ( - color_temperature_mired_to_kelvin as mired_to_kelvin, - color_temperature_kelvin_to_mired as kelvin_to_mired) - -DEPENDENCIES = ['eufy'] - -_LOGGER = logging.getLogger(__name__) - -EUFY_MAX_KELVIN = 6500 -EUFY_MIN_KELVIN = 2700 - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Eufy bulbs.""" - if discovery_info is None: - return - add_entities([EufyLight(discovery_info)], True) - - -class EufyLight(Light): - """Representation of a Eufy light.""" - - def __init__(self, device): - """Initialize the light.""" - import lakeside - - self._temp = None - self._brightness = None - self._hs = None - self._state = None - self._name = device['name'] - self._address = device['address'] - self._code = device['code'] - self._type = device['type'] - self._bulb = lakeside.bulb(self._address, self._code, self._type) - self._colormode = False - if self._type == "T1011": - self._features = SUPPORT_BRIGHTNESS - elif self._type == "T1012": - self._features = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP - elif self._type == "T1013": - self._features = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | \ - SUPPORT_COLOR - self._bulb.connect() - - def update(self): - """Synchronise state from the bulb.""" - self._bulb.update() - if self._bulb.power: - self._brightness = self._bulb.brightness - self._temp = self._bulb.temperature - if self._bulb.colors: - self._colormode = True - self._hs = color_util.color_RGB_to_hs(*self._bulb.colors) - else: - self._colormode = False - self._state = self._bulb.power - - @property - def unique_id(self): - """Return the ID of this light.""" - return self._address - - @property - def name(self): - """Return the name of the device if any.""" - return self._name - - @property - def is_on(self): - """Return true if device is on.""" - return self._state - - @property - def brightness(self): - """Return the brightness of this light between 0..255.""" - return int(self._brightness * 255 / 100) - - @property - def min_mireds(self): - """Return minimum supported color temperature.""" - return kelvin_to_mired(EUFY_MAX_KELVIN) - - @property - def max_mireds(self): - """Return maximu supported color temperature.""" - return kelvin_to_mired(EUFY_MIN_KELVIN) - - @property - def color_temp(self): - """Return the color temperature of this light.""" - temp_in_k = int(EUFY_MIN_KELVIN + (self._temp * - (EUFY_MAX_KELVIN - EUFY_MIN_KELVIN) - / 100)) - return kelvin_to_mired(temp_in_k) - - @property - def hs_color(self): - """Return the color of this light.""" - if not self._colormode: - return None - return self._hs - - @property - def supported_features(self): - """Flag supported features.""" - return self._features - - def turn_on(self, **kwargs): - """Turn the specified light on.""" - brightness = kwargs.get(ATTR_BRIGHTNESS) - colortemp = kwargs.get(ATTR_COLOR_TEMP) - # pylint: disable=invalid-name - hs = kwargs.get(ATTR_HS_COLOR) - - if brightness is not None: - brightness = int(brightness * 100 / 255) - else: - if self._brightness is None: - self._brightness = 100 - brightness = self._brightness - - if colortemp is not None: - self._colormode = False - temp_in_k = mired_to_kelvin(colortemp) - relative_temp = temp_in_k - EUFY_MIN_KELVIN - temp = int(relative_temp * 100 / - (EUFY_MAX_KELVIN - EUFY_MIN_KELVIN)) - else: - temp = None - - if hs is not None: - rgb = color_util.color_hsv_to_RGB( - hs[0], hs[1], brightness / 255 * 100) - self._colormode = True - elif self._colormode: - rgb = color_util.color_hsv_to_RGB( - self._hs[0], self._hs[1], brightness / 255 * 100) - else: - rgb = None - - try: - self._bulb.set_state(power=True, brightness=brightness, - temperature=temp, colors=rgb) - except BrokenPipeError: - self._bulb.connect() - self._bulb.set_state(power=True, brightness=brightness, - temperature=temp, colors=rgb) - - def turn_off(self, **kwargs): - """Turn the specified light off.""" - try: - self._bulb.set_state(power=False) - except BrokenPipeError: - self._bulb.connect() - self._bulb.set_state(power=False) diff --git a/homeassistant/components/light/fibaro.py b/homeassistant/components/light/fibaro.py deleted file mode 100644 index 9b3b3850f39ac..0000000000000 --- a/homeassistant/components/light/fibaro.py +++ /dev/null @@ -1,211 +0,0 @@ -""" -Support for Fibaro lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.fibaro/ -""" - -import logging -import asyncio -from functools import partial - -from homeassistant.const import ( - CONF_WHITE_VALUE) -from homeassistant.components.fibaro import ( - FIBARO_DEVICES, FibaroDevice, - CONF_DIMMING, CONF_COLOR, CONF_RESET_COLOR) -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_WHITE_VALUE, ENTITY_ID_FORMAT, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_WHITE_VALUE, Light) -import homeassistant.util.color as color_util - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['fibaro'] - - -def scaleto255(value): - """Scale the input value from 0-100 to 0-255.""" - # Fibaro has a funny way of storing brightness either 0-100 or 0-99 - # depending on device type (e.g. dimmer vs led) - if value > 98: - value = 100 - return max(0, min(255, ((value * 255.0) / 100.0))) - - -def scaleto100(value): - """Scale the input value from 0-255 to 0-100.""" - # Make sure a low but non-zero value is not rounded down to zero - if 0 < value < 3: - return 1 - return max(0, min(100, ((value * 100.0) / 255.0))) - - -async def async_setup_platform(hass, - config, - async_add_entities, - discovery_info=None): - """Perform the setup for Fibaro controller devices.""" - if discovery_info is None: - return - - async_add_entities( - [FibaroLight(device) - for device in hass.data[FIBARO_DEVICES]['light']], True) - - -class FibaroLight(FibaroDevice, Light): - """Representation of a Fibaro Light, including dimmable.""" - - def __init__(self, fibaro_device): - """Initialize the light.""" - self._brightness = None - self._color = (0, 0) - self._last_brightness = 0 - self._supported_flags = 0 - self._update_lock = asyncio.Lock() - self._white = 0 - - devconf = fibaro_device.device_config - self._reset_color = devconf.get(CONF_RESET_COLOR, False) - supports_color = 'color' in fibaro_device.properties and \ - 'setColor' in fibaro_device.actions - supports_dimming = 'levelChange' in fibaro_device.interfaces - supports_white_v = 'setW' in fibaro_device.actions - - # Configuration can overrride default capability detection - if devconf.get(CONF_DIMMING, supports_dimming): - self._supported_flags |= SUPPORT_BRIGHTNESS - if devconf.get(CONF_COLOR, supports_color): - self._supported_flags |= SUPPORT_COLOR - if devconf.get(CONF_WHITE_VALUE, supports_white_v): - self._supported_flags |= SUPPORT_WHITE_VALUE - - super().__init__(fibaro_device) - self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) - - @property - def brightness(self): - """Return the brightness of the light.""" - return scaleto255(self._brightness) - - @property - def hs_color(self): - """Return the color of the light.""" - return self._color - - @property - def white_value(self): - """Return the white value of this light between 0..255.""" - return self._white - - @property - def supported_features(self): - """Flag supported features.""" - return self._supported_flags - - async def async_turn_on(self, **kwargs): - """Turn the light on.""" - async with self._update_lock: - await self.hass.async_add_executor_job( - partial(self._turn_on, **kwargs)) - - def _turn_on(self, **kwargs): - """Really turn the light on.""" - if self._supported_flags & SUPPORT_BRIGHTNESS: - target_brightness = kwargs.get(ATTR_BRIGHTNESS) - - # No brightness specified, so we either restore it to - # last brightness or switch it on at maximum level - if target_brightness is None: - if self._brightness == 0: - if self._last_brightness: - self._brightness = self._last_brightness - else: - self._brightness = 100 - else: - # We set it to the target brightness and turn it on - self._brightness = scaleto100(target_brightness) - - if self._supported_flags & SUPPORT_COLOR: - if self._reset_color and \ - kwargs.get(ATTR_WHITE_VALUE) is None and \ - kwargs.get(ATTR_HS_COLOR) is None and \ - kwargs.get(ATTR_BRIGHTNESS) is None: - self._color = (100, 0) - - # Update based on parameters - self._white = kwargs.get(ATTR_WHITE_VALUE, self._white) - self._color = kwargs.get(ATTR_HS_COLOR, self._color) - rgb = color_util.color_hs_to_RGB(*self._color) - self.call_set_color( - round(rgb[0] * self._brightness / 100.0), - round(rgb[1] * self._brightness / 100.0), - round(rgb[2] * self._brightness / 100.0), - round(self._white * self._brightness / 100.0)) - - if self.state == 'off': - self.set_level(int(self._brightness)) - return - - if self._reset_color: - bri255 = scaleto255(self._brightness) - self.call_set_color(bri255, bri255, bri255, bri255) - - if self._supported_flags & SUPPORT_BRIGHTNESS: - self.set_level(int(self._brightness)) - return - - # The simplest case is left for last. No dimming, just switch on - self.call_turn_on() - - async def async_turn_off(self, **kwargs): - """Turn the light off.""" - async with self._update_lock: - await self.hass.async_add_executor_job( - partial(self._turn_off, **kwargs)) - - def _turn_off(self, **kwargs): - """Really turn the light off.""" - # Let's save the last brightness level before we switch it off - if (self._supported_flags & SUPPORT_BRIGHTNESS) and \ - self._brightness and self._brightness > 0: - self._last_brightness = self._brightness - self._brightness = 0 - self.call_turn_off() - - @property - def is_on(self): - """Return true if device is on.""" - return self.current_binary_state - - async def async_update(self): - """Update the state.""" - async with self._update_lock: - await self.hass.async_add_executor_job(self._update) - - def _update(self): - """Really update the state.""" - # Brightness handling - if self._supported_flags & SUPPORT_BRIGHTNESS: - self._brightness = float(self.fibaro_device.properties.value) - # Fibaro might report 0-99 or 0-100 for brightness, - # based on device type, so we round up here - if self._brightness > 99: - self._brightness = 100 - # Color handling - if self._supported_flags & SUPPORT_COLOR and \ - 'color' in self.fibaro_device.properties and \ - ',' in self.fibaro_device.properties.color: - # Fibaro communicates the color as an 'R, G, B, W' string - rgbw_s = self.fibaro_device.properties.color - if rgbw_s == '0,0,0,0' and\ - 'lastColorSet' in self.fibaro_device.properties: - rgbw_s = self.fibaro_device.properties.lastColorSet - rgbw_list = [int(i) for i in rgbw_s.split(",")][:4] - if rgbw_list[0] or rgbw_list[1] or rgbw_list[2]: - self._color = color_util.color_RGB_to_hs(*rgbw_list[:3]) - if (self._supported_flags & SUPPORT_WHITE_VALUE) and \ - self.brightness != 0: - self._white = min(255, max(0, rgbw_list[3]*100.0 / - self._brightness)) diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index cab6957c2655c..5ecf3f55e10a8 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -23,6 +23,10 @@ _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_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_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 } - -FLUX_EFFECT_LIST = [ - EFFECT_RANDOM, - ] + list(EFFECT_MAP) +EFFECT_CUSTOM_CODE = 0x60 + +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 @@ 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 @@ def __init__(self, device): 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 @@ def white_value(self): @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 @@ def turn_on(self, **kwargs): 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 2837edbd5b7c9..0000000000000 --- 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 ebe209c745ea6..16be7d4582511 100644 --- a/homeassistant/components/light/hyperion.py +++ b/homeassistant/components/light/hyperion.py @@ -177,6 +177,11 @@ def turn_on(self, **kwargs): 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 a1423cc66826b..0000000000000 --- 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/lcn.py b/homeassistant/components/light/lcn.py deleted file mode 100644 index b9457b7b7d920..0000000000000 --- a/homeassistant/components/light/lcn.py +++ /dev/null @@ -1,182 +0,0 @@ -""" -Support for LCN lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.lcn/ -""" - -from homeassistant.components.lcn import ( - CONF_CONNECTIONS, CONF_DIMMABLE, CONF_OUTPUT, CONF_TRANSITION, DATA_LCN, - OUTPUT_PORTS, LcnDevice, get_connection) -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, - Light) -from homeassistant.const import CONF_ADDRESS - -DEPENDENCIES = ['lcn'] - - -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 - - import pypck - - devices = [] - for config in discovery_info: - address, connection_id = config[CONF_ADDRESS] - addr = pypck.lcn_addr.LcnAddr(*address) - connections = hass.data[DATA_LCN][CONF_CONNECTIONS] - connection = get_connection(connections, connection_id) - address_connection = connection.get_address_conn(addr) - - if config[CONF_OUTPUT] in OUTPUT_PORTS: - device = LcnOutputLight(config, address_connection) - else: # in RELAY_PORTS - device = LcnRelayLight(config, address_connection) - - devices.append(device) - - async_add_entities(devices) - - -class LcnOutputLight(LcnDevice, Light): - """Representation of a LCN light for output ports.""" - - def __init__(self, config, address_connection): - """Initialize the LCN light.""" - super().__init__(config, address_connection) - - self.output = self.pypck.lcn_defs.OutputPort[config[CONF_OUTPUT]] - - self._transition = self.pypck.lcn_defs.time_to_ramp_value( - config[CONF_TRANSITION]) - self.dimmable = config[CONF_DIMMABLE] - - self._brightness = 255 - self._is_on = None - self._is_dimming_to_zero = False - - async def async_added_to_hass(self): - """Run when entity about to be added to hass.""" - await super().async_added_to_hass() - self.hass.async_create_task( - self.address_connection.activate_status_request_handler( - self.output)) - - @property - def supported_features(self): - """Flag supported features.""" - features = SUPPORT_TRANSITION - if self.dimmable: - features |= SUPPORT_BRIGHTNESS - return features - - @property - def brightness(self): - """Return the brightness of this light between 0..255.""" - return self._brightness - - @property - def is_on(self): - """Return True if entity is on.""" - return self._is_on - - async def async_turn_on(self, **kwargs): - """Turn the entity on.""" - self._is_on = True - self._is_dimming_to_zero = False - if ATTR_BRIGHTNESS in kwargs: - percent = int(kwargs[ATTR_BRIGHTNESS] / 255. * 100) - else: - percent = 100 - if ATTR_TRANSITION in kwargs: - transition = self.pypck.lcn_defs.time_to_ramp_value( - kwargs[ATTR_TRANSITION] * 1000) - else: - transition = self._transition - - self.address_connection.dim_output(self.output.value, percent, - transition) - await self.async_update_ha_state() - - async def async_turn_off(self, **kwargs): - """Turn the entity off.""" - self._is_on = False - if ATTR_TRANSITION in kwargs: - transition = self.pypck.lcn_defs.time_to_ramp_value( - kwargs[ATTR_TRANSITION] * 1000) - else: - transition = self._transition - - self._is_dimming_to_zero = bool(transition) - - self.address_connection.dim_output(self.output.value, 0, transition) - await self.async_update_ha_state() - - def input_received(self, input_obj): - """Set light state when LCN input object (command) is received.""" - if not isinstance(input_obj, self.pypck.inputs.ModStatusOutput) or \ - input_obj.get_output_id() != self.output.value: - return - - self._brightness = int(input_obj.get_percent() / 100.*255) - if self.brightness == 0: - self._is_dimming_to_zero = False - if not self._is_dimming_to_zero: - self._is_on = self.brightness > 0 - self.async_schedule_update_ha_state() - - -class LcnRelayLight(LcnDevice, Light): - """Representation of a LCN light for relay ports.""" - - def __init__(self, config, address_connection): - """Initialize the LCN light.""" - super().__init__(config, address_connection) - - self.output = self.pypck.lcn_defs.RelayPort[config[CONF_OUTPUT]] - - self._is_on = None - - async def async_added_to_hass(self): - """Run when entity about to be added to hass.""" - await super().async_added_to_hass() - self.hass.async_create_task( - self.address_connection.activate_status_request_handler( - self.output)) - - @property - def is_on(self): - """Return True if entity is on.""" - return self._is_on - - async def async_turn_on(self, **kwargs): - """Turn the entity on.""" - self._is_on = True - - states = [self.pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 - states[self.output.value] = self.pypck.lcn_defs.RelayStateModifier.ON - self.address_connection.control_relays(states) - - await self.async_update_ha_state() - - async def async_turn_off(self, **kwargs): - """Turn the entity off.""" - self._is_on = False - - states = [self.pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 - states[self.output.value] = self.pypck.lcn_defs.RelayStateModifier.OFF - self.address_connection.control_relays(states) - - await self.async_update_ha_state() - - def input_received(self, input_obj): - """Set light state when LCN input object (command) is received.""" - if not isinstance(input_obj, self.pypck.inputs.ModStatusRelays): - return - - self._is_on = input_obj.get_state(self.output.value) - self.async_schedule_update_ha_state() diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py deleted file mode 100644 index f0cd7b7a7fe62..0000000000000 --- a/homeassistant/components/light/lifx.py +++ /dev/null @@ -1,720 +0,0 @@ -""" -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/ -""" -import asyncio -from datetime import timedelta -from functools import partial -import logging -import math -import sys - -import voluptuous as vol - -from homeassistant import util -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME, ATTR_COLOR_TEMP, - ATTR_EFFECT, ATTR_HS_COLOR, ATTR_KELVIN, ATTR_RGB_COLOR, ATTR_TRANSITION, - ATTR_XY_COLOR, COLOR_GROUP, DOMAIN, LIGHT_TURN_ON_SCHEMA, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, - SUPPORT_TRANSITION, VALID_BRIGHTNESS, VALID_BRIGHTNESS_PCT, Light, - preprocess_turn_on_alternatives) -from homeassistant.components.lifx import ( - DOMAIN as LIFX_DOMAIN, DATA_LIFX_MANAGER, CONF_SERVER, CONF_PORT, - CONF_BROADCAST) -from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv -import homeassistant.helpers.device_registry as dr -from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.helpers.service import extract_entity_ids -import homeassistant.util.color as color_util - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['lifx'] -REQUIREMENTS = ['aiolifx_effects==0.2.1'] - -SCAN_INTERVAL = timedelta(seconds=10) - -DISCOVERY_INTERVAL = 60 -MESSAGE_TIMEOUT = 1.0 -MESSAGE_RETRIES = 8 -UNAVAILABLE_GRACE = 90 - -SERVICE_LIFX_SET_STATE = 'lifx_set_state' - -ATTR_INFRARED = 'infrared' -ATTR_ZONES = 'zones' -ATTR_POWER = 'power' - -LIFX_SET_STATE_SCHEMA = LIGHT_TURN_ON_SCHEMA.extend({ - ATTR_INFRARED: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)), - ATTR_ZONES: vol.All(cv.ensure_list, [cv.positive_int]), - ATTR_POWER: cv.boolean, -}) - -SERVICE_EFFECT_PULSE = 'lifx_effect_pulse' -SERVICE_EFFECT_COLORLOOP = 'lifx_effect_colorloop' -SERVICE_EFFECT_STOP = 'lifx_effect_stop' - -ATTR_POWER_ON = 'power_on' -ATTR_MODE = 'mode' -ATTR_PERIOD = 'period' -ATTR_CYCLES = 'cycles' -ATTR_SPREAD = 'spread' -ATTR_CHANGE = 'change' - -PULSE_MODE_BLINK = 'blink' -PULSE_MODE_BREATHE = 'breathe' -PULSE_MODE_PING = 'ping' -PULSE_MODE_STROBE = 'strobe' -PULSE_MODE_SOLID = 'solid' - -PULSE_MODES = [PULSE_MODE_BLINK, PULSE_MODE_BREATHE, PULSE_MODE_PING, - PULSE_MODE_STROBE, PULSE_MODE_SOLID] - -LIFX_EFFECT_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Optional(ATTR_POWER_ON, default=True): cv.boolean, -}) - -LIFX_EFFECT_PULSE_SCHEMA = LIFX_EFFECT_SCHEMA.extend({ - ATTR_BRIGHTNESS: VALID_BRIGHTNESS, - ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, - vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string, - vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): - vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)), - vol.Coerce(tuple)), - vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): - vol.All(vol.ExactSequence((cv.small_float, cv.small_float)), - vol.Coerce(tuple)), - vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): - vol.All(vol.ExactSequence( - (vol.All(vol.Coerce(float), vol.Range(min=0, max=360)), - vol.All(vol.Coerce(float), vol.Range(min=0, max=100)))), - vol.Coerce(tuple)), - vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP): - vol.All(vol.Coerce(int), vol.Range(min=1)), - vol.Exclusive(ATTR_KELVIN, COLOR_GROUP): - vol.All(vol.Coerce(int), vol.Range(min=0)), - ATTR_PERIOD: vol.All(vol.Coerce(float), vol.Range(min=0.05)), - ATTR_CYCLES: vol.All(vol.Coerce(float), vol.Range(min=1)), - ATTR_MODE: vol.In(PULSE_MODES), -}) - -LIFX_EFFECT_COLORLOOP_SCHEMA = LIFX_EFFECT_SCHEMA.extend({ - ATTR_BRIGHTNESS: VALID_BRIGHTNESS, - ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, - ATTR_PERIOD: vol.All(vol.Coerce(float), vol.Clamp(min=0.05)), - ATTR_CHANGE: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=360)), - ATTR_SPREAD: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=360)), - ATTR_TRANSITION: vol.All(vol.Coerce(float), vol.Range(min=0)), -}) - -LIFX_EFFECT_STOP_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, -}) - - -def aiolifx(): - """Return the aiolifx module.""" - import aiolifx as aiolifx_module - return aiolifx_module - - -def aiolifx_effects(): - """Return the aiolifx_effects module.""" - import aiolifx_effects as aiolifx_effects_module - return aiolifx_effects_module - - -async def async_setup_platform(hass, - config, - async_add_entities, - discovery_info=None): - """Set up the LIFX light platform. Obsolete.""" - _LOGGER.warning('LIFX no longer works with light platform configuration.') - - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up LIFX from a config entry.""" - if sys.platform == 'win32': - _LOGGER.warning("The lifx platform is known to not work on Windows. " - "Consider using the lifx_legacy platform instead") - - # Priority 1: manual config - interfaces = hass.data[LIFX_DOMAIN].get(DOMAIN) - if not interfaces: - # Priority 2: scanned interfaces - lifx_ip_addresses = await aiolifx().LifxScan(hass.loop).scan() - interfaces = [{CONF_SERVER: ip} for ip in lifx_ip_addresses] - if not interfaces: - # Priority 3: default interface - interfaces = [{}] - - lifx_manager = LIFXManager(hass, async_add_entities) - hass.data[DATA_LIFX_MANAGER] = lifx_manager - - for interface in interfaces: - lifx_manager.start_discovery(interface) - - return True - - -def lifx_features(bulb): - """Return a feature map for this bulb, or a default map if unknown.""" - return aiolifx().products.features_map.get(bulb.product) or \ - aiolifx().products.features_map.get(1) - - -def find_hsbk(**kwargs): - """Find the desired color from a number of possible inputs.""" - hue, saturation, brightness, kelvin = [None]*4 - - preprocess_turn_on_alternatives(kwargs) - - if ATTR_HS_COLOR in kwargs: - hue, saturation = kwargs[ATTR_HS_COLOR] - hue = int(hue / 360 * 65535) - saturation = int(saturation / 100 * 65535) - kelvin = 3500 - - if ATTR_COLOR_TEMP in kwargs: - kelvin = int(color_util.color_temperature_mired_to_kelvin( - kwargs[ATTR_COLOR_TEMP])) - saturation = 0 - - if ATTR_BRIGHTNESS in kwargs: - brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS]) - - hsbk = [hue, saturation, brightness, kelvin] - return None if hsbk == [None]*4 else hsbk - - -def merge_hsbk(base, change): - """Copy change on top of base, except when None.""" - if change is None: - return None - return [b if c is None else c for b, c in zip(base, change)] - - -class LIFXManager: - """Representation of all known LIFX entities.""" - - def __init__(self, hass, async_add_entities): - """Initialize the light.""" - self.entities = {} - self.hass = hass - self.async_add_entities = async_add_entities - self.effects_conductor = aiolifx_effects().Conductor(loop=hass.loop) - self.discoveries = [] - self.cleanup_unsub = self.hass.bus.async_listen( - EVENT_HOMEASSISTANT_STOP, - self.cleanup) - - self.register_set_state() - self.register_effects() - - def start_discovery(self, interface): - """Start discovery on a network interface.""" - kwargs = {'discovery_interval': DISCOVERY_INTERVAL} - broadcast_ip = interface.get(CONF_BROADCAST) - if broadcast_ip: - kwargs['broadcast_ip'] = broadcast_ip - lifx_discovery = aiolifx().LifxDiscovery( - self.hass.loop, self, **kwargs) - - kwargs = {} - listen_ip = interface.get(CONF_SERVER) - if listen_ip: - kwargs['listen_ip'] = listen_ip - listen_port = interface.get(CONF_PORT) - if listen_port: - kwargs['listen_port'] = listen_port - lifx_discovery.start(**kwargs) - - self.discoveries.append(lifx_discovery) - - @callback - def cleanup(self, event=None): - """Release resources.""" - self.cleanup_unsub() - - for discovery in self.discoveries: - discovery.cleanup() - - for service in [SERVICE_LIFX_SET_STATE, SERVICE_EFFECT_STOP, - SERVICE_EFFECT_PULSE, SERVICE_EFFECT_COLORLOOP]: - self.hass.services.async_remove(DOMAIN, service) - - def register_set_state(self): - """Register the LIFX set_state service call.""" - async def service_handler(service): - """Apply a service.""" - tasks = [] - for light in self.service_to_entities(service): - if service.service == SERVICE_LIFX_SET_STATE: - task = light.set_state(**service.data) - tasks.append(self.hass.async_create_task(task)) - if tasks: - await asyncio.wait(tasks, loop=self.hass.loop) - - self.hass.services.async_register( - DOMAIN, SERVICE_LIFX_SET_STATE, service_handler, - schema=LIFX_SET_STATE_SCHEMA) - - def register_effects(self): - """Register the LIFX effects as hass service calls.""" - async def service_handler(service): - """Apply a service, i.e. start an effect.""" - entities = self.service_to_entities(service) - if entities: - await self.start_effect( - entities, service.service, **service.data) - - self.hass.services.async_register( - DOMAIN, SERVICE_EFFECT_PULSE, service_handler, - schema=LIFX_EFFECT_PULSE_SCHEMA) - - self.hass.services.async_register( - DOMAIN, SERVICE_EFFECT_COLORLOOP, service_handler, - schema=LIFX_EFFECT_COLORLOOP_SCHEMA) - - self.hass.services.async_register( - DOMAIN, SERVICE_EFFECT_STOP, service_handler, - schema=LIFX_EFFECT_STOP_SCHEMA) - - async def start_effect(self, entities, service, **kwargs): - """Start a light effect on entities.""" - bulbs = [light.bulb for light in entities] - - if service == SERVICE_EFFECT_PULSE: - effect = aiolifx_effects().EffectPulse( - power_on=kwargs.get(ATTR_POWER_ON), - period=kwargs.get(ATTR_PERIOD), - cycles=kwargs.get(ATTR_CYCLES), - mode=kwargs.get(ATTR_MODE), - hsbk=find_hsbk(**kwargs), - ) - await self.effects_conductor.start(effect, bulbs) - elif service == SERVICE_EFFECT_COLORLOOP: - preprocess_turn_on_alternatives(kwargs) - - brightness = None - if ATTR_BRIGHTNESS in kwargs: - brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS]) - - effect = aiolifx_effects().EffectColorloop( - power_on=kwargs.get(ATTR_POWER_ON), - period=kwargs.get(ATTR_PERIOD), - change=kwargs.get(ATTR_CHANGE), - spread=kwargs.get(ATTR_SPREAD), - transition=kwargs.get(ATTR_TRANSITION), - brightness=brightness, - ) - await self.effects_conductor.start(effect, bulbs) - elif service == SERVICE_EFFECT_STOP: - await self.effects_conductor.stop(bulbs) - - def service_to_entities(self, service): - """Return the known entities that a service call mentions.""" - entity_ids = extract_entity_ids(self.hass, service) - if entity_ids: - entities = [entity for entity in self.entities.values() - if entity.entity_id in entity_ids] - else: - entities = list(self.entities.values()) - - return entities - - @callback - def register(self, bulb): - """Handle aiolifx detected bulb.""" - self.hass.async_create_task(self.register_new_bulb(bulb)) - - async def register_new_bulb(self, bulb): - """Handle newly detected bulb.""" - if bulb.mac_addr in self.entities: - entity = self.entities[bulb.mac_addr] - entity.registered = True - _LOGGER.debug("%s register AGAIN", entity.who) - await entity.update_hass() - else: - _LOGGER.debug("%s register NEW", bulb.ip_addr) - - # Read initial state - ack = AwaitAioLIFX().wait - color_resp = await ack(bulb.get_color) - if color_resp: - version_resp = await ack(bulb.get_version) - - if color_resp is None or version_resp is None: - _LOGGER.error("Failed to initialize %s", bulb.ip_addr) - bulb.registered = False - else: - bulb.timeout = MESSAGE_TIMEOUT - bulb.retry_count = MESSAGE_RETRIES - bulb.unregister_timeout = UNAVAILABLE_GRACE - - if lifx_features(bulb)["multizone"]: - entity = LIFXStrip(bulb, self.effects_conductor) - elif lifx_features(bulb)["color"]: - entity = LIFXColor(bulb, self.effects_conductor) - else: - entity = LIFXWhite(bulb, self.effects_conductor) - - _LOGGER.debug("%s register READY", entity.who) - self.entities[bulb.mac_addr] = entity - self.async_add_entities([entity], True) - - @callback - def unregister(self, bulb): - """Handle aiolifx disappearing bulbs.""" - if bulb.mac_addr in self.entities: - entity = self.entities[bulb.mac_addr] - _LOGGER.debug("%s unregister", entity.who) - entity.registered = False - self.hass.async_create_task(entity.async_update_ha_state()) - - -class AwaitAioLIFX: - """Wait for an aiolifx callback and return the message.""" - - def __init__(self): - """Initialize the wrapper.""" - self.message = None - self.event = asyncio.Event() - - @callback - def callback(self, bulb, message): - """Handle responses.""" - self.message = message - self.event.set() - - async def wait(self, method): - """Call an aiolifx method and wait for its response.""" - self.message = None - self.event.clear() - method(callb=self.callback) - - await self.event.wait() - return self.message - - -def convert_8_to_16(value): - """Scale an 8 bit level into 16 bits.""" - return (value << 8) | value - - -def convert_16_to_8(value): - """Scale a 16 bit level into 8 bits.""" - return value >> 8 - - -class LIFXLight(Light): - """Representation of a LIFX light.""" - - def __init__(self, bulb, effects_conductor): - """Initialize the light.""" - self.bulb = bulb - self.effects_conductor = effects_conductor - self.registered = True - self.postponed_update = None - self.lock = asyncio.Lock() - - @property - def device_info(self): - """Return information about the device.""" - info = { - 'identifiers': { - (LIFX_DOMAIN, self.unique_id) - }, - 'name': self.name, - 'connections': { - (dr.CONNECTION_NETWORK_MAC, self.bulb.mac_addr) - }, - 'manufacturer': 'LIFX', - } - - model = aiolifx().products.product_map.get(self.bulb.product) - if model is not None: - info['model'] = model - - return info - - @property - def available(self): - """Return the availability of the bulb.""" - return self.registered - - @property - def unique_id(self): - """Return a unique ID.""" - return self.bulb.mac_addr - - @property - def name(self): - """Return the name of the bulb.""" - return self.bulb.label - - @property - def who(self): - """Return a string identifying the bulb.""" - return "%s (%s)" % (self.bulb.ip_addr, self.name) - - @property - def min_mireds(self): - """Return the coldest color_temp that this light supports.""" - kelvin = lifx_features(self.bulb)['max_kelvin'] - return math.floor(color_util.color_temperature_kelvin_to_mired(kelvin)) - - @property - def max_mireds(self): - """Return the warmest color_temp that this light supports.""" - kelvin = lifx_features(self.bulb)['min_kelvin'] - return math.ceil(color_util.color_temperature_kelvin_to_mired(kelvin)) - - @property - def supported_features(self): - """Flag supported features.""" - support = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION | SUPPORT_EFFECT - - bulb_features = lifx_features(self.bulb) - if bulb_features['min_kelvin'] != bulb_features['max_kelvin']: - support |= SUPPORT_COLOR_TEMP - - return support - - @property - def brightness(self): - """Return the brightness of this light between 0..255.""" - return convert_16_to_8(self.bulb.color[2]) - - @property - def color_temp(self): - """Return the color temperature.""" - _, sat, _, kelvin = self.bulb.color - if sat: - return None - return color_util.color_temperature_kelvin_to_mired(kelvin) - - @property - def is_on(self): - """Return true if light is on.""" - return self.bulb.power_level != 0 - - @property - def effect(self): - """Return the name of the currently running effect.""" - effect = self.effects_conductor.effect(self.bulb) - if effect: - return 'lifx_effect_' + effect.name - return None - - async def update_hass(self, now=None): - """Request new status and push it to hass.""" - self.postponed_update = None - await self.async_update() - await self.async_update_ha_state() - - async def update_during_transition(self, when): - """Update state at the start and end of a transition.""" - if self.postponed_update: - self.postponed_update() - - # Transition has started - await self.update_hass() - - # Transition has ended - if when > 0: - self.postponed_update = async_track_point_in_utc_time( - self.hass, self.update_hass, - util.dt.utcnow() + timedelta(milliseconds=when)) - - async def async_turn_on(self, **kwargs): - """Turn the light on.""" - kwargs[ATTR_POWER] = True - self.hass.async_create_task(self.set_state(**kwargs)) - - async def async_turn_off(self, **kwargs): - """Turn the light off.""" - kwargs[ATTR_POWER] = False - self.hass.async_create_task(self.set_state(**kwargs)) - - async def set_state(self, **kwargs): - """Set a color on the light and turn it on/off.""" - async with self.lock: - bulb = self.bulb - - await self.effects_conductor.stop([bulb]) - - if ATTR_EFFECT in kwargs: - await self.default_effect(**kwargs) - return - - if ATTR_INFRARED in kwargs: - bulb.set_infrared(convert_8_to_16(kwargs[ATTR_INFRARED])) - - if ATTR_TRANSITION in kwargs: - fade = int(kwargs[ATTR_TRANSITION] * 1000) - else: - fade = 0 - - # These are both False if ATTR_POWER is not set - power_on = kwargs.get(ATTR_POWER, False) - power_off = not kwargs.get(ATTR_POWER, True) - - hsbk = find_hsbk(**kwargs) - - # Send messages, waiting for ACK each time - ack = AwaitAioLIFX().wait - - if not self.is_on: - if power_off: - await self.set_power(ack, False) - if hsbk: - await self.set_color(ack, hsbk, kwargs) - if power_on: - await self.set_power(ack, True, duration=fade) - else: - if power_on: - await self.set_power(ack, True) - if hsbk: - await self.set_color(ack, hsbk, kwargs, duration=fade) - if power_off: - await self.set_power(ack, False, duration=fade) - - # Avoid state ping-pong by holding off updates as the state settles - await asyncio.sleep(0.3) - - # Update when the transition starts and ends - await self.update_during_transition(fade) - - async def set_power(self, ack, pwr, duration=0): - """Send a power change to the bulb.""" - await ack(partial(self.bulb.set_power, pwr, duration=duration)) - - async def set_color(self, ack, hsbk, kwargs, duration=0): - """Send a color change to the bulb.""" - hsbk = merge_hsbk(self.bulb.color, hsbk) - await ack(partial(self.bulb.set_color, hsbk, duration=duration)) - - async def default_effect(self, **kwargs): - """Start an effect with default parameters.""" - service = kwargs[ATTR_EFFECT] - data = { - ATTR_ENTITY_ID: self.entity_id, - } - await self.hass.services.async_call(DOMAIN, service, data) - - async def async_update(self): - """Update bulb status.""" - if self.available and not self.lock.locked(): - await AwaitAioLIFX().wait(self.bulb.get_color) - - -class LIFXWhite(LIFXLight): - """Representation of a white-only LIFX light.""" - - @property - def effect_list(self): - """Return the list of supported effects for this light.""" - return [ - SERVICE_EFFECT_PULSE, - SERVICE_EFFECT_STOP, - ] - - -class LIFXColor(LIFXLight): - """Representation of a color LIFX light.""" - - @property - def supported_features(self): - """Flag supported features.""" - support = super().supported_features - support |= SUPPORT_COLOR - return support - - @property - def effect_list(self): - """Return the list of supported effects for this light.""" - return [ - SERVICE_EFFECT_COLORLOOP, - SERVICE_EFFECT_PULSE, - SERVICE_EFFECT_STOP, - ] - - @property - def hs_color(self): - """Return the hs value.""" - hue, sat, _, _ = self.bulb.color - hue = hue / 65535 * 360 - sat = sat / 65535 * 100 - return (hue, sat) if sat else None - - -class LIFXStrip(LIFXColor): - """Representation of a LIFX light strip with multiple zones.""" - - async def set_color(self, ack, hsbk, kwargs, duration=0): - """Send a color change to the bulb.""" - bulb = self.bulb - num_zones = len(bulb.color_zones) - - zones = kwargs.get(ATTR_ZONES) - if zones is None: - # Fast track: setting all zones to the same brightness and color - # can be treated as a single-zone bulb. - if hsbk[2] is not None and hsbk[3] is not None: - await super().set_color(ack, hsbk, kwargs, duration) - return - - zones = list(range(0, num_zones)) - else: - zones = [x for x in set(zones) if x < num_zones] - - # Zone brightness is not reported when powered off - if not self.is_on and hsbk[2] is None: - await self.set_power(ack, True) - await asyncio.sleep(0.3) - await self.update_color_zones() - await self.set_power(ack, False) - await asyncio.sleep(0.3) - - # Send new color to each zone - for index, zone in enumerate(zones): - zone_hsbk = merge_hsbk(bulb.color_zones[zone], hsbk) - apply = 1 if (index == len(zones)-1) else 0 - set_zone = partial(bulb.set_color_zones, - start_index=zone, - end_index=zone, - color=zone_hsbk, - duration=duration, - apply=apply) - await ack(set_zone) - - async def async_update(self): - """Update strip status.""" - if self.available and not self.lock.locked(): - await super().async_update() - await self.update_color_zones() - - async def update_color_zones(self): - """Get updated color information for each zone.""" - zone = 0 - top = 1 - while self.available and zone < top: - # Each get_color_zones can update 8 zones at once - resp = await AwaitAioLIFX().wait(partial( - self.bulb.get_color_zones, - start_index=zone)) - if resp: - zone += 8 - top = resp.count - - # We only await multizone responses so don't ask for just one - if zone == top-1: - zone -= 1 diff --git a/homeassistant/components/light/lightwave.py b/homeassistant/components/light/lightwave.py deleted file mode 100644 index 50c664d90463d..0000000000000 --- a/homeassistant/components/light/lightwave.py +++ /dev/null @@ -1,88 +0,0 @@ -""" -Implements LightwaveRF lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.lightwave/ -""" -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) -from homeassistant.components.lightwave import LIGHTWAVE_LINK -from homeassistant.const import CONF_NAME - -DEPENDENCIES = ['lightwave'] - -MAX_BRIGHTNESS = 255 - - -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Find and return LightWave lights.""" - if not discovery_info: - return - - lights = [] - lwlink = hass.data[LIGHTWAVE_LINK] - - for device_id, device_config in discovery_info.items(): - name = device_config[CONF_NAME] - lights.append(LWRFLight(name, device_id, lwlink)) - - async_add_entities(lights) - - -class LWRFLight(Light): - """Representation of a LightWaveRF light.""" - - def __init__(self, name, device_id, lwlink): - """Initialize LWRFLight entity.""" - self._name = name - self._device_id = device_id - self._state = None - self._brightness = MAX_BRIGHTNESS - self._lwlink = lwlink - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_BRIGHTNESS - - @property - def should_poll(self): - """No polling needed for a LightWave light.""" - return False - - @property - def name(self): - """Lightwave light name.""" - return self._name - - @property - def brightness(self): - """Brightness of this light between 0..MAX_BRIGHTNESS.""" - return self._brightness - - @property - def is_on(self): - """Lightwave light is on state.""" - return self._state - - async def async_turn_on(self, **kwargs): - """Turn the LightWave light on.""" - self._state = True - - if ATTR_BRIGHTNESS in kwargs: - self._brightness = kwargs[ATTR_BRIGHTNESS] - - if self._brightness != MAX_BRIGHTNESS: - self._lwlink.turn_on_with_brightness( - self._device_id, self._name, self._brightness) - else: - self._lwlink.turn_on_light(self._device_id, self._name) - - self.async_schedule_update_ha_state() - - async def async_turn_off(self, **kwargs): - """Turn the LightWave light off.""" - self._state = False - self._lwlink.turn_off(self._device_id, self._name) - self.async_schedule_update_ha_state() diff --git a/homeassistant/components/light/lutron.py b/homeassistant/components/light/lutron.py deleted file mode 100644 index 359ef0114c5b7..0000000000000 --- a/homeassistant/components/light/lutron.py +++ /dev/null @@ -1,89 +0,0 @@ -""" -Support for Lutron lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.lutron/ -""" -import logging - -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) -from homeassistant.components.lutron import ( - LutronDevice, LUTRON_DEVICES, LUTRON_CONTROLLER) - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['lutron'] - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Lutron lights.""" - devs = [] - for (area_name, device) in hass.data[LUTRON_DEVICES]['light']: - dev = LutronLight(area_name, device, hass.data[LUTRON_CONTROLLER]) - devs.append(dev) - - add_entities(devs, True) - - -def to_lutron_level(level): - """Convert the given HASS light level (0-255) to Lutron (0.0-100.0).""" - return float((level * 100) / 255) - - -def to_hass_level(level): - """Convert the given Lutron (0.0-100.0) light level to HASS (0-255).""" - return int((level * 255) / 100) - - -class LutronLight(LutronDevice, Light): - """Representation of a Lutron Light, including dimmable.""" - - def __init__(self, area_name, lutron_device, controller): - """Initialize the light.""" - self._prev_brightness = None - super().__init__(area_name, lutron_device, controller) - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_BRIGHTNESS - - @property - def brightness(self): - """Return the brightness of the light.""" - new_brightness = to_hass_level(self._lutron_device.last_level()) - if new_brightness != 0: - self._prev_brightness = new_brightness - return new_brightness - - def turn_on(self, **kwargs): - """Turn the light on.""" - if ATTR_BRIGHTNESS in kwargs and self._lutron_device.is_dimmable: - brightness = kwargs[ATTR_BRIGHTNESS] - elif self._prev_brightness == 0: - brightness = 255 / 2 - else: - brightness = self._prev_brightness - self._prev_brightness = brightness - self._lutron_device.level = to_lutron_level(brightness) - - def turn_off(self, **kwargs): - """Turn the light off.""" - self._lutron_device.level = 0 - - @property - def device_state_attributes(self): - """Return the state attributes.""" - attr = {'lutron_integration_id': self._lutron_device.id} - return attr - - @property - def is_on(self): - """Return true if device is on.""" - return self._lutron_device.last_level() > 0 - - def update(self): - """Call when forcing a refresh of the device.""" - if self._prev_brightness is None: - self._prev_brightness = to_hass_level(self._lutron_device.level) diff --git a/homeassistant/components/light/lutron_caseta.py b/homeassistant/components/light/lutron_caseta.py deleted file mode 100644 index 21360e71c42a0..0000000000000 --- a/homeassistant/components/light/lutron_caseta.py +++ /dev/null @@ -1,65 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light, DOMAIN) -from homeassistant.components.light.lutron import ( - to_hass_level, to_lutron_level) -from homeassistant.components.lutron_caseta import ( - LUTRON_CASETA_SMARTBRIDGE, LutronCasetaDevice) - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['lutron_caseta'] - - -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] - light_devices = bridge.get_devices_by_domain(DOMAIN) - for light_device in light_devices: - dev = LutronCasetaLight(light_device, bridge) - devs.append(dev) - - async_add_entities(devs, True) - - -class LutronCasetaLight(LutronCasetaDevice, Light): - """Representation of a Lutron Light, including dimmable.""" - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_BRIGHTNESS - - @property - def brightness(self): - """Return the brightness of the light.""" - return to_hass_level(self._state["current_state"]) - - async def async_turn_on(self, **kwargs): - """Turn the light on.""" - brightness = kwargs.get(ATTR_BRIGHTNESS, 255) - self._smartbridge.set_value(self._device_id, - to_lutron_level(brightness)) - - async def async_turn_off(self, **kwargs): - """Turn the light off.""" - self._smartbridge.set_value(self._device_id, 0) - - @property - def is_on(self): - """Return true if device is on.""" - return self._state["current_state"] > 0 - - async def async_update(self): - """Call when forcing a refresh of the device.""" - self._state = self._smartbridge.get_device_by_id(self._device_id) - _LOGGER.debug(self._state) diff --git a/homeassistant/components/light/mochad.py b/homeassistant/components/light/mochad.py deleted file mode 100644 index 2e68c369ba644..0000000000000 --- a/homeassistant/components/light/mochad.py +++ /dev/null @@ -1,137 +0,0 @@ -""" -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/ -""" - -import logging - -import voluptuous as vol - -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light, PLATFORM_SCHEMA) -from homeassistant.components import mochad -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' - - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_PLATFORM): mochad.DOMAIN, - CONF_DEVICES: [{ - vol.Optional(CONF_NAME): cv.string, - vol.Required(CONF_ADDRESS): cv.x10_address, - vol.Optional(mochad.CONF_COMM_TYPE): cv.string, - vol.Optional(CONF_BRIGHTNESS_LEVELS, default=32): - vol.All(vol.Coerce(int), vol.In([32, 64, 256])), - }] -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up X10 dimmers over a mochad controller.""" - devs = config.get(CONF_DEVICES) - add_entities([MochadLight( - hass, mochad.CONTROLLER.ctrl, dev) for dev in devs]) - return True - - -class MochadLight(Light): - """Representation of a X10 dimmer over Mochad.""" - - def __init__(self, hass, ctrl, dev): - """Initialize a Mochad Light Device.""" - from pymochad import device - - self._controller = ctrl - self._address = dev[CONF_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._brightness = 0 - self._state = self._get_device_status() - self._brightness_levels = dev.get(CONF_BRIGHTNESS_LEVELS) - 1 - - @property - def brightness(self): - """Return the brightness of this light between 0..255.""" - return self._brightness - - def _get_device_status(self): - """Get the status of the light from mochad.""" - with mochad.REQ_LOCK: - status = self.light.get_status().rstrip() - return status == 'on' - - @property - def name(self): - """Return the display name of this light.""" - return self._name - - @property - def is_on(self): - """Return true if the light is on.""" - return self._state - - @property - def supported_features(self): - """Return supported features.""" - return SUPPORT_BRIGHTNESS - - @property - def assumed_state(self): - """X10 devices are normally 1-way so we have to assume the state.""" - return True - - def _calculate_brightness_value(self, value): - return int(value * (float(self._brightness_levels) / 255.0)) - - def _adjust_brightness(self, brightness): - if self._brightness > brightness: - bdelta = self._brightness - brightness - mochad_brightness = self._calculate_brightness_value(bdelta) - self.light.send_cmd("dim {}".format(mochad_brightness)) - self._controller.read_data() - elif self._brightness < brightness: - bdelta = brightness - self._brightness - mochad_brightness = self._calculate_brightness_value(bdelta) - self.light.send_cmd("bright {}".format(mochad_brightness)) - self._controller.read_data() - - def turn_on(self, **kwargs): - """Send the command to turn the light on.""" - brightness = kwargs.get(ATTR_BRIGHTNESS, 255) - with mochad.REQ_LOCK: - if self._brightness_levels > 32: - out_brightness = self._calculate_brightness_value(brightness) - self.light.send_cmd('xdim {}'.format(out_brightness)) - self._controller.read_data() - else: - self.light.send_cmd("on") - self._controller.read_data() - # There is no persistence for X10 modules so a fresh on command - # will be full brightness - if self._brightness == 0: - self._brightness = 255 - self._adjust_brightness(brightness) - self._brightness = brightness - self._state = True - - def turn_off(self, **kwargs): - """Send the command to turn the light on.""" - with mochad.REQ_LOCK: - self.light.send_cmd('off') - self._controller.read_data() - # There is no persistence for X10 modules so we need to prepare - # to track a fresh on command will full brightness - if self._brightness_levels == 31: - self._brightness = 0 - self._state = False diff --git a/homeassistant/components/light/mysensors.py b/homeassistant/components/light/mysensors.py deleted file mode 100644 index 23d602c5d400d..0000000000000 --- a/homeassistant/components/light/mysensors.py +++ /dev/null @@ -1,234 +0,0 @@ -""" -Support for MySensors lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.mysensors/ -""" -from homeassistant.components import mysensors -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_WHITE_VALUE, DOMAIN, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_WHITE_VALUE, Light) -from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.util.color import rgb_hex_to_rgb_list -import homeassistant.util.color as color_util - -SUPPORT_MYSENSORS_RGBW = SUPPORT_COLOR | SUPPORT_WHITE_VALUE - - -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): - """Set up the mysensors platform for lights.""" - device_class_map = { - 'S_DIMMER': MySensorsLightDimmer, - 'S_RGB_LIGHT': MySensorsLightRGB, - 'S_RGBW_LIGHT': MySensorsLightRGBW, - } - mysensors.setup_mysensors_platform( - hass, DOMAIN, discovery_info, device_class_map, - async_add_entities=async_add_entities) - - -class MySensorsLight(mysensors.device.MySensorsEntity, Light): - """Representation of a MySensors Light child node.""" - - def __init__(self, *args): - """Initialize a MySensors Light.""" - super().__init__(*args) - self._state = None - self._brightness = None - self._hs = None - self._white = None - - @property - def brightness(self): - """Return the brightness of this light between 0..255.""" - return self._brightness - - @property - def hs_color(self): - """Return the hs color value [int, int].""" - return self._hs - - @property - def white_value(self): - """Return the white value of this light between 0..255.""" - return self._white - - @property - def assumed_state(self): - """Return true if unable to access real state of entity.""" - return self.gateway.optimistic - - @property - def is_on(self): - """Return true if device is on.""" - return self._state - - def _turn_on_light(self): - """Turn on light child device.""" - set_req = self.gateway.const.SetReq - - if self._state: - return - self.gateway.set_child_value( - self.node_id, self.child_id, set_req.V_LIGHT, 1) - - if self.gateway.optimistic: - # optimistically assume that light has changed state - self._state = True - self._values[set_req.V_LIGHT] = STATE_ON - - def _turn_on_dimmer(self, **kwargs): - """Turn on dimmer child device.""" - set_req = self.gateway.const.SetReq - brightness = self._brightness - - if ATTR_BRIGHTNESS not in kwargs or \ - kwargs[ATTR_BRIGHTNESS] == self._brightness or \ - set_req.V_DIMMER not in self._values: - return - brightness = kwargs[ATTR_BRIGHTNESS] - percent = round(100 * brightness / 255) - self.gateway.set_child_value( - self.node_id, self.child_id, set_req.V_DIMMER, percent) - - if self.gateway.optimistic: - # optimistically assume that light has changed state - self._brightness = brightness - self._values[set_req.V_DIMMER] = percent - - def _turn_on_rgb_and_w(self, hex_template, **kwargs): - """Turn on RGB or RGBW child device.""" - rgb = list(color_util.color_hs_to_RGB(*self._hs)) - white = self._white - hex_color = self._values.get(self.value_type) - hs_color = kwargs.get(ATTR_HS_COLOR) - if hs_color is not None: - new_rgb = color_util.color_hs_to_RGB(*hs_color) - else: - new_rgb = None - new_white = kwargs.get(ATTR_WHITE_VALUE) - - if new_rgb is None and new_white is None: - return - if new_rgb is not None: - rgb = list(new_rgb) - if hex_template == '%02x%02x%02x%02x': - if new_white is not None: - rgb.append(new_white) - else: - rgb.append(white) - hex_color = hex_template % tuple(rgb) - if len(rgb) > 3: - white = rgb.pop() - self.gateway.set_child_value( - self.node_id, self.child_id, self.value_type, hex_color) - - if self.gateway.optimistic: - # optimistically assume that light has changed state - self._hs = color_util.color_RGB_to_hs(*rgb) - self._white = white - self._values[self.value_type] = hex_color - - async def async_turn_off(self, **kwargs): - """Turn the device off.""" - value_type = self.gateway.const.SetReq.V_LIGHT - self.gateway.set_child_value( - self.node_id, self.child_id, value_type, 0) - if self.gateway.optimistic: - # optimistically assume that light has changed state - self._state = False - self._values[value_type] = STATE_OFF - self.async_schedule_update_ha_state() - - def _async_update_light(self): - """Update the controller with values from light child.""" - value_type = self.gateway.const.SetReq.V_LIGHT - self._state = self._values[value_type] == STATE_ON - - def _async_update_dimmer(self): - """Update the controller with values from dimmer child.""" - value_type = self.gateway.const.SetReq.V_DIMMER - if value_type in self._values: - self._brightness = round(255 * int(self._values[value_type]) / 100) - if self._brightness == 0: - self._state = False - - def _async_update_rgb_or_w(self): - """Update the controller with values from RGB or RGBW child.""" - value = self._values[self.value_type] - color_list = rgb_hex_to_rgb_list(value) - if len(color_list) > 3: - self._white = color_list.pop() - self._hs = color_util.color_RGB_to_hs(*color_list) - - -class MySensorsLightDimmer(MySensorsLight): - """Dimmer child class to MySensorsLight.""" - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_BRIGHTNESS - - async def async_turn_on(self, **kwargs): - """Turn the device on.""" - self._turn_on_light() - self._turn_on_dimmer(**kwargs) - if self.gateway.optimistic: - self.async_schedule_update_ha_state() - - async def async_update(self): - """Update the controller with the latest value from a sensor.""" - await super().async_update() - self._async_update_light() - self._async_update_dimmer() - - -class MySensorsLightRGB(MySensorsLight): - """RGB child class to MySensorsLight.""" - - @property - def supported_features(self): - """Flag supported features.""" - set_req = self.gateway.const.SetReq - if set_req.V_DIMMER in self._values: - return SUPPORT_BRIGHTNESS | SUPPORT_COLOR - return SUPPORT_COLOR - - async def async_turn_on(self, **kwargs): - """Turn the device on.""" - self._turn_on_light() - self._turn_on_dimmer(**kwargs) - self._turn_on_rgb_and_w('%02x%02x%02x', **kwargs) - if self.gateway.optimistic: - self.async_schedule_update_ha_state() - - async def async_update(self): - """Update the controller with the latest value from a sensor.""" - await super().async_update() - self._async_update_light() - self._async_update_dimmer() - self._async_update_rgb_or_w() - - -class MySensorsLightRGBW(MySensorsLightRGB): - """RGBW child class to MySensorsLightRGB.""" - - # pylint: disable=too-many-ancestors - - @property - def supported_features(self): - """Flag supported features.""" - set_req = self.gateway.const.SetReq - if set_req.V_DIMMER in self._values: - return SUPPORT_BRIGHTNESS | SUPPORT_MYSENSORS_RGBW - return SUPPORT_MYSENSORS_RGBW - - async def async_turn_on(self, **kwargs): - """Turn the device on.""" - self._turn_on_light() - self._turn_on_dimmer(**kwargs) - self._turn_on_rgb_and_w('%02x%02x%02x%02x', **kwargs) - if self.gateway.optimistic: - self.async_schedule_update_ha_state() diff --git a/homeassistant/components/light/plum_lightpad.py b/homeassistant/components/light/plum_lightpad.py deleted file mode 100644 index fa15c842debc8..0000000000000 --- a/homeassistant/components/light/plum_lightpad.py +++ /dev/null @@ -1,185 +0,0 @@ -""" -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/ -""" -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light) -from homeassistant.components.plum_lightpad import PLUM_DATA -import homeassistant.util.color as color_util - -DEPENDENCIES = ['plum_lightpad'] - - -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 - - plum = hass.data[PLUM_DATA] - - entities = [] - - if 'lpid' in discovery_info: - lightpad = plum.get_lightpad(discovery_info['lpid']) - entities.append(GlowRing(lightpad=lightpad)) - - if 'llid' in discovery_info: - logical_load = plum.get_load(discovery_info['llid']) - entities.append(PlumLight(load=logical_load)) - - if entities: - async_add_entities(entities) - - -class PlumLight(Light): - """Represenation of a Plum Lightpad dimmer.""" - - def __init__(self, load): - """Initialize the light.""" - self._load = load - self._brightness = load.level - - async def async_added_to_hass(self): - """Subscribe to dimmerchange events.""" - self._load.add_event_listener('dimmerchange', self.dimmerchange) - - def dimmerchange(self, event): - """Change event handler updating the brightness.""" - self._brightness = event['level'] - self.schedule_update_ha_state() - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return the name of the switch if any.""" - return self._load.name - - @property - def brightness(self) -> int: - """Return the brightness of this switch between 0..255.""" - return self._brightness - - @property - def is_on(self) -> bool: - """Return true if light is on.""" - return self._brightness > 0 - - @property - def supported_features(self): - """Flag supported features.""" - if self._load.dimmable: - return SUPPORT_BRIGHTNESS - return None - - async def async_turn_on(self, **kwargs): - """Turn the light on.""" - if ATTR_BRIGHTNESS in kwargs: - await self._load.turn_on(kwargs[ATTR_BRIGHTNESS]) - else: - await self._load.turn_on() - - async def async_turn_off(self, **kwargs): - """Turn the light off.""" - await self._load.turn_off() - - -class GlowRing(Light): - """Represenation of a Plum Lightpad dimmer glow ring.""" - - def __init__(self, lightpad): - """Initialize the light.""" - self._lightpad = lightpad - self._name = '{} Glow Ring'.format(lightpad.friendly_name) - - self._state = lightpad.glow_enabled - self._brightness = lightpad.glow_intensity * 255.0 - - self._red = lightpad.glow_color['red'] - self._green = lightpad.glow_color['green'] - self._blue = lightpad.glow_color['blue'] - - async def async_added_to_hass(self): - """Subscribe to configchange events.""" - self._lightpad.add_event_listener('configchange', - self.configchange_event) - - def configchange_event(self, event): - """Handle Configuration change event.""" - config = event['changes'] - - self._state = config['glowEnabled'] - self._brightness = config['glowIntensity'] * 255.0 - - self._red = config['glowColor']['red'] - self._green = config['glowColor']['green'] - self._blue = config['glowColor']['blue'] - - self.schedule_update_ha_state() - - @property - def hs_color(self): - """Return the hue and saturation color value [float, float].""" - return color_util.color_RGB_to_hs(self._red, self._green, self._blue) - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return the name of the switch if any.""" - return self._name - - @property - def brightness(self) -> int: - """Return the brightness of this switch between 0..255.""" - return self._brightness - - @property - def glow_intensity(self): - """Brightness in float form.""" - return self._brightness / 255.0 - - @property - def is_on(self) -> bool: - """Return true if light is on.""" - return self._state - - @property - def icon(self): - """Return the crop-portait icon representing the glow ring.""" - return 'mdi:crop-portrait' - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_BRIGHTNESS | SUPPORT_COLOR - - async def async_turn_on(self, **kwargs): - """Turn the light on.""" - if ATTR_BRIGHTNESS in kwargs: - await self._lightpad.set_config( - {"glowIntensity": kwargs[ATTR_BRIGHTNESS]}) - elif ATTR_HS_COLOR in kwargs: - hs_color = kwargs[ATTR_HS_COLOR] - red, green, blue = color_util.color_hs_to_RGB(*hs_color) - await self._lightpad.set_glow_color(red, green, blue, 0) - else: - await self._lightpad.set_config({"glowEnabled": True}) - - async def async_turn_off(self, **kwargs): - """Turn the light off.""" - if ATTR_BRIGHTNESS in kwargs: - await self._lightpad.set_config( - {"glowIntensity": kwargs[ATTR_BRIGHTNESS]}) - else: - await self._lightpad.set_config( - {"glowEnabled": False}) diff --git a/homeassistant/components/light/rflink.py b/homeassistant/components/light/rflink.py index ef389bb84f9bc..726433b4f7030 100644 --- a/homeassistant/components/light/rflink.py +++ b/homeassistant/components/light/rflink.py @@ -185,6 +185,14 @@ def brightness(self): """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 @@ def brightness(self): """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/rfxtrx.py b/homeassistant/components/light/rfxtrx.py deleted file mode 100644 index 1028877348654..0000000000000 --- a/homeassistant/components/light/rfxtrx.py +++ /dev/null @@ -1,85 +0,0 @@ -""" -Support for RFXtrx lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.rfxtrx/ -""" -import logging - -import voluptuous as vol - -from homeassistant.components import rfxtrx -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light, PLATFORM_SCHEMA) -from homeassistant.const import CONF_NAME -from homeassistant.components.rfxtrx import ( - CONF_AUTOMATIC_ADD, CONF_FIRE_EVENT, DEFAULT_SIGNAL_REPETITIONS, - CONF_SIGNAL_REPETITIONS, CONF_DEVICES) -from homeassistant.helpers import config_validation as cv - -DEPENDENCIES = ['rfxtrx'] - -_LOGGER = logging.getLogger(__name__) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_DEVICES, default={}): { - cv.string: vol.Schema({ - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean - }) - }, - vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean, - vol.Optional(CONF_SIGNAL_REPETITIONS, default=DEFAULT_SIGNAL_REPETITIONS): - vol.Coerce(int), -}) - -SUPPORT_RFXTRX = SUPPORT_BRIGHTNESS - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the RFXtrx platform.""" - import RFXtrx as rfxtrxmod - - lights = rfxtrx.get_devices_from_config(config, RfxtrxLight) - add_entities(lights) - - def light_update(event): - """Handle light updates from the RFXtrx gateway.""" - if not isinstance(event.device, rfxtrxmod.LightingDevice) or \ - not event.device.known_to_be_dimmable: - return - - new_device = rfxtrx.get_new_device(event, config, RfxtrxLight) - if new_device: - add_entities([new_device]) - - rfxtrx.apply_received_command(event) - - # Subscribe to main RFXtrx events - if light_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS: - rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(light_update) - - -class RfxtrxLight(rfxtrx.RfxtrxDevice, Light): - """Representation of a RFXtrx light.""" - - @property - def brightness(self): - """Return the brightness of this light between 0..255.""" - return self._brightness - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_RFXTRX - - def turn_on(self, **kwargs): - """Turn the light on.""" - brightness = kwargs.get(ATTR_BRIGHTNESS) - if brightness is None: - self._brightness = 255 - self._send_command('turn_on') - else: - self._brightness = brightness - _brightness = (brightness * 100 // 255) - self._send_command('dim', _brightness) diff --git a/homeassistant/components/light/scsgate.py b/homeassistant/components/light/scsgate.py deleted file mode 100644 index c218e19479141..0000000000000 --- a/homeassistant/components/light/scsgate.py +++ /dev/null @@ -1,121 +0,0 @@ -""" -Support for SCSGate lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.scsgate/ -""" -import logging - -import voluptuous as vol - -from homeassistant.components import scsgate -from homeassistant.components.light import (Light, PLATFORM_SCHEMA) -from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_STATE, CONF_DEVICES, CONF_NAME) -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['scsgate'] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_DEVICES): - cv.schema_with_slug_keys(scsgate.SCSGATE_SCHEMA), -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the SCSGate switches.""" - devices = config.get(CONF_DEVICES) - lights = [] - logger = logging.getLogger(__name__) - - if devices: - for _, entity_info in devices.items(): - if entity_info[scsgate.CONF_SCS_ID] in scsgate.SCSGATE.devices: - continue - - name = entity_info[CONF_NAME] - scs_id = entity_info[scsgate.CONF_SCS_ID] - - logger.info("Adding %s scsgate.light", name) - - light = SCSGateLight(name=name, scs_id=scs_id, logger=logger) - lights.append(light) - - add_entities(lights) - scsgate.SCSGATE.add_devices_to_register(lights) - - -class SCSGateLight(Light): - """Representation of a SCSGate light.""" - - def __init__(self, scs_id, name, logger): - """Initialize the light.""" - self._name = name - self._scs_id = scs_id - self._toggled = False - self._logger = logger - - @property - def scs_id(self): - """Return the SCS ID.""" - return self._scs_id - - @property - def should_poll(self): - """No polling needed for a SCSGate light.""" - return False - - @property - def name(self): - """Return the name of the device if any.""" - return self._name - - @property - def is_on(self): - """Return true if light is on.""" - return self._toggled - - def turn_on(self, **kwargs): - """Turn the device on.""" - from scsgate.tasks import ToggleStatusTask - - scsgate.SCSGATE.append_task( - ToggleStatusTask(target=self._scs_id, toggled=True)) - - self._toggled = True - self.schedule_update_ha_state() - - def turn_off(self, **kwargs): - """Turn the device off.""" - from scsgate.tasks import ToggleStatusTask - - scsgate.SCSGATE.append_task( - ToggleStatusTask(target=self._scs_id, toggled=False)) - - self._toggled = False - self.schedule_update_ha_state() - - def process_event(self, message): - """Handle a SCSGate message related with this light.""" - if self._toggled == message.toggled: - self._logger.info( - "Light %s, ignoring message %s because state already active", - self._scs_id, message) - # Nothing changed, ignoring - return - - self._toggled = message.toggled - self.schedule_update_ha_state() - - command = "off" - if self._toggled: - command = "on" - - self.hass.bus.fire( - 'button_pressed', { - ATTR_ENTITY_ID: self._scs_id, - ATTR_STATE: command, - } - ) diff --git a/homeassistant/components/light/sisyphus.py b/homeassistant/components/light/sisyphus.py deleted file mode 100644 index 75cc86a0154fc..0000000000000 --- a/homeassistant/components/light/sisyphus.py +++ /dev/null @@ -1,78 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.const import CONF_NAME -from homeassistant.components.light import SUPPORT_BRIGHTNESS, Light -from homeassistant.components.sisyphus import DATA_SISYPHUS - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['sisyphus'] - -SUPPORTED_FEATURES = SUPPORT_BRIGHTNESS - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up a single Sisyphus table.""" - name = discovery_info[CONF_NAME] - add_entities( - [SisyphusLight(name, hass.data[DATA_SISYPHUS][name])], - update_before_add=True) - - -class SisyphusLight(Light): - """Represents a Sisyphus table as a light.""" - - def __init__(self, name, table): - """ - Constructor. - - :param name: name of the table - :param table: sisyphus-control Table object - """ - self._name = name - self._table = table - - async def async_added_to_hass(self): - """Add listeners after this object has been initialized.""" - self._table.add_listener( - lambda: self.async_schedule_update_ha_state(False)) - - @property - def name(self): - """Return the ame of the table.""" - return self._name - - @property - def is_on(self): - """Return True if the table is on.""" - return not self._table.is_sleeping - - @property - def brightness(self): - """Return the current brightness of the table's ring light.""" - return self._table.brightness * 255 - - @property - def supported_features(self): - """Return the features supported by the table; i.e. brightness.""" - return SUPPORTED_FEATURES - - async def async_turn_off(self, **kwargs): - """Put the table to sleep.""" - await self._table.sleep() - _LOGGER.debug("Sisyphus table %s: sleep") - - async def async_turn_on(self, **kwargs): - """Wake up the table if necessary, optionally changes brightness.""" - if not self.is_on: - await self._table.wakeup() - _LOGGER.debug("Sisyphus table %s: wakeup") - - if "brightness" in kwargs: - await self._table.set_brightness(kwargs["brightness"] / 255.0) diff --git a/homeassistant/components/light/skybell.py b/homeassistant/components/light/skybell.py deleted file mode 100644 index ecb240f2ef345..0000000000000 --- a/homeassistant/components/light/skybell.py +++ /dev/null @@ -1,89 +0,0 @@ -""" -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/ -""" -import logging - - -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_HS_COLOR, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light) -from homeassistant.components.skybell import ( - DOMAIN as SKYBELL_DOMAIN, SkybellDevice) -import homeassistant.util.color as color_util - -DEPENDENCIES = ['skybell'] - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the platform for a Skybell device.""" - skybell = hass.data.get(SKYBELL_DOMAIN) - - sensors = [] - for device in skybell.get_devices(): - sensors.append(SkybellLight(device)) - - add_entities(sensors, True) - - -def _to_skybell_level(level): - """Convert the given HASS light level (0-255) to Skybell (0-100).""" - return int((level * 100) / 255) - - -def _to_hass_level(level): - """Convert the given Skybell (0-100) light level to HASS (0-255).""" - return int((level * 255) / 100) - - -class SkybellLight(SkybellDevice, Light): - """A binary sensor implementation for Skybell devices.""" - - def __init__(self, device): - """Initialize a light for a Skybell device.""" - super().__init__(device) - self._name = self._device.name - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - def turn_on(self, **kwargs): - """Turn on the light.""" - if ATTR_HS_COLOR in kwargs: - rgb = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) - self._device.led_rgb = rgb - elif ATTR_BRIGHTNESS in kwargs: - self._device.led_intensity = _to_skybell_level( - kwargs[ATTR_BRIGHTNESS]) - else: - self._device.led_intensity = _to_skybell_level(255) - - def turn_off(self, **kwargs): - """Turn off the light.""" - self._device.led_intensity = 0 - - @property - def is_on(self): - """Return true if device is on.""" - return self._device.led_intensity > 0 - - @property - def brightness(self): - """Return the brightness of the light.""" - return _to_hass_level(self._device.led_intensity) - - @property - def hs_color(self): - """Return the color of the light.""" - return color_util.color_RGB_to_hs(*self._device.led_rgb) - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_BRIGHTNESS | SUPPORT_COLOR diff --git a/homeassistant/components/light/tellduslive.py b/homeassistant/components/light/tellduslive.py deleted file mode 100644 index 3f14b34ea78c8..0000000000000 --- a/homeassistant/components/light/tellduslive.py +++ /dev/null @@ -1,80 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.components import light, tellduslive -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) -from homeassistant.components.tellduslive.entry import TelldusLiveEntity -from homeassistant.helpers.dispatcher import async_dispatcher_connect - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Old way of setting up TelldusLive. - - Can only be called when a user accidentally mentions the platform in their - config. But even in that case it would have been ignored. - """ - pass - - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up tellduslive sensors dynamically.""" - async def async_discover_light(device_id): - """Discover and add a discovered sensor.""" - client = hass.data[tellduslive.DOMAIN] - async_add_entities([TelldusLiveLight(client, device_id)]) - - async_dispatcher_connect( - hass, - tellduslive.TELLDUS_DISCOVERY_NEW.format(light.DOMAIN, - tellduslive.DOMAIN), - async_discover_light, - ) - - -class TelldusLiveLight(TelldusLiveEntity, Light): - """Representation of a Tellstick Net light.""" - - def __init__(self, client, device_id): - """Initialize the Tellstick Net light.""" - super().__init__(client, device_id) - self._last_brightness = self.brightness - - def changed(self): - """Define a property of the device that might have changed.""" - self._last_brightness = self.brightness - - @property - def brightness(self): - """Return the brightness of this light between 0..255.""" - return self.device.dim_level - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_BRIGHTNESS - - @property - def is_on(self): - """Return true if light is on.""" - return self.device.is_on - - def turn_on(self, **kwargs): - """Turn the light on.""" - brightness = kwargs.get(ATTR_BRIGHTNESS, self._last_brightness) - self.device.dim(level=brightness) - self.changed() - - def turn_off(self, **kwargs): - """Turn the light off.""" - self.device.turn_off() - self.changed() diff --git a/homeassistant/components/light/tellstick.py b/homeassistant/components/light/tellstick.py deleted file mode 100644 index cf9dd545e99b6..0000000000000 --- a/homeassistant/components/light/tellstick.py +++ /dev/null @@ -1,85 +0,0 @@ -""" -Support for Tellstick lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.tellstick/ -""" - -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) -from homeassistant.components.tellstick import ( - DEFAULT_SIGNAL_REPETITIONS, ATTR_DISCOVER_DEVICES, ATTR_DISCOVER_CONFIG, - DATA_TELLSTICK, TellstickDevice) - - -SUPPORT_TELLSTICK = SUPPORT_BRIGHTNESS - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Tellstick lights.""" - if (discovery_info is None or - discovery_info[ATTR_DISCOVER_DEVICES] is None): - return - - signal_repetitions = discovery_info.get( - ATTR_DISCOVER_CONFIG, DEFAULT_SIGNAL_REPETITIONS) - - add_entities([TellstickLight(hass.data[DATA_TELLSTICK][tellcore_id], - signal_repetitions) - for tellcore_id in discovery_info[ATTR_DISCOVER_DEVICES]], - True) - - -class TellstickLight(TellstickDevice, Light): - """Representation of a Tellstick light.""" - - def __init__(self, tellcore_device, signal_repetitions): - """Initialize the Tellstick light.""" - super().__init__(tellcore_device, signal_repetitions) - - self._brightness = 255 - - @property - def brightness(self): - """Return the brightness of this light between 0..255.""" - return self._brightness - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_TELLSTICK - - def _parse_ha_data(self, kwargs): - """Turn the value from HA into something useful.""" - return kwargs.get(ATTR_BRIGHTNESS) - - def _parse_tellcore_data(self, tellcore_data): - """Turn the value received from tellcore into something useful.""" - if tellcore_data: - return int(tellcore_data) # brightness - return None - - def _update_model(self, new_state, data): - """Update the device entity state to match the arguments.""" - if new_state: - brightness = data - if brightness is not None: - self._brightness = brightness - - # _brightness is not defined when called from super - try: - self._state = (self._brightness > 0) - except AttributeError: - self._state = True - else: - self._state = False - - def _send_device_command(self, requested_state, requested_data): - """Let tellcore update the actual device to the requested state.""" - if requested_state: - if requested_data is not None: - self._brightness = int(requested_data) - - self._tellcore_device.dim(self._brightness) - else: - self._tellcore_device.turn_off() diff --git a/homeassistant/components/light/tplink.py b/homeassistant/components/light/tplink.py index b0f4c6d1a3c12..bd1621a0b358f 100644 --- a/homeassistant/components/light/tplink.py +++ b/homeassistant/components/light/tplink.py @@ -19,7 +19,7 @@ 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/light/tradfri.py b/homeassistant/components/light/tradfri.py deleted file mode 100644 index 50e92f15e3c3c..0000000000000 --- a/homeassistant/components/light/tradfri.py +++ /dev/null @@ -1,378 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.core import callback -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, - SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, SUPPORT_COLOR_TEMP, - SUPPORT_COLOR, Light) -from homeassistant.components.light import \ - PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA -from homeassistant.components.tradfri import ( - KEY_GATEWAY, KEY_API, DOMAIN as TRADFRI_DOMAIN) -from homeassistant.components.tradfri.const import ( - CONF_IMPORT_GROUPS, CONF_GATEWAY_ID) -import homeassistant.util.color as color_util - -_LOGGER = logging.getLogger(__name__) - -ATTR_DIMMER = 'dimmer' -ATTR_HUE = 'hue' -ATTR_SAT = 'saturation' -ATTR_TRANSITION_TIME = 'transition_time' -DEPENDENCIES = ['tradfri'] -PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA -IKEA = 'IKEA of Sweden' -TRADFRI_LIGHT_MANAGER = 'Tradfri Light Manager' -SUPPORTED_FEATURES = SUPPORT_TRANSITION -SUPPORTED_GROUP_FEATURES = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION - - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Load Tradfri lights based on a config entry.""" - gateway_id = config_entry.data[CONF_GATEWAY_ID] - api = hass.data[KEY_API][config_entry.entry_id] - gateway = hass.data[KEY_GATEWAY][config_entry.entry_id] - - devices_commands = await api(gateway.get_devices()) - devices = await api(devices_commands) - lights = [dev for dev in devices if dev.has_light_control] - if lights: - async_add_entities( - TradfriLight(light, api, gateway_id) for light in lights) - - if config_entry.data[CONF_IMPORT_GROUPS]: - groups_commands = await api(gateway.get_groups()) - groups = await api(groups_commands) - if groups: - async_add_entities( - TradfriGroup(group, api, gateway_id) for group in groups) - - -class TradfriGroup(Light): - """The platform class required by hass.""" - - def __init__(self, group, api, gateway_id): - """Initialize a Group.""" - self._api = api - self._unique_id = "group-{}-{}".format(gateway_id, group.id) - self._group = group - self._name = group.name - - self._refresh(group) - - async def async_added_to_hass(self): - """Start thread when added to hass.""" - self._async_start_observe() - - @property - def unique_id(self): - """Return unique ID for this group.""" - return self._unique_id - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORTED_GROUP_FEATURES - - @property - def name(self): - """Return the display name of this group.""" - return self._name - - @property - def is_on(self): - """Return true if group lights are on.""" - return self._group.state - - @property - def brightness(self): - """Return the brightness of the group lights.""" - return self._group.dimmer - - async def async_turn_off(self, **kwargs): - """Instruct the group lights to turn off.""" - await self._api(self._group.set_state(0)) - - async def async_turn_on(self, **kwargs): - """Instruct the group lights to turn on, or dim.""" - keys = {} - if ATTR_TRANSITION in kwargs: - keys['transition_time'] = int(kwargs[ATTR_TRANSITION]) * 10 - - if ATTR_BRIGHTNESS in kwargs: - if kwargs[ATTR_BRIGHTNESS] == 255: - kwargs[ATTR_BRIGHTNESS] = 254 - - await self._api( - self._group.set_dimmer(kwargs[ATTR_BRIGHTNESS], **keys)) - else: - await self._api(self._group.set_state(1)) - - @callback - def _async_start_observe(self, exc=None): - """Start observation of light.""" - # pylint: disable=import-error - from pytradfri.error import PytradfriError - if exc: - _LOGGER.warning("Observation failed for %s", self._name, - exc_info=exc) - - try: - cmd = self._group.observe(callback=self._observe_update, - err_callback=self._async_start_observe, - duration=0) - self.hass.async_create_task(self._api(cmd)) - except PytradfriError as err: - _LOGGER.warning("Observation failed, trying again", exc_info=err) - self._async_start_observe() - - def _refresh(self, group): - """Refresh the light data.""" - self._group = group - self._name = group.name - - @callback - def _observe_update(self, tradfri_device): - """Receive new state data for this light.""" - self._refresh(tradfri_device) - self.async_schedule_update_ha_state() - - async def async_update(self): - """Fetch new state data for the group.""" - await self._api(self._group.update()) - - -class TradfriLight(Light): - """The platform class required by Home Assistant.""" - - def __init__(self, light, api, gateway_id): - """Initialize a Light.""" - self._api = api - self._unique_id = "light-{}-{}".format(gateway_id, light.id) - self._light = None - self._light_control = None - self._light_data = None - self._name = None - self._hs_color = None - self._features = SUPPORTED_FEATURES - self._available = True - self._gateway_id = gateway_id - - self._refresh(light) - - @property - def unique_id(self): - """Return unique ID for light.""" - return self._unique_id - - @property - def device_info(self): - """Return the device info.""" - info = self._light.device_info - - return { - 'identifiers': { - (TRADFRI_DOMAIN, self._light.id) - }, - 'name': self._name, - 'manufacturer': info.manufacturer, - 'model': info.model_number, - 'sw_version': info.firmware_version, - 'via_hub': (TRADFRI_DOMAIN, self._gateway_id), - } - - @property - def min_mireds(self): - """Return the coldest color_temp that this light supports.""" - return self._light_control.min_mireds - - @property - def max_mireds(self): - """Return the warmest color_temp that this light supports.""" - return self._light_control.max_mireds - - async def async_added_to_hass(self): - """Start thread when added to hass.""" - self._async_start_observe() - - @property - def available(self): - """Return True if entity is available.""" - return self._available - - @property - def should_poll(self): - """No polling needed for tradfri light.""" - return False - - @property - def supported_features(self): - """Flag supported features.""" - return self._features - - @property - def name(self): - """Return the display name of this light.""" - return self._name - - @property - def is_on(self): - """Return true if light is on.""" - return self._light_data.state - - @property - def brightness(self): - """Return the brightness of the light.""" - return self._light_data.dimmer - - @property - def color_temp(self): - """Return the color temp value in mireds.""" - return self._light_data.color_temp - - @property - def hs_color(self): - """HS color of the light.""" - if self._light_control.can_set_color: - hsbxy = self._light_data.hsb_xy_color - hue = hsbxy[0] / (self._light_control.max_hue / 360) - sat = hsbxy[1] / (self._light_control.max_saturation / 100) - if hue is not None and sat is not None: - return hue, sat - - async def async_turn_off(self, **kwargs): - """Instruct the light to turn off.""" - # This allows transitioning to off, but resets the brightness - # to 1 for the next set_state(True) command - transition_time = None - if ATTR_TRANSITION in kwargs: - transition_time = int(kwargs[ATTR_TRANSITION]) * 10 - - dimmer_data = {ATTR_DIMMER: 0, ATTR_TRANSITION_TIME: - transition_time} - await self._api(self._light_control.set_dimmer(**dimmer_data)) - else: - await self._api(self._light_control.set_state(False)) - - async def async_turn_on(self, **kwargs): - """Instruct the light to turn on.""" - transition_time = None - if ATTR_TRANSITION in kwargs: - transition_time = int(kwargs[ATTR_TRANSITION]) * 10 - - dimmer_command = None - if ATTR_BRIGHTNESS in kwargs: - brightness = kwargs[ATTR_BRIGHTNESS] - if brightness > 254: - brightness = 254 - elif brightness < 0: - brightness = 0 - dimmer_data = {ATTR_DIMMER: brightness, ATTR_TRANSITION_TIME: - transition_time} - dimmer_command = self._light_control.set_dimmer(**dimmer_data) - transition_time = None - else: - dimmer_command = self._light_control.set_state(True) - - color_command = None - if ATTR_HS_COLOR in kwargs and self._light_control.can_set_color: - hue = int(kwargs[ATTR_HS_COLOR][0] * - (self._light_control.max_hue / 360)) - sat = int(kwargs[ATTR_HS_COLOR][1] * - (self._light_control.max_saturation / 100)) - color_data = {ATTR_HUE: hue, ATTR_SAT: sat, ATTR_TRANSITION_TIME: - transition_time} - color_command = self._light_control.set_hsb(**color_data) - transition_time = None - - temp_command = None - if ATTR_COLOR_TEMP in kwargs and (self._light_control.can_set_temp or - self._light_control.can_set_color): - temp = kwargs[ATTR_COLOR_TEMP] - # White Spectrum bulb - if self._light_control.can_set_temp: - if temp > self.max_mireds: - temp = self.max_mireds - elif temp < self.min_mireds: - temp = self.min_mireds - temp_data = {ATTR_COLOR_TEMP: temp, ATTR_TRANSITION_TIME: - transition_time} - temp_command = self._light_control.set_color_temp(**temp_data) - transition_time = None - # Color bulb (CWS) - # color_temp needs to be set with hue/saturation - elif self._light_control.can_set_color: - temp_k = color_util.color_temperature_mired_to_kelvin(temp) - hs_color = color_util.color_temperature_to_hs(temp_k) - hue = int(hs_color[0] * (self._light_control.max_hue / 360)) - sat = int(hs_color[1] * - (self._light_control.max_saturation / 100)) - color_data = {ATTR_HUE: hue, ATTR_SAT: sat, - ATTR_TRANSITION_TIME: transition_time} - color_command = self._light_control.set_hsb(**color_data) - transition_time = None - - # HSB can always be set, but color temp + brightness is bulb dependant - command = dimmer_command - if command is not None: - command += color_command - else: - command = color_command - - if self._light_control.can_combine_commands: - await self._api(command + temp_command) - else: - if temp_command is not None: - await self._api(temp_command) - if command is not None: - await self._api(command) - - @callback - def _async_start_observe(self, exc=None): - """Start observation of light.""" - # pylint: disable=import-error - from pytradfri.error import PytradfriError - if exc: - self._available = False - self.async_schedule_update_ha_state() - _LOGGER.warning("Observation failed for %s", self._name, - exc_info=exc) - - try: - cmd = self._light.observe(callback=self._observe_update, - err_callback=self._async_start_observe, - duration=0) - self.hass.async_create_task(self._api(cmd)) - except PytradfriError as err: - _LOGGER.warning("Observation failed, trying again", exc_info=err) - self._async_start_observe() - - def _refresh(self, light): - """Refresh the light data.""" - self._light = light - - # Caching of LightControl and light object - self._available = light.reachable - self._light_control = light.light_control - self._light_data = light.light_control.lights[0] - self._name = light.name - self._features = SUPPORTED_FEATURES - - if light.light_control.can_set_dimmer: - self._features |= SUPPORT_BRIGHTNESS - if light.light_control.can_set_color: - self._features |= SUPPORT_COLOR - if light.light_control.can_set_temp: - self._features |= SUPPORT_COLOR_TEMP - - @callback - def _observe_update(self, tradfri_device): - """Receive new state data for this light.""" - self._refresh(tradfri_device) - self.async_schedule_update_ha_state() diff --git a/homeassistant/components/light/tuya.py b/homeassistant/components/light/tuya.py deleted file mode 100644 index 0a1468a6a5101..0000000000000 --- a/homeassistant/components/light/tuya.py +++ /dev/null @@ -1,102 +0,0 @@ -""" -Support for the Tuya light. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.tuya/ -""" -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ENTITY_ID_FORMAT, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR, Light) - -from homeassistant.components.tuya import DATA_TUYA, TuyaDevice -from homeassistant.util import color as colorutil - -DEPENDENCIES = ['tuya'] - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Tuya light platform.""" - if discovery_info is None: - return - tuya = hass.data[DATA_TUYA] - dev_ids = discovery_info.get('dev_ids') - devices = [] - for dev_id in dev_ids: - device = tuya.get_device_by_id(dev_id) - if device is None: - continue - devices.append(TuyaLight(device)) - add_entities(devices) - - -class TuyaLight(TuyaDevice, Light): - """Tuya light device.""" - - def __init__(self, tuya): - """Init Tuya light device.""" - super().__init__(tuya) - self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id()) - - @property - def brightness(self): - """Return the brightness of the light.""" - return int(self.tuya.brightness()) - - @property - def hs_color(self): - """Return the hs_color of the light.""" - return tuple(map(int, self.tuya.hs_color())) - - @property - def color_temp(self): - """Return the color_temp of the light.""" - color_temp = int(self.tuya.color_temp()) - if color_temp is None: - return None - return colorutil.color_temperature_kelvin_to_mired(color_temp) - - @property - def is_on(self): - """Return true if light is on.""" - return self.tuya.state() - - @property - def min_mireds(self): - """Return color temperature min mireds.""" - return colorutil.color_temperature_kelvin_to_mired( - self.tuya.min_color_temp()) - - @property - def max_mireds(self): - """Return color temperature max mireds.""" - return colorutil.color_temperature_kelvin_to_mired( - self.tuya.max_color_temp()) - - def turn_on(self, **kwargs): - """Turn on or control the light.""" - if (ATTR_BRIGHTNESS not in kwargs - and ATTR_HS_COLOR not in kwargs - and ATTR_COLOR_TEMP not in kwargs): - self.tuya.turn_on() - if ATTR_BRIGHTNESS in kwargs: - self.tuya.set_brightness(kwargs[ATTR_BRIGHTNESS]) - if ATTR_HS_COLOR in kwargs: - self.tuya.set_color(kwargs[ATTR_HS_COLOR]) - if ATTR_COLOR_TEMP in kwargs: - color_temp = colorutil.color_temperature_mired_to_kelvin( - kwargs[ATTR_COLOR_TEMP]) - self.tuya.set_color_temp(color_temp) - - def turn_off(self, **kwargs): - """Instruct the light to turn off.""" - self.tuya.turn_off() - - @property - def supported_features(self): - """Flag supported features.""" - supports = SUPPORT_BRIGHTNESS - if self.tuya.support_color(): - supports = supports | SUPPORT_COLOR - if self.tuya.support_color_temp(): - supports = supports | SUPPORT_COLOR_TEMP - return supports diff --git a/homeassistant/components/light/vera.py b/homeassistant/components/light/vera.py deleted file mode 100644 index 702236ac748f8..0000000000000 --- a/homeassistant/components/light/vera.py +++ /dev/null @@ -1,88 +0,0 @@ -""" -Support for Vera lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.vera/ -""" -import logging - -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_HS_COLOR, ENTITY_ID_FORMAT, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light) -from homeassistant.components.vera import ( - VERA_CONTROLLER, VERA_DEVICES, VeraDevice) -import homeassistant.util.color as color_util - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['vera'] - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Vera lights.""" - add_entities( - [VeraLight(device, hass.data[VERA_CONTROLLER]) for - device in hass.data[VERA_DEVICES]['light']], True) - - -class VeraLight(VeraDevice, Light): - """Representation of a Vera Light, including dimmable.""" - - def __init__(self, vera_device, controller): - """Initialize the light.""" - self._state = False - self._color = None - self._brightness = None - VeraDevice.__init__(self, vera_device, controller) - self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) - - @property - def brightness(self): - """Return the brightness of the light.""" - return self._brightness - - @property - def hs_color(self): - """Return the color of the light.""" - return self._color - - @property - def supported_features(self): - """Flag supported features.""" - if self._color: - return SUPPORT_BRIGHTNESS | SUPPORT_COLOR - return SUPPORT_BRIGHTNESS - - def turn_on(self, **kwargs): - """Turn the light on.""" - if ATTR_HS_COLOR in kwargs and self._color: - rgb = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) - self.vera_device.set_color(rgb) - elif ATTR_BRIGHTNESS in kwargs and self.vera_device.is_dimmable: - self.vera_device.set_brightness(kwargs[ATTR_BRIGHTNESS]) - else: - self.vera_device.switch_on() - - self._state = True - self.schedule_update_ha_state(True) - - def turn_off(self, **kwargs): - """Turn the light off.""" - self.vera_device.switch_off() - self._state = False - self.schedule_update_ha_state() - - @property - def is_on(self): - """Return true if device is on.""" - return self._state - - def update(self): - """Call to update state.""" - self._state = self.vera_device.is_switched_on() - if self.vera_device.is_dimmable: - # If it is dimmable, both functions exist. In case color - # is not supported, it will return None - self._brightness = self.vera_device.get_brightness() - rgb = self.vera_device.get_color() - self._color = color_util.color_RGB_to_hs(*rgb) if rgb else None diff --git a/homeassistant/components/light/wemo.py b/homeassistant/components/light/wemo.py deleted file mode 100644 index 38044b7a7362d..0000000000000 --- a/homeassistant/components/light/wemo.py +++ /dev/null @@ -1,324 +0,0 @@ -""" -Support for Belkin WeMo lights. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/light.wemo/ -""" -import asyncio -import logging -from datetime import timedelta - -import requests -import async_timeout - -from homeassistant import util -from homeassistant.components.light import ( - Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR, SUPPORT_TRANSITION) -from homeassistant.exceptions import PlatformNotReady -import homeassistant.util.color as color_util - -DEPENDENCIES = ['wemo'] - -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) - -_LOGGER = logging.getLogger(__name__) - -SUPPORT_WEMO = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_COLOR | - SUPPORT_TRANSITION) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up discovered WeMo switches.""" - from pywemo import discovery - - if discovery_info is not None: - location = discovery_info['ssdp_description'] - mac = discovery_info['mac_address'] - - try: - 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) - raise PlatformNotReady - - if device.model_name == 'Dimmer': - add_entities([WemoDimmer(device)]) - else: - setup_bridge(device, add_entities) - - -def setup_bridge(bridge, add_entities): - """Set up a WeMo link.""" - lights = {} - - @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) - def update_lights(): - """Update the WeMo led objects with latest info from the bridge.""" - bridge.bridge_update() - - new_lights = [] - - for light_id, device in bridge.Lights.items(): - if light_id not in lights: - lights[light_id] = WemoLight(device, update_lights) - new_lights.append(lights[light_id]) - - if new_lights: - add_entities(new_lights) - - update_lights() - - -class WemoLight(Light): - """Representation of a WeMo light.""" - - def __init__(self, device, update_lights): - """Initialize the WeMo light.""" - self.wemo = device - self._state = None - self._update_lights = update_lights - self._available = True - self._update_lock = None - self._brightness = None - self._hs_color = None - self._color_temp = None - self._is_on = None - self._name = self.wemo.name - self._unique_id = self.wemo.uniqueID - - async def async_added_to_hass(self): - """Wemo light added to HASS.""" - # Define inside async context so we know our event loop - self._update_lock = asyncio.Lock() - - @property - def unique_id(self): - """Return the ID of this light.""" - return self._unique_id - - @property - def name(self): - """Return the name of the light.""" - return self._name - - @property - def brightness(self): - """Return the brightness of this light between 0..255.""" - return self._brightness - - @property - def hs_color(self): - """Return the hs color values of this light.""" - return self._hs_color - - @property - def color_temp(self): - """Return the color temperature of this light in mireds.""" - return self._color_temp - - @property - def is_on(self): - """Return true if device is on.""" - return self._is_on - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_WEMO - - @property - def available(self): - """Return if light is available.""" - return self._available - - def turn_on(self, **kwargs): - """Turn the light on.""" - transitiontime = int(kwargs.get(ATTR_TRANSITION, 0)) - - hs_color = kwargs.get(ATTR_HS_COLOR) - - if hs_color is not None: - xy_color = color_util.color_hs_to_xy(*hs_color) - self.wemo.set_color(xy_color, transition=transitiontime) - - if ATTR_COLOR_TEMP in kwargs: - colortemp = kwargs[ATTR_COLOR_TEMP] - self.wemo.set_temperature(mireds=colortemp, - transition=transitiontime) - - if ATTR_BRIGHTNESS in kwargs: - brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness or 255) - self.wemo.turn_on(level=brightness, transition=transitiontime) - else: - self.wemo.turn_on(transition=transitiontime) - - def turn_off(self, **kwargs): - """Turn the light off.""" - transitiontime = int(kwargs.get(ATTR_TRANSITION, 0)) - self.wemo.turn_off(transition=transitiontime) - - def _update(self, force_update=True): - """Synchronize state with bridge.""" - self._update_lights(no_throttle=force_update) - self._state = self.wemo.state - - self._is_on = self._state.get('onoff') != 0 - self._brightness = self._state.get('level', 255) - self._color_temp = self._state.get('temperature_mireds') - self._available = True - - xy_color = self._state.get('color_xy') - - if xy_color: - self._hs_color = color_util.color_xy_to_hs(*xy_color) - else: - self._hs_color = None - - async def async_update(self): - """Synchronize state with bridge.""" - # If an update is in progress, we don't do anything - if self._update_lock.locked(): - return - - try: - with async_timeout.timeout(5): - await asyncio.shield(self._async_locked_update(True)) - except asyncio.TimeoutError: - _LOGGER.warning('Lost connection to %s', self.name) - self._available = False - - async def _async_locked_update(self, force_update): - """Try updating within an async lock.""" - async with self._update_lock: - await self.hass.async_add_executor_job(self._update, force_update) - - -class WemoDimmer(Light): - """Representation of a WeMo dimmer.""" - - def __init__(self, device): - """Initialize the WeMo dimmer.""" - self.wemo = device - self._state = None - self._available = True - self._update_lock = None - self._brightness = None - self._model_name = self.wemo.model_name - self._name = self.wemo.name - self._serialnumber = self.wemo.serialnumber - - def _subscription_callback(self, _device, _type, _params): - """Update the state by the Wemo device.""" - _LOGGER.debug("Subscription update for %s", self.name) - updated = self.wemo.subscription_update(_type, _params) - self.hass.add_job( - self._async_locked_subscription_callback(not updated)) - - async def _async_locked_subscription_callback(self, force_update): - """Handle an update from a subscription.""" - # If an update is in progress, we don't do anything - if self._update_lock.locked(): - return - - await self._async_locked_update(force_update) - self.async_schedule_update_ha_state() - - async def async_added_to_hass(self): - """Wemo dimmer added to HASS.""" - # Define inside async context so we know our event loop - self._update_lock = asyncio.Lock() - - registry = self.hass.components.wemo.SUBSCRIPTION_REGISTRY - await self.hass.async_add_executor_job(registry.register, self.wemo) - registry.on(self.wemo, None, self._subscription_callback) - - async def async_update(self): - """Update WeMo state. - - Wemo has an aggressive retry logic that sometimes can take over a - minute to return. If we don't get a state after 5 seconds, assume the - Wemo dimmer is unreachable. If update goes through, it will be made - available again. - """ - # If an update is in progress, we don't do anything - if self._update_lock.locked(): - return - - try: - with async_timeout.timeout(5): - await asyncio.shield(self._async_locked_update(True)) - except asyncio.TimeoutError: - _LOGGER.warning('Lost connection to %s', self.name) - self._available = False - self.wemo.reconnect_with_device() - - async def _async_locked_update(self, force_update): - """Try updating within an async lock.""" - async with self._update_lock: - await self.hass.async_add_executor_job(self._update, force_update) - - @property - def unique_id(self): - """Return the ID of this WeMo dimmer.""" - return self._serialnumber - - @property - def name(self): - """Return the name of the dimmer if any.""" - return self._name - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_BRIGHTNESS - - @property - def brightness(self): - """Return the brightness of this light between 1 and 100.""" - return self._brightness - - @property - def is_on(self): - """Return true if dimmer is on. Standby is on.""" - return self._state - - def _update(self, force_update=True): - """Update the device state.""" - try: - self._state = self.wemo.get_state(force_update) - - wemobrightness = int(self.wemo.get_brightness(force_update)) - self._brightness = int((wemobrightness * 255) / 100) - - if not self._available: - _LOGGER.info('Reconnected to %s', self.name) - self._available = True - except AttributeError as err: - _LOGGER.warning("Could not update status for %s (%s)", - self.name, err) - self._available = False - - def turn_on(self, **kwargs): - """Turn the dimmer on.""" - self.wemo.on() - - # Wemo dimmer switches use a range of [0, 100] to control - # brightness. Level 255 might mean to set it to previous value - if ATTR_BRIGHTNESS in kwargs: - brightness = kwargs[ATTR_BRIGHTNESS] - brightness = int((brightness / 255) * 100) - else: - brightness = 255 - self.wemo.set_brightness(brightness) - - def turn_off(self, **kwargs): - """Turn the dimmer off.""" - self.wemo.off() - - @property - def available(self): - """Return if dimmer is available.""" - return self._available diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py deleted file mode 100644 index 96c8f20679e09..0000000000000 --- a/homeassistant/components/light/wink.py +++ /dev/null @@ -1,112 +0,0 @@ -""" -Support for Wink lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.wink/ -""" - -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_COLOR, Light) -from homeassistant.components.wink import DOMAIN, WinkDevice -from homeassistant.util import color as color_util -from homeassistant.util.color import \ - color_temperature_mired_to_kelvin as mired_to_kelvin - -DEPENDENCIES = ['wink'] - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Wink lights.""" - import pywink - - for light in pywink.get_light_bulbs(): - _id = light.object_id() + light.name() - if _id not in hass.data[DOMAIN]['unique_ids']: - add_entities([WinkLight(light, hass)]) - for light in pywink.get_light_groups(): - _id = light.object_id() + light.name() - if _id not in hass.data[DOMAIN]['unique_ids']: - add_entities([WinkLight(light, hass)]) - - -class WinkLight(WinkDevice, Light): - """Representation of a Wink light.""" - - async def async_added_to_hass(self): - """Call when entity is added to hass.""" - self.hass.data[DOMAIN]['entities']['light'].append(self) - - @property - def is_on(self): - """Return true if light is on.""" - return self.wink.state() - - @property - def brightness(self): - """Return the brightness of the light.""" - if self.wink.brightness() is not None: - return int(self.wink.brightness() * 255) - return None - - @property - def hs_color(self): - """Define current bulb color.""" - if self.wink.supports_xy_color(): - return color_util.color_xy_to_hs(*self.wink.color_xy()) - - if self.wink.supports_hue_saturation(): - hue = self.wink.color_hue() - saturation = self.wink.color_saturation() - if hue is not None and saturation is not None: - return hue*360, saturation*100 - - return None - - @property - def color_temp(self): - """Define current bulb color in degrees Kelvin.""" - if not self.wink.supports_temperature(): - return None - return color_util.color_temperature_kelvin_to_mired( - self.wink.color_temperature_kelvin()) - - @property - def supported_features(self): - """Flag supported features.""" - supports = SUPPORT_BRIGHTNESS - if self.wink.supports_temperature(): - supports = supports | SUPPORT_COLOR_TEMP - if self.wink.supports_xy_color(): - supports = supports | SUPPORT_COLOR - elif self.wink.supports_hue_saturation(): - supports = supports | SUPPORT_COLOR - return supports - - def turn_on(self, **kwargs): - """Turn the switch on.""" - brightness = kwargs.get(ATTR_BRIGHTNESS) - hs_color = kwargs.get(ATTR_HS_COLOR) - color_temp_mired = kwargs.get(ATTR_COLOR_TEMP) - - state_kwargs = {} - - if hs_color: - if self.wink.supports_xy_color(): - xy_color = color_util.color_hs_to_xy(*hs_color) - state_kwargs['color_xy'] = xy_color - if self.wink.supports_hue_saturation(): - hs_scaled = hs_color[0]/360, hs_color[1]/100 - state_kwargs['color_hue_saturation'] = hs_scaled - - if color_temp_mired: - state_kwargs['color_kelvin'] = mired_to_kelvin(color_temp_mired) - - if brightness: - state_kwargs['brightness'] = brightness / 255.0 - - self.wink.set_state(True, **state_kwargs) - - def turn_off(self, **kwargs): - """Turn the switch off.""" - self.wink.set_state(False) diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py deleted file mode 100644 index 62433ca9f9738..0000000000000 --- a/homeassistant/components/light/xiaomi_miio.py +++ /dev/null @@ -1,818 +0,0 @@ -""" -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/ -""" -import asyncio -import datetime -from datetime import timedelta -from functools import partial -import logging -from math import ceil - -import voluptuous as vol - -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_ENTITY_ID, DOMAIN, PLATFORM_SCHEMA, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, Light) -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN -from homeassistant.exceptions import PlatformNotReady -import homeassistant.helpers.config_validation as cv -from homeassistant.util import color, dt - -REQUIREMENTS = ['python-miio==0.4.4', 'construct==2.9.45'] - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = 'Xiaomi Philips Light' -DATA_KEY = 'light.xiaomi_miio' - -CONF_MODEL = 'model' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_MODEL): vol.In( - ['philips.light.sread1', - 'philips.light.ceiling', - 'philips.light.zyceiling', - 'philips.light.moonlight', - 'philips.light.bulb', - 'philips.light.candle', - 'philips.light.candle2', - 'philips.light.mono1', - 'philips.light.downlight', - ]), -}) - -# The light does not accept cct values < 1 -CCT_MIN = 1 -CCT_MAX = 100 - -DELAYED_TURN_OFF_MAX_DEVIATION_SECONDS = 4 -DELAYED_TURN_OFF_MAX_DEVIATION_MINUTES = 1 - -SUCCESS = ['ok'] -ATTR_MODEL = 'model' -ATTR_SCENE = 'scene' -ATTR_DELAYED_TURN_OFF = 'delayed_turn_off' -ATTR_TIME_PERIOD = 'time_period' -ATTR_NIGHT_LIGHT_MODE = 'night_light_mode' -ATTR_AUTOMATIC_COLOR_TEMPERATURE = 'automatic_color_temperature' -ATTR_REMINDER = 'reminder' -ATTR_EYECARE_MODE = 'eyecare_mode' - -# Moonlight -ATTR_SLEEP_ASSISTANT = 'sleep_assistant' -ATTR_SLEEP_OFF_TIME = 'sleep_off_time' -ATTR_TOTAL_ASSISTANT_SLEEP_TIME = 'total_assistant_sleep_time' -ATTR_BRAND_SLEEP = 'brand_sleep' -ATTR_BRAND = 'brand' - -SERVICE_SET_SCENE = 'xiaomi_miio_set_scene' -SERVICE_SET_DELAYED_TURN_OFF = 'xiaomi_miio_set_delayed_turn_off' -SERVICE_REMINDER_ON = 'xiaomi_miio_reminder_on' -SERVICE_REMINDER_OFF = 'xiaomi_miio_reminder_off' -SERVICE_NIGHT_LIGHT_MODE_ON = 'xiaomi_miio_night_light_mode_on' -SERVICE_NIGHT_LIGHT_MODE_OFF = 'xiaomi_miio_night_light_mode_off' -SERVICE_EYECARE_MODE_ON = 'xiaomi_miio_eyecare_mode_on' -SERVICE_EYECARE_MODE_OFF = 'xiaomi_miio_eyecare_mode_off' - -XIAOMI_MIIO_SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, -}) - -SERVICE_SCHEMA_SET_SCENE = XIAOMI_MIIO_SERVICE_SCHEMA.extend({ - vol.Required(ATTR_SCENE): - vol.All(vol.Coerce(int), vol.Clamp(min=1, max=4)) -}) - -SERVICE_SCHEMA_SET_DELAYED_TURN_OFF = XIAOMI_MIIO_SERVICE_SCHEMA.extend({ - vol.Required(ATTR_TIME_PERIOD): - vol.All(cv.time_period, cv.positive_timedelta) -}) - -SERVICE_TO_METHOD = { - SERVICE_SET_DELAYED_TURN_OFF: { - 'method': 'async_set_delayed_turn_off', - 'schema': SERVICE_SCHEMA_SET_DELAYED_TURN_OFF}, - SERVICE_SET_SCENE: { - 'method': 'async_set_scene', - 'schema': SERVICE_SCHEMA_SET_SCENE}, - SERVICE_REMINDER_ON: {'method': 'async_reminder_on'}, - SERVICE_REMINDER_OFF: {'method': 'async_reminder_off'}, - SERVICE_NIGHT_LIGHT_MODE_ON: {'method': 'async_night_light_mode_on'}, - SERVICE_NIGHT_LIGHT_MODE_OFF: {'method': 'async_night_light_mode_off'}, - SERVICE_EYECARE_MODE_ON: {'method': 'async_eyecare_mode_on'}, - SERVICE_EYECARE_MODE_OFF: {'method': 'async_eyecare_mode_off'}, -} - - -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up the light from config.""" - from miio import Device, DeviceException - if DATA_KEY not in hass.data: - hass.data[DATA_KEY] = {} - - host = config.get(CONF_HOST) - name = config.get(CONF_NAME) - token = config.get(CONF_TOKEN) - model = config.get(CONF_MODEL) - - _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) - - devices = [] - unique_id = None - - if model is None: - try: - miio_device = Device(host, token) - device_info = miio_device.info() - model = device_info.model - unique_id = "{}-{}".format(model, device_info.mac_address) - _LOGGER.info("%s %s %s detected", - model, - device_info.firmware_version, - device_info.hardware_version) - except DeviceException: - raise PlatformNotReady - - if model == 'philips.light.sread1': - from miio import PhilipsEyecare - light = PhilipsEyecare(host, token) - primary_device = XiaomiPhilipsEyecareLamp( - name, light, model, unique_id) - devices.append(primary_device) - hass.data[DATA_KEY][host] = primary_device - - secondary_device = XiaomiPhilipsEyecareLampAmbientLight( - name, light, model, unique_id) - devices.append(secondary_device) - # The ambient light doesn't expose additional services. - # A hass.data[DATA_KEY] entry isn't needed. - elif model in ['philips.light.ceiling', 'philips.light.zyceiling']: - from miio import Ceil - light = Ceil(host, token) - device = XiaomiPhilipsCeilingLamp(name, light, model, unique_id) - devices.append(device) - hass.data[DATA_KEY][host] = device - elif model == 'philips.light.moonlight': - from miio import PhilipsMoonlight - light = PhilipsMoonlight(host, token) - device = XiaomiPhilipsMoonlightLamp(name, light, model, unique_id) - devices.append(device) - hass.data[DATA_KEY][host] = device - elif model in ['philips.light.bulb', - 'philips.light.candle', - 'philips.light.candle2', - 'philips.light.downlight']: - from miio import PhilipsBulb - light = PhilipsBulb(host, token) - device = XiaomiPhilipsBulb(name, light, model, unique_id) - devices.append(device) - hass.data[DATA_KEY][host] = device - elif model == 'philips.light.mono1': - from miio import PhilipsBulb - light = PhilipsBulb(host, token) - device = XiaomiPhilipsGenericLight(name, light, model, unique_id) - devices.append(device) - hass.data[DATA_KEY][host] = device - else: - _LOGGER.error( - 'Unsupported device found! Please create an issue at ' - 'https://github.com/syssi/philipslight/issues ' - 'and provide the following data: %s', model) - return False - - async_add_entities(devices, update_before_add=True) - - async def async_service_handler(service): - """Map services to methods on Xiaomi Philips Lights.""" - method = SERVICE_TO_METHOD.get(service.service) - params = {key: value for key, value in service.data.items() - if key != ATTR_ENTITY_ID} - entity_ids = service.data.get(ATTR_ENTITY_ID) - if entity_ids: - target_devices = [dev for dev in hass.data[DATA_KEY].values() - if dev.entity_id in entity_ids] - else: - target_devices = hass.data[DATA_KEY].values() - - update_tasks = [] - for target_device in target_devices: - if not hasattr(target_device, method['method']): - continue - await getattr(target_device, method['method'])(**params) - update_tasks.append(target_device.async_update_ha_state(True)) - - if update_tasks: - await asyncio.wait(update_tasks, loop=hass.loop) - - for xiaomi_miio_service in SERVICE_TO_METHOD: - schema = SERVICE_TO_METHOD[xiaomi_miio_service].get( - 'schema', XIAOMI_MIIO_SERVICE_SCHEMA) - hass.services.async_register( - DOMAIN, xiaomi_miio_service, async_service_handler, schema=schema) - - -class XiaomiPhilipsAbstractLight(Light): - """Representation of a Abstract Xiaomi Philips Light.""" - - def __init__(self, name, light, model, unique_id): - """Initialize the light device.""" - self._name = name - self._light = light - self._model = model - self._unique_id = unique_id - - self._brightness = None - - self._available = False - self._state = None - self._state_attrs = { - ATTR_MODEL: self._model, - } - - @property - def should_poll(self): - """Poll the light.""" - return True - - @property - def unique_id(self): - """Return an unique ID.""" - return self._unique_id - - @property - def name(self): - """Return the name of the device if any.""" - return self._name - - @property - def available(self): - """Return true when state is known.""" - return self._available - - @property - def device_state_attributes(self): - """Return the state attributes of the device.""" - return self._state_attrs - - @property - def is_on(self): - """Return true if light is on.""" - return self._state - - @property - def brightness(self): - """Return the brightness of this light between 0..255.""" - return self._brightness - - @property - def supported_features(self): - """Return the supported features.""" - return SUPPORT_BRIGHTNESS - - async def _try_command(self, mask_error, func, *args, **kwargs): - """Call a light command handling error messages.""" - from miio import DeviceException - try: - result = await self.hass.async_add_executor_job( - partial(func, *args, **kwargs)) - - _LOGGER.debug("Response received from light: %s", result) - - return result == SUCCESS - except DeviceException as exc: - _LOGGER.error(mask_error, exc) - self._available = False - return False - - async def async_turn_on(self, **kwargs): - """Turn the light on.""" - if ATTR_BRIGHTNESS in kwargs: - brightness = kwargs[ATTR_BRIGHTNESS] - percent_brightness = ceil(100 * brightness / 255.0) - - _LOGGER.debug( - "Setting brightness: %s %s%%", - brightness, percent_brightness) - - result = await self._try_command( - "Setting brightness failed: %s", - self._light.set_brightness, percent_brightness) - - if result: - self._brightness = brightness - else: - await self._try_command( - "Turning the light on failed.", self._light.on) - - async def async_turn_off(self, **kwargs): - """Turn the light off.""" - await self._try_command( - "Turning the light off failed.", self._light.off) - - async def async_update(self): - """Fetch state from the device.""" - from miio import DeviceException - try: - state = await self.hass.async_add_executor_job(self._light.status) - except DeviceException as ex: - self._available = False - _LOGGER.error("Got exception while fetching the state: %s", ex) - return - - _LOGGER.debug("Got new state: %s", state) - self._available = True - self._state = state.is_on - self._brightness = ceil((255 / 100.0) * state.brightness) - - -class XiaomiPhilipsGenericLight(XiaomiPhilipsAbstractLight): - """Representation of a Generic Xiaomi Philips Light.""" - - def __init__(self, name, light, model, unique_id): - """Initialize the light device.""" - super().__init__(name, light, model, unique_id) - - self._state_attrs.update({ - ATTR_SCENE: None, - ATTR_DELAYED_TURN_OFF: None, - }) - - async def async_update(self): - """Fetch state from the device.""" - from miio import DeviceException - try: - state = await self.hass.async_add_executor_job(self._light.status) - except DeviceException as ex: - self._available = False - _LOGGER.error("Got exception while fetching the state: %s", ex) - return - - _LOGGER.debug("Got new state: %s", state) - self._available = True - self._state = state.is_on - self._brightness = ceil((255 / 100.0) * state.brightness) - - delayed_turn_off = self.delayed_turn_off_timestamp( - state.delay_off_countdown, - dt.utcnow(), - self._state_attrs[ATTR_DELAYED_TURN_OFF]) - - self._state_attrs.update({ - ATTR_SCENE: state.scene, - ATTR_DELAYED_TURN_OFF: delayed_turn_off, - }) - - async def async_set_scene(self, scene: int = 1): - """Set the fixed scene.""" - await self._try_command( - "Setting a fixed scene failed.", - self._light.set_scene, scene) - - async def async_set_delayed_turn_off(self, time_period: timedelta): - """Set delayed turn off.""" - await self._try_command( - "Setting the turn off delay failed.", - self._light.delay_off, time_period.total_seconds()) - - @staticmethod - def delayed_turn_off_timestamp(countdown: int, - current: datetime, - previous: datetime): - """Update the turn off timestamp only if necessary.""" - if countdown is not None and countdown > 0: - new = current.replace(microsecond=0) + \ - timedelta(seconds=countdown) - - if previous is None: - return new - - lower = timedelta(seconds=-DELAYED_TURN_OFF_MAX_DEVIATION_SECONDS) - upper = timedelta(seconds=DELAYED_TURN_OFF_MAX_DEVIATION_SECONDS) - diff = previous - new - if lower < diff < upper: - return previous - - return new - - return None - - -class XiaomiPhilipsBulb(XiaomiPhilipsGenericLight): - """Representation of a Xiaomi Philips Bulb.""" - - def __init__(self, name, light, model, unique_id): - """Initialize the light device.""" - super().__init__(name, light, model, unique_id) - - self._color_temp = None - - @property - def color_temp(self): - """Return the color temperature.""" - return self._color_temp - - @property - def min_mireds(self): - """Return the coldest color_temp that this light supports.""" - return 175 - - @property - def max_mireds(self): - """Return the warmest color_temp that this light supports.""" - return 333 - - @property - def supported_features(self): - """Return the supported features.""" - return SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP - - async def async_turn_on(self, **kwargs): - """Turn the light on.""" - if ATTR_COLOR_TEMP in kwargs: - color_temp = kwargs[ATTR_COLOR_TEMP] - percent_color_temp = self.translate( - color_temp, self.max_mireds, - self.min_mireds, CCT_MIN, CCT_MAX) - - if ATTR_BRIGHTNESS in kwargs: - brightness = kwargs[ATTR_BRIGHTNESS] - percent_brightness = ceil(100 * brightness / 255.0) - - if ATTR_BRIGHTNESS in kwargs and ATTR_COLOR_TEMP in kwargs: - _LOGGER.debug( - "Setting brightness and color temperature: " - "%s %s%%, %s mireds, %s%% cct", - brightness, percent_brightness, - color_temp, percent_color_temp) - - result = await self._try_command( - "Setting brightness and color temperature failed: " - "%s bri, %s cct", - self._light.set_brightness_and_color_temperature, - percent_brightness, percent_color_temp) - - if result: - self._color_temp = color_temp - self._brightness = brightness - - elif ATTR_COLOR_TEMP in kwargs: - _LOGGER.debug( - "Setting color temperature: " - "%s mireds, %s%% cct", - color_temp, percent_color_temp) - - result = await self._try_command( - "Setting color temperature failed: %s cct", - self._light.set_color_temperature, percent_color_temp) - - if result: - self._color_temp = color_temp - - elif ATTR_BRIGHTNESS in kwargs: - brightness = kwargs[ATTR_BRIGHTNESS] - percent_brightness = ceil(100 * brightness / 255.0) - - _LOGGER.debug( - "Setting brightness: %s %s%%", - brightness, percent_brightness) - - result = await self._try_command( - "Setting brightness failed: %s", - self._light.set_brightness, percent_brightness) - - if result: - self._brightness = brightness - - else: - await self._try_command( - "Turning the light on failed.", self._light.on) - - async def async_update(self): - """Fetch state from the device.""" - from miio import DeviceException - try: - state = await self.hass.async_add_executor_job(self._light.status) - except DeviceException as ex: - self._available = False - _LOGGER.error("Got exception while fetching the state: %s", ex) - return - - _LOGGER.debug("Got new state: %s", state) - self._available = True - self._state = state.is_on - self._brightness = ceil((255 / 100.0) * state.brightness) - self._color_temp = self.translate( - state.color_temperature, - CCT_MIN, CCT_MAX, - self.max_mireds, self.min_mireds) - - delayed_turn_off = self.delayed_turn_off_timestamp( - state.delay_off_countdown, - dt.utcnow(), - self._state_attrs[ATTR_DELAYED_TURN_OFF]) - - self._state_attrs.update({ - ATTR_SCENE: state.scene, - ATTR_DELAYED_TURN_OFF: delayed_turn_off, - }) - - @staticmethod - def translate(value, left_min, left_max, right_min, right_max): - """Map a value from left span to right span.""" - left_span = left_max - left_min - right_span = right_max - right_min - value_scaled = float(value - left_min) / float(left_span) - return int(right_min + (value_scaled * right_span)) - - -class XiaomiPhilipsCeilingLamp(XiaomiPhilipsBulb): - """Representation of a Xiaomi Philips Ceiling Lamp.""" - - def __init__(self, name, light, model, unique_id): - """Initialize the light device.""" - super().__init__(name, light, model, unique_id) - - self._state_attrs.update({ - ATTR_NIGHT_LIGHT_MODE: None, - ATTR_AUTOMATIC_COLOR_TEMPERATURE: None, - }) - - @property - def min_mireds(self): - """Return the coldest color_temp that this light supports.""" - return 175 - - @property - def max_mireds(self): - """Return the warmest color_temp that this light supports.""" - return 370 - - async def async_update(self): - """Fetch state from the device.""" - from miio import DeviceException - try: - state = await self.hass.async_add_executor_job(self._light.status) - except DeviceException as ex: - self._available = False - _LOGGER.error("Got exception while fetching the state: %s", ex) - return - - _LOGGER.debug("Got new state: %s", state) - self._available = True - self._state = state.is_on - self._brightness = ceil((255 / 100.0) * state.brightness) - self._color_temp = self.translate( - state.color_temperature, - CCT_MIN, CCT_MAX, - self.max_mireds, self.min_mireds) - - delayed_turn_off = self.delayed_turn_off_timestamp( - state.delay_off_countdown, - dt.utcnow(), - self._state_attrs[ATTR_DELAYED_TURN_OFF]) - - self._state_attrs.update({ - ATTR_SCENE: state.scene, - ATTR_DELAYED_TURN_OFF: delayed_turn_off, - ATTR_NIGHT_LIGHT_MODE: state.smart_night_light, - ATTR_AUTOMATIC_COLOR_TEMPERATURE: - state.automatic_color_temperature, - }) - - -class XiaomiPhilipsEyecareLamp(XiaomiPhilipsGenericLight): - """Representation of a Xiaomi Philips Eyecare Lamp 2.""" - - def __init__(self, name, light, model, unique_id): - """Initialize the light device.""" - super().__init__(name, light, model, unique_id) - - self._state_attrs.update({ - ATTR_REMINDER: None, - ATTR_NIGHT_LIGHT_MODE: None, - ATTR_EYECARE_MODE: None, - }) - - async def async_update(self): - """Fetch state from the device.""" - from miio import DeviceException - try: - state = await self.hass.async_add_executor_job(self._light.status) - except DeviceException as ex: - self._available = False - _LOGGER.error("Got exception while fetching the state: %s", ex) - return - - _LOGGER.debug("Got new state: %s", state) - self._available = True - self._state = state.is_on - self._brightness = ceil((255 / 100.0) * state.brightness) - - delayed_turn_off = self.delayed_turn_off_timestamp( - state.delay_off_countdown, - dt.utcnow(), - self._state_attrs[ATTR_DELAYED_TURN_OFF]) - - self._state_attrs.update({ - ATTR_SCENE: state.scene, - ATTR_DELAYED_TURN_OFF: delayed_turn_off, - ATTR_REMINDER: state.reminder, - ATTR_NIGHT_LIGHT_MODE: state.smart_night_light, - ATTR_EYECARE_MODE: state.eyecare, - }) - - async def async_set_delayed_turn_off(self, time_period: timedelta): - """Set delayed turn off.""" - await self._try_command( - "Setting the turn off delay failed.", - self._light.delay_off, round(time_period.total_seconds() / 60)) - - async def async_reminder_on(self): - """Enable the eye fatigue notification.""" - await self._try_command( - "Turning on the reminder failed.", - self._light.reminder_on) - - async def async_reminder_off(self): - """Disable the eye fatigue notification.""" - await self._try_command( - "Turning off the reminder failed.", - self._light.reminder_off) - - async def async_night_light_mode_on(self): - """Turn the smart night light mode on.""" - await self._try_command( - "Turning on the smart night light mode failed.", - self._light.smart_night_light_on) - - async def async_night_light_mode_off(self): - """Turn the smart night light mode off.""" - await self._try_command( - "Turning off the smart night light mode failed.", - self._light.smart_night_light_off) - - async def async_eyecare_mode_on(self): - """Turn the eyecare mode on.""" - await self._try_command( - "Turning on the eyecare mode failed.", - self._light.eyecare_on) - - async def async_eyecare_mode_off(self): - """Turn the eyecare mode off.""" - await self._try_command( - "Turning off the eyecare mode failed.", - self._light.eyecare_off) - - @staticmethod - def delayed_turn_off_timestamp(countdown: int, - current: datetime, - previous: datetime): - """Update the turn off timestamp only if necessary.""" - if countdown is not None and countdown > 0: - new = current.replace(second=0, microsecond=0) + \ - timedelta(minutes=countdown) - - if previous is None: - return new - - lower = timedelta(minutes=-DELAYED_TURN_OFF_MAX_DEVIATION_MINUTES) - upper = timedelta(minutes=DELAYED_TURN_OFF_MAX_DEVIATION_MINUTES) - diff = previous - new - if lower < diff < upper: - return previous - - return new - - return None - - -class XiaomiPhilipsEyecareLampAmbientLight(XiaomiPhilipsAbstractLight): - """Representation of a Xiaomi Philips Eyecare Lamp Ambient Light.""" - - def __init__(self, name, light, model, unique_id): - """Initialize the light device.""" - name = '{} Ambient Light'.format(name) - if unique_id is not None: - unique_id = "{}-{}".format(unique_id, 'ambient') - super().__init__(name, light, model, unique_id) - - async def async_turn_on(self, **kwargs): - """Turn the light on.""" - if ATTR_BRIGHTNESS in kwargs: - brightness = kwargs[ATTR_BRIGHTNESS] - percent_brightness = ceil(100 * brightness / 255.0) - - _LOGGER.debug( - "Setting brightness of the ambient light: %s %s%%", - brightness, percent_brightness) - - result = await self._try_command( - "Setting brightness of the ambient failed: %s", - self._light.set_ambient_brightness, percent_brightness) - - if result: - self._brightness = brightness - else: - await self._try_command( - "Turning the ambient light on failed.", self._light.ambient_on) - - async def async_turn_off(self, **kwargs): - """Turn the light off.""" - await self._try_command( - "Turning the ambient light off failed.", self._light.ambient_off) - - async def async_update(self): - """Fetch state from the device.""" - from miio import DeviceException - try: - state = await self.hass.async_add_executor_job(self._light.status) - except DeviceException as ex: - self._available = False - _LOGGER.error("Got exception while fetching the state: %s", ex) - return - - _LOGGER.debug("Got new state: %s", state) - self._available = True - self._state = state.ambient - self._brightness = ceil((255 / 100.0) * state.ambient_brightness) - - -class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): - """Representation of a Xiaomi Philips Zhirui Bedside Lamp.""" - - def __init__(self, name, light, model, unique_id): - """Initialize the light device.""" - super().__init__(name, light, model, unique_id) - - self._hs_color = None - self._state_attrs.pop(ATTR_DELAYED_TURN_OFF) - self._state_attrs.update({ - ATTR_SLEEP_ASSISTANT: None, - ATTR_SLEEP_OFF_TIME: None, - ATTR_TOTAL_ASSISTANT_SLEEP_TIME: None, - ATTR_BRAND_SLEEP: None, - ATTR_BRAND: None, - }) - - @property - def min_mireds(self): - """Return the coldest color_temp that this light supports.""" - return 153 - - @property - def max_mireds(self): - """Return the warmest color_temp that this light supports.""" - return 588 - - @property - def hs_color(self) -> tuple: - """Return the hs color value.""" - return self._hs_color - - @property - def supported_features(self): - """Return the supported features.""" - return SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP - - async def async_update(self): - """Fetch state from the device.""" - from miio import DeviceException - try: - state = await self.hass.async_add_executor_job(self._light.status) - except DeviceException as ex: - self._available = False - _LOGGER.error("Got exception while fetching the state: %s", ex) - return - - _LOGGER.debug("Got new state: %s", state) - self._available = True - self._state = state.is_on - self._brightness = ceil((255 / 100.0) * state.brightness) - self._color_temp = self.translate( - state.color_temperature, - CCT_MIN, CCT_MAX, - self.max_mireds, self.min_mireds) - self._hs_color = color.color_RGB_to_hs(*state.rgb) - - self._state_attrs.update({ - ATTR_SCENE: state.scene, - ATTR_SLEEP_ASSISTANT: state.sleep_assistant, - ATTR_SLEEP_OFF_TIME: state.sleep_off_time, - ATTR_TOTAL_ASSISTANT_SLEEP_TIME: - state.total_assistant_sleep_time, - ATTR_BRAND_SLEEP: state.brand_sleep, - ATTR_BRAND: state.brand, - }) - - async def async_set_delayed_turn_off(self, time_period: timedelta): - """Set delayed turn off. Unsupported.""" - return diff --git a/homeassistant/components/light/zigbee.py b/homeassistant/components/light/zigbee.py deleted file mode 100644 index 42dc95d11631d..0000000000000 --- a/homeassistant/components/light/zigbee.py +++ /dev/null @@ -1,33 +0,0 @@ -""" -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/ -""" -import voluptuous as vol - -from homeassistant.components.light import Light -from homeassistant.components.zigbee import ( - ZigBeeDigitalOut, ZigBeeDigitalOutConfig, PLATFORM_SCHEMA) - -CONF_ON_STATE = 'on_state' - -DEFAULT_ON_STATE = 'high' -DEPENDENCIES = ['zigbee'] - -STATES = ['high', 'low'] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_ON_STATE, default=DEFAULT_ON_STATE): vol.In(STATES), -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Create and add an entity based on the configuration.""" - add_entities([ZigBeeLight(hass, ZigBeeDigitalOutConfig(config))]) - - -class ZigBeeLight(ZigBeeDigitalOut, Light): - """Use ZigBeeDigitalOut as light.""" - - pass diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py deleted file mode 100644 index da712a2f183c2..0000000000000 --- a/homeassistant/components/light/zwave.py +++ /dev/null @@ -1,383 +0,0 @@ -""" -Support for Z-Wave lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.zwave/ -""" -import logging - -from threading import Timer -from homeassistant.core import callback -from homeassistant.components.light import ( - ATTR_WHITE_VALUE, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, - ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR, - SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, DOMAIN, Light) -from homeassistant.components import zwave -from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.helpers.dispatcher import async_dispatcher_connect -import homeassistant.util.color as color_util - -_LOGGER = logging.getLogger(__name__) - -COLOR_CHANNEL_WARM_WHITE = 0x01 -COLOR_CHANNEL_COLD_WHITE = 0x02 -COLOR_CHANNEL_RED = 0x04 -COLOR_CHANNEL_GREEN = 0x08 -COLOR_CHANNEL_BLUE = 0x10 - -# Some bulbs have an independent warm and cool white light LEDs. These need -# to be treated differently, aka the zw098 workaround. Ensure these are added -# to DEVICE_MAPPINGS below. -# (Manufacturer ID, Product ID) from -# https://github.com/OpenZWave/open-zwave/blob/master/config/manufacturer_specific.xml -AEOTEC_ZW098_LED_BULB_LIGHT = (0x86, 0x62) -AEOTEC_ZWA001_LED_BULB_LIGHT = (0x371, 0x1) -AEOTEC_ZWA002_LED_BULB_LIGHT = (0x371, 0x2) -HANK_HKZW_RGB01_LED_BULB_LIGHT = (0x208, 0x4) -ZIPATO_RGB_BULB_2_LED_BULB_LIGHT = (0x131, 0x3) - -WORKAROUND_ZW098 = 'zw098' - -DEVICE_MAPPINGS = { - AEOTEC_ZW098_LED_BULB_LIGHT: WORKAROUND_ZW098, - AEOTEC_ZWA001_LED_BULB_LIGHT: WORKAROUND_ZW098, - AEOTEC_ZWA002_LED_BULB_LIGHT: WORKAROUND_ZW098, - HANK_HKZW_RGB01_LED_BULB_LIGHT: WORKAROUND_ZW098, - ZIPATO_RGB_BULB_2_LED_BULB_LIGHT: WORKAROUND_ZW098 -} - -# Generate midpoint color temperatures for bulbs that have limited -# support for white light colors -TEMP_COLOR_MAX = 500 # mireds (inverted) -TEMP_COLOR_MIN = 154 -TEMP_MID_HASS = (TEMP_COLOR_MAX - TEMP_COLOR_MIN) / 2 + TEMP_COLOR_MIN -TEMP_WARM_HASS = (TEMP_COLOR_MAX - TEMP_COLOR_MIN) / 3 * 2 + TEMP_COLOR_MIN -TEMP_COLD_HASS = (TEMP_COLOR_MAX - TEMP_COLOR_MIN) / 3 + TEMP_COLOR_MIN - - -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Old method of setting up Z-Wave lights.""" - pass - - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up Z-Wave Light from Config Entry.""" - @callback - def async_add_light(light): - """Add Z-Wave Light.""" - async_add_entities([light]) - - async_dispatcher_connect(hass, 'zwave_new_light', async_add_light) - - -def get_device(node, values, node_config, **kwargs): - """Create Z-Wave entity device.""" - refresh = node_config.get(zwave.CONF_REFRESH_VALUE) - delay = node_config.get(zwave.CONF_REFRESH_DELAY) - _LOGGER.debug("node=%d value=%d node_config=%s CONF_REFRESH_VALUE=%s" - " CONF_REFRESH_DELAY=%s", node.node_id, - values.primary.value_id, node_config, refresh, delay) - - if node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_COLOR): - return ZwaveColorLight(values, refresh, delay) - return ZwaveDimmer(values, refresh, delay) - - -def brightness_state(value): - """Return the brightness and state.""" - if value.data > 0: - return round((value.data / 99) * 255), STATE_ON - return 0, STATE_OFF - - -def byte_to_zwave_brightness(value): - """Convert brightness in 0-255 scale to 0-99 scale. - - `value` -- (int) Brightness byte value from 0-255. - """ - if value > 0: - return max(1, int((value / 255) * 99)) - return 0 - - -def ct_to_hs(temp): - """Convert color temperature (mireds) to hs.""" - colorlist = list( - color_util.color_temperature_to_hs( - color_util.color_temperature_mired_to_kelvin(temp))) - return [int(val) for val in colorlist] - - -class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light): - """Representation of a Z-Wave dimmer.""" - - def __init__(self, values, refresh, delay): - """Initialize the light.""" - zwave.ZWaveDeviceEntity.__init__(self, values, DOMAIN) - self._brightness = None - self._state = None - self._supported_features = None - self._delay = delay - self._refresh_value = refresh - self._zw098 = None - - # Enable appropriate workaround flags for our device - # Make sure that we have values for the key before converting to int - if (self.node.manufacturer_id.strip() and - self.node.product_id.strip()): - specific_sensor_key = (int(self.node.manufacturer_id, 16), - int(self.node.product_id, 16)) - if specific_sensor_key in DEVICE_MAPPINGS: - if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZW098: - _LOGGER.debug("AEOTEC ZW098 workaround enabled") - self._zw098 = 1 - - # Used for value change event handling - self._refreshing = False - self._timer = None - _LOGGER.debug('self._refreshing=%s self.delay=%s', - self._refresh_value, self._delay) - self.value_added() - self.update_properties() - - def update_properties(self): - """Update internal properties based on zwave values.""" - # Brightness - self._brightness, self._state = brightness_state(self.values.primary) - - def value_added(self): - """Call when a new value is added to this entity.""" - self._supported_features = SUPPORT_BRIGHTNESS - if self.values.dimming_duration is not None: - self._supported_features |= SUPPORT_TRANSITION - - def value_changed(self): - """Call when a value for this entity's node has changed.""" - if self._refresh_value: - if self._refreshing: - self._refreshing = False - else: - def _refresh_value(): - """Use timer callback for delayed value refresh.""" - self._refreshing = True - self.values.primary.refresh() - - if self._timer is not None and self._timer.isAlive(): - self._timer.cancel() - - self._timer = Timer(self._delay, _refresh_value) - self._timer.start() - return - super().value_changed() - - @property - def brightness(self): - """Return the brightness of this light between 0..255.""" - return self._brightness - - @property - def is_on(self): - """Return true if device is on.""" - return self._state == STATE_ON - - @property - def supported_features(self): - """Flag supported features.""" - return self._supported_features - - def _set_duration(self, **kwargs): - """Set the transition time for the brightness value. - - Zwave Dimming Duration values: - 0x00 = instant - 0x01-0x7F = 1 second to 127 seconds - 0x80-0xFE = 1 minute to 127 minutes - 0xFF = factory default - """ - if self.values.dimming_duration is None: - if ATTR_TRANSITION in kwargs: - _LOGGER.debug("Dimming not supported by %s.", self.entity_id) - return - - if ATTR_TRANSITION not in kwargs: - self.values.dimming_duration.data = 0xFF - return - - transition = kwargs[ATTR_TRANSITION] - if transition <= 127: - self.values.dimming_duration.data = int(transition) - elif transition > 7620: - self.values.dimming_duration.data = 0xFE - _LOGGER.warning("Transition clipped to 127 minutes for %s.", - self.entity_id) - else: - minutes = int(transition / 60) - _LOGGER.debug("Transition rounded to %d minutes for %s.", - minutes, self.entity_id) - self.values.dimming_duration.data = minutes + 0x7F - - def turn_on(self, **kwargs): - """Turn the device on.""" - self._set_duration(**kwargs) - - # Zwave multilevel switches use a range of [0, 99] to control - # brightness. Level 255 means to set it to previous value. - if ATTR_BRIGHTNESS in kwargs: - self._brightness = kwargs[ATTR_BRIGHTNESS] - brightness = byte_to_zwave_brightness(self._brightness) - else: - brightness = 255 - - if self.node.set_dimmer(self.values.primary.value_id, brightness): - self._state = STATE_ON - - def turn_off(self, **kwargs): - """Turn the device off.""" - self._set_duration(**kwargs) - - if self.node.set_dimmer(self.values.primary.value_id, 0): - self._state = STATE_OFF - - -class ZwaveColorLight(ZwaveDimmer): - """Representation of a Z-Wave color changing light.""" - - def __init__(self, values, refresh, delay): - """Initialize the light.""" - self._color_channels = None - self._hs = None - self._ct = None - self._white = None - - super().__init__(values, refresh, delay) - - def value_added(self): - """Call when a new value is added to this entity.""" - super().value_added() - - self._supported_features |= SUPPORT_COLOR - if self._zw098: - self._supported_features |= SUPPORT_COLOR_TEMP - elif self._color_channels is not None and self._color_channels & ( - COLOR_CHANNEL_WARM_WHITE | COLOR_CHANNEL_COLD_WHITE): - self._supported_features |= SUPPORT_WHITE_VALUE - - def update_properties(self): - """Update internal properties based on zwave values.""" - super().update_properties() - - if self.values.color is None: - return - if self.values.color_channels is None: - return - - # Color Channels - self._color_channels = self.values.color_channels.data - - # Color Data String - data = self.values.color.data - - # RGB is always present in the openzwave color data string. - rgb = [ - int(data[1:3], 16), - int(data[3:5], 16), - int(data[5:7], 16)] - self._hs = color_util.color_RGB_to_hs(*rgb) - - # Parse remaining color channels. Openzwave appends white channels - # that are present. - index = 7 - - # Warm white - if self._color_channels & COLOR_CHANNEL_WARM_WHITE: - warm_white = int(data[index:index+2], 16) - index += 2 - else: - warm_white = 0 - - # Cold white - if self._color_channels & COLOR_CHANNEL_COLD_WHITE: - cold_white = int(data[index:index+2], 16) - index += 2 - else: - cold_white = 0 - - # Color temperature. With the AEOTEC ZW098 bulb, only two color - # temperatures are supported. The warm and cold channel values - # indicate brightness for warm/cold color temperature. - if self._zw098: - if warm_white > 0: - self._ct = TEMP_WARM_HASS - self._hs = ct_to_hs(self._ct) - elif cold_white > 0: - self._ct = TEMP_COLD_HASS - self._hs = ct_to_hs(self._ct) - else: - # RGB color is being used. Just report midpoint. - self._ct = TEMP_MID_HASS - - elif self._color_channels & COLOR_CHANNEL_WARM_WHITE: - self._white = warm_white - - elif self._color_channels & COLOR_CHANNEL_COLD_WHITE: - self._white = cold_white - - # If no rgb channels supported, report None. - if not (self._color_channels & COLOR_CHANNEL_RED or - self._color_channels & COLOR_CHANNEL_GREEN or - self._color_channels & COLOR_CHANNEL_BLUE): - self._hs = None - - @property - def hs_color(self): - """Return the hs color.""" - return self._hs - - @property - def white_value(self): - """Return the white value of this light between 0..255.""" - return self._white - - @property - def color_temp(self): - """Return the color temperature.""" - return self._ct - - def turn_on(self, **kwargs): - """Turn the device on.""" - rgbw = None - - if ATTR_WHITE_VALUE in kwargs: - self._white = kwargs[ATTR_WHITE_VALUE] - - if ATTR_COLOR_TEMP in kwargs: - # Color temperature. With the AEOTEC ZW098 bulb, only two color - # temperatures are supported. The warm and cold channel values - # indicate brightness for warm/cold color temperature. - if self._zw098: - if kwargs[ATTR_COLOR_TEMP] > TEMP_MID_HASS: - self._ct = TEMP_WARM_HASS - rgbw = '#000000ff00' - else: - self._ct = TEMP_COLD_HASS - rgbw = '#00000000ff' - elif ATTR_HS_COLOR in kwargs: - self._hs = kwargs[ATTR_HS_COLOR] - if ATTR_WHITE_VALUE not in kwargs: - # white LED must be off in order for color to work - self._white = 0 - - if ATTR_WHITE_VALUE in kwargs or ATTR_HS_COLOR in kwargs: - rgbw = '#' - for colorval in color_util.color_hs_to_RGB(*self._hs): - rgbw += format(colorval, '02x') - if self._white is not None: - rgbw += format(self._white, '02x') + '00' - else: - rgbw += '0000' - - if rgbw and self.values.color: - self.values.color.data = rgbw - - super().turn_on(**kwargs) diff --git a/homeassistant/components/lightwave.py b/homeassistant/components/lightwave.py deleted file mode 100644 index e1aa1664eba49..0000000000000 --- a/homeassistant/components/lightwave.py +++ /dev/null @@ -1,49 +0,0 @@ -""" -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/ -""" -import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.const import (CONF_HOST, CONF_LIGHTS, CONF_NAME, - CONF_SWITCHES) -from homeassistant.helpers.discovery import async_load_platform - -REQUIREMENTS = ['lightwave==0.15'] -LIGHTWAVE_LINK = 'lightwave_link' -DOMAIN = 'lightwave' - - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema( - cv.has_at_least_one_key(CONF_LIGHTS, CONF_SWITCHES), { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_LIGHTS, default={}): { - cv.string: vol.Schema({vol.Required(CONF_NAME): cv.string}), - }, - vol.Optional(CONF_SWITCHES, default={}): { - cv.string: vol.Schema({vol.Required(CONF_NAME): cv.string}), - } - }) -}, extra=vol.ALLOW_EXTRA) - - -async def async_setup(hass, config): - """Try to start embedded Lightwave broker.""" - from lightwave.lightwave import LWLink - - host = config[DOMAIN][CONF_HOST] - hass.data[LIGHTWAVE_LINK] = LWLink(host) - - lights = config[DOMAIN][CONF_LIGHTS] - if lights: - hass.async_create_task(async_load_platform( - hass, 'light', DOMAIN, lights, config)) - - switches = config[DOMAIN][CONF_SWITCHES] - if switches: - hass.async_create_task(async_load_platform( - hass, 'switch', DOMAIN, switches, config)) - - return True diff --git a/homeassistant/components/lightwave/__init__.py b/homeassistant/components/lightwave/__init__.py new file mode 100644 index 0000000000000..a9e5dcf9823c3 --- /dev/null +++ b/homeassistant/components/lightwave/__init__.py @@ -0,0 +1,46 @@ +"""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, + CONF_SWITCHES) +from homeassistant.helpers.discovery import async_load_platform + +REQUIREMENTS = ['lightwave==0.15'] + +LIGHTWAVE_LINK = 'lightwave_link' + +DOMAIN = 'lightwave' + + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema( + cv.has_at_least_one_key(CONF_LIGHTS, CONF_SWITCHES), { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_LIGHTS, default={}): { + cv.string: vol.Schema({vol.Required(CONF_NAME): cv.string}), + }, + vol.Optional(CONF_SWITCHES, default={}): { + cv.string: vol.Schema({vol.Required(CONF_NAME): cv.string}), + } + }) +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Try to start embedded Lightwave broker.""" + from lightwave.lightwave import LWLink + + host = config[DOMAIN][CONF_HOST] + hass.data[LIGHTWAVE_LINK] = LWLink(host) + + lights = config[DOMAIN][CONF_LIGHTS] + if lights: + hass.async_create_task(async_load_platform( + hass, 'light', DOMAIN, lights, config)) + + switches = config[DOMAIN][CONF_SWITCHES] + if switches: + hass.async_create_task(async_load_platform( + hass, 'switch', DOMAIN, switches, config)) + + return True diff --git a/homeassistant/components/lightwave/light.py b/homeassistant/components/lightwave/light.py new file mode 100644 index 0000000000000..1dfbac37c889a --- /dev/null +++ b/homeassistant/components/lightwave/light.py @@ -0,0 +1,83 @@ +"""Support for LightwaveRF lights.""" +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) +from homeassistant.components.lightwave import LIGHTWAVE_LINK +from homeassistant.const import CONF_NAME + +DEPENDENCIES = ['lightwave'] + +MAX_BRIGHTNESS = 255 + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Find and return LightWave lights.""" + if not discovery_info: + return + + lights = [] + lwlink = hass.data[LIGHTWAVE_LINK] + + for device_id, device_config in discovery_info.items(): + name = device_config[CONF_NAME] + lights.append(LWRFLight(name, device_id, lwlink)) + + async_add_entities(lights) + + +class LWRFLight(Light): + """Representation of a LightWaveRF light.""" + + def __init__(self, name, device_id, lwlink): + """Initialize LWRFLight entity.""" + self._name = name + self._device_id = device_id + self._state = None + self._brightness = MAX_BRIGHTNESS + self._lwlink = lwlink + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS + + @property + def should_poll(self): + """No polling needed for a LightWave light.""" + return False + + @property + def name(self): + """Lightwave light name.""" + return self._name + + @property + def brightness(self): + """Brightness of this light between 0..MAX_BRIGHTNESS.""" + return self._brightness + + @property + def is_on(self): + """Lightwave light is on state.""" + return self._state + + async def async_turn_on(self, **kwargs): + """Turn the LightWave light on.""" + self._state = True + + if ATTR_BRIGHTNESS in kwargs: + self._brightness = kwargs[ATTR_BRIGHTNESS] + + if self._brightness != MAX_BRIGHTNESS: + self._lwlink.turn_on_with_brightness( + self._device_id, self._name, self._brightness) + else: + self._lwlink.turn_on_light(self._device_id, self._name) + + self.async_schedule_update_ha_state() + + async def async_turn_off(self, **kwargs): + """Turn the LightWave light off.""" + self._state = False + self._lwlink.turn_off(self._device_id, self._name) + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/lightwave/switch.py b/homeassistant/components/lightwave/switch.py new file mode 100644 index 0000000000000..d6c00b7fddb66 --- /dev/null +++ b/homeassistant/components/lightwave/switch.py @@ -0,0 +1,60 @@ +"""Support for LightwaveRF switches.""" +from homeassistant.components.lightwave import LIGHTWAVE_LINK +from homeassistant.components.switch import SwitchDevice +from homeassistant.const import CONF_NAME + +DEPENDENCIES = ['lightwave'] + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Find and return LightWave switches.""" + if not discovery_info: + return + + switches = [] + lwlink = hass.data[LIGHTWAVE_LINK] + + for device_id, device_config in discovery_info.items(): + name = device_config[CONF_NAME] + switches.append(LWRFSwitch(name, device_id, lwlink)) + + async_add_entities(switches) + + +class LWRFSwitch(SwitchDevice): + """Representation of a LightWaveRF switch.""" + + def __init__(self, name, device_id, lwlink): + """Initialize LWRFSwitch entity.""" + self._name = name + self._device_id = device_id + self._state = None + self._lwlink = lwlink + + @property + def should_poll(self): + """No polling needed for a LightWave light.""" + return False + + @property + def name(self): + """Lightwave switch name.""" + return self._name + + @property + def is_on(self): + """Lightwave switch is on state.""" + return self._state + + async def async_turn_on(self, **kwargs): + """Turn the LightWave switch on.""" + self._state = True + self._lwlink.turn_on_switch(self._device_id, self._name) + self.async_schedule_update_ha_state() + + async def async_turn_off(self, **kwargs): + """Turn the LightWave switch off.""" + self._state = False + self._lwlink.turn_off(self._device_id, self._name) + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/linode.py b/homeassistant/components/linode.py deleted file mode 100644 index c98ef16c7ed66..0000000000000 --- a/homeassistant/components/linode.py +++ /dev/null @@ -1,98 +0,0 @@ -""" -Support for Linode. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/linode/ -""" -import logging -from datetime import timedelta - -import voluptuous as vol - -from homeassistant.const import CONF_ACCESS_TOKEN -from homeassistant.util import Throttle -import homeassistant.helpers.config_validation as cv - -REQUIREMENTS = ['linode-api==4.1.9b1'] - -_LOGGER = logging.getLogger(__name__) - -ATTR_CREATED = 'created' -ATTR_NODE_ID = 'node_id' -ATTR_NODE_NAME = 'node_name' -ATTR_IPV4_ADDRESS = 'ipv4_address' -ATTR_IPV6_ADDRESS = 'ipv6_address' -ATTR_MEMORY = 'memory' -ATTR_REGION = 'region' -ATTR_VCPUS = 'vcpus' - -CONF_NODES = 'nodes' - -DATA_LINODE = 'data_li' -LINODE_PLATFORMS = ['binary_sensor', 'switch'] -DOMAIN = 'linode' - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_ACCESS_TOKEN): cv.string, - }), -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the Linode component.""" - import linode - - conf = config[DOMAIN] - access_token = conf.get(CONF_ACCESS_TOKEN) - - _linode = Linode(access_token) - - try: - _LOGGER.info("Linode Profile %s", - _linode.manager.get_profile().username) - except linode.errors.ApiError as _ex: - _LOGGER.error(_ex) - return False - - hass.data[DATA_LINODE] = _linode - - return True - - -class Linode: - """Handle all communication with the Linode API.""" - - def __init__(self, access_token): - """Initialize the Linode connection.""" - import linode - - self._access_token = access_token - self.data = None - self.manager = linode.LinodeClient(token=self._access_token) - - def get_node_id(self, node_name): - """Get the status of a Linode Instance.""" - import linode - node_id = None - - try: - all_nodes = self.manager.linode.get_instances() - for node in all_nodes: - if node_name == node.label: - node_id = node.id - except linode.errors.ApiError as _ex: - _LOGGER.error(_ex) - - return node_id - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Use the data from Linode API.""" - import linode - try: - self.data = self.manager.linode.get_instances() - except linode.errors.ApiError as _ex: - _LOGGER.error(_ex) diff --git a/homeassistant/components/linode/__init__.py b/homeassistant/components/linode/__init__.py new file mode 100644 index 0000000000000..8bbd98c0acf77 --- /dev/null +++ b/homeassistant/components/linode/__init__.py @@ -0,0 +1,93 @@ +"""Support for Linode.""" +from datetime import timedelta +import logging + +import voluptuous as vol + +from homeassistant.const import CONF_ACCESS_TOKEN +import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle + +REQUIREMENTS = ['linode-api==4.1.9b1'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_CREATED = 'created' +ATTR_NODE_ID = 'node_id' +ATTR_NODE_NAME = 'node_name' +ATTR_IPV4_ADDRESS = 'ipv4_address' +ATTR_IPV6_ADDRESS = 'ipv6_address' +ATTR_MEMORY = 'memory' +ATTR_REGION = 'region' +ATTR_VCPUS = 'vcpus' + +CONF_NODES = 'nodes' + +DATA_LINODE = 'data_li' +LINODE_PLATFORMS = ['binary_sensor', 'switch'] +DOMAIN = 'linode' + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_ACCESS_TOKEN): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Linode component.""" + import linode + + conf = config[DOMAIN] + access_token = conf.get(CONF_ACCESS_TOKEN) + + _linode = Linode(access_token) + + try: + _LOGGER.info("Linode Profile %s", + _linode.manager.get_profile().username) + except linode.errors.ApiError as _ex: + _LOGGER.error(_ex) + return False + + hass.data[DATA_LINODE] = _linode + + return True + + +class Linode: + """Handle all communication with the Linode API.""" + + def __init__(self, access_token): + """Initialize the Linode connection.""" + import linode + + self._access_token = access_token + self.data = None + self.manager = linode.LinodeClient(token=self._access_token) + + def get_node_id(self, node_name): + """Get the status of a Linode Instance.""" + import linode + node_id = None + + try: + all_nodes = self.manager.linode.get_instances() + for node in all_nodes: + if node_name == node.label: + node_id = node.id + except linode.errors.ApiError as _ex: + _LOGGER.error(_ex) + + return node_id + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Use the data from Linode API.""" + import linode + try: + self.data = self.manager.linode.get_instances() + except linode.errors.ApiError as _ex: + _LOGGER.error(_ex) diff --git a/homeassistant/components/linode/binary_sensor.py b/homeassistant/components/linode/binary_sensor.py new file mode 100644 index 0000000000000..a05681497de16 --- /dev/null +++ b/homeassistant/components/linode/binary_sensor.py @@ -0,0 +1,92 @@ +"""Support for monitoring the state of Linode Nodes.""" +import logging + +import voluptuous as vol + +from homeassistant.components.binary_sensor import ( + PLATFORM_SCHEMA, BinarySensorDevice) +from homeassistant.components.linode import ( + 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__) + +DEFAULT_NAME = 'Node' +DEFAULT_DEVICE_CLASS = 'moving' +DEPENDENCIES = ['linode'] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_NODES): vol.All(cv.ensure_list, [cv.string]), +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Linode droplet sensor.""" + linode = hass.data.get(DATA_LINODE) + nodes = config.get(CONF_NODES) + + dev = [] + for node in nodes: + node_id = linode.get_node_id(node) + if node_id is None: + _LOGGER.error("Node %s is not available", node) + return + dev.append(LinodeBinarySensor(linode, node_id)) + + add_entities(dev, True) + + +class LinodeBinarySensor(BinarySensorDevice): + """Representation of a Linode droplet sensor.""" + + def __init__(self, li, node_id): + """Initialize a new Linode sensor.""" + self._linode = li + self._node_id = node_id + self._state = None + self.data = None + self._attrs = {} + self._name = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self._state + + @property + def device_class(self): + """Return the class of this sensor.""" + return DEFAULT_DEVICE_CLASS + + @property + def device_state_attributes(self): + """Return the state attributes of the Linode Node.""" + return self._attrs + + def update(self): + """Update state of sensor.""" + self._linode.update() + if self._linode.data is not None: + for node in self._linode.data: + if node.id == self._node_id: + self.data = node + if self.data is not None: + self._state = self.data.status == 'running' + self._attrs = { + ATTR_CREATED: self.data.created, + ATTR_NODE_ID: self.data.id, + ATTR_NODE_NAME: self.data.label, + ATTR_IPV4_ADDRESS: self.data.ipv4, + ATTR_IPV6_ADDRESS: self.data.ipv6, + ATTR_MEMORY: self.data.specs.memory, + ATTR_REGION: self.data.region.country, + ATTR_VCPUS: self.data.specs.vcpus, + } + self._name = self.data.label diff --git a/homeassistant/components/linode/switch.py b/homeassistant/components/linode/switch.py new file mode 100644 index 0000000000000..0cab2f4d0f25f --- /dev/null +++ b/homeassistant/components/linode/switch.py @@ -0,0 +1,96 @@ +"""Support for interacting with Linode nodes.""" +import logging + +import voluptuous as vol + +from homeassistant.components.linode import ( + 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__) + +DEPENDENCIES = ['linode'] + +DEFAULT_NAME = 'Node' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_NODES): vol.All(cv.ensure_list, [cv.string]), +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Linode Node switch.""" + linode = hass.data.get(DATA_LINODE) + nodes = config.get(CONF_NODES) + + dev = [] + for node in nodes: + node_id = linode.get_node_id(node) + if node_id is None: + _LOGGER.error("Node %s is not available", node) + return + dev.append(LinodeSwitch(linode, node_id)) + + add_entities(dev, True) + + +class LinodeSwitch(SwitchDevice): + """Representation of a Linode Node switch.""" + + def __init__(self, li, node_id): + """Initialize a new Linode sensor.""" + self._linode = li + self._node_id = node_id + self.data = None + self._state = None + self._attrs = {} + self._name = None + + @property + def name(self): + """Return the name of the switch.""" + return self._name + + @property + def is_on(self): + """Return true if switch is on.""" + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes of the Linode Node.""" + return self._attrs + + def turn_on(self, **kwargs): + """Boot-up the Node.""" + if self.data.status != 'running': + self.data.boot() + + def turn_off(self, **kwargs): + """Shutdown the nodes.""" + if self.data.status == 'running': + self.data.shutdown() + + def update(self): + """Get the latest data from the device and update the data.""" + self._linode.update() + if self._linode.data is not None: + for node in self._linode.data: + if node.id == self._node_id: + self.data = node + if self.data is not None: + self._state = self.data.status == 'running' + self._attrs = { + ATTR_CREATED: self.data.created, + ATTR_NODE_ID: self.data.id, + ATTR_NODE_NAME: self.data.label, + ATTR_IPV4_ADDRESS: self.data.ipv4, + ATTR_IPV6_ADDRESS: self.data.ipv6, + ATTR_MEMORY: self.data.specs.memory, + ATTR_REGION: self.data.region.country, + ATTR_VCPUS: self.data.specs.vcpus, + } + self._name = self.data.label diff --git a/homeassistant/components/lirc.py b/homeassistant/components/lirc.py deleted file mode 100644 index d7ec49e00968f..0000000000000 --- a/homeassistant/components/lirc.py +++ /dev/null @@ -1,91 +0,0 @@ -""" -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 -import threading -import time -import logging - -import voluptuous as vol - -from homeassistant.const import ( - EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START) - -REQUIREMENTS = ['python-lirc==1.2.3'] - -_LOGGER = logging.getLogger(__name__) - -BUTTON_NAME = 'button_name' - -DOMAIN = 'lirc' - -EVENT_IR_COMMAND_RECEIVED = 'ir_command_received' - -ICON = 'mdi:remote' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({}), -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the LIRC capability.""" - import lirc - - # blocking=True gives unexpected behavior (multiple responses for 1 press) - # also by not blocking, we allow hass to shut down the thread gracefully - # on exit. - lirc.init('home-assistant', blocking=False) - lirc_interface = LircInterface(hass) - - def _start_lirc(_event): - lirc_interface.start() - - def _stop_lirc(_event): - lirc_interface.stopped.set() - - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start_lirc) - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _stop_lirc) - - return True - - -class LircInterface(threading.Thread): - """ - This interfaces with the lirc daemon to read IR commands. - - When using lirc in blocking mode, sometimes repeated commands get produced - in the next read of a command so we use a thread here to just wait - around until a non-empty response is obtained from lirc. - """ - - def __init__(self, hass): - """Construct a LIRC interface object.""" - threading.Thread.__init__(self) - self.daemon = True - self.stopped = threading.Event() - self.hass = hass - - def run(self): - """Run the loop of the LIRC interface thread.""" - import lirc - _LOGGER.debug("LIRC interface thread started") - while not self.stopped.isSet(): - try: - code = lirc.nextcode() # list; empty if no buttons pressed - except lirc.NextCodeError: - _LOGGER.warning("Error reading next code from LIRC") - code = None - # interpret result from python-lirc - if code: - code = code[0] - _LOGGER.info("Got new LIRC code %s", code) - self.hass.bus.fire( - EVENT_IR_COMMAND_RECEIVED, {BUTTON_NAME: code}) - else: - time.sleep(0.2) - lirc.deinit() - _LOGGER.debug('LIRC interface thread stopped') diff --git a/homeassistant/components/lirc/__init__.py b/homeassistant/components/lirc/__init__.py new file mode 100644 index 0000000000000..0f00eda20072c --- /dev/null +++ b/homeassistant/components/lirc/__init__.py @@ -0,0 +1,86 @@ +"""Support for LIRC devices.""" +# pylint: disable=no-member, import-error +import threading +import time +import logging + +import voluptuous as vol + +from homeassistant.const import ( + EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START) + +REQUIREMENTS = ['python-lirc==1.2.3'] + +_LOGGER = logging.getLogger(__name__) + +BUTTON_NAME = 'button_name' + +DOMAIN = 'lirc' + +EVENT_IR_COMMAND_RECEIVED = 'ir_command_received' + +ICON = 'mdi:remote' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({}), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the LIRC capability.""" + import lirc + + # blocking=True gives unexpected behavior (multiple responses for 1 press) + # also by not blocking, we allow hass to shut down the thread gracefully + # on exit. + lirc.init('home-assistant', blocking=False) + lirc_interface = LircInterface(hass) + + def _start_lirc(_event): + lirc_interface.start() + + def _stop_lirc(_event): + lirc_interface.stopped.set() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start_lirc) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _stop_lirc) + + return True + + +class LircInterface(threading.Thread): + """ + This interfaces with the lirc daemon to read IR commands. + + When using lirc in blocking mode, sometimes repeated commands get produced + in the next read of a command so we use a thread here to just wait + around until a non-empty response is obtained from lirc. + """ + + def __init__(self, hass): + """Construct a LIRC interface object.""" + threading.Thread.__init__(self) + self.daemon = True + self.stopped = threading.Event() + self.hass = hass + + def run(self): + """Run the loop of the LIRC interface thread.""" + import lirc + _LOGGER.debug("LIRC interface thread started") + while not self.stopped.isSet(): + try: + code = lirc.nextcode() # list; empty if no buttons pressed + except lirc.NextCodeError: + _LOGGER.warning("Error reading next code from LIRC") + code = None + # interpret result from python-lirc + if code: + code = code[0] + _LOGGER.info("Got new LIRC code %s", code) + self.hass.bus.fire( + EVENT_IR_COMMAND_RECEIVED, {BUTTON_NAME: code}) + else: + time.sleep(0.2) + lirc.deinit() + _LOGGER.debug('LIRC interface thread stopped') diff --git a/homeassistant/components/litejet.py b/homeassistant/components/litejet.py deleted file mode 100644 index e7c8452b27b7f..0000000000000 --- a/homeassistant/components/litejet.py +++ /dev/null @@ -1,54 +0,0 @@ -"""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/ -""" -import logging - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers import discovery -from homeassistant.const import CONF_PORT - -REQUIREMENTS = ['pylitejet==0.1'] - -_LOGGER = logging.getLogger(__name__) - -CONF_EXCLUDE_NAMES = 'exclude_names' -CONF_INCLUDE_SWITCHES = 'include_switches' - -DOMAIN = 'litejet' - -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 - }) -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the LiteJet component.""" - from pylitejet import LiteJet - - url = config[DOMAIN].get(CONF_PORT) - - hass.data['litejet_system'] = LiteJet(url) - hass.data['litejet_config'] = config[DOMAIN] - - discovery.load_platform(hass, 'light', DOMAIN, {}, config) - if config[DOMAIN].get(CONF_INCLUDE_SWITCHES): - discovery.load_platform(hass, 'switch', DOMAIN, {}, config) - discovery.load_platform(hass, 'scene', DOMAIN, {}, config) - - return True - - -def is_ignored(hass, name): - """Determine if a load, switch, or scene should be ignored.""" - for prefix in hass.data['litejet_config'].get(CONF_EXCLUDE_NAMES, []): - if name.startswith(prefix): - return True - return False diff --git a/homeassistant/components/litejet/__init__.py b/homeassistant/components/litejet/__init__.py new file mode 100644 index 0000000000000..b4e8e45fa0b74 --- /dev/null +++ b/homeassistant/components/litejet/__init__.py @@ -0,0 +1,50 @@ +"""Support for the LiteJet lighting system.""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import discovery +from homeassistant.const import CONF_PORT + +REQUIREMENTS = ['pylitejet==0.1'] + +_LOGGER = logging.getLogger(__name__) + +CONF_EXCLUDE_NAMES = 'exclude_names' +CONF_INCLUDE_SWITCHES = 'include_switches' + +DOMAIN = 'litejet' + +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, + }) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the LiteJet component.""" + from pylitejet import LiteJet + + url = config[DOMAIN].get(CONF_PORT) + + hass.data['litejet_system'] = LiteJet(url) + hass.data['litejet_config'] = config[DOMAIN] + + discovery.load_platform(hass, 'light', DOMAIN, {}, config) + if config[DOMAIN].get(CONF_INCLUDE_SWITCHES): + discovery.load_platform(hass, 'switch', DOMAIN, {}, config) + discovery.load_platform(hass, 'scene', DOMAIN, {}, config) + + return True + + +def is_ignored(hass, name): + """Determine if a load, switch, or scene should be ignored.""" + for prefix in hass.data['litejet_config'].get(CONF_EXCLUDE_NAMES, []): + if name.startswith(prefix): + return True + return False diff --git a/homeassistant/components/locative/.translations/da.json b/homeassistant/components/locative/.translations/da.json new file mode 100644 index 0000000000000..8211d52fa5dea --- /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 0000000000000..81950c49b4c20 --- /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 a57b27cdd7551..92e6775ea27fd 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 0000000000000..237d21c46eefa --- /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 d8b8d55a608db..ff07393da04b0 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 d98793d96e5b9..96626a57c5b8a 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 5a27fbaec6398..e6a5b56ecda40 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 750977fac874c..71c838679fbf9 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -13,7 +13,8 @@ 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/lock/abode.py b/homeassistant/components/lock/abode.py deleted file mode 100644 index a8777ccb50334..0000000000000 --- a/homeassistant/components/lock/abode.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN -from homeassistant.components.lock import LockDevice - - -DEPENDENCIES = ['abode'] - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Abode lock devices.""" - import abodepy.helpers.constants as CONST - - data = hass.data[ABODE_DOMAIN] - - devices = [] - for device in data.abode.get_devices(generic_type=CONST.TYPE_LOCK): - if data.is_excluded(device): - continue - - devices.append(AbodeLock(data, device)) - - data.devices.extend(devices) - - add_entities(devices) - - -class AbodeLock(AbodeDevice, LockDevice): - """Representation of an Abode lock.""" - - def lock(self, **kwargs): - """Lock the device.""" - self._device.lock() - - def unlock(self, **kwargs): - """Unlock the device.""" - self._device.unlock() - - @property - def is_locked(self): - """Return true if device is on.""" - return self._device.is_locked diff --git a/homeassistant/components/lock/august.py b/homeassistant/components/lock/august.py deleted file mode 100644 index ce6792ceb39d3..0000000000000 --- a/homeassistant/components/lock/august.py +++ /dev/null @@ -1,97 +0,0 @@ -""" -Support for August lock. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/lock.august/ -""" -import logging -from datetime import timedelta - -from homeassistant.components.august import DATA_AUGUST -from homeassistant.components.lock import LockDevice -from homeassistant.const import ATTR_BATTERY_LEVEL - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['august'] - -SCAN_INTERVAL = timedelta(seconds=5) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up August locks.""" - data = hass.data[DATA_AUGUST] - devices = [] - - for lock in data.locks: - _LOGGER.debug("Adding lock for %s", lock.device_name) - devices.append(AugustLock(data, lock)) - - add_entities(devices, True) - - -class AugustLock(LockDevice): - """Representation of an August lock.""" - - def __init__(self, data, lock): - """Initialize the lock.""" - self._data = data - self._lock = lock - self._lock_status = None - self._lock_detail = None - self._changed_by = None - self._available = False - - def lock(self, **kwargs): - """Lock the device.""" - self._data.lock(self._lock.device_id) - - def unlock(self, **kwargs): - """Unlock the device.""" - self._data.unlock(self._lock.device_id) - - def update(self): - """Get the latest state of the sensor.""" - self._lock_status = self._data.get_lock_status(self._lock.device_id) - self._available = self._lock_status is not None - - self._lock_detail = self._data.get_lock_detail(self._lock.device_id) - - from august.activity import ActivityType - activity = self._data.get_latest_device_activity( - self._lock.device_id, - ActivityType.LOCK_OPERATION) - - if activity is not None: - self._changed_by = activity.operated_by - - @property - def name(self): - """Return the name of this device.""" - return self._lock.device_name - - @property - def available(self): - """Return the availability of this sensor.""" - return self._available - - @property - def is_locked(self): - """Return true if device is on.""" - from august.lock import LockStatus - return self._lock_status is LockStatus.LOCKED - - @property - def changed_by(self): - """Last change triggered by.""" - return self._changed_by - - @property - def device_state_attributes(self): - """Return the device specific state attributes.""" - if self._lock_detail is None: - return None - - return { - ATTR_BATTERY_LEVEL: self._lock_detail.battery_level, - } diff --git a/homeassistant/components/lock/bmw_connected_drive.py b/homeassistant/components/lock/bmw_connected_drive.py deleted file mode 100644 index b13665610d80c..0000000000000 --- a/homeassistant/components/lock/bmw_connected_drive.py +++ /dev/null @@ -1,118 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN -from homeassistant.components.lock import LockDevice -from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED - -DEPENDENCIES = ['bmw_connected_drive'] - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the BMW Connected Drive lock.""" - accounts = hass.data[BMW_DOMAIN] - _LOGGER.debug('Found BMW accounts: %s', - ', '.join([a.name for a in accounts])) - devices = [] - for account in accounts: - if not account.read_only: - for vehicle in account.account.vehicles: - device = BMWLock(account, vehicle, 'lock', 'BMW lock') - devices.append(device) - add_entities(devices, True) - - -class BMWLock(LockDevice): - """Representation of a BMW vehicle lock.""" - - def __init__(self, account, vehicle, attribute: str, sensor_name): - """Initialize the lock.""" - self._account = account - self._vehicle = vehicle - self._attribute = attribute - self._name = '{} {}'.format(self._vehicle.name, self._attribute) - self._unique_id = '{}-{}'.format(self._vehicle.vin, self._attribute) - self._sensor_name = sensor_name - self._state = None - - @property - def should_poll(self): - """Do not poll this class. - - Updates are triggered from BMWConnectedDriveAccount. - """ - return False - - @property - def unique_id(self): - """Return the unique ID of the lock.""" - return self._unique_id - - @property - def name(self): - """Return the name of the lock.""" - return self._name - - @property - def device_state_attributes(self): - """Return the state attributes of the lock.""" - vehicle_state = self._vehicle.state - return { - 'car': self._vehicle.name, - 'door_lock_state': vehicle_state.door_lock_state.value - } - - @property - def is_locked(self): - """Return true if lock is locked.""" - return self._state == STATE_LOCKED - - def lock(self, **kwargs): - """Lock the car.""" - _LOGGER.debug("%s: locking doors", self._vehicle.name) - # Optimistic state set here because it takes some time before the - # update callback response - self._state = STATE_LOCKED - self.schedule_update_ha_state() - self._vehicle.remote_services.trigger_remote_door_lock() - - def unlock(self, **kwargs): - """Unlock the car.""" - _LOGGER.debug("%s: unlocking doors", self._vehicle.name) - # Optimistic state set here because it takes some time before the - # update callback response - self._state = STATE_UNLOCKED - self.schedule_update_ha_state() - self._vehicle.remote_services.trigger_remote_door_unlock() - - def update(self): - """Update state of the lock.""" - from bimmer_connected.state import LockState - - _LOGGER.debug("%s: updating data for %s", self._vehicle.name, - self._attribute) - vehicle_state = self._vehicle.state - - # Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED - self._state = STATE_LOCKED \ - if vehicle_state.door_lock_state \ - in [LockState.LOCKED, LockState.SECURED] \ - else STATE_UNLOCKED - - def update_callback(self): - """Schedule a state update.""" - self.schedule_update_ha_state(True) - - async def async_added_to_hass(self): - """Add callback after being added to hass. - - Show latest data after startup. - """ - self._account.add_update_listener(self.update_callback) diff --git a/homeassistant/components/lock/tesla.py b/homeassistant/components/lock/tesla.py deleted file mode 100644 index 2ffb996aec349..0000000000000 --- a/homeassistant/components/lock/tesla.py +++ /dev/null @@ -1,55 +0,0 @@ -""" -Support for Tesla door locks. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/lock.tesla/ -""" -import logging - -from homeassistant.components.lock import ENTITY_ID_FORMAT, LockDevice -from homeassistant.components.tesla import DOMAIN as TESLA_DOMAIN -from homeassistant.components.tesla import TeslaDevice -from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['tesla'] - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Tesla lock platform.""" - devices = [TeslaLock(device, hass.data[TESLA_DOMAIN]['controller']) - for device in hass.data[TESLA_DOMAIN]['devices']['lock']] - add_entities(devices, True) - - -class TeslaLock(TeslaDevice, LockDevice): - """Representation of a Tesla door lock.""" - - def __init__(self, tesla_device, controller): - """Initialise of the lock.""" - self._state = None - super().__init__(tesla_device, controller) - self.entity_id = ENTITY_ID_FORMAT.format(self.tesla_id) - - def lock(self, **kwargs): - """Send the lock command.""" - _LOGGER.debug("Locking doors for: %s", self._name) - self.tesla_device.lock() - - def unlock(self, **kwargs): - """Send the unlock command.""" - _LOGGER.debug("Unlocking doors for: %s", self._name) - self.tesla_device.unlock() - - @property - def is_locked(self): - """Get whether the lock is in locked state.""" - return self._state == STATE_LOCKED - - def update(self): - """Update state of the lock.""" - _LOGGER.debug("Updating state for: %s", self._name) - self.tesla_device.update() - self._state = STATE_LOCKED if self.tesla_device.is_locked() \ - else STATE_UNLOCKED diff --git a/homeassistant/components/lock/vera.py b/homeassistant/components/lock/vera.py deleted file mode 100644 index 21287b6328ead..0000000000000 --- a/homeassistant/components/lock/vera.py +++ /dev/null @@ -1,53 +0,0 @@ -""" -Support for Vera locks. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/lock.vera/ -""" -import logging - -from homeassistant.components.lock import ENTITY_ID_FORMAT, LockDevice -from homeassistant.const import (STATE_LOCKED, STATE_UNLOCKED) -from homeassistant.components.vera import ( - VERA_CONTROLLER, VERA_DEVICES, VeraDevice) - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['vera'] - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Find and return Vera locks.""" - add_entities( - [VeraLock(device, hass.data[VERA_CONTROLLER]) for - device in hass.data[VERA_DEVICES]['lock']], True) - - -class VeraLock(VeraDevice, LockDevice): - """Representation of a Vera lock.""" - - def __init__(self, vera_device, controller): - """Initialize the Vera device.""" - self._state = None - VeraDevice.__init__(self, vera_device, controller) - self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) - - def lock(self, **kwargs): - """Lock the device.""" - self.vera_device.lock() - self._state = STATE_LOCKED - - def unlock(self, **kwargs): - """Unlock the device.""" - self.vera_device.unlock() - self._state = STATE_UNLOCKED - - @property - def is_locked(self): - """Return true if device is on.""" - return self._state == STATE_LOCKED - - def update(self): - """Update state by the Vera device callback.""" - self._state = (STATE_LOCKED if self.vera_device.is_locked(True) - else STATE_UNLOCKED) diff --git a/homeassistant/components/lock/verisure.py b/homeassistant/components/lock/verisure.py deleted file mode 100644 index be9a0a24feee7..0000000000000 --- a/homeassistant/components/lock/verisure.py +++ /dev/null @@ -1,136 +0,0 @@ -""" -Interfaces with Verisure locks. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/verisure/ -""" -import logging -from time import sleep -from time import time -from homeassistant.components.verisure import HUB as hub -from homeassistant.components.verisure import ( - CONF_LOCKS, CONF_DEFAULT_LOCK_CODE, CONF_CODE_DIGITS) -from homeassistant.components.lock import LockDevice -from homeassistant.const import ( - ATTR_CODE, STATE_LOCKED, STATE_UNLOCKED) - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Verisure platform.""" - locks = [] - if int(hub.config.get(CONF_LOCKS, 1)): - hub.update_overview() - locks.extend([ - VerisureDoorlock(device_label) - for device_label in hub.get( - "$.doorLockStatusList[*].deviceLabel")]) - - add_entities(locks) - - -class VerisureDoorlock(LockDevice): - """Representation of a Verisure doorlock.""" - - def __init__(self, device_label): - """Initialize the Verisure lock.""" - self._device_label = device_label - self._state = None - self._digits = hub.config.get(CONF_CODE_DIGITS) - self._changed_by = None - self._change_timestamp = 0 - self._default_lock_code = hub.config.get(CONF_DEFAULT_LOCK_CODE) - - @property - def name(self): - """Return the name of the lock.""" - return hub.get_first( - "$.doorLockStatusList[?(@.deviceLabel=='%s')].area", - self._device_label) - - @property - def state(self): - """Return the state of the lock.""" - return self._state - - @property - def available(self): - """Return True if entity is available.""" - return hub.get_first( - "$.doorLockStatusList[?(@.deviceLabel=='%s')]", - self._device_label) is not None - - @property - def changed_by(self): - """Last change triggered by.""" - return self._changed_by - - @property - def code_format(self): - """Return the required six digit code.""" - return '^\\d{%s}$' % self._digits - - def update(self): - """Update lock status.""" - if time() - self._change_timestamp < 10: - return - hub.update_overview() - status = hub.get_first( - "$.doorLockStatusList[?(@.deviceLabel=='%s')].lockedState", - self._device_label) - if status == 'UNLOCKED': - self._state = STATE_UNLOCKED - elif status == 'LOCKED': - self._state = STATE_LOCKED - elif status != 'PENDING': - _LOGGER.error('Unknown lock state %s', status) - self._changed_by = hub.get_first( - "$.doorLockStatusList[?(@.deviceLabel=='%s')].userString", - self._device_label) - - @property - def is_locked(self): - """Return true if lock is locked.""" - return self._state == STATE_LOCKED - - def unlock(self, **kwargs): - """Send unlock command.""" - if self._state is None: - return - - code = kwargs.get(ATTR_CODE, self._default_lock_code) - if code is None: - _LOGGER.error("Code required but none provided") - return - - self.set_lock_state(code, STATE_UNLOCKED) - - def lock(self, **kwargs): - """Send lock command.""" - if self._state == STATE_LOCKED: - return - - code = kwargs.get(ATTR_CODE, self._default_lock_code) - if code is None: - _LOGGER.error("Code required but none provided") - return - - self.set_lock_state(code, STATE_LOCKED) - - def set_lock_state(self, code, state): - """Send set lock state command.""" - lock_state = 'lock' if state == STATE_LOCKED else 'unlock' - transaction_id = hub.session.set_lock_state( - code, - self._device_label, - lock_state)['doorLockStateChangeTransactionId'] - _LOGGER.debug("Verisure doorlock %s", state) - transaction = {} - while 'result' not in transaction: - sleep(0.5) - transaction = hub.session.get_lock_state_transaction( - transaction_id) - if transaction['result'] == 'OK': - self._state = state - self._change_timestamp = time() diff --git a/homeassistant/components/lock/volvooncall.py b/homeassistant/components/lock/volvooncall.py deleted file mode 100644 index 83301aa3d4eca..0000000000000 --- a/homeassistant/components/lock/volvooncall.py +++ /dev/null @@ -1,38 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.components.lock import LockDevice -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): - """Set up the Volvo On Call lock.""" - if discovery_info is None: - return - - async_add_entities([VolvoLock(hass.data[DATA_KEY], *discovery_info)]) - - -class VolvoLock(VolvoEntity, LockDevice): - """Represents a car lock.""" - - @property - def is_locked(self): - """Return true if lock is locked.""" - return self.instrument.is_locked - - async def async_lock(self, **kwargs): - """Lock the car.""" - await self.instrument.lock() - - async def async_unlock(self, **kwargs): - """Unlock the car.""" - await self.instrument.unlock() diff --git a/homeassistant/components/lock/wink.py b/homeassistant/components/lock/wink.py deleted file mode 100644 index 68cc7a79ae66d..0000000000000 --- a/homeassistant/components/lock/wink.py +++ /dev/null @@ -1,209 +0,0 @@ -""" -Support for Wink locks. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/lock.wink/ -""" -import logging - -import voluptuous as vol - -from homeassistant.components.lock import LockDevice -from homeassistant.components.wink import DOMAIN, WinkDevice -from homeassistant.const import ( - ATTR_CODE, ATTR_ENTITY_ID, ATTR_NAME, STATE_UNKNOWN) -import homeassistant.helpers.config_validation as cv - -DEPENDENCIES = ['wink'] - -_LOGGER = logging.getLogger(__name__) - -SERVICE_SET_VACATION_MODE = 'wink_set_lock_vacation_mode' -SERVICE_SET_ALARM_MODE = 'wink_set_lock_alarm_mode' -SERVICE_SET_ALARM_SENSITIVITY = 'wink_set_lock_alarm_sensitivity' -SERVICE_SET_ALARM_STATE = 'wink_set_lock_alarm_state' -SERVICE_SET_BEEPER_STATE = 'wink_set_lock_beeper_state' -SERVICE_ADD_KEY = 'wink_add_new_lock_key_code' - -ATTR_ENABLED = 'enabled' -ATTR_SENSITIVITY = 'sensitivity' -ATTR_MODE = 'mode' - -ALARM_SENSITIVITY_MAP = { - 'low': 0.2, - 'medium_low': 0.4, - 'medium': 0.6, - 'medium_high': 0.8, - 'high': 1.0, -} - -ALARM_MODES_MAP = { - 'activity': 'alert', - 'forced_entry': 'forced_entry', - 'tamper': 'tamper', -} - -SET_ENABLED_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_ENABLED): cv.string, -}) - -SET_SENSITIVITY_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_SENSITIVITY): vol.In(ALARM_SENSITIVITY_MAP) -}) - -SET_ALARM_MODES_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_MODE): vol.In(ALARM_MODES_MAP) -}) - -ADD_KEY_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_NAME): cv.string, - vol.Required(ATTR_CODE): cv.positive_int, -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Wink platform.""" - import pywink - - for lock in pywink.get_locks(): - _id = lock.object_id() + lock.name() - if _id not in hass.data[DOMAIN]['unique_ids']: - add_entities([WinkLockDevice(lock, hass)]) - - def service_handle(service): - """Handle for services.""" - entity_ids = service.data.get('entity_id') - all_locks = hass.data[DOMAIN]['entities']['lock'] - locks_to_set = [] - if entity_ids is None: - locks_to_set = all_locks - else: - for lock in all_locks: - if lock.entity_id in entity_ids: - locks_to_set.append(lock) - - for lock in locks_to_set: - if service.service == SERVICE_SET_VACATION_MODE: - lock.set_vacation_mode(service.data.get(ATTR_ENABLED)) - elif service.service == SERVICE_SET_ALARM_STATE: - lock.set_alarm_state(service.data.get(ATTR_ENABLED)) - elif service.service == SERVICE_SET_BEEPER_STATE: - lock.set_beeper_state(service.data.get(ATTR_ENABLED)) - elif service.service == SERVICE_SET_ALARM_MODE: - lock.set_alarm_mode(service.data.get(ATTR_MODE)) - elif service.service == SERVICE_SET_ALARM_SENSITIVITY: - lock.set_alarm_sensitivity(service.data.get(ATTR_SENSITIVITY)) - elif service.service == SERVICE_ADD_KEY: - name = service.data.get(ATTR_NAME) - code = service.data.get(ATTR_CODE) - lock.add_new_key(code, name) - - hass.services.register(DOMAIN, SERVICE_SET_VACATION_MODE, - service_handle, - schema=SET_ENABLED_SCHEMA) - - hass.services.register(DOMAIN, SERVICE_SET_ALARM_STATE, - service_handle, - schema=SET_ENABLED_SCHEMA) - - hass.services.register(DOMAIN, SERVICE_SET_BEEPER_STATE, - service_handle, - schema=SET_ENABLED_SCHEMA) - - hass.services.register(DOMAIN, SERVICE_SET_ALARM_MODE, - service_handle, - schema=SET_ALARM_MODES_SCHEMA) - - hass.services.register(DOMAIN, SERVICE_SET_ALARM_SENSITIVITY, - service_handle, - schema=SET_SENSITIVITY_SCHEMA) - - hass.services.register(DOMAIN, SERVICE_ADD_KEY, - service_handle, - schema=ADD_KEY_SCHEMA) - - -class WinkLockDevice(WinkDevice, LockDevice): - """Representation of a Wink lock.""" - - async def async_added_to_hass(self): - """Call when entity is added to hass.""" - self.hass.data[DOMAIN]['entities']['lock'].append(self) - - @property - def is_locked(self): - """Return true if device is locked.""" - return self.wink.state() - - def lock(self, **kwargs): - """Lock the device.""" - self.wink.set_state(True) - - def unlock(self, **kwargs): - """Unlock the device.""" - self.wink.set_state(False) - - def set_alarm_state(self, enabled): - """Set lock's alarm state.""" - self.wink.set_alarm_state(enabled) - - def set_vacation_mode(self, enabled): - """Set lock's vacation mode.""" - self.wink.set_vacation_mode(enabled) - - def set_beeper_state(self, enabled): - """Set lock's beeper mode.""" - self.wink.set_beeper_mode(enabled) - - def add_new_key(self, code, name): - """Add a new user key code.""" - self.wink.add_new_key(code, name) - - def set_alarm_sensitivity(self, sensitivity): - """ - Set lock's alarm sensitivity. - - Valid sensitivities: - 0.2, 0.4, 0.6, 0.8, 1.0 - """ - self.wink.set_alarm_sensitivity(sensitivity) - - def set_alarm_mode(self, mode): - """ - Set lock's alarm mode. - - Valid modes: - alert - Beep when lock is locked or unlocked - tamper - 15 sec alarm when lock is disturbed when locked - forced_entry - 3 min alarm when significant force applied - to door when locked. - """ - self.wink.set_alarm_mode(mode) - - @property - def device_state_attributes(self): - """Return the state attributes.""" - super_attrs = super().device_state_attributes - sensitivity = dict_value_to_key(ALARM_SENSITIVITY_MAP, - self.wink.alarm_sensitivity()) - super_attrs['alarm_sensitivity'] = sensitivity - super_attrs['vacation_mode'] = self.wink.vacation_mode_enabled() - super_attrs['beeper_mode'] = self.wink.beeper_enabled() - super_attrs['auto_lock'] = self.wink.auto_lock_enabled() - alarm_mode = dict_value_to_key(ALARM_MODES_MAP, - self.wink.alarm_mode()) - super_attrs['alarm_mode'] = alarm_mode - super_attrs['alarm_enabled'] = self.wink.alarm_enabled() - return super_attrs - - -def dict_value_to_key(dict_map, comp_value): - """Return the key that has the provided value.""" - for key, value in dict_map.items(): - if value == comp_value: - return key - return STATE_UNKNOWN diff --git a/homeassistant/components/lock/xiaomi_aqara.py b/homeassistant/components/lock/xiaomi_aqara.py deleted file mode 100644 index 15415a7328477..0000000000000 --- a/homeassistant/components/lock/xiaomi_aqara.py +++ /dev/null @@ -1,92 +0,0 @@ -""" -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/ -""" -import logging -from homeassistant.components.xiaomi_aqara import (PY_XIAOMI_GATEWAY, - XiaomiDevice) -from homeassistant.components.lock import LockDevice -from homeassistant.const import (STATE_LOCKED, STATE_UNLOCKED) -from homeassistant.helpers.event import async_call_later -from homeassistant.core import callback - -_LOGGER = logging.getLogger(__name__) - -FINGER_KEY = 'fing_verified' -PASSWORD_KEY = 'psw_verified' -CARD_KEY = 'card_verified' -VERIFIED_WRONG_KEY = 'verified_wrong' - -ATTR_VERIFIED_WRONG_TIMES = 'verified_wrong_times' - -UNLOCK_MAINTAIN_TIME = 5 - - -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Perform the setup for Xiaomi devices.""" - devices = [] - - for gateway in hass.data[PY_XIAOMI_GATEWAY].gateways.values(): - for device in gateway.devices['lock']: - model = device['model'] - if model == 'lock.aq1': - devices.append(XiaomiAqaraLock(device, 'Lock', gateway)) - async_add_entities(devices) - - -class XiaomiAqaraLock(LockDevice, XiaomiDevice): - """Representation of a XiaomiAqaraLock.""" - - def __init__(self, device, name, xiaomi_hub): - """Initialize the XiaomiAqaraLock.""" - self._changed_by = 0 - self._verified_wrong_times = 0 - - super().__init__(device, name, xiaomi_hub) - - @property - def is_locked(self) -> bool: - """Return true if lock is locked.""" - if self._state is not None: - return self._state == STATE_LOCKED - - @property - def changed_by(self) -> int: - """Last change triggered by.""" - return self._changed_by - - @property - def device_state_attributes(self) -> dict: - """Return the state attributes.""" - attributes = { - ATTR_VERIFIED_WRONG_TIMES: self._verified_wrong_times, - } - return attributes - - @callback - def clear_unlock_state(self, _): - """Clear unlock state automatically.""" - self._state = STATE_LOCKED - self.async_schedule_update_ha_state() - - def parse_data(self, data, raw_data): - """Parse data sent by gateway.""" - value = data.get(VERIFIED_WRONG_KEY) - if value is not None: - self._verified_wrong_times = int(value) - return True - - for key in (FINGER_KEY, PASSWORD_KEY, CARD_KEY): - value = data.get(key) - if value is not None: - self._changed_by = int(value) - self._verified_wrong_times = 0 - self._state = STATE_UNLOCKED - async_call_later(self.hass, UNLOCK_MAINTAIN_TIME, - self.clear_unlock_state) - return True - - return False diff --git a/homeassistant/components/lock/zwave.py b/homeassistant/components/lock/zwave.py deleted file mode 100644 index c907d5101a9bf..0000000000000 --- a/homeassistant/components/lock/zwave.py +++ /dev/null @@ -1,365 +0,0 @@ -""" -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/ -""" -import logging - -import voluptuous as vol - -from homeassistant.core import callback -from homeassistant.components.lock import DOMAIN, LockDevice -from homeassistant.components import zwave -from homeassistant.helpers.dispatcher import async_dispatcher_connect -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -ATTR_NOTIFICATION = 'notification' -ATTR_LOCK_STATUS = 'lock_status' -ATTR_CODE_SLOT = 'code_slot' -ATTR_USERCODE = 'usercode' -CONFIG_ADVANCED = 'Advanced' - -SERVICE_SET_USERCODE = 'set_usercode' -SERVICE_GET_USERCODE = 'get_usercode' -SERVICE_CLEAR_USERCODE = 'clear_usercode' - -POLYCONTROL = 0x10E -DANALOCK_V2_BTZE = 0x2 -POLYCONTROL_DANALOCK_V2_BTZE_LOCK = (POLYCONTROL, DANALOCK_V2_BTZE) -WORKAROUND_V2BTZE = 1 -WORKAROUND_DEVICE_STATE = 2 -WORKAROUND_TRACK_MESSAGE = 4 -WORKAROUND_ALARM_TYPE = 8 - -DEVICE_MAPPINGS = { - POLYCONTROL_DANALOCK_V2_BTZE_LOCK: WORKAROUND_V2BTZE, - # Kwikset 914TRL ZW500 - (0x0090, 0x440): WORKAROUND_DEVICE_STATE, - (0x0090, 0x446): WORKAROUND_DEVICE_STATE, - # Yale YRD210, Yale YRD240 - (0x0129, 0x0209): WORKAROUND_DEVICE_STATE | WORKAROUND_ALARM_TYPE, - (0x0129, 0xAA00): WORKAROUND_DEVICE_STATE, - (0x0129, 0x0000): WORKAROUND_DEVICE_STATE, - # Yale YRD220 (as reported by adrum in PR #17386) - (0x0109, 0x0000): WORKAROUND_DEVICE_STATE | WORKAROUND_ALARM_TYPE, - # Schlage BE469 - (0x003B, 0x5044): WORKAROUND_DEVICE_STATE | WORKAROUND_TRACK_MESSAGE, - # Schlage FE599NX - (0x003B, 0x504C): WORKAROUND_DEVICE_STATE, -} - -LOCK_NOTIFICATION = { - '1': 'Manual Lock', - '2': 'Manual Unlock', - '5': 'Keypad Lock', - '6': 'Keypad Unlock', - '11': 'Lock Jammed', - '254': 'Unknown Event' -} -NOTIFICATION_RF_LOCK = '3' -NOTIFICATION_RF_UNLOCK = '4' -LOCK_NOTIFICATION[NOTIFICATION_RF_LOCK] = 'RF Lock' -LOCK_NOTIFICATION[NOTIFICATION_RF_UNLOCK] = 'RF Unlock' - -LOCK_ALARM_TYPE = { - '9': 'Deadbolt Jammed', - '16': 'Unlocked by Bluetooth ', - '18': 'Locked with Keypad by user ', - '19': 'Unlocked with Keypad by user ', - '21': 'Manually Locked ', - '22': 'Manually Unlocked ', - '27': 'Auto re-lock', - '33': 'User deleted: ', - '112': 'Master code changed or User added: ', - '113': 'Duplicate Pin-code: ', - '130': 'RF module, power restored', - '144': 'Unlocked by NFC Tag or Card by user ', - '161': 'Tamper Alarm: ', - '167': 'Low Battery', - '168': 'Critical Battery Level', - '169': 'Battery too low to operate' -} -ALARM_RF_LOCK = '24' -ALARM_RF_UNLOCK = '25' -LOCK_ALARM_TYPE[ALARM_RF_LOCK] = 'Locked by RF' -LOCK_ALARM_TYPE[ALARM_RF_UNLOCK] = 'Unlocked by RF' - -MANUAL_LOCK_ALARM_LEVEL = { - '1': 'by Key Cylinder or Inside thumb turn', - '2': 'by Touch function (lock and leave)' -} - -TAMPER_ALARM_LEVEL = { - '1': 'Too many keypresses', - '2': 'Cover removed' -} - -LOCK_STATUS = { - '1': True, - '2': False, - '3': True, - '4': False, - '5': True, - '6': False, - '9': False, - '18': True, - '19': False, - '21': True, - '22': False, - '24': True, - '25': False, - '27': True -} - -ALARM_TYPE_STD = [ - '18', - '19', - '33', - '112', - '113', - '144' -] - -SET_USERCODE_SCHEMA = vol.Schema({ - vol.Required(zwave.const.ATTR_NODE_ID): vol.Coerce(int), - vol.Required(ATTR_CODE_SLOT): vol.Coerce(int), - vol.Required(ATTR_USERCODE): cv.string, -}) - -GET_USERCODE_SCHEMA = vol.Schema({ - vol.Required(zwave.const.ATTR_NODE_ID): vol.Coerce(int), - vol.Required(ATTR_CODE_SLOT): vol.Coerce(int), -}) - -CLEAR_USERCODE_SCHEMA = vol.Schema({ - vol.Required(zwave.const.ATTR_NODE_ID): vol.Coerce(int), - vol.Required(ATTR_CODE_SLOT): vol.Coerce(int), -}) - - -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Old method of setting up Z-Wave locks.""" - pass - - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up Z-Wave Lock from Config Entry.""" - @callback - def async_add_lock(lock): - """Add Z-Wave Lock.""" - async_add_entities([lock]) - - async_dispatcher_connect(hass, 'zwave_new_lock', async_add_lock) - - network = hass.data[zwave.const.DATA_NETWORK] - - def set_usercode(service): - """Set the usercode to index X on the lock.""" - node_id = service.data.get(zwave.const.ATTR_NODE_ID) - lock_node = network.nodes[node_id] - code_slot = service.data.get(ATTR_CODE_SLOT) - usercode = service.data.get(ATTR_USERCODE) - - for value in lock_node.get_values( - class_id=zwave.const.COMMAND_CLASS_USER_CODE).values(): - if value.index != code_slot: - continue - if len(str(usercode)) < 4: - _LOGGER.error("Invalid code provided: (%s) " - "usercode must be atleast 4 and at most" - " %s digits", - usercode, len(value.data)) - break - value.data = str(usercode) - break - - def get_usercode(service): - """Get a usercode at index X on the lock.""" - node_id = service.data.get(zwave.const.ATTR_NODE_ID) - lock_node = network.nodes[node_id] - code_slot = service.data.get(ATTR_CODE_SLOT) - - for value in lock_node.get_values( - class_id=zwave.const.COMMAND_CLASS_USER_CODE).values(): - if value.index != code_slot: - continue - _LOGGER.info("Usercode at slot %s is: %s", value.index, value.data) - break - - def clear_usercode(service): - """Set usercode to slot X on the lock.""" - node_id = service.data.get(zwave.const.ATTR_NODE_ID) - lock_node = network.nodes[node_id] - code_slot = service.data.get(ATTR_CODE_SLOT) - data = '' - - for value in lock_node.get_values( - class_id=zwave.const.COMMAND_CLASS_USER_CODE).values(): - if value.index != code_slot: - continue - for i in range(len(value.data)): - data += '\0' - i += 1 - _LOGGER.debug('Data to clear lock: %s', data) - value.data = data - _LOGGER.info("Usercode at slot %s is cleared", value.index) - break - - hass.services.async_register( - DOMAIN, SERVICE_SET_USERCODE, set_usercode, - schema=SET_USERCODE_SCHEMA) - hass.services.async_register( - DOMAIN, SERVICE_GET_USERCODE, get_usercode, - schema=GET_USERCODE_SCHEMA) - hass.services.async_register( - DOMAIN, SERVICE_CLEAR_USERCODE, clear_usercode, - schema=CLEAR_USERCODE_SCHEMA) - - -def get_device(node, values, **kwargs): - """Create Z-Wave entity device.""" - return ZwaveLock(values) - - -class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice): - """Representation of a Z-Wave Lock.""" - - def __init__(self, values): - """Initialize the Z-Wave lock device.""" - zwave.ZWaveDeviceEntity.__init__(self, values, DOMAIN) - self._state = None - self._notification = None - self._lock_status = None - self._v2btze = None - self._state_workaround = False - self._track_message_workaround = False - self._previous_message = None - self._alarm_type_workaround = False - - # Enable appropriate workaround flags for our device - # Make sure that we have values for the key before converting to int - if (self.node.manufacturer_id.strip() and - self.node.product_id.strip()): - specific_sensor_key = (int(self.node.manufacturer_id, 16), - int(self.node.product_id, 16)) - if specific_sensor_key in DEVICE_MAPPINGS: - workaround = DEVICE_MAPPINGS[specific_sensor_key] - if workaround & WORKAROUND_V2BTZE: - self._v2btze = 1 - _LOGGER.debug("Polycontrol Danalock v2 BTZE " - "workaround enabled") - if workaround & WORKAROUND_DEVICE_STATE: - self._state_workaround = True - _LOGGER.debug( - "Notification device state workaround enabled") - if workaround & WORKAROUND_TRACK_MESSAGE: - self._track_message_workaround = True - _LOGGER.debug("Message tracking workaround enabled") - if workaround & WORKAROUND_ALARM_TYPE: - self._alarm_type_workaround = True - _LOGGER.debug( - "Alarm Type device state workaround enabled") - self.update_properties() - - def update_properties(self): - """Handle data changes for node values.""" - self._state = self.values.primary.data - _LOGGER.debug("lock state set to %s", self._state) - if self.values.access_control: - notification_data = self.values.access_control.data - self._notification = LOCK_NOTIFICATION.get(str(notification_data)) - if self._state_workaround: - self._state = LOCK_STATUS.get(str(notification_data)) - _LOGGER.debug("workaround: lock state set to %s", self._state) - if self._v2btze: - if self.values.v2btze_advanced and \ - self.values.v2btze_advanced.data == CONFIG_ADVANCED: - self._state = LOCK_STATUS.get(str(notification_data)) - _LOGGER.debug( - "Lock state set from Access Control value and is %s, " - "get=%s", str(notification_data), self.state) - - if self._track_message_workaround: - this_message = self.node.stats['lastReceivedMessage'][5] - - if this_message == zwave.const.COMMAND_CLASS_DOOR_LOCK: - self._state = self.values.primary.data - _LOGGER.debug("set state to %s based on message tracking", - self._state) - if self._previous_message == \ - zwave.const.COMMAND_CLASS_DOOR_LOCK: - if self._state: - self._notification = \ - LOCK_NOTIFICATION[NOTIFICATION_RF_LOCK] - self._lock_status = \ - LOCK_ALARM_TYPE[ALARM_RF_LOCK] - else: - self._notification = \ - LOCK_NOTIFICATION[NOTIFICATION_RF_UNLOCK] - self._lock_status = \ - LOCK_ALARM_TYPE[ALARM_RF_UNLOCK] - return - - self._previous_message = this_message - - if not self.values.alarm_type: - return - - alarm_type = self.values.alarm_type.data - if self.values.alarm_level: - alarm_level = self.values.alarm_level.data - else: - alarm_level = None - - if not alarm_type: - return - - if self._alarm_type_workaround: - self._state = LOCK_STATUS.get(str(alarm_type)) - _LOGGER.debug("workaround: lock state set to %s -- alarm type: %s", - self._state, str(alarm_type)) - - if alarm_type == 21: - self._lock_status = '{}{}'.format( - LOCK_ALARM_TYPE.get(str(alarm_type)), - MANUAL_LOCK_ALARM_LEVEL.get(str(alarm_level))) - return - if str(alarm_type) in ALARM_TYPE_STD: - self._lock_status = '{}{}'.format( - LOCK_ALARM_TYPE.get(str(alarm_type)), str(alarm_level)) - return - if alarm_type == 161: - self._lock_status = '{}{}'.format( - LOCK_ALARM_TYPE.get(str(alarm_type)), - TAMPER_ALARM_LEVEL.get(str(alarm_level))) - return - if alarm_type != 0: - self._lock_status = LOCK_ALARM_TYPE.get(str(alarm_type)) - return - - @property - def is_locked(self): - """Return true if device is locked.""" - return self._state - - def lock(self, **kwargs): - """Lock the device.""" - self.values.primary.data = True - - def unlock(self, **kwargs): - """Unlock the device.""" - self.values.primary.data = False - - @property - def device_state_attributes(self): - """Return the device specific state attributes.""" - data = super().device_state_attributes - if self._notification: - data[ATTR_NOTIFICATION] = self._notification - if self._lock_status: - data[ATTR_LOCK_STATUS] = self._lock_status - return data diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py deleted file mode 100644 index 0c6608e3572dd..0000000000000 --- a/homeassistant/components/logbook.py +++ /dev/null @@ -1,515 +0,0 @@ -""" -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/ -""" -from datetime import timedelta -from itertools import groupby -import logging - -import voluptuous as vol - -from homeassistant.loader import bind_hass -from homeassistant.components import sun -from homeassistant.components.http import HomeAssistantView -from homeassistant.const import ( - ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_HIDDEN, ATTR_NAME, ATTR_SERVICE, - CONF_EXCLUDE, CONF_INCLUDE, EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP, EVENT_LOGBOOK_ENTRY, EVENT_STATE_CHANGED, - EVENT_AUTOMATION_TRIGGERED, EVENT_SCRIPT_STARTED, HTTP_BAD_REQUEST, - STATE_NOT_HOME, STATE_OFF, STATE_ON) -from homeassistant.core import ( - DOMAIN as HA_DOMAIN, State, callback, split_entity_id) -from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME -from homeassistant.components.homekit.const import ( - ATTR_DISPLAY_NAME, ATTR_VALUE, DOMAIN as DOMAIN_HOMEKIT, - EVENT_HOMEKIT_CHANGED) -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util - -_LOGGER = logging.getLogger(__name__) - -ATTR_MESSAGE = 'message' - -CONF_DOMAINS = 'domains' -CONF_ENTITIES = 'entities' -CONTINUOUS_DOMAINS = ['proximity', 'sensor'] - -DEPENDENCIES = ['recorder', 'frontend'] - -DOMAIN = 'logbook' - -GROUP_BY_MINUTES = 15 - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: 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]) - }), - CONF_INCLUDE: vol.Schema({ - vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids, - vol.Optional(CONF_DOMAINS, default=[]): - vol.All(cv.ensure_list, [cv.string]) - }) - }), -}, extra=vol.ALLOW_EXTRA) - -ALL_EVENT_TYPES = [ - EVENT_STATE_CHANGED, EVENT_LOGBOOK_ENTRY, - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - EVENT_ALEXA_SMART_HOME, EVENT_HOMEKIT_CHANGED, - EVENT_AUTOMATION_TRIGGERED, EVENT_SCRIPT_STARTED -] - -LOG_MESSAGE_SCHEMA = vol.Schema({ - vol.Required(ATTR_NAME): cv.string, - vol.Required(ATTR_MESSAGE): cv.template, - vol.Optional(ATTR_DOMAIN): cv.slug, - vol.Optional(ATTR_ENTITY_ID): cv.entity_id, -}) - - -@bind_hass -def log_entry(hass, name, message, domain=None, entity_id=None): - """Add an entry to the logbook.""" - hass.add_job(async_log_entry, hass, name, message, domain, entity_id) - - -@bind_hass -def async_log_entry(hass, name, message, domain=None, entity_id=None): - """Add an entry to the logbook.""" - data = { - ATTR_NAME: name, - ATTR_MESSAGE: message - } - - if domain is not None: - data[ATTR_DOMAIN] = domain - if entity_id is not None: - data[ATTR_ENTITY_ID] = entity_id - hass.bus.async_fire(EVENT_LOGBOOK_ENTRY, data) - - -async def async_setup(hass, config): - """Listen for download events to download files.""" - @callback - def log_message(service): - """Handle sending notification message service calls.""" - message = service.data[ATTR_MESSAGE] - name = service.data[ATTR_NAME] - domain = service.data.get(ATTR_DOMAIN) - entity_id = service.data.get(ATTR_ENTITY_ID) - - message.hass = hass - message = message.async_render() - async_log_entry(hass, name, message, domain, entity_id) - - hass.http.register_view(LogbookView(config.get(DOMAIN, {}))) - - await hass.components.frontend.async_register_built_in_panel( - 'logbook', 'logbook', 'hass:format-list-bulleted-type') - - hass.services.async_register( - DOMAIN, 'log', log_message, schema=LOG_MESSAGE_SCHEMA) - return True - - -class LogbookView(HomeAssistantView): - """Handle logbook view requests.""" - - url = '/api/logbook' - name = 'api:logbook' - extra_urls = ['/api/logbook/{datetime}'] - - def __init__(self, config): - """Initialize the logbook view.""" - self.config = config - - async def get(self, request, datetime=None): - """Retrieve logbook entries.""" - if datetime: - datetime = dt_util.parse_datetime(datetime) - - if datetime is None: - return self.json_message('Invalid datetime', HTTP_BAD_REQUEST) - else: - datetime = dt_util.start_of_local_day() - - period = request.query.get('period') - if period is None: - period = 1 - else: - period = int(period) - - entity_id = request.query.get('entity') - start_day = dt_util.as_utc(datetime) - timedelta(days=period - 1) - end_day = start_day + timedelta(days=period) - hass = request.app['hass'] - - def json_events(): - """Fetch events and generate JSON.""" - return self.json(list( - _get_events(hass, self.config, start_day, end_day, entity_id))) - - return await hass.async_add_job(json_events) - - -def humanify(hass, events): - """Generate a converted list of events into Entry objects. - - Will try to group events if possible: - - if 2+ sensor updates in GROUP_BY_MINUTES, show last - - if home assistant stop and start happen in same minute call it restarted - """ - domain_prefixes = tuple('{}.'.format(dom) for dom in CONTINUOUS_DOMAINS) - - # Group events in batches of GROUP_BY_MINUTES - for _, g_events in groupby( - events, - lambda event: event.time_fired.minute // GROUP_BY_MINUTES): - - events_batch = list(g_events) - - # Keep track of last sensor states - last_sensor_event = {} - - # Group HA start/stop events - # Maps minute of event to 1: stop, 2: stop + start - start_stop_events = {} - - # Process events - for event in events_batch: - if event.event_type == EVENT_STATE_CHANGED: - entity_id = event.data.get('entity_id') - - if entity_id.startswith(domain_prefixes): - last_sensor_event[entity_id] = event - - elif event.event_type == EVENT_HOMEASSISTANT_STOP: - if event.time_fired.minute in start_stop_events: - continue - - start_stop_events[event.time_fired.minute] = 1 - - elif event.event_type == EVENT_HOMEASSISTANT_START: - if event.time_fired.minute not in start_stop_events: - continue - - start_stop_events[event.time_fired.minute] = 2 - - # Yield entries - for event in events_batch: - if event.event_type == EVENT_STATE_CHANGED: - - to_state = State.from_dict(event.data.get('new_state')) - - domain = to_state.domain - - # Skip all but the last sensor state - if domain in CONTINUOUS_DOMAINS and \ - event != last_sensor_event[to_state.entity_id]: - continue - - # Don't show continuous sensor value changes in the logbook - if domain in CONTINUOUS_DOMAINS and \ - to_state.attributes.get('unit_of_measurement'): - continue - - yield { - 'when': event.time_fired, - 'name': to_state.name, - 'message': _entry_message_from_state(domain, to_state), - 'domain': domain, - 'entity_id': to_state.entity_id, - 'context_id': event.context.id, - 'context_user_id': event.context.user_id - } - - elif event.event_type == EVENT_HOMEASSISTANT_START: - if start_stop_events.get(event.time_fired.minute) == 2: - continue - - yield { - 'when': event.time_fired, - 'name': "Home Assistant", - 'message': "started", - 'domain': HA_DOMAIN, - 'context_id': event.context.id, - 'context_user_id': event.context.user_id - } - - elif event.event_type == EVENT_HOMEASSISTANT_STOP: - if start_stop_events.get(event.time_fired.minute) == 2: - action = "restarted" - else: - action = "stopped" - - yield { - 'when': event.time_fired, - 'name': "Home Assistant", - 'message': action, - 'domain': HA_DOMAIN, - 'context_id': event.context.id, - 'context_user_id': event.context.user_id - } - - elif event.event_type == EVENT_LOGBOOK_ENTRY: - domain = event.data.get(ATTR_DOMAIN) - entity_id = event.data.get(ATTR_ENTITY_ID) - if domain is None and entity_id is not None: - try: - domain = split_entity_id(str(entity_id))[0] - except IndexError: - pass - - yield { - 'when': event.time_fired, - 'name': event.data.get(ATTR_NAME), - 'message': event.data.get(ATTR_MESSAGE), - 'domain': domain, - 'entity_id': entity_id, - 'context_id': event.context.id, - 'context_user_id': event.context.user_id - } - - elif event.event_type == EVENT_ALEXA_SMART_HOME: - data = event.data - entity_id = data['request'].get('entity_id') - - if entity_id: - state = hass.states.get(entity_id) - name = state.name if state else entity_id - message = "send command {}/{} for {}".format( - data['request']['namespace'], - data['request']['name'], name) - else: - message = "send command {}/{}".format( - data['request']['namespace'], data['request']['name']) - - yield { - 'when': event.time_fired, - 'name': 'Amazon Alexa', - 'message': message, - 'domain': 'alexa', - 'entity_id': entity_id, - 'context_id': event.context.id, - 'context_user_id': event.context.user_id - } - - elif event.event_type == EVENT_HOMEKIT_CHANGED: - data = event.data - entity_id = data.get(ATTR_ENTITY_ID) - value = data.get(ATTR_VALUE) - - value_msg = " to {}".format(value) if value else '' - message = "send command {}{} for {}".format( - data[ATTR_SERVICE], value_msg, data[ATTR_DISPLAY_NAME]) - - yield { - 'when': event.time_fired, - 'name': 'HomeKit', - 'message': message, - 'domain': DOMAIN_HOMEKIT, - 'entity_id': entity_id, - 'context_id': event.context.id, - 'context_user_id': event.context.user_id - } - - elif event.event_type == EVENT_AUTOMATION_TRIGGERED: - yield { - 'when': event.time_fired, - 'name': event.data.get(ATTR_NAME), - 'message': "has been triggered", - 'domain': 'automation', - 'entity_id': event.data.get(ATTR_ENTITY_ID), - 'context_id': event.context.id, - 'context_user_id': event.context.user_id - } - - elif event.event_type == EVENT_SCRIPT_STARTED: - yield { - 'when': event.time_fired, - 'name': event.data.get(ATTR_NAME), - 'message': 'started', - 'domain': 'script', - 'entity_id': event.data.get(ATTR_ENTITY_ID), - 'context_id': event.context.id, - 'context_user_id': event.context.user_id - } - - -def _get_related_entity_ids(session, entity_filter): - from homeassistant.components.recorder.models import States - from homeassistant.components.recorder.util import \ - RETRIES, QUERY_RETRY_WAIT - from sqlalchemy.exc import SQLAlchemyError - import time - - timer_start = time.perf_counter() - - query = session.query(States).with_entities(States.entity_id).distinct() - - for tryno in range(0, RETRIES): - try: - result = [ - row.entity_id for row in query - if entity_filter(row.entity_id)] - - if _LOGGER.isEnabledFor(logging.DEBUG): - elapsed = time.perf_counter() - timer_start - _LOGGER.debug( - 'fetching %d distinct domain/entity_id pairs took %fs', - len(result), - elapsed) - - return result - except SQLAlchemyError as err: - _LOGGER.error("Error executing query: %s", err) - - if tryno == RETRIES - 1: - raise - else: - time.sleep(QUERY_RETRY_WAIT) - - -def _generate_filter_from_config(config): - from homeassistant.helpers.entityfilter import generate_filter - - excluded_entities = [] - excluded_domains = [] - included_entities = [] - included_domains = [] - - exclude = config.get(CONF_EXCLUDE) - if exclude: - excluded_entities = exclude.get(CONF_ENTITIES, []) - excluded_domains = exclude.get(CONF_DOMAINS, []) - include = config.get(CONF_INCLUDE) - if include: - included_entities = include.get(CONF_ENTITIES, []) - included_domains = include.get(CONF_DOMAINS, []) - - return generate_filter(included_domains, included_entities, - excluded_domains, excluded_entities) - - -def _get_events(hass, config, start_day, end_day, entity_id=None): - """Get events for a period of time.""" - from homeassistant.components.recorder.models import Events, States - from homeassistant.components.recorder.util import ( - execute, session_scope) - - entities_filter = _generate_filter_from_config(config) - - with session_scope(hass=hass) as session: - if entity_id is not None: - entity_ids = [entity_id.lower()] - else: - entity_ids = _get_related_entity_ids(session, entities_filter) - - query = session.query(Events).order_by(Events.time_fired) \ - .outerjoin(States, (Events.event_id == States.event_id)) \ - .filter(Events.event_type.in_(ALL_EVENT_TYPES)) \ - .filter((Events.time_fired > start_day) - & (Events.time_fired < end_day)) \ - .filter(((States.last_updated == States.last_changed) & - States.entity_id.in_(entity_ids)) - | (States.state_id.is_(None))) - - events = execute(query) - - return humanify(hass, _exclude_events(events, entities_filter)) - - -def _exclude_events(events, entities_filter): - filtered_events = [] - for event in events: - domain, entity_id = None, None - - if event.event_type == EVENT_STATE_CHANGED: - entity_id = event.data.get('entity_id') - - if entity_id is None: - continue - - # Do not report on new entities - if event.data.get('old_state') is None: - continue - - new_state = event.data.get('new_state') - - # Do not report on entity removal - if not new_state: - continue - - attributes = new_state.get('attributes', {}) - - # If last_changed != last_updated only attributes have changed - # we do not report on that yet. - last_changed = new_state.get('last_changed') - last_updated = new_state.get('last_updated') - if last_changed != last_updated: - continue - - domain = split_entity_id(entity_id)[0] - - # Also filter auto groups. - if domain == 'group' and attributes.get('auto', False): - continue - - # exclude entities which are customized hidden - hidden = attributes.get(ATTR_HIDDEN, False) - if hidden: - continue - - elif event.event_type == EVENT_LOGBOOK_ENTRY: - domain = event.data.get(ATTR_DOMAIN) - entity_id = event.data.get(ATTR_ENTITY_ID) - - elif event.event_type == EVENT_AUTOMATION_TRIGGERED: - domain = 'automation' - entity_id = event.data.get(ATTR_ENTITY_ID) - - elif event.event_type == EVENT_SCRIPT_STARTED: - domain = 'script' - entity_id = event.data.get(ATTR_ENTITY_ID) - - elif event.event_type == EVENT_ALEXA_SMART_HOME: - domain = 'alexa' - - elif event.event_type == EVENT_HOMEKIT_CHANGED: - domain = DOMAIN_HOMEKIT - - if not entity_id and domain: - entity_id = "%s." % (domain, ) - - if not entity_id or entities_filter(entity_id): - filtered_events.append(event) - - return filtered_events - - -def _entry_message_from_state(domain, state): - """Convert a state to a message for the logbook.""" - # We pass domain in so we don't have to split entity_id again - if domain == 'device_tracker': - if state.state == STATE_NOT_HOME: - return 'is away' - return 'is at {}'.format(state.state) - - if domain == 'sun': - if state.state == sun.STATE_ABOVE_HORIZON: - return 'has risen' - return 'has set' - - if state.state == STATE_ON: - # Future: combine groups and its entity entries ? - return "turned on" - - if state.state == STATE_OFF: - return "turned off" - - return "changed to {}".format(state.state) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py new file mode 100644 index 0000000000000..74a90f0f5f0ca --- /dev/null +++ b/homeassistant/components/logbook/__init__.py @@ -0,0 +1,510 @@ +"""Event parser and human readable log generator.""" +from datetime import timedelta +from itertools import groupby +import logging + +import voluptuous as vol + +from homeassistant.loader import bind_hass +from homeassistant.components import sun +from homeassistant.components.http import HomeAssistantView +from homeassistant.const import ( + ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_HIDDEN, ATTR_NAME, ATTR_SERVICE, + CONF_EXCLUDE, CONF_INCLUDE, EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, EVENT_LOGBOOK_ENTRY, EVENT_STATE_CHANGED, + EVENT_AUTOMATION_TRIGGERED, EVENT_SCRIPT_STARTED, HTTP_BAD_REQUEST, + STATE_NOT_HOME, STATE_OFF, STATE_ON) +from homeassistant.core import ( + DOMAIN as HA_DOMAIN, State, callback, split_entity_id) +from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME +from homeassistant.components.homekit.const import ( + ATTR_DISPLAY_NAME, ATTR_VALUE, DOMAIN as DOMAIN_HOMEKIT, + EVENT_HOMEKIT_CHANGED) +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util + +_LOGGER = logging.getLogger(__name__) + +ATTR_MESSAGE = 'message' + +CONF_DOMAINS = 'domains' +CONF_ENTITIES = 'entities' +CONTINUOUS_DOMAINS = ['proximity', 'sensor'] + +DEPENDENCIES = ['recorder', 'frontend'] + +DOMAIN = 'logbook' + +GROUP_BY_MINUTES = 15 + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: 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]), + }), + CONF_INCLUDE: vol.Schema({ + vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids, + vol.Optional(CONF_DOMAINS, default=[]): + vol.All(cv.ensure_list, [cv.string]), + }) + }), +}, extra=vol.ALLOW_EXTRA) + +ALL_EVENT_TYPES = [ + EVENT_STATE_CHANGED, EVENT_LOGBOOK_ENTRY, + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, + EVENT_ALEXA_SMART_HOME, EVENT_HOMEKIT_CHANGED, + EVENT_AUTOMATION_TRIGGERED, EVENT_SCRIPT_STARTED +] + +LOG_MESSAGE_SCHEMA = vol.Schema({ + vol.Required(ATTR_NAME): cv.string, + vol.Required(ATTR_MESSAGE): cv.template, + vol.Optional(ATTR_DOMAIN): cv.slug, + vol.Optional(ATTR_ENTITY_ID): cv.entity_id, +}) + + +@bind_hass +def log_entry(hass, name, message, domain=None, entity_id=None): + """Add an entry to the logbook.""" + hass.add_job(async_log_entry, hass, name, message, domain, entity_id) + + +@bind_hass +def async_log_entry(hass, name, message, domain=None, entity_id=None): + """Add an entry to the logbook.""" + data = { + ATTR_NAME: name, + ATTR_MESSAGE: message + } + + if domain is not None: + data[ATTR_DOMAIN] = domain + if entity_id is not None: + data[ATTR_ENTITY_ID] = entity_id + hass.bus.async_fire(EVENT_LOGBOOK_ENTRY, data) + + +async def async_setup(hass, config): + """Listen for download events to download files.""" + @callback + def log_message(service): + """Handle sending notification message service calls.""" + message = service.data[ATTR_MESSAGE] + name = service.data[ATTR_NAME] + domain = service.data.get(ATTR_DOMAIN) + entity_id = service.data.get(ATTR_ENTITY_ID) + + message.hass = hass + message = message.async_render() + async_log_entry(hass, name, message, domain, entity_id) + + hass.http.register_view(LogbookView(config.get(DOMAIN, {}))) + + await hass.components.frontend.async_register_built_in_panel( + 'logbook', 'logbook', 'hass:format-list-bulleted-type') + + hass.services.async_register( + DOMAIN, 'log', log_message, schema=LOG_MESSAGE_SCHEMA) + return True + + +class LogbookView(HomeAssistantView): + """Handle logbook view requests.""" + + url = '/api/logbook' + name = 'api:logbook' + extra_urls = ['/api/logbook/{datetime}'] + + def __init__(self, config): + """Initialize the logbook view.""" + self.config = config + + async def get(self, request, datetime=None): + """Retrieve logbook entries.""" + if datetime: + datetime = dt_util.parse_datetime(datetime) + + if datetime is None: + return self.json_message('Invalid datetime', HTTP_BAD_REQUEST) + else: + datetime = dt_util.start_of_local_day() + + period = request.query.get('period') + if period is None: + period = 1 + else: + period = int(period) + + entity_id = request.query.get('entity') + start_day = dt_util.as_utc(datetime) - timedelta(days=period - 1) + end_day = start_day + timedelta(days=period) + hass = request.app['hass'] + + def json_events(): + """Fetch events and generate JSON.""" + return self.json(list( + _get_events(hass, self.config, start_day, end_day, entity_id))) + + return await hass.async_add_job(json_events) + + +def humanify(hass, events): + """Generate a converted list of events into Entry objects. + + Will try to group events if possible: + - if 2+ sensor updates in GROUP_BY_MINUTES, show last + - if home assistant stop and start happen in same minute call it restarted + """ + domain_prefixes = tuple('{}.'.format(dom) for dom in CONTINUOUS_DOMAINS) + + # Group events in batches of GROUP_BY_MINUTES + for _, g_events in groupby( + events, + lambda event: event.time_fired.minute // GROUP_BY_MINUTES): + + events_batch = list(g_events) + + # Keep track of last sensor states + last_sensor_event = {} + + # Group HA start/stop events + # Maps minute of event to 1: stop, 2: stop + start + start_stop_events = {} + + # Process events + for event in events_batch: + if event.event_type == EVENT_STATE_CHANGED: + entity_id = event.data.get('entity_id') + + if entity_id.startswith(domain_prefixes): + last_sensor_event[entity_id] = event + + elif event.event_type == EVENT_HOMEASSISTANT_STOP: + if event.time_fired.minute in start_stop_events: + continue + + start_stop_events[event.time_fired.minute] = 1 + + elif event.event_type == EVENT_HOMEASSISTANT_START: + if event.time_fired.minute not in start_stop_events: + continue + + start_stop_events[event.time_fired.minute] = 2 + + # Yield entries + for event in events_batch: + if event.event_type == EVENT_STATE_CHANGED: + + to_state = State.from_dict(event.data.get('new_state')) + + domain = to_state.domain + + # Skip all but the last sensor state + if domain in CONTINUOUS_DOMAINS and \ + event != last_sensor_event[to_state.entity_id]: + continue + + # Don't show continuous sensor value changes in the logbook + if domain in CONTINUOUS_DOMAINS and \ + to_state.attributes.get('unit_of_measurement'): + continue + + yield { + 'when': event.time_fired, + 'name': to_state.name, + 'message': _entry_message_from_state(domain, to_state), + 'domain': domain, + 'entity_id': to_state.entity_id, + 'context_id': event.context.id, + 'context_user_id': event.context.user_id + } + + elif event.event_type == EVENT_HOMEASSISTANT_START: + if start_stop_events.get(event.time_fired.minute) == 2: + continue + + yield { + 'when': event.time_fired, + 'name': "Home Assistant", + 'message': "started", + 'domain': HA_DOMAIN, + 'context_id': event.context.id, + 'context_user_id': event.context.user_id + } + + elif event.event_type == EVENT_HOMEASSISTANT_STOP: + if start_stop_events.get(event.time_fired.minute) == 2: + action = "restarted" + else: + action = "stopped" + + yield { + 'when': event.time_fired, + 'name': "Home Assistant", + 'message': action, + 'domain': HA_DOMAIN, + 'context_id': event.context.id, + 'context_user_id': event.context.user_id + } + + elif event.event_type == EVENT_LOGBOOK_ENTRY: + domain = event.data.get(ATTR_DOMAIN) + entity_id = event.data.get(ATTR_ENTITY_ID) + if domain is None and entity_id is not None: + try: + domain = split_entity_id(str(entity_id))[0] + except IndexError: + pass + + yield { + 'when': event.time_fired, + 'name': event.data.get(ATTR_NAME), + 'message': event.data.get(ATTR_MESSAGE), + 'domain': domain, + 'entity_id': entity_id, + 'context_id': event.context.id, + 'context_user_id': event.context.user_id + } + + elif event.event_type == EVENT_ALEXA_SMART_HOME: + data = event.data + entity_id = data['request'].get('entity_id') + + if entity_id: + state = hass.states.get(entity_id) + name = state.name if state else entity_id + message = "send command {}/{} for {}".format( + data['request']['namespace'], + data['request']['name'], name) + else: + message = "send command {}/{}".format( + data['request']['namespace'], data['request']['name']) + + yield { + 'when': event.time_fired, + 'name': 'Amazon Alexa', + 'message': message, + 'domain': 'alexa', + 'entity_id': entity_id, + 'context_id': event.context.id, + 'context_user_id': event.context.user_id + } + + elif event.event_type == EVENT_HOMEKIT_CHANGED: + data = event.data + entity_id = data.get(ATTR_ENTITY_ID) + value = data.get(ATTR_VALUE) + + value_msg = " to {}".format(value) if value else '' + message = "send command {}{} for {}".format( + data[ATTR_SERVICE], value_msg, data[ATTR_DISPLAY_NAME]) + + yield { + 'when': event.time_fired, + 'name': 'HomeKit', + 'message': message, + 'domain': DOMAIN_HOMEKIT, + 'entity_id': entity_id, + 'context_id': event.context.id, + 'context_user_id': event.context.user_id + } + + elif event.event_type == EVENT_AUTOMATION_TRIGGERED: + yield { + 'when': event.time_fired, + 'name': event.data.get(ATTR_NAME), + 'message': "has been triggered", + 'domain': 'automation', + 'entity_id': event.data.get(ATTR_ENTITY_ID), + 'context_id': event.context.id, + 'context_user_id': event.context.user_id + } + + elif event.event_type == EVENT_SCRIPT_STARTED: + yield { + 'when': event.time_fired, + 'name': event.data.get(ATTR_NAME), + 'message': 'started', + 'domain': 'script', + 'entity_id': event.data.get(ATTR_ENTITY_ID), + 'context_id': event.context.id, + 'context_user_id': event.context.user_id + } + + +def _get_related_entity_ids(session, entity_filter): + from homeassistant.components.recorder.models import States + from homeassistant.components.recorder.util import \ + RETRIES, QUERY_RETRY_WAIT + from sqlalchemy.exc import SQLAlchemyError + import time + + timer_start = time.perf_counter() + + query = session.query(States).with_entities(States.entity_id).distinct() + + for tryno in range(0, RETRIES): + try: + result = [ + row.entity_id for row in query + if entity_filter(row.entity_id)] + + if _LOGGER.isEnabledFor(logging.DEBUG): + elapsed = time.perf_counter() - timer_start + _LOGGER.debug( + 'fetching %d distinct domain/entity_id pairs took %fs', + len(result), + elapsed) + + return result + except SQLAlchemyError as err: + _LOGGER.error("Error executing query: %s", err) + + if tryno == RETRIES - 1: + raise + else: + time.sleep(QUERY_RETRY_WAIT) + + +def _generate_filter_from_config(config): + from homeassistant.helpers.entityfilter import generate_filter + + excluded_entities = [] + excluded_domains = [] + included_entities = [] + included_domains = [] + + exclude = config.get(CONF_EXCLUDE) + if exclude: + excluded_entities = exclude.get(CONF_ENTITIES, []) + excluded_domains = exclude.get(CONF_DOMAINS, []) + include = config.get(CONF_INCLUDE) + if include: + included_entities = include.get(CONF_ENTITIES, []) + included_domains = include.get(CONF_DOMAINS, []) + + return generate_filter(included_domains, included_entities, + excluded_domains, excluded_entities) + + +def _get_events(hass, config, start_day, end_day, entity_id=None): + """Get events for a period of time.""" + from homeassistant.components.recorder.models import Events, States + from homeassistant.components.recorder.util import ( + execute, session_scope) + + entities_filter = _generate_filter_from_config(config) + + with session_scope(hass=hass) as session: + if entity_id is not None: + entity_ids = [entity_id.lower()] + else: + entity_ids = _get_related_entity_ids(session, entities_filter) + + query = session.query(Events).order_by(Events.time_fired) \ + .outerjoin(States, (Events.event_id == States.event_id)) \ + .filter(Events.event_type.in_(ALL_EVENT_TYPES)) \ + .filter((Events.time_fired > start_day) + & (Events.time_fired < end_day)) \ + .filter(((States.last_updated == States.last_changed) & + States.entity_id.in_(entity_ids)) + | (States.state_id.is_(None))) + + events = execute(query) + + return humanify(hass, _exclude_events(events, entities_filter)) + + +def _exclude_events(events, entities_filter): + filtered_events = [] + for event in events: + domain, entity_id = None, None + + if event.event_type == EVENT_STATE_CHANGED: + entity_id = event.data.get('entity_id') + + if entity_id is None: + continue + + # Do not report on new entities + if event.data.get('old_state') is None: + continue + + new_state = event.data.get('new_state') + + # Do not report on entity removal + if not new_state: + continue + + attributes = new_state.get('attributes', {}) + + # If last_changed != last_updated only attributes have changed + # we do not report on that yet. + last_changed = new_state.get('last_changed') + last_updated = new_state.get('last_updated') + if last_changed != last_updated: + continue + + domain = split_entity_id(entity_id)[0] + + # Also filter auto groups. + if domain == 'group' and attributes.get('auto', False): + continue + + # exclude entities which are customized hidden + hidden = attributes.get(ATTR_HIDDEN, False) + if hidden: + continue + + elif event.event_type == EVENT_LOGBOOK_ENTRY: + domain = event.data.get(ATTR_DOMAIN) + entity_id = event.data.get(ATTR_ENTITY_ID) + + elif event.event_type == EVENT_AUTOMATION_TRIGGERED: + domain = 'automation' + entity_id = event.data.get(ATTR_ENTITY_ID) + + elif event.event_type == EVENT_SCRIPT_STARTED: + domain = 'script' + entity_id = event.data.get(ATTR_ENTITY_ID) + + elif event.event_type == EVENT_ALEXA_SMART_HOME: + domain = 'alexa' + + elif event.event_type == EVENT_HOMEKIT_CHANGED: + domain = DOMAIN_HOMEKIT + + if not entity_id and domain: + entity_id = "%s." % (domain, ) + + if not entity_id or entities_filter(entity_id): + filtered_events.append(event) + + return filtered_events + + +def _entry_message_from_state(domain, state): + """Convert a state to a message for the logbook.""" + # We pass domain in so we don't have to split entity_id again + if domain == 'device_tracker': + if state.state == STATE_NOT_HOME: + return 'is away' + return 'is at {}'.format(state.state) + + if domain == 'sun': + if state.state == sun.STATE_ABOVE_HORIZON: + return 'has risen' + return 'has set' + + if state.state == STATE_ON: + # Future: combine groups and its entity entries ? + return "turned on" + + if state.state == STATE_OFF: + return "turned off" + + return "changed to {}".format(state.state) diff --git a/homeassistant/components/logentries.py b/homeassistant/components/logentries.py deleted file mode 100644 index 6dc76d8d932d8..0000000000000 --- a/homeassistant/components/logentries.py +++ /dev/null @@ -1,65 +0,0 @@ -""" -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/ -""" -import json -import logging -import requests - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.const import (CONF_TOKEN, EVENT_STATE_CHANGED) -from homeassistant.helpers import state as state_helper - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'logentries' - -DEFAULT_HOST = 'https://webhook.logentries.com/noformat/logs/' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_TOKEN): cv.string, - }), -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the Logentries component.""" - conf = config[DOMAIN] - token = conf.get(CONF_TOKEN) - le_wh = '{}{}'.format(DEFAULT_HOST, token) - - def logentries_event_listener(event): - """Listen for new messages on the bus and sends them to Logentries.""" - state = event.data.get('new_state') - if state is None: - return - try: - _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, - } - ] - try: - payload = { - "host": le_wh, - "event": json_body - } - requests.post(le_wh, data=json.dumps(payload), timeout=10) - except requests.exceptions.RequestException as error: - _LOGGER.exception("Error sending to Logentries: %s", error) - - hass.bus.listen(EVENT_STATE_CHANGED, logentries_event_listener) - - return True diff --git a/homeassistant/components/logentries/__init__.py b/homeassistant/components/logentries/__init__.py new file mode 100644 index 0000000000000..383fa0005141e --- /dev/null +++ b/homeassistant/components/logentries/__init__.py @@ -0,0 +1,58 @@ +"""Support for sending data to Logentries webhook endpoint.""" +import json +import logging +import requests + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import (CONF_TOKEN, EVENT_STATE_CHANGED) +from homeassistant.helpers import state as state_helper + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'logentries' + +DEFAULT_HOST = 'https://webhook.logentries.com/noformat/logs/' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_TOKEN): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Logentries component.""" + conf = config[DOMAIN] + token = conf.get(CONF_TOKEN) + le_wh = '{}{}'.format(DEFAULT_HOST, token) + + def logentries_event_listener(event): + """Listen for new messages on the bus and sends them to Logentries.""" + state = event.data.get('new_state') + if state is None: + return + try: + _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, + }] + try: + payload = { + 'host': le_wh, + 'event': json_body + } + requests.post(le_wh, data=json.dumps(payload), timeout=10) + except requests.exceptions.RequestException as error: + _LOGGER.exception("Error sending to Logentries: %s", error) + + hass.bus.listen(EVENT_STATE_CHANGED, logentries_event_listener) + + return True diff --git a/homeassistant/components/logger.py b/homeassistant/components/logger.py deleted file mode 100644 index 21ae7595ab8f5..0000000000000 --- a/homeassistant/components/logger.py +++ /dev/null @@ -1,133 +0,0 @@ -""" -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/ -""" -import logging -from collections import OrderedDict - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv - -DOMAIN = 'logger' - -DATA_LOGGER = 'logger' - -SERVICE_SET_DEFAULT_LEVEL = 'set_default_level' -SERVICE_SET_LEVEL = 'set_level' - -LOGSEVERITY = { - 'CRITICAL': 50, - 'FATAL': 50, - 'ERROR': 40, - 'WARNING': 30, - 'WARN': 30, - 'INFO': 20, - 'DEBUG': 10, - 'NOTSET': 0 -} - -LOGGER_DEFAULT = 'default' -LOGGER_LOGS = 'logs' - -ATTR_LEVEL = 'level' - -_VALID_LOG_LEVEL = vol.All(vol.Upper, vol.In(LOGSEVERITY)) - -SERVICE_SET_DEFAULT_LEVEL_SCHEMA = vol.Schema({ATTR_LEVEL: _VALID_LOG_LEVEL}) -SERVICE_SET_LEVEL_SCHEMA = vol.Schema({cv.string: _VALID_LOG_LEVEL}) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(LOGGER_DEFAULT): _VALID_LOG_LEVEL, - vol.Optional(LOGGER_LOGS): vol.Schema({cv.string: _VALID_LOG_LEVEL}), - }), -}, extra=vol.ALLOW_EXTRA) - - -class HomeAssistantLogFilter(logging.Filter): - """A log filter.""" - - def __init__(self, logfilter): - """Initialize the filter.""" - super().__init__() - - self.logfilter = logfilter - - def filter(self, record): - """Filter the log entries.""" - # Log with filtered severity - if LOGGER_LOGS in self.logfilter: - for filtername in self.logfilter[LOGGER_LOGS]: - logseverity = self.logfilter[LOGGER_LOGS][filtername] - if record.name.startswith(filtername): - return record.levelno >= logseverity - - # Log with default severity - default = self.logfilter[LOGGER_DEFAULT] - return record.levelno >= default - - -async def async_setup(hass, config): - """Set up the logger component.""" - logfilter = {} - - def set_default_log_level(level): - """Set the default log level for components.""" - logfilter[LOGGER_DEFAULT] = LOGSEVERITY[level] - - def set_log_levels(logpoints): - """Set the specified log levels.""" - logs = {} - - # Preserve existing logs - if LOGGER_LOGS in logfilter: - logs.update(logfilter[LOGGER_LOGS]) - - # Add new logpoints mapped to correct severity - for key, value in logpoints.items(): - logs[key] = LOGSEVERITY[value] - - logfilter[LOGGER_LOGS] = OrderedDict( - sorted( - logs.items(), - key=lambda t: len(t[0]), - reverse=True - ) - ) - - # Set default log severity - if LOGGER_DEFAULT in config.get(DOMAIN): - set_default_log_level(config.get(DOMAIN)[LOGGER_DEFAULT]) - else: - set_default_log_level('DEBUG') - - logger = logging.getLogger('') - logger.setLevel(logging.NOTSET) - - # Set log filter for all log handler - for handler in logging.root.handlers: - handler.setLevel(logging.NOTSET) - handler.addFilter(HomeAssistantLogFilter(logfilter)) - - if LOGGER_LOGS in config.get(DOMAIN): - set_log_levels(config.get(DOMAIN)[LOGGER_LOGS]) - - async def async_service_handler(service): - """Handle logger services.""" - if service.service == SERVICE_SET_DEFAULT_LEVEL: - set_default_log_level(service.data.get(ATTR_LEVEL)) - else: - set_log_levels(service.data) - - hass.services.async_register( - DOMAIN, SERVICE_SET_DEFAULT_LEVEL, async_service_handler, - schema=SERVICE_SET_DEFAULT_LEVEL_SCHEMA) - - hass.services.async_register( - DOMAIN, SERVICE_SET_LEVEL, async_service_handler, - schema=SERVICE_SET_LEVEL_SCHEMA) - - return True diff --git a/homeassistant/components/logger/__init__.py b/homeassistant/components/logger/__init__.py new file mode 100644 index 0000000000000..2bfc665694575 --- /dev/null +++ b/homeassistant/components/logger/__init__.py @@ -0,0 +1,128 @@ +"""Support for settting the level of logging for components.""" +import logging +from collections import OrderedDict + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv + +DOMAIN = 'logger' + +DATA_LOGGER = 'logger' + +SERVICE_SET_DEFAULT_LEVEL = 'set_default_level' +SERVICE_SET_LEVEL = 'set_level' + +LOGSEVERITY = { + 'CRITICAL': 50, + 'FATAL': 50, + 'ERROR': 40, + 'WARNING': 30, + 'WARN': 30, + 'INFO': 20, + 'DEBUG': 10, + 'NOTSET': 0 +} + +LOGGER_DEFAULT = 'default' +LOGGER_LOGS = 'logs' + +ATTR_LEVEL = 'level' + +_VALID_LOG_LEVEL = vol.All(vol.Upper, vol.In(LOGSEVERITY)) + +SERVICE_SET_DEFAULT_LEVEL_SCHEMA = vol.Schema({ATTR_LEVEL: _VALID_LOG_LEVEL}) +SERVICE_SET_LEVEL_SCHEMA = vol.Schema({cv.string: _VALID_LOG_LEVEL}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Optional(LOGGER_DEFAULT): _VALID_LOG_LEVEL, + vol.Optional(LOGGER_LOGS): vol.Schema({cv.string: _VALID_LOG_LEVEL}), + }), +}, extra=vol.ALLOW_EXTRA) + + +class HomeAssistantLogFilter(logging.Filter): + """A log filter.""" + + def __init__(self, logfilter): + """Initialize the filter.""" + super().__init__() + + self.logfilter = logfilter + + def filter(self, record): + """Filter the log entries.""" + # Log with filtered severity + if LOGGER_LOGS in self.logfilter: + for filtername in self.logfilter[LOGGER_LOGS]: + logseverity = self.logfilter[LOGGER_LOGS][filtername] + if record.name.startswith(filtername): + return record.levelno >= logseverity + + # Log with default severity + default = self.logfilter[LOGGER_DEFAULT] + return record.levelno >= default + + +async def async_setup(hass, config): + """Set up the logger component.""" + logfilter = {} + + def set_default_log_level(level): + """Set the default log level for components.""" + logfilter[LOGGER_DEFAULT] = LOGSEVERITY[level] + + def set_log_levels(logpoints): + """Set the specified log levels.""" + logs = {} + + # Preserve existing logs + if LOGGER_LOGS in logfilter: + logs.update(logfilter[LOGGER_LOGS]) + + # Add new logpoints mapped to correct severity + for key, value in logpoints.items(): + logs[key] = LOGSEVERITY[value] + + logfilter[LOGGER_LOGS] = OrderedDict( + sorted( + logs.items(), + key=lambda t: len(t[0]), + reverse=True + ) + ) + + # Set default log severity + if LOGGER_DEFAULT in config.get(DOMAIN): + set_default_log_level(config.get(DOMAIN)[LOGGER_DEFAULT]) + else: + set_default_log_level('DEBUG') + + logger = logging.getLogger('') + logger.setLevel(logging.NOTSET) + + # Set log filter for all log handler + for handler in logging.root.handlers: + handler.setLevel(logging.NOTSET) + handler.addFilter(HomeAssistantLogFilter(logfilter)) + + if LOGGER_LOGS in config.get(DOMAIN): + set_log_levels(config.get(DOMAIN)[LOGGER_LOGS]) + + async def async_service_handler(service): + """Handle logger services.""" + if service.service == SERVICE_SET_DEFAULT_LEVEL: + set_default_log_level(service.data.get(ATTR_LEVEL)) + else: + set_log_levels(service.data) + + hass.services.async_register( + DOMAIN, SERVICE_SET_DEFAULT_LEVEL, async_service_handler, + schema=SERVICE_SET_DEFAULT_LEVEL_SCHEMA) + + hass.services.async_register( + DOMAIN, SERVICE_SET_LEVEL, async_service_handler, + schema=SERVICE_SET_LEVEL_SCHEMA) + + return True diff --git a/homeassistant/components/logger/services.yaml b/homeassistant/components/logger/services.yaml new file mode 100644 index 0000000000000..4d1ba649d3651 --- /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.py deleted file mode 100644 index c0a7f4c262178..0000000000000 --- a/homeassistant/components/logi_circle.py +++ /dev/null @@ -1,80 +0,0 @@ -""" -Support for Logi Circle cameras. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/logi_circle/ -""" -import logging -import asyncio - -import voluptuous as vol -import async_timeout - -import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD - -REQUIREMENTS = ['logi_circle==0.1.7'] - -_LOGGER = logging.getLogger(__name__) -_TIMEOUT = 15 # seconds - -CONF_ATTRIBUTION = "Data provided by circle.logi.com" - -NOTIFICATION_ID = 'logi_notification' -NOTIFICATION_TITLE = 'Logi Circle Setup' - -DOMAIN = 'logi_circle' -DEFAULT_CACHEDB = '.logi_cache.pickle' -DEFAULT_ENTITY_NAMESPACE = 'logi_circle' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - }), -}, extra=vol.ALLOW_EXTRA) - - -async def async_setup(hass, config): - """Set up the Logi Circle component.""" - conf = config[DOMAIN] - username = conf[CONF_USERNAME] - password = conf[CONF_PASSWORD] - - try: - from logi_circle import Logi - from logi_circle.exception import BadLogin - from aiohttp.client_exceptions import ClientResponseError - - cache = hass.config.path(DEFAULT_CACHEDB) - logi = Logi(username=username, password=password, cache_file=cache) - - with async_timeout.timeout(_TIMEOUT, loop=hass.loop): - await logi.login() - hass.data[DOMAIN] = await logi.cameras - - if not logi.is_connected: - return False - except (BadLogin, ClientResponseError) as ex: - _LOGGER.error('Unable to connect to Logi Circle API: %s', str(ex)) - hass.components.persistent_notification.create( - 'Error: {}
' - 'You will need to restart hass after fixing.' - ''.format(ex), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) - return False - except asyncio.TimeoutError: - # The TimeoutError exception object returns nothing when casted to a - # string, so we'll handle it separately. - err = '{}s timeout exceeded when connecting to Logi Circle API'.format( - _TIMEOUT) - _LOGGER.error(err) - hass.components.persistent_notification.create( - 'Error: {}
' - 'You will need to restart hass after fixing.' - ''.format(err), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) - return False - return True diff --git a/homeassistant/components/logi_circle/__init__.py b/homeassistant/components/logi_circle/__init__.py new file mode 100644 index 0000000000000..50500f47e421b --- /dev/null +++ b/homeassistant/components/logi_circle/__init__.py @@ -0,0 +1,75 @@ +"""Support for Logi Circle devices.""" +import logging +import asyncio + +import voluptuous as vol +import async_timeout + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD + +REQUIREMENTS = ['logi_circle==0.1.7'] + +_LOGGER = logging.getLogger(__name__) +_TIMEOUT = 15 # seconds + +CONF_ATTRIBUTION = "Data provided by circle.logi.com" + +NOTIFICATION_ID = 'logi_notification' +NOTIFICATION_TITLE = 'Logi Circle Setup' + +DOMAIN = 'logi_circle' +DEFAULT_CACHEDB = '.logi_cache.pickle' +DEFAULT_ENTITY_NAMESPACE = 'logi_circle' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the Logi Circle component.""" + conf = config[DOMAIN] + username = conf[CONF_USERNAME] + password = conf[CONF_PASSWORD] + + try: + from logi_circle import Logi + from logi_circle.exception import BadLogin + from aiohttp.client_exceptions import ClientResponseError + + cache = hass.config.path(DEFAULT_CACHEDB) + logi = Logi(username=username, password=password, cache_file=cache) + + with async_timeout.timeout(_TIMEOUT, loop=hass.loop): + await logi.login() + hass.data[DOMAIN] = await logi.cameras + + if not logi.is_connected: + return False + except (BadLogin, ClientResponseError) as ex: + _LOGGER.error('Unable to connect to Logi Circle API: %s', str(ex)) + hass.components.persistent_notification.create( + 'Error: {}
' + 'You will need to restart hass after fixing.' + ''.format(ex), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + return False + except asyncio.TimeoutError: + # The TimeoutError exception object returns nothing when casted to a + # string, so we'll handle it separately. + err = '{}s timeout exceeded when connecting to Logi Circle API'.format( + _TIMEOUT) + _LOGGER.error(err) + hass.components.persistent_notification.create( + 'Error: {}
' + 'You will need to restart hass after fixing.' + ''.format(err), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + return False + return True diff --git a/homeassistant/components/logi_circle/camera.py b/homeassistant/components/logi_circle/camera.py new file mode 100644 index 0000000000000..51bd7c124a3ff --- /dev/null +++ b/homeassistant/components/logi_circle/camera.py @@ -0,0 +1,205 @@ +"""Support to the Logi Circle cameras.""" +import logging +import asyncio +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.helpers import config_validation as cv +from homeassistant.components.logi_circle import ( + DOMAIN as LOGI_CIRCLE_DOMAIN, CONF_ATTRIBUTION) +from homeassistant.components.camera import ( + Camera, PLATFORM_SCHEMA, CAMERA_SERVICE_SCHEMA, SUPPORT_ON_OFF, + ATTR_ENTITY_ID, ATTR_FILENAME, DOMAIN) +from homeassistant.const import ( + ATTR_ATTRIBUTION, ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, + CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF) + +DEPENDENCIES = ['logi_circle'] + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=60) + +SERVICE_SET_CONFIG = 'logi_circle_set_config' +SERVICE_LIVESTREAM_SNAPSHOT = 'logi_circle_livestream_snapshot' +SERVICE_LIVESTREAM_RECORD = 'logi_circle_livestream_record' +DATA_KEY = 'camera.logi_circle' + +BATTERY_SAVING_MODE_KEY = 'BATTERY_SAVING' +PRIVACY_MODE_KEY = 'PRIVACY_MODE' +LED_MODE_KEY = 'LED' + +ATTR_MODE = 'mode' +ATTR_VALUE = 'value' +ATTR_DURATION = 'duration' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): + cv.time_period, +}) + +LOGI_CIRCLE_SERVICE_SET_CONFIG = CAMERA_SERVICE_SCHEMA.extend({ + vol.Required(ATTR_MODE): vol.In([BATTERY_SAVING_MODE_KEY, LED_MODE_KEY, + PRIVACY_MODE_KEY]), + vol.Required(ATTR_VALUE): cv.boolean +}) + +LOGI_CIRCLE_SERVICE_SNAPSHOT = CAMERA_SERVICE_SCHEMA.extend({ + vol.Required(ATTR_FILENAME): cv.template +}) + +LOGI_CIRCLE_SERVICE_RECORD = CAMERA_SERVICE_SCHEMA.extend({ + vol.Required(ATTR_FILENAME): cv.template, + vol.Required(ATTR_DURATION): cv.positive_int +}) + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Set up a Logi Circle Camera.""" + devices = hass.data[LOGI_CIRCLE_DOMAIN] + + cameras = [] + for device in devices: + cameras.append(LogiCam(device, config)) + + async_add_entities(cameras, True) + + async def service_handler(service): + """Dispatch service calls to target entities.""" + params = {key: value for key, value in service.data.items() + if key != ATTR_ENTITY_ID} + entity_ids = service.data.get(ATTR_ENTITY_ID) + if entity_ids: + target_devices = [dev for dev in cameras + if dev.entity_id in entity_ids] + else: + target_devices = cameras + + for target_device in target_devices: + if service.service == SERVICE_SET_CONFIG: + await target_device.set_config(**params) + if service.service == SERVICE_LIVESTREAM_SNAPSHOT: + await target_device.livestream_snapshot(**params) + if service.service == SERVICE_LIVESTREAM_RECORD: + await target_device.download_livestream(**params) + + hass.services.async_register( + DOMAIN, SERVICE_SET_CONFIG, service_handler, + schema=LOGI_CIRCLE_SERVICE_SET_CONFIG) + + hass.services.async_register( + DOMAIN, SERVICE_LIVESTREAM_SNAPSHOT, service_handler, + schema=LOGI_CIRCLE_SERVICE_SNAPSHOT) + + hass.services.async_register( + DOMAIN, SERVICE_LIVESTREAM_RECORD, service_handler, + schema=LOGI_CIRCLE_SERVICE_RECORD) + + +class LogiCam(Camera): + """An implementation of a Logi Circle camera.""" + + def __init__(self, camera, device_info): + """Initialize Logi Circle camera.""" + super().__init__() + self._camera = camera + self._name = self._camera.name + self._id = self._camera.mac_address + self._has_battery = self._camera.supports_feature('battery_level') + + @property + def unique_id(self): + """Return a unique ID.""" + return self._id + + @property + def name(self): + """Return the name of this camera.""" + return self._name + + @property + def supported_features(self): + """Logi Circle camera's support turning on and off ("soft" switch).""" + return SUPPORT_ON_OFF + + @property + def device_state_attributes(self): + """Return the state attributes.""" + state = { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + 'battery_saving_mode': ( + STATE_ON if self._camera.battery_saving else STATE_OFF), + 'ip_address': self._camera.ip_address, + 'microphone_gain': self._camera.microphone_gain + } + + # Add battery attributes if camera is battery-powered + if self._has_battery: + state[ATTR_BATTERY_CHARGING] = self._camera.is_charging + state[ATTR_BATTERY_LEVEL] = self._camera.battery_level + + return state + + async def async_camera_image(self): + """Return a still image from the camera.""" + return await self._camera.get_snapshot_image() + + async def async_turn_off(self): + """Disable streaming mode for this camera.""" + await self._camera.set_streaming_mode(False) + + async def async_turn_on(self): + """Enable streaming mode for this camera.""" + await self._camera.set_streaming_mode(True) + + @property + def should_poll(self): + """Update the image periodically.""" + return True + + async def set_config(self, mode, value): + """Set an configuration property for the target camera.""" + if mode == LED_MODE_KEY: + await self._camera.set_led(value) + if mode == PRIVACY_MODE_KEY: + await self._camera.set_privacy_mode(value) + if mode == BATTERY_SAVING_MODE_KEY: + await self._camera.set_battery_saving_mode(value) + + async def download_livestream(self, filename, duration): + """Download a recording from the camera's livestream.""" + # Render filename from template. + filename.hass = self.hass + stream_file = filename.async_render( + variables={ATTR_ENTITY_ID: self.entity_id}) + + # Respect configured path whitelist. + if not self.hass.config.is_allowed_path(stream_file): + _LOGGER.error( + "Can't write %s, no access to path!", stream_file) + return + + asyncio.shield(self._camera.record_livestream( + stream_file, timedelta(seconds=duration)), loop=self.hass.loop) + + async def livestream_snapshot(self, filename): + """Download a still frame from the camera's livestream.""" + # Render filename from template. + filename.hass = self.hass + snapshot_file = filename.async_render( + variables={ATTR_ENTITY_ID: self.entity_id}) + + # Respect configured path whitelist. + if not self.hass.config.is_allowed_path(snapshot_file): + _LOGGER.error( + "Can't write %s, no access to path!", snapshot_file) + return + + asyncio.shield(self._camera.get_livestream_image( + snapshot_file), loop=self.hass.loop) + + async def async_update(self): + """Update camera entity and refresh attributes.""" + await self._camera.update() diff --git a/homeassistant/components/logi_circle/sensor.py b/homeassistant/components/logi_circle/sensor.py new file mode 100644 index 0000000000000..74c2039c12052 --- /dev/null +++ b/homeassistant/components/logi_circle/sensor.py @@ -0,0 +1,138 @@ +"""Support for Logi Circle sensors.""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.logi_circle import ( + CONF_ATTRIBUTION, DEFAULT_ENTITY_NAMESPACE, DOMAIN as LOGI_CIRCLE_DOMAIN) +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + ATTR_ATTRIBUTION, ATTR_BATTERY_CHARGING, + CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS, + STATE_ON, STATE_OFF) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.icon import icon_for_battery_level +from homeassistant.util.dt import as_local + +DEPENDENCIES = ['logi_circle'] + +_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'], +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_ENTITY_NAMESPACE, default=DEFAULT_ENTITY_NAMESPACE): + cv.string, + vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), +}) + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Set up a sensor for a Logi Circle device.""" + devices = hass.data[LOGI_CIRCLE_DOMAIN] + time_zone = str(hass.config.time_zone) + + sensors = [] + for sensor_type in config.get(CONF_MONITORED_CONDITIONS): + for device in devices: + if device.supports_feature(sensor_type): + sensors.append(LogiSensor(device, time_zone, sensor_type)) + + async_add_entities(sensors, True) + + +class LogiSensor(Entity): + """A sensor implementation for a Logi Circle camera.""" + + def __init__(self, camera, time_zone, sensor_type): + """Initialize a sensor for Logi Circle camera.""" + self._sensor_type = sensor_type + self._camera = camera + self._id = '{}-{}'.format(self._camera.mac_address, self._sensor_type) + self._icon = 'mdi:{}'.format(SENSOR_TYPES.get(self._sensor_type)[2]) + self._name = "{0} {1}".format( + self._camera.name, SENSOR_TYPES.get(self._sensor_type)[0]) + self._state = None + self._tz = time_zone + + @property + def unique_id(self): + """Return a unique ID.""" + return self._id + + @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.""" + state = { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + 'battery_saving_mode': ( + STATE_ON if self._camera.battery_saving else STATE_OFF), + 'ip_address': self._camera.ip_address, + 'microphone_gain': self._camera.microphone_gain + } + + if self._sensor_type == 'battery_level': + state[ATTR_BATTERY_CHARGING] = self._camera.is_charging + + return state + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + if (self._sensor_type == 'battery_level' and + self._state is not None): + return icon_for_battery_level(battery_level=int(self._state), + charging=False) + if (self._sensor_type == 'privacy_mode' and + self._state is not None): + return 'mdi:eye-off' if self._state == STATE_ON else 'mdi:eye' + if (self._sensor_type == 'streaming_mode' and + self._state is not None): + return ( + 'mdi:camera' if self._state == STATE_ON else 'mdi:camera-off') + return self._icon + + @property + def unit_of_measurement(self): + """Return the units of measurement.""" + return SENSOR_TYPES.get(self._sensor_type)[1] + + async def async_update(self): + """Get the latest data and updates the state.""" + _LOGGER.debug("Pulling data from %s sensor", self._name) + await self._camera.update() + + if self._sensor_type == 'last_activity_time': + last_activity = await self._camera.last_activity + if last_activity is not None: + last_activity_time = as_local(last_activity.end_time_utc) + self._state = '{0:0>2}:{1:0>2}'.format( + last_activity_time.hour, last_activity_time.minute) + else: + state = getattr(self._camera, self._sensor_type, None) + if isinstance(state, bool): + self._state = STATE_ON if state is True else STATE_OFF + else: + self._state = state diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index b4cb2b18dcaf2..03b1cf06d6884 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 0000000000000..d43fc1128ae5f --- /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 0402f352c5c0a..9ed3611da27ff 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 45d75b90f7f25..125cefb90265d 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/luftdaten/sensor.py b/homeassistant/components/luftdaten/sensor.py new file mode 100644 index 0000000000000..398ec30a3f5a1 --- /dev/null +++ b/homeassistant/components/luftdaten/sensor.py @@ -0,0 +1,121 @@ +"""Support for Luftdaten sensors.""" +import logging + +from homeassistant.components.luftdaten import ( + DATA_LUFTDATEN, DATA_LUFTDATEN_CLIENT, DEFAULT_ATTRIBUTION, DOMAIN, + SENSORS, TOPIC_UPDATE) +from homeassistant.components.luftdaten.const import ATTR_SENSOR_ID +from homeassistant.const import ( + ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_SHOW_ON_MAP) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['luftdaten'] + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Set up an Luftdaten sensor based on existing config.""" + pass + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up a Luftdaten sensor based on a config entry.""" + luftdaten = hass.data[DOMAIN][DATA_LUFTDATEN_CLIENT][entry.entry_id] + + sensors = [] + for sensor_type in luftdaten.sensor_conditions: + name, icon, unit = SENSORS[sensor_type] + sensors.append( + LuftdatenSensor( + luftdaten, sensor_type, name, icon, unit, + entry.data[CONF_SHOW_ON_MAP])) + + async_add_entities(sensors, True) + + +class LuftdatenSensor(Entity): + """Implementation of a Luftdaten sensor.""" + + def __init__( + self, luftdaten, sensor_type, name, icon, unit, show): + """Initialize the Luftdaten sensor.""" + self._async_unsub_dispatcher_connect = None + self.luftdaten = luftdaten + self._icon = icon + self._name = name + self._data = None + self.sensor_type = sensor_type + self._unit_of_measurement = unit + self._show_on_map = show + self._attrs = {} + + @property + def icon(self): + """Return the icon.""" + return self._icon + + @property + def state(self): + """Return the state of the device.""" + if self._data is not None: + return self._data[self.sensor_type] + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self._unit_of_measurement + + @property + def should_poll(self): + """Disable polling.""" + return False + + @property + def unique_id(self) -> str: + """Return a unique, friendly identifier for this entity.""" + if self._data is not None: + return '{0}_{1}'.format(self._data['sensor_id'], self.sensor_type) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + self._attrs[ATTR_ATTRIBUTION] = DEFAULT_ATTRIBUTION + + if self._data is not None: + self._attrs[ATTR_SENSOR_ID] = self._data['sensor_id'] + + on_map = ATTR_LATITUDE, ATTR_LONGITUDE + no_map = 'lat', 'long' + lat_format, lon_format = on_map if self._show_on_map else no_map + try: + self._attrs[lon_format] = self._data['longitude'] + self._attrs[lat_format] = self._data['latitude'] + return self._attrs + except KeyError: + return + + 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): + """Get the latest data and update the state.""" + try: + self._data = self.luftdaten.data[DATA_LUFTDATEN] + except KeyError: + return diff --git a/homeassistant/components/lupusec.py b/homeassistant/components/lupusec.py deleted file mode 100644 index 94cb3abc4a279..0000000000000 --- a/homeassistant/components/lupusec.py +++ /dev/null @@ -1,94 +0,0 @@ -""" -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 -""" - -import logging - -import voluptuous as vol - -from homeassistant.helpers import config_validation as cv -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'] - -DOMAIN = 'lupusec' - -NOTIFICATION_ID = 'lupusec_notification' -NOTIFICATION_TITLE = 'Lupusec Security Setup' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: 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 - }), -}, extra=vol.ALLOW_EXTRA) - -LUPUSEC_PLATFORMS = [ - 'alarm_control_panel', 'binary_sensor', 'switch' -] - - -def setup(hass, config): - """Set up Lupusec component.""" - from lupupy.exceptions import LupusecException - - conf = config[DOMAIN] - username = conf[CONF_USERNAME] - password = conf[CONF_PASSWORD] - ip_address = conf[CONF_IP_ADDRESS] - name = conf.get(CONF_NAME) - - try: - hass.data[DOMAIN] = LupusecSystem(username, password, ip_address, name) - except LupusecException as ex: - _LOGGER.error(ex) - - hass.components.persistent_notification.create( - 'Error: {}
' - 'You will need to restart hass after fixing.' - ''.format(ex), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) - return False - - for platform in LUPUSEC_PLATFORMS: - discovery.load_platform(hass, platform, DOMAIN, {}, config) - - return True - - -class LupusecSystem: - """Lupusec System class.""" - - def __init__(self, username, password, ip_address, name): - """Initialize the system.""" - import lupupy - self.lupusec = lupupy.Lupusec(username, password, ip_address) - self.name = name - - -class LupusecDevice(Entity): - """Representation of a Lupusec device.""" - - def __init__(self, data, device): - """Initialize a sensor for Lupusec device.""" - self._data = data - self._device = device - - def update(self): - """Update automation state.""" - self._device.refresh() - - @property - def name(self): - """Return the name of the sensor.""" - return self._device.name diff --git a/homeassistant/components/lupusec/__init__.py b/homeassistant/components/lupusec/__init__.py new file mode 100644 index 0000000000000..8a5f098f74192 --- /dev/null +++ b/homeassistant/components/lupusec/__init__.py @@ -0,0 +1,89 @@ +"""Support for Lupusec Home Security system.""" +import logging + +import voluptuous as vol + +from homeassistant.helpers import config_validation as cv +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'] + +DOMAIN = 'lupusec' + +NOTIFICATION_ID = 'lupusec_notification' +NOTIFICATION_TITLE = 'Lupusec Security Setup' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: 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, + }), +}, extra=vol.ALLOW_EXTRA) + +LUPUSEC_PLATFORMS = [ + 'alarm_control_panel', 'binary_sensor', 'switch' +] + + +def setup(hass, config): + """Set up Lupusec component.""" + from lupupy.exceptions import LupusecException + + conf = config[DOMAIN] + username = conf[CONF_USERNAME] + password = conf[CONF_PASSWORD] + ip_address = conf[CONF_IP_ADDRESS] + name = conf.get(CONF_NAME) + + try: + hass.data[DOMAIN] = LupusecSystem(username, password, ip_address, name) + except LupusecException as ex: + _LOGGER.error(ex) + + hass.components.persistent_notification.create( + 'Error: {}
' + 'You will need to restart hass after fixing.' + ''.format(ex), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + return False + + for platform in LUPUSEC_PLATFORMS: + discovery.load_platform(hass, platform, DOMAIN, {}, config) + + return True + + +class LupusecSystem: + """Lupusec System class.""" + + def __init__(self, username, password, ip_address, name): + """Initialize the system.""" + import lupupy + self.lupusec = lupupy.Lupusec(username, password, ip_address) + self.name = name + + +class LupusecDevice(Entity): + """Representation of a Lupusec device.""" + + def __init__(self, data, device): + """Initialize a sensor for Lupusec device.""" + self._data = data + self._device = device + + def update(self): + """Update automation state.""" + self._device.refresh() + + @property + def name(self): + """Return the name of the sensor.""" + return self._device.name diff --git a/homeassistant/components/lupusec/alarm_control_panel.py b/homeassistant/components/lupusec/alarm_control_panel.py new file mode 100644 index 0000000000000..de62e5bfac2cb --- /dev/null +++ b/homeassistant/components/lupusec/alarm_control_panel.py @@ -0,0 +1,64 @@ +"""Support for Lupusec System alarm control panels.""" +from datetime import timedelta + +from homeassistant.components.alarm_control_panel import AlarmControlPanel +from homeassistant.components.lupusec import DOMAIN as LUPUSEC_DOMAIN +from homeassistant.components.lupusec import LupusecDevice +from homeassistant.const import (STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED) + +DEPENDENCIES = ['lupusec'] + +ICON = 'mdi:security' + +SCAN_INTERVAL = timedelta(seconds=2) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up an alarm control panel for a Lupusec device.""" + if discovery_info is None: + return + + data = hass.data[LUPUSEC_DOMAIN] + + alarm_devices = [LupusecAlarm(data, data.lupusec.get_alarm())] + + add_entities(alarm_devices) + + +class LupusecAlarm(LupusecDevice, AlarmControlPanel): + """An alarm_control_panel implementation for Lupusec.""" + + @property + def icon(self): + """Return the icon.""" + return ICON + + @property + def state(self): + """Return the state of the device.""" + if self._device.is_standby: + state = STATE_ALARM_DISARMED + elif self._device.is_away: + state = STATE_ALARM_ARMED_AWAY + elif self._device.is_home: + state = STATE_ALARM_ARMED_HOME + elif self._device.is_alarm_triggered: + state = STATE_ALARM_TRIGGERED + else: + state = None + return state + + def alarm_arm_away(self, code=None): + """Send arm away command.""" + self._device.set_away() + + def alarm_disarm(self, code=None): + """Send disarm command.""" + self._device.set_standby() + + def alarm_arm_home(self, code=None): + """Send arm home command.""" + self._device.set_home() diff --git a/homeassistant/components/lupusec/binary_sensor.py b/homeassistant/components/lupusec/binary_sensor.py new file mode 100644 index 0000000000000..8a5e103db0dd3 --- /dev/null +++ b/homeassistant/components/lupusec/binary_sensor.py @@ -0,0 +1,48 @@ +"""Support for Lupusec Security System binary sensors.""" +import logging +from datetime import timedelta + +from homeassistant.components.lupusec import (LupusecDevice, + DOMAIN as LUPUSEC_DOMAIN) +from homeassistant.components.binary_sensor import (BinarySensorDevice, + DEVICE_CLASSES) + +DEPENDENCIES = ['lupusec'] + +SCAN_INTERVAL = timedelta(seconds=2) + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up a sensor for an Lupusec device.""" + if discovery_info is None: + return + + import lupupy.constants as CONST + + data = hass.data[LUPUSEC_DOMAIN] + + device_types = [CONST.TYPE_OPENING] + + devices = [] + for device in data.lupusec.get_devices(generic_type=device_types): + devices.append(LupusecBinarySensor(data, device)) + + add_entities(devices) + + +class LupusecBinarySensor(LupusecDevice, BinarySensorDevice): + """A binary sensor implementation for Lupusec device.""" + + @property + def is_on(self): + """Return True if the binary sensor is on.""" + return self._device.is_on + + @property + def device_class(self): + """Return the class of the binary sensor.""" + if self._device.generic_type not in DEVICE_CLASSES: + return None + return self._device.generic_type diff --git a/homeassistant/components/lupusec/switch.py b/homeassistant/components/lupusec/switch.py new file mode 100644 index 0000000000000..8a30d65fec308 --- /dev/null +++ b/homeassistant/components/lupusec/switch.py @@ -0,0 +1,48 @@ +"""Support for Lupusec Security System switches.""" +import logging +from datetime import timedelta + +from homeassistant.components.lupusec import (LupusecDevice, + DOMAIN as LUPUSEC_DOMAIN) +from homeassistant.components.switch import SwitchDevice + +DEPENDENCIES = ['lupusec'] + +SCAN_INTERVAL = timedelta(seconds=2) + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up Lupusec switch devices.""" + if discovery_info is None: + return + + import lupupy.constants as CONST + + data = hass.data[LUPUSEC_DOMAIN] + + devices = [] + + for device in data.lupusec.get_devices(generic_type=CONST.TYPE_SWITCH): + + devices.append(LupusecSwitch(data, device)) + + add_entities(devices) + + +class LupusecSwitch(LupusecDevice, SwitchDevice): + """Representation of a Lupusec switch.""" + + def turn_on(self, **kwargs): + """Turn on the device.""" + self._device.switch_on() + + def turn_off(self, **kwargs): + """Turn off the device.""" + self._device.switch_off() + + @property + def is_on(self): + """Return true if device is on.""" + return self._device.is_on diff --git a/homeassistant/components/lutron.py b/homeassistant/components/lutron.py deleted file mode 100644 index 435039ce4bd45..0000000000000 --- a/homeassistant/components/lutron.py +++ /dev/null @@ -1,157 +0,0 @@ -""" -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/ -""" -import logging - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.const import ( - ATTR_ID, CONF_HOST, CONF_PASSWORD, CONF_USERNAME) -from homeassistant.helpers import discovery -from homeassistant.helpers.entity import Entity -from homeassistant.util import slugify - -REQUIREMENTS = ['pylutron==0.2.0'] - -DOMAIN = 'lutron' - -_LOGGER = logging.getLogger(__name__) - -LUTRON_BUTTONS = 'lutron_buttons' -LUTRON_CONTROLLER = 'lutron_controller' -LUTRON_DEVICES = 'lutron_devices' - -# Attribute on events that indicates what action was taken with the button. -ATTR_ACTION = 'action' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - }) -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, base_config): - """Set up the Lutron component.""" - from pylutron import Lutron - - hass.data[LUTRON_BUTTONS] = [] - hass.data[LUTRON_CONTROLLER] = None - hass.data[LUTRON_DEVICES] = {'light': [], - 'cover': [], - 'switch': [], - 'scene': []} - - config = base_config.get(DOMAIN) - hass.data[LUTRON_CONTROLLER] = Lutron( - config[CONF_HOST], config[CONF_USERNAME], config[CONF_PASSWORD]) - - hass.data[LUTRON_CONTROLLER].load_xml_db() - hass.data[LUTRON_CONTROLLER].connect() - _LOGGER.info("Connected to main repeater at %s", config[CONF_HOST]) - - # Sort our devices into types - for area in hass.data[LUTRON_CONTROLLER].areas: - for output in area.outputs: - if output.type == 'SYSTEM_SHADE': - hass.data[LUTRON_DEVICES]['cover'].append((area.name, output)) - elif output.is_dimmable: - hass.data[LUTRON_DEVICES]['light'].append((area.name, output)) - else: - hass.data[LUTRON_DEVICES]['switch'].append((area.name, output)) - for keypad in area.keypads: - for button in keypad.buttons: - # This is the best way to determine if a button does anything - # useful until pylutron is updated to provide information on - # which buttons actually control scenes. - for led in keypad.leds: - if (led.number == button.number and - button.name != 'Unknown Button' and - button.button_type in ('SingleAction', 'Toggle')): - hass.data[LUTRON_DEVICES]['scene'].append( - (area.name, keypad.name, button, led)) - - hass.data[LUTRON_BUTTONS].append( - LutronButton(hass, keypad, button)) - - for component in ('light', 'cover', 'switch', 'scene'): - discovery.load_platform(hass, component, DOMAIN, None, base_config) - return True - - -class LutronDevice(Entity): - """Representation of a Lutron device entity.""" - - def __init__(self, area_name, lutron_device, controller): - """Initialize the device.""" - self._lutron_device = lutron_device - self._controller = controller - self._area_name = area_name - - async def async_added_to_hass(self): - """Register callbacks.""" - self.hass.async_add_executor_job( - self._lutron_device.subscribe, - self._update_callback, - None - ) - - def _update_callback(self, _device, _context, _event, _params): - """Run when invoked by pylutron when the device state changes.""" - self.schedule_update_ha_state() - - @property - def name(self): - """Return the name of the device.""" - return "{} {}".format(self._area_name, self._lutron_device.name) - - @property - def should_poll(self): - """No polling needed.""" - return False - - -class LutronButton: - """Representation of a button on a Lutron keypad. - - This is responsible for firing events as keypad buttons are pressed - (and possibly released, depending on the button type). It is not - represented as an entity; it simply fires events. - """ - - def __init__(self, hass, keypad, button): - """Register callback for activity on the button.""" - name = '{}: {}'.format(keypad.name, button.name) - self._hass = hass - self._has_release_event = 'RaiseLower' in button.button_type - self._id = slugify(name) - self._event = 'lutron_event' - - button.subscribe(self.button_callback, None) - - def button_callback(self, button, context, event, params): - """Fire an event about a button being pressed or released.""" - from pylutron import Button - - if self._has_release_event: - # A raise/lower button; we will get callbacks when the button is - # pressed and when it's released, so fire events for each. - if event == Button.Event.PRESSED: - action = 'pressed' - else: - action = 'released' - else: - # A single-action button; the Lutron controller won't tell us - # when the button is released, so use a different action name - # than for buttons where we expect a release event. - action = 'single' - - data = {ATTR_ID: self._id, ATTR_ACTION: action} - - self._hass.bus.fire(self._event, data) diff --git a/homeassistant/components/lutron/__init__.py b/homeassistant/components/lutron/__init__.py new file mode 100644 index 0000000000000..e4ebec4cc5a17 --- /dev/null +++ b/homeassistant/components/lutron/__init__.py @@ -0,0 +1,152 @@ +"""Component for interacting with a Lutron RadioRA 2 system.""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import ( + ATTR_ID, CONF_HOST, CONF_PASSWORD, CONF_USERNAME) +from homeassistant.helpers import discovery +from homeassistant.helpers.entity import Entity +from homeassistant.util import slugify + +REQUIREMENTS = ['pylutron==0.2.0'] + +DOMAIN = 'lutron' + +_LOGGER = logging.getLogger(__name__) + +LUTRON_BUTTONS = 'lutron_buttons' +LUTRON_CONTROLLER = 'lutron_controller' +LUTRON_DEVICES = 'lutron_devices' + +# Attribute on events that indicates what action was taken with the button. +ATTR_ACTION = 'action' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + }) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, base_config): + """Set up the Lutron component.""" + from pylutron import Lutron + + hass.data[LUTRON_BUTTONS] = [] + hass.data[LUTRON_CONTROLLER] = None + hass.data[LUTRON_DEVICES] = {'light': [], + 'cover': [], + 'switch': [], + 'scene': []} + + config = base_config.get(DOMAIN) + hass.data[LUTRON_CONTROLLER] = Lutron( + config[CONF_HOST], config[CONF_USERNAME], config[CONF_PASSWORD]) + + hass.data[LUTRON_CONTROLLER].load_xml_db() + hass.data[LUTRON_CONTROLLER].connect() + _LOGGER.info("Connected to main repeater at %s", config[CONF_HOST]) + + # Sort our devices into types + for area in hass.data[LUTRON_CONTROLLER].areas: + for output in area.outputs: + if output.type == 'SYSTEM_SHADE': + hass.data[LUTRON_DEVICES]['cover'].append((area.name, output)) + elif output.is_dimmable: + hass.data[LUTRON_DEVICES]['light'].append((area.name, output)) + else: + hass.data[LUTRON_DEVICES]['switch'].append((area.name, output)) + for keypad in area.keypads: + for button in keypad.buttons: + # This is the best way to determine if a button does anything + # useful until pylutron is updated to provide information on + # which buttons actually control scenes. + for led in keypad.leds: + if (led.number == button.number and + button.name != 'Unknown Button' and + button.button_type in ('SingleAction', 'Toggle')): + hass.data[LUTRON_DEVICES]['scene'].append( + (area.name, keypad.name, button, led)) + + hass.data[LUTRON_BUTTONS].append( + LutronButton(hass, keypad, button)) + + for component in ('light', 'cover', 'switch', 'scene'): + discovery.load_platform(hass, component, DOMAIN, None, base_config) + return True + + +class LutronDevice(Entity): + """Representation of a Lutron device entity.""" + + def __init__(self, area_name, lutron_device, controller): + """Initialize the device.""" + self._lutron_device = lutron_device + self._controller = controller + self._area_name = area_name + + async def async_added_to_hass(self): + """Register callbacks.""" + self.hass.async_add_executor_job( + self._lutron_device.subscribe, + self._update_callback, + None + ) + + def _update_callback(self, _device, _context, _event, _params): + """Run when invoked by pylutron when the device state changes.""" + self.schedule_update_ha_state() + + @property + def name(self): + """Return the name of the device.""" + return "{} {}".format(self._area_name, self._lutron_device.name) + + @property + def should_poll(self): + """No polling needed.""" + return False + + +class LutronButton: + """Representation of a button on a Lutron keypad. + + This is responsible for firing events as keypad buttons are pressed + (and possibly released, depending on the button type). It is not + represented as an entity; it simply fires events. + """ + + def __init__(self, hass, keypad, button): + """Register callback for activity on the button.""" + name = '{}: {}'.format(keypad.name, button.name) + self._hass = hass + self._has_release_event = 'RaiseLower' in button.button_type + self._id = slugify(name) + self._event = 'lutron_event' + + button.subscribe(self.button_callback, None) + + def button_callback(self, button, context, event, params): + """Fire an event about a button being pressed or released.""" + from pylutron import Button + + if self._has_release_event: + # A raise/lower button; we will get callbacks when the button is + # pressed and when it's released, so fire events for each. + if event == Button.Event.PRESSED: + action = 'pressed' + else: + action = 'released' + else: + # A single-action button; the Lutron controller won't tell us + # when the button is released, so use a different action name + # than for buttons where we expect a release event. + action = 'single' + + data = {ATTR_ID: self._id, ATTR_ACTION: action} + + self._hass.bus.fire(self._event, data) diff --git a/homeassistant/components/lutron/cover.py b/homeassistant/components/lutron/cover.py new file mode 100644 index 0000000000000..cc7a57a552224 --- /dev/null +++ b/homeassistant/components/lutron/cover.py @@ -0,0 +1,70 @@ +"""Support for Lutron shades.""" +import logging + +from homeassistant.components.cover import ( + CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_SET_POSITION, + ATTR_POSITION) +from homeassistant.components.lutron import ( + LutronDevice, LUTRON_DEVICES, LUTRON_CONTROLLER) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['lutron'] + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Lutron shades.""" + devs = [] + for (area_name, device) in hass.data[LUTRON_DEVICES]['cover']: + dev = LutronCover(area_name, device, hass.data[LUTRON_CONTROLLER]) + devs.append(dev) + + add_entities(devs, True) + return True + + +class LutronCover(LutronDevice, CoverDevice): + """Representation of a Lutron shade.""" + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION + + @property + def is_closed(self): + """Return if the cover is closed.""" + return self._lutron_device.last_level() < 1 + + @property + def current_cover_position(self): + """Return the current position of cover.""" + return self._lutron_device.last_level() + + def close_cover(self, **kwargs): + """Close the cover.""" + self._lutron_device.level = 0 + + def open_cover(self, **kwargs): + """Open the cover.""" + self._lutron_device.level = 100 + + def set_cover_position(self, **kwargs): + """Move the shade to a specific position.""" + if ATTR_POSITION in kwargs: + position = kwargs[ATTR_POSITION] + self._lutron_device.level = position + + def update(self): + """Call when forcing a refresh of the device.""" + # Reading the property (rather than last_level()) fetches value + level = self._lutron_device.level + _LOGGER.debug("Lutron ID: %d updated to %f", + self._lutron_device.id, level) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + attr = {} + attr['Lutron Integration ID'] = self._lutron_device.id + return attr diff --git a/homeassistant/components/lutron/light.py b/homeassistant/components/lutron/light.py new file mode 100644 index 0000000000000..c0b3b99114705 --- /dev/null +++ b/homeassistant/components/lutron/light.py @@ -0,0 +1,84 @@ +"""Support for Lutron lights.""" +import logging + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) +from homeassistant.components.lutron import ( + LutronDevice, LUTRON_DEVICES, LUTRON_CONTROLLER) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['lutron'] + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Lutron lights.""" + devs = [] + for (area_name, device) in hass.data[LUTRON_DEVICES]['light']: + dev = LutronLight(area_name, device, hass.data[LUTRON_CONTROLLER]) + devs.append(dev) + + add_entities(devs, True) + + +def to_lutron_level(level): + """Convert the given HASS light level (0-255) to Lutron (0.0-100.0).""" + return float((level * 100) / 255) + + +def to_hass_level(level): + """Convert the given Lutron (0.0-100.0) light level to HASS (0-255).""" + return int((level * 255) / 100) + + +class LutronLight(LutronDevice, Light): + """Representation of a Lutron Light, including dimmable.""" + + def __init__(self, area_name, lutron_device, controller): + """Initialize the light.""" + self._prev_brightness = None + super().__init__(area_name, lutron_device, controller) + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS + + @property + def brightness(self): + """Return the brightness of the light.""" + new_brightness = to_hass_level(self._lutron_device.last_level()) + if new_brightness != 0: + self._prev_brightness = new_brightness + return new_brightness + + def turn_on(self, **kwargs): + """Turn the light on.""" + if ATTR_BRIGHTNESS in kwargs and self._lutron_device.is_dimmable: + brightness = kwargs[ATTR_BRIGHTNESS] + elif self._prev_brightness == 0: + brightness = 255 / 2 + else: + brightness = self._prev_brightness + self._prev_brightness = brightness + self._lutron_device.level = to_lutron_level(brightness) + + def turn_off(self, **kwargs): + """Turn the light off.""" + self._lutron_device.level = 0 + + @property + def device_state_attributes(self): + """Return the state attributes.""" + attr = {'lutron_integration_id': self._lutron_device.id} + return attr + + @property + def is_on(self): + """Return true if device is on.""" + return self._lutron_device.last_level() > 0 + + def update(self): + """Call when forcing a refresh of the device.""" + if self._prev_brightness is None: + self._prev_brightness = to_hass_level(self._lutron_device.level) diff --git a/homeassistant/components/lutron/scene.py b/homeassistant/components/lutron/scene.py new file mode 100644 index 0000000000000..f9002f2a8398c --- /dev/null +++ b/homeassistant/components/lutron/scene.py @@ -0,0 +1,44 @@ +"""Support for Lutron scenes.""" +import logging + +from homeassistant.components.lutron import ( + LutronDevice, LUTRON_DEVICES, LUTRON_CONTROLLER) +from homeassistant.components.scene import Scene + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['lutron'] + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Lutron scenes.""" + devs = [] + for scene_data in hass.data[LUTRON_DEVICES]['scene']: + (area_name, keypad_name, device, led) = scene_data + dev = LutronScene(area_name, keypad_name, device, led, + hass.data[LUTRON_CONTROLLER]) + devs.append(dev) + + add_entities(devs, True) + + +class LutronScene(LutronDevice, Scene): + """Representation of a Lutron Scene.""" + + 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 + self._led = lutron_led + + def activate(self): + """Activate the scene.""" + self._lutron_device.press() + + @property + def name(self): + """Return the name of the device.""" + return "{} {}: {}".format( + self._area_name, self._keypad_name, self._lutron_device.name) diff --git a/homeassistant/components/lutron/switch.py b/homeassistant/components/lutron/switch.py new file mode 100644 index 0000000000000..bfdb06be33c99 --- /dev/null +++ b/homeassistant/components/lutron/switch.py @@ -0,0 +1,44 @@ +"""Support for Lutron switches.""" +import logging + +from homeassistant.components.switch import SwitchDevice +from homeassistant.components.lutron import ( + LutronDevice, LUTRON_DEVICES, LUTRON_CONTROLLER) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['lutron'] + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Lutron switches.""" + devs = [] + for (area_name, device) in hass.data[LUTRON_DEVICES]['switch']: + dev = LutronSwitch(area_name, device, hass.data[LUTRON_CONTROLLER]) + devs.append(dev) + + add_entities(devs, True) + + +class LutronSwitch(LutronDevice, SwitchDevice): + """Representation of a Lutron Switch.""" + + def turn_on(self, **kwargs): + """Turn the switch on.""" + self._lutron_device.level = 100 + + def turn_off(self, **kwargs): + """Turn the switch off.""" + self._lutron_device.level = 0 + + @property + def device_state_attributes(self): + """Return the state attributes.""" + attr = {} + attr['lutron_integration_id'] = self._lutron_device.id + return attr + + @property + def is_on(self): + """Return true if device is on.""" + return self._lutron_device.last_level() > 0 diff --git a/homeassistant/components/lutron_caseta.py b/homeassistant/components/lutron_caseta.py deleted file mode 100644 index eb4010e43a1ae..0000000000000 --- a/homeassistant/components/lutron_caseta.py +++ /dev/null @@ -1,108 +0,0 @@ -""" -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/ -""" -import logging - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_HOST -from homeassistant.helpers import discovery -from homeassistant.helpers.entity import Entity - -REQUIREMENTS = ['pylutron-caseta==0.5.0'] - -_LOGGER = logging.getLogger(__name__) - -LUTRON_CASETA_SMARTBRIDGE = 'lutron_smartbridge' - -DOMAIN = 'lutron_caseta' - -CONF_KEYFILE = 'keyfile' -CONF_CERTFILE = 'certfile' -CONF_CA_CERTS = 'ca_certs' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: 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 - }) -}, extra=vol.ALLOW_EXTRA) - -LUTRON_CASETA_COMPONENTS = [ - 'light', 'switch', 'cover', 'scene' -] - - -async def async_setup(hass, base_config): - """Set up the Lutron component.""" - from pylutron_caseta.smartbridge import Smartbridge - - config = base_config.get(DOMAIN) - 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) - 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]) - return False - - _LOGGER.info("Connected to Lutron smartbridge at %s", config[CONF_HOST]) - - for component in LUTRON_CASETA_COMPONENTS: - hass.async_create_task(discovery.async_load_platform( - hass, component, DOMAIN, {}, config)) - - return True - - -class LutronCasetaDevice(Entity): - """Common base class for all Lutron Caseta devices.""" - - def __init__(self, device, bridge): - """Set up the base class. - - [:param]device the device metadata - [:param]bridge the smartbridge object - """ - self._device_id = device["device_id"] - self._device_type = device["type"] - self._device_name = device["name"] - self._device_zone = device["zone"] - self._state = None - self._smartbridge = bridge - - async def async_added_to_hass(self): - """Register callbacks.""" - self._smartbridge.add_subscriber(self._device_id, - self.async_schedule_update_ha_state) - - @property - def name(self): - """Return the name of the device.""" - return self._device_name - - @property - def device_state_attributes(self): - """Return the state attributes.""" - attr = { - 'Device ID': self._device_id, - 'Zone ID': self._device_zone, - } - return attr - - @property - def should_poll(self): - """No polling needed.""" - return False diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py new file mode 100644 index 0000000000000..61c005f60b2d2 --- /dev/null +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -0,0 +1,102 @@ +"""Component for interacting with a Lutron Caseta system.""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_HOST +from homeassistant.helpers import discovery +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['pylutron-caseta==0.5.0'] + +_LOGGER = logging.getLogger(__name__) + +LUTRON_CASETA_SMARTBRIDGE = 'lutron_smartbridge' + +DOMAIN = 'lutron_caseta' + +CONF_KEYFILE = 'keyfile' +CONF_CERTFILE = 'certfile' +CONF_CA_CERTS = 'ca_certs' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: 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, + }) +}, extra=vol.ALLOW_EXTRA) + +LUTRON_CASETA_COMPONENTS = [ + 'light', 'switch', 'cover', 'scene' +] + + +async def async_setup(hass, base_config): + """Set up the Lutron component.""" + from pylutron_caseta.smartbridge import Smartbridge + + config = base_config.get(DOMAIN) + 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) + 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]) + return False + + _LOGGER.info("Connected to Lutron smartbridge at %s", config[CONF_HOST]) + + for component in LUTRON_CASETA_COMPONENTS: + hass.async_create_task(discovery.async_load_platform( + hass, component, DOMAIN, {}, config)) + + return True + + +class LutronCasetaDevice(Entity): + """Common base class for all Lutron Caseta devices.""" + + def __init__(self, device, bridge): + """Set up the base class. + + [:param]device the device metadata + [:param]bridge the smartbridge object + """ + self._device_id = device["device_id"] + self._device_type = device["type"] + self._device_name = device["name"] + self._device_zone = device["zone"] + self._state = None + self._smartbridge = bridge + + async def async_added_to_hass(self): + """Register callbacks.""" + self._smartbridge.add_subscriber(self._device_id, + self.async_schedule_update_ha_state) + + @property + def name(self): + """Return the name of the device.""" + return self._device_name + + @property + def device_state_attributes(self): + """Return the state attributes.""" + attr = { + 'Device ID': self._device_id, + 'Zone ID': self._device_zone, + } + return attr + + @property + def should_poll(self): + """No polling needed.""" + return False diff --git a/homeassistant/components/lutron_caseta/cover.py b/homeassistant/components/lutron_caseta/cover.py new file mode 100644 index 0000000000000..5e09dcc3c8582 --- /dev/null +++ b/homeassistant/components/lutron_caseta/cover.py @@ -0,0 +1,63 @@ +"""Support for Lutron Caseta shades.""" +import logging + +from homeassistant.components.cover import ( + CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_SET_POSITION, + ATTR_POSITION, DOMAIN) +from homeassistant.components.lutron_caseta import ( + LUTRON_CASETA_SMARTBRIDGE, LutronCasetaDevice) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['lutron_caseta'] + + +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] + cover_devices = bridge.get_devices_by_domain(DOMAIN) + for cover_device in cover_devices: + dev = LutronCasetaCover(cover_device, bridge) + devs.append(dev) + + async_add_entities(devs, True) + + +class LutronCasetaCover(LutronCasetaDevice, CoverDevice): + """Representation of a Lutron shade.""" + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION + + @property + def is_closed(self): + """Return if the cover is closed.""" + return self._state['current_state'] < 1 + + @property + def current_cover_position(self): + """Return the current position of cover.""" + return self._state['current_state'] + + async def async_close_cover(self, **kwargs): + """Close the cover.""" + self._smartbridge.set_value(self._device_id, 0) + + async def async_open_cover(self, **kwargs): + """Open the cover.""" + self._smartbridge.set_value(self._device_id, 100) + + async def async_set_cover_position(self, **kwargs): + """Move the shade to a specific position.""" + if ATTR_POSITION in kwargs: + position = kwargs[ATTR_POSITION] + self._smartbridge.set_value(self._device_id, position) + + async def async_update(self): + """Call when forcing a refresh of the device.""" + self._state = self._smartbridge.get_device_by_id(self._device_id) + _LOGGER.debug(self._state) diff --git a/homeassistant/components/lutron_caseta/light.py b/homeassistant/components/lutron_caseta/light.py new file mode 100644 index 0000000000000..3bab781f3b609 --- /dev/null +++ b/homeassistant/components/lutron_caseta/light.py @@ -0,0 +1,60 @@ +"""Support for Lutron Caseta lights.""" +import logging + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light, DOMAIN) +from homeassistant.components.lutron.light import ( + to_hass_level, to_lutron_level) +from homeassistant.components.lutron_caseta import ( + LUTRON_CASETA_SMARTBRIDGE, LutronCasetaDevice) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['lutron_caseta'] + + +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] + light_devices = bridge.get_devices_by_domain(DOMAIN) + for light_device in light_devices: + dev = LutronCasetaLight(light_device, bridge) + devs.append(dev) + + async_add_entities(devs, True) + + +class LutronCasetaLight(LutronCasetaDevice, Light): + """Representation of a Lutron Light, including dimmable.""" + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS + + @property + def brightness(self): + """Return the brightness of the light.""" + return to_hass_level(self._state["current_state"]) + + async def async_turn_on(self, **kwargs): + """Turn the light on.""" + brightness = kwargs.get(ATTR_BRIGHTNESS, 255) + self._smartbridge.set_value(self._device_id, + to_lutron_level(brightness)) + + async def async_turn_off(self, **kwargs): + """Turn the light off.""" + self._smartbridge.set_value(self._device_id, 0) + + @property + def is_on(self): + """Return true if device is on.""" + return self._state["current_state"] > 0 + + async def async_update(self): + """Call when forcing a refresh of the device.""" + self._state = self._smartbridge.get_device_by_id(self._device_id) + _LOGGER.debug(self._state) diff --git a/homeassistant/components/lutron_caseta/scene.py b/homeassistant/components/lutron_caseta/scene.py new file mode 100644 index 0000000000000..c6ca7bad3ac96 --- /dev/null +++ b/homeassistant/components/lutron_caseta/scene.py @@ -0,0 +1,41 @@ +"""Support for Lutron Caseta scenes.""" +import logging + +from homeassistant.components.lutron_caseta import LUTRON_CASETA_SMARTBRIDGE +from homeassistant.components.scene import Scene + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['lutron_caseta'] + + +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] + scenes = bridge.get_scenes() + for scene in scenes: + dev = LutronCasetaScene(scenes[scene], bridge) + devs.append(dev) + + async_add_entities(devs, True) + + +class LutronCasetaScene(Scene): + """Representation of a Lutron Caseta scene.""" + + def __init__(self, scene, bridge): + """Initialize the Lutron Caseta scene.""" + self._scene_name = scene['name'] + self._scene_id = scene['scene_id'] + self._bridge = bridge + + @property + def name(self): + """Return the name of the scene.""" + return self._scene_name + + async def async_activate(self): + """Activate the scene.""" + self._bridge.activate_scene(self._scene_id) diff --git a/homeassistant/components/lutron_caseta/switch.py b/homeassistant/components/lutron_caseta/switch.py new file mode 100644 index 0000000000000..0ef0595187b20 --- /dev/null +++ b/homeassistant/components/lutron_caseta/switch.py @@ -0,0 +1,47 @@ +"""Support for Lutron Caseta switches.""" +import logging + +from homeassistant.components.lutron_caseta import ( + LUTRON_CASETA_SMARTBRIDGE, LutronCasetaDevice) +from homeassistant.components.switch import SwitchDevice, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['lutron_caseta'] + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Set up Lutron switch.""" + devs = [] + bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE] + switch_devices = bridge.get_devices_by_domain(DOMAIN) + + for switch_device in switch_devices: + dev = LutronCasetaLight(switch_device, bridge) + devs.append(dev) + + async_add_entities(devs, True) + return True + + +class LutronCasetaLight(LutronCasetaDevice, SwitchDevice): + """Representation of a Lutron Caseta switch.""" + + async def async_turn_on(self, **kwargs): + """Turn the switch on.""" + self._smartbridge.turn_on(self._device_id) + + async def async_turn_off(self, **kwargs): + """Turn the switch off.""" + self._smartbridge.turn_off(self._device_id) + + @property + def is_on(self): + """Return true if device is on.""" + return self._state["current_state"] > 0 + + async def async_update(self): + """Update when forcing a refresh of the device.""" + self._state = self._smartbridge.get_device_by_id(self._device_id) + _LOGGER.debug(self._state) diff --git a/homeassistant/components/mailbox/__init__.py b/homeassistant/components/mailbox/__init__.py index 2ed12b231649a..1907a1e9e978a 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 ae0939c3da5d6..db5d4e8d6eef1 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.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/asterisk_mbox.py b/homeassistant/components/mailbox/asterisk_mbox.py deleted file mode 100644 index 087018084f2c7..0000000000000 --- a/homeassistant/components/mailbox/asterisk_mbox.py +++ /dev/null @@ -1,75 +0,0 @@ -""" -Asterisk Voicemail interface. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/mailbox.asteriskvm/ -""" -import logging - -from homeassistant.components.asterisk_mbox import DOMAIN as ASTERISK_DOMAIN -from homeassistant.components.mailbox import ( - CONTENT_TYPE_MPEG, Mailbox, StreamError) -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['asterisk_mbox'] - -SIGNAL_MESSAGE_REQUEST = 'asterisk_mbox.message_request' -SIGNAL_MESSAGE_UPDATE = 'asterisk_mbox.message_updated' - - -async def async_get_handler(hass, config, discovery_info=None): - """Set up the Asterix VM platform.""" - return AsteriskMailbox(hass, ASTERISK_DOMAIN) - - -class AsteriskMailbox(Mailbox): - """Asterisk VM Sensor.""" - - def __init__(self, hass, name): - """Initialize Asterisk mailbox.""" - super().__init__(hass, name) - async_dispatcher_connect( - self.hass, SIGNAL_MESSAGE_UPDATE, self._update_callback) - - @callback - def _update_callback(self, msg): - """Update the message count in HA, if needed.""" - self.async_update() - - @property - def media_type(self): - """Return the supported media type.""" - return CONTENT_TYPE_MPEG - - @property - def can_delete(self): - """Return if messages can be deleted.""" - return True - - @property - def has_media(self): - """Return if messages have attached media files.""" - return True - - async def async_get_media(self, msgid): - """Return the media blob for the msgid.""" - from asterisk_mbox import ServerError - client = self.hass.data[ASTERISK_DOMAIN].client - try: - return client.mp3(msgid, sync=True) - except ServerError as err: - raise StreamError(err) - - async def async_get_messages(self): - """Return a list of the current messages.""" - return self.hass.data[ASTERISK_DOMAIN].messages - - def async_delete(self, msgid): - """Delete the specified messages.""" - client = self.hass.data[ASTERISK_DOMAIN].client - _LOGGER.info("Deleting: %s", msgid) - client.delete(msgid) - return True diff --git a/homeassistant/components/mailbox/demo.py b/homeassistant/components/mailbox/demo.py index 2aabde42b36e4..885988adb6b51 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 @@ _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 0000000000000..0e25974031d75 --- /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 0000000000000..905715de72711 --- /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 95897a25f1535..ae973bdc93d64 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 7fa08bb0f2206..3903bd14e258d 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 @@ 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 d4052678d360c..05137254fcccb 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.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 @@ # 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.py deleted file mode 100644 index d30a756845295..0000000000000 --- a/homeassistant/components/map.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -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/ -""" -DOMAIN = 'map' - - -async def async_setup(hass, config): - """Register the built-in map panel.""" - await hass.components.frontend.async_register_built_in_panel( - 'map', 'map', 'hass:tooltip-account') - return True diff --git a/homeassistant/components/map/__init__.py b/homeassistant/components/map/__init__.py new file mode 100644 index 0000000000000..df8ac49a6d569 --- /dev/null +++ b/homeassistant/components/map/__init__.py @@ -0,0 +1,9 @@ +"""Support for showing device locations.""" +DOMAIN = 'map' + + +async def async_setup(hass, config): + """Register the built-in map panel.""" + await hass.components.frontend.async_register_built_in_panel( + 'map', 'map', 'hass:tooltip-account') + return True diff --git a/homeassistant/components/matrix.py b/homeassistant/components/matrix.py deleted file mode 100644 index 5f6c30aaeba5f..0000000000000 --- a/homeassistant/components/matrix.py +++ /dev/null @@ -1,344 +0,0 @@ -""" -The matrix bot component. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/matrix/ -""" -import logging -import os -from functools import partial - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import (ATTR_TARGET, ATTR_MESSAGE) -from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, - CONF_VERIFY_SSL, CONF_NAME, - EVENT_HOMEASSISTANT_STOP, - EVENT_HOMEASSISTANT_START) -from homeassistant.util.json import load_json, save_json -from homeassistant.exceptions import HomeAssistantError - -REQUIREMENTS = ['matrix-client==0.2.0'] - -_LOGGER = logging.getLogger(__name__) - -SESSION_FILE = '.matrix.conf' - -CONF_HOMESERVER = 'homeserver' -CONF_ROOMS = 'rooms' -CONF_COMMANDS = 'commands' -CONF_WORD = 'word' -CONF_EXPRESSION = 'expression' - -EVENT_MATRIX_COMMAND = 'matrix_command' - -DOMAIN = 'matrix' - -COMMAND_SCHEMA = vol.All( - # Basic Schema - vol.Schema({ - 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]), - }), - # Make sure it's either a word or an expression command - cv.has_at_least_one_key(CONF_WORD, CONF_EXPRESSION) -) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_HOMESERVER): cv.url, - 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_COMMANDS, default=[]): [COMMAND_SCHEMA] - }) -}, extra=vol.ALLOW_EXTRA) - -SERVICE_SEND_MESSAGE = 'send_message' - -SERVICE_SCHEMA_SEND_MESSAGE = vol.Schema({ - vol.Required(ATTR_MESSAGE): cv.string, - vol.Required(ATTR_TARGET): vol.All(cv.ensure_list, [cv.string]), -}) - - -def setup(hass, config): - """Set up the Matrix bot component.""" - from matrix_client.client import MatrixRequestError - - config = config[DOMAIN] - - 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], - config[CONF_COMMANDS]) - hass.data[DOMAIN] = bot - except MatrixRequestError as exception: - _LOGGER.error("Matrix failed to log in: %s", str(exception)) - return False - - hass.services.register( - DOMAIN, SERVICE_SEND_MESSAGE, bot.handle_send_message, - schema=SERVICE_SCHEMA_SEND_MESSAGE) - - return True - - -class MatrixBot: - """The Matrix Bot.""" - - def __init__(self, hass, config_file, homeserver, verify_ssl, - username, password, listening_rooms, commands): - """Set up the client.""" - self.hass = hass - - self._session_filepath = config_file - self._auth_tokens = self._get_auth_tokens() - - self._homeserver = homeserver - self._verify_tls = verify_ssl - self._mx_id = username - self._password = password - - self._listening_rooms = listening_rooms - - # We have to fetch the aliases for every room to make sure we don't - # join it twice by accident. However, fetching aliases is costly, - # so we only do it once per room. - self._aliases_fetched_for = set() - - # word commands are stored dict-of-dict: First dict indexes by room ID - # / alias, second dict indexes by the word - self._word_commands = {} - - # regular expression commands are stored as a list of commands per - # room, i.e., a dict-of-list - self._expression_commands = {} - - for command in commands: - if not command.get(CONF_ROOMS): - command[CONF_ROOMS] = listening_rooms - - if command.get(CONF_WORD): - for room_id in command[CONF_ROOMS]: - if room_id not in self._word_commands: - self._word_commands[room_id] = {} - self._word_commands[room_id][command[CONF_WORD]] = command - else: - for room_id in command[CONF_ROOMS]: - if room_id not in self._expression_commands: - self._expression_commands[room_id] = [] - self._expression_commands[room_id].append(command) - - # Log in. This raises a MatrixRequestError if login is unsuccessful - self._client = self._login() - - def handle_matrix_exception(exception): - """Handle exceptions raised inside the Matrix SDK.""" - _LOGGER.error("Matrix exception:\n %s", str(exception)) - - self._client.start_listener_thread( - exception_handler=handle_matrix_exception) - - def stop_client(_): - """Run once when Home Assistant stops.""" - self._client.stop_listener_thread() - - self.hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_client) - - # Joining rooms potentially does a lot of I/O, so we defer it - def handle_startup(_): - """Run once when Home Assistant finished startup.""" - self._join_rooms() - - self.hass.bus.listen_once(EVENT_HOMEASSISTANT_START, handle_startup) - - def _handle_room_message(self, room_id, room, event): - """Handle a message sent to a room.""" - if event['content']['msgtype'] != 'm.text': - return - - if event['sender'] == self._mx_id: - return - - _LOGGER.debug("Handling message: %s", event['content']['body']) - - if event['content']['body'][0] == "!": - # Could trigger a single-word command. - pieces = event['content']['body'].split(' ') - cmd = pieces[0][1:] - - command = self._word_commands.get(room_id, {}).get(cmd) - if command: - event_data = { - 'command': command[CONF_NAME], - 'sender': event['sender'], - 'room': room_id, - 'args': pieces[1:] - } - self.hass.bus.fire(EVENT_MATRIX_COMMAND, event_data) - - # After single-word commands, check all regex commands in the room - for command in self._expression_commands.get(room_id, []): - match = command[CONF_EXPRESSION].match(event['content']['body']) - if not match: - continue - event_data = { - 'command': command[CONF_NAME], - 'sender': event['sender'], - 'room': room_id, - 'args': match.groupdict() - } - self.hass.bus.fire(EVENT_MATRIX_COMMAND, event_data) - - def _join_or_get_room(self, room_id_or_alias): - """Join a room or get it, if we are already in the room. - - We can't just always call join_room(), since that seems to crash - the client if we're already in the room. - """ - rooms = self._client.get_rooms() - if room_id_or_alias in rooms: - _LOGGER.debug("Already in room %s", room_id_or_alias) - return rooms[room_id_or_alias] - - for room in rooms.values(): - if room.room_id not in self._aliases_fetched_for: - room.update_aliases() - self._aliases_fetched_for.add(room.room_id) - - if room_id_or_alias in room.aliases: - _LOGGER.debug("Already in room %s (known as %s)", - room.room_id, room_id_or_alias) - return room - - room = self._client.join_room(room_id_or_alias) - _LOGGER.info("Joined room %s (known as %s)", room.room_id, - room_id_or_alias) - return room - - def _join_rooms(self): - """Join the rooms that we listen for commands in.""" - from matrix_client.client import MatrixRequestError - - for room_id in self._listening_rooms: - try: - room = self._join_or_get_room(room_id) - room.add_listener(partial(self._handle_room_message, room_id), - "m.room.message") - - except MatrixRequestError as ex: - _LOGGER.error("Could not join room %s: %s", room_id, ex) - - def _get_auth_tokens(self): - """ - Read sorted authentication tokens from disk. - - Returns the auth_tokens dictionary. - """ - try: - auth_tokens = load_json(self._session_filepath) - - return auth_tokens - except HomeAssistantError as ex: - _LOGGER.warning( - "Loading authentication tokens from file '%s' failed: %s", - self._session_filepath, str(ex)) - return {} - - def _store_auth_token(self, token): - """Store authentication token to session and persistent storage.""" - self._auth_tokens[self._mx_id] = token - - save_json(self._session_filepath, self._auth_tokens) - - def _login(self): - """Login to the matrix homeserver and return the client instance.""" - from matrix_client.client import MatrixRequestError - - # Attempt to generate a valid client using either of the two possible - # login methods: - client = None - - # If we have an authentication token - if self._mx_id in self._auth_tokens: - try: - client = self._login_by_token() - _LOGGER.debug("Logged in using stored token.") - - except MatrixRequestError as ex: - _LOGGER.warning( - "Login by token failed, falling back to password. " - "login_by_token raised: (%d) %s", - ex.code, ex.content) - - # If we still don't have a client try password. - if not client: - try: - client = self._login_by_password() - _LOGGER.debug("Logged in using password.") - - except MatrixRequestError as ex: - _LOGGER.error( - "Login failed, both token and username/password invalid " - "login_by_password raised: (%d) %s", - ex.code, ex.content) - - # re-raise the error so _setup can catch it. - raise - - return client - - def _login_by_token(self): - """Login using authentication token and return the client.""" - from matrix_client.client import MatrixClient - - return MatrixClient( - base_url=self._homeserver, - token=self._auth_tokens[self._mx_id], - user_id=self._mx_id, - valid_cert_check=self._verify_tls) - - def _login_by_password(self): - """Login using password authentication and return the client.""" - from matrix_client.client import MatrixClient - - _client = MatrixClient( - base_url=self._homeserver, - valid_cert_check=self._verify_tls) - - _client.login_with_password(self._mx_id, self._password) - - self._store_auth_token(_client.token) - - return _client - - def _send_message(self, message, target_rooms): - """Send the message to the matrix server.""" - from matrix_client.client import MatrixRequestError - - for target_room in target_rooms: - try: - room = self._join_or_get_room(target_room) - _LOGGER.debug(room.send_text(message)) - except MatrixRequestError as ex: - _LOGGER.error( - "Unable to deliver message to room '%s': (%d): %s", - target_room, ex.code, ex.content) - - def handle_send_message(self, service): - """Handle the send_message service.""" - self._send_message(service.data[ATTR_MESSAGE], - service.data[ATTR_TARGET]) diff --git a/homeassistant/components/matrix/__init__.py b/homeassistant/components/matrix/__init__.py new file mode 100644 index 0000000000000..4b3c1bf4d7696 --- /dev/null +++ b/homeassistant/components/matrix/__init__.py @@ -0,0 +1,335 @@ +"""The matrix bot component.""" +import logging +import os +from functools import partial + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.notify import (ATTR_TARGET, ATTR_MESSAGE) +from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, + CONF_VERIFY_SSL, CONF_NAME, + EVENT_HOMEASSISTANT_STOP, + EVENT_HOMEASSISTANT_START) +from homeassistant.util.json import load_json, save_json +from homeassistant.exceptions import HomeAssistantError + +REQUIREMENTS = ['matrix-client==0.2.0'] + +_LOGGER = logging.getLogger(__name__) + +SESSION_FILE = '.matrix.conf' + +CONF_HOMESERVER = 'homeserver' +CONF_ROOMS = 'rooms' +CONF_COMMANDS = 'commands' +CONF_WORD = 'word' +CONF_EXPRESSION = 'expression' + +EVENT_MATRIX_COMMAND = 'matrix_command' + +DOMAIN = 'matrix' + +COMMAND_SCHEMA = vol.All( + # Basic Schema + vol.Schema({ + 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]), + }), + # Make sure it's either a word or an expression command + cv.has_at_least_one_key(CONF_WORD, CONF_EXPRESSION) +) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOMESERVER): cv.url, + 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_COMMANDS, default=[]): [COMMAND_SCHEMA] + }) +}, extra=vol.ALLOW_EXTRA) + +SERVICE_SEND_MESSAGE = 'send_message' + +SERVICE_SCHEMA_SEND_MESSAGE = vol.Schema({ + vol.Required(ATTR_MESSAGE): cv.string, + vol.Required(ATTR_TARGET): vol.All(cv.ensure_list, [cv.string]), +}) + + +def setup(hass, config): + """Set up the Matrix bot component.""" + from matrix_client.client import MatrixRequestError + + config = config[DOMAIN] + + 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], + config[CONF_COMMANDS]) + hass.data[DOMAIN] = bot + except MatrixRequestError as exception: + _LOGGER.error("Matrix failed to log in: %s", str(exception)) + return False + + hass.services.register( + DOMAIN, SERVICE_SEND_MESSAGE, bot.handle_send_message, + schema=SERVICE_SCHEMA_SEND_MESSAGE) + + return True + + +class MatrixBot: + """The Matrix Bot.""" + + def __init__(self, hass, config_file, homeserver, verify_ssl, + username, password, listening_rooms, commands): + """Set up the client.""" + self.hass = hass + + self._session_filepath = config_file + self._auth_tokens = self._get_auth_tokens() + + self._homeserver = homeserver + self._verify_tls = verify_ssl + self._mx_id = username + self._password = password + + self._listening_rooms = listening_rooms + + # We have to fetch the aliases for every room to make sure we don't + # join it twice by accident. However, fetching aliases is costly, + # so we only do it once per room. + self._aliases_fetched_for = set() + + # word commands are stored dict-of-dict: First dict indexes by room ID + # / alias, second dict indexes by the word + self._word_commands = {} + + # regular expression commands are stored as a list of commands per + # room, i.e., a dict-of-list + self._expression_commands = {} + + for command in commands: + if not command.get(CONF_ROOMS): + command[CONF_ROOMS] = listening_rooms + + if command.get(CONF_WORD): + for room_id in command[CONF_ROOMS]: + if room_id not in self._word_commands: + self._word_commands[room_id] = {} + self._word_commands[room_id][command[CONF_WORD]] = command + else: + for room_id in command[CONF_ROOMS]: + if room_id not in self._expression_commands: + self._expression_commands[room_id] = [] + self._expression_commands[room_id].append(command) + + # Log in. This raises a MatrixRequestError if login is unsuccessful + self._client = self._login() + + def handle_matrix_exception(exception): + """Handle exceptions raised inside the Matrix SDK.""" + _LOGGER.error("Matrix exception:\n %s", str(exception)) + + self._client.start_listener_thread( + exception_handler=handle_matrix_exception) + + def stop_client(_): + """Run once when Home Assistant stops.""" + self._client.stop_listener_thread() + + self.hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_client) + + # Joining rooms potentially does a lot of I/O, so we defer it + def handle_startup(_): + """Run once when Home Assistant finished startup.""" + self._join_rooms() + + self.hass.bus.listen_once(EVENT_HOMEASSISTANT_START, handle_startup) + + def _handle_room_message(self, room_id, room, event): + """Handle a message sent to a room.""" + if event['content']['msgtype'] != 'm.text': + return + + if event['sender'] == self._mx_id: + return + + _LOGGER.debug("Handling message: %s", event['content']['body']) + + if event['content']['body'][0] == "!": + # Could trigger a single-word command. + pieces = event['content']['body'].split(' ') + cmd = pieces[0][1:] + + command = self._word_commands.get(room_id, {}).get(cmd) + if command: + event_data = { + 'command': command[CONF_NAME], + 'sender': event['sender'], + 'room': room_id, + 'args': pieces[1:] + } + self.hass.bus.fire(EVENT_MATRIX_COMMAND, event_data) + + # After single-word commands, check all regex commands in the room + for command in self._expression_commands.get(room_id, []): + match = command[CONF_EXPRESSION].match(event['content']['body']) + if not match: + continue + event_data = { + 'command': command[CONF_NAME], + 'sender': event['sender'], + 'room': room_id, + 'args': match.groupdict() + } + self.hass.bus.fire(EVENT_MATRIX_COMMAND, event_data) + + def _join_or_get_room(self, room_id_or_alias): + """Join a room or get it, if we are already in the room. + + We can't just always call join_room(), since that seems to crash + the client if we're already in the room. + """ + rooms = self._client.get_rooms() + if room_id_or_alias in rooms: + _LOGGER.debug("Already in room %s", room_id_or_alias) + return rooms[room_id_or_alias] + + for room in rooms.values(): + if room.room_id not in self._aliases_fetched_for: + room.update_aliases() + self._aliases_fetched_for.add(room.room_id) + + if room_id_or_alias in room.aliases: + _LOGGER.debug("Already in room %s (known as %s)", + room.room_id, room_id_or_alias) + return room + + room = self._client.join_room(room_id_or_alias) + _LOGGER.info("Joined room %s (known as %s)", room.room_id, + room_id_or_alias) + return room + + def _join_rooms(self): + """Join the rooms that we listen for commands in.""" + from matrix_client.client import MatrixRequestError + + for room_id in self._listening_rooms: + try: + room = self._join_or_get_room(room_id) + room.add_listener(partial(self._handle_room_message, room_id), + "m.room.message") + + except MatrixRequestError as ex: + _LOGGER.error("Could not join room %s: %s", room_id, ex) + + def _get_auth_tokens(self): + """ + Read sorted authentication tokens from disk. + + Returns the auth_tokens dictionary. + """ + try: + auth_tokens = load_json(self._session_filepath) + + return auth_tokens + except HomeAssistantError as ex: + _LOGGER.warning( + "Loading authentication tokens from file '%s' failed: %s", + self._session_filepath, str(ex)) + return {} + + def _store_auth_token(self, token): + """Store authentication token to session and persistent storage.""" + self._auth_tokens[self._mx_id] = token + + save_json(self._session_filepath, self._auth_tokens) + + def _login(self): + """Login to the matrix homeserver and return the client instance.""" + from matrix_client.client import MatrixRequestError + + # Attempt to generate a valid client using either of the two possible + # login methods: + client = None + + # If we have an authentication token + if self._mx_id in self._auth_tokens: + try: + client = self._login_by_token() + _LOGGER.debug("Logged in using stored token.") + + except MatrixRequestError as ex: + _LOGGER.warning( + "Login by token failed, falling back to password. " + "login_by_token raised: (%d) %s", + ex.code, ex.content) + + # If we still don't have a client try password. + if not client: + try: + client = self._login_by_password() + _LOGGER.debug("Logged in using password.") + + except MatrixRequestError as ex: + _LOGGER.error( + "Login failed, both token and username/password invalid " + "login_by_password raised: (%d) %s", + ex.code, ex.content) + + # re-raise the error so _setup can catch it. + raise + + return client + + def _login_by_token(self): + """Login using authentication token and return the client.""" + from matrix_client.client import MatrixClient + + return MatrixClient( + base_url=self._homeserver, + token=self._auth_tokens[self._mx_id], + user_id=self._mx_id, + valid_cert_check=self._verify_tls) + + def _login_by_password(self): + """Login using password authentication and return the client.""" + from matrix_client.client import MatrixClient + + _client = MatrixClient( + base_url=self._homeserver, + valid_cert_check=self._verify_tls) + + _client.login_with_password(self._mx_id, self._password) + + self._store_auth_token(_client.token) + + return _client + + def _send_message(self, message, target_rooms): + """Send the message to the matrix server.""" + from matrix_client.client import MatrixRequestError + + for target_room in target_rooms: + try: + room = self._join_or_get_room(target_room) + _LOGGER.debug(room.send_text(message)) + except MatrixRequestError as ex: + _LOGGER.error( + "Unable to deliver message to room '%s': (%d): %s", + target_room, ex.code, ex.content) + + def handle_send_message(self, service): + """Handle the send_message service.""" + self._send_message(service.data[ATTR_MESSAGE], + service.data[ATTR_TARGET]) diff --git a/homeassistant/components/matrix/notify.py b/homeassistant/components/matrix/notify.py new file mode 100644 index 0000000000000..f1f53268c2ba8 --- /dev/null +++ b/homeassistant/components/matrix/notify.py @@ -0,0 +1,45 @@ +"""Support for Matrix notifications.""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.notify import (ATTR_TARGET, PLATFORM_SCHEMA, + BaseNotificationService, + ATTR_MESSAGE) + +_LOGGER = logging.getLogger(__name__) + +CONF_DEFAULT_ROOM = 'default_room' + +DOMAIN = 'matrix' +DEPENDENCIES = [DOMAIN] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_DEFAULT_ROOM): cv.string, +}) + + +def get_service(hass, config, discovery_info=None): + """Get the Matrix notification service.""" + return MatrixNotificationService(config.get(CONF_DEFAULT_ROOM)) + + +class MatrixNotificationService(BaseNotificationService): + """Send Notifications to a Matrix Room.""" + + def __init__(self, default_room): + """Set up the notification service.""" + self._default_room = default_room + + def send_message(self, message="", **kwargs): + """Send the message to the matrix server.""" + target_rooms = kwargs.get(ATTR_TARGET) or [self._default_room] + + service_data = { + ATTR_TARGET: target_rooms, + ATTR_MESSAGE: message + } + + return self.hass.services.call( + DOMAIN, 'send_message', service_data=service_data) diff --git a/homeassistant/components/maxcube.py b/homeassistant/components/maxcube.py deleted file mode 100644 index 9980d554232cb..0000000000000 --- a/homeassistant/components/maxcube.py +++ /dev/null @@ -1,108 +0,0 @@ -""" -Platform for the MAX! Cube LAN Gateway. - -For more details about this component, please refer to the documentation -https://home-assistant.io/components/maxcube/ -""" -import logging -import time -from socket import timeout -from threading import Lock - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import load_platform -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SCAN_INTERVAL - -REQUIREMENTS = ['maxcube-api==0.1.0'] - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_PORT = 62910 -DOMAIN = 'maxcube' - -DATA_KEY = 'maxcube' - -NOTIFICATION_ID = 'maxcube_notification' -NOTIFICATION_TITLE = 'Max!Cube gateway setup' - -CONF_GATEWAYS = 'gateways' - -CONFIG_GATEWAY = vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_SCAN_INTERVAL, default=300): cv.time_period, -}) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_GATEWAYS, default={}): - vol.All(cv.ensure_list, [CONFIG_GATEWAY]) - }), -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Establish connection to MAX! Cube.""" - from maxcube.connection import MaxCubeConnection - from maxcube.cube import MaxCube - if DATA_KEY not in hass.data: - hass.data[DATA_KEY] = {} - - connection_failed = 0 - gateways = config[DOMAIN][CONF_GATEWAYS] - for gateway in gateways: - host = gateway[CONF_HOST] - port = gateway[CONF_PORT] - scan_interval = gateway[CONF_SCAN_INTERVAL].total_seconds() - - try: - cube = MaxCube(MaxCubeConnection(host, port)) - hass.data[DATA_KEY][host] = MaxCubeHandle(cube, scan_interval) - except timeout as ex: - _LOGGER.error("Unable to connect to Max!Cube gateway: %s", str(ex)) - hass.components.persistent_notification.create( - 'Error: {}
' - 'You will need to restart Home Assistant after fixing.' - ''.format(ex), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) - connection_failed += 1 - - if connection_failed >= len(gateways): - return False - - load_platform(hass, 'climate', DOMAIN, {}, config) - load_platform(hass, 'binary_sensor', DOMAIN, {}, config) - - return True - - -class MaxCubeHandle: - """Keep the cube instance in one place and centralize the update.""" - - def __init__(self, cube, scan_interval): - """Initialize the Cube Handle.""" - self.cube = cube - self.scan_interval = scan_interval - self.mutex = Lock() - self._updatets = time.time() - - def update(self): - """Pull the latest data from the MAX! Cube.""" - # Acquire mutex to prevent simultaneous update from multiple threads - with self.mutex: - # Only update every update_interval - if (time.time() - self._updatets) >= self.scan_interval: - _LOGGER.debug("Updating") - - try: - self.cube.update() - except timeout: - _LOGGER.error("Max!Cube connection failed") - return False - - self._updatets = time.time() - else: - _LOGGER.debug("Skipping update") diff --git a/homeassistant/components/maxcube/__init__.py b/homeassistant/components/maxcube/__init__.py new file mode 100644 index 0000000000000..c398ccbde4f41 --- /dev/null +++ b/homeassistant/components/maxcube/__init__.py @@ -0,0 +1,103 @@ +"""Support for the MAX! Cube LAN Gateway.""" +import logging +import time +from socket import timeout +from threading import Lock + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SCAN_INTERVAL + +REQUIREMENTS = ['maxcube-api==0.1.0'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_PORT = 62910 +DOMAIN = 'maxcube' + +DATA_KEY = 'maxcube' + +NOTIFICATION_ID = 'maxcube_notification' +NOTIFICATION_TITLE = 'Max!Cube gateway setup' + +CONF_GATEWAYS = 'gateways' + +CONFIG_GATEWAY = vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_SCAN_INTERVAL, default=300): cv.time_period, +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_GATEWAYS, default={}): + vol.All(cv.ensure_list, [CONFIG_GATEWAY]), + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Establish connection to MAX! Cube.""" + from maxcube.connection import MaxCubeConnection + from maxcube.cube import MaxCube + if DATA_KEY not in hass.data: + hass.data[DATA_KEY] = {} + + connection_failed = 0 + gateways = config[DOMAIN][CONF_GATEWAYS] + for gateway in gateways: + host = gateway[CONF_HOST] + port = gateway[CONF_PORT] + scan_interval = gateway[CONF_SCAN_INTERVAL].total_seconds() + + try: + cube = MaxCube(MaxCubeConnection(host, port)) + hass.data[DATA_KEY][host] = MaxCubeHandle(cube, scan_interval) + except timeout as ex: + _LOGGER.error("Unable to connect to Max!Cube gateway: %s", str(ex)) + hass.components.persistent_notification.create( + 'Error: {}
' + 'You will need to restart Home Assistant after fixing.' + ''.format(ex), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + connection_failed += 1 + + if connection_failed >= len(gateways): + return False + + load_platform(hass, 'climate', DOMAIN, {}, config) + load_platform(hass, 'binary_sensor', DOMAIN, {}, config) + + return True + + +class MaxCubeHandle: + """Keep the cube instance in one place and centralize the update.""" + + def __init__(self, cube, scan_interval): + """Initialize the Cube Handle.""" + self.cube = cube + self.scan_interval = scan_interval + self.mutex = Lock() + self._updatets = time.time() + + def update(self): + """Pull the latest data from the MAX! Cube.""" + # Acquire mutex to prevent simultaneous update from multiple threads + with self.mutex: + # Only update every update_interval + if (time.time() - self._updatets) >= self.scan_interval: + _LOGGER.debug("Updating") + + try: + self.cube.update() + except timeout: + _LOGGER.error("Max!Cube connection failed") + return False + + self._updatets = time.time() + else: + _LOGGER.debug("Skipping update") diff --git a/homeassistant/components/maxcube/binary_sensor.py b/homeassistant/components/maxcube/binary_sensor.py new file mode 100644 index 0000000000000..8d5ab84f6d375 --- /dev/null +++ b/homeassistant/components/maxcube/binary_sensor.py @@ -0,0 +1,63 @@ +"""Support for MAX! binary sensors via MAX! Cube.""" +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.maxcube import DATA_KEY + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Iterate through all MAX! Devices and add window shutters.""" + devices = [] + for handler in hass.data[DATA_KEY].values(): + cube = handler.cube + for device in cube.devices: + name = "{} {}".format( + cube.room_by_id(device.room_id).name, device.name) + + # Only add Window Shutters + if cube.is_windowshutter(device): + devices.append( + MaxCubeShutter(handler, name, device.rf_address)) + + if devices: + add_entities(devices) + + +class MaxCubeShutter(BinarySensorDevice): + """Representation of a MAX! Cube Binary Sensor device.""" + + def __init__(self, handler, name, rf_address): + """Initialize MAX! Cube BinarySensorDevice.""" + self._name = name + self._sensor_type = 'window' + self._rf_address = rf_address + self._cubehandle = handler + self._state = None + + @property + def should_poll(self): + """Return the polling state.""" + return True + + @property + def name(self): + """Return the name of the BinarySensorDevice.""" + return self._name + + @property + def device_class(self): + """Return the class of this sensor.""" + return self._sensor_type + + @property + def is_on(self): + """Return true if the binary sensor is on/open.""" + return self._state + + def update(self): + """Get latest data from MAX! Cube.""" + self._cubehandle.update() + device = self._cubehandle.cube.device_by_rf(self._rf_address) + self._state = device.is_open diff --git a/homeassistant/components/maxcube/climate.py b/homeassistant/components/maxcube/climate.py new file mode 100644 index 0000000000000..f5c4533123f17 --- /dev/null +++ b/homeassistant/components/maxcube/climate.py @@ -0,0 +1,191 @@ +"""Support for MAX! Thermostats via MAX! Cube.""" +import socket +import logging + +from homeassistant.components.climate import ( + ClimateDevice, STATE_AUTO, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_OPERATION_MODE) +from homeassistant.components.maxcube import DATA_KEY +from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE + +_LOGGER = logging.getLogger(__name__) + +STATE_MANUAL = 'manual' +STATE_BOOST = 'boost' +STATE_VACATION = 'vacation' + +SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Iterate through all MAX! Devices and add thermostats.""" + devices = [] + for handler in hass.data[DATA_KEY].values(): + cube = handler.cube + for device in cube.devices: + name = '{} {}'.format( + cube.room_by_id(device.room_id).name, device.name) + + if cube.is_thermostat(device) or cube.is_wallthermostat(device): + devices.append( + MaxCubeClimate(handler, name, device.rf_address)) + + if devices: + add_entities(devices) + + +class MaxCubeClimate(ClimateDevice): + """MAX! Cube ClimateDevice.""" + + def __init__(self, handler, name, rf_address): + """Initialize MAX! Cube ClimateDevice.""" + self._name = name + self._operation_list = [STATE_AUTO, STATE_MANUAL, STATE_BOOST, + STATE_VACATION] + self._rf_address = rf_address + self._cubehandle = handler + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS + + @property + def should_poll(self): + """Return the polling state.""" + return True + + @property + def name(self): + """Return the name of the climate device.""" + return self._name + + @property + def min_temp(self): + """Return the minimum temperature.""" + device = self._cubehandle.cube.device_by_rf(self._rf_address) + return self.map_temperature_max_hass(device.min_temperature) + + @property + def max_temp(self): + """Return the maximum temperature.""" + device = self._cubehandle.cube.device_by_rf(self._rf_address) + return self.map_temperature_max_hass(device.max_temperature) + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def current_temperature(self): + """Return the current temperature.""" + device = self._cubehandle.cube.device_by_rf(self._rf_address) + + # Map and return current temperature + return self.map_temperature_max_hass(device.actual_temperature) + + @property + def current_operation(self): + """Return current operation (auto, manual, boost, vacation).""" + device = self._cubehandle.cube.device_by_rf(self._rf_address) + return self.map_mode_max_hass(device.mode) + + @property + def operation_list(self): + """Return the list of available operation modes.""" + return self._operation_list + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + device = self._cubehandle.cube.device_by_rf(self._rf_address) + return self.map_temperature_max_hass(device.target_temperature) + + def set_temperature(self, **kwargs): + """Set new target temperatures.""" + if kwargs.get(ATTR_TEMPERATURE) is None: + return False + + target_temperature = kwargs.get(ATTR_TEMPERATURE) + device = self._cubehandle.cube.device_by_rf(self._rf_address) + + cube = self._cubehandle.cube + + with self._cubehandle.mutex: + try: + cube.set_target_temperature(device, target_temperature) + except (socket.timeout, socket.error): + _LOGGER.error("Setting target temperature failed") + return False + + def set_operation_mode(self, operation_mode): + """Set new operation mode.""" + device = self._cubehandle.cube.device_by_rf(self._rf_address) + mode = self.map_mode_hass_max(operation_mode) + + if mode is None: + return False + + with self._cubehandle.mutex: + try: + self._cubehandle.cube.set_mode(device, mode) + except (socket.timeout, socket.error): + _LOGGER.error("Setting operation mode failed") + return False + + def update(self): + """Get latest data from MAX! Cube.""" + self._cubehandle.update() + + @staticmethod + def map_temperature_max_hass(temperature): + """Map Temperature from MAX! to HASS.""" + if temperature is None: + return 0.0 + + return temperature + + @staticmethod + def map_mode_hass_max(operation_mode): + """Map Home Assistant Operation Modes to MAX! Operation Modes.""" + from maxcube.device import \ + MAX_DEVICE_MODE_AUTOMATIC, \ + MAX_DEVICE_MODE_MANUAL, \ + MAX_DEVICE_MODE_VACATION, \ + MAX_DEVICE_MODE_BOOST + + if operation_mode == STATE_AUTO: + mode = MAX_DEVICE_MODE_AUTOMATIC + elif operation_mode == STATE_MANUAL: + mode = MAX_DEVICE_MODE_MANUAL + elif operation_mode == STATE_VACATION: + mode = MAX_DEVICE_MODE_VACATION + elif operation_mode == STATE_BOOST: + mode = MAX_DEVICE_MODE_BOOST + else: + mode = None + + return mode + + @staticmethod + def map_mode_max_hass(mode): + """Map MAX! Operation Modes to Home Assistant Operation Modes.""" + from maxcube.device import \ + MAX_DEVICE_MODE_AUTOMATIC, \ + MAX_DEVICE_MODE_MANUAL, \ + MAX_DEVICE_MODE_VACATION, \ + MAX_DEVICE_MODE_BOOST + + if mode == MAX_DEVICE_MODE_AUTOMATIC: + operation_mode = STATE_AUTO + elif mode == MAX_DEVICE_MODE_MANUAL: + operation_mode = STATE_MANUAL + elif mode == MAX_DEVICE_MODE_VACATION: + operation_mode = STATE_VACATION + elif mode == MAX_DEVICE_MODE_BOOST: + operation_mode = STATE_BOOST + else: + operation_mode = None + + return operation_mode diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py deleted file mode 100644 index 333f62a9aa7d7..0000000000000 --- a/homeassistant/components/media_extractor.py +++ /dev/null @@ -1,169 +0,0 @@ -""" -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/ -""" -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) -from homeassistant.helpers import config_validation as cv - -REQUIREMENTS = ['youtube_dl==2019.01.24'] - -_LOGGER = logging.getLogger(__name__) - -CONF_CUSTOMIZE_ENTITIES = 'customize' -CONF_DEFAULT_STREAM_QUERY = 'default_query' - -DEFAULT_STREAM_QUERY = 'best' -DEPENDENCIES = ['media_player'] -DOMAIN = 'media_extractor' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(CONF_DEFAULT_STREAM_QUERY): cv.string, - vol.Optional(CONF_CUSTOMIZE_ENTITIES): - vol.Schema({cv.entity_id: vol.Schema({cv.string: cv.string})}), - }), -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the media extractor service.""" - def play_media(call): - """Get stream URL and send it to the play_media service.""" - MediaExtractor(hass, config[DOMAIN], call.data).extract_and_send() - - hass.services.register(DOMAIN, SERVICE_PLAY_MEDIA, play_media, - schema=MEDIA_PLAYER_PLAY_MEDIA_SCHEMA) - - return True - - -class MEDownloadException(Exception): - """Media extractor download exception.""" - - pass - - -class MEQueryException(Exception): - """Media extractor query exception.""" - - pass - - -class MediaExtractor: - """Class which encapsulates all extraction logic.""" - - def __init__(self, hass, component_config, call_data): - """Initialize media extractor.""" - self.hass = hass - self.config = component_config - self.call_data = call_data - - def get_media_url(self): - """Return media content url.""" - return self.call_data.get(ATTR_MEDIA_CONTENT_ID) - - def get_entities(self): - """Return list of entities.""" - return self.call_data.get(ATTR_ENTITY_ID, []) - - def extract_and_send(self): - """Extract exact stream format for each entity_id and play it.""" - try: - stream_selector = self.get_stream_selector() - except MEDownloadException: - _LOGGER.error("Could not retrieve data for the URL: %s", - self.get_media_url()) - else: - entities = self.get_entities() - - if not entities: - self.call_media_player_service(stream_selector, None) - - for entity_id in entities: - self.call_media_player_service(stream_selector, entity_id) - - def get_stream_selector(self): - """Return format selector for the media URL.""" - from youtube_dl import YoutubeDL - from youtube_dl.utils import DownloadError, ExtractorError - - ydl = YoutubeDL({'quiet': True, 'logger': _LOGGER}) - - try: - all_media = ydl.extract_info(self.get_media_url(), process=False) - except DownloadError: - # This exception will be logged by youtube-dl itself - raise MEDownloadException() - - if 'entries' in all_media: - _LOGGER.warning( - "Playlists are not supported, looking for the first video") - entries = list(all_media['entries']) - if entries: - selected_media = entries[0] - else: - _LOGGER.error("Playlist is empty") - raise MEDownloadException() - else: - selected_media = all_media - - def stream_selector(query): - """Find stream URL that matches query.""" - try: - ydl.params['format'] = query - requested_stream = ydl.process_ie_result( - selected_media, download=False) - except (ExtractorError, DownloadError): - _LOGGER.error( - "Could not extract stream for the query: %s", query) - raise MEQueryException() - - return requested_stream['url'] - - return stream_selector - - def call_media_player_service(self, stream_selector, entity_id): - """Call Media player play_media service.""" - stream_query = self.get_stream_query_for_entity(entity_id) - - try: - stream_url = stream_selector(stream_query) - except MEQueryException: - _LOGGER.error("Wrong query format: %s", stream_query) - return - else: - data = {k: v for k, v in self.call_data.items() - if k != ATTR_ENTITY_ID} - data[ATTR_MEDIA_CONTENT_ID] = stream_url - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - self.hass.async_create_task( - self.hass.services.async_call( - MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, data) - ) - - def get_stream_query_for_entity(self, entity_id): - """Get stream format query for entity.""" - default_stream_query = self.config.get( - CONF_DEFAULT_STREAM_QUERY, DEFAULT_STREAM_QUERY) - - if entity_id: - media_content_type = self.call_data.get(ATTR_MEDIA_CONTENT_TYPE) - - return self.config \ - .get(CONF_CUSTOMIZE_ENTITIES, {}) \ - .get(entity_id, {}) \ - .get(media_content_type, default_stream_query) - - return default_stream_query diff --git a/homeassistant/components/media_extractor/__init__.py b/homeassistant/components/media_extractor/__init__.py new file mode 100644 index 0000000000000..efc3e8bddc8f4 --- /dev/null +++ b/homeassistant/components/media_extractor/__init__.py @@ -0,0 +1,167 @@ +"""Decorator service for the media_player.play_media service.""" +import logging + +import voluptuous as vol + +from homeassistant.components.media_player import ( + 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.02.08'] + +_LOGGER = logging.getLogger(__name__) + +CONF_CUSTOMIZE_ENTITIES = 'customize' +CONF_DEFAULT_STREAM_QUERY = 'default_query' + +DEFAULT_STREAM_QUERY = 'best' +DEPENDENCIES = ['media_player'] +DOMAIN = 'media_extractor' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Optional(CONF_DEFAULT_STREAM_QUERY): cv.string, + vol.Optional(CONF_CUSTOMIZE_ENTITIES): + vol.Schema({cv.entity_id: vol.Schema({cv.string: cv.string})}), + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the media extractor service.""" + def play_media(call): + """Get stream URL and send it to the play_media service.""" + MediaExtractor(hass, config[DOMAIN], call.data).extract_and_send() + + hass.services.register(DOMAIN, SERVICE_PLAY_MEDIA, play_media, + schema=MEDIA_PLAYER_PLAY_MEDIA_SCHEMA) + + return True + + +class MEDownloadException(Exception): + """Media extractor download exception.""" + + pass + + +class MEQueryException(Exception): + """Media extractor query exception.""" + + pass + + +class MediaExtractor: + """Class which encapsulates all extraction logic.""" + + def __init__(self, hass, component_config, call_data): + """Initialize media extractor.""" + self.hass = hass + self.config = component_config + self.call_data = call_data + + def get_media_url(self): + """Return media content url.""" + return self.call_data.get(ATTR_MEDIA_CONTENT_ID) + + def get_entities(self): + """Return list of entities.""" + return self.call_data.get(ATTR_ENTITY_ID, []) + + def extract_and_send(self): + """Extract exact stream format for each entity_id and play it.""" + try: + stream_selector = self.get_stream_selector() + except MEDownloadException: + _LOGGER.error("Could not retrieve data for the URL: %s", + self.get_media_url()) + else: + entities = self.get_entities() + + if not entities: + self.call_media_player_service(stream_selector, None) + + for entity_id in entities: + self.call_media_player_service(stream_selector, entity_id) + + def get_stream_selector(self): + """Return format selector for the media URL.""" + from youtube_dl import YoutubeDL + from youtube_dl.utils import DownloadError, ExtractorError + + ydl = YoutubeDL({'quiet': True, 'logger': _LOGGER}) + + try: + all_media = ydl.extract_info(self.get_media_url(), process=False) + except DownloadError: + # This exception will be logged by youtube-dl itself + raise MEDownloadException() + + if 'entries' in all_media: + _LOGGER.warning( + "Playlists are not supported, looking for the first video") + entries = list(all_media['entries']) + if entries: + selected_media = entries[0] + else: + _LOGGER.error("Playlist is empty") + raise MEDownloadException() + else: + selected_media = all_media + + def stream_selector(query): + """Find stream URL that matches query.""" + try: + ydl.params['format'] = query + requested_stream = ydl.process_ie_result( + selected_media, download=False) + except (ExtractorError, DownloadError): + _LOGGER.error( + "Could not extract stream for the query: %s", query) + raise MEQueryException() + + return requested_stream['url'] + + return stream_selector + + def call_media_player_service(self, stream_selector, entity_id): + """Call Media player play_media service.""" + stream_query = self.get_stream_query_for_entity(entity_id) + + try: + stream_url = stream_selector(stream_query) + except MEQueryException: + _LOGGER.error("Wrong query format: %s", stream_query) + return + else: + data = {k: v for k, v in self.call_data.items() + if k != ATTR_ENTITY_ID} + data[ATTR_MEDIA_CONTENT_ID] = stream_url + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + self.hass.async_create_task( + self.hass.services.async_call( + MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, data) + ) + + def get_stream_query_for_entity(self, entity_id): + """Get stream format query for entity.""" + default_stream_query = self.config.get( + CONF_DEFAULT_STREAM_QUERY, DEFAULT_STREAM_QUERY) + + if entity_id: + media_content_type = self.call_data.get(ATTR_MEDIA_CONTENT_TYPE) + + return self.config \ + .get(CONF_CUSTOMIZE_ENTITIES, {}) \ + .get(entity_id, {}) \ + .get(media_content_type, default_stream_query) + + return default_stream_query diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index b526d1659ba9e..ad29a645765bb 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -30,15 +30,63 @@ 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 @@ 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 a0bc3d05dcb4c..d48f90d2bd7eb 100644 --- a/homeassistant/components/media_player/anthemav.py +++ b/homeassistant/components/media_player/anthemav.py @@ -9,8 +9,10 @@ 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/apple_tv.py b/homeassistant/components/media_player/apple_tv.py deleted file mode 100644 index bff8834639d57..0000000000000 --- a/homeassistant/components/media_player/apple_tv.py +++ /dev/null @@ -1,266 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.components.apple_tv import ( - ATTR_ATV, ATTR_POWER, DATA_APPLE_TV, DATA_ENTITIES) -from homeassistant.components.media_player 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) -from homeassistant.const import ( - CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_OFF, - STATE_PAUSED, STATE_PLAYING, STATE_STANDBY) -from homeassistant.core import callback -import homeassistant.util.dt as dt_util - -DEPENDENCIES = ['apple_tv'] - -_LOGGER = logging.getLogger(__name__) - -SUPPORT_APPLE_TV = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PLAY_MEDIA | \ - SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_SEEK | \ - SUPPORT_STOP | SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK - - -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): - """Set up the Apple TV platform.""" - if not discovery_info: - return - - # Manage entity cache for service handler - if DATA_ENTITIES not in hass.data: - hass.data[DATA_ENTITIES] = [] - - name = discovery_info[CONF_NAME] - host = discovery_info[CONF_HOST] - atv = hass.data[DATA_APPLE_TV][host][ATTR_ATV] - power = hass.data[DATA_APPLE_TV][host][ATTR_POWER] - entity = AppleTvDevice(atv, name, power) - - @callback - def on_hass_stop(event): - """Stop push updates when hass stops.""" - atv.push_updater.stop() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) - - if entity not in hass.data[DATA_ENTITIES]: - hass.data[DATA_ENTITIES].append(entity) - - async_add_entities([entity]) - - -class AppleTvDevice(MediaPlayerDevice): - """Representation of an Apple TV device.""" - - def __init__(self, atv, name, power): - """Initialize the Apple TV device.""" - self.atv = atv - self._name = name - self._playing = None - self._power = power - self._power.listeners.append(self) - self.atv.push_updater.listener = self - - async def async_added_to_hass(self): - """Handle when an entity is about to be added to Home Assistant.""" - self._power.init() - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def unique_id(self): - """Return a unique ID.""" - return self.atv.metadata.device_id - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def state(self): - """Return the state of the device.""" - if not self._power.turned_on: - return STATE_OFF - - if self._playing: - from pyatv import const - state = self._playing.play_state - if state in (const.PLAY_STATE_IDLE, const.PLAY_STATE_NO_MEDIA, - const.PLAY_STATE_LOADING): - return STATE_IDLE - if state == const.PLAY_STATE_PLAYING: - return STATE_PLAYING - if state in (const.PLAY_STATE_PAUSED, - const.PLAY_STATE_FAST_FORWARD, - const.PLAY_STATE_FAST_BACKWARD): - # Catch fast forward/backward here so "play" is default action - return STATE_PAUSED - return STATE_STANDBY # Bad or unknown state? - - @callback - def playstatus_update(self, updater, playing): - """Print what is currently playing when it changes.""" - self._playing = playing - self.async_schedule_update_ha_state() - - @callback - def playstatus_error(self, updater, exception): - """Inform about an error and restart push updates.""" - _LOGGER.warning('A %s error occurred: %s', - exception.__class__, exception) - - # This will wait 10 seconds before restarting push updates. If the - # connection continues to fail, it will flood the log (every 10 - # seconds) until it succeeds. A better approach should probably be - # implemented here later. - updater.start(initial_delay=10) - self._playing = None - self.async_schedule_update_ha_state() - - @property - def media_content_type(self): - """Content type of current playing media.""" - if self._playing: - from pyatv import const - media_type = self._playing.media_type - if media_type == const.MEDIA_TYPE_VIDEO: - return MEDIA_TYPE_VIDEO - if media_type == const.MEDIA_TYPE_MUSIC: - return MEDIA_TYPE_MUSIC - if media_type == const.MEDIA_TYPE_TV: - return MEDIA_TYPE_TVSHOW - - @property - def media_duration(self): - """Duration of current playing media in seconds.""" - if self._playing: - return self._playing.total_time - - @property - def media_position(self): - """Position of current playing media in seconds.""" - if self._playing: - return self._playing.position - - @property - def media_position_updated_at(self): - """Last valid time of media position.""" - state = self.state - if state in (STATE_PLAYING, STATE_PAUSED): - return dt_util.utcnow() - - async def async_play_media(self, media_type, media_id, **kwargs): - """Send the play_media command to the media player.""" - await self.atv.airplay.play_url(media_id) - - @property - def media_image_hash(self): - """Hash value for media image.""" - state = self.state - if self._playing and state not in [STATE_OFF, STATE_IDLE]: - return self._playing.hash - - async def async_get_media_image(self): - """Fetch media image of current playing image.""" - state = self.state - if self._playing and state not in [STATE_OFF, STATE_IDLE]: - return (await self.atv.metadata.artwork()), 'image/png' - - return None, None - - @property - def media_title(self): - """Title of current playing media.""" - if self._playing: - if self.state == STATE_IDLE: - return 'Nothing playing' - title = self._playing.title - return title if title else 'No title' - - return 'Establishing a connection to {0}...'.format(self._name) - - @property - def supported_features(self): - """Flag media player features that are supported.""" - return SUPPORT_APPLE_TV - - async def async_turn_on(self): - """Turn the media player on.""" - self._power.set_power_on(True) - - async def async_turn_off(self): - """Turn the media player off.""" - self._playing = None - self._power.set_power_on(False) - - def async_media_play_pause(self): - """Pause media on media player. - - This method must be run in the event loop and returns a coroutine. - """ - if self._playing: - state = self.state - if state == STATE_PAUSED: - return self.atv.remote_control.play() - if state == STATE_PLAYING: - return self.atv.remote_control.pause() - - def async_media_play(self): - """Play media. - - This method must be run in the event loop and returns a coroutine. - """ - if self._playing: - return self.atv.remote_control.play() - - def async_media_stop(self): - """Stop the media player. - - This method must be run in the event loop and returns a coroutine. - """ - if self._playing: - return self.atv.remote_control.stop() - - def async_media_pause(self): - """Pause the media player. - - This method must be run in the event loop and returns a coroutine. - """ - if self._playing: - return self.atv.remote_control.pause() - - def async_media_next_track(self): - """Send next track command. - - This method must be run in the event loop and returns a coroutine. - """ - if self._playing: - return self.atv.remote_control.next() - - def async_media_previous_track(self): - """Send previous track command. - - This method must be run in the event loop and returns a coroutine. - """ - if self._playing: - return self.atv.remote_control.previous() - - def async_media_seek(self, position): - """Send seek command. - - This method must be run in the event loop and returns a coroutine. - """ - if self._playing: - return self.atv.remote_control.set_position(position) diff --git a/homeassistant/components/media_player/aquostv.py b/homeassistant/components/media_player/aquostv.py index 5c1994e65fc8b..59723b4752260 100644 --- a/homeassistant/components/media_player/aquostv.py +++ b/homeassistant/components/media_player/aquostv.py @@ -9,10 +9,12 @@ 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 2c78bb24bbdc3..2daa2656e8306 100644 --- a/homeassistant/components/media_player/blackbird.py +++ b/homeassistant/components/media_player/blackbird.py @@ -10,8 +10,10 @@ 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 998f559bc8aa5..c6a8c51ca58bf 100644 --- a/homeassistant/components/media_player/bluesound.py +++ b/homeassistant/components/media_player/bluesound.py @@ -16,12 +16,13 @@ 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 04dc013108fc6..7efb7abd569d7 100644 --- a/homeassistant/components/media_player/braviatv.py +++ b/homeassistant/components/media_player/braviatv.py @@ -10,10 +10,12 @@ 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/cast.py b/homeassistant/components/media_player/cast.py deleted file mode 100644 index 20a44c0e910e9..0000000000000 --- a/homeassistant/components/media_player/cast.py +++ /dev/null @@ -1,681 +0,0 @@ -""" -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/ -""" -import asyncio -import logging -import threading -from typing import Optional, Tuple - -import attr -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, - 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) -from homeassistant.const import ( - CONF_HOST, EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_OFF, STATE_PAUSED, - STATE_PLAYING) -from homeassistant.core import callback -from homeassistant.exceptions import PlatformNotReady -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, dispatcher_send) -from homeassistant.helpers.typing import ConfigType, HomeAssistantType -import homeassistant.util.dt as dt_util - -DEPENDENCIES = ('cast',) - -_LOGGER = logging.getLogger(__name__) - -CONF_IGNORE_CEC = 'ignore_cec' -CAST_SPLASH = 'https://home-assistant.io/images/cast/splash.png' - -DEFAULT_PORT = 8009 - -SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ - SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \ - SUPPORT_NEXT_TRACK | SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_PLAY - -# Stores a threading.Lock that is held by the internal pychromecast discovery. -INTERNAL_DISCOVERY_RUNNING_KEY = 'cast_discovery_running' -# Stores all ChromecastInfo we encountered through discovery or config as a set -# If we find a chromecast with a new host, the old one will be removed again. -KNOWN_CHROMECAST_INFO_KEY = 'cast_known_chromecasts' -# Stores UUIDs of cast devices that were added as entities. Doesn't store -# None UUIDs. -ADDED_CAST_DEVICES_KEY = 'cast_added_cast_devices' - -# Dispatcher signal fired with a ChromecastInfo every time we discover a new -# Chromecast or receive it through configuration -SIGNAL_CAST_DISCOVERED = 'cast_discovered' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_IGNORE_CEC, default=[]): - vol.All(cv.ensure_list, [cv.string]), -}) - - -@attr.s(slots=True, frozen=True) -class ChromecastInfo: - """Class to hold all data about a chromecast for creating connections. - - This also has the same attributes as the mDNS fields by zeroconf. - """ - - host = attr.ib(type=str) - port = attr.ib(type=int) - 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='') - model_name = attr.ib(type=str, default='') - friendly_name = attr.ib(type=Optional[str], default=None) - - @property - def is_audio_group(self) -> bool: - """Return if this is an audio group.""" - return self.port != DEFAULT_PORT - - @property - def is_information_complete(self) -> bool: - """Return if all information is filled out.""" - return all(attr.astuple(self)) - - @property - def host_port(self) -> Tuple[str, int]: - """Return the host+port tuple.""" - return self.host, self.port - - -def _fill_out_missing_chromecast_info(info: ChromecastInfo) -> ChromecastInfo: - """Fill out missing attributes of ChromecastInfo using blocking HTTP.""" - if info.is_information_complete or info.is_audio_group: - # We have all information, no need to check HTTP API. Or this is an - # audio group, so checking via HTTP won't give us any new information. - return info - - # Fill out missing information via HTTP dial. - from pychromecast import dial - - http_device_status = dial.get_device_status(info.host) - 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, - 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), - model_name=(info.model_name or http_device_status.model_name) - ) - - -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) - _LOGGER.debug("Discovered chromecast %s", info) - - if info.uuid is not None: - # Remove previous cast infos with same uuid from known chromecasts. - same_uuid = set(x for x in hass.data[KNOWN_CHROMECAST_INFO_KEY] - if info.uuid == x.uuid) - hass.data[KNOWN_CHROMECAST_INFO_KEY] -= same_uuid - - hass.data[KNOWN_CHROMECAST_INFO_KEY].add(info) - dispatcher_send(hass, SIGNAL_CAST_DISCOVERED, info) - - -def _setup_internal_discovery(hass: HomeAssistantType) -> None: - """Set up the pychromecast internal discovery.""" - if INTERNAL_DISCOVERY_RUNNING_KEY not in hass.data: - hass.data[INTERNAL_DISCOVERY_RUNNING_KEY] = threading.Lock() - - if not hass.data[INTERNAL_DISCOVERY_RUNNING_KEY].acquire(blocking=False): - # Internal discovery is already running - return - - import pychromecast - - def internal_callback(name): - """Handle zeroconf discovery of a new chromecast.""" - mdns = listener.services[name] - _discover_chromecast(hass, ChromecastInfo( - host=mdns[0], - port=mdns[1], - uuid=mdns[2], - model_name=mdns[3], - friendly_name=mdns[4], - )) - - _LOGGER.debug("Starting internal pychromecast discovery.") - listener, browser = pychromecast.start_discovery(internal_callback) - - def stop_discovery(event): - """Stop discovery of new chromecasts.""" - _LOGGER.debug("Stopping internal pychromecast discovery.") - pychromecast.stop_discovery(browser) - hass.data[INTERNAL_DISCOVERY_RUNNING_KEY].release() - - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_discovery) - - -@callback -def _async_create_cast_device(hass: HomeAssistantType, - info: ChromecastInfo): - """Create a CastDevice Entity from the chromecast object. - - Returns None if the cast device has already been added. - """ - if info.uuid is None: - # Found a cast without UUID, we don't store it because we won't be able - # to update it anyway. - return CastDevice(info) - - # Found a cast with UUID - added_casts = hass.data[ADDED_CAST_DEVICES_KEY] - if info.uuid in added_casts: - # Already added this one, the entity will take care of moved hosts - # itself - return None - # -> New cast device - added_casts.add(info.uuid) - return CastDevice(info) - - -async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, - async_add_entities, discovery_info=None): - """Set up thet Cast platform. - - Deprecated. - """ - _LOGGER.warning( - 'Setting configuration for Cast via platform is deprecated. ' - 'Configure via Cast component instead.') - await _async_setup_platform( - hass, config, async_add_entities, discovery_info) - - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up Cast from a config entry.""" - config = hass.data[CAST_DOMAIN].get('media_player', {}) - if not isinstance(config, list): - config = [config] - - # no pending task - done, _ = await asyncio.wait([ - _async_setup_platform(hass, cfg, async_add_entities, None) - for cfg in config]) - if any([task.exception() for task in done]): - raise PlatformNotReady - - -async def _async_setup_platform(hass: HomeAssistantType, config: ConfigType, - async_add_entities, discovery_info): - """Set up the cast platform.""" - import pychromecast - - # Import CEC IGNORE attributes - pychromecast.IGNORE_CEC += config.get(CONF_IGNORE_CEC, []) - hass.data.setdefault(ADDED_CAST_DEVICES_KEY, set()) - hass.data.setdefault(KNOWN_CHROMECAST_INFO_KEY, set()) - - info = None - if discovery_info is not None: - info = ChromecastInfo(host=discovery_info['host'], - port=discovery_info['port']) - elif CONF_HOST in config: - info = ChromecastInfo(host=config[CONF_HOST], - port=DEFAULT_PORT) - - @callback - def async_cast_discovered(discover: ChromecastInfo) -> None: - """Handle discovery of a new chromecast.""" - if info is not None and info.host_port != discover.host_port: - # Not our requested cast device. - return - - cast_device = _async_create_cast_device(hass, discover) - if cast_device is not None: - async_add_entities([cast_device]) - - remove_handler = async_dispatcher_connect( - hass, SIGNAL_CAST_DISCOVERED, async_cast_discovered) - # Re-play the callback for all past chromecasts, store the objects in - # a list to avoid concurrent modification resulting in exception. - for chromecast in list(hass.data[KNOWN_CHROMECAST_INFO_KEY]): - async_cast_discovered(chromecast) - - if info is None or info.is_audio_group: - # If we were a) explicitly told to enable discovery or - # b) have an audio group cast device, we need internal discovery. - hass.async_add_job(_setup_internal_discovery, hass) - else: - info = await hass.async_add_job(_fill_out_missing_chromecast_info, - info) - if info.friendly_name is None: - _LOGGER.debug("Cannot retrieve detail information for chromecast" - " %s, the device may not be online", info) - remove_handler() - raise PlatformNotReady - - hass.async_add_job(_discover_chromecast, hass, info) - - -class CastStatusListener: - """Helper class to handle pychromecast status callbacks. - - Necessary because a CastDevice entity can create a new socket client - and therefore callbacks from multiple chromecast connections can - potentially arrive. This class allows invalidating past chromecast objects. - """ - - def __init__(self, cast_device, chromecast): - """Initialize the status listener.""" - self._cast_device = cast_device - self._valid = True - - chromecast.register_status_listener(self) - chromecast.socket_client.media_controller.register_status_listener( - self) - chromecast.register_connection_listener(self) - - def new_cast_status(self, cast_status): - """Handle reception of a new CastStatus.""" - if self._valid: - self._cast_device.new_cast_status(cast_status) - - def new_media_status(self, media_status): - """Handle reception of a new MediaStatus.""" - if self._valid: - self._cast_device.new_media_status(media_status) - - def new_connection_status(self, connection_status): - """Handle reception of a new ConnectionStatus.""" - if self._valid: - self._cast_device.new_connection_status(connection_status) - - def invalidate(self): - """Invalidate this status listener. - - All following callbacks won't be forwarded. - """ - self._valid = False - - -class CastDevice(MediaPlayerDevice): - """Representation of a Cast device on the network. - - This class is the holder of the pychromecast.Chromecast object and its - socket client. It therefore handles all reconnects and audio group changing - "elected leader" itself. - """ - - def __init__(self, cast_info): - """Initialize the cast device.""" - self._cast_info = cast_info # type: ChromecastInfo - 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] - - async def async_added_to_hass(self): - """Create chromecast object when added to hass.""" - @callback - def async_cast_discovered(discover: ChromecastInfo): - """Handle discovery of new Chromecast.""" - if self._cast_info.uuid is None: - # We can't handle empty UUIDs - return - if self._cast_info.uuid != discover.uuid: - # Discovered is not our device. - return - _LOGGER.debug("Discovered chromecast with same UUID: %s", discover) - self.hass.async_create_task(self.async_set_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.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop) - self.hass.async_create_task(self.async_set_cast_info(self._cast_info)) - - async def async_will_remove_from_hass(self) -> None: - """Disconnect Chromecast object when removed.""" - await self._async_disconnect() - if self._cast_info.uuid is not None: - # 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) - - 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._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() - - # 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 - )) - self._chromecast = chromecast - self._status_listener = CastStatusListener(self, chromecast) - # Initialise connection status as connected because we can only - # register the connection listener *after* the initial connection - # attempt. If the initial connection failed, we would never reach - # this code anyway. - self._available = True - self.cast_status = chromecast.status - self.media_status = chromecast.media_controller.status - _LOGGER.debug("Connection successful!") - self.async_schedule_update_ha_state() - - 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.") - self._available = False - self.async_schedule_update_ha_state() - - await self.hass.async_add_job(self._chromecast.disconnect) - - self._invalidate() - - self.async_schedule_update_ha_state() - - def _invalidate(self): - """Invalidate some attributes.""" - self._chromecast = None - self.cast_status = None - self.media_status = None - self.media_status_received = None - if self._status_listener is not None: - self._status_listener.invalidate() - self._status_listener = None - - # ========== Callbacks ========== - def new_cast_status(self, cast_status): - """Handle updates of the cast status.""" - self.cast_status = cast_status - self.schedule_update_ha_state() - - def new_media_status(self, media_status): - """Handle updates of the media status.""" - self.media_status = media_status - self.media_status_received = dt_util.utcnow() - self.schedule_update_ha_state() - - def new_connection_status(self, connection_status): - """Handle updates of connection status.""" - from pychromecast.socket_client import CONNECTION_STATUS_CONNECTED, \ - CONNECTION_STATUS_DISCONNECTED - - _LOGGER.debug("Received cast device connection status: %s", - connection_status.status) - if connection_status.status == CONNECTION_STATUS_DISCONNECTED: - self._available = False - self._invalidate() - self.schedule_update_ha_state() - return - - new_available = connection_status.status == CONNECTION_STATUS_CONNECTED - if new_available != self._available: - # 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) - self._available = new_available - self.schedule_update_ha_state() - - # ========== Service Calls ========== - def turn_on(self): - """Turn on the cast device.""" - import pychromecast - - if not self._chromecast.is_idle: - # Already turned on - return - - if self._chromecast.app_id is not None: - # Quit the previous app before starting splash screen - self._chromecast.quit_app() - - # The only way we can turn the Chromecast is on is by launching an app - self._chromecast.play_media(CAST_SPLASH, - pychromecast.STREAM_TYPE_BUFFERED) - - def turn_off(self): - """Turn off the cast device.""" - self._chromecast.quit_app() - - def mute_volume(self, mute): - """Mute the volume.""" - self._chromecast.set_volume_muted(mute) - - def set_volume_level(self, volume): - """Set volume level, range 0..1.""" - self._chromecast.set_volume(volume) - - def media_play(self): - """Send play command.""" - self._chromecast.media_controller.play() - - def media_pause(self): - """Send pause command.""" - self._chromecast.media_controller.pause() - - def media_stop(self): - """Send stop command.""" - self._chromecast.media_controller.stop() - - def media_previous_track(self): - """Send previous track command.""" - self._chromecast.media_controller.rewind() - - def media_next_track(self): - """Send next track command.""" - self._chromecast.media_controller.skip() - - def media_seek(self, position): - """Seek the media to a specific location.""" - self._chromecast.media_controller.seek(position) - - def play_media(self, media_type, media_id, **kwargs): - """Play media from a URL.""" - self._chromecast.media_controller.play_media(media_id, media_type) - - # ========== Properties ========== - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return the name of the device.""" - return self._cast_info.friendly_name - - @property - def device_info(self): - """Return information about the device.""" - cast_info = self._cast_info - - if cast_info.model_name == "Google Cast Group": - return None - - return { - 'name': cast_info.friendly_name, - 'identifiers': { - (CAST_DOMAIN, cast_info.uuid.replace('-', '')) - }, - 'model': cast_info.model_name, - 'manufacturer': cast_info.manufacturer, - } - - @property - def state(self): - """Return the state of the player.""" - if self.media_status is None: - return None - if self.media_status.player_is_playing: - return STATE_PLAYING - if self.media_status.player_is_paused: - return STATE_PAUSED - if self.media_status.player_is_idle: - return STATE_IDLE - if self._chromecast is not None and self._chromecast.is_idle: - return STATE_OFF - return None - - @property - def available(self): - """Return True if the cast device is connected.""" - return self._available - - @property - def volume_level(self): - """Volume level of the media player (0..1).""" - return self.cast_status.volume_level if self.cast_status else None - - @property - def is_volume_muted(self): - """Boolean if volume is currently muted.""" - return self.cast_status.volume_muted if self.cast_status else None - - @property - def media_content_id(self): - """Content ID of current playing media.""" - return self.media_status.content_id if self.media_status else None - - @property - def media_content_type(self): - """Content type of current playing media.""" - if self.media_status is None: - return None - if self.media_status.media_is_tvshow: - return MEDIA_TYPE_TVSHOW - if self.media_status.media_is_movie: - return MEDIA_TYPE_MOVIE - if self.media_status.media_is_musictrack: - return MEDIA_TYPE_MUSIC - return None - - @property - def media_duration(self): - """Duration of current playing media in seconds.""" - return self.media_status.duration if self.media_status else None - - @property - def media_image_url(self): - """Image url of current playing media.""" - if self.media_status is None: - return None - - images = self.media_status.images - - return images[0].url if images and images[0].url else None - - @property - def media_title(self): - """Title of current playing media.""" - return self.media_status.title if self.media_status else None - - @property - def media_artist(self): - """Artist of current playing media (Music track only).""" - return self.media_status.artist if self.media_status else None - - @property - def media_album_name(self): - """Album of current playing media (Music track only).""" - return self.media_status.album_name if self.media_status else None - - @property - def media_album_artist(self): - """Album artist of current playing media (Music track only).""" - return self.media_status.album_artist if self.media_status else None - - @property - def media_track(self): - """Track number of current playing media (Music track only).""" - return self.media_status.track if self.media_status else None - - @property - def media_series_title(self): - """Return the title of the series of current playing media.""" - return self.media_status.series_title if self.media_status else None - - @property - def media_season(self): - """Season of current playing media (TV Show only).""" - return self.media_status.season if self.media_status else None - - @property - def media_episode(self): - """Episode of current playing media (TV Show only).""" - return self.media_status.episode if self.media_status else None - - @property - def app_id(self): - """Return the ID of the current running app.""" - return self._chromecast.app_id if self._chromecast else None - - @property - def app_name(self): - """Name of the current running app.""" - return self._chromecast.app_display_name if self._chromecast else None - - @property - def supported_features(self): - """Flag media player features that are supported.""" - return SUPPORT_CAST - - @property - def media_position(self): - """Position of current playing media in seconds.""" - if self.media_status is None or \ - not (self.media_status.player_is_playing or - self.media_status.player_is_paused or - self.media_status.player_is_idle): - return None - return self.media_status.current_time - - @property - def media_position_updated_at(self): - """When was the position of the current playing media valid. - - Returns value from homeassistant.util.dt.utcnow(). - """ - return self.media_status_received - - @property - def unique_id(self) -> Optional[str]: - """Return a unique ID.""" - return self._cast_info.uuid diff --git a/homeassistant/components/media_player/channels.py b/homeassistant/components/media_player/channels.py index 43259c40f65d3..2f7b169601c9e 100644 --- a/homeassistant/components/media_player/channels.py +++ b/homeassistant/components/media_player/channels.py @@ -9,11 +9,12 @@ 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 2add2bd682a60..24df7c24611a5 100644 --- a/homeassistant/components/media_player/clementine.py +++ b/homeassistant/components/media_player/clementine.py @@ -11,9 +11,11 @@ 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 2711ac1ff11c8..20b292749b4ec 100644 --- a/homeassistant/components/media_player/cmus.py +++ b/homeassistant/components/media_player/cmus.py @@ -9,10 +9,11 @@ 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 0000000000000..bf7e6b4e0cef6 --- /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 8a88e3bd74e42..de455879d3d5a 100644 --- a/homeassistant/components/media_player/demo.py +++ b/homeassistant/components/media_player/demo.py @@ -6,12 +6,13 @@ """ 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 79b69b551cec0..3dc4e550d9b9d 100644 --- a/homeassistant/components/media_player/denon.py +++ b/homeassistant/components/media_player/denon.py @@ -10,10 +10,11 @@ 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 c565a161b101e..380484add53f5 100644 --- a/homeassistant/components/media_player/denonavr.py +++ b/homeassistant/components/media_player/denonavr.py @@ -11,17 +11,19 @@ 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 707014328c63b..9c5a3bf07b82b 100644 --- a/homeassistant/components/media_player/directv.py +++ b/homeassistant/components/media_player/directv.py @@ -9,10 +9,11 @@ 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 802b2b597fc04..03015cd5c01a7 100644 --- a/homeassistant/components/media_player/dlna_dmr.py +++ b/homeassistant/components/media_player/dlna_dmr.py @@ -14,9 +14,11 @@ 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 00c8ff3f4df6a..796aea86414dc 100644 --- a/homeassistant/components/media_player/dunehd.py +++ b/homeassistant/components/media_player/dunehd.py @@ -7,9 +7,11 @@ 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 dd43d48ee6a46..b1259db913d97 100644 --- a/homeassistant/components/media_player/emby.py +++ b/homeassistant/components/media_player/emby.py @@ -9,9 +9,11 @@ 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 bb1618f23513f..38c0ffacc3231 100644 --- a/homeassistant/components/media_player/epson.py +++ b/homeassistant/components/media_player/epson.py @@ -9,10 +9,11 @@ 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 c04ed96d6e01d..58f1913b9f983 100644 --- a/homeassistant/components/media_player/firetv.py +++ b/homeassistant/components/media_player/firetv.py @@ -10,9 +10,11 @@ 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 @@ 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 @@ def available(self): """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 67c84bd7b1b89..ed7041a3b8293 100644 --- a/homeassistant/components/media_player/frontier_silicon.py +++ b/homeassistant/components/media_player/frontier_silicon.py @@ -9,11 +9,12 @@ 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 b4ede671d5222..c72d14ebb8a6a 100644 --- a/homeassistant/components/media_player/gpmdp.py +++ b/homeassistant/components/media_player/gpmdp.py @@ -12,9 +12,10 @@ 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 fa8545bae03c4..c6571894472cb 100644 --- a/homeassistant/components/media_player/gstreamer.py +++ b/homeassistant/components/media_player/gstreamer.py @@ -9,8 +9,10 @@ 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 46d1dd4d69837..334757c086dba 100644 --- a/homeassistant/components/media_player/harman_kardon_avr.py +++ b/homeassistant/components/media_player/harman_kardon_avr.py @@ -10,9 +10,10 @@ 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/hdmi_cec.py b/homeassistant/components/media_player/hdmi_cec.py deleted file mode 100644 index d69d8a74ce6fb..0000000000000 --- a/homeassistant/components/media_player/hdmi_cec.py +++ /dev/null @@ -1,176 +0,0 @@ -""" -Support for HDMI CEC devices as media players. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/hdmi_cec/ -""" -import logging - -from homeassistant.components.hdmi_cec import ATTR_NEW, CecDevice -from homeassistant.components.media_player 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) -from homeassistant.const import ( - STATE_IDLE, STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING) - -DEPENDENCIES = ['hdmi_cec'] - -_LOGGER = logging.getLogger(__name__) - -ENTITY_ID_FORMAT = DOMAIN + '.{}' - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Find and return HDMI devices as +switches.""" - if ATTR_NEW in discovery_info: - _LOGGER.info("Setting up HDMI devices %s", discovery_info[ATTR_NEW]) - entities = [] - for device in discovery_info[ATTR_NEW]: - hdmi_device = hass.data.get(device) - entities.append(CecPlayerDevice( - hdmi_device, hdmi_device.logical_address, - )) - add_entities(entities, True) - - -class CecPlayerDevice(CecDevice, MediaPlayerDevice): - """Representation of a HDMI device as a Media player.""" - - def __init__(self, device, logical) -> None: - """Initialize the HDMI device.""" - CecDevice.__init__(self, device, logical) - self.entity_id = "%s.%s_%s" % ( - DOMAIN, 'hdmi', hex(self._logical_address)[2:]) - - def send_keypress(self, key): - """Send keypress to CEC adapter.""" - from pycec.commands import KeyPressCommand, KeyReleaseCommand - _LOGGER.debug("Sending keypress %s to device %s", hex(key), - hex(self._logical_address)) - self._device.send_command( - KeyPressCommand(key, dst=self._logical_address)) - self._device.send_command( - KeyReleaseCommand(dst=self._logical_address)) - - def send_playback(self, key): - """Send playback status to CEC adapter.""" - from pycec.commands import CecCommand - self._device.async_send_command( - CecCommand(key, dst=self._logical_address)) - - def mute_volume(self, mute): - """Mute volume.""" - from pycec.const import KEY_MUTE_TOGGLE - self.send_keypress(KEY_MUTE_TOGGLE) - - def media_previous_track(self): - """Go to previous track.""" - from pycec.const import KEY_BACKWARD - self.send_keypress(KEY_BACKWARD) - - def turn_on(self): - """Turn device on.""" - self._device.turn_on() - self._state = STATE_ON - - def clear_playlist(self): - """Clear players playlist.""" - raise NotImplementedError() - - def turn_off(self): - """Turn device off.""" - self._device.turn_off() - self._state = STATE_OFF - - def media_stop(self): - """Stop playback.""" - from pycec.const import KEY_STOP - self.send_keypress(KEY_STOP) - self._state = STATE_IDLE - - def play_media(self, media_type, media_id, **kwargs): - """Not supported.""" - raise NotImplementedError() - - def media_next_track(self): - """Skip to next track.""" - from pycec.const import KEY_FORWARD - self.send_keypress(KEY_FORWARD) - - def media_seek(self, position): - """Not supported.""" - raise NotImplementedError() - - def set_volume_level(self, volume): - """Set volume level, range 0..1.""" - raise NotImplementedError() - - def media_pause(self): - """Pause playback.""" - from pycec.const import KEY_PAUSE - self.send_keypress(KEY_PAUSE) - self._state = STATE_PAUSED - - def select_source(self, source): - """Not supported.""" - raise NotImplementedError() - - def media_play(self): - """Start playback.""" - from pycec.const import KEY_PLAY - self.send_keypress(KEY_PLAY) - self._state = STATE_PLAYING - - def volume_up(self): - """Increase volume.""" - from pycec.const import KEY_VOLUME_UP - _LOGGER.debug("%s: volume up", self._logical_address) - self.send_keypress(KEY_VOLUME_UP) - - def volume_down(self): - """Decrease volume.""" - from pycec.const import KEY_VOLUME_DOWN - _LOGGER.debug("%s: volume down", self._logical_address) - self.send_keypress(KEY_VOLUME_DOWN) - - @property - def state(self) -> str: - """Cache state of device.""" - return self._state - - def update(self): - """Update device status.""" - device = self._device - from pycec.const import STATUS_PLAY, STATUS_STOP, STATUS_STILL, \ - POWER_OFF, POWER_ON - if device.power_status in [POWER_OFF, 3]: - self._state = STATE_OFF - elif not self.support_pause: - if device.power_status in [POWER_ON, 4]: - self._state = STATE_ON - elif device.status == STATUS_PLAY: - self._state = STATE_PLAYING - elif device.status == STATUS_STOP: - self._state = STATE_IDLE - elif device.status == STATUS_STILL: - self._state = STATE_PAUSED - else: - _LOGGER.warning("Unknown state: %s", device.status) - - @property - def supported_features(self): - """Flag media player features that are supported.""" - from pycec.const import TYPE_RECORDER, TYPE_PLAYBACK, TYPE_TUNER, \ - TYPE_AUDIO - if self.type_id == TYPE_RECORDER or self.type == TYPE_PLAYBACK: - return (SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PLAY_MEDIA | - SUPPORT_PAUSE | SUPPORT_STOP | SUPPORT_PREVIOUS_TRACK | - SUPPORT_NEXT_TRACK) - if self.type == TYPE_TUNER: - return (SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PLAY_MEDIA | - SUPPORT_PAUSE | SUPPORT_STOP) - if self.type_id == TYPE_AUDIO: - return (SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_VOLUME_STEP | - SUPPORT_VOLUME_MUTE) - return SUPPORT_TURN_ON | SUPPORT_TURN_OFF diff --git a/homeassistant/components/media_player/horizon.py b/homeassistant/components/media_player/horizon.py index 058796ea46dbb..e7cfcfe62b132 100644 --- a/homeassistant/components/media_player/horizon.py +++ b/homeassistant/components/media_player/horizon.py @@ -11,9 +11,11 @@ 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 e2ae179676b3c..f8380032aea4c 100644 --- a/homeassistant/components/media_player/itunes.py +++ b/homeassistant/components/media_player/itunes.py @@ -10,10 +10,12 @@ 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 a83287eb617a0..f8d0cdc5a12cf 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -16,13 +16,14 @@ 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 c2f63c71f89cf..7c5d978937240 100644 --- a/homeassistant/components/media_player/lg_netcast.py +++ b/homeassistant/components/media_player/lg_netcast.py @@ -12,10 +12,11 @@ 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 38b27bd074a34..b45baf88bca2a 100644 --- a/homeassistant/components/media_player/lg_soundbar.py +++ b/homeassistant/components/media_player/lg_soundbar.py @@ -7,8 +7,10 @@ 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 3f8ea2cfd48cb..f69c3c67aec9a 100644 --- a/homeassistant/components/media_player/liveboxplaytv.py +++ b/homeassistant/components/media_player/liveboxplaytv.py @@ -11,10 +11,12 @@ 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 345b58cbbe413..29cc733293605 100644 --- a/homeassistant/components/media_player/mediaroom.py +++ b/homeassistant/components/media_player/mediaroom.py @@ -9,10 +9,12 @@ 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 96b8dcbcf26c7..e98ad47a6e7fa 100644 --- a/homeassistant/components/media_player/monoprice.py +++ b/homeassistant/components/media_player/monoprice.py @@ -9,9 +9,11 @@ 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 e6bc1f2699dfe..61d89c6d0b15d 100644 --- a/homeassistant/components/media_player/mpchc.py +++ b/homeassistant/components/media_player/mpchc.py @@ -11,9 +11,11 @@ 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 09d0a976b82e4..9d8015109b200 100644 --- a/homeassistant/components/media_player/mpd.py +++ b/homeassistant/components/media_player/mpd.py @@ -11,12 +11,14 @@ 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 57dca116f6b09..127be02dac4aa 100644 --- a/homeassistant/components/media_player/nad.py +++ b/homeassistant/components/media_player/nad.py @@ -10,9 +10,10 @@ 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 5ff54201b3c0a..df30c7e078278 100644 --- a/homeassistant/components/media_player/onkyo.py +++ b/homeassistant/components/media_player/onkyo.py @@ -12,9 +12,11 @@ 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 ab23f8a7f9aba..d828284a563e2 100644 --- a/homeassistant/components/media_player/openhome.py +++ b/homeassistant/components/media_player/openhome.py @@ -7,10 +7,11 @@ 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 041efed74bf1d..36a3160d3b52f 100644 --- a/homeassistant/components/media_player/panasonic_bluray.py +++ b/homeassistant/components/media_player/panasonic_bluray.py @@ -10,8 +10,10 @@ 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 bff108d70d7f7..e5ce22e952431 100644 --- a/homeassistant/components/media_player/panasonic_viera.py +++ b/homeassistant/components/media_player/panasonic_viera.py @@ -9,10 +9,12 @@ 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 231ea5302aef8..ca78f7a43182b 100644 --- a/homeassistant/components/media_player/pandora.py +++ b/homeassistant/components/media_player/pandora.py @@ -12,14 +12,14 @@ 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 506e5a9e47933..4f8a133978193 100644 --- a/homeassistant/components/media_player/philips_js.py +++ b/homeassistant/components/media_player/philips_js.py @@ -10,10 +10,11 @@ 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 171343d4adbb0..00fa453100ae7 100644 --- a/homeassistant/components/media_player/pioneer.py +++ b/homeassistant/components/media_player/pioneer.py @@ -10,9 +10,10 @@ 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 0609b75f98db8..c1b883a029589 100644 --- a/homeassistant/components/media_player/pjlink.py +++ b/homeassistant/components/media_player/pjlink.py @@ -9,8 +9,10 @@ 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 2110c42d37187..c67849edee991 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -13,10 +13,11 @@ 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 0000000000000..cbe9870461545 --- /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/roku.py b/homeassistant/components/media_player/roku.py deleted file mode 100644 index 9dc1151064d3e..0000000000000 --- a/homeassistant/components/media_player/roku.py +++ /dev/null @@ -1,194 +0,0 @@ -""" -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/ -""" -import logging -import requests.exceptions - -from homeassistant.components.media_player 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) -from homeassistant.const import (CONF_HOST, STATE_HOME, STATE_IDLE, - STATE_PLAYING) - -DEPENDENCIES = ['roku'] - -DEFAULT_PORT = 8060 - -_LOGGER = logging.getLogger(__name__) - -SUPPORT_ROKU = SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK |\ - SUPPORT_PLAY_MEDIA | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\ - SUPPORT_SELECT_SOURCE | SUPPORT_PLAY - - -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): - """Set up the Roku platform.""" - if not discovery_info: - return - - host = discovery_info[CONF_HOST] - async_add_entities([RokuDevice(host)], True) - - -class RokuDevice(MediaPlayerDevice): - """Representation of a Roku device on the network.""" - - def __init__(self, host): - """Initialize the Roku device.""" - from roku import Roku - - self.roku = Roku(host) - self.ip_address = host - self.channels = [] - self.current_app = None - self._device_info = {} - - def update(self): - """Retrieve latest state.""" - try: - self._device_info = self.roku.device_info - self.ip_address = self.roku.host - self.channels = self.get_source_list() - - if self.roku.current_app is not None: - self.current_app = self.roku.current_app - else: - self.current_app = None - except (requests.exceptions.ConnectionError, - requests.exceptions.ReadTimeout): - pass - - def get_source_list(self): - """Get the list of applications to be used as sources.""" - return ["Home"] + sorted(channel.name for channel in self.roku.apps) - - @property - def should_poll(self): - """Device should be polled.""" - return True - - @property - def name(self): - """Return the name of the device.""" - if self._device_info.userdevicename: - return self._device_info.userdevicename - return "Roku {}".format(self._device_info.sernum) - - @property - def state(self): - """Return the state of the device.""" - if self.current_app is None: - return None - - if (self.current_app.name == "Power Saver" or - self.current_app.is_screensaver): - return STATE_IDLE - if self.current_app.name == "Roku": - return STATE_HOME - if self.current_app.name is not None: - return STATE_PLAYING - - return None - - @property - def supported_features(self): - """Flag media player features that are supported.""" - return SUPPORT_ROKU - - @property - def unique_id(self): - """Return a unique, HASS-friendly identifier for this entity.""" - return self._device_info.sernum - - @property - def media_content_type(self): - """Content type of current playing media.""" - if self.current_app is None: - return None - if self.current_app.name == "Power Saver": - return None - if self.current_app.name == "Roku": - return None - return MEDIA_TYPE_MOVIE - - @property - def media_image_url(self): - """Image url of current playing media.""" - if self.current_app is None: - return None - if self.current_app.name == "Roku": - return None - if self.current_app.name == "Power Saver": - return None - if self.current_app.id is None: - return None - - return 'http://{0}:{1}/query/icon/{2}'.format( - self.ip_address, DEFAULT_PORT, self.current_app.id) - - @property - def app_name(self): - """Name of the current running app.""" - if self.current_app is not None: - return self.current_app.name - - @property - def app_id(self): - """Return the ID of the current running app.""" - if self.current_app is not None: - return self.current_app.id - - @property - def source(self): - """Return the current input source.""" - if self.current_app is not None: - return self.current_app.name - - @property - def source_list(self): - """List of available input sources.""" - return self.channels - - def media_play_pause(self): - """Send play/pause command.""" - if self.current_app is not None: - self.roku.play() - - def media_previous_track(self): - """Send previous track command.""" - if self.current_app is not None: - self.roku.reverse() - - def media_next_track(self): - """Send next track command.""" - if self.current_app is not None: - self.roku.forward() - - def mute_volume(self, mute): - """Mute the volume.""" - if self.current_app is not None: - self.roku.volume_mute() - - def volume_up(self): - """Volume up media player.""" - if self.current_app is not None: - self.roku.volume_up() - - def volume_down(self): - """Volume down media player.""" - if self.current_app is not None: - self.roku.volume_down() - - def select_source(self, source): - """Select input source.""" - if self.current_app is not None: - if source == "Home": - self.roku.home() - else: - channel = self.roku[source] - channel.launch() diff --git a/homeassistant/components/media_player/russound_rio.py b/homeassistant/components/media_player/russound_rio.py index 19cc2228d3266..972594e07e606 100644 --- a/homeassistant/components/media_player/russound_rio.py +++ b/homeassistant/components/media_player/russound_rio.py @@ -9,9 +9,10 @@ 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 7f4d04eb63456..6d919cdf7a8a9 100644 --- a/homeassistant/components/media_player/russound_rnet.py +++ b/homeassistant/components/media_player/russound_rnet.py @@ -9,8 +9,10 @@ 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 9def9875ab4e6..db6bd317c40a8 100644 --- a/homeassistant/components/media_player/samsungtv.py +++ b/homeassistant/components/media_player/samsungtv.py @@ -12,10 +12,11 @@ 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/sisyphus.py b/homeassistant/components/media_player/sisyphus.py deleted file mode 100644 index ef6b02514f0a5..0000000000000 --- a/homeassistant/components/media_player/sisyphus.py +++ /dev/null @@ -1,179 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.components.media_player 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) -from homeassistant.components.sisyphus import DATA_SISYPHUS -from homeassistant.const import ( - CONF_HOST, CONF_NAME, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING) - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['sisyphus'] - -MEDIA_TYPE_TRACK = 'sisyphus_track' - -SUPPORTED_FEATURES = SUPPORT_VOLUME_MUTE \ - | SUPPORT_VOLUME_SET \ - | SUPPORT_TURN_OFF \ - | SUPPORT_TURN_ON \ - | SUPPORT_PAUSE \ - | SUPPORT_SHUFFLE_SET \ - | SUPPORT_PREVIOUS_TRACK \ - | SUPPORT_NEXT_TRACK \ - | SUPPORT_PLAY - - -# pylint: disable=unused-argument -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up a media player entity for a Sisyphus table.""" - name = discovery_info[CONF_NAME] - host = discovery_info[CONF_HOST] - add_entities( - [SisyphusPlayer(name, host, hass.data[DATA_SISYPHUS][name])], True) - - -class SisyphusPlayer(MediaPlayerDevice): - """Representation of a Sisyphus table as a media player device.""" - - def __init__(self, name, host, table): - """Initialize the Sisyphus media device.""" - self._name = name - self._host = host - self._table = table - - async def async_added_to_hass(self): - """Add listeners after this object has been initialized.""" - self._table.add_listener( - lambda: self.async_schedule_update_ha_state(False)) - - @property - def name(self): - """Return the name of the table.""" - return self._name - - @property - def state(self): - """Return the current state of the table; sleeping maps to off.""" - if self._table.state in ["homing", "playing"]: - return STATE_PLAYING - if self._table.state == "paused": - if self._table.is_sleeping: - return STATE_OFF - - return STATE_PAUSED - if self._table.state == "waiting": - return STATE_IDLE - - return None - - @property - def volume_level(self): - """Return the current playback speed (0..1).""" - return self._table.speed - - @property - def shuffle(self): - """Return True if the current playlist is in shuffle mode.""" - return self._table.is_shuffle - - async def async_set_shuffle(self, shuffle): - """Change the shuffle mode of the current playlist.""" - await self._table.set_shuffle(shuffle) - - @property - def media_playlist(self): - """Return the name of the current playlist.""" - return self._table.active_playlist.name \ - if self._table.active_playlist \ - else None - - @property - def media_title(self): - """Return the title of the current track.""" - return self._table.active_track.name \ - if self._table.active_track \ - else None - - @property - def media_content_type(self): - """Return the content type currently playing; i.e. a Sisyphus track.""" - return MEDIA_TYPE_TRACK - - @property - def media_content_id(self): - """Return the track ID of the current track.""" - return self._table.active_track.id \ - if self._table.active_track \ - else None - - @property - def supported_features(self): - """Return the features supported by this table.""" - return SUPPORTED_FEATURES - - @property - def media_image_url(self): - """Return the URL for a thumbnail image of the current track.""" - from sisyphus_control import Track - if self._table.active_track: - return self._table.active_track.get_thumbnail_url( - Track.ThumbnailSize.LARGE) - - return super.media_image_url() - - async def async_turn_on(self): - """Wake up a sleeping table.""" - await self._table.wakeup() - - async def async_turn_off(self): - """Put the table to sleep.""" - await self._table.sleep() - - async def async_volume_down(self): - """Slow down playback.""" - await self._table.set_speed(max(0, self._table.speed - 0.1)) - - async def async_volume_up(self): - """Speed up playback.""" - await self._table.set_speed(min(1.0, self._table.speed + 0.1)) - - async def async_set_volume_level(self, volume): - """Set playback speed (0..1).""" - await self._table.set_speed(volume) - - async def async_media_play(self): - """Start playing.""" - await self._table.play() - - async def async_media_pause(self): - """Pause.""" - await self._table.pause() - - async def async_media_next_track(self): - """Skip to next track.""" - cur_track_index = self._get_current_track_index() - - await self._table.active_playlist.play( - self._table.active_playlist.tracks[cur_track_index + 1]) - - async def async_media_previous_track(self): - """Skip to previous track.""" - cur_track_index = self._get_current_track_index() - - await self._table.active_playlist.play( - self._table.active_playlist.tracks[cur_track_index - 1]) - - def _get_current_track_index(self): - for index, track in enumerate(self._table.active_playlist.tracks): - if track.id == self._table.active_track.id: - return index - - return -1 diff --git a/homeassistant/components/media_player/snapcast.py b/homeassistant/components/media_player/snapcast.py index cfe2f99729558..74b17ae5ff1a6 100644 --- a/homeassistant/components/media_player/snapcast.py +++ b/homeassistant/components/media_player/snapcast.py @@ -10,8 +10,10 @@ 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 e67578539adc2..7665b409d1d23 100644 --- a/homeassistant/components/media_player/songpal.py +++ b/homeassistant/components/media_player/songpal.py @@ -11,9 +11,11 @@ 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/sonos.py b/homeassistant/components/media_player/sonos.py deleted file mode 100644 index b34aabd4c5150..0000000000000 --- a/homeassistant/components/media_player/sonos.py +++ /dev/null @@ -1,1102 +0,0 @@ -""" -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/ -""" -import datetime -import functools as ft -import logging -import socket -import threading -import urllib - -import requests -import voluptuous as vol - -from homeassistant.components.media_player import ( - ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA, - 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) -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, - STATE_PLAYING) -import homeassistant.helpers.config_validation as cv -from homeassistant.util.dt import utcnow - -DEPENDENCIES = ('sonos',) - -_LOGGER = logging.getLogger(__name__) - -PARALLEL_UPDATES = 0 - -# Quiet down pysonos logging to just actual problems. -logging.getLogger('pysonos').setLevel(logging.WARNING) -logging.getLogger('pysonos.data_structures_entry').setLevel(logging.ERROR) - -SUPPORT_SONOS = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\ - SUPPORT_PLAY | SUPPORT_PAUSE | SUPPORT_STOP | SUPPORT_SELECT_SOURCE |\ - SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK |\ - SUPPORT_PLAY_MEDIA | SUPPORT_SHUFFLE_SET | SUPPORT_CLEAR_PLAYLIST - -SERVICE_JOIN = 'sonos_join' -SERVICE_UNJOIN = 'sonos_unjoin' -SERVICE_SNAPSHOT = 'sonos_snapshot' -SERVICE_RESTORE = 'sonos_restore' -SERVICE_SET_TIMER = 'sonos_set_sleep_timer' -SERVICE_CLEAR_TIMER = 'sonos_clear_sleep_timer' -SERVICE_UPDATE_ALARM = 'sonos_update_alarm' -SERVICE_SET_OPTION = 'sonos_set_option' - -DATA_SONOS = 'sonos_devices' - -SOURCE_LINEIN = 'Line-in' -SOURCE_TV = 'TV' - -CONF_ADVERTISE_ADDR = 'advertise_addr' -CONF_INTERFACE_ADDR = 'interface_addr' - -# Service call validation schemas -ATTR_SLEEP_TIME = 'sleep_time' -ATTR_ALARM_ID = 'alarm_id' -ATTR_VOLUME = 'volume' -ATTR_ENABLED = 'enabled' -ATTR_INCLUDE_LINKED_ZONES = 'include_linked_zones' -ATTR_MASTER = 'master' -ATTR_WITH_GROUP = 'with_group' -ATTR_NIGHT_SOUND = 'night_sound' -ATTR_SPEECH_ENHANCE = 'speech_enhance' - -ATTR_SONOS_GROUP = 'sonos_group' - -UPNP_ERRORS_TO_IGNORE = ['701', '711', '712'] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_ADVERTISE_ADDR): cv.string, - vol.Optional(CONF_INTERFACE_ADDR): cv.string, - vol.Optional(CONF_HOSTS): vol.All(cv.ensure_list, [cv.string]), -}) - -SONOS_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, -}) - -SONOS_JOIN_SCHEMA = SONOS_SCHEMA.extend({ - vol.Required(ATTR_MASTER): cv.entity_id, -}) - -SONOS_STATES_SCHEMA = SONOS_SCHEMA.extend({ - vol.Optional(ATTR_WITH_GROUP, default=True): cv.boolean, -}) - -SONOS_SET_TIMER_SCHEMA = SONOS_SCHEMA.extend({ - vol.Required(ATTR_SLEEP_TIME): - vol.All(vol.Coerce(int), vol.Range(min=0, max=86399)) -}) - -SONOS_UPDATE_ALARM_SCHEMA = SONOS_SCHEMA.extend({ - vol.Required(ATTR_ALARM_ID): cv.positive_int, - vol.Optional(ATTR_TIME): cv.time, - vol.Optional(ATTR_VOLUME): cv.small_float, - vol.Optional(ATTR_ENABLED): cv.boolean, - vol.Optional(ATTR_INCLUDE_LINKED_ZONES): cv.boolean, -}) - -SONOS_SET_OPTION_SCHEMA = SONOS_SCHEMA.extend({ - vol.Optional(ATTR_NIGHT_SOUND): cv.boolean, - vol.Optional(ATTR_SPEECH_ENHANCE): cv.boolean, -}) - - -class SonosData: - """Storage class for platform global data.""" - - def __init__(self): - """Initialize the data.""" - self.uids = set() - self.devices = [] - self.topology_lock = threading.Lock() - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Sonos platform. - - Deprecated. - """ - _LOGGER.warning('Loading Sonos via platform config is deprecated.') - _setup_platform(hass, config, add_entities, discovery_info) - - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up Sonos from a config entry.""" - def add_entities(devices, update_before_add=False): - """Sync version of async add devices.""" - hass.add_job(async_add_entities, devices, update_before_add) - - hass.async_add_executor_job( - _setup_platform, hass, hass.data[SONOS_DOMAIN].get('media_player', {}), - add_entities, None) - - -def _setup_platform(hass, config, add_entities, discovery_info): - """Set up the Sonos platform.""" - import pysonos - - if DATA_SONOS not in hass.data: - hass.data[DATA_SONOS] = SonosData() - - advertise_addr = config.get(CONF_ADVERTISE_ADDR) - if advertise_addr: - pysonos.config.EVENT_ADVERTISE_IP = advertise_addr - - players = [] - if discovery_info: - player = pysonos.SoCo(discovery_info.get('host')) - - # If device already exists by config - if player.uid in hass.data[DATA_SONOS].uids: - return - - # If invisible, such as a stereo slave - if not player.is_visible: - return - - players.append(player) - else: - hosts = config.get(CONF_HOSTS) - if hosts: - # Support retro compatibility with comma separated list of hosts - # from config - hosts = hosts[0] if len(hosts) == 1 else hosts - hosts = hosts.split(',') if isinstance(hosts, str) else hosts - for host in hosts: - try: - players.append(pysonos.SoCo(socket.gethostbyname(host))) - except OSError: - _LOGGER.warning("Failed to initialize '%s'", host) - else: - players = pysonos.discover( - interface_addr=config.get(CONF_INTERFACE_ADDR)) - - if not players: - _LOGGER.warning("No Sonos speakers found") - return - - hass.data[DATA_SONOS].uids.update(p.uid for p in players) - add_entities(SonosDevice(p) for p in players) - _LOGGER.debug("Added %s Sonos speakers", len(players)) - - def service_handle(service): - """Handle for services.""" - entity_ids = service.data.get('entity_id') - - devices = hass.data[DATA_SONOS].devices - if entity_ids: - devices = [d for d in devices if d.entity_id in entity_ids] - - if service.service == SERVICE_JOIN: - master = [device for device in hass.data[DATA_SONOS].devices - if device.entity_id == service.data[ATTR_MASTER]] - if master: - with hass.data[DATA_SONOS].topology_lock: - master[0].join(devices) - return - - if service.service == SERVICE_UNJOIN: - with hass.data[DATA_SONOS].topology_lock: - for device in devices: - device.unjoin() - return - - for device in devices: - if service.service == SERVICE_SNAPSHOT: - device.snapshot(service.data[ATTR_WITH_GROUP]) - elif service.service == SERVICE_RESTORE: - device.restore(service.data[ATTR_WITH_GROUP]) - elif service.service == SERVICE_SET_TIMER: - device.set_sleep_timer(service.data[ATTR_SLEEP_TIME]) - elif service.service == SERVICE_CLEAR_TIMER: - device.clear_sleep_timer() - elif service.service == SERVICE_UPDATE_ALARM: - device.set_alarm(**service.data) - elif service.service == SERVICE_SET_OPTION: - device.set_option(**service.data) - - device.schedule_update_ha_state(True) - - hass.services.register( - DOMAIN, SERVICE_JOIN, service_handle, - schema=SONOS_JOIN_SCHEMA) - - hass.services.register( - DOMAIN, SERVICE_UNJOIN, service_handle, - schema=SONOS_SCHEMA) - - hass.services.register( - DOMAIN, SERVICE_SNAPSHOT, service_handle, - schema=SONOS_STATES_SCHEMA) - - hass.services.register( - DOMAIN, SERVICE_RESTORE, service_handle, - schema=SONOS_STATES_SCHEMA) - - hass.services.register( - DOMAIN, SERVICE_SET_TIMER, service_handle, - schema=SONOS_SET_TIMER_SCHEMA) - - hass.services.register( - DOMAIN, SERVICE_CLEAR_TIMER, service_handle, - schema=SONOS_SCHEMA) - - hass.services.register( - DOMAIN, SERVICE_UPDATE_ALARM, service_handle, - schema=SONOS_UPDATE_ALARM_SCHEMA) - - hass.services.register( - DOMAIN, SERVICE_SET_OPTION, service_handle, - schema=SONOS_SET_OPTION_SCHEMA) - - -class _ProcessSonosEventQueue: - """Queue like object for dispatching sonos events.""" - - def __init__(self, handler): - """Initialize Sonos event queue.""" - self._handler = handler - - def put(self, item, block=True, timeout=None): - """Process event.""" - self._handler(item) - - -def _get_entity_from_soco_uid(hass, uid): - """Return SonosDevice from SoCo uid.""" - for entity in hass.data[DATA_SONOS].devices: - if uid == entity.soco.uid: - return entity - return None - - -def soco_error(errorcodes=None): - """Filter out specified UPnP errors from logs and avoid exceptions.""" - def decorator(funct): - """Decorate functions.""" - @ft.wraps(funct) - def wrapper(*args, **kwargs): - """Wrap for all soco UPnP exception.""" - from pysonos.exceptions import SoCoUPnPException, SoCoException - - try: - return funct(*args, **kwargs) - except SoCoUPnPException as err: - if errorcodes and err.error_code in errorcodes: - pass - else: - _LOGGER.error("Error on %s with %s", funct.__name__, err) - except SoCoException as err: - _LOGGER.error("Error on %s with %s", funct.__name__, err) - - return wrapper - return decorator - - -def soco_coordinator(funct): - """Call function on coordinator.""" - @ft.wraps(funct) - def wrapper(device, *args, **kwargs): - """Wrap for call to coordinator.""" - if device.is_coordinator: - return funct(device, *args, **kwargs) - return funct(device.coordinator, *args, **kwargs) - - return wrapper - - -def _timespan_secs(timespan): - """Parse a time-span into number of seconds.""" - if timespan in ('', 'NOT_IMPLEMENTED', None): - return None - - return sum(60 ** x[0] * int(x[1]) for x in enumerate( - reversed(timespan.split(':')))) - - -def _is_radio_uri(uri): - """Return whether the URI is a radio stream.""" - radio_schemes = ( - 'x-rincon-mp3radio:', 'x-sonosapi-stream:', 'x-sonosapi-radio:', - 'x-sonosapi-hls:', 'hls-radio:') - return uri.startswith(radio_schemes) - - -class SonosDevice(MediaPlayerDevice): - """Representation of a Sonos device.""" - - def __init__(self, player): - """Initialize the Sonos device.""" - self._subscriptions = [] - self._receives_events = False - self._volume_increment = 2 - self._unique_id = player.uid - self._player = player - self._model = None - self._player_volume = None - self._player_muted = None - self._shuffle = None - self._name = None - self._coordinator = None - self._sonos_group = None - self._status = None - self._media_duration = None - self._media_position = None - self._media_position_updated_at = None - self._media_image_url = None - self._media_artist = None - self._media_album_name = None - self._media_title = None - self._night_sound = None - self._speech_enhance = None - self._source_name = None - self._available = True - self._favorites = None - self._soco_snapshot = None - self._snapshot_group = None - - self._set_basic_information() - - async def async_added_to_hass(self): - """Subscribe sonos events.""" - self.hass.data[DATA_SONOS].devices.append(self) - self.hass.async_add_executor_job(self._subscribe_to_player_events) - - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def device_info(self): - """Return information about the device.""" - return { - 'identifiers': { - (SONOS_DOMAIN, self._unique_id) - }, - 'name': self._name, - 'model': self._model.replace("Sonos ", ""), - 'manufacturer': 'Sonos', - } - - @property - @soco_coordinator - def state(self): - """Return the state of the device.""" - if self._status in ('PAUSED_PLAYBACK', 'STOPPED'): - return STATE_PAUSED - if self._status in ('PLAYING', 'TRANSITIONING'): - return STATE_PLAYING - if self._status == 'OFF': - return STATE_OFF - return STATE_IDLE - - @property - def is_coordinator(self): - """Return true if player is a coordinator.""" - return self._coordinator is None - - @property - def soco(self): - """Return soco device.""" - return self._player - - @property - def coordinator(self): - """Return coordinator of this player.""" - return self._coordinator - - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._available - - def _check_available(self): - """Check that we can still connect to the player.""" - try: - sock = socket.create_connection( - address=(self.soco.ip_address, 1443), timeout=3) - sock.close() - return True - except socket.error: - return False - - def _set_basic_information(self): - """Set initial device information.""" - speaker_info = self.soco.get_speaker_info(True) - self._name = speaker_info['zone_name'] - self._model = speaker_info['model_name'] - self._shuffle = self.soco.shuffle - - self.update_volume() - - self._set_favorites() - - def _set_favorites(self): - """Set available favorites.""" - # SoCo 0.16 raises a generic Exception on invalid xml in favorites. - # Filter those out now so our list is safe to use. - try: - self._favorites = [] - for fav in self.soco.music_library.get_sonos_favorites(): - try: - if fav.reference.get_uri(): - self._favorites.append(fav) - except Exception: # pylint: disable=broad-except - _LOGGER.debug("Ignoring invalid favorite '%s'", fav.title) - except Exception: # pylint: disable=broad-except - _LOGGER.debug("Ignoring invalid favorite list") - - def _radio_artwork(self, url): - """Return the private URL with artwork for a radio stream.""" - if url not in ('', 'NOT_IMPLEMENTED', None): - if url.find('tts_proxy') > 0: - # If the content is a tts don't try to fetch an image from it. - return None - url = 'http://{host}:{port}/getaa?s=1&u={uri}'.format( - host=self.soco.ip_address, - port=1400, - uri=urllib.parse.quote(url, safe='') - ) - return url - - def _subscribe_to_player_events(self): - """Add event subscriptions.""" - self._receives_events = False - - # New player available, build the current group topology - for device in self.hass.data[DATA_SONOS].devices: - device.update_groups() - - player = self.soco - - def subscribe(service, action): - """Add a subscription to a pysonos service.""" - queue = _ProcessSonosEventQueue(action) - sub = service.subscribe(auto_renew=True, event_queue=queue) - self._subscriptions.append(sub) - - subscribe(player.avTransport, self.update_media) - subscribe(player.renderingControl, self.update_volume) - subscribe(player.zoneGroupTopology, self.update_groups) - subscribe(player.contentDirectory, self.update_content) - - def update(self): - """Retrieve latest state.""" - available = self._check_available() - if self._available != available: - self._available = available - if available: - self._set_basic_information() - self._subscribe_to_player_events() - else: - for subscription in self._subscriptions: - self.hass.async_add_executor_job(subscription.unsubscribe) - self._subscriptions = [] - - self._player_volume = None - self._player_muted = None - self._status = 'OFF' - self._coordinator = None - self._media_duration = None - self._media_position = None - self._media_position_updated_at = None - self._media_image_url = None - self._media_artist = None - self._media_album_name = None - self._media_title = None - self._source_name = None - elif available and not self._receives_events: - self.update_groups() - self.update_volume() - if self.is_coordinator: - self.update_media() - - def update_media(self, event=None): - """Update information about currently playing media.""" - transport_info = self.soco.get_current_transport_info() - new_status = transport_info.get('current_transport_state') - - # Ignore transitions, we should get the target state soon - if new_status == 'TRANSITIONING': - return - - self._shuffle = self.soco.shuffle - - if self.soco.is_playing_tv: - self.update_media_linein(SOURCE_TV) - elif self.soco.is_playing_line_in: - self.update_media_linein(SOURCE_LINEIN) - else: - track_info = self.soco.get_current_track_info() - - if _is_radio_uri(track_info['uri']): - variables = event and event.variables - self.update_media_radio(variables, track_info) - else: - update_position = (new_status != self._status) - self.update_media_music(update_position, track_info) - - self._status = new_status - - self.schedule_update_ha_state() - - # Also update slaves - for entity in self.hass.data[DATA_SONOS].devices: - coordinator = entity.coordinator - if coordinator and coordinator.unique_id == self.unique_id: - entity.schedule_update_ha_state() - - def update_media_linein(self, source): - """Update state when playing from line-in/tv.""" - self._media_duration = None - self._media_position = None - self._media_position_updated_at = None - - self._media_image_url = None - - self._media_artist = source - self._media_album_name = None - self._media_title = None - - self._source_name = source - - def update_media_radio(self, variables, track_info): - """Update state when streaming radio.""" - self._media_duration = None - self._media_position = None - self._media_position_updated_at = None - - media_info = self.soco.avTransport.GetMediaInfo([('InstanceID', 0)]) - self._media_image_url = self._radio_artwork(media_info['CurrentURI']) - - self._media_artist = track_info.get('artist') - self._media_album_name = None - self._media_title = track_info.get('title') - - if self._media_artist and self._media_title: - # artist and album name are in the data, concatenate - # that do display as artist. - # "Information" field in the sonos pc app - self._media_artist = '{artist} - {title}'.format( - artist=self._media_artist, - title=self._media_title - ) - elif variables: - # "On Now" field in the sonos pc app - current_track_metadata = variables.get('current_track_meta_data') - if current_track_metadata: - self._media_artist = \ - current_track_metadata.radio_show.split(',')[0] - - # For radio streams we set the radio station name as the title. - current_uri_metadata = media_info["CurrentURIMetaData"] - if current_uri_metadata not in ('', 'NOT_IMPLEMENTED', None): - # currently soco does not have an API for this - import pysonos - current_uri_metadata = pysonos.xml.XML.fromstring( - pysonos.utils.really_utf8(current_uri_metadata)) - - md_title = current_uri_metadata.findtext( - './/{http://purl.org/dc/elements/1.1/}title') - - if md_title not in ('', 'NOT_IMPLEMENTED', None): - self._media_title = md_title - - if self._media_artist and self._media_title: - # some radio stations put their name into the artist - # name, e.g.: - # media_title = "Station" - # media_artist = "Station - Artist - Title" - # detect this case and trim from the front of - # media_artist for cosmetics - trim = '{title} - '.format(title=self._media_title) - chars = min(len(self._media_artist), len(trim)) - - if self._media_artist[:chars].upper() == trim[:chars].upper(): - self._media_artist = self._media_artist[chars:] - - # Check if currently playing radio station is in favorites - self._source_name = None - for fav in self._favorites: - if fav.reference.get_uri() == media_info['CurrentURI']: - self._source_name = fav.title - - def update_media_music(self, update_media_position, track_info): - """Update state when playing music tracks.""" - self._media_duration = _timespan_secs(track_info.get('duration')) - - position_info = self.soco.avTransport.GetPositionInfo( - [('InstanceID', 0), - ('Channel', 'Master')] - ) - rel_time = _timespan_secs(position_info.get("RelTime")) - - # player no longer reports position? - update_media_position |= rel_time is None and \ - self._media_position is not None - - # player started reporting position? - update_media_position |= rel_time is not None and \ - self._media_position is None - - # position jumped? - if rel_time is not None and self._media_position is not None: - time_diff = utcnow() - self._media_position_updated_at - time_diff = time_diff.total_seconds() - - calculated_position = self._media_position + time_diff - - update_media_position |= abs(calculated_position - rel_time) > 1.5 - - if update_media_position: - self._media_position = rel_time - self._media_position_updated_at = utcnow() - - self._media_image_url = track_info.get('album_art') - - self._media_artist = track_info.get('artist') - self._media_album_name = track_info.get('album') - self._media_title = track_info.get('title') - - self._source_name = None - - def update_volume(self, event=None): - """Update information about currently volume settings.""" - if event: - variables = event.variables - - if 'volume' in variables: - self._player_volume = int(variables['volume']['Master']) - - if 'mute' in variables: - self._player_muted = (variables['mute']['Master'] == '1') - - if 'night_mode' in variables: - self._night_sound = (variables['night_mode'] == '1') - - if 'dialog_level' in variables: - self._speech_enhance = (variables['dialog_level'] == '1') - - self.schedule_update_ha_state() - else: - self._player_volume = self.soco.volume - self._player_muted = self.soco.mute - self._night_sound = self.soco.night_mode - self._speech_enhance = self.soco.dialog_mode - - def update_groups(self, event=None): - """Process a zone group topology event coming from a player.""" - if event: - self._receives_events = True - - if not hasattr(event, 'zone_player_uui_ds_in_group'): - return - - with self.hass.data[DATA_SONOS].topology_lock: - group = event and event.zone_player_uui_ds_in_group - if group: - # New group information is pushed - coordinator_uid, *slave_uids = group.split(',') - else: - coordinator_uid = self.unique_id - slave_uids = [] - - # Try SoCo cache for existing topology - try: - if self.soco.group and self.soco.group.coordinator: - coordinator_uid = self.soco.group.coordinator.uid - slave_uids = [p.uid for p in self.soco.group.members - if p.uid != coordinator_uid] - except requests.exceptions.RequestException: - pass - - if self.unique_id == coordinator_uid: - sonos_group = [] - for uid in (coordinator_uid, *slave_uids): - entity = _get_entity_from_soco_uid(self.hass, uid) - if entity: - sonos_group.append(entity.entity_id) - - self._coordinator = None - self._sonos_group = sonos_group - self.schedule_update_ha_state() - - for slave_uid in slave_uids: - slave = _get_entity_from_soco_uid(self.hass, slave_uid) - if slave: - # pylint: disable=protected-access - slave._coordinator = self - slave._sonos_group = sonos_group - slave.schedule_update_ha_state() - - def update_content(self, event=None): - """Update information about available content.""" - self._set_favorites() - self.schedule_update_ha_state() - - @property - def volume_level(self): - """Volume level of the media player (0..1).""" - return self._player_volume / 100 - - @property - def is_volume_muted(self): - """Return true if volume is muted.""" - return self._player_muted - - @property - @soco_coordinator - def shuffle(self): - """Shuffling state.""" - return self._shuffle - - @property - def media_content_type(self): - """Content type of current playing media.""" - return MEDIA_TYPE_MUSIC - - @property - @soco_coordinator - def media_duration(self): - """Duration of current playing media in seconds.""" - return self._media_duration - - @property - @soco_coordinator - def media_position(self): - """Position of current playing media in seconds.""" - return self._media_position - - @property - @soco_coordinator - def media_position_updated_at(self): - """When was the position of the current playing media valid.""" - return self._media_position_updated_at - - @property - @soco_coordinator - def media_image_url(self): - """Image url of current playing media.""" - return self._media_image_url or None - - @property - @soco_coordinator - def media_artist(self): - """Artist of current playing media, music track only.""" - return self._media_artist - - @property - @soco_coordinator - def media_album_name(self): - """Album name of current playing media, music track only.""" - return self._media_album_name - - @property - @soco_coordinator - def media_title(self): - """Title of current playing media.""" - return self._media_title - - @property - @soco_coordinator - def source(self): - """Name of the current input source.""" - return self._source_name - - @property - @soco_coordinator - def supported_features(self): - """Flag media player features that are supported.""" - return SUPPORT_SONOS - - @soco_error() - def volume_up(self): - """Volume up media player.""" - self._player.volume += self._volume_increment - - @soco_error() - def volume_down(self): - """Volume down media player.""" - self._player.volume -= self._volume_increment - - @soco_error() - def set_volume_level(self, volume): - """Set volume level, range 0..1.""" - self.soco.volume = str(int(volume * 100)) - - @soco_error(UPNP_ERRORS_TO_IGNORE) - @soco_coordinator - def set_shuffle(self, shuffle): - """Enable/Disable shuffle mode.""" - self.soco.shuffle = shuffle - - @soco_error() - def mute_volume(self, mute): - """Mute (true) or unmute (false) media player.""" - self.soco.mute = mute - - @soco_error() - @soco_coordinator - def select_source(self, source): - """Select input source.""" - if source == SOURCE_LINEIN: - self.soco.switch_to_line_in() - elif source == SOURCE_TV: - self.soco.switch_to_tv() - else: - fav = [fav for fav in self._favorites - if fav.title == source] - if len(fav) == 1: - src = fav.pop() - uri = src.reference.get_uri() - if _is_radio_uri(uri): - self.soco.play_uri(uri, title=source) - else: - self.soco.clear_queue() - self.soco.add_to_queue(src.reference) - self.soco.play_from_queue(0) - - @property - @soco_coordinator - def source_list(self): - """List of available input sources.""" - sources = [fav.title for fav in self._favorites] - - model = self._model.upper() - if 'PLAY:5' in model or 'CONNECT' in model: - sources += [SOURCE_LINEIN] - elif 'PLAYBAR' in model: - sources += [SOURCE_LINEIN, SOURCE_TV] - elif 'BEAM' in model: - sources += [SOURCE_TV] - - return sources - - @soco_error() - def turn_on(self): - """Turn the media player on.""" - self.media_play() - - @soco_error() - def turn_off(self): - """Turn off media player.""" - self.media_stop() - - @soco_error(UPNP_ERRORS_TO_IGNORE) - @soco_coordinator - def media_play(self): - """Send play command.""" - self.soco.play() - - @soco_error(UPNP_ERRORS_TO_IGNORE) - @soco_coordinator - def media_stop(self): - """Send stop command.""" - self.soco.stop() - - @soco_error(UPNP_ERRORS_TO_IGNORE) - @soco_coordinator - def media_pause(self): - """Send pause command.""" - self.soco.pause() - - @soco_error(UPNP_ERRORS_TO_IGNORE) - @soco_coordinator - def media_next_track(self): - """Send next track command.""" - self.soco.next() - - @soco_error(UPNP_ERRORS_TO_IGNORE) - @soco_coordinator - def media_previous_track(self): - """Send next track command.""" - self.soco.previous() - - @soco_error(UPNP_ERRORS_TO_IGNORE) - @soco_coordinator - def media_seek(self, position): - """Send seek command.""" - self.soco.seek(str(datetime.timedelta(seconds=int(position)))) - - @soco_error() - @soco_coordinator - def clear_playlist(self): - """Clear players playlist.""" - self.soco.clear_queue() - - @soco_error() - @soco_coordinator - def play_media(self, media_type, media_id, **kwargs): - """ - Send the play_media command to the media player. - - If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the queue. - """ - if kwargs.get(ATTR_MEDIA_ENQUEUE): - from pysonos.exceptions import SoCoUPnPException - try: - self.soco.add_uri_to_queue(media_id) - except SoCoUPnPException: - _LOGGER.error('Error parsing media uri "%s", ' - "please check it's a valid media resource " - 'supported by Sonos', media_id) - else: - self.soco.play_uri(media_id) - - @soco_error() - def join(self, slaves): - """Form a group with other players.""" - if self._coordinator: - self.unjoin() - - for slave in slaves: - if slave.unique_id != self.unique_id: - slave.soco.join(self.soco) - # pylint: disable=protected-access - slave._coordinator = self - - @soco_error() - def unjoin(self): - """Unjoin the player from a group.""" - self.soco.unjoin() - self._coordinator = None - - @soco_error() - def snapshot(self, with_group=True): - """Snapshot the player.""" - from pysonos.snapshot import Snapshot - - self._soco_snapshot = Snapshot(self.soco) - self._soco_snapshot.snapshot() - - if with_group: - self._snapshot_group = self.soco.group - if self._coordinator: - self._coordinator.snapshot(False) - else: - self._snapshot_group = None - - @soco_error() - def restore(self, with_group=True): - """Restore snapshot for the player.""" - from pysonos.exceptions import SoCoException - try: - # need catch exception if a coordinator is going to slave. - # this state will recover with group part. - self._soco_snapshot.restore(False) - except (TypeError, AttributeError, SoCoException): - _LOGGER.debug("Error on restore %s", self.entity_id) - - # restore groups - if with_group and self._snapshot_group: - old = self._snapshot_group - actual = self.soco.group - - ## - # Master have not change, update group - if old.coordinator == actual.coordinator: - if self.soco is not old.coordinator: - # restore state of the groups - self._coordinator.restore(False) - remove = actual.members - old.members - add = old.members - actual.members - - # remove new members - for soco_dev in list(remove): - soco_dev.unjoin() - - # add old members - for soco_dev in list(add): - soco_dev.join(old.coordinator) - return - - ## - # old is already master, rejoin - if old.coordinator.group.coordinator == old.coordinator: - self.soco.join(old.coordinator) - return - - ## - # restore old master, update group - old.coordinator.unjoin() - coordinator = _get_entity_from_soco_uid( - self.hass, old.coordinator.uid) - coordinator.restore(False) - - for s_dev in list(old.members): - if s_dev != old.coordinator: - s_dev.join(old.coordinator) - - @soco_error() - @soco_coordinator - def set_sleep_timer(self, sleep_time): - """Set the timer on the player.""" - self.soco.set_sleep_timer(sleep_time) - - @soco_error() - @soco_coordinator - def clear_sleep_timer(self): - """Clear the timer on the player.""" - self.soco.set_sleep_timer(None) - - @soco_error() - @soco_coordinator - def set_alarm(self, **data): - """Set the alarm clock on the player.""" - from pysonos import alarms - alarm = None - for one_alarm in alarms.get_alarms(self.soco): - # pylint: disable=protected-access - if one_alarm._alarm_id == str(data[ATTR_ALARM_ID]): - alarm = one_alarm - if alarm is None: - _LOGGER.warning("did not find alarm with id %s", - data[ATTR_ALARM_ID]) - return - if ATTR_TIME in data: - alarm.start_time = data[ATTR_TIME] - if ATTR_VOLUME in data: - alarm.volume = int(data[ATTR_VOLUME] * 100) - if ATTR_ENABLED in data: - alarm.enabled = data[ATTR_ENABLED] - if ATTR_INCLUDE_LINKED_ZONES in data: - alarm.include_linked_zones = data[ATTR_INCLUDE_LINKED_ZONES] - alarm.save() - - @soco_error() - def set_option(self, **data): - """Modify playback options.""" - if ATTR_NIGHT_SOUND in data and self._night_sound is not None: - self.soco.night_mode = data[ATTR_NIGHT_SOUND] - - if ATTR_SPEECH_ENHANCE in data and self._speech_enhance is not None: - self.soco.dialog_mode = data[ATTR_SPEECH_ENHANCE] - - @property - def device_state_attributes(self): - """Return device specific state attributes.""" - attributes = {ATTR_SONOS_GROUP: self._sonos_group} - - if self._night_sound is not None: - attributes[ATTR_NIGHT_SOUND] = self._night_sound - - if self._speech_enhance is not None: - attributes[ATTR_SPEECH_ENHANCE] = self._speech_enhance - - return attributes diff --git a/homeassistant/components/media_player/soundtouch.py b/homeassistant/components/media_player/soundtouch.py index 037a9b88fc63d..b2045b9b65eb7 100644 --- a/homeassistant/components/media_player/soundtouch.py +++ b/homeassistant/components/media_player/soundtouch.py @@ -10,10 +10,11 @@ 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 4fbd43f3f1602..9965487ded9b7 100644 --- a/homeassistant/components/media_player/spotify.py +++ b/homeassistant/components/media_player/spotify.py @@ -11,10 +11,11 @@ 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 73b6a070419ad..5f6fd525a112e 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -14,11 +14,13 @@ 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 75f6d92a98c28..2261aadc2f68f 100644 --- a/homeassistant/components/media_player/ue_smart_radio.py +++ b/homeassistant/components/media_player/ue_smart_radio.py @@ -11,10 +11,11 @@ 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 18b953a037246..5730a0867311b 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -10,6 +10,8 @@ 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 @@ 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 @@ def supported_features(self): 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 5aae8661bd888..395f5bb369e4d 100644 --- a/homeassistant/components/media_player/vizio.py +++ b/homeassistant/components/media_player/vizio.py @@ -11,10 +11,11 @@ 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 5cc4196d4e11c..592243938d767 100644 --- a/homeassistant/components/media_player/vlc.py +++ b/homeassistant/components/media_player/vlc.py @@ -9,9 +9,10 @@ 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 bd43e6c371039..a72f34fac1de6 100644 --- a/homeassistant/components/media_player/volumio.py +++ b/homeassistant/components/media_player/volumio.py @@ -15,11 +15,12 @@ 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/webostv.py b/homeassistant/components/media_player/webostv.py deleted file mode 100644 index f80e29d35a0e0..0000000000000 --- a/homeassistant/components/media_player/webostv.py +++ /dev/null @@ -1,402 +0,0 @@ -""" -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/ -""" -import asyncio -from datetime import timedelta -import logging -from urllib.parse import urlparse -from typing import Dict # noqa: F401 pylint: disable=unused-import - -import voluptuous as vol - -from homeassistant import util -from homeassistant.components.media_player import ( - MEDIA_TYPE_CHANNEL, PLATFORM_SCHEMA, 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) -from homeassistant.const import ( - CONF_CUSTOMIZE, CONF_FILENAME, CONF_HOST, CONF_NAME, CONF_TIMEOUT, - STATE_OFF, STATE_PAUSED, STATE_PLAYING) -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.script import Script - -REQUIREMENTS = ['pylgtv==0.1.9', 'websockets==6.0'] - -_CONFIGURING = {} # type: Dict[str, str] -_LOGGER = logging.getLogger(__name__) - -CONF_SOURCES = 'sources' -CONF_ON_ACTION = 'turn_on_action' - -DEFAULT_NAME = "LG webOS Smart TV" -LIVETV_APP_ID = 'com.webos.app.livetv' - -WEBOSTV_CONFIG_FILE = 'webostv.conf' - -SUPPORT_WEBOSTV = SUPPORT_TURN_OFF | \ - SUPPORT_NEXT_TRACK | SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | \ - SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_STEP | \ - SUPPORT_SELECT_SOURCE | SUPPORT_PLAY_MEDIA | SUPPORT_PLAY - -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) - -CUSTOMIZE_SCHEMA = vol.Schema({ - vol.Optional(CONF_SOURCES): vol.All(cv.ensure_list, [cv.string]), -}) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_CUSTOMIZE, default={}): CUSTOMIZE_SCHEMA, - vol.Optional(CONF_FILENAME, default=WEBOSTV_CONFIG_FILE): cv.string, - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_TIMEOUT, default=8): cv.positive_int, -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the LG WebOS TV platform.""" - if discovery_info is not None: - host = urlparse(discovery_info[1]).hostname - else: - host = config.get(CONF_HOST) - - if host is None: - _LOGGER.error("No TV found in configuration file or with discovery") - return False - - # Only act if we are not already configuring this host - if host in _CONFIGURING: - return - - name = config.get(CONF_NAME) - customize = config.get(CONF_CUSTOMIZE) - timeout = config.get(CONF_TIMEOUT) - turn_on_action = config.get(CONF_ON_ACTION) - - config = hass.config.path(config.get(CONF_FILENAME)) - - setup_tv(host, name, customize, config, timeout, hass, - add_entities, turn_on_action) - - -def setup_tv(host, name, customize, config, timeout, hass, - add_entities, turn_on_action): - """Set up a LG WebOS TV based on host parameter.""" - from pylgtv import WebOsClient - from pylgtv import PyLGTVPairException - from websockets.exceptions import ConnectionClosed - - client = WebOsClient(host, config, timeout) - - if not client.is_registered(): - if host in _CONFIGURING: - # Try to pair. - try: - client.register() - except PyLGTVPairException: - _LOGGER.warning( - "Connected to LG webOS TV %s but not paired", host) - return - except (OSError, ConnectionClosed, asyncio.TimeoutError): - _LOGGER.error("Unable to connect to host %s", host) - return - else: - # Not registered, request configuration. - _LOGGER.warning("LG webOS TV %s needs to be paired", host) - request_configuration( - host, name, customize, config, timeout, hass, - add_entities, turn_on_action) - return - - # If we came here and configuring this host, mark as done. - if client.is_registered() and host in _CONFIGURING: - request_id = _CONFIGURING.pop(host) - configurator = hass.components.configurator - configurator.request_done(request_id) - - add_entities([LgWebOSDevice(host, name, customize, config, timeout, - hass, turn_on_action)], True) - - -def request_configuration( - host, name, customize, config, timeout, hass, - add_entities, turn_on_action): - """Request configuration steps from the user.""" - configurator = hass.components.configurator - - # We got an error if this method is called while we are configuring - if host in _CONFIGURING: - configurator.notify_errors( - _CONFIGURING[host], 'Failed to pair, please try again.') - return - - def lgtv_configuration_callback(data): - """Handle actions when configuration callback is called.""" - setup_tv(host, name, customize, config, timeout, hass, - add_entities, turn_on_action) - - _CONFIGURING[host] = configurator.request_config( - name, lgtv_configuration_callback, - description='Click start and accept the pairing request on your TV.', - description_image='/static/images/config_webos.png', - submit_caption='Start pairing request' - ) - - -class LgWebOSDevice(MediaPlayerDevice): - """Representation of a LG WebOS TV.""" - - def __init__(self, host, name, customize, config, timeout, - hass, on_action): - """Initialize the webos device.""" - from pylgtv import WebOsClient - self._client = WebOsClient(host, config, timeout) - self._on_script = Script(hass, on_action) if on_action else None - self._customize = customize - - self._name = name - # Assume that the TV is not muted - self._muted = False - # Assume that the TV is in Play mode - self._playing = True - self._volume = 0 - self._current_source = None - self._current_source_id = None - self._state = None - self._source_list = {} - self._app_list = {} - self._channel = None - - @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) - def update(self): - """Retrieve the latest data.""" - from websockets.exceptions import ConnectionClosed - try: - current_input = self._client.get_input() - if current_input is not None: - self._current_source_id = current_input - if self._state in (None, STATE_OFF): - self._state = STATE_PLAYING - else: - self._state = STATE_OFF - self._current_source = None - self._current_source_id = None - self._channel = None - - if self._state is not STATE_OFF: - self._muted = self._client.get_muted() - self._volume = self._client.get_volume() - self._channel = self._client.get_current_channel() - - self._source_list = {} - self._app_list = {} - conf_sources = self._customize.get(CONF_SOURCES, []) - - for app in self._client.get_apps(): - self._app_list[app['id']] = app - if app['id'] == self._current_source_id: - self._current_source = app['title'] - self._source_list[app['title']] = app - elif (not conf_sources or - app['id'] in conf_sources or - any(word in app['title'] - for word in conf_sources) or - any(word in app['id'] - for word in conf_sources)): - self._source_list[app['title']] = app - - for source in self._client.get_inputs(): - if source['id'] == self._current_source_id: - self._current_source = source['label'] - self._source_list[source['label']] = source - elif (not conf_sources or - source['label'] in conf_sources or - any(source['label'].find(word) != -1 - for word in conf_sources)): - self._source_list[source['label']] = source - except (OSError, ConnectionClosed, TypeError, - asyncio.TimeoutError): - self._state = STATE_OFF - self._current_source = None - self._current_source_id = None - self._channel = None - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def state(self): - """Return the state of the device.""" - return self._state - - @property - def is_volume_muted(self): - """Boolean if volume is currently muted.""" - return self._muted - - @property - def volume_level(self): - """Volume level of the media player (0..1).""" - return self._volume / 100.0 - - @property - def source(self): - """Return the current input source.""" - return self._current_source - - @property - def source_list(self): - """List of available input sources.""" - return sorted(self._source_list.keys()) - - @property - def media_content_type(self): - """Content type of current playing media.""" - return MEDIA_TYPE_CHANNEL - - @property - def media_title(self): - """Title of current playing media.""" - if (self._channel is not None) and ('channelName' in self._channel): - return self._channel['channelName'] - return None - - @property - def media_image_url(self): - """Image url of current playing media.""" - if self._current_source_id in self._app_list: - icon = self._app_list[self._current_source_id]['largeIcon'] - if not icon.startswith('http'): - icon = self._app_list[self._current_source_id]['icon'] - return icon - return None - - @property - def supported_features(self): - """Flag media player features that are supported.""" - if self._on_script: - return SUPPORT_WEBOSTV | SUPPORT_TURN_ON - return SUPPORT_WEBOSTV - - def turn_off(self): - """Turn off media player.""" - from websockets.exceptions import ConnectionClosed - self._state = STATE_OFF - try: - self._client.power_off() - except (OSError, ConnectionClosed, TypeError, - asyncio.TimeoutError): - pass - - def turn_on(self): - """Turn on the media player.""" - if self._on_script: - self._on_script.run() - - def volume_up(self): - """Volume up the media player.""" - self._client.volume_up() - - def volume_down(self): - """Volume down media player.""" - self._client.volume_down() - - def set_volume_level(self, volume): - """Set volume level, range 0..1.""" - tv_volume = volume * 100 - self._client.set_volume(tv_volume) - - def mute_volume(self, mute): - """Send mute command.""" - self._muted = mute - self._client.set_mute(mute) - - def media_play_pause(self): - """Simulate play pause media player.""" - if self._playing: - self.media_pause() - else: - self.media_play() - - def select_source(self, source): - """Select input source.""" - source_dict = self._source_list.get(source) - if source_dict is None: - _LOGGER.warning("Source %s not found for %s", source, self.name) - return - self._current_source_id = source_dict['id'] - if source_dict.get('title'): - self._current_source = source_dict['title'] - self._client.launch_app(source_dict['id']) - elif source_dict.get('label'): - self._current_source = source_dict['label'] - self._client.set_input(source_dict['id']) - - def play_media(self, media_type, media_id, **kwargs): - """Play a piece of media.""" - _LOGGER.debug( - "Call play media type <%s>, Id <%s>", media_type, media_id) - - if media_type == MEDIA_TYPE_CHANNEL: - _LOGGER.debug("Searching channel...") - partial_match_channel_id = None - perfect_match_channel_id = None - - for channel in self._client.get_channels(): - if media_id == channel['channelNumber']: - perfect_match_channel_id = channel['channelId'] - continue - elif media_id.lower() == channel['channelName'].lower(): - perfect_match_channel_id = channel['channelId'] - continue - elif media_id.lower() in channel['channelName'].lower(): - partial_match_channel_id = channel['channelId'] - - if perfect_match_channel_id is not None: - _LOGGER.info( - "Switching to channel <%s> with perfect match", - perfect_match_channel_id) - self._client.set_channel(perfect_match_channel_id) - elif partial_match_channel_id is not None: - _LOGGER.info( - "Switching to channel <%s> with partial match", - partial_match_channel_id) - self._client.set_channel(partial_match_channel_id) - - return - - def media_play(self): - """Send play command.""" - self._playing = True - self._state = STATE_PLAYING - self._client.play() - - def media_pause(self): - """Send media pause command to media player.""" - self._playing = False - self._state = STATE_PAUSED - self._client.pause() - - def media_next_track(self): - """Send next track command.""" - current_input = self._client.get_input() - if current_input == LIVETV_APP_ID: - self._client.channel_up() - else: - self._client.fast_forward() - - def media_previous_track(self): - """Send the previous track command.""" - current_input = self._client.get_input() - if current_input == LIVETV_APP_ID: - self._client.channel_down() - else: - self._client.rewind() diff --git a/homeassistant/components/media_player/xiaomi_tv.py b/homeassistant/components/media_player/xiaomi_tv.py index 09d36f82db07a..e3b25c3c31f80 100644 --- a/homeassistant/components/media_player/xiaomi_tv.py +++ b/homeassistant/components/media_player/xiaomi_tv.py @@ -9,8 +9,9 @@ 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 0bb34aee7e190..f652d95e713af 100644 --- a/homeassistant/components/media_player/yamaha.py +++ b/homeassistant/components/media_player/yamaha.py @@ -10,18 +10,20 @@ 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 535a2ad01ca5d..6aa06b604c5d1 100644 --- a/homeassistant/components/media_player/yamaha_musiccast.py +++ b/homeassistant/components/media_player/yamaha_musiccast.py @@ -9,10 +9,11 @@ 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 57ef69c923e05..abad22d89eb5b 100644 --- a/homeassistant/components/media_player/ziggo_mediabox_xl.py +++ b/homeassistant/components/media_player/ziggo_mediabox_xl.py @@ -10,9 +10,11 @@ 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.py deleted file mode 100644 index 638d8c55bd565..0000000000000 --- a/homeassistant/components/melissa.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -Support for Melissa climate. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/melissa/ -""" -import logging - -import voluptuous as vol - -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.discovery import async_load_platform - -REQUIREMENTS = ["py-melissa-climate==2.0.0"] - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "melissa" -DATA_MELISSA = 'MELISSA' - - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - }), -}, extra=vol.ALLOW_EXTRA) - - -async def async_setup(hass, config): - """Set up the Melissa Climate component.""" - import melissa - - conf = config[DOMAIN] - username = conf.get(CONF_USERNAME) - password = conf.get(CONF_PASSWORD) - api = melissa.AsyncMelissa(username=username, password=password) - await api.async_connect() - hass.data[DATA_MELISSA] = api - - hass.async_create_task( - async_load_platform(hass, 'climate', DOMAIN, {}, config)) - return True diff --git a/homeassistant/components/melissa/__init__.py b/homeassistant/components/melissa/__init__.py new file mode 100644 index 0000000000000..2037caa11c334 --- /dev/null +++ b/homeassistant/components/melissa/__init__.py @@ -0,0 +1,39 @@ +"""Support for Melissa climate.""" +import logging + +import voluptuous as vol + +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.discovery import async_load_platform + +REQUIREMENTS = ["py-melissa-climate==2.0.0"] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'melissa' +DATA_MELISSA = 'MELISSA' + + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the Melissa Climate component.""" + import melissa + + conf = config[DOMAIN] + username = conf.get(CONF_USERNAME) + password = conf.get(CONF_PASSWORD) + api = melissa.AsyncMelissa(username=username, password=password) + await api.async_connect() + hass.data[DATA_MELISSA] = api + + hass.async_create_task( + async_load_platform(hass, 'climate', DOMAIN, {}, config)) + return True diff --git a/homeassistant/components/microsoft_face.py b/homeassistant/components/microsoft_face.py deleted file mode 100644 index 9be2f8eadf590..0000000000000 --- a/homeassistant/components/microsoft_face.py +++ /dev/null @@ -1,326 +0,0 @@ -""" -Support for Microsoft face recognition. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/microsoft_face/ -""" -import asyncio -import json -import logging - -import aiohttp -from aiohttp.hdrs import CONTENT_TYPE -import async_timeout -import voluptuous as vol - -from homeassistant.const import CONF_API_KEY, CONF_TIMEOUT, ATTR_NAME -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity -from homeassistant.util import slugify - -_LOGGER = logging.getLogger(__name__) - -ATTR_CAMERA_ENTITY = 'camera_entity' -ATTR_GROUP = 'group' -ATTR_PERSON = 'person' - -CONF_AZURE_REGION = 'azure_region' - -DATA_MICROSOFT_FACE = 'microsoft_face' -DEFAULT_TIMEOUT = 10 -DEPENDENCIES = ['camera'] -DOMAIN = 'microsoft_face' - -FACE_API_URL = "api.cognitive.microsoft.com/face/v1.0/{0}" - -SERVICE_CREATE_GROUP = 'create_group' -SERVICE_CREATE_PERSON = 'create_person' -SERVICE_DELETE_GROUP = 'delete_group' -SERVICE_DELETE_PERSON = 'delete_person' -SERVICE_FACE_PERSON = 'face_person' -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_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - }), -}, extra=vol.ALLOW_EXTRA) - -SCHEMA_GROUP_SERVICE = vol.Schema({ - vol.Required(ATTR_NAME): cv.string, -}) - -SCHEMA_PERSON_SERVICE = SCHEMA_GROUP_SERVICE.extend({ - vol.Required(ATTR_GROUP): cv.slugify, -}) - -SCHEMA_FACE_SERVICE = vol.Schema({ - vol.Required(ATTR_PERSON): cv.string, - vol.Required(ATTR_GROUP): cv.slugify, - vol.Required(ATTR_CAMERA_ENTITY): cv.entity_id, -}) - -SCHEMA_TRAIN_SERVICE = vol.Schema({ - vol.Required(ATTR_GROUP): cv.slugify, -}) - - -async def async_setup(hass, config): - """Set up Microsoft Face.""" - entities = {} - face = MicrosoftFace( - hass, - config[DOMAIN].get(CONF_AZURE_REGION), - config[DOMAIN].get(CONF_API_KEY), - config[DOMAIN].get(CONF_TIMEOUT), - entities - ) - - try: - # read exists group/person from cloud and create entities - await face.update_store() - except HomeAssistantError as err: - _LOGGER.error("Can't load data from face api: %s", err) - return False - - hass.data[DATA_MICROSOFT_FACE] = face - - async def async_create_group(service): - """Create a new person group.""" - name = service.data[ATTR_NAME] - g_id = slugify(name) - - try: - await face.call_api( - 'put', "persongroups/{0}".format(g_id), {'name': name}) - face.store[g_id] = {} - - entities[g_id] = MicrosoftFaceGroupEntity(hass, face, g_id, name) - await entities[g_id].async_update_ha_state() - except HomeAssistantError as err: - _LOGGER.error("Can't create group '%s' with error: %s", g_id, err) - - hass.services.async_register( - DOMAIN, SERVICE_CREATE_GROUP, async_create_group, - schema=SCHEMA_GROUP_SERVICE) - - async def async_delete_group(service): - """Delete a person group.""" - g_id = slugify(service.data[ATTR_NAME]) - - try: - await face.call_api('delete', "persongroups/{0}".format(g_id)) - face.store.pop(g_id) - - entity = entities.pop(g_id) - hass.states.async_remove(entity.entity_id) - except HomeAssistantError as err: - _LOGGER.error("Can't delete group '%s' with error: %s", g_id, err) - - hass.services.async_register( - DOMAIN, SERVICE_DELETE_GROUP, async_delete_group, - schema=SCHEMA_GROUP_SERVICE) - - async def async_train_group(service): - """Train a person group.""" - g_id = service.data[ATTR_GROUP] - - try: - await face.call_api( - 'post', "persongroups/{0}/train".format(g_id)) - except HomeAssistantError as err: - _LOGGER.error("Can't train group '%s' with error: %s", g_id, err) - - hass.services.async_register( - DOMAIN, SERVICE_TRAIN_GROUP, async_train_group, - schema=SCHEMA_TRAIN_SERVICE) - - async def async_create_person(service): - """Create a person in a group.""" - name = service.data[ATTR_NAME] - g_id = service.data[ATTR_GROUP] - - try: - user_data = await face.call_api( - 'post', "persongroups/{0}/persons".format(g_id), {'name': name} - ) - - face.store[g_id][name] = user_data['personId'] - await entities[g_id].async_update_ha_state() - except HomeAssistantError as err: - _LOGGER.error("Can't create person '%s' with error: %s", name, err) - - hass.services.async_register( - DOMAIN, SERVICE_CREATE_PERSON, async_create_person, - schema=SCHEMA_PERSON_SERVICE) - - async def async_delete_person(service): - """Delete a person in a group.""" - name = service.data[ATTR_NAME] - g_id = service.data[ATTR_GROUP] - p_id = face.store[g_id].get(name) - - try: - await face.call_api( - 'delete', "persongroups/{0}/persons/{1}".format(g_id, p_id)) - - face.store[g_id].pop(name) - await entities[g_id].async_update_ha_state() - except HomeAssistantError as err: - _LOGGER.error("Can't delete person '%s' with error: %s", p_id, err) - - hass.services.async_register( - DOMAIN, SERVICE_DELETE_PERSON, async_delete_person, - schema=SCHEMA_PERSON_SERVICE) - - async def async_face_person(service): - """Add a new face picture to a person.""" - g_id = service.data[ATTR_GROUP] - p_id = face.store[g_id].get(service.data[ATTR_PERSON]) - - camera_entity = service.data[ATTR_CAMERA_ENTITY] - camera = hass.components.camera - - try: - image = await camera.async_get_image(hass, camera_entity) - - await face.call_api( - 'post', - "persongroups/{0}/persons/{1}/persistedFaces".format( - g_id, p_id), - image.content, - binary=True - ) - except HomeAssistantError as err: - _LOGGER.error("Can't delete person '%s' with error: %s", p_id, err) - - hass.services.async_register( - DOMAIN, SERVICE_FACE_PERSON, async_face_person, - schema=SCHEMA_FACE_SERVICE) - - return True - - -class MicrosoftFaceGroupEntity(Entity): - """Person-Group state/data Entity.""" - - def __init__(self, hass, api, g_id, name): - """Initialize person/group entity.""" - self.hass = hass - self._api = api - self._id = g_id - self._name = name - - @property - def name(self): - """Return the name of the entity.""" - return self._name - - @property - def entity_id(self): - """Return entity id.""" - return "{0}.{1}".format(DOMAIN, self._id) - - @property - def state(self): - """Return the state of the entity.""" - return len(self._api.store[self._id]) - - @property - def should_poll(self): - """Return True if entity has to be polled for state.""" - return False - - @property - def device_state_attributes(self): - """Return device specific state attributes.""" - attr = {} - for name, p_id in self._api.store[self._id].items(): - attr[name] = p_id - - return attr - - -class MicrosoftFace: - """Microsoft Face api for HomeAssistant.""" - - def __init__(self, hass, server_loc, api_key, timeout, entities): - """Initialize Microsoft Face api.""" - self.hass = hass - self.websession = async_get_clientsession(hass) - self.timeout = timeout - self._api_key = api_key - self._server_url = "https://{0}.{1}".format(server_loc, FACE_API_URL) - self._store = {} - self._entities = entities - - @property - def store(self): - """Store group/person data and IDs.""" - return self._store - - async def update_store(self): - """Load all group/person data into local store.""" - groups = await self.call_api('get', 'persongroups') - - tasks = [] - for group in groups: - g_id = group['personGroupId'] - self._store[g_id] = {} - self._entities[g_id] = MicrosoftFaceGroupEntity( - self.hass, self, g_id, group['name']) - - persons = await self.call_api( - 'get', "persongroups/{0}/persons".format(g_id)) - - for person in persons: - self._store[g_id][person['name']] = person['personId'] - - tasks.append(self._entities[g_id].async_update_ha_state()) - - if tasks: - await asyncio.wait(tasks, loop=self.hass.loop) - - async def call_api(self, method, function, data=None, binary=False, - params=None): - """Make an api call.""" - headers = {"Ocp-Apim-Subscription-Key": self._api_key} - url = self._server_url.format(function) - - payload = None - if binary: - headers[CONTENT_TYPE] = "application/octet-stream" - payload = data - else: - headers[CONTENT_TYPE] = "application/json" - if data is not None: - payload = json.dumps(data).encode() - else: - payload = None - - try: - with async_timeout.timeout(self.timeout, loop=self.hass.loop): - response = await getattr(self.websession, method)( - url, data=payload, headers=headers, params=params) - - answer = await response.json() - - _LOGGER.debug("Read from microsoft face api: %s", answer) - if response.status < 300: - return answer - - _LOGGER.warning("Error %d microsoft face api %s", - response.status, response.url) - raise HomeAssistantError(answer['error']['message']) - - except aiohttp.ClientError: - _LOGGER.warning("Can't connect to microsoft face api") - - except asyncio.TimeoutError: - _LOGGER.warning("Timeout from microsoft face api %s", response.url) - - raise HomeAssistantError("Network error on microsoft face api.") diff --git a/homeassistant/components/microsoft_face/__init__.py b/homeassistant/components/microsoft_face/__init__.py new file mode 100644 index 0000000000000..9b3ee960fb23c --- /dev/null +++ b/homeassistant/components/microsoft_face/__init__.py @@ -0,0 +1,321 @@ +"""Support for Microsoft face recognition.""" +import asyncio +import json +import logging + +import aiohttp +from aiohttp.hdrs import CONTENT_TYPE +import async_timeout +import voluptuous as vol + +from homeassistant.const import CONF_API_KEY, CONF_TIMEOUT, ATTR_NAME +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.util import slugify + +_LOGGER = logging.getLogger(__name__) + +ATTR_CAMERA_ENTITY = 'camera_entity' +ATTR_GROUP = 'group' +ATTR_PERSON = 'person' + +CONF_AZURE_REGION = 'azure_region' + +DATA_MICROSOFT_FACE = 'microsoft_face' +DEFAULT_TIMEOUT = 10 +DEPENDENCIES = ['camera'] +DOMAIN = 'microsoft_face' + +FACE_API_URL = "api.cognitive.microsoft.com/face/v1.0/{0}" + +SERVICE_CREATE_GROUP = 'create_group' +SERVICE_CREATE_PERSON = 'create_person' +SERVICE_DELETE_GROUP = 'delete_group' +SERVICE_DELETE_PERSON = 'delete_person' +SERVICE_FACE_PERSON = 'face_person' +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_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + }), +}, extra=vol.ALLOW_EXTRA) + +SCHEMA_GROUP_SERVICE = vol.Schema({ + vol.Required(ATTR_NAME): cv.string, +}) + +SCHEMA_PERSON_SERVICE = SCHEMA_GROUP_SERVICE.extend({ + vol.Required(ATTR_GROUP): cv.slugify, +}) + +SCHEMA_FACE_SERVICE = vol.Schema({ + vol.Required(ATTR_PERSON): cv.string, + vol.Required(ATTR_GROUP): cv.slugify, + vol.Required(ATTR_CAMERA_ENTITY): cv.entity_id, +}) + +SCHEMA_TRAIN_SERVICE = vol.Schema({ + vol.Required(ATTR_GROUP): cv.slugify, +}) + + +async def async_setup(hass, config): + """Set up Microsoft Face.""" + entities = {} + face = MicrosoftFace( + hass, + config[DOMAIN].get(CONF_AZURE_REGION), + config[DOMAIN].get(CONF_API_KEY), + config[DOMAIN].get(CONF_TIMEOUT), + entities + ) + + try: + # read exists group/person from cloud and create entities + await face.update_store() + except HomeAssistantError as err: + _LOGGER.error("Can't load data from face api: %s", err) + return False + + hass.data[DATA_MICROSOFT_FACE] = face + + async def async_create_group(service): + """Create a new person group.""" + name = service.data[ATTR_NAME] + g_id = slugify(name) + + try: + await face.call_api( + 'put', "persongroups/{0}".format(g_id), {'name': name}) + face.store[g_id] = {} + + entities[g_id] = MicrosoftFaceGroupEntity(hass, face, g_id, name) + await entities[g_id].async_update_ha_state() + except HomeAssistantError as err: + _LOGGER.error("Can't create group '%s' with error: %s", g_id, err) + + hass.services.async_register( + DOMAIN, SERVICE_CREATE_GROUP, async_create_group, + schema=SCHEMA_GROUP_SERVICE) + + async def async_delete_group(service): + """Delete a person group.""" + g_id = slugify(service.data[ATTR_NAME]) + + try: + await face.call_api('delete', "persongroups/{0}".format(g_id)) + face.store.pop(g_id) + + entity = entities.pop(g_id) + hass.states.async_remove(entity.entity_id) + except HomeAssistantError as err: + _LOGGER.error("Can't delete group '%s' with error: %s", g_id, err) + + hass.services.async_register( + DOMAIN, SERVICE_DELETE_GROUP, async_delete_group, + schema=SCHEMA_GROUP_SERVICE) + + async def async_train_group(service): + """Train a person group.""" + g_id = service.data[ATTR_GROUP] + + try: + await face.call_api( + 'post', "persongroups/{0}/train".format(g_id)) + except HomeAssistantError as err: + _LOGGER.error("Can't train group '%s' with error: %s", g_id, err) + + hass.services.async_register( + DOMAIN, SERVICE_TRAIN_GROUP, async_train_group, + schema=SCHEMA_TRAIN_SERVICE) + + async def async_create_person(service): + """Create a person in a group.""" + name = service.data[ATTR_NAME] + g_id = service.data[ATTR_GROUP] + + try: + user_data = await face.call_api( + 'post', "persongroups/{0}/persons".format(g_id), {'name': name} + ) + + face.store[g_id][name] = user_data['personId'] + await entities[g_id].async_update_ha_state() + except HomeAssistantError as err: + _LOGGER.error("Can't create person '%s' with error: %s", name, err) + + hass.services.async_register( + DOMAIN, SERVICE_CREATE_PERSON, async_create_person, + schema=SCHEMA_PERSON_SERVICE) + + async def async_delete_person(service): + """Delete a person in a group.""" + name = service.data[ATTR_NAME] + g_id = service.data[ATTR_GROUP] + p_id = face.store[g_id].get(name) + + try: + await face.call_api( + 'delete', "persongroups/{0}/persons/{1}".format(g_id, p_id)) + + face.store[g_id].pop(name) + await entities[g_id].async_update_ha_state() + except HomeAssistantError as err: + _LOGGER.error("Can't delete person '%s' with error: %s", p_id, err) + + hass.services.async_register( + DOMAIN, SERVICE_DELETE_PERSON, async_delete_person, + schema=SCHEMA_PERSON_SERVICE) + + async def async_face_person(service): + """Add a new face picture to a person.""" + g_id = service.data[ATTR_GROUP] + p_id = face.store[g_id].get(service.data[ATTR_PERSON]) + + camera_entity = service.data[ATTR_CAMERA_ENTITY] + camera = hass.components.camera + + try: + image = await camera.async_get_image(hass, camera_entity) + + await face.call_api( + 'post', + "persongroups/{0}/persons/{1}/persistedFaces".format( + g_id, p_id), + image.content, + binary=True + ) + except HomeAssistantError as err: + _LOGGER.error("Can't delete person '%s' with error: %s", p_id, err) + + hass.services.async_register( + DOMAIN, SERVICE_FACE_PERSON, async_face_person, + schema=SCHEMA_FACE_SERVICE) + + return True + + +class MicrosoftFaceGroupEntity(Entity): + """Person-Group state/data Entity.""" + + def __init__(self, hass, api, g_id, name): + """Initialize person/group entity.""" + self.hass = hass + self._api = api + self._id = g_id + self._name = name + + @property + def name(self): + """Return the name of the entity.""" + return self._name + + @property + def entity_id(self): + """Return entity id.""" + return "{0}.{1}".format(DOMAIN, self._id) + + @property + def state(self): + """Return the state of the entity.""" + return len(self._api.store[self._id]) + + @property + def should_poll(self): + """Return True if entity has to be polled for state.""" + return False + + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + attr = {} + for name, p_id in self._api.store[self._id].items(): + attr[name] = p_id + + return attr + + +class MicrosoftFace: + """Microsoft Face api for HomeAssistant.""" + + def __init__(self, hass, server_loc, api_key, timeout, entities): + """Initialize Microsoft Face api.""" + self.hass = hass + self.websession = async_get_clientsession(hass) + self.timeout = timeout + self._api_key = api_key + self._server_url = "https://{0}.{1}".format(server_loc, FACE_API_URL) + self._store = {} + self._entities = entities + + @property + def store(self): + """Store group/person data and IDs.""" + return self._store + + async def update_store(self): + """Load all group/person data into local store.""" + groups = await self.call_api('get', 'persongroups') + + tasks = [] + for group in groups: + g_id = group['personGroupId'] + self._store[g_id] = {} + self._entities[g_id] = MicrosoftFaceGroupEntity( + self.hass, self, g_id, group['name']) + + persons = await self.call_api( + 'get', "persongroups/{0}/persons".format(g_id)) + + for person in persons: + self._store[g_id][person['name']] = person['personId'] + + tasks.append(self._entities[g_id].async_update_ha_state()) + + if tasks: + await asyncio.wait(tasks, loop=self.hass.loop) + + async def call_api(self, method, function, data=None, binary=False, + params=None): + """Make an api call.""" + headers = {"Ocp-Apim-Subscription-Key": self._api_key} + url = self._server_url.format(function) + + payload = None + if binary: + headers[CONTENT_TYPE] = "application/octet-stream" + payload = data + else: + headers[CONTENT_TYPE] = "application/json" + if data is not None: + payload = json.dumps(data).encode() + else: + payload = None + + try: + with async_timeout.timeout(self.timeout, loop=self.hass.loop): + response = await getattr(self.websession, method)( + url, data=payload, headers=headers, params=params) + + answer = await response.json() + + _LOGGER.debug("Read from microsoft face api: %s", answer) + if response.status < 300: + return answer + + _LOGGER.warning("Error %d microsoft face api %s", + response.status, response.url) + raise HomeAssistantError(answer['error']['message']) + + except aiohttp.ClientError: + _LOGGER.warning("Can't connect to microsoft face api") + + except asyncio.TimeoutError: + _LOGGER.warning("Timeout from microsoft face api %s", response.url) + + raise HomeAssistantError("Network error on microsoft face api.") diff --git a/homeassistant/components/microsoft_face/services.yaml b/homeassistant/components/microsoft_face/services.yaml new file mode 100644 index 0000000000000..386f7083f92a9 --- /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.py deleted file mode 100644 index 7e6738b95f8c2..0000000000000 --- a/homeassistant/components/mochad.py +++ /dev/null @@ -1,89 +0,0 @@ -""" -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/ -""" -import logging -import threading - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) -from homeassistant.const import (CONF_HOST, CONF_PORT) - -REQUIREMENTS = ['pymochad==0.2.0'] - -_LOGGER = logging.getLogger(__name__) - -CONTROLLER = None - -CONF_COMM_TYPE = 'comm_type' - -DOMAIN = 'mochad' - -REQ_LOCK = threading.Lock() - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(CONF_HOST, default='localhost'): cv.string, - vol.Optional(CONF_PORT, default=1099): cv.port, - }) -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the mochad component.""" - conf = config[DOMAIN] - host = conf.get(CONF_HOST) - port = conf.get(CONF_PORT) - - from pymochad import exceptions - - global CONTROLLER - try: - CONTROLLER = MochadCtrl(host, port) - except exceptions.ConfigurationError: - _LOGGER.exception() - return False - - def stop_mochad(event): - """Stop the Mochad service.""" - CONTROLLER.disconnect() - - def start_mochad(event): - """Start the Mochad service.""" - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_mochad) - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_mochad) - - return True - - -class MochadCtrl: - """Mochad controller.""" - - def __init__(self, host, port): - """Initialize a PyMochad controller.""" - super(MochadCtrl, self).__init__() - self._host = host - self._port = port - - from pymochad import controller - - self.ctrl = controller.PyMochad(server=self._host, port=self._port) - - @property - def host(self): - """Return the server where mochad is running.""" - return self._host - - @property - def port(self): - """Return the port mochad is running on.""" - return self._port - - def disconnect(self): - """Close the connection to the mochad socket.""" - self.ctrl.socket.close() diff --git a/homeassistant/components/mochad/__init__.py b/homeassistant/components/mochad/__init__.py new file mode 100644 index 0000000000000..e10adf693fe70 --- /dev/null +++ b/homeassistant/components/mochad/__init__.py @@ -0,0 +1,84 @@ +"""Support for CM15A/CM19A X10 Controller using mochad daemon.""" +import logging +import threading + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) +from homeassistant.const import (CONF_HOST, CONF_PORT) + +REQUIREMENTS = ['pymochad==0.2.0'] + +_LOGGER = logging.getLogger(__name__) + +CONTROLLER = None + +CONF_COMM_TYPE = 'comm_type' + +DOMAIN = 'mochad' + +REQ_LOCK = threading.Lock() + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Optional(CONF_HOST, default='localhost'): cv.string, + vol.Optional(CONF_PORT, default=1099): cv.port, + }) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the mochad component.""" + conf = config[DOMAIN] + host = conf.get(CONF_HOST) + port = conf.get(CONF_PORT) + + from pymochad import exceptions + + global CONTROLLER + try: + CONTROLLER = MochadCtrl(host, port) + except exceptions.ConfigurationError: + _LOGGER.exception() + return False + + def stop_mochad(event): + """Stop the Mochad service.""" + CONTROLLER.disconnect() + + def start_mochad(event): + """Start the Mochad service.""" + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_mochad) + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_mochad) + + return True + + +class MochadCtrl: + """Mochad controller.""" + + def __init__(self, host, port): + """Initialize a PyMochad controller.""" + super(MochadCtrl, self).__init__() + self._host = host + self._port = port + + from pymochad import controller + + self.ctrl = controller.PyMochad(server=self._host, port=self._port) + + @property + def host(self): + """Return the server where mochad is running.""" + return self._host + + @property + def port(self): + """Return the port mochad is running on.""" + return self._port + + def disconnect(self): + """Close the connection to the mochad socket.""" + self.ctrl.socket.close() diff --git a/homeassistant/components/mochad/light.py b/homeassistant/components/mochad/light.py new file mode 100644 index 0000000000000..d2e1a567d2750 --- /dev/null +++ b/homeassistant/components/mochad/light.py @@ -0,0 +1,131 @@ +"""Support for X10 dimmer over Mochad.""" +import logging + +import voluptuous as vol + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light, PLATFORM_SCHEMA) +from homeassistant.components import mochad +from homeassistant.const import ( + CONF_NAME, CONF_PLATFORM, CONF_DEVICES, CONF_ADDRESS) +from homeassistant.helpers import config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['mochad'] + +CONF_BRIGHTNESS_LEVELS = 'brightness_levels' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_PLATFORM): mochad.DOMAIN, + CONF_DEVICES: [{ + vol.Optional(CONF_NAME): cv.string, + vol.Required(CONF_ADDRESS): cv.x10_address, + vol.Optional(mochad.CONF_COMM_TYPE): cv.string, + vol.Optional(CONF_BRIGHTNESS_LEVELS, default=32): + vol.All(vol.Coerce(int), vol.In([32, 64, 256])), + }] +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up X10 dimmers over a mochad controller.""" + devs = config.get(CONF_DEVICES) + add_entities([MochadLight( + hass, mochad.CONTROLLER.ctrl, dev) for dev in devs]) + return True + + +class MochadLight(Light): + """Representation of a X10 dimmer over Mochad.""" + + def __init__(self, hass, ctrl, dev): + """Initialize a Mochad Light Device.""" + from pymochad import device + + self._controller = ctrl + self._address = dev[CONF_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._brightness = 0 + self._state = self._get_device_status() + self._brightness_levels = dev.get(CONF_BRIGHTNESS_LEVELS) - 1 + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + return self._brightness + + def _get_device_status(self): + """Get the status of the light from mochad.""" + with mochad.REQ_LOCK: + status = self.light.get_status().rstrip() + return status == 'on' + + @property + def name(self): + """Return the display name of this light.""" + return self._name + + @property + def is_on(self): + """Return true if the light is on.""" + return self._state + + @property + def supported_features(self): + """Return supported features.""" + return SUPPORT_BRIGHTNESS + + @property + def assumed_state(self): + """X10 devices are normally 1-way so we have to assume the state.""" + return True + + def _calculate_brightness_value(self, value): + return int(value * (float(self._brightness_levels) / 255.0)) + + def _adjust_brightness(self, brightness): + if self._brightness > brightness: + bdelta = self._brightness - brightness + mochad_brightness = self._calculate_brightness_value(bdelta) + self.light.send_cmd("dim {}".format(mochad_brightness)) + self._controller.read_data() + elif self._brightness < brightness: + bdelta = brightness - self._brightness + mochad_brightness = self._calculate_brightness_value(bdelta) + self.light.send_cmd("bright {}".format(mochad_brightness)) + self._controller.read_data() + + def turn_on(self, **kwargs): + """Send the command to turn the light on.""" + brightness = kwargs.get(ATTR_BRIGHTNESS, 255) + with mochad.REQ_LOCK: + if self._brightness_levels > 32: + out_brightness = self._calculate_brightness_value(brightness) + self.light.send_cmd('xdim {}'.format(out_brightness)) + self._controller.read_data() + else: + self.light.send_cmd("on") + self._controller.read_data() + # There is no persistence for X10 modules so a fresh on command + # will be full brightness + if self._brightness == 0: + self._brightness = 255 + self._adjust_brightness(brightness) + self._brightness = brightness + self._state = True + + def turn_off(self, **kwargs): + """Send the command to turn the light on.""" + with mochad.REQ_LOCK: + self.light.send_cmd('off') + self._controller.read_data() + # There is no persistence for X10 modules so we need to prepare + # to track a fresh on command will full brightness + if self._brightness_levels == 31: + self._brightness = 0 + self._state = False diff --git a/homeassistant/components/mochad/switch.py b/homeassistant/components/mochad/switch.py new file mode 100644 index 0000000000000..03fd2db07bf2f --- /dev/null +++ b/homeassistant/components/mochad/switch.py @@ -0,0 +1,103 @@ +"""Support for X10 switch over Mochad.""" +import logging + +import voluptuous as vol + +from homeassistant.components import mochad +from homeassistant.components.switch import SwitchDevice +from homeassistant.const import (CONF_NAME, CONF_DEVICES, + CONF_PLATFORM, CONF_ADDRESS) +from homeassistant.helpers import config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['mochad'] + + +PLATFORM_SCHEMA = vol.Schema({ + vol.Required(CONF_PLATFORM): mochad.DOMAIN, + CONF_DEVICES: [{ + vol.Optional(CONF_NAME): cv.string, + vol.Required(CONF_ADDRESS): cv.x10_address, + vol.Optional(mochad.CONF_COMM_TYPE): cv.string, + }] +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up X10 switches over a mochad controller.""" + devs = config.get(CONF_DEVICES) + add_entities([MochadSwitch( + hass, mochad.CONTROLLER.ctrl, dev) for dev in devs]) + return True + + +class MochadSwitch(SwitchDevice): + """Representation of a X10 switch over Mochad.""" + + def __init__(self, hass, ctrl, dev): + """Initialize a Mochad Switch Device.""" + from pymochad import device + + self._controller = ctrl + 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) + # 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': + self._state = self._get_device_status() + else: + self._state = False + + @property + def name(self): + """Get the name of the switch.""" + return self._name + + def turn_on(self, **kwargs): + """Turn the switch on.""" + from pymochad.exceptions import MochadException + _LOGGER.debug("Reconnect %s:%s", self._controller.server, + self._controller.port) + with mochad.REQ_LOCK: + try: + # Recycle socket on new command to recover mochad connection + self._controller.reconnect() + self.switch.send_cmd('on') + # No read data on CM19A which is rf only + if self._comm_type == 'pl': + self._controller.read_data() + self._state = True + except (MochadException, OSError) as exc: + _LOGGER.error("Error with mochad communication: %s", exc) + + def turn_off(self, **kwargs): + """Turn the switch off.""" + from pymochad.exceptions import MochadException + _LOGGER.debug("Reconnect %s:%s", self._controller.server, + self._controller.port) + with mochad.REQ_LOCK: + try: + # Recycle socket on new command to recover mochad connection + self._controller.reconnect() + self.switch.send_cmd('off') + # No read data on CM19A which is rf only + if self._comm_type == 'pl': + self._controller.read_data() + self._state = False + except (MochadException, OSError) as exc: + _LOGGER.error("Error with mochad communication: %s", exc) + + def _get_device_status(self): + """Get the status of the switch from mochad.""" + with mochad.REQ_LOCK: + status = self.switch.get_status().rstrip() + return status == 'on' + + @property + def is_on(self): + """Return true if switch is on.""" + return self._state diff --git a/homeassistant/components/modbus.py b/homeassistant/components/modbus.py deleted file mode 100644 index 40ede019c1015..0000000000000 --- a/homeassistant/components/modbus.py +++ /dev/null @@ -1,230 +0,0 @@ -""" -Support for Modbus. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/modbus/ -""" -import logging -import threading - -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) - -DOMAIN = 'modbus' - -REQUIREMENTS = ['pymodbus==1.5.2'] - -# Type of network -CONF_BAUDRATE = 'baudrate' -CONF_BYTESIZE = 'bytesize' -CONF_STOPBITS = 'stopbits' -CONF_PARITY = 'parity' - -SERIAL_SCHEMA = { - 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'), - vol.Required(CONF_PORT): cv.string, - vol.Required(CONF_PARITY): vol.Any('E', 'O', 'N'), - vol.Required(CONF_STOPBITS): vol.Any(1, 2), - vol.Required(CONF_TYPE): 'serial', - vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout, -} - -ETHERNET_SCHEMA = { - 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) - - -_LOGGER = logging.getLogger(__name__) - -SERVICE_WRITE_REGISTER = 'write_register' -SERVICE_WRITE_COIL = 'write_coil' - -ATTR_ADDRESS = 'address' -ATTR_UNIT = 'unit' -ATTR_VALUE = 'value' - -SERVICE_WRITE_REGISTER_SCHEMA = vol.Schema({ - 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.Required(ATTR_UNIT): cv.positive_int, - vol.Required(ATTR_ADDRESS): cv.positive_int, - vol.Required(ATTR_STATE): cv.boolean -}) - -HUB = None - - -def setup(hass, config): - """Set up Modbus component.""" - # Modbus connection type - client_type = config[DOMAIN][CONF_TYPE] - - # 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) - - def stop_modbus(event): - """Stop Modbus service.""" - HUB.close() - - def start_modbus(event): - """Start Modbus service.""" - HUB.connect() - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus) - - # Register services for modbus - hass.services.register( - DOMAIN, SERVICE_WRITE_REGISTER, write_register, - schema=SERVICE_WRITE_REGISTER_SCHEMA) - hass.services.register( - DOMAIN, SERVICE_WRITE_COIL, write_coil, - schema=SERVICE_WRITE_COIL_SCHEMA) - - def write_register(service): - """Write modbus registers.""" - unit = int(float(service.data.get(ATTR_UNIT))) - address = int(float(service.data.get(ATTR_ADDRESS))) - value = service.data.get(ATTR_VALUE) - if isinstance(value, list): - HUB.write_registers( - unit, - address, - [int(float(i)) for i in value]) - else: - HUB.write_register( - unit, - address, - int(float(value))) - - def write_coil(service): - """Write modbus coil.""" - unit = service.data.get(ATTR_UNIT) - address = service.data.get(ATTR_ADDRESS) - state = service.data.get(ATTR_STATE) - HUB.write_coil(unit, address, state) - - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_modbus) - - return True - - -class ModbusHub: - """Thread safe wrapper class for pymodbus.""" - - def __init__(self, modbus_client): - """Initialize the modbus hub.""" - self._client = modbus_client - self._lock = threading.Lock() - - def close(self): - """Disconnect client.""" - with self._lock: - self._client.close() - - def connect(self): - """Connect client.""" - with self._lock: - self._client.connect() - - def read_coils(self, unit, address, count): - """Read coils.""" - with self._lock: - kwargs = {'unit': unit} if unit else {} - return self._client.read_coils( - address, - count, - **kwargs) - - def read_input_registers(self, unit, address, count): - """Read input registers.""" - with self._lock: - kwargs = {'unit': unit} if unit else {} - return self._client.read_input_registers( - address, - count, - **kwargs) - - def read_holding_registers(self, unit, address, count): - """Read holding registers.""" - with self._lock: - kwargs = {'unit': unit} if unit else {} - return self._client.read_holding_registers( - address, - count, - **kwargs) - - def write_coil(self, unit, address, value): - """Write coil.""" - with self._lock: - kwargs = {'unit': unit} if unit else {} - self._client.write_coil( - address, - value, - **kwargs) - - def write_register(self, unit, address, value): - """Write register.""" - with self._lock: - kwargs = {'unit': unit} if unit else {} - self._client.write_register( - address, - value, - **kwargs) - - def write_registers(self, unit, address, values): - """Write registers.""" - with self._lock: - kwargs = {'unit': unit} if unit else {} - self._client.write_registers( - address, - values, - **kwargs) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py new file mode 100644 index 0000000000000..f42423bf9a871 --- /dev/null +++ b/homeassistant/components/modbus/__init__.py @@ -0,0 +1,247 @@ +"""Support for Modbus.""" +import logging +import threading + +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_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' + +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'), + vol.Required(CONF_PORT): cv.string, + vol.Required(CONF_PARITY): vol.Any('E', 'O', 'N'), + vol.Required(CONF_STOPBITS): vol.Any(1, 2), + vol.Required(CONF_TYPE): 'serial', + vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout, +}) + +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.All(cv.ensure_list, [vol.Any(SERIAL_SCHEMA, ETHERNET_SCHEMA)]) +}, extra=vol.ALLOW_EXTRA,) + +_LOGGER = logging.getLogger(__name__) + +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 +}) + + +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 + hass.data[DOMAIN] = hub_collect = {} + + 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.""" + for client in hub_collect.values(): + client.close() + + def start_modbus(event): + """Start Modbus service.""" + for client in hub_collect.values(): + client.connect() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus) + + # Register services for modbus + hass.services.register( + DOMAIN, SERVICE_WRITE_REGISTER, write_register, + schema=SERVICE_WRITE_REGISTER_SCHEMA) + hass.services.register( + DOMAIN, SERVICE_WRITE_COIL, write_coil, + schema=SERVICE_WRITE_COIL_SCHEMA) + + def write_register(service): + """Write modbus registers.""" + 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_collect[client_name].write_registers( + unit, + address, + [int(float(i)) for i in value]) + else: + hub_collect[client_name].write_register( + unit, + address, + int(float(value))) + + def write_coil(service): + """Write modbus coil.""" + unit = service.data.get(ATTR_UNIT) + address = service.data.get(ATTR_ADDRESS) + state = service.data.get(ATTR_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) + + return True + + +class ModbusHub: + """Thread safe wrapper class for pymodbus.""" + + 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.""" + with self._lock: + self._client.close() + + def connect(self): + """Connect client.""" + with self._lock: + self._client.connect() + + def read_coils(self, unit, address, count): + """Read coils.""" + with self._lock: + kwargs = {'unit': unit} if unit else {} + return self._client.read_coils( + address, + count, + **kwargs) + + def read_input_registers(self, unit, address, count): + """Read input registers.""" + with self._lock: + kwargs = {'unit': unit} if unit else {} + return self._client.read_input_registers( + address, + count, + **kwargs) + + def read_holding_registers(self, unit, address, count): + """Read holding registers.""" + with self._lock: + kwargs = {'unit': unit} if unit else {} + return self._client.read_holding_registers( + address, + count, + **kwargs) + + def write_coil(self, unit, address, value): + """Write coil.""" + with self._lock: + kwargs = {'unit': unit} if unit else {} + self._client.write_coil( + address, + value, + **kwargs) + + def write_register(self, unit, address, value): + """Write register.""" + with self._lock: + kwargs = {'unit': unit} if unit else {} + self._client.write_register( + address, + value, + **kwargs) + + def write_registers(self, unit, address, values): + """Write registers.""" + with self._lock: + kwargs = {'unit': unit} if unit else {} + self._client.write_registers( + address, + values, + **kwargs) diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py new file mode 100644 index 0000000000000..38511ffed7ec0 --- /dev/null +++ b/homeassistant/components/modbus/binary_sensor.py @@ -0,0 +1,67 @@ +"""Support for Modbus Coil sensors.""" +import logging +import voluptuous as vol + +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 +from homeassistant.components.sensor import PLATFORM_SCHEMA + +_LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['modbus'] + +CONF_COIL = 'coil' +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, + }] +}) + + +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( + hub, coil.get(CONF_NAME), coil.get(CONF_SLAVE), + coil.get(CONF_COIL))) + add_entities(sensors) + + +class ModbusCoilSensor(BinarySensorDevice): + """Modbus coil sensor.""" + + 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) + self._value = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def is_on(self): + """Return the state of the sensor.""" + return self._value + + def update(self): + """Update the state of the sensor.""" + result = self._hub.read_coils(self._slave, self._coil, 1) + try: + self._value = result.bits[0] + except AttributeError: + _LOGGER.error("No response from hub %s, slave %s, coil %s", + self._hub.name, self._slave, self._coil) diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py new file mode 100644 index 0000000000000..ed8cbda863f7f --- /dev/null +++ b/homeassistant/components/modbus/climate.py @@ -0,0 +1,143 @@ +"""Support for Generic Modbus Thermostats.""" +import logging +import struct + +import voluptuous as vol + +from homeassistant.const import ( + CONF_NAME, CONF_SLAVE, ATTR_TEMPERATURE) +from homeassistant.components.climate import ( + ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE) +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 +CONF_TARGET_TEMP = 'target_temp_register' +CONF_CURRENT_TEMP = 'current_temp_register' +CONF_DATA_TYPE = 'data_type' +CONF_COUNT = 'data_count' +CONF_PRECISION = 'precision' + +DATA_TYPE_INT = 'int' +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, + vol.Required(CONF_CURRENT_TEMP): cv.positive_int, + vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_FLOAT): + vol.In([DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT]), + vol.Optional(CONF_COUNT, default=2): cv.positive_int, + vol.Optional(CONF_PRECISION, default=1): cv.positive_int +}) + +SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Modbus Thermostat Platform.""" + name = config.get(CONF_NAME) + modbus_slave = config.get(CONF_SLAVE) + target_temp_register = config.get(CONF_TARGET_TEMP) + current_temp_register = config.get(CONF_CURRENT_TEMP) + 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( + 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, 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 + self._current_temperature_register = current_temp_register + self._target_temperature = None + self._current_temperature = None + self._data_type = data_type + self._count = int(count) + self._precision = precision + self._structure = '>f' + + data_types = {DATA_TYPE_INT: {1: 'h', 2: 'i', 4: 'q'}, + DATA_TYPE_UINT: {1: 'H', 2: 'I', 4: 'Q'}, + DATA_TYPE_FLOAT: {1: 'e', 2: 'f', 4: 'd'}} + + self._structure = '>{}'.format(data_types[self._data_type] + [self._count]) + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS + + def update(self): + """Update Target & Current Temperature.""" + self._target_temperature = self.read_register( + self._target_temperature_register) + self._current_temperature = self.read_register( + self._current_temperature_register) + + @property + def name(self): + """Return the name of the climate device.""" + return self._name + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._current_temperature + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._target_temperature + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + target_temperature = kwargs.get(ATTR_TEMPERATURE) + if target_temperature is None: + return + byte_string = struct.pack(self._structure, target_temperature) + register_value = struct.unpack('>h', byte_string[0:2])[0] + + try: + self.write_register(self._target_temperature_register, + register_value) + except AttributeError as ex: + _LOGGER.error(ex) + + def read_register(self, register): + """Read holding register using the modbus hub slave.""" + try: + result = self._hub.read_holding_registers(self._slave, register, + self._count) + except AttributeError as ex: + _LOGGER.error(ex) + byte_string = b''.join( + [x.to_bytes(2, byteorder='big') for x in result.registers]) + val = struct.unpack(self._structure, byte_string)[0] + register_value = format(val, '.{}f'.format(self._precision)) + return register_value + + def write_register(self, register, value): + """Write register using the modbus hub slave.""" + self._hub.write_registers(self._slave, register, [value, 0]) diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py new file mode 100644 index 0000000000000..6ba8d92d15533 --- /dev/null +++ b/homeassistant/components/modbus/sensor.py @@ -0,0 +1,184 @@ +"""Support for Modbus Register sensors.""" +import logging +import struct + +import voluptuous as vol + +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.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_COUNT = 'count' +CONF_REVERSE_ORDER = 'reverse_order' +CONF_PRECISION = 'precision' +CONF_REGISTER = 'register' +CONF_REGISTERS = 'registers' +CONF_SCALE = 'scale' +CONF_DATA_TYPE = 'data_type' +CONF_REGISTER_TYPE = 'register_type' + +REGISTER_TYPE_HOLDING = 'holding' +REGISTER_TYPE_INPUT = 'input' + +DATA_TYPE_INT = 'int' +DATA_TYPE_UINT = 'uint' +DATA_TYPE_FLOAT = 'float' +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): + vol.In([REGISTER_TYPE_HOLDING, REGISTER_TYPE_INPUT]), + vol.Optional(CONF_COUNT, default=1): cv.positive_int, + vol.Optional(CONF_REVERSE_ORDER, default=False): cv.boolean, + vol.Optional(CONF_OFFSET, default=0): vol.Coerce(float), + vol.Optional(CONF_PRECISION, default=0): cv.positive_int, + vol.Optional(CONF_SCALE, default=1): vol.Coerce(float), + vol.Optional(CONF_SLAVE): cv.positive_int, + vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_INT): + 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, + }] +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Modbus sensors.""" + sensors = [] + data_types = {DATA_TYPE_INT: {1: 'h', 2: 'i', 4: 'q'}} + data_types[DATA_TYPE_UINT] = {1: 'H', 2: 'I', 4: 'Q'} + data_types[DATA_TYPE_FLOAT] = {1: 'e', 2: 'f', 4: 'd'} + + for register in config.get(CONF_REGISTERS): + 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)]) + except KeyError: + _LOGGER.error("Unable to detect data type for %s sensor, " + "try a custom type", register.get(CONF_NAME)) + continue + else: + structure = register.get(CONF_STRUCTURE) + + try: + size = struct.calcsize(structure) + except struct.error as err: + _LOGGER.error( + "Error in sensor %s structure: %s", + register.get(CONF_NAME), err) + continue + + if register.get(CONF_COUNT) * 2 != size: + _LOGGER.error( + "Structure size (%d bytes) mismatch registers count " + "(%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), + register.get(CONF_REGISTER_TYPE), + register.get(CONF_UNIT_OF_MEASUREMENT), + register.get(CONF_COUNT), + register.get(CONF_REVERSE_ORDER), + register.get(CONF_SCALE), + register.get(CONF_OFFSET), + structure, + register.get(CONF_PRECISION))) + + if not sensors: + return False + add_entities(sensors) + + +class ModbusRegisterSensor(RestoreEntity): + """Modbus register sensor.""" + + 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) + self._register_type = register_type + self._unit_of_measurement = unit_of_measurement + self._count = int(count) + self._reverse_order = reverse_order + self._scale = scale + self._offset = offset + self._precision = precision + 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.""" + return self._value + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit_of_measurement + + def update(self): + """Update the state of the sensor.""" + if self._register_type == REGISTER_TYPE_INPUT: + result = self._hub.read_input_registers( + self._slave, + self._register, + self._count) + else: + result = self._hub.read_holding_registers( + self._slave, + self._register, + self._count) + val = 0 + + try: + registers = result.registers + if self._reverse_order: + registers.reverse() + except AttributeError: + _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] + ) + val = struct.unpack(self._structure, byte_string)[0] + self._value = format( + self._scale * val + self._offset, '.{}f'.format(self._precision)) diff --git a/homeassistant/components/modbus/services.yaml b/homeassistant/components/modbus/services.yaml new file mode 100644 index 0000000000000..0fd9e5a49e738 --- /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/modbus/switch.py b/homeassistant/components/modbus/switch.py new file mode 100644 index 0000000000000..47ad8e98958ba --- /dev/null +++ b/homeassistant/components/modbus/switch.py @@ -0,0 +1,212 @@ +"""Support for Modbus switches.""" +import logging +import voluptuous as vol + +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, 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" +CONF_COILS = "coils" +CONF_REGISTER = "register" +CONF_REGISTERS = "registers" +CONF_VERIFY_STATE = "verify_state" +CONF_VERIFY_REGISTER = "verify_register" +CONF_REGISTER_TYPE = "register_type" +CONF_STATE_ON = "state_on" +CONF_STATE_OFF = "state_off" + +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, + vol.Required(CONF_COMMAND_ON): cv.positive_int, + vol.Required(CONF_COMMAND_OFF): cv.positive_int, + vol.Optional(CONF_VERIFY_STATE, default=True): cv.boolean, + vol.Optional(CONF_VERIFY_REGISTER): + cv.positive_int, + vol.Optional(CONF_REGISTER_TYPE, default=REGISTER_TYPE_HOLDING): + vol.In([REGISTER_TYPE_HOLDING, REGISTER_TYPE_INPUT]), + vol.Optional(CONF_STATE_ON): cv.positive_int, + vol.Optional(CONF_STATE_OFF): cv.positive_int, +}) + +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, +}) + +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], + })) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Read configuration and create Modbus devices.""" + 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( + 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), + register.get(CONF_COMMAND_ON), + register.get(CONF_COMMAND_OFF), + register.get(CONF_VERIFY_STATE), + register.get(CONF_VERIFY_REGISTER), + register.get(CONF_REGISTER_TYPE), + register.get(CONF_STATE_ON), + register.get(CONF_STATE_OFF))) + + add_entities(switches) + + +class ModbusCoilSwitch(ToggleEntity, RestoreEntity): + """Representation of a Modbus coil switch.""" + + 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.""" + return self._is_on + + @property + def name(self): + """Return the name of the switch.""" + return self._name + + def turn_on(self, **kwargs): + """Set switch on.""" + self._hub.write_coil(self._slave, self._coil, True) + + def turn_off(self, **kwargs): + """Set switch off.""" + self._hub.write_coil(self._slave, self._coil, False) + + def update(self): + """Update the state of the switch.""" + 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 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, 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 + self._command_on = command_on + self._command_off = command_off + self._verify_state = verify_state + self._verify_register = ( + verify_register if verify_register else self._register) + self._register_type = register_type + + if state_on is not None: + self._state_on = state_on + else: + self._state_on = self._command_on + + if state_off is not None: + self._state_off = state_off + else: + self._state_off = self._command_off + + self._is_on = None + + def turn_on(self, **kwargs): + """Set switch 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.""" + self._hub.write_register( + self._slave, self._register, self._command_off) + if not self._verify_state: + self._is_on = False + + def update(self): + """Update the state of the switch.""" + if not self._verify_state: + return + + value = 0 + if self._register_type == REGISTER_TYPE_INPUT: + result = self._hub.read_input_registers( + self._slave, self._register, 1) + else: + result = self._hub.read_holding_registers( + self._slave, self._register, 1) + + try: + value = int(result.registers[0]) + except AttributeError: + _LOGGER.error( + "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 + elif value == self._state_off: + self._is_on = False + else: + _LOGGER.error( + "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 0000000000000..ebe5696f514b8 --- /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 9757716b1bffa..663d79f3c14e6 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 ed2a3cd6c521c..e430b1fbc9f50 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -234,7 +234,7 @@ def validate_device_has_at_least_one_identifier(value: ConfigType) -> \ 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 b3e4d452b5c6f..8e1b62414b7af 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -19,8 +19,8 @@ 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 @@ 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 @@ 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 @@ async def _subscribe_topics(self): 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 @@ async def async_alarm_arm_away(self, code=None): 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 be176a39a255f..569d69a9ad875 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -13,7 +13,8 @@ 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 @@ async def async_added_to_hass(self): 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 @@ def message_received(topic, payload, qos): 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 db46f11b88e7e..c028ca5a6f694 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -17,9 +17,9 @@ 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 @@ async def async_added_to_hass(self): 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 75fae0e9c1544..829be266b09ec 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -10,8 +10,8 @@ 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.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 @@ def validate_options(value): 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 @@ def current_cover_tilt_position(self): """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 9a2daf388cb13..688912070bdc6 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -11,7 +11,7 @@ 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 @@ '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 @@ '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 @@ '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 @@ '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_device_message_received(topic, payload, qos): 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.py deleted file mode 100644 index 2cde7825734f7..0000000000000 --- a/homeassistant/components/mqtt_eventstream.py +++ /dev/null @@ -1,109 +0,0 @@ -""" -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/ -""" -import asyncio -import json - -import voluptuous as vol - -from homeassistant.core import callback -from homeassistant.components.mqtt import ( - valid_publish_topic, valid_subscribe_topic) -from homeassistant.const import ( - ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, - EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL) -from homeassistant.core import EventOrigin, State -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.json import JSONEncoder - -DOMAIN = 'mqtt_eventstream' -DEPENDENCIES = ['mqtt'] - -CONF_PUBLISH_TOPIC = 'publish_topic' -CONF_SUBSCRIBE_TOPIC = 'subscribe_topic' -CONF_PUBLISH_EVENTSTREAM_RECEIVED = 'publish_eventstream_received' -CONF_IGNORE_EVENT = 'ignore_event' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(CONF_PUBLISH_TOPIC): valid_publish_topic, - 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 - }), -}, extra=vol.ALLOW_EXTRA) - - -@asyncio.coroutine -def async_setup(hass, config): - """Set up the MQTT eventstream component.""" - mqtt = hass.components.mqtt - conf = config.get(DOMAIN, {}) - pub_topic = conf.get(CONF_PUBLISH_TOPIC) - sub_topic = conf.get(CONF_SUBSCRIBE_TOPIC) - ignore_event = conf.get(CONF_IGNORE_EVENT) - - @callback - def _event_publisher(event): - """Handle events by publishing them on the MQTT queue.""" - if event.origin != EventOrigin.local: - return - if event.event_type == EVENT_TIME_CHANGED: - return - - # User-defined events to ignore - if event.event_type in ignore_event: - return - - # Filter out the events that were triggered by publishing - # to the MQTT topic, or you will end up in an infinite loop. - if event.event_type == EVENT_CALL_SERVICE: - if ( - event.data.get('domain') == mqtt.DOMAIN and - event.data.get('service') == mqtt.SERVICE_PUBLISH and - event.data[ATTR_SERVICE_DATA].get('topic') == pub_topic - ): - return - - event_info = {'event_type': event.event_type, 'event_data': event.data} - msg = json.dumps(event_info, cls=JSONEncoder) - mqtt.async_publish(pub_topic, msg) - - # Only listen for local events if you are going to publish them. - if pub_topic: - hass.bus.async_listen(MATCH_ALL, _event_publisher) - - # Process events from a remote server that are received on a queue. - @callback - def _event_receiver(topic, payload, qos): - """Receive events published by and fire them on this hass instance.""" - event = json.loads(payload) - event_type = event.get('event_type') - event_data = event.get('event_data') - - # Special case handling for event STATE_CHANGED - # We will try to convert state dicts back to State objects - # Copied over from the _handle_api_post_events_event method - # of the api component. - if event_type == EVENT_STATE_CHANGED and event_data: - for key in ('old_state', 'new_state'): - state = State.from_dict(event_data.get(key)) - - if state: - event_data[key] = state - - hass.bus.async_fire( - event_type, - event_data=event_data, - origin=EventOrigin.remote - ) - - # Only subscribe if you specified a topic. - if sub_topic: - yield from mqtt.async_subscribe(sub_topic, _event_receiver) - - return True diff --git a/homeassistant/components/mqtt_eventstream/__init__.py b/homeassistant/components/mqtt_eventstream/__init__.py new file mode 100644 index 0000000000000..6e545d19fe26f --- /dev/null +++ b/homeassistant/components/mqtt_eventstream/__init__.py @@ -0,0 +1,104 @@ +"""Connect two Home Assistant instances via MQTT.""" +import asyncio +import json + +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.components.mqtt import ( + valid_publish_topic, valid_subscribe_topic) +from homeassistant.const import ( + ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, + EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL) +from homeassistant.core import EventOrigin, State +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.json import JSONEncoder + +DOMAIN = 'mqtt_eventstream' +DEPENDENCIES = ['mqtt'] + +CONF_PUBLISH_TOPIC = 'publish_topic' +CONF_SUBSCRIBE_TOPIC = 'subscribe_topic' +CONF_PUBLISH_EVENTSTREAM_RECEIVED = 'publish_eventstream_received' +CONF_IGNORE_EVENT = 'ignore_event' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Optional(CONF_PUBLISH_TOPIC): valid_publish_topic, + 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, + }), +}, extra=vol.ALLOW_EXTRA) + + +@asyncio.coroutine +def async_setup(hass, config): + """Set up the MQTT eventstream component.""" + mqtt = hass.components.mqtt + conf = config.get(DOMAIN, {}) + pub_topic = conf.get(CONF_PUBLISH_TOPIC) + sub_topic = conf.get(CONF_SUBSCRIBE_TOPIC) + ignore_event = conf.get(CONF_IGNORE_EVENT) + + @callback + def _event_publisher(event): + """Handle events by publishing them on the MQTT queue.""" + if event.origin != EventOrigin.local: + return + if event.event_type == EVENT_TIME_CHANGED: + return + + # User-defined events to ignore + if event.event_type in ignore_event: + return + + # Filter out the events that were triggered by publishing + # to the MQTT topic, or you will end up in an infinite loop. + if event.event_type == EVENT_CALL_SERVICE: + if ( + event.data.get('domain') == mqtt.DOMAIN and + event.data.get('service') == mqtt.SERVICE_PUBLISH and + event.data[ATTR_SERVICE_DATA].get('topic') == pub_topic + ): + return + + event_info = {'event_type': event.event_type, 'event_data': event.data} + msg = json.dumps(event_info, cls=JSONEncoder) + mqtt.async_publish(pub_topic, msg) + + # Only listen for local events if you are going to publish them. + if pub_topic: + hass.bus.async_listen(MATCH_ALL, _event_publisher) + + # Process events from a remote server that are received on a queue. + @callback + def _event_receiver(topic, payload, qos): + """Receive events published by and fire them on this hass instance.""" + event = json.loads(payload) + event_type = event.get('event_type') + event_data = event.get('event_data') + + # Special case handling for event STATE_CHANGED + # We will try to convert state dicts back to State objects + # Copied over from the _handle_api_post_events_event method + # of the api component. + if event_type == EVENT_STATE_CHANGED and event_data: + for key in ('old_state', 'new_state'): + state = State.from_dict(event_data.get(key)) + + if state: + event_data[key] = state + + hass.bus.async_fire( + event_type, + event_data=event_data, + origin=EventOrigin.remote + ) + + # Only subscribe if you specified a topic. + if sub_topic: + yield from mqtt.async_subscribe(sub_topic, _event_receiver) + + return True diff --git a/homeassistant/components/mqtt_statestream.py b/homeassistant/components/mqtt_statestream.py deleted file mode 100644 index 3a0e5d39ff0d2..0000000000000 --- a/homeassistant/components/mqtt_statestream.py +++ /dev/null @@ -1,94 +0,0 @@ -""" -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/ -""" -import json - -import voluptuous as vol - -from homeassistant.const import (CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, - CONF_INCLUDE, MATCH_ALL) -from homeassistant.core import callback -from homeassistant.components.mqtt import valid_publish_topic -from homeassistant.helpers.entityfilter import generate_filter -from homeassistant.helpers.event import async_track_state_change -from homeassistant.helpers.json import JSONEncoder -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' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: 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.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.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 - }) -}, extra=vol.ALLOW_EXTRA) - - -async def async_setup(hass, config): - """Set up the MQTT state feed.""" - conf = config.get(DOMAIN, {}) - base_topic = conf.get(CONF_BASE_TOPIC) - pub_include = conf.get(CONF_INCLUDE, {}) - pub_exclude = conf.get(CONF_EXCLUDE, {}) - publish_attributes = conf.get(CONF_PUBLISH_ATTRIBUTES) - publish_timestamps = conf.get(CONF_PUBLISH_TIMESTAMPS) - publish_filter = generate_filter(pub_include.get(CONF_DOMAINS, []), - pub_include.get(CONF_ENTITIES, []), - pub_exclude.get(CONF_DOMAINS, []), - pub_exclude.get(CONF_ENTITIES, [])) - if not base_topic.endswith('/'): - base_topic = base_topic + '/' - - @callback - def _state_publisher(entity_id, old_state, new_state): - if new_state is None: - return - - if not publish_filter(entity_id): - return - - payload = new_state.state - - mybase = base_topic + entity_id.replace('.', '/') + '/' - hass.components.mqtt.async_publish(mybase + 'state', payload, 1, True) - - if publish_timestamps: - if new_state.last_updated: - hass.components.mqtt.async_publish( - mybase + 'last_updated', - new_state.last_updated.isoformat(), - 1, - True) - if new_state.last_changed: - hass.components.mqtt.async_publish( - mybase + 'last_changed', - new_state.last_changed.isoformat(), - 1, - True) - - if publish_attributes: - for key, val in new_state.attributes.items(): - encoded_val = json.dumps(val, cls=JSONEncoder) - hass.components.mqtt.async_publish(mybase + key, - encoded_val, 1, True) - - async_track_state_change(hass, MATCH_ALL, _state_publisher) - return True diff --git a/homeassistant/components/mqtt_statestream/__init__.py b/homeassistant/components/mqtt_statestream/__init__.py new file mode 100644 index 0000000000000..18a70bf75bb3a --- /dev/null +++ b/homeassistant/components/mqtt_statestream/__init__.py @@ -0,0 +1,90 @@ +"""Publish simple item state changes via MQTT.""" +import json + +import voluptuous as vol + +from homeassistant.const import (CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, + CONF_INCLUDE, MATCH_ALL) +from homeassistant.core import callback +from homeassistant.components.mqtt import valid_publish_topic +from homeassistant.helpers.entityfilter import generate_filter +from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.json import JSONEncoder +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' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: 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.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.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, + }) +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the MQTT state feed.""" + conf = config.get(DOMAIN, {}) + base_topic = conf.get(CONF_BASE_TOPIC) + pub_include = conf.get(CONF_INCLUDE, {}) + pub_exclude = conf.get(CONF_EXCLUDE, {}) + publish_attributes = conf.get(CONF_PUBLISH_ATTRIBUTES) + publish_timestamps = conf.get(CONF_PUBLISH_TIMESTAMPS) + publish_filter = generate_filter(pub_include.get(CONF_DOMAINS, []), + pub_include.get(CONF_ENTITIES, []), + pub_exclude.get(CONF_DOMAINS, []), + pub_exclude.get(CONF_ENTITIES, [])) + if not base_topic.endswith('/'): + base_topic = base_topic + '/' + + @callback + def _state_publisher(entity_id, old_state, new_state): + if new_state is None: + return + + if not publish_filter(entity_id): + return + + payload = new_state.state + + mybase = base_topic + entity_id.replace('.', '/') + '/' + hass.components.mqtt.async_publish(mybase + 'state', payload, 1, True) + + if publish_timestamps: + if new_state.last_updated: + hass.components.mqtt.async_publish( + mybase + 'last_updated', + new_state.last_updated.isoformat(), + 1, + True) + if new_state.last_changed: + hass.components.mqtt.async_publish( + mybase + 'last_changed', + new_state.last_changed.isoformat(), + 1, + True) + + if publish_attributes: + for key, val in new_state.attributes.items(): + encoded_val = json.dumps(val, cls=JSONEncoder) + hass.components.mqtt.async_publish(mybase + key, + encoded_val, 1, True) + + async_track_state_change(hass, MATCH_ALL, _state_publisher) + return True diff --git a/homeassistant/components/mychevy.py b/homeassistant/components/mychevy.py deleted file mode 100644 index 209027ad4727c..0000000000000 --- a/homeassistant/components/mychevy.py +++ /dev/null @@ -1,155 +0,0 @@ -""" -MyChevy Component. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/mychevy/ -""" -from datetime import timedelta -import logging -import threading -import time - -import voluptuous as vol - -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery -from homeassistant.util import Throttle - -REQUIREMENTS = ["mychevy==1.2.0"] - -DOMAIN = 'mychevy' -UPDATE_TOPIC = DOMAIN -ERROR_TOPIC = DOMAIN + "_error" - -MYCHEVY_SUCCESS = "success" -MYCHEVY_ERROR = "error" - -NOTIFICATION_ID = 'mychevy_website_notification' -NOTIFICATION_TITLE = 'MyChevy website status' - -_LOGGER = logging.getLogger(__name__) - -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30) -ERROR_SLEEP_TIME = timedelta(minutes=30) - -CONF_COUNTRY = 'country' -DEFAULT_COUNTRY = 'us' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: 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'])) - }), -}, extra=vol.ALLOW_EXTRA) - - -class EVSensorConfig: - """The EV sensor configuration.""" - - def __init__(self, name, attr, unit_of_measurement=None, icon=None, - extra_attrs=None): - """Create new sensor configuration.""" - self.name = name - self.attr = attr - self.extra_attrs = extra_attrs or [] - self.unit_of_measurement = unit_of_measurement - self.icon = icon - - -class EVBinarySensorConfig: - """The EV binary sensor configuration.""" - - def __init__(self, name, attr, device_class=None): - """Create new binary sensor configuration.""" - self.name = name - self.attr = attr - self.device_class = device_class - - -def setup(hass, base_config): - """Set up the mychevy component.""" - import mychevy.mychevy as mc - - config = base_config.get(DOMAIN) - - email = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - country = config.get(CONF_COUNTRY) - hass.data[DOMAIN] = MyChevyHub(mc.MyChevy(email, password, country), hass, - base_config) - hass.data[DOMAIN].start() - - return True - - -class MyChevyHub(threading.Thread): - """MyChevy Hub. - - Connecting to the mychevy website is done through a selenium - webscraping process. That can only run synchronously. In order to - prevent blocking of other parts of Home Assistant the architecture - launches a polling loop in a thread. - - When new data is received, sensors are updated, and hass is - signaled that there are updates. Sensors are not created until the - first update, which will be 60 - 120 seconds after the platform - starts. - """ - - def __init__(self, client, hass, hass_config): - """Initialize MyChevy Hub.""" - super().__init__() - self._client = client - self.hass = hass - self.hass_config = hass_config - self.cars = [] - self.status = None - self.ready = False - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Update sensors from mychevy website. - - This is a synchronous polling call that takes a very long time - (like 2 to 3 minutes long time) - - """ - self._client.login() - self._client.get_cars() - self.cars = self._client.cars - if self.ready is not True: - discovery.load_platform(self.hass, 'sensor', DOMAIN, {}, - self.hass_config) - discovery.load_platform(self.hass, 'binary_sensor', DOMAIN, {}, - self.hass_config) - self.ready = True - self.cars = self._client.update_cars() - - def get_car(self, vid): - """Compatibility to work with one car.""" - if self.cars: - for car in self.cars: - if car.vid == vid: - return car - return None - - def run(self): - """Thread run loop.""" - # We add the status device first outside of the loop - - # And then busy wait on threads - while True: - try: - _LOGGER.info("Starting mychevy loop") - self.update() - self.hass.helpers.dispatcher.dispatcher_send(UPDATE_TOPIC) - time.sleep(MIN_TIME_BETWEEN_UPDATES.seconds) - except Exception: # pylint: disable=broad-except - _LOGGER.exception( - "Error updating mychevy data. " - "This probably means the OnStar link is down again") - self.hass.helpers.dispatcher.dispatcher_send(ERROR_TOPIC) - time.sleep(ERROR_SLEEP_TIME.seconds) diff --git a/homeassistant/components/mychevy/__init__.py b/homeassistant/components/mychevy/__init__.py new file mode 100644 index 0000000000000..e6fd7f19c2a3a --- /dev/null +++ b/homeassistant/components/mychevy/__init__.py @@ -0,0 +1,150 @@ +"""Support for MyChevy.""" +from datetime import timedelta +import logging +import threading +import time + +import voluptuous as vol + +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import discovery +from homeassistant.util import Throttle + +REQUIREMENTS = ['mychevy==1.2.0'] + +DOMAIN = 'mychevy' +UPDATE_TOPIC = DOMAIN +ERROR_TOPIC = DOMAIN + "_error" + +MYCHEVY_SUCCESS = "success" +MYCHEVY_ERROR = "error" + +NOTIFICATION_ID = 'mychevy_website_notification' +NOTIFICATION_TITLE = 'MyChevy website status' + +_LOGGER = logging.getLogger(__name__) + +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30) +ERROR_SLEEP_TIME = timedelta(minutes=30) + +CONF_COUNTRY = 'country' +DEFAULT_COUNTRY = 'us' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: 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'])), + }), +}, extra=vol.ALLOW_EXTRA) + + +class EVSensorConfig: + """The EV sensor configuration.""" + + def __init__(self, name, attr, unit_of_measurement=None, icon=None, + extra_attrs=None): + """Create new sensor configuration.""" + self.name = name + self.attr = attr + self.extra_attrs = extra_attrs or [] + self.unit_of_measurement = unit_of_measurement + self.icon = icon + + +class EVBinarySensorConfig: + """The EV binary sensor configuration.""" + + def __init__(self, name, attr, device_class=None): + """Create new binary sensor configuration.""" + self.name = name + self.attr = attr + self.device_class = device_class + + +def setup(hass, base_config): + """Set up the mychevy component.""" + import mychevy.mychevy as mc + + config = base_config.get(DOMAIN) + + email = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + country = config.get(CONF_COUNTRY) + hass.data[DOMAIN] = MyChevyHub(mc.MyChevy(email, password, country), hass, + base_config) + hass.data[DOMAIN].start() + + return True + + +class MyChevyHub(threading.Thread): + """MyChevy Hub. + + Connecting to the mychevy website is done through a selenium + webscraping process. That can only run synchronously. In order to + prevent blocking of other parts of Home Assistant the architecture + launches a polling loop in a thread. + + When new data is received, sensors are updated, and hass is + signaled that there are updates. Sensors are not created until the + first update, which will be 60 - 120 seconds after the platform + starts. + """ + + def __init__(self, client, hass, hass_config): + """Initialize MyChevy Hub.""" + super().__init__() + self._client = client + self.hass = hass + self.hass_config = hass_config + self.cars = [] + self.status = None + self.ready = False + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Update sensors from mychevy website. + + This is a synchronous polling call that takes a very long time + (like 2 to 3 minutes long time) + + """ + self._client.login() + self._client.get_cars() + self.cars = self._client.cars + if self.ready is not True: + discovery.load_platform(self.hass, 'sensor', DOMAIN, {}, + self.hass_config) + discovery.load_platform(self.hass, 'binary_sensor', DOMAIN, {}, + self.hass_config) + self.ready = True + self.cars = self._client.update_cars() + + def get_car(self, vid): + """Compatibility to work with one car.""" + if self.cars: + for car in self.cars: + if car.vid == vid: + return car + return None + + def run(self): + """Thread run loop.""" + # We add the status device first outside of the loop + + # And then busy wait on threads + while True: + try: + _LOGGER.info("Starting mychevy loop") + self.update() + self.hass.helpers.dispatcher.dispatcher_send(UPDATE_TOPIC) + time.sleep(MIN_TIME_BETWEEN_UPDATES.seconds) + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "Error updating mychevy data. " + "This probably means the OnStar link is down again") + self.hass.helpers.dispatcher.dispatcher_send(ERROR_TOPIC) + time.sleep(ERROR_SLEEP_TIME.seconds) diff --git a/homeassistant/components/mychevy/binary_sensor.py b/homeassistant/components/mychevy/binary_sensor.py new file mode 100644 index 0000000000000..67f12a14359dd --- /dev/null +++ b/homeassistant/components/mychevy/binary_sensor.py @@ -0,0 +1,84 @@ +"""Support for MyChevy binary sensors.""" +import logging + +from homeassistant.components.mychevy import ( + EVBinarySensorConfig, DOMAIN as MYCHEVY_DOMAIN, UPDATE_TOPIC +) +from homeassistant.components.binary_sensor import ( + ENTITY_ID_FORMAT, BinarySensorDevice) +from homeassistant.core import callback +from homeassistant.util import slugify + +_LOGGER = logging.getLogger(__name__) + +SENSORS = [ + EVBinarySensorConfig("Plugged In", "plugged_in", "plug") +] + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Set up the MyChevy sensors.""" + if discovery_info is None: + return + + sensors = [] + hub = hass.data[MYCHEVY_DOMAIN] + for sconfig in SENSORS: + for car in hub.cars: + sensors.append(EVBinarySensor(hub, sconfig, car.vid)) + + async_add_entities(sensors) + + +class EVBinarySensor(BinarySensorDevice): + """Base EVSensor class. + + 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): + """Initialize sensor with car connection.""" + self._conn = connection + self._name = config.name + self._attr = config.attr + self._type = config.device_class + 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))) + + @property + def name(self): + """Return the name.""" + return self._name + + @property + def is_on(self): + """Return if on.""" + return self._is_on + + @property + def _car(self): + """Return the car.""" + return self._conn.get_car(self._car_vid) + + async def async_added_to_hass(self): + """Register callbacks.""" + self.hass.helpers.dispatcher.async_dispatcher_connect( + UPDATE_TOPIC, self.async_update_callback) + + @callback + def async_update_callback(self): + """Update state.""" + if self._car is not None: + self._is_on = getattr(self._car, self._attr, None) + self.async_schedule_update_ha_state() + + @property + def should_poll(self): + """Return the polling state.""" + return False diff --git a/homeassistant/components/mychevy/sensor.py b/homeassistant/components/mychevy/sensor.py new file mode 100644 index 0000000000000..c7d140e0c4c76 --- /dev/null +++ b/homeassistant/components/mychevy/sensor.py @@ -0,0 +1,173 @@ +"""Support for MyChevy sensors.""" +import logging + +from homeassistant.components.mychevy import ( + EVSensorConfig, DOMAIN as MYCHEVY_DOMAIN, MYCHEVY_ERROR, MYCHEVY_SUCCESS, + UPDATE_TOPIC, ERROR_TOPIC +) +from homeassistant.components.sensor import ENTITY_ID_FORMAT +from homeassistant.core import callback +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 = [ + EVSensorConfig("Mileage", "totalMiles", "miles", "mdi:speedometer"), + EVSensorConfig("Electric Range", "electricRange", "miles", + "mdi:speedometer"), + EVSensorConfig("Charged By", "estimatedFullChargeBy"), + EVSensorConfig("Charge Mode", "chargeMode"), + EVSensorConfig("Battery Level", BATTERY_SENSOR, "%", "mdi:battery", + ["charging"]) +] + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the MyChevy sensors.""" + if discovery_info is None: + return + + hub = hass.data[MYCHEVY_DOMAIN] + sensors = [MyChevyStatus()] + for sconfig in SENSORS: + for car in hub.cars: + sensors.append(EVSensor(hub, sconfig, car.vid)) + + add_entities(sensors) + + +class MyChevyStatus(Entity): + """A string representing the charge mode.""" + + _name = "MyChevy Status" + _icon = 'mdi:car-connected' + + def __init__(self): + """Initialize sensor with car connection.""" + self._state = None + + async def async_added_to_hass(self): + """Register callbacks.""" + self.hass.helpers.dispatcher.async_dispatcher_connect( + UPDATE_TOPIC, self.success) + + self.hass.helpers.dispatcher.async_dispatcher_connect( + ERROR_TOPIC, self.error) + + @callback + def success(self): + """Update state, trigger updates.""" + if self._state != MYCHEVY_SUCCESS: + _LOGGER.debug("Successfully connected to mychevy website") + self._state = MYCHEVY_SUCCESS + self.async_schedule_update_ha_state() + + @callback + def error(self): + """Update state, trigger updates.""" + _LOGGER.error( + "Connection to mychevy website failed. " + "This probably means the mychevy to OnStar link is down") + self._state = MYCHEVY_ERROR + self.async_schedule_update_ha_state() + + @property + def icon(self): + """Return the icon.""" + return self._icon + + @property + def name(self): + """Return the name.""" + return self._name + + @property + def state(self): + """Return the state.""" + return self._state + + @property + def should_poll(self): + """Return the polling state.""" + return False + + +class EVSensor(Entity): + """Base EVSensor class. + + 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): + """Initialize sensor with car connection.""" + self._conn = connection + self._name = config.name + self._attr = config.attr + self._extra_attrs = config.extra_attrs + self._unit_of_measurement = config.unit_of_measurement + self._icon = config.icon + self._state = None + self._state_attributes = {} + self._car_vid = car_vid + + self.entity_id = ENTITY_ID_FORMAT.format( + '{}_{}_{}'.format( + MYCHEVY_DOMAIN, slugify(self._car.name), slugify(self._name))) + + async def async_added_to_hass(self): + """Register callbacks.""" + self.hass.helpers.dispatcher.async_dispatcher_connect( + UPDATE_TOPIC, self.async_update_callback) + + @property + def _car(self): + """Return the car.""" + return self._conn.get_car(self._car_vid) + + @property + def icon(self): + """Return the icon.""" + if self._attr == BATTERY_SENSOR: + charging = self._state_attributes.get("charging", False) + return icon_for_battery_level(self.state, charging) + return self._icon + + @property + def name(self): + """Return the name.""" + return self._name + + @callback + def async_update_callback(self): + """Update state.""" + if self._car is not None: + self._state = getattr(self._car, self._attr, None) + for attr in self._extra_attrs: + self._state_attributes[attr] = getattr(self._car, attr) + self.async_schedule_update_ha_state() + + @property + def state(self): + """Return the state.""" + return self._state + + @property + def device_state_attributes(self): + """Return all the state attributes.""" + return self._state_attributes + + @property + def unit_of_measurement(self): + """Return the unit of measurement the state is expressed in.""" + return self._unit_of_measurement + + @property + def should_poll(self): + """Return the polling state.""" + return False diff --git a/homeassistant/components/mycroft.py b/homeassistant/components/mycroft.py deleted file mode 100644 index 834572bc551f1..0000000000000 --- a/homeassistant/components/mycroft.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -Support for Mycroft AI. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/mycroft -""" - -import logging - -import voluptuous as vol - -from homeassistant.const import CONF_HOST -from homeassistant.helpers import discovery -import homeassistant.helpers.config_validation as cv - -REQUIREMENTS = ['mycroftapi==2.0'] - -_LOGGER = logging.getLogger(__name__) - - -DOMAIN = 'mycroft' - - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_HOST): cv.string - }) -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the Mycroft component.""" - hass.data[DOMAIN] = config[DOMAIN][CONF_HOST] - discovery.load_platform(hass, 'notify', DOMAIN, {}, config) - return True diff --git a/homeassistant/components/mycroft/__init__.py b/homeassistant/components/mycroft/__init__.py new file mode 100644 index 0000000000000..29f6383f686b1 --- /dev/null +++ b/homeassistant/components/mycroft/__init__.py @@ -0,0 +1,27 @@ +"""Support for Mycroft AI.""" +import logging + +import voluptuous as vol + +from homeassistant.const import CONF_HOST +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['mycroftapi==2.0'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'mycroft' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOST): cv.string + }) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Mycroft component.""" + hass.data[DOMAIN] = config[DOMAIN][CONF_HOST] + discovery.load_platform(hass, 'notify', DOMAIN, {}, config) + return True diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index 49f8560c6b336..7ca21ac582a00 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/mysensors/binary_sensor.py b/homeassistant/components/mysensors/binary_sensor.py new file mode 100644 index 0000000000000..57e8f1c1ef8ff --- /dev/null +++ b/homeassistant/components/mysensors/binary_sensor.py @@ -0,0 +1,43 @@ +"""Support for MySensors binary sensors.""" +from homeassistant.components import mysensors +from homeassistant.components.binary_sensor import ( + DEVICE_CLASSES, DOMAIN, BinarySensorDevice) +from homeassistant.const import STATE_ON + +SENSORS = { + 'S_DOOR': 'door', + 'S_MOTION': 'motion', + 'S_SMOKE': 'smoke', + 'S_SPRINKLER': 'safety', + 'S_WATER_LEAK': 'safety', + 'S_SOUND': 'sound', + 'S_VIBRATION': 'vibration', + 'S_MOISTURE': 'moisture', +} + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Set up the mysensors platform for binary sensors.""" + mysensors.setup_mysensors_platform( + hass, DOMAIN, discovery_info, MySensorsBinarySensor, + async_add_entities=async_add_entities) + + +class MySensorsBinarySensor( + mysensors.device.MySensorsEntity, BinarySensorDevice): + """Representation of a MySensors Binary Sensor child node.""" + + @property + def is_on(self): + """Return True if the binary sensor is on.""" + return self._values.get(self.value_type) == STATE_ON + + @property + def device_class(self): + """Return the class of this sensor, from DEVICE_CLASSES.""" + pres = self.gateway.const.Presentation + device_class = SENSORS.get(pres(self.child_type).name) + if device_class in DEVICE_CLASSES: + return device_class + return None diff --git a/homeassistant/components/mysensors/climate.py b/homeassistant/components/mysensors/climate.py new file mode 100644 index 0000000000000..20d608e1ca5f4 --- /dev/null +++ b/homeassistant/components/mysensors/climate.py @@ -0,0 +1,177 @@ +"""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, + STATE_COOL, STATE_HEAT, STATE_OFF, SUPPORT_FAN_MODE, + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW, + ClimateDevice) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT + +DICT_HA_TO_MYS = { + STATE_AUTO: 'AutoChangeOver', + STATE_COOL: 'CoolOn', + STATE_HEAT: 'HeatOn', + STATE_OFF: 'Off', +} +DICT_MYS_TO_HA = { + 'AutoChangeOver': STATE_AUTO, + 'CoolOn': STATE_COOL, + 'HeatOn': STATE_HEAT, + 'Off': STATE_OFF, +} + +FAN_LIST = ['Auto', 'Min', 'Normal', 'Max'] +OPERATION_LIST = [STATE_OFF, STATE_AUTO, STATE_COOL, STATE_HEAT] + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Set up the mysensors climate.""" + mysensors.setup_mysensors_platform( + hass, DOMAIN, discovery_info, MySensorsHVAC, + async_add_entities=async_add_entities) + + +class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateDevice): + """Representation of a MySensors HVAC.""" + + @property + def supported_features(self): + """Return the list of supported features.""" + features = SUPPORT_OPERATION_MODE + set_req = self.gateway.const.SetReq + if set_req.V_HVAC_SPEED in self._values: + features = features | SUPPORT_FAN_MODE + if (set_req.V_HVAC_SETPOINT_COOL in self._values and + set_req.V_HVAC_SETPOINT_HEAT in self._values): + features = ( + features | SUPPORT_TARGET_TEMPERATURE_HIGH | + SUPPORT_TARGET_TEMPERATURE_LOW) + else: + features = features | SUPPORT_TARGET_TEMPERATURE + return features + + @property + def assumed_state(self): + """Return True if unable to access real state of entity.""" + return self.gateway.optimistic + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS if self.gateway.metric else TEMP_FAHRENHEIT + + @property + def current_temperature(self): + """Return the current temperature.""" + value = self._values.get(self.gateway.const.SetReq.V_TEMP) + + if value is not None: + value = float(value) + + return value + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + set_req = self.gateway.const.SetReq + if set_req.V_HVAC_SETPOINT_COOL in self._values and \ + set_req.V_HVAC_SETPOINT_HEAT in self._values: + return None + temp = self._values.get(set_req.V_HVAC_SETPOINT_COOL) + if temp is None: + temp = self._values.get(set_req.V_HVAC_SETPOINT_HEAT) + return float(temp) if temp is not None else None + + @property + def target_temperature_high(self): + """Return the highbound target temperature we try to reach.""" + set_req = self.gateway.const.SetReq + if set_req.V_HVAC_SETPOINT_HEAT in self._values: + temp = self._values.get(set_req.V_HVAC_SETPOINT_COOL) + return float(temp) if temp is not None else None + + @property + def target_temperature_low(self): + """Return the lowbound target temperature we try to reach.""" + set_req = self.gateway.const.SetReq + if set_req.V_HVAC_SETPOINT_COOL in self._values: + temp = self._values.get(set_req.V_HVAC_SETPOINT_HEAT) + return float(temp) if temp is not None else None + + @property + def current_operation(self): + """Return current operation ie. heat, cool, idle.""" + return self._values.get(self.value_type) + + @property + def operation_list(self): + """List of available operation modes.""" + return OPERATION_LIST + + @property + def current_fan_mode(self): + """Return the fan setting.""" + return self._values.get(self.gateway.const.SetReq.V_HVAC_SPEED) + + @property + def fan_list(self): + """List of available fan modes.""" + return FAN_LIST + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + set_req = self.gateway.const.SetReq + temp = kwargs.get(ATTR_TEMPERATURE) + low = kwargs.get(ATTR_TARGET_TEMP_LOW) + high = kwargs.get(ATTR_TARGET_TEMP_HIGH) + heat = self._values.get(set_req.V_HVAC_SETPOINT_HEAT) + cool = self._values.get(set_req.V_HVAC_SETPOINT_COOL) + updates = [] + if temp is not None: + if heat is not None: + # Set HEAT Target temperature + value_type = set_req.V_HVAC_SETPOINT_HEAT + elif cool is not None: + # Set COOL Target temperature + value_type = set_req.V_HVAC_SETPOINT_COOL + if heat is not None or cool is not None: + updates = [(value_type, temp)] + elif all(val is not None for val in (low, high, heat, cool)): + updates = [ + (set_req.V_HVAC_SETPOINT_HEAT, low), + (set_req.V_HVAC_SETPOINT_COOL, high)] + for value_type, value in updates: + self.gateway.set_child_value( + self.node_id, self.child_id, value_type, value) + if self.gateway.optimistic: + # Optimistically assume that device has changed state + self._values[value_type] = value + self.async_schedule_update_ha_state() + + async def async_set_fan_mode(self, fan_mode): + """Set new target temperature.""" + set_req = self.gateway.const.SetReq + self.gateway.set_child_value( + self.node_id, self.child_id, set_req.V_HVAC_SPEED, fan_mode) + if self.gateway.optimistic: + # Optimistically assume that device has changed state + self._values[set_req.V_HVAC_SPEED] = fan_mode + self.async_schedule_update_ha_state() + + async def async_set_operation_mode(self, operation_mode): + """Set new target temperature.""" + self.gateway.set_child_value( + self.node_id, self.child_id, self.value_type, + DICT_HA_TO_MYS[operation_mode]) + if self.gateway.optimistic: + # Optimistically assume that device has changed state + self._values[self.value_type] = operation_mode + self.async_schedule_update_ha_state() + + async def async_update(self): + """Update the controller with the latest value from a sensor.""" + await super().async_update() + self._values[self.value_type] = DICT_MYS_TO_HA[ + self._values[self.value_type]] diff --git a/homeassistant/components/mysensors/cover.py b/homeassistant/components/mysensors/cover.py new file mode 100644 index 0000000000000..01605bb9afe42 --- /dev/null +++ b/homeassistant/components/mysensors/cover.py @@ -0,0 +1,81 @@ +"""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 + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Set up the mysensors platform for covers.""" + mysensors.setup_mysensors_platform( + hass, DOMAIN, discovery_info, MySensorsCover, + async_add_entities=async_add_entities) + + +class MySensorsCover(mysensors.device.MySensorsEntity, CoverDevice): + """Representation of the value of a MySensors Cover child node.""" + + @property + def assumed_state(self): + """Return True if unable to access real state of entity.""" + return self.gateway.optimistic + + @property + def is_closed(self): + """Return True if cover is closed.""" + set_req = self.gateway.const.SetReq + if set_req.V_DIMMER in self._values: + return self._values.get(set_req.V_DIMMER) == 0 + return self._values.get(set_req.V_LIGHT) == STATE_OFF + + @property + def current_cover_position(self): + """Return current position of cover. + + None is unknown, 0 is closed, 100 is fully open. + """ + set_req = self.gateway.const.SetReq + return self._values.get(set_req.V_DIMMER) + + async def async_open_cover(self, **kwargs): + """Move the cover up.""" + set_req = self.gateway.const.SetReq + self.gateway.set_child_value( + self.node_id, self.child_id, set_req.V_UP, 1) + if self.gateway.optimistic: + # Optimistically assume that cover has changed state. + if set_req.V_DIMMER in self._values: + self._values[set_req.V_DIMMER] = 100 + else: + self._values[set_req.V_LIGHT] = STATE_ON + self.async_schedule_update_ha_state() + + async def async_close_cover(self, **kwargs): + """Move the cover down.""" + set_req = self.gateway.const.SetReq + self.gateway.set_child_value( + self.node_id, self.child_id, set_req.V_DOWN, 1) + if self.gateway.optimistic: + # Optimistically assume that cover has changed state. + if set_req.V_DIMMER in self._values: + self._values[set_req.V_DIMMER] = 0 + else: + self._values[set_req.V_LIGHT] = STATE_OFF + self.async_schedule_update_ha_state() + + async def async_set_cover_position(self, **kwargs): + """Move the cover to a specific position.""" + position = kwargs.get(ATTR_POSITION) + set_req = self.gateway.const.SetReq + self.gateway.set_child_value( + self.node_id, self.child_id, set_req.V_DIMMER, position) + if self.gateway.optimistic: + # Optimistically assume that cover has changed state. + self._values[set_req.V_DIMMER] = position + self.async_schedule_update_ha_state() + + async def async_stop_cover(self, **kwargs): + """Stop the device.""" + set_req = self.gateway.const.SetReq + self.gateway.set_child_value( + self.node_id, self.child_id, set_req.V_STOP, 1) diff --git a/homeassistant/components/mysensors/device_tracker.py b/homeassistant/components/mysensors/device_tracker.py new file mode 100644 index 0000000000000..b50286585a46b --- /dev/null +++ b/homeassistant/components/mysensors/device_tracker.py @@ -0,0 +1,55 @@ +"""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 +from homeassistant.util import slugify + + +async def async_setup_scanner(hass, config, async_see, discovery_info=None): + """Set up the MySensors device scanner.""" + new_devices = mysensors.setup_mysensors_platform( + hass, DOMAIN, discovery_info, MySensorsDeviceScanner, + device_args=(hass, async_see)) + if not new_devices: + return False + + for device in new_devices: + gateway_id = id(device.gateway) + dev_id = ( + gateway_id, device.node_id, device.child_id, + device.value_type) + async_dispatcher_connect( + hass, mysensors.const.CHILD_CALLBACK.format(*dev_id), + device.async_update_callback) + async_dispatcher_connect( + hass, + mysensors.const.NODE_CALLBACK.format(gateway_id, device.node_id), + device.async_update_callback) + + return True + + +class MySensorsDeviceScanner(mysensors.device.MySensorsDevice): + """Represent a MySensors scanner.""" + + def __init__(self, hass, async_see, *args): + """Set up instance.""" + super().__init__(*args) + self.async_see = async_see + self.hass = hass + + async def _async_update_callback(self): + """Update the device.""" + await self.async_update() + node = self.gateway.sensors[self.node_id] + child = node.children[self.child_id] + position = child.values[self.value_type] + latitude, longitude, _ = position.split(',') + + await self.async_see( + dev_id=slugify(self.name), + host_name=self.name, + gps=(latitude, longitude), + battery=node.battery_level, + attributes=self.device_state_attributes + ) diff --git a/homeassistant/components/mysensors/light.py b/homeassistant/components/mysensors/light.py new file mode 100644 index 0000000000000..56511b73dfe79 --- /dev/null +++ b/homeassistant/components/mysensors/light.py @@ -0,0 +1,229 @@ +"""Support for MySensors lights.""" +from homeassistant.components import mysensors +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_WHITE_VALUE, DOMAIN, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_WHITE_VALUE, Light) +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.util.color import rgb_hex_to_rgb_list +import homeassistant.util.color as color_util + +SUPPORT_MYSENSORS_RGBW = SUPPORT_COLOR | SUPPORT_WHITE_VALUE + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Set up the mysensors platform for lights.""" + device_class_map = { + 'S_DIMMER': MySensorsLightDimmer, + 'S_RGB_LIGHT': MySensorsLightRGB, + 'S_RGBW_LIGHT': MySensorsLightRGBW, + } + mysensors.setup_mysensors_platform( + hass, DOMAIN, discovery_info, device_class_map, + async_add_entities=async_add_entities) + + +class MySensorsLight(mysensors.device.MySensorsEntity, Light): + """Representation of a MySensors Light child node.""" + + def __init__(self, *args): + """Initialize a MySensors Light.""" + super().__init__(*args) + self._state = None + self._brightness = None + self._hs = None + self._white = None + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + return self._brightness + + @property + def hs_color(self): + """Return the hs color value [int, int].""" + return self._hs + + @property + def white_value(self): + """Return the white value of this light between 0..255.""" + return self._white + + @property + def assumed_state(self): + """Return true if unable to access real state of entity.""" + return self.gateway.optimistic + + @property + def is_on(self): + """Return true if device is on.""" + return self._state + + def _turn_on_light(self): + """Turn on light child device.""" + set_req = self.gateway.const.SetReq + + if self._state: + return + self.gateway.set_child_value( + self.node_id, self.child_id, set_req.V_LIGHT, 1) + + if self.gateway.optimistic: + # optimistically assume that light has changed state + self._state = True + self._values[set_req.V_LIGHT] = STATE_ON + + def _turn_on_dimmer(self, **kwargs): + """Turn on dimmer child device.""" + set_req = self.gateway.const.SetReq + brightness = self._brightness + + if ATTR_BRIGHTNESS not in kwargs or \ + kwargs[ATTR_BRIGHTNESS] == self._brightness or \ + set_req.V_DIMMER not in self._values: + return + brightness = kwargs[ATTR_BRIGHTNESS] + percent = round(100 * brightness / 255) + self.gateway.set_child_value( + self.node_id, self.child_id, set_req.V_DIMMER, percent) + + if self.gateway.optimistic: + # optimistically assume that light has changed state + self._brightness = brightness + self._values[set_req.V_DIMMER] = percent + + def _turn_on_rgb_and_w(self, hex_template, **kwargs): + """Turn on RGB or RGBW child device.""" + rgb = list(color_util.color_hs_to_RGB(*self._hs)) + white = self._white + hex_color = self._values.get(self.value_type) + hs_color = kwargs.get(ATTR_HS_COLOR) + if hs_color is not None: + new_rgb = color_util.color_hs_to_RGB(*hs_color) + else: + new_rgb = None + new_white = kwargs.get(ATTR_WHITE_VALUE) + + if new_rgb is None and new_white is None: + return + if new_rgb is not None: + rgb = list(new_rgb) + if hex_template == '%02x%02x%02x%02x': + if new_white is not None: + rgb.append(new_white) + else: + rgb.append(white) + hex_color = hex_template % tuple(rgb) + if len(rgb) > 3: + white = rgb.pop() + self.gateway.set_child_value( + self.node_id, self.child_id, self.value_type, hex_color) + + if self.gateway.optimistic: + # optimistically assume that light has changed state + self._hs = color_util.color_RGB_to_hs(*rgb) + self._white = white + self._values[self.value_type] = hex_color + + async def async_turn_off(self, **kwargs): + """Turn the device off.""" + value_type = self.gateway.const.SetReq.V_LIGHT + self.gateway.set_child_value( + self.node_id, self.child_id, value_type, 0) + if self.gateway.optimistic: + # optimistically assume that light has changed state + self._state = False + self._values[value_type] = STATE_OFF + self.async_schedule_update_ha_state() + + def _async_update_light(self): + """Update the controller with values from light child.""" + value_type = self.gateway.const.SetReq.V_LIGHT + self._state = self._values[value_type] == STATE_ON + + def _async_update_dimmer(self): + """Update the controller with values from dimmer child.""" + value_type = self.gateway.const.SetReq.V_DIMMER + if value_type in self._values: + self._brightness = round(255 * int(self._values[value_type]) / 100) + if self._brightness == 0: + self._state = False + + def _async_update_rgb_or_w(self): + """Update the controller with values from RGB or RGBW child.""" + value = self._values[self.value_type] + color_list = rgb_hex_to_rgb_list(value) + if len(color_list) > 3: + self._white = color_list.pop() + self._hs = color_util.color_RGB_to_hs(*color_list) + + +class MySensorsLightDimmer(MySensorsLight): + """Dimmer child class to MySensorsLight.""" + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS + + async def async_turn_on(self, **kwargs): + """Turn the device on.""" + self._turn_on_light() + self._turn_on_dimmer(**kwargs) + if self.gateway.optimistic: + self.async_schedule_update_ha_state() + + async def async_update(self): + """Update the controller with the latest value from a sensor.""" + await super().async_update() + self._async_update_light() + self._async_update_dimmer() + + +class MySensorsLightRGB(MySensorsLight): + """RGB child class to MySensorsLight.""" + + @property + def supported_features(self): + """Flag supported features.""" + set_req = self.gateway.const.SetReq + if set_req.V_DIMMER in self._values: + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR + return SUPPORT_COLOR + + async def async_turn_on(self, **kwargs): + """Turn the device on.""" + self._turn_on_light() + self._turn_on_dimmer(**kwargs) + self._turn_on_rgb_and_w('%02x%02x%02x', **kwargs) + if self.gateway.optimistic: + self.async_schedule_update_ha_state() + + async def async_update(self): + """Update the controller with the latest value from a sensor.""" + await super().async_update() + self._async_update_light() + self._async_update_dimmer() + self._async_update_rgb_or_w() + + +class MySensorsLightRGBW(MySensorsLightRGB): + """RGBW child class to MySensorsLightRGB.""" + + # pylint: disable=too-many-ancestors + + @property + def supported_features(self): + """Flag supported features.""" + set_req = self.gateway.const.SetReq + if set_req.V_DIMMER in self._values: + return SUPPORT_BRIGHTNESS | SUPPORT_MYSENSORS_RGBW + return SUPPORT_MYSENSORS_RGBW + + async def async_turn_on(self, **kwargs): + """Turn the device on.""" + self._turn_on_light() + self._turn_on_dimmer(**kwargs) + self._turn_on_rgb_and_w('%02x%02x%02x%02x', **kwargs) + if self.gateway.optimistic: + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/mysensors/notify.py b/homeassistant/components/mysensors/notify.py new file mode 100644 index 0000000000000..ab198bc21bc08 --- /dev/null +++ b/homeassistant/components/mysensors/notify.py @@ -0,0 +1,45 @@ +"""MySensors notification service.""" +from homeassistant.components import mysensors +from homeassistant.components.notify import ( + ATTR_TARGET, DOMAIN, BaseNotificationService) + + +async def async_get_service(hass, config, discovery_info=None): + """Get the MySensors notification service.""" + new_devices = mysensors.setup_mysensors_platform( + hass, DOMAIN, discovery_info, MySensorsNotificationDevice) + if not new_devices: + return None + return MySensorsNotificationService(hass) + + +class MySensorsNotificationDevice(mysensors.device.MySensorsDevice): + """Represent a MySensors Notification device.""" + + def send_msg(self, msg): + """Send a message.""" + for sub_msg in [msg[i:i + 25] for i in range(0, len(msg), 25)]: + # Max mysensors payload is 25 bytes. + self.gateway.set_child_value( + self.node_id, self.child_id, self.value_type, sub_msg) + + def __repr__(self): + """Return the representation.""" + return "".format(self.name) + + +class MySensorsNotificationService(BaseNotificationService): + """Implement a MySensors notification service.""" + + def __init__(self, hass): + """Initialize the service.""" + self.devices = mysensors.get_mysensors_devices(hass, DOMAIN) + + async def async_send_message(self, message="", **kwargs): + """Send a message to a user.""" + target_devices = kwargs.get(ATTR_TARGET) + devices = [device for device in self.devices.values() + if target_devices is None or device.name in target_devices] + + for device in devices: + device.send_msg(message) diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py new file mode 100644 index 0000000000000..ce6d5da2b4ccd --- /dev/null +++ b/homeassistant/components/mysensors/sensor.py @@ -0,0 +1,83 @@ +"""Support for MySensors sensors.""" +from homeassistant.components import mysensors +from homeassistant.components.sensor import DOMAIN +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT + +SENSORS = { + 'V_TEMP': [None, 'mdi:thermometer'], + 'V_HUM': ['%', 'mdi:water-percent'], + 'V_DIMMER': ['%', 'mdi:percent'], + 'V_LIGHT_LEVEL': ['%', 'white-balance-sunny'], + 'V_DIRECTION': ['°', 'mdi:compass'], + 'V_WEIGHT': ['kg', 'mdi:weight-kilogram'], + 'V_DISTANCE': ['m', 'mdi:ruler'], + 'V_IMPEDANCE': ['ohm', None], + 'V_WATT': ['W', None], + 'V_KWH': ['kWh', None], + 'V_FLOW': ['m', None], + 'V_VOLUME': ['m³', None], + 'V_VOLTAGE': ['V', 'mdi:flash'], + 'V_CURRENT': ['A', 'mdi:flash-auto'], + 'V_PERCENTAGE': ['%', 'mdi:percent'], + 'V_LEVEL': { + 'S_SOUND': ['dB', 'mdi:volume-high'], 'S_VIBRATION': ['Hz', None], + 'S_LIGHT_LEVEL': ['lx', 'white-balance-sunny']}, + 'V_ORP': ['mV', None], + 'V_EC': ['μS/cm', None], + 'V_VAR': ['var', None], + 'V_VA': ['VA', None], +} + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Set up the MySensors platform for sensors.""" + mysensors.setup_mysensors_platform( + hass, DOMAIN, discovery_info, MySensorsSensor, + async_add_entities=async_add_entities) + + +class MySensorsSensor(mysensors.device.MySensorsEntity): + """Representation of a MySensors Sensor child node.""" + + @property + def force_update(self): + """Return True if state updates should be forced. + + If True, a state change will be triggered anytime the state property is + updated, not just when the value changes. + """ + return True + + @property + def state(self): + """Return the state of the device.""" + return self._values.get(self.value_type) + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + _, icon = self._get_sensor_type() + return icon + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity.""" + set_req = self.gateway.const.SetReq + if (float(self.gateway.protocol_version) >= 1.5 and + set_req.V_UNIT_PREFIX in self._values): + return self._values[set_req.V_UNIT_PREFIX] + unit, _ = self._get_sensor_type() + return unit + + def _get_sensor_type(self): + """Return list with unit and icon of sensor type.""" + pres = self.gateway.const.Presentation + set_req = self.gateway.const.SetReq + SENSORS[set_req.V_TEMP.name][0] = ( + TEMP_CELSIUS if self.gateway.metric else TEMP_FAHRENHEIT) + sensor_type = SENSORS.get(set_req(self.value_type).name, [None, None]) + if isinstance(sensor_type, dict): + sensor_type = sensor_type.get( + pres(self.child_type).name, [None, None]) + return sensor_type diff --git a/homeassistant/components/mysensors/switch.py b/homeassistant/components/mysensors/switch.py new file mode 100644 index 0000000000000..0ad9be1d50835 --- /dev/null +++ b/homeassistant/components/mysensors/switch.py @@ -0,0 +1,145 @@ +"""Support for MySensors switches.""" +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components import mysensors +from homeassistant.components.switch import DOMAIN, SwitchDevice +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON + +ATTR_IR_CODE = 'V_IR_SEND' +SERVICE_SEND_IR_CODE = 'mysensors_send_ir_code' + +SEND_IR_CODE_SERVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_IR_CODE): cv.string, +}) + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Set up the mysensors platform for switches.""" + device_class_map = { + 'S_DOOR': MySensorsSwitch, + 'S_MOTION': MySensorsSwitch, + 'S_SMOKE': MySensorsSwitch, + 'S_LIGHT': MySensorsSwitch, + 'S_LOCK': MySensorsSwitch, + 'S_IR': MySensorsIRSwitch, + 'S_BINARY': MySensorsSwitch, + 'S_SPRINKLER': MySensorsSwitch, + 'S_WATER_LEAK': MySensorsSwitch, + 'S_SOUND': MySensorsSwitch, + 'S_VIBRATION': MySensorsSwitch, + 'S_MOISTURE': MySensorsSwitch, + 'S_WATER_QUALITY': MySensorsSwitch, + } + mysensors.setup_mysensors_platform( + hass, DOMAIN, discovery_info, device_class_map, + async_add_entities=async_add_entities) + + async def async_send_ir_code_service(service): + """Set IR code as device state attribute.""" + entity_ids = service.data.get(ATTR_ENTITY_ID) + ir_code = service.data.get(ATTR_IR_CODE) + devices = mysensors.get_mysensors_devices(hass, DOMAIN) + + if entity_ids: + _devices = [device for device in devices.values() + if isinstance(device, MySensorsIRSwitch) and + device.entity_id in entity_ids] + else: + _devices = [device for device in devices.values() + if isinstance(device, MySensorsIRSwitch)] + + kwargs = {ATTR_IR_CODE: ir_code} + for device in _devices: + await device.async_turn_on(**kwargs) + + hass.services.async_register( + DOMAIN, SERVICE_SEND_IR_CODE, async_send_ir_code_service, + schema=SEND_IR_CODE_SERVICE_SCHEMA) + + +class MySensorsSwitch(mysensors.device.MySensorsEntity, SwitchDevice): + """Representation of the value of a MySensors Switch child node.""" + + @property + def assumed_state(self): + """Return True if unable to access real state of entity.""" + return self.gateway.optimistic + + @property + def current_power_w(self): + """Return the current power usage in W.""" + set_req = self.gateway.const.SetReq + return self._values.get(set_req.V_WATT) + + @property + def is_on(self): + """Return True if switch is on.""" + return self._values.get(self.value_type) == STATE_ON + + async def async_turn_on(self, **kwargs): + """Turn the switch on.""" + 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 + self._values[self.value_type] = STATE_ON + self.async_schedule_update_ha_state() + + async def async_turn_off(self, **kwargs): + """Turn the switch off.""" + 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 + self._values[self.value_type] = STATE_OFF + self.async_schedule_update_ha_state() + + +class MySensorsIRSwitch(MySensorsSwitch): + """IR switch child class to MySensorsSwitch.""" + + def __init__(self, *args): + """Set up instance attributes.""" + super().__init__(*args) + self._ir_code = None + + @property + def is_on(self): + """Return True if switch is on.""" + set_req = self.gateway.const.SetReq + return self._values.get(set_req.V_LIGHT) == STATE_ON + + async def async_turn_on(self, **kwargs): + """Turn the IR switch on.""" + set_req = self.gateway.const.SetReq + if ATTR_IR_CODE in kwargs: + self._ir_code = kwargs[ATTR_IR_CODE] + self.gateway.set_child_value( + self.node_id, self.child_id, self.value_type, self._ir_code) + 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 + 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 + await self.async_turn_off() + + async def async_turn_off(self, **kwargs): + """Turn the IR switch off.""" + set_req = self.gateway.const.SetReq + 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 + self._values[set_req.V_LIGHT] = STATE_OFF + self.async_schedule_update_ha_state() + + async def async_update(self): + """Update the controller with the latest value from a sensor.""" + await super().async_update() + self._ir_code = self._values.get(self.value_type) diff --git a/homeassistant/components/mythicbeastsdns.py b/homeassistant/components/mythicbeastsdns.py deleted file mode 100644 index d73e4619c789e..0000000000000 --- a/homeassistant/components/mythicbeastsdns.py +++ /dev/null @@ -1,58 +0,0 @@ -""" -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/ -""" -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.helpers.aiohttp_client import async_get_clientsession - -REQUIREMENTS = ['mbddns==0.1.2'] - -_LOGGER = logging.getLogger(__name__) - -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), - }) -}, extra=vol.ALLOW_EXTRA) - - -async def async_setup(hass, config): - """Initialize the Mythic Beasts component.""" - import mbddns - - domain = config[DOMAIN][CONF_DOMAIN] - password = config[DOMAIN][CONF_PASSWORD] - host = config[DOMAIN][CONF_HOST] - update_interval = config[DOMAIN][CONF_UPDATE_INTERVAL] - - session = async_get_clientsession(hass) - - result = await mbddns.update(domain, password, host, session=session) - - if not result: - return False - - async def update_domain_interval(now): - """Update the DNS entry.""" - await mbddns.update(domain, password, host, session=session) - - async_track_time_interval(hass, update_domain_interval, update_interval) - - return True diff --git a/homeassistant/components/mythicbeastsdns/__init__.py b/homeassistant/components/mythicbeastsdns/__init__.py new file mode 100644 index 0000000000000..3d0d250557bf4 --- /dev/null +++ b/homeassistant/components/mythicbeastsdns/__init__.py @@ -0,0 +1,66 @@ +"""Support for Mythic Beasts Dynamic DNS service.""" +from datetime import timedelta +import logging + +import voluptuous as vol + +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'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'mythicbeastsdns' + +DEFAULT_INTERVAL = timedelta(minutes=10) + +CONFIG_SCHEMA = vol.Schema({ + 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) + + +async def async_setup(hass, config): + """Initialize the Mythic Beasts component.""" + import mbddns + + domain = config[DOMAIN][CONF_DOMAIN] + password = config[DOMAIN][CONF_PASSWORD] + host = config[DOMAIN][CONF_HOST] + update_interval = config[DOMAIN][CONF_SCAN_INTERVAL] + + session = async_get_clientsession(hass) + + result = await mbddns.update(domain, password, host, session=session) + + if not result: + return False + + async def update_domain_interval(now): + """Update the DNS entry.""" + await mbddns.update(domain, password, host, session=session) + + async_track_time_interval(hass, update_domain_interval, update_interval) + + return True diff --git a/homeassistant/components/namecheapdns.py b/homeassistant/components/namecheapdns.py deleted file mode 100644 index f817544ca77be..0000000000000 --- a/homeassistant/components/namecheapdns.py +++ /dev/null @@ -1,77 +0,0 @@ -""" -Integrate with namecheap DNS services. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/namecheapdns/ -""" -import logging -from datetime import timedelta - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_DOMAIN -from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.aiohttp_client import async_get_clientsession - -REQUIREMENTS = ['defusedxml==0.5.0'] - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'namecheapdns' - -INTERVAL = timedelta(minutes=5) - -UPDATE_URL = 'https://dynamicdns.park-your-domain.com/update' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_DOMAIN): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_HOST, default='@'): cv.string, - }) -}, extra=vol.ALLOW_EXTRA) - - -async def async_setup(hass, config): - """Initialize the namecheap DNS component.""" - host = config[DOMAIN][CONF_HOST] - domain = config[DOMAIN][CONF_DOMAIN] - password = config[DOMAIN][CONF_PASSWORD] - - session = async_get_clientsession(hass) - - result = await _update_namecheapdns(session, host, domain, password) - - if not result: - return False - - async def update_domain_interval(now): - """Update the namecheap DNS entry.""" - await _update_namecheapdns(session, host, domain, password) - - async_track_time_interval(hass, update_domain_interval, INTERVAL) - - return result - - -async def _update_namecheapdns(session, host, domain, password): - """Update namecheap DNS entry.""" - import defusedxml.ElementTree as ET - - params = { - 'host': host, - 'domain': domain, - 'password': password, - } - - resp = await session.get(UPDATE_URL, params=params) - xml_string = await resp.text() - root = ET.fromstring(xml_string) - err_count = root.find('ErrCount').text - - if int(err_count) != 0: - _LOGGER.warning("Updating namecheap domain failed: %s", domain) - return False - - return True diff --git a/homeassistant/components/namecheapdns/__init__.py b/homeassistant/components/namecheapdns/__init__.py new file mode 100644 index 0000000000000..f86e7d1855678 --- /dev/null +++ b/homeassistant/components/namecheapdns/__init__.py @@ -0,0 +1,72 @@ +"""Support for namecheap DNS services.""" +import logging +from datetime import timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_DOMAIN +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +REQUIREMENTS = ['defusedxml==0.5.0'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'namecheapdns' + +INTERVAL = timedelta(minutes=5) + +UPDATE_URL = 'https://dynamicdns.park-your-domain.com/update' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_DOMAIN): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_HOST, default='@'): cv.string, + }) +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Initialize the namecheap DNS component.""" + host = config[DOMAIN][CONF_HOST] + domain = config[DOMAIN][CONF_DOMAIN] + password = config[DOMAIN][CONF_PASSWORD] + + session = async_get_clientsession(hass) + + result = await _update_namecheapdns(session, host, domain, password) + + if not result: + return False + + async def update_domain_interval(now): + """Update the namecheap DNS entry.""" + await _update_namecheapdns(session, host, domain, password) + + async_track_time_interval(hass, update_domain_interval, INTERVAL) + + return result + + +async def _update_namecheapdns(session, host, domain, password): + """Update namecheap DNS entry.""" + import defusedxml.ElementTree as ET + + params = { + 'host': host, + 'domain': domain, + 'password': password, + } + + resp = await session.get(UPDATE_URL, params=params) + xml_string = await resp.text() + root = ET.fromstring(xml_string) + err_count = root.find('ErrCount').text + + if int(err_count) != 0: + _LOGGER.warning("Updating namecheap domain failed: %s", domain) + return False + + return True diff --git a/homeassistant/components/neato.py b/homeassistant/components/neato.py deleted file mode 100644 index 31410d1c9b2cb..0000000000000 --- a/homeassistant/components/neato.py +++ /dev/null @@ -1,229 +0,0 @@ -""" -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/ -""" -import logging -from datetime import timedelta -from urllib.error import HTTPError - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -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'] - -DOMAIN = 'neato' -NEATO_ROBOTS = 'neato_robots' -NEATO_LOGIN = 'neato_login' -NEATO_MAP_DATA = 'neato_map_data' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - }) -}, extra=vol.ALLOW_EXTRA) - -MODE = { - 1: 'Eco', - 2: 'Turbo' -} - -ACTION = { - 0: 'Invalid', - 1: 'House Cleaning', - 2: 'Spot Cleaning', - 3: 'Manual Cleaning', - 4: 'Docking', - 5: 'User Menu Active', - 6: 'Suspended Cleaning', - 7: 'Updating', - 8: 'Copying logs', - 9: 'Recovering Location', - 10: 'IEC test', - 11: 'Map cleaning', - 12: 'Exploring map (creating a persistent map)', - 13: 'Acquiring Persistent Map IDs', - 14: 'Creating & Uploading Map', - 15: 'Suspended Exploration' -} - -ERRORS = { - 'ui_error_battery_battundervoltlithiumsafety': 'Replace battery', - 'ui_error_battery_critical': 'Replace battery', - 'ui_error_battery_invalidsensor': 'Replace battery', - 'ui_error_battery_lithiumadapterfailure': 'Replace battery', - 'ui_error_battery_mismatch': 'Replace battery', - 'ui_error_battery_nothermistor': 'Replace battery', - 'ui_error_battery_overtemp': 'Replace battery', - 'ui_error_battery_overvolt': 'Replace battery', - 'ui_error_battery_undercurrent': 'Replace battery', - 'ui_error_battery_undertemp': 'Replace battery', - 'ui_error_battery_undervolt': 'Replace battery', - 'ui_error_battery_unplugged': 'Replace battery', - 'ui_error_brush_stuck': 'Brush stuck', - 'ui_error_brush_overloaded': 'Brush overloaded', - 'ui_error_bumper_stuck': 'Bumper stuck', - 'ui_error_check_battery_switch': 'Check battery', - 'ui_error_corrupt_scb': 'Call customer service corrupt board', - 'ui_error_deck_debris': 'Deck debris', - 'ui_error_dflt_app': 'Check Neato app', - 'ui_error_disconnect_chrg_cable': 'Disconnected charge cable', - 'ui_error_disconnect_usb_cable': 'Disconnected USB cable', - 'ui_error_dust_bin_missing': 'Dust bin missing', - 'ui_error_dust_bin_full': 'Dust bin full', - 'ui_error_dust_bin_emptied': 'Dust bin emptied', - 'ui_error_hardware_failure': 'Hardware failure', - 'ui_error_ldrop_stuck': 'Clear my path', - 'ui_error_lds_jammed': 'Clear my path', - 'ui_error_lds_bad_packets': 'Check Neato app', - 'ui_error_lds_disconnected': 'Check Neato app', - 'ui_error_lds_missed_packets': 'Check Neato app', - 'ui_error_lwheel_stuck': 'Clear my path', - 'ui_error_navigation_backdrop_frontbump': 'Clear my path', - 'ui_error_navigation_backdrop_leftbump': 'Clear my path', - 'ui_error_navigation_backdrop_wheelextended': 'Clear my path', - 'ui_error_navigation_noprogress': 'Clear my path', - 'ui_error_navigation_origin_unclean': 'Clear my path', - 'ui_error_navigation_pathproblems': 'Cannot return to base', - 'ui_error_navigation_pinkycommsfail': 'Clear my path', - 'ui_error_navigation_falling': 'Clear my path', - 'ui_error_navigation_noexitstogo': 'Clear my path', - 'ui_error_navigation_nomotioncommands': 'Clear my path', - 'ui_error_navigation_rightdrop_leftbump': 'Clear my path', - 'ui_error_navigation_undockingfailed': 'Clear my path', - 'ui_error_picked_up': 'Picked up', - 'ui_error_qa_fail': 'Check Neato app', - 'ui_error_rdrop_stuck': 'Clear my path', - 'ui_error_reconnect_failed': 'Reconnect failed', - 'ui_error_rwheel_stuck': 'Clear my path', - 'ui_error_stuck': 'Stuck!', - 'ui_error_unable_to_return_to_base': 'Unable to return to base', - 'ui_error_unable_to_see': 'Clean vacuum sensors', - 'ui_error_vacuum_slip': 'Clear my path', - 'ui_error_vacuum_stuck': 'Clear my path', - 'ui_error_warning': 'Error check app', - 'batt_base_connect_fail': 'Battery failed to connect to base', - 'batt_base_no_power': 'Battery base has no power', - 'batt_low': 'Battery low', - 'batt_on_base': 'Battery on base', - 'clean_tilt_on_start': 'Clean the tilt on start', - 'dustbin_full': 'Dust bin full', - 'dustbin_missing': 'Dust bin missing', - 'gen_picked_up': 'Picked up', - 'hw_fail': 'Hardware failure', - 'hw_tof_sensor_sensor': 'Hardware sensor disconnected', - 'lds_bad_packets': 'Bad packets', - 'lds_deck_debris': 'Debris on deck', - 'lds_disconnected': 'Disconnected', - 'lds_jammed': 'Jammed', - 'lds_missed_packets': 'Missed packets', - 'maint_brush_stuck': 'Brush stuck', - 'maint_brush_overload': 'Brush overloaded', - 'maint_bumper_stuck': 'Bumper stuck', - 'maint_customer_support_qa': 'Contact customer support', - 'maint_vacuum_stuck': 'Vacuum is stuck', - 'maint_vacuum_slip': 'Vacuum is stuck', - 'maint_left_drop_stuck': 'Vacuum is stuck', - 'maint_left_wheel_stuck': 'Vacuum is stuck', - 'maint_right_drop_stuck': 'Vacuum is stuck', - 'maint_right_wheel_stuck': 'Vacuum is stuck', - 'not_on_charge_base': 'Not on the charge base', - 'nav_robot_falling': 'Clear my path', - 'nav_no_path': 'Clear my path', - 'nav_path_problem': 'Clear my path', - 'nav_backdrop_frontbump': 'Clear my path', - 'nav_backdrop_leftbump': 'Clear my path', - 'nav_backdrop_wheelextended': 'Clear my path', - 'nav_mag_sensor': 'Clear my path', - 'nav_no_exit': 'Clear my path', - 'nav_no_movement': 'Clear my path', - 'nav_rightdrop_leftbump': 'Clear my path', - 'nav_undocking_failed': 'Clear my path' -} - -ALERTS = { - 'ui_alert_dust_bin_full': 'Please empty dust bin', - 'ui_alert_recovering_location': 'Returning to start', - 'ui_alert_battery_chargebasecommerr': 'Battery error', - 'ui_alert_busy_charging': 'Busy charging', - 'ui_alert_charging_base': 'Base charging', - 'ui_alert_charging_power': 'Charging power', - 'ui_alert_connect_chrg_cable': 'Connect charge cable', - 'ui_alert_info_thank_you': 'Thank you', - 'ui_alert_invalid': 'Invalid check app', - 'ui_alert_old_error': 'Old error', - 'ui_alert_swupdate_fail': 'Update failed', - 'dustbin_full': 'Please empty dust bin', - 'maint_brush_change': 'Change the brush', - 'maint_filter_change': 'Change the filter', - 'clean_completed_to_start': 'Cleaning completed', - 'nav_floorplan_not_created': 'No floorplan found', - 'nav_floorplan_load_fail': 'Failed to load floorplan', - 'nav_floorplan_localization_fail': 'Failed to load floorplan', - 'clean_incomplete_to_start': 'Cleaning incomplete', - 'log_upload_failed': 'Logs failed to upload' -} - - -def setup(hass, config): - """Set up the Neato component.""" - from pybotvac import Account - - hass.data[NEATO_LOGIN] = NeatoHub(hass, config[DOMAIN], Account) - hub = hass.data[NEATO_LOGIN] - if not hub.login(): - _LOGGER.debug("Failed to login to Neato API") - return False - hub.update_robots() - for component in ('camera', 'vacuum', 'switch'): - discovery.load_platform(hass, component, DOMAIN, {}, config) - - return True - - -class NeatoHub: - """A My Neato hub wrapper class.""" - - def __init__(self, hass, domain_config, neato): - """Initialize the Neato hub.""" - self.config = domain_config - self._neato = neato - self._hass = hass - - self.my_neato = neato( - domain_config[CONF_USERNAME], - domain_config[CONF_PASSWORD]) - self._hass.data[NEATO_ROBOTS] = self.my_neato.robots - self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps - - def login(self): - """Login to My Neato.""" - try: - _LOGGER.debug("Trying to connect to Neato API") - self.my_neato = self._neato( - self.config[CONF_USERNAME], self.config[CONF_PASSWORD]) - return True - except HTTPError: - _LOGGER.error("Unable to connect to Neato API") - return False - - @Throttle(timedelta(seconds=300)) - def update_robots(self): - """Update the robot states.""" - _LOGGER.debug("Running HUB.update_robots %s", - self._hass.data[NEATO_ROBOTS]) - self._hass.data[NEATO_ROBOTS] = self.my_neato.robots - self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps - - def download_map(self, url): - """Download a new map image.""" - map_image_data = self.my_neato.get_map_image(url) - return map_image_data diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py new file mode 100644 index 0000000000000..2b4af3e1e9129 --- /dev/null +++ b/homeassistant/components/neato/__init__.py @@ -0,0 +1,224 @@ +"""Support for Neato botvac connected vacuum cleaners.""" +import logging +from datetime import timedelta +from urllib.error import HTTPError + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers import discovery +from homeassistant.util import Throttle + +REQUIREMENTS = ['pybotvac==0.0.13'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'neato' +NEATO_ROBOTS = 'neato_robots' +NEATO_LOGIN = 'neato_login' +NEATO_MAP_DATA = 'neato_map_data' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + }) +}, extra=vol.ALLOW_EXTRA) + +MODE = { + 1: 'Eco', + 2: 'Turbo' +} + +ACTION = { + 0: 'Invalid', + 1: 'House Cleaning', + 2: 'Spot Cleaning', + 3: 'Manual Cleaning', + 4: 'Docking', + 5: 'User Menu Active', + 6: 'Suspended Cleaning', + 7: 'Updating', + 8: 'Copying logs', + 9: 'Recovering Location', + 10: 'IEC test', + 11: 'Map cleaning', + 12: 'Exploring map (creating a persistent map)', + 13: 'Acquiring Persistent Map IDs', + 14: 'Creating & Uploading Map', + 15: 'Suspended Exploration' +} + +ERRORS = { + 'ui_error_battery_battundervoltlithiumsafety': 'Replace battery', + 'ui_error_battery_critical': 'Replace battery', + 'ui_error_battery_invalidsensor': 'Replace battery', + 'ui_error_battery_lithiumadapterfailure': 'Replace battery', + 'ui_error_battery_mismatch': 'Replace battery', + 'ui_error_battery_nothermistor': 'Replace battery', + 'ui_error_battery_overtemp': 'Replace battery', + 'ui_error_battery_overvolt': 'Replace battery', + 'ui_error_battery_undercurrent': 'Replace battery', + 'ui_error_battery_undertemp': 'Replace battery', + 'ui_error_battery_undervolt': 'Replace battery', + 'ui_error_battery_unplugged': 'Replace battery', + 'ui_error_brush_stuck': 'Brush stuck', + 'ui_error_brush_overloaded': 'Brush overloaded', + 'ui_error_bumper_stuck': 'Bumper stuck', + 'ui_error_check_battery_switch': 'Check battery', + 'ui_error_corrupt_scb': 'Call customer service corrupt board', + 'ui_error_deck_debris': 'Deck debris', + 'ui_error_dflt_app': 'Check Neato app', + 'ui_error_disconnect_chrg_cable': 'Disconnected charge cable', + 'ui_error_disconnect_usb_cable': 'Disconnected USB cable', + 'ui_error_dust_bin_missing': 'Dust bin missing', + 'ui_error_dust_bin_full': 'Dust bin full', + 'ui_error_dust_bin_emptied': 'Dust bin emptied', + 'ui_error_hardware_failure': 'Hardware failure', + 'ui_error_ldrop_stuck': 'Clear my path', + 'ui_error_lds_jammed': 'Clear my path', + 'ui_error_lds_bad_packets': 'Check Neato app', + 'ui_error_lds_disconnected': 'Check Neato app', + 'ui_error_lds_missed_packets': 'Check Neato app', + 'ui_error_lwheel_stuck': 'Clear my path', + 'ui_error_navigation_backdrop_frontbump': 'Clear my path', + 'ui_error_navigation_backdrop_leftbump': 'Clear my path', + 'ui_error_navigation_backdrop_wheelextended': 'Clear my path', + 'ui_error_navigation_noprogress': 'Clear my path', + 'ui_error_navigation_origin_unclean': 'Clear my path', + 'ui_error_navigation_pathproblems': 'Cannot return to base', + 'ui_error_navigation_pinkycommsfail': 'Clear my path', + 'ui_error_navigation_falling': 'Clear my path', + 'ui_error_navigation_noexitstogo': 'Clear my path', + 'ui_error_navigation_nomotioncommands': 'Clear my path', + 'ui_error_navigation_rightdrop_leftbump': 'Clear my path', + 'ui_error_navigation_undockingfailed': 'Clear my path', + 'ui_error_picked_up': 'Picked up', + 'ui_error_qa_fail': 'Check Neato app', + 'ui_error_rdrop_stuck': 'Clear my path', + 'ui_error_reconnect_failed': 'Reconnect failed', + 'ui_error_rwheel_stuck': 'Clear my path', + 'ui_error_stuck': 'Stuck!', + 'ui_error_unable_to_return_to_base': 'Unable to return to base', + 'ui_error_unable_to_see': 'Clean vacuum sensors', + 'ui_error_vacuum_slip': 'Clear my path', + 'ui_error_vacuum_stuck': 'Clear my path', + 'ui_error_warning': 'Error check app', + 'batt_base_connect_fail': 'Battery failed to connect to base', + 'batt_base_no_power': 'Battery base has no power', + 'batt_low': 'Battery low', + 'batt_on_base': 'Battery on base', + 'clean_tilt_on_start': 'Clean the tilt on start', + 'dustbin_full': 'Dust bin full', + 'dustbin_missing': 'Dust bin missing', + 'gen_picked_up': 'Picked up', + 'hw_fail': 'Hardware failure', + 'hw_tof_sensor_sensor': 'Hardware sensor disconnected', + 'lds_bad_packets': 'Bad packets', + 'lds_deck_debris': 'Debris on deck', + 'lds_disconnected': 'Disconnected', + 'lds_jammed': 'Jammed', + 'lds_missed_packets': 'Missed packets', + 'maint_brush_stuck': 'Brush stuck', + 'maint_brush_overload': 'Brush overloaded', + 'maint_bumper_stuck': 'Bumper stuck', + 'maint_customer_support_qa': 'Contact customer support', + 'maint_vacuum_stuck': 'Vacuum is stuck', + 'maint_vacuum_slip': 'Vacuum is stuck', + 'maint_left_drop_stuck': 'Vacuum is stuck', + 'maint_left_wheel_stuck': 'Vacuum is stuck', + 'maint_right_drop_stuck': 'Vacuum is stuck', + 'maint_right_wheel_stuck': 'Vacuum is stuck', + 'not_on_charge_base': 'Not on the charge base', + 'nav_robot_falling': 'Clear my path', + 'nav_no_path': 'Clear my path', + 'nav_path_problem': 'Clear my path', + 'nav_backdrop_frontbump': 'Clear my path', + 'nav_backdrop_leftbump': 'Clear my path', + 'nav_backdrop_wheelextended': 'Clear my path', + 'nav_mag_sensor': 'Clear my path', + 'nav_no_exit': 'Clear my path', + 'nav_no_movement': 'Clear my path', + 'nav_rightdrop_leftbump': 'Clear my path', + 'nav_undocking_failed': 'Clear my path' +} + +ALERTS = { + 'ui_alert_dust_bin_full': 'Please empty dust bin', + 'ui_alert_recovering_location': 'Returning to start', + 'ui_alert_battery_chargebasecommerr': 'Battery error', + 'ui_alert_busy_charging': 'Busy charging', + 'ui_alert_charging_base': 'Base charging', + 'ui_alert_charging_power': 'Charging power', + 'ui_alert_connect_chrg_cable': 'Connect charge cable', + 'ui_alert_info_thank_you': 'Thank you', + 'ui_alert_invalid': 'Invalid check app', + 'ui_alert_old_error': 'Old error', + 'ui_alert_swupdate_fail': 'Update failed', + 'dustbin_full': 'Please empty dust bin', + 'maint_brush_change': 'Change the brush', + 'maint_filter_change': 'Change the filter', + 'clean_completed_to_start': 'Cleaning completed', + 'nav_floorplan_not_created': 'No floorplan found', + 'nav_floorplan_load_fail': 'Failed to load floorplan', + 'nav_floorplan_localization_fail': 'Failed to load floorplan', + 'clean_incomplete_to_start': 'Cleaning incomplete', + 'log_upload_failed': 'Logs failed to upload' +} + + +def setup(hass, config): + """Set up the Neato component.""" + from pybotvac import Account + + hass.data[NEATO_LOGIN] = NeatoHub(hass, config[DOMAIN], Account) + hub = hass.data[NEATO_LOGIN] + if not hub.login(): + _LOGGER.debug("Failed to login to Neato API") + return False + hub.update_robots() + for component in ('camera', 'vacuum', 'switch'): + discovery.load_platform(hass, component, DOMAIN, {}, config) + + return True + + +class NeatoHub: + """A My Neato hub wrapper class.""" + + def __init__(self, hass, domain_config, neato): + """Initialize the Neato hub.""" + self.config = domain_config + self._neato = neato + self._hass = hass + + self.my_neato = neato( + domain_config[CONF_USERNAME], + domain_config[CONF_PASSWORD]) + self._hass.data[NEATO_ROBOTS] = self.my_neato.robots + self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps + + def login(self): + """Login to My Neato.""" + try: + _LOGGER.debug("Trying to connect to Neato API") + self.my_neato = self._neato( + self.config[CONF_USERNAME], self.config[CONF_PASSWORD]) + return True + except HTTPError: + _LOGGER.error("Unable to connect to Neato API") + return False + + @Throttle(timedelta(seconds=300)) + def update_robots(self): + """Update the robot states.""" + _LOGGER.debug("Running HUB.update_robots %s", + self._hass.data[NEATO_ROBOTS]) + self._hass.data[NEATO_ROBOTS] = self.my_neato.robots + self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps + + def download_map(self, url): + """Download a new map image.""" + map_image_data = self.my_neato.get_map_image(url) + return map_image_data diff --git a/homeassistant/components/neato/camera.py b/homeassistant/components/neato/camera.py new file mode 100644 index 0000000000000..530aa8fc6f197 --- /dev/null +++ b/homeassistant/components/neato/camera.py @@ -0,0 +1,65 @@ +"""Support for loading picture from Neato.""" +import logging + +from datetime import timedelta +from homeassistant.components.camera import Camera +from homeassistant.components.neato import ( + NEATO_MAP_DATA, NEATO_ROBOTS, NEATO_LOGIN) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['neato'] + +SCAN_INTERVAL = timedelta(minutes=10) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Neato Camera.""" + dev = [] + for robot in hass.data[NEATO_ROBOTS]: + if 'maps' in robot.traits: + dev.append(NeatoCleaningMap(hass, robot)) + _LOGGER.debug("Adding robots for cleaning maps %s", dev) + add_entities(dev, True) + + +class NeatoCleaningMap(Camera): + """Neato cleaning map for last clean.""" + + def __init__(self, hass, robot): + """Initialize Neato cleaning map.""" + super().__init__() + self.robot = robot + self._robot_name = '{} {}'.format(self.robot.name, 'Cleaning Map') + self._robot_serial = self.robot.serial + self.neato = hass.data[NEATO_LOGIN] + self._image_url = None + self._image = None + + def camera_image(self): + """Return image response.""" + self.update() + return self._image + + def update(self): + """Check the contents of the map list.""" + self.neato.update_robots() + image_url = None + map_data = self.hass.data[NEATO_MAP_DATA] + image_url = map_data[self._robot_serial]['maps'][0]['url'] + if image_url == self._image_url: + _LOGGER.debug("The map image_url is the same as old") + return + image = self.neato.download_map(image_url) + self._image = image.read() + self._image_url = image_url + + @property + def name(self): + """Return the name of this camera.""" + return self._robot_name + + @property + def unique_id(self): + """Return unique ID.""" + return self._robot_serial diff --git a/homeassistant/components/neato/switch.py b/homeassistant/components/neato/switch.py new file mode 100644 index 0000000000000..fcc72762b8d05 --- /dev/null +++ b/homeassistant/components/neato/switch.py @@ -0,0 +1,103 @@ +"""Support for Neato Connected Vacuums switches.""" +import logging +from datetime import timedelta +import requests +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.components.neato import NEATO_ROBOTS, NEATO_LOGIN + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['neato'] + +SCAN_INTERVAL = timedelta(minutes=10) + +SWITCH_TYPE_SCHEDULE = 'schedule' + +SWITCH_TYPES = { + SWITCH_TYPE_SCHEDULE: ['Schedule'] +} + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Neato switches.""" + dev = [] + for robot in hass.data[NEATO_ROBOTS]: + for type_name in SWITCH_TYPES: + dev.append(NeatoConnectedSwitch(hass, robot, type_name)) + _LOGGER.debug("Adding switches %s", dev) + add_entities(dev) + + +class NeatoConnectedSwitch(ToggleEntity): + """Neato Connected Switches.""" + + def __init__(self, hass, robot, switch_type): + """Initialize the Neato Connected switches.""" + self.type = switch_type + self.robot = robot + self.neato = hass.data[NEATO_LOGIN] + self._robot_name = '{} {}'.format( + self.robot.name, SWITCH_TYPES[self.type][0]) + try: + self._state = self.robot.state + except (requests.exceptions.ConnectionError, + requests.exceptions.HTTPError) as ex: + _LOGGER.warning("Neato connection error: %s", ex) + self._state = None + self._schedule_state = None + self._clean_state = None + self._robot_serial = self.robot.serial + + def update(self): + """Update the states of Neato switches.""" + _LOGGER.debug("Running switch update") + self.neato.update_robots() + try: + self._state = self.robot.state + except (requests.exceptions.ConnectionError, + requests.exceptions.HTTPError) as ex: + _LOGGER.warning("Neato connection error: %s", ex) + self._state = None + return + _LOGGER.debug('self._state=%s', self._state) + if self.type == SWITCH_TYPE_SCHEDULE: + _LOGGER.debug("State: %s", self._state) + if self._state['details']['isScheduleEnabled']: + self._schedule_state = STATE_ON + else: + self._schedule_state = STATE_OFF + _LOGGER.debug("Schedule state: %s", self._schedule_state) + + @property + def name(self): + """Return the name of the switch.""" + return self._robot_name + + @property + def available(self): + """Return True if entity is available.""" + return self._state + + @property + def unique_id(self): + """Return a unique ID.""" + return self._robot_serial + + @property + def is_on(self): + """Return true if switch is on.""" + if self.type == SWITCH_TYPE_SCHEDULE: + if self._schedule_state == STATE_ON: + return True + return False + + def turn_on(self, **kwargs): + """Turn the switch on.""" + if self.type == SWITCH_TYPE_SCHEDULE: + self.robot.enable_schedule() + + def turn_off(self, **kwargs): + """Turn the switch off.""" + if self.type == SWITCH_TYPE_SCHEDULE: + self.robot.disable_schedule() diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py new file mode 100644 index 0000000000000..45cfd273aca42 --- /dev/null +++ b/homeassistant/components/neato/vacuum.py @@ -0,0 +1,226 @@ +"""Support for Neato Connected Vacuums.""" +import logging +from datetime import timedelta +import requests + +from homeassistant.components.vacuum import ( + StateVacuumDevice, SUPPORT_BATTERY, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, + SUPPORT_STATE, SUPPORT_STOP, SUPPORT_START, STATE_IDLE, + STATE_PAUSED, STATE_CLEANING, STATE_DOCKED, STATE_RETURNING, STATE_ERROR, + SUPPORT_MAP, ATTR_STATUS, ATTR_BATTERY_LEVEL, ATTR_BATTERY_ICON, + SUPPORT_LOCATE, SUPPORT_CLEAN_SPOT) +from homeassistant.components.neato import ( + NEATO_ROBOTS, NEATO_LOGIN, NEATO_MAP_DATA, ACTION, ERRORS, MODE, ALERTS) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['neato'] + +SCAN_INTERVAL = timedelta(minutes=5) + +SUPPORT_NEATO = SUPPORT_BATTERY | SUPPORT_PAUSE | SUPPORT_RETURN_HOME | \ + SUPPORT_STOP | SUPPORT_START | SUPPORT_CLEAN_SPOT | \ + SUPPORT_STATE | SUPPORT_MAP | SUPPORT_LOCATE + +ATTR_CLEAN_START = 'clean_start' +ATTR_CLEAN_STOP = 'clean_stop' +ATTR_CLEAN_AREA = 'clean_area' +ATTR_CLEAN_BATTERY_START = 'battery_level_at_clean_start' +ATTR_CLEAN_BATTERY_END = 'battery_level_at_clean_end' +ATTR_CLEAN_SUSP_COUNT = 'clean_suspension_count' +ATTR_CLEAN_SUSP_TIME = 'clean_suspension_time' + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Neato vacuum.""" + dev = [] + for robot in hass.data[NEATO_ROBOTS]: + dev.append(NeatoConnectedVacuum(hass, robot)) + _LOGGER.debug("Adding vacuums %s", dev) + add_entities(dev, True) + + +class NeatoConnectedVacuum(StateVacuumDevice): + """Representation of a Neato Connected Vacuum.""" + + def __init__(self, hass, robot): + """Initialize the Neato Connected Vacuum.""" + self.robot = robot + self.neato = hass.data[NEATO_LOGIN] + self._name = '{}'.format(self.robot.name) + self._status_state = None + self._clean_state = None + self._state = None + self._mapdata = hass.data[NEATO_MAP_DATA] + self.clean_time_start = None + self.clean_time_stop = None + self.clean_area = None + self.clean_battery_start = None + self.clean_battery_end = None + self.clean_suspension_charge_count = None + self.clean_suspension_time = None + self._available = False + self._battery_level = None + self._robot_serial = self.robot.serial + + def update(self): + """Update the states of Neato Vacuums.""" + _LOGGER.debug("Running Neato Vacuums update") + self.neato.update_robots() + try: + self._state = self.robot.state + self._available = True + except (requests.exceptions.ConnectionError, + requests.exceptions.HTTPError) as ex: + _LOGGER.warning("Neato connection error: %s", ex) + self._state = None + self._available = False + return + _LOGGER.debug('self._state=%s', self._state) + if 'alert' in self._state: + robot_alert = ALERTS.get(self._state['alert']) + else: + robot_alert = None + if self._state['state'] == 1: + if self._state['details']['isCharging']: + self._clean_state = STATE_DOCKED + self._status_state = 'Charging' + elif (self._state['details']['isDocked'] and + not self._state['details']['isCharging']): + self._clean_state = STATE_DOCKED + self._status_state = 'Docked' + else: + self._clean_state = STATE_IDLE + self._status_state = 'Stopped' + + if robot_alert is not None: + self._status_state = robot_alert + elif self._state['state'] == 2: + if robot_alert is None: + self._clean_state = STATE_CLEANING + self._status_state = ( + MODE.get(self._state['cleaning']['mode']) + + ' ' + ACTION.get(self._state['action'])) + else: + self._status_state = robot_alert + elif self._state['state'] == 3: + self._clean_state = STATE_PAUSED + self._status_state = 'Paused' + elif self._state['state'] == 4: + self._clean_state = STATE_ERROR + self._status_state = ERRORS.get(self._state['error']) + + if not self._mapdata.get(self._robot_serial, {}).get('maps', []): + return + self.clean_time_start = ( + (self._mapdata[self._robot_serial]['maps'][0]['start_at'] + .strip('Z')) + .replace('T', ' ')) + self.clean_time_stop = ( + (self._mapdata[self._robot_serial]['maps'][0]['end_at'].strip('Z')) + .replace('T', ' ')) + self.clean_area = ( + self._mapdata[self._robot_serial]['maps'][0]['cleaned_area']) + self.clean_suspension_charge_count = ( + self._mapdata[self._robot_serial]['maps'][0] + ['suspended_cleaning_charging_count']) + self.clean_suspension_time = ( + self._mapdata[self._robot_serial]['maps'][0] + ['time_in_suspended_cleaning']) + self.clean_battery_start = ( + self._mapdata[self._robot_serial]['maps'][0]['run_charge_at_start'] + ) + self.clean_battery_end = ( + self._mapdata[self._robot_serial]['maps'][0]['run_charge_at_end']) + + self._battery_level = self._state['details']['charge'] + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def supported_features(self): + """Flag vacuum cleaner robot features that are supported.""" + return SUPPORT_NEATO + + @property + def battery_level(self): + """Return the battery level of the vacuum cleaner.""" + return self._battery_level + + @property + def available(self): + """Return if the robot is available.""" + return self._available + + @property + def state(self): + """Return the status of the vacuum cleaner.""" + return self._clean_state + + @property + def unique_id(self): + """Return a unique ID.""" + return self._robot_serial + + @property + def device_state_attributes(self): + """Return the state attributes of the vacuum cleaner.""" + data = {} + + if self._status_state is not None: + data[ATTR_STATUS] = self._status_state + + if self.battery_level is not None: + data[ATTR_BATTERY_LEVEL] = self.battery_level + data[ATTR_BATTERY_ICON] = self.battery_icon + + if self.clean_time_start is not None: + data[ATTR_CLEAN_START] = self.clean_time_start + if self.clean_time_stop is not None: + data[ATTR_CLEAN_STOP] = self.clean_time_stop + if self.clean_area is not None: + data[ATTR_CLEAN_AREA] = self.clean_area + if self.clean_suspension_charge_count is not None: + data[ATTR_CLEAN_SUSP_COUNT] = ( + self.clean_suspension_charge_count) + if self.clean_suspension_time is not None: + data[ATTR_CLEAN_SUSP_TIME] = self.clean_suspension_time + if self.clean_battery_start is not None: + data[ATTR_CLEAN_BATTERY_START] = self.clean_battery_start + if self.clean_battery_end is not None: + data[ATTR_CLEAN_BATTERY_END] = self.clean_battery_end + + return data + + def start(self): + """Start cleaning or resume cleaning.""" + if self._state['state'] == 1: + self.robot.start_cleaning() + elif self._state['state'] == 3: + self.robot.resume_cleaning() + + def pause(self): + """Pause the vacuum.""" + self.robot.pause_cleaning() + + def return_to_base(self, **kwargs): + """Set the vacuum cleaner to return to the dock.""" + if self._clean_state == STATE_CLEANING: + self.robot.pause_cleaning() + self._clean_state = STATE_RETURNING + self.robot.send_to_base() + + def stop(self, **kwargs): + """Stop the vacuum cleaner.""" + self.robot.stop_cleaning() + + def locate(self, **kwargs): + """Locate the robot by making it emit a sound.""" + self.robot.locate() + + def clean_spot(self, **kwargs): + """Run a spot cleaning starting from the base.""" + self.robot.start_spot_cleaning() diff --git a/homeassistant/components/ness_alarm.py b/homeassistant/components/ness_alarm.py deleted file mode 100644 index e97ee903abccf..0000000000000 --- a/homeassistant/components/ness_alarm.py +++ /dev/null @@ -1,121 +0,0 @@ -""" -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 -from collections import namedtuple - -import voluptuous as vol - -from homeassistant.components.binary_sensor import DEVICE_CLASSES -from homeassistant.const import 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 - -REQUIREMENTS = ['nessclient==0.9.9'] - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'ness_alarm' -DATA_NESS = 'ness_alarm' - -CONF_DEVICE_HOST = 'host' -CONF_DEVICE_PORT = 'port' -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' -SIGNAL_ARMING_STATE_CHANGED = 'ness_alarm.arming_state_changed' - -ZoneChangedData = namedtuple('ZoneChangedData', ['zone_id', 'state']) - -DEFAULT_ZONE_TYPE = 'motion' -ZONE_SCHEMA = vol.Schema({ - vol.Required(CONF_ZONE_NAME): cv.string, - vol.Required(CONF_ZONE_ID): cv.positive_int, - vol.Optional(CONF_ZONE_TYPE, default=DEFAULT_ZONE_TYPE): - vol.In(DEVICE_CLASSES)}) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_DEVICE_HOST): cv.string, - vol.Required(CONF_DEVICE_PORT): cv.port, - vol.Optional(CONF_ZONES, default=DEFAULT_ZONES): - vol.All(cv.ensure_list, [ZONE_SCHEMA]), - }), -}, extra=vol.ALLOW_EXTRA) - -SERVICE_PANIC = 'panic' -SERVICE_AUX = 'aux' - -SERVICE_SCHEMA_PANIC = vol.Schema({ - vol.Required(ATTR_CODE): cv.string, -}) -SERVICE_SCHEMA_AUX = vol.Schema({ - vol.Required(ATTR_OUTPUT_ID): cv.positive_int, - vol.Optional(ATTR_STATE, default=True): cv.boolean, -}) - - -async def async_setup(hass, config): - """Set up the Ness Alarm platform.""" - from nessclient import Client, ArmingState - conf = config[DOMAIN] - - zones = conf[CONF_ZONES] - host = conf[CONF_DEVICE_HOST] - port = conf[CONF_DEVICE_PORT] - - client = Client(host=host, port=port, loop=hass.loop) - hass.data[DATA_NESS] = client - - async def _close(event): - await client.close() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _close) - - hass.async_create_task( - async_load_platform(hass, 'binary_sensor', DOMAIN, {CONF_ZONES: zones}, - config)) - hass.async_create_task( - async_load_platform(hass, 'alarm_control_panel', DOMAIN, {}, config)) - - def on_zone_change(zone_id: int, state: bool): - """Receives and propagates zone state updates.""" - async_dispatcher_send(hass, SIGNAL_ZONE_CHANGED, ZoneChangedData( - zone_id=zone_id, - state=state, - )) - - def on_state_change(arming_state: ArmingState): - """Receives and propagates arming state updates.""" - async_dispatcher_send(hass, SIGNAL_ARMING_STATE_CHANGED, arming_state) - - client.on_zone_change(on_zone_change) - client.on_state_change(on_state_change) - - # Force update for current arming status and current zone states - hass.loop.create_task(client.keepalive()) - hass.loop.create_task(client.update()) - - async def handle_panic(call): - await client.panic(call.data[ATTR_CODE]) - - async def handle_aux(call): - await client.aux(call.data[ATTR_OUTPUT_ID], call.data[ATTR_STATE]) - - hass.services.async_register(DOMAIN, SERVICE_PANIC, handle_panic, - schema=SERVICE_SCHEMA_PANIC) - hass.services.async_register(DOMAIN, SERVICE_AUX, handle_aux, - schema=SERVICE_SCHEMA_AUX) - - return True diff --git a/homeassistant/components/ness_alarm/__init__.py b/homeassistant/components/ness_alarm/__init__.py new file mode 100644 index 0000000000000..dae244ece3fd9 --- /dev/null +++ b/homeassistant/components/ness_alarm/__init__.py @@ -0,0 +1,114 @@ +"""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 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 + +REQUIREMENTS = ['nessclient==0.9.9'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'ness_alarm' +DATA_NESS = 'ness_alarm' + +CONF_DEVICE_HOST = 'host' +CONF_DEVICE_PORT = 'port' +CONF_ZONES = 'zones' +CONF_ZONE_NAME = 'name' +CONF_ZONE_TYPE = 'type' +CONF_ZONE_ID = 'id' +ATTR_OUTPUT_ID = 'output_id' +DEFAULT_ZONES = [] + +SIGNAL_ZONE_CHANGED = 'ness_alarm.zone_changed' +SIGNAL_ARMING_STATE_CHANGED = 'ness_alarm.arming_state_changed' + +ZoneChangedData = namedtuple('ZoneChangedData', ['zone_id', 'state']) + +DEFAULT_ZONE_TYPE = 'motion' +ZONE_SCHEMA = vol.Schema({ + vol.Required(CONF_ZONE_NAME): cv.string, + vol.Required(CONF_ZONE_ID): cv.positive_int, + vol.Optional(CONF_ZONE_TYPE, default=DEFAULT_ZONE_TYPE): + vol.In(DEVICE_CLASSES)}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_DEVICE_HOST): cv.string, + vol.Required(CONF_DEVICE_PORT): cv.port, + vol.Optional(CONF_ZONES, default=DEFAULT_ZONES): + vol.All(cv.ensure_list, [ZONE_SCHEMA]), + }), +}, extra=vol.ALLOW_EXTRA) + +SERVICE_PANIC = 'panic' +SERVICE_AUX = 'aux' + +SERVICE_SCHEMA_PANIC = vol.Schema({ + vol.Required(ATTR_CODE): cv.string, +}) +SERVICE_SCHEMA_AUX = vol.Schema({ + vol.Required(ATTR_OUTPUT_ID): cv.positive_int, + vol.Optional(ATTR_STATE, default=True): cv.boolean, +}) + + +async def async_setup(hass, config): + """Set up the Ness Alarm platform.""" + from nessclient import Client, ArmingState + conf = config[DOMAIN] + + zones = conf[CONF_ZONES] + host = conf[CONF_DEVICE_HOST] + port = conf[CONF_DEVICE_PORT] + + client = Client(host=host, port=port, loop=hass.loop) + hass.data[DATA_NESS] = client + + async def _close(event): + await client.close() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _close) + + hass.async_create_task( + async_load_platform(hass, 'binary_sensor', DOMAIN, {CONF_ZONES: zones}, + config)) + hass.async_create_task( + async_load_platform(hass, 'alarm_control_panel', DOMAIN, {}, config)) + + def on_zone_change(zone_id: int, state: bool): + """Receives and propagates zone state updates.""" + async_dispatcher_send(hass, SIGNAL_ZONE_CHANGED, ZoneChangedData( + zone_id=zone_id, + state=state, + )) + + def on_state_change(arming_state: ArmingState): + """Receives and propagates arming state updates.""" + async_dispatcher_send(hass, SIGNAL_ARMING_STATE_CHANGED, arming_state) + + client.on_zone_change(on_zone_change) + client.on_state_change(on_state_change) + + # Force update for current arming status and current zone states + hass.loop.create_task(client.keepalive()) + hass.loop.create_task(client.update()) + + async def handle_panic(call): + await client.panic(call.data[ATTR_CODE]) + + async def handle_aux(call): + await client.aux(call.data[ATTR_OUTPUT_ID], call.data[ATTR_STATE]) + + hass.services.async_register(DOMAIN, SERVICE_PANIC, handle_panic, + schema=SERVICE_SCHEMA_PANIC) + hass.services.async_register(DOMAIN, SERVICE_AUX, handle_aux, + schema=SERVICE_SCHEMA_AUX) + + return True diff --git a/homeassistant/components/nest/.translations/da.json b/homeassistant/components/nest/.translations/da.json index 5edf3a00af41e..7dfd1c8b250f6 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 7f0fe27df73ea..fe6a34cf4044b 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_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 @@ 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/nest/binary_sensor.py b/homeassistant/components/nest/binary_sensor.py new file mode 100644 index 0000000000000..1077fdb073e7b --- /dev/null +++ b/homeassistant/components/nest/binary_sensor.py @@ -0,0 +1,159 @@ +"""Support for Nest Thermostat binary sensors.""" +from itertools import chain +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +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'} + +CLIMATE_BINARY_TYPES = { + 'fan': None, + 'is_using_emergency_heat': 'heat', + 'is_locked': None, + 'has_leaf': None, +} + +CAMERA_BINARY_TYPES = { + 'motion_detected': 'motion', + 'sound_detected': 'sound', + 'person_detected': 'occupancy', +} + +STRUCTURE_BINARY_TYPES = { + 'away': None, +} + +STRUCTURE_BINARY_STATE_MAP = { + 'away': {'away': True, 'home': False}, +} + +_BINARY_TYPES_DEPRECATED = [ + 'hvac_ac_state', + 'hvac_aux_heater_state', + 'hvac_heater_state', + 'hvac_heat_x2_state', + 'hvac_heat_x3_state', + 'hvac_alt_heat_state', + 'hvac_alt_heat_x2_state', + 'hvac_emer_heat_state', +] + +_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): + """Set up the Nest binary sensors. + + No longer used. + """ + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up a Nest binary sensor based on a config entry.""" + nest = hass.data[DATA_NEST] + + discovery_info = \ + hass.data.get(DATA_NEST_CONFIG, {}).get(CONF_BINARY_SENSORS, {}) + + # Add all available binary sensors if no Nest binary sensor config is set + if discovery_info == {}: + conditions = _VALID_BINARY_SENSOR_TYPES + else: + conditions = discovery_info.get(CONF_MONITORED_CONDITIONS, {}) + + for variable in conditions: + if variable in _BINARY_TYPES_DEPRECATED: + wstr = (variable + " is no a longer supported " + "monitored_conditions. See " + "https://home-assistant.io/components/binary_sensor.nest/ " + "for valid options.") + _LOGGER.error(wstr) + + def get_binary_sensors(): + """Get the Nest binary sensors.""" + sensors = [] + for structure in nest.structures(): + 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()) + for structure, device in device_chain: + sensors += [NestBinarySensor(structure, device, variable) + for variable in conditions + if variable in BINARY_TYPES] + sensors += [NestBinarySensor(structure, device, variable) + for variable in conditions + if variable in CLIMATE_BINARY_TYPES + and device.is_thermostat] + + if device.is_camera: + sensors += [NestBinarySensor(structure, device, variable) + for variable in conditions + if variable in CAMERA_BINARY_TYPES] + for activity_zone in device.activity_zones: + sensors += [NestActivityZoneSensor( + structure, device, activity_zone)] + + return sensors + + async_add_entities(await hass.async_add_job(get_binary_sensors), True) + + +class NestBinarySensor(NestSensorDevice, BinarySensorDevice): + """Represents a Nest binary sensor.""" + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self._state + + @property + def device_class(self): + """Return the device class of the binary sensor.""" + return _VALID_BINARY_SENSOR_TYPES.get(self.variable) + + def update(self): + """Retrieve latest state.""" + value = getattr(self.device, self.variable) + if self.variable in STRUCTURE_BINARY_TYPES: + self._state = bool(STRUCTURE_BINARY_STATE_MAP + [self.variable].get(value)) + else: + self._state = bool(value) + + +class NestActivityZoneSensor(NestBinarySensor): + """Represents a Nest binary sensor for activity in a zone.""" + + def __init__(self, structure, device, zone): + """Initialize the sensor.""" + super(NestActivityZoneSensor, self).__init__(structure, device, "") + self.zone = zone + self._name = "{} {} activity".format(self._name, self.zone.name) + + @property + def unique_id(self): + """Return unique id based on camera serial and zone id.""" + return "{}-{}".format(self.device.serial, self.zone.zone_id) + + @property + def device_class(self): + """Return the device class of the binary sensor.""" + return 'motion' + + def update(self): + """Retrieve latest state.""" + self._state = self.device.has_ongoing_motion_in_zone(self.zone.zone_id) diff --git a/homeassistant/components/nest/camera.py b/homeassistant/components/nest/camera.py new file mode 100644 index 0000000000000..8b450e02b4677 --- /dev/null +++ b/homeassistant/components/nest/camera.py @@ -0,0 +1,156 @@ +"""Support for Nest Cameras.""" +import logging +from datetime import timedelta + +import requests + +from homeassistant.components import nest +from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera, + SUPPORT_ON_OFF) +from homeassistant.util.dt import utcnow + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['nest'] + +NEST_BRAND = 'Nest' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up a Nest Cam. + + No longer in use. + """ + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up a Nest sensor based on a config entry.""" + camera_devices = \ + await hass.async_add_job(hass.data[nest.DATA_NEST].cameras) + cameras = [NestCamera(structure, device) + for structure, device in camera_devices] + async_add_entities(cameras, True) + + +class NestCamera(Camera): + """Representation of a Nest Camera.""" + + def __init__(self, structure, device): + """Initialize a Nest Camera.""" + super(NestCamera, self).__init__() + self.structure = structure + self.device = device + self._location = None + self._name = None + self._online = None + self._is_streaming = None + self._is_video_history_enabled = False + # Default to non-NestAware subscribed, but will be fixed during update + self._time_between_snapshots = timedelta(seconds=30) + self._last_image = None + self._next_snapshot_at = None + + @property + def name(self): + """Return the name of the nest, if any.""" + return self._name + + @property + def unique_id(self): + """Return the serial number.""" + return self.device.device_id + + @property + def device_info(self): + """Return information about the device.""" + return { + 'identifiers': { + (nest.DOMAIN, self.device.device_id) + }, + 'name': self.device.name_long, + 'manufacturer': 'Nest Labs', + 'model': "Camera", + } + + @property + def should_poll(self): + """Nest camera should poll periodically.""" + return True + + @property + def is_recording(self): + """Return true if the device is recording.""" + return self._is_streaming + + @property + def brand(self): + """Return the brand of the camera.""" + return NEST_BRAND + + @property + def supported_features(self): + """Nest Cam support turn on and off.""" + return SUPPORT_ON_OFF + + @property + def is_on(self): + """Return true if on.""" + return self._online and self._is_streaming + + def turn_off(self): + """Turn off camera.""" + _LOGGER.debug('Turn off camera %s', self._name) + # Calling Nest API in is_streaming setter. + # device.is_streaming would not immediately change until the process + # finished in Nest Cam. + self.device.is_streaming = False + + def turn_on(self): + """Turn on camera.""" + if not self._online: + _LOGGER.error('Camera %s is offline.', self._name) + return + + _LOGGER.debug('Turn on camera %s', self._name) + # Calling Nest API in is_streaming setter. + # device.is_streaming would not immediately change until the process + # finished in Nest Cam. + self.device.is_streaming = True + + def update(self): + """Cache value from Python-nest.""" + self._location = self.device.where + self._name = self.device.name + self._online = self.device.online + self._is_streaming = self.device.is_streaming + self._is_video_history_enabled = self.device.is_video_history_enabled + + if self._is_video_history_enabled: + # NestAware allowed 10/min + self._time_between_snapshots = timedelta(seconds=6) + else: + # Otherwise, 2/min + self._time_between_snapshots = timedelta(seconds=30) + + def _ready_for_snapshot(self, now): + return (self._next_snapshot_at is None or + now > self._next_snapshot_at) + + def camera_image(self): + """Return a still image response from the camera.""" + now = utcnow() + if self._ready_for_snapshot(now): + url = self.device.snapshot_url + + try: + response = requests.get(url) + except requests.exceptions.RequestException as error: + _LOGGER.error("Error getting camera image: %s", error) + return None + + self._next_snapshot_at = now + self._time_between_snapshots + self._last_image = response.content + + return self._last_image diff --git a/homeassistant/components/nest/climate.py b/homeassistant/components/nest/climate.py new file mode 100644 index 0000000000000..8746a1959ae2d --- /dev/null +++ b/homeassistant/components/nest/climate.py @@ -0,0 +1,290 @@ +"""Support for Nest thermostats.""" +import logging + +import voluptuous as vol + +from homeassistant.components.nest import ( + DATA_NEST, SIGNAL_NEST_UPDATE, DOMAIN as NEST_DOMAIN) +from homeassistant.components.climate import ( + STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_ECO, ClimateDevice, + PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + ATTR_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW, + SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE, SUPPORT_FAN_MODE) +from homeassistant.const import ( + TEMP_CELSIUS, TEMP_FAHRENHEIT, + CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF) +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +DEPENDENCIES = ['nest'] +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_SCAN_INTERVAL): + vol.All(vol.Coerce(int), vol.Range(min=1)), +}) + +NEST_MODE_HEAT_COOL = 'heat-cool' + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Nest thermostat. + + No longer in use. + """ + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the Nest climate device based on a config entry.""" + temp_unit = hass.config.units.temperature_unit + + thermostats = await hass.async_add_job(hass.data[DATA_NEST].thermostats) + + all_devices = [NestThermostat(structure, device, temp_unit) + for structure, device in thermostats] + + async_add_entities(all_devices, True) + + +class NestThermostat(ClimateDevice): + """Representation of a Nest thermostat.""" + + def __init__(self, structure, device, temp_unit): + """Initialize the thermostat.""" + self._unit = temp_unit + self.structure = structure + self.device = device + self._fan_list = [STATE_ON, STATE_AUTO] + + # Set the default supported features + self._support_flags = (SUPPORT_TARGET_TEMPERATURE | + SUPPORT_OPERATION_MODE | SUPPORT_AWAY_MODE) + + # Not all nest devices support cooling and heating remove unused + self._operation_list = [STATE_OFF] + + # Add supported nest thermostat features + if self.device.can_heat: + self._operation_list.append(STATE_HEAT) + + if self.device.can_cool: + self._operation_list.append(STATE_COOL) + + if self.device.can_heat and self.device.can_cool: + self._operation_list.append(STATE_AUTO) + self._support_flags = (self._support_flags | + SUPPORT_TARGET_TEMPERATURE_HIGH | + SUPPORT_TARGET_TEMPERATURE_LOW) + + self._operation_list.append(STATE_ECO) + + # feature of device + self._has_fan = self.device.has_fan + if self._has_fan: + self._support_flags = (self._support_flags | SUPPORT_FAN_MODE) + + # data attributes + self._away = None + self._location = None + self._name = None + self._humidity = None + self._target_temperature = None + self._temperature = None + self._temperature_scale = None + self._mode = None + self._fan = None + self._eco_temperature = None + self._is_locked = None + self._locked_temperature = None + self._min_temperature = None + self._max_temperature = None + + @property + def should_poll(self): + """Do not need poll thanks using Nest streaming API.""" + return False + + async def async_added_to_hass(self): + """Register update signal handler.""" + async def async_update_state(): + """Update device state.""" + await self.async_update_ha_state(True) + + async_dispatcher_connect(self.hass, SIGNAL_NEST_UPDATE, + async_update_state) + + @property + def supported_features(self): + """Return the list of supported features.""" + return self._support_flags + + @property + def unique_id(self): + """Return unique ID for this device.""" + return self.device.serial + + @property + def device_info(self): + """Return information about the device.""" + return { + 'identifiers': { + (NEST_DOMAIN, self.device.device_id), + }, + 'name': self.device.name_long, + 'manufacturer': 'Nest Labs', + 'model': "Thermostat", + 'sw_version': self.device.software_version, + } + + @property + def name(self): + """Return the name of the nest, if any.""" + return self._name + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return self._temperature_scale + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._temperature + + @property + def current_operation(self): + """Return current operation ie. heat, cool, idle.""" + if self._mode in [STATE_HEAT, STATE_COOL, STATE_OFF, STATE_ECO]: + return self._mode + if self._mode == NEST_MODE_HEAT_COOL: + return STATE_AUTO + return None + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + if self._mode not in (NEST_MODE_HEAT_COOL, STATE_ECO): + return self._target_temperature + return None + + @property + def target_temperature_low(self): + """Return the lower bound temperature we try to reach.""" + if self._mode == STATE_ECO: + return self._eco_temperature[0] + if self._mode == NEST_MODE_HEAT_COOL: + return self._target_temperature[0] + return None + + @property + def target_temperature_high(self): + """Return the upper bound temperature we try to reach.""" + if self._mode == STATE_ECO: + return self._eco_temperature[1] + if self._mode == NEST_MODE_HEAT_COOL: + return self._target_temperature[1] + return None + + @property + def is_away_mode_on(self): + """Return if away mode is on.""" + return self._away + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + import nest + temp = None + target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) + target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) + if self._mode == NEST_MODE_HEAT_COOL: + if target_temp_low is not None and target_temp_high is not None: + temp = (target_temp_low, target_temp_high) + _LOGGER.debug("Nest set_temperature-output-value=%s", temp) + else: + temp = kwargs.get(ATTR_TEMPERATURE) + _LOGGER.debug("Nest set_temperature-output-value=%s", temp) + try: + if temp is not None: + self.device.target = temp + except nest.nest.APIError as api_error: + _LOGGER.error("An error occurred while setting temperature: %s", + api_error) + # restore target temperature + self.schedule_update_ha_state(True) + + def set_operation_mode(self, operation_mode): + """Set operation mode.""" + if operation_mode in [STATE_HEAT, STATE_COOL, STATE_OFF, STATE_ECO]: + device_mode = operation_mode + elif operation_mode == STATE_AUTO: + device_mode = NEST_MODE_HEAT_COOL + else: + device_mode = STATE_OFF + _LOGGER.error( + "An error occurred while setting device mode. " + "Invalid operation mode: %s", operation_mode) + self.device.mode = device_mode + + @property + def operation_list(self): + """List of available operation modes.""" + return self._operation_list + + def turn_away_mode_on(self): + """Turn away on.""" + self.structure.away = True + + def turn_away_mode_off(self): + """Turn away off.""" + self.structure.away = False + + @property + def current_fan_mode(self): + """Return whether the fan is on.""" + if self._has_fan: + # Return whether the fan is on + return STATE_ON if self._fan else STATE_AUTO + # No Fan available so disable slider + return None + + @property + def fan_list(self): + """List of available fan modes.""" + if self._has_fan: + return self._fan_list + return None + + def set_fan_mode(self, fan_mode): + """Turn fan on/off.""" + if self._has_fan: + self.device.fan = fan_mode.lower() + + @property + def min_temp(self): + """Identify min_temp in Nest API or defaults if not available.""" + return self._min_temperature + + @property + def max_temp(self): + """Identify max_temp in Nest API or defaults if not available.""" + return self._max_temperature + + def update(self): + """Cache value from Python-nest.""" + self._location = self.device.where + self._name = self.device.name + self._humidity = self.device.humidity + self._temperature = self.device.temperature + self._mode = self.device.mode + self._target_temperature = self.device.target + self._fan = self.device.fan + self._away = self.structure.away == 'away' + self._eco_temperature = self.device.eco_temperature + self._locked_temperature = self.device.locked_temperature + self._min_temperature = self.device.min_temperature + self._max_temperature = self.device.max_temperature + self._is_locked = self.device.is_locked + if self.device.temperature_scale == 'C': + self._temperature_scale = TEMP_CELSIUS + else: + self._temperature_scale = TEMP_FAHRENHEIT diff --git a/homeassistant/components/nest/sensor.py b/homeassistant/components/nest/sensor.py new file mode 100644 index 0000000000000..10fa83d23e0fa --- /dev/null +++ b/homeassistant/components/nest/sensor.py @@ -0,0 +1,186 @@ +"""Support for Nest Thermostat sensors.""" +import logging + +from homeassistant.components.climate import ( + STATE_COOL, STATE_HEAT) +from homeassistant.components.nest import ( + DATA_NEST, DATA_NEST_CONFIG, CONF_SENSORS, NestSensorDevice) +from homeassistant.const import ( + TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_MONITORED_CONDITIONS, + DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, STATE_OFF) + +DEPENDENCIES = ['nest'] + +SENSOR_TYPES = ['humidity', 'operation_mode', 'hvac_state'] + +TEMP_SENSOR_TYPES = ['temperature', 'target'] + +PROTECT_SENSOR_TYPES = ['co_status', + 'smoke_status', + 'battery_health', + # color_status: "gray", "green", "yellow", "red" + 'color_status'] + +STRUCTURE_SENSOR_TYPES = ['eta'] + +# security_state is structure level sensor, but only meaningful when +# Nest Cam exist +STRUCTURE_CAMERA_SENSOR_TYPES = ['security_state'] + +_VALID_SENSOR_TYPES = SENSOR_TYPES + TEMP_SENSOR_TYPES + PROTECT_SENSOR_TYPES \ + + STRUCTURE_SENSOR_TYPES + STRUCTURE_CAMERA_SENSOR_TYPES + +SENSOR_UNITS = {'humidity': '%'} + +SENSOR_DEVICE_CLASSES = {'humidity': DEVICE_CLASS_HUMIDITY} + +VARIABLE_NAME_MAPPING = {'eta': 'eta_begin', 'operation_mode': 'mode'} + +VALUE_MAPPING = { + 'hvac_state': { + 'heating': STATE_HEAT, 'cooling': STATE_COOL, 'off': STATE_OFF}} + +SENSOR_TYPES_DEPRECATED = ['last_ip', + 'local_ip', + 'last_connection', + 'battery_level'] + +DEPRECATED_WEATHER_VARS = ['weather_humidity', + 'weather_temperature', + 'weather_condition', + 'wind_speed', + 'wind_direction'] + +_SENSOR_TYPES_DEPRECATED = SENSOR_TYPES_DEPRECATED + DEPRECATED_WEATHER_VARS + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Nest Sensor. + + No longer used. + """ + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up a Nest sensor based on a config entry.""" + nest = hass.data[DATA_NEST] + + discovery_info = hass.data.get(DATA_NEST_CONFIG, {}).get(CONF_SENSORS, {}) + + # Add all available sensors if no Nest sensor config is set + if discovery_info == {}: + conditions = _VALID_SENSOR_TYPES + else: + conditions = discovery_info.get(CONF_MONITORED_CONDITIONS, {}) + + for variable in conditions: + if variable in _SENSOR_TYPES_DEPRECATED: + if variable in DEPRECATED_WEATHER_VARS: + wstr = ("Nest no longer provides weather data like %s. See " + "https://home-assistant.io/components/#weather " + "for a list of other weather components to use." % + variable) + else: + wstr = (variable + " is no a longer supported " + "monitored_conditions. See " + "https://home-assistant.io/components/" + "binary_sensor.nest/ for valid options.") + _LOGGER.error(wstr) + + def get_sensors(): + """Get the Nest sensors.""" + all_sensors = [] + for structure in nest.structures(): + all_sensors += [NestBasicSensor(structure, None, variable) + for variable in conditions + if variable in STRUCTURE_SENSOR_TYPES] + + for structure, device in nest.thermostats(): + all_sensors += [NestBasicSensor(structure, device, variable) + for variable in conditions + if variable in SENSOR_TYPES] + all_sensors += [NestTempSensor(structure, device, variable) + for variable in conditions + if variable in TEMP_SENSOR_TYPES] + + for structure, device in nest.smoke_co_alarms(): + all_sensors += [NestBasicSensor(structure, device, variable) + for variable in conditions + if variable in PROTECT_SENSOR_TYPES] + + structures_has_camera = {} + for structure, device in nest.cameras(): + structures_has_camera[structure] = True + for structure in structures_has_camera: + all_sensors += [NestBasicSensor(structure, None, variable) + for variable in conditions + if variable in STRUCTURE_CAMERA_SENSOR_TYPES] + + return all_sensors + + async_add_entities(await hass.async_add_job(get_sensors), True) + + +class NestBasicSensor(NestSensorDevice): + """Representation a basic Nest sensor.""" + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def device_class(self): + """Return the device class of the sensor.""" + return SENSOR_DEVICE_CLASSES.get(self.variable) + + def update(self): + """Retrieve latest state.""" + self._unit = SENSOR_UNITS.get(self.variable) + + if self.variable in VARIABLE_NAME_MAPPING: + self._state = getattr(self.device, + VARIABLE_NAME_MAPPING[self.variable]) + elif self.variable in VALUE_MAPPING: + state = getattr(self.device, self.variable) + self._state = VALUE_MAPPING[self.variable].get(state, state) + elif self.variable in PROTECT_SENSOR_TYPES \ + and self.variable != 'color_status': + # keep backward compatibility + state = getattr(self.device, self.variable) + self._state = state.capitalize() if state is not None else None + else: + self._state = getattr(self.device, self.variable) + + +class NestTempSensor(NestSensorDevice): + """Representation of a Nest Temperature sensor.""" + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def device_class(self): + """Return the device class of the sensor.""" + return DEVICE_CLASS_TEMPERATURE + + def update(self): + """Retrieve latest state.""" + if self.device.temperature_scale == 'C': + self._unit = TEMP_CELSIUS + else: + self._unit = TEMP_FAHRENHEIT + + temp = getattr(self.device, self.variable) + if temp is None: + self._state = None + + if isinstance(temp, tuple): + low, high = temp + self._state = "%s-%s" % (int(low), int(high)) + else: + self._state = round(temp, 1) diff --git a/homeassistant/components/nest/services.yaml b/homeassistant/components/nest/services.yaml index e10e626464378..0015c83342d06 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.py deleted file mode 100644 index 50bd290797d69..0000000000000 --- a/homeassistant/components/netatmo.py +++ /dev/null @@ -1,119 +0,0 @@ -""" -Support for the Netatmo devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/netatmo/ -""" -import logging -from datetime import timedelta -from urllib.error import HTTPError - -import voluptuous as vol - -from homeassistant.const import ( - CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME, CONF_DISCOVERY) -from homeassistant.helpers import discovery -import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle - -REQUIREMENTS = ['pyatmo==1.4'] - -_LOGGER = logging.getLogger(__name__) - -CONF_SECRET_KEY = 'secret_key' - -DOMAIN = 'netatmo' - -NETATMO_AUTH = None -DEFAULT_DISCOVERY = True - -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) -MIN_TIME_BETWEEN_EVENT_UPDATES = timedelta(seconds=10) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_SECRET_KEY): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY): cv.boolean, - }) -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the Netatmo devices.""" - import pyatmo - - global NETATMO_AUTH - try: - NETATMO_AUTH = pyatmo.ClientAuth( - config[DOMAIN][CONF_API_KEY], config[DOMAIN][CONF_SECRET_KEY], - config[DOMAIN][CONF_USERNAME], config[DOMAIN][CONF_PASSWORD], - 'read_station read_camera access_camera ' - 'read_thermostat write_thermostat ' - 'read_presence access_presence read_homecoach') - except HTTPError: - _LOGGER.error("Unable to connect to Netatmo API") - return False - - if config[DOMAIN][CONF_DISCOVERY]: - for component in 'camera', 'sensor', 'binary_sensor', 'climate': - discovery.load_platform(hass, component, DOMAIN, {}, config) - - return True - - -class CameraData: - """Get the latest data from Netatmo.""" - - def __init__(self, auth, home=None): - """Initialize the data object.""" - self.auth = auth - self.camera_data = None - self.camera_names = [] - self.module_names = [] - self.home = home - self.camera_type = None - - def get_camera_names(self): - """Return all camera available on the API as a list.""" - self.camera_names = [] - self.update() - if not self.home: - for home in self.camera_data.cameras: - for camera in self.camera_data.cameras[home].values(): - self.camera_names.append(camera['name']) - else: - for camera in self.camera_data.cameras[self.home].values(): - self.camera_names.append(camera['name']) - return self.camera_names - - def get_module_names(self, camera_name): - """Return all module available on the API as a list.""" - self.module_names = [] - self.update() - cam_id = self.camera_data.cameraByName(camera=camera_name, - home=self.home)['id'] - for module in self.camera_data.modules.values(): - if cam_id == module['cam_id']: - self.module_names.append(module['name']) - return self.module_names - - def get_camera_type(self, camera=None, home=None, cid=None): - """Return camera type for a camera, cid has preference over camera.""" - self.camera_type = self.camera_data.cameraType(camera=camera, - home=home, cid=cid) - return self.camera_type - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Call the Netatmo API to update the data.""" - import pyatmo - self.camera_data = pyatmo.CameraData(self.auth, size=100) - - @Throttle(MIN_TIME_BETWEEN_EVENT_UPDATES) - def update_event(self): - """Call the Netatmo API to update the events.""" - self.camera_data.updateEvent( - home=self.home, cameratype=self.camera_type) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py new file mode 100644 index 0000000000000..495e22aae24fe --- /dev/null +++ b/homeassistant/components/netatmo/__init__.py @@ -0,0 +1,114 @@ +"""Support for the Netatmo devices.""" +import logging +from datetime import timedelta +from urllib.error import HTTPError + +import voluptuous as vol + +from homeassistant.const import ( + CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME, CONF_DISCOVERY) +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle + +REQUIREMENTS = ['pyatmo==1.8'] + +_LOGGER = logging.getLogger(__name__) + +CONF_SECRET_KEY = 'secret_key' + +DOMAIN = 'netatmo' + +NETATMO_AUTH = None +DEFAULT_DISCOVERY = True + +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) +MIN_TIME_BETWEEN_EVENT_UPDATES = timedelta(seconds=10) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_SECRET_KEY): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY): cv.boolean, + }) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Netatmo devices.""" + import pyatmo + + global NETATMO_AUTH + try: + NETATMO_AUTH = pyatmo.ClientAuth( + config[DOMAIN][CONF_API_KEY], config[DOMAIN][CONF_SECRET_KEY], + config[DOMAIN][CONF_USERNAME], config[DOMAIN][CONF_PASSWORD], + 'read_station read_camera access_camera ' + 'read_thermostat write_thermostat ' + 'read_presence access_presence read_homecoach') + except HTTPError: + _LOGGER.error("Unable to connect to Netatmo API") + return False + + if config[DOMAIN][CONF_DISCOVERY]: + for component in 'camera', 'sensor', 'binary_sensor', 'climate': + discovery.load_platform(hass, component, DOMAIN, {}, config) + + return True + + +class CameraData: + """Get the latest data from Netatmo.""" + + def __init__(self, auth, home=None): + """Initialize the data object.""" + self.auth = auth + self.camera_data = None + self.camera_names = [] + self.module_names = [] + self.home = home + self.camera_type = None + + def get_camera_names(self): + """Return all camera available on the API as a list.""" + self.camera_names = [] + self.update() + if not self.home: + for home in self.camera_data.cameras: + for camera in self.camera_data.cameras[home].values(): + self.camera_names.append(camera['name']) + else: + for camera in self.camera_data.cameras[self.home].values(): + self.camera_names.append(camera['name']) + return self.camera_names + + def get_module_names(self, camera_name): + """Return all module available on the API as a list.""" + self.module_names = [] + self.update() + cam_id = self.camera_data.cameraByName(camera=camera_name, + home=self.home)['id'] + for module in self.camera_data.modules.values(): + if cam_id == module['cam_id']: + self.module_names.append(module['name']) + return self.module_names + + def get_camera_type(self, camera=None, home=None, cid=None): + """Return camera type for a camera, cid has preference over camera.""" + self.camera_type = self.camera_data.cameraType(camera=camera, + home=home, cid=cid) + return self.camera_type + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Call the Netatmo API to update the data.""" + import pyatmo + self.camera_data = pyatmo.CameraData(self.auth, size=100) + + @Throttle(MIN_TIME_BETWEEN_EVENT_UPDATES) + def update_event(self): + """Call the Netatmo API to update the events.""" + self.camera_data.updateEvent( + home=self.home, cameratype=self.camera_type) diff --git a/homeassistant/components/netatmo/binary_sensor.py b/homeassistant/components/netatmo/binary_sensor.py new file mode 100644 index 0000000000000..727ed0a68c7b5 --- /dev/null +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -0,0 +1,191 @@ +"""Support for the Netatmo binary sensors.""" +import logging + +import voluptuous as vol + +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA) +from homeassistant.components.netatmo import CameraData +from homeassistant.const import CONF_TIMEOUT +from homeassistant.helpers import config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['netatmo'] + +# These are the available sensors mapped to binary_sensor class +WELCOME_SENSOR_TYPES = { + "Someone known": "motion", + "Someone unknown": "motion", + "Motion": "motion", +} +PRESENCE_SENSOR_TYPES = { + "Outdoor motion": "motion", + "Outdoor human": "motion", + "Outdoor animal": "motion", + "Outdoor vehicle": "motion" +} +TAG_SENSOR_TYPES = { + "Tag Vibration": "vibration", + "Tag Open": "opening" +} + +CONF_HOME = 'home' +CONF_CAMERAS = 'cameras' +CONF_WELCOME_SENSORS = 'welcome_sensors' +CONF_PRESENCE_SENSORS = 'presence_sensors' +CONF_TAG_SENSORS = 'tag_sensors' + +DEFAULT_TIMEOUT = 90 + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_CAMERAS, default=[]): + vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_HOME): cv.string, + vol.Optional(CONF_PRESENCE_SENSORS, default=list(PRESENCE_SENSOR_TYPES)): + vol.All(cv.ensure_list, [vol.In(PRESENCE_SENSOR_TYPES)]), + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + vol.Optional(CONF_WELCOME_SENSORS, default=list(WELCOME_SENSOR_TYPES)): + vol.All(cv.ensure_list, [vol.In(WELCOME_SENSOR_TYPES)]), +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the access to Netatmo binary sensor.""" + netatmo = hass.components.netatmo + home = config.get(CONF_HOME) + timeout = config.get(CONF_TIMEOUT) + if timeout is None: + timeout = DEFAULT_TIMEOUT + + module_name = None + + import pyatmo + try: + data = CameraData(netatmo.NETATMO_AUTH, home) + if not data.get_camera_names(): + return None + except pyatmo.NoDevice: + return None + + welcome_sensors = config.get( + CONF_WELCOME_SENSORS, WELCOME_SENSOR_TYPES) + presence_sensors = config.get( + CONF_PRESENCE_SENSORS, PRESENCE_SENSOR_TYPES) + tag_sensors = config.get(CONF_TAG_SENSORS, TAG_SENSOR_TYPES) + + for camera_name in data.get_camera_names(): + camera_type = data.get_camera_type(camera=camera_name, home=home) + if camera_type == 'NACamera': + if CONF_CAMERAS in config: + if config[CONF_CAMERAS] != [] and \ + camera_name not in config[CONF_CAMERAS]: + continue + for variable in welcome_sensors: + add_entities([NetatmoBinarySensor( + data, camera_name, module_name, home, timeout, + camera_type, variable)], True) + if camera_type == 'NOC': + if CONF_CAMERAS in config: + if config[CONF_CAMERAS] != [] and \ + camera_name not in config[CONF_CAMERAS]: + continue + for variable in presence_sensors: + add_entities([NetatmoBinarySensor( + data, camera_name, module_name, home, timeout, + camera_type, variable)], True) + + for module_name in data.get_module_names(camera_name): + for variable in tag_sensors: + camera_type = None + add_entities([NetatmoBinarySensor( + data, camera_name, module_name, home, timeout, + camera_type, variable)], True) + + +class NetatmoBinarySensor(BinarySensorDevice): + """Represent a single binary sensor in a Netatmo Camera device.""" + + def __init__(self, data, camera_name, module_name, home, + timeout, camera_type, sensor): + """Set up for access to the Netatmo camera events.""" + self._data = data + self._camera_name = camera_name + self._module_name = module_name + self._home = home + self._timeout = timeout + if home: + self._name = '{} / {}'.format(home, camera_name) + else: + self._name = camera_name + if module_name: + self._name += ' / ' + module_name + self._sensor_name = sensor + self._name += ' ' + sensor + self._cameratype = camera_type + self._state = None + + @property + def name(self): + """Return the name of the Netatmo device and this sensor.""" + return self._name + + @property + def device_class(self): + """Return the class of this sensor, from DEVICE_CLASSES.""" + if self._cameratype == 'NACamera': + return WELCOME_SENSOR_TYPES.get(self._sensor_name) + if self._cameratype == 'NOC': + return PRESENCE_SENSOR_TYPES.get(self._sensor_name) + return TAG_SENSOR_TYPES.get(self._sensor_name) + + @property + def is_on(self): + """Return true if binary sensor is on.""" + return self._state + + def update(self): + """Request an update from the Netatmo API.""" + self._data.update() + self._data.update_event() + + if self._cameratype == 'NACamera': + if self._sensor_name == "Someone known": + self._state =\ + self._data.camera_data.someoneKnownSeen( + self._home, self._camera_name, self._timeout) + elif self._sensor_name == "Someone unknown": + self._state =\ + self._data.camera_data.someoneUnknownSeen( + self._home, self._camera_name, self._timeout) + elif self._sensor_name == "Motion": + self._state =\ + self._data.camera_data.motionDetected( + self._home, self._camera_name, self._timeout) + elif self._cameratype == 'NOC': + if self._sensor_name == "Outdoor motion": + self._state =\ + self._data.camera_data.outdoormotionDetected( + self._home, self._camera_name, self._timeout) + elif self._sensor_name == "Outdoor human": + self._state =\ + self._data.camera_data.humanDetected( + self._home, self._camera_name, self._timeout) + elif self._sensor_name == "Outdoor animal": + self._state =\ + self._data.camera_data.animalDetected( + self._home, self._camera_name, self._timeout) + elif self._sensor_name == "Outdoor vehicle": + self._state =\ + self._data.camera_data.carDetected( + self._home, self._camera_name, self._timeout) + if self._sensor_name == "Tag Vibration": + self._state =\ + self._data.camera_data.moduleMotionDetected( + self._home, self._module_name, self._camera_name, + self._timeout) + elif self._sensor_name == "Tag Open": + self._state =\ + self._data.camera_data.moduleOpened( + self._home, self._module_name, self._camera_name, + self._timeout) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py new file mode 100644 index 0000000000000..a3a5461631d3c --- /dev/null +++ b/homeassistant/components/netatmo/camera.py @@ -0,0 +1,105 @@ +"""Support for the Netatmo cameras.""" +import logging + +import requests +import voluptuous as vol + +from homeassistant.const import CONF_VERIFY_SSL +from homeassistant.components.netatmo import CameraData +from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA) +from homeassistant.helpers import config_validation as cv + +DEPENDENCIES = ['netatmo'] + +_LOGGER = logging.getLogger(__name__) + +CONF_HOME = 'home' +CONF_CAMERAS = 'cameras' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, + vol.Optional(CONF_HOME): cv.string, + vol.Optional(CONF_CAMERAS, default=[]): + vol.All(cv.ensure_list, [cv.string]), +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up access to Netatmo cameras.""" + netatmo = hass.components.netatmo + home = config.get(CONF_HOME) + verify_ssl = config.get(CONF_VERIFY_SSL, True) + import pyatmo + try: + data = CameraData(netatmo.NETATMO_AUTH, home) + for camera_name in data.get_camera_names(): + camera_type = data.get_camera_type(camera=camera_name, home=home) + if CONF_CAMERAS in config: + if config[CONF_CAMERAS] != [] and \ + camera_name not in config[CONF_CAMERAS]: + continue + add_entities([NetatmoCamera(data, camera_name, home, + camera_type, verify_ssl)]) + except pyatmo.NoDevice: + return None + + +class NetatmoCamera(Camera): + """Representation of the images published from a Netatmo camera.""" + + def __init__(self, data, camera_name, home, camera_type, verify_ssl): + """Set up for access to the Netatmo camera images.""" + super(NetatmoCamera, self).__init__() + self._data = data + self._camera_name = camera_name + self._verify_ssl = verify_ssl + if home: + self._name = home + ' / ' + camera_name + else: + self._name = camera_name + self._vpnurl, self._localurl = self._data.camera_data.cameraUrls( + camera=camera_name + ) + self._cameratype = camera_type + + def camera_image(self): + """Return a still image response from the camera.""" + try: + if self._localurl: + response = requests.get('{0}/live/snapshot_720.jpg'.format( + self._localurl), timeout=10) + elif self._vpnurl: + response = requests.get('{0}/live/snapshot_720.jpg'.format( + self._vpnurl), timeout=10, verify=self._verify_ssl) + else: + _LOGGER.error("Welcome VPN URL is None") + self._data.update() + (self._vpnurl, self._localurl) = \ + self._data.camera_data.cameraUrls(camera=self._camera_name) + return None + except requests.exceptions.RequestException as error: + _LOGGER.error("Welcome URL changed: %s", error) + self._data.update() + (self._vpnurl, self._localurl) = \ + self._data.camera_data.cameraUrls(camera=self._camera_name) + return None + return response.content + + @property + def name(self): + """Return the name of this Netatmo camera device.""" + return self._name + + @property + def brand(self): + """Return the camera brand.""" + return "Netatmo" + + @property + def model(self): + """Return the camera model.""" + if self._cameratype == "NOC": + return "Presence" + if self._cameratype == "NACamera": + return "Welcome" + return None diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py new file mode 100644 index 0000000000000..2b9bcbebaf29e --- /dev/null +++ b/homeassistant/components/netatmo/climate.py @@ -0,0 +1,170 @@ +"""Support for Netatmo Smart thermostats.""" +import logging +from datetime import timedelta +import voluptuous as vol + +from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE +from homeassistant.components.climate import ( + STATE_HEAT, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA, + SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE) +from homeassistant.util import Throttle +import homeassistant.helpers.config_validation as cv + +DEPENDENCIES = ['netatmo'] + +_LOGGER = logging.getLogger(__name__) + +CONF_RELAY = 'relay' +CONF_THERMOSTAT = 'thermostat' + +DEFAULT_AWAY_TEMPERATURE = 14 +# # The default offset is 2 hours (when you use the thermostat itself) +DEFAULT_TIME_OFFSET = 7200 +# # Return cached results if last scan was less then this time ago +# # NetAtmo Data is uploaded to server every hour +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_RELAY): cv.string, + vol.Optional(CONF_THERMOSTAT, default=[]): + vol.All(cv.ensure_list, [cv.string]), +}) + +SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | + SUPPORT_AWAY_MODE) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the NetAtmo Thermostat.""" + netatmo = hass.components.netatmo + device = config.get(CONF_RELAY) + + import pyatmo + try: + data = ThermostatData(netatmo.NETATMO_AUTH, device) + for module_name in data.get_module_names(): + if CONF_THERMOSTAT in config: + if config[CONF_THERMOSTAT] != [] and \ + module_name not in config[CONF_THERMOSTAT]: + continue + add_entities([NetatmoThermostat(data, module_name)], True) + except pyatmo.NoDevice: + return None + + +class NetatmoThermostat(ClimateDevice): + """Representation a Netatmo thermostat.""" + + def __init__(self, data, module_name, away_temp=None): + """Initialize the sensor.""" + self._data = data + self._state = None + self._name = module_name + self._target_temperature = None + self._away = None + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._data.current_temperature + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._target_temperature + + @property + def current_operation(self): + """Return the current state of the thermostat.""" + state = self._data.thermostatdata.relay_cmd + if state == 0: + return STATE_IDLE + if state == 100: + return STATE_HEAT + + @property + def is_away_mode_on(self): + """Return true if away mode is on.""" + return self._away + + def turn_away_mode_on(self): + """Turn away on.""" + mode = "away" + temp = None + self._data.thermostatdata.setthermpoint(mode, temp, endTimeOffset=None) + self._away = True + + def turn_away_mode_off(self): + """Turn away off.""" + mode = "program" + temp = None + self._data.thermostatdata.setthermpoint(mode, temp, endTimeOffset=None) + self._away = False + + def set_temperature(self, **kwargs): + """Set new target temperature for 2 hours.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature is None: + return + mode = "manual" + self._data.thermostatdata.setthermpoint( + mode, temperature, DEFAULT_TIME_OFFSET) + self._target_temperature = temperature + self._away = False + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the latest data from NetAtmo API and updates the states.""" + self._data.update() + self._target_temperature = self._data.thermostatdata.setpoint_temp + self._away = self._data.setpoint_mode == 'away' + + +class ThermostatData: + """Get the latest data from Netatmo.""" + + def __init__(self, auth, device=None): + """Initialize the data object.""" + self.auth = auth + self.thermostatdata = None + self.module_names = [] + self.device = device + self.current_temperature = None + self.target_temperature = None + self.setpoint_mode = None + + def get_module_names(self): + """Return all module available on the API as a list.""" + self.update() + if not self.device: + for device in self.thermostatdata.modules: + for module in self.thermostatdata.modules[device].values(): + self.module_names.append(module['module_name']) + else: + for module in self.thermostatdata.modules[self.device].values(): + self.module_names.append(module['module_name']) + return self.module_names + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Call the NetAtmo API to update the data.""" + import pyatmo + self.thermostatdata = pyatmo.ThermostatData(self.auth) + self.target_temperature = self.thermostatdata.setpoint_temp + self.setpoint_mode = self.thermostatdata.setpoint_mode + self.current_temperature = self.thermostatdata.temp diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py new file mode 100644 index 0000000000000..78a118528b97f --- /dev/null +++ b/homeassistant/components/netatmo/sensor.py @@ -0,0 +1,390 @@ +"""Support for the NetAtmo Weather Service.""" +import logging +from time import time +import threading + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + TEMP_CELSIUS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_BATTERY) +from homeassistant.helpers.entity import Entity +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +CONF_MODULES = 'modules' +CONF_STATION = 'station' + +DEPENDENCIES = ['netatmo'] + +# This is the NetAtmo data upload interval in seconds +NETATMO_UPDATE_INTERVAL = 600 + +SENSOR_TYPES = { + 'temperature': ['Temperature', TEMP_CELSIUS, None, + DEVICE_CLASS_TEMPERATURE], + 'co2': ['CO2', 'ppm', 'mdi:cloud', None], + 'pressure': ['Pressure', 'mbar', 'mdi:gauge', None], + 'noise': ['Noise', 'dB', 'mdi:volume-high', None], + 'humidity': ['Humidity', '%', None, DEVICE_CLASS_HUMIDITY], + 'rain': ['Rain', 'mm', 'mdi:weather-rainy', None], + 'sum_rain_1': ['sum_rain_1', 'mm', 'mdi:weather-rainy', None], + 'sum_rain_24': ['sum_rain_24', 'mm', 'mdi:weather-rainy', None], + 'battery_vp': ['Battery', '', 'mdi:battery', None], + 'battery_lvl': ['Battery_lvl', '', 'mdi:battery', None], + 'battery_percent': ['battery_percent', '%', None, DEVICE_CLASS_BATTERY], + 'min_temp': ['Min Temp.', TEMP_CELSIUS, 'mdi:thermometer', None], + 'max_temp': ['Max Temp.', TEMP_CELSIUS, 'mdi:thermometer', None], + 'windangle': ['Angle', '', 'mdi:compass', None], + 'windangle_value': ['Angle Value', 'º', 'mdi:compass', None], + 'windstrength': ['Strength', 'km/h', 'mdi:weather-windy', None], + 'gustangle': ['Gust Angle', '', 'mdi:compass', None], + 'gustangle_value': ['Gust Angle Value', 'º', 'mdi:compass', None], + 'guststrength': ['Gust Strength', 'km/h', 'mdi:weather-windy', None], + 'rf_status': ['Radio', '', 'mdi:signal', None], + 'rf_status_lvl': ['Radio_lvl', '', 'mdi:signal', None], + 'wifi_status': ['Wifi', '', 'mdi:wifi', None], + 'wifi_status_lvl': ['Wifi_lvl', 'dBm', 'mdi:wifi', None], +} + +MODULE_SCHEMA = vol.Schema({ + vol.Required(cv.string): vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_STATION): cv.string, + vol.Optional(CONF_MODULES): MODULE_SCHEMA, +}) + +MODULE_TYPE_OUTDOOR = 'NAModule1' +MODULE_TYPE_WIND = 'NAModule2' +MODULE_TYPE_RAIN = 'NAModule3' +MODULE_TYPE_INDOOR = 'NAModule4' + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the available Netatmo weather sensors.""" + netatmo = hass.components.netatmo + data = NetAtmoData(netatmo.NETATMO_AUTH, config.get(CONF_STATION, None)) + + dev = [] + import pyatmo + try: + if CONF_MODULES in config: + # Iterate each module + for module_name, monitored_conditions in \ + config[CONF_MODULES].items(): + # Test if module exists + if module_name not in data.get_module_names(): + _LOGGER.error('Module name: "%s" not found', module_name) + continue + # Only create sensors for monitored properties + for variable in monitored_conditions: + dev.append(NetAtmoSensor(data, module_name, variable)) + else: + for module_name in data.get_module_names(): + for variable in \ + data.station_data.monitoredConditions(module_name): + if variable in SENSOR_TYPES.keys(): + dev.append(NetAtmoSensor(data, module_name, variable)) + else: + _LOGGER.warning("Ignoring unknown var %s for mod %s", + variable, module_name) + except pyatmo.NoDevice: + return None + + add_entities(dev, True) + + +class NetAtmoSensor(Entity): + """Implementation of a Netatmo sensor.""" + + def __init__(self, netatmo_data, module_name, sensor_type): + """Initialize the sensor.""" + self._name = 'Netatmo {} {}'.format(module_name, + SENSOR_TYPES[sensor_type][0]) + self.netatmo_data = netatmo_data + self.module_name = module_name + self.type = sensor_type + self._state = None + self._device_class = SENSOR_TYPES[self.type][3] + self._icon = SENSOR_TYPES[self.type][2] + self._unit_of_measurement = SENSOR_TYPES[self.type][1] + self._module_type = self.netatmo_data. \ + station_data.moduleByName(module=module_name)['type'] + module_id = self.netatmo_data. \ + station_data.moduleByName(module=module_name)['_id'] + self._unique_id = '{}-{}'.format(module_id, self.type) + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self._icon + + @property + def device_class(self): + """Return the device class of the sensor.""" + return self._device_class + + @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 unique_id(self): + """Return the unique ID for this sensor.""" + return self._unique_id + + def update(self): + """Get the latest data from NetAtmo API and updates the states.""" + self.netatmo_data.update() + data = self.netatmo_data.data.get(self.module_name) + + if data is None: + _LOGGER.warning("No data found for %s", self.module_name) + self._state = None + return + + 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: + """Get the latest data from NetAtmo.""" + + def __init__(self, auth, station): + """Initialize the data object.""" + self.auth = auth + self.data = None + self.station_data = None + self.station = station + self._next_update = time() + self._update_in_progress = threading.Lock() + + def get_module_names(self): + """Return all module available on the API as a list.""" + self.update() + return self.data.keys() + + def _detect_platform_type(self): + """Return the XXXData object corresponding to the specified platform. + + The return can be a WeatherStationData or a HomeCoachData. + """ + import pyatmo + for data_class in [pyatmo.WeatherStationData, pyatmo.HomeCoachData]: + try: + station_data = data_class(self.auth) + _LOGGER.debug("%s detected!", str(data_class.__name__)) + return station_data + except TypeError: + continue + + def update(self): + """Call the Netatmo API to update the data. + + This method is not throttled by the builtin Throttle decorator + but with a custom logic, which takes into account the time + of the last update from the cloud. + """ + if time() < self._next_update or \ + not self._update_in_progress.acquire(False): + return + + try: + self.station_data = self._detect_platform_type() + if not self.station_data: + raise Exception("No Weather nor HomeCoach devices found") + + if self.station is not None: + self.data = self.station_data.lastData( + station=self.station, exclude=3600) + else: + self.data = self.station_data.lastData(exclude=3600) + + newinterval = 0 + 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() + if newinterval > NETATMO_UPDATE_INTERVAL - 30: + newinterval = NETATMO_UPDATE_INTERVAL + else: + if newinterval < NETATMO_UPDATE_INTERVAL / 2: + # Never hammer the NetAtmo API more than + # twice per update interval + newinterval = NETATMO_UPDATE_INTERVAL / 2 + _LOGGER.info( + "NetAtmo refresh interval reset to %d seconds", + newinterval) + else: + # Last update time not found, fall back to default value + newinterval = NETATMO_UPDATE_INTERVAL + + self._next_update = time() + newinterval + finally: + self._update_in_progress.release() diff --git a/homeassistant/components/netgear_lte.py b/homeassistant/components/netgear_lte.py deleted file mode 100644 index 7658015ea67a2..0000000000000 --- a/homeassistant/components/netgear_lte.py +++ /dev/null @@ -1,158 +0,0 @@ -""" -Support for Netgear LTE modems. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/netgear_lte/ -""" -import asyncio -from datetime import timedelta -import logging - -import voluptuous as vol -import attr -import aiohttp - -from homeassistant.const import ( - CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP) -from homeassistant.core import callback -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.aiohttp_client import async_create_clientsession -from homeassistant.util import Throttle - -REQUIREMENTS = ['eternalegypt==0.0.5'] - -_LOGGER = logging.getLogger(__name__) - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) - -DOMAIN = 'netgear_lte' -DATA_KEY = 'netgear_lte' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - })]) -}, extra=vol.ALLOW_EXTRA) - - -@attr.s -class ModemData: - """Class for modem state.""" - - host = attr.ib() - modem = attr.ib() - - serial_number = attr.ib(init=False, default=None) - unread_count = attr.ib(init=False, default=None) - usage = attr.ib(init=False, default=None) - connected = attr.ib(init=False, default=True) - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - async def async_update(self): - """Call the API to update the data.""" - import eternalegypt - try: - information = await self.modem.information() - self.serial_number = information.serial_number - self.unread_count = sum(1 for x in information.sms if x.unread) - self.usage = information.usage - if not self.connected: - _LOGGER.warning("Connected to %s", self.host) - self.connected = True - except eternalegypt.Error: - if self.connected: - _LOGGER.warning("Lost connection to %s", self.host) - self.connected = False - self.unread_count = None - self.usage = None - - -@attr.s -class LTEData: - """Shared state.""" - - websession = attr.ib() - modem_data = attr.ib(init=False, factory=dict) - - def get_modem_data(self, config): - """Get the requested or the only modem_data value.""" - if CONF_HOST in config: - return self.modem_data.get(config[CONF_HOST]) - if len(self.modem_data) == 1: - return next(iter(self.modem_data.values())) - - return None - - -async def async_setup(hass, config): - """Set up Netgear LTE component.""" - if DATA_KEY not in hass.data: - websession = async_create_clientsession( - hass, cookie_jar=aiohttp.CookieJar(unsafe=True)) - hass.data[DATA_KEY] = LTEData(websession) - - tasks = [_setup_lte(hass, conf) for conf in config.get(DOMAIN, [])] - if tasks: - await asyncio.wait(tasks) - - return True - - -async def _setup_lte(hass, lte_config): - """Set up a Netgear LTE modem.""" - import eternalegypt - - host = lte_config[CONF_HOST] - password = lte_config[CONF_PASSWORD] - - websession = hass.data[DATA_KEY].websession - modem = eternalegypt.Modem(hostname=host, websession=websession) - - modem_data = ModemData(host, modem) - - try: - await _login(hass, modem_data, password) - except eternalegypt.Error: - retry_task = hass.loop.create_task( - _retry_login(hass, modem_data, password)) - - @callback - def cleanup_retry(event): - """Clean up retry task resources.""" - if not retry_task.done(): - retry_task.cancel() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_retry) - - -async def _login(hass, modem_data, password): - """Log in and complete setup.""" - await modem_data.modem.login(password=password) - await modem_data.async_update() - hass.data[DATA_KEY].modem_data[modem_data.host] = modem_data - - async def cleanup(event): - """Clean up resources.""" - await modem_data.modem.logout() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup) - - -async def _retry_login(hass, modem_data, password): - """Sleep and retry setup.""" - import eternalegypt - - _LOGGER.warning( - "Could not connect to %s. Will keep trying.", modem_data.host) - - modem_data.connected = False - delay = 15 - - while not modem_data.connected: - await asyncio.sleep(delay) - - try: - await _login(hass, modem_data, password) - except eternalegypt.Error: - delay = min(2*delay, 300) diff --git a/homeassistant/components/netgear_lte/__init__.py b/homeassistant/components/netgear_lte/__init__.py new file mode 100644 index 0000000000000..5f8c680b7f02b --- /dev/null +++ b/homeassistant/components/netgear_lte/__init__.py @@ -0,0 +1,153 @@ +"""Support for Netgear LTE modems.""" +import asyncio +from datetime import timedelta +import logging + +import aiohttp +import attr +import voluptuous as vol + +from homeassistant.const import ( + CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP) +from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_create_clientsession +from homeassistant.util import Throttle + +REQUIREMENTS = ['eternalegypt==0.0.5'] + +_LOGGER = logging.getLogger(__name__) + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) + +DOMAIN = 'netgear_lte' +DATA_KEY = 'netgear_lte' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + })]) +}, extra=vol.ALLOW_EXTRA) + + +@attr.s +class ModemData: + """Class for modem state.""" + + host = attr.ib() + modem = attr.ib() + + serial_number = attr.ib(init=False, default=None) + unread_count = attr.ib(init=False, default=None) + usage = attr.ib(init=False, default=None) + connected = attr.ib(init=False, default=True) + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + async def async_update(self): + """Call the API to update the data.""" + import eternalegypt + try: + information = await self.modem.information() + self.serial_number = information.serial_number + self.unread_count = sum(1 for x in information.sms if x.unread) + self.usage = information.usage + if not self.connected: + _LOGGER.warning("Connected to %s", self.host) + self.connected = True + except eternalegypt.Error: + if self.connected: + _LOGGER.warning("Lost connection to %s", self.host) + self.connected = False + self.unread_count = None + self.usage = None + + +@attr.s +class LTEData: + """Shared state.""" + + websession = attr.ib() + modem_data = attr.ib(init=False, factory=dict) + + def get_modem_data(self, config): + """Get the requested or the only modem_data value.""" + if CONF_HOST in config: + return self.modem_data.get(config[CONF_HOST]) + if len(self.modem_data) == 1: + return next(iter(self.modem_data.values())) + + return None + + +async def async_setup(hass, config): + """Set up Netgear LTE component.""" + if DATA_KEY not in hass.data: + websession = async_create_clientsession( + hass, cookie_jar=aiohttp.CookieJar(unsafe=True)) + hass.data[DATA_KEY] = LTEData(websession) + + tasks = [_setup_lte(hass, conf) for conf in config.get(DOMAIN, [])] + if tasks: + await asyncio.wait(tasks) + + return True + + +async def _setup_lte(hass, lte_config): + """Set up a Netgear LTE modem.""" + import eternalegypt + + host = lte_config[CONF_HOST] + password = lte_config[CONF_PASSWORD] + + websession = hass.data[DATA_KEY].websession + modem = eternalegypt.Modem(hostname=host, websession=websession) + + modem_data = ModemData(host, modem) + + try: + await _login(hass, modem_data, password) + except eternalegypt.Error: + retry_task = hass.loop.create_task( + _retry_login(hass, modem_data, password)) + + @callback + def cleanup_retry(event): + """Clean up retry task resources.""" + if not retry_task.done(): + retry_task.cancel() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_retry) + + +async def _login(hass, modem_data, password): + """Log in and complete setup.""" + await modem_data.modem.login(password=password) + await modem_data.async_update() + hass.data[DATA_KEY].modem_data[modem_data.host] = modem_data + + async def cleanup(event): + """Clean up resources.""" + await modem_data.modem.logout() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup) + + +async def _retry_login(hass, modem_data, password): + """Sleep and retry setup.""" + import eternalegypt + + _LOGGER.warning( + "Could not connect to %s. Will keep trying", modem_data.host) + + modem_data.connected = False + delay = 15 + + while not modem_data.connected: + await asyncio.sleep(delay) + + try: + await _login(hass, modem_data, password) + except eternalegypt.Error: + delay = min(2*delay, 300) diff --git a/homeassistant/components/netgear_lte/notify.py b/homeassistant/components/netgear_lte/notify.py new file mode 100644 index 0000000000000..20a20b2129182 --- /dev/null +++ b/homeassistant/components/netgear_lte/notify.py @@ -0,0 +1,51 @@ +"""Suport for Netgear LTE notifications.""" +import logging + +import attr +import voluptuous as vol + +from homeassistant.components.notify import ( + 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__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_HOST): cv.string, + vol.Required(ATTR_TARGET): vol.All(cv.ensure_list, [cv.string]), +}) + + +async def async_get_service(hass, config, discovery_info=None): + """Get the notification service.""" + return NetgearNotifyService(hass, config) + + +@attr.s +class NetgearNotifyService(BaseNotificationService): + """Implementation of a notification service.""" + + hass = attr.ib() + config = attr.ib() + + async def async_send_message(self, message="", **kwargs): + """Send a message to a user.""" + modem_data = self.hass.data[DATA_KEY].get_modem_data(self.config) + if not modem_data: + _LOGGER.error("No modem available") + return + + phone = self.config.get(ATTR_TARGET) + targets = kwargs.get(ATTR_TARGET, phone) + if targets and message: + for target in targets: + import eternalegypt + try: + await modem_data.modem.sms(target, message) + except eternalegypt.Error: + _LOGGER.error("Unable to send to %s", target) diff --git a/homeassistant/components/netgear_lte/sensor.py b/homeassistant/components/netgear_lte/sensor.py new file mode 100644 index 0000000000000..339fa678d61cf --- /dev/null +++ b/homeassistant/components/netgear_lte/sensor.py @@ -0,0 +1,93 @@ +"""Support for Netgear LTE sensors.""" +import attr +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_HOST, CONF_SENSORS +from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +from ..netgear_lte import DATA_KEY + +DEPENDENCIES = ['netgear_lte'] + +SENSOR_SMS = 'sms' +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])]), +}) + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info): + """Set up Netgear LTE sensor devices.""" + modem_data = hass.data[DATA_KEY].get_modem_data(config) + + if not modem_data: + raise PlatformNotReady + + sensors = [] + for sensor_type in config[CONF_SENSORS]: + if sensor_type == SENSOR_SMS: + sensors.append(SMSSensor(modem_data, sensor_type)) + elif sensor_type == SENSOR_USAGE: + sensors.append(UsageSensor(modem_data, sensor_type)) + + async_add_entities(sensors, True) + + +@attr.s +class LTESensor(Entity): + """Base LTE sensor entity.""" + + modem_data = attr.ib() + sensor_type = attr.ib() + + async def async_update(self): + """Update state.""" + await self.modem_data.async_update() + + @property + def unique_id(self): + """Return a unique ID like 'usage_5TG365AB0078V'.""" + return "{}_{}".format(self.sensor_type, self.modem_data.serial_number) + + +class SMSSensor(LTESensor): + """Unread SMS sensor entity.""" + + @property + def name(self): + """Return the name of the sensor.""" + return "Netgear LTE SMS" + + @property + def state(self): + """Return the state of the sensor.""" + return self.modem_data.unread_count + + +class UsageSensor(LTESensor): + """Data usage sensor entity.""" + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return "MiB" + + @property + def name(self): + """Return the name of the sensor.""" + return "Netgear LTE usage" + + @property + def state(self): + """Return the state of the sensor.""" + if self.modem_data.usage is None: + return None + + return round(self.modem_data.usage / 1024**2, 1) diff --git a/homeassistant/components/no_ip.py b/homeassistant/components/no_ip.py deleted file mode 100644 index beb11ed738f46..0000000000000 --- a/homeassistant/components/no_ip.py +++ /dev/null @@ -1,113 +0,0 @@ -""" -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/ -""" -import asyncio -import base64 -from datetime import timedelta -import logging - -import aiohttp -from aiohttp.hdrs import USER_AGENT, AUTHORIZATION -import async_timeout -import voluptuous as vol - -from homeassistant.const import ( - CONF_DOMAIN, CONF_TIMEOUT, CONF_PASSWORD, CONF_USERNAME) -from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'no_ip' - -# We should set a dedicated address for the user agent. -EMAIL = 'hello@home-assistant.io' - -INTERVAL = timedelta(minutes=5) - -DEFAULT_TIMEOUT = 10 - -NO_IP_ERRORS = { - 'nohost': "Hostname supplied does not exist under specified account", - 'badauth': "Invalid username password combination", - 'badagent': "Client disabled", - '!donator': - "An update request was sent with a feature that is not available", - 'abuse': "Username is blocked due to abuse", - '911': "A fatal error on NO-IP's side such as a database outage", -} - -UPDATE_URL = 'https://dynupdate.noip.com/nic/update' -HA_USER_AGENT = "{} {}".format(SERVER_SOFTWARE, EMAIL) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_DOMAIN): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - }) -}, extra=vol.ALLOW_EXTRA) - - -async def async_setup(hass, config): - """Initialize the NO-IP component.""" - domain = config[DOMAIN].get(CONF_DOMAIN) - user = config[DOMAIN].get(CONF_USERNAME) - password = config[DOMAIN].get(CONF_PASSWORD) - timeout = config[DOMAIN].get(CONF_TIMEOUT) - - auth_str = base64.b64encode('{}:{}'.format(user, password).encode('utf-8')) - - session = hass.helpers.aiohttp_client.async_get_clientsession() - - result = await _update_no_ip( - hass, session, domain, auth_str, timeout) - - if not result: - return False - - async def update_domain_interval(now): - """Update the NO-IP entry.""" - await _update_no_ip(hass, session, domain, auth_str, timeout) - - hass.helpers.event.async_track_time_interval( - update_domain_interval, INTERVAL) - - return True - - -async def _update_no_ip(hass, session, domain, auth_str, timeout): - """Update NO-IP.""" - url = UPDATE_URL - - params = { - 'hostname': domain, - } - - headers = { - AUTHORIZATION: "Basic {}".format(auth_str.decode('utf-8')), - USER_AGENT: HA_USER_AGENT, - } - - try: - with async_timeout.timeout(timeout, loop=hass.loop): - resp = await session.get(url, params=params, headers=headers) - body = await resp.text() - - if body.startswith('good') or body.startswith('nochg'): - return True - - _LOGGER.warning("Updating NO-IP failed: %s => %s", domain, - NO_IP_ERRORS[body.strip()]) - - except aiohttp.ClientError: - _LOGGER.warning("Can't connect to NO-IP API") - - except asyncio.TimeoutError: - _LOGGER.warning("Timeout from NO-IP API for domain: %s", domain) - - return False diff --git a/homeassistant/components/no_ip/__init__.py b/homeassistant/components/no_ip/__init__.py new file mode 100644 index 0000000000000..6a714747484a6 --- /dev/null +++ b/homeassistant/components/no_ip/__init__.py @@ -0,0 +1,108 @@ +"""Integrate with NO-IP Dynamic DNS service.""" +import asyncio +import base64 +from datetime import timedelta +import logging + +import aiohttp +from aiohttp.hdrs import USER_AGENT, AUTHORIZATION +import async_timeout +import voluptuous as vol + +from homeassistant.const import ( + CONF_DOMAIN, CONF_TIMEOUT, CONF_PASSWORD, CONF_USERNAME) +from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'no_ip' + +# We should set a dedicated address for the user agent. +EMAIL = 'hello@home-assistant.io' + +INTERVAL = timedelta(minutes=5) + +DEFAULT_TIMEOUT = 10 + +NO_IP_ERRORS = { + 'nohost': "Hostname supplied does not exist under specified account", + 'badauth': "Invalid username password combination", + 'badagent': "Client disabled", + '!donator': + "An update request was sent with a feature that is not available", + 'abuse': "Username is blocked due to abuse", + '911': "A fatal error on NO-IP's side such as a database outage", +} + +UPDATE_URL = 'https://dynupdate.noip.com/nic/update' +HA_USER_AGENT = "{} {}".format(SERVER_SOFTWARE, EMAIL) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_DOMAIN): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + }) +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Initialize the NO-IP component.""" + domain = config[DOMAIN].get(CONF_DOMAIN) + user = config[DOMAIN].get(CONF_USERNAME) + password = config[DOMAIN].get(CONF_PASSWORD) + timeout = config[DOMAIN].get(CONF_TIMEOUT) + + auth_str = base64.b64encode('{}:{}'.format(user, password).encode('utf-8')) + + session = hass.helpers.aiohttp_client.async_get_clientsession() + + result = await _update_no_ip( + hass, session, domain, auth_str, timeout) + + if not result: + return False + + async def update_domain_interval(now): + """Update the NO-IP entry.""" + await _update_no_ip(hass, session, domain, auth_str, timeout) + + hass.helpers.event.async_track_time_interval( + update_domain_interval, INTERVAL) + + return True + + +async def _update_no_ip(hass, session, domain, auth_str, timeout): + """Update NO-IP.""" + url = UPDATE_URL + + params = { + 'hostname': domain, + } + + headers = { + AUTHORIZATION: "Basic {}".format(auth_str.decode('utf-8')), + USER_AGENT: HA_USER_AGENT, + } + + try: + with async_timeout.timeout(timeout, loop=hass.loop): + resp = await session.get(url, params=params, headers=headers) + body = await resp.text() + + if body.startswith('good') or body.startswith('nochg'): + return True + + _LOGGER.warning("Updating NO-IP failed: %s => %s", domain, + NO_IP_ERRORS[body.strip()]) + + except aiohttp.ClientError: + _LOGGER.warning("Can't connect to NO-IP API") + + except asyncio.TimeoutError: + _LOGGER.warning("Timeout from NO-IP API for domain: %s", domain) + + return False diff --git a/homeassistant/components/notify/ecobee.py b/homeassistant/components/notify/ecobee.py deleted file mode 100644 index 31e4c4751c800..0000000000000 --- a/homeassistant/components/notify/ecobee.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -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/ -""" -import logging - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components import ecobee -from homeassistant.components.notify import ( - BaseNotificationService, PLATFORM_SCHEMA) - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['ecobee'] - -CONF_INDEX = 'index' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_INDEX, default=0): cv.positive_int, -}) - - -def get_service(hass, config, discovery_info=None): - """Get the Ecobee notification service.""" - index = config.get(CONF_INDEX) - return EcobeeNotificationService(index) - - -class EcobeeNotificationService(BaseNotificationService): - """Implement the notification service for the Ecobee thermostat.""" - - def __init__(self, thermostat_index): - """Initialize the service.""" - self.thermostat_index = thermostat_index - - def send_message(self, message="", **kwargs): - """Send a message to a command line.""" - ecobee.NETWORK.ecobee.send_message(self.thermostat_index, message) diff --git a/homeassistant/components/notify/joaoapps_join.py b/homeassistant/components/notify/joaoapps_join.py deleted file mode 100644 index a75ff9cd165b7..0000000000000 --- a/homeassistant/components/notify/joaoapps_join.py +++ /dev/null @@ -1,69 +0,0 @@ -""" -Join platform for notify component. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/notify.join/ -""" -import logging -import voluptuous as vol -from homeassistant.components.notify import ( - ATTR_DATA, ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, - BaseNotificationService) -from homeassistant.const import CONF_API_KEY -import homeassistant.helpers.config_validation as cv - -REQUIREMENTS = ['python-join-api==0.0.2'] - -_LOGGER = logging.getLogger(__name__) - -CONF_DEVICE_ID = 'device_id' -CONF_DEVICE_IDS = 'device_ids' -CONF_DEVICE_NAMES = 'device_names' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_API_KEY): cv.string, - vol.Optional(CONF_DEVICE_ID): cv.string, - vol.Optional(CONF_DEVICE_IDS): cv.string, - vol.Optional(CONF_DEVICE_NAMES): cv.string, -}) - - -def get_service(hass, config, discovery_info=None): - """Get the Join notification service.""" - api_key = config.get(CONF_API_KEY) - device_id = config.get(CONF_DEVICE_ID) - device_ids = config.get(CONF_DEVICE_IDS) - device_names = config.get(CONF_DEVICE_NAMES) - if api_key: - from pyjoin import get_devices - if not get_devices(api_key): - _LOGGER.error("Error connecting to Join. Check the API key") - return False - if device_id is None and device_ids is None and device_names is None: - _LOGGER.error("No device was provided. Please specify device_id" - ", device_ids, or device_names") - return False - return JoinNotificationService(api_key, device_id, - device_ids, device_names) - - -class JoinNotificationService(BaseNotificationService): - """Implement the notification service for Join.""" - - def __init__(self, api_key, device_id, device_ids, device_names): - """Initialize the service.""" - self._api_key = api_key - self._device_id = device_id - self._device_ids = device_ids - self._device_names = device_names - - def send_message(self, message="", **kwargs): - """Send a message to a user.""" - from pyjoin import send_notification - title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) - data = kwargs.get(ATTR_DATA) or {} - send_notification( - device_id=self._device_id, device_ids=self._device_ids, - device_names=self._device_names, text=message, title=title, - icon=data.get('icon'), smallicon=data.get('smallicon'), - vibration=data.get('vibration'), api_key=self._api_key) diff --git a/homeassistant/components/notify/knx.py b/homeassistant/components/notify/knx.py deleted file mode 100644 index 750e39455696e..0000000000000 --- a/homeassistant/components/notify/knx.py +++ /dev/null @@ -1,90 +0,0 @@ -""" -KNX/IP notification service. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/notify.knx/ -""" - -import voluptuous as vol - -from homeassistant.components.knx import DATA_KNX, ATTR_DISCOVER_DEVICES -from homeassistant.components.notify import PLATFORM_SCHEMA, \ - BaseNotificationService -from homeassistant.const import CONF_NAME -from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv - -CONF_ADDRESS = 'address' -DEFAULT_NAME = 'KNX Notify' -DEPENDENCIES = ['knx'] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_ADDRESS): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string -}) - - -async def async_get_service(hass, config, discovery_info=None): - """Get the KNX notification service.""" - return async_get_service_discovery(hass, discovery_info) \ - if discovery_info is not None else \ - async_get_service_config(hass, config) - - -@callback -def async_get_service_discovery(hass, discovery_info): - """Set up notifications for KNX platform configured via xknx.yaml.""" - notification_devices = [] - for device_name in discovery_info[ATTR_DISCOVER_DEVICES]: - device = hass.data[DATA_KNX].xknx.devices[device_name] - notification_devices.append(device) - return \ - KNXNotificationService(notification_devices) \ - if notification_devices else \ - None - - -@callback -def async_get_service_config(hass, config): - """Set up notification for KNX platform configured within platform.""" - import xknx - notification = xknx.devices.Notification( - hass.data[DATA_KNX].xknx, - name=config.get(CONF_NAME), - group_address=config.get(CONF_ADDRESS)) - hass.data[DATA_KNX].xknx.devices.add(notification) - return KNXNotificationService([notification, ]) - - -class KNXNotificationService(BaseNotificationService): - """Implement demo notification service.""" - - def __init__(self, devices): - """Initialize the service.""" - self.devices = devices - - @property - def targets(self): - """Return a dictionary of registered targets.""" - ret = {} - for device in self.devices: - ret[device.name] = device.name - return ret - - async def async_send_message(self, message="", **kwargs): - """Send a notification to knx bus.""" - if "target" in kwargs: - await self._async_send_to_device(message, kwargs["target"]) - else: - await self._async_send_to_all_devices(message) - - async def _async_send_to_all_devices(self, message): - """Send a notification to knx bus to all connected devices.""" - for device in self.devices: - await device.set(message) - - async def _async_send_to_device(self, message, names): - """Send a notification to knx bus to device with given names.""" - for device in self.devices: - if device.name in names: - await device.set(message) diff --git a/homeassistant/components/notify/lametric.py b/homeassistant/components/notify/lametric.py deleted file mode 100644 index 61bc5172af058..0000000000000 --- a/homeassistant/components/notify/lametric.py +++ /dev/null @@ -1,123 +0,0 @@ -""" -Notifier for LaMetric time. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/notify.lametric/ -""" -import logging - -from requests.exceptions import ConnectionError as RequestsConnectionError -import voluptuous as vol - -from homeassistant.components.notify import ( - ATTR_TARGET, ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService) -from homeassistant.const import CONF_ICON -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"] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - 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) -}) - - -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]) - - -class LaMetricNotificationService(BaseNotificationService): - """Implement the notification service for LaMetric.""" - - def __init__(self, hasslametricmanager, icon, lifetime, cycles, priority): - """Initialize the service.""" - self.hasslametricmanager = hasslametricmanager - self._icon = icon - self._lifetime = lifetime - self._cycles = cycles - self._priority = priority - self._devices = [] - - def send_message(self, message="", **kwargs): - """Send a message to some LaMetric device.""" - from lmnotify import SimpleFrame, Sound, Model - from oauthlib.oauth2 import TokenExpiredError - - targets = kwargs.get(ATTR_TARGET) - data = kwargs.get(ATTR_DATA) - _LOGGER.debug("Targets/Data: %s/%s", targets, data) - icon = self._icon - cycles = self._cycles - sound = None - priority = self._priority - - # Additional data? - if data is not None: - if "icon" in data: - icon = data["icon"] - if "sound" in data: - try: - sound = Sound(category="notifications", - sound_id=data["sound"]) - _LOGGER.debug("Adding notification sound %s", - data["sound"]) - except AssertionError: - _LOGGER.error("Sound ID %s unknown, ignoring", - data["sound"]) - if "cycles" in data: - cycles = int(data['cycles']) - if "priority" in data: - if data['priority'] in AVAILABLE_PRIORITIES: - priority = data['priority'] - else: - _LOGGER.warning("Priority %s invalid, using default %s", - data['priority'], priority) - - text_frame = SimpleFrame(icon, message) - _LOGGER.debug("Icon/Message/Cycles/Lifetime: %s, %s, %d, %d", - icon, message, self._cycles, self._lifetime) - - frames = [text_frame] - - model = Model(frames=frames, cycles=cycles, sound=sound) - lmn = self.hasslametricmanager.manager - try: - self._devices = lmn.get_devices() - except TokenExpiredError: - _LOGGER.debug("Token expired, fetching new token") - lmn.get_token() - self._devices = lmn.get_devices() - except RequestsConnectionError: - _LOGGER.warning("Problem connecting to LaMetric, " - "using cached devices instead") - for dev in self._devices: - if targets is None or dev["name"] in targets: - try: - lmn.set_device(dev) - lmn.send_notification(model, lifetime=self._lifetime, - priority=priority) - _LOGGER.debug("Sent notification to LaMetric %s", - dev["name"]) - except OSError: - _LOGGER.warning("Cannot connect to LaMetric %s", - dev["name"]) diff --git a/homeassistant/components/notify/matrix.py b/homeassistant/components/notify/matrix.py deleted file mode 100644 index fc29ad91dc915..0000000000000 --- a/homeassistant/components/notify/matrix.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -Matrix notification service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/notify.matrix/ -""" -import logging - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import (ATTR_TARGET, PLATFORM_SCHEMA, - BaseNotificationService, - ATTR_MESSAGE) - -_LOGGER = logging.getLogger(__name__) - -CONF_DEFAULT_ROOM = 'default_room' - -DOMAIN = 'matrix' -DEPENDENCIES = [DOMAIN] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_DEFAULT_ROOM): cv.string, -}) - - -def get_service(hass, config, discovery_info=None): - """Get the Matrix notification service.""" - return MatrixNotificationService(config.get(CONF_DEFAULT_ROOM)) - - -class MatrixNotificationService(BaseNotificationService): - """Send Notifications to a Matrix Room.""" - - def __init__(self, default_room): - """Set up the notification service.""" - self._default_room = default_room - - def send_message(self, message="", **kwargs): - """Send the message to the matrix server.""" - target_rooms = kwargs.get(ATTR_TARGET) or [self._default_room] - - service_data = { - ATTR_TARGET: target_rooms, - ATTR_MESSAGE: message - } - - return self.hass.services.call( - DOMAIN, 'send_message', service_data=service_data) diff --git a/homeassistant/components/notify/mysensors.py b/homeassistant/components/notify/mysensors.py deleted file mode 100644 index 71ce7fb0b74ee..0000000000000 --- a/homeassistant/components/notify/mysensors.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -MySensors notification service. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/notify.mysensors/ -""" -from homeassistant.components import mysensors -from homeassistant.components.notify import ( - ATTR_TARGET, DOMAIN, BaseNotificationService) - - -async def async_get_service(hass, config, discovery_info=None): - """Get the MySensors notification service.""" - new_devices = mysensors.setup_mysensors_platform( - hass, DOMAIN, discovery_info, MySensorsNotificationDevice) - if not new_devices: - return None - return MySensorsNotificationService(hass) - - -class MySensorsNotificationDevice(mysensors.device.MySensorsDevice): - """Represent a MySensors Notification device.""" - - def send_msg(self, msg): - """Send a message.""" - for sub_msg in [msg[i:i + 25] for i in range(0, len(msg), 25)]: - # Max mysensors payload is 25 bytes. - self.gateway.set_child_value( - self.node_id, self.child_id, self.value_type, sub_msg) - - def __repr__(self): - """Return the representation.""" - return "".format(self.name) - - -class MySensorsNotificationService(BaseNotificationService): - """Implement a MySensors notification service.""" - - def __init__(self, hass): - """Initialize the service.""" - self.devices = mysensors.get_mysensors_devices(hass, DOMAIN) - - async def async_send_message(self, message="", **kwargs): - """Send a message to a user.""" - target_devices = kwargs.get(ATTR_TARGET) - devices = [device for device in self.devices.values() - if target_devices is None or device.name in target_devices] - - for device in devices: - device.send_msg(message) diff --git a/homeassistant/components/notify/netgear_lte.py b/homeassistant/components/notify/netgear_lte.py deleted file mode 100644 index 9ba804e193d81..0000000000000 --- a/homeassistant/components/notify/netgear_lte.py +++ /dev/null @@ -1,57 +0,0 @@ -"""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/ -""" - -import logging - -import voluptuous as vol -import attr - -from homeassistant.components.notify import ( - BaseNotificationService, ATTR_TARGET, PLATFORM_SCHEMA) -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__) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_HOST): cv.string, - vol.Required(ATTR_TARGET): vol.All(cv.ensure_list, [cv.string]), -}) - - -async def async_get_service(hass, config, discovery_info=None): - """Get the notification service.""" - return NetgearNotifyService(hass, config) - - -@attr.s -class NetgearNotifyService(BaseNotificationService): - """Implementation of a notification service.""" - - hass = attr.ib() - config = attr.ib() - - async def async_send_message(self, message="", **kwargs): - """Send a message to a user.""" - modem_data = self.hass.data[DATA_KEY].get_modem_data(self.config) - if not modem_data: - _LOGGER.error("No modem available") - return - - phone = self.config.get(ATTR_TARGET) - targets = kwargs.get(ATTR_TARGET, phone) - if targets and message: - for target in targets: - import eternalegypt - try: - await modem_data.modem.sms(target, message) - except eternalegypt.Error: - _LOGGER.error("Unable to send to %s", target) diff --git a/homeassistant/components/notify/pushover.py b/homeassistant/components/notify/pushover.py index 3ec0b27e7c4e0..b249ca804b302 100644 --- a/homeassistant/components/notify/pushover.py +++ b/homeassistant/components/notify/pushover.py @@ -10,7 +10,7 @@ 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 @@ 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/tibber.py b/homeassistant/components/notify/tibber.py deleted file mode 100644 index ddbcb3f6c6507..0000000000000 --- a/homeassistant/components/notify/tibber.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -Tibber platform for notify component. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/notify.tibber/ -""" -import asyncio -import logging - -from homeassistant.components.notify import ( - ATTR_TITLE, ATTR_TITLE_DEFAULT, BaseNotificationService) -from homeassistant.components.tibber import DOMAIN as TIBBER_DOMAIN - - -_LOGGER = logging.getLogger(__name__) - - -async def async_get_service(hass, config, discovery_info=None): - """Get the Tibber notification service.""" - tibber_connection = hass.data[TIBBER_DOMAIN] - return TibberNotificationService(tibber_connection.send_notification) - - -class TibberNotificationService(BaseNotificationService): - """Implement the notification service for Tibber.""" - - def __init__(self, notify): - """Initialize the service.""" - self._notify = notify - - async def async_send_message(self, message=None, **kwargs): - """Send a message to Tibber devices.""" - title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) - try: - await self._notify(title=title, message=message) - except asyncio.TimeoutError: - _LOGGER.error("Timeout sending message with Tibber") diff --git a/homeassistant/components/notify/tplink_lte.py b/homeassistant/components/notify/tplink_lte.py deleted file mode 100644 index 9bb80e2591c3a..0000000000000 --- a/homeassistant/components/notify/tplink_lte.py +++ /dev/null @@ -1,50 +0,0 @@ -"""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/ -""" - -import logging - -import attr - -from homeassistant.components.notify import ( - ATTR_TARGET, BaseNotificationService) - -from ..tplink_lte import DATA_KEY - -DEPENDENCIES = ['tplink_lte'] - -_LOGGER = logging.getLogger(__name__) - - -async def async_get_service(hass, config, discovery_info=None): - """Get the notification service.""" - if discovery_info is None: - return - return TplinkNotifyService(hass, discovery_info) - - -@attr.s -class TplinkNotifyService(BaseNotificationService): - """Implementation of a notification service.""" - - hass = attr.ib() - config = attr.ib() - - async def async_send_message(self, message="", **kwargs): - """Send a message to a user.""" - import tp_connected - modem_data = self.hass.data[DATA_KEY].get_modem_data(self.config) - if not modem_data: - _LOGGER.error("No modem available") - return - - phone = self.config[ATTR_TARGET] - targets = kwargs.get(ATTR_TARGET, phone) - if targets and message: - for target in targets: - try: - await modem_data.modem.sms(target, message) - except tp_connected.Error: - _LOGGER.error("Unable to send to %s", target) diff --git a/homeassistant/components/notify/webostv.py b/homeassistant/components/notify/webostv.py deleted file mode 100644 index 92762b03aea54..0000000000000 --- a/homeassistant/components/notify/webostv.py +++ /dev/null @@ -1,73 +0,0 @@ -""" -LG WebOS TV notification service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/notify.webostv/ -""" -import logging - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import ( - ATTR_DATA, BaseNotificationService, PLATFORM_SCHEMA) -from homeassistant.const import (CONF_FILENAME, CONF_HOST, CONF_ICON) - -REQUIREMENTS = ['pylgtv==0.1.9'] - -_LOGGER = logging.getLogger(__name__) - -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 -}) - - -def get_service(hass, config, discovery_info=None): - """Return the notify service.""" - from pylgtv import WebOsClient - 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) - - if not client.is_registered(): - try: - client.register() - except PyLGTVPairException: - _LOGGER.error("Pairing with TV failed") - return None - except OSError: - _LOGGER.error("TV unreachable") - return None - - return LgWebOSNotificationService(client, config.get(CONF_ICON)) - - -class LgWebOSNotificationService(BaseNotificationService): - """Implement the notification service for LG WebOS TV.""" - - def __init__(self, client, icon_path): - """Initialize the service.""" - self._client = client - self._icon_path = icon_path - - def send_message(self, message="", **kwargs): - """Send a message to the tv.""" - from pylgtv import PyLGTVPairException - - try: - data = kwargs.get(ATTR_DATA) - icon_path = data.get(CONF_ICON, self._icon_path) if data else \ - self._icon_path - self._client.send_message(message, icon_path=icon_path) - except PyLGTVPairException: - _LOGGER.error("Pairing with TV failed") - except FileNotFoundError: - _LOGGER.error("Icon %s not found", icon_path) - except OSError: - _LOGGER.error("TV unreachable") diff --git a/homeassistant/components/notify/xmpp.py b/homeassistant/components/notify/xmpp.py index eac20c62797fb..462dd007d530f 100644 --- a/homeassistant/components/notify/xmpp.py +++ b/homeassistant/components/notify/xmpp.py @@ -21,7 +21,7 @@ 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.py deleted file mode 100644 index fb14f119dbde5..0000000000000 --- a/homeassistant/components/nuheat.py +++ /dev/null @@ -1,45 +0,0 @@ -""" -Support for NuHeat thermostats. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/nuheat/ -""" -import logging - -import voluptuous as vol - -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_DEVICES -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery - -REQUIREMENTS = ["nuheat==0.3.0"] - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "nuheat" - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_DEVICES, default=[]): - vol.All(cv.ensure_list, [cv.string]), - }), -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the NuHeat thermostat component.""" - import nuheat - - conf = config[DOMAIN] - username = conf.get(CONF_USERNAME) - password = conf.get(CONF_PASSWORD) - devices = conf.get(CONF_DEVICES) - - api = nuheat.NuHeat(username, password) - api.authenticate() - hass.data[DOMAIN] = (api, devices) - - discovery.load_platform(hass, "climate", DOMAIN, {}, config) - return True diff --git a/homeassistant/components/nuheat/__init__.py b/homeassistant/components/nuheat/__init__.py new file mode 100644 index 0000000000000..4ea37339ef35f --- /dev/null +++ b/homeassistant/components/nuheat/__init__.py @@ -0,0 +1,40 @@ +"""Support for NuHeat thermostats.""" +import logging + +import voluptuous as vol + +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_DEVICES +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import discovery + +REQUIREMENTS = ["nuheat==0.3.0"] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'nuheat' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_DEVICES, default=[]): + vol.All(cv.ensure_list, [cv.string]), + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the NuHeat thermostat component.""" + import nuheat + + conf = config[DOMAIN] + username = conf.get(CONF_USERNAME) + password = conf.get(CONF_PASSWORD) + devices = conf.get(CONF_DEVICES) + + api = nuheat.NuHeat(username, password) + api.authenticate() + hass.data[DOMAIN] = (api, devices) + + discovery.load_platform(hass, "climate", DOMAIN, {}, config) + return True diff --git a/homeassistant/components/nuimo_controller.py b/homeassistant/components/nuimo_controller.py deleted file mode 100644 index 0f8fbb3907316..0000000000000 --- a/homeassistant/components/nuimo_controller.py +++ /dev/null @@ -1,186 +0,0 @@ -""" -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/ -""" -import logging -import threading -import time - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.const import (CONF_MAC, CONF_NAME, EVENT_HOMEASSISTANT_STOP) - -REQUIREMENTS = [ - '--only-binary=all ' # avoid compilation of gattlib - 'nuimo==0.1.0'] - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'nuimo_controller' -EVENT_NUIMO = 'nuimo_input' - -DEFAULT_NAME = 'None' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(CONF_MAC): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string - }), -}, extra=vol.ALLOW_EXTRA) - -SERVICE_NUIMO = 'led_matrix' -DEFAULT_INTERVAL = 2.0 - -SERVICE_NUIMO_SCHEMA = vol.Schema({ - vol.Required('matrix'): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional('interval', default=DEFAULT_INTERVAL): float -}) - -DEFAULT_ADAPTER = 'hci0' - - -def setup(hass, config): - """Set up the Nuimo component.""" - conf = config[DOMAIN] - mac = conf.get(CONF_MAC) - name = conf.get(CONF_NAME) - NuimoThread(hass, mac, name).start() - return True - - -class NuimoLogger: - """Handle Nuimo Controller event callbacks.""" - - def __init__(self, hass, name): - """Initialize Logger object.""" - self._hass = hass - self._name = name - - def received_gesture_event(self, event): - """Input Event received.""" - _LOGGER.debug("Received event: name=%s, gesture_id=%s,value=%s", - event.name, event.gesture, event.value) - self._hass.bus.fire(EVENT_NUIMO, - {'type': event.name, 'value': event.value, - 'name': self._name}) - - -class NuimoThread(threading.Thread): - """Manage one Nuimo controller.""" - - def __init__(self, hass, mac, name): - """Initialize thread object.""" - super(NuimoThread, self).__init__() - self._hass = hass - self._mac = mac - self._name = name - self._hass_is_running = True - self._nuimo = None - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.stop) - - def run(self): - """Set up the connection or be idle.""" - while self._hass_is_running: - if not self._nuimo or not self._nuimo.is_connected(): - self._attach() - self._connect() - else: - time.sleep(1) - - if self._nuimo: - self._nuimo.disconnect() - self._nuimo = None - - def stop(self, event): - """Terminate Thread by unsetting flag.""" - _LOGGER.debug('Stopping thread for Nuimo %s', self._mac) - self._hass_is_running = False - - def _attach(self): - """Create a Nuimo object from MAC address or discovery.""" - # pylint: disable=import-error - from nuimo import NuimoController, NuimoDiscoveryManager - - if self._nuimo: - self._nuimo.disconnect() - self._nuimo = None - - if self._mac: - self._nuimo = NuimoController(self._mac) - else: - nuimo_manager = NuimoDiscoveryManager( - bluetooth_adapter=DEFAULT_ADAPTER, delegate=DiscoveryLogger()) - nuimo_manager.start_discovery() - # Were any Nuimos found? - if not nuimo_manager.nuimos: - _LOGGER.debug("No Nuimo devices detected") - return - # Take the first Nuimo found. - self._nuimo = nuimo_manager.nuimos[0] - self._mac = self._nuimo.addr - - def _connect(self): - """Build up connection and set event delegator and service.""" - if not self._nuimo: - return - - try: - self._nuimo.connect() - _LOGGER.debug("Connected to %s", self._mac) - except RuntimeError as error: - _LOGGER.error("Could not connect to %s: %s", self._mac, error) - time.sleep(1) - return - - nuimo_event_delegate = NuimoLogger(self._hass, self._name) - self._nuimo.set_delegate(nuimo_event_delegate) - - def handle_write_matrix(call): - """Handle led matrix service.""" - matrix = call.data.get('matrix', None) - name = call.data.get(CONF_NAME, DEFAULT_NAME) - interval = call.data.get('interval', DEFAULT_INTERVAL) - if self._name == name and matrix: - self._nuimo.write_matrix(matrix, interval) - - self._hass.services.register( - DOMAIN, SERVICE_NUIMO, handle_write_matrix, - schema=SERVICE_NUIMO_SCHEMA) - - self._nuimo.write_matrix(HOMEASSIST_LOGO, 2.0) - - -# must be 9x9 matrix -HOMEASSIST_LOGO = ( - " . " + - " ... " + - " ..... " + - " ....... " + - "..... ..." + - " ....... " + - " .. .... " + - " .. .... " + - ".........") - - -class DiscoveryLogger: - """Handle Nuimo Discovery callbacks.""" - - # pylint: disable=no-self-use - def discovery_started(self): - """Discovery started.""" - _LOGGER.info("Started discovery") - - # pylint: disable=no-self-use - def discovery_finished(self): - """Discovery finished.""" - _LOGGER.info("Finished discovery") - - # pylint: disable=no-self-use - def controller_added(self, nuimo): - """Return that a controller was found.""" - _LOGGER.info("Added Nuimo: %s", nuimo) diff --git a/homeassistant/components/nuimo_controller/__init__.py b/homeassistant/components/nuimo_controller/__init__.py new file mode 100644 index 0000000000000..70509469d2bcf --- /dev/null +++ b/homeassistant/components/nuimo_controller/__init__.py @@ -0,0 +1,181 @@ +"""Support for Nuimo device over Bluetooth LE.""" +import logging +import threading +import time + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import (CONF_MAC, CONF_NAME, EVENT_HOMEASSISTANT_STOP) + +REQUIREMENTS = [ + '--only-binary=all ' # avoid compilation of gattlib + 'nuimo==0.1.0'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'nuimo_controller' +EVENT_NUIMO = 'nuimo_input' + +DEFAULT_NAME = 'None' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Optional(CONF_MAC): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string + }), +}, extra=vol.ALLOW_EXTRA) + +SERVICE_NUIMO = 'led_matrix' +DEFAULT_INTERVAL = 2.0 + +SERVICE_NUIMO_SCHEMA = vol.Schema({ + vol.Required('matrix'): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional('interval', default=DEFAULT_INTERVAL): float +}) + +DEFAULT_ADAPTER = 'hci0' + + +def setup(hass, config): + """Set up the Nuimo component.""" + conf = config[DOMAIN] + mac = conf.get(CONF_MAC) + name = conf.get(CONF_NAME) + NuimoThread(hass, mac, name).start() + return True + + +class NuimoLogger: + """Handle Nuimo Controller event callbacks.""" + + def __init__(self, hass, name): + """Initialize Logger object.""" + self._hass = hass + self._name = name + + def received_gesture_event(self, event): + """Input Event received.""" + _LOGGER.debug("Received event: name=%s, gesture_id=%s,value=%s", + event.name, event.gesture, event.value) + self._hass.bus.fire(EVENT_NUIMO, + {'type': event.name, 'value': event.value, + 'name': self._name}) + + +class NuimoThread(threading.Thread): + """Manage one Nuimo controller.""" + + def __init__(self, hass, mac, name): + """Initialize thread object.""" + super(NuimoThread, self).__init__() + self._hass = hass + self._mac = mac + self._name = name + self._hass_is_running = True + self._nuimo = None + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.stop) + + def run(self): + """Set up the connection or be idle.""" + while self._hass_is_running: + if not self._nuimo or not self._nuimo.is_connected(): + self._attach() + self._connect() + else: + time.sleep(1) + + if self._nuimo: + self._nuimo.disconnect() + self._nuimo = None + + def stop(self, event): + """Terminate Thread by unsetting flag.""" + _LOGGER.debug('Stopping thread for Nuimo %s', self._mac) + self._hass_is_running = False + + def _attach(self): + """Create a Nuimo object from MAC address or discovery.""" + # pylint: disable=import-error + from nuimo import NuimoController, NuimoDiscoveryManager + + if self._nuimo: + self._nuimo.disconnect() + self._nuimo = None + + if self._mac: + self._nuimo = NuimoController(self._mac) + else: + nuimo_manager = NuimoDiscoveryManager( + bluetooth_adapter=DEFAULT_ADAPTER, delegate=DiscoveryLogger()) + nuimo_manager.start_discovery() + # Were any Nuimos found? + if not nuimo_manager.nuimos: + _LOGGER.debug("No Nuimo devices detected") + return + # Take the first Nuimo found. + self._nuimo = nuimo_manager.nuimos[0] + self._mac = self._nuimo.addr + + def _connect(self): + """Build up connection and set event delegator and service.""" + if not self._nuimo: + return + + try: + self._nuimo.connect() + _LOGGER.debug("Connected to %s", self._mac) + except RuntimeError as error: + _LOGGER.error("Could not connect to %s: %s", self._mac, error) + time.sleep(1) + return + + nuimo_event_delegate = NuimoLogger(self._hass, self._name) + self._nuimo.set_delegate(nuimo_event_delegate) + + def handle_write_matrix(call): + """Handle led matrix service.""" + matrix = call.data.get('matrix', None) + name = call.data.get(CONF_NAME, DEFAULT_NAME) + interval = call.data.get('interval', DEFAULT_INTERVAL) + if self._name == name and matrix: + self._nuimo.write_matrix(matrix, interval) + + self._hass.services.register( + DOMAIN, SERVICE_NUIMO, handle_write_matrix, + schema=SERVICE_NUIMO_SCHEMA) + + self._nuimo.write_matrix(HOMEASSIST_LOGO, 2.0) + + +# must be 9x9 matrix +HOMEASSIST_LOGO = ( + " . " + + " ... " + + " ..... " + + " ....... " + + "..... ..." + + " ....... " + + " .. .... " + + " .. .... " + + ".........") + + +class DiscoveryLogger: + """Handle Nuimo Discovery callbacks.""" + + # pylint: disable=no-self-use + def discovery_started(self): + """Discovery started.""" + _LOGGER.info("Started discovery") + + # pylint: disable=no-self-use + def discovery_finished(self): + """Discovery finished.""" + _LOGGER.info("Finished discovery") + + # pylint: disable=no-self-use + def controller_added(self, nuimo): + """Return that a controller was found.""" + _LOGGER.info("Added Nuimo: %s", nuimo) diff --git a/homeassistant/components/octoprint.py b/homeassistant/components/octoprint.py deleted file mode 100644 index 869f3bd7d6e95..0000000000000 --- a/homeassistant/components/octoprint.py +++ /dev/null @@ -1,252 +0,0 @@ -""" -Support for monitoring OctoPrint 3D printers. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/octoprint/ -""" -import logging -import time - -import requests -import voluptuous as vol -from aiohttp.hdrs import CONTENT_TYPE - -from homeassistant.components.discovery import SERVICE_OCTOPRINT -from homeassistant.const import ( - CONF_API_KEY, CONF_HOST, CONTENT_TYPE_JSON, CONF_NAME, CONF_PATH, - CONF_PORT, CONF_SSL, TEMP_CELSIUS, CONF_MONITORED_CONDITIONS, CONF_SENSORS, - CONF_BINARY_SENSORS) -from homeassistant.helpers import discovery -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import load_platform -from homeassistant.util import slugify as util_slugify - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'octoprint' -CONF_NUMBER_OF_TOOLS = 'number_of_tools' -CONF_BED = 'bed' -DEFAULT_NAME = 'OctoPrint' - - -def has_all_unique_names(value): - """Validate that printers have an unique name.""" - names = [util_slugify(printer['name']) for printer in value] - vol.Schema(vol.Unique())(names) - return value - - -def ensure_valid_path(value): - """Validate the path, ensuring it starts and ends with a /.""" - vol.Schema(cv.string)(value) - if value[0] != '/': - value = '/' + value - if value[-1] != '/': - value += '/' - return value - - -BINARY_SENSOR_TYPES = { - # API Endpoint, Group, Key, unit - 'Printing': ['printer', 'state', 'printing', None], - "Printing Error": ['printer', 'state', 'error', None] -} - -BINARY_SENSOR_SCHEMA = vol.Schema({ - vol.Optional(CONF_MONITORED_CONDITIONS, default=list(BINARY_SENSOR_TYPES)): - vol.All(cv.ensure_list, [vol.In(BINARY_SENSOR_TYPES)]), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, -}) - -SENSOR_TYPES = { - # API Endpoint, Group, Key, unit, icon - 'Temperatures': ['printer', 'temperature', '*', TEMP_CELSIUS], - "Current State": ['printer', 'state', 'text', None, 'mdi:printer-3d'], - "Job Percentage": ['job', 'progress', 'completion', '%', - 'mdi:file-percent'], - "Time Remaining": ['job', 'progress', 'printTimeLeft', 'seconds', - 'mdi:clock-end'], - "Time Elapsed": ['job', 'progress', 'printTime', 'seconds', - 'mdi:clock-start'], -} - -SENSOR_SCHEMA = vol.Schema({ - vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, -}) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_SSL, default=False): cv.boolean, - vol.Optional(CONF_PORT, default=80): cv.port, - vol.Optional(CONF_PATH, default='/'): ensure_valid_path, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_NUMBER_OF_TOOLS, default=0): cv.positive_int, - vol.Optional(CONF_BED, default=False): cv.boolean, - vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, - vol.Optional(CONF_BINARY_SENSORS, default={}): BINARY_SENSOR_SCHEMA - })], has_all_unique_names), -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the OctoPrint component.""" - printers = hass.data[DOMAIN] = {} - success = False - - def device_discovered(service, info): - """Get called when an Octoprint server has been discovered.""" - _LOGGER.debug("Found an Octoprint server: %s", info) - - discovery.listen(hass, SERVICE_OCTOPRINT, device_discovered) - - if DOMAIN not in config: - # Skip the setup if there is no configuration present - return True - - for printer in config[DOMAIN]: - name = printer[CONF_NAME] - ssl = 's' if printer[CONF_SSL] else '' - base_url = 'http{}://{}:{}{}api/'.format(ssl, - printer[CONF_HOST], - printer[CONF_PORT], - printer[CONF_PATH]) - api_key = printer[CONF_API_KEY] - number_of_tools = printer[CONF_NUMBER_OF_TOOLS] - bed = printer[CONF_BED] - try: - octoprint_api = OctoPrintAPI(base_url, api_key, bed, - number_of_tools) - printers[base_url] = octoprint_api - octoprint_api.get('printer') - octoprint_api.get('job') - except requests.exceptions.RequestException as conn_err: - _LOGGER.error("Error setting up OctoPrint API: %r", conn_err) - continue - - sensors = printer[CONF_SENSORS][CONF_MONITORED_CONDITIONS] - load_platform(hass, 'sensor', DOMAIN, {'name': name, - 'base_url': base_url, - 'sensors': sensors}, config) - b_sensors = printer[CONF_BINARY_SENSORS][CONF_MONITORED_CONDITIONS] - load_platform(hass, 'binary_sensor', DOMAIN, {'name': name, - 'base_url': base_url, - 'sensors': b_sensors}, - config) - success = True - - return success - - -class OctoPrintAPI: - """Simple JSON wrapper for OctoPrint's API.""" - - def __init__(self, api_url, key, bed, number_of_tools): - """Initialize OctoPrint API and set headers needed later.""" - self.api_url = api_url - self.headers = { - CONTENT_TYPE: CONTENT_TYPE_JSON, - 'X-Api-Key': key, - } - self.printer_last_reading = [{}, None] - self.job_last_reading = [{}, None] - self.job_available = False - self.printer_available = False - self.available = False - self.printer_error_logged = False - self.job_error_logged = False - self.bed = bed - self.number_of_tools = number_of_tools - - def get_tools(self): - """Get the list of tools that temperature is monitored on.""" - tools = [] - if self.number_of_tools > 0: - for tool_number in range(0, self.number_of_tools): - tools.append('tool' + str(tool_number)) - if self.bed: - tools.append('bed') - if not self.bed and self.number_of_tools == 0: - temps = self.printer_last_reading[0].get('temperature') - if temps is not None: - tools = temps.keys() - return tools - - def get(self, endpoint): - """Send a get request, and return the response as a dict.""" - # Only query the API at most every 30 seconds - now = time.time() - if endpoint == 'job': - last_time = self.job_last_reading[1] - if last_time is not None: - if now - last_time < 30.0: - return self.job_last_reading[0] - elif endpoint == 'printer': - last_time = self.printer_last_reading[1] - if last_time is not None: - if now - last_time < 30.0: - return self.printer_last_reading[0] - - url = self.api_url + endpoint - try: - response = requests.get( - url, headers=self.headers, timeout=9) - response.raise_for_status() - if endpoint == 'job': - self.job_last_reading[0] = response.json() - self.job_last_reading[1] = time.time() - self.job_available = True - elif endpoint == 'printer': - self.printer_last_reading[0] = response.json() - self.printer_last_reading[1] = time.time() - self.printer_available = True - self.available = self.printer_available and self.job_available - if self.available: - self.job_error_logged = False - self.printer_error_logged = False - return response.json() - except Exception as conn_exc: # pylint: disable=broad-except - log_string = "Failed to update OctoPrint status. " + \ - " Error: %s" % (conn_exc) - # Only log the first failure - if endpoint == 'job': - log_string = "Endpoint: job " + log_string - if not self.job_error_logged: - _LOGGER.error(log_string) - self.job_error_logged = True - self.job_available = False - elif endpoint == 'printer': - log_string = "Endpoint: printer " + log_string - if not self.printer_error_logged: - _LOGGER.error(log_string) - self.printer_error_logged = True - self.printer_available = False - self.available = False - return None - - def update(self, sensor_type, end_point, group, tool=None): - """Return the value for sensor_type from the provided endpoint.""" - response = self.get(end_point) - if response is not None: - return get_value_from_json(response, sensor_type, group, tool) - return response - - -def get_value_from_json(json_dict, sensor_type, group, tool): - """Return the value for sensor_type from the JSON.""" - if group not in json_dict: - return None - - if sensor_type in json_dict[group]: - if sensor_type == 'target' and json_dict[sensor_type] is None: - return 0 - return json_dict[group][sensor_type] - - if tool is not None: - if sensor_type in json_dict[group][tool]: - return json_dict[group][tool][sensor_type] - - return None diff --git a/homeassistant/components/octoprint/__init__.py b/homeassistant/components/octoprint/__init__.py new file mode 100644 index 0000000000000..35740a7be0dcf --- /dev/null +++ b/homeassistant/components/octoprint/__init__.py @@ -0,0 +1,248 @@ +"""Support for monitoring OctoPrint 3D printers.""" +import logging +import time + +import requests +import voluptuous as vol +from aiohttp.hdrs import CONTENT_TYPE + +from homeassistant.components.discovery import SERVICE_OCTOPRINT +from homeassistant.const import ( + CONF_API_KEY, CONF_HOST, CONTENT_TYPE_JSON, CONF_NAME, CONF_PATH, + CONF_PORT, CONF_SSL, TEMP_CELSIUS, CONF_MONITORED_CONDITIONS, CONF_SENSORS, + CONF_BINARY_SENSORS) +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform +from homeassistant.util import slugify as util_slugify + +_LOGGER = logging.getLogger(__name__) + +CONF_BED = 'bed' +CONF_NUMBER_OF_TOOLS = 'number_of_tools' + +DEFAULT_NAME = 'OctoPrint' +DOMAIN = 'octoprint' + + +def has_all_unique_names(value): + """Validate that printers have an unique name.""" + names = [util_slugify(printer['name']) for printer in value] + vol.Schema(vol.Unique())(names) + return value + + +def ensure_valid_path(value): + """Validate the path, ensuring it starts and ends with a /.""" + vol.Schema(cv.string)(value) + if value[0] != '/': + value = '/' + value + if value[-1] != '/': + value += '/' + return value + + +BINARY_SENSOR_TYPES = { + # API Endpoint, Group, Key, unit + 'Printing': ['printer', 'state', 'printing', None], + "Printing Error": ['printer', 'state', 'error', None] +} + +BINARY_SENSOR_SCHEMA = vol.Schema({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(BINARY_SENSOR_TYPES)): + vol.All(cv.ensure_list, [vol.In(BINARY_SENSOR_TYPES)]), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + +SENSOR_TYPES = { + # API Endpoint, Group, Key, unit, icon + 'Temperatures': ['printer', 'temperature', '*', TEMP_CELSIUS], + "Current State": ['printer', 'state', 'text', None, 'mdi:printer-3d'], + "Job Percentage": ['job', 'progress', 'completion', '%', + 'mdi:file-percent'], + "Time Remaining": ['job', 'progress', 'printTimeLeft', 'seconds', + 'mdi:clock-end'], + "Time Elapsed": ['job', 'progress', 'printTime', 'seconds', + 'mdi:clock-start'], +} + +SENSOR_SCHEMA = vol.Schema({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_SSL, default=False): cv.boolean, + vol.Optional(CONF_PORT, default=80): cv.port, + vol.Optional(CONF_PATH, default='/'): ensure_valid_path, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NUMBER_OF_TOOLS, default=0): cv.positive_int, + vol.Optional(CONF_BED, default=False): cv.boolean, + vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, + vol.Optional(CONF_BINARY_SENSORS, default={}): BINARY_SENSOR_SCHEMA + })], has_all_unique_names), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the OctoPrint component.""" + printers = hass.data[DOMAIN] = {} + success = False + + def device_discovered(service, info): + """Get called when an Octoprint server has been discovered.""" + _LOGGER.debug("Found an Octoprint server: %s", info) + + discovery.listen(hass, SERVICE_OCTOPRINT, device_discovered) + + if DOMAIN not in config: + # Skip the setup if there is no configuration present + return True + + for printer in config[DOMAIN]: + name = printer[CONF_NAME] + ssl = 's' if printer[CONF_SSL] else '' + base_url = 'http{}://{}:{}{}api/'.format(ssl, + printer[CONF_HOST], + printer[CONF_PORT], + printer[CONF_PATH]) + api_key = printer[CONF_API_KEY] + number_of_tools = printer[CONF_NUMBER_OF_TOOLS] + bed = printer[CONF_BED] + try: + octoprint_api = OctoPrintAPI(base_url, api_key, bed, + number_of_tools) + printers[base_url] = octoprint_api + octoprint_api.get('printer') + octoprint_api.get('job') + except requests.exceptions.RequestException as conn_err: + _LOGGER.error("Error setting up OctoPrint API: %r", conn_err) + continue + + sensors = printer[CONF_SENSORS][CONF_MONITORED_CONDITIONS] + load_platform(hass, 'sensor', DOMAIN, {'name': name, + 'base_url': base_url, + 'sensors': sensors}, config) + b_sensors = printer[CONF_BINARY_SENSORS][CONF_MONITORED_CONDITIONS] + load_platform(hass, 'binary_sensor', DOMAIN, {'name': name, + 'base_url': base_url, + 'sensors': b_sensors}, + config) + success = True + + return success + + +class OctoPrintAPI: + """Simple JSON wrapper for OctoPrint's API.""" + + def __init__(self, api_url, key, bed, number_of_tools): + """Initialize OctoPrint API and set headers needed later.""" + self.api_url = api_url + self.headers = { + CONTENT_TYPE: CONTENT_TYPE_JSON, + 'X-Api-Key': key, + } + self.printer_last_reading = [{}, None] + self.job_last_reading = [{}, None] + self.job_available = False + self.printer_available = False + self.available = False + self.printer_error_logged = False + self.job_error_logged = False + self.bed = bed + self.number_of_tools = number_of_tools + + def get_tools(self): + """Get the list of tools that temperature is monitored on.""" + tools = [] + if self.number_of_tools > 0: + for tool_number in range(0, self.number_of_tools): + tools.append('tool' + str(tool_number)) + if self.bed: + tools.append('bed') + if not self.bed and self.number_of_tools == 0: + temps = self.printer_last_reading[0].get('temperature') + if temps is not None: + tools = temps.keys() + return tools + + def get(self, endpoint): + """Send a get request, and return the response as a dict.""" + # Only query the API at most every 30 seconds + now = time.time() + if endpoint == 'job': + last_time = self.job_last_reading[1] + if last_time is not None: + if now - last_time < 30.0: + return self.job_last_reading[0] + elif endpoint == 'printer': + last_time = self.printer_last_reading[1] + if last_time is not None: + if now - last_time < 30.0: + return self.printer_last_reading[0] + + url = self.api_url + endpoint + try: + response = requests.get( + url, headers=self.headers, timeout=9) + response.raise_for_status() + if endpoint == 'job': + self.job_last_reading[0] = response.json() + self.job_last_reading[1] = time.time() + self.job_available = True + elif endpoint == 'printer': + self.printer_last_reading[0] = response.json() + self.printer_last_reading[1] = time.time() + self.printer_available = True + self.available = self.printer_available and self.job_available + if self.available: + self.job_error_logged = False + self.printer_error_logged = False + return response.json() + except Exception as conn_exc: # pylint: disable=broad-except + log_string = "Failed to update OctoPrint status. " + \ + " Error: %s" % (conn_exc) + # Only log the first failure + if endpoint == 'job': + log_string = "Endpoint: job " + log_string + if not self.job_error_logged: + _LOGGER.error(log_string) + self.job_error_logged = True + self.job_available = False + elif endpoint == 'printer': + log_string = "Endpoint: printer " + log_string + if not self.printer_error_logged: + _LOGGER.error(log_string) + self.printer_error_logged = True + self.printer_available = False + self.available = False + return None + + def update(self, sensor_type, end_point, group, tool=None): + """Return the value for sensor_type from the provided endpoint.""" + response = self.get(end_point) + if response is not None: + return get_value_from_json(response, sensor_type, group, tool) + return response + + +def get_value_from_json(json_dict, sensor_type, group, tool): + """Return the value for sensor_type from the JSON.""" + if group not in json_dict: + return None + + if sensor_type in json_dict[group]: + if sensor_type == 'target' and json_dict[sensor_type] is None: + return 0 + return json_dict[group][sensor_type] + + if tool is not None: + if sensor_type in json_dict[group][tool]: + return json_dict[group][tool][sensor_type] + + return None diff --git a/homeassistant/components/octoprint/binary_sensor.py b/homeassistant/components/octoprint/binary_sensor.py new file mode 100644 index 0000000000000..cb86017779617 --- /dev/null +++ b/homeassistant/components/octoprint/binary_sensor.py @@ -0,0 +1,79 @@ +"""Support for monitoring OctoPrint binary sensors.""" +import logging + +import requests + +from homeassistant.components.octoprint import (BINARY_SENSOR_TYPES, + DOMAIN as COMPONENT_DOMAIN) +from homeassistant.components.binary_sensor import BinarySensorDevice + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['octoprint'] + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the available OctoPrint binary sensors.""" + if discovery_info is None: + return + + name = discovery_info['name'] + base_url = discovery_info['base_url'] + monitored_conditions = discovery_info['sensors'] + octoprint_api = hass.data[COMPONENT_DOMAIN][base_url] + + devices = [] + for octo_type in monitored_conditions: + new_sensor = OctoPrintBinarySensor( + octoprint_api, octo_type, BINARY_SENSOR_TYPES[octo_type][2], + name, BINARY_SENSOR_TYPES[octo_type][3], + BINARY_SENSOR_TYPES[octo_type][0], + BINARY_SENSOR_TYPES[octo_type][1], 'flags') + devices.append(new_sensor) + add_entities(devices, True) + + +class OctoPrintBinarySensor(BinarySensorDevice): + """Representation an OctoPrint binary sensor.""" + + def __init__(self, api, condition, sensor_type, sensor_name, unit, + endpoint, group, tool=None): + """Initialize a new OctoPrint sensor.""" + self.sensor_name = sensor_name + if tool is None: + self._name = '{} {}'.format(sensor_name, condition) + else: + self._name = '{} {}'.format(sensor_name, condition) + self.sensor_type = sensor_type + self.api = api + self._state = False + self._unit_of_measurement = unit + self.api_endpoint = endpoint + self.api_group = group + self.api_tool = tool + _LOGGER.debug("Created OctoPrint binary sensor %r", self) + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def is_on(self): + """Return true if binary sensor is on.""" + return bool(self._state) + + @property + def device_class(self): + """Return the class of this sensor, from DEVICE_CLASSES.""" + return None + + def update(self): + """Update state of sensor.""" + try: + self._state = self.api.update( + self.sensor_type, self.api_endpoint, self.api_group, + self.api_tool) + except requests.exceptions.ConnectionError: + # Error calling the api, already logged in api.update() + return diff --git a/homeassistant/components/octoprint/sensor.py b/homeassistant/components/octoprint/sensor.py new file mode 100644 index 0000000000000..2df307f02ef52 --- /dev/null +++ b/homeassistant/components/octoprint/sensor.py @@ -0,0 +1,118 @@ +"""Support for monitoring OctoPrint sensors.""" +import logging + +import requests + +from homeassistant.components.octoprint import (SENSOR_TYPES, + DOMAIN as COMPONENT_DOMAIN) +from homeassistant.const import (TEMP_CELSIUS) +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['octoprint'] + +NOTIFICATION_ID = 'octoprint_notification' +NOTIFICATION_TITLE = 'OctoPrint sensor setup error' + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the available OctoPrint sensors.""" + if discovery_info is None: + return + + name = discovery_info['name'] + base_url = discovery_info['base_url'] + monitored_conditions = discovery_info['sensors'] + octoprint_api = hass.data[COMPONENT_DOMAIN][base_url] + tools = octoprint_api.get_tools() + + if "Temperatures" in monitored_conditions: + if not tools: + hass.components.persistent_notification.create( + 'Your printer appears to be offline.
' + 'If you do not want to have your printer on
' + ' at all times, and you would like to monitor
' + 'temperatures, please add
' + 'bed and/or number_of_tools to your config
' + 'and restart.', + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + + devices = [] + types = ["actual", "target"] + for octo_type in monitored_conditions: + if octo_type == "Temperatures": + for tool in tools: + for temp_type in types: + new_sensor = OctoPrintSensor( + octoprint_api, temp_type, temp_type, name, + SENSOR_TYPES[octo_type][3], SENSOR_TYPES[octo_type][0], + SENSOR_TYPES[octo_type][1], tool) + devices.append(new_sensor) + else: + new_sensor = OctoPrintSensor( + octoprint_api, octo_type, SENSOR_TYPES[octo_type][2], + name, SENSOR_TYPES[octo_type][3], SENSOR_TYPES[octo_type][0], + SENSOR_TYPES[octo_type][1], None, SENSOR_TYPES[octo_type][4]) + devices.append(new_sensor) + add_entities(devices, True) + + +class OctoPrintSensor(Entity): + """Representation of an OctoPrint sensor.""" + + def __init__(self, api, condition, sensor_type, sensor_name, unit, + endpoint, group, tool=None, icon=None): + """Initialize a new OctoPrint sensor.""" + self.sensor_name = sensor_name + if tool is None: + self._name = '{} {}'.format(sensor_name, condition) + else: + self._name = '{} {} {} {}'.format( + sensor_name, condition, tool, 'temp') + self.sensor_type = sensor_type + self.api = api + self._state = None + self._unit_of_measurement = unit + self.api_endpoint = endpoint + self.api_group = group + self.api_tool = tool + self._icon = icon + _LOGGER.debug("Created OctoPrint sensor %r", self) + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + sensor_unit = self.unit_of_measurement + if sensor_unit in (TEMP_CELSIUS, "%"): + # API sometimes returns null and not 0 + if self._state is None: + self._state = 0 + return round(self._state, 2) + 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): + """Update state of sensor.""" + try: + self._state = self.api.update( + self.sensor_type, self.api_endpoint, self.api_group, + self.api_tool) + except requests.exceptions.ConnectionError: + # Error calling the api, already logged in api.update() + return + + @property + def icon(self): + """Icon to use in the frontend.""" + return self._icon diff --git a/homeassistant/components/onboarding/__init__.py b/homeassistant/components/onboarding/__init__.py index 25aca9f8afaaf..6bbe546dcb198 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 497fa827f083d..804589200fa5d 100644 --- a/homeassistant/components/onboarding/views.py +++ b/homeassistant/components/onboarding/views.py @@ -94,6 +94,10 @@ async def post(self, request, data): }) 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 cf74aae75777a..7676806cfdfb9 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 @@ 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 @@ }), }, 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/opentherm_gw/binary_sensor.py b/homeassistant/components/opentherm_gw/binary_sensor.py new file mode 100644 index 0000000000000..b35998c807bce --- /dev/null +++ b/homeassistant/components/opentherm_gw/binary_sensor.py @@ -0,0 +1,140 @@ +"""Support for OpenTherm Gateway binary sensors.""" +import logging + +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, ENTITY_ID_FORMAT) +from homeassistant.components.opentherm_gw import ( + DATA_GW_VARS, DATA_OPENTHERM_GW, SIGNAL_OPENTHERM_GW_UPDATE) +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import async_generate_entity_id + +_LOGGER = logging.getLogger(__name__) + +DEVICE_CLASS_COLD = 'cold' +DEVICE_CLASS_HEAT = 'heat' +DEVICE_CLASS_PROBLEM = 'problem' + +DEPENDENCIES = ['opentherm_gw'] + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Set up the OpenTherm Gateway binary sensors.""" + if discovery_info is None: + return + gw_vars = hass.data[DATA_OPENTHERM_GW][DATA_GW_VARS] + sensor_info = { + # [device_class, friendly_name] + gw_vars.DATA_MASTER_CH_ENABLED: [ + None, "Thermostat Central Heating Enabled"], + gw_vars.DATA_MASTER_DHW_ENABLED: [ + None, "Thermostat Hot Water Enabled"], + gw_vars.DATA_MASTER_COOLING_ENABLED: [ + None, "Thermostat Cooling Enabled"], + gw_vars.DATA_MASTER_OTC_ENABLED: [ + None, "Thermostat Outside Temperature Correction Enabled"], + gw_vars.DATA_MASTER_CH2_ENABLED: [ + None, "Thermostat Central Heating 2 Enabled"], + gw_vars.DATA_SLAVE_FAULT_IND: [ + DEVICE_CLASS_PROBLEM, "Boiler Fault Indication"], + gw_vars.DATA_SLAVE_CH_ACTIVE: [ + DEVICE_CLASS_HEAT, "Boiler Central Heating Status"], + gw_vars.DATA_SLAVE_DHW_ACTIVE: [ + DEVICE_CLASS_HEAT, "Boiler Hot Water Status"], + gw_vars.DATA_SLAVE_FLAME_ON: [ + DEVICE_CLASS_HEAT, "Boiler Flame Status"], + gw_vars.DATA_SLAVE_COOLING_ACTIVE: [ + DEVICE_CLASS_COLD, "Boiler Cooling Status"], + gw_vars.DATA_SLAVE_CH2_ACTIVE: [ + DEVICE_CLASS_HEAT, "Boiler Central Heating 2 Status"], + gw_vars.DATA_SLAVE_DIAG_IND: [ + DEVICE_CLASS_PROBLEM, "Boiler Diagnostics Indication"], + gw_vars.DATA_SLAVE_DHW_PRESENT: [None, "Boiler Hot Water Present"], + gw_vars.DATA_SLAVE_CONTROL_TYPE: [None, "Boiler Control Type"], + gw_vars.DATA_SLAVE_COOLING_SUPPORTED: [None, "Boiler Cooling Support"], + gw_vars.DATA_SLAVE_DHW_CONFIG: [ + None, "Boiler Hot Water Configuration"], + gw_vars.DATA_SLAVE_MASTER_LOW_OFF_PUMP: [ + None, "Boiler Pump Commands Support"], + gw_vars.DATA_SLAVE_CH2_PRESENT: [ + None, "Boiler Central Heating 2 Present"], + gw_vars.DATA_SLAVE_SERVICE_REQ: [ + DEVICE_CLASS_PROBLEM, "Boiler Service Required"], + gw_vars.DATA_SLAVE_REMOTE_RESET: [None, "Boiler Remote Reset Support"], + gw_vars.DATA_SLAVE_LOW_WATER_PRESS: [ + DEVICE_CLASS_PROBLEM, "Boiler Low Water Pressure"], + gw_vars.DATA_SLAVE_GAS_FAULT: [ + DEVICE_CLASS_PROBLEM, "Boiler Gas Fault"], + gw_vars.DATA_SLAVE_AIR_PRESS_FAULT: [ + DEVICE_CLASS_PROBLEM, "Boiler Air Pressure Fault"], + gw_vars.DATA_SLAVE_WATER_OVERTEMP: [ + DEVICE_CLASS_PROBLEM, "Boiler Water Overtemperature"], + gw_vars.DATA_REMOTE_TRANSFER_DHW: [ + None, "Remote Hot Water Setpoint Transfer Support"], + gw_vars.DATA_REMOTE_TRANSFER_MAX_CH: [ + None, "Remote Maximum Central Heating Setpoint Write Support"], + gw_vars.DATA_REMOTE_RW_DHW: [ + None, "Remote Hot Water Setpoint Write Support"], + gw_vars.DATA_REMOTE_RW_MAX_CH: [ + None, "Remote Central Heating Setpoint Write Support"], + gw_vars.DATA_ROVRD_MAN_PRIO: [ + None, "Remote Override Manual Change Priority"], + gw_vars.DATA_ROVRD_AUTO_PRIO: [ + None, "Remote Override Program Change Priority"], + gw_vars.OTGW_GPIO_A_STATE: [None, "Gateway GPIO A State"], + gw_vars.OTGW_GPIO_B_STATE: [None, "Gateway GPIO B State"], + gw_vars.OTGW_IGNORE_TRANSITIONS: [None, "Gateway Ignore Transitions"], + gw_vars.OTGW_OVRD_HB: [None, "Gateway Override High Byte"], + } + sensors = [] + for var in discovery_info: + device_class = sensor_info[var][0] + friendly_name = sensor_info[var][1] + entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, var, hass=hass) + sensors.append(OpenThermBinarySensor(entity_id, var, device_class, + friendly_name)) + async_add_entities(sensors) + + +class OpenThermBinarySensor(BinarySensorDevice): + """Represent an OpenTherm Gateway binary sensor.""" + + def __init__(self, entity_id, var, device_class, friendly_name): + """Initialize the binary sensor.""" + self.entity_id = entity_id + self._var = var + self._state = None + self._device_class = device_class + self._friendly_name = friendly_name + + async def async_added_to_hass(self): + """Subscribe to updates from the component.""" + _LOGGER.debug( + "Added OpenTherm Gateway binary sensor %s", self._friendly_name) + async_dispatcher_connect(self.hass, SIGNAL_OPENTHERM_GW_UPDATE, + self.receive_report) + + async def receive_report(self, status): + """Handle status updates from the component.""" + self._state = bool(status.get(self._var)) + self.async_schedule_update_ha_state() + + @property + def name(self): + """Return the friendly name.""" + return self._friendly_name + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self._state + + @property + def device_class(self): + """Return the class of this device.""" + return self._device_class + + @property + def should_poll(self): + """Return False because entity pushes its state.""" + return False diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py new file mode 100644 index 0000000000000..ff6acc1a8845d --- /dev/null +++ b/homeassistant/components/opentherm_gw/climate.py @@ -0,0 +1,171 @@ +"""Support for OpenTherm Gateway climate devices.""" +import logging + +from homeassistant.components.climate import (ClimateDevice, STATE_IDLE, + STATE_HEAT, STATE_COOL, + SUPPORT_TARGET_TEMPERATURE) +from homeassistant.components.opentherm_gw import ( + CONF_FLOOR_TEMP, CONF_PRECISION, DATA_DEVICE, DATA_GW_VARS, + DATA_OPENTHERM_GW, SIGNAL_OPENTHERM_GW_UPDATE) +from homeassistant.const import (ATTR_TEMPERATURE, CONF_NAME, PRECISION_HALVES, + PRECISION_TENTHS, PRECISION_WHOLE, + TEMP_CELSIUS) +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['opentherm_gw'] + +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]) + + +class OpenThermGateway(ClimateDevice): + """Representation of a climate device.""" + + def __init__(self, hass, config): + """Initialize the device.""" + self._gateway = hass.data[DATA_OPENTHERM_GW][DATA_DEVICE] + self._gw_vars = hass.data[DATA_OPENTHERM_GW][DATA_GW_VARS] + self.friendly_name = config.get(CONF_NAME) + self.floor_temp = config.get(CONF_FLOOR_TEMP) + self.temp_precision = config.get(CONF_PRECISION) + self._current_operation = STATE_IDLE + self._current_temperature = 0.0 + self._target_temperature = 0.0 + self._away_mode_a = None + self._away_mode_b = None + self._away_state_a = False + self._away_state_b = False + + async def async_added_to_hass(self): + """Connect to the OpenTherm Gateway device.""" + _LOGGER.debug("Added device %s", self.friendly_name) + async_dispatcher_connect(self.hass, SIGNAL_OPENTHERM_GW_UPDATE, + self.receive_report) + + async def receive_report(self, status): + """Receive and handle a new report from the Gateway.""" + ch_active = status.get(self._gw_vars.DATA_SLAVE_CH_ACTIVE) + flame_on = status.get(self._gw_vars.DATA_SLAVE_FLAME_ON) + cooling_active = status.get(self._gw_vars.DATA_SLAVE_COOLING_ACTIVE) + if ch_active and flame_on: + self._current_operation = STATE_HEAT + elif cooling_active: + self._current_operation = STATE_COOL + else: + self._current_operation = STATE_IDLE + self._current_temperature = status.get(self._gw_vars.DATA_ROOM_TEMP) + + temp = status.get(self._gw_vars.DATA_ROOM_SETPOINT_OVRD) + if temp is None: + temp = status.get(self._gw_vars.DATA_ROOM_SETPOINT) + self._target_temperature = temp + + # GPIO mode 5: 0 == Away + # GPIO mode 6: 1 == Away + gpio_a_state = status.get(self._gw_vars.OTGW_GPIO_A) + if gpio_a_state == 5: + self._away_mode_a = 0 + elif gpio_a_state == 6: + self._away_mode_a = 1 + else: + self._away_mode_a = None + gpio_b_state = status.get(self._gw_vars.OTGW_GPIO_B) + if gpio_b_state == 5: + self._away_mode_b = 0 + elif gpio_b_state == 6: + self._away_mode_b = 1 + else: + self._away_mode_b = None + if self._away_mode_a is not None: + self._away_state_a = (status.get( + self._gw_vars.OTGW_GPIO_A_STATE) == self._away_mode_a) + if self._away_mode_b is not None: + self._away_state_b = (status.get( + self._gw_vars.OTGW_GPIO_B_STATE) == self._away_mode_b) + self.async_schedule_update_ha_state() + + @property + def name(self): + """Return the friendly name.""" + return self.friendly_name + + @property + def precision(self): + """Return the precision of the system.""" + if self.temp_precision is not None: + return self.temp_precision + if self.hass.config.units.temperature_unit == TEMP_CELSIUS: + return PRECISION_HALVES + return PRECISION_WHOLE + + @property + def should_poll(self): + """Disable polling for this entity.""" + return False + + @property + def temperature_unit(self): + """Return the unit of measurement used by the platform.""" + return TEMP_CELSIUS + + @property + def current_operation(self): + """Return current operation ie. heat, cool, idle.""" + return self._current_operation + + @property + def current_temperature(self): + """Return the current temperature.""" + if self.floor_temp is True: + if self.temp_precision == PRECISION_HALVES: + return int(2 * self._current_temperature) / 2 + if self.temp_precision == PRECISION_TENTHS: + return int(10 * self._current_temperature) / 10 + return int(self._current_temperature) + return self._current_temperature + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._target_temperature + + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + return self.temp_precision + + @property + def is_away_mode_on(self): + """Return true if away mode is on.""" + return self._away_state_a or self._away_state_b + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + if ATTR_TEMPERATURE in kwargs: + temp = float(kwargs[ATTR_TEMPERATURE]) + self._target_temperature = await self._gateway.set_target_temp( + temp) + self.async_schedule_update_ha_state() + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS + + @property + def min_temp(self): + """Return the minimum temperature.""" + return 1 + + @property + def max_temp(self): + """Return the maximum temperature.""" + return 30 diff --git a/homeassistant/components/opentherm_gw/sensor.py b/homeassistant/components/opentherm_gw/sensor.py new file mode 100644 index 0000000000000..070f847e5e53c --- /dev/null +++ b/homeassistant/components/opentherm_gw/sensor.py @@ -0,0 +1,206 @@ +"""Support for OpenTherm Gateway sensors.""" +import logging + +from homeassistant.components.opentherm_gw import ( + DATA_GW_VARS, DATA_OPENTHERM_GW, SIGNAL_OPENTHERM_GW_UPDATE) +from homeassistant.components.sensor import ENTITY_ID_FORMAT +from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity, async_generate_entity_id + +_LOGGER = logging.getLogger(__name__) + +UNIT_BAR = 'bar' +UNIT_HOUR = 'h' +UNIT_KW = 'kW' +UNIT_L_MIN = 'L/min' +UNIT_PERCENT = '%' + +DEPENDENCIES = ['opentherm_gw'] + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Set up the OpenTherm Gateway sensors.""" + if discovery_info is None: + return + gw_vars = hass.data[DATA_OPENTHERM_GW][DATA_GW_VARS] + sensor_info = { + # [device_class, unit, friendly_name] + gw_vars.DATA_CONTROL_SETPOINT: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Control Setpoint"], + gw_vars.DATA_MASTER_MEMBERID: [None, None, "Thermostat Member ID"], + gw_vars.DATA_SLAVE_MEMBERID: [None, None, "Boiler Member ID"], + gw_vars.DATA_SLAVE_OEM_FAULT: [None, None, "Boiler OEM Fault Code"], + gw_vars.DATA_COOLING_CONTROL: [ + None, UNIT_PERCENT, "Cooling Control Signal"], + gw_vars.DATA_CONTROL_SETPOINT_2: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Control Setpoint 2"], + gw_vars.DATA_ROOM_SETPOINT_OVRD: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Room Setpoint Override"], + gw_vars.DATA_SLAVE_MAX_RELATIVE_MOD: [ + None, UNIT_PERCENT, "Boiler Maximum Relative Modulation"], + gw_vars.DATA_SLAVE_MAX_CAPACITY: [ + None, UNIT_KW, "Boiler Maximum Capacity"], + gw_vars.DATA_SLAVE_MIN_MOD_LEVEL: [ + None, UNIT_PERCENT, "Boiler Minimum Modulation Level"], + gw_vars.DATA_ROOM_SETPOINT: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Room Setpoint"], + gw_vars.DATA_REL_MOD_LEVEL: [ + None, UNIT_PERCENT, "Relative Modulation Level"], + gw_vars.DATA_CH_WATER_PRESS: [ + None, UNIT_BAR, "Central Heating Water Pressure"], + gw_vars.DATA_DHW_FLOW_RATE: [None, UNIT_L_MIN, "Hot Water Flow Rate"], + gw_vars.DATA_ROOM_SETPOINT_2: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Room Setpoint 2"], + gw_vars.DATA_ROOM_TEMP: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Room Temperature"], + gw_vars.DATA_CH_WATER_TEMP: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, + "Central Heating Water Temperature"], + gw_vars.DATA_DHW_TEMP: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Hot Water Temperature"], + gw_vars.DATA_OUTSIDE_TEMP: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Outside Temperature"], + gw_vars.DATA_RETURN_WATER_TEMP: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, + "Return Water Temperature"], + gw_vars.DATA_SOLAR_STORAGE_TEMP: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, + "Solar Storage Temperature"], + gw_vars.DATA_SOLAR_COLL_TEMP: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, + "Solar Collector Temperature"], + gw_vars.DATA_CH_WATER_TEMP_2: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, + "Central Heating 2 Water Temperature"], + gw_vars.DATA_DHW_TEMP_2: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Hot Water 2 Temperature"], + gw_vars.DATA_EXHAUST_TEMP: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Exhaust Temperature"], + gw_vars.DATA_SLAVE_DHW_MAX_SETP: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, + "Hot Water Maximum Setpoint"], + gw_vars.DATA_SLAVE_DHW_MIN_SETP: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, + "Hot Water Minimum Setpoint"], + gw_vars.DATA_SLAVE_CH_MAX_SETP: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, + "Boiler Maximum Central Heating Setpoint"], + gw_vars.DATA_SLAVE_CH_MIN_SETP: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, + "Boiler Minimum Central Heating Setpoint"], + gw_vars.DATA_DHW_SETPOINT: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Hot Water Setpoint"], + gw_vars.DATA_MAX_CH_SETPOINT: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, + "Maximum Central Heating Setpoint"], + gw_vars.DATA_OEM_DIAG: [None, None, "OEM Diagnostic Code"], + gw_vars.DATA_TOTAL_BURNER_STARTS: [ + None, None, "Total Burner Starts"], + gw_vars.DATA_CH_PUMP_STARTS: [ + None, None, "Central Heating Pump Starts"], + gw_vars.DATA_DHW_PUMP_STARTS: [None, None, "Hot Water Pump Starts"], + gw_vars.DATA_DHW_BURNER_STARTS: [ + None, None, "Hot Water Burner Starts"], + gw_vars.DATA_TOTAL_BURNER_HOURS: [ + None, UNIT_HOUR, "Total Burner Hours"], + gw_vars.DATA_CH_PUMP_HOURS: [ + None, UNIT_HOUR, "Central Heating Pump Hours"], + gw_vars.DATA_DHW_PUMP_HOURS: [None, UNIT_HOUR, "Hot Water Pump Hours"], + gw_vars.DATA_DHW_BURNER_HOURS: [ + None, UNIT_HOUR, "Hot Water Burner Hours"], + gw_vars.DATA_MASTER_OT_VERSION: [ + None, None, "Thermostat OpenTherm Version"], + gw_vars.DATA_SLAVE_OT_VERSION: [ + None, None, "Boiler OpenTherm Version"], + gw_vars.DATA_MASTER_PRODUCT_TYPE: [ + None, None, "Thermostat Product Type"], + gw_vars.DATA_MASTER_PRODUCT_VERSION: [ + None, None, "Thermostat Product Version"], + gw_vars.DATA_SLAVE_PRODUCT_TYPE: [None, None, "Boiler Product Type"], + gw_vars.DATA_SLAVE_PRODUCT_VERSION: [ + None, None, "Boiler Product Version"], + gw_vars.OTGW_MODE: [None, None, "Gateway/Monitor Mode"], + gw_vars.OTGW_DHW_OVRD: [None, None, "Gateway Hot Water Override Mode"], + gw_vars.OTGW_ABOUT: [None, None, "Gateway Firmware Version"], + gw_vars.OTGW_BUILD: [None, None, "Gateway Firmware Build"], + gw_vars.OTGW_CLOCKMHZ: [None, None, "Gateway Clock Speed"], + gw_vars.OTGW_LED_A: [None, None, "Gateway LED A Mode"], + gw_vars.OTGW_LED_B: [None, None, "Gateway LED B Mode"], + gw_vars.OTGW_LED_C: [None, None, "Gateway LED C Mode"], + gw_vars.OTGW_LED_D: [None, None, "Gateway LED D Mode"], + gw_vars.OTGW_LED_E: [None, None, "Gateway LED E Mode"], + gw_vars.OTGW_LED_F: [None, None, "Gateway LED F Mode"], + gw_vars.OTGW_GPIO_A: [None, None, "Gateway GPIO A Mode"], + gw_vars.OTGW_GPIO_B: [None, None, "Gateway GPIO B Mode"], + gw_vars.OTGW_SB_TEMP: [ + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, + "Gateway Setback Temperature"], + gw_vars.OTGW_SETP_OVRD_MODE: [ + None, None, "Gateway Room Setpoint Override Mode"], + gw_vars.OTGW_SMART_PWR: [None, None, "Gateway Smart Power Mode"], + gw_vars.OTGW_THRM_DETECT: [None, None, "Gateway Thermostat Detection"], + gw_vars.OTGW_VREF: [None, None, "Gateway Reference Voltage Setting"], + } + sensors = [] + for var in discovery_info: + device_class = sensor_info[var][0] + unit = sensor_info[var][1] + friendly_name = sensor_info[var][2] + entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, var, hass=hass) + sensors.append( + OpenThermSensor(entity_id, var, device_class, unit, friendly_name)) + async_add_entities(sensors) + + +class OpenThermSensor(Entity): + """Representation of an OpenTherm Gateway sensor.""" + + def __init__(self, entity_id, var, device_class, unit, friendly_name): + """Initialize the OpenTherm Gateway sensor.""" + self.entity_id = entity_id + self._var = var + self._value = None + self._device_class = device_class + self._unit = unit + self._friendly_name = friendly_name + + async def async_added_to_hass(self): + """Subscribe to updates from the component.""" + _LOGGER.debug("Added OpenTherm Gateway sensor %s", self._friendly_name) + async_dispatcher_connect(self.hass, SIGNAL_OPENTHERM_GW_UPDATE, + self.receive_report) + + async def receive_report(self, status): + """Handle status updates from the component.""" + value = status.get(self._var) + if isinstance(value, float): + value = '{:2.1f}'.format(value) + self._value = value + self.async_schedule_update_ha_state() + + @property + def name(self): + """Return the friendly name of the sensor.""" + return self._friendly_name + + @property + def device_class(self): + """Return the device class.""" + return self._device_class + + @property + def state(self): + """Return the state of the device.""" + return self._value + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit + + @property + def should_poll(self): + """Return False because entity pushes its state.""" + return False diff --git a/homeassistant/components/openuv/.translations/da.json b/homeassistant/components/openuv/.translations/da.json index 5cda5c6e66322..a783c8646e0e5 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 bb054f0b3a658..5e06be81d31b9 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 f6c52ffd04e8f..2c4c47e8da44e 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 32c3da0d3e58d..52383366c4dcf 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 @@ 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 .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 3e9bb0b0bc3f5..b790427b22800 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 0f566e5a9ef41..7150a8499d82a 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 63527db42a6b8..489a100a5e528 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 @@ 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 0000000000000..7f4053f8ead71 --- /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 ba264ad4b473f..d70ca8b114ec6 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 d0ba27aeddd0e..cc918dcf674e6 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 @@ 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 @@ } }, 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.py deleted file mode 100644 index 740a28a9dec4a..0000000000000 --- a/homeassistant/components/panel_custom.py +++ /dev/null @@ -1,170 +0,0 @@ -""" -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/ -""" -import logging -import os - -import voluptuous as vol - -from homeassistant.loader import bind_hass -import homeassistant.helpers.config_validation as cv - -DOMAIN = 'panel_custom' -DEPENDENCIES = ['frontend'] - -CONF_COMPONENT_NAME = 'name' -CONF_SIDEBAR_TITLE = 'sidebar_title' -CONF_SIDEBAR_ICON = 'sidebar_icon' -CONF_URL_PATH = 'url_path' -CONF_CONFIG = 'config' -CONF_WEBCOMPONENT_PATH = 'webcomponent_path' -CONF_JS_URL = 'js_url' -CONF_MODULE_URL = 'module_url' -CONF_EMBED_IFRAME = 'embed_iframe' -CONF_TRUST_EXTERNAL_SCRIPT = 'trust_external_script' -CONF_URL_EXCLUSIVE_GROUP = 'url_exclusive_group' - -MSG_URL_CONFLICT = \ - 'Pass in only one of webcomponent_path, module_url or js_url' - -DEFAULT_EMBED_IFRAME = False -DEFAULT_TRUST_EXTERNAL = False - -DEFAULT_ICON = 'mdi:bookmark' -LEGACY_URL = '/api/panel_custom/{}' - -PANEL_DIR = 'panels' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ - vol.Required(CONF_COMPONENT_NAME): cv.string, - vol.Optional(CONF_SIDEBAR_TITLE): cv.string, - vol.Optional(CONF_SIDEBAR_ICON, default=DEFAULT_ICON): cv.icon, - vol.Optional(CONF_URL_PATH): cv.string, - vol.Optional(CONF_CONFIG): dict, - vol.Exclusive(CONF_WEBCOMPONENT_PATH, CONF_URL_EXCLUSIVE_GROUP, - msg=MSG_URL_CONFLICT): cv.string, - vol.Exclusive(CONF_JS_URL, CONF_URL_EXCLUSIVE_GROUP, - msg=MSG_URL_CONFLICT): cv.string, - vol.Exclusive(CONF_MODULE_URL, CONF_URL_EXCLUSIVE_GROUP, - msg=MSG_URL_CONFLICT): cv.string, - vol.Optional(CONF_EMBED_IFRAME, - default=DEFAULT_EMBED_IFRAME): cv.boolean, - vol.Optional(CONF_TRUST_EXTERNAL_SCRIPT, - default=DEFAULT_TRUST_EXTERNAL): cv.boolean, - })]) -}, extra=vol.ALLOW_EXTRA) - -_LOGGER = logging.getLogger(__name__) - - -@bind_hass -async def async_register_panel( - hass, - # The url to serve the panel - frontend_url_path, - # The webcomponent name that loads your panel - webcomponent_name, - # Title/icon for sidebar - sidebar_title=None, - sidebar_icon=None, - # HTML source of your panel - html_url=None, - # JS source of your panel - js_url=None, - # JS module of your panel - module_url=None, - # If your panel should be run inside an iframe - embed_iframe=DEFAULT_EMBED_IFRAME, - # Should user be asked for confirmation when loading external source - trust_external=DEFAULT_TRUST_EXTERNAL, - # Configuration to be passed to the panel - config=None): - """Register a new custom panel.""" - if js_url is None and html_url is None and module_url is None: - raise ValueError('Either js_url, module_url or html_url is required.') - elif (js_url and html_url) or (module_url and html_url): - raise ValueError('Pass in only one of JS url, Module url or HTML url.') - - if config is not None and not isinstance(config, dict): - raise ValueError('Config needs to be a dictionary.') - - custom_panel_config = { - 'name': webcomponent_name, - 'embed_iframe': embed_iframe, - 'trust_external': trust_external, - } - - if js_url is not None: - custom_panel_config['js_url'] = js_url - - if module_url is not None: - custom_panel_config['module_url'] = module_url - - if html_url is not None: - custom_panel_config['html_url'] = html_url - - if config is not None: - # Make copy because we're mutating it - config = dict(config) - else: - config = {} - - config['_panel_custom'] = custom_panel_config - - await hass.components.frontend.async_register_built_in_panel( - component_name='custom', - sidebar_title=sidebar_title, - sidebar_icon=sidebar_icon, - frontend_url_path=frontend_url_path, - config=config - ) - - -async def async_setup(hass, config): - """Initialize custom panel.""" - success = False - - for panel in config.get(DOMAIN): - name = panel[CONF_COMPONENT_NAME] - - kwargs = { - 'webcomponent_name': panel[CONF_COMPONENT_NAME], - 'frontend_url_path': panel.get(CONF_URL_PATH, name), - 'sidebar_title': panel.get(CONF_SIDEBAR_TITLE), - 'sidebar_icon': panel.get(CONF_SIDEBAR_ICON), - 'config': panel.get(CONF_CONFIG), - 'trust_external': panel[CONF_TRUST_EXTERNAL_SCRIPT], - 'embed_iframe': panel[CONF_EMBED_IFRAME], - } - - panel_path = panel.get(CONF_WEBCOMPONENT_PATH) - - if panel_path is None: - panel_path = hass.config.path( - PANEL_DIR, '{}.html'.format(name)) - - if CONF_JS_URL in panel: - kwargs['js_url'] = panel[CONF_JS_URL] - - elif CONF_MODULE_URL in panel: - 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) - continue - - else: - url = LEGACY_URL.format(name) - hass.http.register_static_path(url, panel_path) - kwargs['html_url'] = url - - await async_register_panel(hass, **kwargs) - - success = True - - return success diff --git a/homeassistant/components/panel_custom/__init__.py b/homeassistant/components/panel_custom/__init__.py new file mode 100644 index 0000000000000..f6602169eb21f --- /dev/null +++ b/homeassistant/components/panel_custom/__init__.py @@ -0,0 +1,165 @@ +"""Register a custom front end panel.""" +import logging +import os + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.loader import bind_hass + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'panel_custom' +DEPENDENCIES = ['frontend'] + +CONF_COMPONENT_NAME = 'name' +CONF_SIDEBAR_TITLE = 'sidebar_title' +CONF_SIDEBAR_ICON = 'sidebar_icon' +CONF_URL_PATH = 'url_path' +CONF_CONFIG = 'config' +CONF_WEBCOMPONENT_PATH = 'webcomponent_path' +CONF_JS_URL = 'js_url' +CONF_MODULE_URL = 'module_url' +CONF_EMBED_IFRAME = 'embed_iframe' +CONF_TRUST_EXTERNAL_SCRIPT = 'trust_external_script' +CONF_URL_EXCLUSIVE_GROUP = 'url_exclusive_group' + +MSG_URL_CONFLICT = \ + 'Pass in only one of webcomponent_path, module_url or js_url' + +DEFAULT_EMBED_IFRAME = False +DEFAULT_TRUST_EXTERNAL = False + +DEFAULT_ICON = 'mdi:bookmark' +LEGACY_URL = '/api/panel_custom/{}' + +PANEL_DIR = 'panels' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ + vol.Required(CONF_COMPONENT_NAME): cv.string, + vol.Optional(CONF_SIDEBAR_TITLE): cv.string, + vol.Optional(CONF_SIDEBAR_ICON, default=DEFAULT_ICON): cv.icon, + vol.Optional(CONF_URL_PATH): cv.string, + vol.Optional(CONF_CONFIG): dict, + vol.Exclusive(CONF_WEBCOMPONENT_PATH, CONF_URL_EXCLUSIVE_GROUP, + msg=MSG_URL_CONFLICT): cv.string, + vol.Exclusive(CONF_JS_URL, CONF_URL_EXCLUSIVE_GROUP, + msg=MSG_URL_CONFLICT): cv.string, + vol.Exclusive(CONF_MODULE_URL, CONF_URL_EXCLUSIVE_GROUP, + msg=MSG_URL_CONFLICT): cv.string, + vol.Optional(CONF_EMBED_IFRAME, + default=DEFAULT_EMBED_IFRAME): cv.boolean, + vol.Optional(CONF_TRUST_EXTERNAL_SCRIPT, + default=DEFAULT_TRUST_EXTERNAL): cv.boolean, + })]) +}, extra=vol.ALLOW_EXTRA) + + +@bind_hass +async def async_register_panel( + hass, + # The url to serve the panel + frontend_url_path, + # The webcomponent name that loads your panel + webcomponent_name, + # Title/icon for sidebar + sidebar_title=None, + sidebar_icon=None, + # HTML source of your panel + html_url=None, + # JS source of your panel + js_url=None, + # JS module of your panel + module_url=None, + # If your panel should be run inside an iframe + embed_iframe=DEFAULT_EMBED_IFRAME, + # Should user be asked for confirmation when loading external source + trust_external=DEFAULT_TRUST_EXTERNAL, + # Configuration to be passed to the panel + config=None): + """Register a new custom panel.""" + if js_url is None and html_url is None and module_url is None: + raise ValueError('Either js_url, module_url or html_url is required.') + elif (js_url and html_url) or (module_url and html_url): + raise ValueError('Pass in only one of JS url, Module url or HTML url.') + + if config is not None and not isinstance(config, dict): + raise ValueError('Config needs to be a dictionary.') + + custom_panel_config = { + 'name': webcomponent_name, + 'embed_iframe': embed_iframe, + 'trust_external': trust_external, + } + + if js_url is not None: + custom_panel_config['js_url'] = js_url + + if module_url is not None: + custom_panel_config['module_url'] = module_url + + if html_url is not None: + custom_panel_config['html_url'] = html_url + + if config is not None: + # Make copy because we're mutating it + config = dict(config) + else: + config = {} + + config['_panel_custom'] = custom_panel_config + + await hass.components.frontend.async_register_built_in_panel( + component_name='custom', + sidebar_title=sidebar_title, + sidebar_icon=sidebar_icon, + frontend_url_path=frontend_url_path, + config=config + ) + + +async def async_setup(hass, config): + """Initialize custom panel.""" + success = False + + for panel in config.get(DOMAIN): + name = panel[CONF_COMPONENT_NAME] + + kwargs = { + 'webcomponent_name': panel[CONF_COMPONENT_NAME], + 'frontend_url_path': panel.get(CONF_URL_PATH, name), + 'sidebar_title': panel.get(CONF_SIDEBAR_TITLE), + 'sidebar_icon': panel.get(CONF_SIDEBAR_ICON), + 'config': panel.get(CONF_CONFIG), + 'trust_external': panel[CONF_TRUST_EXTERNAL_SCRIPT], + 'embed_iframe': panel[CONF_EMBED_IFRAME], + } + + panel_path = panel.get(CONF_WEBCOMPONENT_PATH) + + if panel_path is None: + panel_path = hass.config.path( + PANEL_DIR, '{}.html'.format(name)) + + if CONF_JS_URL in panel: + kwargs['js_url'] = panel[CONF_JS_URL] + + elif CONF_MODULE_URL in panel: + 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) + continue + + else: + url = LEGACY_URL.format(name) + hass.http.register_static_path(url, panel_path) + kwargs['html_url'] = url + + await async_register_panel(hass, **kwargs) + + success = True + + return success diff --git a/homeassistant/components/panel_iframe.py b/homeassistant/components/panel_iframe.py deleted file mode 100644 index 030fbbf93248f..0000000000000 --- a/homeassistant/components/panel_iframe.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -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/ -""" -import voluptuous as vol - -from homeassistant.const import (CONF_ICON, CONF_URL) -import homeassistant.helpers.config_validation as cv - -DEPENDENCIES = ['frontend'] - -DOMAIN = 'panel_iframe' - -CONF_TITLE = 'title' - -CONF_RELATIVE_URL_ERROR_MSG = "Invalid relative URL. Absolute path required." -CONF_RELATIVE_URL_REGEX = r'\A/' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: cv.schema_with_slug_keys( - vol.Schema({ - # pylint: disable=no-value-for-parameter - vol.Optional(CONF_TITLE): cv.string, - vol.Optional(CONF_ICON): cv.icon, - vol.Required(CONF_URL): vol.Any( - vol.Match( - CONF_RELATIVE_URL_REGEX, - msg=CONF_RELATIVE_URL_ERROR_MSG), - vol.Url()), - }) - ) -}, extra=vol.ALLOW_EXTRA) - - -async def async_setup(hass, config): - """Set up the iFrame frontend panels.""" - for url_path, info in config[DOMAIN].items(): - await hass.components.frontend.async_register_built_in_panel( - 'iframe', info.get(CONF_TITLE), info.get(CONF_ICON), - url_path, {'url': info[CONF_URL]}) - - return True diff --git a/homeassistant/components/panel_iframe/__init__.py b/homeassistant/components/panel_iframe/__init__.py new file mode 100644 index 0000000000000..b82f9fa978942 --- /dev/null +++ b/homeassistant/components/panel_iframe/__init__.py @@ -0,0 +1,39 @@ +"""Register an iFrame front end panel.""" +import voluptuous as vol + +from homeassistant.const import CONF_ICON, CONF_URL +import homeassistant.helpers.config_validation as cv + +DEPENDENCIES = ['frontend'] + +DOMAIN = 'panel_iframe' + +CONF_TITLE = 'title' + +CONF_RELATIVE_URL_ERROR_MSG = "Invalid relative URL. Absolute path required." +CONF_RELATIVE_URL_REGEX = r'\A/' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: cv.schema_with_slug_keys( + vol.Schema({ + # pylint: disable=no-value-for-parameter + vol.Optional(CONF_TITLE): cv.string, + vol.Optional(CONF_ICON): cv.icon, + vol.Required(CONF_URL): vol.Any( + vol.Match( + CONF_RELATIVE_URL_REGEX, + msg=CONF_RELATIVE_URL_ERROR_MSG), + vol.Url()), + }) + ) +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the iFrame frontend panels.""" + for url_path, info in config[DOMAIN].items(): + await hass.components.frontend.async_register_built_in_panel( + 'iframe', info.get(CONF_TITLE), info.get(CONF_ICON), + url_path, {'url': info[CONF_URL]}) + + return True diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index d38501b9b07e4..0a648f6eff797 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 0000000000000..6fb7d42e0ee76 --- /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.py deleted file mode 100644 index ed43562d22167..0000000000000 --- a/homeassistant/components/plant.py +++ /dev/null @@ -1,382 +0,0 @@ -"""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 -from collections import deque -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 -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__) - -DEFAULT_NAME = 'plant' - -READING_BATTERY = 'battery' -READING_TEMPERATURE = ATTR_TEMPERATURE -READING_MOISTURE = 'moisture' -READING_CONDUCTIVITY = 'conductivity' -READING_BRIGHTNESS = 'brightness' - -ATTR_PROBLEM = 'problem' -ATTR_SENSORS = 'sensors' -PROBLEM_NONE = 'none' -ATTR_MAX_BRIGHTNESS_HISTORY = 'max_brightness' - -# we're not returning only one value, we're returning a dict here. So we need -# to have a separate literal for it to avoid confusion. -ATTR_DICT_OF_UNITS_OF_MEASUREMENT = 'unit_of_measurement_dict' - -CONF_MIN_BATTERY_LEVEL = 'min_' + READING_BATTERY -CONF_MIN_TEMPERATURE = 'min_' + READING_TEMPERATURE -CONF_MAX_TEMPERATURE = 'max_' + READING_TEMPERATURE -CONF_MIN_MOISTURE = 'min_' + READING_MOISTURE -CONF_MAX_MOISTURE = 'max_' + READING_MOISTURE -CONF_MIN_CONDUCTIVITY = 'min_' + READING_CONDUCTIVITY -CONF_MAX_CONDUCTIVITY = 'max_' + READING_CONDUCTIVITY -CONF_MIN_BRIGHTNESS = 'min_' + READING_BRIGHTNESS -CONF_MAX_BRIGHTNESS = 'max_' + READING_BRIGHTNESS -CONF_CHECK_DAYS = 'check_days' - -CONF_SENSOR_BATTERY_LEVEL = READING_BATTERY -CONF_SENSOR_MOISTURE = READING_MOISTURE -CONF_SENSOR_CONDUCTIVITY = READING_CONDUCTIVITY -CONF_SENSOR_TEMPERATURE = READING_TEMPERATURE -CONF_SENSOR_BRIGHTNESS = READING_BRIGHTNESS - -DEFAULT_MIN_BATTERY_LEVEL = 20 -DEFAULT_MIN_MOISTURE = 20 -DEFAULT_MAX_MOISTURE = 60 -DEFAULT_MIN_CONDUCTIVITY = 500 -DEFAULT_MAX_CONDUCTIVITY = 3000 -DEFAULT_CHECK_DAYS = 3 - -SCHEMA_SENSORS = vol.Schema({ - vol.Optional(CONF_SENSOR_BATTERY_LEVEL): cv.entity_id, - vol.Optional(CONF_SENSOR_MOISTURE): cv.entity_id, - vol.Optional(CONF_SENSOR_CONDUCTIVITY): cv.entity_id, - vol.Optional(CONF_SENSOR_TEMPERATURE): cv.entity_id, - vol.Optional(CONF_SENSOR_BRIGHTNESS): cv.entity_id, -}) - -PLANT_SCHEMA = vol.Schema({ - vol.Required(CONF_SENSORS): vol.Schema(SCHEMA_SENSORS), - vol.Optional(CONF_MIN_BATTERY_LEVEL, - default=DEFAULT_MIN_BATTERY_LEVEL): cv.positive_int, - vol.Optional(CONF_MIN_TEMPERATURE): vol.Coerce(float), - vol.Optional(CONF_MAX_TEMPERATURE): vol.Coerce(float), - vol.Optional(CONF_MIN_MOISTURE, - default=DEFAULT_MIN_MOISTURE): cv.positive_int, - vol.Optional(CONF_MAX_MOISTURE, - default=DEFAULT_MAX_MOISTURE): cv.positive_int, - vol.Optional(CONF_MIN_CONDUCTIVITY, - default=DEFAULT_MIN_CONDUCTIVITY): cv.positive_int, - vol.Optional(CONF_MAX_CONDUCTIVITY, - default=DEFAULT_MAX_CONDUCTIVITY): cv.positive_int, - vol.Optional(CONF_MIN_BRIGHTNESS): cv.positive_int, - vol.Optional(CONF_MAX_BRIGHTNESS): cv.positive_int, - vol.Optional(CONF_CHECK_DAYS, - default=DEFAULT_CHECK_DAYS): cv.positive_int, -}) - -DOMAIN = 'plant' -DEPENDENCIES = ['zone', 'group'] - -GROUP_NAME_ALL_PLANTS = 'all plants' -ENTITY_ID_ALL_PLANTS = group.ENTITY_ID_FORMAT.format('all_plants') - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: { - cv.string: PLANT_SCHEMA - }, -}, extra=vol.ALLOW_EXTRA) - - -# Flag for enabling/disabling the loading of the history from the database. -# This feature is turned off right now as its tests are not 100% stable. -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) - - entities = [] - for plant_name, plant_config in config[DOMAIN].items(): - _LOGGER.info("Added plant %s", plant_name) - entity = Plant(plant_name, plant_config) - entities.append(entity) - - await component.async_add_entities(entities) - return True - - -class Plant(Entity): - """Plant monitors the well-being of a plant. - - It also checks the measurements against - configurable min and max values. - """ - - READINGS = { - READING_BATTERY: { - ATTR_UNIT_OF_MEASUREMENT: '%', - 'min': CONF_MIN_BATTERY_LEVEL, - }, - READING_TEMPERATURE: { - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, - 'min': CONF_MIN_TEMPERATURE, - 'max': CONF_MAX_TEMPERATURE, - }, - READING_MOISTURE: { - ATTR_UNIT_OF_MEASUREMENT: '%', - 'min': CONF_MIN_MOISTURE, - 'max': CONF_MAX_MOISTURE, - }, - READING_CONDUCTIVITY: { - ATTR_UNIT_OF_MEASUREMENT: 'µS/cm', - 'min': CONF_MIN_CONDUCTIVITY, - 'max': CONF_MAX_CONDUCTIVITY, - }, - READING_BRIGHTNESS: { - ATTR_UNIT_OF_MEASUREMENT: 'lux', - 'min': CONF_MIN_BRIGHTNESS, - 'max': CONF_MAX_BRIGHTNESS, - } - } - - def __init__(self, name, config): - """Initialize the Plant component.""" - self._config = config - self._sensormap = dict() - self._readingmap = dict() - self._unit_of_measurement = dict() - for reading, entity_id in config['sensors'].items(): - self._sensormap[entity_id] = reading - self._readingmap[reading] = entity_id - self._state = None - self._name = name - self._battery = None - self._moisture = None - self._conductivity = None - self._temperature = None - self._brightness = None - self._problems = PROBLEM_NONE - - self._conf_check_days = 3 # default check interval: 3 days - if CONF_CHECK_DAYS in self._config: - self._conf_check_days = self._config[CONF_CHECK_DAYS] - self._brightness_history = DailyHistory(self._conf_check_days) - - @callback - def state_changed(self, entity_id, _, new_state): - """Update the sensor status. - - This callback is triggered, when the sensor state changes. - """ - value = new_state.state - _LOGGER.debug("Received callback from %s with value %s", - entity_id, value) - if value == STATE_UNKNOWN: - return - - reading = self._sensormap[entity_id] - if reading == READING_MOISTURE: - self._moisture = int(float(value)) - elif reading == READING_BATTERY: - self._battery = int(float(value)) - elif reading == READING_TEMPERATURE: - self._temperature = float(value) - elif reading == READING_CONDUCTIVITY: - 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) - else: - raise HomeAssistantError( - "Unknown reading from sensor {}: {}".format(entity_id, value)) - if ATTR_UNIT_OF_MEASUREMENT in new_state.attributes: - self._unit_of_measurement[reading] = \ - new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - self._update_state() - - def _update_state(self): - """Update the state of the class based sensor data.""" - result = [] - for sensor_name in self._sensormap.values(): - params = self.READINGS[sensor_name] - value = getattr(self, '_{}'.format(sensor_name)) - if value is not None: - if sensor_name == READING_BRIGHTNESS: - result.append(self._check_min( - sensor_name, self._brightness_history.max, params)) - else: - result.append(self._check_min(sensor_name, value, params)) - result.append(self._check_max(sensor_name, value, params)) - - result = [r for r in result if r is not None] - - if result: - self._state = STATE_PROBLEM - self._problems = ', '.join(result) - else: - self._state = STATE_OK - self._problems = PROBLEM_NONE - _LOGGER.debug("New data processed") - self.async_schedule_update_ha_state() - - def _check_min(self, sensor_name, value, params): - """If configured, check the value against the defined minimum value.""" - if 'min' in params and params['min'] in self._config: - min_value = self._config[params['min']] - if value < min_value: - return '{} low'.format(sensor_name) - - def _check_max(self, sensor_name, value, params): - """If configured, check the value against the defined maximum value.""" - if 'max' in params and params['max'] in self._config: - max_value = self._config[params['max']] - if value > max_value: - return '{} high'.format(sensor_name) - return None - - async def async_added_to_hass(self): - """After being added to hass, load from history.""" - if ENABLE_LOAD_HISTORY and 'recorder' in self.hass.config.components: - # 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) - - for entity_id in self._sensormap: - state = self.hass.states.get(entity_id) - if state is not None: - self.state_changed(entity_id, None, state) - - async def _load_history_from_db(self): - """Load the history of the brightness values from the database. - - This only needs to be done once during startup. - """ - from homeassistant.components.recorder.models import States - 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.") - return - - _LOGGER.debug("initializing values for %s from the database", - self._name) - with session_scope(hass=self.hass) as session: - query = session.query(States).filter( - (States.entity_id == entity_id.lower()) and - (States.last_updated > start_date) - ).order_by(States.last_updated.asc()) - states = execute(query) - - for state in states: - # filter out all None, NaN and "unknown" states - # only keep real values - try: - self._brightness_history.add_measurement( - int(state.state), state.last_updated) - except ValueError: - pass - _LOGGER.debug("initializing from database completed") - self.async_schedule_update_ha_state() - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def state(self): - """Return the state of the entity.""" - return self._state - - @property - def state_attributes(self): - """Return the attributes of the entity. - - Provide the individual measurements from the - sensor in the attributes of the device. - """ - attrib = { - ATTR_PROBLEM: self._problems, - ATTR_SENSORS: self._readingmap, - ATTR_DICT_OF_UNITS_OF_MEASUREMENT: self._unit_of_measurement, - } - - for reading in self._sensormap.values(): - attrib[reading] = getattr(self, '_{}'.format(reading)) - - if self._brightness_history.max is not None: - attrib[ATTR_MAX_BRIGHTNESS_HISTORY] = self._brightness_history.max - - return attrib - - -class DailyHistory: - """Stores one measurement per day for a maximum number of days. - - At the moment only the maximum value per day is kept. - """ - - def __init__(self, max_length): - """Create new DailyHistory with a maximum length of the history.""" - self.max_length = max_length - self._days = None - self._max_dict = dict() - self.max = None - - def add_measurement(self, value, timestamp=None): - """Add a new measurement for a certain day.""" - day = (timestamp or datetime.now()).date() - if value is None: - return - if self._days is None: - self._days = deque() - self._add_day(day, value) - else: - current_day = self._days[-1] - if day == current_day: - self._max_dict[day] = max(value, self._max_dict[day]) - elif day > current_day: - self._add_day(day, value) - else: - _LOGGER.warning('received old measurement, not storing it!') - - self.max = max(self._max_dict.values()) - - def _add_day(self, day, value): - """Add a new day to the history. - - Deletes the oldest day, if the queue becomes too long. - """ - if len(self._days) == self.max_length: - oldest = self._days.popleft() - del self._max_dict[oldest] - self._days.append(day) - self._max_dict[day] = value diff --git a/homeassistant/components/plant/__init__.py b/homeassistant/components/plant/__init__.py new file mode 100644 index 0000000000000..27324ad57a39a --- /dev/null +++ b/homeassistant/components/plant/__init__.py @@ -0,0 +1,379 @@ +"""Support for monitoring plants.""" +from collections import deque +from datetime import datetime, timedelta +import logging + +import voluptuous as vol + +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.helpers.event import async_track_state_change + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'plant' + +READING_BATTERY = 'battery' +READING_TEMPERATURE = ATTR_TEMPERATURE +READING_MOISTURE = 'moisture' +READING_CONDUCTIVITY = 'conductivity' +READING_BRIGHTNESS = 'brightness' + +ATTR_PROBLEM = 'problem' +ATTR_SENSORS = 'sensors' +PROBLEM_NONE = 'none' +ATTR_MAX_BRIGHTNESS_HISTORY = 'max_brightness' + +# we're not returning only one value, we're returning a dict here. So we need +# to have a separate literal for it to avoid confusion. +ATTR_DICT_OF_UNITS_OF_MEASUREMENT = 'unit_of_measurement_dict' + +CONF_MIN_BATTERY_LEVEL = 'min_' + READING_BATTERY +CONF_MIN_TEMPERATURE = 'min_' + READING_TEMPERATURE +CONF_MAX_TEMPERATURE = 'max_' + READING_TEMPERATURE +CONF_MIN_MOISTURE = 'min_' + READING_MOISTURE +CONF_MAX_MOISTURE = 'max_' + READING_MOISTURE +CONF_MIN_CONDUCTIVITY = 'min_' + READING_CONDUCTIVITY +CONF_MAX_CONDUCTIVITY = 'max_' + READING_CONDUCTIVITY +CONF_MIN_BRIGHTNESS = 'min_' + READING_BRIGHTNESS +CONF_MAX_BRIGHTNESS = 'max_' + READING_BRIGHTNESS +CONF_CHECK_DAYS = 'check_days' + +CONF_SENSOR_BATTERY_LEVEL = READING_BATTERY +CONF_SENSOR_MOISTURE = READING_MOISTURE +CONF_SENSOR_CONDUCTIVITY = READING_CONDUCTIVITY +CONF_SENSOR_TEMPERATURE = READING_TEMPERATURE +CONF_SENSOR_BRIGHTNESS = READING_BRIGHTNESS + +DEFAULT_MIN_BATTERY_LEVEL = 20 +DEFAULT_MIN_MOISTURE = 20 +DEFAULT_MAX_MOISTURE = 60 +DEFAULT_MIN_CONDUCTIVITY = 500 +DEFAULT_MAX_CONDUCTIVITY = 3000 +DEFAULT_CHECK_DAYS = 3 + +SCHEMA_SENSORS = vol.Schema({ + vol.Optional(CONF_SENSOR_BATTERY_LEVEL): cv.entity_id, + vol.Optional(CONF_SENSOR_MOISTURE): cv.entity_id, + vol.Optional(CONF_SENSOR_CONDUCTIVITY): cv.entity_id, + vol.Optional(CONF_SENSOR_TEMPERATURE): cv.entity_id, + vol.Optional(CONF_SENSOR_BRIGHTNESS): cv.entity_id, +}) + +PLANT_SCHEMA = vol.Schema({ + vol.Required(CONF_SENSORS): vol.Schema(SCHEMA_SENSORS), + vol.Optional(CONF_MIN_BATTERY_LEVEL, + default=DEFAULT_MIN_BATTERY_LEVEL): cv.positive_int, + vol.Optional(CONF_MIN_TEMPERATURE): vol.Coerce(float), + vol.Optional(CONF_MAX_TEMPERATURE): vol.Coerce(float), + vol.Optional(CONF_MIN_MOISTURE, + default=DEFAULT_MIN_MOISTURE): cv.positive_int, + vol.Optional(CONF_MAX_MOISTURE, + default=DEFAULT_MAX_MOISTURE): cv.positive_int, + vol.Optional(CONF_MIN_CONDUCTIVITY, + default=DEFAULT_MIN_CONDUCTIVITY): cv.positive_int, + vol.Optional(CONF_MAX_CONDUCTIVITY, + default=DEFAULT_MAX_CONDUCTIVITY): cv.positive_int, + vol.Optional(CONF_MIN_BRIGHTNESS): cv.positive_int, + vol.Optional(CONF_MAX_BRIGHTNESS): cv.positive_int, + vol.Optional(CONF_CHECK_DAYS, + default=DEFAULT_CHECK_DAYS): cv.positive_int, +}) + +DOMAIN = 'plant' +DEPENDENCIES = ['zone', 'group'] + +GROUP_NAME_ALL_PLANTS = 'all plants' +ENTITY_ID_ALL_PLANTS = group.ENTITY_ID_FORMAT.format('all_plants') + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: { + cv.string: PLANT_SCHEMA + }, +}, extra=vol.ALLOW_EXTRA) + + +# Flag for enabling/disabling the loading of the history from the database. +# This feature is turned off right now as its tests are not 100% stable. +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) + + entities = [] + for plant_name, plant_config in config[DOMAIN].items(): + _LOGGER.info("Added plant %s", plant_name) + entity = Plant(plant_name, plant_config) + entities.append(entity) + + await component.async_add_entities(entities) + return True + + +class Plant(Entity): + """Plant monitors the well-being of a plant. + + It also checks the measurements against + configurable min and max values. + """ + + READINGS = { + READING_BATTERY: { + ATTR_UNIT_OF_MEASUREMENT: '%', + 'min': CONF_MIN_BATTERY_LEVEL, + }, + READING_TEMPERATURE: { + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + 'min': CONF_MIN_TEMPERATURE, + 'max': CONF_MAX_TEMPERATURE, + }, + READING_MOISTURE: { + ATTR_UNIT_OF_MEASUREMENT: '%', + 'min': CONF_MIN_MOISTURE, + 'max': CONF_MAX_MOISTURE, + }, + READING_CONDUCTIVITY: { + ATTR_UNIT_OF_MEASUREMENT: 'µS/cm', + 'min': CONF_MIN_CONDUCTIVITY, + 'max': CONF_MAX_CONDUCTIVITY, + }, + READING_BRIGHTNESS: { + ATTR_UNIT_OF_MEASUREMENT: 'lux', + 'min': CONF_MIN_BRIGHTNESS, + 'max': CONF_MAX_BRIGHTNESS, + } + } + + def __init__(self, name, config): + """Initialize the Plant component.""" + self._config = config + self._sensormap = dict() + self._readingmap = dict() + self._unit_of_measurement = dict() + for reading, entity_id in config['sensors'].items(): + self._sensormap[entity_id] = reading + self._readingmap[reading] = entity_id + self._state = None + self._name = name + self._battery = None + self._moisture = None + self._conductivity = None + self._temperature = None + self._brightness = None + self._problems = PROBLEM_NONE + + self._conf_check_days = 3 # default check interval: 3 days + if CONF_CHECK_DAYS in self._config: + self._conf_check_days = self._config[CONF_CHECK_DAYS] + self._brightness_history = DailyHistory(self._conf_check_days) + + @callback + def state_changed(self, entity_id, _, new_state): + """Update the sensor status. + + This callback is triggered, when the sensor state changes. + """ + value = new_state.state + _LOGGER.debug("Received callback from %s with value %s", + entity_id, value) + if value == STATE_UNKNOWN: + return + + reading = self._sensormap[entity_id] + if reading == READING_MOISTURE: + self._moisture = int(float(value)) + elif reading == READING_BATTERY: + self._battery = int(float(value)) + elif reading == READING_TEMPERATURE: + self._temperature = float(value) + elif reading == READING_CONDUCTIVITY: + 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) + else: + raise HomeAssistantError( + "Unknown reading from sensor {}: {}".format(entity_id, value)) + if ATTR_UNIT_OF_MEASUREMENT in new_state.attributes: + self._unit_of_measurement[reading] = \ + new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + self._update_state() + + def _update_state(self): + """Update the state of the class based sensor data.""" + result = [] + for sensor_name in self._sensormap.values(): + params = self.READINGS[sensor_name] + value = getattr(self, '_{}'.format(sensor_name)) + if value is not None: + if sensor_name == READING_BRIGHTNESS: + result.append(self._check_min( + sensor_name, self._brightness_history.max, params)) + else: + result.append(self._check_min(sensor_name, value, params)) + result.append(self._check_max(sensor_name, value, params)) + + result = [r for r in result if r is not None] + + if result: + self._state = STATE_PROBLEM + self._problems = ', '.join(result) + else: + self._state = STATE_OK + self._problems = PROBLEM_NONE + _LOGGER.debug("New data processed") + self.async_schedule_update_ha_state() + + def _check_min(self, sensor_name, value, params): + """If configured, check the value against the defined minimum value.""" + if 'min' in params and params['min'] in self._config: + min_value = self._config[params['min']] + if value < min_value: + return '{} low'.format(sensor_name) + + def _check_max(self, sensor_name, value, params): + """If configured, check the value against the defined maximum value.""" + if 'max' in params and params['max'] in self._config: + max_value = self._config[params['max']] + if value > max_value: + return '{} high'.format(sensor_name) + return None + + async def async_added_to_hass(self): + """After being added to hass, load from history.""" + if ENABLE_LOAD_HISTORY and 'recorder' in self.hass.config.components: + # 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) + + for entity_id in self._sensormap: + state = self.hass.states.get(entity_id) + if state is not None: + self.state_changed(entity_id, None, state) + + async def _load_history_from_db(self): + """Load the history of the brightness values from the database. + + This only needs to be done once during startup. + """ + from homeassistant.components.recorder.models import States + 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") + return + + _LOGGER.debug("Initializing values for %s from the database", + self._name) + with session_scope(hass=self.hass) as session: + query = session.query(States).filter( + (States.entity_id == entity_id.lower()) and + (States.last_updated > start_date) + ).order_by(States.last_updated.asc()) + states = execute(query) + + for state in states: + # filter out all None, NaN and "unknown" states + # only keep real values + try: + self._brightness_history.add_measurement( + int(state.state), state.last_updated) + except ValueError: + pass + _LOGGER.debug("Initializing from database completed") + self.async_schedule_update_ha_state() + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the entity.""" + return self._state + + @property + def state_attributes(self): + """Return the attributes of the entity. + + Provide the individual measurements from the + sensor in the attributes of the device. + """ + attrib = { + ATTR_PROBLEM: self._problems, + ATTR_SENSORS: self._readingmap, + ATTR_DICT_OF_UNITS_OF_MEASUREMENT: self._unit_of_measurement, + } + + for reading in self._sensormap.values(): + attrib[reading] = getattr(self, '_{}'.format(reading)) + + if self._brightness_history.max is not None: + attrib[ATTR_MAX_BRIGHTNESS_HISTORY] = self._brightness_history.max + + return attrib + + +class DailyHistory: + """Stores one measurement per day for a maximum number of days. + + At the moment only the maximum value per day is kept. + """ + + def __init__(self, max_length): + """Create new DailyHistory with a maximum length of the history.""" + self.max_length = max_length + self._days = None + self._max_dict = dict() + self.max = None + + def add_measurement(self, value, timestamp=None): + """Add a new measurement for a certain day.""" + day = (timestamp or datetime.now()).date() + if value is None: + return + if self._days is None: + self._days = deque() + self._add_day(day, value) + else: + current_day = self._days[-1] + if day == current_day: + self._max_dict[day] = max(value, self._max_dict[day]) + elif day > current_day: + self._add_day(day, value) + else: + _LOGGER.warning("Received old measurement, not storing it") + + self.max = max(self._max_dict.values()) + + def _add_day(self, day, value): + """Add a new day to the history. + + Deletes the oldest day, if the queue becomes too long. + """ + if len(self._days) == self.max_length: + oldest = self._days.popleft() + del self._max_dict[oldest] + self._days.append(day) + self._max_dict[day] = value diff --git a/homeassistant/components/plum_lightpad.py b/homeassistant/components/plum_lightpad.py deleted file mode 100644 index 979f257f25fa7..0000000000000 --- a/homeassistant/components/plum_lightpad.py +++ /dev/null @@ -1,76 +0,0 @@ -""" -Support for Plum Lightpad switches. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/plum_lightpad -""" -import asyncio -import logging - -import voluptuous as vol - -from homeassistant.const import ( - CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP) -from homeassistant.helpers import discovery -from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv - -REQUIREMENTS = ['plumlightpad==0.0.11'] - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'plum_lightpad' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - }), -}, extra=vol.ALLOW_EXTRA) - -PLUM_DATA = 'plum' - - -async def async_setup(hass, config): - """Plum Lightpad Platform initialization.""" - from plumlightpad import Plum - - conf = config[DOMAIN] - plum = Plum(conf[CONF_USERNAME], conf[CONF_PASSWORD]) - - hass.data[PLUM_DATA] = plum - - def cleanup(event): - """Clean up resources.""" - plum.cleanup() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup) - - cloud_web_sesison = async_get_clientsession(hass, verify_ssl=True) - await plum.loadCloudData(cloud_web_sesison) - - async def new_load(device): - """Load light and sensor platforms when LogicalLoad is detected.""" - await asyncio.wait([ - hass.async_create_task( - discovery.async_load_platform( - hass, 'light', DOMAIN, - discovered=device, hass_config=conf)) - ]) - - async def new_lightpad(device): - """Load light and binary sensor platforms when Lightpad detected.""" - await asyncio.wait([ - hass.async_create_task( - discovery.async_load_platform( - hass, 'light', DOMAIN, - discovered=device, hass_config=conf)) - ]) - - device_web_session = async_get_clientsession(hass, verify_ssl=False) - hass.async_create_task( - plum.discover(hass.loop, - loadListener=new_load, lightpadListener=new_lightpad, - websession=device_web_session)) - - return True diff --git a/homeassistant/components/plum_lightpad/__init__.py b/homeassistant/components/plum_lightpad/__init__.py new file mode 100644 index 0000000000000..5b99223d25aed --- /dev/null +++ b/homeassistant/components/plum_lightpad/__init__.py @@ -0,0 +1,71 @@ +"""Support for Plum Lightpad devices.""" +import asyncio +import logging + +import voluptuous as vol + +from homeassistant.const import ( + CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP) +from homeassistant.helpers import discovery +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['plumlightpad==0.0.11'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'plum_lightpad' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + +PLUM_DATA = 'plum' + + +async def async_setup(hass, config): + """Plum Lightpad Platform initialization.""" + from plumlightpad import Plum + + conf = config[DOMAIN] + plum = Plum(conf[CONF_USERNAME], conf[CONF_PASSWORD]) + + hass.data[PLUM_DATA] = plum + + def cleanup(event): + """Clean up resources.""" + plum.cleanup() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup) + + cloud_web_sesison = async_get_clientsession(hass, verify_ssl=True) + await plum.loadCloudData(cloud_web_sesison) + + async def new_load(device): + """Load light and sensor platforms when LogicalLoad is detected.""" + await asyncio.wait([ + hass.async_create_task( + discovery.async_load_platform( + hass, 'light', DOMAIN, + discovered=device, hass_config=conf)) + ]) + + async def new_lightpad(device): + """Load light and binary sensor platforms when Lightpad detected.""" + await asyncio.wait([ + hass.async_create_task( + discovery.async_load_platform( + hass, 'light', DOMAIN, + discovered=device, hass_config=conf)) + ]) + + device_web_session = async_get_clientsession(hass, verify_ssl=False) + hass.async_create_task( + plum.discover(hass.loop, + loadListener=new_load, lightpadListener=new_lightpad, + websession=device_web_session)) + + return True diff --git a/homeassistant/components/plum_lightpad/light.py b/homeassistant/components/plum_lightpad/light.py new file mode 100644 index 0000000000000..43cceaa671f85 --- /dev/null +++ b/homeassistant/components/plum_lightpad/light.py @@ -0,0 +1,179 @@ +"""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 +import homeassistant.util.color as color_util + +DEPENDENCIES = ['plum_lightpad'] + + +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 + + plum = hass.data[PLUM_DATA] + + entities = [] + + if 'lpid' in discovery_info: + lightpad = plum.get_lightpad(discovery_info['lpid']) + entities.append(GlowRing(lightpad=lightpad)) + + if 'llid' in discovery_info: + logical_load = plum.get_load(discovery_info['llid']) + entities.append(PlumLight(load=logical_load)) + + if entities: + async_add_entities(entities) + + +class PlumLight(Light): + """Representation of a Plum Lightpad dimmer.""" + + def __init__(self, load): + """Initialize the light.""" + self._load = load + self._brightness = load.level + + async def async_added_to_hass(self): + """Subscribe to dimmerchange events.""" + self._load.add_event_listener('dimmerchange', self.dimmerchange) + + def dimmerchange(self, event): + """Change event handler updating the brightness.""" + self._brightness = event['level'] + self.schedule_update_ha_state() + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return the name of the switch if any.""" + return self._load.name + + @property + def brightness(self) -> int: + """Return the brightness of this switch between 0..255.""" + return self._brightness + + @property + def is_on(self) -> bool: + """Return true if light is on.""" + return self._brightness > 0 + + @property + def supported_features(self): + """Flag supported features.""" + if self._load.dimmable: + return SUPPORT_BRIGHTNESS + return None + + async def async_turn_on(self, **kwargs): + """Turn the light on.""" + if ATTR_BRIGHTNESS in kwargs: + await self._load.turn_on(kwargs[ATTR_BRIGHTNESS]) + else: + await self._load.turn_on() + + async def async_turn_off(self, **kwargs): + """Turn the light off.""" + await self._load.turn_off() + + +class GlowRing(Light): + """Representation of a Plum Lightpad dimmer glow ring.""" + + def __init__(self, lightpad): + """Initialize the light.""" + self._lightpad = lightpad + self._name = '{} Glow Ring'.format(lightpad.friendly_name) + + self._state = lightpad.glow_enabled + self._brightness = lightpad.glow_intensity * 255.0 + + self._red = lightpad.glow_color['red'] + self._green = lightpad.glow_color['green'] + self._blue = lightpad.glow_color['blue'] + + async def async_added_to_hass(self): + """Subscribe to configchange events.""" + self._lightpad.add_event_listener( + 'configchange', self.configchange_event) + + def configchange_event(self, event): + """Handle Configuration change event.""" + config = event['changes'] + + self._state = config['glowEnabled'] + self._brightness = config['glowIntensity'] * 255.0 + + self._red = config['glowColor']['red'] + self._green = config['glowColor']['green'] + self._blue = config['glowColor']['blue'] + + self.schedule_update_ha_state() + + @property + def hs_color(self): + """Return the hue and saturation color value [float, float].""" + return color_util.color_RGB_to_hs(self._red, self._green, self._blue) + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return the name of the switch if any.""" + return self._name + + @property + def brightness(self) -> int: + """Return the brightness of this switch between 0..255.""" + return self._brightness + + @property + def glow_intensity(self): + """Brightness in float form.""" + return self._brightness / 255.0 + + @property + def is_on(self) -> bool: + """Return true if light is on.""" + return self._state + + @property + def icon(self): + """Return the crop-portrait icon representing the glow ring.""" + return 'mdi:crop-portrait' + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR + + async def async_turn_on(self, **kwargs): + """Turn the light on.""" + if ATTR_BRIGHTNESS in kwargs: + await self._lightpad.set_config( + {"glowIntensity": kwargs[ATTR_BRIGHTNESS]}) + elif ATTR_HS_COLOR in kwargs: + hs_color = kwargs[ATTR_HS_COLOR] + red, green, blue = color_util.color_hs_to_RGB(*hs_color) + await self._lightpad.set_glow_color(red, green, blue, 0) + else: + await self._lightpad.set_config({"glowEnabled": True}) + + async def async_turn_off(self, **kwargs): + """Turn the light off.""" + if ATTR_BRIGHTNESS in kwargs: + await self._lightpad.set_config( + {"glowIntensity": kwargs[ATTR_BRIGHTNESS]}) + else: + 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 0000000000000..109bcbe6c3701 --- /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 6d24d56233c1a..874f0832b6c35 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 6616d6b24ec10..f223ded998f1a 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 @@ 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 @@ 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 @@ async def update(self, *args): 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 @@ async def async_added_to_hass(self): _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/point/binary_sensor.py b/homeassistant/components/point/binary_sensor.py new file mode 100644 index 0000000000000..c29ce42168219 --- /dev/null +++ b/homeassistant/components/point/binary_sensor.py @@ -0,0 +1,104 @@ +"""Support for Minut Point binary sensors.""" +import logging + +from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice +from homeassistant.components.point import MinutPointEntity +from homeassistant.components.point.const import ( + DOMAIN as POINT_DOMAIN, POINT_DISCOVERY_NEW, SIGNAL_WEBHOOK) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +_LOGGER = logging.getLogger(__name__) + +EVENTS = { + 'battery': # On means low, Off means normal + ('battery_low', ''), + 'button_press': # On means the button was pressed, Off means normal + ('short_button_press', ''), + 'cold': # On means cold, Off means normal + ('temperature_low', 'temperature_risen_normal'), + 'connectivity': # On means connected, Off means disconnected + ('device_online', 'device_offline'), + 'dry': # On means too dry, Off means normal + ('humidity_low', 'humidity_risen_normal'), + 'heat': # On means hot, Off means normal + ('temperature_high', 'temperature_dropped_normal'), + 'moisture': # On means wet, Off means dry + ('humidity_high', 'humidity_dropped_normal'), + 'sound': # On means sound detected, Off means no sound (clear) + ('avg_sound_high', 'sound_level_dropped_normal'), + 'tamper': # On means the point was removed or attached + ('tamper', ''), +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up a Point's binary sensors based on a config entry.""" + async def async_discover_sensor(device_id): + """Discover and add a discovered sensor.""" + client = hass.data[POINT_DOMAIN][config_entry.entry_id] + async_add_entities( + (MinutPointBinarySensor(client, device_id, device_class) + for device_class in EVENTS), True) + + async_dispatcher_connect( + hass, POINT_DISCOVERY_NEW.format(DOMAIN, POINT_DOMAIN), + async_discover_sensor) + + +class MinutPointBinarySensor(MinutPointEntity, BinarySensorDevice): + """The platform class required by Home Assistant.""" + + def __init__(self, point_client, device_id, device_class): + """Initialize the binary sensor.""" + super().__init__(point_client, device_id, device_class) + + self._async_unsub_hook_dispatcher_connect = None + self._events = EVENTS[device_class] + self._is_on = None + + async def async_added_to_hass(self): + """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) + + async def async_will_remove_from_hass(self): + """Disconnect dispatcher listener when removed.""" + await super().async_will_remove_from_hass() + if self._async_unsub_hook_dispatcher_connect: + self._async_unsub_hook_dispatcher_connect() + + async def _update_callback(self): + """Update the value of the sensor.""" + if not self.is_updated: + return + if self._events[0] in self.device.ongoing_events: + self._is_on = True + else: + self._is_on = None + self.async_schedule_update_ha_state() + + @callback + def _webhook_event(self, data, webhook): + """Process new event from the webhook.""" + if self.device.webhook != webhook: + return + _type = data.get('event', {}).get('type') + _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]: + self._is_on = True + if _type == self._events[1]: + self._is_on = None + self.async_schedule_update_ha_state() + + @property + def is_on(self): + """Return the state of the binary sensor.""" + if self.device_class == 'connectivity': + # connectivity is the other way around. + return not self._is_on + return self._is_on diff --git a/homeassistant/components/point/config_flow.py b/homeassistant/components/point/config_flow.py index 8cda30c7171a5..64583a5ab385e 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/point/sensor.py b/homeassistant/components/point/sensor.py new file mode 100644 index 0000000000000..90b83ba42e3ee --- /dev/null +++ b/homeassistant/components/point/sensor.py @@ -0,0 +1,71 @@ +"""Support for Minut Point sensors.""" +import logging + +from homeassistant.components.point import MinutPointEntity +from homeassistant.components.point.const import ( + DOMAIN as POINT_DOMAIN, POINT_DISCOVERY_NEW) +from homeassistant.components.sensor import DOMAIN +from homeassistant.const import ( + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, + TEMP_CELSIUS) +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.util.dt import parse_datetime + +_LOGGER = logging.getLogger(__name__) + +DEVICE_CLASS_SOUND = 'sound_level' + +SENSOR_TYPES = { + DEVICE_CLASS_TEMPERATURE: (None, 1, TEMP_CELSIUS), + DEVICE_CLASS_PRESSURE: (None, 0, 'hPa'), + DEVICE_CLASS_HUMIDITY: (None, 1, '%'), + DEVICE_CLASS_SOUND: ('mdi:ear-hearing', 1, 'dBa'), +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up a Point's sensors based on a config entry.""" + async def async_discover_sensor(device_id): + """Discover and add a discovered sensor.""" + client = hass.data[POINT_DOMAIN][config_entry.entry_id] + async_add_entities((MinutPointSensor(client, device_id, sensor_type) + for sensor_type in SENSOR_TYPES), True) + + async_dispatcher_connect( + hass, POINT_DISCOVERY_NEW.format(DOMAIN, POINT_DOMAIN), + async_discover_sensor) + + +class MinutPointSensor(MinutPointEntity): + """The platform class required by Home Assistant.""" + + def __init__(self, point_client, device_id, device_class): + """Initialize the sensor.""" + super().__init__(point_client, device_id, device_class) + self._device_prop = SENSOR_TYPES[device_class] + + 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 = 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() + + @property + def icon(self): + """Return the icon representation.""" + return self._device_prop[0] + + @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 + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._device_prop[2] diff --git a/homeassistant/components/prometheus.py b/homeassistant/components/prometheus.py deleted file mode 100644 index dc868530f88b9..0000000000000 --- a/homeassistant/components/prometheus.py +++ /dev/null @@ -1,282 +0,0 @@ -""" -Support for Prometheus metrics export. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/prometheus/ -""" -import logging - -import voluptuous as vol -from aiohttp import web - -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 -from homeassistant.helpers import entityfilter, state as state_helper -from homeassistant.util.temperature import fahrenheit_to_celsius - -REQUIREMENTS = ['prometheus_client==0.2.0'] - -_LOGGER = logging.getLogger(__name__) - -API_ENDPOINT = '/api/prometheus' - -DOMAIN = 'prometheus' -DEPENDENCIES = ['http'] - -CONF_FILTER = 'filter' -CONF_PROM_NAMESPACE = 'namespace' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.All({ - vol.Optional(CONF_FILTER, default={}): entityfilter.FILTER_SCHEMA, - vol.Optional(CONF_PROM_NAMESPACE): cv.string, - }) -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Activate Prometheus component.""" - import prometheus_client - - hass.http.register_view(PrometheusView(prometheus_client)) - - conf = config[DOMAIN] - entity_filter = conf[CONF_FILTER] - namespace = conf.get(CONF_PROM_NAMESPACE) - climate_units = hass.config.units.temperature_unit - metrics = PrometheusMetrics(prometheus_client, entity_filter, namespace, - climate_units) - - hass.bus.listen(EVENT_STATE_CHANGED, metrics.handle_event) - return True - - -class PrometheusMetrics: - """Model all of the metrics which should be exposed to Prometheus.""" - - def __init__(self, prometheus_client, entity_filter, namespace, - climate_units): - """Initialize Prometheus Metrics.""" - self.prometheus_client = prometheus_client - self._filter = entity_filter - if namespace: - self.metrics_prefix = "{}_".format(namespace) - else: - self.metrics_prefix = "" - self._metrics = {} - self._climate_units = climate_units - - def handle_event(self, event): - """Listen for new messages on the bus, and add them to Prometheus.""" - state = event.data.get('new_state') - if state is None: - return - - entity_id = state.entity_id - _LOGGER.debug("Handling state update for %s", entity_id) - domain, _ = hacore.split_entity_id(entity_id) - - if not self._filter(state.entity_id): - return - - handler = '_handle_{}'.format(domain) - - if hasattr(self, handler): - getattr(self, handler)(state) - - metric = self._metric( - 'state_change', - self.prometheus_client.Counter, - 'The number of state changes', - ) - metric.labels(**self._labels(state)).inc() - - def _metric(self, metric, factory, documentation, labels=None): - if labels is None: - labels = ['entity', 'friendly_name', 'domain'] - - try: - return self._metrics[metric] - except KeyError: - full_metric_name = "{}{}".format(self.metrics_prefix, metric) - self._metrics[metric] = factory( - full_metric_name, documentation, labels) - return self._metrics[metric] - - @staticmethod - def _labels(state): - return { - 'entity': state.entity_id, - 'domain': state.domain, - 'friendly_name': state.attributes.get('friendly_name'), - } - - def _battery(self, state): - if 'battery_level' in state.attributes: - metric = self._metric( - 'battery_level_percent', - self.prometheus_client.Gauge, - 'Battery level as a percentage of its capacity', - ) - try: - value = float(state.attributes['battery_level']) - metric.labels(**self._labels(state)).set(value) - except ValueError: - pass - - def _handle_binary_sensor(self, state): - metric = self._metric( - 'binary_sensor_state', - self.prometheus_client.Gauge, - 'State of the binary sensor (0/1)', - ) - value = state_helper.state_as_number(state) - metric.labels(**self._labels(state)).set(value) - - def _handle_input_boolean(self, state): - metric = self._metric( - 'input_boolean_state', - self.prometheus_client.Gauge, - 'State of the input boolean (0/1)', - ) - value = state_helper.state_as_number(state) - metric.labels(**self._labels(state)).set(value) - - def _handle_device_tracker(self, state): - metric = self._metric( - 'device_tracker_state', - self.prometheus_client.Gauge, - 'State of the device tracker (0/1)', - ) - value = state_helper.state_as_number(state) - metric.labels(**self._labels(state)).set(value) - - def _handle_light(self, state): - metric = self._metric( - 'light_state', - self.prometheus_client.Gauge, - 'Load level of a light (0..1)', - ) - - try: - if 'brightness' in state.attributes: - value = state.attributes['brightness'] / 255.0 - else: - value = state_helper.state_as_number(state) - value = value * 100 - metric.labels(**self._labels(state)).set(value) - except ValueError: - pass - - def _handle_lock(self, state): - metric = self._metric( - 'lock_state', - self.prometheus_client.Gauge, - 'State of the lock (0/1)', - ) - value = state_helper.state_as_number(state) - metric.labels(**self._labels(state)).set(value) - - def _handle_climate(self, state): - temp = state.attributes.get(ATTR_TEMPERATURE) - if temp: - if self._climate_units == TEMP_FAHRENHEIT: - temp = fahrenheit_to_celsius(temp) - metric = self._metric( - 'temperature_c', self.prometheus_client.Gauge, - 'Temperature in degrees Celsius') - metric.labels(**self._labels(state)).set(temp) - - current_temp = state.attributes.get(ATTR_CURRENT_TEMPERATURE) - if current_temp: - if self._climate_units == TEMP_FAHRENHEIT: - current_temp = fahrenheit_to_celsius(current_temp) - metric = self._metric( - 'current_temperature_c', self.prometheus_client.Gauge, - 'Current Temperature in degrees Celsius') - metric.labels(**self._labels(state)).set(current_temp) - - metric = self._metric( - 'climate_state', self.prometheus_client.Gauge, - 'State of the thermostat (0/1)') - try: - value = state_helper.state_as_number(state) - metric.labels(**self._labels(state)).set(value) - except ValueError: - pass - - def _handle_sensor(self, state): - - unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - metric = state.entity_id.split(".")[1] - - if '_' not in str(metric): - metric = state.entity_id.replace('.', '_') - - try: - int(metric.split("_")[-1]) - metric = "_".join(metric.split("_")[:-1]) - except ValueError: - pass - - _metric = self._metric(metric, self.prometheus_client.Gauge, - state.entity_id) - - try: - value = state_helper.state_as_number(state) - if unit == TEMP_FAHRENHEIT: - value = fahrenheit_to_celsius(value) - _metric.labels(**self._labels(state)).set(value) - except ValueError: - pass - - self._battery(state) - - def _handle_switch(self, state): - metric = self._metric( - 'switch_state', - self.prometheus_client.Gauge, - 'State of the switch (0/1)', - ) - - try: - value = state_helper.state_as_number(state) - metric.labels(**self._labels(state)).set(value) - except ValueError: - pass - - def _handle_zwave(self, state): - self._battery(state) - - def _handle_automation(self, state): - metric = self._metric( - 'automation_triggered_count', - self.prometheus_client.Counter, - 'Count of times an automation has been triggered', - ) - - metric.labels(**self._labels(state)).inc() - - -class PrometheusView(HomeAssistantView): - """Handle Prometheus requests.""" - - url = API_ENDPOINT - name = 'api:prometheus' - - def __init__(self, prometheus_client): - """Initialize Prometheus view.""" - self.prometheus_client = prometheus_client - - async def get(self, request): - """Handle request for Prometheus metrics.""" - _LOGGER.debug("Received Prometheus metrics request") - - return web.Response( - body=self.prometheus_client.generate_latest(), - content_type=CONTENT_TYPE_TEXT_PLAIN) diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py new file mode 100644 index 0000000000000..4508611e51b46 --- /dev/null +++ b/homeassistant/components/prometheus/__init__.py @@ -0,0 +1,277 @@ +"""Support for Prometheus metrics export.""" +import logging + +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 ( + 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'] + +_LOGGER = logging.getLogger(__name__) + +API_ENDPOINT = '/api/prometheus' + +DOMAIN = 'prometheus' +DEPENDENCIES = ['http'] + +CONF_FILTER = 'filter' +CONF_PROM_NAMESPACE = 'namespace' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All({ + vol.Optional(CONF_FILTER, default={}): entityfilter.FILTER_SCHEMA, + vol.Optional(CONF_PROM_NAMESPACE): cv.string, + }) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Activate Prometheus component.""" + import prometheus_client + + hass.http.register_view(PrometheusView(prometheus_client)) + + conf = config[DOMAIN] + entity_filter = conf[CONF_FILTER] + namespace = conf.get(CONF_PROM_NAMESPACE) + climate_units = hass.config.units.temperature_unit + metrics = PrometheusMetrics(prometheus_client, entity_filter, namespace, + climate_units) + + hass.bus.listen(EVENT_STATE_CHANGED, metrics.handle_event) + return True + + +class PrometheusMetrics: + """Model all of the metrics which should be exposed to Prometheus.""" + + def __init__(self, prometheus_client, entity_filter, namespace, + climate_units): + """Initialize Prometheus Metrics.""" + self.prometheus_client = prometheus_client + self._filter = entity_filter + if namespace: + self.metrics_prefix = "{}_".format(namespace) + else: + self.metrics_prefix = "" + self._metrics = {} + self._climate_units = climate_units + + def handle_event(self, event): + """Listen for new messages on the bus, and add them to Prometheus.""" + state = event.data.get('new_state') + if state is None: + return + + entity_id = state.entity_id + _LOGGER.debug("Handling state update for %s", entity_id) + domain, _ = hacore.split_entity_id(entity_id) + + if not self._filter(state.entity_id): + return + + handler = '_handle_{}'.format(domain) + + if hasattr(self, handler): + getattr(self, handler)(state) + + metric = self._metric( + 'state_change', + self.prometheus_client.Counter, + 'The number of state changes', + ) + metric.labels(**self._labels(state)).inc() + + def _metric(self, metric, factory, documentation, labels=None): + if labels is None: + labels = ['entity', 'friendly_name', 'domain'] + + try: + return self._metrics[metric] + except KeyError: + full_metric_name = "{}{}".format(self.metrics_prefix, metric) + self._metrics[metric] = factory( + full_metric_name, documentation, labels) + return self._metrics[metric] + + @staticmethod + def _labels(state): + return { + 'entity': state.entity_id, + 'domain': state.domain, + 'friendly_name': state.attributes.get('friendly_name'), + } + + def _battery(self, state): + if 'battery_level' in state.attributes: + metric = self._metric( + 'battery_level_percent', + self.prometheus_client.Gauge, + 'Battery level as a percentage of its capacity', + ) + try: + value = float(state.attributes['battery_level']) + metric.labels(**self._labels(state)).set(value) + except ValueError: + pass + + def _handle_binary_sensor(self, state): + metric = self._metric( + 'binary_sensor_state', + self.prometheus_client.Gauge, + 'State of the binary sensor (0/1)', + ) + value = state_helper.state_as_number(state) + metric.labels(**self._labels(state)).set(value) + + def _handle_input_boolean(self, state): + metric = self._metric( + 'input_boolean_state', + self.prometheus_client.Gauge, + 'State of the input boolean (0/1)', + ) + value = state_helper.state_as_number(state) + metric.labels(**self._labels(state)).set(value) + + def _handle_device_tracker(self, state): + metric = self._metric( + 'device_tracker_state', + self.prometheus_client.Gauge, + 'State of the device tracker (0/1)', + ) + value = state_helper.state_as_number(state) + metric.labels(**self._labels(state)).set(value) + + def _handle_light(self, state): + metric = self._metric( + 'light_state', + self.prometheus_client.Gauge, + 'Load level of a light (0..1)', + ) + + try: + if 'brightness' in state.attributes: + value = state.attributes['brightness'] / 255.0 + else: + value = state_helper.state_as_number(state) + value = value * 100 + metric.labels(**self._labels(state)).set(value) + except ValueError: + pass + + def _handle_lock(self, state): + metric = self._metric( + 'lock_state', + self.prometheus_client.Gauge, + 'State of the lock (0/1)', + ) + value = state_helper.state_as_number(state) + metric.labels(**self._labels(state)).set(value) + + def _handle_climate(self, state): + temp = state.attributes.get(ATTR_TEMPERATURE) + if temp: + if self._climate_units == TEMP_FAHRENHEIT: + temp = fahrenheit_to_celsius(temp) + metric = self._metric( + 'temperature_c', self.prometheus_client.Gauge, + 'Temperature in degrees Celsius') + metric.labels(**self._labels(state)).set(temp) + + current_temp = state.attributes.get(ATTR_CURRENT_TEMPERATURE) + if current_temp: + if self._climate_units == TEMP_FAHRENHEIT: + current_temp = fahrenheit_to_celsius(current_temp) + metric = self._metric( + 'current_temperature_c', self.prometheus_client.Gauge, + 'Current Temperature in degrees Celsius') + metric.labels(**self._labels(state)).set(current_temp) + + metric = self._metric( + 'climate_state', self.prometheus_client.Gauge, + 'State of the thermostat (0/1)') + try: + value = state_helper.state_as_number(state) + metric.labels(**self._labels(state)).set(value) + except ValueError: + pass + + def _handle_sensor(self, state): + + unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + metric = state.entity_id.split(".")[1] + + if '_' not in str(metric): + metric = state.entity_id.replace('.', '_') + + try: + int(metric.split("_")[-1]) + metric = "_".join(metric.split("_")[:-1]) + except ValueError: + pass + + _metric = self._metric(metric, self.prometheus_client.Gauge, + state.entity_id) + + try: + value = state_helper.state_as_number(state) + if unit == TEMP_FAHRENHEIT: + value = fahrenheit_to_celsius(value) + _metric.labels(**self._labels(state)).set(value) + except ValueError: + pass + + self._battery(state) + + def _handle_switch(self, state): + metric = self._metric( + 'switch_state', + self.prometheus_client.Gauge, + 'State of the switch (0/1)', + ) + + try: + value = state_helper.state_as_number(state) + metric.labels(**self._labels(state)).set(value) + except ValueError: + pass + + def _handle_zwave(self, state): + self._battery(state) + + def _handle_automation(self, state): + metric = self._metric( + 'automation_triggered_count', + self.prometheus_client.Counter, + 'Count of times an automation has been triggered', + ) + + metric.labels(**self._labels(state)).inc() + + +class PrometheusView(HomeAssistantView): + """Handle Prometheus requests.""" + + url = API_ENDPOINT + name = 'api:prometheus' + + def __init__(self, prometheus_client): + """Initialize Prometheus view.""" + self.prometheus_client = prometheus_client + + async def get(self, request): + """Handle request for Prometheus metrics.""" + _LOGGER.debug("Received Prometheus metrics request") + + return web.Response( + body=self.prometheus_client.generate_latest(), + content_type=CONTENT_TYPE_TEXT_PLAIN) diff --git a/homeassistant/components/proximity.py b/homeassistant/components/proximity.py deleted file mode 100644 index e8d86d480e524..0000000000000 --- a/homeassistant/components/proximity.py +++ /dev/null @@ -1,256 +0,0 @@ -""" -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/ -""" -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) -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import track_state_change -from homeassistant.util.distance import convert -from homeassistant.util.location import distance - -_LOGGER = logging.getLogger(__name__) - -ATTR_DIR_OF_TRAVEL = 'dir_of_travel' -ATTR_DIST_FROM = 'dist_to_zone' -ATTR_NEAREST = 'nearest' - -CONF_IGNORED_ZONES = 'ignored_zones' -CONF_TOLERANCE = 'tolerance' - -DEFAULT_DIR_OF_TRAVEL = 'not set' -DEFAULT_DIST_TO_ZONE = 'not set' -DEFAULT_NEAREST = 'not set' -DEFAULT_PROXIMITY_ZONE = 'home' -DEFAULT_TOLERANCE = 1 -DEPENDENCIES = ['zone', 'device_tracker'] -DOMAIN = 'proximity' - -UNITS = ['km', 'm', 'mi', 'ft'] - -ZONE_SCHEMA = vol.Schema({ - vol.Optional(CONF_ZONE, default=DEFAULT_PROXIMITY_ZONE): cv.string, - vol.Optional(CONF_DEVICES, default=[]): - vol.All(cv.ensure_list, [cv.entity_id]), - vol.Optional(CONF_IGNORED_ZONES, default=[]): - vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_TOLERANCE, default=DEFAULT_TOLERANCE): cv.positive_int, - vol.Optional(CONF_UNIT_OF_MEASUREMENT): vol.All(cv.string, vol.In(UNITS)), -}) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: cv.schema_with_slug_keys(ZONE_SCHEMA), -}, extra=vol.ALLOW_EXTRA) - - -def setup_proximity_component(hass, name, config): - """Set up the individual proximity component.""" - ignored_zones = config.get(CONF_IGNORED_ZONES) - proximity_devices = config.get(CONF_DEVICES) - tolerance = config.get(CONF_TOLERANCE) - proximity_zone = name - unit_of_measurement = config.get( - CONF_UNIT_OF_MEASUREMENT, hass.config.units.length_unit) - zone_id = 'zone.{}'.format(config.get(CONF_ZONE)) - - proximity = Proximity(hass, proximity_zone, DEFAULT_DIST_TO_ZONE, - DEFAULT_DIR_OF_TRAVEL, DEFAULT_NEAREST, - ignored_zones, proximity_devices, tolerance, - zone_id, unit_of_measurement) - proximity.entity_id = '{}.{}'.format(DOMAIN, proximity_zone) - - proximity.schedule_update_ha_state() - - track_state_change( - hass, proximity_devices, proximity.check_proximity_state_change) - - return True - - -def setup(hass, config): - """Get the zones and offsets from configuration.yaml.""" - for zone, proximity_config in config[DOMAIN].items(): - setup_proximity_component(hass, zone, proximity_config) - - return True - - -class Proximity(Entity): - """Representation of a Proximity.""" - - def __init__(self, hass, zone_friendly_name, dist_to, dir_of_travel, - nearest, ignored_zones, proximity_devices, tolerance, - proximity_zone, unit_of_measurement): - """Initialize the proximity.""" - self.hass = hass - self.friendly_name = zone_friendly_name - self.dist_to = dist_to - self.dir_of_travel = dir_of_travel - self.nearest = nearest - self.ignored_zones = ignored_zones - self.proximity_devices = proximity_devices - self.tolerance = tolerance - self.proximity_zone = proximity_zone - self._unit_of_measurement = unit_of_measurement - - @property - def name(self): - """Return the name of the entity.""" - return self.friendly_name - - @property - def state(self): - """Return the state.""" - return self.dist_to - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity.""" - return self._unit_of_measurement - - @property - def state_attributes(self): - """Return the state attributes.""" - return { - ATTR_DIR_OF_TRAVEL: self.dir_of_travel, - ATTR_NEAREST: self.nearest, - } - - def check_proximity_state_change(self, entity, old_state, new_state): - """Perform the proximity checking.""" - entity_name = new_state.name - devices_to_calculate = False - devices_in_zone = '' - - zone_state = self.hass.states.get(self.proximity_zone) - proximity_latitude = zone_state.attributes.get('latitude') - proximity_longitude = zone_state.attributes.get('longitude') - - # Check for devices in the monitored zone. - for device in self.proximity_devices: - device_state = self.hass.states.get(device) - - if device_state is None: - devices_to_calculate = True - continue - - if device_state.state not in self.ignored_zones: - devices_to_calculate = True - - # Check the location of all devices. - if (device_state.state).lower() == (self.friendly_name).lower(): - device_friendly = device_state.name - if devices_in_zone != '': - devices_in_zone = devices_in_zone + ', ' - devices_in_zone = devices_in_zone + device_friendly - - # No-one to track so reset the entity. - if not devices_to_calculate: - self.dist_to = 'not set' - self.dir_of_travel = 'not set' - self.nearest = 'not set' - self.schedule_update_ha_state() - return - - # At least one device is in the monitored zone so update the entity. - if devices_in_zone != '': - self.dist_to = 0 - self.dir_of_travel = 'arrived' - self.nearest = devices_in_zone - self.schedule_update_ha_state() - return - - # We can't check proximity because latitude and longitude don't exist. - if 'latitude' not in new_state.attributes: - return - - # Collect distances to the zone for all devices. - distances_to_zone = {} - for device in self.proximity_devices: - # Ignore devices in an ignored zone. - device_state = self.hass.states.get(device) - if device_state.state in self.ignored_zones: - continue - - # Ignore devices if proximity cannot be calculated. - if 'latitude' not in device_state.attributes: - continue - - # Calculate the distance to the proximity zone. - dist_to_zone = distance(proximity_latitude, - proximity_longitude, - device_state.attributes['latitude'], - device_state.attributes['longitude']) - - # Add the device and distance to a dictionary. - distances_to_zone[device] = round( - convert(dist_to_zone, 'm', self.unit_of_measurement), 1) - - # Loop through each of the distances collected and work out the - # closest. - closest_device = None # type: str - dist_to_zone = None # type: float - - for device in distances_to_zone: - if not dist_to_zone or distances_to_zone[device] < dist_to_zone: - closest_device = device - dist_to_zone = distances_to_zone[device] - - # If the closest device is one of the other devices. - if closest_device != entity: - self.dist_to = round(distances_to_zone[closest_device]) - self.dir_of_travel = 'unknown' - device_state = self.hass.states.get(closest_device) - self.nearest = device_state.name - self.schedule_update_ha_state() - return - - # Stop if we cannot calculate the direction of travel (i.e. we don't - # have a previous state and a current LAT and LONG). - if old_state is None or 'latitude' not in old_state.attributes: - self.dist_to = round(distances_to_zone[entity]) - self.dir_of_travel = 'unknown' - self.nearest = entity_name - self.schedule_update_ha_state() - return - - # Reset the variables - distance_travelled = 0 - - # Calculate the distance travelled. - old_distance = distance(proximity_latitude, proximity_longitude, - old_state.attributes['latitude'], - old_state.attributes['longitude']) - new_distance = distance(proximity_latitude, proximity_longitude, - new_state.attributes['latitude'], - new_state.attributes['longitude']) - distance_travelled = round(new_distance - old_distance, 1) - - # Check for tolerance - if distance_travelled < self.tolerance * -1: - direction_of_travel = 'towards' - elif distance_travelled > self.tolerance: - direction_of_travel = 'away_from' - else: - direction_of_travel = 'stationary' - - # Update the proximity entity - self.dist_to = round(dist_to_zone) - self.dir_of_travel = direction_of_travel - self.nearest = entity_name - self.schedule_update_ha_state() - _LOGGER.debug('proximity.%s update entity: distance=%s: direction=%s: ' - 'device=%s', self.friendly_name, round(dist_to_zone), - direction_of_travel, entity_name) - - _LOGGER.info('%s: proximity calculation complete', entity_name) diff --git a/homeassistant/components/proximity/__init__.py b/homeassistant/components/proximity/__init__.py new file mode 100644 index 0000000000000..0a617bcec9011 --- /dev/null +++ b/homeassistant/components/proximity/__init__.py @@ -0,0 +1,248 @@ +"""Support for tracking the proximity of a device.""" +import logging + +import voluptuous as vol + +from homeassistant.const import ( + 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 +from homeassistant.util.location import distance + +_LOGGER = logging.getLogger(__name__) + +ATTR_DIR_OF_TRAVEL = 'dir_of_travel' +ATTR_DIST_FROM = 'dist_to_zone' +ATTR_NEAREST = 'nearest' + +CONF_IGNORED_ZONES = 'ignored_zones' +CONF_TOLERANCE = 'tolerance' + +DEFAULT_DIR_OF_TRAVEL = 'not set' +DEFAULT_DIST_TO_ZONE = 'not set' +DEFAULT_NEAREST = 'not set' +DEFAULT_PROXIMITY_ZONE = 'home' +DEFAULT_TOLERANCE = 1 +DEPENDENCIES = ['zone', 'device_tracker'] +DOMAIN = 'proximity' + +UNITS = ['km', 'm', 'mi', 'ft'] + +ZONE_SCHEMA = vol.Schema({ + vol.Optional(CONF_ZONE, default=DEFAULT_PROXIMITY_ZONE): cv.string, + vol.Optional(CONF_DEVICES, default=[]): + vol.All(cv.ensure_list, [cv.entity_id]), + vol.Optional(CONF_IGNORED_ZONES, default=[]): + vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_TOLERANCE, default=DEFAULT_TOLERANCE): cv.positive_int, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): vol.All(cv.string, vol.In(UNITS)), +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: cv.schema_with_slug_keys(ZONE_SCHEMA), +}, extra=vol.ALLOW_EXTRA) + + +def setup_proximity_component(hass, name, config): + """Set up the individual proximity component.""" + ignored_zones = config.get(CONF_IGNORED_ZONES) + proximity_devices = config.get(CONF_DEVICES) + tolerance = config.get(CONF_TOLERANCE) + proximity_zone = name + unit_of_measurement = config.get( + CONF_UNIT_OF_MEASUREMENT, hass.config.units.length_unit) + zone_id = 'zone.{}'.format(config.get(CONF_ZONE)) + + proximity = Proximity(hass, proximity_zone, DEFAULT_DIST_TO_ZONE, + DEFAULT_DIR_OF_TRAVEL, DEFAULT_NEAREST, + ignored_zones, proximity_devices, tolerance, + zone_id, unit_of_measurement) + proximity.entity_id = '{}.{}'.format(DOMAIN, proximity_zone) + + proximity.schedule_update_ha_state() + + track_state_change( + hass, proximity_devices, proximity.check_proximity_state_change) + + return True + + +def setup(hass, config): + """Get the zones and offsets from configuration.yaml.""" + for zone, proximity_config in config[DOMAIN].items(): + setup_proximity_component(hass, zone, proximity_config) + + return True + + +class Proximity(Entity): + """Representation of a Proximity.""" + + def __init__(self, hass, zone_friendly_name, dist_to, dir_of_travel, + nearest, ignored_zones, proximity_devices, tolerance, + proximity_zone, unit_of_measurement): + """Initialize the proximity.""" + self.hass = hass + self.friendly_name = zone_friendly_name + self.dist_to = dist_to + self.dir_of_travel = dir_of_travel + self.nearest = nearest + self.ignored_zones = ignored_zones + self.proximity_devices = proximity_devices + self.tolerance = tolerance + self.proximity_zone = proximity_zone + self._unit_of_measurement = unit_of_measurement + + @property + def name(self): + """Return the name of the entity.""" + return self.friendly_name + + @property + def state(self): + """Return the state.""" + return self.dist_to + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity.""" + return self._unit_of_measurement + + @property + def state_attributes(self): + """Return the state attributes.""" + return { + ATTR_DIR_OF_TRAVEL: self.dir_of_travel, + ATTR_NEAREST: self.nearest, + } + + def check_proximity_state_change(self, entity, old_state, new_state): + """Perform the proximity checking.""" + entity_name = new_state.name + devices_to_calculate = False + devices_in_zone = '' + + zone_state = self.hass.states.get(self.proximity_zone) + proximity_latitude = zone_state.attributes.get('latitude') + proximity_longitude = zone_state.attributes.get('longitude') + + # Check for devices in the monitored zone. + for device in self.proximity_devices: + device_state = self.hass.states.get(device) + + if device_state is None: + devices_to_calculate = True + continue + + if device_state.state not in self.ignored_zones: + devices_to_calculate = True + + # Check the location of all devices. + if (device_state.state).lower() == (self.friendly_name).lower(): + device_friendly = device_state.name + if devices_in_zone != '': + devices_in_zone = devices_in_zone + ', ' + devices_in_zone = devices_in_zone + device_friendly + + # No-one to track so reset the entity. + if not devices_to_calculate: + self.dist_to = 'not set' + self.dir_of_travel = 'not set' + self.nearest = 'not set' + self.schedule_update_ha_state() + return + + # At least one device is in the monitored zone so update the entity. + if devices_in_zone != '': + self.dist_to = 0 + self.dir_of_travel = 'arrived' + self.nearest = devices_in_zone + self.schedule_update_ha_state() + return + + # We can't check proximity because latitude and longitude don't exist. + if 'latitude' not in new_state.attributes: + return + + # Collect distances to the zone for all devices. + distances_to_zone = {} + for device in self.proximity_devices: + # Ignore devices in an ignored zone. + device_state = self.hass.states.get(device) + if device_state.state in self.ignored_zones: + continue + + # Ignore devices if proximity cannot be calculated. + if 'latitude' not in device_state.attributes: + continue + + # Calculate the distance to the proximity zone. + dist_to_zone = distance(proximity_latitude, + proximity_longitude, + device_state.attributes['latitude'], + device_state.attributes['longitude']) + + # Add the device and distance to a dictionary. + distances_to_zone[device] = round( + convert(dist_to_zone, 'm', self.unit_of_measurement), 1) + + # Loop through each of the distances collected and work out the + # closest. + closest_device = None # type: str + dist_to_zone = None # type: float + + for device in distances_to_zone: + if not dist_to_zone or distances_to_zone[device] < dist_to_zone: + closest_device = device + dist_to_zone = distances_to_zone[device] + + # If the closest device is one of the other devices. + if closest_device != entity: + self.dist_to = round(distances_to_zone[closest_device]) + self.dir_of_travel = 'unknown' + device_state = self.hass.states.get(closest_device) + self.nearest = device_state.name + self.schedule_update_ha_state() + return + + # Stop if we cannot calculate the direction of travel (i.e. we don't + # have a previous state and a current LAT and LONG). + if old_state is None or 'latitude' not in old_state.attributes: + self.dist_to = round(distances_to_zone[entity]) + self.dir_of_travel = 'unknown' + self.nearest = entity_name + self.schedule_update_ha_state() + return + + # Reset the variables + distance_travelled = 0 + + # Calculate the distance travelled. + old_distance = distance(proximity_latitude, proximity_longitude, + old_state.attributes['latitude'], + old_state.attributes['longitude']) + new_distance = distance(proximity_latitude, proximity_longitude, + new_state.attributes['latitude'], + new_state.attributes['longitude']) + distance_travelled = round(new_distance - old_distance, 1) + + # Check for tolerance + if distance_travelled < self.tolerance * -1: + direction_of_travel = 'towards' + elif distance_travelled > self.tolerance: + direction_of_travel = 'away_from' + else: + direction_of_travel = 'stationary' + + # Update the proximity entity + self.dist_to = round(dist_to_zone) + self.dir_of_travel = direction_of_travel + self.nearest = entity_name + self.schedule_update_ha_state() + _LOGGER.debug('proximity.%s update entity: distance=%s: direction=%s: ' + 'device=%s', self.friendly_name, round(dist_to_zone), + direction_of_travel, entity_name) + + _LOGGER.info('%s: proximity calculation complete', entity_name) diff --git a/homeassistant/components/python_script.py b/homeassistant/components/python_script.py deleted file mode 100644 index 3cfa069664499..0000000000000 --- a/homeassistant/components/python_script.py +++ /dev/null @@ -1,211 +0,0 @@ -""" -Component to allow running Python scripts. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/python_script/ -""" -import datetime -import glob -import logging -import os -import time - -import voluptuous as vol - -from homeassistant.const import SERVICE_RELOAD -from homeassistant.exceptions import HomeAssistantError -from homeassistant.loader import bind_hass -from homeassistant.util import sanitize_filename -import homeassistant.util.dt as dt_util - -REQUIREMENTS = ['restrictedpython==4.0b7'] - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'python_script' - -FOLDER = 'python_scripts' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema(dict) -}, extra=vol.ALLOW_EXTRA) - -ALLOWED_HASS = set(['bus', 'services', 'states']) -ALLOWED_EVENTBUS = set(['fire']) -ALLOWED_STATEMACHINE = set(['entity_ids', 'all', 'get', 'is_state', - 'is_state_attr', 'remove', 'set']) -ALLOWED_SERVICEREGISTRY = set(['services', 'has_service', 'call']) -ALLOWED_TIME = set(['sleep', 'strftime', 'strptime', 'gmtime', 'localtime', - 'ctime', 'time', 'mktime']) -ALLOWED_DATETIME = set(['date', 'time', 'datetime', 'timedelta', 'tzinfo']) -ALLOWED_DT_UTIL = set([ - 'utcnow', 'now', 'as_utc', 'as_timestamp', 'as_local', - 'utc_from_timestamp', 'start_of_local_day', 'parse_datetime', 'parse_date', - 'get_age']) - - -class ScriptError(HomeAssistantError): - """When a script error occurs.""" - - pass - - -def setup(hass, config): - """Initialize the Python script component.""" - path = hass.config.path(FOLDER) - - if not os.path.isdir(path): - _LOGGER.warning("Folder %s not found in configuration folder", FOLDER) - return False - - discover_scripts(hass) - - def reload_scripts_handler(call): - """Handle reload service calls.""" - discover_scripts(hass) - hass.services.register(DOMAIN, SERVICE_RELOAD, reload_scripts_handler) - - return True - - -def discover_scripts(hass): - """Discover python scripts in folder.""" - path = hass.config.path(FOLDER) - - if not os.path.isdir(path): - _LOGGER.warning("Folder %s not found in configuration folder", FOLDER) - return False - - def python_script_service_handler(call): - """Handle python script service calls.""" - execute_script(hass, call.service, call.data) - - existing = hass.services.services.get(DOMAIN, {}).keys() - for existing_service in existing: - if existing_service == SERVICE_RELOAD: - continue - hass.services.remove(DOMAIN, existing_service) - - for fil in glob.iglob(os.path.join(path, '*.py')): - name = os.path.splitext(os.path.basename(fil))[0] - hass.services.register(DOMAIN, name, python_script_service_handler) - - -@bind_hass -def execute_script(hass, name, data=None): - """Execute a script.""" - filename = '{}.py'.format(name) - with open(hass.config.path(FOLDER, sanitize_filename(filename))) as fil: - source = fil.read() - execute(hass, filename, source, data) - - -@bind_hass -def execute(hass, filename, source, data=None): - """Execute Python source.""" - from RestrictedPython import compile_restricted_exec - from RestrictedPython.Guards import safe_builtins, full_write_guard, \ - guarded_iter_unpack_sequence, guarded_unpack_sequence - from RestrictedPython.Utilities import utility_builtins - from RestrictedPython.Eval import default_guarded_getitem - - compiled = compile_restricted_exec(source, filename=filename) - - if compiled.errors: - _LOGGER.error("Error loading script %s: %s", filename, - ", ".join(compiled.errors)) - return - - if compiled.warnings: - _LOGGER.warning("Warning loading script %s: %s", filename, - ", ".join(compiled.warnings)) - - def protected_getattr(obj, name, default=None): - """Restricted method to get attributes.""" - # pylint: disable=too-many-boolean-expressions - if name.startswith('async_'): - raise ScriptError("Not allowed to access async methods") - elif (obj is hass and name not in ALLOWED_HASS or - obj is hass.bus and name not in ALLOWED_EVENTBUS or - obj is hass.states and name not in ALLOWED_STATEMACHINE or - obj is hass.services and name not in ALLOWED_SERVICEREGISTRY or - obj is dt_util and name not in ALLOWED_DT_UTIL or - obj is datetime and name not in ALLOWED_DATETIME or - isinstance(obj, TimeWrapper) and name not in ALLOWED_TIME): - raise ScriptError("Not allowed to access {}.{}".format( - obj.__class__.__name__, name)) - - return getattr(obj, name, default) - - builtins = safe_builtins.copy() - builtins.update(utility_builtins) - builtins['datetime'] = datetime - builtins['sorted'] = sorted - builtins['time'] = TimeWrapper() - builtins['dt_util'] = dt_util - restricted_globals = { - '__builtins__': builtins, - '_print_': StubPrinter, - '_getattr_': protected_getattr, - '_write_': full_write_guard, - '_getiter_': iter, - '_getitem_': default_guarded_getitem, - '_iter_unpack_sequence_': guarded_iter_unpack_sequence, - '_unpack_sequence_': guarded_unpack_sequence, - } - logger = logging.getLogger('{}.{}'.format(__name__, filename)) - local = { - 'hass': hass, - 'data': data or {}, - 'logger': logger - } - - try: - _LOGGER.info("Executing %s: %s", filename, data) - # pylint: disable=exec-used - exec(compiled.code, restricted_globals, local) - except ScriptError as err: - logger.error("Error executing script: %s", err) - except Exception as err: # pylint: disable=broad-except - logger.exception("Error executing script: %s", err) - - -class StubPrinter: - """Class to handle printing inside scripts.""" - - def __init__(self, _getattr_): - """Initialize our printer.""" - pass - - def _call_print(self, *objects, **kwargs): - """Print text.""" - # pylint: disable=no-self-use - _LOGGER.warning( - "Don't use print() inside scripts. Use logger.info() instead") - - -class TimeWrapper: - """Wrap the time module.""" - - # Class variable, only going to warn once per Home Assistant run - warned = False - - # pylint: disable=no-self-use - def sleep(self, *args, **kwargs): - """Sleep method that warns once.""" - if not TimeWrapper.warned: - TimeWrapper.warned = True - _LOGGER.warning("Using time.sleep can reduce the performance of " - "Home Assistant") - - time.sleep(*args, **kwargs) - - def __getattr__(self, attr): - """Fetch an attribute from Time module.""" - attribute = getattr(time, attr) - if callable(attribute): - def wrapper(*args, **kw): - """Wrap to return callable method if callable.""" - return attribute(*args, **kw) - return wrapper - return attribute diff --git a/homeassistant/components/python_script/__init__.py b/homeassistant/components/python_script/__init__.py new file mode 100644 index 0000000000000..3d0952b89fbac --- /dev/null +++ b/homeassistant/components/python_script/__init__.py @@ -0,0 +1,211 @@ +""" +Component to allow running Python scripts. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/python_script/ +""" +import datetime +import glob +import logging +import os +import time + +import voluptuous as vol + +from homeassistant.const import SERVICE_RELOAD +from homeassistant.exceptions import HomeAssistantError +from homeassistant.loader import bind_hass +from homeassistant.util import sanitize_filename +import homeassistant.util.dt as dt_util + +REQUIREMENTS = ['restrictedpython==4.0b8'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'python_script' + +FOLDER = 'python_scripts' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema(dict) +}, extra=vol.ALLOW_EXTRA) + +ALLOWED_HASS = set(['bus', 'services', 'states']) +ALLOWED_EVENTBUS = set(['fire']) +ALLOWED_STATEMACHINE = set(['entity_ids', 'all', 'get', 'is_state', + 'is_state_attr', 'remove', 'set']) +ALLOWED_SERVICEREGISTRY = set(['services', 'has_service', 'call']) +ALLOWED_TIME = set(['sleep', 'strftime', 'strptime', 'gmtime', 'localtime', + 'ctime', 'time', 'mktime']) +ALLOWED_DATETIME = set(['date', 'time', 'datetime', 'timedelta', 'tzinfo']) +ALLOWED_DT_UTIL = set([ + 'utcnow', 'now', 'as_utc', 'as_timestamp', 'as_local', + 'utc_from_timestamp', 'start_of_local_day', 'parse_datetime', 'parse_date', + 'get_age']) + + +class ScriptError(HomeAssistantError): + """When a script error occurs.""" + + pass + + +def setup(hass, config): + """Initialize the Python script component.""" + path = hass.config.path(FOLDER) + + if not os.path.isdir(path): + _LOGGER.warning("Folder %s not found in configuration folder", FOLDER) + return False + + discover_scripts(hass) + + def reload_scripts_handler(call): + """Handle reload service calls.""" + discover_scripts(hass) + hass.services.register(DOMAIN, SERVICE_RELOAD, reload_scripts_handler) + + return True + + +def discover_scripts(hass): + """Discover python scripts in folder.""" + path = hass.config.path(FOLDER) + + if not os.path.isdir(path): + _LOGGER.warning("Folder %s not found in configuration folder", FOLDER) + return False + + def python_script_service_handler(call): + """Handle python script service calls.""" + execute_script(hass, call.service, call.data) + + existing = hass.services.services.get(DOMAIN, {}).keys() + for existing_service in existing: + if existing_service == SERVICE_RELOAD: + continue + hass.services.remove(DOMAIN, existing_service) + + for fil in glob.iglob(os.path.join(path, '*.py')): + name = os.path.splitext(os.path.basename(fil))[0] + hass.services.register(DOMAIN, name, python_script_service_handler) + + +@bind_hass +def execute_script(hass, name, data=None): + """Execute a script.""" + filename = '{}.py'.format(name) + with open(hass.config.path(FOLDER, sanitize_filename(filename))) as fil: + source = fil.read() + execute(hass, filename, source, data) + + +@bind_hass +def execute(hass, filename, source, data=None): + """Execute Python source.""" + from RestrictedPython import compile_restricted_exec + from RestrictedPython.Guards import safe_builtins, full_write_guard, \ + guarded_iter_unpack_sequence, guarded_unpack_sequence + from RestrictedPython.Utilities import utility_builtins + from RestrictedPython.Eval import default_guarded_getitem + + compiled = compile_restricted_exec(source, filename=filename) + + if compiled.errors: + _LOGGER.error("Error loading script %s: %s", filename, + ", ".join(compiled.errors)) + return + + if compiled.warnings: + _LOGGER.warning("Warning loading script %s: %s", filename, + ", ".join(compiled.warnings)) + + def protected_getattr(obj, name, default=None): + """Restricted method to get attributes.""" + # pylint: disable=too-many-boolean-expressions + if name.startswith('async_'): + raise ScriptError("Not allowed to access async methods") + elif (obj is hass and name not in ALLOWED_HASS or + obj is hass.bus and name not in ALLOWED_EVENTBUS or + obj is hass.states and name not in ALLOWED_STATEMACHINE or + obj is hass.services and name not in ALLOWED_SERVICEREGISTRY or + obj is dt_util and name not in ALLOWED_DT_UTIL or + obj is datetime and name not in ALLOWED_DATETIME or + isinstance(obj, TimeWrapper) and name not in ALLOWED_TIME): + raise ScriptError("Not allowed to access {}.{}".format( + obj.__class__.__name__, name)) + + return getattr(obj, name, default) + + builtins = safe_builtins.copy() + builtins.update(utility_builtins) + builtins['datetime'] = datetime + builtins['sorted'] = sorted + builtins['time'] = TimeWrapper() + builtins['dt_util'] = dt_util + restricted_globals = { + '__builtins__': builtins, + '_print_': StubPrinter, + '_getattr_': protected_getattr, + '_write_': full_write_guard, + '_getiter_': iter, + '_getitem_': default_guarded_getitem, + '_iter_unpack_sequence_': guarded_iter_unpack_sequence, + '_unpack_sequence_': guarded_unpack_sequence, + } + logger = logging.getLogger('{}.{}'.format(__name__, filename)) + local = { + 'hass': hass, + 'data': data or {}, + 'logger': logger + } + + try: + _LOGGER.info("Executing %s: %s", filename, data) + # pylint: disable=exec-used + exec(compiled.code, restricted_globals, local) + except ScriptError as err: + logger.error("Error executing script: %s", err) + except Exception as err: # pylint: disable=broad-except + logger.exception("Error executing script: %s", err) + + +class StubPrinter: + """Class to handle printing inside scripts.""" + + def __init__(self, _getattr_): + """Initialize our printer.""" + pass + + def _call_print(self, *objects, **kwargs): + """Print text.""" + # pylint: disable=no-self-use + _LOGGER.warning( + "Don't use print() inside scripts. Use logger.info() instead") + + +class TimeWrapper: + """Wrap the time module.""" + + # Class variable, only going to warn once per Home Assistant run + warned = False + + # pylint: disable=no-self-use + def sleep(self, *args, **kwargs): + """Sleep method that warns once.""" + if not TimeWrapper.warned: + TimeWrapper.warned = True + _LOGGER.warning("Using time.sleep can reduce the performance of " + "Home Assistant") + + time.sleep(*args, **kwargs) + + def __getattr__(self, attr): + """Fetch an attribute from Time module.""" + attribute = getattr(time, attr) + if callable(attribute): + def wrapper(*args, **kw): + """Wrap to return callable method if callable.""" + return attribute(*args, **kw) + return wrapper + return attribute 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 0000000000000..61d29894fe251 --- /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 0885c7e9e669c..5ce254c4026ae 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 97b9f84f26c81..12e77ed8e4623 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 4eb9e390d7139..0591af8acfa06 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.py deleted file mode 100644 index 5e834cdf7ec2d..0000000000000 --- a/homeassistant/components/raspihats.py +++ /dev/null @@ -1,241 +0,0 @@ -""" -Support for controlling raspihats boards. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/raspihats/ -""" -import logging -import threading -import time - -from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) - -REQUIREMENTS = ['raspihats==2.2.3', 'smbus-cffi==0.5.1'] - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'raspihats' - -CONF_I2C_HATS = 'i2c_hats' -CONF_BOARD = 'board' -CONF_ADDRESS = 'address' -CONF_CHANNELS = 'channels' -CONF_INDEX = 'index' -CONF_INVERT_LOGIC = 'invert_logic' -CONF_INITIAL_STATE = 'initial_state' - -I2C_HAT_NAMES = [ - 'Di16', 'Rly10', 'Di6Rly6', - 'DI16ac', 'DQ10rly', 'DQ16oc', 'DI6acDQ6rly' -] - -I2C_HATS_MANAGER = 'I2CH_MNG' - - -def setup(hass, config): - """Set up the raspihats component.""" - hass.data[I2C_HATS_MANAGER] = I2CHatsManager() - - def start_i2c_hats_keep_alive(event): - """Start I2C-HATs keep alive.""" - hass.data[I2C_HATS_MANAGER].start_keep_alive() - - def stop_i2c_hats_keep_alive(event): - """Stop I2C-HATs keep alive.""" - hass.data[I2C_HATS_MANAGER].stop_keep_alive() - - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_i2c_hats_keep_alive) - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_i2c_hats_keep_alive) - return True - - -def log_message(source, *parts): - """Build log message.""" - message = source.__class__.__name__ - for part in parts: - message += ": " + str(part) - return message - - -class I2CHatsException(Exception): - """I2C-HATs exception.""" - - -class I2CHatsDIScanner: - """Scan Digital Inputs and fire callbacks.""" - - _DIGITAL_INPUTS = "di" - _OLD_VALUE = "old_value" - _CALLBACKS = "callbacks" - - def setup(self, i2c_hat): - """Set up the I2C-HAT instance for digital inputs scanner.""" - if hasattr(i2c_hat, self._DIGITAL_INPUTS): - digital_inputs = getattr(i2c_hat, self._DIGITAL_INPUTS) - old_value = None - # Add old value attribute - setattr(digital_inputs, self._OLD_VALUE, old_value) - # Add callbacks dict attribute {channel: callback} - setattr(digital_inputs, self._CALLBACKS, {}) - - def register_callback(self, i2c_hat, channel, callback): - """Register edge callback.""" - if hasattr(i2c_hat, self._DIGITAL_INPUTS): - digital_inputs = getattr(i2c_hat, self._DIGITAL_INPUTS) - callbacks = getattr(digital_inputs, self._CALLBACKS) - callbacks[channel] = callback - setattr(digital_inputs, self._CALLBACKS, callbacks) - - def scan(self, i2c_hat): - """Scan I2C-HATs digital inputs and fire callbacks.""" - if hasattr(i2c_hat, self._DIGITAL_INPUTS): - digital_inputs = getattr(i2c_hat, self._DIGITAL_INPUTS) - callbacks = getattr(digital_inputs, self._CALLBACKS) - old_value = getattr(digital_inputs, self._OLD_VALUE) - value = digital_inputs.value # i2c data transfer - if old_value is not None and value != old_value: - for channel in range(0, len(digital_inputs.channels)): - state = (value >> channel) & 0x01 - old_state = (old_value >> channel) & 0x01 - if state != old_state: - callback = callbacks.get(channel, None) - if callback is not None: - callback(state) - setattr(digital_inputs, self._OLD_VALUE, value) - - -class I2CHatsManager(threading.Thread): - """Manages all I2C-HATs instances.""" - - _EXCEPTION = "exception" - _CALLBACKS = "callbacks" - - def __init__(self): - """Init I2C-HATs Manager.""" - threading.Thread.__init__(self) - self._lock = threading.Lock() - self._i2c_hats = {} - self._run = False - self._di_scanner = I2CHatsDIScanner() - - def register_board(self, board, address): - """Register I2C-HAT.""" - with self._lock: - i2c_hat = self._i2c_hats.get(address) - if i2c_hat is None: - # pylint: disable=import-error,no-name-in-module - import raspihats.i2c_hats as module - constructor = getattr(module, board) - i2c_hat = constructor(address) - setattr(i2c_hat, self._CALLBACKS, {}) - - # Setting exception attribute will trigger online callbacks - # when keep alive thread starts. - setattr(i2c_hat, self._EXCEPTION, None) - - self._di_scanner.setup(i2c_hat) - self._i2c_hats[address] = i2c_hat - status_word = i2c_hat.status # read status_word to reset bits - _LOGGER.info( - log_message(self, i2c_hat, "registered", status_word)) - - def run(self): - """Keep alive for I2C-HATs.""" - # pylint: disable=import-error,no-name-in-module - from raspihats.i2c_hats import ResponseException - - _LOGGER.info(log_message(self, "starting")) - - while self._run: - with self._lock: - for i2c_hat in list(self._i2c_hats.values()): - try: - self._di_scanner.scan(i2c_hat) - self._read_status(i2c_hat) - - if hasattr(i2c_hat, self._EXCEPTION): - if getattr(i2c_hat, self._EXCEPTION) is not None: - _LOGGER.warning( - log_message(self, i2c_hat, "online again") - ) - delattr(i2c_hat, self._EXCEPTION) - # trigger online callbacks - callbacks = getattr(i2c_hat, self._CALLBACKS) - for callback in list(callbacks.values()): - callback() - except ResponseException as ex: - if not hasattr(i2c_hat, self._EXCEPTION): - _LOGGER.error( - log_message(self, i2c_hat, ex) - ) - setattr(i2c_hat, self._EXCEPTION, ex) - time.sleep(0.05) - _LOGGER.info(log_message(self, "exiting")) - - def _read_status(self, i2c_hat): - """Read I2C-HATs status.""" - status_word = i2c_hat.status - if status_word.value != 0x00: - _LOGGER.error(log_message(self, i2c_hat, status_word)) - - def start_keep_alive(self): - """Start keep alive mechanism.""" - self._run = True - threading.Thread.start(self) - - def stop_keep_alive(self): - """Stop keep alive mechanism.""" - self._run = False - self.join() - - def register_di_callback(self, address, channel, callback): - """Register I2C-HAT digital input edge callback.""" - with self._lock: - i2c_hat = self._i2c_hats[address] - self._di_scanner.register_callback(i2c_hat, channel, callback) - - def register_online_callback(self, address, channel, callback): - """Register I2C-HAT online callback.""" - with self._lock: - i2c_hat = self._i2c_hats[address] - callbacks = getattr(i2c_hat, self._CALLBACKS) - callbacks[channel] = callback - setattr(i2c_hat, self._CALLBACKS, callbacks) - - def read_di(self, address, channel): - """Read a value from a I2C-HAT digital input.""" - # pylint: disable=import-error,no-name-in-module - from raspihats.i2c_hats import ResponseException - - with self._lock: - i2c_hat = self._i2c_hats[address] - try: - value = i2c_hat.di.value - return (value >> channel) & 0x01 - except ResponseException as ex: - raise I2CHatsException(str(ex)) - - def write_dq(self, address, channel, value): - """Write a value to a I2C-HAT digital output.""" - # pylint: disable=import-error,no-name-in-module - from raspihats.i2c_hats import ResponseException - - with self._lock: - i2c_hat = self._i2c_hats[address] - try: - i2c_hat.dq.channels[channel] = value - except ResponseException as ex: - raise I2CHatsException(str(ex)) - - def read_dq(self, address, channel): - """Read a value from a I2C-HAT digital output.""" - # pylint: disable=import-error,no-name-in-module - from raspihats.i2c_hats import ResponseException - - with self._lock: - i2c_hat = self._i2c_hats[address] - try: - return i2c_hat.dq.channels[channel] - except ResponseException as ex: - raise I2CHatsException(str(ex)) diff --git a/homeassistant/components/raspihats/__init__.py b/homeassistant/components/raspihats/__init__.py new file mode 100644 index 0000000000000..69b03a36769e6 --- /dev/null +++ b/homeassistant/components/raspihats/__init__.py @@ -0,0 +1,236 @@ +"""Support for controlling raspihats boards.""" +import logging +import threading +import time + +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) + +REQUIREMENTS = ['raspihats==2.2.3', 'smbus-cffi==0.5.1'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'raspihats' + +CONF_I2C_HATS = 'i2c_hats' +CONF_BOARD = 'board' +CONF_ADDRESS = 'address' +CONF_CHANNELS = 'channels' +CONF_INDEX = 'index' +CONF_INVERT_LOGIC = 'invert_logic' +CONF_INITIAL_STATE = 'initial_state' + +I2C_HAT_NAMES = [ + 'Di16', 'Rly10', 'Di6Rly6', + 'DI16ac', 'DQ10rly', 'DQ16oc', 'DI6acDQ6rly' +] + +I2C_HATS_MANAGER = 'I2CH_MNG' + + +def setup(hass, config): + """Set up the raspihats component.""" + hass.data[I2C_HATS_MANAGER] = I2CHatsManager() + + def start_i2c_hats_keep_alive(event): + """Start I2C-HATs keep alive.""" + hass.data[I2C_HATS_MANAGER].start_keep_alive() + + def stop_i2c_hats_keep_alive(event): + """Stop I2C-HATs keep alive.""" + hass.data[I2C_HATS_MANAGER].stop_keep_alive() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_i2c_hats_keep_alive) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_i2c_hats_keep_alive) + return True + + +def log_message(source, *parts): + """Build log message.""" + message = source.__class__.__name__ + for part in parts: + message += ": " + str(part) + return message + + +class I2CHatsException(Exception): + """I2C-HATs exception.""" + + +class I2CHatsDIScanner: + """Scan Digital Inputs and fire callbacks.""" + + _DIGITAL_INPUTS = "di" + _OLD_VALUE = "old_value" + _CALLBACKS = "callbacks" + + def setup(self, i2c_hat): + """Set up the I2C-HAT instance for digital inputs scanner.""" + if hasattr(i2c_hat, self._DIGITAL_INPUTS): + digital_inputs = getattr(i2c_hat, self._DIGITAL_INPUTS) + old_value = None + # Add old value attribute + setattr(digital_inputs, self._OLD_VALUE, old_value) + # Add callbacks dict attribute {channel: callback} + setattr(digital_inputs, self._CALLBACKS, {}) + + def register_callback(self, i2c_hat, channel, callback): + """Register edge callback.""" + if hasattr(i2c_hat, self._DIGITAL_INPUTS): + digital_inputs = getattr(i2c_hat, self._DIGITAL_INPUTS) + callbacks = getattr(digital_inputs, self._CALLBACKS) + callbacks[channel] = callback + setattr(digital_inputs, self._CALLBACKS, callbacks) + + def scan(self, i2c_hat): + """Scan I2C-HATs digital inputs and fire callbacks.""" + if hasattr(i2c_hat, self._DIGITAL_INPUTS): + digital_inputs = getattr(i2c_hat, self._DIGITAL_INPUTS) + callbacks = getattr(digital_inputs, self._CALLBACKS) + old_value = getattr(digital_inputs, self._OLD_VALUE) + value = digital_inputs.value # i2c data transfer + if old_value is not None and value != old_value: + for channel in range(0, len(digital_inputs.channels)): + state = (value >> channel) & 0x01 + old_state = (old_value >> channel) & 0x01 + if state != old_state: + callback = callbacks.get(channel, None) + if callback is not None: + callback(state) + setattr(digital_inputs, self._OLD_VALUE, value) + + +class I2CHatsManager(threading.Thread): + """Manages all I2C-HATs instances.""" + + _EXCEPTION = "exception" + _CALLBACKS = "callbacks" + + def __init__(self): + """Init I2C-HATs Manager.""" + threading.Thread.__init__(self) + self._lock = threading.Lock() + self._i2c_hats = {} + self._run = False + self._di_scanner = I2CHatsDIScanner() + + def register_board(self, board, address): + """Register I2C-HAT.""" + with self._lock: + i2c_hat = self._i2c_hats.get(address) + if i2c_hat is None: + # pylint: disable=import-error,no-name-in-module + import raspihats.i2c_hats as module + constructor = getattr(module, board) + i2c_hat = constructor(address) + setattr(i2c_hat, self._CALLBACKS, {}) + + # Setting exception attribute will trigger online callbacks + # when keep alive thread starts. + setattr(i2c_hat, self._EXCEPTION, None) + + self._di_scanner.setup(i2c_hat) + self._i2c_hats[address] = i2c_hat + status_word = i2c_hat.status # read status_word to reset bits + _LOGGER.info( + log_message(self, i2c_hat, "registered", status_word)) + + def run(self): + """Keep alive for I2C-HATs.""" + # pylint: disable=import-error,no-name-in-module + from raspihats.i2c_hats import ResponseException + + _LOGGER.info(log_message(self, "starting")) + + while self._run: + with self._lock: + for i2c_hat in list(self._i2c_hats.values()): + try: + self._di_scanner.scan(i2c_hat) + self._read_status(i2c_hat) + + if hasattr(i2c_hat, self._EXCEPTION): + if getattr(i2c_hat, self._EXCEPTION) is not None: + _LOGGER.warning( + log_message(self, i2c_hat, "online again") + ) + delattr(i2c_hat, self._EXCEPTION) + # trigger online callbacks + callbacks = getattr(i2c_hat, self._CALLBACKS) + for callback in list(callbacks.values()): + callback() + except ResponseException as ex: + if not hasattr(i2c_hat, self._EXCEPTION): + _LOGGER.error( + log_message(self, i2c_hat, ex) + ) + setattr(i2c_hat, self._EXCEPTION, ex) + time.sleep(0.05) + _LOGGER.info(log_message(self, "exiting")) + + def _read_status(self, i2c_hat): + """Read I2C-HATs status.""" + status_word = i2c_hat.status + if status_word.value != 0x00: + _LOGGER.error(log_message(self, i2c_hat, status_word)) + + def start_keep_alive(self): + """Start keep alive mechanism.""" + self._run = True + threading.Thread.start(self) + + def stop_keep_alive(self): + """Stop keep alive mechanism.""" + self._run = False + self.join() + + def register_di_callback(self, address, channel, callback): + """Register I2C-HAT digital input edge callback.""" + with self._lock: + i2c_hat = self._i2c_hats[address] + self._di_scanner.register_callback(i2c_hat, channel, callback) + + def register_online_callback(self, address, channel, callback): + """Register I2C-HAT online callback.""" + with self._lock: + i2c_hat = self._i2c_hats[address] + callbacks = getattr(i2c_hat, self._CALLBACKS) + callbacks[channel] = callback + setattr(i2c_hat, self._CALLBACKS, callbacks) + + def read_di(self, address, channel): + """Read a value from a I2C-HAT digital input.""" + # pylint: disable=import-error,no-name-in-module + from raspihats.i2c_hats import ResponseException + + with self._lock: + i2c_hat = self._i2c_hats[address] + try: + value = i2c_hat.di.value + return (value >> channel) & 0x01 + except ResponseException as ex: + raise I2CHatsException(str(ex)) + + def write_dq(self, address, channel, value): + """Write a value to a I2C-HAT digital output.""" + # pylint: disable=import-error,no-name-in-module + from raspihats.i2c_hats import ResponseException + + with self._lock: + i2c_hat = self._i2c_hats[address] + try: + i2c_hat.dq.channels[channel] = value + except ResponseException as ex: + raise I2CHatsException(str(ex)) + + def read_dq(self, address, channel): + """Read a value from a I2C-HAT digital output.""" + # pylint: disable=import-error,no-name-in-module + from raspihats.i2c_hats import ResponseException + + with self._lock: + i2c_hat = self._i2c_hats[address] + try: + return i2c_hat.dq.channels[channel] + except ResponseException as ex: + raise I2CHatsException(str(ex)) diff --git a/homeassistant/components/raspihats/binary_sensor.py b/homeassistant/components/raspihats/binary_sensor.py new file mode 100644 index 0000000000000..04885402e722a --- /dev/null +++ b/homeassistant/components/raspihats/binary_sensor.py @@ -0,0 +1,114 @@ +"""Support for raspihats board binary sensors.""" +import logging + +import voluptuous as vol + +from homeassistant.components.binary_sensor import ( + PLATFORM_SCHEMA, BinarySensorDevice) +from homeassistant.components.raspihats import ( + CONF_ADDRESS, CONF_BOARD, CONF_CHANNELS, CONF_I2C_HATS, CONF_INDEX, + CONF_INVERT_LOGIC, I2C_HAT_NAMES, I2C_HATS_MANAGER, I2CHatsException) +from homeassistant.const import ( + CONF_DEVICE_CLASS, CONF_NAME, DEVICE_DEFAULT_NAME) +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['raspihats'] + +DEFAULT_INVERT_LOGIC = False +DEFAULT_DEVICE_CLASS = None + +_CHANNELS_SCHEMA = vol.Schema([{ + vol.Required(CONF_INDEX): cv.positive_int, + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, + vol.Optional(CONF_DEVICE_CLASS, default=DEFAULT_DEVICE_CLASS): cv.string, +}]) + +_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, +}]) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_I2C_HATS): _I2C_HATS_SCHEMA, +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the raspihats binary_sensor devices.""" + I2CHatBinarySensor.I2C_HATS_MANAGER = hass.data[I2C_HATS_MANAGER] + binary_sensors = [] + i2c_hat_configs = config.get(CONF_I2C_HATS) + for i2c_hat_config in i2c_hat_configs: + address = i2c_hat_config[CONF_ADDRESS] + board = i2c_hat_config[CONF_BOARD] + try: + I2CHatBinarySensor.I2C_HATS_MANAGER.register_board(board, address) + for channel_config in i2c_hat_config[CONF_CHANNELS]: + binary_sensors.append( + I2CHatBinarySensor( + address, + channel_config[CONF_INDEX], + channel_config[CONF_NAME], + channel_config[CONF_INVERT_LOGIC], + channel_config[CONF_DEVICE_CLASS] + ) + ) + except I2CHatsException as ex: + _LOGGER.error("Failed to register %s I2CHat@%s %s", + board, hex(address), str(ex)) + add_entities(binary_sensors) + + +class I2CHatBinarySensor(BinarySensorDevice): + """Representation of a binary sensor that uses a I2C-HAT digital input.""" + + I2C_HATS_MANAGER = None + + def __init__(self, address, channel, name, invert_logic, device_class): + """Initialize the raspihats sensor.""" + self._address = address + self._channel = channel + self._name = name or DEVICE_DEFAULT_NAME + self._invert_logic = invert_logic + self._device_class = device_class + self._state = self.I2C_HATS_MANAGER.read_di( + self._address, self._channel) + + def online_callback(): + """Call fired when board is online.""" + self.schedule_update_ha_state() + + self.I2C_HATS_MANAGER.register_online_callback( + self._address, self._channel, online_callback) + + def edge_callback(state): + """Read digital input state.""" + self._state = state + self.schedule_update_ha_state() + + self.I2C_HATS_MANAGER.register_di_callback( + self._address, self._channel, edge_callback) + + @property + def device_class(self): + """Return the class of this sensor.""" + return self._device_class + + @property + def name(self): + """Return the name of this sensor.""" + return self._name + + @property + def should_poll(self): + """No polling needed for this sensor.""" + return False + + @property + def is_on(self): + """Return the state of this sensor.""" + return self._state != self._invert_logic diff --git a/homeassistant/components/raspihats/switch.py b/homeassistant/components/raspihats/switch.py new file mode 100644 index 0000000000000..10bb2f748c4a1 --- /dev/null +++ b/homeassistant/components/raspihats/switch.py @@ -0,0 +1,135 @@ +"""Support for raspihats board switches.""" +import logging + +import voluptuous as vol + +from homeassistant.components.raspihats import ( + CONF_ADDRESS, CONF_BOARD, CONF_CHANNELS, CONF_I2C_HATS, CONF_INDEX, + CONF_INITIAL_STATE, CONF_INVERT_LOGIC, I2C_HAT_NAMES, I2C_HATS_MANAGER, + I2CHatsException) +from homeassistant.components.switch import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import ToggleEntity + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['raspihats'] + +_CHANNELS_SCHEMA = vol.Schema([{ + vol.Required(CONF_INDEX): cv.positive_int, + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_INVERT_LOGIC, default=False): cv.boolean, + vol.Optional(CONF_INITIAL_STATE): cv.boolean, +}]) + +_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, +}]) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_I2C_HATS): _I2C_HATS_SCHEMA, +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the raspihats switch devices.""" + I2CHatSwitch.I2C_HATS_MANAGER = hass.data[I2C_HATS_MANAGER] + switches = [] + i2c_hat_configs = config.get(CONF_I2C_HATS) + for i2c_hat_config in i2c_hat_configs: + board = i2c_hat_config[CONF_BOARD] + address = i2c_hat_config[CONF_ADDRESS] + try: + I2CHatSwitch.I2C_HATS_MANAGER.register_board(board, address) + for channel_config in i2c_hat_config[CONF_CHANNELS]: + switches.append( + I2CHatSwitch( + board, address, channel_config[CONF_INDEX], + channel_config[CONF_NAME], + channel_config[CONF_INVERT_LOGIC], + channel_config.get(CONF_INITIAL_STATE) + ) + ) + except I2CHatsException as ex: + _LOGGER.error( + "Failed to register %s I2CHat@%s %s", board, hex(address), + str(ex)) + add_entities(switches) + + +class I2CHatSwitch(ToggleEntity): + """Representation a switch that uses a I2C-HAT digital output.""" + + I2C_HATS_MANAGER = None + + def __init__(self, board, address, channel, name, invert_logic, + initial_state): + """Initialize switch.""" + self._board = board + self._address = address + self._channel = channel + self._name = name or DEVICE_DEFAULT_NAME + self._invert_logic = invert_logic + if initial_state is not None: + if self._invert_logic: + state = not initial_state + else: + state = initial_state + self.I2C_HATS_MANAGER.write_dq( + self._address, self._channel, state) + + def online_callback(): + """Call fired when board is online.""" + self.schedule_update_ha_state() + + self.I2C_HATS_MANAGER.register_online_callback( + self._address, self._channel, online_callback) + + def _log_message(self, message): + """Create log message.""" + string = self._name + " " + string += self._board + "I2CHat@" + hex(self._address) + " " + string += "channel:" + str(self._channel) + message + return string + + @property + def name(self): + """Return the name of the switch.""" + return self._name + + @property + def should_poll(self): + """Return the polling state.""" + return False + + @property + def is_on(self): + """Return true if device is on.""" + try: + state = self.I2C_HATS_MANAGER.read_dq(self._address, self._channel) + return state != self._invert_logic + except I2CHatsException as ex: + _LOGGER.error(self._log_message("Is ON check failed, " + str(ex))) + return False + + def turn_on(self, **kwargs): + """Turn the device on.""" + try: + state = self._invert_logic is False + self.I2C_HATS_MANAGER.write_dq(self._address, self._channel, state) + self.schedule_update_ha_state() + except I2CHatsException as ex: + _LOGGER.error(self._log_message("Turn ON failed, " + str(ex))) + + def turn_off(self, **kwargs): + """Turn the device off.""" + try: + state = self._invert_logic is not False + self.I2C_HATS_MANAGER.write_dq(self._address, self._channel, state) + self.schedule_update_ha_state() + except I2CHatsException as ex: + _LOGGER.error( + self._log_message("Turn OFF failed:, " + str(ex))) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index eb97d197e3ed1..9b852b4a00a1e 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 .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 a94e8e95c6f6d..82619e35a0ebc 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 162cb41d92e17..b792359603989 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 @@ 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/remote/apple_tv.py b/homeassistant/components/remote/apple_tv.py deleted file mode 100644 index 72696143bfedc..0000000000000 --- a/homeassistant/components/remote/apple_tv.py +++ /dev/null @@ -1,87 +0,0 @@ -""" -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/ -""" - -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): - """Set up the Apple TV remote platform.""" - if not discovery_info: - return - - name = discovery_info[CONF_NAME] - host = discovery_info[CONF_HOST] - atv = hass.data[DATA_APPLE_TV][host][ATTR_ATV] - power = hass.data[DATA_APPLE_TV][host][ATTR_POWER] - async_add_entities([AppleTVRemote(atv, power, name)]) - - -class AppleTVRemote(remote.RemoteDevice): - """Device that sends commands to an Apple TV.""" - - def __init__(self, atv, power, name): - """Initialize device.""" - self._atv = atv - self._name = name - self._power = power - self._power.listeners.append(self) - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def unique_id(self): - """Return a unique ID.""" - return self._atv.metadata.device_id - - @property - def is_on(self): - """Return true if device is on.""" - return self._power.turned_on - - @property - def should_poll(self): - """No polling needed for Apple TV.""" - return False - - async def async_turn_on(self, **kwargs): - """Turn the device on. - - This method is a coroutine. - """ - self._power.set_power_on(True) - - async def async_turn_off(self, **kwargs): - """Turn the device off. - - This method is a coroutine. - """ - self._power.set_power_on(False) - - def async_send_command(self, command, **kwargs): - """Send a command to one device. - - This method must be run in the event loop and returns a coroutine. - """ - # Send commands in specified order but schedule only one coroutine - async def _send_commands(): - for single_command in command: - if not hasattr(self._atv.remote_control, single_command): - continue - - await getattr(self._atv.remote_control, single_command)() - - return _send_commands() diff --git a/homeassistant/components/remote/harmony.py b/homeassistant/components/remote/harmony.py deleted file mode 100644 index a5e4f5a8528b5..0000000000000 --- a/homeassistant/components/remote/harmony.py +++ /dev/null @@ -1,422 +0,0 @@ -""" -Support for Harmony Hub devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/remote.harmony/ -""" -import asyncio -import json -import logging - -import voluptuous as vol - -from homeassistant.components import remote -from homeassistant.components.remote import ( - ATTR_ACTIVITY, ATTR_DELAY_SECS, ATTR_DEVICE, ATTR_NUM_REPEATS, - DEFAULT_DELAY_SECS, DOMAIN, PLATFORM_SCHEMA -) -from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_STOP -) -import homeassistant.helpers.config_validation as cv -from homeassistant.exceptions import PlatformNotReady -from homeassistant.util import slugify - -REQUIREMENTS = ['aioharmony==0.1.5'] - -_LOGGER = logging.getLogger(__name__) - -ATTR_CHANNEL = 'channel' -ATTR_CURRENT_ACTIVITY = 'current_activity' - -DEFAULT_PORT = 8088 -DEVICES = [] -CONF_DEVICE_CACHE = 'harmony_device_cache' - -SERVICE_SYNC = 'harmony_sync' -SERVICE_CHANGE_CHANNEL = 'harmony_change_channel' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(ATTR_ACTIVITY): cv.string, - vol.Required(CONF_NAME): cv.string, - vol.Optional(ATTR_DELAY_SECS, default=DEFAULT_DELAY_SECS): - vol.Coerce(float), - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, -}) - -HARMONY_SYNC_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, -}) - -HARMONY_CHANGE_CHANNEL_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_CHANNEL): cv.positive_int -}) - - -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up the Harmony platform.""" - activity = None - - if CONF_DEVICE_CACHE not in hass.data: - hass.data[CONF_DEVICE_CACHE] = [] - - if discovery_info: - # Find the discovered device in the list of user configurations - override = next((c for c in hass.data[CONF_DEVICE_CACHE] - if c.get(CONF_NAME) == discovery_info.get(CONF_NAME)), - None) - - port = DEFAULT_PORT - delay_secs = DEFAULT_DELAY_SECS - if override is not None: - activity = override.get(ATTR_ACTIVITY) - delay_secs = override.get(ATTR_DELAY_SECS) - port = override.get(CONF_PORT, DEFAULT_PORT) - - host = ( - discovery_info.get(CONF_NAME), - discovery_info.get(CONF_HOST), - port) - - # Ignore hub name when checking if this hub is known - ip and port only - if host[1:] in ((h.host, h.port) for h in DEVICES): - _LOGGER.debug("Discovered host already known: %s", host) - return - elif CONF_HOST in config: - host = ( - config.get(CONF_NAME), - config.get(CONF_HOST), - config.get(CONF_PORT), - ) - activity = config.get(ATTR_ACTIVITY) - delay_secs = config.get(ATTR_DELAY_SECS) - else: - hass.data[CONF_DEVICE_CACHE].append(config) - return - - name, address, port = host - _LOGGER.info("Loading Harmony Platform: %s at %s:%s, startup activity: %s", - name, address, port, activity) - - harmony_conf_file = hass.config.path( - '{}{}{}'.format('harmony_', slugify(name), '.conf')) - try: - device = HarmonyRemote( - name, address, port, activity, harmony_conf_file, delay_secs) - if not await device.connect(): - raise PlatformNotReady - - DEVICES.append(device) - async_add_entities([device]) - register_services(hass) - except (ValueError, AttributeError): - raise PlatformNotReady - - -def register_services(hass): - """Register all services for harmony devices.""" - hass.services.async_register( - DOMAIN, SERVICE_SYNC, _sync_service, - schema=HARMONY_SYNC_SCHEMA) - - hass.services.async_register( - DOMAIN, SERVICE_CHANGE_CHANNEL, _change_channel_service, - schema=HARMONY_CHANGE_CHANNEL_SCHEMA) - - -async def _apply_service(service, service_func, *service_func_args): - """Handle services to apply.""" - entity_ids = service.data.get('entity_id') - - if entity_ids: - _devices = [device for device in DEVICES - if device.entity_id in entity_ids] - else: - _devices = DEVICES - - for device in _devices: - await service_func(device, *service_func_args) - - -async def _sync_service(service): - await _apply_service(service, HarmonyRemote.sync) - - -async def _change_channel_service(service): - channel = service.data.get(ATTR_CHANNEL) - await _apply_service(service, HarmonyRemote.change_channel, channel) - - -class HarmonyRemote(remote.RemoteDevice): - """Remote representation used to control a Harmony device.""" - - def __init__(self, name, host, port, activity, out_path, delay_secs): - """Initialize HarmonyRemote class.""" - from aioharmony.harmonyapi import HarmonyAPI as HarmonyClient - - self._name = name - self.host = host - self.port = port - self._state = None - self._current_activity = None - self._default_activity = activity - self._client = HarmonyClient(ip_address=host) - self._config_path = out_path - self._delay_secs = delay_secs - self._available = False - - async def async_added_to_hass(self): - """Complete the initialization.""" - from aioharmony.harmonyapi import ClientCallbackType - - _LOGGER.debug("%s: Harmony Hub added", self._name) - # Register the callbacks - self._client.callbacks = ClientCallbackType( - new_activity=self.new_activity, - config_updated=self.new_config, - connect=self.got_connected, - disconnect=self.got_disconnected - ) - - # Store Harmony HUB config, this will also update our current - # activity - await self.new_config() - - import aioharmony.exceptions as aioexc - - async def shutdown(_): - """Close connection on shutdown.""" - _LOGGER.debug("%s: Closing Harmony Hub", self._name) - try: - await self._client.close() - except aioexc.TimeOut: - _LOGGER.warning("%s: Disconnect timed-out", self._name) - - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) - - @property - def name(self): - """Return the Harmony device's name.""" - return self._name - - @property - def should_poll(self): - """Return the fact that we should not be polled.""" - return False - - @property - def device_state_attributes(self): - """Add platform specific attributes.""" - return {ATTR_CURRENT_ACTIVITY: self._current_activity} - - @property - def is_on(self): - """Return False if PowerOff is the current activity, otherwise True.""" - return self._current_activity not in [None, 'PowerOff'] - - @property - def available(self): - """Return True if connected to Hub, otherwise False.""" - return self._available - - async def connect(self): - """Connect to the Harmony HUB.""" - import aioharmony.exceptions as aioexc - - _LOGGER.debug("%s: Connecting", self._name) - try: - if not await self._client.connect(): - _LOGGER.warning("%s: Unable to connect to HUB.", self._name) - await self._client.close() - return False - except aioexc.TimeOut: - _LOGGER.warning("%s: Connection timed-out", self._name) - return False - - return True - - def new_activity(self, activity_info: tuple) -> None: - """Call for updating the current activity.""" - activity_id, activity_name = activity_info - _LOGGER.debug("%s: activity reported as: %s", self._name, - activity_name) - self._current_activity = activity_name - self._state = bool(activity_id != -1) - self._available = True - self.async_schedule_update_ha_state() - - async def new_config(self, _=None): - """Call for updating the current activity.""" - _LOGGER.debug("%s: configuration has been updated", self._name) - self.new_activity(self._client.current_activity) - await self.hass.async_add_executor_job(self.write_config_file) - - async def got_connected(self, _=None): - """Notification that we're connected to the HUB.""" - _LOGGER.debug("%s: connected to the HUB.", self._name) - if not self._available: - # We were disconnected before. - await self.new_config() - - async def got_disconnected(self, _=None): - """Notification that we're disconnected from the HUB.""" - _LOGGER.debug("%s: disconnected from the HUB.", self._name) - self._available = False - # We're going to wait for 10 seconds before announcing we're - # unavailable, this to allow a reconnection to happen. - await asyncio.sleep(10) - - if not self._available: - # Still disconnected. Let the state engine know. - self.async_schedule_update_ha_state() - - async def async_turn_on(self, **kwargs): - """Start an activity from the Harmony device.""" - import aioharmony.exceptions as aioexc - - _LOGGER.debug("%s: Turn On", self.name) - - activity = kwargs.get(ATTR_ACTIVITY, self._default_activity) - - if activity: - activity_id = None - if activity.isdigit() or activity == '-1': - _LOGGER.debug("%s: Activity is numeric", self.name) - if self._client.get_activity_name(int(activity)): - activity_id = activity - - if activity_id is None: - _LOGGER.debug("%s: Find activity ID based on name", self.name) - activity_id = self._client.get_activity_id( - str(activity).strip()) - - if activity_id is None: - _LOGGER.error("%s: Activity %s is invalid", - self.name, activity) - return - - try: - await self._client.start_activity(activity_id) - except aioexc.TimeOut: - _LOGGER.error("%s: Starting activity %s timed-out", - self.name, - activity) - else: - _LOGGER.error("%s: No activity specified with turn_on service", - self.name) - - async def async_turn_off(self, **kwargs): - """Start the PowerOff activity.""" - import aioharmony.exceptions as aioexc - _LOGGER.debug("%s: Turn Off", self.name) - try: - await self._client.power_off() - except aioexc.TimeOut: - _LOGGER.error("%s: Powering off timed-out", self.name) - - # pylint: disable=arguments-differ - async def async_send_command(self, command, **kwargs): - """Send a list of commands to one device.""" - from aioharmony.harmonyapi import SendCommandDevice - import aioharmony.exceptions as aioexc - - _LOGGER.debug("%s: Send Command", self.name) - device = kwargs.get(ATTR_DEVICE) - if device is None: - _LOGGER.error("%s: Missing required argument: device", self.name) - return - - device_id = None - if device.isdigit(): - _LOGGER.debug("%s: Device %s is numeric", - self.name, device) - if self._client.get_device_name(int(device)): - device_id = device - - if device_id is None: - _LOGGER.debug("%s: Find device ID %s based on device name", - self.name, device) - device_id = self._client.get_device_id(str(device).strip()) - - if device_id is None: - _LOGGER.error("%s: Device %s is invalid", self.name, device) - return - - num_repeats = kwargs.get(ATTR_NUM_REPEATS) - delay_secs = kwargs.get(ATTR_DELAY_SECS, self._delay_secs) - - # Creating list of commands to send. - snd_cmnd_list = [] - for _ in range(num_repeats): - for single_command in command: - send_command = SendCommandDevice( - device=device_id, - command=single_command, - delay=0 - ) - snd_cmnd_list.append(send_command) - if delay_secs > 0: - snd_cmnd_list.append(float(delay_secs)) - - _LOGGER.debug("%s: Sending commands", self.name) - try: - result_list = await self._client.send_commands(snd_cmnd_list) - except aioexc.TimeOut: - _LOGGER.error("%s: Sending commands timed-out", self.name) - return - - for result in result_list: - _LOGGER.error("Sending command %s to device %s failed with code " - "%s: %s", - result.command.command, - result.command.device, - result.code, - result.msg - ) - - async def change_channel(self, channel): - """Change the channel using Harmony remote.""" - import aioharmony.exceptions as aioexc - - _LOGGER.debug("%s: Changing channel to %s", - self.name, channel) - try: - await self._client.change_channel(channel) - except aioexc.TimeOut: - _LOGGER.error("%s: Changing channel to %s timed-out", - self.name, - channel) - - async def sync(self): - """Sync the Harmony device with the web service.""" - import aioharmony.exceptions as aioexc - - _LOGGER.debug("%s: Syncing hub with Harmony cloud", self.name) - try: - await self._client.sync() - except aioexc.TimeOut: - _LOGGER.error("%s: Syncing hub with Harmony cloud timed-out", - self.name) - else: - await self.hass.async_add_executor_job(self.write_config_file) - - def write_config_file(self): - """Write Harmony configuration file.""" - _LOGGER.debug("%s: Writing hub config to file: %s", - self.name, - self._config_path) - if self._client.config is None: - _LOGGER.warning("%s: No configuration received from hub", - self.name) - return - - try: - with open(self._config_path, 'w+', encoding='utf-8') as file_out: - json.dump(self._client.json_config, file_out, - sort_keys=True, indent=4) - except IOError as exc: - _LOGGER.error("%s: Unable to write HUB configuration to %s: %s", - self.name, self._config_path, exc) diff --git a/homeassistant/components/remote/itach.py b/homeassistant/components/remote/itach.py deleted file mode 100644 index e7f23dfcd13ea..0000000000000 --- a/homeassistant/components/remote/itach.py +++ /dev/null @@ -1,116 +0,0 @@ -""" -Support for iTach IR Devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/remote.itach/ -""" - -import logging - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components import remote -from homeassistant.const import ( - DEVICE_DEFAULT_NAME, CONF_NAME, CONF_MAC, CONF_HOST, CONF_PORT, - CONF_DEVICES) -from homeassistant.components.remote import PLATFORM_SCHEMA - -REQUIREMENTS = ['pyitachip2ir==0.0.7'] - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_PORT = 4998 -CONNECT_TIMEOUT = 5000 - -CONF_MODADDR = 'modaddr' -CONF_CONNADDR = 'connaddr' -CONF_COMMANDS = 'commands' -CONF_DATA = 'data' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_MAC): cv.string, - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [{ - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_MODADDR): vol.Coerce(int), - 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 - }]) - }]) -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the ITach connection and devices.""" - import pyitachip2ir - itachip2ir = pyitachip2ir.ITachIP2IR( - config.get(CONF_MAC), config.get(CONF_HOST), - int(config.get(CONF_PORT))) - - if not itachip2ir.ready(CONNECT_TIMEOUT): - _LOGGER.error("Unable to find iTach") - return False - - devices = [] - for data in config.get(CONF_DEVICES): - name = data.get(CONF_NAME) - modaddr = int(data.get(CONF_MODADDR, 1)) - connaddr = int(data.get(CONF_CONNADDR, 1)) - cmddatas = "" - for cmd in data.get(CONF_COMMANDS): - cmdname = cmd[CONF_NAME].strip() - if not cmdname: - cmdname = '""' - cmddata = cmd[CONF_DATA].strip() - if not cmddata: - cmddata = '""' - cmddatas += "{}\n{}\n".format(cmdname, cmddata) - itachip2ir.addDevice(name, modaddr, connaddr, cmddatas) - devices.append(ITachIP2IRRemote(itachip2ir, name)) - add_entities(devices, True) - return True - - -class ITachIP2IRRemote(remote.RemoteDevice): - """Device that sends commands to an ITachIP2IR device.""" - - def __init__(self, itachip2ir, name): - """Initialize device.""" - self.itachip2ir = itachip2ir - self._power = False - self._name = name or DEVICE_DEFAULT_NAME - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def is_on(self): - """Return true if device is on.""" - return self._power - - def turn_on(self, **kwargs): - """Turn the device on.""" - self._power = True - self.itachip2ir.send(self._name, "ON", 1) - self.schedule_update_ha_state() - - def turn_off(self, **kwargs): - """Turn the device off.""" - self._power = False - self.itachip2ir.send(self._name, "OFF", 1) - self.schedule_update_ha_state() - - def send_command(self, command, **kwargs): - """Send a command to one device.""" - for single_command in command: - self.itachip2ir.send(self._name, single_command, 1) - - def update(self): - """Update the device.""" - self.itachip2ir.update() diff --git a/homeassistant/components/remote/kira.py b/homeassistant/components/remote/kira.py deleted file mode 100644 index 24fc54ee78c5b..0000000000000 --- a/homeassistant/components/remote/kira.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -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/ -""" -import functools as ft -import logging - -from homeassistant.components import remote -from homeassistant.const import CONF_DEVICE, CONF_NAME -from homeassistant.helpers.entity import Entity - -DOMAIN = 'kira' - -_LOGGER = logging.getLogger(__name__) - -CONF_REMOTE = "remote" - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Kira platform.""" - if discovery_info: - name = discovery_info.get(CONF_NAME) - device = discovery_info.get(CONF_DEVICE) - - kira = hass.data[DOMAIN][CONF_REMOTE][name] - add_entities([KiraRemote(device, kira)]) - return True - - -class KiraRemote(Entity): - """Remote representation used to send commands to a Kira device.""" - - def __init__(self, name, kira): - """Initialize KiraRemote class.""" - _LOGGER.debug("KiraRemote device init started for: %s", name) - self._name = name - self._kira = kira - - @property - def name(self): - """Return the Kira device's name.""" - return self._name - - def update(self): - """No-op.""" - - def send_command(self, command, **kwargs): - """Send a command to one device.""" - for single_command in command: - code_tuple = (single_command, - kwargs.get(remote.ATTR_DEVICE)) - _LOGGER.info("Sending Command: %s to %s", *code_tuple) - self._kira.sendCode(code_tuple) - - def async_send_command(self, command, **kwargs): - """Send a command to a device. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(ft.partial( - self.send_command, command, **kwargs)) diff --git a/homeassistant/components/remote/roku.py b/homeassistant/components/remote/roku.py deleted file mode 100644 index 86a7105dafedd..0000000000000 --- a/homeassistant/components/remote/roku.py +++ /dev/null @@ -1,72 +0,0 @@ -""" -Support for the Roku remote. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/remote.roku/ -""" -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): - """Set up the Roku remote platform.""" - if not discovery_info: - return - - host = discovery_info[CONF_HOST] - async_add_entities([RokuRemote(host)], True) - - -class RokuRemote(remote.RemoteDevice): - """Device that sends commands to an Roku.""" - - def __init__(self, host): - """Initialize the Roku device.""" - from roku import Roku - - self.roku = Roku(host) - self._device_info = {} - - def update(self): - """Retrieve latest state.""" - try: - self._device_info = self.roku.device_info - except (requests.exceptions.ConnectionError, - requests.exceptions.ReadTimeout): - pass - - @property - def name(self): - """Return the name of the device.""" - if self._device_info.userdevicename: - return self._device_info.userdevicename - return "Roku {}".format(self._device_info.sernum) - - @property - def unique_id(self): - """Return a unique ID.""" - return self._device_info.sernum - - @property - def is_on(self): - """Return true if device is on.""" - return True - - @property - def should_poll(self): - """No polling needed for Roku.""" - return False - - def send_command(self, command, **kwargs): - """Send a command to one device.""" - for single_command in command: - if not hasattr(self.roku, single_command): - continue - - getattr(self.roku, single_command)() diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py deleted file mode 100644 index c8ffd04332145..0000000000000 --- a/homeassistant/components/remote/xiaomi_miio.py +++ /dev/null @@ -1,266 +0,0 @@ -""" -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/ -""" -import asyncio -import logging -import time - -from datetime import timedelta - -import voluptuous as vol - -from homeassistant.components.remote import ( - PLATFORM_SCHEMA, DOMAIN, ATTR_NUM_REPEATS, ATTR_DELAY_SECS, - DEFAULT_DELAY_SECS, RemoteDevice) -from homeassistant.const import ( - CONF_NAME, CONF_HOST, CONF_TOKEN, CONF_TIMEOUT, - ATTR_ENTITY_ID, ATTR_HIDDEN, CONF_COMMAND) -from homeassistant.exceptions import PlatformNotReady -import homeassistant.helpers.config_validation as cv -from homeassistant.util.dt import utcnow - -REQUIREMENTS = ['python-miio==0.4.4', 'construct==2.9.45'] - -_LOGGER = logging.getLogger(__name__) - -SERVICE_LEARN = 'xiaomi_miio_learn_command' -DATA_KEY = 'remote.xiaomi_miio' - -CONF_SLOT = 'slot' -CONF_COMMANDS = 'commands' - -DEFAULT_TIMEOUT = 10 -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_SLOT, default=1): - vol.All(int, vol.Range(min=1, max=1000000)), -}) - -COMMAND_SCHEMA = vol.Schema({ - vol.Required(CONF_COMMAND): vol.All(cv.ensure_list, [cv.string]) - }) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME): cv.string, - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): - vol.All(int, vol.Range(min=0)), - vol.Optional(CONF_SLOT, default=DEFAULT_SLOT): - vol.All(int, vol.Range(min=1, max=1000000)), - vol.Optional(ATTR_HIDDEN, default=True): cv.boolean, - vol.Required(CONF_TOKEN): vol.All(str, vol.Length(min=32, max=32)), - vol.Optional(CONF_COMMANDS, default={}): - cv.schema_with_slug_keys(COMMAND_SCHEMA), -}, extra=vol.ALLOW_EXTRA) - - -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up the Xiaomi IR Remote (Chuangmi IR) platform.""" - from miio import ChuangmiIr, DeviceException - - host = config.get(CONF_HOST) - token = config.get(CONF_TOKEN) - - # Create handler - _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) - - # The Chuang Mi IR Remote Controller wants to be re-discovered every - # 5 minutes. As long as polling is disabled the device should be - # re-discovered (lazy_discover=False) in front of every command. - device = ChuangmiIr(host, token, lazy_discover=False) - - # Check that we can communicate with device. - try: - device_info = device.info() - model = device_info.model - unique_id = "{}-{}".format(model, device_info.mac_address) - _LOGGER.info("%s %s %s detected", - model, - device_info.firmware_version, - device_info.hardware_version) - except DeviceException as ex: - _LOGGER.error("Device unavailable or token incorrect: %s", ex) - raise PlatformNotReady - - if DATA_KEY not in hass.data: - hass.data[DATA_KEY] = {} - - friendly_name = config.get(CONF_NAME, "xiaomi_miio_" + - host.replace('.', '_')) - slot = config.get(CONF_SLOT) - timeout = config.get(CONF_TIMEOUT) - - hidden = config.get(ATTR_HIDDEN) - - xiaomi_miio_remote = XiaomiMiioRemote(friendly_name, device, unique_id, - slot, timeout, hidden, - config.get(CONF_COMMANDS)) - - hass.data[DATA_KEY][host] = xiaomi_miio_remote - - async_add_entities([xiaomi_miio_remote]) - - async def async_service_handler(service): - """Handle a learn command.""" - if service.service != SERVICE_LEARN: - _LOGGER.error("We should not handle service: %s", service.service) - return - - entity_id = service.data.get(ATTR_ENTITY_ID) - entity = None - for remote in hass.data[DATA_KEY].values(): - if remote.entity_id == entity_id: - entity = remote - - if not entity: - _LOGGER.error("entity_id: '%s' not found", entity_id) - return - - device = entity.device - - slot = service.data.get(CONF_SLOT, entity.slot) - - await hass.async_add_executor_job(device.learn, slot) - - timeout = service.data.get(CONF_TIMEOUT, entity.timeout) - - _LOGGER.info("Press the key you want Home Assistant to learn") - start_time = utcnow() - while (utcnow() - start_time) < timedelta(seconds=timeout): - message = await hass.async_add_executor_job( - device.read, slot) - _LOGGER.debug("Message received from device: '%s'", message) - - if 'code' in message and message['code']: - log_msg = "Received command is: {}".format(message['code']) - _LOGGER.info(log_msg) - hass.components.persistent_notification.async_create( - log_msg, title='Xiaomi Miio Remote') - return - - if ('error' in message and - message['error']['message'] == "learn timeout"): - await hass.async_add_executor_job(device.learn, slot) - - await asyncio.sleep(1, loop=hass.loop) - - _LOGGER.error("Timeout. No infrared command captured") - hass.components.persistent_notification.async_create( - "Timeout. No infrared command captured", - title='Xiaomi Miio Remote') - - hass.services.async_register(DOMAIN, SERVICE_LEARN, async_service_handler, - schema=LEARN_COMMAND_SCHEMA) - - -class XiaomiMiioRemote(RemoteDevice): - """Representation of a Xiaomi Miio Remote device.""" - - def __init__(self, friendly_name, device, unique_id, - slot, timeout, hidden, commands): - """Initialize the remote.""" - self._name = friendly_name - self._device = device - self._unique_id = unique_id - self._is_hidden = hidden - self._slot = slot - self._timeout = timeout - self._state = False - self._commands = commands - - @property - def unique_id(self): - """Return an unique ID.""" - return self._unique_id - - @property - def name(self): - """Return the name of the remote.""" - return self._name - - @property - def device(self): - """Return the remote object.""" - return self._device - - @property - def hidden(self): - """Return if we should hide entity.""" - return self._is_hidden - - @property - def slot(self): - """Return the slot to save learned command.""" - return self._slot - - @property - def timeout(self): - """Return the timeout for learning command.""" - return self._timeout - - @property - def is_on(self): - """Return False if device is unreachable, else True.""" - from miio import DeviceException - try: - self.device.info() - return True - except DeviceException: - return False - - @property - def should_poll(self): - """We should not be polled for device up state.""" - return False - - @property - def device_state_attributes(self): - """Hide remote by default.""" - if self._is_hidden: - return {'hidden': 'true'} - return - - async def async_turn_on(self, **kwargs): - """Turn the device on.""" - _LOGGER.error("Device does not support turn_on, " - "please use 'remote.send_command' to send commands.") - - async def async_turn_off(self, **kwargs): - """Turn the device off.""" - _LOGGER.error("Device does not support turn_off, " - "please use 'remote.send_command' to send commands.") - - def _send_command(self, payload): - """Send a command.""" - from miio import DeviceException - - _LOGGER.debug("Sending payload: '%s'", payload) - try: - self.device.play(payload) - except DeviceException as ex: - _LOGGER.error( - "Transmit of IR command failed, %s, exception: %s", - payload, ex) - - def send_command(self, command, **kwargs): - """Send a command.""" - num_repeats = kwargs.get(ATTR_NUM_REPEATS) - - delay = kwargs.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS) - - for _ in range(num_repeats): - for payload in command: - if payload in self._commands: - for local_payload in self._commands[payload][CONF_COMMAND]: - self._send_command(local_payload) - else: - self._send_command(payload) - time.sleep(delay) diff --git a/homeassistant/components/rest_command.py b/homeassistant/components/rest_command.py deleted file mode 100644 index c1ccc73b81c2d..0000000000000 --- a/homeassistant/components/rest_command.py +++ /dev/null @@ -1,126 +0,0 @@ -""" -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/ -""" -import asyncio -import logging - -import aiohttp -from aiohttp import hdrs -import async_timeout -import voluptuous as vol - -from homeassistant.const import ( - CONF_TIMEOUT, CONF_USERNAME, CONF_PASSWORD, CONF_URL, CONF_PAYLOAD, - CONF_METHOD, CONF_HEADERS, CONF_VERIFY_SSL) -from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv - -DOMAIN = 'rest_command' - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_TIMEOUT = 10 -DEFAULT_METHOD = 'get' -DEFAULT_VERIFY_SSL = True - -SUPPORT_REST_METHODS = [ - 'get', - 'post', - 'put', - 'delete', -] - -CONF_CONTENT_TYPE = 'content_type' - -COMMAND_SCHEMA = vol.Schema({ - vol.Required(CONF_URL): cv.template, - vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): - vol.All(vol.Lower, vol.In(SUPPORT_REST_METHODS)), - vol.Optional(CONF_HEADERS): vol.Schema({cv.string: cv.string}), - vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string, - vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string, - vol.Optional(CONF_PAYLOAD): cv.template, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.Coerce(int), - vol.Optional(CONF_CONTENT_TYPE): cv.string, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, -}) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: cv.schema_with_slug_keys(COMMAND_SCHEMA), -}, extra=vol.ALLOW_EXTRA) - - -async def async_setup(hass, config): - """Set up the REST command component.""" - def async_register_rest_command(name, command_config): - """Create service for rest command.""" - websession = async_get_clientsession( - hass, - command_config.get(CONF_VERIFY_SSL) - ) - timeout = command_config[CONF_TIMEOUT] - method = command_config[CONF_METHOD] - - template_url = command_config[CONF_URL] - template_url.hass = hass - - auth = None - if CONF_USERNAME in command_config: - username = command_config[CONF_USERNAME] - password = command_config.get(CONF_PASSWORD, '') - auth = aiohttp.BasicAuth(username, password=password) - - template_payload = None - if CONF_PAYLOAD in command_config: - template_payload = command_config[CONF_PAYLOAD] - template_payload.hass = hass - - headers = None - if CONF_HEADERS in command_config: - headers = command_config[CONF_HEADERS] - - if CONF_CONTENT_TYPE in command_config: - content_type = command_config[CONF_CONTENT_TYPE] - if headers is None: - headers = {} - headers[hdrs.CONTENT_TYPE] = content_type - - async def async_service_handler(service): - """Execute a shell command service.""" - payload = None - if template_payload: - payload = bytes( - template_payload.async_render(variables=service.data), - 'utf-8') - - try: - with async_timeout.timeout(timeout, loop=hass.loop): - request = await getattr(websession, method)( - template_url.async_render(variables=service.data), - data=payload, - auth=auth, - headers=headers - ) - - if request.status < 400: - _LOGGER.info("Success call %s.", request.url) - else: - _LOGGER.warning( - "Error %d on call %s.", request.status, request.url) - - except asyncio.TimeoutError: - _LOGGER.warning("Timeout call %s.", request.url) - - except aiohttp.ClientError: - _LOGGER.error("Client error %s.", request.url) - - # register services - hass.services.async_register(DOMAIN, name, async_service_handler) - - for command, command_config in config[DOMAIN].items(): - async_register_rest_command(command, command_config) - - return True diff --git a/homeassistant/components/rest_command/__init__.py b/homeassistant/components/rest_command/__init__.py new file mode 100644 index 0000000000000..01c5d837ca96a --- /dev/null +++ b/homeassistant/components/rest_command/__init__.py @@ -0,0 +1,121 @@ +"""Support for exposing regular REST commands as services.""" +import asyncio +import logging + +import aiohttp +from aiohttp import hdrs +import async_timeout +import voluptuous as vol + +from homeassistant.const import ( + CONF_TIMEOUT, CONF_USERNAME, CONF_PASSWORD, CONF_URL, CONF_PAYLOAD, + CONF_METHOD, CONF_HEADERS, CONF_VERIFY_SSL) +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv + +DOMAIN = 'rest_command' + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_TIMEOUT = 10 +DEFAULT_METHOD = 'get' +DEFAULT_VERIFY_SSL = True + +SUPPORT_REST_METHODS = [ + 'get', + 'post', + 'put', + 'delete', +] + +CONF_CONTENT_TYPE = 'content_type' + +COMMAND_SCHEMA = vol.Schema({ + vol.Required(CONF_URL): cv.template, + vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): + vol.All(vol.Lower, vol.In(SUPPORT_REST_METHODS)), + vol.Optional(CONF_HEADERS): vol.Schema({cv.string: cv.string}), + vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string, + vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string, + vol.Optional(CONF_PAYLOAD): cv.template, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.Coerce(int), + vol.Optional(CONF_CONTENT_TYPE): cv.string, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: cv.schema_with_slug_keys(COMMAND_SCHEMA), +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the REST command component.""" + def async_register_rest_command(name, command_config): + """Create service for rest command.""" + websession = async_get_clientsession( + hass, + command_config.get(CONF_VERIFY_SSL) + ) + timeout = command_config[CONF_TIMEOUT] + method = command_config[CONF_METHOD] + + template_url = command_config[CONF_URL] + template_url.hass = hass + + auth = None + if CONF_USERNAME in command_config: + username = command_config[CONF_USERNAME] + password = command_config.get(CONF_PASSWORD, '') + auth = aiohttp.BasicAuth(username, password=password) + + template_payload = None + if CONF_PAYLOAD in command_config: + template_payload = command_config[CONF_PAYLOAD] + template_payload.hass = hass + + headers = None + if CONF_HEADERS in command_config: + headers = command_config[CONF_HEADERS] + + if CONF_CONTENT_TYPE in command_config: + content_type = command_config[CONF_CONTENT_TYPE] + if headers is None: + headers = {} + headers[hdrs.CONTENT_TYPE] = content_type + + async def async_service_handler(service): + """Execute a shell command service.""" + payload = None + if template_payload: + payload = bytes( + template_payload.async_render(variables=service.data), + 'utf-8') + + try: + with async_timeout.timeout(timeout, loop=hass.loop): + request = await getattr(websession, method)( + template_url.async_render(variables=service.data), + data=payload, + auth=auth, + headers=headers + ) + + if request.status < 400: + _LOGGER.info("Success call %s.", request.url) + else: + _LOGGER.warning( + "Error %d on call %s.", request.status, request.url) + + except asyncio.TimeoutError: + _LOGGER.warning("Timeout call %s.", request.url) + + except aiohttp.ClientError: + _LOGGER.error("Client error %s.", request.url) + + # register services + hass.services.async_register(DOMAIN, name, async_service_handler) + + for command, command_config in config[DOMAIN].items(): + async_register_rest_command(command, command_config) + + return True diff --git a/homeassistant/components/rflink.py b/homeassistant/components/rflink.py deleted file mode 100644 index b91ff251bd112..0000000000000 --- a/homeassistant/components/rflink.py +++ /dev/null @@ -1,550 +0,0 @@ -""" -Support for Rflink components. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/rflink/ -""" -import asyncio -from collections import defaultdict -import logging -import async_timeout - -import voluptuous as vol - -from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_COMMAND, CONF_HOST, CONF_PORT, - STATE_ON, EVENT_HOMEASSISTANT_STOP) -from homeassistant.core import CoreState, callback -from homeassistant.exceptions import HomeAssistantError -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.deprecation import get_deprecated -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.dispatcher import ( - async_dispatcher_send, async_dispatcher_connect) -from homeassistant.helpers.restore_state import RestoreEntity - -REQUIREMENTS = ['rflink==0.0.37'] - -_LOGGER = logging.getLogger(__name__) - -ATTR_EVENT = 'event' -ATTR_STATE = 'state' - -CONF_ALIASES = 'aliases' -CONF_ALIASSES = 'aliasses' -CONF_GROUP_ALIASES = 'group_aliases' -CONF_GROUP_ALIASSES = 'group_aliasses' -CONF_GROUP = 'group' -CONF_NOGROUP_ALIASES = 'nogroup_aliases' -CONF_NOGROUP_ALIASSES = 'nogroup_aliasses' -CONF_DEVICE_DEFAULTS = 'device_defaults' -CONF_DEVICE_ID = 'device_id' -CONF_DEVICES = 'devices' -CONF_AUTOMATIC_ADD = 'automatic_add' -CONF_FIRE_EVENT = 'fire_event' -CONF_IGNORE_DEVICES = 'ignore_devices' -CONF_RECONNECT_INTERVAL = 'reconnect_interval' -CONF_SIGNAL_REPETITIONS = 'signal_repetitions' -CONF_WAIT_FOR_ACK = 'wait_for_ack' - -DATA_DEVICE_REGISTER = 'rflink_device_register' -DATA_ENTITY_LOOKUP = 'rflink_entity_lookup' -DATA_ENTITY_GROUP_LOOKUP = 'rflink_entity_group_only_lookup' -DEFAULT_RECONNECT_INTERVAL = 10 -DEFAULT_SIGNAL_REPETITIONS = 1 -CONNECTION_TIMEOUT = 10 - -EVENT_BUTTON_PRESSED = 'button_pressed' -EVENT_KEY_COMMAND = 'command' -EVENT_KEY_ID = 'id' -EVENT_KEY_SENSOR = 'sensor' -EVENT_KEY_UNIT = 'unit' - -RFLINK_GROUP_COMMANDS = ['allon', 'alloff'] - -DOMAIN = 'rflink' - -SERVICE_SEND_COMMAND = 'send_command' - -SIGNAL_AVAILABILITY = 'rflink_device_available' -SIGNAL_HANDLE_EVENT = 'rflink_handle_event_{}' - -TMP_ENTITY = 'tmp.{}' - -DEVICE_DEFAULTS_SCHEMA = vol.Schema({ - vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean, - vol.Optional(CONF_SIGNAL_REPETITIONS, - default=DEFAULT_SIGNAL_REPETITIONS): vol.Coerce(int), -}) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_PORT): vol.Any(cv.port, cv.string), - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_WAIT_FOR_ACK, default=True): cv.boolean, - vol.Optional(CONF_RECONNECT_INTERVAL, - default=DEFAULT_RECONNECT_INTERVAL): int, - vol.Optional(CONF_IGNORE_DEVICES, default=[]): - vol.All(cv.ensure_list, [cv.string]), - }), -}, extra=vol.ALLOW_EXTRA) - -SEND_COMMAND_SCHEMA = vol.Schema({ - vol.Required(CONF_DEVICE_ID): cv.string, - vol.Required(CONF_COMMAND): cv.string, -}) - - -def identify_event_type(event): - """Look at event to determine type of device. - - Async friendly. - """ - if EVENT_KEY_COMMAND in event: - return EVENT_KEY_COMMAND - if EVENT_KEY_SENSOR in event: - return EVENT_KEY_SENSOR - return 'unknown' - - -async def async_setup(hass, config): - """Set up the Rflink component.""" - from rflink.protocol import create_rflink_connection - import serial - - # Allow entities to register themselves by device_id to be looked up when - # new rflink events arrive to be handled - hass.data[DATA_ENTITY_LOOKUP] = { - EVENT_KEY_COMMAND: defaultdict(list), - EVENT_KEY_SENSOR: defaultdict(list), - } - hass.data[DATA_ENTITY_GROUP_LOOKUP] = { - EVENT_KEY_COMMAND: defaultdict(list), - } - - # Allow platform to specify function to register new unknown devices - hass.data[DATA_DEVICE_REGISTER] = {} - - async def async_send_command(call): - """Send Rflink command.""" - _LOGGER.debug('Rflink command for %s', str(call.data)) - if not (await RflinkCommand.send_command( - call.data.get(CONF_DEVICE_ID), - call.data.get(CONF_COMMAND))): - _LOGGER.error('Failed Rflink command for %s', str(call.data)) - - hass.services.async_register( - DOMAIN, SERVICE_SEND_COMMAND, async_send_command, - schema=SEND_COMMAND_SCHEMA) - - @callback - def event_callback(event): - """Handle incoming Rflink events. - - Rflink events arrive as dictionaries of varying content - depending on their type. Identify the events and distribute - accordingly. - """ - event_type = identify_event_type(event) - _LOGGER.debug('event of type %s: %s', event_type, event) - - # Don't propagate non entity events (eg: version string, ack response) - if event_type not in hass.data[DATA_ENTITY_LOOKUP]: - _LOGGER.debug('unhandled event of type: %s', event_type) - return - - # Lookup entities who registered this device id as device id or alias - event_id = event.get(EVENT_KEY_ID, None) - - is_group_event = (event_type == EVENT_KEY_COMMAND and - event[EVENT_KEY_COMMAND] in RFLINK_GROUP_COMMANDS) - if is_group_event: - entity_ids = hass.data[DATA_ENTITY_GROUP_LOOKUP][event_type].get( - event_id, []) - else: - entity_ids = hass.data[DATA_ENTITY_LOOKUP][event_type][event_id] - - _LOGGER.debug('entity_ids: %s', entity_ids) - if entity_ids: - # Propagate event to every entity matching the device id - for entity in entity_ids: - _LOGGER.debug('passing event to %s', entity) - async_dispatcher_send(hass, - SIGNAL_HANDLE_EVENT.format(entity), - event) - elif not is_group_event: - # If device is not yet known, register with platform (if loaded) - if event_type in hass.data[DATA_DEVICE_REGISTER]: - _LOGGER.debug('device_id not known, adding new device') - # Add bogus event_id first to avoid race if we get another - # event before the device is created - # Any additional events received before the device has been - # created will thus be ignored. - hass.data[DATA_ENTITY_LOOKUP][event_type][ - event_id].append(TMP_ENTITY.format(event_id)) - hass.async_create_task( - hass.data[DATA_DEVICE_REGISTER][event_type](event)) - else: - _LOGGER.debug('device_id not known and automatic add disabled') - - # When connecting to tcp host instead of serial port (optional) - host = config[DOMAIN].get(CONF_HOST) - # TCP port when host configured, otherwise serial port - port = config[DOMAIN][CONF_PORT] - - @callback - def reconnect(exc=None): - """Schedule reconnect after connection has been unexpectedly lost.""" - # Reset protocol binding before starting reconnect - RflinkCommand.set_rflink_protocol(None) - - async_dispatcher_send(hass, SIGNAL_AVAILABILITY, False) - - # If HA is not stopping, initiate new connection - if hass.state != CoreState.stopping: - _LOGGER.warning('disconnected from Rflink, reconnecting') - hass.async_create_task(connect()) - - async def connect(): - """Set up connection and hook it into HA for reconnect/shutdown.""" - _LOGGER.info('Initiating Rflink connection') - - # Rflink create_rflink_connection decides based on the value of host - # (string or None) if serial or tcp mode should be used - - # Initiate serial/tcp connection to Rflink gateway - connection = create_rflink_connection( - port=port, - host=host, - event_callback=event_callback, - disconnect_callback=reconnect, - loop=hass.loop, - ignore=config[DOMAIN][CONF_IGNORE_DEVICES] - ) - - try: - with async_timeout.timeout(CONNECTION_TIMEOUT, - loop=hass.loop): - transport, protocol = await connection - - except (serial.serialutil.SerialException, ConnectionRefusedError, - TimeoutError, OSError, asyncio.TimeoutError) as exc: - reconnect_interval = config[DOMAIN][CONF_RECONNECT_INTERVAL] - _LOGGER.exception( - "Error connecting to Rflink, reconnecting in %s", - reconnect_interval) - # Connection to Rflink device is lost, make entities unavailable - async_dispatcher_send(hass, SIGNAL_AVAILABILITY, False) - - hass.loop.call_later(reconnect_interval, reconnect, exc) - return - - # There is a valid connection to a Rflink device now so - # mark entities as available - async_dispatcher_send(hass, SIGNAL_AVAILABILITY, True) - - # Bind protocol to command class to allow entities to send commands - RflinkCommand.set_rflink_protocol( - protocol, config[DOMAIN][CONF_WAIT_FOR_ACK]) - - # handle shutdown of Rflink asyncio transport - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, - lambda x: transport.close()) - - _LOGGER.info('Connected to Rflink') - - hass.async_create_task(connect()) - return True - - -class RflinkDevice(Entity): - """Representation of a Rflink device. - - Contains the common logic for Rflink entities. - """ - - platform = None - _state = None - _available = True - - def __init__(self, device_id, initial_event=None, name=None, aliases=None, - group=True, group_aliases=None, nogroup_aliases=None, - fire_event=False, - signal_repetitions=DEFAULT_SIGNAL_REPETITIONS): - """Initialize the device.""" - # Rflink specific attributes for every component type - self._initial_event = initial_event - self._device_id = device_id - if name: - self._name = name - else: - self._name = device_id - - self._aliases = aliases - self._group = group - self._group_aliases = group_aliases - self._nogroup_aliases = nogroup_aliases - self._should_fire_event = fire_event - self._signal_repetitions = signal_repetitions - - @callback - def handle_event_callback(self, event): - """Handle incoming event for device type.""" - # Call platform specific event handler - self._handle_event(event) - - # Propagate changes through ha - self.async_schedule_update_ha_state() - - # Put command onto bus for user to subscribe to - if self._should_fire_event and identify_event_type( - event) == EVENT_KEY_COMMAND: - self.hass.bus.async_fire(EVENT_BUTTON_PRESSED, { - ATTR_ENTITY_ID: self.entity_id, - ATTR_STATE: event[EVENT_KEY_COMMAND], - }) - _LOGGER.debug("Fired bus event for %s: %s", - self.entity_id, event[EVENT_KEY_COMMAND]) - - def _handle_event(self, event): - """Platform specific event handler.""" - raise NotImplementedError() - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return a name for the device.""" - return self._name - - @property - def is_on(self): - """Return true if device is on.""" - if self.assumed_state: - return False - return self._state - - @property - def assumed_state(self): - """Assume device state until first device event sets state.""" - return self._state is None - - @property - def available(self): - """Return True if entity is available.""" - return self._available - - @callback - def _availability_callback(self, availability): - """Update availability state.""" - self._available = availability - self.async_schedule_update_ha_state() - - async def async_added_to_hass(self): - """Register update callback.""" - # Remove temporary bogus entity_id if added - tmp_entity = TMP_ENTITY.format(self._device_id) - if tmp_entity in self.hass.data[DATA_ENTITY_LOOKUP][ - EVENT_KEY_COMMAND][self._device_id]: - self.hass.data[DATA_ENTITY_LOOKUP][ - EVENT_KEY_COMMAND][self._device_id].remove(tmp_entity) - - # Register id and aliases - self.hass.data[DATA_ENTITY_LOOKUP][ - EVENT_KEY_COMMAND][self._device_id].append(self.entity_id) - if self._group: - self.hass.data[DATA_ENTITY_GROUP_LOOKUP][ - EVENT_KEY_COMMAND][self._device_id].append(self.entity_id) - # aliases respond to both normal and group commands (allon/alloff) - if self._aliases: - for _id in self._aliases: - self.hass.data[DATA_ENTITY_LOOKUP][ - EVENT_KEY_COMMAND][_id].append(self.entity_id) - self.hass.data[DATA_ENTITY_GROUP_LOOKUP][ - EVENT_KEY_COMMAND][_id].append(self.entity_id) - # group_aliases only respond to group commands (allon/alloff) - if self._group_aliases: - for _id in self._group_aliases: - self.hass.data[DATA_ENTITY_GROUP_LOOKUP][ - EVENT_KEY_COMMAND][_id].append(self.entity_id) - # nogroup_aliases only respond to normal commands - if self._nogroup_aliases: - for _id in self._nogroup_aliases: - self.hass.data[DATA_ENTITY_LOOKUP][ - EVENT_KEY_COMMAND][_id].append(self.entity_id) - async_dispatcher_connect(self.hass, SIGNAL_AVAILABILITY, - self._availability_callback) - async_dispatcher_connect(self.hass, - SIGNAL_HANDLE_EVENT.format(self.entity_id), - self.handle_event_callback) - - # Process the initial event now that the entity is created - if self._initial_event: - self.handle_event_callback(self._initial_event) - - -class RflinkCommand(RflinkDevice): - """Singleton class to make Rflink command interface available to entities. - - This class is to be inherited by every Entity class that is actionable - (switches/lights). It exposes the Rflink command interface for these - entities. - - The Rflink interface is managed as a class level and set during setup (and - reset on reconnect). - """ - - # Keep repetition tasks to cancel if state is changed before repetitions - # are sent - _repetition_task = None - - _protocol = None - - @classmethod - def set_rflink_protocol(cls, protocol, wait_ack=None): - """Set the Rflink asyncio protocol as a class variable.""" - cls._protocol = protocol - if wait_ack is not None: - cls._wait_ack = wait_ack - - @classmethod - def is_connected(cls): - """Return connection status.""" - return bool(cls._protocol) - - @classmethod - async def send_command(cls, device_id, action): - """Send device command to Rflink and wait for acknowledgement.""" - return await cls._protocol.send_command_ack(device_id, action) - - async def _async_handle_command(self, command, *args): - """Do bookkeeping for command, send it to rflink and update state.""" - self.cancel_queued_send_commands() - - if command == 'turn_on': - cmd = 'on' - self._state = True - - elif command == 'turn_off': - cmd = 'off' - self._state = False - - elif command == 'dim': - # convert brightness to rflink dim level - cmd = str(int(args[0] / 17)) - self._state = True - - elif command == 'toggle': - cmd = 'on' - # if the state is unknown or false, it gets set as true - # if the state is true, it gets set as false - self._state = self._state in [None, False] - - # Cover options for RFlink - elif command == 'close_cover': - cmd = 'DOWN' - self._state = False - - elif command == 'open_cover': - cmd = 'UP' - self._state = True - - elif command == 'stop_cover': - cmd = 'STOP' - self._state = True - - # Send initial command and queue repetitions. - # This allows the entity state to be updated quickly and not having to - # wait for all repetitions to be sent - await self._async_send_command(cmd, self._signal_repetitions) - - # Update state of entity - await self.async_update_ha_state() - - def cancel_queued_send_commands(self): - """Cancel queued signal repetition commands. - - For example when user changed state while repetitions are still - queued for broadcast. Or when an incoming Rflink command (remote - switch) changes the state. - """ - # cancel any outstanding tasks from the previous state change - if self._repetition_task: - self._repetition_task.cancel() - - async def _async_send_command(self, cmd, repetitions): - """Send a command for device to Rflink gateway.""" - _LOGGER.debug( - "Sending command: %s to Rflink device: %s", cmd, self._device_id) - - if not self.is_connected(): - raise HomeAssistantError('Cannot send command, not connected!') - - if self._wait_ack: - # Puts command on outgoing buffer then waits for Rflink to confirm - # the command has been send out in the ether. - await self._protocol.send_command_ack(self._device_id, cmd) - else: - # Puts command on outgoing buffer and returns straight away. - # Rflink protocol/transport handles asynchronous writing of buffer - # to serial/tcp device. Does not wait for command send - # confirmation. - self._protocol.send_command(self._device_id, cmd) - - if repetitions > 1: - self._repetition_task = self.hass.async_create_task( - self._async_send_command(cmd, repetitions - 1)) - - -class SwitchableRflinkDevice(RflinkCommand, RestoreEntity): - """Rflink entity which can switch on/off (eg: light, switch).""" - - async def async_added_to_hass(self): - """Restore RFLink device state (ON/OFF).""" - await super().async_added_to_hass() - - old_state = await self.async_get_last_state() - if old_state is not None: - self._state = old_state.state == STATE_ON - - def _handle_event(self, event): - """Adjust state if Rflink picks up a remote command for this device.""" - self.cancel_queued_send_commands() - - command = event['command'] - if command in ['on', 'allon']: - self._state = True - elif command in ['off', 'alloff']: - self._state = False - - def async_turn_on(self, **kwargs): - """Turn the device on.""" - return self._async_handle_command("turn_on") - - def async_turn_off(self, **kwargs): - """Turn the device off.""" - return self._async_handle_command("turn_off") - - -DEPRECATED_CONFIG_OPTIONS = [ - CONF_ALIASSES, - CONF_GROUP_ALIASSES, - CONF_NOGROUP_ALIASSES] -REPLACEMENT_CONFIG_OPTIONS = [ - CONF_ALIASES, - CONF_GROUP_ALIASES, - CONF_NOGROUP_ALIASES] - - -def remove_deprecated(config): - """Remove deprecated config options from device config.""" - for index, deprecated_option in enumerate(DEPRECATED_CONFIG_OPTIONS): - if deprecated_option in config: - replacement_option = REPLACEMENT_CONFIG_OPTIONS[index] - # generate deprecation warning - get_deprecated(config, replacement_option, deprecated_option) - # remove old config value replacing new one - config[replacement_option] = config.pop(deprecated_option) diff --git a/homeassistant/components/rflink/__init__.py b/homeassistant/components/rflink/__init__.py new file mode 100644 index 0000000000000..98e80580fea60 --- /dev/null +++ b/homeassistant/components/rflink/__init__.py @@ -0,0 +1,546 @@ +"""Support for Rflink devices.""" +import asyncio +from collections import defaultdict +import logging +import async_timeout + +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_COMMAND, CONF_HOST, CONF_PORT, + STATE_ON, EVENT_HOMEASSISTANT_STOP) +from homeassistant.core import CoreState, callback +from homeassistant.exceptions import HomeAssistantError +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.deprecation import get_deprecated +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.dispatcher import ( + async_dispatcher_send, async_dispatcher_connect) +from homeassistant.helpers.restore_state import RestoreEntity + +REQUIREMENTS = ['rflink==0.0.37'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_EVENT = 'event' +ATTR_STATE = 'state' + +CONF_ALIASES = 'aliases' +CONF_ALIASSES = 'aliasses' +CONF_GROUP_ALIASES = 'group_aliases' +CONF_GROUP_ALIASSES = 'group_aliasses' +CONF_GROUP = 'group' +CONF_NOGROUP_ALIASES = 'nogroup_aliases' +CONF_NOGROUP_ALIASSES = 'nogroup_aliasses' +CONF_DEVICE_DEFAULTS = 'device_defaults' +CONF_DEVICE_ID = 'device_id' +CONF_DEVICES = 'devices' +CONF_AUTOMATIC_ADD = 'automatic_add' +CONF_FIRE_EVENT = 'fire_event' +CONF_IGNORE_DEVICES = 'ignore_devices' +CONF_RECONNECT_INTERVAL = 'reconnect_interval' +CONF_SIGNAL_REPETITIONS = 'signal_repetitions' +CONF_WAIT_FOR_ACK = 'wait_for_ack' + +DATA_DEVICE_REGISTER = 'rflink_device_register' +DATA_ENTITY_LOOKUP = 'rflink_entity_lookup' +DATA_ENTITY_GROUP_LOOKUP = 'rflink_entity_group_only_lookup' +DEFAULT_RECONNECT_INTERVAL = 10 +DEFAULT_SIGNAL_REPETITIONS = 1 +CONNECTION_TIMEOUT = 10 + +EVENT_BUTTON_PRESSED = 'button_pressed' +EVENT_KEY_COMMAND = 'command' +EVENT_KEY_ID = 'id' +EVENT_KEY_SENSOR = 'sensor' +EVENT_KEY_UNIT = 'unit' + +RFLINK_GROUP_COMMANDS = ['allon', 'alloff'] + +DOMAIN = 'rflink' + +SERVICE_SEND_COMMAND = 'send_command' + +SIGNAL_AVAILABILITY = 'rflink_device_available' +SIGNAL_HANDLE_EVENT = 'rflink_handle_event_{}' + +TMP_ENTITY = 'tmp.{}' + +DEVICE_DEFAULTS_SCHEMA = vol.Schema({ + vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean, + vol.Optional(CONF_SIGNAL_REPETITIONS, + default=DEFAULT_SIGNAL_REPETITIONS): vol.Coerce(int), +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_PORT): vol.Any(cv.port, cv.string), + vol.Optional(CONF_HOST): cv.string, + vol.Optional(CONF_WAIT_FOR_ACK, default=True): cv.boolean, + vol.Optional(CONF_RECONNECT_INTERVAL, + default=DEFAULT_RECONNECT_INTERVAL): int, + vol.Optional(CONF_IGNORE_DEVICES, default=[]): + vol.All(cv.ensure_list, [cv.string]), + }), +}, extra=vol.ALLOW_EXTRA) + +SEND_COMMAND_SCHEMA = vol.Schema({ + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Required(CONF_COMMAND): cv.string, +}) + + +def identify_event_type(event): + """Look at event to determine type of device. + + Async friendly. + """ + if EVENT_KEY_COMMAND in event: + return EVENT_KEY_COMMAND + if EVENT_KEY_SENSOR in event: + return EVENT_KEY_SENSOR + return 'unknown' + + +async def async_setup(hass, config): + """Set up the Rflink component.""" + from rflink.protocol import create_rflink_connection + import serial + + # Allow entities to register themselves by device_id to be looked up when + # new rflink events arrive to be handled + hass.data[DATA_ENTITY_LOOKUP] = { + EVENT_KEY_COMMAND: defaultdict(list), + EVENT_KEY_SENSOR: defaultdict(list), + } + hass.data[DATA_ENTITY_GROUP_LOOKUP] = { + EVENT_KEY_COMMAND: defaultdict(list), + } + + # Allow platform to specify function to register new unknown devices + hass.data[DATA_DEVICE_REGISTER] = {} + + async def async_send_command(call): + """Send Rflink command.""" + _LOGGER.debug('Rflink command for %s', str(call.data)) + if not (await RflinkCommand.send_command( + call.data.get(CONF_DEVICE_ID), + call.data.get(CONF_COMMAND))): + _LOGGER.error('Failed Rflink command for %s', str(call.data)) + + hass.services.async_register( + DOMAIN, SERVICE_SEND_COMMAND, async_send_command, + schema=SEND_COMMAND_SCHEMA) + + @callback + def event_callback(event): + """Handle incoming Rflink events. + + Rflink events arrive as dictionaries of varying content + depending on their type. Identify the events and distribute + accordingly. + """ + event_type = identify_event_type(event) + _LOGGER.debug('event of type %s: %s', event_type, event) + + # Don't propagate non entity events (eg: version string, ack response) + if event_type not in hass.data[DATA_ENTITY_LOOKUP]: + _LOGGER.debug('unhandled event of type: %s', event_type) + return + + # Lookup entities who registered this device id as device id or alias + event_id = event.get(EVENT_KEY_ID, None) + + is_group_event = (event_type == EVENT_KEY_COMMAND and + event[EVENT_KEY_COMMAND] in RFLINK_GROUP_COMMANDS) + if is_group_event: + entity_ids = hass.data[DATA_ENTITY_GROUP_LOOKUP][event_type].get( + event_id, []) + else: + entity_ids = hass.data[DATA_ENTITY_LOOKUP][event_type][event_id] + + _LOGGER.debug('entity_ids: %s', entity_ids) + if entity_ids: + # Propagate event to every entity matching the device id + for entity in entity_ids: + _LOGGER.debug('passing event to %s', entity) + async_dispatcher_send(hass, + SIGNAL_HANDLE_EVENT.format(entity), + event) + elif not is_group_event: + # If device is not yet known, register with platform (if loaded) + if event_type in hass.data[DATA_DEVICE_REGISTER]: + _LOGGER.debug('device_id not known, adding new device') + # Add bogus event_id first to avoid race if we get another + # event before the device is created + # Any additional events received before the device has been + # created will thus be ignored. + hass.data[DATA_ENTITY_LOOKUP][event_type][ + event_id].append(TMP_ENTITY.format(event_id)) + hass.async_create_task( + hass.data[DATA_DEVICE_REGISTER][event_type](event)) + else: + _LOGGER.debug('device_id not known and automatic add disabled') + + # When connecting to tcp host instead of serial port (optional) + host = config[DOMAIN].get(CONF_HOST) + # TCP port when host configured, otherwise serial port + port = config[DOMAIN][CONF_PORT] + + @callback + def reconnect(exc=None): + """Schedule reconnect after connection has been unexpectedly lost.""" + # Reset protocol binding before starting reconnect + RflinkCommand.set_rflink_protocol(None) + + async_dispatcher_send(hass, SIGNAL_AVAILABILITY, False) + + # If HA is not stopping, initiate new connection + if hass.state != CoreState.stopping: + _LOGGER.warning('disconnected from Rflink, reconnecting') + hass.async_create_task(connect()) + + async def connect(): + """Set up connection and hook it into HA for reconnect/shutdown.""" + _LOGGER.info('Initiating Rflink connection') + + # Rflink create_rflink_connection decides based on the value of host + # (string or None) if serial or tcp mode should be used + + # Initiate serial/tcp connection to Rflink gateway + connection = create_rflink_connection( + port=port, + host=host, + event_callback=event_callback, + disconnect_callback=reconnect, + loop=hass.loop, + ignore=config[DOMAIN][CONF_IGNORE_DEVICES] + ) + + try: + with async_timeout.timeout(CONNECTION_TIMEOUT, + loop=hass.loop): + transport, protocol = await connection + + except (serial.serialutil.SerialException, ConnectionRefusedError, + TimeoutError, OSError, asyncio.TimeoutError) as exc: + reconnect_interval = config[DOMAIN][CONF_RECONNECT_INTERVAL] + _LOGGER.exception( + "Error connecting to Rflink, reconnecting in %s", + reconnect_interval) + # Connection to Rflink device is lost, make entities unavailable + async_dispatcher_send(hass, SIGNAL_AVAILABILITY, False) + + hass.loop.call_later(reconnect_interval, reconnect, exc) + return + + # There is a valid connection to a Rflink device now so + # mark entities as available + async_dispatcher_send(hass, SIGNAL_AVAILABILITY, True) + + # Bind protocol to command class to allow entities to send commands + RflinkCommand.set_rflink_protocol( + protocol, config[DOMAIN][CONF_WAIT_FOR_ACK]) + + # handle shutdown of Rflink asyncio transport + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, + lambda x: transport.close()) + + _LOGGER.info('Connected to Rflink') + + hass.async_create_task(connect()) + return True + + +class RflinkDevice(Entity): + """Representation of a Rflink device. + + Contains the common logic for Rflink entities. + """ + + platform = None + _state = None + _available = True + + def __init__(self, device_id, initial_event=None, name=None, aliases=None, + group=True, group_aliases=None, nogroup_aliases=None, + fire_event=False, + signal_repetitions=DEFAULT_SIGNAL_REPETITIONS): + """Initialize the device.""" + # Rflink specific attributes for every component type + self._initial_event = initial_event + self._device_id = device_id + if name: + self._name = name + else: + self._name = device_id + + self._aliases = aliases + self._group = group + self._group_aliases = group_aliases + self._nogroup_aliases = nogroup_aliases + self._should_fire_event = fire_event + self._signal_repetitions = signal_repetitions + + @callback + def handle_event_callback(self, event): + """Handle incoming event for device type.""" + # Call platform specific event handler + self._handle_event(event) + + # Propagate changes through ha + self.async_schedule_update_ha_state() + + # Put command onto bus for user to subscribe to + if self._should_fire_event and identify_event_type( + event) == EVENT_KEY_COMMAND: + self.hass.bus.async_fire(EVENT_BUTTON_PRESSED, { + ATTR_ENTITY_ID: self.entity_id, + ATTR_STATE: event[EVENT_KEY_COMMAND], + }) + _LOGGER.debug("Fired bus event for %s: %s", + self.entity_id, event[EVENT_KEY_COMMAND]) + + def _handle_event(self, event): + """Platform specific event handler.""" + raise NotImplementedError() + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return a name for the device.""" + return self._name + + @property + def is_on(self): + """Return true if device is on.""" + if self.assumed_state: + return False + return self._state + + @property + def assumed_state(self): + """Assume device state until first device event sets state.""" + return self._state is None + + @property + def available(self): + """Return True if entity is available.""" + return self._available + + @callback + def _availability_callback(self, availability): + """Update availability state.""" + self._available = availability + self.async_schedule_update_ha_state() + + async def async_added_to_hass(self): + """Register update callback.""" + 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][ + EVENT_KEY_COMMAND][self._device_id]: + self.hass.data[DATA_ENTITY_LOOKUP][ + EVENT_KEY_COMMAND][self._device_id].remove(tmp_entity) + + # Register id and aliases + self.hass.data[DATA_ENTITY_LOOKUP][ + EVENT_KEY_COMMAND][self._device_id].append(self.entity_id) + if self._group: + self.hass.data[DATA_ENTITY_GROUP_LOOKUP][ + EVENT_KEY_COMMAND][self._device_id].append(self.entity_id) + # aliases respond to both normal and group commands (allon/alloff) + if self._aliases: + for _id in self._aliases: + self.hass.data[DATA_ENTITY_LOOKUP][ + EVENT_KEY_COMMAND][_id].append(self.entity_id) + self.hass.data[DATA_ENTITY_GROUP_LOOKUP][ + EVENT_KEY_COMMAND][_id].append(self.entity_id) + # group_aliases only respond to group commands (allon/alloff) + if self._group_aliases: + for _id in self._group_aliases: + self.hass.data[DATA_ENTITY_GROUP_LOOKUP][ + EVENT_KEY_COMMAND][_id].append(self.entity_id) + # nogroup_aliases only respond to normal commands + if self._nogroup_aliases: + for _id in self._nogroup_aliases: + self.hass.data[DATA_ENTITY_LOOKUP][ + EVENT_KEY_COMMAND][_id].append(self.entity_id) + async_dispatcher_connect(self.hass, SIGNAL_AVAILABILITY, + self._availability_callback) + async_dispatcher_connect(self.hass, + SIGNAL_HANDLE_EVENT.format(self.entity_id), + self.handle_event_callback) + + # Process the initial event now that the entity is created + if self._initial_event: + self.handle_event_callback(self._initial_event) + + +class RflinkCommand(RflinkDevice): + """Singleton class to make Rflink command interface available to entities. + + This class is to be inherited by every Entity class that is actionable + (switches/lights). It exposes the Rflink command interface for these + entities. + + The Rflink interface is managed as a class level and set during setup (and + reset on reconnect). + """ + + # Keep repetition tasks to cancel if state is changed before repetitions + # are sent + _repetition_task = None + + _protocol = None + + @classmethod + def set_rflink_protocol(cls, protocol, wait_ack=None): + """Set the Rflink asyncio protocol as a class variable.""" + cls._protocol = protocol + if wait_ack is not None: + cls._wait_ack = wait_ack + + @classmethod + def is_connected(cls): + """Return connection status.""" + return bool(cls._protocol) + + @classmethod + async def send_command(cls, device_id, action): + """Send device command to Rflink and wait for acknowledgement.""" + return await cls._protocol.send_command_ack(device_id, action) + + async def _async_handle_command(self, command, *args): + """Do bookkeeping for command, send it to rflink and update state.""" + self.cancel_queued_send_commands() + + if command == 'turn_on': + cmd = 'on' + self._state = True + + elif command == 'turn_off': + cmd = 'off' + self._state = False + + elif command == 'dim': + # convert brightness to rflink dim level + cmd = str(int(args[0] / 17)) + self._state = True + + elif command == 'toggle': + cmd = 'on' + # if the state is unknown or false, it gets set as true + # if the state is true, it gets set as false + self._state = self._state in [None, False] + + # Cover options for RFlink + elif command == 'close_cover': + cmd = 'DOWN' + self._state = False + + elif command == 'open_cover': + cmd = 'UP' + self._state = True + + elif command == 'stop_cover': + cmd = 'STOP' + self._state = True + + # Send initial command and queue repetitions. + # This allows the entity state to be updated quickly and not having to + # wait for all repetitions to be sent + await self._async_send_command(cmd, self._signal_repetitions) + + # Update state of entity + await self.async_update_ha_state() + + def cancel_queued_send_commands(self): + """Cancel queued signal repetition commands. + + For example when user changed state while repetitions are still + queued for broadcast. Or when an incoming Rflink command (remote + switch) changes the state. + """ + # cancel any outstanding tasks from the previous state change + if self._repetition_task: + self._repetition_task.cancel() + + async def _async_send_command(self, cmd, repetitions): + """Send a command for device to Rflink gateway.""" + _LOGGER.debug( + "Sending command: %s to Rflink device: %s", cmd, self._device_id) + + if not self.is_connected(): + raise HomeAssistantError('Cannot send command, not connected!') + + if self._wait_ack: + # Puts command on outgoing buffer then waits for Rflink to confirm + # the command has been send out in the ether. + await self._protocol.send_command_ack(self._device_id, cmd) + else: + # Puts command on outgoing buffer and returns straight away. + # Rflink protocol/transport handles asynchronous writing of buffer + # to serial/tcp device. Does not wait for command send + # confirmation. + self._protocol.send_command(self._device_id, cmd) + + if repetitions > 1: + self._repetition_task = self.hass.async_create_task( + self._async_send_command(cmd, repetitions - 1)) + + +class SwitchableRflinkDevice(RflinkCommand, RestoreEntity): + """Rflink entity which can switch on/off (eg: light, switch).""" + + async def async_added_to_hass(self): + """Restore RFLink device state (ON/OFF).""" + await super().async_added_to_hass() + + old_state = await self.async_get_last_state() + if old_state is not None: + self._state = old_state.state == STATE_ON + + def _handle_event(self, event): + """Adjust state if Rflink picks up a remote command for this device.""" + self.cancel_queued_send_commands() + + command = event['command'] + if command in ['on', 'allon']: + self._state = True + elif command in ['off', 'alloff']: + self._state = False + + def async_turn_on(self, **kwargs): + """Turn the device on.""" + return self._async_handle_command("turn_on") + + def async_turn_off(self, **kwargs): + """Turn the device off.""" + return self._async_handle_command("turn_off") + + +DEPRECATED_CONFIG_OPTIONS = [ + CONF_ALIASSES, + CONF_GROUP_ALIASSES, + CONF_NOGROUP_ALIASSES] +REPLACEMENT_CONFIG_OPTIONS = [ + CONF_ALIASES, + CONF_GROUP_ALIASES, + CONF_NOGROUP_ALIASES] + + +def remove_deprecated(config): + """Remove deprecated config options from device config.""" + for index, deprecated_option in enumerate(DEPRECATED_CONFIG_OPTIONS): + if deprecated_option in config: + replacement_option = REPLACEMENT_CONFIG_OPTIONS[index] + # generate deprecation warning + get_deprecated(config, replacement_option, deprecated_option) + # remove old config value replacing new one + config[replacement_option] = config.pop(deprecated_option) diff --git a/homeassistant/components/rflink/services.yaml b/homeassistant/components/rflink/services.yaml new file mode 100644 index 0000000000000..9269326ece6fa --- /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.py deleted file mode 100644 index f2c82842bc116..0000000000000 --- a/homeassistant/components/rfxtrx.py +++ /dev/null @@ -1,392 +0,0 @@ -""" -Support for RFXtrx components. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/rfxtrx/ -""" -from collections import OrderedDict -import logging - -import voluptuous as vol - -from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_NAME, ATTR_STATE, CONF_DEVICE, CONF_DEVICES, - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS) -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity -from homeassistant.util import slugify - -REQUIREMENTS = ['pyRFXtrx==0.23'] - -DOMAIN = 'rfxtrx' - -DEFAULT_SIGNAL_REPETITIONS = 1 - -ATTR_AUTOMATIC_ADD = 'automatic_add' -ATTR_DEVICE = 'device' -ATTR_DEBUG = 'debug' -ATTR_FIRE_EVENT = 'fire_event' -ATTR_DATA_TYPE = 'data_type' -ATTR_DUMMY = 'dummy' -CONF_DATA_BITS = 'data_bits' -CONF_AUTOMATIC_ADD = 'automatic_add' -CONF_DATA_TYPE = 'data_type' -CONF_SIGNAL_REPETITIONS = 'signal_repetitions' -CONF_FIRE_EVENT = 'fire_event' -CONF_DUMMY = 'dummy' -CONF_DEBUG = 'debug' -CONF_OFF_DELAY = 'off_delay' -EVENT_BUTTON_PRESSED = 'button_pressed' - -DATA_TYPES = OrderedDict([ - ('Temperature', TEMP_CELSIUS), - ('Temperature2', TEMP_CELSIUS), - ('Humidity', '%'), - ('Barometer', ''), - ('Wind direction', ''), - ('Rain rate', ''), - ('Energy usage', 'W'), - ('Total usage', 'W'), - ('Sound', ''), - ('Sensor Status', ''), - ('Counter value', ''), - ('UV', 'uv')]) - -RECEIVED_EVT_SUBSCRIBERS = [] -RFX_DEVICES = {} -_LOGGER = logging.getLogger(__name__) -DATA_RFXOBJECT = 'rfxobject' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_DEVICE): cv.string, - vol.Optional(CONF_DEBUG, default=False): cv.boolean, - vol.Optional(CONF_DUMMY, default=False): cv.boolean, - }), -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the RFXtrx component.""" - # Declare the Handle event - def handle_receive(event): - """Handle received messages from RFXtrx gateway.""" - # Log RFXCOM event - if not event.device.id_string: - return - _LOGGER.debug("Receive RFXCOM event from " - "(Device_id: %s Class: %s Sub: %s, Pkt_id: %s)", - slugify(event.device.id_string.lower()), - event.device.__class__.__name__, - event.device.subtype, - "".join("{0:02x}".format(x) for x in event.data)) - - # Callback to HA registered components. - for subscriber in RECEIVED_EVT_SUBSCRIBERS: - subscriber(event) - - # Try to load the RFXtrx module. - import RFXtrx as rfxtrxmod - - device = config[DOMAIN][ATTR_DEVICE] - debug = config[DOMAIN][ATTR_DEBUG] - dummy_connection = config[DOMAIN][ATTR_DUMMY] - - if dummy_connection: - rfx_object = rfxtrxmod.Connect( - device, None, debug=debug, - transport_protocol=rfxtrxmod.DummyTransport2) - else: - rfx_object = rfxtrxmod.Connect(device, None, debug=debug) - - def _start_rfxtrx(event): - rfx_object.event_callback = handle_receive - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start_rfxtrx) - - def _shutdown_rfxtrx(event): - """Close connection with RFXtrx.""" - rfx_object.close_connection() - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown_rfxtrx) - - hass.data[DATA_RFXOBJECT] = rfx_object - return True - - -def get_rfx_object(packetid): - """Return the RFXObject with the packetid.""" - import RFXtrx as rfxtrxmod - - try: - binarypacket = bytearray.fromhex(packetid) - except ValueError: - return None - - pkt = rfxtrxmod.lowlevel.parse(binarypacket) - if pkt is None: - return None - if isinstance(pkt, rfxtrxmod.lowlevel.SensorPacket): - obj = rfxtrxmod.SensorEvent(pkt) - elif isinstance(pkt, rfxtrxmod.lowlevel.Status): - obj = rfxtrxmod.StatusEvent(pkt) - else: - obj = rfxtrxmod.ControlEvent(pkt) - return obj - - -def get_pt2262_deviceid(device_id, nb_data_bits): - """Extract and return the address bits from a Lighting4/PT2262 packet.""" - if nb_data_bits is None: - return - import binascii - try: - data = bytearray.fromhex(device_id) - except ValueError: - return None - mask = 0xFF & ~((1 << nb_data_bits) - 1) - - data[len(data)-1] &= mask - - return binascii.hexlify(data) - - -def get_pt2262_cmd(device_id, data_bits): - """Extract and return the data bits from a Lighting4/PT2262 packet.""" - try: - data = bytearray.fromhex(device_id) - except ValueError: - return None - - mask = 0xFF & ((1 << data_bits) - 1) - - return hex(data[-1] & mask) - - -def get_pt2262_device(device_id): - """Look for the device which id matches the given device_id parameter.""" - for device in RFX_DEVICES.values(): - if (hasattr(device, 'is_lighting4') and - device.masked_id is not None and - device.masked_id == get_pt2262_deviceid(device_id, - device.data_bits)): - _LOGGER.debug("rfxtrx: found matching device %s for %s", - device_id, - device.masked_id) - return device - return None - - -def find_possible_pt2262_device(device_id): - """Look for the device which id matches the given device_id parameter.""" - for dev_id, device in RFX_DEVICES.items(): - if hasattr(device, 'is_lighting4') and len(dev_id) == len(device_id): - size = None - for i, (char1, char2) in enumerate(zip(dev_id, device_id)): - if char1 != char2: - break - size = i - - if size is not None: - size = len(dev_id) - size - 1 - _LOGGER.info("rfxtrx: found possible device %s for %s " - "with the following configuration:\n" - "data_bits=%d\n" - "command_on=0x%s\n" - "command_off=0x%s\n", - device_id, - dev_id, - size * 4, - dev_id[-size:], device_id[-size:]) - return device - - return None - - -def get_devices_from_config(config, device): - """Read rfxtrx configuration.""" - signal_repetitions = config[CONF_SIGNAL_REPETITIONS] - - devices = [] - for packet_id, entity_info in config[CONF_DEVICES].items(): - event = get_rfx_object(packet_id) - if event is None: - _LOGGER.error("Invalid device: %s", packet_id) - continue - device_id = slugify(event.device.id_string.lower()) - if device_id in RFX_DEVICES: - continue - _LOGGER.debug("Add %s rfxtrx", entity_info[ATTR_NAME]) - - # Check if i must fire event - fire_event = entity_info[ATTR_FIRE_EVENT] - datas = {ATTR_STATE: False, ATTR_FIRE_EVENT: fire_event} - - new_device = device(entity_info[ATTR_NAME], event, datas, - signal_repetitions) - RFX_DEVICES[device_id] = new_device - devices.append(new_device) - return devices - - -def get_new_device(event, config, device): - """Add entity if not exist and the automatic_add is True.""" - device_id = slugify(event.device.id_string.lower()) - if device_id in RFX_DEVICES: - return - - if not config[ATTR_AUTOMATIC_ADD]: - return - - pkt_id = "".join("{0:02x}".format(x) for x in event.data) - _LOGGER.debug( - "Automatic add %s rfxtrx device (Class: %s Sub: %s Packet_id: %s)", - device_id, - event.device.__class__.__name__, - event.device.subtype, - pkt_id - ) - datas = {ATTR_STATE: False, ATTR_FIRE_EVENT: False} - signal_repetitions = config[CONF_SIGNAL_REPETITIONS] - new_device = device(pkt_id, event, datas, - signal_repetitions) - RFX_DEVICES[device_id] = new_device - return new_device - - -def apply_received_command(event): - """Apply command from rfxtrx.""" - device_id = slugify(event.device.id_string.lower()) - # Check if entity exists or previously added automatically - if device_id not in RFX_DEVICES: - return - - _LOGGER.debug( - "Device_id: %s device_update. Command: %s", - device_id, - event.values['Command'] - ) - - if event.values['Command'] == 'On'\ - or event.values['Command'] == 'Off': - - # Update the rfxtrx device state - is_on = event.values['Command'] == 'On' - RFX_DEVICES[device_id].update_state(is_on) - - elif hasattr(RFX_DEVICES[device_id], 'brightness')\ - and event.values['Command'] == 'Set level': - _brightness = (event.values['Dim level'] * 255 // 100) - - # Update the rfxtrx device state - is_on = _brightness > 0 - RFX_DEVICES[device_id].update_state(is_on, _brightness) - - # Fire event - if RFX_DEVICES[device_id].should_fire_event: - RFX_DEVICES[device_id].hass.bus.fire( - EVENT_BUTTON_PRESSED, { - ATTR_ENTITY_ID: - RFX_DEVICES[device_id].entity_id, - ATTR_STATE: event.values['Command'].lower() - } - ) - _LOGGER.debug( - "Rfxtrx fired event: (event_type: %s, %s: %s, %s: %s)", - EVENT_BUTTON_PRESSED, - ATTR_ENTITY_ID, - RFX_DEVICES[device_id].entity_id, - ATTR_STATE, - event.values['Command'].lower() - ) - - -class RfxtrxDevice(Entity): - """Represents a Rfxtrx device. - - Contains the common logic for Rfxtrx lights and switches. - """ - - def __init__(self, name, event, datas, signal_repetitions): - """Initialize the device.""" - self.signal_repetitions = signal_repetitions - self._name = name - self._event = event - self._state = datas[ATTR_STATE] - self._should_fire_event = datas[ATTR_FIRE_EVENT] - self._brightness = 0 - self.added_to_hass = False - - async def async_added_to_hass(self): - """Subscribe RFXtrx events.""" - self.added_to_hass = True - - @property - def should_poll(self): - """No polling needed for a RFXtrx switch.""" - return False - - @property - def name(self): - """Return the name of the device if any.""" - return self._name - - @property - def should_fire_event(self): - """Return is the device must fire event.""" - return self._should_fire_event - - @property - def is_on(self): - """Return true if device is on.""" - return self._state - - @property - def assumed_state(self): - """Return true if unable to access real state of entity.""" - return True - - def turn_off(self, **kwargs): - """Turn the device off.""" - self._send_command("turn_off") - - def update_state(self, state, brightness=0): - """Update det state of the device.""" - self._state = state - self._brightness = brightness - if self.added_to_hass: - self.schedule_update_ha_state() - - def _send_command(self, command, brightness=0): - if not self._event: - return - rfx_object = self.hass.data[DATA_RFXOBJECT] - - if command == "turn_on": - for _ in range(self.signal_repetitions): - self._event.device.send_on(rfx_object.transport) - self._state = True - - elif command == "dim": - for _ in range(self.signal_repetitions): - self._event.device.send_dim(rfx_object.transport, brightness) - self._state = True - - elif command == 'turn_off': - for _ in range(self.signal_repetitions): - self._event.device.send_off(rfx_object.transport) - self._state = False - self._brightness = 0 - - elif command == "roll_up": - for _ in range(self.signal_repetitions): - self._event.device.send_open(rfx_object.transport) - - elif command == "roll_down": - for _ in range(self.signal_repetitions): - self._event.device.send_close(rfx_object.transport) - - elif command == "stop_roll": - for _ in range(self.signal_repetitions): - self._event.device.send_stop(rfx_object.transport) - - if self.added_to_hass: - self.schedule_update_ha_state() diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py new file mode 100644 index 0000000000000..a7b703ef2ab9c --- /dev/null +++ b/homeassistant/components/rfxtrx/__init__.py @@ -0,0 +1,387 @@ +"""Support for RFXtrx devices.""" +from collections import OrderedDict +import logging + +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_NAME, ATTR_STATE, CONF_DEVICE, CONF_DEVICES, + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.util import slugify + +REQUIREMENTS = ['pyRFXtrx==0.23'] + +DOMAIN = 'rfxtrx' + +DEFAULT_SIGNAL_REPETITIONS = 1 + +ATTR_AUTOMATIC_ADD = 'automatic_add' +ATTR_DEVICE = 'device' +ATTR_DEBUG = 'debug' +ATTR_FIRE_EVENT = 'fire_event' +ATTR_DATA_TYPE = 'data_type' +ATTR_DUMMY = 'dummy' +CONF_DATA_BITS = 'data_bits' +CONF_AUTOMATIC_ADD = 'automatic_add' +CONF_DATA_TYPE = 'data_type' +CONF_SIGNAL_REPETITIONS = 'signal_repetitions' +CONF_FIRE_EVENT = 'fire_event' +CONF_DUMMY = 'dummy' +CONF_DEBUG = 'debug' +CONF_OFF_DELAY = 'off_delay' +EVENT_BUTTON_PRESSED = 'button_pressed' + +DATA_TYPES = OrderedDict([ + ('Temperature', TEMP_CELSIUS), + ('Temperature2', TEMP_CELSIUS), + ('Humidity', '%'), + ('Barometer', ''), + ('Wind direction', ''), + ('Rain rate', ''), + ('Energy usage', 'W'), + ('Total usage', 'W'), + ('Sound', ''), + ('Sensor Status', ''), + ('Counter value', ''), + ('UV', 'uv')]) + +RECEIVED_EVT_SUBSCRIBERS = [] +RFX_DEVICES = {} +_LOGGER = logging.getLogger(__name__) +DATA_RFXOBJECT = 'rfxobject' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_DEVICE): cv.string, + vol.Optional(CONF_DEBUG, default=False): cv.boolean, + vol.Optional(CONF_DUMMY, default=False): cv.boolean, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the RFXtrx component.""" + # Declare the Handle event + def handle_receive(event): + """Handle received messages from RFXtrx gateway.""" + # Log RFXCOM event + if not event.device.id_string: + return + _LOGGER.debug("Receive RFXCOM event from " + "(Device_id: %s Class: %s Sub: %s, Pkt_id: %s)", + slugify(event.device.id_string.lower()), + event.device.__class__.__name__, + event.device.subtype, + "".join("{0:02x}".format(x) for x in event.data)) + + # Callback to HA registered components. + for subscriber in RECEIVED_EVT_SUBSCRIBERS: + subscriber(event) + + # Try to load the RFXtrx module. + import RFXtrx as rfxtrxmod + + device = config[DOMAIN][ATTR_DEVICE] + debug = config[DOMAIN][ATTR_DEBUG] + dummy_connection = config[DOMAIN][ATTR_DUMMY] + + if dummy_connection: + rfx_object = rfxtrxmod.Connect( + device, None, debug=debug, + transport_protocol=rfxtrxmod.DummyTransport2) + else: + rfx_object = rfxtrxmod.Connect(device, None, debug=debug) + + def _start_rfxtrx(event): + rfx_object.event_callback = handle_receive + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start_rfxtrx) + + def _shutdown_rfxtrx(event): + """Close connection with RFXtrx.""" + rfx_object.close_connection() + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown_rfxtrx) + + hass.data[DATA_RFXOBJECT] = rfx_object + return True + + +def get_rfx_object(packetid): + """Return the RFXObject with the packetid.""" + import RFXtrx as rfxtrxmod + + try: + binarypacket = bytearray.fromhex(packetid) + except ValueError: + return None + + pkt = rfxtrxmod.lowlevel.parse(binarypacket) + if pkt is None: + return None + if isinstance(pkt, rfxtrxmod.lowlevel.SensorPacket): + obj = rfxtrxmod.SensorEvent(pkt) + elif isinstance(pkt, rfxtrxmod.lowlevel.Status): + obj = rfxtrxmod.StatusEvent(pkt) + else: + obj = rfxtrxmod.ControlEvent(pkt) + return obj + + +def get_pt2262_deviceid(device_id, nb_data_bits): + """Extract and return the address bits from a Lighting4/PT2262 packet.""" + if nb_data_bits is None: + return + import binascii + try: + data = bytearray.fromhex(device_id) + except ValueError: + return None + mask = 0xFF & ~((1 << nb_data_bits) - 1) + + data[len(data)-1] &= mask + + return binascii.hexlify(data) + + +def get_pt2262_cmd(device_id, data_bits): + """Extract and return the data bits from a Lighting4/PT2262 packet.""" + try: + data = bytearray.fromhex(device_id) + except ValueError: + return None + + mask = 0xFF & ((1 << data_bits) - 1) + + return hex(data[-1] & mask) + + +def get_pt2262_device(device_id): + """Look for the device which id matches the given device_id parameter.""" + for device in RFX_DEVICES.values(): + if (hasattr(device, 'is_lighting4') and + device.masked_id is not None and + device.masked_id == get_pt2262_deviceid(device_id, + device.data_bits)): + _LOGGER.debug("rfxtrx: found matching device %s for %s", + device_id, + device.masked_id) + return device + return None + + +def find_possible_pt2262_device(device_id): + """Look for the device which id matches the given device_id parameter.""" + for dev_id, device in RFX_DEVICES.items(): + if hasattr(device, 'is_lighting4') and len(dev_id) == len(device_id): + size = None + for i, (char1, char2) in enumerate(zip(dev_id, device_id)): + if char1 != char2: + break + size = i + + if size is not None: + size = len(dev_id) - size - 1 + _LOGGER.info("rfxtrx: found possible device %s for %s " + "with the following configuration:\n" + "data_bits=%d\n" + "command_on=0x%s\n" + "command_off=0x%s\n", + device_id, + dev_id, + size * 4, + dev_id[-size:], device_id[-size:]) + return device + + return None + + +def get_devices_from_config(config, device): + """Read rfxtrx configuration.""" + signal_repetitions = config[CONF_SIGNAL_REPETITIONS] + + devices = [] + for packet_id, entity_info in config[CONF_DEVICES].items(): + event = get_rfx_object(packet_id) + if event is None: + _LOGGER.error("Invalid device: %s", packet_id) + continue + device_id = slugify(event.device.id_string.lower()) + if device_id in RFX_DEVICES: + continue + _LOGGER.debug("Add %s rfxtrx", entity_info[ATTR_NAME]) + + # Check if i must fire event + fire_event = entity_info[ATTR_FIRE_EVENT] + datas = {ATTR_STATE: False, ATTR_FIRE_EVENT: fire_event} + + new_device = device(entity_info[ATTR_NAME], event, datas, + signal_repetitions) + RFX_DEVICES[device_id] = new_device + devices.append(new_device) + return devices + + +def get_new_device(event, config, device): + """Add entity if not exist and the automatic_add is True.""" + device_id = slugify(event.device.id_string.lower()) + if device_id in RFX_DEVICES: + return + + if not config[ATTR_AUTOMATIC_ADD]: + return + + pkt_id = "".join("{0:02x}".format(x) for x in event.data) + _LOGGER.debug( + "Automatic add %s rfxtrx device (Class: %s Sub: %s Packet_id: %s)", + device_id, + event.device.__class__.__name__, + event.device.subtype, + pkt_id + ) + datas = {ATTR_STATE: False, ATTR_FIRE_EVENT: False} + signal_repetitions = config[CONF_SIGNAL_REPETITIONS] + new_device = device(pkt_id, event, datas, + signal_repetitions) + RFX_DEVICES[device_id] = new_device + return new_device + + +def apply_received_command(event): + """Apply command from rfxtrx.""" + device_id = slugify(event.device.id_string.lower()) + # Check if entity exists or previously added automatically + if device_id not in RFX_DEVICES: + return + + _LOGGER.debug( + "Device_id: %s device_update. Command: %s", + device_id, + event.values['Command'] + ) + + if event.values['Command'] == 'On'\ + or event.values['Command'] == 'Off': + + # Update the rfxtrx device state + is_on = event.values['Command'] == 'On' + RFX_DEVICES[device_id].update_state(is_on) + + elif hasattr(RFX_DEVICES[device_id], 'brightness')\ + and event.values['Command'] == 'Set level': + _brightness = (event.values['Dim level'] * 255 // 100) + + # Update the rfxtrx device state + is_on = _brightness > 0 + RFX_DEVICES[device_id].update_state(is_on, _brightness) + + # Fire event + if RFX_DEVICES[device_id].should_fire_event: + RFX_DEVICES[device_id].hass.bus.fire( + EVENT_BUTTON_PRESSED, { + ATTR_ENTITY_ID: + RFX_DEVICES[device_id].entity_id, + ATTR_STATE: event.values['Command'].lower() + } + ) + _LOGGER.debug( + "Rfxtrx fired event: (event_type: %s, %s: %s, %s: %s)", + EVENT_BUTTON_PRESSED, + ATTR_ENTITY_ID, + RFX_DEVICES[device_id].entity_id, + ATTR_STATE, + event.values['Command'].lower() + ) + + +class RfxtrxDevice(Entity): + """Represents a Rfxtrx device. + + Contains the common logic for Rfxtrx lights and switches. + """ + + def __init__(self, name, event, datas, signal_repetitions): + """Initialize the device.""" + self.signal_repetitions = signal_repetitions + self._name = name + self._event = event + self._state = datas[ATTR_STATE] + self._should_fire_event = datas[ATTR_FIRE_EVENT] + self._brightness = 0 + self.added_to_hass = False + + async def async_added_to_hass(self): + """Subscribe RFXtrx events.""" + self.added_to_hass = True + + @property + def should_poll(self): + """No polling needed for a RFXtrx switch.""" + return False + + @property + def name(self): + """Return the name of the device if any.""" + return self._name + + @property + def should_fire_event(self): + """Return is the device must fire event.""" + return self._should_fire_event + + @property + def is_on(self): + """Return true if device is on.""" + return self._state + + @property + def assumed_state(self): + """Return true if unable to access real state of entity.""" + return True + + def turn_off(self, **kwargs): + """Turn the device off.""" + self._send_command("turn_off") + + def update_state(self, state, brightness=0): + """Update det state of the device.""" + self._state = state + self._brightness = brightness + if self.added_to_hass: + self.schedule_update_ha_state() + + def _send_command(self, command, brightness=0): + if not self._event: + return + rfx_object = self.hass.data[DATA_RFXOBJECT] + + if command == "turn_on": + for _ in range(self.signal_repetitions): + self._event.device.send_on(rfx_object.transport) + self._state = True + + elif command == "dim": + for _ in range(self.signal_repetitions): + self._event.device.send_dim(rfx_object.transport, brightness) + self._state = True + + elif command == 'turn_off': + for _ in range(self.signal_repetitions): + self._event.device.send_off(rfx_object.transport) + self._state = False + self._brightness = 0 + + elif command == "roll_up": + for _ in range(self.signal_repetitions): + self._event.device.send_open(rfx_object.transport) + + elif command == "roll_down": + for _ in range(self.signal_repetitions): + self._event.device.send_close(rfx_object.transport) + + elif command == "stop_roll": + for _ in range(self.signal_repetitions): + self._event.device.send_stop(rfx_object.transport) + + if self.added_to_hass: + self.schedule_update_ha_state() diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py new file mode 100644 index 0000000000000..9a49bd02b97fc --- /dev/null +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -0,0 +1,223 @@ +"""Support for RFXtrx binary sensors.""" +import logging + +import voluptuous as vol + +from homeassistant.components import rfxtrx +from homeassistant.components.binary_sensor import ( + DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice) +from homeassistant.components.rfxtrx import ( + ATTR_NAME, CONF_AUTOMATIC_ADD, CONF_DATA_BITS, CONF_DEVICES, + CONF_FIRE_EVENT, CONF_OFF_DELAY) +from homeassistant.const import ( + CONF_COMMAND_OFF, CONF_COMMAND_ON, CONF_DEVICE_CLASS, CONF_NAME) +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import event as evt +from homeassistant.util import dt as dt_util +from homeassistant.util import slugify + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['rfxtrx'] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_DEVICES, default={}): { + cv.string: vol.Schema({ + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean, + vol.Optional(CONF_OFF_DELAY): + 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_AUTOMATIC_ADD, default=False): cv.boolean, +}, extra=vol.ALLOW_EXTRA) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Binary Sensor platform to RFXtrx.""" + import RFXtrx as rfxtrxmod + sensors = [] + + for packet_id, entity in config[CONF_DEVICES].items(): + event = rfxtrx.get_rfx_object(packet_id) + device_id = slugify(event.device.id_string.lower()) + + if device_id in rfxtrx.RFX_DEVICES: + continue + + if entity.get(CONF_DATA_BITS) is not None: + _LOGGER.debug( + "Masked device id: %s", rfxtrx.get_pt2262_deviceid( + device_id, entity.get(CONF_DATA_BITS))) + + _LOGGER.debug("Add %s rfxtrx.binary_sensor (class %s)", + entity[ATTR_NAME], entity.get(CONF_DEVICE_CLASS)) + + device = RfxtrxBinarySensor( + event, entity.get(CONF_NAME), entity.get(CONF_DEVICE_CLASS), + entity[CONF_FIRE_EVENT], entity.get(CONF_OFF_DELAY), + entity.get(CONF_DATA_BITS), entity.get(CONF_COMMAND_ON), + entity.get(CONF_COMMAND_OFF)) + device.hass = hass + sensors.append(device) + rfxtrx.RFX_DEVICES[device_id] = device + + add_entities(sensors) + + def binary_sensor_update(event): + """Call for control updates from the RFXtrx gateway.""" + if not isinstance(event, rfxtrxmod.ControlEvent): + return + + device_id = slugify(event.device.id_string.lower()) + + if device_id in rfxtrx.RFX_DEVICES: + sensor = rfxtrx.RFX_DEVICES[device_id] + else: + sensor = rfxtrx.get_pt2262_device(device_id) + + if sensor is None: + # Add the entity if not exists and automatic_add is True + if not config[CONF_AUTOMATIC_ADD]: + return + + if event.device.packettype == 0x13: + poss_dev = rfxtrx.find_possible_pt2262_device(device_id) + if poss_dev is not None: + poss_id = slugify(poss_dev.event.device.id_string.lower()) + _LOGGER.debug( + "Found possible matching device ID: %s", poss_id) + + pkt_id = "".join("{0:02x}".format(x) for x in event.data) + sensor = RfxtrxBinarySensor(event, pkt_id) + sensor.hass = hass + rfxtrx.RFX_DEVICES[device_id] = sensor + add_entities([sensor]) + _LOGGER.info( + "Added binary sensor %s (Device ID: %s Class: %s Sub: %s)", + pkt_id, slugify(event.device.id_string.lower()), + event.device.__class__.__name__, event.device.subtype) + + elif not isinstance(sensor, RfxtrxBinarySensor): + return + else: + _LOGGER.debug( + "Binary sensor update (Device ID: %s Class: %s Sub: %s)", + slugify(event.device.id_string.lower()), + event.device.__class__.__name__, event.device.subtype) + + if sensor.is_lighting4: + if sensor.data_bits is not None: + cmd = rfxtrx.get_pt2262_cmd(device_id, sensor.data_bits) + sensor.apply_cmd(int(cmd, 16)) + else: + sensor.update_state(True) + else: + rfxtrx.apply_received_command(event) + + if (sensor.is_on and sensor.off_delay is not None and + sensor.delay_listener is None): + + def off_delay_listener(now): + """Switch device off after a delay.""" + sensor.delay_listener = None + sensor.update_state(False) + + sensor.delay_listener = evt.track_point_in_time( + hass, off_delay_listener, dt_util.utcnow() + sensor.off_delay) + + # Subscribe to main RFXtrx events + if binary_sensor_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS: + rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(binary_sensor_update) + + +class RfxtrxBinarySensor(BinarySensorDevice): + """A representation of a RFXtrx binary sensor.""" + + def __init__(self, event, name, device_class=None, + should_fire=False, off_delay=None, data_bits=None, + cmd_on=None, cmd_off=None): + """Initialize the RFXtrx sensor.""" + self.event = event + self._name = name + self._should_fire_event = should_fire + self._device_class = device_class + self._off_delay = off_delay + self._state = False + self.is_lighting4 = (event.device.packettype == 0x13) + self.delay_listener = None + self._data_bits = data_bits + self._cmd_on = cmd_on + self._cmd_off = cmd_off + + if data_bits is not None: + self._masked_id = rfxtrx.get_pt2262_deviceid( + event.device.id_string.lower(), data_bits) + else: + self._masked_id = None + + @property + def name(self): + """Return the device name.""" + return self._name + + @property + def masked_id(self): + """Return the masked device id (isolated address bits).""" + return self._masked_id + + @property + def data_bits(self): + """Return the number of data bits.""" + return self._data_bits + + @property + def cmd_on(self): + """Return the value of the 'On' command.""" + return self._cmd_on + + @property + def cmd_off(self): + """Return the value of the 'Off' command.""" + return self._cmd_off + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def should_fire_event(self): + """Return is the device must fire event.""" + return self._should_fire_event + + @property + def device_class(self): + """Return the sensor class.""" + return self._device_class + + @property + def off_delay(self): + """Return the off_delay attribute value.""" + return self._off_delay + + @property + def is_on(self): + """Return true if the sensor state is True.""" + return self._state + + def apply_cmd(self, cmd): + """Apply a command for updating the state.""" + if cmd == self.cmd_on: + self.update_state(True) + elif cmd == self.cmd_off: + self.update_state(False) + + def update_state(self, state): + """Update the state of the device.""" + self._state = state + self.schedule_update_ha_state() diff --git a/homeassistant/components/rfxtrx/cover.py b/homeassistant/components/rfxtrx/cover.py new file mode 100644 index 0000000000000..5a657923683b3 --- /dev/null +++ b/homeassistant/components/rfxtrx/cover.py @@ -0,0 +1,75 @@ +"""Support for RFXtrx covers.""" +import voluptuous as vol + +from homeassistant.components import rfxtrx +from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME +from homeassistant.components.rfxtrx import ( + CONF_AUTOMATIC_ADD, CONF_FIRE_EVENT, DEFAULT_SIGNAL_REPETITIONS, + CONF_SIGNAL_REPETITIONS, CONF_DEVICES) +from homeassistant.helpers import config_validation as cv + +DEPENDENCIES = ['rfxtrx'] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_DEVICES, default={}): { + cv.string: vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean + }) + }, + vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean, + vol.Optional(CONF_SIGNAL_REPETITIONS, default=DEFAULT_SIGNAL_REPETITIONS): + vol.Coerce(int), +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the RFXtrx cover.""" + import RFXtrx as rfxtrxmod + + covers = rfxtrx.get_devices_from_config(config, RfxtrxCover) + add_entities(covers) + + def cover_update(event): + """Handle cover updates from the RFXtrx gateway.""" + if not isinstance(event.device, rfxtrxmod.LightingDevice) or \ + event.device.known_to_be_dimmable or \ + not event.device.known_to_be_rollershutter: + return + + new_device = rfxtrx.get_new_device(event, config, RfxtrxCover) + if new_device: + add_entities([new_device]) + + rfxtrx.apply_received_command(event) + + # Subscribe to main RFXtrx events + if cover_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS: + rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(cover_update) + + +class RfxtrxCover(rfxtrx.RfxtrxDevice, CoverDevice): + """Representation of a RFXtrx cover.""" + + @property + def should_poll(self): + """Return the polling state. No polling available in RFXtrx cover.""" + return False + + @property + def is_closed(self): + """Return if the cover is closed.""" + return None + + def open_cover(self, **kwargs): + """Move the cover up.""" + self._send_command("roll_up") + + def close_cover(self, **kwargs): + """Move the cover down.""" + self._send_command("roll_down") + + def stop_cover(self, **kwargs): + """Stop the cover.""" + self._send_command("stop_roll") diff --git a/homeassistant/components/rfxtrx/light.py b/homeassistant/components/rfxtrx/light.py new file mode 100644 index 0000000000000..d0b75c2f9627e --- /dev/null +++ b/homeassistant/components/rfxtrx/light.py @@ -0,0 +1,80 @@ +"""Support for RFXtrx lights.""" +import logging + +import voluptuous as vol + +from homeassistant.components import rfxtrx +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light, PLATFORM_SCHEMA) +from homeassistant.const import CONF_NAME +from homeassistant.components.rfxtrx import ( + CONF_AUTOMATIC_ADD, CONF_FIRE_EVENT, DEFAULT_SIGNAL_REPETITIONS, + CONF_SIGNAL_REPETITIONS, CONF_DEVICES) +from homeassistant.helpers import config_validation as cv + +DEPENDENCIES = ['rfxtrx'] + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_DEVICES, default={}): { + cv.string: vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean + }) + }, + vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean, + vol.Optional(CONF_SIGNAL_REPETITIONS, default=DEFAULT_SIGNAL_REPETITIONS): + vol.Coerce(int), +}) + +SUPPORT_RFXTRX = SUPPORT_BRIGHTNESS + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the RFXtrx platform.""" + import RFXtrx as rfxtrxmod + + lights = rfxtrx.get_devices_from_config(config, RfxtrxLight) + add_entities(lights) + + def light_update(event): + """Handle light updates from the RFXtrx gateway.""" + if not isinstance(event.device, rfxtrxmod.LightingDevice) or \ + not event.device.known_to_be_dimmable: + return + + new_device = rfxtrx.get_new_device(event, config, RfxtrxLight) + if new_device: + add_entities([new_device]) + + rfxtrx.apply_received_command(event) + + # Subscribe to main RFXtrx events + if light_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS: + rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(light_update) + + +class RfxtrxLight(rfxtrx.RfxtrxDevice, Light): + """Representation of a RFXtrx light.""" + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + return self._brightness + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_RFXTRX + + def turn_on(self, **kwargs): + """Turn the light on.""" + brightness = kwargs.get(ATTR_BRIGHTNESS) + if brightness is None: + self._brightness = 255 + self._send_command('turn_on') + else: + self._brightness = brightness + _brightness = (brightness * 100 // 255) + self._send_command('dim', _brightness) diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py new file mode 100644 index 0000000000000..74c6463556334 --- /dev/null +++ b/homeassistant/components/rfxtrx/sensor.py @@ -0,0 +1,145 @@ +"""Support for RFXtrx sensors.""" +import logging + +import voluptuous as vol + +from homeassistant.components import rfxtrx +from homeassistant.components.rfxtrx import ( + ATTR_DATA_TYPE, ATTR_FIRE_EVENT, CONF_AUTOMATIC_ADD, CONF_DATA_TYPE, + CONF_DEVICES, CONF_FIRE_EVENT, DATA_TYPES) +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME, CONF_NAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.util import slugify + +DEPENDENCIES = ['rfxtrx'] + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_DEVICES, default={}): { + cv.string: vol.Schema({ + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean, + vol.Optional(CONF_DATA_TYPE, default=[]): + vol.All(cv.ensure_list, [vol.In(DATA_TYPES.keys())]), + }) + }, + vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean, +}, extra=vol.ALLOW_EXTRA) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the RFXtrx platform.""" + from RFXtrx import SensorEvent + sensors = [] + for packet_id, entity_info in config[CONF_DEVICES].items(): + event = rfxtrx.get_rfx_object(packet_id) + device_id = "sensor_{}".format(slugify(event.device.id_string.lower())) + if device_id in rfxtrx.RFX_DEVICES: + continue + _LOGGER.info("Add %s rfxtrx.sensor", entity_info[ATTR_NAME]) + + sub_sensors = {} + data_types = entity_info[ATTR_DATA_TYPE] + if not data_types: + data_types = [''] + for data_type in DATA_TYPES: + if data_type in event.values: + data_types = [data_type] + break + for _data_type in data_types: + new_sensor = RfxtrxSensor(None, entity_info[ATTR_NAME], + _data_type, entity_info[ATTR_FIRE_EVENT]) + sensors.append(new_sensor) + sub_sensors[_data_type] = new_sensor + rfxtrx.RFX_DEVICES[device_id] = sub_sensors + add_entities(sensors) + + def sensor_update(event): + """Handle sensor updates from the RFXtrx gateway.""" + if not isinstance(event, SensorEvent): + return + + device_id = "sensor_" + slugify(event.device.id_string.lower()) + + if device_id in rfxtrx.RFX_DEVICES: + sensors = rfxtrx.RFX_DEVICES[device_id] + for data_type in sensors: + # Some multi-sensor devices send individual messages for each + # of their sensors. Update only if event contains the + # right data_type for the sensor. + if data_type not in event.values: + continue + sensor = sensors[data_type] + sensor.event = event + # Fire event + if sensor.should_fire_event: + sensor.hass.bus.fire( + "signal_received", { + ATTR_ENTITY_ID: sensor.entity_id, + } + ) + return + + # Add entity if not exist and the automatic_add is True + if not config[CONF_AUTOMATIC_ADD]: + return + + pkt_id = "".join("{0:02x}".format(x) for x in event.data) + _LOGGER.info("Automatic add rfxtrx.sensor: %s", pkt_id) + + data_type = '' + for _data_type in DATA_TYPES: + if _data_type in event.values: + data_type = _data_type + break + new_sensor = RfxtrxSensor(event, pkt_id, data_type) + sub_sensors = {} + sub_sensors[new_sensor.data_type] = new_sensor + rfxtrx.RFX_DEVICES[device_id] = sub_sensors + add_entities([new_sensor]) + + if sensor_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS: + rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(sensor_update) + + +class RfxtrxSensor(Entity): + """Representation of a RFXtrx sensor.""" + + def __init__(self, event, name, data_type, should_fire_event=False): + """Initialize the sensor.""" + self.event = event + self._name = name + self.should_fire_event = should_fire_event + self.data_type = data_type + self._unit_of_measurement = DATA_TYPES.get(data_type, '') + + def __str__(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + if not self.event: + return None + return self.event.values.get(self.data_type) + + @property + def name(self): + """Get the name of the sensor.""" + return "{} {}".format(self._name, self.data_type) + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + if not self.event: + return None + return self.event.values + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return self._unit_of_measurement diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py new file mode 100644 index 0000000000000..141cf2c2c1a61 --- /dev/null +++ b/homeassistant/components/rfxtrx/switch.py @@ -0,0 +1,62 @@ +"""Support for RFXtrx switches.""" +import logging + +import voluptuous as vol + +from homeassistant.components import rfxtrx +from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.rfxtrx import ( + CONF_AUTOMATIC_ADD, CONF_FIRE_EVENT, DEFAULT_SIGNAL_REPETITIONS, + CONF_SIGNAL_REPETITIONS, CONF_DEVICES) +from homeassistant.helpers import config_validation as cv +from homeassistant.const import CONF_NAME + +DEPENDENCIES = ['rfxtrx'] + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_DEVICES, default={}): { + cv.string: vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean, + }) + }, + vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean, + vol.Optional(CONF_SIGNAL_REPETITIONS, default=DEFAULT_SIGNAL_REPETITIONS): + vol.Coerce(int), +}) + + +def setup_platform(hass, config, add_entities_callback, discovery_info=None): + """Set up the RFXtrx platform.""" + import RFXtrx as rfxtrxmod + + # Add switch from config file + switches = rfxtrx.get_devices_from_config(config, RfxtrxSwitch) + add_entities_callback(switches) + + def switch_update(event): + """Handle sensor updates from the RFXtrx gateway.""" + if not isinstance(event.device, rfxtrxmod.LightingDevice) or \ + event.device.known_to_be_dimmable or \ + event.device.known_to_be_rollershutter: + return + + new_device = rfxtrx.get_new_device(event, config, RfxtrxSwitch) + if new_device: + add_entities_callback([new_device]) + + rfxtrx.apply_received_command(event) + + # Subscribe to main RFXtrx events + if switch_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS: + rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(switch_update) + + +class RfxtrxSwitch(rfxtrx.RfxtrxDevice, SwitchDevice): + """Representation of a RFXtrx switch.""" + + def turn_on(self, **kwargs): + """Turn the device on.""" + self._send_command("turn_on") diff --git a/homeassistant/components/ring.py b/homeassistant/components/ring.py deleted file mode 100644 index 2e048caa52f80..0000000000000 --- a/homeassistant/components/ring.py +++ /dev/null @@ -1,60 +0,0 @@ -""" -Support for Ring Doorbell/Chimes. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/ring/ -""" -import logging -from requests.exceptions import HTTPError, ConnectTimeout - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD - -REQUIREMENTS = ['ring_doorbell==0.2.2'] - -_LOGGER = logging.getLogger(__name__) - -CONF_ATTRIBUTION = "Data provided by Ring.com" - -NOTIFICATION_ID = 'ring_notification' -NOTIFICATION_TITLE = 'Ring Setup' - -DATA_RING = 'ring' -DOMAIN = 'ring' -DEFAULT_CACHEDB = '.ring_cache.pickle' -DEFAULT_ENTITY_NAMESPACE = 'ring' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - }), -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the Ring component.""" - conf = config[DOMAIN] - username = conf[CONF_USERNAME] - password = conf[CONF_PASSWORD] - - try: - from ring_doorbell import Ring - - cache = hass.config.path(DEFAULT_CACHEDB) - ring = Ring(username=username, password=password, cache_file=cache) - if not ring.is_connected: - return False - hass.data['ring'] = ring - except (ConnectTimeout, HTTPError) as ex: - _LOGGER.error("Unable to connect to Ring service: %s", str(ex)) - hass.components.persistent_notification.create( - 'Error: {}
' - 'You will need to restart hass after fixing.' - ''.format(ex), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) - return False - return True diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py new file mode 100644 index 0000000000000..526388a0918d2 --- /dev/null +++ b/homeassistant/components/ring/__init__.py @@ -0,0 +1,55 @@ +"""Support for Ring Doorbell/Chimes.""" +import logging +from requests.exceptions import HTTPError, ConnectTimeout + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD + +REQUIREMENTS = ['ring_doorbell==0.2.2'] + +_LOGGER = logging.getLogger(__name__) + +CONF_ATTRIBUTION = "Data provided by Ring.com" + +NOTIFICATION_ID = 'ring_notification' +NOTIFICATION_TITLE = 'Ring Setup' + +DATA_RING = 'ring' +DOMAIN = 'ring' +DEFAULT_CACHEDB = '.ring_cache.pickle' +DEFAULT_ENTITY_NAMESPACE = 'ring' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Ring component.""" + conf = config[DOMAIN] + username = conf[CONF_USERNAME] + password = conf[CONF_PASSWORD] + + try: + from ring_doorbell import Ring + + cache = hass.config.path(DEFAULT_CACHEDB) + ring = Ring(username=username, password=password, cache_file=cache) + if not ring.is_connected: + return False + hass.data['ring'] = ring + except (ConnectTimeout, HTTPError) as ex: + _LOGGER.error("Unable to connect to Ring service: %s", str(ex)) + hass.components.persistent_notification.create( + 'Error: {}
' + 'You will need to restart hass after fixing.' + ''.format(ex), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + return False + return True diff --git a/homeassistant/components/roku.py b/homeassistant/components/roku.py deleted file mode 100644 index 5ceebb3dee5eb..0000000000000 --- a/homeassistant/components/roku.py +++ /dev/null @@ -1,115 +0,0 @@ -""" -Support for Roku platform. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/roku/ -""" -import logging - -import voluptuous as vol - -from homeassistant.components.discovery import SERVICE_ROKU -from homeassistant.const import CONF_HOST -from homeassistant.helpers import discovery -import homeassistant.helpers.config_validation as cv - -REQUIREMENTS = ['python-roku==3.1.5'] - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'roku' - -SERVICE_SCAN = 'roku_scan' - -ATTR_ROKU = 'roku' - -DATA_ROKU = 'data_roku' - -NOTIFICATION_ID = 'roku_notification' -NOTIFICATION_TITLE = 'Roku Setup' -NOTIFICATION_SCAN_ID = 'roku_scan_notification' -NOTIFICATION_SCAN_TITLE = 'Roku Scan' - - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ - vol.Required(CONF_HOST): cv.string - })]) -}, extra=vol.ALLOW_EXTRA) - -# Currently no attributes but it might change later -ROKU_SCAN_SCHEMA = vol.Schema({}) - - -def setup(hass, config): - """Set up the Roku component.""" - hass.data[DATA_ROKU] = {} - - def service_handler(service): - """Handle service calls.""" - if service.service == SERVICE_SCAN: - scan_for_rokus(hass) - - def roku_discovered(service, info): - """Set up an Roku that was auto discovered.""" - _setup_roku(hass, config, { - CONF_HOST: info['host'] - }) - - discovery.listen(hass, SERVICE_ROKU, roku_discovered) - - for conf in config.get(DOMAIN, []): - _setup_roku(hass, config, conf) - - hass.services.register( - DOMAIN, SERVICE_SCAN, service_handler, - schema=ROKU_SCAN_SCHEMA) - - return True - - -def scan_for_rokus(hass): - """Scan for devices and present a notification of the ones found.""" - from roku import Roku, RokuException - rokus = Roku.discover() - - devices = [] - for roku in rokus: - try: - r_info = roku.device_info - except RokuException: # skip non-roku device - continue - devices.append('Name: {0}
Host: {1}
'.format( - r_info.userdevicename if r_info.userdevicename - else "{} {}".format(r_info.modelname, r_info.sernum), - roku.host)) - if not devices: - devices = ['No device(s) found'] - - hass.components.persistent_notification.create( - 'The following devices were found:

' + - '

'.join(devices), - title=NOTIFICATION_SCAN_TITLE, - notification_id=NOTIFICATION_SCAN_ID) - - -def _setup_roku(hass, hass_config, roku_config): - """Set up a Roku.""" - from roku import Roku - host = roku_config[CONF_HOST] - - if host in hass.data[DATA_ROKU]: - return - - roku = Roku(host) - r_info = roku.device_info - - hass.data[DATA_ROKU][host] = { - ATTR_ROKU: r_info.sernum - } - - discovery.load_platform( - hass, 'media_player', DOMAIN, roku_config, hass_config) - - discovery.load_platform( - hass, 'remote', DOMAIN, roku_config, hass_config) diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py new file mode 100644 index 0000000000000..89bb1a9acb8fb --- /dev/null +++ b/homeassistant/components/roku/__init__.py @@ -0,0 +1,110 @@ +"""Support for Roku.""" +import logging + +import voluptuous as vol + +from homeassistant.components.discovery import SERVICE_ROKU +from homeassistant.const import CONF_HOST +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['python-roku==3.1.5'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'roku' + +SERVICE_SCAN = 'roku_scan' + +ATTR_ROKU = 'roku' + +DATA_ROKU = 'data_roku' + +NOTIFICATION_ID = 'roku_notification' +NOTIFICATION_TITLE = 'Roku Setup' +NOTIFICATION_SCAN_ID = 'roku_scan_notification' +NOTIFICATION_SCAN_TITLE = 'Roku Scan' + + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ + vol.Required(CONF_HOST): cv.string + })]) +}, extra=vol.ALLOW_EXTRA) + +# Currently no attributes but it might change later +ROKU_SCAN_SCHEMA = vol.Schema({}) + + +def setup(hass, config): + """Set up the Roku component.""" + hass.data[DATA_ROKU] = {} + + def service_handler(service): + """Handle service calls.""" + if service.service == SERVICE_SCAN: + scan_for_rokus(hass) + + def roku_discovered(service, info): + """Set up an Roku that was auto discovered.""" + _setup_roku(hass, config, { + CONF_HOST: info['host'] + }) + + discovery.listen(hass, SERVICE_ROKU, roku_discovered) + + for conf in config.get(DOMAIN, []): + _setup_roku(hass, config, conf) + + hass.services.register( + DOMAIN, SERVICE_SCAN, service_handler, + schema=ROKU_SCAN_SCHEMA) + + return True + + +def scan_for_rokus(hass): + """Scan for devices and present a notification of the ones found.""" + from roku import Roku, RokuException + rokus = Roku.discover() + + devices = [] + for roku in rokus: + try: + r_info = roku.device_info + except RokuException: # skip non-roku device + continue + devices.append('Name: {0}
Host: {1}
'.format( + r_info.userdevicename if r_info.userdevicename + else "{} {}".format(r_info.modelname, r_info.sernum), + roku.host)) + if not devices: + devices = ['No device(s) found'] + + hass.components.persistent_notification.create( + 'The following devices were found:

' + + '

'.join(devices), + title=NOTIFICATION_SCAN_TITLE, + notification_id=NOTIFICATION_SCAN_ID) + + +def _setup_roku(hass, hass_config, roku_config): + """Set up a Roku.""" + from roku import Roku + host = roku_config[CONF_HOST] + + if host in hass.data[DATA_ROKU]: + return + + roku = Roku(host) + r_info = roku.device_info + + hass.data[DATA_ROKU][host] = { + ATTR_ROKU: r_info.sernum + } + + discovery.load_platform( + hass, 'media_player', DOMAIN, roku_config, hass_config) + + discovery.load_platform( + hass, 'remote', DOMAIN, roku_config, hass_config) diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py new file mode 100644 index 0000000000000..3cf27af067433 --- /dev/null +++ b/homeassistant/components/roku/media_player.py @@ -0,0 +1,190 @@ +"""Support for the Roku media player.""" +import logging +import requests.exceptions + +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) +from homeassistant.const import (CONF_HOST, STATE_HOME, STATE_IDLE, + STATE_PLAYING) + +DEPENDENCIES = ['roku'] + +DEFAULT_PORT = 8060 + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_ROKU = SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK |\ + SUPPORT_PLAY_MEDIA | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\ + SUPPORT_SELECT_SOURCE | SUPPORT_PLAY + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Set up the Roku platform.""" + if not discovery_info: + return + + host = discovery_info[CONF_HOST] + async_add_entities([RokuDevice(host)], True) + + +class RokuDevice(MediaPlayerDevice): + """Representation of a Roku device on the network.""" + + def __init__(self, host): + """Initialize the Roku device.""" + from roku import Roku + + self.roku = Roku(host) + self.ip_address = host + self.channels = [] + self.current_app = None + self._device_info = {} + + def update(self): + """Retrieve latest state.""" + try: + self._device_info = self.roku.device_info + self.ip_address = self.roku.host + self.channels = self.get_source_list() + + if self.roku.current_app is not None: + self.current_app = self.roku.current_app + else: + self.current_app = None + except (requests.exceptions.ConnectionError, + requests.exceptions.ReadTimeout): + pass + + def get_source_list(self): + """Get the list of applications to be used as sources.""" + return ["Home"] + sorted(channel.name for channel in self.roku.apps) + + @property + def should_poll(self): + """Device should be polled.""" + return True + + @property + def name(self): + """Return the name of the device.""" + if self._device_info.userdevicename: + return self._device_info.userdevicename + return "Roku {}".format(self._device_info.sernum) + + @property + def state(self): + """Return the state of the device.""" + if self.current_app is None: + return None + + if (self.current_app.name == "Power Saver" or + self.current_app.is_screensaver): + return STATE_IDLE + if self.current_app.name == "Roku": + return STATE_HOME + if self.current_app.name is not None: + return STATE_PLAYING + + return None + + @property + def supported_features(self): + """Flag media player features that are supported.""" + return SUPPORT_ROKU + + @property + def unique_id(self): + """Return a unique, HASS-friendly identifier for this entity.""" + return self._device_info.sernum + + @property + def media_content_type(self): + """Content type of current playing media.""" + if self.current_app is None: + return None + if self.current_app.name == "Power Saver": + return None + if self.current_app.name == "Roku": + return None + return MEDIA_TYPE_MOVIE + + @property + def media_image_url(self): + """Image url of current playing media.""" + if self.current_app is None: + return None + if self.current_app.name == "Roku": + return None + if self.current_app.name == "Power Saver": + return None + if self.current_app.id is None: + return None + + return 'http://{0}:{1}/query/icon/{2}'.format( + self.ip_address, DEFAULT_PORT, self.current_app.id) + + @property + def app_name(self): + """Name of the current running app.""" + if self.current_app is not None: + return self.current_app.name + + @property + def app_id(self): + """Return the ID of the current running app.""" + if self.current_app is not None: + return self.current_app.id + + @property + def source(self): + """Return the current input source.""" + if self.current_app is not None: + return self.current_app.name + + @property + def source_list(self): + """List of available input sources.""" + return self.channels + + def media_play_pause(self): + """Send play/pause command.""" + if self.current_app is not None: + self.roku.play() + + def media_previous_track(self): + """Send previous track command.""" + if self.current_app is not None: + self.roku.reverse() + + def media_next_track(self): + """Send next track command.""" + if self.current_app is not None: + self.roku.forward() + + def mute_volume(self, mute): + """Mute the volume.""" + if self.current_app is not None: + self.roku.volume_mute() + + def volume_up(self): + """Volume up media player.""" + if self.current_app is not None: + self.roku.volume_up() + + def volume_down(self): + """Volume down media player.""" + if self.current_app is not None: + self.roku.volume_down() + + def select_source(self, source): + """Select input source.""" + if self.current_app is not None: + if source == "Home": + self.roku.home() + else: + channel = self.roku[source] + channel.launch() diff --git a/homeassistant/components/roku/remote.py b/homeassistant/components/roku/remote.py new file mode 100644 index 0000000000000..5529918010cf4 --- /dev/null +++ b/homeassistant/components/roku/remote.py @@ -0,0 +1,66 @@ +"""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): + """Set up the Roku remote platform.""" + if not discovery_info: + return + + host = discovery_info[CONF_HOST] + async_add_entities([RokuRemote(host)], True) + + +class RokuRemote(remote.RemoteDevice): + """Device that sends commands to an Roku.""" + + def __init__(self, host): + """Initialize the Roku device.""" + from roku import Roku + + self.roku = Roku(host) + self._device_info = {} + + def update(self): + """Retrieve latest state.""" + try: + self._device_info = self.roku.device_info + except (requests.exceptions.ConnectionError, + requests.exceptions.ReadTimeout): + pass + + @property + def name(self): + """Return the name of the device.""" + if self._device_info.userdevicename: + return self._device_info.userdevicename + return "Roku {}".format(self._device_info.sernum) + + @property + def unique_id(self): + """Return a unique ID.""" + return self._device_info.sernum + + @property + def is_on(self): + """Return true if device is on.""" + return True + + @property + def should_poll(self): + """No polling needed for Roku.""" + return False + + def send_command(self, command, **kwargs): + """Send a command to one device.""" + for single_command in command: + if not hasattr(self.roku, single_command): + continue + + getattr(self.roku, single_command)() diff --git a/homeassistant/components/route53.py b/homeassistant/components/route53.py deleted file mode 100644 index 4c35983feed87..0000000000000 --- a/homeassistant/components/route53.py +++ /dev/null @@ -1,135 +0,0 @@ -""" -Update the IP addresses of your Route53 DNS records. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/route53/ -""" -from datetime import timedelta -import logging -from typing import List - -import voluptuous as vol - -from homeassistant.const import CONF_DOMAIN, CONF_TTL, CONF_ZONE -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import track_time_interval - -REQUIREMENTS = ['boto3==1.9.16', 'ipify==1.0.0'] - -_LOGGER = logging.getLogger(__name__) - -CONF_ACCESS_KEY_ID = 'aws_access_key_id' -CONF_SECRET_ACCESS_KEY = 'aws_secret_access_key' -CONF_RECORDS = 'records' - -DOMAIN = 'route53' - -INTERVAL = timedelta(minutes=60) -DEFAULT_TTL = 300 - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_ACCESS_KEY_ID): cv.string, - vol.Required(CONF_DOMAIN): cv.string, - vol.Required(CONF_RECORDS): vol.All(cv.ensure_list, [cv.string]), - vol.Required(CONF_SECRET_ACCESS_KEY): cv.string, - vol.Required(CONF_ZONE): cv.string, - vol.Optional(CONF_TTL, default=DEFAULT_TTL): cv.positive_int, - }) -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the Route53 component.""" - domain = config[DOMAIN][CONF_DOMAIN] - records = config[DOMAIN][CONF_RECORDS] - zone = config[DOMAIN][CONF_ZONE] - aws_access_key_id = config[DOMAIN][CONF_ACCESS_KEY_ID] - aws_secret_access_key = config[DOMAIN][CONF_SECRET_ACCESS_KEY] - ttl = config[DOMAIN][CONF_TTL] - - def update_records_interval(now): - """Set up recurring update.""" - _update_route53( - aws_access_key_id, - aws_secret_access_key, - zone, - domain, - records, - ttl - ) - - def update_records_service(now): - """Set up service for manual trigger.""" - _update_route53( - aws_access_key_id, - aws_secret_access_key, - zone, - domain, - records, - ttl - ) - - track_time_interval(hass, update_records_interval, INTERVAL) - - hass.services.register(DOMAIN, 'update_records', update_records_service) - return True - - -def _update_route53( - aws_access_key_id: str, - aws_secret_access_key: str, - zone: str, - domain: str, - records: List[str], - ttl: int, -): - import boto3 - from ipify import get_ip - from ipify import exceptions - - _LOGGER.debug("Starting update for zone %s", zone) - - client = boto3.client( - DOMAIN, - aws_access_key_id=aws_access_key_id, - aws_secret_access_key=aws_secret_access_key, - ) - - # Get the IP Address and build an array of changes - try: - ipaddress = get_ip() - - except exceptions.ConnectionError: - _LOGGER.warning("Unable to reach the ipify service") - return - - except exceptions.ServiceError: - _LOGGER.warning("Unable to complete the ipfy request") - return - - changes = [] - for record in records: - _LOGGER.debug("Processing record: %s", record) - - changes.append({ - 'Action': 'UPSERT', - 'ResourceRecordSet': { - 'Name': '{}.{}'.format(record, domain), - 'Type': 'A', - 'TTL': ttl, - 'ResourceRecords': [ - {'Value': ipaddress}, - ], - } - }) - - _LOGGER.debug("Submitting the following changes to Route53") - _LOGGER.debug(changes) - - response = client.change_resource_record_sets( - HostedZoneId=zone, ChangeBatch={'Changes': changes}) - _LOGGER.debug("Response is %s", response) - - if response['ResponseMetadata']['HTTPStatusCode'] != 200: - _LOGGER.warning(response) diff --git a/homeassistant/components/route53/__init__.py b/homeassistant/components/route53/__init__.py new file mode 100644 index 0000000000000..725dec8b8e54b --- /dev/null +++ b/homeassistant/components/route53/__init__.py @@ -0,0 +1,130 @@ +"""Update the IP addresses of your Route53 DNS records.""" +from datetime import timedelta +import logging +from typing import List + +import voluptuous as vol + +from homeassistant.const import CONF_DOMAIN, CONF_TTL, CONF_ZONE +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import track_time_interval + +REQUIREMENTS = ['boto3==1.9.16', 'ipify==1.0.0'] + +_LOGGER = logging.getLogger(__name__) + +CONF_ACCESS_KEY_ID = 'aws_access_key_id' +CONF_SECRET_ACCESS_KEY = 'aws_secret_access_key' +CONF_RECORDS = 'records' + +DOMAIN = 'route53' + +INTERVAL = timedelta(minutes=60) +DEFAULT_TTL = 300 + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_ACCESS_KEY_ID): cv.string, + vol.Required(CONF_DOMAIN): cv.string, + vol.Required(CONF_RECORDS): vol.All(cv.ensure_list, [cv.string]), + vol.Required(CONF_SECRET_ACCESS_KEY): cv.string, + vol.Required(CONF_ZONE): cv.string, + vol.Optional(CONF_TTL, default=DEFAULT_TTL): cv.positive_int, + }) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Route53 component.""" + domain = config[DOMAIN][CONF_DOMAIN] + records = config[DOMAIN][CONF_RECORDS] + zone = config[DOMAIN][CONF_ZONE] + aws_access_key_id = config[DOMAIN][CONF_ACCESS_KEY_ID] + aws_secret_access_key = config[DOMAIN][CONF_SECRET_ACCESS_KEY] + ttl = config[DOMAIN][CONF_TTL] + + def update_records_interval(now): + """Set up recurring update.""" + _update_route53( + aws_access_key_id, + aws_secret_access_key, + zone, + domain, + records, + ttl + ) + + def update_records_service(now): + """Set up service for manual trigger.""" + _update_route53( + aws_access_key_id, + aws_secret_access_key, + zone, + domain, + records, + ttl + ) + + track_time_interval(hass, update_records_interval, INTERVAL) + + hass.services.register(DOMAIN, 'update_records', update_records_service) + return True + + +def _update_route53( + aws_access_key_id: str, + aws_secret_access_key: str, + zone: str, + domain: str, + records: List[str], + ttl: int, +): + import boto3 + from ipify import get_ip + from ipify import exceptions + + _LOGGER.debug("Starting update for zone %s", zone) + + client = boto3.client( + DOMAIN, + aws_access_key_id=aws_access_key_id, + aws_secret_access_key=aws_secret_access_key, + ) + + # Get the IP Address and build an array of changes + try: + ipaddress = get_ip() + + except exceptions.ConnectionError: + _LOGGER.warning("Unable to reach the ipify service") + return + + except exceptions.ServiceError: + _LOGGER.warning("Unable to complete the ipfy request") + return + + changes = [] + for record in records: + _LOGGER.debug("Processing record: %s", record) + + changes.append({ + 'Action': 'UPSERT', + 'ResourceRecordSet': { + 'Name': '{}.{}'.format(record, domain), + 'Type': 'A', + 'TTL': ttl, + 'ResourceRecords': [ + {'Value': ipaddress}, + ], + } + }) + + _LOGGER.debug("Submitting the following changes to Route53") + _LOGGER.debug(changes) + + response = client.change_resource_record_sets( + HostedZoneId=zone, ChangeBatch={'Changes': changes}) + _LOGGER.debug("Response is %s", response) + + if response['ResponseMetadata']['HTTPStatusCode'] != 200: + _LOGGER.warning(response) diff --git a/homeassistant/components/rpi_gpio.py b/homeassistant/components/rpi_gpio.py deleted file mode 100644 index 5f52341f1cb30..0000000000000 --- a/homeassistant/components/rpi_gpio.py +++ /dev/null @@ -1,68 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) - -REQUIREMENTS = ['RPi.GPIO==0.6.5'] - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'rpi_gpio' - - -def setup(hass, config): - """Set up the Raspberry PI GPIO component.""" - from RPi import GPIO # pylint: disable=import-error - - def cleanup_gpio(event): - """Stuff to do before stopping.""" - GPIO.cleanup() - - def prepare_gpio(event): - """Stuff to do when home assistant starts.""" - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_gpio) - - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, prepare_gpio) - GPIO.setmode(GPIO.BCM) - return True - - -def setup_output(port): - """Set up a GPIO as output.""" - from RPi import GPIO # pylint: disable=import-error - GPIO.setup(port, GPIO.OUT) - - -def setup_input(port, pull_mode): - """Set up a GPIO as input.""" - from RPi import GPIO # pylint: disable=import-error - GPIO.setup(port, GPIO.IN, - GPIO.PUD_DOWN if pull_mode == 'DOWN' else GPIO.PUD_UP) - - -def write_output(port, value): - """Write a value to a GPIO.""" - from RPi import GPIO # pylint: disable=import-error - GPIO.output(port, value) - - -def read_input(port): - """Read a value from a GPIO.""" - from RPi import GPIO # pylint: disable=import-error - return GPIO.input(port) - - -def edge_detect(port, event_callback, bounce): - """Add detection for RISING and FALLING events.""" - from RPi import GPIO # pylint: disable=import-error - GPIO.add_event_detect( - port, - GPIO.BOTH, - callback=event_callback, - bouncetime=bounce) diff --git a/homeassistant/components/rpi_gpio/__init__.py b/homeassistant/components/rpi_gpio/__init__.py new file mode 100644 index 0000000000000..b5bd0796f160b --- /dev/null +++ b/homeassistant/components/rpi_gpio/__init__.py @@ -0,0 +1,63 @@ +"""Support for controlling GPIO pins of a Raspberry Pi.""" +import logging + +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) + +REQUIREMENTS = ['RPi.GPIO==0.6.5'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'rpi_gpio' + + +def setup(hass, config): + """Set up the Raspberry PI GPIO component.""" + from RPi import GPIO # pylint: disable=import-error + + def cleanup_gpio(event): + """Stuff to do before stopping.""" + GPIO.cleanup() + + def prepare_gpio(event): + """Stuff to do when home assistant starts.""" + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_gpio) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, prepare_gpio) + GPIO.setmode(GPIO.BCM) + return True + + +def setup_output(port): + """Set up a GPIO as output.""" + from RPi import GPIO # pylint: disable=import-error + GPIO.setup(port, GPIO.OUT) + + +def setup_input(port, pull_mode): + """Set up a GPIO as input.""" + from RPi import GPIO # pylint: disable=import-error + GPIO.setup(port, GPIO.IN, + GPIO.PUD_DOWN if pull_mode == 'DOWN' else GPIO.PUD_UP) + + +def write_output(port, value): + """Write a value to a GPIO.""" + from RPi import GPIO # pylint: disable=import-error + GPIO.output(port, value) + + +def read_input(port): + """Read a value from a GPIO.""" + from RPi import GPIO # pylint: disable=import-error + return GPIO.input(port) + + +def edge_detect(port, event_callback, bounce): + """Add detection for RISING and FALLING events.""" + from RPi import GPIO # pylint: disable=import-error + GPIO.add_event_detect( + port, + GPIO.BOTH, + callback=event_callback, + bouncetime=bounce) diff --git a/homeassistant/components/rpi_gpio/binary_sensor.py b/homeassistant/components/rpi_gpio/binary_sensor.py new file mode 100644 index 0000000000000..559ae9584049b --- /dev/null +++ b/homeassistant/components/rpi_gpio/binary_sensor.py @@ -0,0 +1,89 @@ +"""Support for binary sensor using RPi GPIO.""" +import logging + +import voluptuous as vol + +from homeassistant.components import rpi_gpio +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA) +from homeassistant.const import DEVICE_DEFAULT_NAME +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +CONF_BOUNCETIME = 'bouncetime' +CONF_INVERT_LOGIC = 'invert_logic' +CONF_PORTS = 'ports' +CONF_PULL_MODE = 'pull_mode' + +DEFAULT_BOUNCETIME = 50 +DEFAULT_INVERT_LOGIC = False +DEFAULT_PULL_MODE = 'UP' + +DEPENDENCIES = ['rpi_gpio'] + +_SENSORS_SCHEMA = vol.Schema({ + cv.positive_int: cv.string, +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_PORTS): _SENSORS_SCHEMA, + vol.Optional(CONF_BOUNCETIME, default=DEFAULT_BOUNCETIME): cv.positive_int, + vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, + vol.Optional(CONF_PULL_MODE, default=DEFAULT_PULL_MODE): cv.string, +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Raspberry PI GPIO devices.""" + pull_mode = config.get(CONF_PULL_MODE) + bouncetime = config.get(CONF_BOUNCETIME) + invert_logic = config.get(CONF_INVERT_LOGIC) + + binary_sensors = [] + ports = config.get('ports') + for port_num, port_name in ports.items(): + binary_sensors.append(RPiGPIOBinarySensor( + port_name, port_num, pull_mode, bouncetime, invert_logic)) + add_entities(binary_sensors, True) + + +class RPiGPIOBinarySensor(BinarySensorDevice): + """Represent a binary sensor that uses Raspberry Pi GPIO.""" + + def __init__(self, name, port, pull_mode, bouncetime, invert_logic): + """Initialize the RPi binary sensor.""" + self._name = name or DEVICE_DEFAULT_NAME + self._port = port + self._pull_mode = pull_mode + self._bouncetime = bouncetime + self._invert_logic = invert_logic + self._state = None + + rpi_gpio.setup_input(self._port, self._pull_mode) + + def read_gpio(port): + """Read state from GPIO.""" + self._state = rpi_gpio.read_input(self._port) + self.schedule_update_ha_state() + + rpi_gpio.edge_detect(self._port, read_gpio, self._bouncetime) + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def is_on(self): + """Return the state of the entity.""" + return self._state != self._invert_logic + + def update(self): + """Update the GPIO state.""" + self._state = rpi_gpio.read_input(self._port) diff --git a/homeassistant/components/rpi_gpio/cover.py b/homeassistant/components/rpi_gpio/cover.py new file mode 100644 index 0000000000000..403f7ec6867af --- /dev/null +++ b/homeassistant/components/rpi_gpio/cover.py @@ -0,0 +1,111 @@ +"""Support for controlling a Raspberry Pi cover.""" +import logging +from time import sleep + +import voluptuous as vol + +from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME +from homeassistant.components import rpi_gpio +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +CONF_COVERS = 'covers' +CONF_RELAY_PIN = 'relay_pin' +CONF_RELAY_TIME = 'relay_time' +CONF_STATE_PIN = 'state_pin' +CONF_STATE_PULL_MODE = 'state_pull_mode' +CONF_INVERT_STATE = 'invert_state' +CONF_INVERT_RELAY = 'invert_relay' + +DEFAULT_RELAY_TIME = .2 +DEFAULT_STATE_PULL_MODE = 'UP' +DEFAULT_INVERT_STATE = False +DEFAULT_INVERT_RELAY = False +DEPENDENCIES = ['rpi_gpio'] + +_COVERS_SCHEMA = vol.All( + cv.ensure_list, + [ + vol.Schema({ + CONF_NAME: cv.string, + CONF_RELAY_PIN: cv.positive_int, + CONF_STATE_PIN: cv.positive_int, + }) + ] +) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_COVERS): _COVERS_SCHEMA, + vol.Optional(CONF_STATE_PULL_MODE, default=DEFAULT_STATE_PULL_MODE): + cv.string, + vol.Optional(CONF_RELAY_TIME, default=DEFAULT_RELAY_TIME): cv.positive_int, + vol.Optional(CONF_INVERT_STATE, default=DEFAULT_INVERT_STATE): cv.boolean, + vol.Optional(CONF_INVERT_RELAY, default=DEFAULT_INVERT_RELAY): cv.boolean, +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the RPi cover platform.""" + relay_time = config.get(CONF_RELAY_TIME) + state_pull_mode = config.get(CONF_STATE_PULL_MODE) + invert_state = config.get(CONF_INVERT_STATE) + invert_relay = config.get(CONF_INVERT_RELAY) + covers = [] + covers_conf = config.get(CONF_COVERS) + + for cover in covers_conf: + covers.append(RPiGPIOCover( + cover[CONF_NAME], cover[CONF_RELAY_PIN], cover[CONF_STATE_PIN], + state_pull_mode, relay_time, invert_state, invert_relay)) + add_entities(covers) + + +class RPiGPIOCover(CoverDevice): + """Representation of a Raspberry GPIO cover.""" + + def __init__(self, name, relay_pin, state_pin, state_pull_mode, + relay_time, invert_state, invert_relay): + """Initialize the cover.""" + self._name = name + self._state = False + self._relay_pin = relay_pin + self._state_pin = state_pin + self._state_pull_mode = state_pull_mode + self._relay_time = relay_time + self._invert_state = invert_state + self._invert_relay = invert_relay + rpi_gpio.setup_output(self._relay_pin) + rpi_gpio.setup_input(self._state_pin, self._state_pull_mode) + rpi_gpio.write_output(self._relay_pin, 0 if self._invert_relay else 1) + + @property + def name(self): + """Return the name of the cover if any.""" + return self._name + + def update(self): + """Update the state of the cover.""" + self._state = rpi_gpio.read_input(self._state_pin) + + @property + def is_closed(self): + """Return true if cover is closed.""" + return self._state != self._invert_state + + def _trigger(self): + """Trigger the cover.""" + rpi_gpio.write_output(self._relay_pin, 1 if self._invert_relay else 0) + sleep(self._relay_time) + rpi_gpio.write_output(self._relay_pin, 0 if self._invert_relay else 1) + + def close_cover(self, **kwargs): + """Close the cover.""" + if not self.is_closed: + self._trigger() + + def open_cover(self, **kwargs): + """Open the cover.""" + if self.is_closed: + self._trigger() diff --git a/homeassistant/components/rpi_gpio/switch.py b/homeassistant/components/rpi_gpio/switch.py new file mode 100644 index 0000000000000..bdb79d03eec81 --- /dev/null +++ b/homeassistant/components/rpi_gpio/switch.py @@ -0,0 +1,80 @@ +"""Allows to configure a switch using RPi GPIO.""" +import logging + +import voluptuous as vol + +from homeassistant.components.switch import PLATFORM_SCHEMA +from homeassistant.components import rpi_gpio +from homeassistant.const import DEVICE_DEFAULT_NAME +from homeassistant.helpers.entity import ToggleEntity +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['rpi_gpio'] + +CONF_PULL_MODE = 'pull_mode' +CONF_PORTS = 'ports' +CONF_INVERT_LOGIC = 'invert_logic' + +DEFAULT_INVERT_LOGIC = False + +_SWITCHES_SCHEMA = vol.Schema({ + cv.positive_int: cv.string, +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_PORTS): _SWITCHES_SCHEMA, + vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Raspberry PI GPIO devices.""" + invert_logic = config.get(CONF_INVERT_LOGIC) + + switches = [] + ports = config.get(CONF_PORTS) + for port, name in ports.items(): + switches.append(RPiGPIOSwitch(name, port, invert_logic)) + add_entities(switches) + + +class RPiGPIOSwitch(ToggleEntity): + """Representation of a Raspberry Pi GPIO.""" + + def __init__(self, name, port, invert_logic): + """Initialize the pin.""" + self._name = name or DEVICE_DEFAULT_NAME + self._port = port + self._invert_logic = invert_logic + self._state = False + rpi_gpio.setup_output(self._port) + rpi_gpio.write_output(self._port, 1 if self._invert_logic else 0) + + @property + def name(self): + """Return the name of the switch.""" + return self._name + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def is_on(self): + """Return true if device is on.""" + return self._state + + def turn_on(self, **kwargs): + """Turn the device on.""" + rpi_gpio.write_output(self._port, 0 if self._invert_logic else 1) + self._state = True + self.schedule_update_ha_state() + + def turn_off(self, **kwargs): + """Turn the device off.""" + rpi_gpio.write_output(self._port, 1 if self._invert_logic else 0) + self._state = False + self.schedule_update_ha_state() diff --git a/homeassistant/components/rpi_pfio.py b/homeassistant/components/rpi_pfio.py deleted file mode 100644 index 286be87bce902..0000000000000 --- a/homeassistant/components/rpi_pfio.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) - -REQUIREMENTS = ['pifacecommon==4.2.2', 'pifacedigitalio==3.0.5'] - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'rpi_pfio' - -DATA_PFIO_LISTENER = 'pfio_listener' - - -def setup(hass, config): - """Set up the Raspberry PI PFIO component.""" - import pifacedigitalio as PFIO - - pifacedigital = PFIO.PiFaceDigital() - hass.data[DATA_PFIO_LISTENER] = PFIO.InputEventListener(chip=pifacedigital) - - def cleanup_pfio(event): - """Stuff to do before stopping.""" - PFIO.deinit() - - def prepare_pfio(event): - """Stuff to do when home assistant starts.""" - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_pfio) - - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, prepare_pfio) - PFIO.init() - - return True - - -def write_output(port, value): - """Write a value to a PFIO.""" - import pifacedigitalio as PFIO - PFIO.digital_write(port, value) - - -def read_input(port): - """Read a value from a PFIO.""" - import pifacedigitalio as PFIO - return PFIO.digital_read(port) - - -def edge_detect(hass, port, event_callback, settle): - """Add detection for RISING and FALLING events.""" - import pifacedigitalio as PFIO - hass.data[DATA_PFIO_LISTENER].register( - port, PFIO.IODIR_BOTH, event_callback, settle_time=settle) - - -def activate_listener(hass): - """Activate the registered listener events.""" - hass.data[DATA_PFIO_LISTENER].activate() diff --git a/homeassistant/components/rpi_pfio/__init__.py b/homeassistant/components/rpi_pfio/__init__.py new file mode 100644 index 0000000000000..b096d9fe98ab7 --- /dev/null +++ b/homeassistant/components/rpi_pfio/__init__.py @@ -0,0 +1,58 @@ +"""Support for controlling the PiFace Digital I/O module on a RPi.""" +import logging + +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) + +REQUIREMENTS = ['pifacecommon==4.2.2', 'pifacedigitalio==3.0.5'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'rpi_pfio' + +DATA_PFIO_LISTENER = 'pfio_listener' + + +def setup(hass, config): + """Set up the Raspberry PI PFIO component.""" + import pifacedigitalio as PFIO + + pifacedigital = PFIO.PiFaceDigital() + hass.data[DATA_PFIO_LISTENER] = PFIO.InputEventListener(chip=pifacedigital) + + def cleanup_pfio(event): + """Stuff to do before stopping.""" + PFIO.deinit() + + def prepare_pfio(event): + """Stuff to do when home assistant starts.""" + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_pfio) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, prepare_pfio) + PFIO.init() + + return True + + +def write_output(port, value): + """Write a value to a PFIO.""" + import pifacedigitalio as PFIO + PFIO.digital_write(port, value) + + +def read_input(port): + """Read a value from a PFIO.""" + import pifacedigitalio as PFIO + return PFIO.digital_read(port) + + +def edge_detect(hass, port, event_callback, settle): + """Add detection for RISING and FALLING events.""" + import pifacedigitalio as PFIO + hass.data[DATA_PFIO_LISTENER].register( + port, PFIO.IODIR_BOTH, event_callback, settle_time=settle) + + +def activate_listener(hass): + """Activate the registered listener events.""" + hass.data[DATA_PFIO_LISTENER].activate() diff --git a/homeassistant/components/rpi_pfio/binary_sensor.py b/homeassistant/components/rpi_pfio/binary_sensor.py new file mode 100644 index 0000000000000..677ec3bb16f12 --- /dev/null +++ b/homeassistant/components/rpi_pfio/binary_sensor.py @@ -0,0 +1,87 @@ +"""Support for binary sensor using the PiFace Digital I/O module on a RPi.""" +import logging + +import voluptuous as vol + +from homeassistant.components.binary_sensor import ( + PLATFORM_SCHEMA, BinarySensorDevice) +from homeassistant.components import rpi_pfio +from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +CONF_INVERT_LOGIC = 'invert_logic' +CONF_PORTS = 'ports' +CONF_SETTLE_TIME = 'settle_time' + +DEFAULT_INVERT_LOGIC = False +DEFAULT_SETTLE_TIME = 20 + +DEPENDENCIES = ['rpi_pfio'] + +PORT_SCHEMA = vol.Schema({ + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_SETTLE_TIME, default=DEFAULT_SETTLE_TIME): + cv.positive_int, + vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_PORTS, default={}): vol.Schema({ + cv.positive_int: PORT_SCHEMA, + }) +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the PiFace Digital Input devices.""" + binary_sensors = [] + ports = config.get(CONF_PORTS) + for port, port_entity in ports.items(): + name = port_entity.get(CONF_NAME) + settle_time = port_entity[CONF_SETTLE_TIME] / 1000 + invert_logic = port_entity[CONF_INVERT_LOGIC] + + binary_sensors.append(RPiPFIOBinarySensor( + hass, port, name, settle_time, invert_logic)) + add_entities(binary_sensors, True) + + rpi_pfio.activate_listener(hass) + + +class RPiPFIOBinarySensor(BinarySensorDevice): + """Represent a binary sensor that a PiFace Digital Input.""" + + def __init__(self, hass, port, name, settle_time, invert_logic): + """Initialize the RPi binary sensor.""" + self._port = port + self._name = name or DEVICE_DEFAULT_NAME + self._invert_logic = invert_logic + self._state = None + + def read_pfio(port): + """Read state from PFIO.""" + self._state = rpi_pfio.read_input(self._port) + self.schedule_update_ha_state() + + rpi_pfio.edge_detect(hass, self._port, read_pfio, settle_time) + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def is_on(self): + """Return the state of the entity.""" + return self._state != self._invert_logic + + def update(self): + """Update the PFIO state.""" + self._state = rpi_pfio.read_input(self._port) diff --git a/homeassistant/components/rpi_pfio/switch.py b/homeassistant/components/rpi_pfio/switch.py new file mode 100644 index 0000000000000..fc158bd666f97 --- /dev/null +++ b/homeassistant/components/rpi_pfio/switch.py @@ -0,0 +1,82 @@ +"""Support for switches using the PiFace Digital I/O module on a RPi.""" +import logging + +import voluptuous as vol + +from homeassistant.components import rpi_pfio +from homeassistant.components.switch import PLATFORM_SCHEMA +from homeassistant.const import ATTR_NAME, DEVICE_DEFAULT_NAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import ToggleEntity + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['rpi_pfio'] + +ATTR_INVERT_LOGIC = 'invert_logic' + +CONF_PORTS = 'ports' + +DEFAULT_INVERT_LOGIC = False + +PORT_SCHEMA = vol.Schema({ + vol.Optional(ATTR_NAME): cv.string, + vol.Optional(ATTR_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_PORTS, default={}): vol.Schema({ + cv.positive_int: PORT_SCHEMA, + }) +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the PiFace Digital Output devices.""" + switches = [] + ports = config.get(CONF_PORTS) + for port, port_entity in ports.items(): + name = port_entity.get(ATTR_NAME) + invert_logic = port_entity[ATTR_INVERT_LOGIC] + + switches.append(RPiPFIOSwitch(port, name, invert_logic)) + add_entities(switches) + + +class RPiPFIOSwitch(ToggleEntity): + """Representation of a PiFace Digital Output.""" + + def __init__(self, port, name, invert_logic): + """Initialize the pin.""" + self._port = port + self._name = name or DEVICE_DEFAULT_NAME + self._invert_logic = invert_logic + self._state = False + rpi_pfio.write_output(self._port, 1 if self._invert_logic else 0) + + @property + def name(self): + """Return the name of the switch.""" + return self._name + + @property + def should_poll(self): + """Return the polling state.""" + return False + + @property + def is_on(self): + """Return true if device is on.""" + return self._state + + def turn_on(self, **kwargs): + """Turn the device on.""" + rpi_pfio.write_output(self._port, 0 if self._invert_logic else 1) + self._state = True + self.schedule_update_ha_state() + + def turn_off(self, **kwargs): + """Turn the device off.""" + rpi_pfio.write_output(self._port, 1 if self._invert_logic else 0) + self._state = False + self.schedule_update_ha_state() diff --git a/homeassistant/components/rss_feed_template.py b/homeassistant/components/rss_feed_template.py deleted file mode 100644 index 34bee1ec5fcf9..0000000000000 --- a/homeassistant/components/rss_feed_template.py +++ /dev/null @@ -1,102 +0,0 @@ -""" -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/ -""" - -from html import escape -from aiohttp import web - -import voluptuous as vol - -from homeassistant.components.http import HomeAssistantView -import homeassistant.helpers.config_validation as cv - -CONTENT_TYPE_XML = 'text/xml' -DEPENDENCIES = ['http'] - -DOMAIN = 'rss_feed_template' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - cv.match_all: vol.Schema({ - vol.Optional('requires_api_password', default=True): cv.boolean, - vol.Optional('title'): cv.template, - vol.Required('items'): vol.All( - cv.ensure_list, - [{ - vol.Optional('title'): cv.template, - vol.Optional('description'): cv.template, - }] - ) - }) - }) - }, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the RSS feed template component.""" - for (feeduri, feedconfig) in config[DOMAIN].items(): - url = '/api/rss_template/%s' % feeduri - - requires_auth = feedconfig.get('requires_api_password') - - title = feedconfig.get('title') - if title is not None: - title.hass = hass - - items = feedconfig.get('items') - for item in items: - if 'title' in item: - item['title'].hass = hass - if 'description' in item: - item['description'].hass = hass - - rss_view = RssView(url, requires_auth, title, items) - hass.http.register_view(rss_view) - - return True - - -class RssView(HomeAssistantView): - """Export states and other values as RSS.""" - - requires_auth = True - url = None - name = 'rss_template' - _title = None - _items = None - - def __init__(self, url, requires_auth, title, items): - """Initialize the rss view.""" - self.url = url - self.requires_auth = requires_auth - self._title = title - self._items = items - - async def get(self, request, entity_id=None): - """Generate the RSS view XML.""" - response = '\n\n' - - response += '\n' - if self._title is not None: - response += (' %s\n' % - escape(self._title.async_render())) - - for item in self._items: - response += ' \n' - if 'title' in item: - response += ' ' - response += escape(item['title'].async_render()) - response += '\n' - if 'description' in item: - response += ' ' - response += escape(item['description'].async_render()) - response += '\n' - response += ' \n' - - response += '\n' - - return web.Response( - body=response, content_type=CONTENT_TYPE_XML, status=200) diff --git a/homeassistant/components/rss_feed_template/__init__.py b/homeassistant/components/rss_feed_template/__init__.py new file mode 100644 index 0000000000000..3c93fe2ac8359 --- /dev/null +++ b/homeassistant/components/rss_feed_template/__init__.py @@ -0,0 +1,96 @@ +"""Support to export sensor values via RSS feed.""" +from html import escape +from aiohttp import web + +import voluptuous as vol + +from homeassistant.components.http import HomeAssistantView +import homeassistant.helpers.config_validation as cv + +CONTENT_TYPE_XML = 'text/xml' +DEPENDENCIES = ['http'] + +DOMAIN = 'rss_feed_template' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + cv.match_all: vol.Schema({ + vol.Optional('requires_api_password', default=True): cv.boolean, + vol.Optional('title'): cv.template, + vol.Required('items'): vol.All( + cv.ensure_list, + [{ + vol.Optional('title'): cv.template, + vol.Optional('description'): cv.template, + }] + ) + }) + }) + }, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the RSS feed template component.""" + for (feeduri, feedconfig) in config[DOMAIN].items(): + url = '/api/rss_template/%s' % feeduri + + requires_auth = feedconfig.get('requires_api_password') + + title = feedconfig.get('title') + if title is not None: + title.hass = hass + + items = feedconfig.get('items') + for item in items: + if 'title' in item: + item['title'].hass = hass + if 'description' in item: + item['description'].hass = hass + + rss_view = RssView(url, requires_auth, title, items) + hass.http.register_view(rss_view) + + return True + + +class RssView(HomeAssistantView): + """Export states and other values as RSS.""" + + requires_auth = True + url = None + name = 'rss_template' + _title = None + _items = None + + def __init__(self, url, requires_auth, title, items): + """Initialize the rss view.""" + self.url = url + self.requires_auth = requires_auth + self._title = title + self._items = items + + async def get(self, request, entity_id=None): + """Generate the RSS view XML.""" + response = '\n\n' + + response += '\n' + if self._title is not None: + response += (' %s\n' % + escape(self._title.async_render())) + + for item in self._items: + response += ' \n' + if 'title' in item: + response += ' ' + response += escape(item['title'].async_render()) + response += '\n' + if 'description' in item: + response += ' ' + response += escape(item['description'].async_render()) + response += '\n' + response += ' \n' + + response += '\n' + + return web.Response( + body=response, content_type=CONTENT_TYPE_XML, status=200) diff --git a/homeassistant/components/sabnzbd.py b/homeassistant/components/sabnzbd.py deleted file mode 100644 index 25fce22c6414b..0000000000000 --- a/homeassistant/components/sabnzbd.py +++ /dev/null @@ -1,257 +0,0 @@ -""" -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/ -""" -import logging -from datetime import timedelta - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components.discovery import SERVICE_SABNZBD -from homeassistant.const import ( - CONF_HOST, CONF_API_KEY, CONF_NAME, CONF_PORT, CONF_SENSORS, CONF_SSL) -from homeassistant.core import callback -from homeassistant.helpers import discovery -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.event import async_track_time_interval -from homeassistant.util.json import load_json, save_json - -REQUIREMENTS = ['pysabnzbd==1.1.0'] - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'sabnzbd' -DATA_SABNZBD = 'sabznbd' - -_CONFIGURING = {} - -ATTR_SPEED = 'speed' -BASE_URL_FORMAT = '{}://{}:{}/' -CONFIG_FILE = 'sabnzbd.conf' -DEFAULT_HOST = 'localhost' -DEFAULT_NAME = 'SABnzbd' -DEFAULT_PORT = 8080 -DEFAULT_SPEED_LIMIT = '100' -DEFAULT_SSL = False - -UPDATE_INTERVAL = timedelta(seconds=30) - -SERVICE_PAUSE = 'pause' -SERVICE_RESUME = 'resume' -SERVICE_SET_SPEED = 'set_speed' - -SIGNAL_SABNZBD_UPDATED = 'sabnzbd_updated' - -SENSOR_TYPES = { - 'current_status': ['Status', None, 'status'], - 'speed': ['Speed', 'MB/s', 'kbpersec'], - 'queue_size': ['Queue', 'MB', 'mb'], - 'queue_remaining': ['Left', 'MB', 'mbleft'], - 'disk_size': ['Disk', 'GB', 'diskspacetotal1'], - 'disk_free': ['Disk Free', 'GB', 'diskspace1'], - 'queue_count': ['Queue Count', None, 'noofslots_total'], - 'day_size': ['Daily Total', 'GB', 'day_size'], - 'week_size': ['Weekly Total', 'GB', 'week_size'], - 'month_size': ['Monthly Total', 'GB', 'month_size'], - 'total_size': ['Total', 'GB', 'total_size'], -} - -SPEED_LIMIT_SCHEMA = vol.Schema({ - vol.Optional(ATTR_SPEED, default=DEFAULT_SPEED_LIMIT): cv.string, -}) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_API_KEY): cv.string, - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_SENSORS): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, - }), -}, extra=vol.ALLOW_EXTRA) - - -async def async_check_sabnzbd(sab_api): - """Check if we can reach SABnzbd.""" - from pysabnzbd import SabnzbdApiException - - try: - await sab_api.check_available() - return True - except SabnzbdApiException: - _LOGGER.error("Connection to SABnzbd API failed") - return False - - -async def async_configure_sabnzbd(hass, config, use_ssl, name=DEFAULT_NAME, - api_key=None): - """Try to configure Sabnzbd and request api key if configuration fails.""" - from pysabnzbd import SabnzbdApi - - host = config[CONF_HOST] - port = config[CONF_PORT] - uri_scheme = 'https' if use_ssl else 'http' - base_url = BASE_URL_FORMAT.format(uri_scheme, host, port) - if api_key is None: - conf = await hass.async_add_job(load_json, - hass.config.path(CONFIG_FILE)) - api_key = conf.get(base_url, {}).get(CONF_API_KEY, '') - - sab_api = SabnzbdApi(base_url, api_key, - session=async_get_clientsession(hass)) - if await async_check_sabnzbd(sab_api): - async_setup_sabnzbd(hass, sab_api, config, name) - else: - async_request_configuration(hass, config, base_url) - - -async def async_setup(hass, config): - """Set up the SABnzbd component.""" - async def sabnzbd_discovered(service, info): - """Handle service discovery.""" - ssl = info.get('properties', {}).get('https', '0') == '1' - await async_configure_sabnzbd(hass, info, ssl) - - discovery.async_listen(hass, SERVICE_SABNZBD, sabnzbd_discovered) - - conf = config.get(DOMAIN) - if conf is not None: - use_ssl = conf.get(CONF_SSL) - name = conf.get(CONF_NAME) - api_key = conf.get(CONF_API_KEY) - await async_configure_sabnzbd(hass, conf, use_ssl, name, api_key) - return True - - -@callback -def async_setup_sabnzbd(hass, sab_api, config, name): - """Set up SABnzbd sensors and services.""" - sab_api_data = SabnzbdApiData(sab_api, name, config.get(CONF_SENSORS, {})) - - if config.get(CONF_SENSORS): - hass.data[DATA_SABNZBD] = sab_api_data - hass.async_create_task( - discovery.async_load_platform(hass, 'sensor', DOMAIN, {}, config)) - - async def async_service_handler(service): - """Handle service calls.""" - if service.service == SERVICE_PAUSE: - await sab_api_data.async_pause_queue() - elif service.service == SERVICE_RESUME: - await sab_api_data.async_resume_queue() - elif service.service == SERVICE_SET_SPEED: - speed = service.data.get(ATTR_SPEED) - await sab_api_data.async_set_queue_speed(speed) - - hass.services.async_register(DOMAIN, SERVICE_PAUSE, - async_service_handler, - schema=vol.Schema({})) - - hass.services.async_register(DOMAIN, SERVICE_RESUME, - async_service_handler, - schema=vol.Schema({})) - - hass.services.async_register(DOMAIN, SERVICE_SET_SPEED, - async_service_handler, - schema=SPEED_LIMIT_SCHEMA) - - async def async_update_sabnzbd(now): - """Refresh SABnzbd queue data.""" - from pysabnzbd import SabnzbdApiException - try: - await sab_api.refresh_data() - async_dispatcher_send(hass, SIGNAL_SABNZBD_UPDATED, None) - except SabnzbdApiException as err: - _LOGGER.error(err) - - async_track_time_interval(hass, async_update_sabnzbd, UPDATE_INTERVAL) - - -@callback -def async_request_configuration(hass, config, host): - """Request configuration steps from the user.""" - from pysabnzbd import SabnzbdApi - - configurator = hass.components.configurator - # We got an error if this method is called while we are configuring - if host in _CONFIGURING: - configurator.async_notify_errors( - _CONFIGURING[host], - 'Failed to register, please try again.') - - return - - async def async_configuration_callback(data): - """Handle configuration changes.""" - api_key = data.get(CONF_API_KEY) - sab_api = SabnzbdApi(host, api_key, - session=async_get_clientsession(hass)) - if not await async_check_sabnzbd(sab_api): - return - - def success(): - """Signal successful setup.""" - conf = load_json(hass.config.path(CONFIG_FILE)) - conf[host] = {CONF_API_KEY: api_key} - save_json(hass.config.path(CONFIG_FILE), conf) - req_config = _CONFIGURING.pop(host) - configurator.request_done(req_config) - - hass.async_add_job(success) - async_setup_sabnzbd(hass, sab_api, config, - config.get(CONF_NAME, DEFAULT_NAME)) - - _CONFIGURING[host] = configurator.async_request_config( - DEFAULT_NAME, - async_configuration_callback, - description='Enter the API Key', - submit_caption='Confirm', - fields=[{'id': CONF_API_KEY, 'name': 'API Key', 'type': ''}] - ) - - -class SabnzbdApiData: - """Class for storing/refreshing sabnzbd api queue data.""" - - def __init__(self, sab_api, name, sensors): - """Initialize component.""" - self.sab_api = sab_api - self.name = name - self.sensors = sensors - - async def async_pause_queue(self): - """Pause Sabnzbd queue.""" - from pysabnzbd import SabnzbdApiException - try: - return await self.sab_api.pause_queue() - except SabnzbdApiException as err: - _LOGGER.error(err) - return False - - async def async_resume_queue(self): - """Resume Sabnzbd queue.""" - from pysabnzbd import SabnzbdApiException - try: - return await self.sab_api.resume_queue() - except SabnzbdApiException as err: - _LOGGER.error(err) - return False - - async def async_set_queue_speed(self, limit): - """Set speed limit for the Sabnzbd queue.""" - from pysabnzbd import SabnzbdApiException - try: - return await self.sab_api.set_speed_limit(limit) - except SabnzbdApiException as err: - _LOGGER.error(err) - return False - - def get_queue_field(self, field): - """Return the value for the given field from the Sabnzbd queue.""" - return self.sab_api.queue.get(field) diff --git a/homeassistant/components/sabnzbd/__init__.py b/homeassistant/components/sabnzbd/__init__.py new file mode 100644 index 0000000000000..d070872f85c44 --- /dev/null +++ b/homeassistant/components/sabnzbd/__init__.py @@ -0,0 +1,252 @@ +"""Support for monitoring an SABnzbd NZB client.""" +import logging +from datetime import timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.discovery import SERVICE_SABNZBD +from homeassistant.const import ( + CONF_HOST, CONF_API_KEY, CONF_NAME, CONF_PORT, CONF_SENSORS, CONF_SSL) +from homeassistant.core import callback +from homeassistant.helpers import discovery +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.json import load_json, save_json + +REQUIREMENTS = ['pysabnzbd==1.1.0'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'sabnzbd' +DATA_SABNZBD = 'sabznbd' + +_CONFIGURING = {} + +ATTR_SPEED = 'speed' +BASE_URL_FORMAT = '{}://{}:{}/' +CONFIG_FILE = 'sabnzbd.conf' +DEFAULT_HOST = 'localhost' +DEFAULT_NAME = 'SABnzbd' +DEFAULT_PORT = 8080 +DEFAULT_SPEED_LIMIT = '100' +DEFAULT_SSL = False + +UPDATE_INTERVAL = timedelta(seconds=30) + +SERVICE_PAUSE = 'pause' +SERVICE_RESUME = 'resume' +SERVICE_SET_SPEED = 'set_speed' + +SIGNAL_SABNZBD_UPDATED = 'sabnzbd_updated' + +SENSOR_TYPES = { + 'current_status': ['Status', None, 'status'], + 'speed': ['Speed', 'MB/s', 'kbpersec'], + 'queue_size': ['Queue', 'MB', 'mb'], + 'queue_remaining': ['Left', 'MB', 'mbleft'], + 'disk_size': ['Disk', 'GB', 'diskspacetotal1'], + 'disk_free': ['Disk Free', 'GB', 'diskspace1'], + 'queue_count': ['Queue Count', None, 'noofslots_total'], + 'day_size': ['Daily Total', 'GB', 'day_size'], + 'week_size': ['Weekly Total', 'GB', 'week_size'], + 'month_size': ['Monthly Total', 'GB', 'month_size'], + 'total_size': ['Total', 'GB', 'total_size'], +} + +SPEED_LIMIT_SCHEMA = vol.Schema({ + vol.Optional(ATTR_SPEED, default=DEFAULT_SPEED_LIMIT): cv.string, +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_SENSORS): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + }), +}, extra=vol.ALLOW_EXTRA) + + +async def async_check_sabnzbd(sab_api): + """Check if we can reach SABnzbd.""" + from pysabnzbd import SabnzbdApiException + + try: + await sab_api.check_available() + return True + except SabnzbdApiException: + _LOGGER.error("Connection to SABnzbd API failed") + return False + + +async def async_configure_sabnzbd(hass, config, use_ssl, name=DEFAULT_NAME, + api_key=None): + """Try to configure Sabnzbd and request api key if configuration fails.""" + from pysabnzbd import SabnzbdApi + + host = config[CONF_HOST] + port = config[CONF_PORT] + uri_scheme = 'https' if use_ssl else 'http' + base_url = BASE_URL_FORMAT.format(uri_scheme, host, port) + if api_key is None: + conf = await hass.async_add_job(load_json, + hass.config.path(CONFIG_FILE)) + api_key = conf.get(base_url, {}).get(CONF_API_KEY, '') + + sab_api = SabnzbdApi(base_url, api_key, + session=async_get_clientsession(hass)) + if await async_check_sabnzbd(sab_api): + async_setup_sabnzbd(hass, sab_api, config, name) + else: + async_request_configuration(hass, config, base_url) + + +async def async_setup(hass, config): + """Set up the SABnzbd component.""" + async def sabnzbd_discovered(service, info): + """Handle service discovery.""" + ssl = info.get('properties', {}).get('https', '0') == '1' + await async_configure_sabnzbd(hass, info, ssl) + + discovery.async_listen(hass, SERVICE_SABNZBD, sabnzbd_discovered) + + conf = config.get(DOMAIN) + if conf is not None: + use_ssl = conf.get(CONF_SSL) + name = conf.get(CONF_NAME) + api_key = conf.get(CONF_API_KEY) + await async_configure_sabnzbd(hass, conf, use_ssl, name, api_key) + return True + + +@callback +def async_setup_sabnzbd(hass, sab_api, config, name): + """Set up SABnzbd sensors and services.""" + sab_api_data = SabnzbdApiData(sab_api, name, config.get(CONF_SENSORS, {})) + + if config.get(CONF_SENSORS): + hass.data[DATA_SABNZBD] = sab_api_data + hass.async_create_task( + discovery.async_load_platform(hass, 'sensor', DOMAIN, {}, config)) + + async def async_service_handler(service): + """Handle service calls.""" + if service.service == SERVICE_PAUSE: + await sab_api_data.async_pause_queue() + elif service.service == SERVICE_RESUME: + await sab_api_data.async_resume_queue() + elif service.service == SERVICE_SET_SPEED: + speed = service.data.get(ATTR_SPEED) + await sab_api_data.async_set_queue_speed(speed) + + hass.services.async_register(DOMAIN, SERVICE_PAUSE, + async_service_handler, + schema=vol.Schema({})) + + hass.services.async_register(DOMAIN, SERVICE_RESUME, + async_service_handler, + schema=vol.Schema({})) + + hass.services.async_register(DOMAIN, SERVICE_SET_SPEED, + async_service_handler, + schema=SPEED_LIMIT_SCHEMA) + + async def async_update_sabnzbd(now): + """Refresh SABnzbd queue data.""" + from pysabnzbd import SabnzbdApiException + try: + await sab_api.refresh_data() + async_dispatcher_send(hass, SIGNAL_SABNZBD_UPDATED, None) + except SabnzbdApiException as err: + _LOGGER.error(err) + + async_track_time_interval(hass, async_update_sabnzbd, UPDATE_INTERVAL) + + +@callback +def async_request_configuration(hass, config, host): + """Request configuration steps from the user.""" + from pysabnzbd import SabnzbdApi + + configurator = hass.components.configurator + # We got an error if this method is called while we are configuring + if host in _CONFIGURING: + configurator.async_notify_errors( + _CONFIGURING[host], + 'Failed to register, please try again.') + + return + + async def async_configuration_callback(data): + """Handle configuration changes.""" + api_key = data.get(CONF_API_KEY) + sab_api = SabnzbdApi(host, api_key, + session=async_get_clientsession(hass)) + if not await async_check_sabnzbd(sab_api): + return + + def success(): + """Signal successful setup.""" + conf = load_json(hass.config.path(CONFIG_FILE)) + conf[host] = {CONF_API_KEY: api_key} + save_json(hass.config.path(CONFIG_FILE), conf) + req_config = _CONFIGURING.pop(host) + configurator.request_done(req_config) + + hass.async_add_job(success) + async_setup_sabnzbd(hass, sab_api, config, + config.get(CONF_NAME, DEFAULT_NAME)) + + _CONFIGURING[host] = configurator.async_request_config( + DEFAULT_NAME, + async_configuration_callback, + description='Enter the API Key', + submit_caption='Confirm', + fields=[{'id': CONF_API_KEY, 'name': 'API Key', 'type': ''}] + ) + + +class SabnzbdApiData: + """Class for storing/refreshing sabnzbd api queue data.""" + + def __init__(self, sab_api, name, sensors): + """Initialize component.""" + self.sab_api = sab_api + self.name = name + self.sensors = sensors + + async def async_pause_queue(self): + """Pause Sabnzbd queue.""" + from pysabnzbd import SabnzbdApiException + try: + return await self.sab_api.pause_queue() + except SabnzbdApiException as err: + _LOGGER.error(err) + return False + + async def async_resume_queue(self): + """Resume Sabnzbd queue.""" + from pysabnzbd import SabnzbdApiException + try: + return await self.sab_api.resume_queue() + except SabnzbdApiException as err: + _LOGGER.error(err) + return False + + async def async_set_queue_speed(self, limit): + """Set speed limit for the Sabnzbd queue.""" + from pysabnzbd import SabnzbdApiException + try: + return await self.sab_api.set_speed_limit(limit) + except SabnzbdApiException as err: + _LOGGER.error(err) + return False + + def get_queue_field(self, field): + """Return the value for the given field from the Sabnzbd queue.""" + return self.sab_api.queue.get(field) diff --git a/homeassistant/components/sabnzbd/sensor.py b/homeassistant/components/sabnzbd/sensor.py new file mode 100644 index 0000000000000..ca8fc64eea4a5 --- /dev/null +++ b/homeassistant/components/sabnzbd/sensor.py @@ -0,0 +1,73 @@ +"""Support for monitoring an SABnzbd NZB client.""" +import logging + +from homeassistant.components.sabnzbd import DATA_SABNZBD, \ + SIGNAL_SABNZBD_UPDATED, SENSOR_TYPES +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity + +DEPENDENCIES = ['sabnzbd'] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Set up the SABnzbd sensors.""" + if discovery_info is None: + return + + sab_api_data = hass.data[DATA_SABNZBD] + sensors = sab_api_data.sensors + client_name = sab_api_data.name + async_add_entities([SabnzbdSensor(sensor, sab_api_data, client_name) + for sensor in sensors]) + + +class SabnzbdSensor(Entity): + """Representation of an SABnzbd sensor.""" + + def __init__(self, sensor_type, sabnzbd_api_data, client_name): + """Initialize the sensor.""" + self._client_name = client_name + self._field_name = SENSOR_TYPES[sensor_type][2] + self._name = SENSOR_TYPES[sensor_type][0] + self._sabnzbd_api = sabnzbd_api_data + self._state = None + self._type = sensor_type + self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + + 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) + + @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 + + def should_poll(self): + """Don't poll. Will be updated by dispatcher signal.""" + return False + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self._unit_of_measurement + + def update_state(self, args): + """Get the latest data and updates the states.""" + self._state = self._sabnzbd_api.get_queue_field(self._field_name) + + if self._type == 'speed': + self._state = round(float(self._state) / 1024, 1) + elif 'size' in self._type: + self._state = round(float(self._state), 2) + + self.schedule_update_ha_state() diff --git a/homeassistant/components/satel_integra.py b/homeassistant/components/satel_integra.py deleted file mode 100644 index 25295c6f2ce0d..0000000000000 --- a/homeassistant/components/satel_integra.py +++ /dev/null @@ -1,163 +0,0 @@ -""" -Support for Satel Integra devices. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/satel_integra/ -""" - -import asyncio -import logging - - -import voluptuous as vol - -from homeassistant.core import callback -from homeassistant.const import ( - STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, - STATE_ALARM_TRIGGERED, 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 - -REQUIREMENTS = ['satel_integra==0.2.0'] - -DEFAULT_ALARM_NAME = 'satel_integra' -DEFAULT_PORT = 7094 -DEFAULT_CONF_ARM_HOME_MODE = 1 -DEFAULT_DEVICE_PARTITION = 1 -DEFAULT_ZONE_TYPE = 'motion' - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'satel_integra' - -DATA_SATEL = 'satel_integra' - -CONF_DEVICE_HOST = 'host' -CONF_DEVICE_PORT = 'port' -CONF_DEVICE_PARTITION = 'partition' -CONF_ARM_HOME_MODE = 'arm_home_mode' -CONF_ZONE_NAME = 'name' -CONF_ZONE_TYPE = 'type' -CONF_ZONES = 'zones' -CONF_OUTPUTS = 'outputs' - -ZONES = 'zones' - -SIGNAL_PANEL_MESSAGE = 'satel_integra.panel_message' -SIGNAL_PANEL_ARM_AWAY = 'satel_integra.panel_arm_away' -SIGNAL_PANEL_ARM_HOME = 'satel_integra.panel_arm_home' -SIGNAL_PANEL_DISARM = 'satel_integra.panel_disarm' - -SIGNAL_ZONES_UPDATED = 'satel_integra.zones_updated' -SIGNAL_OUTPUTS_UPDATED = 'satel_integra.outputs_updated' - -ZONE_SCHEMA = vol.Schema({ - vol.Required(CONF_ZONE_NAME): cv.string, - vol.Optional(CONF_ZONE_TYPE, default=DEFAULT_ZONE_TYPE): cv.string}) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_DEVICE_HOST): cv.string, - vol.Optional(CONF_DEVICE_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_DEVICE_PARTITION, - default=DEFAULT_DEVICE_PARTITION): cv.positive_int, - vol.Optional(CONF_ARM_HOME_MODE, - default=DEFAULT_CONF_ARM_HOME_MODE): vol.In([1, 2, 3]), - vol.Optional(CONF_ZONES, - default={}): {vol.Coerce(int): ZONE_SCHEMA}, - vol.Optional(CONF_OUTPUTS, - default={}): {vol.Coerce(int): ZONE_SCHEMA}, - }), -}, extra=vol.ALLOW_EXTRA) - - -async def async_setup(hass, config): - """Set up the Satel Integra component.""" - conf = config.get(DOMAIN) - - zones = conf.get(CONF_ZONES) - outputs = conf.get(CONF_OUTPUTS) - host = conf.get(CONF_DEVICE_HOST) - port = conf.get(CONF_DEVICE_PORT) - partition = conf.get(CONF_DEVICE_PARTITION) - - from satel_integra.satel_integra import AsyncSatel, AlarmState - - controller = AsyncSatel(host, port, hass.loop, zones, outputs, partition) - - hass.data[DATA_SATEL] = controller - - result = await controller.connect() - - if not result: - return False - - async def _close(): - controller.close() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _close()) - - _LOGGER.debug("Arm home config: %s, mode: %s ", - conf, - conf.get(CONF_ARM_HOME_MODE)) - - task_control_panel = hass.async_create_task( - async_load_platform(hass, 'alarm_control_panel', DOMAIN, conf, config)) - - task_zones = hass.async_create_task( - async_load_platform(hass, 'binary_sensor', DOMAIN, - {CONF_ZONES: zones, CONF_OUTPUTS: outputs}, config) - ) - - await asyncio.wait([task_control_panel, task_zones], loop=hass.loop) - - @callback - def alarm_status_update_callback(status): - """Send status update received from alarm to home assistant.""" - _LOGGER.debug("Alarm status callback, status: %s", status) - hass_alarm_status = STATE_ALARM_DISARMED - - if status == AlarmState.ARMED_MODE0: - hass_alarm_status = STATE_ALARM_ARMED_AWAY - - elif status in [ - AlarmState.ARMED_MODE0, - AlarmState.ARMED_MODE1, - AlarmState.ARMED_MODE2, - AlarmState.ARMED_MODE3 - ]: - hass_alarm_status = STATE_ALARM_ARMED_HOME - - elif status in [AlarmState.TRIGGERED, AlarmState.TRIGGERED_FIRE]: - hass_alarm_status = STATE_ALARM_TRIGGERED - - elif status == AlarmState.DISARMED: - hass_alarm_status = STATE_ALARM_DISARMED - - _LOGGER.debug("Sending hass_alarm_status: %s...", hass_alarm_status) - async_dispatcher_send(hass, SIGNAL_PANEL_MESSAGE, hass_alarm_status) - - @callback - def zones_update_callback(status): - """Update zone objects as per notification from the alarm.""" - _LOGGER.debug("Zones callback, status: %s", status) - async_dispatcher_send(hass, SIGNAL_ZONES_UPDATED, status[ZONES]) - - @callback - def outputs_update_callback(status): - """Update zone objects as per notification from the alarm.""" - _LOGGER.debug("Outputs updated callback , status: %s", status) - async_dispatcher_send(hass, SIGNAL_OUTPUTS_UPDATED, status["outputs"]) - - # Create a task instead of adding a tracking job, since this task will - # run until the connection to satel_integra is closed. - hass.loop.create_task(controller.keep_alive()) - hass.loop.create_task( - controller.monitor_status( - alarm_status_update_callback, - zones_update_callback, - outputs_update_callback) - ) - - return True diff --git a/homeassistant/components/satel_integra/__init__.py b/homeassistant/components/satel_integra/__init__.py new file mode 100644 index 0000000000000..bff365a079fc6 --- /dev/null +++ b/homeassistant/components/satel_integra/__init__.py @@ -0,0 +1,157 @@ +"""Support for Satel Integra devices.""" +import asyncio +import logging + + +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, 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 + +REQUIREMENTS = ['satel_integra==0.2.0'] + +DEFAULT_ALARM_NAME = 'satel_integra' +DEFAULT_PORT = 7094 +DEFAULT_CONF_ARM_HOME_MODE = 1 +DEFAULT_DEVICE_PARTITION = 1 +DEFAULT_ZONE_TYPE = 'motion' + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'satel_integra' + +DATA_SATEL = 'satel_integra' + +CONF_DEVICE_HOST = 'host' +CONF_DEVICE_PORT = 'port' +CONF_DEVICE_PARTITION = 'partition' +CONF_ARM_HOME_MODE = 'arm_home_mode' +CONF_ZONE_NAME = 'name' +CONF_ZONE_TYPE = 'type' +CONF_ZONES = 'zones' +CONF_OUTPUTS = 'outputs' + +ZONES = 'zones' + +SIGNAL_PANEL_MESSAGE = 'satel_integra.panel_message' +SIGNAL_PANEL_ARM_AWAY = 'satel_integra.panel_arm_away' +SIGNAL_PANEL_ARM_HOME = 'satel_integra.panel_arm_home' +SIGNAL_PANEL_DISARM = 'satel_integra.panel_disarm' + +SIGNAL_ZONES_UPDATED = 'satel_integra.zones_updated' +SIGNAL_OUTPUTS_UPDATED = 'satel_integra.outputs_updated' + +ZONE_SCHEMA = vol.Schema({ + vol.Required(CONF_ZONE_NAME): cv.string, + vol.Optional(CONF_ZONE_TYPE, default=DEFAULT_ZONE_TYPE): cv.string}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_DEVICE_HOST): cv.string, + vol.Optional(CONF_DEVICE_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_DEVICE_PARTITION, + default=DEFAULT_DEVICE_PARTITION): cv.positive_int, + vol.Optional(CONF_ARM_HOME_MODE, + default=DEFAULT_CONF_ARM_HOME_MODE): vol.In([1, 2, 3]), + vol.Optional(CONF_ZONES, + default={}): {vol.Coerce(int): ZONE_SCHEMA}, + vol.Optional(CONF_OUTPUTS, + default={}): {vol.Coerce(int): ZONE_SCHEMA}, + }), +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the Satel Integra component.""" + conf = config.get(DOMAIN) + + zones = conf.get(CONF_ZONES) + outputs = conf.get(CONF_OUTPUTS) + host = conf.get(CONF_DEVICE_HOST) + port = conf.get(CONF_DEVICE_PORT) + partition = conf.get(CONF_DEVICE_PARTITION) + + from satel_integra.satel_integra import AsyncSatel, AlarmState + + controller = AsyncSatel(host, port, hass.loop, zones, outputs, partition) + + hass.data[DATA_SATEL] = controller + + result = await controller.connect() + + if not result: + return False + + async def _close(): + controller.close() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _close()) + + _LOGGER.debug("Arm home config: %s, mode: %s ", + conf, + conf.get(CONF_ARM_HOME_MODE)) + + task_control_panel = hass.async_create_task( + async_load_platform(hass, 'alarm_control_panel', DOMAIN, conf, config)) + + task_zones = hass.async_create_task( + async_load_platform(hass, 'binary_sensor', DOMAIN, + {CONF_ZONES: zones, CONF_OUTPUTS: outputs}, config) + ) + + await asyncio.wait([task_control_panel, task_zones], loop=hass.loop) + + @callback + def alarm_status_update_callback(status): + """Send status update received from alarm to home assistant.""" + _LOGGER.debug("Alarm status callback, status: %s", status) + hass_alarm_status = STATE_ALARM_DISARMED + + if status == AlarmState.ARMED_MODE0: + hass_alarm_status = STATE_ALARM_ARMED_AWAY + + elif status in [ + AlarmState.ARMED_MODE0, + AlarmState.ARMED_MODE1, + AlarmState.ARMED_MODE2, + AlarmState.ARMED_MODE3 + ]: + hass_alarm_status = STATE_ALARM_ARMED_HOME + + elif status in [AlarmState.TRIGGERED, AlarmState.TRIGGERED_FIRE]: + hass_alarm_status = STATE_ALARM_TRIGGERED + + elif status == AlarmState.DISARMED: + hass_alarm_status = STATE_ALARM_DISARMED + + _LOGGER.debug("Sending hass_alarm_status: %s...", hass_alarm_status) + async_dispatcher_send(hass, SIGNAL_PANEL_MESSAGE, hass_alarm_status) + + @callback + def zones_update_callback(status): + """Update zone objects as per notification from the alarm.""" + _LOGGER.debug("Zones callback, status: %s", status) + async_dispatcher_send(hass, SIGNAL_ZONES_UPDATED, status[ZONES]) + + @callback + def outputs_update_callback(status): + """Update zone objects as per notification from the alarm.""" + _LOGGER.debug("Outputs updated callback , status: %s", status) + async_dispatcher_send(hass, SIGNAL_OUTPUTS_UPDATED, status["outputs"]) + + # Create a task instead of adding a tracking job, since this task will + # run until the connection to satel_integra is closed. + hass.loop.create_task(controller.keep_alive()) + hass.loop.create_task( + controller.monitor_status( + alarm_status_update_callback, + zones_update_callback, + outputs_update_callback) + ) + + return True diff --git a/homeassistant/components/satel_integra/alarm_control_panel.py b/homeassistant/components/satel_integra/alarm_control_panel.py new file mode 100644 index 0000000000000..360acdb24977a --- /dev/null +++ b/homeassistant/components/satel_integra/alarm_control_panel.py @@ -0,0 +1,83 @@ +"""Support for Satel Integra alarm, using ETHM module.""" +import logging + +import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.satel_integra import ( + CONF_ARM_HOME_MODE, DATA_SATEL, SIGNAL_PANEL_MESSAGE) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['satel_integra'] + + +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 + + device = SatelIntegraAlarmPanel( + "Alarm Panel", discovery_info.get(CONF_ARM_HOME_MODE)) + async_add_entities([device]) + + +class SatelIntegraAlarmPanel(alarm.AlarmControlPanel): + """Representation of an AlarmDecoder-based alarm panel.""" + + def __init__(self, name, arm_home_mode): + """Initialize the alarm panel.""" + self._name = name + self._state = None + self._arm_home_mode = arm_home_mode + + async def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect( + self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback) + + @callback + def _message_callback(self, message): + """Handle received messages.""" + if message != self._state: + self._state = message + self.async_schedule_update_ha_state() + else: + _LOGGER.warning("Ignoring alarm status message, same state") + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def should_poll(self): + """Return the polling state.""" + return False + + @property + def code_format(self): + """Return the regex for code format or None if no code is required.""" + return alarm.FORMAT_NUMBER + + @property + def state(self): + """Return the state of the device.""" + return self._state + + async def async_alarm_disarm(self, code=None): + """Send disarm command.""" + if code: + await self.hass.data[DATA_SATEL].disarm(code) + + async def async_alarm_arm_away(self, code=None): + """Send arm away command.""" + if code: + await self.hass.data[DATA_SATEL].arm(code) + + async def async_alarm_arm_home(self, code=None): + """Send arm home command.""" + if code: + await self.hass.data[DATA_SATEL].arm( + code, self._arm_home_mode) diff --git a/homeassistant/components/satel_integra/binary_sensor.py b/homeassistant/components/satel_integra/binary_sensor.py new file mode 100644 index 0000000000000..34ced6287123e --- /dev/null +++ b/homeassistant/components/satel_integra/binary_sensor.py @@ -0,0 +1,96 @@ +"""Support for Satel Integra zone states- represented as binary sensors.""" +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.satel_integra import (CONF_ZONES, + CONF_OUTPUTS, + CONF_ZONE_NAME, + CONF_ZONE_TYPE, + SIGNAL_ZONES_UPDATED, + SIGNAL_OUTPUTS_UPDATED) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +DEPENDENCIES = ['satel_integra'] + +_LOGGER = logging.getLogger(__name__) + + +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 + + configured_zones = discovery_info[CONF_ZONES] + + devices = [] + + 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) + devices.append(device) + + configured_outputs = discovery_info[CONF_OUTPUTS] + + 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) + devices.append(device) + + async_add_entities(devices) + + +class SatelIntegraBinarySensor(BinarySensorDevice): + """Representation of an Satel Integra binary sensor.""" + + def __init__(self, device_number, device_name, zone_type, react_to_signal): + """Initialize the binary_sensor.""" + self._device_number = device_number + self._name = device_name + self._zone_type = zone_type + self._state = 0 + self._react_to_signal = react_to_signal + + async def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect( + self.hass, self._react_to_signal, self._devices_updated) + + @property + def name(self): + """Return the name of the entity.""" + return self._name + + @property + def icon(self): + """Icon for device by its type.""" + if self._zone_type == 'smoke': + return "mdi:fire" + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def is_on(self): + """Return true if sensor is on.""" + return self._state == 1 + + @property + def device_class(self): + """Return the class of this sensor, from DEVICE_CLASSES.""" + return self._zone_type + + @callback + def _devices_updated(self, zones): + """Update the zone's state, if needed.""" + if self._device_number in zones \ + and self._state != zones[self._device_number]: + self._state = zones[self._device_number] + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index b3ab522887557..802512dbf5d95 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 @@ 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/elkm1.py b/homeassistant/components/scene/elkm1.py deleted file mode 100644 index 47dd17a56ae2d..0000000000000 --- a/homeassistant/components/scene/elkm1.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -Support for control of ElkM1 tasks ("macros"). - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/scene.elkm1/ -""" - - -from homeassistant.components.elkm1 import ( - DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities) -from homeassistant.components.scene import Scene - -DEPENDENCIES = [ELK_DOMAIN] - - -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Create the Elk-M1 scene platform.""" - if discovery_info is None: - return - elk = hass.data[ELK_DOMAIN]['elk'] - entities = create_elk_entities(hass, elk.tasks, 'task', ElkTask, []) - async_add_entities(entities, True) - - -class ElkTask(ElkEntity, Scene): - """Elk-M1 task as scene.""" - - async def async_activate(self): - """Activate the task.""" - self._element.activate() diff --git a/homeassistant/components/scene/fibaro.py b/homeassistant/components/scene/fibaro.py deleted file mode 100644 index a0bd4e7ff40ae..0000000000000 --- a/homeassistant/components/scene/fibaro.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -Support for Fibaro scenes. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/scene.fibaro/ -""" -import logging - -from homeassistant.components.scene import ( - Scene) -from homeassistant.components.fibaro import ( - FIBARO_DEVICES, FibaroDevice) - -DEPENDENCIES = ['fibaro'] - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Perform the setup for Fibaro scenes.""" - if discovery_info is None: - return - - async_add_entities( - [FibaroScene(scene) - for scene in hass.data[FIBARO_DEVICES]['scene']], True) - - -class FibaroScene(FibaroDevice, Scene): - """Representation of a Fibaro scene entity.""" - - def activate(self): - """Activate the scene.""" - self.fibaro_device.start() diff --git a/homeassistant/components/scene/homeassistant.py b/homeassistant/components/scene/homeassistant.py index 5812512ccef07..96e24138b4a9d 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 7676deb1a9cd3..7f0709aa6c1cd 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/knx.py b/homeassistant/components/scene/knx.py deleted file mode 100644 index cd333ba79b446..0000000000000 --- a/homeassistant/components/scene/knx.py +++ /dev/null @@ -1,75 +0,0 @@ -""" -Support for KNX scenes. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/scene.knx/ -""" -import voluptuous as vol - -from homeassistant.components.knx import ATTR_DISCOVER_DEVICES, DATA_KNX -from homeassistant.components.scene import CONF_PLATFORM, Scene -from homeassistant.const import CONF_NAME -from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv - -CONF_ADDRESS = 'address' -CONF_SCENE_NUMBER = 'scene_number' - -DEFAULT_NAME = 'KNX SCENE' -DEPENDENCIES = ['knx'] - -PLATFORM_SCHEMA = vol.Schema({ - vol.Required(CONF_PLATFORM): 'knx', - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_ADDRESS): cv.string, - vol.Required(CONF_SCENE_NUMBER): cv.positive_int, -}) - - -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) - else: - async_add_entities_config(hass, config, async_add_entities) - - -@callback -def async_add_entities_discovery(hass, discovery_info, async_add_entities): - """Set up scenes 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(KNXScene(device)) - async_add_entities(entities) - - -@callback -def async_add_entities_config(hass, config, async_add_entities): - """Set up scene for KNX platform configured within platform.""" - import xknx - scene = xknx.devices.Scene( - hass.data[DATA_KNX].xknx, - name=config.get(CONF_NAME), - group_address=config.get(CONF_ADDRESS), - scene_number=config.get(CONF_SCENE_NUMBER)) - hass.data[DATA_KNX].xknx.devices.add(scene) - async_add_entities([KNXScene(scene)]) - - -class KNXScene(Scene): - """Representation of a KNX scene.""" - - def __init__(self, scene): - """Init KNX scene.""" - self.scene = scene - - @property - def name(self): - """Return the name of the scene.""" - return self.scene.name - - async def async_activate(self): - """Activate the scene.""" - await self.scene.run() diff --git a/homeassistant/components/scene/lifx_cloud.py b/homeassistant/components/scene/lifx_cloud.py index c1dda86343d3b..c877bddbe53d7 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 e12643fa651bf..2563c9ceb0c48 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/scene/lutron.py b/homeassistant/components/scene/lutron.py deleted file mode 100644 index bdb8bc344fec6..0000000000000 --- a/homeassistant/components/scene/lutron.py +++ /dev/null @@ -1,53 +0,0 @@ -""" -Support for Lutron scenes. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/scene.lutron/ -""" -import logging - -from homeassistant.components.lutron import ( - LutronDevice, LUTRON_DEVICES, LUTRON_CONTROLLER) -from homeassistant.components.scene import Scene - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['lutron'] - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Lutron scenes.""" - devs = [] - for scene_data in hass.data[LUTRON_DEVICES]['scene']: - (area_name, keypad_name, device, led) = scene_data - dev = LutronScene(area_name, keypad_name, device, led, - hass.data[LUTRON_CONTROLLER]) - devs.append(dev) - - add_entities(devs, True) - - -class LutronScene(LutronDevice, Scene): - """Representation of a Lutron Scene.""" - - 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 - self._led = lutron_led - - def activate(self): - """Activate the scene.""" - self._lutron_device.press() - - @property - def name(self): - """Return the name of the device.""" - return "{} {}: {}".format(self._area_name, - self._keypad_name, - self._lutron_device.name) diff --git a/homeassistant/components/scene/lutron_caseta.py b/homeassistant/components/scene/lutron_caseta.py deleted file mode 100644 index 0ef974e277864..0000000000000 --- a/homeassistant/components/scene/lutron_caseta.py +++ /dev/null @@ -1,46 +0,0 @@ -""" -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/ -""" -import logging - -from homeassistant.components.lutron_caseta import LUTRON_CASETA_SMARTBRIDGE -from homeassistant.components.scene import Scene - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['lutron_caseta'] - - -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] - scenes = bridge.get_scenes() - for scene in scenes: - dev = LutronCasetaScene(scenes[scene], bridge) - devs.append(dev) - - async_add_entities(devs, True) - - -class LutronCasetaScene(Scene): - """Representation of a Lutron Caseta scene.""" - - def __init__(self, scene, bridge): - """Initialize the Lutron Caseta scene.""" - self._scene_name = scene["name"] - self._scene_id = scene["scene_id"] - self._bridge = bridge - - @property - def name(self): - """Return the name of the scene.""" - return self._scene_name - - async def async_activate(self): - """Activate the scene.""" - self._bridge.activate_scene(self._scene_id) diff --git a/homeassistant/components/scene/tahoma.py b/homeassistant/components/scene/tahoma.py deleted file mode 100644 index 5846d97c7f910..0000000000000 --- a/homeassistant/components/scene/tahoma.py +++ /dev/null @@ -1,48 +0,0 @@ -""" -Support for Tahoma scenes. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/scene.tahoma/ -""" -import logging - -from homeassistant.components.scene import Scene -from homeassistant.components.tahoma import ( - DOMAIN as TAHOMA_DOMAIN) - -DEPENDENCIES = ['tahoma'] - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Tahoma scenes.""" - controller = hass.data[TAHOMA_DOMAIN]['controller'] - scenes = [] - for scene in hass.data[TAHOMA_DOMAIN]['scenes']: - scenes.append(TahomaScene(scene, controller)) - add_entities(scenes, True) - - -class TahomaScene(Scene): - """Representation of a Tahoma scene entity.""" - - def __init__(self, tahoma_scene, controller): - """Initialize the scene.""" - self.tahoma_scene = tahoma_scene - self.controller = controller - self._name = self.tahoma_scene.name - - def activate(self): - """Activate the scene.""" - self.controller.launch_action_group(self.tahoma_scene.oid) - - @property - def name(self): - """Return the name of the scene.""" - return self._name - - @property - def device_state_attributes(self): - """Return the state attributes of the scene.""" - return {'tahoma_scene_oid': self.tahoma_scene.oid} diff --git a/homeassistant/components/scene/tuya.py b/homeassistant/components/scene/tuya.py deleted file mode 100644 index 2e03e5dba9a28..0000000000000 --- a/homeassistant/components/scene/tuya.py +++ /dev/null @@ -1,40 +0,0 @@ -""" -Support for the Tuya scene. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/scene.tuya/ -""" -from homeassistant.components.scene import Scene, DOMAIN -from homeassistant.components.tuya import DATA_TUYA, TuyaDevice - -DEPENDENCIES = ['tuya'] - -ENTITY_ID_FORMAT = DOMAIN + '.{}' - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Tuya scenes.""" - if discovery_info is None: - return - tuya = hass.data[DATA_TUYA] - dev_ids = discovery_info.get('dev_ids') - devices = [] - for dev_id in dev_ids: - device = tuya.get_device_by_id(dev_id) - if device is None: - continue - devices.append(TuyaScene(device)) - add_entities(devices) - - -class TuyaScene(TuyaDevice, Scene): - """Tuya Scene.""" - - def __init__(self, tuya): - """Init Tuya scene.""" - super().__init__(tuya) - self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id()) - - def activate(self): - """Activate the scene.""" - self.tuya.activate() diff --git a/homeassistant/components/scene/velux.py b/homeassistant/components/scene/velux.py deleted file mode 100644 index 77ba30158e41f..0000000000000 --- a/homeassistant/components/scene/velux.py +++ /dev/null @@ -1,39 +0,0 @@ -""" -Support for VELUX scenes. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/scene.velux/ -""" - -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.""" - entities = [] - for scene in hass.data[DATA_VELUX].pyvlx.scenes: - entities.append(VeluxScene(scene)) - async_add_entities(entities) - - -class VeluxScene(Scene): - """Representation of a velux scene.""" - - def __init__(self, scene): - """Init velux scene.""" - _LOGGER.info("Adding VELUX scene: %s", scene) - self.scene = scene - - @property - def name(self): - """Return the name of the scene.""" - return self.scene.name - - async def async_activate(self): - """Activate the scene.""" - await self.scene.run() diff --git a/homeassistant/components/scene/vera.py b/homeassistant/components/scene/vera.py deleted file mode 100644 index 6cae1195f8739..0000000000000 --- a/homeassistant/components/scene/vera.py +++ /dev/null @@ -1,55 +0,0 @@ -""" -Support for Vera scenes. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/scene.vera/ -""" -import logging - -from homeassistant.util import slugify -from homeassistant.components.scene import Scene -from homeassistant.components.vera import ( - VERA_CONTROLLER, VERA_SCENES, VERA_ID_FORMAT) - -DEPENDENCIES = ['vera'] - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Vera scenes.""" - add_entities( - [VeraScene(scene, hass.data[VERA_CONTROLLER]) - for scene in hass.data[VERA_SCENES]], True) - - -class VeraScene(Scene): - """Representation of a Vera scene entity.""" - - def __init__(self, vera_scene, controller): - """Initialize the scene.""" - self.vera_scene = vera_scene - self.controller = controller - - self._name = self.vera_scene.name - # Append device id to prevent name clashes in HA. - self.vera_id = VERA_ID_FORMAT.format( - slugify(vera_scene.name), vera_scene.scene_id) - - def update(self): - """Update the scene status.""" - self.vera_scene.refresh() - - def activate(self): - """Activate the scene.""" - self.vera_scene.activate() - - @property - def name(self): - """Return the name of the scene.""" - return self._name - - @property - def device_state_attributes(self): - """Return the state attributes of the scene.""" - return {'vera_scene_id': self.vera_scene.vera_scene_id} diff --git a/homeassistant/components/scene/wink.py b/homeassistant/components/scene/wink.py deleted file mode 100644 index 35db96c3b8b3e..0000000000000 --- a/homeassistant/components/scene/wink.py +++ /dev/null @@ -1,41 +0,0 @@ -""" -Support for Wink scenes. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/scene.wink/ -""" -import logging - -from homeassistant.components.scene import Scene -from homeassistant.components.wink import DOMAIN, WinkDevice - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['wink'] - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Wink platform.""" - import pywink - - for scene in pywink.get_scenes(): - _id = scene.object_id() + scene.name() - if _id not in hass.data[DOMAIN]['unique_ids']: - add_entities([WinkScene(scene, hass)]) - - -class WinkScene(WinkDevice, Scene): - """Representation of a Wink shortcut/scene.""" - - def __init__(self, wink, hass): - """Initialize the Wink device.""" - super().__init__(wink, hass) - hass.data[DOMAIN]['entities']['scene'].append(self) - - async def async_added_to_hass(self): - """Call when entity is added to hass.""" - self.hass.data[DOMAIN]['entities']['scene'].append(self) - - def activate(self): - """Activate the scene.""" - self.wink.activate() diff --git a/homeassistant/components/script.py b/homeassistant/components/script.py deleted file mode 100644 index 15df690746807..0000000000000 --- a/homeassistant/components/script.py +++ /dev/null @@ -1,193 +0,0 @@ -""" -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/ -""" -import asyncio -import logging - -import voluptuous as vol - -from homeassistant.const import ( - ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, - SERVICE_TOGGLE, SERVICE_RELOAD, STATE_ON, CONF_ALIAS, - EVENT_SCRIPT_STARTED, ATTR_NAME) -from homeassistant.loader import bind_hass -from homeassistant.helpers.entity import ToggleEntity -from homeassistant.helpers.entity_component import EntityComponent -import homeassistant.helpers.config_validation as cv - -from homeassistant.helpers.script import Script - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'script' -DEPENDENCIES = ['group'] - -ATTR_CAN_CANCEL = 'can_cancel' -ATTR_LAST_ACTION = 'last_action' -ATTR_LAST_TRIGGERED = 'last_triggered' -ATTR_VARIABLES = 'variables' - -CONF_SEQUENCE = 'sequence' - -ENTITY_ID_FORMAT = DOMAIN + '.{}' - -GROUP_NAME_ALL_SCRIPTS = 'all scripts' - -SCRIPT_ENTRY_SCHEMA = vol.Schema({ - CONF_ALIAS: cv.string, - vol.Required(CONF_SEQUENCE): cv.SCRIPT_SCHEMA, -}) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: cv.schema_with_slug_keys(SCRIPT_ENTRY_SCHEMA) -}, extra=vol.ALLOW_EXTRA) - -SCRIPT_SERVICE_SCHEMA = vol.Schema(dict) -SCRIPT_TURN_ONOFF_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Optional(ATTR_VARIABLES): dict, -}) -RELOAD_SERVICE_SCHEMA = vol.Schema({}) - - -@bind_hass -def is_on(hass, entity_id): - """Return if the script is on based on the statemachine.""" - return hass.states.is_state(entity_id, STATE_ON) - - -async def async_setup(hass, config): - """Load the scripts from the configuration.""" - component = EntityComponent( - _LOGGER, DOMAIN, hass, group_name=GROUP_NAME_ALL_SCRIPTS) - - await _async_process_config(hass, config, component) - - async def reload_service(service): - """Call a service to reload scripts.""" - conf = await component.async_prepare_reload() - if conf is None: - return - - await _async_process_config(hass, conf, component) - - async def turn_on_service(service): - """Call a service to turn script on.""" - # We could turn on script directly here, but we only want to offer - # one way to do it. Otherwise no easy way to detect invocations. - var = service.data.get(ATTR_VARIABLES) - for script in component.async_extract_from_service(service): - await hass.services.async_call(DOMAIN, script.object_id, var, - context=service.context) - - async def turn_off_service(service): - """Cancel a script.""" - # Stopping a script is ok to be done in parallel - await asyncio.wait( - [script.async_turn_off() for script - in component.async_extract_from_service(service)], loop=hass.loop) - - async def toggle_service(service): - """Toggle a script.""" - for script in component.async_extract_from_service(service): - await script.async_toggle(context=service.context) - - hass.services.async_register(DOMAIN, SERVICE_RELOAD, reload_service, - schema=RELOAD_SERVICE_SCHEMA) - hass.services.async_register(DOMAIN, SERVICE_TURN_ON, turn_on_service, - schema=SCRIPT_TURN_ONOFF_SCHEMA) - hass.services.async_register(DOMAIN, SERVICE_TURN_OFF, turn_off_service, - schema=SCRIPT_TURN_ONOFF_SCHEMA) - hass.services.async_register(DOMAIN, SERVICE_TOGGLE, toggle_service, - schema=SCRIPT_TURN_ONOFF_SCHEMA) - - return True - - -async def _async_process_config(hass, config, component): - """Process script configuration.""" - async def service_handler(service): - """Execute a service call to script.