# Conflicts:
#	homeassistant/components/sensor/smappee.py
This commit is contained in:
vandenberghev 2018-06-04 19:44:04 +02:00
commit 2da6d3c223
905 changed files with 40310 additions and 13650 deletions

View File

@ -4,6 +4,8 @@ source = homeassistant
omit = omit =
homeassistant/__main__.py homeassistant/__main__.py
homeassistant/scripts/*.py homeassistant/scripts/*.py
homeassistant/util/async.py
homeassistant/monkey_patch.py
homeassistant/helpers/typing.py homeassistant/helpers/typing.py
homeassistant/helpers/signal.py homeassistant/helpers/signal.py
@ -29,7 +31,7 @@ omit =
homeassistant/components/arduino.py homeassistant/components/arduino.py
homeassistant/components/*/arduino.py homeassistant/components/*/arduino.py
homeassistant/components/bmw_connected_drive.py homeassistant/components/bmw_connected_drive/*.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
@ -94,6 +96,12 @@ omit =
homeassistant/components/envisalink.py homeassistant/components/envisalink.py
homeassistant/components/*/envisalink.py homeassistant/components/*/envisalink.py
homeassistant/components/fritzbox.py
homeassistant/components/*/fritzbox.py
homeassistant/components/eufy.py
homeassistant/components/*/eufy.py
homeassistant/components/gc100.py homeassistant/components/gc100.py
homeassistant/components/*/gc100.py homeassistant/components/*/gc100.py
@ -106,16 +114,25 @@ omit =
homeassistant/components/hive.py homeassistant/components/hive.py
homeassistant/components/*/hive.py homeassistant/components/*/hive.py
homeassistant/components/homekit_controller/__init__.py
homeassistant/components/*/homekit_controller.py
homeassistant/components/homematic/__init__.py homeassistant/components/homematic/__init__.py
homeassistant/components/*/homematic.py homeassistant/components/*/homematic.py
homeassistant/components/homematicip_cloud.py
homeassistant/components/*/homematicip_cloud.py
homeassistant/components/hydrawise.py
homeassistant/components/*/hydrawise.py
homeassistant/components/ihc/* homeassistant/components/ihc/*
homeassistant/components/*/ihc.py homeassistant/components/*/ihc.py
homeassistant/components/insteon_local.py homeassistant/components/insteon_local.py
homeassistant/components/*/insteon_local.py homeassistant/components/*/insteon_local.py
homeassistant/components/insteon_plm.py homeassistant/components/insteon_plm/*
homeassistant/components/*/insteon_plm.py homeassistant/components/*/insteon_plm.py
homeassistant/components/ios.py homeassistant/components/ios.py
@ -139,6 +156,9 @@ omit =
homeassistant/components/knx.py homeassistant/components/knx.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/*/lametric.py homeassistant/components/*/lametric.py
@ -154,12 +174,12 @@ omit =
homeassistant/components/mailgun.py homeassistant/components/mailgun.py
homeassistant/components/*/mailgun.py homeassistant/components/*/mailgun.py
homeassistant/components/matrix.py
homeassistant/components/*/matrix.py
homeassistant/components/maxcube.py homeassistant/components/maxcube.py
homeassistant/components/*/maxcube.py homeassistant/components/*/maxcube.py
homeassistant/components/mercedesme.py
homeassistant/components/*/mercedesme.py
homeassistant/components/mochad.py homeassistant/components/mochad.py
homeassistant/components/*/mochad.py homeassistant/components/*/mochad.py
@ -190,8 +210,8 @@ omit =
homeassistant/components/pilight.py homeassistant/components/pilight.py
homeassistant/components/*/pilight.py homeassistant/components/*/pilight.py
homeassistant/components/qwikswitch.py homeassistant/components/switch/qwikswitch.py
homeassistant/components/*/qwikswitch.py homeassistant/components/light/qwikswitch.py
homeassistant/components/rachio.py homeassistant/components/rachio.py
homeassistant/components/*/rachio.py homeassistant/components/*/rachio.py
@ -199,6 +219,9 @@ omit =
homeassistant/components/raincloud.py homeassistant/components/raincloud.py
homeassistant/components/*/raincloud.py homeassistant/components/*/raincloud.py
homeassistant/components/rainmachine/*
homeassistant/components/*/rainmachine.py
homeassistant/components/raspihats.py homeassistant/components/raspihats.py
homeassistant/components/*/raspihats.py homeassistant/components/*/raspihats.py
@ -211,6 +234,9 @@ omit =
homeassistant/components/rpi_pfio.py homeassistant/components/rpi_pfio.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/*/satel_integra.py homeassistant/components/*/satel_integra.py
@ -286,11 +312,9 @@ omit =
homeassistant/components/*/wink.py homeassistant/components/*/wink.py
homeassistant/components/xiaomi_aqara.py homeassistant/components/xiaomi_aqara.py
homeassistant/components/binary_sensor/xiaomi_aqara.py homeassistant/components/*/xiaomi_aqara.py
homeassistant/components/cover/xiaomi_aqara.py
homeassistant/components/light/xiaomi_aqara.py homeassistant/components/*/xiaomi_miio.py
homeassistant/components/sensor/xiaomi_aqara.py
homeassistant/components/switch/xiaomi_aqara.py
homeassistant/components/zabbix.py homeassistant/components/zabbix.py
homeassistant/components/*/zabbix.py homeassistant/components/*/zabbix.py
@ -309,6 +333,7 @@ omit =
homeassistant/components/alarm_control_panel/canary.py homeassistant/components/alarm_control_panel/canary.py
homeassistant/components/alarm_control_panel/concord232.py homeassistant/components/alarm_control_panel/concord232.py
homeassistant/components/alarm_control_panel/ialarm.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/manual_mqtt.py
homeassistant/components/alarm_control_panel/nx584.py homeassistant/components/alarm_control_panel/nx584.py
homeassistant/components/alarm_control_panel/simplisafe.py homeassistant/components/alarm_control_panel/simplisafe.py
@ -328,10 +353,12 @@ omit =
homeassistant/components/calendar/todoist.py homeassistant/components/calendar/todoist.py
homeassistant/components/camera/bloomsky.py homeassistant/components/camera/bloomsky.py
homeassistant/components/camera/canary.py homeassistant/components/camera/canary.py
homeassistant/components/camera/familyhub.py
homeassistant/components/camera/ffmpeg.py homeassistant/components/camera/ffmpeg.py
homeassistant/components/camera/foscam.py homeassistant/components/camera/foscam.py
homeassistant/components/camera/mjpeg.py homeassistant/components/camera/mjpeg.py
homeassistant/components/camera/onvif.py homeassistant/components/camera/onvif.py
homeassistant/components/camera/proxy.py
homeassistant/components/camera/ring.py homeassistant/components/camera/ring.py
homeassistant/components/camera/rpi_camera.py homeassistant/components/camera/rpi_camera.py
homeassistant/components/camera/synology.py homeassistant/components/camera/synology.py
@ -352,11 +379,13 @@ omit =
homeassistant/components/climate/touchline.py homeassistant/components/climate/touchline.py
homeassistant/components/climate/venstar.py homeassistant/components/climate/venstar.py
homeassistant/components/cover/garadget.py homeassistant/components/cover/garadget.py
homeassistant/components/cover/gogogate2.py
homeassistant/components/cover/homematic.py homeassistant/components/cover/homematic.py
homeassistant/components/cover/knx.py homeassistant/components/cover/knx.py
homeassistant/components/cover/myq.py homeassistant/components/cover/myq.py
homeassistant/components/cover/opengarage.py homeassistant/components/cover/opengarage.py
homeassistant/components/cover/rpi_gpio.py homeassistant/components/cover/rpi_gpio.py
homeassistant/components/cover/ryobi_gdo.py
homeassistant/components/cover/scsgate.py homeassistant/components/cover/scsgate.py
homeassistant/components/device_tracker/actiontec.py homeassistant/components/device_tracker/actiontec.py
homeassistant/components/device_tracker/aruba.py homeassistant/components/device_tracker/aruba.py
@ -369,6 +398,7 @@ omit =
homeassistant/components/device_tracker/cisco_ios.py homeassistant/components/device_tracker/cisco_ios.py
homeassistant/components/device_tracker/ddwrt.py homeassistant/components/device_tracker/ddwrt.py
homeassistant/components/device_tracker/fritz.py homeassistant/components/device_tracker/fritz.py
homeassistant/components/device_tracker/google_maps.py
homeassistant/components/device_tracker/gpslogger.py homeassistant/components/device_tracker/gpslogger.py
homeassistant/components/device_tracker/hitron_coda.py homeassistant/components/device_tracker/hitron_coda.py
homeassistant/components/device_tracker/huawei_router.py homeassistant/components/device_tracker/huawei_router.py
@ -395,30 +425,31 @@ omit =
homeassistant/components/emoncms_history.py homeassistant/components/emoncms_history.py
homeassistant/components/emulated_hue/upnp.py homeassistant/components/emulated_hue/upnp.py
homeassistant/components/fan/mqtt.py homeassistant/components/fan/mqtt.py
homeassistant/components/fan/xiaomi_miio.py homeassistant/components/folder_watcher.py
homeassistant/components/feedreader.py
homeassistant/components/foursquare.py homeassistant/components/foursquare.py
homeassistant/components/goalfeed.py homeassistant/components/goalfeed.py
homeassistant/components/ifttt.py homeassistant/components/ifttt.py
homeassistant/components/image_processing/dlib_face_detect.py homeassistant/components/image_processing/dlib_face_detect.py
homeassistant/components/image_processing/dlib_face_identify.py homeassistant/components/image_processing/dlib_face_identify.py
homeassistant/components/image_processing/seven_segments.py homeassistant/components/image_processing/seven_segments.py
homeassistant/components/keyboard.py
homeassistant/components/keyboard_remote.py homeassistant/components/keyboard_remote.py
homeassistant/components/keyboard.py
homeassistant/components/light/avion.py homeassistant/components/light/avion.py
homeassistant/components/light/blinksticklight.py homeassistant/components/light/blinksticklight.py
homeassistant/components/light/blinkt.py homeassistant/components/light/blinkt.py
homeassistant/components/light/decora.py
homeassistant/components/light/decora_wifi.py homeassistant/components/light/decora_wifi.py
homeassistant/components/light/decora.py
homeassistant/components/light/flux_led.py homeassistant/components/light/flux_led.py
homeassistant/components/light/greenwave.py homeassistant/components/light/greenwave.py
homeassistant/components/light/hue.py homeassistant/components/light/hue.py
homeassistant/components/light/hyperion.py homeassistant/components/light/hyperion.py
homeassistant/components/light/iglo.py homeassistant/components/light/iglo.py
homeassistant/components/light/lifx.py
homeassistant/components/light/lifx_legacy.py homeassistant/components/light/lifx_legacy.py
homeassistant/components/light/lifx.py
homeassistant/components/light/limitlessled.py homeassistant/components/light/limitlessled.py
homeassistant/components/light/lw12wifi.py
homeassistant/components/light/mystrom.py homeassistant/components/light/mystrom.py
homeassistant/components/light/nanoleaf_aurora.py
homeassistant/components/light/osramlightify.py homeassistant/components/light/osramlightify.py
homeassistant/components/light/piglow.py homeassistant/components/light/piglow.py
homeassistant/components/light/rpi_gpio_pwm.py homeassistant/components/light/rpi_gpio_pwm.py
@ -427,7 +458,6 @@ omit =
homeassistant/components/light/tplink.py homeassistant/components/light/tplink.py
homeassistant/components/light/tradfri.py homeassistant/components/light/tradfri.py
homeassistant/components/light/x10.py homeassistant/components/light/x10.py
homeassistant/components/light/xiaomi_miio.py
homeassistant/components/light/yeelight.py homeassistant/components/light/yeelight.py
homeassistant/components/light/yeelightsunflower.py homeassistant/components/light/yeelightsunflower.py
homeassistant/components/light/zengge.py homeassistant/components/light/zengge.py
@ -436,12 +466,14 @@ omit =
homeassistant/components/lock/nello.py homeassistant/components/lock/nello.py
homeassistant/components/lock/nuki.py homeassistant/components/lock/nuki.py
homeassistant/components/lock/sesame.py homeassistant/components/lock/sesame.py
homeassistant/components/map.py
homeassistant/components/media_extractor.py homeassistant/components/media_extractor.py
homeassistant/components/media_player/anthemav.py homeassistant/components/media_player/anthemav.py
homeassistant/components/media_player/aquostv.py homeassistant/components/media_player/aquostv.py
homeassistant/components/media_player/bluesound.py homeassistant/components/media_player/bluesound.py
homeassistant/components/media_player/braviatv.py homeassistant/components/media_player/braviatv.py
homeassistant/components/media_player/cast.py homeassistant/components/media_player/cast.py
homeassistant/components/media_player/channels.py
homeassistant/components/media_player/clementine.py homeassistant/components/media_player/clementine.py
homeassistant/components/media_player/cmus.py homeassistant/components/media_player/cmus.py
homeassistant/components/media_player/denon.py homeassistant/components/media_player/denon.py
@ -482,8 +514,8 @@ omit =
homeassistant/components/media_player/vlc.py homeassistant/components/media_player/vlc.py
homeassistant/components/media_player/volumio.py homeassistant/components/media_player/volumio.py
homeassistant/components/media_player/xiaomi_tv.py homeassistant/components/media_player/xiaomi_tv.py
homeassistant/components/media_player/yamaha.py
homeassistant/components/media_player/yamaha_musiccast.py homeassistant/components/media_player/yamaha_musiccast.py
homeassistant/components/media_player/yamaha.py
homeassistant/components/media_player/ziggo_mediabox_xl.py homeassistant/components/media_player/ziggo_mediabox_xl.py
homeassistant/components/mycroft.py homeassistant/components/mycroft.py
homeassistant/components/notify/aws_lambda.py homeassistant/components/notify/aws_lambda.py
@ -494,6 +526,7 @@ omit =
homeassistant/components/notify/clicksend.py homeassistant/components/notify/clicksend.py
homeassistant/components/notify/clicksend_tts.py homeassistant/components/notify/clicksend_tts.py
homeassistant/components/notify/discord.py homeassistant/components/notify/discord.py
homeassistant/components/notify/flock.py
homeassistant/components/notify/free_mobile.py homeassistant/components/notify/free_mobile.py
homeassistant/components/notify/gntp.py homeassistant/components/notify/gntp.py
homeassistant/components/notify/group.py homeassistant/components/notify/group.py
@ -502,11 +535,10 @@ omit =
homeassistant/components/notify/kodi.py homeassistant/components/notify/kodi.py
homeassistant/components/notify/lannouncer.py homeassistant/components/notify/lannouncer.py
homeassistant/components/notify/llamalab_automate.py homeassistant/components/notify/llamalab_automate.py
homeassistant/components/notify/matrix.py homeassistant/components/notify/mastodon.py
homeassistant/components/notify/message_bird.py homeassistant/components/notify/message_bird.py
homeassistant/components/notify/mycroft.py homeassistant/components/notify/mycroft.py
homeassistant/components/notify/nfandroidtv.py homeassistant/components/notify/nfandroidtv.py
homeassistant/components/notify/nma.py
homeassistant/components/notify/prowl.py homeassistant/components/notify/prowl.py
homeassistant/components/notify/pushbullet.py homeassistant/components/notify/pushbullet.py
homeassistant/components/notify/pushetta.py homeassistant/components/notify/pushetta.py
@ -518,6 +550,7 @@ omit =
homeassistant/components/notify/simplepush.py homeassistant/components/notify/simplepush.py
homeassistant/components/notify/slack.py homeassistant/components/notify/slack.py
homeassistant/components/notify/smtp.py homeassistant/components/notify/smtp.py
homeassistant/components/notify/stride.py
homeassistant/components/notify/synology_chat.py homeassistant/components/notify/synology_chat.py
homeassistant/components/notify/syslog.py homeassistant/components/notify/syslog.py
homeassistant/components/notify/telegram.py homeassistant/components/notify/telegram.py
@ -531,7 +564,6 @@ omit =
homeassistant/components/remember_the_milk/__init__.py homeassistant/components/remember_the_milk/__init__.py
homeassistant/components/remote/harmony.py homeassistant/components/remote/harmony.py
homeassistant/components/remote/itach.py homeassistant/components/remote/itach.py
homeassistant/components/remote/xiaomi_miio.py
homeassistant/components/scene/hunterdouglas_powerview.py homeassistant/components/scene/hunterdouglas_powerview.py
homeassistant/components/scene/lifx_cloud.py homeassistant/components/scene/lifx_cloud.py
homeassistant/components/sensor/airvisual.py homeassistant/components/sensor/airvisual.py
@ -554,13 +586,13 @@ omit =
homeassistant/components/sensor/crimereports.py homeassistant/components/sensor/crimereports.py
homeassistant/components/sensor/cups.py homeassistant/components/sensor/cups.py
homeassistant/components/sensor/currencylayer.py homeassistant/components/sensor/currencylayer.py
homeassistant/components/sensor/darksky.py
homeassistant/components/sensor/deluge.py homeassistant/components/sensor/deluge.py
homeassistant/components/sensor/deutsche_bahn.py homeassistant/components/sensor/deutsche_bahn.py
homeassistant/components/sensor/dht.py homeassistant/components/sensor/dht.py
homeassistant/components/sensor/discogs.py homeassistant/components/sensor/discogs.py
homeassistant/components/sensor/dnsip.py homeassistant/components/sensor/dnsip.py
homeassistant/components/sensor/dovado.py homeassistant/components/sensor/dovado.py
homeassistant/components/sensor/domain_expiry.py
homeassistant/components/sensor/dte_energy_bridge.py homeassistant/components/sensor/dte_energy_bridge.py
homeassistant/components/sensor/dublin_bus_transport.py homeassistant/components/sensor/dublin_bus_transport.py
homeassistant/components/sensor/dwd_weather_warnings.py homeassistant/components/sensor/dwd_weather_warnings.py
@ -573,9 +605,11 @@ omit =
homeassistant/components/sensor/fastdotcom.py homeassistant/components/sensor/fastdotcom.py
homeassistant/components/sensor/fedex.py homeassistant/components/sensor/fedex.py
homeassistant/components/sensor/filesize.py homeassistant/components/sensor/filesize.py
homeassistant/components/sensor/fints.py
homeassistant/components/sensor/fitbit.py homeassistant/components/sensor/fitbit.py
homeassistant/components/sensor/fixer.py homeassistant/components/sensor/fixer.py
homeassistant/components/sensor/folder.py homeassistant/components/sensor/folder.py
homeassistant/components/sensor/foobot.py
homeassistant/components/sensor/fritzbox_callmonitor.py homeassistant/components/sensor/fritzbox_callmonitor.py
homeassistant/components/sensor/fritzbox_netmonitor.py homeassistant/components/sensor/fritzbox_netmonitor.py
homeassistant/components/sensor/gearbest.py homeassistant/components/sensor/gearbest.py
@ -588,9 +622,10 @@ omit =
homeassistant/components/sensor/haveibeenpwned.py homeassistant/components/sensor/haveibeenpwned.py
homeassistant/components/sensor/hp_ilo.py homeassistant/components/sensor/hp_ilo.py
homeassistant/components/sensor/htu21d.py homeassistant/components/sensor/htu21d.py
homeassistant/components/sensor/imap.py
homeassistant/components/sensor/imap_email_content.py homeassistant/components/sensor/imap_email_content.py
homeassistant/components/sensor/imap.py
homeassistant/components/sensor/influxdb.py homeassistant/components/sensor/influxdb.py
homeassistant/components/sensor/iperf3.py
homeassistant/components/sensor/irish_rail_transport.py homeassistant/components/sensor/irish_rail_transport.py
homeassistant/components/sensor/kwb.py homeassistant/components/sensor/kwb.py
homeassistant/components/sensor/lacrosse.py homeassistant/components/sensor/lacrosse.py
@ -601,6 +636,7 @@ omit =
homeassistant/components/sensor/lyft.py homeassistant/components/sensor/lyft.py
homeassistant/components/sensor/metoffice.py homeassistant/components/sensor/metoffice.py
homeassistant/components/sensor/miflora.py homeassistant/components/sensor/miflora.py
homeassistant/components/sensor/mitemp_bt.py
homeassistant/components/sensor/modem_callerid.py homeassistant/components/sensor/modem_callerid.py
homeassistant/components/sensor/mopar.py homeassistant/components/sensor/mopar.py
homeassistant/components/sensor/mqtt_room.py homeassistant/components/sensor/mqtt_room.py
@ -621,6 +657,7 @@ omit =
homeassistant/components/sensor/plex.py homeassistant/components/sensor/plex.py
homeassistant/components/sensor/pocketcasts.py homeassistant/components/sensor/pocketcasts.py
homeassistant/components/sensor/pollen.py homeassistant/components/sensor/pollen.py
homeassistant/components/sensor/postnl.py
homeassistant/components/sensor/pushbullet.py homeassistant/components/sensor/pushbullet.py
homeassistant/components/sensor/pvoutput.py homeassistant/components/sensor/pvoutput.py
homeassistant/components/sensor/pyload.py homeassistant/components/sensor/pyload.py
@ -628,18 +665,20 @@ omit =
homeassistant/components/sensor/radarr.py homeassistant/components/sensor/radarr.py
homeassistant/components/sensor/rainbird.py homeassistant/components/sensor/rainbird.py
homeassistant/components/sensor/ripple.py homeassistant/components/sensor/ripple.py
homeassistant/components/sensor/sabnzbd.py
homeassistant/components/sensor/scrape.py homeassistant/components/sensor/scrape.py
homeassistant/components/sensor/sense.py homeassistant/components/sensor/sense.py
homeassistant/components/sensor/sensehat.py homeassistant/components/sensor/sensehat.py
homeassistant/components/sensor/serial.py
homeassistant/components/sensor/serial_pm.py homeassistant/components/sensor/serial_pm.py
homeassistant/components/sensor/serial.py
homeassistant/components/sensor/sht31.py
homeassistant/components/sensor/shodan.py homeassistant/components/sensor/shodan.py
homeassistant/components/sensor/sigfox.py
homeassistant/components/sensor/simulated.py homeassistant/components/sensor/simulated.py
homeassistant/components/sensor/skybeacon.py homeassistant/components/sensor/skybeacon.py
homeassistant/components/sensor/sma.py homeassistant/components/sensor/sma.py
homeassistant/components/sensor/snmp.py homeassistant/components/sensor/snmp.py
homeassistant/components/sensor/sochain.py homeassistant/components/sensor/sochain.py
homeassistant/components/sensor/socialblade.py
homeassistant/components/sensor/sonarr.py homeassistant/components/sensor/sonarr.py
homeassistant/components/sensor/speedtest.py homeassistant/components/sensor/speedtest.py
homeassistant/components/sensor/spotcrime.py homeassistant/components/sensor/spotcrime.py
@ -647,6 +686,7 @@ omit =
homeassistant/components/sensor/supervisord.py homeassistant/components/sensor/supervisord.py
homeassistant/components/sensor/swiss_hydrological_data.py homeassistant/components/sensor/swiss_hydrological_data.py
homeassistant/components/sensor/swiss_public_transport.py homeassistant/components/sensor/swiss_public_transport.py
homeassistant/components/sensor/syncthru.py
homeassistant/components/sensor/synologydsm.py homeassistant/components/sensor/synologydsm.py
homeassistant/components/sensor/systemmonitor.py homeassistant/components/sensor/systemmonitor.py
homeassistant/components/sensor/sytadin.py homeassistant/components/sensor/sytadin.py
@ -656,15 +696,18 @@ omit =
homeassistant/components/sensor/tibber.py homeassistant/components/sensor/tibber.py
homeassistant/components/sensor/time_date.py homeassistant/components/sensor/time_date.py
homeassistant/components/sensor/torque.py homeassistant/components/sensor/torque.py
homeassistant/components/sensor/trafikverket_weatherstation.py
homeassistant/components/sensor/transmission.py homeassistant/components/sensor/transmission.py
homeassistant/components/sensor/travisci.py homeassistant/components/sensor/travisci.py
homeassistant/components/sensor/twitch.py homeassistant/components/sensor/twitch.py
homeassistant/components/sensor/uber.py homeassistant/components/sensor/uber.py
homeassistant/components/sensor/upnp.py homeassistant/components/sensor/upnp.py
homeassistant/components/sensor/ups.py homeassistant/components/sensor/ups.py
homeassistant/components/sensor/uscis.py
homeassistant/components/sensor/vasttrafik.py homeassistant/components/sensor/vasttrafik.py
homeassistant/components/sensor/viaggiatreno.py homeassistant/components/sensor/viaggiatreno.py
homeassistant/components/sensor/waqi.py homeassistant/components/sensor/waqi.py
homeassistant/components/sensor/waze_travel_time.py
homeassistant/components/sensor/whois.py homeassistant/components/sensor/whois.py
homeassistant/components/sensor/worldtidesinfo.py homeassistant/components/sensor/worldtidesinfo.py
homeassistant/components/sensor/worxlandroid.py homeassistant/components/sensor/worxlandroid.py
@ -690,14 +733,13 @@ omit =
homeassistant/components/switch/orvibo.py homeassistant/components/switch/orvibo.py
homeassistant/components/switch/pulseaudio_loopback.py homeassistant/components/switch/pulseaudio_loopback.py
homeassistant/components/switch/rainbird.py homeassistant/components/switch/rainbird.py
homeassistant/components/switch/rainmachine.py
homeassistant/components/switch/rest.py homeassistant/components/switch/rest.py
homeassistant/components/switch/rpi_rf.py homeassistant/components/switch/rpi_rf.py
homeassistant/components/switch/snmp.py homeassistant/components/switch/snmp.py
homeassistant/components/switch/telnet.py homeassistant/components/switch/telnet.py
homeassistant/components/switch/tplink.py homeassistant/components/switch/tplink.py
homeassistant/components/switch/transmission.py homeassistant/components/switch/transmission.py
homeassistant/components/switch/xiaomi_miio.py homeassistant/components/switch/vesync.py
homeassistant/components/telegram_bot/* homeassistant/components/telegram_bot/*
homeassistant/components/thingspeak.py homeassistant/components/thingspeak.py
homeassistant/components/tts/amazon_polly.py homeassistant/components/tts/amazon_polly.py
@ -706,7 +748,6 @@ omit =
homeassistant/components/tts/picotts.py homeassistant/components/tts/picotts.py
homeassistant/components/vacuum/mqtt.py homeassistant/components/vacuum/mqtt.py
homeassistant/components/vacuum/roomba.py homeassistant/components/vacuum/roomba.py
homeassistant/components/vacuum/xiaomi_miio.py
homeassistant/components/weather/bom.py homeassistant/components/weather/bom.py
homeassistant/components/weather/buienradar.py homeassistant/components/weather/buienradar.py
homeassistant/components/weather/darksky.py homeassistant/components/weather/darksky.py

View File

@ -1,35 +1,45 @@
Make sure you are running the latest version of Home Assistant before reporting an issue. <!-- READ THIS FIRST:
- If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases
- Do not report issues for components if you are using custom components: files in <config-dir>/custom_components
- This is for bugs only. Feature and enhancement requests should go in our community forum: https://community.home-assistant.io/c/feature-requests
- Provide as many details as possible. Paste logs, configuration sample and code into the backticks. Do not delete any text from this template!
-->
You should only file an issue if you found a bug. Feature and enhancement requests should go in [the Feature Requests section](https://community.home-assistant.io/c/feature-requests) of our community forum: **Home Assistant release with the issue:**
<!--
**Home Assistant release (`hass --version`):** - Frontend -> Developer tools -> Info
- Or use this command: hass --version
-->
**Python release (`python3 --version`):** **Last working Home Assistant release (if known):**
**Operating environment (Hass.io/Docker/Windows/etc.):**
<!--
Please provide details about your environment.
-->
**Component/platform:** **Component/platform:**
<!--
Please add the link to the documentation at https://www.home-assistant.io/components/ of the component/platform in question.
-->
**Description of problem:** **Description of problem:**
**Expected:**
**Problem-relevant `configuration.yaml` entries and (fill out even if it seems unimportant):**
**Problem-relevant `configuration.yaml` entries and steps to reproduce:**
```yaml ```yaml
``` ```
1.
2.
3.
**Traceback (if applicable):** **Traceback (if applicable):**
```bash ```
``` ```
**Additional info:** **Additional information:**

50
.github/ISSUE_TEMPLATE/Bug_report.md vendored Normal file
View File

@ -0,0 +1,50 @@
---
name: Bug report
about: Create a report to help us improve
---
<!-- READ THIS FIRST:
- If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases
- Do not report issues for components if you are using custom components: files in <config-dir>/custom_components
- This is for bugs only. Feature and enhancement requests should go in our community forum: https://community.home-assistant.io/c/feature-requests
- Provide as many details as possible. Paste logs, configuration sample and code into the backticks. Do not delete any text from this template!
-->
**Home Assistant release with the issue:**
<!--
- Frontend -> Developer tools -> Info
- Or use this command: hass --version
-->
**Last working Home Assistant release (if known):**
**Operating environment (Hass.io/Docker/Windows/etc.):**
<!--
Please provide details about your environment.
-->
**Component/platform:**
<!--
Please add the link to the documentation at https://www.home-assistant.io/components/ of the component/platform in question.
-->
**Description of problem:**
**Problem-relevant `configuration.yaml` entries and (fill out even if it seems unimportant):**
```yaml
```
**Traceback (if applicable):**
```
```
**Additional information:**

View File

@ -20,7 +20,7 @@ If user exposed functionality or configuration variables are added/changed:
If the code communicates with devices, web services, or third-party tools: If the code communicates with devices, web services, or third-party tools:
- [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]). - [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]).
- [ ] New dependencies are only imported inside functions that use them ([example][ex-import]). - [ ] New dependencies are only imported inside functions that use them ([example][ex-import]).
- [ ] New dependencies have been added to `requirements_all.txt` by running `script/gen_requirements_all.py`. - [ ] New or updated dependencies have been added to `requirements_all.txt` by running `script/gen_requirements_all.py`.
- [ ] New files were added to `.coveragerc`. - [ ] New files were added to `.coveragerc`.
If the code does not interact with devices: If the code does not interact with devices:

1
.gitignore vendored
View File

@ -21,6 +21,7 @@ Icon
*.iml *.iml
# pytest # pytest
.pytest_cache
.cache .cache
# GITHUB Proposed Python stuff: # GITHUB Proposed Python stuff:

0
.gitmodules vendored
View File

View File

@ -10,8 +10,8 @@ matrix:
env: TOXENV=lint env: TOXENV=lint
- python: "3.5.3" - python: "3.5.3"
env: TOXENV=pylint env: TOXENV=pylint
# - python: "3.5" - python: "3.5.3"
# env: TOXENV=typing env: TOXENV=typing
- python: "3.5.3" - python: "3.5.3"
env: TOXENV=py35 env: TOXENV=py35
- python: "3.6" - python: "3.6"
@ -31,7 +31,7 @@ script: travis_wait 30 tox --develop
services: services:
- docker - docker
before_deploy: before_deploy:
- docker pull lokalise/lokalise-cli@sha256:79b3108211ed1fcc9f7b09a011bfc53c240fc2f3b7fa7f0c8390f593271b4cd7 - docker pull lokalise/lokalise-cli@sha256:2198814ebddfda56ee041a4b427521757dd57f75415ea9693696a64c550cef21
deploy: deploy:
skip_cleanup: true skip_cleanup: true
provider: script provider: script

View File

@ -29,9 +29,6 @@ homeassistant/components/weblink.py @home-assistant/core
homeassistant/components/websocket_api.py @home-assistant/core homeassistant/components/websocket_api.py @home-assistant/core
homeassistant/components/zone.py @home-assistant/core homeassistant/components/zone.py @home-assistant/core
# To monitor non-pypi additions
requirements_all.txt @andrey-git
# HomeAssistant developer Teams # HomeAssistant developer Teams
Dockerfile @home-assistant/docker Dockerfile @home-assistant/docker
virtualization/Docker/* @home-assistant/docker virtualization/Docker/* @home-assistant/docker
@ -43,20 +40,25 @@ homeassistant/components/hassio.py @home-assistant/hassio
# Individual components # Individual components
homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt
homeassistant/components/alarm_control_panel/manual_mqtt.py @colinodell
homeassistant/components/binary_sensor/hikvision.py @mezz64 homeassistant/components/binary_sensor/hikvision.py @mezz64
homeassistant/components/bmw_connected_drive.py @ChristianKuehnel homeassistant/components/bmw_connected_drive.py @ChristianKuehnel
homeassistant/components/camera/yi.py @bachya homeassistant/components/camera/yi.py @bachya
homeassistant/components/climate/ephember.py @ttroy50 homeassistant/components/climate/ephember.py @ttroy50
homeassistant/components/climate/eq3btsmart.py @rytilahti homeassistant/components/climate/eq3btsmart.py @rytilahti
homeassistant/components/climate/sensibo.py @andrey-git homeassistant/components/climate/sensibo.py @andrey-git
homeassistant/components/cover/group.py @cdce8p
homeassistant/components/cover/template.py @PhracturedBlue homeassistant/components/cover/template.py @PhracturedBlue
homeassistant/components/device_tracker/automatic.py @armills homeassistant/components/device_tracker/automatic.py @armills
homeassistant/components/device_tracker/tile.py @bachya homeassistant/components/device_tracker/tile.py @bachya
homeassistant/components/history_graph.py @andrey-git homeassistant/components/history_graph.py @andrey-git
homeassistant/components/light/tplink.py @rytilahti homeassistant/components/light/tplink.py @rytilahti
homeassistant/components/light/yeelight.py @rytilahti homeassistant/components/light/yeelight.py @rytilahti
homeassistant/components/lock/nello.py @pschmitt
homeassistant/components/lock/nuki.py @pschmitt
homeassistant/components/media_player/emby.py @mezz64 homeassistant/components/media_player/emby.py @mezz64
homeassistant/components/media_player/kodi.py @armills homeassistant/components/media_player/kodi.py @armills
homeassistant/components/media_player/liveboxplaytv.py @pschmitt
homeassistant/components/media_player/mediaroom.py @dgomes homeassistant/components/media_player/mediaroom.py @dgomes
homeassistant/components/media_player/monoprice.py @etsinko homeassistant/components/media_player/monoprice.py @etsinko
homeassistant/components/media_player/sonos.py @amelchio homeassistant/components/media_player/sonos.py @amelchio
@ -64,32 +66,42 @@ homeassistant/components/media_player/xiaomi_tv.py @fattdev
homeassistant/components/media_player/yamaha_musiccast.py @jalmeroth homeassistant/components/media_player/yamaha_musiccast.py @jalmeroth
homeassistant/components/plant.py @ChristianKuehnel homeassistant/components/plant.py @ChristianKuehnel
homeassistant/components/sensor/airvisual.py @bachya homeassistant/components/sensor/airvisual.py @bachya
homeassistant/components/sensor/filter.py @dgomes
homeassistant/components/sensor/gearbest.py @HerrHofrat homeassistant/components/sensor/gearbest.py @HerrHofrat
homeassistant/components/sensor/irish_rail_transport.py @ttroy50 homeassistant/components/sensor/irish_rail_transport.py @ttroy50
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
homeassistant/components/sensor/pollen.py @bachya homeassistant/components/sensor/pollen.py @bachya
homeassistant/components/sensor/sytadin.py @gautric homeassistant/components/sensor/qnap.py @colinodell
homeassistant/components/sensor/sma.py @kellerza
homeassistant/components/sensor/sql.py @dgomes homeassistant/components/sensor/sql.py @dgomes
homeassistant/components/sensor/sytadin.py @gautric
homeassistant/components/sensor/tibber.py @danielhiversen homeassistant/components/sensor/tibber.py @danielhiversen
homeassistant/components/sensor/upnp.py @dgomes
homeassistant/components/sensor/waqi.py @andrey-git homeassistant/components/sensor/waqi.py @andrey-git
homeassistant/components/switch/rainmachine.py @bachya
homeassistant/components/switch/tplink.py @rytilahti homeassistant/components/switch/tplink.py @rytilahti
homeassistant/components/vacuum/roomba.py @pschmitt
homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
homeassistant/components/*/axis.py @kane610 homeassistant/components/*/axis.py @kane610
homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel
homeassistant/components/*/broadlink.py @danielhiversen homeassistant/components/*/broadlink.py @danielhiversen
homeassistant/components/*/deconz.py @kane610
homeassistant/components/eight_sleep.py @mezz64 homeassistant/components/eight_sleep.py @mezz64
homeassistant/components/*/eight_sleep.py @mezz64 homeassistant/components/*/eight_sleep.py @mezz64
homeassistant/components/hive.py @Rendili @KJonline homeassistant/components/hive.py @Rendili @KJonline
homeassistant/components/*/hive.py @Rendili @KJonline homeassistant/components/*/hive.py @Rendili @KJonline
homeassistant/components/homekit/* @cdce8p homeassistant/components/homekit/* @cdce8p
homeassistant/components/*/deconz.py @kane610
homeassistant/components/*/rfxtrx.py @danielhiversen
homeassistant/components/velux.py @Julius2342
homeassistant/components/*/velux.py @Julius2342
homeassistant/components/knx.py @Julius2342 homeassistant/components/knx.py @Julius2342
homeassistant/components/*/knx.py @Julius2342 homeassistant/components/*/knx.py @Julius2342
homeassistant/components/konnected.py @heythisisnate
homeassistant/components/*/konnected.py @heythisisnate
homeassistant/components/matrix.py @tinloaf
homeassistant/components/*/matrix.py @tinloaf
homeassistant/components/qwikswitch.py @kellerza
homeassistant/components/*/qwikswitch.py @kellerza
homeassistant/components/rainmachine/* @bachya
homeassistant/components/*/rainmachine.py @bachya
homeassistant/components/*/rfxtrx.py @danielhiversen
homeassistant/components/tahoma.py @philklei homeassistant/components/tahoma.py @philklei
homeassistant/components/*/tahoma.py @philklei homeassistant/components/*/tahoma.py @philklei
homeassistant/components/tesla.py @zabuldon homeassistant/components/tesla.py @zabuldon
@ -97,5 +109,9 @@ homeassistant/components/*/tesla.py @zabuldon
homeassistant/components/tellduslive.py @molobrakos @fredrike homeassistant/components/tellduslive.py @molobrakos @fredrike
homeassistant/components/*/tellduslive.py @molobrakos @fredrike homeassistant/components/*/tellduslive.py @molobrakos @fredrike
homeassistant/components/*/tradfri.py @ggravlingen homeassistant/components/*/tradfri.py @ggravlingen
homeassistant/components/velux.py @Julius2342
homeassistant/components/*/velux.py @Julius2342
homeassistant/components/*/xiaomi_aqara.py @danielhiversen @syssi homeassistant/components/*/xiaomi_aqara.py @danielhiversen @syssi
homeassistant/components/*/xiaomi_miio.py @rytilahti @syssi homeassistant/components/*/xiaomi_miio.py @rytilahti @syssi
homeassistant/scripts/check_config.py @kellerza

View File

@ -4,7 +4,7 @@ Everybody is invited and welcome to contribute to Home Assistant. There is a lot
The process is straight-forward. The process is straight-forward.
- Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/devel/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0) - Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0)
- Fork the Home Assistant [git repository](https://github.com/home-assistant/home-assistant). - Fork the Home Assistant [git repository](https://github.com/home-assistant/home-assistant).
- Write the code for your device, notification service, sensor, or IoT thing. - Write the code for your device, notification service, sensor, or IoT thing.
- Ensure tests work. - Ensure tests work.

View File

@ -12,6 +12,7 @@ LABEL maintainer="Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>"
#ENV INSTALL_LIBCEC no #ENV INSTALL_LIBCEC no
#ENV INSTALL_PHANTOMJS no #ENV INSTALL_PHANTOMJS no
#ENV INSTALL_SSOCR no #ENV INSTALL_SSOCR no
#ENV INSTALL_IPERF3 no
VOLUME /config VOLUME /config

View File

@ -1,606 +0,0 @@
swagger: '2.0'
info:
title: Home Assistant
description: Home Assistant REST API
version: "1.0.1"
# the domain of the service
host: localhost:8123
# array of all schemes that your API supports
schemes:
- http
- https
securityDefinitions:
#api_key:
# type: apiKey
# description: API password
# name: api_password
# in: query
api_key:
type: apiKey
description: API password
name: x-ha-access
in: header
# will be prefixed to all paths
basePath: /api
consumes:
- application/json
produces:
- application/json
paths:
/:
get:
summary: API alive message
description: Returns message if API is up and running.
tags:
- Core
security:
- api_key: []
responses:
200:
description: API is up and running
schema:
$ref: '#/definitions/Message'
default:
description: Error
schema:
$ref: '#/definitions/Message'
/config:
get:
summary: API alive message
description: Returns the current configuration as JSON.
tags:
- Core
security:
- api_key: []
responses:
200:
description: Current configuration
schema:
$ref: '#/definitions/ApiConfig'
default:
description: Error
schema:
$ref: '#/definitions/Message'
/discovery_info:
get:
summary: Basic information about Home Assistant instance
tags:
- Core
responses:
200:
description: Basic information
schema:
$ref: '#/definitions/DiscoveryInfo'
default:
description: Error
schema:
$ref: '#/definitions/Message'
/bootstrap:
get:
summary: Returns all data needed to bootstrap Home Assistant.
tags:
- Core
security:
- api_key: []
responses:
200:
description: Bootstrap information
schema:
$ref: '#/definitions/BootstrapInfo'
default:
description: Error
schema:
$ref: '#/definitions/Message'
/events:
get:
summary: Array of event objects.
description: Returns an array of event objects. Each event object contain event name and listener count.
tags:
- Events
security:
- api_key: []
responses:
200:
description: Events
schema:
type: array
items:
$ref: '#/definitions/Event'
default:
description: Error
schema:
$ref: '#/definitions/Message'
/services:
get:
summary: Array of service objects.
description: Returns an array of service objects. Each object contains the domain and which services it contains.
tags:
- Services
security:
- api_key: []
responses:
200:
description: Services
schema:
type: array
items:
$ref: '#/definitions/Service'
default:
description: Error
schema:
$ref: '#/definitions/Message'
/history:
get:
summary: Array of state changes in the past.
description: Returns an array of state changes in the past. Each object contains further detail for the entities.
tags:
- State
security:
- api_key: []
responses:
200:
description: State changes
schema:
type: array
items:
$ref: '#/definitions/History'
default:
description: Error
schema:
$ref: '#/definitions/Message'
/states:
get:
summary: Array of state objects.
description: |
Returns an array of state objects. Each state has the following attributes: entity_id, state, last_changed and attributes.
tags:
- State
security:
- api_key: []
responses:
200:
description: States
schema:
type: array
items:
$ref: '#/definitions/State'
default:
description: Error
schema:
$ref: '#/definitions/Message'
/states/{entity_id}:
get:
summary: Specific state object.
description: |
Returns a state object for specified entity_id.
tags:
- State
security:
- api_key: []
parameters:
- name: entity_id
in: path
description: entity_id of the entity to query
required: true
type: string
responses:
200:
description: State
schema:
$ref: '#/definitions/State'
404:
description: Not found
schema:
$ref: '#/definitions/Message'
default:
description: Error
schema:
$ref: '#/definitions/Message'
post:
description: |
Updates or creates the current state of an entity.
tags:
- State
consumes:
- application/json
parameters:
- name: entity_id
in: path
description: entity_id to set the state of
required: true
type: string
- $ref: '#/parameters/State'
responses:
200:
description: State of existing entity was set
schema:
$ref: '#/definitions/State'
201:
description: State of new entity was set
schema:
$ref: '#/definitions/State'
headers:
location:
type: string
description: location of the new entity
default:
description: Error
schema:
$ref: '#/definitions/Message'
/error_log:
get:
summary: Error log
description: |
Retrieve all errors logged during the current session of Home Assistant as a plaintext response.
tags:
- Core
security:
- api_key: []
produces:
- text/plain
responses:
200:
description: Plain text error log
default:
description: Error
schema:
$ref: '#/definitions/Message'
/camera_proxy/camera.{entity_id}:
get:
summary: Camera image.
description: |
Returns the data (image) from the specified camera entity_id.
tags:
- Camera
security:
- api_key: []
produces:
- image/jpeg
parameters:
- name: entity_id
in: path
description: entity_id of the camera to query
required: true
type: string
responses:
200:
description: Camera image
schema:
type: file
default:
description: Error
schema:
$ref: '#/definitions/Message'
/events/{event_type}:
post:
description: |
Fires an event with event_type
tags:
- Events
security:
- api_key: []
consumes:
- application/json
parameters:
- name: event_type
in: path
description: event_type to fire event with
required: true
type: string
- $ref: '#/parameters/EventData'
responses:
200:
description: Response message
schema:
$ref: '#/definitions/Message'
default:
description: Error
schema:
$ref: '#/definitions/Message'
/services/{domain}/{service}:
post:
description: |
Calls a service within a specific domain. Will return when the service has been executed or 10 seconds has past, whichever comes first.
tags:
- Services
security:
- api_key: []
consumes:
- application/json
parameters:
- name: domain
in: path
description: domain of the service
required: true
type: string
- name: service
in: path
description: service to call
required: true
type: string
- $ref: '#/parameters/ServiceData'
responses:
200:
description: List of states that have changed while the service was being executed. The result will include any changed states that changed while the service was being executed, even if their change was the result of something else happening in the system.
schema:
type: array
items:
$ref: '#/definitions/State'
default:
description: Error
schema:
$ref: '#/definitions/Message'
/template:
post:
description: |
Render a Home Assistant template.
tags:
- Template
security:
- api_key: []
consumes:
- application/json
produces:
- text/plain
parameters:
- $ref: '#/parameters/Template'
responses:
200:
description: Returns the rendered template in plain text.
schema:
type: string
default:
description: Error
schema:
$ref: '#/definitions/Message'
/event_forwarding:
post:
description: |
Setup event forwarding to another Home Assistant instance.
tags:
- Core
security:
- api_key: []
consumes:
- application/json
parameters:
- $ref: '#/parameters/EventForwarding'
responses:
200:
description: It will return a message if event forwarding was setup successful.
schema:
$ref: '#/definitions/Message'
default:
description: Error
schema:
$ref: '#/definitions/Message'
delete:
description: |
Cancel event forwarding to another Home Assistant instance.
tags:
- Core
consumes:
- application/json
parameters:
- $ref: '#/parameters/EventForwarding'
responses:
200:
description: It will return a message if event forwarding was cancelled successful.
schema:
$ref: '#/definitions/Message'
default:
description: Error
schema:
$ref: '#/definitions/Message'
/stream:
get:
summary: Server-sent events
description: The server-sent events feature is a one-way channel from your Home Assistant server to a client which is acting as a consumer.
tags:
- Core
- Events
security:
- api_key: []
produces:
- text/event-stream
parameters:
- name: restrict
in: query
description: comma-separated list of event_types to filter
required: false
type: string
responses:
default:
description: Stream of events
schema:
type: object
x-events:
state_changed:
type: object
properties:
entity_id:
type: string
old_state:
$ref: '#/definitions/State'
new_state:
$ref: '#/definitions/State'
definitions:
ApiConfig:
type: object
properties:
components:
type: array
description: List of component types
items:
type: string
description: Component type
latitude:
type: number
format: float
description: Latitude of Home Assistant server
longitude:
type: number
format: float
description: Longitude of Home Assistant server
location_name:
type: string
unit_system:
type: object
properties:
length:
type: string
mass:
type: string
temperature:
type: string
volume:
type: string
time_zone:
type: string
version:
type: string
DiscoveryInfo:
type: object
properties:
base_url:
type: string
location_name:
type: string
requires_api_password:
type: boolean
version:
type: string
BootstrapInfo:
type: object
properties:
config:
$ref: '#/definitions/ApiConfig'
events:
type: array
items:
$ref: '#/definitions/Event'
services:
type: array
items:
$ref: '#/definitions/Service'
states:
type: array
items:
$ref: '#/definitions/State'
Event:
type: object
properties:
event:
type: string
listener_count:
type: integer
Service:
type: object
properties:
domain:
type: string
services:
type: object
additionalProperties:
$ref: '#/definitions/DomainService'
DomainService:
type: object
properties:
description:
type: string
fields:
type: object
description: Object with service fields that can be called
State:
type: object
properties:
attributes:
$ref: '#/definitions/StateAttributes'
state:
type: string
entity_id:
type: string
last_changed:
type: string
format: date-time
StateAttributes:
type: object
additionalProperties:
type: string
History:
allOf:
- $ref: '#/definitions/State'
- type: object
properties:
last_updated:
type: string
format: date-time
Message:
type: object
properties:
message:
type: string
parameters:
State:
name: body
in: body
description: State parameter
required: false
schema:
type: object
required:
- state
properties:
attributes:
$ref: '#/definitions/StateAttributes'
state:
type: string
EventData:
name: body
in: body
description: event_data
required: false
schema:
type: object
ServiceData:
name: body
in: body
description: service_data
required: false
schema:
type: object
Template:
name: body
in: body
description: Template to render
required: true
schema:
type: object
required:
- template
properties:
template:
description: Jinja2 template string
type: string
EventForwarding:
name: body
in: body
description: Event Forwarding parameter
required: true
schema:
type: object
required:
- host
- api_password
properties:
host:
type: string
api_password:
type: string
port:
type: integer

View File

@ -8,7 +8,8 @@ import subprocess
import sys import sys
import threading import threading
from typing import Optional, List from typing import Optional, List, Dict, Any # noqa #pylint: disable=unused-import
from homeassistant import monkey_patch from homeassistant import monkey_patch
from homeassistant.const import ( from homeassistant.const import (
@ -126,6 +127,10 @@ def get_arguments() -> argparse.Namespace:
default=None, default=None,
help='Log file to write to. If not set, CONFIG/home-assistant.log ' help='Log file to write to. If not set, CONFIG/home-assistant.log '
'is used') 'is used')
parser.add_argument(
'--log-no-color',
action='store_true',
help="Disable color logs")
parser.add_argument( parser.add_argument(
'--runner', '--runner',
action='store_true', action='store_true',
@ -255,17 +260,18 @@ def setup_and_run_hass(config_dir: str,
config = { config = {
'frontend': {}, 'frontend': {},
'demo': {} 'demo': {}
} } # type: Dict[str, Any]
hass = bootstrap.from_config_dict( hass = bootstrap.from_config_dict(
config, config_dir=config_dir, verbose=args.verbose, config, config_dir=config_dir, verbose=args.verbose,
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days, skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days,
log_file=args.log_file) log_file=args.log_file, log_no_color=args.log_no_color)
else: else:
config_file = ensure_config_file(config_dir) config_file = ensure_config_file(config_dir)
print('Config directory:', config_dir) print('Config directory:', config_dir)
hass = bootstrap.from_config_file( hass = bootstrap.from_config_file(
config_file, verbose=args.verbose, skip_pip=args.skip_pip, config_file, verbose=args.verbose, skip_pip=args.skip_pip,
log_rotate_days=args.log_rotate_days, log_file=args.log_file) log_rotate_days=args.log_rotate_days, log_file=args.log_file,
log_no_color=args.log_no_color)
if hass is None: if hass is None:
return None return None

503
homeassistant/auth.py Normal file
View File

@ -0,0 +1,503 @@
"""Provide an authentication layer for Home Assistant."""
import asyncio
import binascii
from collections import OrderedDict
from datetime import datetime, timedelta
import os
import importlib
import logging
import uuid
import attr
import voluptuous as vol
from voluptuous.humanize import humanize_error
from homeassistant import data_entry_flow, requirements
from homeassistant.core import callback
from homeassistant.const import CONF_TYPE, CONF_NAME, CONF_ID
from homeassistant.util.decorator import Registry
from homeassistant.util import dt as dt_util
_LOGGER = logging.getLogger(__name__)
AUTH_PROVIDERS = Registry()
AUTH_PROVIDER_SCHEMA = vol.Schema({
vol.Required(CONF_TYPE): str,
vol.Optional(CONF_NAME): str,
# Specify ID if you have two auth providers for same type.
vol.Optional(CONF_ID): str,
}, extra=vol.ALLOW_EXTRA)
ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30)
DATA_REQS = 'auth_reqs_processed'
def generate_secret(entropy: int = 32) -> str:
"""Generate a secret.
Backport of secrets.token_hex from Python 3.6
Event loop friendly.
"""
return binascii.hexlify(os.urandom(entropy)).decode('ascii')
class AuthProvider:
"""Provider of user authentication."""
DEFAULT_TITLE = 'Unnamed auth provider'
initialized = False
def __init__(self, hass, store, config):
"""Initialize an auth provider."""
self.hass = hass
self.store = store
self.config = config
@property
def id(self): # pylint: disable=invalid-name
"""Return id of the auth provider.
Optional, can be None.
"""
return self.config.get(CONF_ID)
@property
def type(self):
"""Return type of the provider."""
return self.config[CONF_TYPE]
@property
def name(self):
"""Return the name of the auth provider."""
return self.config.get(CONF_NAME, self.DEFAULT_TITLE)
async def async_credentials(self):
"""Return all credentials of this provider."""
return await self.store.credentials_for_provider(self.type, self.id)
@callback
def async_create_credentials(self, data):
"""Create credentials."""
return Credentials(
auth_provider_type=self.type,
auth_provider_id=self.id,
data=data,
)
# Implement by extending class
async def async_initialize(self):
"""Initialize the auth provider.
Optional.
"""
async def async_credential_flow(self):
"""Return the data flow for logging in with auth provider."""
raise NotImplementedError
async def async_get_or_create_credentials(self, flow_result):
"""Get credentials based on the flow result."""
raise NotImplementedError
async def async_user_meta_for_credentials(self, credentials):
"""Return extra user metadata for credentials.
Will be used to populate info when creating a new user.
"""
return {}
@attr.s(slots=True)
class User:
"""A user."""
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
is_owner = attr.ib(type=bool, default=False)
is_active = attr.ib(type=bool, default=False)
name = attr.ib(type=str, default=None)
# For persisting and see if saved?
# store = attr.ib(type=AuthStore, default=None)
# List of credentials of a user.
credentials = attr.ib(type=list, default=attr.Factory(list))
# Tokens associated with a user.
refresh_tokens = attr.ib(type=dict, default=attr.Factory(dict))
def as_dict(self):
"""Convert user object to a dictionary."""
return {
'id': self.id,
'is_owner': self.is_owner,
'is_active': self.is_active,
'name': self.name,
}
@attr.s(slots=True)
class RefreshToken:
"""RefreshToken for a user to grant new access tokens."""
user = attr.ib(type=User)
client_id = attr.ib(type=str)
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow))
access_token_expiration = attr.ib(type=timedelta,
default=ACCESS_TOKEN_EXPIRATION)
token = attr.ib(type=str,
default=attr.Factory(lambda: generate_secret(64)))
access_tokens = attr.ib(type=list, default=attr.Factory(list))
@attr.s(slots=True)
class AccessToken:
"""Access token to access the API.
These will only ever be stored in memory and not be persisted.
"""
refresh_token = attr.ib(type=RefreshToken)
created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow))
token = attr.ib(type=str,
default=attr.Factory(generate_secret))
@property
def expires(self):
"""Return datetime when this token expires."""
return self.created_at + self.refresh_token.access_token_expiration
@attr.s(slots=True)
class Credentials:
"""Credentials for a user on an auth provider."""
auth_provider_type = attr.ib(type=str)
auth_provider_id = attr.ib(type=str)
# Allow the auth provider to store data to represent their auth.
data = attr.ib(type=dict)
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
is_new = attr.ib(type=bool, default=True)
@attr.s(slots=True)
class Client:
"""Client that interacts with Home Assistant on behalf of a user."""
name = attr.ib(type=str)
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
secret = attr.ib(type=str, default=attr.Factory(generate_secret))
redirect_uris = attr.ib(type=list, default=attr.Factory(list))
async def load_auth_provider_module(hass, provider):
"""Load an auth provider."""
try:
module = importlib.import_module(
'homeassistant.auth_providers.{}'.format(provider))
except ImportError:
_LOGGER.warning('Unable to find auth provider %s', provider)
return None
if hass.config.skip_pip or not hasattr(module, 'REQUIREMENTS'):
return module
processed = hass.data.get(DATA_REQS)
if processed is None:
processed = hass.data[DATA_REQS] = set()
elif provider in processed:
return module
req_success = await requirements.async_process_requirements(
hass, 'auth provider {}'.format(provider), module.REQUIREMENTS)
if not req_success:
return None
return module
async def auth_manager_from_config(hass, provider_configs):
"""Initialize an auth manager from config."""
store = AuthStore(hass)
if provider_configs:
providers = await asyncio.gather(
*[_auth_provider_from_config(hass, store, config)
for config in provider_configs])
else:
providers = []
# So returned auth providers are in same order as config
provider_hash = OrderedDict()
for provider in providers:
if provider is None:
continue
key = (provider.type, provider.id)
if key in provider_hash:
_LOGGER.error(
'Found duplicate provider: %s. Please add unique IDs if you '
'want to have the same provider twice.', key)
continue
provider_hash[key] = provider
manager = AuthManager(hass, store, provider_hash)
return manager
async def _auth_provider_from_config(hass, store, config):
"""Initialize an auth provider from a config."""
provider_name = config[CONF_TYPE]
module = await load_auth_provider_module(hass, provider_name)
if module is None:
return None
try:
config = module.CONFIG_SCHEMA(config)
except vol.Invalid as err:
_LOGGER.error('Invalid configuration for auth provider %s: %s',
provider_name, humanize_error(config, err))
return None
return AUTH_PROVIDERS[provider_name](hass, store, config)
class AuthManager:
"""Manage the authentication for Home Assistant."""
def __init__(self, hass, store, providers):
"""Initialize the auth manager."""
self._store = store
self._providers = providers
self.login_flow = data_entry_flow.FlowManager(
hass, self._async_create_login_flow,
self._async_finish_login_flow)
self.access_tokens = {}
@property
def async_auth_providers(self):
"""Return a list of available auth providers."""
return self._providers.values()
async def async_get_user(self, user_id):
"""Retrieve a user."""
return await self._store.async_get_user(user_id)
async def async_get_or_create_user(self, credentials):
"""Get or create a user."""
return await self._store.async_get_or_create_user(
credentials, self._async_get_auth_provider(credentials))
async def async_link_user(self, user, credentials):
"""Link credentials to an existing user."""
await self._store.async_link_user(user, credentials)
async def async_remove_user(self, user):
"""Remove a user."""
await self._store.async_remove_user(user)
async def async_create_refresh_token(self, user, client_id):
"""Create a new refresh token for a user."""
return await self._store.async_create_refresh_token(user, client_id)
async def async_get_refresh_token(self, token):
"""Get refresh token by token."""
return await self._store.async_get_refresh_token(token)
@callback
def async_create_access_token(self, refresh_token):
"""Create a new access token."""
access_token = AccessToken(refresh_token)
self.access_tokens[access_token.token] = access_token
return access_token
@callback
def async_get_access_token(self, token):
"""Get an access token."""
return self.access_tokens.get(token)
async def async_create_client(self, name, *, redirect_uris=None,
no_secret=False):
"""Create a new client."""
return await self._store.async_create_client(
name, redirect_uris, no_secret)
async def async_get_client(self, client_id):
"""Get a client."""
return await self._store.async_get_client(client_id)
async def _async_create_login_flow(self, handler, *, source, data):
"""Create a login flow."""
auth_provider = self._providers[handler]
if not auth_provider.initialized:
auth_provider.initialized = True
await auth_provider.async_initialize()
return await auth_provider.async_credential_flow()
async def _async_finish_login_flow(self, result):
"""Result of a credential login flow."""
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
return None
auth_provider = self._providers[result['handler']]
return await auth_provider.async_get_or_create_credentials(
result['data'])
@callback
def _async_get_auth_provider(self, credentials):
"""Helper to get auth provider from a set of credentials."""
auth_provider_key = (credentials.auth_provider_type,
credentials.auth_provider_id)
return self._providers[auth_provider_key]
class AuthStore:
"""Stores authentication info.
Any mutation to an object should happen inside the auth store.
The auth store is lazy. It won't load the data from disk until a method is
called that needs it.
"""
def __init__(self, hass):
"""Initialize the auth store."""
self.hass = hass
self.users = None
self.clients = None
self._load_lock = asyncio.Lock(loop=hass.loop)
async def credentials_for_provider(self, provider_type, provider_id):
"""Return credentials for specific auth provider type and id."""
if self.users is None:
await self.async_load()
return [
credentials
for user in self.users.values()
for credentials in user.credentials
if (credentials.auth_provider_type == provider_type and
credentials.auth_provider_id == provider_id)
]
async def async_get_user(self, user_id):
"""Retrieve a user."""
if self.users is None:
await self.async_load()
return self.users.get(user_id)
async def async_get_or_create_user(self, credentials, auth_provider):
"""Get or create a new user for given credentials.
If link_user is passed in, the credentials will be linked to the passed
in user if the credentials are new.
"""
if self.users is None:
await self.async_load()
# New credentials, store in user
if credentials.is_new:
info = await auth_provider.async_user_meta_for_credentials(
credentials)
# Make owner and activate user if it's the first user.
if self.users:
is_owner = False
is_active = False
else:
is_owner = True
is_active = True
new_user = User(
is_owner=is_owner,
is_active=is_active,
name=info.get('name'),
)
self.users[new_user.id] = new_user
await self.async_link_user(new_user, credentials)
return new_user
for user in self.users.values():
for creds in user.credentials:
if (creds.auth_provider_type == credentials.auth_provider_type
and creds.auth_provider_id ==
credentials.auth_provider_id):
return user
raise ValueError('We got credentials with ID but found no user')
async def async_link_user(self, user, credentials):
"""Add credentials to an existing user."""
user.credentials.append(credentials)
await self.async_save()
credentials.is_new = False
async def async_remove_user(self, user):
"""Remove a user."""
self.users.pop(user.id)
await self.async_save()
async def async_create_refresh_token(self, user, client_id):
"""Create a new token for a user."""
refresh_token = RefreshToken(user, client_id)
user.refresh_tokens[refresh_token.token] = refresh_token
await self.async_save()
return refresh_token
async def async_get_refresh_token(self, token):
"""Get refresh token by token."""
if self.users is None:
await self.async_load()
for user in self.users.values():
refresh_token = user.refresh_tokens.get(token)
if refresh_token is not None:
return refresh_token
return None
async def async_create_client(self, name, redirect_uris, no_secret):
"""Create a new client."""
if self.clients is None:
await self.async_load()
kwargs = {
'name': name,
'redirect_uris': redirect_uris
}
if no_secret:
kwargs['secret'] = None
client = Client(**kwargs)
self.clients[client.id] = client
await self.async_save()
return client
async def async_get_client(self, client_id):
"""Get a client."""
if self.clients is None:
await self.async_load()
return self.clients.get(client_id)
async def async_load(self):
"""Load the users."""
async with self._load_lock:
self.users = {}
self.clients = {}
async def async_save(self):
"""Save users."""
pass

View File

@ -0,0 +1 @@
"""Auth providers for Home Assistant."""

View File

@ -0,0 +1,181 @@
"""Home Assistant auth provider."""
import base64
from collections import OrderedDict
import hashlib
import hmac
import voluptuous as vol
from homeassistant import auth, data_entry_flow
from homeassistant.exceptions import HomeAssistantError
from homeassistant.util import json
PATH_DATA = '.users.json'
CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({
}, extra=vol.PREVENT_EXTRA)
class InvalidAuth(HomeAssistantError):
"""Raised when we encounter invalid authentication."""
class InvalidUser(HomeAssistantError):
"""Raised when invalid user is specified.
Will not be raised when validating authentication.
"""
class Data:
"""Hold the user data."""
def __init__(self, path, data):
"""Initialize the user data store."""
self.path = path
if data is None:
data = {
'salt': auth.generate_secret(),
'users': []
}
self._data = data
@property
def users(self):
"""Return users."""
return self._data['users']
def validate_login(self, username, password):
"""Validate a username and password.
Raises InvalidAuth if auth invalid.
"""
password = self.hash_password(password)
found = None
# Compare all users to avoid timing attacks.
for user in self._data['users']:
if username == user['username']:
found = user
if found is None:
# Do one more compare to make timing the same as if user was found.
hmac.compare_digest(password, password)
raise InvalidAuth
if not hmac.compare_digest(password,
base64.b64decode(found['password'])):
raise InvalidAuth
def hash_password(self, password, for_storage=False):
"""Encode a password."""
hashed = hashlib.pbkdf2_hmac(
'sha512', password.encode(), self._data['salt'].encode(), 100000)
if for_storage:
hashed = base64.b64encode(hashed).decode()
return hashed
def add_user(self, username, password):
"""Add a user."""
if any(user['username'] == username for user in self.users):
raise InvalidUser
self.users.append({
'username': username,
'password': self.hash_password(password, True),
})
def change_password(self, username, new_password):
"""Update the password of a user.
Raises InvalidUser if user cannot be found.
"""
for user in self.users:
if user['username'] == username:
user['password'] = self.hash_password(new_password, True)
break
else:
raise InvalidUser
def save(self):
"""Save data."""
json.save_json(self.path, self._data)
def load_data(path):
"""Load auth data."""
return Data(path, json.load_json(path, None))
@auth.AUTH_PROVIDERS.register('homeassistant')
class HassAuthProvider(auth.AuthProvider):
"""Auth provider based on a local storage of users in HASS config dir."""
DEFAULT_TITLE = 'Home Assistant Local'
async def async_credential_flow(self):
"""Return a flow to login."""
return LoginFlow(self)
async def async_validate_login(self, username, password):
"""Helper to validate a username and password."""
def validate():
"""Validate creds."""
data = self._auth_data()
data.validate_login(username, password)
await self.hass.async_add_job(validate)
async def async_get_or_create_credentials(self, flow_result):
"""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
})
def _auth_data(self):
"""Return the auth provider data."""
return load_data(self.hass.config.path(PATH_DATA))
class LoginFlow(data_entry_flow.FlowHandler):
"""Handler for the login flow."""
def __init__(self, auth_provider):
"""Initialize the login flow."""
self._auth_provider = auth_provider
async def async_step_init(self, user_input=None):
"""Handle the step of the form."""
errors = {}
if user_input is not None:
try:
await self._auth_provider.async_validate_login(
user_input['username'], user_input['password'])
except InvalidAuth:
errors['base'] = 'invalid_auth'
if not errors:
return self.async_create_entry(
title=self._auth_provider.name,
data=user_input
)
schema = OrderedDict()
schema['username'] = str
schema['password'] = str
return self.async_show_form(
step_id='init',
data_schema=vol.Schema(schema),
errors=errors,
)

View File

@ -0,0 +1,118 @@
"""Example auth provider."""
from collections import OrderedDict
import hmac
import voluptuous as vol
from homeassistant.exceptions import HomeAssistantError
from homeassistant import auth, data_entry_flow
from homeassistant.core import callback
USER_SCHEMA = vol.Schema({
vol.Required('username'): str,
vol.Required('password'): str,
vol.Optional('name'): str,
})
CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({
vol.Required('users'): [USER_SCHEMA]
}, extra=vol.PREVENT_EXTRA)
class InvalidAuthError(HomeAssistantError):
"""Raised when submitting invalid authentication."""
@auth.AUTH_PROVIDERS.register('insecure_example')
class ExampleAuthProvider(auth.AuthProvider):
"""Example auth provider based on hardcoded usernames and passwords."""
async def async_credential_flow(self):
"""Return a flow to login."""
return LoginFlow(self)
@callback
def async_validate_login(self, username, password):
"""Helper to validate a username and password."""
user = None
# Compare all users to avoid timing attacks.
for usr in self.config['users']:
if hmac.compare_digest(username.encode('utf-8'),
usr['username'].encode('utf-8')):
user = usr
if user is None:
# Do one more compare to make timing the same as if user was found.
hmac.compare_digest(password.encode('utf-8'),
password.encode('utf-8'))
raise InvalidAuthError
if not hmac.compare_digest(user['password'].encode('utf-8'),
password.encode('utf-8')):
raise InvalidAuthError
async def async_get_or_create_credentials(self, flow_result):
"""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):
"""Return extra user metadata for credentials.
Will be used to populate info when creating a new user.
"""
username = credentials.data['username']
for user in self.config['users']:
if user['username'] == username:
return {
'name': user.get('name')
}
return {}
class LoginFlow(data_entry_flow.FlowHandler):
"""Handler for the login flow."""
def __init__(self, auth_provider):
"""Initialize the login flow."""
self._auth_provider = auth_provider
async def async_step_init(self, user_input=None):
"""Handle the step of the form."""
errors = {}
if user_input is not None:
try:
self._auth_provider.async_validate_login(
user_input['username'], user_input['password'])
except InvalidAuthError:
errors['base'] = 'invalid_auth'
if not errors:
return self.async_create_entry(
title=self._auth_provider.name,
data=user_input
)
schema = OrderedDict()
schema['username'] = str
schema['password'] = str
return self.async_show_form(
step_id='init',
data_schema=vol.Schema(schema),
errors=errors,
)

View File

@ -12,8 +12,7 @@ from typing import Any, Optional, Dict
import voluptuous as vol import voluptuous as vol
from homeassistant import ( from homeassistant import (
core, config as conf_util, config_entries, loader, core, config as conf_util, config_entries, components as core_components)
components as core_components)
from homeassistant.components import persistent_notification from homeassistant.components import persistent_notification
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
@ -42,7 +41,8 @@ def from_config_dict(config: Dict[str, Any],
verbose: bool = False, verbose: bool = False,
skip_pip: bool = False, skip_pip: bool = False,
log_rotate_days: Any = None, log_rotate_days: Any = None,
log_file: Any = None) \ log_file: Any = None,
log_no_color: bool = False) \
-> Optional[core.HomeAssistant]: -> Optional[core.HomeAssistant]:
"""Try to configure Home Assistant from a configuration dictionary. """Try to configure Home Assistant from a configuration dictionary.
@ -60,21 +60,21 @@ def from_config_dict(config: Dict[str, Any],
hass = hass.loop.run_until_complete( hass = hass.loop.run_until_complete(
async_from_config_dict( async_from_config_dict(
config, hass, config_dir, enable_log, verbose, skip_pip, config, hass, config_dir, enable_log, verbose, skip_pip,
log_rotate_days, log_file) log_rotate_days, log_file, log_no_color)
) )
return hass return hass
@asyncio.coroutine async def async_from_config_dict(config: Dict[str, Any],
def async_from_config_dict(config: Dict[str, Any], hass: core.HomeAssistant,
hass: core.HomeAssistant, config_dir: Optional[str] = None,
config_dir: Optional[str] = None, enable_log: bool = True,
enable_log: bool = True, verbose: bool = False,
verbose: bool = False, skip_pip: bool = False,
skip_pip: bool = False, log_rotate_days: Any = None,
log_rotate_days: Any = None, log_file: Any = None,
log_file: Any = None) \ log_no_color: bool = False) \
-> Optional[core.HomeAssistant]: -> Optional[core.HomeAssistant]:
"""Try to configure Home Assistant from a configuration dictionary. """Try to configure Home Assistant from a configuration dictionary.
@ -84,40 +84,30 @@ def async_from_config_dict(config: Dict[str, Any],
start = time() start = time()
if enable_log: if enable_log:
async_enable_logging(hass, verbose, log_rotate_days, log_file) async_enable_logging(hass, verbose, log_rotate_days, log_file,
log_no_color)
if sys.version_info[:2] < (3, 5):
_LOGGER.warning(
'Python 3.4 support has been deprecated and will be removed in '
'the beginning of 2018. Please upgrade Python or your operating '
'system. More info: https://home-assistant.io/blog/2017/10/06/'
'deprecating-python-3.4-support/'
)
core_config = config.get(core.DOMAIN, {}) core_config = config.get(core.DOMAIN, {})
try: try:
yield from conf_util.async_process_ha_core_config(hass, core_config) await conf_util.async_process_ha_core_config(hass, core_config)
except vol.Invalid as ex: except vol.Invalid as ex:
conf_util.async_log_exception(ex, 'homeassistant', core_config, hass) conf_util.async_log_exception(ex, 'homeassistant', core_config, hass)
return None return None
yield from hass.async_add_job(conf_util.process_ha_config_upgrade, hass) await hass.async_add_job(conf_util.process_ha_config_upgrade, hass)
hass.config.skip_pip = skip_pip hass.config.skip_pip = skip_pip
if skip_pip: if skip_pip:
_LOGGER.warning("Skipping pip installation of required modules. " _LOGGER.warning("Skipping pip installation of required modules. "
"This may cause issues") "This may cause issues")
if not loader.PREPARED:
yield from hass.async_add_job(loader.prepare, hass)
# Make a copy because we are mutating it. # Make a copy because we are mutating it.
config = OrderedDict(config) config = OrderedDict(config)
# Merge packages # Merge packages
conf_util.merge_packages_config( conf_util.merge_packages_config(
config, core_config.get(conf_util.CONF_PACKAGES, {})) hass, config, core_config.get(conf_util.CONF_PACKAGES, {}))
# Ensure we have no None values after merge # Ensure we have no None values after merge
for key, value in config.items(): for key, value in config.items():
@ -125,7 +115,7 @@ def async_from_config_dict(config: Dict[str, Any],
config[key] = {} config[key] = {}
hass.config_entries = config_entries.ConfigEntries(hass, config) hass.config_entries = config_entries.ConfigEntries(hass, config)
yield from hass.config_entries.async_load() await hass.config_entries.async_load()
# Filter out the repeating and common config section [homeassistant] # Filter out the repeating and common config section [homeassistant]
components = set(key.split(' ')[0] for key in config.keys() components = set(key.split(' ')[0] for key in config.keys()
@ -134,13 +124,13 @@ def async_from_config_dict(config: Dict[str, Any],
# setup components # setup components
# pylint: disable=not-an-iterable # pylint: disable=not-an-iterable
res = yield from core_components.async_setup(hass, config) res = await core_components.async_setup(hass, config)
if not res: if not res:
_LOGGER.error("Home Assistant core failed to initialize. " _LOGGER.error("Home Assistant core failed to initialize. "
"further initialization aborted") "further initialization aborted")
return hass return hass
yield from persistent_notification.async_setup(hass, config) await persistent_notification.async_setup(hass, config)
_LOGGER.info("Home Assistant core initialized") _LOGGER.info("Home Assistant core initialized")
@ -150,7 +140,7 @@ def async_from_config_dict(config: Dict[str, Any],
continue continue
hass.async_add_job(async_setup_component(hass, component, config)) hass.async_add_job(async_setup_component(hass, component, config))
yield from hass.async_block_till_done() await hass.async_block_till_done()
# stage 2 # stage 2
for component in components: for component in components:
@ -158,7 +148,7 @@ def async_from_config_dict(config: Dict[str, Any],
continue continue
hass.async_add_job(async_setup_component(hass, component, config)) hass.async_add_job(async_setup_component(hass, component, config))
yield from hass.async_block_till_done() await hass.async_block_till_done()
stop = time() stop = time()
_LOGGER.info("Home Assistant initialized in %.2fs", stop-start) _LOGGER.info("Home Assistant initialized in %.2fs", stop-start)
@ -172,7 +162,8 @@ def from_config_file(config_path: str,
verbose: bool = False, verbose: bool = False,
skip_pip: bool = True, skip_pip: bool = True,
log_rotate_days: Any = None, log_rotate_days: Any = None,
log_file: Any = None): log_file: Any = None,
log_no_color: bool = False):
"""Read the configuration file and try to start all the functionality. """Read the configuration file and try to start all the functionality.
Will add functionality to 'hass' parameter if given, Will add functionality to 'hass' parameter if given,
@ -184,19 +175,20 @@ def from_config_file(config_path: str,
# run task # run task
hass = hass.loop.run_until_complete( hass = hass.loop.run_until_complete(
async_from_config_file( async_from_config_file(
config_path, hass, verbose, skip_pip, log_rotate_days, log_file) config_path, hass, verbose, skip_pip,
log_rotate_days, log_file, log_no_color)
) )
return hass return hass
@asyncio.coroutine async def async_from_config_file(config_path: str,
def async_from_config_file(config_path: str, hass: core.HomeAssistant,
hass: core.HomeAssistant, verbose: bool = False,
verbose: bool = False, skip_pip: bool = True,
skip_pip: bool = True, log_rotate_days: Any = None,
log_rotate_days: Any = None, log_file: Any = None,
log_file: Any = None): log_no_color: bool = False):
"""Read the configuration file and try to start all the functionality. """Read the configuration file and try to start all the functionality.
Will add functionality to 'hass' parameter. Will add functionality to 'hass' parameter.
@ -205,12 +197,13 @@ def async_from_config_file(config_path: str,
# Set config dir to directory holding config file # Set config dir to directory holding config file
config_dir = os.path.abspath(os.path.dirname(config_path)) config_dir = os.path.abspath(os.path.dirname(config_path))
hass.config.config_dir = config_dir hass.config.config_dir = config_dir
yield from async_mount_local_lib_path(config_dir, hass.loop) await async_mount_local_lib_path(config_dir, hass.loop)
async_enable_logging(hass, verbose, log_rotate_days, log_file) async_enable_logging(hass, verbose, log_rotate_days, log_file,
log_no_color)
try: try:
config_dict = yield from hass.async_add_job( config_dict = await hass.async_add_job(
conf_util.load_yaml_config_file, config_path) conf_util.load_yaml_config_file, config_path)
except HomeAssistantError as err: except HomeAssistantError as err:
_LOGGER.error("Error loading %s: %s", config_path, err) _LOGGER.error("Error loading %s: %s", config_path, err)
@ -218,46 +211,57 @@ def async_from_config_file(config_path: str,
finally: finally:
clear_secret_cache() clear_secret_cache()
hass = yield from async_from_config_dict( hass = await async_from_config_dict(
config_dict, hass, enable_log=False, skip_pip=skip_pip) config_dict, hass, enable_log=False, skip_pip=skip_pip)
return hass return hass
@core.callback @core.callback
def async_enable_logging(hass: core.HomeAssistant, verbose: bool = False, def async_enable_logging(hass: core.HomeAssistant,
log_rotate_days=None, log_file=None) -> None: verbose: bool = False,
log_rotate_days=None,
log_file=None,
log_no_color: bool = False) -> None:
"""Set up the logging. """Set up the logging.
This method must be run in the event loop. This method must be run in the event loop.
""" """
logging.basicConfig(level=logging.INFO)
fmt = ("%(asctime)s %(levelname)s (%(threadName)s) " fmt = ("%(asctime)s %(levelname)s (%(threadName)s) "
"[%(name)s] %(message)s") "[%(name)s] %(message)s")
colorfmt = "%(log_color)s{}%(reset)s".format(fmt)
datefmt = '%Y-%m-%d %H:%M:%S' datefmt = '%Y-%m-%d %H:%M:%S'
if not log_no_color:
try:
from colorlog import ColoredFormatter
# basicConfig must be called after importing colorlog in order to
# ensure that the handlers it sets up wraps the correct streams.
logging.basicConfig(level=logging.INFO)
colorfmt = "%(log_color)s{}%(reset)s".format(fmt)
logging.getLogger().handlers[0].setFormatter(ColoredFormatter(
colorfmt,
datefmt=datefmt,
reset=True,
log_colors={
'DEBUG': 'cyan',
'INFO': 'green',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'red',
}
))
except ImportError:
pass
# If the above initialization failed for any reason, setup the default
# formatting. If the above succeeds, this wil result in a no-op.
logging.basicConfig(format=fmt, datefmt=datefmt, level=logging.INFO)
# Suppress overly verbose logs from libraries that aren't helpful # Suppress overly verbose logs from libraries that aren't helpful
logging.getLogger('requests').setLevel(logging.WARNING) logging.getLogger('requests').setLevel(logging.WARNING)
logging.getLogger('urllib3').setLevel(logging.WARNING) logging.getLogger('urllib3').setLevel(logging.WARNING)
logging.getLogger('aiohttp.access').setLevel(logging.WARNING) logging.getLogger('aiohttp.access').setLevel(logging.WARNING)
try:
from colorlog import ColoredFormatter
logging.getLogger().handlers[0].setFormatter(ColoredFormatter(
colorfmt,
datefmt=datefmt,
reset=True,
log_colors={
'DEBUG': 'cyan',
'INFO': 'green',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'red',
}
))
except ImportError:
pass
# Log errors to a file if we have write access to file or config dir # Log errors to a file if we have write access to file or config dir
if log_file is None: if log_file is None:
err_log_path = hass.config.path(ERROR_LOG_FILENAME) err_log_path = hass.config.path(ERROR_LOG_FILENAME)
@ -274,7 +278,8 @@ def async_enable_logging(hass: core.HomeAssistant, verbose: bool = False,
if log_rotate_days: if log_rotate_days:
err_handler = logging.handlers.TimedRotatingFileHandler( err_handler = logging.handlers.TimedRotatingFileHandler(
err_log_path, when='midnight', backupCount=log_rotate_days) err_log_path, when='midnight',
backupCount=log_rotate_days) # type: logging.FileHandler
else: else:
err_handler = logging.FileHandler( err_handler = logging.FileHandler(
err_log_path, mode='w', delay=True) err_log_path, mode='w', delay=True)
@ -284,17 +289,16 @@ def async_enable_logging(hass: core.HomeAssistant, verbose: bool = False,
async_handler = AsyncHandler(hass.loop, err_handler) async_handler = AsyncHandler(hass.loop, err_handler)
@asyncio.coroutine async def async_stop_async_handler(event):
def async_stop_async_handler(event):
"""Cleanup async handler.""" """Cleanup async handler."""
logging.getLogger('').removeHandler(async_handler) logging.getLogger('').removeHandler(async_handler)
yield from async_handler.async_close(blocking=True) await async_handler.async_close(blocking=True)
hass.bus.async_listen_once( hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_CLOSE, async_stop_async_handler) EVENT_HOMEASSISTANT_CLOSE, async_stop_async_handler)
logger = logging.getLogger('') logger = logging.getLogger('')
logger.addHandler(async_handler) logger.addHandler(async_handler) # type: ignore
logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)
# Save the log file location for access by other components. # Save the log file location for access by other components.
@ -313,15 +317,14 @@ def mount_local_lib_path(config_dir: str) -> str:
return deps_dir return deps_dir
@asyncio.coroutine async def async_mount_local_lib_path(config_dir: str,
def async_mount_local_lib_path(config_dir: str, loop: asyncio.AbstractEventLoop) -> str:
loop: asyncio.AbstractEventLoop) -> str:
"""Add local library to Python Path. """Add local library to Python Path.
This function is a coroutine. This function is a coroutine.
""" """
deps_dir = os.path.join(config_dir, 'deps') deps_dir = os.path.join(config_dir, 'deps')
lib_dir = yield from async_get_user_site(deps_dir, loop=loop) lib_dir = await async_get_user_site(deps_dir, loop=loop)
if lib_dir not in sys.path: if lib_dir not in sys.path:
sys.path.insert(0, lib_dir) sys.path.insert(0, lib_dir)
return deps_dir return deps_dir

View File

@ -19,7 +19,7 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['abodepy==0.12.2'] REQUIREMENTS = ['abodepy==0.13.1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -27,6 +27,7 @@ CONF_ATTRIBUTION = "Data provided by goabode.com"
CONF_POLLING = 'polling' CONF_POLLING = 'polling'
DOMAIN = 'abode' DOMAIN = 'abode'
DEFAULT_CACHEDB = './abodepy_cache.pickle'
NOTIFICATION_ID = 'abode_notification' NOTIFICATION_ID = 'abode_notification'
NOTIFICATION_TITLE = 'Abode Security Setup' NOTIFICATION_TITLE = 'Abode Security Setup'
@ -80,19 +81,20 @@ TRIGGER_SCHEMA = vol.Schema({
ABODE_PLATFORMS = [ ABODE_PLATFORMS = [
'alarm_control_panel', 'binary_sensor', 'lock', 'switch', 'cover', 'alarm_control_panel', 'binary_sensor', 'lock', 'switch', 'cover',
'camera', 'light' 'camera', 'light', 'sensor'
] ]
class AbodeSystem(object): class AbodeSystem(object):
"""Abode System class.""" """Abode System class."""
def __init__(self, username, password, name, polling, exclude, lights): def __init__(self, username, password, cache,
name, polling, exclude, lights):
"""Initialize the system.""" """Initialize the system."""
import abodepy import abodepy
self.abode = abodepy.Abode( self.abode = abodepy.Abode(
username, password, auto_login=True, get_devices=True, username, password, auto_login=True, get_devices=True,
get_automations=True) get_automations=True, cache_path=cache)
self.name = name self.name = name
self.polling = polling self.polling = polling
self.exclude = exclude self.exclude = exclude
@ -129,8 +131,9 @@ def setup(hass, config):
lights = conf.get(CONF_LIGHTS) lights = conf.get(CONF_LIGHTS)
try: try:
cache = hass.config.path(DEFAULT_CACHEDB)
hass.data[DOMAIN] = AbodeSystem( hass.data[DOMAIN] = AbodeSystem(
username, password, name, polling, exclude, lights) username, password, cache, name, polling, exclude, lights)
except (AbodeException, ConnectTimeout, HTTPError) as ex: except (AbodeException, ConnectTimeout, HTTPError) as ex:
_LOGGER.error("Unable to connect to Abode: %s", str(ex)) _LOGGER.error("Unable to connect to Abode: %s", str(ex))

View File

@ -100,8 +100,8 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
@property @property
def code_format(self): def code_format(self):
"""Return the regex for code format or None if no code is required.""" """Return one or more digits/characters."""
return '^\\d{4,6}$' return 'Number'
@property @property
def state(self): def state(self):

View File

@ -6,6 +6,7 @@ https://home-assistant.io/components/alarm_control_panel.alarmdotcom/
""" """
import asyncio import asyncio
import logging import logging
import re
import voluptuous as vol import voluptuous as vol
@ -17,7 +18,7 @@ from homeassistant.const import (
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyalarmdotcom==0.3.1'] REQUIREMENTS = ['pyalarmdotcom==0.3.2']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -79,8 +80,12 @@ class AlarmDotCom(alarm.AlarmControlPanel):
@property @property
def code_format(self): def code_format(self):
"""Return one or more characters if code is defined.""" """Return one or more digits/characters."""
return None if self._code is None else '.+' if self._code is None:
return None
elif isinstance(self._code, str) and re.search('^\\d+$', self._code):
return 'Number'
return 'Any'
@property @property
def state(self): def state(self):
@ -93,6 +98,13 @@ class AlarmDotCom(alarm.AlarmControlPanel):
return STATE_ALARM_ARMED_AWAY return STATE_ALARM_ARMED_AWAY
return STATE_UNKNOWN return STATE_UNKNOWN
@property
def device_state_attributes(self):
"""Return the state attributes."""
return {
'sensor_status': self._alarm.sensor_status
}
@asyncio.coroutine @asyncio.coroutine
def async_alarm_disarm(self, code=None): def async_alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""

View File

@ -80,7 +80,7 @@ class Concord232Alarm(alarm.AlarmControlPanel):
@property @property
def code_format(self): def code_format(self):
"""Return the characters if code is defined.""" """Return the characters if code is defined."""
return '[0-9]{4}([0-9]{2})?' return 'Number'
@property @property
def state(self): def state(self):

View File

@ -12,13 +12,14 @@ import requests
import homeassistant.components.alarm_control_panel as alarm import homeassistant.components.alarm_control_panel as alarm
from homeassistant.const import ( from homeassistant.const import (
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED) STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED,
STATE_ALARM_ARMED_NIGHT)
from homeassistant.components.egardia import ( from homeassistant.components.egardia import (
EGARDIA_DEVICE, EGARDIA_SERVER, EGARDIA_DEVICE, EGARDIA_SERVER,
REPORT_SERVER_CODES_IGNORE, CONF_REPORT_SERVER_CODES, REPORT_SERVER_CODES_IGNORE, CONF_REPORT_SERVER_CODES,
CONF_REPORT_SERVER_ENABLED, CONF_REPORT_SERVER_PORT CONF_REPORT_SERVER_ENABLED, CONF_REPORT_SERVER_PORT
) )
REQUIREMENTS = ['pythonegardia==1.0.38'] DEPENDENCIES = ['egardia']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -27,6 +28,8 @@ STATES = {
'DAY HOME': STATE_ALARM_ARMED_HOME, 'DAY HOME': STATE_ALARM_ARMED_HOME,
'DISARM': STATE_ALARM_DISARMED, 'DISARM': STATE_ALARM_DISARMED,
'ARMHOME': STATE_ALARM_ARMED_HOME, 'ARMHOME': STATE_ALARM_ARMED_HOME,
'HOME': STATE_ALARM_ARMED_HOME,
'NIGHT HOME': STATE_ALARM_ARMED_NIGHT,
'TRIGGERED': STATE_ALARM_TRIGGERED 'TRIGGERED': STATE_ALARM_TRIGGERED
} }

View File

@ -106,7 +106,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
"""Regex for code format or None if no code is required.""" """Regex for code format or None if no code is required."""
if self._code: if self._code:
return None return None
return '^\\d{4,6}$' return 'Number'
@property @property
def state(self): def state(self):

View File

@ -0,0 +1,175 @@
"""
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_devices, 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_devices([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
elif isinstance(self._code, str) and re.search('^\\d+$', self._code):
return 'Number'
return 'Any'
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

View File

@ -7,6 +7,7 @@ https://home-assistant.io/components/alarm_control_panel.manual/
import copy import copy
import datetime import datetime
import logging import logging
import re
import voluptuous as vol import voluptuous as vol
@ -201,8 +202,12 @@ class ManualAlarm(alarm.AlarmControlPanel):
@property @property
def code_format(self): def code_format(self):
"""Return one or more characters.""" """Return one or more digits/characters."""
return None if self._code is None else '.+' if self._code is None:
return None
elif isinstance(self._code, str) and re.search('^\\d+$', self._code):
return 'Number'
return 'Any'
def alarm_disarm(self, code=None): def alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""

View File

@ -8,6 +8,7 @@ import asyncio
import copy import copy
import datetime import datetime
import logging import logging
import re
import voluptuous as vol import voluptuous as vol
@ -237,8 +238,12 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
@property @property
def code_format(self): def code_format(self):
"""Return one or more characters.""" """Return one or more digits/characters."""
return None if self._code is None else '.+' if self._code is None:
return None
elif isinstance(self._code, str) and re.search('^\\d+$', self._code):
return 'Number'
return 'Any'
def alarm_disarm(self, code=None): def alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""

View File

@ -6,6 +6,7 @@ https://home-assistant.io/components/alarm_control_panel.mqtt/
""" """
import asyncio import asyncio
import logging import logging
import re
import voluptuous as vol import voluptuous as vol
@ -117,8 +118,12 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
@property @property
def code_format(self): def code_format(self):
"""One or more characters if code is defined.""" """Return one or more digits/characters."""
return None if self._code is None else '.+' if self._code is None:
return None
elif isinstance(self._code, str) and re.search('^\\d+$', self._code):
return 'Number'
return 'Any'
@asyncio.coroutine @asyncio.coroutine
def async_alarm_disarm(self, code=None): def async_alarm_disarm(self, code=None):

View File

@ -69,8 +69,8 @@ class NX584Alarm(alarm.AlarmControlPanel):
@property @property
def code_format(self): def code_format(self):
"""Return che characters if code is defined.""" """Return one or more digits/characters."""
return '[0-9]{4}([0-9]{2})?' return 'Number'
@property @property
def state(self): def state(self):

View File

@ -66,7 +66,7 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel):
@property @property
def code_format(self): def code_format(self):
"""Return the regex for code format or None if no code is required.""" """Return the regex for code format or None if no code is required."""
return '^\\d{4,6}$' return 'Number'
@property @property
def state(self): def state(self):

View File

@ -69,3 +69,13 @@ alarmdecoder_alarm_toggle_chime:
code: code:
description: A required code to toggle the alarm control panel chime with. description: A required code to toggle the alarm control panel chime with.
example: 1234 example: 1234
ifttt_push_alarm_state:
description: Update the alarm state to the specified value.
fields:
entity_id:
description: Name of the alarm control panel which state has to be updated.
example: 'alarm_control_panel.downstairs'
state:
description: The state to which the alarm control panel has to be set.
example: 'armed_night'

View File

@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.simplisafe/ https://home-assistant.io/components/alarm_control_panel.simplisafe/
""" """
import logging import logging
import re
import voluptuous as vol import voluptuous as vol
@ -83,8 +84,12 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
@property @property
def code_format(self): def code_format(self):
"""Return one or more characters if code is defined.""" """Return one or more digits/characters."""
return None if self._code is None else '.+' if self._code is None:
return None
elif isinstance(self._code, str) and re.search('^\\d+$', self._code):
return 'Number'
return 'Any'
@property @property
def state(self): def state(self):

View File

@ -18,7 +18,7 @@ from homeassistant.const import (
STATE_ALARM_ARMED_CUSTOM_BYPASS) STATE_ALARM_ARMED_CUSTOM_BYPASS)
REQUIREMENTS = ['total_connect_client==0.16'] REQUIREMENTS = ['total_connect_client==0.18']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -60,8 +60,8 @@ class VerisureAlarm(alarm.AlarmControlPanel):
@property @property
def code_format(self): def code_format(self):
"""Return the code format as regex.""" """Return one or more digits/characters."""
return '^\\d{%s}$' % self._digits return 'Number'
@property @property
def changed_by(self): def changed_by(self):

View File

@ -6,18 +6,20 @@ from datetime import datetime
from uuid import uuid4 from uuid import uuid4
from homeassistant.components import ( from homeassistant.components import (
alert, automation, cover, fan, group, input_boolean, light, lock, alert, automation, cover, climate, fan, group, input_boolean, light, lock,
media_player, scene, script, switch, http, sensor) media_player, scene, script, switch, http, sensor)
import homeassistant.core as ha import homeassistant.core as ha
import homeassistant.util.color as color_util import homeassistant.util.color as color_util
from homeassistant.util.temperature import convert as convert_temperature
from homeassistant.util.decorator import Registry from homeassistant.util.decorator import Registry
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_NAME, SERVICE_LOCK, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, CONF_NAME,
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_LOCK, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON,
SERVICE_UNLOCK, SERVICE_VOLUME_SET, TEMP_FAHRENHEIT, TEMP_CELSIUS, SERVICE_UNLOCK, SERVICE_VOLUME_SET, TEMP_FAHRENHEIT, TEMP_CELSIUS,
CONF_UNIT_OF_MEASUREMENT, STATE_LOCKED, STATE_UNLOCKED, STATE_ON) CONF_UNIT_OF_MEASUREMENT, STATE_LOCKED, STATE_UNLOCKED, STATE_ON)
from .const import CONF_FILTER, CONF_ENTITY_CONFIG from .const import CONF_FILTER, CONF_ENTITY_CONFIG
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -34,6 +36,16 @@ API_TEMP_UNITS = {
TEMP_CELSIUS: 'CELSIUS', TEMP_CELSIUS: 'CELSIUS',
} }
API_THERMOSTAT_MODES = {
climate.STATE_HEAT: 'HEAT',
climate.STATE_COOL: 'COOL',
climate.STATE_AUTO: 'AUTO',
climate.STATE_ECO: 'ECO',
climate.STATE_IDLE: 'OFF',
climate.STATE_FAN_ONLY: 'OFF',
climate.STATE_DRY: 'OFF',
}
SMART_HOME_HTTP_ENDPOINT = '/api/alexa/smart_home' SMART_HOME_HTTP_ENDPOINT = '/api/alexa/smart_home'
CONF_DESCRIPTION = 'description' CONF_DESCRIPTION = 'description'
@ -383,8 +395,60 @@ class _AlexaTemperatureSensor(_AlexaInterface):
raise _UnsupportedProperty(name) raise _UnsupportedProperty(name)
unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT] unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT]
temp = self.entity.state
if self.entity.domain == climate.DOMAIN:
temp = self.entity.attributes.get(
climate.ATTR_CURRENT_TEMPERATURE)
return { return {
'value': float(self.entity.state), 'value': float(temp),
'scale': API_TEMP_UNITS[unit],
}
class _AlexaThermostatController(_AlexaInterface):
def name(self):
return 'Alexa.ThermostatController'
def properties_supported(self):
properties = []
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & climate.SUPPORT_TARGET_TEMPERATURE:
properties.append({'name': 'targetSetpoint'})
if supported & climate.SUPPORT_TARGET_TEMPERATURE_LOW:
properties.append({'name': 'lowerSetpoint'})
if supported & climate.SUPPORT_TARGET_TEMPERATURE_HIGH:
properties.append({'name': 'upperSetpoint'})
if supported & climate.SUPPORT_OPERATION_MODE:
properties.append({'name': 'thermostatMode'})
return properties
def properties_retrievable(self):
return True
def get_property(self, name):
if name == 'thermostatMode':
ha_mode = self.entity.attributes.get(climate.ATTR_OPERATION_MODE)
mode = API_THERMOSTAT_MODES.get(ha_mode)
if mode is None:
_LOGGER.error("%s (%s) has unsupported %s value '%s'",
self.entity.entity_id, type(self.entity),
climate.ATTR_OPERATION_MODE, ha_mode)
raise _UnsupportedProperty(name)
return mode
unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT]
temp = None
if name == 'targetSetpoint':
temp = self.entity.attributes.get(ATTR_TEMPERATURE)
elif name == 'lowerSetpoint':
temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW)
elif name == 'upperSetpoint':
temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_HIGH)
if temp is None:
raise _UnsupportedProperty(name)
return {
'value': float(temp),
'scale': API_TEMP_UNITS[unit], 'scale': API_TEMP_UNITS[unit],
} }
@ -415,6 +479,16 @@ class _SwitchCapabilities(_AlexaEntity):
return [_AlexaPowerController(self.entity)] return [_AlexaPowerController(self.entity)]
@ENTITY_ADAPTERS.register(climate.DOMAIN)
class _ClimateCapabilities(_AlexaEntity):
def default_display_categories(self):
return [_DisplayCategory.THERMOSTAT]
def interfaces(self):
yield _AlexaThermostatController(self.entity)
yield _AlexaTemperatureSensor(self.entity)
@ENTITY_ADAPTERS.register(cover.DOMAIN) @ENTITY_ADAPTERS.register(cover.DOMAIN)
class _CoverCapabilities(_AlexaEntity): class _CoverCapabilities(_AlexaEntity):
def default_display_categories(self): def default_display_categories(self):
@ -438,9 +512,7 @@ class _LightCapabilities(_AlexaEntity):
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & light.SUPPORT_BRIGHTNESS: if supported & light.SUPPORT_BRIGHTNESS:
yield _AlexaBrightnessController(self.entity) yield _AlexaBrightnessController(self.entity)
if supported & light.SUPPORT_RGB_COLOR: if supported & light.SUPPORT_COLOR:
yield _AlexaColorController(self.entity)
if supported & light.SUPPORT_XY_COLOR:
yield _AlexaColorController(self.entity) yield _AlexaColorController(self.entity)
if supported & light.SUPPORT_COLOR_TEMP: if supported & light.SUPPORT_COLOR_TEMP:
yield _AlexaColorTemperatureController(self.entity) yield _AlexaColorTemperatureController(self.entity)
@ -684,17 +756,26 @@ def api_message(request,
return response return response
def api_error(request, error_type='INTERNAL_ERROR', error_message=""): def api_error(request,
namespace='Alexa',
error_type='INTERNAL_ERROR',
error_message="",
payload=None):
"""Create a API formatted error response. """Create a API formatted error response.
Async friendly. Async friendly.
""" """
payload = { payload = payload or {}
'type': error_type, payload['type'] = error_type
'message': error_message, payload['message'] = error_message
}
return api_message(request, name='ErrorResponse', payload=payload) _LOGGER.info("Request %s/%s error %s: %s",
request[API_HEADER]['namespace'],
request[API_HEADER]['name'],
error_type, error_message)
return api_message(
request, name='ErrorResponse', namespace=namespace, payload=payload)
@HANDLERS.register(('Alexa.Discovery', 'Discover')) @HANDLERS.register(('Alexa.Discovery', 'Discover'))
@ -842,25 +923,16 @@ def async_api_adjust_brightness(hass, config, request, entity):
@asyncio.coroutine @asyncio.coroutine
def async_api_set_color(hass, config, request, entity): def async_api_set_color(hass, config, request, entity):
"""Process a set color request.""" """Process a set color request."""
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES)
rgb = color_util.color_hsb_to_RGB( rgb = color_util.color_hsb_to_RGB(
float(request[API_PAYLOAD]['color']['hue']), float(request[API_PAYLOAD]['color']['hue']),
float(request[API_PAYLOAD]['color']['saturation']), float(request[API_PAYLOAD]['color']['saturation']),
float(request[API_PAYLOAD]['color']['brightness']) float(request[API_PAYLOAD]['color']['brightness'])
) )
if supported & light.SUPPORT_RGB_COLOR > 0: yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { ATTR_ENTITY_ID: entity.entity_id,
ATTR_ENTITY_ID: entity.entity_id, light.ATTR_RGB_COLOR: rgb,
light.ATTR_RGB_COLOR: rgb, }, blocking=False)
}, blocking=False)
else:
xyz = color_util.color_RGB_to_xy(*rgb)
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_XY_COLOR: (xyz[0], xyz[1]),
light.ATTR_BRIGHTNESS: xyz[2],
}, blocking=False)
return api_message(request) return api_message(request)
@ -1115,7 +1187,6 @@ def async_api_select_input(hass, config, request, entity):
else: else:
msg = 'failed to map input {} to a media source on {}'.format( msg = 'failed to map input {} to a media source on {}'.format(
media_input, entity.entity_id) media_input, entity.entity_id)
_LOGGER.error(msg)
return api_error( return api_error(
request, error_type='INVALID_VALUE', error_message=msg) request, error_type='INVALID_VALUE', error_message=msg)
@ -1287,6 +1358,150 @@ def async_api_previous(hass, config, request, entity):
return api_message(request) return api_message(request)
def api_error_temp_range(request, temp, min_temp, max_temp, unit):
"""Create temperature value out of range API error response.
Async friendly.
"""
temp_range = {
'minimumValue': {
'value': min_temp,
'scale': API_TEMP_UNITS[unit],
},
'maximumValue': {
'value': max_temp,
'scale': API_TEMP_UNITS[unit],
},
}
msg = 'The requested temperature {} is out of range'.format(temp)
return api_error(
request,
error_type='TEMPERATURE_VALUE_OUT_OF_RANGE',
error_message=msg,
payload={'validRange': temp_range},
)
def temperature_from_object(temp_obj, to_unit, interval=False):
"""Get temperature from Temperature object in requested unit."""
from_unit = TEMP_CELSIUS
temp = float(temp_obj['value'])
if temp_obj['scale'] == 'FAHRENHEIT':
from_unit = TEMP_FAHRENHEIT
elif temp_obj['scale'] == 'KELVIN':
# convert to Celsius if absolute temperature
if not interval:
temp -= 273.15
return convert_temperature(temp, from_unit, to_unit, interval)
@HANDLERS.register(('Alexa.ThermostatController', 'SetTargetTemperature'))
@extract_entity
async def async_api_set_target_temp(hass, config, request, entity):
"""Process a set target temperature request."""
unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT]
min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP)
max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP)
data = {
ATTR_ENTITY_ID: entity.entity_id
}
payload = request[API_PAYLOAD]
if 'targetSetpoint' in payload:
temp = temperature_from_object(
payload['targetSetpoint'], unit)
if temp < min_temp or temp > max_temp:
return api_error_temp_range(
request, temp, min_temp, max_temp, unit)
data[ATTR_TEMPERATURE] = temp
if 'lowerSetpoint' in payload:
temp_low = temperature_from_object(
payload['lowerSetpoint'], unit)
if temp_low < min_temp or temp_low > max_temp:
return api_error_temp_range(
request, temp_low, min_temp, max_temp, unit)
data[climate.ATTR_TARGET_TEMP_LOW] = temp_low
if 'upperSetpoint' in payload:
temp_high = temperature_from_object(
payload['upperSetpoint'], unit)
if temp_high < min_temp or temp_high > max_temp:
return api_error_temp_range(
request, temp_high, min_temp, max_temp, unit)
data[climate.ATTR_TARGET_TEMP_HIGH] = temp_high
await hass.services.async_call(
entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False)
return api_message(request)
@HANDLERS.register(('Alexa.ThermostatController', 'AdjustTargetTemperature'))
@extract_entity
async def async_api_adjust_target_temp(hass, config, request, entity):
"""Process an adjust target temperature request."""
unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT]
min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP)
max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP)
temp_delta = temperature_from_object(
request[API_PAYLOAD]['targetSetpointDelta'], unit, interval=True)
target_temp = float(entity.attributes.get(ATTR_TEMPERATURE)) + temp_delta
if target_temp < min_temp or target_temp > max_temp:
return api_error_temp_range(
request, target_temp, min_temp, max_temp, unit)
data = {
ATTR_ENTITY_ID: entity.entity_id,
ATTR_TEMPERATURE: target_temp,
}
await hass.services.async_call(
entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False)
return api_message(request)
@HANDLERS.register(('Alexa.ThermostatController', 'SetThermostatMode'))
@extract_entity
async def async_api_set_thermostat_mode(hass, config, request, entity):
"""Process a set thermostat mode request."""
mode = request[API_PAYLOAD]['thermostatMode']
mode = mode if isinstance(mode, str) else mode['value']
operation_list = entity.attributes.get(climate.ATTR_OPERATION_LIST)
# Work around a pylint false positive due to
# https://github.com/PyCQA/pylint/issues/1830
# pylint: disable=stop-iteration-return
ha_mode = next(
(k for k, v in API_THERMOSTAT_MODES.items() if v == mode),
None
)
if ha_mode not in operation_list:
msg = 'The requested thermostat mode {} is not supported'.format(mode)
return api_error(
request,
namespace='Alexa.ThermostatController',
error_type='UNSUPPORTED_THERMOSTAT_MODE',
error_message=msg
)
data = {
ATTR_ENTITY_ID: entity.entity_id,
climate.ATTR_OPERATION_MODE: ha_mode,
}
await hass.services.async_call(
entity.domain, climate.SERVICE_SET_OPERATION_MODE, data,
blocking=False)
return api_message(request)
@HANDLERS.register(('Alexa', 'ReportState')) @HANDLERS.register(('Alexa', 'ReportState'))
@extract_entity @extract_entity
@asyncio.coroutine @asyncio.coroutine

View File

@ -10,14 +10,15 @@ from datetime import timedelta
import aiohttp import aiohttp
import voluptuous as vol import voluptuous as vol
from requests.exceptions import HTTPError, ConnectTimeout from requests.exceptions import HTTPError, ConnectTimeout
from requests.exceptions import ConnectionError as ConnectError
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD, CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD,
CONF_SENSORS, CONF_SCAN_INTERVAL, HTTP_BASIC_AUTHENTICATION) CONF_SENSORS, CONF_SWITCHES, CONF_SCAN_INTERVAL, HTTP_BASIC_AUTHENTICATION)
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['amcrest==1.2.1'] REQUIREMENTS = ['amcrest==1.2.2']
DEPENDENCIES = ['ffmpeg'] DEPENDENCIES = ['ffmpeg']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -63,6 +64,12 @@ SENSORS = {
'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'], '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({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
@ -81,6 +88,8 @@ CONFIG_SCHEMA = vol.Schema({
cv.time_period, cv.time_period,
vol.Optional(CONF_SENSORS): vol.Optional(CONF_SENSORS):
vol.All(cv.ensure_list, [vol.In(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) }, extra=vol.ALLOW_EXTRA)
@ -93,14 +102,15 @@ def setup(hass, config):
amcrest_cams = config[DOMAIN] amcrest_cams = config[DOMAIN]
for device in amcrest_cams: for device in amcrest_cams:
camera = AmcrestCamera(device.get(CONF_HOST),
device.get(CONF_PORT),
device.get(CONF_USERNAME),
device.get(CONF_PASSWORD)).camera
try: 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 camera.current_time
except (ConnectTimeout, HTTPError) as ex: except (ConnectError, ConnectTimeout, HTTPError) as ex:
_LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex)) _LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex))
hass.components.persistent_notification.create( hass.components.persistent_notification.create(
'Error: {}<br />' 'Error: {}<br />'
@ -108,12 +118,13 @@ def setup(hass, config):
''.format(ex), ''.format(ex),
title=NOTIFICATION_TITLE, title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID) notification_id=NOTIFICATION_ID)
return False continue
ffmpeg_arguments = device.get(CONF_FFMPEG_ARGUMENTS) ffmpeg_arguments = device.get(CONF_FFMPEG_ARGUMENTS)
name = device.get(CONF_NAME) name = device.get(CONF_NAME)
resolution = RESOLUTION_LIST[device.get(CONF_RESOLUTION)] resolution = RESOLUTION_LIST[device.get(CONF_RESOLUTION)]
sensors = device.get(CONF_SENSORS) sensors = device.get(CONF_SENSORS)
switches = device.get(CONF_SWITCHES)
stream_source = STREAM_SOURCE_LIST[device.get(CONF_STREAM_SOURCE)] stream_source = STREAM_SOURCE_LIST[device.get(CONF_STREAM_SOURCE)]
username = device.get(CONF_USERNAME) username = device.get(CONF_USERNAME)
@ -143,6 +154,13 @@ def setup(hass, config):
CONF_SENSORS: sensors, CONF_SENSORS: sensors,
}, config) }, config)
if switches:
discovery.load_platform(
hass, 'switch', DOMAIN, {
CONF_NAME: name,
CONF_SWITCHES: switches
}, config)
return True return True

View File

@ -2,7 +2,7 @@
Rest API for Home Assistant. Rest API for Home Assistant.
For more details about the RESTful API, please refer to the documentation at For more details about the RESTful API, please refer to the documentation at
https://home-assistant.io/developers/api/ https://developers.home-assistant.io/docs/en/external_api_rest.html
""" """
import asyncio import asyncio
import json import json
@ -11,31 +11,34 @@ import logging
from aiohttp import web from aiohttp import web
import async_timeout import async_timeout
import homeassistant.core as ha
import homeassistant.remote as rem
from homeassistant.bootstrap import DATA_LOGGING from homeassistant.bootstrap import DATA_LOGGING
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__)
from homeassistant.exceptions import TemplateError
from homeassistant.helpers.state import AsyncTrackStates
from homeassistant.helpers.service import async_get_all_descriptions
from homeassistant.helpers import template
from homeassistant.components.http import HomeAssistantView 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.exceptions import TemplateError
from homeassistant.helpers import template
from homeassistant.helpers.service import async_get_all_descriptions
from homeassistant.helpers.state import AsyncTrackStates
import homeassistant.remote as rem
_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' DOMAIN = 'api'
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']
STREAM_PING_PAYLOAD = "ping" STREAM_PING_PAYLOAD = 'ping'
STREAM_PING_INTERVAL = 50 # seconds STREAM_PING_INTERVAL = 50 # seconds
_LOGGER = logging.getLogger(__name__)
def setup(hass, config): def setup(hass, config):
"""Register the API with the HTTP interface.""" """Register the API with the HTTP interface."""
@ -52,9 +55,8 @@ def setup(hass, config):
hass.http.register_view(APIComponentsView) hass.http.register_view(APIComponentsView)
hass.http.register_view(APITemplateView) hass.http.register_view(APITemplateView)
log_path = hass.data.get(DATA_LOGGING, None) if DATA_LOGGING in hass.data:
if log_path: hass.http.register_view(APIErrorLog)
hass.http.register_static_path(URL_API_ERROR_LOG, log_path, False)
return True return True
@ -63,22 +65,21 @@ class APIStatusView(HomeAssistantView):
"""View to handle Status requests.""" """View to handle Status requests."""
url = URL_API url = URL_API
name = "api:status" name = 'api:status'
@ha.callback @ha.callback
def get(self, request): def get(self, request):
"""Retrieve if API is running.""" """Retrieve if API is running."""
return self.json_message('API running.') return self.json_message("API running.")
class APIEventStream(HomeAssistantView): class APIEventStream(HomeAssistantView):
"""View to handle EventStream requests.""" """View to handle EventStream requests."""
url = URL_API_STREAM url = URL_API_STREAM
name = "api:stream" name = 'api:stream'
@asyncio.coroutine async def get(self, request):
def get(self, request):
"""Provide a streaming interface for the event bus.""" """Provide a streaming interface for the event bus."""
# pylint: disable=no-self-use # pylint: disable=no-self-use
hass = request.app['hass'] hass = request.app['hass']
@ -89,8 +90,7 @@ class APIEventStream(HomeAssistantView):
if restrict: if restrict:
restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP] restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP]
@asyncio.coroutine async def forward_events(event):
def forward_events(event):
"""Forward events to the open request.""" """Forward events to the open request."""
if event.event_type == EVENT_TIME_CHANGED: if event.event_type == EVENT_TIME_CHANGED:
return return
@ -98,56 +98,56 @@ class APIEventStream(HomeAssistantView):
if restrict and event.event_type not in restrict: if restrict and event.event_type not in restrict:
return return
_LOGGER.debug('STREAM %s FORWARDING %s', id(stop_obj), event) _LOGGER.debug("STREAM %s FORWARDING %s", id(stop_obj), event)
if event.event_type == EVENT_HOMEASSISTANT_STOP: if event.event_type == EVENT_HOMEASSISTANT_STOP:
data = stop_obj data = stop_obj
else: else:
data = json.dumps(event, cls=rem.JSONEncoder) data = json.dumps(event, cls=rem.JSONEncoder)
yield from to_write.put(data) await to_write.put(data)
response = web.StreamResponse() response = web.StreamResponse()
response.content_type = 'text/event-stream' response.content_type = 'text/event-stream'
yield from response.prepare(request) await response.prepare(request)
unsub_stream = hass.bus.async_listen(MATCH_ALL, forward_events) unsub_stream = hass.bus.async_listen(MATCH_ALL, forward_events)
try: try:
_LOGGER.debug('STREAM %s ATTACHED', id(stop_obj)) _LOGGER.debug("STREAM %s ATTACHED", id(stop_obj))
# Fire off one message so browsers fire open event right away # Fire off one message so browsers fire open event right away
yield from to_write.put(STREAM_PING_PAYLOAD) await to_write.put(STREAM_PING_PAYLOAD)
while True: while True:
try: try:
with async_timeout.timeout(STREAM_PING_INTERVAL, with async_timeout.timeout(STREAM_PING_INTERVAL,
loop=hass.loop): loop=hass.loop):
payload = yield from to_write.get() payload = await to_write.get()
if payload is stop_obj: if payload is stop_obj:
break break
msg = "data: {}\n\n".format(payload) msg = "data: {}\n\n".format(payload)
_LOGGER.debug('STREAM %s WRITING %s', id(stop_obj), _LOGGER.debug(
msg.strip()) "STREAM %s WRITING %s", id(stop_obj), msg.strip())
yield from response.write(msg.encode("UTF-8")) await response.write(msg.encode('UTF-8'))
except asyncio.TimeoutError: except asyncio.TimeoutError:
yield from to_write.put(STREAM_PING_PAYLOAD) await to_write.put(STREAM_PING_PAYLOAD)
except asyncio.CancelledError: except asyncio.CancelledError:
_LOGGER.debug('STREAM %s ABORT', id(stop_obj)) _LOGGER.debug("STREAM %s ABORT", id(stop_obj))
finally: finally:
_LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj)) _LOGGER.debug("STREAM %s RESPONSE CLOSED", id(stop_obj))
unsub_stream() unsub_stream()
class APIConfigView(HomeAssistantView): class APIConfigView(HomeAssistantView):
"""View to handle Config requests.""" """View to handle Configuration requests."""
url = URL_API_CONFIG url = URL_API_CONFIG
name = "api:config" name = 'api:config'
@ha.callback @ha.callback
def get(self, request): def get(self, request):
@ -156,22 +156,22 @@ class APIConfigView(HomeAssistantView):
class APIDiscoveryView(HomeAssistantView): class APIDiscoveryView(HomeAssistantView):
"""View to provide discovery info.""" """View to provide Discovery information."""
requires_auth = False requires_auth = False
url = URL_API_DISCOVERY_INFO url = URL_API_DISCOVERY_INFO
name = "api:discovery" name = 'api:discovery'
@ha.callback @ha.callback
def get(self, request): def get(self, request):
"""Get discovery info.""" """Get discovery information."""
hass = request.app['hass'] hass = request.app['hass']
needs_auth = hass.config.api.api_password is not None needs_auth = hass.config.api.api_password is not None
return self.json({ return self.json({
'base_url': hass.config.api.base_url, ATTR_BASE_URL: hass.config.api.base_url,
'location_name': hass.config.location_name, ATTR_LOCATION_NAME: hass.config.location_name,
'requires_api_password': needs_auth, ATTR_REQUIRES_API_PASSWORD: needs_auth,
'version': __version__ ATTR_VERSION: __version__,
}) })
@ -190,8 +190,8 @@ class APIStatesView(HomeAssistantView):
class APIEntityStateView(HomeAssistantView): class APIEntityStateView(HomeAssistantView):
"""View to handle EntityState requests.""" """View to handle EntityState requests."""
url = "/api/states/{entity_id}" url = '/api/states/{entity_id}'
name = "api:entity-state" name = 'api:entity-state'
@ha.callback @ha.callback
def get(self, request, entity_id): def get(self, request, entity_id):
@ -199,22 +199,21 @@ class APIEntityStateView(HomeAssistantView):
state = request.app['hass'].states.get(entity_id) state = request.app['hass'].states.get(entity_id)
if state: if state:
return self.json(state) return self.json(state)
return self.json_message('Entity not found', HTTP_NOT_FOUND) return self.json_message("Entity not found.", HTTP_NOT_FOUND)
@asyncio.coroutine async def post(self, request, entity_id):
def post(self, request, entity_id):
"""Update state of entity.""" """Update state of entity."""
hass = request.app['hass'] hass = request.app['hass']
try: try:
data = yield from request.json() data = await request.json()
except ValueError: except ValueError:
return self.json_message('Invalid JSON specified', return self.json_message(
HTTP_BAD_REQUEST) "Invalid JSON specified.", HTTP_BAD_REQUEST)
new_state = data.get('state') new_state = data.get('state')
if new_state is None: if new_state is None:
return self.json_message('No state specified', HTTP_BAD_REQUEST) return self.json_message("No state specified.", HTTP_BAD_REQUEST)
attributes = data.get('attributes') attributes = data.get('attributes')
force_update = data.get('force_update', False) force_update = data.get('force_update', False)
@ -236,15 +235,15 @@ class APIEntityStateView(HomeAssistantView):
def delete(self, request, entity_id): def delete(self, request, entity_id):
"""Remove entity.""" """Remove entity."""
if request.app['hass'].states.async_remove(entity_id): if request.app['hass'].states.async_remove(entity_id):
return self.json_message('Entity removed') return self.json_message("Entity removed.")
return self.json_message('Entity not found', HTTP_NOT_FOUND) return self.json_message("Entity not found.", HTTP_NOT_FOUND)
class APIEventListenersView(HomeAssistantView): class APIEventListenersView(HomeAssistantView):
"""View to handle EventListeners requests.""" """View to handle EventListeners requests."""
url = URL_API_EVENTS url = URL_API_EVENTS
name = "api:event-listeners" name = 'api:event-listeners'
@ha.callback @ha.callback
def get(self, request): def get(self, request):
@ -256,21 +255,20 @@ class APIEventView(HomeAssistantView):
"""View to handle Event requests.""" """View to handle Event requests."""
url = '/api/events/{event_type}' url = '/api/events/{event_type}'
name = "api:event" name = 'api:event'
@asyncio.coroutine async def post(self, request, event_type):
def post(self, request, event_type):
"""Fire events.""" """Fire events."""
body = yield from request.text() body = await request.text()
try: try:
event_data = json.loads(body) if body else None event_data = json.loads(body) if body else None
except ValueError: except ValueError:
return self.json_message('Event data should be valid JSON', return self.json_message(
HTTP_BAD_REQUEST) "Event data should be valid JSON.", HTTP_BAD_REQUEST)
if event_data is not None and not isinstance(event_data, dict): if event_data is not None and not isinstance(event_data, dict):
return self.json_message('Event data should be a JSON object', return self.json_message(
HTTP_BAD_REQUEST) "Event data should be a JSON object", HTTP_BAD_REQUEST)
# Special case handling for event STATE_CHANGED # Special case handling for event STATE_CHANGED
# We will try to convert state dicts back to State objects # We will try to convert state dicts back to State objects
@ -281,8 +279,8 @@ class APIEventView(HomeAssistantView):
if state: if state:
event_data[key] = state event_data[key] = state
request.app['hass'].bus.async_fire(event_type, event_data, request.app['hass'].bus.async_fire(
ha.EventOrigin.remote) event_type, event_data, ha.EventOrigin.remote)
return self.json_message("Event {} fired.".format(event_type)) return self.json_message("Event {} fired.".format(event_type))
@ -291,37 +289,35 @@ class APIServicesView(HomeAssistantView):
"""View to handle Services requests.""" """View to handle Services requests."""
url = URL_API_SERVICES url = URL_API_SERVICES
name = "api:services" name = 'api:services'
@asyncio.coroutine async def get(self, request):
def get(self, request):
"""Get registered services.""" """Get registered services."""
services = yield from async_services_json(request.app['hass']) services = await async_services_json(request.app['hass'])
return self.json(services) return self.json(services)
class APIDomainServicesView(HomeAssistantView): class APIDomainServicesView(HomeAssistantView):
"""View to handle DomainServices requests.""" """View to handle DomainServices requests."""
url = "/api/services/{domain}/{service}" url = '/api/services/{domain}/{service}'
name = "api:domain-services" name = 'api:domain-services'
@asyncio.coroutine async def post(self, request, domain, service):
def post(self, request, domain, service):
"""Call a service. """Call a service.
Returns a list of changed states. Returns a list of changed states.
""" """
hass = request.app['hass'] hass = request.app['hass']
body = yield from request.text() body = await request.text()
try: try:
data = json.loads(body) if body else None data = json.loads(body) if body else None
except ValueError: except ValueError:
return self.json_message('Data should be valid JSON', return self.json_message(
HTTP_BAD_REQUEST) "Data should be valid JSON.", HTTP_BAD_REQUEST)
with AsyncTrackStates(hass) as changed_states: with AsyncTrackStates(hass) as changed_states:
yield from hass.services.async_call(domain, service, data, True) await hass.services.async_call(domain, service, data, True)
return self.json(changed_states) return self.json(changed_states)
@ -330,7 +326,7 @@ class APIComponentsView(HomeAssistantView):
"""View to handle Components requests.""" """View to handle Components requests."""
url = URL_API_COMPONENTS url = URL_API_COMPONENTS
name = "api:components" name = 'api:components'
@ha.callback @ha.callback
def get(self, request): def get(self, request):
@ -339,32 +335,41 @@ class APIComponentsView(HomeAssistantView):
class APITemplateView(HomeAssistantView): class APITemplateView(HomeAssistantView):
"""View to handle requests.""" """View to handle Template requests."""
url = URL_API_TEMPLATE url = URL_API_TEMPLATE
name = "api:template" name = 'api:template'
@asyncio.coroutine async def post(self, request):
def post(self, request):
"""Render a template.""" """Render a template."""
try: try:
data = yield from request.json() data = await request.json()
tpl = template.Template(data['template'], request.app['hass']) tpl = template.Template(data['template'], request.app['hass'])
return tpl.async_render(data.get('variables')) return tpl.async_render(data.get('variables'))
except (ValueError, TemplateError) as ex: except (ValueError, TemplateError) as ex:
return self.json_message('Error rendering template: {}'.format(ex), return self.json_message(
HTTP_BAD_REQUEST) "Error rendering template: {}".format(ex), HTTP_BAD_REQUEST)
@asyncio.coroutine class APIErrorLog(HomeAssistantView):
def async_services_json(hass): """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."""
return web.FileResponse(request.app['hass'].data[DATA_LOGGING])
async def async_services_json(hass):
"""Generate services data to JSONify.""" """Generate services data to JSONify."""
descriptions = yield from async_get_all_descriptions(hass) descriptions = await async_get_all_descriptions(hass)
return [{"domain": key, "services": value} return [{'domain': key, 'services': value}
for key, value in descriptions.items()] for key, value in descriptions.items()]
def async_events_json(hass): def async_events_json(hass):
"""Generate event data to JSONify.""" """Generate event data to JSONify."""
return [{"event": key, "listener_count": value} return [{'event': key, 'listener_count': value}
for key, value in hass.bus.async_listeners().items()] for key, value in hass.bus.async_listeners().items()]

View File

@ -17,7 +17,7 @@ from homeassistant.helpers import discovery
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyatv==0.3.9'] REQUIREMENTS = ['pyatv==0.3.10']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -0,0 +1,351 @@
"""Component to allow users to login and get tokens.
All requests will require passing in a valid client ID and secret via HTTP
Basic Auth.
# GET /auth/providers
Return a list of auth providers. Example:
[
{
"name": "Local",
"id": null,
"type": "local_provider",
}
]
# POST /auth/login_flow
Create a login flow. Will return the first step of the flow.
Pass in parameter 'handler' to specify the auth provider to use. Auth providers
are identified by type and id.
{
"handler": ["local_provider", null]
}
Return value will be a step in a data entry flow. See the docs for data entry
flow for details.
{
"data_schema": [
{"name": "username", "type": "string"},
{"name": "password", "type": "string"}
],
"errors": {},
"flow_id": "8f7e42faab604bcab7ac43c44ca34d58",
"handler": ["insecure_example", null],
"step_id": "init",
"type": "form"
}
# POST /auth/login_flow/{flow_id}
Progress the flow. Most flows will be 1 page, but could optionally add extra
login challenges, like TFA. Once the flow has finished, the returned step will
have type "create_entry" and "result" key will contain an authorization code.
{
"flow_id": "8f7e42faab604bcab7ac43c44ca34d58",
"handler": ["insecure_example", null],
"result": "411ee2f916e648d691e937ae9344681e",
"source": "user",
"title": "Example",
"type": "create_entry",
"version": 1
}
# POST /auth/token
This is an OAuth2 endpoint for granting tokens. We currently support the grant
types "authorization_code" and "refresh_token". Because we follow the OAuth2
spec, data should be send in formatted as x-www-form-urlencoded. Examples will
be in JSON as it's more readable.
## Grant type authorization_code
Exchange the authorization code retrieved from the login flow for tokens.
{
"grant_type": "authorization_code",
"code": "411ee2f916e648d691e937ae9344681e"
}
Return value will be the access and refresh tokens. The access token will have
a limited expiration. New access tokens can be requested using the refresh
token.
{
"access_token": "ABCDEFGH",
"expires_in": 1800,
"refresh_token": "IJKLMNOPQRST",
"token_type": "Bearer"
}
## Grant type refresh_token
Request a new access token using a refresh token.
{
"grant_type": "refresh_token",
"refresh_token": "IJKLMNOPQRST"
}
Return value will be a new access token. The access token will have
a limited expiration.
{
"access_token": "ABCDEFGH",
"expires_in": 1800,
"token_type": "Bearer"
}
"""
import logging
import uuid
import aiohttp.web
import voluptuous as vol
from homeassistant import data_entry_flow
from homeassistant.core import callback
from homeassistant.helpers.data_entry_flow import (
FlowManagerIndexView, FlowManagerResourceView)
from homeassistant.components.http.view import HomeAssistantView
from homeassistant.components.http.data_validator import RequestDataValidator
from .client import verify_client
DOMAIN = 'auth'
DEPENDENCIES = ['http']
_LOGGER = logging.getLogger(__name__)
async def async_setup(hass, config):
"""Component to allow users to login."""
store_credentials, retrieve_credentials = _create_cred_store()
hass.http.register_view(AuthProvidersView)
hass.http.register_view(LoginFlowIndexView(hass.auth.login_flow))
hass.http.register_view(
LoginFlowResourceView(hass.auth.login_flow, store_credentials))
hass.http.register_view(GrantTokenView(retrieve_credentials))
hass.http.register_view(LinkUserView(retrieve_credentials))
return True
class AuthProvidersView(HomeAssistantView):
"""View to get available auth providers."""
url = '/auth/providers'
name = 'api:auth:providers'
requires_auth = False
@verify_client
async def get(self, request, client):
"""Get available auth providers."""
return self.json([{
'name': provider.name,
'id': provider.id,
'type': provider.type,
} for provider in request.app['hass'].auth.async_auth_providers])
class LoginFlowIndexView(FlowManagerIndexView):
"""View to create a config flow."""
url = '/auth/login_flow'
name = 'api:auth:login_flow'
requires_auth = False
async def get(self, request):
"""Do not allow index of flows in progress."""
return aiohttp.web.Response(status=405)
# pylint: disable=arguments-differ
@verify_client
@RequestDataValidator(vol.Schema({
vol.Required('handler'): vol.Any(str, list),
vol.Required('redirect_uri'): str,
}))
async def post(self, request, client, data):
"""Create a new login flow."""
if data['redirect_uri'] not in client.redirect_uris:
return self.json_message('invalid redirect uri', )
# pylint: disable=no-value-for-parameter
return await super().post(request)
class LoginFlowResourceView(FlowManagerResourceView):
"""View to interact with the flow manager."""
url = '/auth/login_flow/{flow_id}'
name = 'api:auth:login_flow:resource'
requires_auth = False
def __init__(self, flow_mgr, store_credentials):
"""Initialize the login flow resource view."""
super().__init__(flow_mgr)
self._store_credentials = store_credentials
# pylint: disable=arguments-differ
async def get(self, request):
"""Do not allow getting status of a flow in progress."""
return self.json_message('Invalid flow specified', 404)
# pylint: disable=arguments-differ
@verify_client
@RequestDataValidator(vol.Schema(dict), allow_empty=True)
async def post(self, request, client, flow_id, data):
"""Handle progressing a login flow request."""
try:
result = await self._flow_mgr.async_configure(flow_id, data)
except data_entry_flow.UnknownFlow:
return self.json_message('Invalid flow specified', 404)
except vol.Invalid:
return self.json_message('User input malformed', 400)
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
return self.json(self._prepare_result_json(result))
result.pop('data')
result['result'] = self._store_credentials(client.id, result['result'])
return self.json(result)
class GrantTokenView(HomeAssistantView):
"""View to grant tokens."""
url = '/auth/token'
name = 'api:auth:token'
requires_auth = False
def __init__(self, retrieve_credentials):
"""Initialize the grant token view."""
self._retrieve_credentials = retrieve_credentials
@verify_client
async def post(self, request, client):
"""Grant a token."""
hass = request.app['hass']
data = await request.post()
grant_type = data.get('grant_type')
if grant_type == 'authorization_code':
return await self._async_handle_auth_code(
hass, client.id, data)
elif grant_type == 'refresh_token':
return await self._async_handle_refresh_token(
hass, client.id, data)
return self.json({
'error': 'unsupported_grant_type',
}, status_code=400)
async def _async_handle_auth_code(self, hass, client_id, data):
"""Handle authorization code request."""
code = data.get('code')
if code is None:
return self.json({
'error': 'invalid_request',
}, status_code=400)
credentials = self._retrieve_credentials(client_id, code)
if credentials is None:
return self.json({
'error': 'invalid_request',
}, status_code=400)
user = await hass.auth.async_get_or_create_user(credentials)
refresh_token = await hass.auth.async_create_refresh_token(user,
client_id)
access_token = hass.auth.async_create_access_token(refresh_token)
return self.json({
'access_token': access_token.token,
'token_type': 'Bearer',
'refresh_token': refresh_token.token,
'expires_in':
int(refresh_token.access_token_expiration.total_seconds()),
})
async def _async_handle_refresh_token(self, hass, client_id, data):
"""Handle authorization code request."""
token = data.get('refresh_token')
if token is None:
return self.json({
'error': 'invalid_request',
}, status_code=400)
refresh_token = await hass.auth.async_get_refresh_token(token)
if refresh_token is None or refresh_token.client_id != client_id:
return self.json({
'error': 'invalid_grant',
}, status_code=400)
access_token = hass.auth.async_create_access_token(refresh_token)
return self.json({
'access_token': access_token.token,
'token_type': 'Bearer',
'expires_in':
int(refresh_token.access_token_expiration.total_seconds()),
})
class LinkUserView(HomeAssistantView):
"""View to link existing users to new credentials."""
url = '/auth/link_user'
name = 'api:auth:link_user'
def __init__(self, retrieve_credentials):
"""Initialize the link user view."""
self._retrieve_credentials = retrieve_credentials
@RequestDataValidator(vol.Schema({
'code': str,
'client_id': str,
}))
async def post(self, request, data):
"""Link a user."""
hass = request.app['hass']
user = request['hass_user']
credentials = self._retrieve_credentials(
data['client_id'], data['code'])
if credentials is None:
return self.json_message('Invalid code', status_code=400)
await hass.auth.async_link_user(user, credentials)
return self.json_message('User linked')
@callback
def _create_cred_store():
"""Create a credential store."""
temp_credentials = {}
@callback
def store_credentials(client_id, credentials):
"""Store credentials and return a code to retrieve it."""
code = uuid.uuid4().hex
temp_credentials[(client_id, code)] = credentials
return code
@callback
def retrieve_credentials(client_id, code):
"""Retrieve credentials."""
return temp_credentials.pop((client_id, code), None)
return store_credentials, retrieve_credentials

View File

@ -0,0 +1,79 @@
"""Helpers to resolve client ID/secret."""
import base64
from functools import wraps
import hmac
import aiohttp.hdrs
def verify_client(method):
"""Decorator to verify client id/secret on requests."""
@wraps(method)
async def wrapper(view, request, *args, **kwargs):
"""Verify client id/secret before doing request."""
client = await _verify_client(request)
if client is None:
return view.json({
'error': 'invalid_client',
}, status_code=401)
return await method(
view, request, *args, **kwargs, client=client)
return wrapper
async def _verify_client(request):
"""Method to verify the client id/secret in consistent time.
By using a consistent time for looking up client id and comparing the
secret, we prevent attacks by malicious actors trying different client ids
and are able to derive from the time it takes to process the request if
they guessed the client id correctly.
"""
if aiohttp.hdrs.AUTHORIZATION not in request.headers:
return None
auth_type, auth_value = \
request.headers.get(aiohttp.hdrs.AUTHORIZATION).split(' ', 1)
if auth_type != 'Basic':
return None
decoded = base64.b64decode(auth_value).decode('utf-8')
try:
client_id, client_secret = decoded.split(':', 1)
except ValueError:
# If no ':' in decoded
client_id, client_secret = decoded, None
return await async_secure_get_client(
request.app['hass'], client_id, client_secret)
async def async_secure_get_client(hass, client_id, client_secret):
"""Get a client id/secret in consistent time."""
client = await hass.auth.async_get_client(client_id)
if client is None:
if client_secret is not None:
# Still do a compare so we run same time as if a client was found.
hmac.compare_digest(client_secret.encode('utf-8'),
client_secret.encode('utf-8'))
return None
if client.secret is None:
return client
elif client_secret is None:
# Still do a compare so we run same time as if a secret was passed.
hmac.compare_digest(client.secret.encode('utf-8'),
client.secret.encode('utf-8'))
return None
elif hmac.compare_digest(client_secret.encode('utf-8'),
client.secret.encode('utf-8')):
return client
return None

View File

@ -6,6 +6,7 @@ https://home-assistant.io/components/automation/
""" """
import asyncio import asyncio
from functools import partial from functools import partial
import importlib
import logging import logging
import voluptuous as vol import voluptuous as vol
@ -22,7 +23,6 @@ from homeassistant.helpers import extract_domain_configs, script, condition
from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.restore_state import async_get_last_state from homeassistant.helpers.restore_state import async_get_last_state
from homeassistant.loader import get_platform
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -58,12 +58,14 @@ _LOGGER = logging.getLogger(__name__)
def _platform_validator(config): def _platform_validator(config):
"""Validate it is a valid platform.""" """Validate it is a valid platform."""
platform = get_platform(DOMAIN, config[CONF_PLATFORM]) try:
platform = importlib.import_module(
'homeassistant.components.automation.{}'.format(
config[CONF_PLATFORM]))
except ImportError:
raise vol.Invalid('Invalid platform specified') from None
if not hasattr(platform, 'TRIGGER_SCHEMA'): return platform.TRIGGER_SCHEMA(config)
return config
return getattr(platform, 'TRIGGER_SCHEMA')(config)
_TRIGGER_SCHEMA = vol.All( _TRIGGER_SCHEMA = vol.All(
@ -71,7 +73,7 @@ _TRIGGER_SCHEMA = vol.All(
[ [
vol.All( vol.All(
vol.Schema({ vol.Schema({
vol.Required(CONF_PLATFORM): cv.platform_validator(DOMAIN) vol.Required(CONF_PLATFORM): str
}, extra=vol.ALLOW_EXTRA), }, extra=vol.ALLOW_EXTRA),
_platform_validator _platform_validator
), ),
@ -96,7 +98,7 @@ SERVICE_SCHEMA = vol.Schema({
}) })
TRIGGER_SERVICE_SCHEMA = vol.Schema({ TRIGGER_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(ATTR_VARIABLES, default={}): dict, vol.Optional(ATTR_VARIABLES, default={}): dict,
}) })

View File

@ -50,13 +50,23 @@ DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))
async def async_setup(hass, config): async def async_setup(hass, config):
"""Track states and offer events for binary sensors.""" """Track states and offer events for binary sensors."""
component = EntityComponent( component = hass.data[DOMAIN] = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
await component.async_setup(config) await component.async_setup(config)
return True return True
async def async_setup_entry(hass, entry):
"""Setup a config entry."""
return await hass.data[DOMAIN].async_setup_entry(entry)
async def async_unload_entry(hass, entry):
"""Unload a config entry."""
return await hass.data[DOMAIN].async_unload_entry(entry)
# pylint: disable=no-self-use # pylint: disable=no-self-use
class BinarySensorDevice(Entity): class BinarySensorDevice(Entity):
"""Represent a binary sensor.""" """Represent a binary sensor."""

View File

@ -217,4 +217,4 @@ class BayesianBinarySensor(BinarySensorDevice):
@asyncio.coroutine @asyncio.coroutine
def async_update(self): def async_update(self):
"""Get the latest data and update the states.""" """Get the latest data and update the states."""
self._deviation = bool(self.probability > self._probability_threshold) self._deviation = bool(self.probability >= self._probability_threshold)

View File

@ -11,7 +11,6 @@ import voluptuous as vol
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA) BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.const import CONF_MONITORED_CONDITIONS
from homeassistant.loader import get_component
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -31,7 +30,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the available BloomSky weather binary sensors.""" """Set up the available BloomSky weather binary sensors."""
bloomsky = get_component('bloomsky') bloomsky = hass.components.bloomsky
# Default needed in case of discovery # Default needed in case of discovery
sensors = config.get(CONF_MONITORED_CONDITIONS, SENSOR_TYPES) sensors = config.get(CONF_MONITORED_CONDITIONS, SENSOR_TYPES)

View File

@ -0,0 +1,196 @@
"""
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 asyncio
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN
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_devices, 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_devices(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:
"""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:
result['check_control_messages'] = check_control_messages
elif self._attribute == 'charging_status':
result['charging_status'] = vehicle_state.charging_status.value
# pylint: disable=W0212
result['last_charging_end_result'] = \
vehicle_state._attributes['lastChargingEndResult']
if self._attribute == 'connection_status':
# pylint: disable=W0212
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=W0212
self._state = (vehicle_state._attributes['connectionStatus'] ==
'CONNECTED')
@staticmethod
def _format_cbs_report(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:
result['{} distance'.format(service_type)] = \
'{} km'.format(report.due_distance)
return result
def update_callback(self):
"""Schedule a state update."""
self.schedule_update_ha_state(True)
@asyncio.coroutine
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)

View File

@ -6,28 +6,39 @@ https://home-assistant.io/components/binary_sensor.deconz/
""" """
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.deconz import ( from homeassistant.components.deconz import (
DOMAIN as DATA_DECONZ, DATA_DECONZ_ID) CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ, DATA_DECONZ_ID,
DATA_DECONZ_UNSUB)
from homeassistant.const import ATTR_BATTERY_LEVEL from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
DEPENDENCIES = ['deconz'] DEPENDENCIES = ['deconz']
async def async_setup_platform(hass, config, async_add_devices, async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None): discovery_info=None):
"""Old way of setting up deCONZ binary sensors."""
pass
async def async_setup_entry(hass, config_entry, async_add_devices):
"""Set up the deCONZ binary sensor.""" """Set up the deCONZ binary sensor."""
if discovery_info is None: @callback
return def async_add_sensor(sensors):
"""Add binary sensor from deCONZ."""
from pydeconz.sensor import DECONZ_BINARY_SENSOR
entities = []
allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True)
for sensor in sensors:
if sensor.type in DECONZ_BINARY_SENSOR and \
not (not allow_clip_sensor and sensor.type.startswith('CLIP')):
entities.append(DeconzBinarySensor(sensor))
async_add_devices(entities, True)
from pydeconz.sensor import DECONZ_BINARY_SENSOR hass.data[DATA_DECONZ_UNSUB].append(
sensors = hass.data[DATA_DECONZ].sensors async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor))
entities = []
for key in sorted(sensors.keys(), key=int): async_add_sensor(hass.data[DATA_DECONZ].sensors.values())
sensor = sensors[key]
if sensor and sensor.type in DECONZ_BINARY_SENSOR:
entities.append(DeconzBinarySensor(sensor))
async_add_devices(entities, True)
class DeconzBinarySensor(BinarySensorDevice): class DeconzBinarySensor(BinarySensorDevice):
@ -93,9 +104,9 @@ class DeconzBinarySensor(BinarySensorDevice):
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes of the sensor.""" """Return the state attributes of the sensor."""
from pydeconz.sensor import PRESENCE from pydeconz.sensor import PRESENCE
attr = { attr = {}
ATTR_BATTERY_LEVEL: self._sensor.battery, if self._sensor.battery:
} attr[ATTR_BATTERY_LEVEL] = self._sensor.battery
if self._sensor.type in PRESENCE: if self._sensor.type in PRESENCE and self._sensor.dark is not None:
attr['dark'] = self._sensor.dark attr['dark'] = self._sensor.dark
return attr return attr

View File

@ -12,7 +12,7 @@ from homeassistant.const import STATE_ON, STATE_OFF
from homeassistant.components.egardia import ( from homeassistant.components.egardia import (
EGARDIA_DEVICE, ATTR_DISCOVER_DEVICES) EGARDIA_DEVICE, ATTR_DISCOVER_DEVICES)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['egardia']
EGARDIA_TYPE_TO_DEVICE_CLASS = {'IR Sensor': 'motion', EGARDIA_TYPE_TO_DEVICE_CLASS = {'IR Sensor': 'motion',
'Door Contact': 'opening', 'Door Contact': 'opening',
'IR': 'motion'} 'IR': 'motion'}

View File

@ -6,6 +6,7 @@ https://home-assistant.io/components/binary_sensor.envisalink/
""" """
import asyncio import asyncio
import logging import logging
import datetime
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
@ -14,6 +15,7 @@ from homeassistant.components.envisalink import (
DATA_EVL, ZONE_SCHEMA, CONF_ZONENAME, CONF_ZONETYPE, EnvisalinkDevice, DATA_EVL, ZONE_SCHEMA, CONF_ZONENAME, CONF_ZONETYPE, EnvisalinkDevice,
SIGNAL_ZONE_UPDATE) SIGNAL_ZONE_UPDATE)
from homeassistant.const import ATTR_LAST_TRIP_TIME from homeassistant.const import ATTR_LAST_TRIP_TIME
from homeassistant.util import dt as dt_util
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -63,7 +65,25 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""
attr = {} attr = {}
attr[ATTR_LAST_TRIP_TIME] = self._info['last_fault']
# 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 return attr
@property @property

View File

@ -32,6 +32,7 @@ class HiveBinarySensorEntity(BinarySensorDevice):
self.device_type = hivedevice["HA_DeviceType"] self.device_type = hivedevice["HA_DeviceType"]
self.node_device_type = hivedevice["Hive_DeviceType"] self.node_device_type = hivedevice["Hive_DeviceType"]
self.session = hivesession self.session = hivesession
self.attributes = {}
self.data_updatesource = '{}.{}'.format(self.device_type, self.data_updatesource = '{}.{}'.format(self.device_type,
self.node_id) self.node_id)
@ -52,6 +53,11 @@ class HiveBinarySensorEntity(BinarySensorDevice):
"""Return the name of the binary sensor.""" """Return the name of the binary sensor."""
return self.node_name return self.node_name
@property
def device_state_attributes(self):
"""Show Device Attributes."""
return self.attributes
@property @property
def is_on(self): def is_on(self):
"""Return true if the binary sensor is on.""" """Return true if the binary sensor is on."""
@ -61,3 +67,5 @@ class HiveBinarySensorEntity(BinarySensorDevice):
def update(self): def update(self):
"""Update all Node data from Hive.""" """Update all Node data from Hive."""
self.session.core.update_data(self.node_id) self.session.core.update_data(self.node_id)
self.attributes = self.session.attributes.state_attributes(
self.node_id)

View File

@ -0,0 +1,85 @@
"""
Support for HomematicIP binary sensor.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.homematicip_cloud/
"""
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.homematicip_cloud import (
HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN,
ATTR_HOME_ID)
DEPENDENCIES = ['homematicip_cloud']
_LOGGER = logging.getLogger(__name__)
ATTR_WINDOW_STATE = 'window_state'
ATTR_EVENT_DELAY = 'event_delay'
ATTR_MOTION_DETECTED = 'motion_detected'
ATTR_ILLUMINATION = 'illumination'
HMIP_OPEN = 'open'
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up the HomematicIP binary sensor devices."""
from homematicip.device import (ShutterContact, MotionDetectorIndoor)
if discovery_info is None:
return
home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]]
devices = []
for device in home.devices:
if isinstance(device, ShutterContact):
devices.append(HomematicipShutterContact(home, device))
elif isinstance(device, MotionDetectorIndoor):
devices.append(HomematicipMotionDetector(home, device))
if devices:
async_add_devices(devices)
class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorDevice):
"""HomematicIP shutter contact."""
def __init__(self, home, device):
"""Initialize the shutter contact."""
super().__init__(home, device)
@property
def device_class(self):
"""Return the class of this sensor."""
return 'door'
@property
def is_on(self):
"""Return true if the shutter contact is on/open."""
if self._device.sabotage:
return True
if self._device.windowState is None:
return None
return self._device.windowState.lower() == HMIP_OPEN
class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice):
"""MomematicIP motion detector."""
def __init__(self, home, device):
"""Initialize the shutter contact."""
super().__init__(home, device)
@property
def device_class(self):
"""Return the class of this sensor."""
return 'motion'
@property
def is_on(self):
"""Return true if motion is detected."""
if self._device.sabotage:
return True
return self._device.motionDetected

View File

@ -0,0 +1,81 @@
"""
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_devices, 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_devices(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')]

View File

@ -17,24 +17,25 @@ _LOGGER = logging.getLogger(__name__)
SENSOR_TYPES = {'openClosedSensor': 'opening', SENSOR_TYPES = {'openClosedSensor': 'opening',
'motionSensor': 'motion', 'motionSensor': 'motion',
'doorSensor': 'door', 'doorSensor': 'door',
'leakSensor': 'moisture'} 'wetLeakSensor': 'moisture'}
@asyncio.coroutine @asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the INSTEON PLM device class for the hass platform.""" """Set up the INSTEON PLM device class for the hass platform."""
plm = hass.data['insteon_plm'] plm = hass.data['insteon_plm'].get('plm')
address = discovery_info['address'] address = discovery_info['address']
device = plm.devices[address] device = plm.devices[address]
state_key = discovery_info['state_key'] state_key = discovery_info['state_key']
name = device.states[state_key].name
if name != 'dryLeakSensor':
_LOGGER.debug('Adding device %s entity %s to Binary Sensor platform',
device.address.hex, device.states[state_key].name)
_LOGGER.debug('Adding device %s entity %s to Binary Sensor platform', new_entity = InsteonPLMBinarySensor(device, state_key)
device.address.hex, device.states[state_key].name)
new_entity = InsteonPLMBinarySensor(device, state_key) async_add_devices([new_entity])
async_add_devices([new_entity])
class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice): class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice):
@ -53,5 +54,4 @@ class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice):
@property @property
def is_on(self): def is_on(self):
"""Return the boolean response if the node is on.""" """Return the boolean response if the node is on."""
sensorstate = self._insteon_device_state.value return bool(self._insteon_device_state.value)
return bool(sensorstate)

View File

@ -117,8 +117,10 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice):
# pylint: disable=protected-access # pylint: disable=protected-access
if _is_val_unknown(self._node.status._val): if _is_val_unknown(self._node.status._val):
self._computed_state = None self._computed_state = None
self._status_was_unknown = True
else: else:
self._computed_state = bool(self._node.status._val) self._computed_state = bool(self._node.status._val)
self._status_was_unknown = False
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self) -> None: def async_added_to_hass(self) -> None:
@ -156,9 +158,13 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice):
# pylint: disable=protected-access # pylint: disable=protected-access
if not _is_val_unknown(self._negative_node.status._val): if not _is_val_unknown(self._negative_node.status._val):
# If the negative node has a value, it means the negative node is # If the negative node has a value, it means the negative node is
# in use for this device. Therefore, we cannot determine the state # in use for this device. Next we need to check to see if the
# of the sensor until we receive our first ON event. # negative and positive nodes disagree on the state (both ON or
self._computed_state = None # both OFF).
if self._negative_node.status._val == self._node.status._val:
# The states disagree, therefore we cannot determine the state
# of the sensor until we receive our first ON event.
self._computed_state = None
def _negative_node_control_handler(self, event: object) -> None: def _negative_node_control_handler(self, event: object) -> None:
"""Handle an "On" control event from the "negative" node.""" """Handle an "On" control event from the "negative" node."""
@ -189,14 +195,21 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice):
self.schedule_update_ha_state() self.schedule_update_ha_state()
self._heartbeat() self._heartbeat()
# pylint: disable=unused-argument
def on_update(self, event: object) -> None: def on_update(self, event: object) -> None:
"""Ignore primary node status updates. """Primary node status updates.
We listen directly to the Control events on all nodes for this We MOSTLY ignore these updates, as we listen directly to the Control
device. events on all nodes for this device. However, there is one edge case:
If a leak sensor is unknown, due to a recent reboot of the ISY, the
status will get updated to dry upon the first heartbeat. This status
update is the only way that a leak sensor's status changes without
an accompanying Control event, so we need to watch for it.
""" """
pass if self._status_was_unknown and self._computed_state is None:
self._computed_state = bool(int(self._node.status))
self._status_was_unknown = False
self.schedule_update_ha_state()
self._heartbeat()
@property @property
def value(self) -> object: def value(self) -> object:

View File

@ -0,0 +1,82 @@
"""
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_devices,
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_devices(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()

View File

@ -7,7 +7,7 @@ https://home-assistant.io/components/maxcube/
import logging import logging
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.maxcube import MAXCUBE_HANDLE from homeassistant.components.maxcube import DATA_KEY
from homeassistant.const import STATE_UNKNOWN from homeassistant.const import STATE_UNKNOWN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -15,16 +15,17 @@ _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Iterate through all MAX! Devices and add window shutters.""" """Iterate through all MAX! Devices and add window shutters."""
cube = hass.data[MAXCUBE_HANDLE].cube
devices = [] 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)
for device in cube.devices: # Only add Window Shutters
name = "{} {}".format( if cube.is_windowshutter(device):
cube.room_by_id(device.room_id).name, device.name) devices.append(
MaxCubeShutter(handler, name, device.rf_address))
# Only add Window Shutters
if cube.is_windowshutter(device):
devices.append(MaxCubeShutter(hass, name, device.rf_address))
if devices: if devices:
add_devices(devices) add_devices(devices)
@ -33,12 +34,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class MaxCubeShutter(BinarySensorDevice): class MaxCubeShutter(BinarySensorDevice):
"""Representation of a MAX! Cube Binary Sensor device.""" """Representation of a MAX! Cube Binary Sensor device."""
def __init__(self, hass, name, rf_address): def __init__(self, handler, name, rf_address):
"""Initialize MAX! Cube BinarySensorDevice.""" """Initialize MAX! Cube BinarySensorDevice."""
self._name = name self._name = name
self._sensor_type = 'window' self._sensor_type = 'window'
self._rf_address = rf_address self._rf_address = rf_address
self._cubehandle = hass.data[MAXCUBE_HANDLE] self._cubehandle = handler
self._state = STATE_UNKNOWN self._state = STATE_UNKNOWN
@property @property

View File

@ -1,97 +0,0 @@
"""
Support for Mercedes cars with Mercedes ME.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.mercedesme/
"""
import logging
import datetime
from homeassistant.components.binary_sensor import (BinarySensorDevice)
from homeassistant.components.mercedesme import (
DATA_MME, FEATURE_NOT_AVAILABLE, MercedesMeEntity, BINARY_SENSORS)
DEPENDENCIES = ['mercedesme']
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the sensor platform."""
data = hass.data[DATA_MME].data
if not data.cars:
_LOGGER.error("No cars found. Check component log.")
return
devices = []
for car in data.cars:
for key, value in sorted(BINARY_SENSORS.items()):
if car['availabilities'].get(key, 'INVALID') == 'VALID':
devices.append(MercedesMEBinarySensor(
data, key, value[0], car["vin"], None))
else:
_LOGGER.warning(FEATURE_NOT_AVAILABLE, key, car["license"])
add_devices(devices, True)
class MercedesMEBinarySensor(MercedesMeEntity, BinarySensorDevice):
"""Representation of a Sensor."""
@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."""
if self._internal_name == "windowsClosed":
return {
"window_front_left": self._car["windowStatusFrontLeft"],
"window_front_right": self._car["windowStatusFrontRight"],
"window_rear_left": self._car["windowStatusRearLeft"],
"window_rear_right": self._car["windowStatusRearRight"],
"original_value": self._car[self._internal_name],
"last_update": datetime.datetime.fromtimestamp(
self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'),
"car": self._car["license"]
}
elif self._internal_name == "tireWarningLight":
return {
"front_right_tire_pressure_kpa":
self._car["frontRightTirePressureKpa"],
"front_left_tire_pressure_kpa":
self._car["frontLeftTirePressureKpa"],
"rear_right_tire_pressure_kpa":
self._car["rearRightTirePressureKpa"],
"rear_left_tire_pressure_kpa":
self._car["rearLeftTirePressureKpa"],
"original_value": self._car[self._internal_name],
"last_update": datetime.datetime.fromtimestamp(
self._car["lastUpdate"]
).strftime('%Y-%m-%d %H:%M:%S'),
"car": self._car["license"],
}
return {
"original_value": self._car[self._internal_name],
"last_update": datetime.datetime.fromtimestamp(
self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'),
"car": self._car["license"]
}
def update(self):
"""Fetch new state data for the sensor."""
self._car = next(
car for car in self._data.cars if car["vin"] == self._vin)
if self._internal_name == "windowsClosed":
self._state = bool(self._car[self._internal_name] == "CLOSED")
elif self._internal_name == "tireWarningLight":
self._state = bool(self._car[self._internal_name] != "INACTIVE")
else:
self._state = self._car[self._internal_name] is True
_LOGGER.debug("Updated %s Value: %s IsOn: %s",
self._internal_name, self._state, self.is_on)

View File

@ -31,7 +31,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
sensors = [] sensors = []
hub = hass.data[MYCHEVY_DOMAIN] hub = hass.data[MYCHEVY_DOMAIN]
for sconfig in SENSORS: for sconfig in SENSORS:
sensors.append(EVBinarySensor(hub, sconfig)) for car in hub.cars:
sensors.append(EVBinarySensor(hub, sconfig, car.vid))
async_add_devices(sensors) async_add_devices(sensors)
@ -45,16 +46,18 @@ class EVBinarySensor(BinarySensorDevice):
""" """
def __init__(self, connection, config): def __init__(self, connection, config, car_vid):
"""Initialize sensor with car connection.""" """Initialize sensor with car connection."""
self._conn = connection self._conn = connection
self._name = config.name self._name = config.name
self._attr = config.attr self._attr = config.attr
self._type = config.device_class self._type = config.device_class
self._is_on = None self._is_on = None
self._car_vid = car_vid
self.entity_id = ENTITY_ID_FORMAT.format( self.entity_id = ENTITY_ID_FORMAT.format(
'{}_{}'.format(MYCHEVY_DOMAIN, slugify(self._name))) '{}_{}_{}'.format(MYCHEVY_DOMAIN,
slugify(self._car.name),
slugify(self._name)))
@property @property
def name(self): def name(self):
@ -66,6 +69,11 @@ class EVBinarySensor(BinarySensorDevice):
"""Return if on.""" """Return if on."""
return self._is_on return self._is_on
@property
def _car(self):
"""Return the car."""
return self._conn.get_car(self._car_vid)
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
"""Register callbacks.""" """Register callbacks."""
@ -75,8 +83,8 @@ class EVBinarySensor(BinarySensorDevice):
@callback @callback
def async_update_callback(self): def async_update_callback(self):
"""Update state.""" """Update state."""
if self._conn.car is not None: if self._car is not None:
self._is_on = getattr(self._conn.car, self._attr, None) self._is_on = getattr(self._car, self._attr, None)
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
@property @property

View File

@ -9,12 +9,24 @@ from homeassistant.components.binary_sensor import (
DEVICE_CLASSES, DOMAIN, BinarySensorDevice) DEVICE_CLASSES, DOMAIN, BinarySensorDevice)
from homeassistant.const import STATE_ON 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',
}
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the MySensors platform for binary sensors.""" async def async_setup_platform(
hass, config, async_add_devices, discovery_info=None):
"""Set up the mysensors platform for binary sensors."""
mysensors.setup_mysensors_platform( mysensors.setup_mysensors_platform(
hass, DOMAIN, discovery_info, MySensorsBinarySensor, hass, DOMAIN, discovery_info, MySensorsBinarySensor,
add_devices=add_devices) async_add_devices=async_add_devices)
class MySensorsBinarySensor(mysensors.MySensorsEntity, BinarySensorDevice): class MySensorsBinarySensor(mysensors.MySensorsEntity, BinarySensorDevice):
@ -29,18 +41,7 @@ class MySensorsBinarySensor(mysensors.MySensorsEntity, BinarySensorDevice):
def device_class(self): def device_class(self):
"""Return the class of this sensor, from DEVICE_CLASSES.""" """Return the class of this sensor, from DEVICE_CLASSES."""
pres = self.gateway.const.Presentation pres = self.gateway.const.Presentation
class_map = { device_class = SENSORS.get(pres(self.child_type).name)
pres.S_DOOR: 'opening', if device_class in DEVICE_CLASSES:
pres.S_MOTION: 'motion', return device_class
pres.S_SMOKE: 'smoke', return None
}
if float(self.gateway.protocol_version) >= 1.5:
class_map.update({
pres.S_SPRINKLER: 'sprinkler',
pres.S_WATER_LEAK: 'leak',
pres.S_SOUND: 'sound',
pres.S_VIBRATION: 'vibration',
pres.S_MOISTURE: 'moisture',
})
if class_map.get(self.child_type) in DEVICE_CLASSES:
return class_map.get(self.child_type)

View File

@ -7,27 +7,36 @@ https://home-assistant.io/components/binary_sensor.nest/
from itertools import chain from itertools import chain
import logging import logging
from homeassistant.components.binary_sensor import (BinarySensorDevice) from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.sensor.nest import NestSensor from homeassistant.components.nest import DATA_NEST, NestSensorDevice
from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.const import CONF_MONITORED_CONDITIONS
from homeassistant.components.nest import DATA_NEST
DEPENDENCIES = ['nest'] DEPENDENCIES = ['nest']
BINARY_TYPES = ['online'] BINARY_TYPES = {'online': 'connectivity'}
CLIMATE_BINARY_TYPES = [ CLIMATE_BINARY_TYPES = {
'fan', 'fan': None,
'is_using_emergency_heat', 'is_using_emergency_heat': 'heat',
'is_locked', 'is_locked': None,
'has_leaf', 'has_leaf': None,
] }
CAMERA_BINARY_TYPES = [ CAMERA_BINARY_TYPES = {
'motion_detected', 'motion_detected': 'motion',
'sound_detected', 'sound_detected': 'sound',
'person_detected', 'person_detected': 'occupancy',
] }
STRUCTURE_BINARY_TYPES = {
'away': None,
# 'security_state', # pending python-nest update
}
STRUCTURE_BINARY_STATE_MAP = {
'away': {'away': True, 'home': False},
'security_state': {'deter': True, 'ok': False},
}
_BINARY_TYPES_DEPRECATED = [ _BINARY_TYPES_DEPRECATED = [
'hvac_ac_state', 'hvac_ac_state',
@ -40,8 +49,8 @@ _BINARY_TYPES_DEPRECATED = [
'hvac_emer_heat_state', 'hvac_emer_heat_state',
] ]
_VALID_BINARY_SENSOR_TYPES = BINARY_TYPES + CLIMATE_BINARY_TYPES \ _VALID_BINARY_SENSOR_TYPES = {**BINARY_TYPES, **CLIMATE_BINARY_TYPES,
+ CAMERA_BINARY_TYPES **CAMERA_BINARY_TYPES, **STRUCTURE_BINARY_TYPES}
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -68,6 +77,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
_LOGGER.error(wstr) _LOGGER.error(wstr)
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(), device_chain = chain(nest.thermostats(),
nest.smoke_co_alarms(), nest.smoke_co_alarms(),
nest.cameras()) nest.cameras())
@ -88,11 +101,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
sensors += [NestActivityZoneSensor(structure, sensors += [NestActivityZoneSensor(structure,
device, device,
activity_zone)] activity_zone)]
add_devices(sensors, True) add_devices(sensors, True)
class NestBinarySensor(NestSensor, BinarySensorDevice): class NestBinarySensor(NestSensorDevice, BinarySensorDevice):
"""Represents a Nest binary sensor.""" """Represents a Nest binary sensor."""
@property @property
@ -100,9 +112,19 @@ class NestBinarySensor(NestSensor, BinarySensorDevice):
"""Return true if the binary sensor is on.""" """Return true if the binary sensor is on."""
return self._state 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): def update(self):
"""Retrieve latest state.""" """Retrieve latest state."""
self._state = bool(getattr(self.device, self.variable)) value = getattr(self.device, self.variable)
if self.variable in STRUCTURE_BINARY_TYPES:
self._state = bool(STRUCTURE_BINARY_STATE_MAP
[self.variable][value])
else:
self._state = bool(value)
class NestActivityZoneSensor(NestBinarySensor): class NestActivityZoneSensor(NestBinarySensor):
@ -115,9 +137,9 @@ class NestActivityZoneSensor(NestBinarySensor):
self._name = "{} {} activity".format(self._name, self.zone.name) self._name = "{} {} activity".format(self._name, self.zone.name)
@property @property
def name(self): def device_class(self):
"""Return the name of the nest, if any.""" """Return the device class of the binary sensor."""
return self._name return 'motion'
def update(self): def update(self):
"""Retrieve latest state.""" """Retrieve latest state."""

View File

@ -13,7 +13,6 @@ import voluptuous as vol
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA) BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.components.netatmo import CameraData from homeassistant.components.netatmo import CameraData
from homeassistant.loader import get_component
from homeassistant.const import CONF_TIMEOUT from homeassistant.const import CONF_TIMEOUT
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
@ -61,7 +60,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
# pylint: disable=unused-argument # pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the access to Netatmo binary sensor.""" """Set up the access to Netatmo binary sensor."""
netatmo = get_component('netatmo') netatmo = hass.components.netatmo
home = config.get(CONF_HOME) home = config.get(CONF_HOME)
timeout = config.get(CONF_TIMEOUT) timeout = config.get(CONF_TIMEOUT)
if timeout is None: if timeout is None:

View File

@ -0,0 +1,70 @@
"""
Support for Qwikswitch Binary Sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.qwikswitch/
"""
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.qwikswitch import QSEntity, DOMAIN as QWIKSWITCH
from homeassistant.core import callback
DEPENDENCIES = [QWIKSWITCH]
_LOGGER = logging.getLogger(__name__)
async def async_setup_platform(hass, _, add_devices, discovery_info=None):
"""Add binary sensor from the main Qwikswitch component."""
if discovery_info is None:
return
qsusb = hass.data[QWIKSWITCH]
_LOGGER.debug("Setup qwikswitch.binary_sensor %s, %s",
qsusb, discovery_info)
devs = [QSBinarySensor(sensor) for sensor in discovery_info[QWIKSWITCH]]
add_devices(devs)
class QSBinarySensor(QSEntity, BinarySensorDevice):
"""Sensor based on a Qwikswitch relay/dimmer module."""
_val = False
def __init__(self, sensor):
"""Initialize the sensor."""
from pyqwikswitch import SENSORS
super().__init__(sensor['id'], sensor['name'])
self.channel = sensor['channel']
sensor_type = sensor['type']
self._decode, _ = SENSORS[sensor_type]
self._invert = not sensor.get('invert', False)
self._class = sensor.get('class', 'door')
@callback
def update_packet(self, packet):
"""Receive update packet from QSUSB."""
val = self._decode(packet, channel=self.channel)
_LOGGER.debug("Update %s (%s:%s) decoded as %s: %s",
self.entity_id, self.qsid, self.channel, val, packet)
if val is not None:
self._val = bool(val)
self.async_schedule_update_ha_state()
@property
def is_on(self):
"""Check if device is on (non-zero)."""
return self._val == self._invert
@property
def unique_id(self):
"""Return a unique identifier for this sensor."""
return "qs{}:{}".format(self.qsid, self.channel)
@property
def device_class(self):
"""Return the class of this sensor."""
return self._class

View File

@ -0,0 +1,102 @@
"""
This platform provides binary sensors for key RainMachine data.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.rainmachine/
"""
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.rainmachine import (
BINARY_SENSORS, DATA_RAINMACHINE, DATA_UPDATE_TOPIC, TYPE_FREEZE,
TYPE_FREEZE_PROTECTION, TYPE_HOT_DAYS, TYPE_HOURLY, TYPE_MONTH,
TYPE_RAINDELAY, TYPE_RAINSENSOR, TYPE_WEEKDAY, RainMachineEntity)
from homeassistant.const import CONF_MONITORED_CONDITIONS
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
DEPENDENCIES = ['rainmachine']
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the RainMachine Switch platform."""
if discovery_info is None:
return
rainmachine = hass.data[DATA_RAINMACHINE]
binary_sensors = []
for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]:
name, icon = BINARY_SENSORS[sensor_type]
binary_sensors.append(
RainMachineBinarySensor(rainmachine, sensor_type, name, icon))
add_devices(binary_sensors, True)
class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice):
"""A sensor implementation for raincloud device."""
def __init__(self, rainmachine, sensor_type, name, icon):
"""Initialize the sensor."""
super().__init__(rainmachine)
self._icon = icon
self._name = name
self._sensor_type = sensor_type
self._state = None
@property
def icon(self) -> str:
"""Return the icon."""
return self._icon
@property
def is_on(self):
"""Return the status of the sensor."""
return self._state
@property
def should_poll(self):
"""Disable polling."""
return False
@property
def unique_id(self) -> str:
"""Return a unique, HASS-friendly identifier for this entity."""
return '{0}_{1}'.format(
self.rainmachine.device_mac.replace(':', ''), self._sensor_type)
@callback
def update_data(self):
"""Update the state."""
self.async_schedule_update_ha_state(True)
async def async_added_to_hass(self):
"""Register callbacks."""
async_dispatcher_connect(self.hass, DATA_UPDATE_TOPIC,
self.update_data)
def update(self):
"""Update the state."""
if self._sensor_type == TYPE_FREEZE:
self._state = self.rainmachine.restrictions['current']['freeze']
elif self._sensor_type == TYPE_FREEZE_PROTECTION:
self._state = self.rainmachine.restrictions['global'][
'freezeProtectEnabled']
elif self._sensor_type == TYPE_HOT_DAYS:
self._state = self.rainmachine.restrictions['global'][
'hotDaysExtraWatering']
elif self._sensor_type == TYPE_HOURLY:
self._state = self.rainmachine.restrictions['current']['hourly']
elif self._sensor_type == TYPE_MONTH:
self._state = self.rainmachine.restrictions['current']['month']
elif self._sensor_type == TYPE_RAINDELAY:
self._state = self.rainmachine.restrictions['current']['rainDelay']
elif self._sensor_type == TYPE_RAINSENSOR:
self._state = self.rainmachine.restrictions['current'][
'rainSensor']
elif self._sensor_type == TYPE_WEEKDAY:
self._state = self.rainmachine.restrictions['current']['weekDay']

View File

@ -4,7 +4,6 @@ Support for showing random states.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.random/ https://home-assistant.io/components/binary_sensor.random/
""" """
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
@ -24,8 +23,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
}) })
@asyncio.coroutine async def async_setup_platform(
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): hass, config, async_add_devices, discovery_info=None):
"""Set up the Random binary sensor.""" """Set up the Random binary sensor."""
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
device_class = config.get(CONF_DEVICE_CLASS) device_class = config.get(CONF_DEVICE_CLASS)
@ -57,8 +56,7 @@ class RandomSensor(BinarySensorDevice):
"""Return the sensor class of the sensor.""" """Return the sensor class of the sensor."""
return self._device_class return self._device_class
@asyncio.coroutine async def async_update(self):
def async_update(self):
"""Get new state and update the sensor's state.""" """Get new state and update the sensor's state."""
from random import getrandbits from random import getrandbits
self._state = bool(getrandbits(1)) self._state = bool(getrandbits(1))

View File

@ -10,7 +10,7 @@ import voluptuous as vol
from homeassistant.components import rfxtrx from homeassistant.components import rfxtrx
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA, BinarySensorDevice) DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice)
from homeassistant.components.rfxtrx import ( from homeassistant.components.rfxtrx import (
ATTR_NAME, CONF_AUTOMATIC_ADD, CONF_DATA_BITS, CONF_DEVICES, ATTR_NAME, CONF_AUTOMATIC_ADD, CONF_DATA_BITS, CONF_DEVICES,
CONF_FIRE_EVENT, CONF_OFF_DELAY) CONF_FIRE_EVENT, CONF_OFF_DELAY)
@ -29,8 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_DEVICES, default={}): { vol.Optional(CONF_DEVICES, default={}): {
cv.string: vol.Schema({ cv.string: vol.Schema({
vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_DEVICE_CLASS): vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean, vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean,
vol.Optional(CONF_OFF_DELAY): vol.Optional(CONF_OFF_DELAY):
vol.Any(cv.time_period, cv.positive_timedelta), vol.Any(cv.time_period, cv.positive_timedelta),

View File

@ -14,7 +14,7 @@ from homeassistant.components.binary_sensor import (
from homeassistant.const import CONF_NAME from homeassistant.const import CONF_NAME
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['tapsaff==0.1.3'] REQUIREMENTS = ['tapsaff==0.2.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -23,7 +23,7 @@ from homeassistant.helpers.entity import generate_entity_id
from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.event import async_track_state_change
from homeassistant.util import utcnow from homeassistant.util import utcnow
REQUIREMENTS = ['numpy==1.14.0'] REQUIREMENTS = ['numpy==1.14.3']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -7,7 +7,6 @@ https://home-assistant.io/components/binary_sensor.wemo/
import logging import logging
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.loader import get_component
DEPENDENCIES = ['wemo'] DEPENDENCIES = ['wemo']
@ -25,18 +24,18 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
device = discovery.device_from_description(location, mac) device = discovery.device_from_description(location, mac)
if device: if device:
add_devices_callback([WemoBinarySensor(device)]) add_devices_callback([WemoBinarySensor(hass, device)])
class WemoBinarySensor(BinarySensorDevice): class WemoBinarySensor(BinarySensorDevice):
"""Representation a WeMo binary sensor.""" """Representation a WeMo binary sensor."""
def __init__(self, device): def __init__(self, hass, device):
"""Initialize the WeMo sensor.""" """Initialize the WeMo sensor."""
self.wemo = device self.wemo = device
self._state = None self._state = None
wemo = get_component('wemo') wemo = hass.components.wemo
wemo.SUBSCRIPTION_REGISTRY.register(self.wemo) wemo.SUBSCRIPTION_REGISTRY.register(self.wemo)
wemo.SUBSCRIPTION_REGISTRY.on(self.wemo, None, self._update_callback) wemo.SUBSCRIPTION_REGISTRY.on(self.wemo, None, self._update_callback)

View File

@ -17,21 +17,22 @@ import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['holidays==0.9.4'] REQUIREMENTS = ['holidays==0.9.5']
# List of all countries currently supported by holidays # List of all countries currently supported by holidays
# There seems to be no way to get the list out at runtime # There seems to be no way to get the list out at runtime
ALL_COUNTRIES = ['Australia', 'AU', 'Austria', 'AT', 'Belgium', 'BE', 'Canada', ALL_COUNTRIES = ['Argentina', 'AR', 'Australia', 'AU', 'Austria', 'AT',
'CA', 'Colombia', 'CO', 'Czech', 'CZ', 'Denmark', 'DK', 'Belgium', 'BE', 'Canada', 'CA', 'Colombia', 'CO', 'Czech',
'England', 'EuropeanCentralBank', 'ECB', 'TAR', 'Finland', 'CZ', 'Denmark', 'DK', 'England', 'EuropeanCentralBank',
'FI', 'France', 'FRA', 'Germany', 'DE', 'Ireland', 'ECB', 'TAR', 'Finland', 'FI', 'France', 'FRA', 'Germany',
'Isle of Man', 'Italy', 'IT', 'Japan', 'JP', 'Mexico', 'MX', 'DE', 'Hungary', 'HU', 'Ireland', 'Isle of Man', 'Italy',
'Netherlands', 'NL', 'NewZealand', 'NZ', 'Northern Ireland', 'IT', 'Japan', 'JP', 'Mexico', 'MX', 'Netherlands', 'NL',
'NewZealand', 'NZ', 'Northern Ireland',
'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT', 'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT',
'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI', 'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI',
'Slovakia', 'SK', 'South Africa', 'ZA', 'Spain', 'ES', 'Slovakia', 'SK', 'South Africa', 'ZA', 'Spain', 'ES',
'Sweden', 'SE', 'UnitedKingdom', 'UK', 'UnitedStates', 'US', 'Sweden', 'SE', 'Switzerland', 'CH', 'UnitedKingdom', 'UK',
'Wales'] 'UnitedStates', 'US', 'Wales']
CONF_COUNTRY = 'country' CONF_COUNTRY = 'country'
CONF_PROVINCE = 'province' CONF_PROVINCE = 'province'
CONF_WORKDAYS = 'workdays' CONF_WORKDAYS = 'workdays'
@ -47,13 +48,13 @@ DEFAULT_OFFSET = 0
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_COUNTRY): vol.In(ALL_COUNTRIES), vol.Required(CONF_COUNTRY): vol.In(ALL_COUNTRIES),
vol.Optional(CONF_PROVINCE): cv.string, vol.Optional(CONF_EXCLUDES, default=DEFAULT_EXCLUDES):
vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_OFFSET, default=DEFAULT_OFFSET): vol.Coerce(int), vol.Optional(CONF_OFFSET, default=DEFAULT_OFFSET): vol.Coerce(int),
vol.Optional(CONF_PROVINCE): cv.string,
vol.Optional(CONF_WORKDAYS, default=DEFAULT_WORKDAYS): vol.Optional(CONF_WORKDAYS, default=DEFAULT_WORKDAYS):
vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]), vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]),
vol.Optional(CONF_EXCLUDES, default=DEFAULT_EXCLUDES):
vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]),
}) })
@ -74,14 +75,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if province: if province:
# 'state' and 'prov' are not interchangeable, so need to make # 'state' and 'prov' are not interchangeable, so need to make
# sure we use the right one # sure we use the right one
if (hasattr(obj_holidays, "PROVINCES") and if (hasattr(obj_holidays, 'PROVINCES') and
province in obj_holidays.PROVINCES): province in obj_holidays.PROVINCES):
obj_holidays = getattr(holidays, country)(prov=province, obj_holidays = getattr(holidays, country)(
years=year) prov=province, years=year)
elif (hasattr(obj_holidays, "STATES") and elif (hasattr(obj_holidays, 'STATES') and
province in obj_holidays.STATES): province in obj_holidays.STATES):
obj_holidays = getattr(holidays, country)(state=province, obj_holidays = getattr(holidays, country)(
years=year) state=province, years=year)
else: else:
_LOGGER.error("There is no province/state %s in country %s", _LOGGER.error("There is no province/state %s in country %s",
province, country) province, country)

View File

@ -25,30 +25,35 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for (_, gateway) in hass.data[PY_XIAOMI_GATEWAY].gateways.items(): for (_, gateway) in hass.data[PY_XIAOMI_GATEWAY].gateways.items():
for device in gateway.devices['binary_sensor']: for device in gateway.devices['binary_sensor']:
model = device['model'] model = device['model']
if model in ['motion', 'sensor_motion.aq2']: if model in ['motion', 'sensor_motion', 'sensor_motion.aq2']:
devices.append(XiaomiMotionSensor(device, hass, gateway)) devices.append(XiaomiMotionSensor(device, hass, gateway))
elif model in ['magnet', 'sensor_magnet.aq2']: elif model in ['magnet', 'sensor_magnet', 'sensor_magnet.aq2']:
devices.append(XiaomiDoorSensor(device, gateway)) devices.append(XiaomiDoorSensor(device, gateway))
elif model == 'sensor_wleak.aq1': elif model == 'sensor_wleak.aq1':
devices.append(XiaomiWaterLeakSensor(device, gateway)) devices.append(XiaomiWaterLeakSensor(device, gateway))
elif model == 'smoke': elif model in ['smoke', 'sensor_smoke']:
devices.append(XiaomiSmokeSensor(device, gateway)) devices.append(XiaomiSmokeSensor(device, gateway))
elif model == 'natgas': elif model in ['natgas', 'sensor_natgas']:
devices.append(XiaomiNatgasSensor(device, gateway)) devices.append(XiaomiNatgasSensor(device, gateway))
elif model in ['switch', 'sensor_switch.aq2', 'sensor_switch.aq3']: elif model in ['switch', 'sensor_switch',
devices.append(XiaomiButton(device, 'Switch', 'status', 'sensor_switch.aq2', 'sensor_switch.aq3']:
if 'proto' not in device or int(device['proto'][0:1]) == 1:
data_key = 'status'
else:
data_key = 'channel_0'
devices.append(XiaomiButton(device, 'Switch', data_key,
hass, gateway)) hass, gateway))
elif model == '86sw1': elif model in ['86sw1', 'sensor_86sw1', 'sensor_86sw1.aq1']:
devices.append(XiaomiButton(device, 'Wall Switch', 'channel_0', devices.append(XiaomiButton(device, 'Wall Switch', 'channel_0',
hass, gateway)) hass, gateway))
elif model == '86sw2': elif model in ['86sw2', 'sensor_86sw2', 'sensor_86sw2.aq1']:
devices.append(XiaomiButton(device, 'Wall Switch (Left)', devices.append(XiaomiButton(device, 'Wall Switch (Left)',
'channel_0', hass, gateway)) 'channel_0', hass, gateway))
devices.append(XiaomiButton(device, 'Wall Switch (Right)', devices.append(XiaomiButton(device, 'Wall Switch (Right)',
'channel_1', hass, gateway)) 'channel_1', hass, gateway))
devices.append(XiaomiButton(device, 'Wall Switch (Both)', devices.append(XiaomiButton(device, 'Wall Switch (Both)',
'dual_channel', hass, gateway)) 'dual_channel', hass, gateway))
elif model == 'cube': elif model in ['cube', 'sensor_cube', 'sensor_cube.aqgl01']:
devices.append(XiaomiCube(device, hass, gateway)) devices.append(XiaomiCube(device, hass, gateway))
add_devices(devices) add_devices(devices)
@ -129,8 +134,12 @@ class XiaomiMotionSensor(XiaomiBinarySensor):
"""Initialize the XiaomiMotionSensor.""" """Initialize the XiaomiMotionSensor."""
self._hass = hass self._hass = hass
self._no_motion_since = 0 self._no_motion_since = 0
if 'proto' not in device or int(device['proto'][0:1]) == 1:
data_key = 'status'
else:
data_key = 'motion_status'
XiaomiBinarySensor.__init__(self, device, 'Motion Sensor', xiaomi_hub, XiaomiBinarySensor.__init__(self, device, 'Motion Sensor', xiaomi_hub,
'status', 'motion') data_key, 'motion')
@property @property
def device_state_attributes(self): def device_state_attributes(self):
@ -321,6 +330,8 @@ class XiaomiButton(XiaomiBinarySensor):
click_type = 'both' click_type = 'both'
elif value == 'shake': elif value == 'shake':
click_type = 'shake' click_type = 'shake'
elif value == 'long_click':
return False
else: else:
_LOGGER.warning("Unsupported click_type detected: %s", value) _LOGGER.warning("Unsupported click_type detected: %s", value)
return False return False

View File

@ -31,12 +31,21 @@ async def async_setup_platform(hass, config, async_add_devices,
if discovery_info is None: if discovery_info is None:
return return
from zigpy.zcl.clusters.general import OnOff
from zigpy.zcl.clusters.security import IasZone from zigpy.zcl.clusters.security import IasZone
if IasZone.cluster_id in discovery_info['in_clusters']:
await _async_setup_iaszone(hass, config, async_add_devices,
discovery_info)
elif OnOff.cluster_id in discovery_info['out_clusters']:
await _async_setup_remote(hass, config, async_add_devices,
discovery_info)
in_clusters = discovery_info['in_clusters']
async def _async_setup_iaszone(hass, config, async_add_devices,
discovery_info):
device_class = None device_class = None
cluster = in_clusters[IasZone.cluster_id] from zigpy.zcl.clusters.security import IasZone
cluster = discovery_info['in_clusters'][IasZone.cluster_id]
if discovery_info['new_join']: if discovery_info['new_join']:
await cluster.bind() await cluster.bind()
ieee = cluster.endpoint.device.application.ieee ieee = cluster.endpoint.device.application.ieee
@ -53,8 +62,34 @@ async def async_setup_platform(hass, config, async_add_devices,
async_add_devices([sensor], update_before_add=True) async_add_devices([sensor], update_before_add=True)
async def _async_setup_remote(hass, config, async_add_devices, discovery_info):
async def safe(coro):
"""Run coro, catching ZigBee delivery errors, and ignoring them."""
import zigpy.exceptions
try:
await coro
except zigpy.exceptions.DeliveryError as exc:
_LOGGER.warning("Ignoring error during setup: %s", exc)
if discovery_info['new_join']:
from zigpy.zcl.clusters.general import OnOff, LevelControl
out_clusters = discovery_info['out_clusters']
if OnOff.cluster_id in out_clusters:
cluster = out_clusters[OnOff.cluster_id]
await safe(cluster.bind())
await safe(cluster.configure_reporting(0, 0, 600, 1))
if LevelControl.cluster_id in out_clusters:
cluster = out_clusters[LevelControl.cluster_id]
await safe(cluster.bind())
await safe(cluster.configure_reporting(0, 1, 600, 1))
sensor = Switch(**discovery_info)
async_add_devices([sensor], update_before_add=True)
class BinarySensor(zha.Entity, BinarySensorDevice): class BinarySensor(zha.Entity, BinarySensorDevice):
"""THe ZHA Binary Sensor.""" """The ZHA Binary Sensor."""
_domain = DOMAIN _domain = DOMAIN
@ -73,7 +108,7 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return True if entity is on.""" """Return True if entity is on."""
if self._state == 'unknown': if self._state is None:
return False return False
return bool(self._state) return bool(self._state)
@ -98,7 +133,126 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
from bellows.types.basic import uint16_t from bellows.types.basic import uint16_t
result = await zha.safe_read(self._endpoint.ias_zone, result = await zha.safe_read(self._endpoint.ias_zone,
['zone_status']) ['zone_status'],
allow_cache=False)
state = result.get('zone_status', self._state) state = result.get('zone_status', self._state)
if isinstance(state, (int, uint16_t)): if isinstance(state, (int, uint16_t)):
self._state = result.get('zone_status', self._state) & 3 self._state = result.get('zone_status', self._state) & 3
class Switch(zha.Entity, BinarySensorDevice):
"""ZHA switch/remote controller/button."""
_domain = DOMAIN
class OnOffListener:
"""Listener for the OnOff ZigBee cluster."""
def __init__(self, entity):
"""Initialize OnOffListener."""
self._entity = entity
def cluster_command(self, tsn, command_id, args):
"""Handle commands received to this cluster."""
if command_id in (0x0000, 0x0040):
self._entity.set_state(False)
elif command_id in (0x0001, 0x0041, 0x0042):
self._entity.set_state(True)
elif command_id == 0x0002:
self._entity.set_state(not self._entity.is_on)
def attribute_updated(self, attrid, value):
"""Handle attribute updates on this cluster."""
if attrid == 0:
self._entity.set_state(value)
def zdo_command(self, *args, **kwargs):
"""Handle ZDO commands on this cluster."""
pass
class LevelListener:
"""Listener for the LevelControl ZigBee cluster."""
def __init__(self, entity):
"""Initialize LevelListener."""
self._entity = entity
def cluster_command(self, tsn, command_id, args):
"""Handle commands received to this cluster."""
if command_id in (0x0000, 0x0004): # move_to_level, -with_on_off
self._entity.set_level(args[0])
elif command_id in (0x0001, 0x0005): # move, -with_on_off
# We should dim slowly -- for now, just step once
rate = args[1]
if args[0] == 0xff:
rate = 10 # Should read default move rate
self._entity.move_level(-rate if args[0] else rate)
elif command_id == 0x0002: # step
# Step (technically shouldn't change on/off)
self._entity.move_level(-args[1] if args[0] else args[1])
def attribute_update(self, attrid, value):
"""Handle attribute updates on this cluster."""
if attrid == 0:
self._entity.set_level(value)
def zdo_command(self, *args, **kwargs):
"""Handle ZDO commands on this cluster."""
pass
def __init__(self, **kwargs):
"""Initialize Switch."""
super().__init__(**kwargs)
self._state = False
self._level = 0
from zigpy.zcl.clusters import general
self._out_listeners = {
general.OnOff.cluster_id: self.OnOffListener(self),
general.LevelControl.cluster_id: self.LevelListener(self),
}
@property
def should_poll(self) -> bool:
"""Let zha handle polling."""
return False
@property
def is_on(self) -> bool:
"""Return true if the binary sensor is on."""
return self._state
@property
def device_state_attributes(self):
"""Return the device state attributes."""
self._device_state_attributes.update({
'level': self._state and self._level or 0
})
return self._device_state_attributes
def move_level(self, change):
"""Increment the level, setting state if appropriate."""
if not self._state and change > 0:
self._level = 0
self._level = min(255, max(0, self._level + change))
self._state = bool(self._level)
self.async_schedule_update_ha_state()
def set_level(self, level):
"""Set the level, setting state if appropriate."""
self._level = level
self._state = bool(self._level)
self.async_schedule_update_ha_state()
def set_state(self, state):
"""Set the state."""
self._state = state
if self._level == 0:
self._level = 255
self.async_schedule_update_ha_state()
async def async_update(self):
"""Retrieve latest state."""
from zigpy.zcl.clusters.general import OnOff
result = await zha.safe_read(
self._endpoint.out_clusters[OnOff.cluster_id], ['on_off'])
self._state = result.get('on_off', self._state)

View File

@ -1,105 +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/bmw_connected_drive/
"""
import logging
import datetime
import voluptuous as vol
from homeassistant.helpers import discovery
from homeassistant.helpers.event import track_utc_time_change
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD
)
REQUIREMENTS = ['bimmer_connected==0.4.1']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'bmw_connected_drive'
CONF_VALUES = 'values'
CONF_COUNTRY = 'country'
ACCOUNT_SCHEMA = vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_COUNTRY): cv.string,
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: {
cv.string: ACCOUNT_SCHEMA
},
}, extra=vol.ALLOW_EXTRA)
BMW_COMPONENTS = ['device_tracker', 'sensor']
UPDATE_INTERVAL = 5 # in minutes
def setup(hass, config):
"""Set up the BMW connected drive components."""
accounts = []
for name, account_config in config[DOMAIN].items():
username = account_config[CONF_USERNAME]
password = account_config[CONF_PASSWORD]
country = account_config[CONF_COUNTRY]
_LOGGER.debug('Adding new account %s', name)
bimmer = BMWConnectedDriveAccount(username, password, country, name)
accounts.append(bimmer)
# update every UPDATE_INTERVAL minutes, starting now
# this should even out the load on the servers
now = datetime.datetime.now()
track_utc_time_change(
hass, bimmer.update,
minute=range(now.minute % UPDATE_INTERVAL, 60, UPDATE_INTERVAL),
second=now.second)
hass.data[DOMAIN] = accounts
for account in accounts:
account.update()
for component in BMW_COMPONENTS:
discovery.load_platform(hass, component, DOMAIN, {}, config)
return True
class BMWConnectedDriveAccount(object):
"""Representation of a BMW vehicle."""
def __init__(self, username: str, password: str, country: str,
name: str) -> None:
"""Constructor."""
from bimmer_connected.account import ConnectedDriveAccount
self.account = ConnectedDriveAccount(username, password, country)
self.name = name
self._update_listeners = []
def update(self, *_):
"""Update the state of all vehicles.
Notify all listeners about the update.
"""
_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.exception(exception)
def add_update_listener(self, listener):
"""Add a listener for update notifications."""
self._update_listeners.append(listener)

View File

@ -0,0 +1,154 @@
"""
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/
"""
import datetime
import logging
import voluptuous as vol
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
from homeassistant.helpers import discovery
from homeassistant.helpers.event import track_utc_time_change
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['bimmer_connected==0.5.1']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'bmw_connected_drive'
CONF_REGION = 'region'
ATTR_VIN = 'vin'
ACCOUNT_SCHEMA = vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_REGION): vol.Any('north_america', 'china',
'rest_of_world'),
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: {
cv.string: ACCOUNT_SCHEMA
},
}, extra=vol.ALLOW_EXTRA)
SERVICE_SCHEMA = vol.Schema({
vol.Required(ATTR_VIN): cv.string,
})
BMW_COMPONENTS = ['binary_sensor', 'device_tracker', 'lock', 'sensor']
UPDATE_INTERVAL = 5 # in minutes
SERVICE_UPDATE_STATE = 'update_state'
_SERVICE_MAP = {
'light_flash': 'trigger_remote_light_flash',
'sound_horn': 'trigger_remote_horn',
'activate_air_conditioning': 'trigger_remote_air_conditioning',
}
def setup(hass, config: dict):
"""Set up the BMW connected drive components."""
accounts = []
for name, account_config in config[DOMAIN].items():
accounts.append(setup_account(account_config, hass, name))
hass.data[DOMAIN] = accounts
def _update_all(call) -> None:
"""Update all BMW accounts."""
for cd_account in hass.data[DOMAIN]:
cd_account.update()
# Service to manually trigger updates for all accounts.
hass.services.register(DOMAIN, SERVICE_UPDATE_STATE, _update_all)
_update_all(None)
for component in BMW_COMPONENTS:
discovery.load_platform(hass, component, DOMAIN, {}, config)
return True
def setup_account(account_config: dict, hass, name: str) \
-> 'BMWConnectedDriveAccount':
"""Set up a new BMWConnectedDriveAccount based on the config."""
username = account_config[CONF_USERNAME]
password = account_config[CONF_PASSWORD]
region = account_config[CONF_REGION]
_LOGGER.debug('Adding new account %s', name)
cd_account = BMWConnectedDriveAccount(username, password, region, name)
def execute_service(call):
"""Execute a service for a vehicle.
This must be a member function as we need access to the cd_account
object here.
"""
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)
return
function_name = _SERVICE_MAP[call.service]
function_call = getattr(vehicle.remote_services, function_name)
function_call()
# register the remote services
for service in _SERVICE_MAP:
hass.services.register(
DOMAIN, service,
execute_service,
schema=SERVICE_SCHEMA)
# update every UPDATE_INTERVAL minutes, starting now
# this should even out the load on the servers
now = datetime.datetime.now()
track_utc_time_change(
hass, cd_account.update,
minute=range(now.minute % UPDATE_INTERVAL, 60, UPDATE_INTERVAL),
second=now.second)
return cd_account
class BMWConnectedDriveAccount(object):
"""Representation of a BMW vehicle."""
def __init__(self, username: str, password: str, region_str: str,
name: str) -> None:
"""Constructor."""
from bimmer_connected.account import ConnectedDriveAccount
from bimmer_connected.country_selector import get_region_from_name
region = get_region_from_name(region_str)
self.account = ConnectedDriveAccount(username, password, region)
self.name = name
self._update_listeners = []
def update(self, *_):
"""Update the state of all vehicles.
Notify all listeners about the update.
"""
_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.exception(exception)
def add_update_listener(self, listener):
"""Add a listener for update notifications."""
self._update_listeners.append(listener)

View File

@ -0,0 +1,42 @@
# Describes the format for available services for bmw_connected_drive
#
# The services related to locking/unlocking are implemented in the lock
# component to avoid redundancy.
light_flash:
description: >
Flash the lights of the vehicle. The vehicle is identified via the vin
(see below).
fields:
vin:
description: >
The vehicle identification number (VIN) of the vehicle, 17 characters
example: WBANXXXXXX1234567
sound_horn:
description: >
Sound the horn of the vehicle. The vehicle is identified via the vin
(see below).
fields:
vin:
description: >
The vehicle identification number (VIN) of the vehicle, 17 characters
example: WBANXXXXXX1234567
activate_air_conditioning:
description: >
Start the air conditioning of the vehicle. What exactly is started here
depends on the type of vehicle. It might range from just ventilation over
auxiliary heating to real air conditioning. The vehicle is identified via
the vin (see below).
fields:
vin:
description: >
The vehicle identification number (VIN) of the vehicle, 17 characters
example: WBANXXXXXX1234567
update_state:
description: >
Fetch the last state of the vehicles of all your accounts from the BMW
server. This does *not* trigger an update from the vehicle, it just gets
the data from the BMW servers. This service does not require any attributes.

View File

@ -194,7 +194,9 @@ class WebDavCalendarData(object):
@staticmethod @staticmethod
def is_over(vevent): def is_over(vevent):
"""Return if the event is over.""" """Return if the event is over."""
return dt.now() > WebDavCalendarData.get_end_date(vevent) return dt.now() >= WebDavCalendarData.to_datetime(
WebDavCalendarData.get_end_date(vevent)
)
@staticmethod @staticmethod
def get_hass_date(obj): def get_hass_date(obj):
@ -230,4 +232,4 @@ class WebDavCalendarData(object):
else: else:
enddate = obj.dtstart.value + timedelta(days=1) enddate = obj.dtstart.value + timedelta(days=1)
return WebDavCalendarData.to_datetime(enddate) return enddate

View File

@ -11,6 +11,7 @@ from datetime import timedelta
from homeassistant.components.calendar import CalendarEventDevice from homeassistant.components.calendar import CalendarEventDevice
from homeassistant.components.google import ( from homeassistant.components.google import (
CONF_CAL_ID, CONF_ENTITIES, CONF_TRACK, TOKEN_FILE, CONF_CAL_ID, CONF_ENTITIES, CONF_TRACK, TOKEN_FILE,
CONF_IGNORE_AVAILABILITY, CONF_SEARCH,
GoogleCalendarService) GoogleCalendarService)
from homeassistant.util import Throttle, dt from homeassistant.util import Throttle, dt
@ -18,7 +19,7 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_GOOGLE_SEARCH_PARAMS = { DEFAULT_GOOGLE_SEARCH_PARAMS = {
'orderBy': 'startTime', 'orderBy': 'startTime',
'maxResults': 1, 'maxResults': 5,
'singleEvents': True, 'singleEvents': True,
} }
@ -45,24 +46,35 @@ class GoogleCalendarEventDevice(CalendarEventDevice):
def __init__(self, hass, calendar_service, calendar, data): def __init__(self, hass, calendar_service, calendar, data):
"""Create the Calendar event device.""" """Create the Calendar event device."""
self.data = GoogleCalendarData(calendar_service, calendar, self.data = GoogleCalendarData(calendar_service, calendar,
data.get('search', None)) data.get(CONF_SEARCH),
data.get(CONF_IGNORE_AVAILABILITY))
super().__init__(hass, data) super().__init__(hass, data)
class GoogleCalendarData(object): class GoogleCalendarData(object):
"""Class to utilize calendar service object to get next event.""" """Class to utilize calendar service object to get next event."""
def __init__(self, calendar_service, calendar_id, search=None): def __init__(self, calendar_service, calendar_id, search,
ignore_availability):
"""Set up how we are going to search the google calendar.""" """Set up how we are going to search the google calendar."""
self.calendar_service = calendar_service self.calendar_service = calendar_service
self.calendar_id = calendar_id self.calendar_id = calendar_id
self.search = search self.search = search
self.ignore_availability = ignore_availability
self.event = None self.event = None
@Throttle(MIN_TIME_BETWEEN_UPDATES) @Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self): def update(self):
"""Get the latest data.""" """Get the latest data."""
service = self.calendar_service.get() from httplib2 import ServerNotFoundError
try:
service = self.calendar_service.get()
except ServerNotFoundError:
_LOGGER.warning("Unable to connect to Google, using cached data")
return False
params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS) params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS)
params['timeMin'] = dt.now().isoformat('T') params['timeMin'] = dt.now().isoformat('T')
params['calendarId'] = self.calendar_id params['calendarId'] = self.calendar_id
@ -73,5 +85,17 @@ class GoogleCalendarData(object):
result = events.list(**params).execute() result = events.list(**params).execute()
items = result.get('items', []) items = result.get('items', [])
self.event = items[0] if len(items) == 1 else None
new_event = None
for item in items:
if (not self.ignore_availability
and 'transparency' in item.keys()):
if item['transparency'] == 'opaque':
new_event = item
break
else:
new_event = item
break
self.event = new_event
return True return True

View File

@ -1,21 +1,26 @@
# Describes the format for available calendar services # Describes the format for available calendar services
todoist: todoist_new_task:
new_task: description: Create a new task and add it to a project.
description: Create a new task and add it to a project. fields:
fields: content:
content: description: The name of the task.
description: The name of the task (Required). example: Pick up the mail
example: Pick up the mail project:
project: description: The name of the project this task should belong to. Defaults to Inbox.
description: The name of the project this task should belong to. Defaults to Inbox (Optional). example: Errands
example: Errands labels:
labels: description: Any labels that you want to apply to this task, separated by a comma.
description: Any labels that you want to apply to this task, separated by a comma (Optional). example: Chores,Deliveries
example: Chores,Deliveries priority:
priority: description: The priority of this task, from 1 (normal) to 4 (urgent).
description: The priority of this task, from 1 (normal) to 4 (urgent) (Optional). example: 2
example: 2 due_date_string:
due_date: description: The day this task is due, in natural language.
description: The day this task is due, in format YYYY-MM-DD (Optional). example: "tomorrow"
example: "2018-04-01" due_date_lang:
description: The language of due_date_string.
example: "en"
due_date:
description: The day this task is due, in format YYYY-MM-DD.
example: "2018-04-01"

View File

@ -41,6 +41,14 @@ CONTENT = 'content'
DESCRIPTION = 'description' DESCRIPTION = 'description'
# Calendar Platform: Used in the '_get_date()' method # Calendar Platform: Used in the '_get_date()' method
DATETIME = 'dateTime' DATETIME = 'dateTime'
# Service Call: When is this task due (in natural language)?
DUE_DATE_STRING = 'due_date_string'
# Service Call: The language of DUE_DATE_STRING
DUE_DATE_LANG = 'due_date_lang'
# Service Call: The available options of DUE_DATE_LANG
DUE_DATE_VALID_LANGS = ['en', 'da', 'pl', 'zh', 'ko', 'de',
'pt', 'ja', 'it', 'fr', 'sv', 'ru',
'es', 'nl']
# Attribute: When is this task due? # Attribute: When is this task due?
# Service Call: When is this task due? # Service Call: When is this task due?
DUE_DATE = 'due_date' DUE_DATE = 'due_date'
@ -83,7 +91,11 @@ NEW_TASK_SERVICE_SCHEMA = vol.Schema({
vol.Optional(PROJECT_NAME, default='inbox'): vol.All(cv.string, vol.Lower), vol.Optional(PROJECT_NAME, default='inbox'): vol.All(cv.string, vol.Lower),
vol.Optional(LABELS): cv.ensure_list_csv, vol.Optional(LABELS): cv.ensure_list_csv,
vol.Optional(PRIORITY): vol.All(vol.Coerce(int), vol.Range(min=1, max=4)), vol.Optional(PRIORITY): vol.All(vol.Coerce(int), vol.Range(min=1, max=4)),
vol.Optional(DUE_DATE): cv.string,
vol.Exclusive(DUE_DATE_STRING, 'due_date'): cv.string,
vol.Optional(DUE_DATE_LANG):
vol.All(cv.string, vol.In(DUE_DATE_VALID_LANGS)),
vol.Exclusive(DUE_DATE, 'due_date'): cv.string,
}) })
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@ -186,6 +198,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if PRIORITY in call.data: if PRIORITY in call.data:
item.update(priority=call.data[PRIORITY]) item.update(priority=call.data[PRIORITY])
if DUE_DATE_STRING in call.data:
item.update(date_string=call.data[DUE_DATE_STRING])
if DUE_DATE_LANG in call.data:
item.update(date_lang=call.data[DUE_DATE_LANG])
if DUE_DATE in call.data: if DUE_DATE in call.data:
due_date = dt.parse_datetime(call.data[DUE_DATE]) due_date = dt.parse_datetime(call.data[DUE_DATE])
if due_date is None: if due_date is None:
@ -496,6 +514,10 @@ class TodoistProjectData(object):
# We had no valid tasks # We had no valid tasks
return True return True
# Make sure the task collection is reset to prevent an
# infinite collection repeating the same tasks
self.all_project_tasks.clear()
# Organize the best tasks (so users can see all the tasks # Organize the best tasks (so users can see all the tasks
# they have, organized) # they have, organized)
while project_tasks: while project_tasks:

View File

@ -6,6 +6,7 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/camera/ https://home-assistant.io/components/camera/
""" """
import asyncio import asyncio
import base64
import collections import collections
from contextlib import suppress from contextlib import suppress
from datetime import timedelta from datetime import timedelta
@ -13,20 +14,20 @@ import logging
import hashlib import hashlib
from random import SystemRandom from random import SystemRandom
import aiohttp import attr
from aiohttp import web from aiohttp import web
import async_timeout import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.const import (ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE) from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED
from homeassistant.components import websocket_api
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
DOMAIN = 'camera' DOMAIN = 'camera'
@ -53,6 +54,9 @@ ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
TOKEN_CHANGE_INTERVAL = timedelta(minutes=5) TOKEN_CHANGE_INTERVAL = timedelta(minutes=5)
_RND = SystemRandom() _RND = SystemRandom()
FALLBACK_STREAM_INTERVAL = 1 # seconds
MIN_STREAM_INTERVAL = 0.5 # seconds
CAMERA_SERVICE_SCHEMA = vol.Schema({ CAMERA_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
}) })
@ -61,6 +65,20 @@ CAMERA_SERVICE_SNAPSHOT = CAMERA_SERVICE_SCHEMA.extend({
vol.Required(ATTR_FILENAME): cv.template vol.Required(ATTR_FILENAME): cv.template
}) })
WS_TYPE_CAMERA_THUMBNAIL = 'camera_thumbnail'
SCHEMA_WS_CAMERA_THUMBNAIL = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
'type': WS_TYPE_CAMERA_THUMBNAIL,
'entity_id': cv.entity_id
})
@attr.s
class Image:
"""Represent an image."""
content_type = attr.ib(type=str)
content = attr.ib(type=bytes)
@bind_hass @bind_hass
def enable_motion_detection(hass, entity_id=None): def enable_motion_detection(hass, entity_id=None):
@ -89,43 +107,40 @@ def async_snapshot(hass, filename, entity_id=None):
@bind_hass @bind_hass
@asyncio.coroutine async def async_get_image(hass, entity_id, timeout=10):
def async_get_image(hass, entity_id, timeout=10):
"""Fetch an image from a camera entity.""" """Fetch an image from a camera entity."""
websession = async_get_clientsession(hass) component = hass.data.get(DOMAIN)
state = hass.states.get(entity_id)
if state is None: if component is None:
raise HomeAssistantError( raise HomeAssistantError('Camera component not setup')
"No entity '{0}' for grab an image".format(entity_id))
url = "{0}{1}".format( camera = component.get_entity(entity_id)
hass.config.api.base_url,
state.attributes.get(ATTR_ENTITY_PICTURE)
)
try: if camera is None:
raise HomeAssistantError('Camera not found')
with suppress(asyncio.CancelledError, asyncio.TimeoutError):
with async_timeout.timeout(timeout, loop=hass.loop): with async_timeout.timeout(timeout, loop=hass.loop):
response = yield from websession.get(url) image = await camera.async_camera_image()
if response.status != 200: if image:
raise HomeAssistantError("Error {0} on {1}".format( return Image(camera.content_type, image)
response.status, url))
image = yield from response.read() raise HomeAssistantError('Unable to get image')
return image
except (asyncio.TimeoutError, aiohttp.ClientError):
raise HomeAssistantError("Can't connect to {0}".format(url))
@asyncio.coroutine @asyncio.coroutine
def async_setup(hass, config): def async_setup(hass, config):
"""Set up the camera component.""" """Set up the camera component."""
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) component = hass.data[DOMAIN] = \
EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
hass.http.register_view(CameraImageView(component)) hass.http.register_view(CameraImageView(component))
hass.http.register_view(CameraMjpegStream(component)) hass.http.register_view(CameraMjpegStream(component))
hass.components.websocket_api.async_register_command(
WS_TYPE_CAMERA_THUMBNAIL, websocket_camera_thumbnail,
SCHEMA_WS_CAMERA_THUMBNAIL
)
yield from component.async_setup(config) yield from component.async_setup(config)
@ -241,6 +256,11 @@ class Camera(Entity):
"""Return the camera model.""" """Return the camera model."""
return None return None
@property
def frame_interval(self):
"""Return the interval between frames of the mjpeg stream."""
return 0.5
def camera_image(self): def camera_image(self):
"""Return bytes of camera image.""" """Return bytes of camera image."""
raise NotImplementedError() raise NotImplementedError()
@ -252,19 +272,17 @@ class Camera(Entity):
""" """
return self.hass.async_add_job(self.camera_image) return self.hass.async_add_job(self.camera_image)
@asyncio.coroutine async def handle_async_still_stream(self, request, interval):
def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from camera images. """Generate an HTTP MJPEG stream from camera images.
This method must be run in the event loop. This method must be run in the event loop.
""" """
response = web.StreamResponse() response = web.StreamResponse()
response.content_type = ('multipart/x-mixed-replace; ' response.content_type = ('multipart/x-mixed-replace; '
'boundary=--frameboundary') 'boundary=--frameboundary')
yield from response.prepare(request) await response.prepare(request)
async def write(img_bytes): async def write_to_mjpeg_stream(img_bytes):
"""Write image to stream.""" """Write image to stream."""
await response.write(bytes( await response.write(bytes(
'--frameboundary\r\n' '--frameboundary\r\n'
@ -277,21 +295,21 @@ class Camera(Entity):
try: try:
while True: while True:
img_bytes = yield from self.async_camera_image() img_bytes = await self.async_camera_image()
if not img_bytes: if not img_bytes:
break break
if img_bytes and img_bytes != last_image: if img_bytes and img_bytes != last_image:
yield from write(img_bytes) await write_to_mjpeg_stream(img_bytes)
# Chrome seems to always ignore first picture, # Chrome seems to always ignore first picture,
# print it twice. # print it twice.
if last_image is None: if last_image is None:
yield from write(img_bytes) await write_to_mjpeg_stream(img_bytes)
last_image = img_bytes last_image = img_bytes
yield from asyncio.sleep(.5) await asyncio.sleep(interval)
except asyncio.CancelledError: except asyncio.CancelledError:
_LOGGER.debug("Stream closed by frontend.") _LOGGER.debug("Stream closed by frontend.")
@ -299,7 +317,16 @@ class Camera(Entity):
finally: finally:
if response is not None: if response is not None:
yield from response.write_eof() await response.write_eof()
async def handle_async_mjpeg_stream(self, request):
"""Serve an HTTP MJPEG stream from the camera.
This method can be overridden by camera plaforms to proxy
a direct stream from the camera.
This method must be run in the event loop.
"""
await self.handle_async_still_stream(request, self.frame_interval)
@property @property
def state(self): def state(self):
@ -329,20 +356,20 @@ class Camera(Entity):
@property @property
def state_attributes(self): def state_attributes(self):
"""Return the camera state attributes.""" """Return the camera state attributes."""
attr = { attrs = {
'access_token': self.access_tokens[-1], 'access_token': self.access_tokens[-1],
} }
if self.model: if self.model:
attr['model_name'] = self.model attrs['model_name'] = self.model
if self.brand: if self.brand:
attr['brand'] = self.brand attrs['brand'] = self.brand
if self.motion_detection_enabled: if self.motion_detection_enabled:
attr['motion_detection'] = self.motion_detection_enabled attrs['motion_detection'] = self.motion_detection_enabled
return attr return attrs
@callback @callback
def async_update_token(self): def async_update_token(self):
@ -411,7 +438,43 @@ class CameraMjpegStream(CameraView):
url = '/api/camera_proxy_stream/{entity_id}' url = '/api/camera_proxy_stream/{entity_id}'
name = 'api:camera:stream' name = 'api:camera:stream'
@asyncio.coroutine async def handle(self, request, camera):
def handle(self, request, camera): """Serve camera stream, possibly with interval."""
"""Serve camera image.""" interval = request.query.get('interval')
yield from camera.handle_async_mjpeg_stream(request) if interval is None:
await camera.handle_async_mjpeg_stream(request)
return
try:
# Compose camera stream from stills
interval = float(request.query.get('interval'))
if interval < MIN_STREAM_INTERVAL:
raise ValueError("Stream interval must be be > {}"
.format(MIN_STREAM_INTERVAL))
await camera.handle_async_still_stream(request, interval)
return
except ValueError:
return web.Response(status=400)
@callback
def websocket_camera_thumbnail(hass, connection, msg):
"""Handle get camera thumbnail websocket command.
Async friendly.
"""
async def send_camera_still():
"""Send a camera still."""
try:
image = await async_get_image(hass, msg['entity_id'])
connection.send_message_outside(websocket_api.result_message(
msg['id'], {
'content_type': image.content_type,
'content': base64.b64encode(image.content).decode('utf-8')
}
))
except HomeAssistantError:
connection.send_message_outside(websocket_api.error_message(
msg['id'], 'image_fetch_failed', 'Unable to fetch image'))
hass.async_add_job(send_camera_still())

View File

@ -9,7 +9,6 @@ import logging
import requests import requests
from homeassistant.components.camera import Camera from homeassistant.components.camera import Camera
from homeassistant.loader import get_component
DEPENDENCIES = ['bloomsky'] DEPENDENCIES = ['bloomsky']
@ -17,7 +16,7 @@ DEPENDENCIES = ['bloomsky']
# pylint: disable=unused-argument # pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up access to BloomSky cameras.""" """Set up access to BloomSky cameras."""
bloomsky = get_component('bloomsky') bloomsky = hass.components.bloomsky
for device in bloomsky.BLOOMSKY.devices.values(): for device in bloomsky.BLOOMSKY.devices.values():
add_devices([BloomSkyCamera(bloomsky.BLOOMSKY, device)]) add_devices([BloomSkyCamera(bloomsky.BLOOMSKY, device)])

View File

@ -0,0 +1,58 @@
"""
Family Hub camera for Samsung Refrigerators.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/camera.familyhub/
"""
import logging
import voluptuous as vol
from homeassistant.components.camera import Camera
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import CONF_IP_ADDRESS, CONF_NAME
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['python-family-hub-local==0.0.2']
DEFAULT_NAME = 'FamilyHub Camera'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_IP_ADDRESS): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
async def async_setup_platform(
hass, config, async_add_devices, discovery_info=None):
"""Set up the Family Hub Camera."""
from pyfamilyhublocal import FamilyHubCam
address = config.get(CONF_IP_ADDRESS)
name = config.get(CONF_NAME)
session = async_get_clientsession(hass)
family_hub_cam = FamilyHubCam(address, hass.loop, session)
async_add_devices([FamilyHubCamera(name, family_hub_cam)], True)
class FamilyHubCamera(Camera):
"""The representation of a Family Hub camera."""
def __init__(self, name, family_hub_cam):
"""Initialize camera component."""
super().__init__()
self._name = name
self.family_hub_cam = family_hub_cam
async def async_camera_image(self):
"""Return a still image response."""
return await self.family_hub_cam.async_get_cam_image()
@property
def name(self):
"""Return the name of this camera."""
return self._name

View File

@ -28,6 +28,7 @@ _LOGGER = logging.getLogger(__name__)
CONF_CONTENT_TYPE = 'content_type' CONF_CONTENT_TYPE = 'content_type'
CONF_LIMIT_REFETCH_TO_URL_CHANGE = 'limit_refetch_to_url_change' CONF_LIMIT_REFETCH_TO_URL_CHANGE = 'limit_refetch_to_url_change'
CONF_STILL_IMAGE_URL = 'still_image_url' CONF_STILL_IMAGE_URL = 'still_image_url'
CONF_FRAMERATE = 'framerate'
DEFAULT_NAME = 'Generic Camera' DEFAULT_NAME = 'Generic Camera'
@ -40,6 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_PASSWORD): cv.string, vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_CONTENT_TYPE, default=DEFAULT_CONTENT_TYPE): cv.string, vol.Optional(CONF_CONTENT_TYPE, default=DEFAULT_CONTENT_TYPE): cv.string,
vol.Optional(CONF_FRAMERATE, default=2): cv.positive_int,
}) })
@ -62,6 +64,7 @@ class GenericCamera(Camera):
self._still_image_url = device_info[CONF_STILL_IMAGE_URL] self._still_image_url = device_info[CONF_STILL_IMAGE_URL]
self._still_image_url.hass = hass self._still_image_url.hass = hass
self._limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE] self._limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE]
self._frame_interval = 1 / device_info[CONF_FRAMERATE]
self.content_type = device_info[CONF_CONTENT_TYPE] self.content_type = device_info[CONF_CONTENT_TYPE]
username = device_info.get(CONF_USERNAME) username = device_info.get(CONF_USERNAME)
@ -78,6 +81,11 @@ class GenericCamera(Camera):
self._last_url = None self._last_url = None
self._last_image = None self._last_image = None
@property
def frame_interval(self):
"""Return the interval between frames of the mjpeg stream."""
return self._frame_interval
def camera_image(self): def camera_image(self):
"""Return bytes of camera image.""" """Return bytes of camera image."""
return run_coroutine_threadsafe( return run_coroutine_threadsafe(

View File

@ -11,31 +11,44 @@ import os
import voluptuous as vol import voluptuous as vol
from homeassistant.const import CONF_NAME from homeassistant.const import CONF_NAME
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA from homeassistant.components.camera import (
Camera, CAMERA_SERVICE_SCHEMA, DOMAIN, PLATFORM_SCHEMA)
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_FILE_PATH = 'file_path' CONF_FILE_PATH = 'file_path'
DEFAULT_NAME = 'Local File' DEFAULT_NAME = 'Local File'
SERVICE_UPDATE_FILE_PATH = 'local_file_update_file_path'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_FILE_PATH): cv.string, vol.Required(CONF_FILE_PATH): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string
}) })
CAMERA_SERVICE_UPDATE_FILE_PATH = CAMERA_SERVICE_SCHEMA.extend({
vol.Required(CONF_FILE_PATH): cv.string
})
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Camera that works with local files.""" """Set up the Camera that works with local files."""
file_path = config[CONF_FILE_PATH] file_path = config[CONF_FILE_PATH]
camera = LocalFile(config[CONF_NAME], file_path)
# check filepath given is readable def update_file_path_service(call):
if not os.access(file_path, os.R_OK): """Update the file path."""
_LOGGER.warning("Could not read camera %s image from file: %s", file_path = call.data.get(CONF_FILE_PATH)
config[CONF_NAME], file_path) camera.update_file_path(file_path)
return True
add_devices([LocalFile(config[CONF_NAME], file_path)]) hass.services.register(
DOMAIN,
SERVICE_UPDATE_FILE_PATH,
update_file_path_service,
schema=CAMERA_SERVICE_UPDATE_FILE_PATH)
add_devices([camera])
class LocalFile(Camera): class LocalFile(Camera):
@ -46,6 +59,7 @@ class LocalFile(Camera):
super().__init__() super().__init__()
self._name = name self._name = name
self.check_file_path_access(file_path)
self._file_path = file_path self._file_path = file_path
# Set content type of local file # Set content type of local file
content, _ = mimetypes.guess_type(file_path) content, _ = mimetypes.guess_type(file_path)
@ -61,7 +75,26 @@ class LocalFile(Camera):
_LOGGER.warning("Could not read camera %s image from file: %s", _LOGGER.warning("Could not read camera %s image from file: %s",
self._name, self._file_path) self._name, self._file_path)
def check_file_path_access(self, file_path):
"""Check that filepath given is readable."""
if not os.access(file_path, os.R_OK):
_LOGGER.warning("Could not read camera %s image from file: %s",
self._name, file_path)
def update_file_path(self, file_path):
"""Update the file_path."""
self.check_file_path_access(file_path)
self._file_path = file_path
self.schedule_update_ha_state()
@property @property
def name(self): def name(self):
"""Return the name of this camera.""" """Return the name of this camera."""
return self._name return self._name
@property
def device_state_attributes(self):
"""Return the camera state attributes."""
return {
'file_path': self._file_path,
}

View File

@ -19,7 +19,6 @@ from homeassistant.helpers import config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_TOPIC = 'topic' CONF_TOPIC = 'topic'
DEFAULT_NAME = 'MQTT Camera' DEFAULT_NAME = 'MQTT Camera'
DEPENDENCIES = ['mqtt'] DEPENDENCIES = ['mqtt']
@ -33,9 +32,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@asyncio.coroutine @asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the MQTT Camera.""" """Set up the MQTT Camera."""
topic = config[CONF_TOPIC] if discovery_info is not None:
config = PLATFORM_SCHEMA(discovery_info)
async_add_devices([MqttCamera(config[CONF_NAME], topic)]) async_add_devices([MqttCamera(
config.get(CONF_NAME),
config.get(CONF_TOPIC)
)])
class MqttCamera(Camera): class MqttCamera(Camera):

View File

@ -12,7 +12,6 @@ import voluptuous as vol
from homeassistant.const import CONF_VERIFY_SSL from homeassistant.const import CONF_VERIFY_SSL
from homeassistant.components.netatmo import CameraData from homeassistant.components.netatmo import CameraData
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA) from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
from homeassistant.loader import get_component
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
DEPENDENCIES = ['netatmo'] DEPENDENCIES = ['netatmo']
@ -33,7 +32,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
# pylint: disable=unused-argument # pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up access to Netatmo cameras.""" """Set up access to Netatmo cameras."""
netatmo = get_component('netatmo') netatmo = hass.components.netatmo
home = config.get(CONF_HOME) home = config.get(CONF_HOME)
verify_ssl = config.get(CONF_VERIFY_SSL, True) verify_ssl = config.get(CONF_VERIFY_SSL, True)
import lnetatmo import lnetatmo

View File

@ -6,6 +6,7 @@ https://home-assistant.io/components/camera.onvif/
""" """
import asyncio import asyncio
import logging import logging
import os
import voluptuous as vol import voluptuous as vol
@ -103,92 +104,128 @@ class ONVIFHassCamera(Camera):
def __init__(self, hass, config): def __init__(self, hass, config):
"""Initialize a ONVIF camera.""" """Initialize a ONVIF camera."""
from onvif import ONVIFCamera, exceptions
super().__init__() super().__init__()
import onvif
self._username = config.get(CONF_USERNAME)
self._password = config.get(CONF_PASSWORD)
self._host = config.get(CONF_HOST)
self._port = config.get(CONF_PORT)
self._name = config.get(CONF_NAME) self._name = config.get(CONF_NAME)
self._ffmpeg_arguments = config.get(CONF_EXTRA_ARGUMENTS) self._ffmpeg_arguments = config.get(CONF_EXTRA_ARGUMENTS)
self._profile_index = config.get(CONF_PROFILE)
self._input = None self._input = None
camera = None self._media_service = \
onvif.ONVIFService('http://{}:{}/onvif/device_service'.format(
self._host, self._port),
self._username, self._password,
'{}/wsdl/media.wsdl'.format(os.path.dirname(
onvif.__file__)))
self._ptz_service = \
onvif.ONVIFService('http://{}:{}/onvif/device_service'.format(
self._host, self._port),
self._username, self._password,
'{}/wsdl/ptz.wsdl'.format(os.path.dirname(
onvif.__file__)))
def obtain_input_uri(self):
"""Set the input uri for the camera."""
from onvif import exceptions
_LOGGER.debug("Connecting with ONVIF Camera: %s on port %s",
self._host, self._port)
try: try:
_LOGGER.debug("Connecting with ONVIF Camera: %s on port %s", profiles = self._media_service.GetProfiles()
config.get(CONF_HOST), config.get(CONF_PORT))
camera = ONVIFCamera( if self._profile_index >= len(profiles):
config.get(CONF_HOST), config.get(CONF_PORT),
config.get(CONF_USERNAME), config.get(CONF_PASSWORD)
)
media_service = camera.create_media_service()
self._profiles = media_service.GetProfiles()
self._profile_index = config.get(CONF_PROFILE)
if self._profile_index >= len(self._profiles):
_LOGGER.warning("ONVIF Camera '%s' doesn't provide profile %d." _LOGGER.warning("ONVIF Camera '%s' doesn't provide profile %d."
" Using the last profile.", " Using the last profile.",
self._name, self._profile_index) self._name, self._profile_index)
self._profile_index = -1 self._profile_index = -1
req = media_service.create_type('GetStreamUri')
req = self._media_service.create_type('GetStreamUri')
# pylint: disable=protected-access # pylint: disable=protected-access
req.ProfileToken = self._profiles[self._profile_index]._token req.ProfileToken = profiles[self._profile_index]._token
self._input = media_service.GetStreamUri(req).Uri.replace( uri_no_auth = self._media_service.GetStreamUri(req).Uri
'rtsp://', 'rtsp://{}:{}@'.format( uri_for_log = uri_no_auth.replace(
config.get(CONF_USERNAME), 'rtsp://', 'rtsp://<user>:<password>@', 1)
config.get(CONF_PASSWORD)), 1) self._input = uri_no_auth.replace(
'rtsp://', 'rtsp://{}:{}@'.format(self._username,
self._password), 1)
_LOGGER.debug( _LOGGER.debug(
"ONVIF Camera Using the following URL for %s: %s", "ONVIF Camera Using the following URL for %s: %s",
self._name, self._input) self._name, uri_for_log)
except Exception as err: # we won't need the media service anymore
_LOGGER.error("Unable to communicate with ONVIF Camera: %s", err) self._media_service = None
raise
try:
self._ptz = camera.create_ptz_service()
except exceptions.ONVIFError as err: except exceptions.ONVIFError as err:
self._ptz = None _LOGGER.debug("Couldn't setup camera '%s'. Error: %s",
_LOGGER.warning("Unable to setup PTZ for ONVIF Camera: %s", err) self._name, err)
return
def perform_ptz(self, pan, tilt, zoom): def perform_ptz(self, pan, tilt, zoom):
"""Perform a PTZ action on the camera.""" """Perform a PTZ action on the camera."""
if self._ptz: from onvif import exceptions
if self._ptz_service:
pan_val = 1 if pan == DIR_RIGHT else -1 if pan == DIR_LEFT else 0 pan_val = 1 if pan == DIR_RIGHT else -1 if pan == DIR_LEFT else 0
tilt_val = 1 if tilt == DIR_UP else -1 if tilt == DIR_DOWN else 0 tilt_val = 1 if tilt == DIR_UP else -1 if tilt == DIR_DOWN else 0
zoom_val = 1 if zoom == ZOOM_IN else -1 if zoom == ZOOM_OUT else 0 zoom_val = 1 if zoom == ZOOM_IN else -1 if zoom == ZOOM_OUT else 0
req = {"Velocity": { req = {"Velocity": {
"PanTilt": {"_x": pan_val, "_y": tilt_val}, "PanTilt": {"_x": pan_val, "_y": tilt_val},
"Zoom": {"_x": zoom_val}}} "Zoom": {"_x": zoom_val}}}
self._ptz.ContinuousMove(req) try:
self._ptz_service.ContinuousMove(req)
except exceptions.ONVIFError as err:
if "Bad Request" in err.reason:
self._ptz_service = None
_LOGGER.debug("Camera '%s' doesn't support PTZ.",
self._name)
else:
_LOGGER.debug("Camera '%s' doesn't support PTZ.", self._name)
@asyncio.coroutine async def async_added_to_hass(self):
def async_added_to_hass(self):
"""Callback when entity is added to hass.""" """Callback when entity is added to hass."""
if ONVIF_DATA not in self.hass.data: if ONVIF_DATA not in self.hass.data:
self.hass.data[ONVIF_DATA] = {} self.hass.data[ONVIF_DATA] = {}
self.hass.data[ONVIF_DATA][ENTITIES] = [] self.hass.data[ONVIF_DATA][ENTITIES] = []
self.hass.data[ONVIF_DATA][ENTITIES].append(self) self.hass.data[ONVIF_DATA][ENTITIES].append(self)
@asyncio.coroutine async def async_camera_image(self):
def async_camera_image(self):
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
from haffmpeg import ImageFrame, IMAGE_JPEG from haffmpeg import ImageFrame, IMAGE_JPEG
if not self._input:
await self.hass.async_add_job(self.obtain_input_uri)
if not self._input:
return None
ffmpeg = ImageFrame( ffmpeg = ImageFrame(
self.hass.data[DATA_FFMPEG].binary, loop=self.hass.loop) self.hass.data[DATA_FFMPEG].binary, loop=self.hass.loop)
image = yield from asyncio.shield(ffmpeg.get_image( image = await asyncio.shield(ffmpeg.get_image(
self._input, output_format=IMAGE_JPEG, self._input, output_format=IMAGE_JPEG,
extra_cmd=self._ffmpeg_arguments), loop=self.hass.loop) extra_cmd=self._ffmpeg_arguments), loop=self.hass.loop)
return image return image
@asyncio.coroutine async def handle_async_mjpeg_stream(self, request):
def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from the camera.""" """Generate an HTTP MJPEG stream from the camera."""
from haffmpeg import CameraMjpeg from haffmpeg import CameraMjpeg
if not self._input:
await self.hass.async_add_job(self.obtain_input_uri)
if not self._input:
return None
stream = CameraMjpeg(self.hass.data[DATA_FFMPEG].binary, stream = CameraMjpeg(self.hass.data[DATA_FFMPEG].binary,
loop=self.hass.loop) loop=self.hass.loop)
yield from stream.open_camera( await stream.open_camera(
self._input, extra_cmd=self._ffmpeg_arguments) self._input, extra_cmd=self._ffmpeg_arguments)
yield from async_aiohttp_proxy_stream( await async_aiohttp_proxy_stream(
self.hass, request, stream, self.hass, request, stream,
'multipart/x-mixed-replace;boundary=ffserver') 'multipart/x-mixed-replace;boundary=ffserver')
yield from stream.close() await stream.close()
@property @property
def name(self): def name(self):

View File

@ -56,34 +56,6 @@ async def async_setup_platform(hass, config, async_add_devices,
async_add_devices([ProxyCamera(hass, config)]) async_add_devices([ProxyCamera(hass, config)])
async def _read_frame(req):
"""Read a single frame from an MJPEG stream."""
# based on https://gist.github.com/russss/1143799
import cgi
# Read in HTTP headers:
stream = req.content
# multipart/x-mixed-replace; boundary=--frameboundary
_mimetype, options = cgi.parse_header(req.headers['content-type'])
boundary = options.get('boundary').encode('utf-8')
if not boundary:
_LOGGER.error("Malformed MJPEG missing boundary")
raise Exception("Can't find content-type")
line = await stream.readline()
# Seek ahead to the first chunk
while line.strip() != boundary:
line = await stream.readline()
# Read in chunk headers
while line.strip() != b'':
parts = line.split(b':')
if len(parts) > 1 and parts[0].lower() == b'content-length':
# Grab chunk length
length = int(parts[1].strip())
line = await stream.readline()
image = await stream.read(length)
return image
def _resize_image(image, opts): def _resize_image(image, opts):
"""Resize image.""" """Resize image."""
from PIL import Image from PIL import Image
@ -227,9 +199,9 @@ class ProxyCamera(Camera):
'boundary=--frameboundary') 'boundary=--frameboundary')
await response.prepare(request) await response.prepare(request)
def write(img_bytes): async def write(img_bytes):
"""Write image to stream.""" """Write image to stream."""
response.write(bytes( await response.write(bytes(
'--frameboundary\r\n' '--frameboundary\r\n'
'Content-Type: {}\r\n' 'Content-Type: {}\r\n'
'Content-Length: {}\r\n\r\n'.format( 'Content-Length: {}\r\n\r\n'.format(
@ -240,13 +212,23 @@ class ProxyCamera(Camera):
req = await stream_coro req = await stream_coro
try: try:
# This would be nicer as an async generator
# But that would only be supported for python >=3.6
data = b''
stream = req.content
while True: while True:
image = await _read_frame(req) chunk = await stream.read(102400)
if not image: if not chunk:
break break
image = await self.hass.async_add_job( data += chunk
_resize_image, image, self._stream_opts) jpg_start = data.find(b'\xff\xd8')
write(image) jpg_end = data.find(b'\xff\xd9')
if jpg_start != -1 and jpg_end != -1:
image = data[jpg_start:jpg_end + 2]
image = await self.hass.async_add_job(
_resize_image, image, self._stream_opts)
await write(image)
data = data[jpg_end + 2:]
except asyncio.CancelledError: except asyncio.CancelledError:
_LOGGER.debug("Stream closed by frontend.") _LOGGER.debug("Stream closed by frontend.")
req.close() req.close()

View File

@ -24,6 +24,16 @@ snapshot:
description: Template of a Filename. Variable is entity_id. description: Template of a Filename. Variable is entity_id.
example: '/tmp/snapshot_{{ entity_id }}' example: '/tmp/snapshot_{{ entity_id }}'
local_file_update_file_path:
description: Update the file_path for a local_file camera.
fields:
entity_id:
description: Name(s) of entities to update.
example: 'camera.local_file'
file_path:
description: Path to the new image file.
example: '/images/newimage.jpg'
onvif_ptz: onvif_ptz:
description: Pan/Tilt/Zoom service for ONVIF camera. description: Pan/Tilt/Zoom service for ONVIF camera.
fields: fields:
@ -39,4 +49,3 @@ onvif_ptz:
zoom: zoom:
description: "Zoom. Allowed values: ZOOM_IN, ZOOM_OUT" description: "Zoom. Allowed values: ZOOM_IN, ZOOM_OUT"
example: "ZOOM_IN" example: "ZOOM_IN"

View File

@ -4,7 +4,6 @@ Support for Xeoma Cameras.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.xeoma/ https://home-assistant.io/components/camera.xeoma/
""" """
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
@ -14,7 +13,7 @@ from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME) CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME)
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
REQUIREMENTS = ['pyxeoma==1.3'] REQUIREMENTS = ['pyxeoma==1.4.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -41,8 +40,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
}) })
@asyncio.coroutine async def async_setup_platform(hass, config, async_add_devices,
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): discovery_info=None):
"""Discover and setup Xeoma Cameras.""" """Discover and setup Xeoma Cameras."""
from pyxeoma.xeoma import Xeoma, XeomaError from pyxeoma.xeoma import Xeoma, XeomaError
@ -53,8 +52,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
xeoma = Xeoma(host, login, password) xeoma = Xeoma(host, login, password)
try: try:
yield from xeoma.async_test_connection() await xeoma.async_test_connection()
discovered_image_names = yield from xeoma.async_get_image_names() discovered_image_names = await xeoma.async_get_image_names()
discovered_cameras = [ discovered_cameras = [
{ {
CONF_IMAGE_NAME: image_name, CONF_IMAGE_NAME: image_name,
@ -103,12 +102,11 @@ class XeomaCamera(Camera):
self._password = password self._password = password
self._last_image = None self._last_image = None
@asyncio.coroutine async def async_camera_image(self):
def async_camera_image(self):
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
from pyxeoma.xeoma import XeomaError from pyxeoma.xeoma import XeomaError
try: try:
image = yield from self._xeoma.async_get_camera_image( image = await self._xeoma.async_get_camera_image(
self._image, self._username, self._password) self._image, self._username, self._password)
self._last_image = image self._last_image = image
except XeomaError as err: except XeomaError as err:

View File

@ -15,7 +15,7 @@ from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
from homeassistant.util import Throttle from homeassistant.util import Throttle
REQUIREMENTS = ['py-canary==0.4.1'] REQUIREMENTS = ['py-canary==0.5.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -22,6 +22,12 @@ from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_TURN_ON, SERVICE_TURN_OFF,
STATE_ON, STATE_OFF, STATE_UNKNOWN, TEMP_CELSIUS, PRECISION_WHOLE, STATE_ON, STATE_OFF, STATE_UNKNOWN, TEMP_CELSIUS, PRECISION_WHOLE,
PRECISION_TENTHS, ) PRECISION_TENTHS, )
DEFAULT_MIN_TEMP = 7
DEFAULT_MAX_TEMP = 35
DEFAULT_MIN_HUMITIDY = 30
DEFAULT_MAX_HUMIDITY = 99
DOMAIN = 'climate' DOMAIN = 'climate'
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'
@ -40,6 +46,7 @@ STATE_HEAT = 'heat'
STATE_COOL = 'cool' STATE_COOL = 'cool'
STATE_IDLE = 'idle' STATE_IDLE = 'idle'
STATE_AUTO = 'auto' STATE_AUTO = 'auto'
STATE_MANUAL = 'manual'
STATE_DRY = 'dry' STATE_DRY = 'dry'
STATE_FAN_ONLY = 'fan_only' STATE_FAN_ONLY = 'fan_only'
STATE_ECO = 'eco' STATE_ECO = 'eco'
@ -777,19 +784,21 @@ class ClimateDevice(Entity):
@property @property
def min_temp(self): def min_temp(self):
"""Return the minimum temperature.""" """Return the minimum temperature."""
return convert_temperature(7, TEMP_CELSIUS, self.temperature_unit) return convert_temperature(DEFAULT_MIN_TEMP, TEMP_CELSIUS,
self.temperature_unit)
@property @property
def max_temp(self): def max_temp(self):
"""Return the maximum temperature.""" """Return the maximum temperature."""
return convert_temperature(35, TEMP_CELSIUS, self.temperature_unit) return convert_temperature(DEFAULT_MAX_TEMP, TEMP_CELSIUS,
self.temperature_unit)
@property @property
def min_humidity(self): def min_humidity(self):
"""Return the minimum humidity.""" """Return the minimum humidity."""
return 30 return DEFAULT_MIN_HUMITIDY
@property @property
def max_humidity(self): def max_humidity(self):
"""Return the maximum humidity.""" """Return the maximum humidity."""
return 99 return DEFAULT_MAX_HUMIDITY

View File

@ -14,10 +14,10 @@ from homeassistant.components.climate import (
ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, SUPPORT_TARGET_TEMPERATURE, ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE,
SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_TARGET_HUMIDITY_HIGH, SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_TARGET_HUMIDITY_HIGH,
SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_FAN_MODE,
SUPPORT_TARGET_TEMPERATURE_LOW) SUPPORT_TARGET_TEMPERATURE_LOW, STATE_OFF)
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, STATE_OFF, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT) ATTR_ENTITY_ID, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
_CONFIGURING = {} _CONFIGURING = {}
@ -50,7 +50,7 @@ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE |
SUPPORT_HOLD_MODE | SUPPORT_OPERATION_MODE | SUPPORT_HOLD_MODE | SUPPORT_OPERATION_MODE |
SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH | SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH |
SUPPORT_AUX_HEAT | SUPPORT_TARGET_TEMPERATURE_HIGH | SUPPORT_AUX_HEAT | SUPPORT_TARGET_TEMPERATURE_HIGH |
SUPPORT_TARGET_TEMPERATURE_LOW) SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_FAN_MODE)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
@ -122,6 +122,7 @@ class Thermostat(ClimateDevice):
self._climate_list = self.climate_list self._climate_list = self.climate_list
self._operation_list = ['auto', 'auxHeatOnly', 'cool', self._operation_list = ['auto', 'auxHeatOnly', 'cool',
'heat', 'off'] 'heat', 'off']
self._fan_list = ['auto', 'on']
self.update_without_throttle = False self.update_without_throttle = False
def update(self): def update(self):
@ -180,24 +181,29 @@ class Thermostat(ClimateDevice):
return self.thermostat['runtime']['desiredCool'] / 10.0 return self.thermostat['runtime']['desiredCool'] / 10.0
return None return None
@property
def desired_fan_mode(self):
"""Return the desired fan mode of operation."""
return self.thermostat['runtime']['desiredFanMode']
@property @property
def fan(self): def fan(self):
"""Return the current fan state.""" """Return the current fan status."""
if 'fan' in self.thermostat['equipmentStatus']: if 'fan' in self.thermostat['equipmentStatus']:
return STATE_ON return STATE_ON
return STATE_OFF return STATE_OFF
@property
def current_fan_mode(self):
"""Return the fan setting."""
return self.thermostat['runtime']['desiredFanMode']
@property @property
def current_hold_mode(self): def current_hold_mode(self):
"""Return current hold mode.""" """Return current hold mode."""
mode = self._current_hold_mode mode = self._current_hold_mode
return None if mode == AWAY_MODE else mode return None if mode == AWAY_MODE else mode
@property
def fan_list(self):
"""Return the available fan modes."""
return self._fan_list
@property @property
def _current_hold_mode(self): def _current_hold_mode(self):
events = self.thermostat['events'] events = self.thermostat['events']
@ -206,7 +212,7 @@ class Thermostat(ClimateDevice):
if event['type'] == 'hold': if event['type'] == 'hold':
if event['holdClimateRef'] == 'away': if event['holdClimateRef'] == 'away':
if int(event['endDate'][0:4]) - \ if int(event['endDate'][0:4]) - \
int(event['startDate'][0:4]) <= 1: int(event['startDate'][0:4]) <= 1:
# A temporary hold from away climate is a hold # A temporary hold from away climate is a hold
return 'away' return 'away'
# A permanent hold from away climate # A permanent hold from away climate
@ -228,7 +234,7 @@ class Thermostat(ClimateDevice):
def current_operation(self): def current_operation(self):
"""Return current operation.""" """Return current operation."""
if self.operation_mode == 'auxHeatOnly' or \ if self.operation_mode == 'auxHeatOnly' or \
self.operation_mode == 'heatPump': self.operation_mode == 'heatPump':
return STATE_HEAT return STATE_HEAT
return self.operation_mode return self.operation_mode
@ -271,10 +277,11 @@ class Thermostat(ClimateDevice):
operation = STATE_HEAT operation = STATE_HEAT
else: else:
operation = status operation = status
return { return {
"actual_humidity": self.thermostat['runtime']['actualHumidity'], "actual_humidity": self.thermostat['runtime']['actualHumidity'],
"fan": self.fan, "fan": self.fan,
"mode": self.mode, "climate_mode": self.mode,
"operation": operation, "operation": operation,
"climate_list": self.climate_list, "climate_list": self.climate_list,
"fan_min_on_time": self.fan_min_on_time "fan_min_on_time": self.fan_min_on_time
@ -342,25 +349,46 @@ class Thermostat(ClimateDevice):
cool_temp_setpoint, heat_temp_setpoint, cool_temp_setpoint, heat_temp_setpoint,
self.hold_preference()) self.hold_preference())
_LOGGER.debug("Setting ecobee hold_temp to: heat=%s, is=%s, " _LOGGER.debug("Setting ecobee hold_temp to: heat=%s, is=%s, "
"cool=%s, is=%s", heat_temp, isinstance( "cool=%s, is=%s", heat_temp,
heat_temp, (int, float)), cool_temp, isinstance(heat_temp, (int, float)), cool_temp,
isinstance(cool_temp, (int, float))) isinstance(cool_temp, (int, float)))
self.update_without_throttle = True 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): def set_temp_hold(self, temp):
"""Set temperature hold in modes other than auto.""" """Set temperature hold in modes other than auto.
# Set arbitrary range when not in auto mode
if self.current_operation == STATE_HEAT: 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 heat_temp = temp
cool_temp = temp + 20
elif self.current_operation == STATE_COOL:
heat_temp = temp - 20
cool_temp = temp cool_temp = temp
else: else:
# In auto mode set temperature between delta = self.thermostat['settings']['heatCoolMinDelta'] / 10
heat_temp = temp - 10 heat_temp = temp - delta
cool_temp = temp + 10 cool_temp = temp + delta
self.set_auto_temp_hold(heat_temp, cool_temp) self.set_auto_temp_hold(heat_temp, cool_temp)
def set_temperature(self, **kwargs): def set_temperature(self, **kwargs):
@ -369,8 +397,8 @@ class Thermostat(ClimateDevice):
high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH)
temp = kwargs.get(ATTR_TEMPERATURE) temp = kwargs.get(ATTR_TEMPERATURE)
if self.current_operation == STATE_AUTO and (low_temp is not None or if self.current_operation == STATE_AUTO and \
high_temp is not None): (low_temp is not None or high_temp is not None):
self.set_auto_temp_hold(low_temp, high_temp) self.set_auto_temp_hold(low_temp, high_temp)
elif temp is not None: elif temp is not None:
self.set_temp_hold(temp) self.set_temp_hold(temp)

View File

@ -15,7 +15,7 @@ from homeassistant.const import (
CONF_MAC, CONF_DEVICES, TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_HALVES) CONF_MAC, CONF_DEVICES, TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_HALVES)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['python-eq3bt==0.1.9'] REQUIREMENTS = ['python-eq3bt==0.1.9', 'construct==2.9.41']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -0,0 +1,153 @@
"""
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_LOCKED)
from homeassistant.components.climate import (
ATTR_OPERATION_MODE, ClimateDevice, STATE_ECO, STATE_HEAT, STATE_MANUAL,
SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE)
from homeassistant.const import (
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]
MIN_TEMPERATURE = 8
MAX_TEMPERATURE = 28
def setup_platform(hass, config, add_devices, 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_devices(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."""
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 == self._comfort_temperature:
return STATE_HEAT
elif 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)
@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_DEVICE_LOCKED: self._device.device_lock,
ATTR_STATE_LOCKED: self._device.lock,
ATTR_STATE_BATTERY_LOW: self._device.battery_low,
}
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()

View File

@ -14,7 +14,8 @@ from homeassistant.core import DOMAIN as HA_DOMAIN
from homeassistant.components.climate import ( from homeassistant.components.climate import (
STATE_HEAT, STATE_COOL, STATE_IDLE, STATE_AUTO, ClimateDevice, STATE_HEAT, STATE_COOL, STATE_IDLE, STATE_AUTO, ClimateDevice,
ATTR_OPERATION_MODE, ATTR_AWAY_MODE, SUPPORT_OPERATION_MODE, ATTR_OPERATION_MODE, ATTR_AWAY_MODE, SUPPORT_OPERATION_MODE,
SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA) SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA,
DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP)
from homeassistant.const import ( from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE,
CONF_NAME, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, CONF_NAME, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF,
@ -267,8 +268,7 @@ class GenericThermostat(ClimateDevice):
if self._min_temp: if self._min_temp:
return self._min_temp return self._min_temp
# get default temp from super class return DEFAULT_MIN_TEMP
return ClimateDevice.min_temp.fget(self)
@property @property
def max_temp(self): def max_temp(self):
@ -277,8 +277,7 @@ class GenericThermostat(ClimateDevice):
if self._max_temp: if self._max_temp:
return self._max_temp return self._max_temp
# Get default temp from super class return DEFAULT_MAX_TEMP
return ClimateDevice.max_temp.fget(self)
@asyncio.coroutine @asyncio.coroutine
def _async_sensor_changed(self, entity_id, old_state, new_state): def _async_sensor_changed(self, entity_id, old_state, new_state):

View File

@ -38,7 +38,10 @@ class HiveClimateEntity(ClimateDevice):
self.node_id = hivedevice["Hive_NodeID"] self.node_id = hivedevice["Hive_NodeID"]
self.node_name = hivedevice["Hive_NodeName"] self.node_name = hivedevice["Hive_NodeName"]
self.device_type = hivedevice["HA_DeviceType"] self.device_type = hivedevice["HA_DeviceType"]
if self.device_type == "Heating":
self.thermostat_node_id = hivedevice["Thermostat_NodeID"]
self.session = hivesession self.session = hivesession
self.attributes = {}
self.data_updatesource = '{}.{}'.format(self.device_type, self.data_updatesource = '{}.{}'.format(self.device_type,
self.node_id) self.node_id)
@ -71,6 +74,11 @@ class HiveClimateEntity(ClimateDevice):
friendly_name = "Hot Water" friendly_name = "Hot Water"
return friendly_name return friendly_name
@property
def device_state_attributes(self):
"""Show Device Attributes."""
return self.attributes
@property @property
def temperature_unit(self): def temperature_unit(self):
"""Return the unit of measurement.""" """Return the unit of measurement."""
@ -175,4 +183,9 @@ class HiveClimateEntity(ClimateDevice):
def update(self): def update(self):
"""Update all Node data from Hive.""" """Update all Node data from Hive."""
node = self.node_id
if self.device_type == "Heating":
node = self.thermostat_node_id
self.session.core.update_data(self.node_id) self.session.core.update_data(self.node_id)
self.attributes = self.session.attributes.state_attributes(node)

View File

@ -0,0 +1,101 @@
"""
Support for HomematicIP climate.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/climate.homematicip_cloud/
"""
import logging
from homeassistant.components.climate import (
ClimateDevice, SUPPORT_TARGET_TEMPERATURE, ATTR_TEMPERATURE,
STATE_AUTO, STATE_MANUAL)
from homeassistant.const import TEMP_CELSIUS
from homeassistant.components.homematicip_cloud import (
HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN,
ATTR_HOME_ID)
_LOGGER = logging.getLogger(__name__)
STATE_BOOST = 'Boost'
HA_STATE_TO_HMIP = {
STATE_AUTO: 'AUTOMATIC',
STATE_MANUAL: 'MANUAL',
}
HMIP_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_HMIP.items()}
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up the HomematicIP climate devices."""
from homematicip.group import HeatingGroup
if discovery_info is None:
return
home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]]
devices = []
for device in home.groups:
if isinstance(device, HeatingGroup):
devices.append(HomematicipHeatingGroup(home, device))
if devices:
async_add_devices(devices)
class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice):
"""Representation of a MomematicIP heating group."""
def __init__(self, home, device):
"""Initialize heating group."""
device.modelType = 'Group-Heating'
super().__init__(home, device)
@property
def temperature_unit(self):
"""Return the unit of measurement."""
return TEMP_CELSIUS
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_TARGET_TEMPERATURE
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._device.setPointTemperature
@property
def current_temperature(self):
"""Return the current temperature."""
return self._device.actualTemperature
@property
def current_humidity(self):
"""Return the current humidity."""
return self._device.humidity
@property
def current_operation(self):
"""Return current operation ie. automatic or manual."""
return HMIP_STATE_TO_HA.get(self._device.controlMode)
@property
def min_temp(self):
"""Return the minimum temperature."""
return self._device.minTemperature
@property
def max_temp(self):
"""Return the maximum temperature."""
return self._device.maxTemperature
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_point_temperature(temperature)

View File

@ -20,7 +20,7 @@ from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT,
ATTR_TEMPERATURE, CONF_REGION) ATTR_TEMPERATURE, CONF_REGION)
REQUIREMENTS = ['evohomeclient==0.2.5', 'somecomfort==0.5.0'] REQUIREMENTS = ['evohomeclient==0.2.5', 'somecomfort==0.5.2']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -10,7 +10,7 @@ import logging
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ClimateDevice, STATE_AUTO, SUPPORT_TARGET_TEMPERATURE, ClimateDevice, STATE_AUTO, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_OPERATION_MODE) SUPPORT_OPERATION_MODE)
from homeassistant.components.maxcube import MAXCUBE_HANDLE from homeassistant.components.maxcube import DATA_KEY
from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -24,16 +24,16 @@ SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Iterate through all MAX! Devices and add thermostats.""" """Iterate through all MAX! Devices and add thermostats."""
cube = hass.data[MAXCUBE_HANDLE].cube
devices = [] 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)
for device in cube.devices: if cube.is_thermostat(device) or cube.is_wallthermostat(device):
name = '{} {}'.format( devices.append(
cube.room_by_id(device.room_id).name, device.name) MaxCubeClimate(handler, name, device.rf_address))
if cube.is_thermostat(device) or cube.is_wallthermostat(device):
devices.append(MaxCubeClimate(hass, name, device.rf_address))
if devices: if devices:
add_devices(devices) add_devices(devices)
@ -42,14 +42,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class MaxCubeClimate(ClimateDevice): class MaxCubeClimate(ClimateDevice):
"""MAX! Cube ClimateDevice.""" """MAX! Cube ClimateDevice."""
def __init__(self, hass, name, rf_address): def __init__(self, handler, name, rf_address):
"""Initialize MAX! Cube ClimateDevice.""" """Initialize MAX! Cube ClimateDevice."""
self._name = name self._name = name
self._unit_of_measurement = TEMP_CELSIUS self._unit_of_measurement = TEMP_CELSIUS
self._operation_list = [STATE_AUTO, STATE_MANUAL, STATE_BOOST, self._operation_list = [STATE_AUTO, STATE_MANUAL, STATE_BOOST,
STATE_VACATION] STATE_VACATION]
self._rf_address = rf_address self._rf_address = rf_address
self._cubehandle = hass.data[MAXCUBE_HANDLE] self._cubehandle = handler
@property @property
def supported_features(self): def supported_features(self):

View File

@ -0,0 +1,148 @@
"""
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)
import homeassistant.components.modbus as 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_devices, 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_devices([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])

View File

@ -31,10 +31,12 @@ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_HIGH |
SUPPORT_OPERATION_MODE) SUPPORT_OPERATION_MODE)
def setup_platform(hass, config, add_devices, discovery_info=None): async def async_setup_platform(
"""Set up the MySensors climate.""" hass, config, async_add_devices, discovery_info=None):
"""Set up the mysensors climate."""
mysensors.setup_mysensors_platform( mysensors.setup_mysensors_platform(
hass, DOMAIN, discovery_info, MySensorsHVAC, add_devices=add_devices) hass, DOMAIN, discovery_info, MySensorsHVAC,
async_add_devices=async_add_devices)
class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice): class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
@ -113,7 +115,7 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
"""List of available fan modes.""" """List of available fan modes."""
return ['Auto', 'Min', 'Normal', 'Max'] return ['Auto', 'Min', 'Normal', 'Max']
def set_temperature(self, **kwargs): async def async_set_temperature(self, **kwargs):
"""Set new target temperature.""" """Set new target temperature."""
set_req = self.gateway.const.SetReq set_req = self.gateway.const.SetReq
temp = kwargs.get(ATTR_TEMPERATURE) temp = kwargs.get(ATTR_TEMPERATURE)
@ -141,9 +143,9 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
if self.gateway.optimistic: if self.gateway.optimistic:
# Optimistically assume that device has changed state # Optimistically assume that device has changed state
self._values[value_type] = value self._values[value_type] = value
self.schedule_update_ha_state() self.async_schedule_update_ha_state()
def set_fan_mode(self, fan_mode): async def async_set_fan_mode(self, fan_mode):
"""Set new target temperature.""" """Set new target temperature."""
set_req = self.gateway.const.SetReq set_req = self.gateway.const.SetReq
self.gateway.set_child_value( self.gateway.set_child_value(
@ -151,9 +153,9 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
if self.gateway.optimistic: if self.gateway.optimistic:
# Optimistically assume that device has changed state # Optimistically assume that device has changed state
self._values[set_req.V_HVAC_SPEED] = fan_mode self._values[set_req.V_HVAC_SPEED] = fan_mode
self.schedule_update_ha_state() self.async_schedule_update_ha_state()
def set_operation_mode(self, operation_mode): async def async_set_operation_mode(self, operation_mode):
"""Set new target temperature.""" """Set new target temperature."""
self.gateway.set_child_value( self.gateway.set_child_value(
self.node_id, self.child_id, self.value_type, self.node_id, self.child_id, self.value_type,
@ -161,10 +163,10 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
if self.gateway.optimistic: if self.gateway.optimistic:
# Optimistically assume that device has changed state # Optimistically assume that device has changed state
self._values[self.value_type] = operation_mode self._values[self.value_type] = operation_mode
self.schedule_update_ha_state() self.async_schedule_update_ha_state()
def update(self): async def async_update(self):
"""Update the controller with the latest value from a sensor.""" """Update the controller with the latest value from a sensor."""
super().update() await super().async_update()
self._values[self.value_type] = DICT_MYS_TO_HA[ self._values[self.value_type] = DICT_MYS_TO_HA[
self._values[self.value_type]] self._values[self.value_type]]

View File

@ -8,7 +8,7 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.nest import DATA_NEST from homeassistant.components.nest import DATA_NEST, SIGNAL_NEST_UPDATE
from homeassistant.components.climate import ( from homeassistant.components.climate import (
STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_ECO, ClimateDevice, STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_ECO, ClimateDevice,
PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
@ -18,6 +18,7 @@ from homeassistant.components.climate import (
from homeassistant.const import ( from homeassistant.const import (
TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_CELSIUS, TEMP_FAHRENHEIT,
CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF, STATE_UNKNOWN) CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF, STATE_UNKNOWN)
from homeassistant.helpers.dispatcher import async_dispatcher_connect
DEPENDENCIES = ['nest'] DEPENDENCIES = ['nest']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -37,11 +38,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
temp_unit = hass.config.units.temperature_unit temp_unit = hass.config.units.temperature_unit
add_devices( all_devices = [NestThermostat(structure, device, temp_unit)
[NestThermostat(structure, device, temp_unit) for structure, device in hass.data[DATA_NEST].thermostats()]
for structure, device in hass.data[DATA_NEST].thermostats()],
True add_devices(all_devices, True)
)
class NestThermostat(ClimateDevice): class NestThermostat(ClimateDevice):
@ -97,6 +97,20 @@ class NestThermostat(ClimateDevice):
self._min_temperature = None self._min_temperature = None
self._max_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 @property
def supported_features(self): def supported_features(self):
"""Return the list of supported features.""" """Return the list of supported features."""
@ -134,7 +148,9 @@ class NestThermostat(ClimateDevice):
@property @property
def target_temperature(self): def target_temperature(self):
"""Return the temperature we try to reach.""" """Return the temperature we try to reach."""
if self._mode != NEST_MODE_HEAT_COOL and not self.is_away_mode_on: if self._mode != NEST_MODE_HEAT_COOL and \
self._mode != STATE_ECO and \
not self.is_away_mode_on:
return self._target_temperature return self._target_temperature
return None return None
@ -168,18 +184,24 @@ class NestThermostat(ClimateDevice):
def set_temperature(self, **kwargs): def set_temperature(self, **kwargs):
"""Set new target temperature.""" """Set new target temperature."""
import nest import nest
temp = None
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
if self._mode == NEST_MODE_HEAT_COOL: if self._mode == NEST_MODE_HEAT_COOL:
if target_temp_low is not None and target_temp_high is not None: if target_temp_low is not None and target_temp_high is not None:
temp = (target_temp_low, target_temp_high) temp = (target_temp_low, target_temp_high)
_LOGGER.debug("Nest set_temperature-output-value=%s", temp)
else: else:
temp = kwargs.get(ATTR_TEMPERATURE) temp = kwargs.get(ATTR_TEMPERATURE)
_LOGGER.debug("Nest set_temperature-output-value=%s", temp) _LOGGER.debug("Nest set_temperature-output-value=%s", temp)
try: try:
self.device.target = temp if temp is not None:
except nest.nest.APIError: self.device.target = temp
_LOGGER.error("An error occured while setting the temperature") 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): def set_operation_mode(self, operation_mode):
"""Set operation mode.""" """Set operation mode."""
@ -187,6 +209,11 @@ class NestThermostat(ClimateDevice):
device_mode = operation_mode device_mode = operation_mode
elif operation_mode == STATE_AUTO: elif operation_mode == STATE_AUTO:
device_mode = NEST_MODE_HEAT_COOL 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 self.device.mode = device_mode
@property @property

Some files were not shown because too many files have changed in this diff Show More