Merge pull request #17809 from home-assistant/rc

0.81
This commit is contained in:
Paulus Schoutsen 2018-10-26 19:45:29 +02:00 committed by GitHub
commit e28170a0a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
472 changed files with 19297 additions and 2781 deletions

View File

@ -102,6 +102,9 @@ omit =
homeassistant/components/egardia.py homeassistant/components/egardia.py
homeassistant/components/*/egardia.py homeassistant/components/*/egardia.py
homeassistant/components/elkm1/*
homeassistant/components/*/elkm1.py
homeassistant/components/enocean.py homeassistant/components/enocean.py
homeassistant/components/*/enocean.py homeassistant/components/*/enocean.py
@ -245,6 +248,9 @@ omit =
homeassistant/components/opencv.py homeassistant/components/opencv.py
homeassistant/components/*/opencv.py homeassistant/components/*/opencv.py
homeassistant/components/opentherm_gw.py
homeassistant/components/*/opentherm_gw.py
homeassistant/components/openuv/__init__.py homeassistant/components/openuv/__init__.py
homeassistant/components/*/openuv.py homeassistant/components/*/openuv.py
@ -284,6 +290,9 @@ omit =
homeassistant/components/scsgate.py homeassistant/components/scsgate.py
homeassistant/components/*/scsgate.py homeassistant/components/*/scsgate.py
homeassistant/components/simplisafe/__init__.py
homeassistant/components/*/simplisafe.py
homeassistant/components/sisyphus.py homeassistant/components/sisyphus.py
homeassistant/components/*/sisyphus.py homeassistant/components/*/sisyphus.py
@ -379,7 +388,7 @@ omit =
homeassistant/components/zigbee.py homeassistant/components/zigbee.py
homeassistant/components/*/zigbee.py homeassistant/components/*/zigbee.py
homeassistant/components/zoneminder.py homeassistant/components/zoneminder/*
homeassistant/components/*/zoneminder.py homeassistant/components/*/zoneminder.py
homeassistant/components/tuya.py homeassistant/components/tuya.py
@ -395,7 +404,6 @@ omit =
homeassistant/components/alarm_control_panel/ifttt.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/totalconnect.py homeassistant/components/alarm_control_panel/totalconnect.py
homeassistant/components/alarm_control_panel/yale_smart_alarm.py homeassistant/components/alarm_control_panel/yale_smart_alarm.py
homeassistant/components/apiai.py homeassistant/components/apiai.py
@ -426,7 +434,6 @@ omit =
homeassistant/components/camera/xeoma.py homeassistant/components/camera/xeoma.py
homeassistant/components/camera/xiaomi.py homeassistant/components/camera/xiaomi.py
homeassistant/components/camera/yi.py homeassistant/components/camera/yi.py
homeassistant/components/climate/econet.py
homeassistant/components/climate/ephember.py homeassistant/components/climate/ephember.py
homeassistant/components/climate/eq3btsmart.py homeassistant/components/climate/eq3btsmart.py
homeassistant/components/climate/flexit.py homeassistant/components/climate/flexit.py
@ -434,8 +441,8 @@ omit =
homeassistant/components/climate/homematic.py homeassistant/components/climate/homematic.py
homeassistant/components/climate/honeywell.py homeassistant/components/climate/honeywell.py
homeassistant/components/climate/knx.py homeassistant/components/climate/knx.py
homeassistant/components/climate/mill.py
homeassistant/components/climate/oem.py homeassistant/components/climate/oem.py
homeassistant/components/climate/opentherm_gw.py
homeassistant/components/climate/proliphix.py homeassistant/components/climate/proliphix.py
homeassistant/components/climate/radiotherm.py homeassistant/components/climate/radiotherm.py
homeassistant/components/climate/sensibo.py homeassistant/components/climate/sensibo.py
@ -451,7 +458,6 @@ omit =
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
@ -478,6 +484,7 @@ omit =
homeassistant/components/device_tracker/netgear.py homeassistant/components/device_tracker/netgear.py
homeassistant/components/device_tracker/nmap_tracker.py homeassistant/components/device_tracker/nmap_tracker.py
homeassistant/components/device_tracker/ping.py homeassistant/components/device_tracker/ping.py
homeassistant/components/device_tracker/quantum_gateway.py
homeassistant/components/device_tracker/ritassist.py homeassistant/components/device_tracker/ritassist.py
homeassistant/components/device_tracker/sky_hub.py homeassistant/components/device_tracker/sky_hub.py
homeassistant/components/device_tracker/snmp.py homeassistant/components/device_tracker/snmp.py
@ -561,6 +568,7 @@ omit =
homeassistant/components/media_player/itunes.py homeassistant/components/media_player/itunes.py
homeassistant/components/media_player/kodi.py homeassistant/components/media_player/kodi.py
homeassistant/components/media_player/lg_netcast.py homeassistant/components/media_player/lg_netcast.py
homeassistant/components/media_player/lg_soundbar.py
homeassistant/components/media_player/liveboxplaytv.py homeassistant/components/media_player/liveboxplaytv.py
homeassistant/components/media_player/mediaroom.py homeassistant/components/media_player/mediaroom.py
homeassistant/components/media_player/mpchc.py homeassistant/components/media_player/mpchc.py
@ -604,6 +612,7 @@ omit =
homeassistant/components/notify/gntp.py homeassistant/components/notify/gntp.py
homeassistant/components/notify/group.py homeassistant/components/notify/group.py
homeassistant/components/notify/hipchat.py homeassistant/components/notify/hipchat.py
homeassistant/components/notify/homematic.py
homeassistant/components/notify/instapush.py homeassistant/components/notify/instapush.py
homeassistant/components/notify/kodi.py homeassistant/components/notify/kodi.py
homeassistant/components/notify/lannouncer.py homeassistant/components/notify/lannouncer.py
@ -636,6 +645,7 @@ 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/route53.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
@ -747,6 +757,7 @@ 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/rtorrent.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
@ -776,6 +787,7 @@ omit =
homeassistant/components/sensor/tank_utility.py homeassistant/components/sensor/tank_utility.py
homeassistant/components/sensor/ted5000.py homeassistant/components/sensor/ted5000.py
homeassistant/components/sensor/temper.py homeassistant/components/sensor/temper.py
homeassistant/components/sensor/thermoworks_smoke.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/trafikverket_weatherstation.py
@ -816,6 +828,7 @@ omit =
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/rest.py homeassistant/components/switch/rest.py
homeassistant/components/switch/recswitch.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/switchbot.py homeassistant/components/switch/switchbot.py
@ -832,6 +845,7 @@ 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/water_heater/econet.py
homeassistant/components/watson_iot.py homeassistant/components/watson_iot.py
homeassistant/components/weather/bom.py homeassistant/components/weather/bom.py
homeassistant/components/weather/buienradar.py homeassistant/components/weather/buienradar.py

View File

@ -2,59 +2,68 @@
# when the code that they own is touched. # when the code that they own is touched.
# https://github.com/blog/2392-introducing-code-owners # https://github.com/blog/2392-introducing-code-owners
# Home Assistant Core
setup.py @home-assistant/core setup.py @home-assistant/core
homeassistant/*.py @home-assistant/core homeassistant/*.py @home-assistant/core
homeassistant/helpers/* @home-assistant/core homeassistant/helpers/* @home-assistant/core
homeassistant/util/* @home-assistant/core homeassistant/util/* @home-assistant/core
homeassistant/components/api.py @home-assistant/core homeassistant/components/api.py @home-assistant/core
homeassistant/components/auth/* @home-assistant/core
homeassistant/components/automation/* @home-assistant/core homeassistant/components/automation/* @home-assistant/core
homeassistant/components/cloud/* @home-assistant/core
homeassistant/components/config/* @home-assistant/core
homeassistant/components/configurator.py @home-assistant/core homeassistant/components/configurator.py @home-assistant/core
homeassistant/components/group.py @home-assistant/core homeassistant/components/conversation/* @home-assistant/core
homeassistant/components/frontend/* @home-assistant/core
homeassistant/components/group/* @home-assistant/core
homeassistant/components/history.py @home-assistant/core homeassistant/components/history.py @home-assistant/core
homeassistant/components/http/* @home-assistant/core homeassistant/components/http/* @home-assistant/core
homeassistant/components/input_*.py @home-assistant/core homeassistant/components/input_*.py @home-assistant/core
homeassistant/components/introduction.py @home-assistant/core homeassistant/components/introduction.py @home-assistant/core
homeassistant/components/logger.py @home-assistant/core homeassistant/components/logger.py @home-assistant/core
homeassistant/components/lovelace/* @home-assistant/core
homeassistant/components/mqtt/* @home-assistant/core homeassistant/components/mqtt/* @home-assistant/core
homeassistant/components/panel_custom.py @home-assistant/core homeassistant/components/panel_custom.py @home-assistant/core
homeassistant/components/panel_iframe.py @home-assistant/core homeassistant/components/panel_iframe.py @home-assistant/core
homeassistant/components/persistent_notification.py @home-assistant/core homeassistant/components/onboarding/* @home-assistant/core
homeassistant/components/persistent_notification/* @home-assistant/core
homeassistant/components/scene/__init__.py @home-assistant/core homeassistant/components/scene/__init__.py @home-assistant/core
homeassistant/components/scene/hass.py @home-assistant/core homeassistant/components/scene/hass.py @home-assistant/core
homeassistant/components/script.py @home-assistant/core homeassistant/components/script.py @home-assistant/core
homeassistant/components/shell_command.py @home-assistant/core homeassistant/components/shell_command.py @home-assistant/core
homeassistant/components/sun.py @home-assistant/core homeassistant/components/sun.py @home-assistant/core
homeassistant/components/updater.py @home-assistant/core homeassistant/components/updater.py @home-assistant/core
homeassistant/components/weblink.py @home-assistant/core homeassistant/components/weblink/* @home-assistant/core
homeassistant/components/websocket_api.py @home-assistant/core homeassistant/components/websocket_api.py @home-assistant/core
homeassistant/components/zone.py @home-assistant/core homeassistant/components/zone/* @home-assistant/core
# HomeAssistant developer Teams # Home Assistant Developer Teams
Dockerfile @home-assistant/docker Dockerfile @home-assistant/docker
virtualization/Docker/* @home-assistant/docker virtualization/Docker/* @home-assistant/docker
homeassistant/components/zwave/* @home-assistant/z-wave homeassistant/components/zwave/* @home-assistant/z-wave
homeassistant/components/*/zwave.py @home-assistant/z-wave homeassistant/components/*/zwave.py @home-assistant/z-wave
homeassistant/components/hassio.py @home-assistant/hassio homeassistant/components/hassio/* @home-assistant/hassio
# Individual components # Individual platforms
homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt
homeassistant/components/alarm_control_panel/manual_mqtt.py @colinodell homeassistant/components/alarm_control_panel/manual_mqtt.py @colinodell
homeassistant/components/alarm_control_panel/simplisafe.py @bachya
homeassistant/components/binary_sensor/hikvision.py @mezz64 homeassistant/components/binary_sensor/hikvision.py @mezz64
homeassistant/components/bmw_connected_drive.py @ChristianKuehnel homeassistant/components/binary_sensor/threshold.py @fabaff
homeassistant/components/camera/yi.py @bachya homeassistant/components/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/mill.py @danielhiversen
homeassistant/components/climate/sensibo.py @andrey-git homeassistant/components/climate/sensibo.py @andrey-git
homeassistant/components/cover/group.py @cdce8p 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/huawei_router.py @abmantis homeassistant/components/device_tracker/huawei_router.py @abmantis
homeassistant/components/device_tracker/quantum_gateway.py @cisasteelersfan
homeassistant/components/device_tracker/tile.py @bachya homeassistant/components/device_tracker/tile.py @bachya
homeassistant/components/history_graph.py @andrey-git homeassistant/components/history_graph.py @andrey-git
homeassistant/components/light/lifx.py @amelchio homeassistant/components/influx.py @fabaff
homeassistant/components/light/lifx_legacy.py @amelchio homeassistant/components/light/lifx_legacy.py @amelchio
homeassistant/components/light/tplink.py @rytilahti homeassistant/components/light/tplink.py @rytilahti
homeassistant/components/light/yeelight.py @rytilahti homeassistant/components/light/yeelight.py @rytilahti
@ -65,74 +74,173 @@ homeassistant/components/media_player/kodi.py @armills
homeassistant/components/media_player/liveboxplaytv.py @pschmitt 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/mpd.py @fabaff
homeassistant/components/media_player/sonos.py @amelchio homeassistant/components/media_player/sonos.py @amelchio
homeassistant/components/media_player/xiaomi_tv.py @fattdev 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/no_ip.py @fabaff
homeassistant/components/notify/file.py @fabaff
homeassistant/components/notify/flock.py @fabaff
homeassistant/components/notify/instapush.py @fabaff
homeassistant/components/notify/mastodon.py @fabaff
homeassistant/components/notify/smtp.py @fabaff
homeassistant/components/notify/syslog.py @fabaff
homeassistant/components/notify/xmpp.py @fabaff
homeassistant/components/plant.py @ChristianKuehnel homeassistant/components/plant.py @ChristianKuehnel
homeassistant/components/scene/lifx_cloud.py @amelchio homeassistant/components/scene/lifx_cloud.py @amelchio
homeassistant/components/sensor/airvisual.py @bachya homeassistant/components/sensor/airvisual.py @bachya
homeassistant/components/sensor/alpha_vantage.py @fabaff
homeassistant/components/sensor/bitcoin.py @fabaff
homeassistant/components/sensor/cpuspeed.py @fabaff
homeassistant/components/sensor/cups.py @fabaff
homeassistant/components/sensor/darksky.py @fabaff
homeassistant/components/sensor/file.py @fabaff
homeassistant/components/sensor/filter.py @dgomes homeassistant/components/sensor/filter.py @dgomes
homeassistant/components/sensor/fixer.py @fabaff
homeassistant/components/sensor/gearbest.py @HerrHofrat homeassistant/components/sensor/gearbest.py @HerrHofrat
homeassistant/components/sensor/gitter.py @fabaff
homeassistant/components/sensor/glances.py @fabaff
homeassistant/components/sensor/gpsd.py @fabaff
homeassistant/components/sensor/irish_rail_transport.py @ttroy50 homeassistant/components/sensor/irish_rail_transport.py @ttroy50
homeassistant/components/sensor/jewish_calendar.py @tsvi homeassistant/components/sensor/jewish_calendar.py @tsvi
homeassistant/components/sensor/linux_battery.py @fabaff
homeassistant/components/sensor/luftdaten.py @fabaff
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
homeassistant/components/sensor/min_max.py @fabaff
homeassistant/components/sensor/moon.py @fabaff
homeassistant/components/sensor/netdata.py @fabaff
homeassistant/components/sensor/nsw_fuel_station.py @nickw444 homeassistant/components/sensor/nsw_fuel_station.py @nickw444
homeassistant/components/sensor/pi_hole.py @fabaff
homeassistant/components/sensor/pollen.py @bachya homeassistant/components/sensor/pollen.py @bachya
homeassistant/components/sensor/pvoutput.py @fabaff
homeassistant/components/sensor/qnap.py @colinodell homeassistant/components/sensor/qnap.py @colinodell
homeassistant/components/sensor/scrape.py @fabaff
homeassistant/components/sensor/serial.py @fabaff
homeassistant/components/sensor/shodan.py @fabaff
homeassistant/components/sensor/sma.py @kellerza homeassistant/components/sensor/sma.py @kellerza
homeassistant/components/sensor/sql.py @dgomes homeassistant/components/sensor/sql.py @dgomes
homeassistant/components/sensor/statistics.py @fabaff
homeassistant/components/sensor/swiss*.py @fabaff
homeassistant/components/sensor/sytadin.py @gautric homeassistant/components/sensor/sytadin.py @gautric
homeassistant/components/sensor/time_data.py @fabaff
homeassistant/components/sensor/version.py @fabaff
homeassistant/components/sensor/waqi.py @andrey-git homeassistant/components/sensor/waqi.py @andrey-git
homeassistant/components/sensor/worldclock.py @fabaff
homeassistant/components/shiftr.py @fabaff
homeassistant/components/spaceapi.py @fabaff
homeassistant/components/switch/tplink.py @rytilahti homeassistant/components/switch/tplink.py @rytilahti
homeassistant/components/vacuum/roomba.py @pschmitt homeassistant/components/vacuum/roomba.py @pschmitt
homeassistant/components/weather/__init__.py @fabaff
homeassistant/components/weather/darksky.py @fabaff
homeassistant/components/weather/demo.py @fabaff
homeassistant/components/weather/met.py @danielhiversen
homeassistant/components/weather/openweathermap.py @fabaff
homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
# A
homeassistant/components/arduino.py @fabaff
homeassistant/components/*/arduino.py @fabaff
homeassistant/components/*/arest.py @fabaff
homeassistant/components/*/axis.py @kane610 homeassistant/components/*/axis.py @kane610
# B
homeassistant/components/blink/* @fronzbot homeassistant/components/blink/* @fronzbot
homeassistant/components/*/blink.py @fronzbot homeassistant/components/*/blink.py @fronzbot
homeassistant/components/bmw_connected_drive.py @ChristianKuehnel
homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel
homeassistant/components/*/broadlink.py @danielhiversen homeassistant/components/*/broadlink.py @danielhiversen
# C
homeassistant/components/counter/* @fabaff
# D
homeassistant/components/*/deconz.py @kane610 homeassistant/components/*/deconz.py @kane610
homeassistant/components/digital_ocean.py @fabaff
homeassistant/components/*/digital_ocean.py @fabaff
homeassistant/components/dweet.py @fabaff
homeassistant/components/*/dweet.py @fabaff
# E
homeassistant/components/ecovacs.py @OverloadUT homeassistant/components/ecovacs.py @OverloadUT
homeassistant/components/*/ecovacs.py @OverloadUT homeassistant/components/*/ecovacs.py @OverloadUT
homeassistant/components/edp_redy.py @abmantis
homeassistant/components/*/edp_redy.py @abmantis homeassistant/components/*/edp_redy.py @abmantis
homeassistant/components/edp_redy.py @abmantis
homeassistant/components/eight_sleep.py @mezz64 homeassistant/components/eight_sleep.py @mezz64
homeassistant/components/*/eight_sleep.py @mezz64 homeassistant/components/*/eight_sleep.py @mezz64
# H
homeassistant/components/hive.py @Rendili @KJonline homeassistant/components/hive.py @Rendili @KJonline
homeassistant/components/*/hive.py @Rendili @KJonline homeassistant/components/*/hive.py @Rendili @KJonline
homeassistant/components/homekit/* @cdce8p homeassistant/components/homekit/* @cdce8p
homeassistant/components/huawei_lte.py @scop homeassistant/components/huawei_lte.py @scop
homeassistant/components/*/huawei_lte.py @scop homeassistant/components/*/huawei_lte.py @scop
# K
homeassistant/components/knx.py @Julius2342 homeassistant/components/knx.py @Julius2342
homeassistant/components/*/knx.py @Julius2342 homeassistant/components/*/knx.py @Julius2342
homeassistant/components/konnected.py @heythisisnate homeassistant/components/konnected.py @heythisisnate
homeassistant/components/*/konnected.py @heythisisnate homeassistant/components/*/konnected.py @heythisisnate
# L
homeassistant/components/lifx.py @amelchio
homeassistant/components/*/lifx.py @amelchio
# M
homeassistant/components/matrix.py @tinloaf homeassistant/components/matrix.py @tinloaf
homeassistant/components/*/matrix.py @tinloaf homeassistant/components/*/matrix.py @tinloaf
homeassistant/components/melissa.py @kennedyshead
homeassistant/components/*/melissa.py @kennedyshead
homeassistant/components/*/mystrom.py @fabaff
# O
homeassistant/components/openuv/* @bachya homeassistant/components/openuv/* @bachya
homeassistant/components/*/openuv.py @bachya homeassistant/components/*/openuv.py @bachya
# Q
homeassistant/components/qwikswitch.py @kellerza homeassistant/components/qwikswitch.py @kellerza
homeassistant/components/*/qwikswitch.py @kellerza homeassistant/components/*/qwikswitch.py @kellerza
# R
homeassistant/components/rainmachine/* @bachya homeassistant/components/rainmachine/* @bachya
homeassistant/components/*/rainmachine.py @bachya homeassistant/components/*/rainmachine.py @bachya
homeassistant/components/*/random.py @fabaff
homeassistant/components/*/rfxtrx.py @danielhiversen homeassistant/components/*/rfxtrx.py @danielhiversen
# S
homeassistant/components/simplisafe/* @bachya
homeassistant/components/*/simplisafe.py @bachya
# T
homeassistant/components/tahoma.py @philklei homeassistant/components/tahoma.py @philklei
homeassistant/components/*/tahoma.py @philklei homeassistant/components/*/tahoma.py @philklei
homeassistant/components/tesla.py @zabuldon
homeassistant/components/*/tesla.py @zabuldon
homeassistant/components/tellduslive.py @molobrakos @fredrike homeassistant/components/tellduslive.py @molobrakos @fredrike
homeassistant/components/*/tellduslive.py @molobrakos @fredrike homeassistant/components/*/tellduslive.py @molobrakos @fredrike
homeassistant/components/tesla.py @zabuldon
homeassistant/components/*/tesla.py @zabuldon
homeassistant/components/thethingsnetwork.py @fabaff
homeassistant/components/*/thethingsnetwork.py @fabaff
homeassistant/components/tibber/* @danielhiversen homeassistant/components/tibber/* @danielhiversen
homeassistant/components/*/tibber.py @danielhiversen homeassistant/components/*/tibber.py @danielhiversen
homeassistant/components/tradfri/* @ggravlingen
homeassistant/components/*/tradfri.py @ggravlingen homeassistant/components/*/tradfri.py @ggravlingen
# U
homeassistant/components/unifi.py @kane610
homeassistant/components/switch/unifi.py @kane610
homeassistant/components/upcloud.py @scop homeassistant/components/upcloud.py @scop
homeassistant/components/*/upcloud.py @scop homeassistant/components/*/upcloud.py @scop
# V
homeassistant/components/velux.py @Julius2342 homeassistant/components/velux.py @Julius2342
homeassistant/components/*/velux.py @Julius2342 homeassistant/components/*/velux.py @Julius2342
# X
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/components/zoneminder.py @rohankapoorcom
# Z
homeassistant/components/zoneminder/ @rohankapoorcom
homeassistant/components/*/zoneminder.py @rohankapoorcom homeassistant/components/*/zoneminder.py @rohankapoorcom
# Other code
homeassistant/scripts/check_config.py @kellerza homeassistant/scripts/check_config.py @kellerza

View File

@ -16,6 +16,9 @@ from . import auth_store, models
from .mfa_modules import auth_mfa_module_from_config, MultiFactorAuthModule from .mfa_modules import auth_mfa_module_from_config, MultiFactorAuthModule
from .providers import auth_provider_from_config, AuthProvider, LoginFlow from .providers import auth_provider_from_config, AuthProvider, LoginFlow
EVENT_USER_ADDED = 'user_added'
EVENT_USER_REMOVED = 'user_removed'
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_MfaModuleDict = Dict[str, MultiFactorAuthModule] _MfaModuleDict = Dict[str, MultiFactorAuthModule]
_ProviderKey = Tuple[str, Optional[str]] _ProviderKey = Tuple[str, Optional[str]]
@ -126,23 +129,38 @@ class AuthManager:
async def async_create_system_user(self, name: str) -> models.User: async def async_create_system_user(self, name: str) -> models.User:
"""Create a system user.""" """Create a system user."""
return await self._store.async_create_user( user = await self._store.async_create_user(
name=name, name=name,
system_generated=True, system_generated=True,
is_active=True, is_active=True,
groups=[],
) )
self.hass.bus.async_fire(EVENT_USER_ADDED, {
'user_id': user.id
})
return user
async def async_create_user(self, name: str) -> models.User: async def async_create_user(self, name: str) -> models.User:
"""Create a user.""" """Create a user."""
group = (await self._store.async_get_groups())[0]
kwargs = { kwargs = {
'name': name, 'name': name,
'is_active': True, 'is_active': True,
'groups': [group]
} # type: Dict[str, Any] } # type: Dict[str, Any]
if await self._user_should_be_owner(): if await self._user_should_be_owner():
kwargs['is_owner'] = True kwargs['is_owner'] = True
return await self._store.async_create_user(**kwargs) user = await self._store.async_create_user(**kwargs)
self.hass.bus.async_fire(EVENT_USER_ADDED, {
'user_id': user.id
})
return user
async def async_get_or_create_user(self, credentials: models.Credentials) \ async def async_get_or_create_user(self, credentials: models.Credentials) \
-> models.User: -> models.User:
@ -162,12 +180,18 @@ class AuthManager:
info = await auth_provider.async_user_meta_for_credentials( info = await auth_provider.async_user_meta_for_credentials(
credentials) credentials)
return await self._store.async_create_user( user = await self._store.async_create_user(
credentials=credentials, credentials=credentials,
name=info.name, name=info.name,
is_active=info.is_active, is_active=info.is_active,
) )
self.hass.bus.async_fire(EVENT_USER_ADDED, {
'user_id': user.id
})
return user
async def async_link_user(self, user: models.User, async def async_link_user(self, user: models.User,
credentials: models.Credentials) -> None: credentials: models.Credentials) -> None:
"""Link credentials to an existing user.""" """Link credentials to an existing user."""
@ -185,6 +209,10 @@ class AuthManager:
await self._store.async_remove_user(user) await self._store.async_remove_user(user)
self.hass.bus.async_fire(EVENT_USER_REMOVED, {
'user_id': user.id
})
async def async_activate_user(self, user: models.User) -> None: async def async_activate_user(self, user: models.User) -> None:
"""Activate a user.""" """Activate a user."""
await self._store.async_activate_user(user) await self._store.async_activate_user(user)

View File

@ -10,9 +10,11 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from . import models from . import models
from .permissions import DEFAULT_POLICY
STORAGE_VERSION = 1 STORAGE_VERSION = 1
STORAGE_KEY = 'auth' STORAGE_KEY = 'auth'
INITIAL_GROUP_NAME = 'All Access'
class AuthStore: class AuthStore:
@ -28,9 +30,18 @@ class AuthStore:
"""Initialize the auth store.""" """Initialize the auth store."""
self.hass = hass self.hass = hass
self._users = None # type: Optional[Dict[str, models.User]] self._users = None # type: Optional[Dict[str, models.User]]
self._groups = None # type: Optional[Dict[str, models.Group]]
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY, self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY,
private=True) private=True)
async def async_get_groups(self) -> List[models.Group]:
"""Retrieve all users."""
if self._groups is None:
await self._async_load()
assert self._groups is not None
return list(self._groups.values())
async def async_get_users(self) -> List[models.User]: async def async_get_users(self) -> List[models.User]:
"""Retrieve all users.""" """Retrieve all users."""
if self._users is None: if self._users is None:
@ -51,14 +62,20 @@ class AuthStore:
self, name: Optional[str], is_owner: Optional[bool] = None, self, name: Optional[str], is_owner: Optional[bool] = None,
is_active: Optional[bool] = None, is_active: Optional[bool] = None,
system_generated: Optional[bool] = None, system_generated: Optional[bool] = None,
credentials: Optional[models.Credentials] = None) -> models.User: credentials: Optional[models.Credentials] = None,
groups: Optional[List[models.Group]] = None) -> models.User:
"""Create a new user.""" """Create a new user."""
if self._users is None: if self._users is None:
await self._async_load() await self._async_load()
assert self._users is not None assert self._users is not None
assert self._groups is not None
kwargs = { kwargs = {
'name': name 'name': name,
# Until we get group management, we just put everyone in the
# same group.
'groups': groups or [],
} # type: Dict[str, Any] } # type: Dict[str, Any]
if is_owner is not None: if is_owner is not None:
@ -219,19 +236,40 @@ class AuthStore:
return return
users = OrderedDict() # type: Dict[str, models.User] users = OrderedDict() # type: Dict[str, models.User]
groups = OrderedDict() # type: Dict[str, models.Group]
# When creating objects we mention each attribute explicetely. This # When creating objects we mention each attribute explicetely. This
# prevents crashing if user rolls back HA version after a new property # prevents crashing if user rolls back HA version after a new property
# was added. # was added.
for group_dict in data.get('groups', []):
groups[group_dict['id']] = models.Group(
name=group_dict['name'],
id=group_dict['id'],
policy=group_dict.get('policy', DEFAULT_POLICY),
)
migrate_group = None
if not groups:
migrate_group = models.Group(
name=INITIAL_GROUP_NAME,
policy=DEFAULT_POLICY
)
groups[migrate_group.id] = migrate_group
for user_dict in data['users']: for user_dict in data['users']:
users[user_dict['id']] = models.User( users[user_dict['id']] = models.User(
name=user_dict['name'], name=user_dict['name'],
groups=[groups[group_id] for group_id
in user_dict.get('group_ids', [])],
id=user_dict['id'], id=user_dict['id'],
is_owner=user_dict['is_owner'], is_owner=user_dict['is_owner'],
is_active=user_dict['is_active'], is_active=user_dict['is_active'],
system_generated=user_dict['system_generated'], system_generated=user_dict['system_generated'],
) )
if migrate_group is not None and not user_dict['system_generated']:
users[user_dict['id']].groups = [migrate_group]
for cred_dict in data['credentials']: for cred_dict in data['credentials']:
users[cred_dict['user_id']].credentials.append(models.Credentials( users[cred_dict['user_id']].credentials.append(models.Credentials(
@ -286,6 +324,7 @@ class AuthStore:
) )
users[rt_dict['user_id']].refresh_tokens[token.id] = token users[rt_dict['user_id']].refresh_tokens[token.id] = token
self._groups = groups
self._users = users self._users = users
@callback @callback
@ -300,10 +339,12 @@ class AuthStore:
def _data_to_save(self) -> Dict: def _data_to_save(self) -> Dict:
"""Return the data to store.""" """Return the data to store."""
assert self._users is not None assert self._users is not None
assert self._groups is not None
users = [ users = [
{ {
'id': user.id, 'id': user.id,
'group_ids': [group.id for group in user.groups],
'is_owner': user.is_owner, 'is_owner': user.is_owner,
'is_active': user.is_active, 'is_active': user.is_active,
'name': user.name, 'name': user.name,
@ -312,6 +353,18 @@ class AuthStore:
for user in self._users.values() for user in self._users.values()
] ]
groups = []
for group in self._groups.values():
g_dict = {
'name': group.name,
'id': group.id,
} # type: Dict[str, Any]
if group.policy is not DEFAULT_POLICY:
g_dict['policy'] = group.policy
groups.append(g_dict)
credentials = [ credentials = [
{ {
'id': credential.id, 'id': credential.id,
@ -348,6 +401,7 @@ class AuthStore:
return { return {
'users': users, 'users': users,
'groups': groups,
'credentials': credentials, 'credentials': credentials,
'refresh_tokens': refresh_tokens, 'refresh_tokens': refresh_tokens,
} }
@ -355,3 +409,14 @@ class AuthStore:
def _set_defaults(self) -> None: def _set_defaults(self) -> None:
"""Set default values for auth store.""" """Set default values for auth store."""
self._users = OrderedDict() # type: Dict[str, models.User] self._users = OrderedDict() # type: Dict[str, models.User]
# Add default group
all_access_group = models.Group(
name=INITIAL_GROUP_NAME,
policy=DEFAULT_POLICY,
)
groups = OrderedDict() # type: Dict[str, models.Group]
groups[all_access_group.id] = all_access_group
self._groups = groups

View File

@ -7,6 +7,7 @@ import attr
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from . import permissions as perm_mdl
from .util import generate_secret from .util import generate_secret
TOKEN_TYPE_NORMAL = 'normal' TOKEN_TYPE_NORMAL = 'normal'
@ -14,6 +15,15 @@ TOKEN_TYPE_SYSTEM = 'system'
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = 'long_lived_access_token' TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = 'long_lived_access_token'
@attr.s(slots=True)
class Group:
"""A group."""
name = attr.ib(type=str) # type: Optional[str]
policy = attr.ib(type=perm_mdl.PolicyType)
id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex)
@attr.s(slots=True) @attr.s(slots=True)
class User: class User:
"""A user.""" """A user."""
@ -24,6 +34,8 @@ class User:
is_active = attr.ib(type=bool, default=False) is_active = attr.ib(type=bool, default=False)
system_generated = attr.ib(type=bool, default=False) system_generated = attr.ib(type=bool, default=False)
groups = attr.ib(type=List, factory=list, cmp=False) # type: List[Group]
# List of credentials of a user. # List of credentials of a user.
credentials = attr.ib( credentials = attr.ib(
type=list, factory=list, cmp=False type=list, factory=list, cmp=False
@ -34,6 +46,28 @@ class User:
type=dict, factory=dict, cmp=False type=dict, factory=dict, cmp=False
) # type: Dict[str, RefreshToken] ) # type: Dict[str, RefreshToken]
_permissions = attr.ib(
type=perm_mdl.PolicyPermissions,
init=False,
cmp=False,
default=None,
)
@property
def permissions(self) -> perm_mdl.AbstractPermissions:
"""Return permissions object for user."""
if self.is_owner:
return perm_mdl.OwnerPermissions
if self._permissions is not None:
return self._permissions
self._permissions = perm_mdl.PolicyPermissions(
perm_mdl.merge_policies([
group.policy for group in self.groups]))
return self._permissions
@attr.s(slots=True) @attr.s(slots=True)
class RefreshToken: class RefreshToken:

View File

@ -0,0 +1,252 @@
"""Permissions for Home Assistant."""
from typing import ( # noqa: F401
cast, Any, Callable, Dict, List, Mapping, Set, Tuple, Union)
import voluptuous as vol
from homeassistant.core import State
CategoryType = Union[Mapping[str, 'CategoryType'], bool, None]
PolicyType = Mapping[str, CategoryType]
# Default policy if group has no policy applied.
DEFAULT_POLICY = {
"entities": True
} # type: PolicyType
CAT_ENTITIES = 'entities'
ENTITY_DOMAINS = 'domains'
ENTITY_ENTITY_IDS = 'entity_ids'
VALUES_SCHEMA = vol.Any(True, vol.Schema({
str: True
}))
ENTITY_POLICY_SCHEMA = vol.Any(True, vol.Schema({
vol.Optional(ENTITY_DOMAINS): VALUES_SCHEMA,
vol.Optional(ENTITY_ENTITY_IDS): VALUES_SCHEMA,
}))
POLICY_SCHEMA = vol.Schema({
vol.Optional(CAT_ENTITIES): ENTITY_POLICY_SCHEMA
})
class AbstractPermissions:
"""Default permissions class."""
def check_entity(self, entity_id: str, *keys: str) -> bool:
"""Test if we can access entity."""
raise NotImplementedError
def filter_states(self, states: List[State]) -> List[State]:
"""Filter a list of states for what the user is allowed to see."""
raise NotImplementedError
class PolicyPermissions(AbstractPermissions):
"""Handle permissions."""
def __init__(self, policy: PolicyType) -> None:
"""Initialize the permission class."""
self._policy = policy
self._compiled = {} # type: Dict[str, Callable[..., bool]]
def check_entity(self, entity_id: str, *keys: str) -> bool:
"""Test if we can access entity."""
func = self._policy_func(CAT_ENTITIES, _compile_entities)
return func(entity_id, keys)
def filter_states(self, states: List[State]) -> List[State]:
"""Filter a list of states for what the user is allowed to see."""
func = self._policy_func(CAT_ENTITIES, _compile_entities)
keys = ('read',)
return [entity for entity in states if func(entity.entity_id, keys)]
def _policy_func(self, category: str,
compile_func: Callable[[CategoryType], Callable]) \
-> Callable[..., bool]:
"""Get a policy function."""
func = self._compiled.get(category)
if func:
return func
func = self._compiled[category] = compile_func(
self._policy.get(category))
return func
def __eq__(self, other: Any) -> bool:
"""Equals check."""
# pylint: disable=protected-access
return (isinstance(other, PolicyPermissions) and
other._policy == self._policy)
class _OwnerPermissions(AbstractPermissions):
"""Owner permissions."""
# pylint: disable=no-self-use
def check_entity(self, entity_id: str, *keys: str) -> bool:
"""Test if we can access entity."""
return True
def filter_states(self, states: List[State]) -> List[State]:
"""Filter a list of states for what the user is allowed to see."""
return states
OwnerPermissions = _OwnerPermissions() # pylint: disable=invalid-name
def _compile_entities(policy: CategoryType) \
-> Callable[[str, Tuple[str]], bool]:
"""Compile policy into a function that tests policy."""
# None, Empty Dict, False
if not policy:
def apply_policy_deny_all(entity_id: str, keys: Tuple[str]) -> bool:
"""Decline all."""
return False
return apply_policy_deny_all
if policy is True:
def apply_policy_allow_all(entity_id: str, keys: Tuple[str]) -> bool:
"""Approve all."""
return True
return apply_policy_allow_all
assert isinstance(policy, dict)
domains = policy.get(ENTITY_DOMAINS)
entity_ids = policy.get(ENTITY_ENTITY_IDS)
funcs = [] # type: List[Callable[[str, Tuple[str]], Union[None, bool]]]
# The order of these functions matter. The more precise are at the top.
# If a function returns None, they cannot handle it.
# If a function returns a boolean, that's the result to return.
# Setting entity_ids to a boolean is final decision for permissions
# So return right away.
if isinstance(entity_ids, bool):
def apply_entity_id_policy(entity_id: str, keys: Tuple[str]) -> bool:
"""Test if allowed entity_id."""
return entity_ids # type: ignore
return apply_entity_id_policy
if entity_ids is not None:
def allowed_entity_id(entity_id: str, keys: Tuple[str]) \
-> Union[None, bool]:
"""Test if allowed entity_id."""
return entity_ids.get(entity_id) # type: ignore
funcs.append(allowed_entity_id)
if isinstance(domains, bool):
def allowed_domain(entity_id: str, keys: Tuple[str]) \
-> Union[None, bool]:
"""Test if allowed domain."""
return domains
funcs.append(allowed_domain)
elif domains is not None:
def allowed_domain(entity_id: str, keys: Tuple[str]) \
-> Union[None, bool]:
"""Test if allowed domain."""
domain = entity_id.split(".", 1)[0]
return domains.get(domain) # type: ignore
funcs.append(allowed_domain)
# Can happen if no valid subcategories specified
if not funcs:
def apply_policy_deny_all_2(entity_id: str, keys: Tuple[str]) -> bool:
"""Decline all."""
return False
return apply_policy_deny_all_2
if len(funcs) == 1:
func = funcs[0]
def apply_policy_func(entity_id: str, keys: Tuple[str]) -> bool:
"""Apply a single policy function."""
return func(entity_id, keys) is True
return apply_policy_func
def apply_policy_funcs(entity_id: str, keys: Tuple[str]) -> bool:
"""Apply several policy functions."""
for func in funcs:
result = func(entity_id, keys)
if result is not None:
return result
return False
return apply_policy_funcs
def merge_policies(policies: List[PolicyType]) -> PolicyType:
"""Merge policies."""
new_policy = {} # type: Dict[str, CategoryType]
seen = set() # type: Set[str]
for policy in policies:
for category in policy:
if category in seen:
continue
seen.add(category)
new_policy[category] = _merge_policies([
policy.get(category) for policy in policies])
cast(PolicyType, new_policy)
return new_policy
def _merge_policies(sources: List[CategoryType]) -> CategoryType:
"""Merge a policy."""
# When merging policies, the most permissive wins.
# This means we order it like this:
# True > Dict > None
#
# True: allow everything
# Dict: specify more granular permissions
# None: no opinion
#
# If there are multiple sources with a dict as policy, we recursively
# merge each key in the source.
policy = None # type: CategoryType
seen = set() # type: Set[str]
for source in sources:
if source is None:
continue
# A source that's True will always win. Shortcut return.
if source is True:
return True
assert isinstance(source, dict)
if policy is None:
policy = {}
assert isinstance(policy, dict)
for key in source:
if key in seen:
continue
seen.add(key)
key_sources = []
for src in sources:
if isinstance(src, dict):
key_sources.append(src.get(key))
policy[key] = _merge_policies(key_sources)
return policy

View File

@ -12,6 +12,8 @@ import itertools as it
import logging import logging
from typing import Awaitable from typing import Awaitable
import voluptuous as vol
import homeassistant.core as ha import homeassistant.core as ha
import homeassistant.config as conf_util import homeassistant.config as conf_util
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
@ -21,11 +23,16 @@ from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE,
SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART,
RESTART_EXIT_CODE) RESTART_EXIT_CODE)
from homeassistant.helpers import config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SERVICE_RELOAD_CORE_CONFIG = 'reload_core_config' SERVICE_RELOAD_CORE_CONFIG = 'reload_core_config'
SERVICE_CHECK_CONFIG = 'check_config' SERVICE_CHECK_CONFIG = 'check_config'
SERVICE_UPDATE_ENTITY = 'update_entity'
SCHEMA_UPDATE_ENTITY = vol.Schema({
ATTR_ENTITY_ID: cv.entity_id
})
def is_on(hass, entity_id=None): def is_on(hass, entity_id=None):
@ -133,12 +140,20 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
if call.service == SERVICE_HOMEASSISTANT_RESTART: if call.service == SERVICE_HOMEASSISTANT_RESTART:
hass.async_create_task(hass.async_stop(RESTART_EXIT_CODE)) hass.async_create_task(hass.async_stop(RESTART_EXIT_CODE))
async def async_handle_update_service(call):
"""Service handler for updating an entity."""
await hass.helpers.entity_component.async_update_entity(
call.data[ATTR_ENTITY_ID])
hass.services.async_register( hass.services.async_register(
ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service) ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service)
hass.services.async_register( hass.services.async_register(
ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service) ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service)
hass.services.async_register( hass.services.async_register(
ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service) ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service)
hass.services.async_register(
ha.DOMAIN, SERVICE_UPDATE_ENTITY, async_handle_update_service,
schema=SCHEMA_UPDATE_ENTITY)
async def async_handle_reload_config(call): async def async_handle_reload_config(call):
"""Service handler for reloading core config.""" """Service handler for reloading core config."""

View File

@ -18,7 +18,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.13.1'] REQUIREMENTS = ['abodepy==0.14.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -0,0 +1,204 @@
"""
Each ElkM1 area will be created as a separate alarm_control_panel in HASS.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.elkm1/
"""
import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.const import (
ATTR_CODE, ATTR_ENTITY_ID, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMING, STATE_ALARM_DISARMED,
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED)
from homeassistant.components.elkm1 import (
DOMAIN as ELK_DOMAIN, create_elk_entities, ElkEntity)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_send)
DEPENDENCIES = [ELK_DOMAIN]
SIGNAL_ARM_ENTITY = 'elkm1_arm'
SIGNAL_DISPLAY_MESSAGE = 'elkm1_display_message'
ELK_ALARM_SERVICE_SCHEMA = vol.Schema({
vol.Required(ATTR_ENTITY_ID, default=[]): cv.entity_ids,
vol.Required(ATTR_CODE): vol.All(vol.Coerce(int), vol.Range(0, 999999)),
})
DISPLAY_MESSAGE_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID, default=[]): cv.entity_ids,
vol.Optional('clear', default=2): vol.In([0, 1, 2]),
vol.Optional('beep', default=False): cv.boolean,
vol.Optional('timeout', default=0): vol.Range(min=0, max=65535),
vol.Optional('line1', default=''): cv.string,
vol.Optional('line2', default=''): cv.string,
})
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the ElkM1 alarm platform."""
if discovery_info is None:
return
elk = hass.data[ELK_DOMAIN]['elk']
entities = create_elk_entities(hass, elk.areas, 'area', ElkArea, [])
async_add_entities(entities, True)
def _dispatch(signal, entity_ids, *args):
for entity_id in entity_ids:
async_dispatcher_send(
hass, '{}_{}'.format(signal, entity_id), *args)
def _arm_service(service):
entity_ids = service.data.get(ATTR_ENTITY_ID, [])
arm_level = _arm_services().get(service.service)
args = (arm_level, service.data.get(ATTR_CODE))
_dispatch(SIGNAL_ARM_ENTITY, entity_ids, *args)
for service in _arm_services():
hass.services.async_register(
alarm.DOMAIN, service, _arm_service, ELK_ALARM_SERVICE_SCHEMA)
def _display_message_service(service):
entity_ids = service.data.get(ATTR_ENTITY_ID, [])
data = service.data
args = (data['clear'], data['beep'], data['timeout'],
data['line1'], data['line2'])
_dispatch(SIGNAL_DISPLAY_MESSAGE, entity_ids, *args)
hass.services.async_register(
alarm.DOMAIN, 'elkm1_alarm_display_message',
_display_message_service, DISPLAY_MESSAGE_SERVICE_SCHEMA)
def _arm_services():
from elkm1_lib.const import ArmLevel
return {
'elkm1_alarm_arm_vacation': ArmLevel.ARMED_VACATION.value,
'elkm1_alarm_arm_home_instant': ArmLevel.ARMED_STAY_INSTANT.value,
'elkm1_alarm_arm_night_instant': ArmLevel.ARMED_NIGHT_INSTANT.value,
}
class ElkArea(ElkEntity, alarm.AlarmControlPanel):
"""Representation of an Area / Partition within the ElkM1 alarm panel."""
def __init__(self, element, elk, elk_data):
"""Initialize Area as Alarm Control Panel."""
super().__init__(element, elk, elk_data)
self._changed_by_entity_id = ''
self._state = None
async def async_added_to_hass(self):
"""Register callback for ElkM1 changes."""
await super().async_added_to_hass()
for keypad in self._elk.keypads:
keypad.add_callback(self._watch_keypad)
async_dispatcher_connect(
self.hass, '{}_{}'.format(SIGNAL_ARM_ENTITY, self.entity_id),
self._arm_service)
async_dispatcher_connect(
self.hass, '{}_{}'.format(SIGNAL_DISPLAY_MESSAGE, self.entity_id),
self._display_message)
def _watch_keypad(self, keypad, changeset):
if keypad.area != self._element.index:
return
if changeset.get('last_user') is not None:
self._changed_by_entity_id = self.hass.data[
ELK_DOMAIN]['keypads'].get(keypad.index, '')
self.async_schedule_update_ha_state(True)
@property
def code_format(self):
"""Return the alarm code format."""
return '^[0-9]{4}([0-9]{2})?$'
@property
def state(self):
"""Return the state of the element."""
return self._state
@property
def device_state_attributes(self):
"""Attributes of the area."""
from elkm1_lib.const import AlarmState, ArmedStatus, ArmUpState
attrs = self.initial_attrs()
elmt = self._element
attrs['is_exit'] = elmt.is_exit
attrs['timer1'] = elmt.timer1
attrs['timer2'] = elmt.timer2
if elmt.armed_status is not None:
attrs['armed_status'] = \
ArmedStatus(elmt.armed_status).name.lower()
if elmt.arm_up_state is not None:
attrs['arm_up_state'] = ArmUpState(elmt.arm_up_state).name.lower()
if elmt.alarm_state is not None:
attrs['alarm_state'] = AlarmState(elmt.alarm_state).name.lower()
attrs['changed_by_entity_id'] = self._changed_by_entity_id
return attrs
def _element_changed(self, element, changeset):
from elkm1_lib.const import ArmedStatus
elk_state_to_hass_state = {
ArmedStatus.DISARMED.value: STATE_ALARM_DISARMED,
ArmedStatus.ARMED_AWAY.value: STATE_ALARM_ARMED_AWAY,
ArmedStatus.ARMED_STAY.value: STATE_ALARM_ARMED_HOME,
ArmedStatus.ARMED_STAY_INSTANT.value: STATE_ALARM_ARMED_HOME,
ArmedStatus.ARMED_TO_NIGHT.value: STATE_ALARM_ARMED_NIGHT,
ArmedStatus.ARMED_TO_NIGHT_INSTANT.value: STATE_ALARM_ARMED_NIGHT,
ArmedStatus.ARMED_TO_VACATION.value: STATE_ALARM_ARMED_AWAY,
}
if self._element.alarm_state is None:
self._state = None
elif self._area_is_in_alarm_state():
self._state = STATE_ALARM_TRIGGERED
elif self._entry_exit_timer_is_running():
self._state = STATE_ALARM_ARMING \
if self._element.is_exit else STATE_ALARM_PENDING
else:
self._state = elk_state_to_hass_state[self._element.armed_status]
def _entry_exit_timer_is_running(self):
return self._element.timer1 > 0 or self._element.timer2 > 0
def _area_is_in_alarm_state(self):
from elkm1_lib.const import AlarmState
return self._element.alarm_state >= AlarmState.FIRE_ALARM.value
async def async_alarm_disarm(self, code=None):
"""Send disarm command."""
self._element.disarm(int(code))
async def async_alarm_arm_home(self, code=None):
"""Send arm home command."""
from elkm1_lib.const import ArmLevel
self._element.arm(ArmLevel.ARMED_STAY.value, int(code))
async def async_alarm_arm_away(self, code=None):
"""Send arm away command."""
from elkm1_lib.const import ArmLevel
self._element.arm(ArmLevel.ARMED_AWAY.value, int(code))
async def async_alarm_arm_night(self, code=None):
"""Send arm night command."""
from elkm1_lib.const import ArmLevel
self._element.arm(ArmLevel.ARMED_NIGHT.value, int(code))
async def _arm_service(self, arm_level, code):
self._element.arm(arm_level, code)
async def _display_message(self, clear, beep, timeout, line1, line2):
"""Display a message on all keypads for the area."""
self._element.display_message(clear, beep, timeout, line1, line2)

View File

@ -79,3 +79,55 @@ ifttt_push_alarm_state:
state: state:
description: The state to which the alarm control panel has to be set. description: The state to which the alarm control panel has to be set.
example: 'armed_night' example: 'armed_night'
elkm1_alarm_arm_vacation:
description: Arm the ElkM1 in vacation mode.
fields:
entity_id:
description: Name of alarm control panel to arm.
example: 'alarm_control_panel.main'
code:
description: An code to arm the alarm control panel.
example: 1234
elkm1_alarm_arm_home_instant:
description: Arm the ElkM1 in home instant mode.
fields:
entity_id:
description: Name of alarm control panel to arm.
example: 'alarm_control_panel.main'
code:
description: An code to arm the alarm control panel.
example: 1234
elkm1_alarm_arm_night_instant:
description: Arm the ElkM1 in night instant mode.
fields:
entity_id:
description: Name of alarm control panel to arm.
example: 'alarm_control_panel.main'
code:
description: An code to arm the alarm control panel.
example: 1234
elkm1_alarm_display_message:
description: Display a message on all of the ElkM1 keypads for an area.
fields:
entity_id:
description: Name of alarm control panel to display messages on.
example: 'alarm_control_panel.main'
clear:
description: 0=clear message, 1=clear message with * key, 2=Display until timeout; default 2
example: 1
beep:
description: 0=no beep, 1=beep; default 0
example: 1
timeout:
description: Time to display message, 0=forever, max 65535, default 0
example: 4242
line1:
description: Up to 16 characters of text (truncated if too long). Default blank.
example: The answer to life,
line2:
description: Up to 16 characters of text (truncated if too long). Default blank.
example: the universe, and everything.

View File

@ -1,5 +1,5 @@
""" """
Interfaces with SimpliSafe alarm control panel. This platform provides alarm control functionality for SimpliSafe.
For more details about this platform, please refer to the documentation at 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/
@ -7,86 +7,44 @@ https://home-assistant.io/components/alarm_control_panel.simplisafe/
import logging import logging
import re import re
import voluptuous as vol from homeassistant.components.alarm_control_panel import AlarmControlPanel
from homeassistant.components.simplisafe.const import (
from homeassistant.components.alarm_control_panel import ( DATA_CLIENT, DOMAIN, TOPIC_UPDATE)
PLATFORM_SCHEMA, AlarmControlPanel)
from homeassistant.const import ( from homeassistant.const import (
CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY, CONF_CODE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED) STATE_ALARM_DISARMED)
from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.core import callback
from homeassistant.util.json import load_json, save_json from homeassistant.helpers.dispatcher import async_dispatcher_connect
REQUIREMENTS = ['simplisafe-python==3.1.2']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_ALARM_ACTIVE = "alarm_active" ATTR_ALARM_ACTIVE = 'alarm_active'
ATTR_TEMPERATURE = "temperature" ATTR_TEMPERATURE = 'temperature'
DATA_FILE = '.simplisafe'
DEFAULT_NAME = 'SimpliSafe'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_CODE): cv.string,
})
async def async_setup_platform( async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None): hass, config, async_add_entities, discovery_info=None):
"""Set up the SimpliSafe platform.""" """Set up a SimpliSafe alarm control panel based on existing config."""
from simplipy import API pass
from simplipy.errors import SimplipyError
username = config[CONF_USERNAME]
password = config[CONF_PASSWORD]
name = config.get(CONF_NAME)
code = config.get(CONF_CODE)
websession = aiohttp_client.async_get_clientsession(hass) async def async_setup_entry(hass, entry, async_add_entities):
"""Set up a SimpliSafe alarm control panel based on a config entry."""
config_data = await hass.async_add_executor_job( systems = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id]
load_json, hass.config.path(DATA_FILE)) async_add_entities([
SimpliSafeAlarm(system, entry.data.get(CONF_CODE))
try: for system in systems
if config_data: ], True)
try:
simplisafe = await API.login_via_token(
config_data['refresh_token'], websession)
_LOGGER.debug('Logging in with refresh token')
except SimplipyError:
_LOGGER.info('Refresh token expired; attempting credentials')
simplisafe = await API.login_via_credentials(
username, password, websession)
else:
simplisafe = await API.login_via_credentials(
username, password, websession)
_LOGGER.debug('Logging in with credentials')
except SimplipyError as err:
_LOGGER.error("There was an error during setup: %s", err)
return
config_data = {'refresh_token': simplisafe.refresh_token}
await hass.async_add_executor_job(
save_json, hass.config.path(DATA_FILE), config_data)
systems = await simplisafe.get_systems()
async_add_entities(
[SimpliSafeAlarm(system, name, code) for system in systems], True)
class SimpliSafeAlarm(AlarmControlPanel): class SimpliSafeAlarm(AlarmControlPanel):
"""Representation of a SimpliSafe alarm.""" """Representation of a SimpliSafe alarm."""
def __init__(self, system, name, code): def __init__(self, system, code):
"""Initialize the SimpliSafe alarm.""" """Initialize the SimpliSafe alarm."""
self._async_unsub_dispatcher_connect = None
self._attrs = {} self._attrs = {}
self._code = str(code) if code else None self._code = code
self._name = name
self._system = system self._system = system
self._state = None self._state = None
@ -98,9 +56,7 @@ class SimpliSafeAlarm(AlarmControlPanel):
@property @property
def name(self): def name(self):
"""Return the name of the device.""" """Return the name of the device."""
if self._name: return self._system.address
return self._name
return 'Alarm {}'.format(self._system.system_id)
@property @property
def code_format(self): def code_format(self):
@ -128,6 +84,21 @@ class SimpliSafeAlarm(AlarmControlPanel):
_LOGGER.warning("Wrong code entered for %s", state) _LOGGER.warning("Wrong code entered for %s", state)
return check return check
async def async_added_to_hass(self):
"""Register callbacks."""
@callback
def update():
"""Update the state."""
self.async_schedule_update_ha_state(True)
self._async_unsub_dispatcher_connect = async_dispatcher_connect(
self.hass, TOPIC_UPDATE, update)
async def async_will_remove_from_hass(self) -> None:
"""Disconnect dispatcher listener when removed."""
if self._async_unsub_dispatcher_connect:
self._async_unsub_dispatcher_connect()
async def async_alarm_disarm(self, code=None): async def async_alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""
if not self._validate_code(code, 'disarming'): if not self._validate_code(code, 'disarming'):
@ -151,22 +122,24 @@ class SimpliSafeAlarm(AlarmControlPanel):
async def async_update(self): async def async_update(self):
"""Update alarm status.""" """Update alarm status."""
await self._system.update() from simplipy.system import SystemStates
if self._system.state == self._system.SystemStates.off: await self._system.update()
self._state = STATE_ALARM_DISARMED
elif self._system.state in (
self._system.SystemStates.home,
self._system.SystemStates.home_count):
self._state = STATE_ALARM_ARMED_HOME
elif self._system.state in (
self._system.SystemStates.away,
self._system.SystemStates.away_count,
self._system.SystemStates.exit_delay):
self._state = STATE_ALARM_ARMED_AWAY
else:
self._state = None
self._attrs[ATTR_ALARM_ACTIVE] = self._system.alarm_going_off self._attrs[ATTR_ALARM_ACTIVE] = self._system.alarm_going_off
if self._system.temperature: if self._system.temperature:
self._attrs[ATTR_TEMPERATURE] = self._system.temperature self._attrs[ATTR_TEMPERATURE] = self._system.temperature
if self._system.state == SystemStates.error:
return
if self._system.state == SystemStates.off:
self._state = STATE_ALARM_DISARMED
elif self._system.state in (SystemStates.home,
SystemStates.home_count):
self._state = STATE_ALARM_ARMED_HOME
elif self._system.state in (SystemStates.away, SystemStates.away_count,
SystemStates.exit_delay):
self._state = STATE_ALARM_ARMED_AWAY
else:
self._state = None

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.18'] REQUIREMENTS = ['total_connect_client==0.20']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -49,10 +49,9 @@ def setup(hass, config):
# It doesn't really matter why we're not able to get the status, just that # It doesn't really matter why we're not able to get the status, just that
# we can't. # we can't.
# pylint: disable=broad-except
try: try:
DATA.update(no_throttle=True) DATA.update(no_throttle=True)
except Exception: except Exception: # pylint: disable=broad-except
_LOGGER.exception("Failure while testing APCUPSd status retrieval.") _LOGGER.exception("Failure while testing APCUPSd status retrieval.")
return False return False
return True return True

View File

@ -16,7 +16,7 @@ from homeassistant.const import (
from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.event import track_time_interval
from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.dispatcher import dispatcher_send
REQUIREMENTS = ['pyarlo==0.2.0'] REQUIREMENTS = ['pyarlo==0.2.2']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -81,7 +81,7 @@ def setup(hass, config):
def hub_refresh(event_time): def hub_refresh(event_time):
"""Call ArloHub to refresh information.""" """Call ArloHub to refresh information."""
_LOGGER.info("Updating Arlo Hub component") _LOGGER.debug("Updating Arlo Hub component")
hass.data[DATA_ARLO].update(update_cameras=True, hass.data[DATA_ARLO].update(update_cameras=True,
update_base_station=True) update_base_station=True)
dispatcher_send(hass, SIGNAL_UPDATE_ARLO) dispatcher_send(hass, SIGNAL_UPDATE_ARLO)

View File

@ -31,7 +31,7 @@ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_PORT): int, vol.Required(CONF_PORT): cv.port,
}), }),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)

View File

@ -4,7 +4,6 @@ Support for August devices.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/august/ https://home-assistant.io/components/august/
""" """
import logging import logging
from datetime import timedelta from datetime import timedelta
@ -124,6 +123,7 @@ def setup_august(hass, config, api, authenticator):
return True return True
if state == AuthenticationState.BAD_PASSWORD: if state == AuthenticationState.BAD_PASSWORD:
_LOGGER.error("Invalid password provided")
return False return False
if state == AuthenticationState.REQUIRES_VALIDATION: if state == AuthenticationState.REQUIRES_VALIDATION:
request_configuration(hass, config, api, authenticator) request_configuration(hass, config, api, authenticator)
@ -165,6 +165,7 @@ class AugustData:
self._doorbell_detail_by_id = {} self._doorbell_detail_by_id = {}
self._lock_status_by_id = {} self._lock_status_by_id = {}
self._lock_detail_by_id = {} self._lock_detail_by_id = {}
self._door_state_by_id = {}
self._activities_by_id = {} self._activities_by_id = {}
@property @property
@ -184,6 +185,7 @@ class AugustData:
def get_device_activities(self, device_id, *activity_types): def get_device_activities(self, device_id, *activity_types):
"""Return a list of activities.""" """Return a list of activities."""
_LOGGER.debug("Getting device activities")
self._update_device_activities() self._update_device_activities()
activities = self._activities_by_id.get(device_id, []) activities = self._activities_by_id.get(device_id, [])
@ -199,6 +201,7 @@ class AugustData:
@Throttle(MIN_TIME_BETWEEN_UPDATES) @Throttle(MIN_TIME_BETWEEN_UPDATES)
def _update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT): def _update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT):
"""Update data object with latest from August API.""" """Update data object with latest from August API."""
_LOGGER.debug("Updating device activities")
for house_id in self.house_ids: for house_id in self.house_ids:
activities = self._api.get_house_activities(self._access_token, activities = self._api.get_house_activities(self._access_token,
house_id, house_id,
@ -218,14 +221,21 @@ class AugustData:
def _update_doorbells(self): def _update_doorbells(self):
detail_by_id = {} detail_by_id = {}
_LOGGER.debug("Start retrieving doorbell details")
for doorbell in self._doorbells: for doorbell in self._doorbells:
_LOGGER.debug("Updating status for %s",
doorbell.device_name)
detail_by_id[doorbell.device_id] = self._api.get_doorbell_detail( detail_by_id[doorbell.device_id] = self._api.get_doorbell_detail(
self._access_token, doorbell.device_id) self._access_token, doorbell.device_id)
_LOGGER.debug("Completed retrieving doorbell details")
self._doorbell_detail_by_id = detail_by_id self._doorbell_detail_by_id = detail_by_id
def get_lock_status(self, lock_id): def get_lock_status(self, lock_id):
"""Return lock status.""" """Return status if the door is locked or unlocked.
This is status for the lock itself.
"""
self._update_locks() self._update_locks()
return self._lock_status_by_id.get(lock_id) return self._lock_status_by_id.get(lock_id)
@ -234,17 +244,43 @@ class AugustData:
self._update_locks() self._update_locks()
return self._lock_detail_by_id.get(lock_id) return self._lock_detail_by_id.get(lock_id)
def get_door_state(self, lock_id):
"""Return status if the door is open or closed.
This is the status from the door sensor.
"""
self._update_doors()
return self._door_state_by_id.get(lock_id)
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def _update_doors(self):
state_by_id = {}
_LOGGER.debug("Start retrieving door status")
for lock in self._locks:
_LOGGER.debug("Updating status for %s",
lock.device_name)
state_by_id[lock.device_id] = self._api.get_lock_door_status(
self._access_token, lock.device_id)
_LOGGER.debug("Completed retrieving door status")
self._door_state_by_id = state_by_id
@Throttle(MIN_TIME_BETWEEN_UPDATES) @Throttle(MIN_TIME_BETWEEN_UPDATES)
def _update_locks(self): def _update_locks(self):
status_by_id = {} status_by_id = {}
detail_by_id = {} detail_by_id = {}
_LOGGER.debug("Start retrieving locks status")
for lock in self._locks: for lock in self._locks:
_LOGGER.debug("Updating status for %s",
lock.device_name)
status_by_id[lock.device_id] = self._api.get_lock_status( status_by_id[lock.device_id] = self._api.get_lock_status(
self._access_token, lock.device_id) self._access_token, lock.device_id)
detail_by_id[lock.device_id] = self._api.get_lock_detail( detail_by_id[lock.device_id] = self._api.get_lock_detail(
self._access_token, lock.device_id) self._access_token, lock.device_id)
_LOGGER.debug("Completed retrieving locks status")
self._lock_status_by_id = status_by_id self._lock_status_by_id = status_by_id
self._lock_detail_by_id = detail_by_id self._lock_detail_by_id = detail_by_id

View File

@ -0,0 +1,34 @@
{
"mfa_setup": {
"notify": {
"abort": {
"no_available_service": "Nu sunt disponibile servicii de notificare."
},
"error": {
"invalid_code": "Cod invalid, va rugam incercati din nou."
},
"step": {
"init": {
"description": "Selecta\u021bi unul dintre serviciile de notificare:",
"title": "Configura\u021bi o parol\u0103 unic\u0103 livrat\u0103 de o component\u0103 de notificare"
},
"setup": {
"description": "O parol\u0103 unic\u0103 a fost trimis\u0103 prin **notify.{notify_service}**. Introduce\u021bi parola mai jos:",
"title": "Verifica\u021bi configurarea"
}
},
"title": "Notifica\u021bi o parol\u0103 unic\u0103"
},
"totp": {
"error": {
"invalid_code": "Cod invalid, va rugam incercati din nou. Dac\u0103 primi\u021bi aceast\u0103 eroare \u00een mod consecvent, asigura\u021bi-v\u0103 c\u0103 ceasul sistemului dvs. Home Assistant este corect."
},
"step": {
"init": {
"title": "Configura\u021bi autentificarea cu doi factori utiliz\u00e2nd TOTP"
}
},
"title": "TOTP"
}
}
}

View File

@ -0,0 +1,74 @@
"""
Offer geo location automation rules.
For more details about this automation trigger, please refer to the
documentation at
https://home-assistant.io/docs/automation/trigger/#geo-location-trigger
"""
import voluptuous as vol
from homeassistant.components.geo_location import DOMAIN
from homeassistant.core import callback
from homeassistant.const import (
CONF_EVENT, CONF_PLATFORM, CONF_SOURCE, CONF_ZONE, EVENT_STATE_CHANGED)
from homeassistant.helpers import (
condition, config_validation as cv)
from homeassistant.helpers.config_validation import entity_domain
EVENT_ENTER = 'enter'
EVENT_LEAVE = 'leave'
DEFAULT_EVENT = EVENT_ENTER
TRIGGER_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'geo_location',
vol.Required(CONF_SOURCE): cv.string,
vol.Required(CONF_ZONE): entity_domain('zone'),
vol.Required(CONF_EVENT, default=DEFAULT_EVENT):
vol.Any(EVENT_ENTER, EVENT_LEAVE),
})
def source_match(state, source):
"""Check if the state matches the provided source."""
return state and state.attributes.get('source') == source
async def async_trigger(hass, config, action):
"""Listen for state changes based on configuration."""
source = config.get(CONF_SOURCE).lower()
zone_entity_id = config.get(CONF_ZONE)
trigger_event = config.get(CONF_EVENT)
@callback
def state_change_listener(event):
"""Handle specific state changes."""
# Skip if the event is not a geo_location entity.
if not event.data.get('entity_id').startswith(DOMAIN):
return
# Skip if the event's source does not match the trigger's source.
from_state = event.data.get('old_state')
to_state = event.data.get('new_state')
if not source_match(from_state, source) \
and not source_match(to_state, source):
return
zone_state = hass.states.get(zone_entity_id)
from_match = condition.zone(hass, zone_state, from_state)
to_match = condition.zone(hass, zone_state, to_state)
# pylint: disable=too-many-boolean-expressions
if trigger_event == EVENT_ENTER and not from_match and to_match or \
trigger_event == EVENT_LEAVE and from_match and not to_match:
hass.async_run_job(action({
'trigger': {
'platform': 'geo_location',
'source': source,
'entity_id': event.data.get('entity_id'),
'from_state': from_state,
'to_state': to_state,
'zone': zone_state,
'event': trigger_event,
},
}, context=event.context))
return hass.bus.async_listen(EVENT_STATE_CHANGED, state_change_listener)

View File

@ -11,13 +11,12 @@ from aiohttp import hdrs
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.const import CONF_PLATFORM from homeassistant.const import CONF_PLATFORM, CONF_WEBHOOK_ID
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ('webhook',) DEPENDENCIES = ('webhook',)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_WEBHOOK_ID = 'webhook_id'
TRIGGER_SCHEMA = vol.Schema({ TRIGGER_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'webhook', vol.Required(CONF_PLATFORM): 'webhook',

View File

@ -38,6 +38,7 @@ AXIS_INCLUDE = EVENT_TYPES + PLATFORMS
AXIS_DEFAULT_HOST = '192.168.0.90' AXIS_DEFAULT_HOST = '192.168.0.90'
AXIS_DEFAULT_USERNAME = 'root' AXIS_DEFAULT_USERNAME = 'root'
AXIS_DEFAULT_PASSWORD = 'pass' AXIS_DEFAULT_PASSWORD = 'pass'
DEFAULT_PORT = 80
DEVICE_SCHEMA = vol.Schema({ DEVICE_SCHEMA = vol.Schema({
vol.Required(CONF_INCLUDE): vol.Required(CONF_INCLUDE):
@ -47,7 +48,7 @@ DEVICE_SCHEMA = vol.Schema({
vol.Optional(CONF_USERNAME, default=AXIS_DEFAULT_USERNAME): cv.string, vol.Optional(CONF_USERNAME, default=AXIS_DEFAULT_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD, default=AXIS_DEFAULT_PASSWORD): cv.string, vol.Optional(CONF_PASSWORD, default=AXIS_DEFAULT_PASSWORD): cv.string,
vol.Optional(CONF_TRIGGER_TIME, default=0): cv.positive_int, vol.Optional(CONF_TRIGGER_TIME, default=0): cv.positive_int,
vol.Optional(CONF_PORT, default=80): cv.positive_int, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(ATTR_LOCATION, default=''): cv.string, vol.Optional(ATTR_LOCATION, default=''): cv.string,
}) })

View File

@ -4,16 +4,26 @@ Support for August binary sensors.
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/sensor.august/ https://home-assistant.io/components/sensor.august/
""" """
import logging
from datetime import timedelta, datetime from datetime import timedelta, datetime
from homeassistant.components.august import DATA_AUGUST from homeassistant.components.august import DATA_AUGUST
from homeassistant.components.binary_sensor import (BinarySensorDevice) from homeassistant.components.binary_sensor import (BinarySensorDevice)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['august'] DEPENDENCIES = ['august']
SCAN_INTERVAL = timedelta(seconds=5) SCAN_INTERVAL = timedelta(seconds=5)
def _retrieve_door_state(data, lock):
"""Get the latest state of the DoorSense sensor."""
from august.lock import LockDoorStatus
doorstate = data.get_door_state(lock.device_id)
return doorstate == LockDoorStatus.OPEN
def _retrieve_online_state(data, doorbell): def _retrieve_online_state(data, doorbell):
"""Get the latest state of the sensor.""" """Get the latest state of the sensor."""
detail = data.get_doorbell_detail(doorbell.device_id) detail = data.get_doorbell_detail(doorbell.device_id)
@ -46,7 +56,11 @@ def _activity_time_based_state(data, doorbell, activity_types):
# Sensor types: Name, device_class, state_provider # Sensor types: Name, device_class, state_provider
SENSOR_TYPES = { SENSOR_TYPES_DOOR = {
'door_open': ['Open', 'door', _retrieve_door_state],
}
SENSOR_TYPES_DOORBELL = {
'doorbell_ding': ['Ding', 'occupancy', _retrieve_ding_state], 'doorbell_ding': ['Ding', 'occupancy', _retrieve_ding_state],
'doorbell_motion': ['Motion', 'motion', _retrieve_motion_state], 'doorbell_motion': ['Motion', 'motion', _retrieve_motion_state],
'doorbell_online': ['Online', 'connectivity', _retrieve_online_state], 'doorbell_online': ['Online', 'connectivity', _retrieve_online_state],
@ -58,14 +72,78 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
data = hass.data[DATA_AUGUST] data = hass.data[DATA_AUGUST]
devices = [] devices = []
from august.lock import LockDoorStatus
for door in data.locks:
for sensor_type in SENSOR_TYPES_DOOR:
state_provider = SENSOR_TYPES_DOOR[sensor_type][2]
if state_provider(data, door) is LockDoorStatus.UNKNOWN:
_LOGGER.debug(
"Not adding sensor class %s for lock %s ",
SENSOR_TYPES_DOOR[sensor_type][1], door.device_name
)
continue
_LOGGER.debug(
"Adding sensor class %s for %s",
SENSOR_TYPES_DOOR[sensor_type][1], door.device_name
)
devices.append(AugustDoorBinarySensor(data, sensor_type, door))
for doorbell in data.doorbells: for doorbell in data.doorbells:
for sensor_type in SENSOR_TYPES: for sensor_type in SENSOR_TYPES_DOORBELL:
devices.append(AugustBinarySensor(data, sensor_type, doorbell)) _LOGGER.debug("Adding doorbell sensor class %s for %s",
SENSOR_TYPES_DOORBELL[sensor_type][1],
doorbell.device_name)
devices.append(
AugustDoorbellBinarySensor(data, sensor_type,
doorbell)
)
add_entities(devices, True) add_entities(devices, True)
class AugustBinarySensor(BinarySensorDevice): class AugustDoorBinarySensor(BinarySensorDevice):
"""Representation of an August Door binary sensor."""
def __init__(self, data, sensor_type, door):
"""Initialize the sensor."""
self._data = data
self._sensor_type = sensor_type
self._door = door
self._state = None
self._available = False
@property
def available(self):
"""Return the availability of this sensor."""
return self._available
@property
def is_on(self):
"""Return true if the binary sensor is on."""
return self._state
@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
return SENSOR_TYPES_DOOR[self._sensor_type][1]
@property
def name(self):
"""Return the name of the binary sensor."""
return "{} {}".format(self._door.device_name,
SENSOR_TYPES_DOOR[self._sensor_type][0])
def update(self):
"""Get the latest state of the sensor."""
state_provider = SENSOR_TYPES_DOOR[self._sensor_type][2]
self._state = state_provider(self._data, self._door)
from august.lock import LockDoorStatus
self._available = self._state != LockDoorStatus.UNKNOWN
class AugustDoorbellBinarySensor(BinarySensorDevice):
"""Representation of an August binary sensor.""" """Representation of an August binary sensor."""
def __init__(self, data, sensor_type, doorbell): def __init__(self, data, sensor_type, doorbell):
@ -83,15 +161,15 @@ class AugustBinarySensor(BinarySensorDevice):
@property @property
def device_class(self): def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES.""" """Return the class of this device, from component DEVICE_CLASSES."""
return SENSOR_TYPES[self._sensor_type][1] return SENSOR_TYPES_DOORBELL[self._sensor_type][1]
@property @property
def name(self): def name(self):
"""Return the name of the binary sensor.""" """Return the name of the binary sensor."""
return "{} {}".format(self._doorbell.device_name, return "{} {}".format(self._doorbell.device_name,
SENSOR_TYPES[self._sensor_type][0]) SENSOR_TYPES_DOORBELL[self._sensor_type][0])
def update(self): def update(self):
"""Get the latest state of the sensor.""" """Get the latest state of the sensor."""
state_provider = SENSOR_TYPES[self._sensor_type][2] state_provider = SENSOR_TYPES_DOORBELL[self._sensor_type][2]
self._state = state_provider(self._data, self._doorbell) self._state = state_provider(self._data, self._doorbell)

View File

@ -50,6 +50,12 @@ class BloomSkySensor(BinarySensorDevice):
self._sensor_name = sensor_name self._sensor_name = sensor_name
self._name = '{} {}'.format(device['DeviceName'], sensor_name) self._name = '{} {}'.format(device['DeviceName'], sensor_name)
self._state = None self._state = None
self._unique_id = '{}-{}'.format(self._device_id, self._sensor_name)
@property
def unique_id(self):
"""Return a unique ID."""
return self._unique_id
@property @property
def name(self): def name(self):

View File

@ -8,6 +8,7 @@ import logging
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN
from homeassistant.const import LENGTH_KILOMETERS
DEPENDENCIES = ['bmw_connected_drive'] DEPENDENCIES = ['bmw_connected_drive']
@ -117,7 +118,8 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
result['lights_parking'] = vehicle_state.parking_lights.value result['lights_parking'] = vehicle_state.parking_lights.value
elif self._attribute == 'condition_based_services': elif self._attribute == 'condition_based_services':
for report in vehicle_state.condition_based_services: for report in vehicle_state.condition_based_services:
result.update(self._format_cbs_report(report)) result.update(
self._format_cbs_report(report))
elif self._attribute == 'check_control_messages': elif self._attribute == 'check_control_messages':
check_control_messages = vehicle_state.check_control_messages check_control_messages = vehicle_state.check_control_messages
if not check_control_messages: if not check_control_messages:
@ -175,8 +177,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
self._state = (vehicle_state._attributes['connectionStatus'] == self._state = (vehicle_state._attributes['connectionStatus'] ==
'CONNECTED') 'CONNECTED')
@staticmethod def _format_cbs_report(self, report):
def _format_cbs_report(report):
result = {} result = {}
service_type = report.service_type.lower().replace('_', ' ') service_type = report.service_type.lower().replace('_', ' ')
result['{} status'.format(service_type)] = report.state.value result['{} status'.format(service_type)] = report.state.value
@ -184,8 +185,10 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
result['{} date'.format(service_type)] = \ result['{} date'.format(service_type)] = \
report.due_date.strftime('%Y-%m-%d') report.due_date.strftime('%Y-%m-%d')
if report.due_distance is not None: if report.due_distance is not None:
result['{} distance'.format(service_type)] = \ distance = round(self.hass.config.units.length(
'{} km'.format(report.due_distance) report.due_distance, LENGTH_KILOMETERS))
result['{} distance'.format(service_type)] = '{} {}'.format(
distance, self.hass.config.units.length_unit)
return result return result
def update_callback(self): def update_callback(self):

View File

@ -69,7 +69,7 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities):
entities = [] entities = []
for device_name in discovery_info[ATTR_DISCOVER_DEVICES]: for device_name in discovery_info[ATTR_DISCOVER_DEVICES]:
device = hass.data[DATA_KNX].xknx.devices[device_name] device = hass.data[DATA_KNX].xknx.devices[device_name]
entities.append(KNXBinarySensor(hass, device)) entities.append(KNXBinarySensor(device))
async_add_entities(entities) async_add_entities(entities)
@ -87,7 +87,7 @@ def async_add_entities_config(hass, config, async_add_entities):
reset_after=config.get(CONF_RESET_AFTER)) reset_after=config.get(CONF_RESET_AFTER))
hass.data[DATA_KNX].xknx.devices.add(binary_sensor) hass.data[DATA_KNX].xknx.devices.add(binary_sensor)
entity = KNXBinarySensor(hass, binary_sensor) entity = KNXBinarySensor(binary_sensor)
automations = config.get(CONF_AUTOMATION) automations = config.get(CONF_AUTOMATION)
if automations is not None: if automations is not None:
for automation in automations: for automation in automations:
@ -103,11 +103,9 @@ def async_add_entities_config(hass, config, async_add_entities):
class KNXBinarySensor(BinarySensorDevice): class KNXBinarySensor(BinarySensorDevice):
"""Representation of a KNX binary sensor.""" """Representation of a KNX binary sensor."""
def __init__(self, hass, device): def __init__(self, device):
"""Initialize of KNX binary sensor.""" """Initialize of KNX binary sensor."""
self.device = device self.device = device
self.hass = hass
self.async_register_callbacks()
self.automations = [] self.automations = []
@callback @callback
@ -118,6 +116,10 @@ class KNXBinarySensor(BinarySensorDevice):
await self.async_update_ha_state() await self.async_update_ha_state()
self.device.register_device_updated_cb(after_update_callback) self.device.register_device_updated_cb(after_update_callback)
async def async_added_to_hass(self):
"""Store register state change callback."""
self.async_register_callbacks()
@property @property
def name(self): def name(self):
"""Return the name of the KNX device.""" """Return the name of the KNX device."""

View File

@ -15,19 +15,21 @@ from homeassistant.components.binary_sensor import (
BinarySensorDevice, DEVICE_CLASSES_SCHEMA) BinarySensorDevice, DEVICE_CLASSES_SCHEMA)
from homeassistant.const import ( from homeassistant.const import (
CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON, CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON,
CONF_PAYLOAD_OFF, CONF_DEVICE_CLASS) CONF_PAYLOAD_OFF, CONF_DEVICE_CLASS, CONF_DEVICE)
from homeassistant.components.mqtt import ( from homeassistant.components.mqtt import (
ATTR_DISCOVERY_HASH, CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC, ATTR_DISCOVERY_HASH, CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC,
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS,
MqttAvailability, MqttDiscoveryUpdate) MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo)
from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
import homeassistant.helpers.event as evt
from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.helpers.typing import HomeAssistantType, ConfigType
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'MQTT Binary sensor' DEFAULT_NAME = 'MQTT Binary sensor'
CONF_OFF_DELAY = 'off_delay'
CONF_UNIQUE_ID = 'unique_id' CONF_UNIQUE_ID = 'unique_id'
DEFAULT_PAYLOAD_OFF = 'OFF' DEFAULT_PAYLOAD_OFF = 'OFF'
DEFAULT_PAYLOAD_ON = 'ON' DEFAULT_PAYLOAD_ON = 'ON'
@ -41,9 +43,12 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
vol.Optional(CONF_OFF_DELAY):
vol.All(vol.Coerce(int), vol.Range(min=0)),
# Integrations shouldn't never expose unique_id through configuration # Integrations shouldn't never expose unique_id through configuration
# this here is an exception because MQTT is a msg transport, not a protocol # this here is an exception because MQTT is a msg transport, not a protocol
vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA,
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
@ -80,28 +85,32 @@ async def _async_setup_entity(hass, config, async_add_entities,
config.get(CONF_DEVICE_CLASS), config.get(CONF_DEVICE_CLASS),
config.get(CONF_QOS), config.get(CONF_QOS),
config.get(CONF_FORCE_UPDATE), config.get(CONF_FORCE_UPDATE),
config.get(CONF_OFF_DELAY),
config.get(CONF_PAYLOAD_ON), config.get(CONF_PAYLOAD_ON),
config.get(CONF_PAYLOAD_OFF), config.get(CONF_PAYLOAD_OFF),
config.get(CONF_PAYLOAD_AVAILABLE), config.get(CONF_PAYLOAD_AVAILABLE),
config.get(CONF_PAYLOAD_NOT_AVAILABLE), config.get(CONF_PAYLOAD_NOT_AVAILABLE),
value_template, value_template,
config.get(CONF_UNIQUE_ID), config.get(CONF_UNIQUE_ID),
config.get(CONF_DEVICE),
discovery_hash, discovery_hash,
)]) )])
class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate, class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
BinarySensorDevice): MqttEntityDeviceInfo, BinarySensorDevice):
"""Representation a binary sensor that is updated by MQTT.""" """Representation a binary sensor that is updated by MQTT."""
def __init__(self, name, state_topic, availability_topic, device_class, def __init__(self, name, state_topic, availability_topic, device_class,
qos, force_update, payload_on, payload_off, payload_available, qos, force_update, off_delay, payload_on, payload_off,
payload_not_available, value_template, payload_available, payload_not_available, value_template,
unique_id: Optional[str], discovery_hash): unique_id: Optional[str], device_config: Optional[ConfigType],
discovery_hash):
"""Initialize the MQTT binary sensor.""" """Initialize the MQTT binary sensor."""
MqttAvailability.__init__(self, availability_topic, qos, MqttAvailability.__init__(self, availability_topic, qos,
payload_available, payload_not_available) payload_available, payload_not_available)
MqttDiscoveryUpdate.__init__(self, discovery_hash) MqttDiscoveryUpdate.__init__(self, discovery_hash)
MqttEntityDeviceInfo.__init__(self, device_config)
self._name = name self._name = name
self._state = None self._state = None
self._state_topic = state_topic self._state_topic = state_topic
@ -110,9 +119,11 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
self._payload_off = payload_off self._payload_off = payload_off
self._qos = qos self._qos = qos
self._force_update = force_update self._force_update = force_update
self._off_delay = off_delay
self._template = value_template self._template = value_template
self._unique_id = unique_id self._unique_id = unique_id
self._discovery_hash = discovery_hash self._discovery_hash = discovery_hash
self._delay_listener = None
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Subscribe mqtt events.""" """Subscribe mqtt events."""
@ -135,6 +146,20 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
self._name, self._state_topic) self._name, self._state_topic)
return return
if (self._state and self._off_delay is not None):
@callback
def off_delay_listener(now):
"""Switch device off after a delay."""
self._delay_listener = None
self._state = False
self.async_schedule_update_ha_state()
if self._delay_listener is not None:
self._delay_listener()
self._delay_listener = evt.async_call_later(
self.hass, self._off_delay, off_delay_listener)
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
await mqtt.async_subscribe( await mqtt.async_subscribe(

View File

@ -7,45 +7,33 @@ https://home-assistant.io/components/binary_sensor.octoprint/
import logging import logging
import requests import requests
import voluptuous as vol
from homeassistant.const import CONF_NAME, CONF_MONITORED_CONDITIONS from homeassistant.components.octoprint import (BINARY_SENSOR_TYPES,
from homeassistant.components.binary_sensor import ( DOMAIN as COMPONENT_DOMAIN)
BinarySensorDevice, PLATFORM_SCHEMA) from homeassistant.components.binary_sensor import BinarySensorDevice
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['octoprint'] DEPENDENCIES = ['octoprint']
DOMAIN = "octoprint"
DEFAULT_NAME = 'OctoPrint'
SENSOR_TYPES = {
# API Endpoint, Group, Key, unit
'Printing': ['printer', 'state', 'printing', None],
'Printing Error': ['printer', 'state', 'error', None]
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)):
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
def setup_platform(hass, config, add_entities, discovery_info=None): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the available OctoPrint binary sensors.""" """Set up the available OctoPrint binary sensors."""
octoprint_api = hass.data[DOMAIN]["api"] if discovery_info is None:
name = config.get(CONF_NAME) return
monitored_conditions = config.get(
CONF_MONITORED_CONDITIONS, SENSOR_TYPES.keys()) name = discovery_info['name']
base_url = discovery_info['base_url']
monitored_conditions = discovery_info['sensors']
octoprint_api = hass.data[COMPONENT_DOMAIN][base_url]
devices = [] devices = []
for octo_type in monitored_conditions: for octo_type in monitored_conditions:
new_sensor = OctoPrintBinarySensor( new_sensor = OctoPrintBinarySensor(
octoprint_api, octo_type, SENSOR_TYPES[octo_type][2], octoprint_api, octo_type, BINARY_SENSOR_TYPES[octo_type][2],
name, SENSOR_TYPES[octo_type][3], SENSOR_TYPES[octo_type][0], name, BINARY_SENSOR_TYPES[octo_type][3],
SENSOR_TYPES[octo_type][1], 'flags') BINARY_SENSOR_TYPES[octo_type][0],
BINARY_SENSOR_TYPES[octo_type][1], 'flags')
devices.append(new_sensor) devices.append(new_sensor)
add_entities(devices, True) add_entities(devices, True)

View File

@ -0,0 +1,145 @@
"""
Support for OpenTherm Gateway binary sensors.
For more details about this platform, please refer to the documentation at
http://home-assistant.io/components/binary_sensor.opentherm_gw/
"""
import logging
from homeassistant.components.binary_sensor import (
BinarySensorDevice, ENTITY_ID_FORMAT)
from homeassistant.components.opentherm_gw import (
DATA_GW_VARS, DATA_OPENTHERM_GW, SIGNAL_OPENTHERM_GW_UPDATE)
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import async_generate_entity_id
DEVICE_CLASS_COLD = 'cold'
DEVICE_CLASS_HEAT = 'heat'
DEVICE_CLASS_PROBLEM = 'problem'
DEPENDENCIES = ['opentherm_gw']
_LOGGER = logging.getLogger(__name__)
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the OpenTherm Gateway binary sensors."""
if discovery_info is None:
return
gw_vars = hass.data[DATA_OPENTHERM_GW][DATA_GW_VARS]
sensor_info = {
# [device_class, friendly_name]
gw_vars.DATA_MASTER_CH_ENABLED: [
None, "Thermostat Central Heating Enabled"],
gw_vars.DATA_MASTER_DHW_ENABLED: [
None, "Thermostat Hot Water Enabled"],
gw_vars.DATA_MASTER_COOLING_ENABLED: [
None, "Thermostat Cooling Enabled"],
gw_vars.DATA_MASTER_OTC_ENABLED: [
None, "Thermostat Outside Temperature Correction Enabled"],
gw_vars.DATA_MASTER_CH2_ENABLED: [
None, "Thermostat Central Heating 2 Enabled"],
gw_vars.DATA_SLAVE_FAULT_IND: [
DEVICE_CLASS_PROBLEM, "Boiler Fault Indication"],
gw_vars.DATA_SLAVE_CH_ACTIVE: [
DEVICE_CLASS_HEAT, "Boiler Central Heating Status"],
gw_vars.DATA_SLAVE_DHW_ACTIVE: [
DEVICE_CLASS_HEAT, "Boiler Hot Water Status"],
gw_vars.DATA_SLAVE_FLAME_ON: [
DEVICE_CLASS_HEAT, "Boiler Flame Status"],
gw_vars.DATA_SLAVE_COOLING_ACTIVE: [
DEVICE_CLASS_COLD, "Boiler Cooling Status"],
gw_vars.DATA_SLAVE_CH2_ACTIVE: [
DEVICE_CLASS_HEAT, "Boiler Central Heating 2 Status"],
gw_vars.DATA_SLAVE_DIAG_IND: [
DEVICE_CLASS_PROBLEM, "Boiler Diagnostics Indication"],
gw_vars.DATA_SLAVE_DHW_PRESENT: [None, "Boiler Hot Water Present"],
gw_vars.DATA_SLAVE_CONTROL_TYPE: [None, "Boiler Control Type"],
gw_vars.DATA_SLAVE_COOLING_SUPPORTED: [None, "Boiler Cooling Support"],
gw_vars.DATA_SLAVE_DHW_CONFIG: [
None, "Boiler Hot Water Configuration"],
gw_vars.DATA_SLAVE_MASTER_LOW_OFF_PUMP: [
None, "Boiler Pump Commands Support"],
gw_vars.DATA_SLAVE_CH2_PRESENT: [
None, "Boiler Central Heating 2 Present"],
gw_vars.DATA_SLAVE_SERVICE_REQ: [
DEVICE_CLASS_PROBLEM, "Boiler Service Required"],
gw_vars.DATA_SLAVE_REMOTE_RESET: [None, "Boiler Remote Reset Support"],
gw_vars.DATA_SLAVE_LOW_WATER_PRESS: [
DEVICE_CLASS_PROBLEM, "Boiler Low Water Pressure"],
gw_vars.DATA_SLAVE_GAS_FAULT: [
DEVICE_CLASS_PROBLEM, "Boiler Gas Fault"],
gw_vars.DATA_SLAVE_AIR_PRESS_FAULT: [
DEVICE_CLASS_PROBLEM, "Boiler Air Pressure Fault"],
gw_vars.DATA_SLAVE_WATER_OVERTEMP: [
DEVICE_CLASS_PROBLEM, "Boiler Water Overtemperature"],
gw_vars.DATA_REMOTE_TRANSFER_DHW: [
None, "Remote Hot Water Setpoint Transfer Support"],
gw_vars.DATA_REMOTE_TRANSFER_MAX_CH: [
None, "Remote Maximum Central Heating Setpoint Write Support"],
gw_vars.DATA_REMOTE_RW_DHW: [
None, "Remote Hot Water Setpoint Write Support"],
gw_vars.DATA_REMOTE_RW_MAX_CH: [
None, "Remote Central Heating Setpoint Write Support"],
gw_vars.DATA_ROVRD_MAN_PRIO: [
None, "Remote Override Manual Change Priority"],
gw_vars.DATA_ROVRD_AUTO_PRIO: [
None, "Remote Override Program Change Priority"],
gw_vars.OTGW_GPIO_A_STATE: [None, "Gateway GPIO A State"],
gw_vars.OTGW_GPIO_B_STATE: [None, "Gateway GPIO B State"],
gw_vars.OTGW_IGNORE_TRANSITIONS: [None, "Gateway Ignore Transitions"],
gw_vars.OTGW_OVRD_HB: [None, "Gateway Override High Byte"],
}
sensors = []
for var in discovery_info:
device_class = sensor_info[var][0]
friendly_name = sensor_info[var][1]
entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, var, hass=hass)
sensors.append(OpenThermBinarySensor(entity_id, var, device_class,
friendly_name))
async_add_entities(sensors)
class OpenThermBinarySensor(BinarySensorDevice):
"""Represent an OpenTherm Gateway binary sensor."""
def __init__(self, entity_id, var, device_class, friendly_name):
"""Initialize the binary sensor."""
self.entity_id = entity_id
self._var = var
self._state = None
self._device_class = device_class
self._friendly_name = friendly_name
async def async_added_to_hass(self):
"""Subscribe to updates from the component."""
_LOGGER.debug(
"Added OpenTherm Gateway binary sensor %s", self._friendly_name)
async_dispatcher_connect(self.hass, SIGNAL_OPENTHERM_GW_UPDATE,
self.receive_report)
async def receive_report(self, status):
"""Handle status updates from the component."""
self._state = bool(status.get(self._var))
self.async_schedule_update_ha_state()
@property
def name(self):
"""Return the friendly name."""
return self._friendly_name
@property
def is_on(self):
"""Return true if the binary sensor is on."""
return self._state
@property
def device_class(self):
"""Return the class of this device."""
return self._device_class
@property
def should_poll(self):
"""Return False because entity pushes its state."""
return False

View File

@ -50,12 +50,12 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorDevice):
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(openuv) super().__init__(openuv)
self._async_unsub_dispatcher_connect = None
self._entry_id = entry_id self._entry_id = entry_id
self._icon = icon self._icon = icon
self._latitude = openuv.client.latitude self._latitude = openuv.client.latitude
self._longitude = openuv.client.longitude self._longitude = openuv.client.longitude
self._name = name self._name = name
self._dispatch_remove = None
self._sensor_type = sensor_type self._sensor_type = sensor_type
self._state = None self._state = None
@ -80,16 +80,20 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorDevice):
return '{0}_{1}_{2}'.format( return '{0}_{1}_{2}'.format(
self._latitude, self._longitude, self._sensor_type) self._latitude, self._longitude, self._sensor_type)
async def async_added_to_hass(self):
"""Register callbacks."""
@callback @callback
def _update_data(self): def update():
"""Update the state.""" """Update the state."""
self.async_schedule_update_ha_state(True) self.async_schedule_update_ha_state(True)
async def async_added_to_hass(self): self._async_unsub_dispatcher_connect = async_dispatcher_connect(
"""Register callbacks.""" self.hass, TOPIC_UPDATE, update)
self._dispatch_remove = async_dispatcher_connect(
self.hass, TOPIC_UPDATE, self._update_data) async def async_will_remove_from_hass(self):
self.async_on_remove(self._dispatch_remove) """Disconnect dispatcher listener when removed."""
if self._async_unsub_dispatcher_connect:
self._async_unsub_dispatcher_connect()
async def async_update(self): async def async_update(self):
"""Update the state.""" """Update the state."""

View File

@ -4,19 +4,18 @@ Tracks the latency of a host by sending ICMP echo requests (ping).
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.ping/ https://home-assistant.io/components/binary_sensor.ping/
""" """
import asyncio
from datetime import timedelta
import logging import logging
import re
import subprocess import subprocess
import re
import sys import sys
from datetime import timedelta
import voluptuous as vol import voluptuous as vol
from homeassistant.components.binary_sensor import (
PLATFORM_SCHEMA, BinarySensorDevice)
from homeassistant.const import CONF_HOST, CONF_NAME
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.const import CONF_NAME, CONF_HOST
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -49,14 +48,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
}) })
async def async_setup_platform( def setup_platform(hass, config, add_entities, discovery_info=None):
hass, config, async_add_entities, discovery_info=None):
"""Set up the Ping Binary sensor.""" """Set up the Ping Binary sensor."""
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
host = config.get(CONF_HOST) host = config.get(CONF_HOST)
count = config.get(CONF_PING_COUNT) count = config.get(CONF_PING_COUNT)
async_add_entities([PingBinarySensor(name, PingData(host, count))], True) add_entities([PingBinarySensor(name, PingData(host, count))], True)
class PingBinarySensor(BinarySensorDevice): class PingBinarySensor(BinarySensorDevice):
@ -93,9 +91,9 @@ class PingBinarySensor(BinarySensorDevice):
ATTR_ROUND_TRIP_TIME_MIN: self.ping.data['min'], ATTR_ROUND_TRIP_TIME_MIN: self.ping.data['min'],
} }
async def async_update(self): def update(self):
"""Get the latest data.""" """Get the latest data."""
await self.ping.update() self.ping.update()
class PingData: class PingData:
@ -116,13 +114,12 @@ class PingData:
'ping', '-n', '-q', '-c', str(self._count), '-W1', 'ping', '-n', '-q', '-c', str(self._count), '-W1',
self._ip_address] self._ip_address]
async def ping(self): def ping(self):
"""Send ICMP echo request and return details if success.""" """Send ICMP echo request and return details if success."""
pinger = await asyncio.create_subprocess_shell( pinger = subprocess.Popen(
' '.join(self._ping_cmd), stdout=subprocess.PIPE, self._ping_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stderr=subprocess.PIPE)
try: try:
out = await pinger.communicate() out = pinger.communicate()
_LOGGER.debug("Output is %s", str(out)) _LOGGER.debug("Output is %s", str(out))
if sys.platform == 'win32': if sys.platform == 'win32':
match = WIN32_PING_MATCHER.search(str(out).split('\n')[-1]) match = WIN32_PING_MATCHER.search(str(out).split('\n')[-1])
@ -131,8 +128,7 @@ class PingData:
'min': rtt_min, 'min': rtt_min,
'avg': rtt_avg, 'avg': rtt_avg,
'max': rtt_max, 'max': rtt_max,
'mdev': '', 'mdev': ''}
}
if 'max/' not in str(out): if 'max/' not in str(out):
match = PING_MATCHER_BUSYBOX.search(str(out).split('\n')[-1]) match = PING_MATCHER_BUSYBOX.search(str(out).split('\n')[-1])
rtt_min, rtt_avg, rtt_max = match.groups() rtt_min, rtt_avg, rtt_max = match.groups()
@ -140,20 +136,18 @@ class PingData:
'min': rtt_min, 'min': rtt_min,
'avg': rtt_avg, 'avg': rtt_avg,
'max': rtt_max, 'max': rtt_max,
'mdev': '', 'mdev': ''}
}
match = PING_MATCHER.search(str(out).split('\n')[-1]) match = PING_MATCHER.search(str(out).split('\n')[-1])
rtt_min, rtt_avg, rtt_max, rtt_mdev = match.groups() rtt_min, rtt_avg, rtt_max, rtt_mdev = match.groups()
return { return {
'min': rtt_min, 'min': rtt_min,
'avg': rtt_avg, 'avg': rtt_avg,
'max': rtt_max, 'max': rtt_max,
'mdev': rtt_mdev, 'mdev': rtt_mdev}
}
except (subprocess.CalledProcessError, AttributeError): except (subprocess.CalledProcessError, AttributeError):
return False return False
async def update(self): def update(self):
"""Retrieve the latest details from the host.""" """Retrieve the latest details from the host."""
self.data = await self.ping() self.data = self.ping()
self.available = bool(self.data) self.available = bool(self.data)

View File

@ -0,0 +1,105 @@
"""
Support for Rflink binary sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.rflink/
"""
import logging
import voluptuous as vol
from homeassistant.components.binary_sensor import (
DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice)
from homeassistant.components.rflink import (
CONF_ALIASES, CONF_DEVICES, RflinkDevice)
from homeassistant.const import (
CONF_FORCE_UPDATE, CONF_NAME, CONF_DEVICE_CLASS)
import homeassistant.helpers.config_validation as cv
import homeassistant.helpers.event as evt
CONF_OFF_DELAY = 'off_delay'
DEFAULT_FORCE_UPDATE = False
DEPENDENCIES = ['rflink']
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_DEVICES, default={}): {
cv.string: vol.Schema({
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE):
cv.boolean,
vol.Optional(CONF_OFF_DELAY): cv.positive_int,
vol.Optional(CONF_ALIASES, default=[]):
vol.All(cv.ensure_list, [cv.string]),
})
},
}, extra=vol.ALLOW_EXTRA)
def devices_from_config(domain_config):
"""Parse configuration and add Rflink sensor devices."""
devices = []
for device_id, config in domain_config[CONF_DEVICES].items():
device = RflinkBinarySensor(device_id, **config)
devices.append(device)
return devices
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the Rflink platform."""
async_add_entities(devices_from_config(config))
class RflinkBinarySensor(RflinkDevice, BinarySensorDevice):
"""Representation of an Rflink binary sensor."""
def __init__(self, device_id, device_class=None,
force_update=None, off_delay=None,
**kwargs):
"""Handle sensor specific args and super init."""
self._state = None
self._device_class = device_class
self._force_update = force_update
self._off_delay = off_delay
self._delay_listener = None
super().__init__(device_id, **kwargs)
def _handle_event(self, event):
"""Domain specific event handler."""
command = event['command']
if command == 'on':
self._state = True
elif command == 'off':
self._state = False
if (self._state and self._off_delay is not None):
def off_delay_listener(now):
"""Switch device off after a delay."""
self._delay_listener = None
self._state = False
self.async_schedule_update_ha_state()
if self._delay_listener is not None:
self._delay_listener()
self._delay_listener = evt.async_call_later(
self.hass, self._off_delay, off_delay_listener)
@property
def is_on(self):
"""Return true if the binary sensor is on."""
return self._state
@property
def device_class(self):
"""Return the class of this sensor."""
return self._device_class
@property
def force_update(self):
"""Force update."""
return self._force_update

View File

@ -73,6 +73,7 @@ class RingBinarySensor(BinarySensorDevice):
SENSOR_TYPES.get(self._sensor_type)[0]) SENSOR_TYPES.get(self._sensor_type)[0])
self._device_class = SENSOR_TYPES.get(self._sensor_type)[2] self._device_class = SENSOR_TYPES.get(self._sensor_type)[2]
self._state = None self._state = None
self._unique_id = '{}-{}'.format(self._data.id, self._sensor_type)
@property @property
def name(self): def name(self):
@ -89,6 +90,11 @@ class RingBinarySensor(BinarySensorDevice):
"""Return the class of the binary sensor.""" """Return the class of the binary sensor."""
return self._device_class return self._device_class
@property
def unique_id(self):
"""Return a unique ID."""
return self._unique_id
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""

View File

@ -22,7 +22,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.15.1'] REQUIREMENTS = ['numpy==1.15.2']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -14,15 +14,16 @@ from homeassistant.const import CONF_NAME, WEEKDAYS
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['holidays==0.9.7'] REQUIREMENTS = ['holidays==0.9.8']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# 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 = [ ALL_COUNTRIES = [
'Argentina', 'AR', 'Australia', 'AU', 'Austria', 'AT', 'Belarus', 'BY' 'Argentina', 'AR', 'Australia', 'AU', 'Austria', 'AT',
'Belgium', 'BE', 'Canada', 'CA', 'Colombia', 'CO', 'Czech', 'CZ', 'Brazil', 'BR', 'Belarus', 'BY', 'Belgium', 'BE',
'Canada', 'CA', 'Colombia', 'CO', 'Croatia', 'HR', 'Czech', 'CZ',
'Denmark', 'DK', 'England', 'EuropeanCentralBank', 'ECB', 'TAR', 'Denmark', 'DK', 'England', 'EuropeanCentralBank', 'ECB', 'TAR',
'Finland', 'FI', 'France', 'FRA', 'Germany', 'DE', 'Hungary', 'HU', 'Finland', 'FI', 'France', 'FRA', 'Germany', 'DE', 'Hungary', 'HU',
'India', 'IND', 'Ireland', 'Isle of Man', 'Italy', 'IT', 'Japan', 'JP', 'India', 'IND', 'Ireland', 'Isle of Man', 'Italy', 'IT', 'Japan', 'JP',
@ -30,7 +31,7 @@ ALL_COUNTRIES = [
'Northern Ireland', 'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT', 'Northern Ireland', 'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT',
'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI', 'Slovakia', 'SK', 'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI', 'Slovakia', 'SK',
'South Africa', 'ZA', 'Spain', 'ES', 'Sweden', 'SE', 'Switzerland', 'CH', 'South Africa', 'ZA', 'Spain', 'ES', 'Sweden', 'SE', 'Switzerland', 'CH',
'UnitedKingdom', 'UK', 'UnitedStates', 'US', 'Wales', 'Ukraine', 'UA', 'UnitedKingdom', 'UK', 'UnitedStates', 'US', 'Wales',
] ]
ALLOWED_DAYS = WEEKDAYS + ['holiday'] ALLOWED_DAYS = WEEKDAYS + ['holiday']

View File

@ -4,6 +4,8 @@ import logging
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.xiaomi_aqara import (PY_XIAOMI_GATEWAY, from homeassistant.components.xiaomi_aqara import (PY_XIAOMI_GATEWAY,
XiaomiDevice) XiaomiDevice)
from homeassistant.core import callback
from homeassistant.helpers.event import async_call_later
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -36,21 +38,24 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
elif model in ['natgas', 'sensor_natgas']: elif model in ['natgas', 'sensor_natgas']:
devices.append(XiaomiNatgasSensor(device, gateway)) devices.append(XiaomiNatgasSensor(device, gateway))
elif model in ['switch', 'sensor_switch', elif model in ['switch', 'sensor_switch',
'sensor_switch.aq2', 'sensor_switch.aq3']: 'sensor_switch.aq2', 'sensor_switch.aq3',
'remote.b1acn01']:
if 'proto' not in device or int(device['proto'][0:1]) == 1: if 'proto' not in device or int(device['proto'][0:1]) == 1:
data_key = 'status' data_key = 'status'
else: else:
data_key = 'button_0' data_key = 'button_0'
devices.append(XiaomiButton(device, 'Switch', data_key, devices.append(XiaomiButton(device, 'Switch', data_key,
hass, gateway)) hass, gateway))
elif model in ['86sw1', 'sensor_86sw1', 'sensor_86sw1.aq1']: elif model in ['86sw1', 'sensor_86sw1', 'sensor_86sw1.aq1',
'remote.b186acn01']:
if 'proto' not in device or int(device['proto'][0:1]) == 1: if 'proto' not in device or int(device['proto'][0:1]) == 1:
data_key = 'channel_0' data_key = 'channel_0'
else: else:
data_key = 'button_0' data_key = 'button_0'
devices.append(XiaomiButton(device, 'Wall Switch', data_key, devices.append(XiaomiButton(device, 'Wall Switch', data_key,
hass, gateway)) hass, gateway))
elif model in ['86sw2', 'sensor_86sw2', 'sensor_86sw2.aq1']: elif model in ['86sw2', 'sensor_86sw2', 'sensor_86sw2.aq1',
'remote.b286acn01']:
if 'proto' not in device or int(device['proto'][0:1]) == 1: if 'proto' not in device or int(device['proto'][0:1]) == 1:
data_key_left = 'channel_0' data_key_left = 'channel_0'
data_key_right = 'channel_1' data_key_right = 'channel_1'
@ -65,6 +70,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
'dual_channel', hass, gateway)) 'dual_channel', hass, gateway))
elif model in ['cube', 'sensor_cube', 'sensor_cube.aqgl01']: elif model in ['cube', 'sensor_cube', 'sensor_cube.aqgl01']:
devices.append(XiaomiCube(device, hass, gateway)) devices.append(XiaomiCube(device, hass, gateway))
elif model in ['vibration', 'vibration.aq1']:
devices.append(XiaomiVibration(device, 'Vibration',
'status', gateway))
else:
_LOGGER.warning('Unmapped Device Model %s', model)
add_entities(devices) add_entities(devices)
@ -144,6 +155,7 @@ 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
self._unsub_set_no_motion = None
if 'proto' not in device or int(device['proto'][0:1]) == 1: if 'proto' not in device or int(device['proto'][0:1]) == 1:
data_key = 'status' data_key = 'status'
else: else:
@ -158,6 +170,13 @@ class XiaomiMotionSensor(XiaomiBinarySensor):
attrs.update(super().device_state_attributes) attrs.update(super().device_state_attributes)
return attrs return attrs
@callback
def _async_set_no_motion(self, now):
"""Set state to False."""
self._unsub_set_no_motion = None
self._state = False
self.async_schedule_update_ha_state()
def parse_data(self, data, raw_data): def parse_data(self, data, raw_data):
"""Parse data sent by gateway.""" """Parse data sent by gateway."""
if raw_data['cmd'] == 'heartbeat': if raw_data['cmd'] == 'heartbeat':
@ -179,6 +198,15 @@ class XiaomiMotionSensor(XiaomiBinarySensor):
return False return False
if value == MOTION: if value == MOTION:
if self._data_key == 'motion_status':
if self._unsub_set_no_motion:
self._unsub_set_no_motion()
self._unsub_set_no_motion = async_call_later(
self._hass,
180,
self._async_set_no_motion
)
else:
self._should_poll = True self._should_poll = True
if self.entity_id is not None: if self.entity_id is not None:
self._hass.bus.fire('motion', { self._hass.bus.fire('motion', {
@ -311,6 +339,38 @@ class XiaomiSmokeSensor(XiaomiBinarySensor):
return False return False
class XiaomiVibration(XiaomiBinarySensor):
"""Representation of a Xiaomi Vibration Sensor."""
def __init__(self, device, name, data_key, xiaomi_hub):
"""Initialize the XiaomiVibration."""
self._last_action = None
super().__init__(device, name, xiaomi_hub, data_key, None)
@property
def device_state_attributes(self):
"""Return the state attributes."""
attrs = {ATTR_LAST_ACTION: self._last_action}
attrs.update(super().device_state_attributes)
return attrs
def parse_data(self, data, raw_data):
"""Parse data sent by gateway."""
value = data.get(self._data_key)
if value not in ('vibrate', 'tilt', 'free_fall'):
_LOGGER.warning("Unsupported movement_type detected: %s",
value)
return False
self.hass.bus.fire('xiaomi_aqara.movement', {
'entity_id': self.entity_id,
'movement_type': value
})
self._last_action = value
return True
class XiaomiButton(XiaomiBinarySensor): class XiaomiButton(XiaomiBinarySensor):
"""Representation of a Xiaomi Button.""" """Representation of a Xiaomi Button."""

View File

@ -253,5 +253,9 @@ class Remote(zha.Entity, BinarySensorDevice):
"""Retrieve latest state.""" """Retrieve latest state."""
from zigpy.zcl.clusters.general import OnOff from zigpy.zcl.clusters.general import OnOff
result = await zha.safe_read( result = await zha.safe_read(
self._endpoint.out_clusters[OnOff.cluster_id], ['on_off']) self._endpoint.out_clusters[OnOff.cluster_id],
['on_off'],
allow_cache=False,
only_cache=(not self._initialized)
)
self._state = result.get('on_off', self._state) self._state = result.get('on_off', self._state)

View File

@ -7,10 +7,11 @@ https://home-assistant.io/components/binary_sensor.zwave/
import logging import logging
import datetime import datetime
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.event import track_point_in_time from homeassistant.helpers.event import track_point_in_time
from homeassistant.components import zwave from homeassistant.components import zwave
from homeassistant.components.zwave import ( # noqa pylint: disable=unused-import from homeassistant.components.zwave import workaround
async_setup_platform, workaround)
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
DOMAIN, DOMAIN,
BinarySensorDevice) BinarySensorDevice)
@ -19,6 +20,23 @@ _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = [] DEPENDENCIES = []
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Old method of setting up Z-Wave binary sensors."""
pass
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Z-Wave binary sensors from Config Entry."""
@callback
def async_add_binary_sensor(binary_sensor):
"""Add Z-Wave binary sensor."""
async_add_entities([binary_sensor])
async_dispatcher_connect(hass, 'zwave_new_binary_sensor',
async_add_binary_sensor)
def get_device(values, **kwargs): def get_device(values, **kwargs):
"""Create Z-Wave entity device.""" """Create Z-Wave entity device."""
device_mapping = workaround.get_device_mapping(values.primary) device_mapping = workaround.get_device_mapping(values.primary)

View File

@ -15,7 +15,7 @@ from homeassistant.const import (
CONF_BINARY_SENSORS, CONF_SENSORS, CONF_FILENAME, CONF_BINARY_SENSORS, CONF_SENSORS, CONF_FILENAME,
CONF_MONITORED_CONDITIONS, TEMP_FAHRENHEIT) CONF_MONITORED_CONDITIONS, TEMP_FAHRENHEIT)
REQUIREMENTS = ['blinkpy==0.10.0'] REQUIREMENTS = ['blinkpy==0.10.1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -9,7 +9,7 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD)
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
from homeassistant.helpers.event import track_utc_time_change from homeassistant.helpers.event import track_utc_time_change
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -85,6 +85,7 @@ def setup_account(account_config: dict, hass, name: str) \
password = account_config[CONF_PASSWORD] password = account_config[CONF_PASSWORD]
region = account_config[CONF_REGION] region = account_config[CONF_REGION]
read_only = account_config[CONF_READ_ONLY] read_only = account_config[CONF_READ_ONLY]
_LOGGER.debug('Adding new account %s', name) _LOGGER.debug('Adding new account %s', name)
cd_account = BMWConnectedDriveAccount(username, password, region, name, cd_account = BMWConnectedDriveAccount(username, password, region, name,
read_only) read_only)

View File

@ -68,6 +68,7 @@ class GoogleCalendarData:
self.event = None self.event = None
def _prepare_query(self): def _prepare_query(self):
# pylint: disable=import-error
from httplib2 import ServerNotFoundError from httplib2 import ServerNotFoundError
try: try:

View File

@ -518,6 +518,8 @@ class TodoistProjectData:
def update(self): def update(self):
"""Get the latest data.""" """Get the latest data."""
if self._id is None: if self._id is None:
self._api.reset_state()
self._api.sync()
project_task_data = [ project_task_data = [
task for task in self._api.state[TASKS] task for task in self._api.state[TASKS]
if not self._project_id_whitelist or if not self._project_id_whitelist or
@ -527,6 +529,7 @@ class TodoistProjectData:
# If we have no data, we can just return right away. # If we have no data, we can just return right away.
if not project_task_data: if not project_task_data:
_LOGGER.debug("No data for %s", self._name)
self.event = None self.event = None
return True return True
@ -541,6 +544,8 @@ class TodoistProjectData:
if not project_tasks: if not project_tasks:
# We had no valid tasks # We had no valid tasks
_LOGGER.debug("No valid tasks for %s", self._name)
self.event = None
return True return True
# Make sure the task collection is reset to prevent an # Make sure the task collection is reset to prevent an

View File

@ -53,6 +53,11 @@ class BloomSkyCamera(Camera):
return self._last_image return self._last_image
@property
def unique_id(self):
"""Return a unique ID."""
return self._id
@property @property
def name(self): def name(self):
"""Return the name of this BloomSky device.""" """Return the name of this BloomSky device."""

View File

@ -32,7 +32,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
async def async_setup_platform(hass, config, async_add_entities, async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None): discovery_info=None):
"""Set up a FFmpeg camera.""" """Set up a FFmpeg camera."""
if not hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_INPUT)): if not await hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_INPUT)):
return return
async_add_entities([FFmpegCamera(hass, config)]) async_add_entities([FFmpegCamera(hass, config)])

View File

@ -63,3 +63,8 @@ class NeatoCleaningMap(Camera):
def name(self): def name(self):
"""Return the name of this camera.""" """Return the name of this camera."""
return self._robot_name return self._robot_name
@property
def unique_id(self):
"""Return unique ID."""
return self._robot_serial

View File

@ -97,6 +97,11 @@ class RingCam(Camera):
"""Return the name of this camera.""" """Return the name of this camera."""
return self._name return self._name
@property
def unique_id(self):
"""Return a unique ID."""
return self._camera.id
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""

View File

@ -35,7 +35,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_MODEL): vol.Any(MODEL_YI, vol.Required(CONF_MODEL): vol.Any(MODEL_YI,
MODEL_XIAOFANG), MODEL_XIAOFANG),
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string, vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string,
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_PASSWORD): cv.string,

View File

@ -32,7 +32,7 @@ CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_NAME): cv.string, vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string, vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string,
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_PASSWORD): cv.string,

View File

@ -7,9 +7,9 @@
"step": { "step": {
"confirm": { "confirm": {
"description": "Deseja configurar o Google Cast?", "description": "Deseja configurar o Google Cast?",
"title": "" "title": "Google Cast"
} }
}, },
"title": "" "title": "Google Cast"
} }
} }

View File

@ -0,0 +1,15 @@
{
"config": {
"abort": {
"no_devices_found": "Nu s-au g\u0103sit dispozitive Google Cast \u00een re\u021bea.",
"single_instance_allowed": "Este necesar\u0103 o singur\u0103 configura\u021bie a serviciului Google Cast."
},
"step": {
"confirm": {
"description": "Dori\u021bi s\u0103 configura\u021bi Google Cast?",
"title": "Google Cast"
}
},
"title": "Google Cast"
}
}

View File

@ -48,11 +48,6 @@ 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'
STATE_ELECTRIC = 'electric'
STATE_PERFORMANCE = 'performance'
STATE_HIGH_DEMAND = 'high_demand'
STATE_HEAT_PUMP = 'heat_pump'
STATE_GAS = 'gas'
SUPPORT_TARGET_TEMPERATURE = 1 SUPPORT_TARGET_TEMPERATURE = 1
SUPPORT_TARGET_TEMPERATURE_HIGH = 2 SUPPORT_TARGET_TEMPERATURE_HIGH = 2

View File

@ -40,6 +40,15 @@ HA_STATE_TO_DAIKIN = {
STATE_OFF: 'off', STATE_OFF: 'off',
} }
DAIKIN_TO_HA_STATE = {
'fan': STATE_FAN_ONLY,
'dry': STATE_DRY,
'cool': STATE_COOL,
'hot': STATE_HEAT,
'auto': STATE_AUTO,
'off': STATE_OFF,
}
HA_ATTR_TO_DAIKIN = { HA_ATTR_TO_DAIKIN = {
ATTR_OPERATION_MODE: 'mode', ATTR_OPERATION_MODE: 'mode',
ATTR_FAN_MODE: 'f_rate', ATTR_FAN_MODE: 'f_rate',
@ -75,9 +84,7 @@ class DaikinClimate(ClimateDevice):
self._api = api self._api = api
self._force_refresh = False self._force_refresh = False
self._list = { self._list = {
ATTR_OPERATION_MODE: list( ATTR_OPERATION_MODE: list(HA_STATE_TO_DAIKIN),
map(str.title, set(HA_STATE_TO_DAIKIN.values()))
),
ATTR_FAN_MODE: list( ATTR_FAN_MODE: list(
map( map(
str.title, str.title,
@ -136,11 +143,11 @@ class DaikinClimate(ClimateDevice):
elif key == ATTR_OPERATION_MODE: elif key == ATTR_OPERATION_MODE:
# Daikin can return also internal states auto-1 or auto-7 # Daikin can return also internal states auto-1 or auto-7
# and we need to translate them as AUTO # and we need to translate them as AUTO
value = re.sub( daikin_mode = re.sub(
'[^a-z]', '[^a-z]', '',
'', self._api.device.represent(daikin_attr)[1])
self._api.device.represent(daikin_attr)[1] ha_mode = DAIKIN_TO_HA_STATE.get(daikin_mode)
).title() value = ha_mode
if value is None: if value is None:
_LOGGER.error("Invalid value requested for key %s", key) _LOGGER.error("Invalid value requested for key %s", key)
@ -167,8 +174,8 @@ class DaikinClimate(ClimateDevice):
daikin_attr = HA_ATTR_TO_DAIKIN.get(attr) daikin_attr = HA_ATTR_TO_DAIKIN.get(attr)
if daikin_attr is not None: if daikin_attr is not None:
if value.title() in self._list[attr]: if value in self._list[attr]:
values[daikin_attr] = value.lower() values[daikin_attr] = HA_STATE_TO_DAIKIN[value]
else: else:
_LOGGER.error("Invalid value %s for %s", attr, value) _LOGGER.error("Invalid value %s for %s", attr, value)

View File

@ -0,0 +1,176 @@
"""
Support for Dyson Pure Hot+Cool link fan.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.dyson/
"""
import logging
from homeassistant.components.dyson import DYSON_DEVICES
from homeassistant.components.climate import (
ClimateDevice, STATE_HEAT, STATE_COOL, STATE_IDLE,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE)
from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE
_LOGGER = logging.getLogger(__name__)
STATE_DIFFUSE = "Diffuse Mode"
STATE_FOCUS = "Focus Mode"
FAN_LIST = [STATE_FOCUS, STATE_DIFFUSE]
OPERATION_LIST = [STATE_HEAT, STATE_COOL]
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE
| SUPPORT_OPERATION_MODE)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Dyson fan components."""
if discovery_info is None:
return
from libpurecoollink.dyson_pure_hotcool_link import DysonPureHotCoolLink
# Get Dyson Devices from parent component.
add_devices(
[DysonPureHotCoolLinkDevice(device)
for device in hass.data[DYSON_DEVICES]
if isinstance(device, DysonPureHotCoolLink)]
)
class DysonPureHotCoolLinkDevice(ClimateDevice):
"""Representation of a Dyson climate fan."""
def __init__(self, device):
"""Initialize the fan."""
self._device = device
self._current_temp = None
async def async_added_to_hass(self):
"""Call when entity is added to hass."""
self.hass.async_add_job(self._device.add_message_listener,
self.on_message)
def on_message(self, message):
"""Call when new messages received from the climate."""
from libpurecoollink.dyson_pure_state import DysonPureHotCoolState
if isinstance(message, DysonPureHotCoolState):
_LOGGER.debug("Message received for climate device %s : %s",
self.name, message)
self.schedule_update_ha_state()
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property
def name(self):
"""Return the display name of this climate."""
return self._device.name
@property
def temperature_unit(self):
"""Return the unit of measurement."""
return TEMP_CELSIUS
@property
def current_temperature(self):
"""Return the current temperature."""
if self._device.environmental_state:
temperature_kelvin = self._device.environmental_state.temperature
if temperature_kelvin != 0:
self._current_temp = float("{0:.1f}".format(
temperature_kelvin - 273))
return self._current_temp
@property
def target_temperature(self):
"""Return the target temperature."""
heat_target = int(self._device.state.heat_target) / 10
return int(heat_target - 273)
@property
def current_humidity(self):
"""Return the current humidity."""
if self._device.environmental_state:
if self._device.environmental_state.humidity == 0:
return None
return self._device.environmental_state.humidity
return None
@property
def current_operation(self):
"""Return current operation ie. heat, cool, idle."""
from libpurecoollink.const import HeatMode, HeatState
if self._device.state.heat_mode == HeatMode.HEAT_ON.value:
if self._device.state.heat_state == HeatState.HEAT_STATE_ON.value:
return STATE_HEAT
return STATE_IDLE
return STATE_COOL
@property
def operation_list(self):
"""Return the list of available operation modes."""
return OPERATION_LIST
@property
def current_fan_mode(self):
"""Return the fan setting."""
from libpurecoollink.const import FocusMode
if self._device.state.focus_mode == FocusMode.FOCUS_ON.value:
return STATE_FOCUS
return STATE_DIFFUSE
@property
def fan_list(self):
"""Return the list of available fan modes."""
return FAN_LIST
def set_temperature(self, **kwargs):
"""Set new target temperature."""
target_temp = kwargs.get(ATTR_TEMPERATURE)
if target_temp is None:
return
target_temp = int(target_temp)
_LOGGER.debug("Set %s temperature %s", self.name, target_temp)
# Limit the target temperature into acceptable range.
target_temp = min(self.max_temp, target_temp)
target_temp = max(self.min_temp, target_temp)
from libpurecoollink.const import HeatTarget, HeatMode
self._device.set_configuration(
heat_target=HeatTarget.celsius(target_temp),
heat_mode=HeatMode.HEAT_ON)
def set_fan_mode(self, fan_mode):
"""Set new fan mode."""
_LOGGER.debug("Set %s focus mode %s", self.name, fan_mode)
from libpurecoollink.const import FocusMode
if fan_mode == STATE_FOCUS:
self._device.set_configuration(focus_mode=FocusMode.FOCUS_ON)
elif fan_mode == STATE_DIFFUSE:
self._device.set_configuration(focus_mode=FocusMode.FOCUS_OFF)
def set_operation_mode(self, operation_mode):
"""Set operation mode."""
_LOGGER.debug("Set %s heat mode %s", self.name, operation_mode)
from libpurecoollink.const import HeatMode
if operation_mode == STATE_HEAT:
self._device.set_configuration(heat_mode=HeatMode.HEAT_ON)
elif operation_mode == STATE_COOL:
self._device.set_configuration(heat_mode=HeatMode.HEAT_OFF)
@property
def min_temp(self):
"""Return the minimum temperature."""
return 1
@property
def max_temp(self):
"""Return the maximum temperature."""
return 37

View File

@ -0,0 +1,193 @@
"""
Support for control of Elk-M1 connected thermostats.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.elkm1/
"""
from homeassistant.components.climate import (
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, PRECISION_WHOLE, STATE_AUTO,
STATE_COOL, STATE_FAN_ONLY, STATE_HEAT, STATE_IDLE, SUPPORT_AUX_HEAT,
SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE_HIGH,
SUPPORT_TARGET_TEMPERATURE_LOW, ClimateDevice)
from homeassistant.components.elkm1 import (
DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities)
from homeassistant.const import STATE_ON
DEPENDENCIES = [ELK_DOMAIN]
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Create the Elk-M1 thermostat platform."""
if discovery_info is None:
return
elk = hass.data[ELK_DOMAIN]['elk']
async_add_entities(create_elk_entities(
hass, elk.thermostats, 'thermostat', ElkThermostat, []), True)
class ElkThermostat(ElkEntity, ClimateDevice):
"""Representation of an Elk-M1 Thermostat."""
def __init__(self, element, elk, elk_data):
"""Initialize climate entity."""
super().__init__(element, elk, elk_data)
self._state = None
@property
def supported_features(self):
"""Return the list of supported features."""
return (SUPPORT_OPERATION_MODE | SUPPORT_FAN_MODE | SUPPORT_AUX_HEAT
| SUPPORT_TARGET_TEMPERATURE_HIGH
| SUPPORT_TARGET_TEMPERATURE_LOW)
@property
def temperature_unit(self):
"""Return the temperature unit."""
return self._temperature_unit
@property
def current_temperature(self):
"""Return the current temperature."""
return self._element.current_temp
@property
def target_temperature(self):
"""Return the temperature we are trying to reach."""
from elkm1_lib.const import ThermostatMode
if (self._element.mode == ThermostatMode.HEAT.value) or (
self._element.mode == ThermostatMode.EMERGENCY_HEAT.value):
return self._element.heat_setpoint
if self._element.mode == ThermostatMode.COOL.value:
return self._element.cool_setpoint
return None
@property
def target_temperature_high(self):
"""Return the high target temperature."""
return self._element.cool_setpoint
@property
def target_temperature_low(self):
"""Return the low target temperature."""
return self._element.heat_setpoint
@property
def target_temperature_step(self):
"""Return the supported step of target temperature."""
return 1
@property
def current_humidity(self):
"""Return the current humidity."""
return self._element.humidity
@property
def current_operation(self):
"""Return current operation ie. heat, cool, idle."""
return self._state
@property
def operation_list(self):
"""Return the list of available operation modes."""
return [STATE_IDLE, STATE_HEAT, STATE_COOL, STATE_AUTO, STATE_FAN_ONLY]
@property
def precision(self):
"""Return the precision of the system."""
return PRECISION_WHOLE
@property
def is_aux_heat_on(self):
"""Return if aux heater is on."""
from elkm1_lib.const import ThermostatMode
return self._element.mode == ThermostatMode.EMERGENCY_HEAT.value
@property
def min_temp(self):
"""Return the minimum temperature supported."""
return 1
@property
def max_temp(self):
"""Return the maximum temperature supported."""
return 99
@property
def current_fan_mode(self):
"""Return the fan setting."""
from elkm1_lib.const import ThermostatFan
if self._element.fan == ThermostatFan.AUTO.value:
return STATE_AUTO
if self._element.fan == ThermostatFan.ON.value:
return STATE_ON
return None
def _elk_set(self, mode, fan):
from elkm1_lib.const import ThermostatSetting
if mode is not None:
self._element.set(ThermostatSetting.MODE.value, mode)
if fan is not None:
self._element.set(ThermostatSetting.FAN.value, fan)
async def async_set_operation_mode(self, operation_mode):
"""Set thermostat operation mode."""
from elkm1_lib.const import ThermostatFan, ThermostatMode
settings = {
STATE_IDLE: (ThermostatMode.OFF.value, ThermostatFan.AUTO.value),
STATE_HEAT: (ThermostatMode.HEAT.value, None),
STATE_COOL: (ThermostatMode.COOL.value, None),
STATE_AUTO: (ThermostatMode.AUTO.value, None),
STATE_FAN_ONLY: (ThermostatMode.OFF.value, ThermostatFan.ON.value)
}
self._elk_set(settings[operation_mode][0], settings[operation_mode][1])
async def async_turn_aux_heat_on(self):
"""Turn auxiliary heater on."""
from elkm1_lib.const import ThermostatMode
self._elk_set(ThermostatMode.EMERGENCY_HEAT.value, None)
async def async_turn_aux_heat_off(self):
"""Turn auxiliary heater off."""
from elkm1_lib.const import ThermostatMode
self._elk_set(ThermostatMode.HEAT.value, None)
@property
def fan_list(self):
"""Return the list of available fan modes."""
return [STATE_AUTO, STATE_ON]
async def async_set_fan_mode(self, fan_mode):
"""Set new target fan mode."""
from elkm1_lib.const import ThermostatFan
if fan_mode == STATE_AUTO:
self._elk_set(None, ThermostatFan.AUTO.value)
elif fan_mode == STATE_ON:
self._elk_set(None, ThermostatFan.ON.value)
async def async_set_temperature(self, **kwargs):
"""Set new target temperature."""
from elkm1_lib.const import ThermostatSetting
low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW)
high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH)
if low_temp is not None:
self._element.set(
ThermostatSetting.HEAT_SETPOINT.value, round(low_temp))
if high_temp is not None:
self._element.set(
ThermostatSetting.COOL_SETPOINT.value, round(high_temp))
def _element_changed(self, element, changeset):
from elkm1_lib.const import ThermostatFan, ThermostatMode
mode_to_state = {
ThermostatMode.OFF.value: STATE_IDLE,
ThermostatMode.COOL.value: STATE_COOL,
ThermostatMode.HEAT.value: STATE_HEAT,
ThermostatMode.EMERGENCY_HEAT.value: STATE_HEAT,
ThermostatMode.AUTO.value: STATE_AUTO,
}
self._state = mode_to_state.get(self._element.mode)
if self._state == STATE_IDLE and \
self._element.fan == ThermostatFan.ON.value:
self._state = STATE_FAN_ONLY

View File

@ -10,12 +10,13 @@ import voluptuous as vol
from homeassistant.components.climate import ( from homeassistant.components.climate import (
STATE_ON, STATE_OFF, STATE_AUTO, PLATFORM_SCHEMA, ClimateDevice, STATE_ON, STATE_OFF, STATE_AUTO, PLATFORM_SCHEMA, ClimateDevice,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE) SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE,
SUPPORT_ON_OFF)
from homeassistant.const import ( 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', 'construct==2.9.41'] REQUIREMENTS = ['python-eq3bt==0.1.9', 'construct==2.9.45']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -39,7 +40,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
}) })
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
SUPPORT_AWAY_MODE) SUPPORT_AWAY_MODE | SUPPORT_ON_OFF)
def setup_platform(hass, config, add_entities, discovery_info=None): def setup_platform(hass, config, add_entities, discovery_info=None):
@ -53,14 +54,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(devices) add_entities(devices)
# pylint: disable=import-error
class EQ3BTSmartThermostat(ClimateDevice): class EQ3BTSmartThermostat(ClimateDevice):
"""Representation of an eQ-3 Bluetooth Smart thermostat.""" """Representation of an eQ-3 Bluetooth Smart thermostat."""
def __init__(self, _mac, _name): def __init__(self, _mac, _name):
"""Initialize the thermostat.""" """Initialize the thermostat."""
# We want to avoid name clash with this module. # We want to avoid name clash with this module.
import eq3bt as eq3 import eq3bt as eq3 # pylint: disable=import-error
self.modes = { self.modes = {
eq3.Mode.Open: STATE_ON, eq3.Mode.Open: STATE_ON,
@ -151,6 +151,14 @@ class EQ3BTSmartThermostat(ClimateDevice):
"""Return if we are away.""" """Return if we are away."""
return self.current_operation == STATE_AWAY return self.current_operation == STATE_AWAY
def turn_on(self):
"""Turn device on."""
self.set_operation_mode(STATE_AUTO)
def turn_off(self):
"""Turn device off."""
self.set_operation_mode(STATE_OFF)
@property @property
def min_temp(self): def min_temp(self):
"""Return the minimum temperature.""" """Return the minimum temperature."""
@ -176,7 +184,7 @@ class EQ3BTSmartThermostat(ClimateDevice):
def update(self): def update(self):
"""Update the data from the thermostat.""" """Update the data from the thermostat."""
from bluepy.btle import BTLEException from bluepy.btle import BTLEException # pylint: disable=import-error
try: try:
self._thermostat.update() self._thermostat.update()
except BTLEException as ex: except BTLEException as ex:

View File

@ -75,7 +75,7 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities):
entities = [] entities = []
for device_name in discovery_info[ATTR_DISCOVER_DEVICES]: for device_name in discovery_info[ATTR_DISCOVER_DEVICES]:
device = hass.data[DATA_KNX].xknx.devices[device_name] device = hass.data[DATA_KNX].xknx.devices[device_name]
entities.append(KNXClimate(hass, device)) entities.append(KNXClimate(device))
async_add_entities(entities) async_add_entities(entities)
@ -110,17 +110,15 @@ def async_add_entities_config(hass, config, async_add_entities):
group_address_operation_mode_comfort=config.get( group_address_operation_mode_comfort=config.get(
CONF_OPERATION_MODE_COMFORT_ADDRESS)) CONF_OPERATION_MODE_COMFORT_ADDRESS))
hass.data[DATA_KNX].xknx.devices.add(climate) hass.data[DATA_KNX].xknx.devices.add(climate)
async_add_entities([KNXClimate(hass, climate)]) async_add_entities([KNXClimate(climate)])
class KNXClimate(ClimateDevice): class KNXClimate(ClimateDevice):
"""Representation of a KNX climate device.""" """Representation of a KNX climate device."""
def __init__(self, hass, device): def __init__(self, device):
"""Initialize of a KNX climate device.""" """Initialize of a KNX climate device."""
self.device = device self.device = device
self.hass = hass
self.async_register_callbacks()
@property @property
def supported_features(self): def supported_features(self):
@ -137,6 +135,10 @@ class KNXClimate(ClimateDevice):
await self.async_update_ha_state() await self.async_update_ha_state()
self.device.register_device_updated_cb(after_update_callback) self.device.register_device_updated_cb(after_update_callback)
async def async_added_to_hass(self):
"""Store register state change callback."""
self.async_register_callbacks()
@property @property
def name(self): def name(self):
"""Return the name of the KNX device.""" """Return the name of the KNX device."""

View File

@ -0,0 +1,153 @@
"""
Support for mill wifi-enabled home heaters.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.mill/
"""
import logging
import voluptuous as vol
from homeassistant.components.climate import (
ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_FAN_MODE, SUPPORT_ON_OFF)
from homeassistant.const import (
ATTR_TEMPERATURE, CONF_PASSWORD, CONF_USERNAME,
STATE_ON, STATE_OFF, TEMP_CELSIUS)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
REQUIREMENTS = ['millheater==0.1.2']
_LOGGER = logging.getLogger(__name__)
MAX_TEMP = 35
MIN_TEMP = 5
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE |
SUPPORT_FAN_MODE | SUPPORT_ON_OFF)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
})
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the Mill heater."""
from mill import Mill
mill_data_connection = Mill(config[CONF_USERNAME],
config[CONF_PASSWORD],
websession=async_get_clientsession(hass))
if not await mill_data_connection.connect():
_LOGGER.error("Failed to connect to Mill")
return
await mill_data_connection.update_heaters()
dev = []
for heater in mill_data_connection.heaters.values():
dev.append(MillHeater(heater, mill_data_connection))
async_add_entities(dev)
class MillHeater(ClimateDevice):
"""Representation of a Mill Thermostat device."""
def __init__(self, heater, mill_data_connection):
"""Initialize the thermostat."""
self._heater = heater
self._conn = mill_data_connection
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property
def available(self):
"""Return True if entity is available."""
return self._heater.device_status == 0 # weird api choice
@property
def unique_id(self):
"""Return a unique ID."""
return self._heater.device_id
@property
def name(self):
"""Return the name of the entity."""
return self._heater.name
@property
def temperature_unit(self):
"""Return the unit of measurement which this thermostat uses."""
return TEMP_CELSIUS
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._heater.set_temp
@property
def target_temperature_step(self):
"""Return the supported step of target temperature."""
return 1
@property
def current_temperature(self):
"""Return the current temperature."""
return self._heater.current_temp
@property
def current_fan_mode(self):
"""Return the fan setting."""
return STATE_ON if self._heater.fan_status == 1 else STATE_OFF
@property
def fan_list(self):
"""List of available fan modes."""
return [STATE_ON, STATE_OFF]
@property
def is_on(self):
"""Return true if heater is on."""
return self._heater.power_status == 1
@property
def min_temp(self):
"""Return the minimum temperature."""
return MIN_TEMP
@property
def max_temp(self):
"""Return the maximum temperature."""
return MAX_TEMP
async def async_set_temperature(self, **kwargs):
"""Set new target temperature."""
temperature = kwargs.get(ATTR_TEMPERATURE)
if temperature is None:
return
await self._conn.set_heater_temp(self._heater.device_id,
int(temperature))
async def async_set_fan_mode(self, fan_mode):
"""Set new target fan mode."""
fan_status = 1 if fan_mode == STATE_ON else 0
await self._conn.heater_control(self._heater.device_id,
fan_status=fan_status)
async def async_turn_on(self):
"""Turn Mill unit on."""
await self._conn.heater_control(self._heater.device_id,
power_status=1)
async def async_turn_off(self):
"""Turn Mill unit off."""
await self._conn.heater_control(self._heater.device_id,
power_status=0)
async def async_update(self):
"""Retrieve latest state."""
self._heater = await self._conn.update_device(self._heater.device_id)

View File

@ -75,6 +75,7 @@ CONF_SEND_IF_OFF = 'send_if_off'
CONF_MIN_TEMP = 'min_temp' CONF_MIN_TEMP = 'min_temp'
CONF_MAX_TEMP = 'max_temp' CONF_MAX_TEMP = 'max_temp'
CONF_TEMP_STEP = 'temp_step'
SCHEMA_BASE = CLIMATE_PLATFORM_SCHEMA.extend(MQTT_BASE_PLATFORM_SCHEMA.schema) SCHEMA_BASE = CLIMATE_PLATFORM_SCHEMA.extend(MQTT_BASE_PLATFORM_SCHEMA.schema)
PLATFORM_SCHEMA = SCHEMA_BASE.extend({ PLATFORM_SCHEMA = SCHEMA_BASE.extend({
@ -124,7 +125,8 @@ PLATFORM_SCHEMA = SCHEMA_BASE.extend({
vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string, vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string,
vol.Optional(CONF_MIN_TEMP, default=DEFAULT_MIN_TEMP): vol.Coerce(float), vol.Optional(CONF_MIN_TEMP, default=DEFAULT_MIN_TEMP): vol.Coerce(float),
vol.Optional(CONF_MAX_TEMP, default=DEFAULT_MAX_TEMP): vol.Coerce(float) vol.Optional(CONF_MAX_TEMP, default=DEFAULT_MAX_TEMP): vol.Coerce(float),
vol.Optional(CONF_TEMP_STEP, default=1.0): vol.Coerce(float)
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
@ -213,6 +215,7 @@ async def _async_setup_entity(hass, config, async_add_entities,
config.get(CONF_PAYLOAD_NOT_AVAILABLE), config.get(CONF_PAYLOAD_NOT_AVAILABLE),
config.get(CONF_MIN_TEMP), config.get(CONF_MIN_TEMP),
config.get(CONF_MAX_TEMP), config.get(CONF_MAX_TEMP),
config.get(CONF_TEMP_STEP),
discovery_hash, discovery_hash,
)]) )])
@ -226,7 +229,7 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
current_swing_mode, current_operation, aux, send_if_off, current_swing_mode, current_operation, aux, send_if_off,
payload_on, payload_off, availability_topic, payload_on, payload_off, availability_topic,
payload_available, payload_not_available, payload_available, payload_not_available,
min_temp, max_temp, discovery_hash): min_temp, max_temp, temp_step, discovery_hash):
"""Initialize the climate device.""" """Initialize the climate device."""
MqttAvailability.__init__(self, availability_topic, qos, MqttAvailability.__init__(self, availability_topic, qos,
payload_available, payload_not_available) payload_available, payload_not_available)
@ -237,19 +240,26 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
self._value_templates = value_templates self._value_templates = value_templates
self._qos = qos self._qos = qos
self._retain = retain self._retain = retain
# set to None in non-optimistic mode
self._target_temperature = self._current_fan_mode = \
self._current_operation = self._current_swing_mode = None
if self._topic[CONF_TEMPERATURE_STATE_TOPIC] is None:
self._target_temperature = target_temperature self._target_temperature = target_temperature
self._unit_of_measurement = hass.config.units.temperature_unit self._unit_of_measurement = hass.config.units.temperature_unit
self._away = away self._away = away
self._hold = hold self._hold = hold
self._current_temperature = None self._current_temperature = None
if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None:
self._current_fan_mode = current_fan_mode self._current_fan_mode = current_fan_mode
if self._topic[CONF_MODE_STATE_TOPIC] is None:
self._current_operation = current_operation self._current_operation = current_operation
self._aux = aux self._aux = aux
if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None:
self._current_swing_mode = current_swing_mode self._current_swing_mode = current_swing_mode
self._fan_list = fan_mode_list self._fan_list = fan_mode_list
self._operation_list = mode_list self._operation_list = mode_list
self._swing_list = swing_mode_list self._swing_list = swing_mode_list
self._target_temperature_step = 1 self._target_temperature_step = temp_step
self._send_if_off = send_if_off self._send_if_off = send_if_off
self._payload_on = payload_on self._payload_on = payload_on
self._payload_off = payload_off self._payload_off = payload_off

View File

@ -1,34 +1,23 @@
""" """
Support for OpenTherm Gateway devices. Support for OpenTherm Gateway climate devices.
For more details about this component, please refer to the documentation at For more details about this platform, please refer to the documentation at
http://home-assistant.io/components/climate.opentherm_gw/ http://home-assistant.io/components/climate.opentherm_gw/
""" """
import logging import logging
import voluptuous as vol from homeassistant.components.climate import (ClimateDevice, STATE_IDLE,
STATE_HEAT, STATE_COOL,
from homeassistant.components.climate import (ClimateDevice, PLATFORM_SCHEMA,
STATE_IDLE, STATE_HEAT,
STATE_COOL,
SUPPORT_TARGET_TEMPERATURE) SUPPORT_TARGET_TEMPERATURE)
from homeassistant.const import (ATTR_TEMPERATURE, CONF_DEVICE, CONF_NAME, from homeassistant.components.opentherm_gw import (
PRECISION_HALVES, PRECISION_TENTHS, CONF_FLOOR_TEMP, CONF_PRECISION, DATA_DEVICE, DATA_GW_VARS,
TEMP_CELSIUS, PRECISION_WHOLE) DATA_OPENTHERM_GW, SIGNAL_OPENTHERM_GW_UPDATE)
import homeassistant.helpers.config_validation as cv from homeassistant.const import (ATTR_TEMPERATURE, CONF_NAME, PRECISION_HALVES,
PRECISION_TENTHS, PRECISION_WHOLE,
TEMP_CELSIUS)
from homeassistant.helpers.dispatcher import async_dispatcher_connect
REQUIREMENTS = ['pyotgw==0.1b0'] DEPENDENCIES = ['opentherm_gw']
CONF_FLOOR_TEMP = "floor_temperature"
CONF_PRECISION = 'precision'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_DEVICE): cv.string,
vol.Optional(CONF_NAME, default="OpenTherm Gateway"): cv.string,
vol.Optional(CONF_PRECISION): vol.In([PRECISION_TENTHS, PRECISION_HALVES,
PRECISION_WHOLE]),
vol.Optional(CONF_FLOOR_TEMP, default=False): cv.boolean,
})
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE) SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -37,19 +26,17 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_platform(hass, config, async_add_entities, async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None): discovery_info=None):
"""Set up the opentherm_gw device.""" """Set up the opentherm_gw device."""
gateway = OpenThermGateway(config) gateway = OpenThermGateway(hass, discovery_info)
async_add_entities([gateway]) async_add_entities([gateway])
class OpenThermGateway(ClimateDevice): class OpenThermGateway(ClimateDevice):
"""Representation of a climate device.""" """Representation of a climate device."""
def __init__(self, config): def __init__(self, hass, config):
"""Initialize the sensor.""" """Initialize the device."""
import pyotgw self._gateway = hass.data[DATA_OPENTHERM_GW][DATA_DEVICE]
self.pyotgw = pyotgw self._gw_vars = hass.data[DATA_OPENTHERM_GW][DATA_GW_VARS]
self.gateway = self.pyotgw.pyotgw()
self._device = config[CONF_DEVICE]
self.friendly_name = config.get(CONF_NAME) self.friendly_name = config.get(CONF_NAME)
self.floor_temp = config.get(CONF_FLOOR_TEMP) self.floor_temp = config.get(CONF_FLOOR_TEMP)
self.temp_precision = config.get(CONF_PRECISION) self.temp_precision = config.get(CONF_PRECISION)
@ -63,40 +50,38 @@ class OpenThermGateway(ClimateDevice):
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Connect to the OpenTherm Gateway device.""" """Connect to the OpenTherm Gateway device."""
await self.gateway.connect(self.hass.loop, self._device) _LOGGER.debug("Added device %s", self.friendly_name)
self.gateway.subscribe(self.receive_report) async_dispatcher_connect(self.hass, SIGNAL_OPENTHERM_GW_UPDATE,
_LOGGER.debug("Connected to %s on %s", self.friendly_name, self.receive_report)
self._device)
async def receive_report(self, status): async def receive_report(self, status):
"""Receive and handle a new report from the Gateway.""" """Receive and handle a new report from the Gateway."""
_LOGGER.debug("Received report: %s", status) ch_active = status.get(self._gw_vars.DATA_SLAVE_CH_ACTIVE)
ch_active = status.get(self.pyotgw.DATA_SLAVE_CH_ACTIVE) flame_on = status.get(self._gw_vars.DATA_SLAVE_FLAME_ON)
flame_on = status.get(self.pyotgw.DATA_SLAVE_FLAME_ON) cooling_active = status.get(self._gw_vars.DATA_SLAVE_COOLING_ACTIVE)
cooling_active = status.get(self.pyotgw.DATA_SLAVE_COOLING_ACTIVE)
if ch_active and flame_on: if ch_active and flame_on:
self._current_operation = STATE_HEAT self._current_operation = STATE_HEAT
elif cooling_active: elif cooling_active:
self._current_operation = STATE_COOL self._current_operation = STATE_COOL
else: else:
self._current_operation = STATE_IDLE self._current_operation = STATE_IDLE
self._current_temperature = status.get(self.pyotgw.DATA_ROOM_TEMP) self._current_temperature = status.get(self._gw_vars.DATA_ROOM_TEMP)
temp = status.get(self.pyotgw.DATA_ROOM_SETPOINT_OVRD) temp = status.get(self._gw_vars.DATA_ROOM_SETPOINT_OVRD)
if temp is None: if temp is None:
temp = status.get(self.pyotgw.DATA_ROOM_SETPOINT) temp = status.get(self._gw_vars.DATA_ROOM_SETPOINT)
self._target_temperature = temp self._target_temperature = temp
# GPIO mode 5: 0 == Away # GPIO mode 5: 0 == Away
# GPIO mode 6: 1 == Away # GPIO mode 6: 1 == Away
gpio_a_state = status.get(self.pyotgw.OTGW_GPIO_A) gpio_a_state = status.get(self._gw_vars.OTGW_GPIO_A)
if gpio_a_state == 5: if gpio_a_state == 5:
self._away_mode_a = 0 self._away_mode_a = 0
elif gpio_a_state == 6: elif gpio_a_state == 6:
self._away_mode_a = 1 self._away_mode_a = 1
else: else:
self._away_mode_a = None self._away_mode_a = None
gpio_b_state = status.get(self.pyotgw.OTGW_GPIO_B) gpio_b_state = status.get(self._gw_vars.OTGW_GPIO_B)
if gpio_b_state == 5: if gpio_b_state == 5:
self._away_mode_b = 0 self._away_mode_b = 0
elif gpio_b_state == 6: elif gpio_b_state == 6:
@ -104,11 +89,11 @@ class OpenThermGateway(ClimateDevice):
else: else:
self._away_mode_b = None self._away_mode_b = None
if self._away_mode_a is not None: if self._away_mode_a is not None:
self._away_state_a = (status.get(self.pyotgw.OTGW_GPIO_A_STATE) == self._away_state_a = (status.get(
self._away_mode_a) self._gw_vars.OTGW_GPIO_A_STATE) == self._away_mode_a)
if self._away_mode_b is not None: if self._away_mode_b is not None:
self._away_state_b = (status.get(self.pyotgw.OTGW_GPIO_B_STATE) == self._away_state_b = (status.get(
self._away_mode_b) self._gw_vars.OTGW_GPIO_B_STATE) == self._away_mode_b)
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
@property @property
@ -170,7 +155,7 @@ class OpenThermGateway(ClimateDevice):
"""Set new target temperature.""" """Set new target temperature."""
if ATTR_TEMPERATURE in kwargs: if ATTR_TEMPERATURE in kwargs:
temp = float(kwargs[ATTR_TEMPERATURE]) temp = float(kwargs[ATTR_TEMPERATURE])
self._target_temperature = await self.gateway.set_target_temp( self._target_temperature = await self._gateway.set_target_temp(
temp) temp)
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()

View File

@ -123,26 +123,6 @@ nuheat_resume_program:
description: Name(s) of entities to change. description: Name(s) of entities to change.
example: 'climate.kitchen' example: 'climate.kitchen'
econet_add_vacation:
description: Add a vacation to your water heater.
fields:
entity_id:
description: Name(s) of entities to change.
example: 'climate.water_heater'
start_date:
description: The timestamp of when the vacation should start. (Optional, defaults to now)
example: 1513186320
end_date:
description: The timestamp of when the vacation should end.
example: 1513445520
econet_delete_vacation:
description: Delete your existing vacation from your water heater.
fields:
entity_id:
description: Name(s) of entities to change.
example: 'climate.water_heater'
sensibo_assume_state: sensibo_assume_state:
description: Set Sensibo device to external state. description: Set Sensibo device to external state.
fields: fields:

View File

@ -8,7 +8,7 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.toon/ https://home-assistant.io/components/climate.toon/
""" """
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ATTR_TEMPERATURE, STATE_COOL, STATE_ECO, STATE_HEAT, STATE_PERFORMANCE, ATTR_TEMPERATURE, STATE_COOL, STATE_ECO, STATE_HEAT, STATE_AUTO,
SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, ClimateDevice) SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, ClimateDevice)
import homeassistant.components.toon as toon_main import homeassistant.components.toon as toon_main
from homeassistant.const import TEMP_CELSIUS from homeassistant.const import TEMP_CELSIUS
@ -34,7 +34,7 @@ class ThermostatDevice(ClimateDevice):
self._temperature = None self._temperature = None
self._setpoint = None self._setpoint = None
self._operation_list = [ self._operation_list = [
STATE_PERFORMANCE, STATE_AUTO,
STATE_HEAT, STATE_HEAT,
STATE_ECO, STATE_ECO,
STATE_COOL, STATE_COOL,
@ -84,7 +84,7 @@ class ThermostatDevice(ClimateDevice):
def set_operation_mode(self, operation_mode): def set_operation_mode(self, operation_mode):
"""Set new operation mode.""" """Set new operation mode."""
toonlib_values = { toonlib_values = {
STATE_PERFORMANCE: 'Comfort', STATE_AUTO: 'Comfort',
STATE_HEAT: 'Home', STATE_HEAT: 'Home',
STATE_ECO: 'Away', STATE_ECO: 'Away',
STATE_COOL: 'Sleep', STATE_COOL: 'Sleep',

View File

@ -7,8 +7,7 @@ https://home-assistant.io/components/climate.tuya/
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ATTR_TEMPERATURE, ENTITY_ID_FORMAT, STATE_AUTO, STATE_COOL, STATE_ECO, ATTR_TEMPERATURE, ENTITY_ID_FORMAT, STATE_AUTO, STATE_COOL, STATE_ECO,
STATE_ELECTRIC, STATE_FAN_ONLY, STATE_GAS, STATE_HEAT, STATE_HEAT_PUMP, STATE_FAN_ONLY, STATE_HEAT, SUPPORT_FAN_MODE, SUPPORT_ON_OFF,
STATE_HIGH_DEMAND, STATE_PERFORMANCE, SUPPORT_FAN_MODE, SUPPORT_ON_OFF,
SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, ClimateDevice) SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, ClimateDevice)
from homeassistant.components.fan import SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH from homeassistant.components.fan import SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH
from homeassistant.components.tuya import DATA_TUYA, TuyaDevice from homeassistant.components.tuya import DATA_TUYA, TuyaDevice
@ -23,13 +22,8 @@ HA_STATE_TO_TUYA = {
STATE_AUTO: 'auto', STATE_AUTO: 'auto',
STATE_COOL: 'cold', STATE_COOL: 'cold',
STATE_ECO: 'eco', STATE_ECO: 'eco',
STATE_ELECTRIC: 'electric',
STATE_FAN_ONLY: 'wind', STATE_FAN_ONLY: 'wind',
STATE_GAS: 'gas',
STATE_HEAT: 'hot', STATE_HEAT: 'hot',
STATE_HEAT_PUMP: 'heat_pump',
STATE_HIGH_DEMAND: 'high_demand',
STATE_PERFORMANCE: 'performance',
} }
TUYA_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_TUYA.items()} TUYA_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_TUYA.items()}

View File

@ -1,5 +1,5 @@
""" """
Support for Wink thermostats, Air Conditioners, and Water Heaters. Support for Wink thermostats and Air Conditioners.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.wink/ https://home-assistant.io/components/climate.wink/
@ -8,9 +8,9 @@ import logging
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ATTR_CURRENT_HUMIDITY, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, ATTR_CURRENT_HUMIDITY, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
ATTR_TEMPERATURE, STATE_AUTO, STATE_COOL, STATE_ECO, STATE_ELECTRIC, ATTR_TEMPERATURE, STATE_AUTO, STATE_COOL, STATE_ECO,
STATE_FAN_ONLY, STATE_GAS, STATE_HEAT, STATE_HEAT_PUMP, STATE_HIGH_DEMAND, STATE_FAN_ONLY, STATE_HEAT, SUPPORT_AUX_HEAT,
STATE_PERFORMANCE, SUPPORT_AUX_HEAT, SUPPORT_AWAY_MODE, SUPPORT_FAN_MODE, SUPPORT_AWAY_MODE, SUPPORT_FAN_MODE,
SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW,
ClimateDevice) ClimateDevice)
@ -24,11 +24,9 @@ _LOGGER = logging.getLogger(__name__)
ATTR_ECO_TARGET = 'eco_target' ATTR_ECO_TARGET = 'eco_target'
ATTR_EXTERNAL_TEMPERATURE = 'external_temperature' ATTR_EXTERNAL_TEMPERATURE = 'external_temperature'
ATTR_OCCUPIED = 'occupied' ATTR_OCCUPIED = 'occupied'
ATTR_RHEEM_TYPE = 'rheem_type'
ATTR_SCHEDULE_ENABLED = 'schedule_enabled' ATTR_SCHEDULE_ENABLED = 'schedule_enabled'
ATTR_SMART_TEMPERATURE = 'smart_temperature' ATTR_SMART_TEMPERATURE = 'smart_temperature'
ATTR_TOTAL_CONSUMPTION = 'total_consumption' ATTR_TOTAL_CONSUMPTION = 'total_consumption'
ATTR_VACATION_MODE = 'vacation_mode'
ATTR_HEAT_ON = 'heat_on' ATTR_HEAT_ON = 'heat_on'
ATTR_COOL_ON = 'cool_on' ATTR_COOL_ON = 'cool_on'
@ -42,14 +40,9 @@ HA_STATE_TO_WINK = {
STATE_AUTO: 'auto', STATE_AUTO: 'auto',
STATE_COOL: 'cool_only', STATE_COOL: 'cool_only',
STATE_ECO: 'eco', STATE_ECO: 'eco',
STATE_ELECTRIC: 'electric_only',
STATE_FAN_ONLY: 'fan_only', STATE_FAN_ONLY: 'fan_only',
STATE_GAS: 'gas',
STATE_HEAT: 'heat_only', STATE_HEAT: 'heat_only',
STATE_HEAT_PUMP: 'heat_pump',
STATE_HIGH_DEMAND: 'high_demand',
STATE_OFF: 'off', STATE_OFF: 'off',
STATE_PERFORMANCE: 'performance',
} }
WINK_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_WINK.items()} WINK_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_WINK.items()}
@ -62,9 +55,6 @@ SUPPORT_FLAGS_THERMOSTAT = (
SUPPORT_FLAGS_AC = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | SUPPORT_FLAGS_AC = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
SUPPORT_FAN_MODE) SUPPORT_FAN_MODE)
SUPPORT_FLAGS_HEATER = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
SUPPORT_AWAY_MODE)
def setup_platform(hass, config, add_entities, discovery_info=None): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Wink climate devices.""" """Set up the Wink climate devices."""
@ -77,10 +67,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
_id = climate.object_id() + climate.name() _id = climate.object_id() + climate.name()
if _id not in hass.data[DOMAIN]['unique_ids']: if _id not in hass.data[DOMAIN]['unique_ids']:
add_entities([WinkAC(climate, hass)]) add_entities([WinkAC(climate, hass)])
for water_heater in pywink.get_water_heaters():
_id = water_heater.object_id() + water_heater.name()
if _id not in hass.data[DOMAIN]['unique_ids']:
add_entities([WinkWaterHeater(water_heater, hass)])
class WinkThermostat(WinkDevice, ClimateDevice): class WinkThermostat(WinkDevice, ClimateDevice):
@ -504,93 +490,3 @@ class WinkAC(WinkDevice, ClimateDevice):
elif fan_mode == SPEED_HIGH: elif fan_mode == SPEED_HIGH:
speed = 1.0 speed = 1.0
self.wink.set_ac_fan_speed(speed) self.wink.set_ac_fan_speed(speed)
class WinkWaterHeater(WinkDevice, ClimateDevice):
"""Representation of a Wink water heater."""
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS_HEATER
@property
def temperature_unit(self):
"""Return the unit of measurement."""
# The Wink API always returns temp in Celsius
return TEMP_CELSIUS
@property
def device_state_attributes(self):
"""Return the optional device state attributes."""
data = {}
data[ATTR_VACATION_MODE] = self.wink.vacation_mode_enabled()
data[ATTR_RHEEM_TYPE] = self.wink.rheem_type()
return data
@property
def current_operation(self):
"""
Return current operation one of the following.
["eco", "performance", "heat_pump",
"high_demand", "electric_only", "gas]
"""
if not self.wink.is_on():
current_op = STATE_OFF
else:
current_op = WINK_STATE_TO_HA.get(self.wink.current_mode())
if current_op is None:
current_op = STATE_UNKNOWN
return current_op
@property
def operation_list(self):
"""List of available operation modes."""
op_list = ['off']
modes = self.wink.modes()
for mode in modes:
if mode == 'aux':
continue
ha_mode = WINK_STATE_TO_HA.get(mode)
if ha_mode is not None:
op_list.append(ha_mode)
else:
error = "Invalid operation mode mapping. " + mode + \
" doesn't map. Please report this."
_LOGGER.error(error)
return op_list
def set_temperature(self, **kwargs):
"""Set new target temperature."""
target_temp = kwargs.get(ATTR_TEMPERATURE)
self.wink.set_temperature(target_temp)
def set_operation_mode(self, operation_mode):
"""Set operation mode."""
op_mode_to_set = HA_STATE_TO_WINK.get(operation_mode)
self.wink.set_operation_mode(op_mode_to_set)
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self.wink.current_set_point()
def turn_away_mode_on(self):
"""Turn away on."""
self.wink.set_vacation_mode(True)
def turn_away_mode_off(self):
"""Turn away off."""
self.wink.set_vacation_mode(False)
@property
def min_temp(self):
"""Return the minimum temperature."""
return self.wink.min_set_point()
@property
def max_temp(self):
"""Return the maximum temperature."""
return self.wink.max_set_point()

View File

@ -18,21 +18,23 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import (async_dispatcher_connect, from homeassistant.helpers.dispatcher import (async_dispatcher_connect,
async_dispatcher_send) async_dispatcher_send)
REQUIREMENTS = ['zhong_hong_hvac==1.0.9']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_GATEWAY_ADDRRESS = 'gateway_address' CONF_GATEWAY_ADDRRESS = 'gateway_address'
REQUIREMENTS = ['zhong_hong_hvac==1.0.9'] DEFAULT_PORT = 9999
DEFAULT_GATEWAY_ADDRRESS = 1
SIGNAL_DEVICE_ADDED = 'zhong_hong_device_added' SIGNAL_DEVICE_ADDED = 'zhong_hong_device_added'
SIGNAL_ZHONG_HONG_HUB_START = 'zhong_hong_hub_start' SIGNAL_ZHONG_HONG_HUB_START = 'zhong_hong_hub_start'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): vol.Required(CONF_HOST): cv.string,
cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_PORT, default=9999): vol.Optional(CONF_GATEWAY_ADDRRESS, default=DEFAULT_GATEWAY_ADDRRESS):
vol.Coerce(int), cv.positive_int,
vol.Optional(CONF_GATEWAY_ADDRRESS, default=1):
vol.Coerce(int),
}) })

View File

@ -6,14 +6,15 @@ https://home-assistant.io/components/climate.zwave/
""" """
# Because we do not compile openzwave on CI # Because we do not compile openzwave on CI
import logging import logging
from homeassistant.core import callback
from homeassistant.components.climate import ( from homeassistant.components.climate import (
DOMAIN, ClimateDevice, STATE_AUTO, STATE_COOL, STATE_HEAT, DOMAIN, ClimateDevice, STATE_AUTO, STATE_COOL, STATE_HEAT,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE,
SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE) SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE)
from homeassistant.components.zwave import ( # noqa pylint: disable=unused-import from homeassistant.components.zwave import ZWaveDeviceEntity
ZWaveDeviceEntity, async_setup_platform)
from homeassistant.const import ( from homeassistant.const import (
STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
from homeassistant.helpers.dispatcher import async_dispatcher_connect
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -42,6 +43,22 @@ STATE_MAPPINGS = {
} }
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Old method of setting up Z-Wave climate devices."""
pass
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Z-Wave Climate device from Config Entry."""
@callback
def async_add_climate(climate):
"""Add Z-Wave Climate Device."""
async_add_entities([climate])
async_dispatcher_connect(hass, 'zwave_new_climate', async_add_climate)
def get_device(hass, values, **kwargs): def get_device(hass, values, **kwargs):
"""Create Z-Wave entity device.""" """Create Z-Wave entity device."""
temp_unit = hass.config.units.temperature_unit temp_unit = hass.config.units.temperature_unit

View File

@ -162,7 +162,7 @@ class Cloud:
@property @property
def subscription_expired(self): def subscription_expired(self):
"""Return a boolean if the subscription has expired.""" """Return a boolean if the subscription has expired."""
return dt_util.utcnow() > self.expiration_date + timedelta(days=3) return dt_util.utcnow() > self.expiration_date + timedelta(days=7)
@property @property
def expiration_date(self): def expiration_date(self):

View File

@ -113,6 +113,24 @@ def check_token(cloud):
raise _map_aws_exception(err) raise _map_aws_exception(err)
def renew_access_token(cloud):
"""Renew access token."""
from botocore.exceptions import ClientError
cognito = _cognito(
cloud,
access_token=cloud.access_token,
refresh_token=cloud.refresh_token)
try:
cognito.renew_access_token()
cloud.id_token = cognito.id_token
cloud.access_token = cognito.access_token
cloud.write_user_info()
except ClientError as err:
raise _map_aws_exception(err)
def _authenticate(cloud, email, password): def _authenticate(cloud, email, password):
"""Log in and return an authenticated Cognito instance.""" """Log in and return an authenticated Cognito instance."""
from botocore.exceptions import ClientError from botocore.exceptions import ClientError

View File

@ -14,7 +14,7 @@ from homeassistant.components import websocket_api
from . import auth_api from . import auth_api
from .const import DOMAIN, REQUEST_TIMEOUT from .const import DOMAIN, REQUEST_TIMEOUT
from .iot import STATE_DISCONNECTED from .iot import STATE_DISCONNECTED, STATE_CONNECTED
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -249,13 +249,28 @@ async def websocket_subscription(hass, connection, msg):
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
response = await cloud.fetch_subscription_info() response = await cloud.fetch_subscription_info()
if response.status == 200: if response.status != 200:
connection.send_message(websocket_api.result_message(
msg['id'], await response.json()))
else:
connection.send_message(websocket_api.error_message( connection.send_message(websocket_api.error_message(
msg['id'], 'request_failed', 'Failed to request subscription')) msg['id'], 'request_failed', 'Failed to request subscription'))
data = await response.json()
# Check if a user is subscribed but local info is outdated
# In that case, let's refresh and reconnect
if data.get('provider') and cloud.iot.state != STATE_CONNECTED:
_LOGGER.debug(
"Found disconnected account with valid subscriotion, connecting")
await hass.async_add_executor_job(
auth_api.renew_access_token, cloud)
# Cancel reconnect in progress
if cloud.iot.state != STATE_DISCONNECTED:
await cloud.iot.disconnect()
hass.async_create_task(cloud.iot.connect())
connection.send_message(websocket_api.result_message(msg['id'], data))
@websocket_api.async_response @websocket_api.async_response
async def websocket_update_prefs(hass, connection, msg): async def websocket_update_prefs(hass, connection, msg):

View File

@ -92,6 +92,7 @@ def _user_info(user):
'is_owner': user.is_owner, 'is_owner': user.is_owner,
'is_active': user.is_active, 'is_active': user.is_active,
'system_generated': user.system_generated, 'system_generated': user.system_generated,
'group_ids': [group.id for group in user.groups],
'credentials': [ 'credentials': [
{ {
'type': c.auth_provider_type, 'type': c.auth_provider_type,

View File

@ -5,10 +5,10 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.deconz/ https://home-assistant.io/components/cover.deconz/
""" """
from homeassistant.components.deconz.const import ( from homeassistant.components.deconz.const import (
COVER_TYPES, DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB, COVER_TYPES, DAMPERS, DOMAIN as DATA_DECONZ, DATA_DECONZ_ID,
DECONZ_DOMAIN) DATA_DECONZ_UNSUB, DECONZ_DOMAIN, WINDOW_COVERS)
from homeassistant.components.cover import ( from homeassistant.components.cover import (
ATTR_POSITION, CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN, ATTR_POSITION, CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_STOP,
SUPPORT_SET_POSITION) SUPPORT_SET_POSITION)
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
@ -16,6 +16,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
DEPENDENCIES = ['deconz'] DEPENDENCIES = ['deconz']
ZIGBEE_SPEC = ['lumi.curtain']
async def async_setup_platform(hass, config, async_add_entities, async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None): discovery_info=None):
@ -34,6 +36,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
entities = [] entities = []
for light in lights: for light in lights:
if light.type in COVER_TYPES: if light.type in COVER_TYPES:
if light.modelid in ZIGBEE_SPEC:
entities.append(DeconzCoverZigbeeSpec(light))
else:
entities.append(DeconzCover(light)) entities.append(DeconzCover(light))
async_add_entities(entities, True) async_add_entities(entities, True)
@ -49,7 +54,10 @@ class DeconzCover(CoverDevice):
def __init__(self, cover): def __init__(self, cover):
"""Set up cover and add update callback to get data from websocket.""" """Set up cover and add update callback to get data from websocket."""
self._cover = cover self._cover = cover
self._features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION self._features = SUPPORT_OPEN
self._features |= SUPPORT_CLOSE
self._features |= SUPPORT_STOP
self._features |= SUPPORT_SET_POSITION
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Subscribe to covers events.""" """Subscribe to covers events."""
@ -91,7 +99,11 @@ class DeconzCover(CoverDevice):
@property @property
def device_class(self): def device_class(self):
"""Return the class of the cover.""" """Return the class of the cover."""
if self._cover.type in DAMPERS:
return 'damper' return 'damper'
if self._cover.type in WINDOW_COVERS:
return 'window'
return None
@property @property
def supported_features(self): def supported_features(self):
@ -127,6 +139,11 @@ class DeconzCover(CoverDevice):
data = {ATTR_POSITION: 0} data = {ATTR_POSITION: 0}
await self.async_set_cover_position(**data) await self.async_set_cover_position(**data)
async def async_stop_cover(self, **kwargs):
"""Stop cover."""
data = {'bri_inc': 0}
await self._cover.async_set_state(data)
@property @property
def device_info(self): def device_info(self):
"""Return a device description for device registry.""" """Return a device description for device registry."""
@ -144,3 +161,26 @@ class DeconzCover(CoverDevice):
'sw_version': self._cover.swversion, 'sw_version': self._cover.swversion,
'via_hub': (DECONZ_DOMAIN, bridgeid), 'via_hub': (DECONZ_DOMAIN, bridgeid),
} }
class DeconzCoverZigbeeSpec(DeconzCover):
"""Zigbee spec is the inverse of how deCONZ normally reports attributes."""
@property
def current_cover_position(self):
"""Return the current position of the cover."""
return 100 - int(self._cover.brightness / 255 * 100)
@property
def is_closed(self):
"""Return if the cover is closed."""
return self._cover.state
async def async_set_cover_position(self, **kwargs):
"""Move the cover to a specific position."""
position = kwargs[ATTR_POSITION]
data = {'on': False}
if position < 100:
data['on'] = True
data['bri'] = 255 - int(position / 100 * 255)
await self._cover.async_set_state(data)

View File

@ -64,7 +64,7 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities):
entities = [] entities = []
for device_name in discovery_info[ATTR_DISCOVER_DEVICES]: for device_name in discovery_info[ATTR_DISCOVER_DEVICES]:
device = hass.data[DATA_KNX].xknx.devices[device_name] device = hass.data[DATA_KNX].xknx.devices[device_name]
entities.append(KNXCover(hass, device)) entities.append(KNXCover(device))
async_add_entities(entities) async_add_entities(entities)
@ -88,18 +88,15 @@ def async_add_entities_config(hass, config, async_add_entities):
invert_angle=config.get(CONF_INVERT_ANGLE)) invert_angle=config.get(CONF_INVERT_ANGLE))
hass.data[DATA_KNX].xknx.devices.add(cover) hass.data[DATA_KNX].xknx.devices.add(cover)
async_add_entities([KNXCover(hass, cover)]) async_add_entities([KNXCover(cover)])
class KNXCover(CoverDevice): class KNXCover(CoverDevice):
"""Representation of a KNX cover.""" """Representation of a KNX cover."""
def __init__(self, hass, device): def __init__(self, device):
"""Initialize the cover.""" """Initialize the cover."""
self.device = device self.device = device
self.hass = hass
self.async_register_callbacks()
self._unsubscribe_auto_updater = None self._unsubscribe_auto_updater = None
@callback @callback
@ -110,6 +107,10 @@ class KNXCover(CoverDevice):
await self.async_update_ha_state() await self.async_update_ha_state()
self.device.register_device_updated_cb(after_update_callback) self.device.register_device_updated_cb(after_update_callback)
async def async_added_to_hass(self):
"""Store register state change callback."""
self.async_register_callbacks()
@property @property
def name(self): def name(self):
"""Return the name of the KNX device.""" """Return the name of the KNX device."""

View File

@ -19,12 +19,12 @@ from homeassistant.components.cover import (
from homeassistant.exceptions import TemplateError from homeassistant.exceptions import TemplateError
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_VALUE_TEMPLATE, CONF_OPTIMISTIC, STATE_OPEN, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_OPTIMISTIC, STATE_OPEN,
STATE_CLOSED, STATE_UNKNOWN) STATE_CLOSED, STATE_UNKNOWN, CONF_DEVICE)
from homeassistant.components.mqtt import ( from homeassistant.components.mqtt import (
ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC,
CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE,
CONF_QOS, CONF_RETAIN, valid_publish_topic, valid_subscribe_topic, CONF_QOS, CONF_RETAIN, valid_publish_topic, valid_subscribe_topic,
MqttAvailability, MqttDiscoveryUpdate) MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo)
from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
@ -96,6 +96,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
vol.Optional(CONF_TILT_INVERT_STATE, vol.Optional(CONF_TILT_INVERT_STATE,
default=DEFAULT_TILT_INVERT_STATE): cv.boolean, default=DEFAULT_TILT_INVERT_STATE): cv.boolean,
vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA,
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
@ -155,11 +156,13 @@ async def _async_setup_entity(hass, config, async_add_entities,
config.get(CONF_POSITION_TOPIC), config.get(CONF_POSITION_TOPIC),
set_position_template, set_position_template,
config.get(CONF_UNIQUE_ID), config.get(CONF_UNIQUE_ID),
config.get(CONF_DEVICE),
discovery_hash discovery_hash
)]) )])
class MqttCover(MqttAvailability, MqttDiscoveryUpdate, CoverDevice): class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
CoverDevice):
"""Representation of a cover that can be controlled using MQTT.""" """Representation of a cover that can be controlled using MQTT."""
def __init__(self, name, state_topic, command_topic, availability_topic, def __init__(self, name, state_topic, command_topic, availability_topic,
@ -169,11 +172,13 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, CoverDevice):
optimistic, value_template, tilt_open_position, optimistic, value_template, tilt_open_position,
tilt_closed_position, tilt_min, tilt_max, tilt_optimistic, tilt_closed_position, tilt_min, tilt_max, tilt_optimistic,
tilt_invert, position_topic, set_position_template, tilt_invert, position_topic, set_position_template,
unique_id: Optional[str], discovery_hash): unique_id: Optional[str], device_config: Optional[ConfigType],
discovery_hash):
"""Initialize the cover.""" """Initialize the cover."""
MqttAvailability.__init__(self, availability_topic, qos, MqttAvailability.__init__(self, availability_topic, qos,
payload_available, payload_not_available) payload_available, payload_not_available)
MqttDiscoveryUpdate.__init__(self, discovery_hash) MqttDiscoveryUpdate.__init__(self, discovery_hash)
MqttEntityDeviceInfo.__init__(self, device_config)
self._position = None self._position = None
self._state = None self._state = None
self._name = name self._name = name

View File

@ -9,8 +9,9 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.rflink import ( from homeassistant.components.rflink import (
DATA_ENTITY_GROUP_LOOKUP, DATA_ENTITY_LOOKUP, CONF_ALIASES, CONF_DEVICE_DEFAULTS, CONF_DEVICES, CONF_FIRE_EVENT,
DEVICE_DEFAULTS_SCHEMA, EVENT_KEY_COMMAND, RflinkCommand) CONF_GROUP, CONF_GROUP_ALIASES, CONF_NOGROUP_ALIASES,
CONF_SIGNAL_REPETITIONS, DEVICE_DEFAULTS_SCHEMA, RflinkCommand)
from homeassistant.components.cover import ( from homeassistant.components.cover import (
CoverDevice, PLATFORM_SCHEMA) CoverDevice, PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -22,19 +23,6 @@ DEPENDENCIES = ['rflink']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_ALIASES = 'aliases'
CONF_GROUP_ALIASES = 'group_aliases'
CONF_GROUP = 'group'
CONF_NOGROUP_ALIASES = 'nogroup_aliases'
CONF_DEVICE_DEFAULTS = 'device_defaults'
CONF_DEVICES = 'devices'
CONF_AUTOMATIC_ADD = 'automatic_add'
CONF_FIRE_EVENT = 'fire_event'
CONF_IGNORE_DEVICES = 'ignore_devices'
CONF_RECONNECT_INTERVAL = 'reconnect_interval'
CONF_SIGNAL_REPETITIONS = 'signal_repetitions'
CONF_WAIT_FOR_ACK = 'wait_for_ack'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_DEVICE_DEFAULTS, default=DEVICE_DEFAULTS_SCHEMA({})): vol.Optional(CONF_DEVICE_DEFAULTS, default=DEVICE_DEFAULTS_SCHEMA({})):
DEVICE_DEFAULTS_SCHEMA, DEVICE_DEFAULTS_SCHEMA,
@ -55,33 +43,21 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
}) })
def devices_from_config(domain_config, hass=None): def devices_from_config(domain_config):
"""Parse configuration and add Rflink cover devices.""" """Parse configuration and add Rflink cover devices."""
devices = [] devices = []
for device_id, config in domain_config[CONF_DEVICES].items(): for device_id, config in domain_config[CONF_DEVICES].items():
device_config = dict(domain_config[CONF_DEVICE_DEFAULTS], **config) device_config = dict(domain_config[CONF_DEVICE_DEFAULTS], **config)
device = RflinkCover(device_id, hass, **device_config) device = RflinkCover(device_id, **device_config)
devices.append(device) devices.append(device)
# Register entity (and aliases) to listen to incoming rflink events
# Device id and normal aliases respond to normal and group command
hass.data[DATA_ENTITY_LOOKUP][
EVENT_KEY_COMMAND][device_id].append(device)
if config[CONF_GROUP]:
hass.data[DATA_ENTITY_GROUP_LOOKUP][
EVENT_KEY_COMMAND][device_id].append(device)
for _id in config[CONF_ALIASES]:
hass.data[DATA_ENTITY_LOOKUP][
EVENT_KEY_COMMAND][_id].append(device)
hass.data[DATA_ENTITY_GROUP_LOOKUP][
EVENT_KEY_COMMAND][_id].append(device)
return devices return devices
async def async_setup_platform(hass, config, async_add_entities, async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None): discovery_info=None):
"""Set up the Rflink cover platform.""" """Set up the Rflink cover platform."""
async_add_entities(devices_from_config(config, hass)) async_add_entities(devices_from_config(config))
class RflinkCover(RflinkCommand, CoverDevice): class RflinkCover(RflinkCommand, CoverDevice):

View File

@ -1,103 +0,0 @@
"""
Ryobi platform for the cover component.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/cover.ryobi_gdo/
"""
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.cover import (
CoverDevice, PLATFORM_SCHEMA, SUPPORT_OPEN, SUPPORT_CLOSE)
from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD, STATE_UNKNOWN, STATE_CLOSED)
REQUIREMENTS = ['py_ryobi_gdo==0.0.10']
_LOGGER = logging.getLogger(__name__)
CONF_DEVICE_ID = 'device_id'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]),
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
})
SUPPORTED_FEATURES = (SUPPORT_OPEN | SUPPORT_CLOSE)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Ryobi covers."""
from py_ryobi_gdo import RyobiGDO as ryobi_door
covers = []
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
devices = config.get(CONF_DEVICE_ID)
for device_id in devices:
my_door = ryobi_door(username, password, device_id)
_LOGGER.debug("Getting the API key")
if my_door.get_api_key() is False:
_LOGGER.error("Wrong credentials, no API key retrieved")
return
_LOGGER.debug("Checking if the device ID is present")
if my_door.check_device_id() is False:
_LOGGER.error("%s not in your device list", device_id)
return
_LOGGER.debug("Adding device %s to covers", device_id)
covers.append(RyobiCover(hass, my_door))
if covers:
_LOGGER.debug("Adding covers")
add_entities(covers, True)
class RyobiCover(CoverDevice):
"""Representation of a ryobi cover."""
def __init__(self, hass, ryobi_door):
"""Initialize the cover."""
self.ryobi_door = ryobi_door
self._name = 'ryobi_gdo_{}'.format(ryobi_door.get_device_id())
self._door_state = None
@property
def name(self):
"""Return the name of the cover."""
return self._name
@property
def is_closed(self):
"""Return if the cover is closed."""
if self._door_state == STATE_UNKNOWN:
return False
return self._door_state == STATE_CLOSED
@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
return 'garage'
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORTED_FEATURES
def close_cover(self, **kwargs):
"""Close the cover."""
_LOGGER.debug("Closing garage door")
self.ryobi_door.close_device()
def open_cover(self, **kwargs):
"""Open the cover."""
_LOGGER.debug("Opening garage door")
self.ryobi_door.open_device()
def update(self):
"""Update status from the door."""
_LOGGER.debug("Updating RyobiGDO status")
self.ryobi_door.update()
self._door_state = self.ryobi_door.get_door_status()

View File

@ -278,9 +278,10 @@ class CoverTemplate(CoverDevice):
async def async_open_cover(self, **kwargs): async def async_open_cover(self, **kwargs):
"""Move the cover up.""" """Move the cover up."""
if self._open_script: if self._open_script:
await self._open_script.async_run() await self._open_script.async_run(context=self._context)
elif self._position_script: elif self._position_script:
await self._position_script.async_run({"position": 100}) await self._position_script.async_run(
{"position": 100}, context=self._context)
if self._optimistic: if self._optimistic:
self._position = 100 self._position = 100
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
@ -288,9 +289,10 @@ class CoverTemplate(CoverDevice):
async def async_close_cover(self, **kwargs): async def async_close_cover(self, **kwargs):
"""Move the cover down.""" """Move the cover down."""
if self._close_script: if self._close_script:
await self._close_script.async_run() await self._close_script.async_run(context=self._context)
elif self._position_script: elif self._position_script:
await self._position_script.async_run({"position": 0}) await self._position_script.async_run(
{"position": 0}, context=self._context)
if self._optimistic: if self._optimistic:
self._position = 0 self._position = 0
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
@ -298,20 +300,21 @@ class CoverTemplate(CoverDevice):
async def async_stop_cover(self, **kwargs): async def async_stop_cover(self, **kwargs):
"""Fire the stop action.""" """Fire the stop action."""
if self._stop_script: if self._stop_script:
await self._stop_script.async_run() await self._stop_script.async_run(context=self._context)
async def async_set_cover_position(self, **kwargs): async def async_set_cover_position(self, **kwargs):
"""Set cover position.""" """Set cover position."""
self._position = kwargs[ATTR_POSITION] self._position = kwargs[ATTR_POSITION]
await self._position_script.async_run( await self._position_script.async_run(
{"position": self._position}) {"position": self._position}, context=self._context)
if self._optimistic: if self._optimistic:
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
async def async_open_cover_tilt(self, **kwargs): async def async_open_cover_tilt(self, **kwargs):
"""Tilt the cover open.""" """Tilt the cover open."""
self._tilt_value = 100 self._tilt_value = 100
await self._tilt_script.async_run({"tilt": self._tilt_value}) await self._tilt_script.async_run(
{"tilt": self._tilt_value}, context=self._context)
if self._tilt_optimistic: if self._tilt_optimistic:
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
@ -319,14 +322,15 @@ class CoverTemplate(CoverDevice):
"""Tilt the cover closed.""" """Tilt the cover closed."""
self._tilt_value = 0 self._tilt_value = 0
await self._tilt_script.async_run( await self._tilt_script.async_run(
{"tilt": self._tilt_value}) {"tilt": self._tilt_value}, context=self._context)
if self._tilt_optimistic: if self._tilt_optimistic:
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
async def async_set_cover_tilt_position(self, **kwargs): async def async_set_cover_tilt_position(self, **kwargs):
"""Move the cover tilt to a specific position.""" """Move the cover tilt to a specific position."""
self._tilt_value = kwargs[ATTR_TILT_POSITION] self._tilt_value = kwargs[ATTR_TILT_POSITION]
await self._tilt_script.async_run({"tilt": self._tilt_value}) await self._tilt_script.async_run(
{"tilt": self._tilt_value}, context=self._context)
if self._tilt_optimistic: if self._tilt_optimistic:
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()

View File

@ -4,21 +4,37 @@ Support for Z-Wave cover components.
For more details about this platform, please refer to the documentation For more details about this platform, please refer to the documentation
https://home-assistant.io/components/cover.zwave/ https://home-assistant.io/components/cover.zwave/
""" """
# Because we do not compile openzwave on CI
# pylint: disable=import-error
import logging import logging
from homeassistant.core import callback
from homeassistant.components.cover import ( from homeassistant.components.cover import (
DOMAIN, SUPPORT_OPEN, SUPPORT_CLOSE, ATTR_POSITION) DOMAIN, SUPPORT_OPEN, SUPPORT_CLOSE, ATTR_POSITION)
from homeassistant.components import zwave from homeassistant.components import zwave
from homeassistant.components.zwave import ( # noqa pylint: disable=unused-import from homeassistant.components.zwave import (
ZWaveDeviceEntity, async_setup_platform, workaround) ZWaveDeviceEntity, workaround)
from homeassistant.components.cover import CoverDevice from homeassistant.components.cover import CoverDevice
from homeassistant.helpers.dispatcher import async_dispatcher_connect
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Old method of setting up Z-Wave covers."""
pass
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Z-Wave Cover from Config Entry."""
@callback
def async_add_cover(cover):
"""Add Z-Wave Cover."""
async_add_entities([cover])
async_dispatcher_connect(hass, 'zwave_new_cover', async_add_cover)
def get_device(hass, values, node_config, **kwargs): def get_device(hass, values, node_config, **kwargs):
"""Create Z-Wave entity device.""" """Create Z-Wave entity device."""
invert_buttons = node_config.get(zwave.CONF_INVERT_OPENCLOSE_BUTTONS) invert_buttons = node_config.get(zwave.CONF_INVERT_OPENCLOSE_BUTTONS)

View File

@ -28,6 +28,6 @@
"title": "Op\u00e7\u00f5es extra de configura\u00e7\u00e3o para deCONZ" "title": "Op\u00e7\u00f5es extra de configura\u00e7\u00e3o para deCONZ"
} }
}, },
"title": "deCONZ" "title": "Gateway Zigbee deCONZ"
} }
} }

View File

@ -16,7 +16,9 @@ CONF_ALLOW_DECONZ_GROUPS = 'allow_deconz_groups'
ATTR_DARK = 'dark' ATTR_DARK = 'dark'
ATTR_ON = 'on' ATTR_ON = 'on'
COVER_TYPES = ["Level controllable output"] DAMPERS = ["Level controllable output"]
WINDOW_COVERS = ["Window covering device"]
COVER_TYPES = DAMPERS + WINDOW_COVERS
POWER_PLUGS = ["On/Off plug-in unit", "Smart plug"] POWER_PLUGS = ["On/Off plug-in unit", "Smart plug"]
SIRENS = ["Warning device"] SIRENS = ["Warning device"]

View File

@ -10,20 +10,26 @@ import re
import requests import requests
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import ( from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner) DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_DDWRT_DATA_REGEX = re.compile(r'\{(\w+)::([^\}]*)\}') _DDWRT_DATA_REGEX = re.compile(r'\{(\w+)::([^\}]*)\}')
_MAC_REGEX = re.compile(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})') _MAC_REGEX = re.compile(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})')
DEFAULT_SSL = False
DEFAULT_VERIFY_SSL = True
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
}) })
@ -39,7 +45,9 @@ class DdWrtDeviceScanner(DeviceScanner):
"""This class queries a wireless router running DD-WRT firmware.""" """This class queries a wireless router running DD-WRT firmware."""
def __init__(self, config): def __init__(self, config):
"""Initialize the scanner.""" """Initialize the DD-WRT scanner."""
self.protocol = 'https' if config[CONF_SSL] else 'http'
self.verify_ssl = config[CONF_VERIFY_SSL]
self.host = config[CONF_HOST] self.host = config[CONF_HOST]
self.username = config[CONF_USERNAME] self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD] self.password = config[CONF_PASSWORD]
@ -48,7 +56,8 @@ class DdWrtDeviceScanner(DeviceScanner):
self.mac2name = {} self.mac2name = {}
# Test the router is accessible # Test the router is accessible
url = 'http://{}/Status_Wireless.live.asp'.format(self.host) url = '{}://{}/Status_Wireless.live.asp'.format(
self.protocol, self.host)
data = self.get_ddwrt_data(url) data = self.get_ddwrt_data(url)
if not data: if not data:
raise ConnectionError('Cannot connect to DD-Wrt router') raise ConnectionError('Cannot connect to DD-Wrt router')
@ -63,7 +72,8 @@ class DdWrtDeviceScanner(DeviceScanner):
"""Return the name of the given device or None if we don't know.""" """Return the name of the given device or None if we don't know."""
# If not initialised and not already scanned and not found. # If not initialised and not already scanned and not found.
if device not in self.mac2name: if device not in self.mac2name:
url = 'http://{}/Status_Lan.live.asp'.format(self.host) url = '{}://{}/Status_Lan.live.asp'.format(
self.protocol, self.host)
data = self.get_ddwrt_data(url) data = self.get_ddwrt_data(url)
if not data: if not data:
@ -98,7 +108,8 @@ class DdWrtDeviceScanner(DeviceScanner):
""" """
_LOGGER.info("Checking ARP") _LOGGER.info("Checking ARP")
url = 'http://{}/Status_Wireless.live.asp'.format(self.host) url = '{}://{}/Status_Wireless.live.asp'.format(
self.protocol, self.host)
data = self.get_ddwrt_data(url) data = self.get_ddwrt_data(url)
if not data: if not data:
@ -125,7 +136,8 @@ class DdWrtDeviceScanner(DeviceScanner):
"""Retrieve data from DD-WRT and return parsed result.""" """Retrieve data from DD-WRT and return parsed result."""
try: try:
response = requests.get( response = requests.get(
url, auth=(self.username, self.password), timeout=4) url, auth=(self.username, self.password),
timeout=4, verify=self.verify_ssl)
except requests.exceptions.Timeout: except requests.exceptions.Timeout:
_LOGGER.exception("Connection to the router timed out") _LOGGER.exception("Connection to the router timed out")
return return
@ -142,5 +154,4 @@ class DdWrtDeviceScanner(DeviceScanner):
def _parse_ddwrt_response(data_str): def _parse_ddwrt_response(data_str):
"""Parse the DD-WRT data format.""" """Parse the DD-WRT data format."""
return { return {
key: val for key, val in _DDWRT_DATA_REGEX key: val for key, val in _DDWRT_DATA_REGEX.findall(data_str)}
.findall(data_str)}

View File

@ -43,8 +43,7 @@ class FritzBoxScanner(DeviceScanner):
self.password = config[CONF_PASSWORD] self.password = config[CONF_PASSWORD]
self.success_init = True self.success_init = True
# pylint: disable=import-error import fritzconnection as fc # pylint: disable=import-error
import fritzconnection as fc
# Establish a connection to the FRITZ!Box. # Establish a connection to the FRITZ!Box.
try: try:

View File

@ -19,7 +19,7 @@ from homeassistant.helpers.event import track_time_interval
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.util import slugify, dt as dt_util from homeassistant.util import slugify, dt as dt_util
REQUIREMENTS = ['locationsharinglib==3.0.3'] REQUIREMENTS = ['locationsharinglib==3.0.6']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -15,7 +15,7 @@ from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT, CONF_SSL, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT, CONF_SSL,
CONF_DEVICES, CONF_EXCLUDE) CONF_DEVICES, CONF_EXCLUDE)
REQUIREMENTS = ['pynetgear==0.4.2'] REQUIREMENTS = ['pynetgear==0.5.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -0,0 +1,69 @@
"""
Support for Verizon FiOS Quantum Gateways.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.quantum_gateway/
"""
import logging
from requests.exceptions import RequestException
import voluptuous as vol
from homeassistant.components.device_tracker import (DOMAIN, PLATFORM_SCHEMA,
DeviceScanner)
from homeassistant.const import (CONF_HOST, CONF_PASSWORD)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['quantum-gateway==0.0.3']
_LOGGER = logging.getLogger(__name__)
DEFAULT_HOST = 'myfiosgateway.com'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Required(CONF_PASSWORD): cv.string
})
def get_scanner(hass, config):
"""Validate the configuration and return a Quantum Gateway scanner."""
scanner = QuantumGatewayDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None
class QuantumGatewayDeviceScanner(DeviceScanner):
"""This class queries a Quantum Gateway."""
def __init__(self, config):
"""Initialize the scanner."""
from quantum_gateway import QuantumGatewayScanner
self.host = config[CONF_HOST]
self.password = config[CONF_PASSWORD]
_LOGGER.debug('Initializing')
try:
self.quantum = QuantumGatewayScanner(self.host, self.password)
self.success_init = self.quantum.success_init
except RequestException:
self.success_init = False
_LOGGER.error("Unable to connect to gateway. Check host.")
if not self.success_init:
_LOGGER.error("Unable to login to gateway. Check password and "
"host.")
def scan_devices(self):
"""Scan for new devices and return a list of found MACs."""
connected_devices = []
try:
connected_devices = self.quantum.scan_devices()
except RequestException:
_LOGGER.error("Unable to scan devices. Check connection to router")
return connected_devices
def get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
return self.quantum.get_device_name(device)

View File

@ -86,10 +86,9 @@ class UnifiDeviceScanner(DeviceScanner):
def _disconnect(self): def _disconnect(self):
"""Disconnect the current SSH connection.""" """Disconnect the current SSH connection."""
# pylint: disable=broad-except
try: try:
self.ssh.logout() self.ssh.logout()
except Exception: except Exception: # pylint: disable=broad-except
pass pass
finally: finally:
self.ssh = None self.ssh = None

View File

@ -8,10 +8,12 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_TOKEN
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (DOMAIN, PLATFORM_SCHEMA,
DeviceScanner) REQUIREMENTS = ['python-miio==0.4.2', 'construct==2.9.45']
from homeassistant.const import (CONF_HOST, CONF_TOKEN)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -20,8 +22,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)), vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)),
}) })
REQUIREMENTS = ['python-miio==0.4.1', 'construct==2.9.41']
def get_scanner(hass, config): def get_scanner(hass, config):
"""Return a Xiaomi MiIO device scanner.""" """Return a Xiaomi MiIO device scanner."""
@ -56,7 +56,7 @@ class XiaomiMiioDeviceScanner(DeviceScanner):
self.device = device self.device = device
async def async_scan_devices(self): async def async_scan_devices(self):
"""Scan for devices and return a list containing found device ids.""" """Scan for devices and return a list containing found device IDs."""
from miio import DeviceException from miio import DeviceException
devices = [] devices = []
@ -68,7 +68,7 @@ class XiaomiMiioDeviceScanner(DeviceScanner):
devices.append(device['mac']) devices.append(device['mac'])
except DeviceException as ex: except DeviceException as ex:
_LOGGER.error("Got exception while fetching the state: %s", ex) _LOGGER.error("Unable to fetch the state: %s", ex)
return devices return devices

View File

@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.helpers.discovery import async_load_platform, async_discover from homeassistant.helpers.discovery import async_load_platform, async_discover
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
REQUIREMENTS = ['netdisco==2.1.0'] REQUIREMENTS = ['netdisco==2.2.0']
DOMAIN = 'discovery' DOMAIN = 'discovery'
@ -43,6 +43,7 @@ SERVICE_DAIKIN = 'daikin'
SERVICE_SABNZBD = 'sabnzbd' SERVICE_SABNZBD = 'sabnzbd'
SERVICE_SAMSUNG_PRINTER = 'samsung_printer' SERVICE_SAMSUNG_PRINTER = 'samsung_printer'
SERVICE_HOMEKIT = 'homekit' SERVICE_HOMEKIT = 'homekit'
SERVICE_OCTOPRINT = 'octoprint'
CONFIG_ENTRY_HANDLERS = { CONFIG_ENTRY_HANDLERS = {
SERVICE_DECONZ: 'deconz', SERVICE_DECONZ: 'deconz',
@ -67,6 +68,7 @@ SERVICE_HANDLERS = {
SERVICE_SABNZBD: ('sabnzbd', None), SERVICE_SABNZBD: ('sabnzbd', None),
SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'), SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'),
SERVICE_KONNECTED: ('konnected', None), SERVICE_KONNECTED: ('konnected', None),
SERVICE_OCTOPRINT: ('octoprint', None),
'panasonic_viera': ('media_player', 'panasonic_viera'), 'panasonic_viera': ('media_player', 'panasonic_viera'),
'plex_mediaserver': ('media_player', 'plex'), 'plex_mediaserver': ('media_player', 'plex'),
'roku': ('media_player', 'roku'), 'roku': ('media_player', 'roku'),
@ -84,6 +86,7 @@ SERVICE_HANDLERS = {
'songpal': ('media_player', 'songpal'), 'songpal': ('media_player', 'songpal'),
'kodi': ('media_player', 'kodi'), 'kodi': ('media_player', 'kodi'),
'volumio': ('media_player', 'volumio'), 'volumio': ('media_player', 'volumio'),
'lg_smart_device': ('media_player', 'lg_soundbar'),
'nanoleaf_aurora': ('light', 'nanoleaf_aurora'), 'nanoleaf_aurora': ('light', 'nanoleaf_aurora'),
'freebox': ('device_tracker', 'freebox'), 'freebox': ('device_tracker', 'freebox'),
} }

View File

@ -102,5 +102,6 @@ def setup(hass, config):
discovery.load_platform(hass, "sensor", DOMAIN, {}, config) discovery.load_platform(hass, "sensor", DOMAIN, {}, config)
discovery.load_platform(hass, "fan", DOMAIN, {}, config) discovery.load_platform(hass, "fan", DOMAIN, {}, config)
discovery.load_platform(hass, "vacuum", DOMAIN, {}, config) discovery.load_platform(hass, "vacuum", DOMAIN, {}, config)
discovery.load_platform(hass, "climate", DOMAIN, {}, config)
return True return True

View File

@ -0,0 +1,232 @@
"""
Support the ElkM1 Gold and ElkM1 EZ8 alarm / integration panels.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/elkm1/
"""
import logging
import re
import voluptuous as vol
from homeassistant.const import (
CONF_EXCLUDE, CONF_HOST, CONF_INCLUDE, CONF_PASSWORD,
CONF_TEMPERATURE_UNIT, CONF_USERNAME, TEMP_FAHRENHEIT)
from homeassistant.core import HomeAssistant, callback # noqa
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import ConfigType # noqa
DOMAIN = "elkm1"
REQUIREMENTS = ['elkm1-lib==0.7.10']
CONF_AREA = 'area'
CONF_COUNTER = 'counter'
CONF_KEYPAD = 'keypad'
CONF_OUTPUT = 'output'
CONF_SETTING = 'setting'
CONF_TASK = 'task'
CONF_THERMOSTAT = 'thermostat'
CONF_PLC = 'plc'
CONF_ZONE = 'zone'
CONF_ENABLED = 'enabled'
_LOGGER = logging.getLogger(__name__)
SUPPORTED_DOMAINS = ['alarm_control_panel', 'climate', 'light', 'scene',
'sensor', 'switch']
SPEAK_SERVICE_SCHEMA = vol.Schema({
vol.Required('number'):
vol.All(vol.Coerce(int), vol.Range(min=0, max=999))
})
def _host_validator(config):
"""Validate that a host is properly configured."""
if config[CONF_HOST].startswith('elks://'):
if CONF_USERNAME not in config or CONF_PASSWORD not in config:
raise vol.Invalid("Specify username and password for elks://")
elif not config[CONF_HOST].startswith('elk://') and not config[
CONF_HOST].startswith('serial://'):
raise vol.Invalid("Invalid host URL")
return config
def _elk_range_validator(rng):
def _housecode_to_int(val):
match = re.search(r'^([a-p])(0[1-9]|1[0-6]|[1-9])$', val.lower())
if match:
return (ord(match.group(1)) - ord('a')) * 16 + int(match.group(2))
raise vol.Invalid("Invalid range")
def _elk_value(val):
return int(val) if val.isdigit() else _housecode_to_int(val)
vals = [s.strip() for s in str(rng).split('-')]
start = _elk_value(vals[0])
end = start if len(vals) == 1 else _elk_value(vals[1])
return (start, end)
CONFIG_SCHEMA_SUBDOMAIN = vol.Schema({
vol.Optional(CONF_ENABLED, default=True): cv.boolean,
vol.Optional(CONF_INCLUDE, default=[]): [_elk_range_validator],
vol.Optional(CONF_EXCLUDE, default=[]): [_elk_range_validator],
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema(
{
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_USERNAME, default=''): cv.string,
vol.Optional(CONF_PASSWORD, default=''): cv.string,
vol.Optional(CONF_TEMPERATURE_UNIT, default=TEMP_FAHRENHEIT):
cv.temperature_unit,
vol.Optional(CONF_AREA): CONFIG_SCHEMA_SUBDOMAIN,
vol.Optional(CONF_COUNTER): CONFIG_SCHEMA_SUBDOMAIN,
vol.Optional(CONF_KEYPAD): CONFIG_SCHEMA_SUBDOMAIN,
vol.Optional(CONF_OUTPUT): CONFIG_SCHEMA_SUBDOMAIN,
vol.Optional(CONF_PLC): CONFIG_SCHEMA_SUBDOMAIN,
vol.Optional(CONF_SETTING): CONFIG_SCHEMA_SUBDOMAIN,
vol.Optional(CONF_TASK): CONFIG_SCHEMA_SUBDOMAIN,
vol.Optional(CONF_THERMOSTAT): CONFIG_SCHEMA_SUBDOMAIN,
vol.Optional(CONF_ZONE): CONFIG_SCHEMA_SUBDOMAIN,
},
_host_validator,
)
}, extra=vol.ALLOW_EXTRA)
async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool:
"""Set up the Elk M1 platform."""
from elkm1_lib.const import Max
import elkm1_lib as elkm1
configs = {
CONF_AREA: Max.AREAS.value,
CONF_COUNTER: Max.COUNTERS.value,
CONF_KEYPAD: Max.KEYPADS.value,
CONF_OUTPUT: Max.OUTPUTS.value,
CONF_PLC: Max.LIGHTS.value,
CONF_SETTING: Max.SETTINGS.value,
CONF_TASK: Max.TASKS.value,
CONF_THERMOSTAT: Max.THERMOSTATS.value,
CONF_ZONE: Max.ZONES.value,
}
def _included(ranges, set_to, values):
for rng in ranges:
if not rng[0] <= rng[1] <= len(values):
raise vol.Invalid("Invalid range {}".format(rng))
values[rng[0]-1:rng[1]] = [set_to] * (rng[1] - rng[0] + 1)
conf = hass_config[DOMAIN]
config = {'temperature_unit': conf[CONF_TEMPERATURE_UNIT]}
config['panel'] = {'enabled': True, 'included': [True]}
for item, max_ in configs.items():
config[item] = {'enabled': conf[item][CONF_ENABLED],
'included': [not conf[item]['include']] * max_}
try:
_included(conf[item]['include'], True, config[item]['included'])
_included(conf[item]['exclude'], False, config[item]['included'])
except (ValueError, vol.Invalid) as err:
_LOGGER.error("Config item: %s; %s", item, err)
return False
elk = elkm1.Elk({'url': conf[CONF_HOST], 'userid': conf[CONF_USERNAME],
'password': conf[CONF_PASSWORD]})
elk.connect()
_create_elk_services(hass, elk)
hass.data[DOMAIN] = {'elk': elk, 'config': config, 'keypads': {}}
for component in SUPPORTED_DOMAINS:
hass.async_create_task(
discovery.async_load_platform(hass, component, DOMAIN, {}))
return True
def _create_elk_services(hass, elk):
def _speak_word_service(service):
elk.panel.speak_word(service.data.get('number'))
def _speak_phrase_service(service):
elk.panel.speak_phrase(service.data.get('number'))
hass.services.async_register(
DOMAIN, 'speak_word', _speak_word_service, SPEAK_SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, 'speak_phrase', _speak_phrase_service, SPEAK_SERVICE_SCHEMA)
def create_elk_entities(hass, elk_elements, element_type, class_, entities):
"""Create the ElkM1 devices of a particular class."""
elk_data = hass.data[DOMAIN]
if elk_data['config'][element_type]['enabled']:
elk = elk_data['elk']
for element in elk_elements:
if elk_data['config'][element_type]['included'][element.index]:
entities.append(class_(element, elk, elk_data))
return entities
class ElkEntity(Entity):
"""Base class for all Elk entities."""
def __init__(self, element, elk, elk_data):
"""Initialize the base of all Elk devices."""
self._elk = elk
self._element = element
self._temperature_unit = elk_data['config']['temperature_unit']
self._unique_id = 'elkm1_{}'.format(
self._element.default_name('_').lower())
@property
def name(self):
"""Name of the element."""
return self._element.name
@property
def unique_id(self):
"""Return unique id of the element."""
return self._unique_id
@property
def should_poll(self) -> bool:
"""Don't poll this device."""
return False
@property
def device_state_attributes(self):
"""Return the default attributes of the element."""
return {**self._element.as_dict(), **self.initial_attrs()}
@property
def available(self):
"""Is the entity available to be updated."""
return self._elk.is_connected()
def initial_attrs(self):
"""Return the underlying element's attributes as a dict."""
attrs = {}
attrs['index'] = self._element.index + 1
return attrs
def _element_changed(self, element, changeset):
pass
@callback
def _element_callback(self, element, changeset):
"""Handle callback from an Elk element that has changed."""
self._element_changed(element, changeset)
self.async_schedule_update_ha_state(True)
async def async_added_to_hass(self):
"""Register callback for ElkM1 changes and update entity state."""
self._element.add_callback(self._element_callback)
self._element_callback(self._element, {})

View File

@ -0,0 +1,12 @@
speak_word:
description: Speak a word. See list of words in ElkM1 ASCII Protocol documentation.
fields:
number:
description: Word number to speak.
example: 142
speak_phrase:
description: Speak a phrase. See list of phrases in ElkM1 ASCII Protocol documentation.
fields:
number:
description: Phrase number to speak.
example: 42

View File

@ -10,20 +10,23 @@ from typing import Optional
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.components import mqtt from homeassistant.components import fan, mqtt
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_OPTIMISTIC, CONF_STATE, STATE_ON, STATE_OFF, CONF_NAME, CONF_OPTIMISTIC, CONF_STATE, STATE_ON, STATE_OFF,
CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON) CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, CONF_DEVICE)
from homeassistant.components.mqtt import ( from homeassistant.components.mqtt import (
ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC,
CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE,
CONF_QOS, CONF_RETAIN, MqttAvailability, MqttDiscoveryUpdate) CONF_QOS, CONF_RETAIN, MqttAvailability, MqttDiscoveryUpdate,
MqttEntityDeviceInfo)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.helpers.typing import HomeAssistantType, ConfigType
from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM,
SPEED_HIGH, FanEntity, SPEED_HIGH, FanEntity,
SUPPORT_SET_SPEED, SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, SUPPORT_OSCILLATE,
SPEED_OFF, ATTR_SPEED) SPEED_OFF, ATTR_SPEED)
from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -77,19 +80,32 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
SPEED_MEDIUM, SPEED_HIGH]): cv.ensure_list, SPEED_MEDIUM, SPEED_HIGH]): cv.ensure_list,
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA,
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, async def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
async_add_entities, discovery_info=None): async_add_entities, discovery_info=None):
"""Set up the MQTT fan platform.""" """Set up MQTT fan through configuration.yaml."""
if discovery_info is not None: await _async_setup_entity(hass, config, async_add_entities)
config = PLATFORM_SCHEMA(discovery_info)
discovery_hash = None
if discovery_info is not None and ATTR_DISCOVERY_HASH in discovery_info:
discovery_hash = discovery_info[ATTR_DISCOVERY_HASH]
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up MQTT fan dynamically through MQTT discovery."""
async def async_discover(discovery_payload):
"""Discover and add a MQTT fan."""
config = PLATFORM_SCHEMA(discovery_payload)
await _async_setup_entity(hass, config, async_add_entities,
discovery_payload[ATTR_DISCOVERY_HASH])
async_dispatcher_connect(
hass, MQTT_DISCOVERY_NEW.format(fan.DOMAIN, 'mqtt'),
async_discover)
async def _async_setup_entity(hass, config, async_add_entities,
discovery_hash=None):
"""Set up the MQTT fan."""
async_add_entities([MqttFan( async_add_entities([MqttFan(
config.get(CONF_NAME), config.get(CONF_NAME),
{ {
@ -124,21 +140,24 @@ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
config.get(CONF_PAYLOAD_AVAILABLE), config.get(CONF_PAYLOAD_AVAILABLE),
config.get(CONF_PAYLOAD_NOT_AVAILABLE), config.get(CONF_PAYLOAD_NOT_AVAILABLE),
config.get(CONF_UNIQUE_ID), config.get(CONF_UNIQUE_ID),
config.get(CONF_DEVICE),
discovery_hash, discovery_hash,
)]) )])
class MqttFan(MqttAvailability, MqttDiscoveryUpdate, FanEntity): class MqttFan(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
FanEntity):
"""A MQTT fan component.""" """A MQTT fan component."""
def __init__(self, name, topic, templates, qos, retain, payload, def __init__(self, name, topic, templates, qos, retain, payload,
speed_list, optimistic, availability_topic, payload_available, speed_list, optimistic, availability_topic, payload_available,
payload_not_available, unique_id: Optional[str], payload_not_available, unique_id: Optional[str],
discovery_hash): device_config: Optional[ConfigType], discovery_hash):
"""Initialize the MQTT fan.""" """Initialize the MQTT fan."""
MqttAvailability.__init__(self, availability_topic, qos, MqttAvailability.__init__(self, availability_topic, qos,
payload_available, payload_not_available) payload_available, payload_not_available)
MqttDiscoveryUpdate.__init__(self, discovery_hash) MqttDiscoveryUpdate.__init__(self, discovery_hash)
MqttEntityDeviceInfo.__init__(self, device_config)
self._name = name self._name = name
self._topic = topic self._topic = topic
self._qos = qos self._qos = qos

View File

@ -224,7 +224,7 @@ class TemplateFan(FanEntity):
# pylint: disable=arguments-differ # pylint: disable=arguments-differ
async def async_turn_on(self, speed: str = None) -> None: async def async_turn_on(self, speed: str = None) -> None:
"""Turn on the fan.""" """Turn on the fan."""
await self._on_script.async_run() await self._on_script.async_run(context=self._context)
self._state = STATE_ON self._state = STATE_ON
if speed is not None: if speed is not None:
@ -233,7 +233,7 @@ class TemplateFan(FanEntity):
# pylint: disable=arguments-differ # pylint: disable=arguments-differ
async def async_turn_off(self) -> None: async def async_turn_off(self) -> None:
"""Turn off the fan.""" """Turn off the fan."""
await self._off_script.async_run() await self._off_script.async_run(context=self._context)
self._state = STATE_OFF self._state = STATE_OFF
async def async_set_speed(self, speed: str) -> None: async def async_set_speed(self, speed: str) -> None:
@ -243,7 +243,8 @@ class TemplateFan(FanEntity):
if speed in self._speed_list: if speed in self._speed_list:
self._speed = speed self._speed = speed
await self._set_speed_script.async_run({ATTR_SPEED: speed}) await self._set_speed_script.async_run(
{ATTR_SPEED: speed}, context=self._context)
else: else:
_LOGGER.error( _LOGGER.error(
'Received invalid speed: %s. Expected: %s.', 'Received invalid speed: %s. Expected: %s.',
@ -257,7 +258,7 @@ class TemplateFan(FanEntity):
if oscillating in _VALID_OSC: if oscillating in _VALID_OSC:
self._oscillating = oscillating self._oscillating = oscillating
await self._set_oscillating_script.async_run( await self._set_oscillating_script.async_run(
{ATTR_OSCILLATING: oscillating}) {ATTR_OSCILLATING: oscillating}, context=self._context)
else: else:
_LOGGER.error( _LOGGER.error(
'Received invalid oscillating value: %s. Expected: %s.', 'Received invalid oscillating value: %s. Expected: %s.',
@ -271,7 +272,7 @@ class TemplateFan(FanEntity):
if direction in _VALID_DIRECTIONS: if direction in _VALID_DIRECTIONS:
self._direction = direction self._direction = direction
await self._set_direction_script.async_run( await self._set_direction_script.async_run(
{ATTR_DIRECTION: direction}) {ATTR_DIRECTION: direction}, context=self._context)
else: else:
_LOGGER.error( _LOGGER.error(
'Received invalid direction: %s. Expected: %s.', 'Received invalid direction: %s. Expected: %s.',

View File

@ -11,13 +11,15 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.fan import (FanEntity, PLATFORM_SCHEMA, from homeassistant.components.fan import (
SUPPORT_SET_SPEED, DOMAIN, ) DOMAIN, PLATFORM_SCHEMA, SUPPORT_SET_SPEED, FanEntity)
from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN, from homeassistant.const import (
ATTR_ENTITY_ID, ) ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN)
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['python-miio==0.4.2', 'construct==2.9.45']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Xiaomi Miio Device' DEFAULT_NAME = 'Xiaomi Miio Device'
@ -49,8 +51,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
'zhimi.humidifier.ca1']), 'zhimi.humidifier.ca1']),
}) })
REQUIREMENTS = ['python-miio==0.4.1', 'construct==2.9.41']
ATTR_MODEL = 'model' ATTR_MODEL = 'model'
# Air Purifier # Air Purifier

View File

@ -7,11 +7,12 @@ https://home-assistant.io/components/fan.zwave/
import logging import logging
import math import math
from homeassistant.core import callback
from homeassistant.components.fan import ( from homeassistant.components.fan import (
DOMAIN, FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, DOMAIN, FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
SUPPORT_SET_SPEED) SUPPORT_SET_SPEED)
from homeassistant.components import zwave from homeassistant.components import zwave
from homeassistant.components.zwave import async_setup_platform # noqa pylint: disable=unused-import from homeassistant.helpers.dispatcher import async_dispatcher_connect
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -35,6 +36,22 @@ SPEED_TO_VALUE = {
} }
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Old method of setting up Z-Wave fans."""
pass
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Z-Wave Fan from Config Entry."""
@callback
def async_add_fan(fan):
"""Add Z-Wave Fan."""
async_add_entities([fan])
async_dispatcher_connect(hass, 'zwave_new_fan', async_add_fan)
def get_device(values, **kwargs): def get_device(values, **kwargs):
"""Create Z-Wave entity device.""" """Create Z-Wave entity device."""
return ZwaveFan(values) return ZwaveFan(values)

View File

@ -24,7 +24,7 @@ from homeassistant.core import callback
from homeassistant.helpers.translation import async_get_translations from homeassistant.helpers.translation import async_get_translations
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
REQUIREMENTS = ['home-assistant-frontend==20181018.0'] REQUIREMENTS = ['home-assistant-frontend==20181026.0']
DOMAIN = 'frontend' DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log',

View File

@ -1,32 +1,34 @@
""" """
Geo Location component. Geo Location component.
This component covers platforms that deal with external events that contain
a geo location related to the installed HA instance.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/geo_location/ https://home-assistant.io/components/geo_location/
""" """
import logging
from datetime import timedelta from datetime import timedelta
import logging
from typing import Optional from typing import Optional
from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity 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
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_DISTANCE = 'distance' ATTR_DISTANCE = 'distance'
ATTR_SOURCE = 'source'
DOMAIN = 'geo_location' DOMAIN = 'geo_location'
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'
GROUP_NAME_ALL_EVENTS = 'All Geo Location Events' GROUP_NAME_ALL_EVENTS = 'All Geo Location Events'
SCAN_INTERVAL = timedelta(seconds=60) SCAN_INTERVAL = timedelta(seconds=60)
async def async_setup(hass, config): async def async_setup(hass, config):
"""Set up this component.""" """Set up the Geo Location component."""
component = EntityComponent( component = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_EVENTS) _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_EVENTS)
await component.async_setup(config) await component.async_setup(config)
@ -43,6 +45,11 @@ class GeoLocationEvent(Entity):
return round(self.distance, 1) return round(self.distance, 1)
return None return None
@property
def source(self) -> str:
"""Return source value of this external event."""
raise NotImplementedError
@property @property
def distance(self) -> Optional[float]: def distance(self) -> Optional[float]:
"""Return distance value of this external event.""" """Return distance value of this external event."""
@ -66,4 +73,6 @@ class GeoLocationEvent(Entity):
data[ATTR_LATITUDE] = round(self.latitude, 5) data[ATTR_LATITUDE] = round(self.latitude, 5)
if self.longitude is not None: if self.longitude is not None:
data[ATTR_LONGITUDE] = round(self.longitude, 5) data[ATTR_LONGITUDE] = round(self.longitude, 5)
if self.source is not None:
data[ATTR_SOURCE] = self.source
return data return data

View File

@ -4,10 +4,10 @@ Demo platform for the geo location component.
For more details about this platform, please refer to the documentation For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/ https://home-assistant.io/components/demo/
""" """
import logging
import random
from datetime import timedelta from datetime import timedelta
from math import pi, cos, sin, radians import logging
from math import cos, pi, radians, sin
import random
from typing import Optional from typing import Optional
from homeassistant.components.geo_location import GeoLocationEvent from homeassistant.components.geo_location import GeoLocationEvent
@ -16,7 +16,7 @@ from homeassistant.helpers.event import track_time_interval
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
AVG_KM_PER_DEGREE = 111.0 AVG_KM_PER_DEGREE = 111.0
DEFAULT_UNIT_OF_MEASUREMENT = "km" DEFAULT_UNIT_OF_MEASUREMENT = 'km'
DEFAULT_UPDATE_INTERVAL = timedelta(minutes=1) DEFAULT_UPDATE_INTERVAL = timedelta(minutes=1)
MAX_RADIUS_IN_KM = 50 MAX_RADIUS_IN_KM = 50
NUMBER_OF_DEMO_DEVICES = 5 NUMBER_OF_DEMO_DEVICES = 5
@ -26,6 +26,8 @@ EVENT_NAMES = ["Bushfire", "Hazard Reduction", "Grass Fire", "Burn off",
"Cyclone", "Waterspout", "Dust Storm", "Blizzard", "Ice Storm", "Cyclone", "Waterspout", "Dust Storm", "Blizzard", "Ice Storm",
"Earthquake", "Tsunami"] "Earthquake", "Tsunami"]
SOURCE = 'demo'
def setup_platform(hass, config, add_entities, discovery_info=None): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Demo geo locations.""" """Set up the Demo geo locations."""
@ -100,6 +102,11 @@ class DemoGeoLocationEvent(GeoLocationEvent):
self._longitude = longitude self._longitude = longitude
self._unit_of_measurement = unit_of_measurement self._unit_of_measurement = unit_of_measurement
@property
def source(self) -> str:
"""Return source value of this external event."""
return SOURCE
@property @property
def name(self) -> Optional[str]: def name(self) -> Optional[str]:
"""Return the name of the event.""" """Return the name of the event."""

View File

@ -1,24 +1,23 @@
""" """
Generic GeoJSON events platform. Generic GeoJSON events platform.
Retrieves current events (typically incidents or alerts) in GeoJSON format, and
displays information on events filtered by distance to the HA instance's
location.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/geo_location/geo_json_events/ https://home-assistant.io/components/geo_location/geo_json_events/
""" """
import logging
from datetime import timedelta from datetime import timedelta
import logging
from typing import Optional from typing import Optional
import voluptuous as vol import voluptuous as vol
from homeassistant.components.geo_location import (
PLATFORM_SCHEMA, GeoLocationEvent)
from homeassistant.const import (
CONF_RADIUS, CONF_SCAN_INTERVAL, CONF_URL, EVENT_HOMEASSISTANT_START)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.geo_location import GeoLocationEvent from homeassistant.helpers.dispatcher import (
from homeassistant.const import CONF_RADIUS, CONF_URL, CONF_SCAN_INTERVAL, \ async_dispatcher_connect, dispatcher_send)
EVENT_HOMEASSISTANT_START
from homeassistant.components.geo_location import PLATFORM_SCHEMA
from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.event import track_time_interval
REQUIREMENTS = ['geojson_client==0.1'] REQUIREMENTS = ['geojson_client==0.1']
@ -28,14 +27,18 @@ _LOGGER = logging.getLogger(__name__)
ATTR_EXTERNAL_ID = 'external_id' ATTR_EXTERNAL_ID = 'external_id'
DEFAULT_RADIUS_IN_KM = 20.0 DEFAULT_RADIUS_IN_KM = 20.0
DEFAULT_UNIT_OF_MEASUREMENT = "km" DEFAULT_UNIT_OF_MEASUREMENT = 'km'
SCAN_INTERVAL = timedelta(minutes=5) SCAN_INTERVAL = timedelta(minutes=5)
SIGNAL_DELETE_ENTITY = 'geo_json_events_delete_{}'
SIGNAL_UPDATE_ENTITY = 'geo_json_events_update_{}'
SOURCE = 'geo_json_events'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_URL): cv.string, vol.Required(CONF_URL): cv.string,
vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS_IN_KM): vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS_IN_KM): vol.Coerce(float),
vol.Coerce(float),
}) })
@ -45,7 +48,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
radius_in_km = config[CONF_RADIUS] radius_in_km = config[CONF_RADIUS]
# Initialize the entity manager. # Initialize the entity manager.
GeoJsonFeedManager(hass, add_entities, scan_interval, url, radius_in_km) feed = GeoJsonFeedManager(hass, add_entities, scan_interval, url,
radius_in_km)
def start_feed_manager(event):
"""Start feed manager."""
feed.startup()
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_feed_manager)
class GeoJsonFeedManager: class GeoJsonFeedManager:
@ -54,94 +64,117 @@ class GeoJsonFeedManager:
def __init__(self, hass, add_entities, scan_interval, url, radius_in_km): def __init__(self, hass, add_entities, scan_interval, url, radius_in_km):
"""Initialize the GeoJSON Feed Manager.""" """Initialize the GeoJSON Feed Manager."""
from geojson_client.generic_feed import GenericFeed from geojson_client.generic_feed import GenericFeed
self._hass = hass self._hass = hass
self._feed = GenericFeed((hass.config.latitude, hass.config.longitude), self._feed = GenericFeed(
(hass.config.latitude, hass.config.longitude),
filter_radius=radius_in_km, url=url) filter_radius=radius_in_km, url=url)
self._add_entities = add_entities self._add_entities = add_entities
self._scan_interval = scan_interval self._scan_interval = scan_interval
self._feed_entries = [] self.feed_entries = {}
self._managed_entities = [] self._managed_external_ids = set()
hass.bus.listen_once(
EVENT_HOMEASSISTANT_START, lambda _: self._update()) def startup(self):
"""Start up this manager."""
self._update()
self._init_regular_updates() self._init_regular_updates()
def _init_regular_updates(self): def _init_regular_updates(self):
"""Schedule regular updates at the specified interval.""" """Schedule regular updates at the specified interval."""
track_time_interval(self._hass, lambda now: self._update(), track_time_interval(
self._scan_interval) self._hass, lambda now: self._update(), self._scan_interval)
def _update(self): def _update(self):
"""Update the feed and then update connected entities.""" """Update the feed and then update connected entities."""
import geojson_client import geojson_client
status, feed_entries = self._feed.update() status, feed_entries = self._feed.update()
if status == geojson_client.UPDATE_OK: if status == geojson_client.UPDATE_OK:
_LOGGER.debug("Data retrieved %s", feed_entries) _LOGGER.debug("Data retrieved %s", feed_entries)
# Keep a copy of all feed entries for future lookups by entities. # Keep a copy of all feed entries for future lookups by entities.
self._feed_entries = feed_entries.copy() self.feed_entries = {entry.external_id: entry
keep_entries = self._update_or_remove_entities(feed_entries) for entry in feed_entries}
self._generate_new_entities(keep_entries) # For entity management the external ids from the feed are used.
feed_external_ids = set(self.feed_entries)
remove_external_ids = self._managed_external_ids.difference(
feed_external_ids)
self._remove_entities(remove_external_ids)
update_external_ids = self._managed_external_ids.intersection(
feed_external_ids)
self._update_entities(update_external_ids)
create_external_ids = feed_external_ids.difference(
self._managed_external_ids)
self._generate_new_entities(create_external_ids)
elif status == geojson_client.UPDATE_OK_NO_DATA: elif status == geojson_client.UPDATE_OK_NO_DATA:
_LOGGER.debug("Update successful, but no data received from %s", _LOGGER.debug(
self._feed) "Update successful, but no data received from %s", self._feed)
else: else:
_LOGGER.warning("Update not successful, no data received from %s", _LOGGER.warning(
self._feed) "Update not successful, no data received from %s", self._feed)
# Remove all entities. # Remove all entities.
self._update_or_remove_entities([]) self._remove_entities(self._managed_external_ids.copy())
def _update_or_remove_entities(self, feed_entries): def _generate_new_entities(self, external_ids):
"""Update existing entries and remove obsolete entities."""
_LOGGER.debug("Entries for updating: %s", feed_entries)
remove_entry = None
# Remove obsolete entities for events that have disappeared
managed_entities = self._managed_entities.copy()
for entity in managed_entities:
# Remove entry from previous iteration - if applicable.
if remove_entry:
feed_entries.remove(remove_entry)
remove_entry = None
for entry in feed_entries:
if entity.external_id == entry.external_id:
# Existing entity - update details.
_LOGGER.debug("Existing entity found %s", entity)
remove_entry = entry
entity.schedule_update_ha_state(True)
break
else:
# Remove obsolete entity.
_LOGGER.debug("Entity not current anymore %s", entity)
self._managed_entities.remove(entity)
self._hass.add_job(entity.async_remove())
# Remove entry from very last iteration - if applicable.
if remove_entry:
feed_entries.remove(remove_entry)
# Return the remaining entries that new entities must be created for.
return feed_entries
def _generate_new_entities(self, entries):
"""Generate new entities for events.""" """Generate new entities for events."""
new_entities = [] new_entities = []
for entry in entries: for external_id in external_ids:
new_entity = GeoJsonLocationEvent(self, entry) new_entity = GeoJsonLocationEvent(self, external_id)
_LOGGER.debug("New entity added %s", new_entity) _LOGGER.debug("New entity added %s", external_id)
new_entities.append(new_entity) new_entities.append(new_entity)
# Add new entities to HA and keep track of them in this manager. self._managed_external_ids.add(external_id)
# Add new entities to HA.
self._add_entities(new_entities, True) self._add_entities(new_entities, True)
self._managed_entities.extend(new_entities)
def get_feed_entry(self, external_id): def _update_entities(self, external_ids):
"""Return a feed entry identified by external id.""" """Update entities."""
return next((entry for entry in self._feed_entries for external_id in external_ids:
if entry.external_id == external_id), None) _LOGGER.debug("Existing entity found %s", external_id)
dispatcher_send(
self._hass, SIGNAL_UPDATE_ENTITY.format(external_id))
def _remove_entities(self, external_ids):
"""Remove entities."""
for external_id in external_ids:
_LOGGER.debug("Entity not current anymore %s", external_id)
self._managed_external_ids.remove(external_id)
dispatcher_send(
self._hass, SIGNAL_DELETE_ENTITY.format(external_id))
class GeoJsonLocationEvent(GeoLocationEvent): class GeoJsonLocationEvent(GeoLocationEvent):
"""This represents an external event with GeoJSON data.""" """This represents an external event with GeoJSON data."""
def __init__(self, feed_manager, feed_entry): def __init__(self, feed_manager, external_id):
"""Initialize entity with data from feed entry.""" """Initialize entity with data from feed entry."""
self._feed_manager = feed_manager self._feed_manager = feed_manager
self._update_from_feed(feed_entry) self._external_id = external_id
self._name = None
self._distance = None
self._latitude = None
self._longitude = None
self._remove_signal_delete = None
self._remove_signal_update = None
async def async_added_to_hass(self):
"""Call when entity is added to hass."""
self._remove_signal_delete = async_dispatcher_connect(
self.hass, SIGNAL_DELETE_ENTITY.format(self._external_id),
self._delete_callback)
self._remove_signal_update = async_dispatcher_connect(
self.hass, SIGNAL_UPDATE_ENTITY.format(self._external_id),
self._update_callback)
@callback
def _delete_callback(self):
"""Remove this entity."""
self._remove_signal_delete()
self._remove_signal_update()
self.hass.async_create_task(self.async_remove())
@callback
def _update_callback(self):
"""Call update method."""
self.async_schedule_update_ha_state(True)
@property @property
def should_poll(self): def should_poll(self):
@ -150,7 +183,8 @@ class GeoJsonLocationEvent(GeoLocationEvent):
async def async_update(self): async def async_update(self):
"""Update this entity from the data held in the feed manager.""" """Update this entity from the data held in the feed manager."""
feed_entry = self._feed_manager.get_feed_entry(self.external_id) _LOGGER.debug("Updating %s", self._external_id)
feed_entry = self._feed_manager.feed_entries.get(self._external_id)
if feed_entry: if feed_entry:
self._update_from_feed(feed_entry) self._update_from_feed(feed_entry)
@ -160,7 +194,11 @@ class GeoJsonLocationEvent(GeoLocationEvent):
self._distance = feed_entry.distance_to_home self._distance = feed_entry.distance_to_home
self._latitude = feed_entry.coordinates[0] self._latitude = feed_entry.coordinates[0]
self._longitude = feed_entry.coordinates[1] self._longitude = feed_entry.coordinates[1]
self.external_id = feed_entry.external_id
@property
def source(self) -> str:
"""Return source value of this external event."""
return SOURCE
@property @property
def name(self) -> Optional[str]: def name(self) -> Optional[str]:
@ -191,6 +229,6 @@ class GeoJsonLocationEvent(GeoLocationEvent):
def device_state_attributes(self): def device_state_attributes(self):
"""Return the device state attributes.""" """Return the device state attributes."""
attributes = {} attributes = {}
if self.external_id: if self._external_id:
attributes[ATTR_EXTERNAL_ID] = self.external_id attributes[ATTR_EXTERNAL_ID] = self._external_id
return attributes return attributes

View File

@ -0,0 +1,288 @@
"""
NSW Rural Fire Service Feed platform.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/geo_location/nsw_rural_fire_service_feed/
"""
from datetime import timedelta
import logging
from typing import Optional
import voluptuous as vol
from homeassistant.components.geo_location import (
PLATFORM_SCHEMA, GeoLocationEvent)
from homeassistant.const import (
ATTR_ATTRIBUTION, ATTR_LOCATION, CONF_RADIUS, CONF_SCAN_INTERVAL,
EVENT_HOMEASSISTANT_START)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, dispatcher_send)
from homeassistant.helpers.event import track_time_interval
REQUIREMENTS = ['geojson_client==0.1']
_LOGGER = logging.getLogger(__name__)
ATTR_CATEGORY = 'category'
ATTR_COUNCIL_AREA = 'council_area'
ATTR_EXTERNAL_ID = 'external_id'
ATTR_FIRE = 'fire'
ATTR_PUBLICATION_DATE = 'publication_date'
ATTR_RESPONSIBLE_AGENCY = 'responsible_agency'
ATTR_SIZE = 'size'
ATTR_STATUS = 'status'
ATTR_TYPE = 'type'
CONF_CATEGORIES = 'categories'
DEFAULT_RADIUS_IN_KM = 20.0
DEFAULT_UNIT_OF_MEASUREMENT = 'km'
SCAN_INTERVAL = timedelta(minutes=5)
SIGNAL_DELETE_ENTITY = 'nsw_rural_fire_service_feed_delete_{}'
SIGNAL_UPDATE_ENTITY = 'nsw_rural_fire_service_feed_update_{}'
SOURCE = 'nsw_rural_fire_service_feed'
VALID_CATEGORIES = [
'Advice',
'Emergency Warning',
'Not Applicable',
'Watch and Act',
]
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_CATEGORIES, default=[]):
vol.All(cv.ensure_list, [vol.In(VALID_CATEGORIES)]),
vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS_IN_KM): vol.Coerce(float),
})
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the GeoJSON Events platform."""
scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
radius_in_km = config[CONF_RADIUS]
categories = config.get(CONF_CATEGORIES)
# Initialize the entity manager.
feed = NswRuralFireServiceFeedManager(
hass, add_entities, scan_interval, radius_in_km, categories)
def start_feed_manager(event):
"""Start feed manager."""
feed.startup()
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_feed_manager)
class NswRuralFireServiceFeedManager:
"""Feed Manager for NSW Rural Fire Service GeoJSON feed."""
def __init__(self, hass, add_entities, scan_interval, radius_in_km,
categories):
"""Initialize the GeoJSON Feed Manager."""
from geojson_client.nsw_rural_fire_service_feed \
import NswRuralFireServiceFeed
self._hass = hass
self._feed = NswRuralFireServiceFeed(
(hass.config.latitude, hass.config.longitude),
filter_radius=radius_in_km, filter_categories=categories)
self._add_entities = add_entities
self._scan_interval = scan_interval
self.feed_entries = {}
self._managed_external_ids = set()
def startup(self):
"""Start up this manager."""
self._update()
self._init_regular_updates()
def _init_regular_updates(self):
"""Schedule regular updates at the specified interval."""
track_time_interval(
self._hass, lambda now: self._update(), self._scan_interval)
def _update(self):
"""Update the feed and then update connected entities."""
import geojson_client
status, feed_entries = self._feed.update()
if status == geojson_client.UPDATE_OK:
_LOGGER.debug("Data retrieved %s", feed_entries)
# Keep a copy of all feed entries for future lookups by entities.
self.feed_entries = {entry.external_id: entry
for entry in feed_entries}
# For entity management the external ids from the feed are used.
feed_external_ids = set(self.feed_entries)
remove_external_ids = self._managed_external_ids.difference(
feed_external_ids)
self._remove_entities(remove_external_ids)
update_external_ids = self._managed_external_ids.intersection(
feed_external_ids)
self._update_entities(update_external_ids)
create_external_ids = feed_external_ids.difference(
self._managed_external_ids)
self._generate_new_entities(create_external_ids)
elif status == geojson_client.UPDATE_OK_NO_DATA:
_LOGGER.debug(
"Update successful, but no data received from %s", self._feed)
else:
_LOGGER.warning(
"Update not successful, no data received from %s", self._feed)
# Remove all entities.
self._remove_entities(self._managed_external_ids.copy())
def _generate_new_entities(self, external_ids):
"""Generate new entities for events."""
new_entities = []
for external_id in external_ids:
new_entity = NswRuralFireServiceLocationEvent(self, external_id)
_LOGGER.debug("New entity added %s", external_id)
new_entities.append(new_entity)
self._managed_external_ids.add(external_id)
# Add new entities to HA.
self._add_entities(new_entities, True)
def _update_entities(self, external_ids):
"""Update entities."""
for external_id in external_ids:
_LOGGER.debug("Existing entity found %s", external_id)
dispatcher_send(
self._hass, SIGNAL_UPDATE_ENTITY.format(external_id))
def _remove_entities(self, external_ids):
"""Remove entities."""
for external_id in external_ids:
_LOGGER.debug("Entity not current anymore %s", external_id)
self._managed_external_ids.remove(external_id)
dispatcher_send(
self._hass, SIGNAL_DELETE_ENTITY.format(external_id))
class NswRuralFireServiceLocationEvent(GeoLocationEvent):
"""This represents an external event with GeoJSON data."""
def __init__(self, feed_manager, external_id):
"""Initialize entity with data from feed entry."""
self._feed_manager = feed_manager
self._external_id = external_id
self._name = None
self._distance = None
self._latitude = None
self._longitude = None
self._attribution = None
self._category = None
self._publication_date = None
self._location = None
self._council_area = None
self._status = None
self._type = None
self._fire = None
self._size = None
self._responsible_agency = None
self._remove_signal_delete = None
self._remove_signal_update = None
async def async_added_to_hass(self):
"""Call when entity is added to hass."""
self._remove_signal_delete = async_dispatcher_connect(
self.hass, SIGNAL_DELETE_ENTITY.format(self._external_id),
self._delete_callback)
self._remove_signal_update = async_dispatcher_connect(
self.hass, SIGNAL_UPDATE_ENTITY.format(self._external_id),
self._update_callback)
@callback
def _delete_callback(self):
"""Remove this entity."""
self._remove_signal_delete()
self._remove_signal_update()
self.hass.async_create_task(self.async_remove())
@callback
def _update_callback(self):
"""Call update method."""
self.async_schedule_update_ha_state(True)
@property
def should_poll(self):
"""No polling needed for GeoJSON location events."""
return False
async def async_update(self):
"""Update this entity from the data held in the feed manager."""
_LOGGER.debug("Updating %s", self._external_id)
feed_entry = self._feed_manager.feed_entries.get(self._external_id)
if feed_entry:
self._update_from_feed(feed_entry)
def _update_from_feed(self, feed_entry):
"""Update the internal state from the provided feed entry."""
self._name = feed_entry.title
self._distance = feed_entry.distance_to_home
self._latitude = feed_entry.coordinates[0]
self._longitude = feed_entry.coordinates[1]
self._attribution = feed_entry.attribution
self._category = feed_entry.category
self._publication_date = feed_entry.publication_date
self._location = feed_entry.location
self._council_area = feed_entry.council_area
self._status = feed_entry.status
self._type = feed_entry.type
self._fire = feed_entry.fire
self._size = feed_entry.size
self._responsible_agency = feed_entry.responsible_agency
@property
def source(self) -> str:
"""Return source value of this external event."""
return SOURCE
@property
def name(self) -> Optional[str]:
"""Return the name of the entity."""
return self._name
@property
def distance(self) -> Optional[float]:
"""Return distance value of this external event."""
return self._distance
@property
def latitude(self) -> Optional[float]:
"""Return latitude value of this external event."""
return self._latitude
@property
def longitude(self) -> Optional[float]:
"""Return longitude value of this external event."""
return self._longitude
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return DEFAULT_UNIT_OF_MEASUREMENT
@property
def device_state_attributes(self):
"""Return the device state attributes."""
attributes = {}
for key, value in (
(ATTR_EXTERNAL_ID, self._external_id),
(ATTR_CATEGORY, self._category),
(ATTR_LOCATION, self._location),
(ATTR_ATTRIBUTION, self._attribution),
(ATTR_PUBLICATION_DATE, self._publication_date),
(ATTR_COUNCIL_AREA, self._council_area),
(ATTR_STATUS, self._status),
(ATTR_TYPE, self._type),
(ATTR_FIRE, self._fire),
(ATTR_SIZE, self._size),
(ATTR_RESPONSIBLE_AGENCY, self._responsible_agency),
):
if value or isinstance(value, bool):
attributes[key] = value
return attributes

View File

@ -12,7 +12,7 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
REQUIREMENTS = ['pysher==0.2.0'] REQUIREMENTS = ['pysher==1.0.4']
DOMAIN = 'goalfeed' DOMAIN = 'goalfeed'

View File

@ -144,8 +144,7 @@ class GraphiteFeeder(threading.Thread):
try: try:
self._report_attributes( self._report_attributes(
event.data['entity_id'], event.data['new_state']) event.data['entity_id'], event.data['new_state'])
# pylint: disable=broad-except except Exception: # pylint: disable=broad-except
except Exception:
# Catch this so we can avoid the thread dying and # Catch this so we can avoid the thread dying and
# make it visible. # make it visible.
_LOGGER.exception("Failed to process STATE_CHANGED event") _LOGGER.exception("Failed to process STATE_CHANGED event")

View File

@ -30,6 +30,7 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}'
CONF_ENTITIES = 'entities' CONF_ENTITIES = 'entities'
CONF_VIEW = 'view' CONF_VIEW = 'view'
CONF_CONTROL = 'control' CONF_CONTROL = 'control'
CONF_ALL = 'all'
ATTR_ADD_ENTITIES = 'add_entities' ATTR_ADD_ENTITIES = 'add_entities'
ATTR_AUTO = 'auto' ATTR_AUTO = 'auto'
@ -39,6 +40,7 @@ ATTR_OBJECT_ID = 'object_id'
ATTR_ORDER = 'order' ATTR_ORDER = 'order'
ATTR_VIEW = 'view' ATTR_VIEW = 'view'
ATTR_VISIBLE = 'visible' ATTR_VISIBLE = 'visible'
ATTR_ALL = 'all'
SERVICE_SET_VISIBILITY = 'set_visibility' SERVICE_SET_VISIBILITY = 'set_visibility'
SERVICE_SET = 'set' SERVICE_SET = 'set'
@ -60,6 +62,7 @@ SET_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ICON): cv.string, vol.Optional(ATTR_ICON): cv.string,
vol.Optional(ATTR_CONTROL): CONTROL_TYPES, vol.Optional(ATTR_CONTROL): CONTROL_TYPES,
vol.Optional(ATTR_VISIBLE): cv.boolean, vol.Optional(ATTR_VISIBLE): cv.boolean,
vol.Optional(ATTR_ALL): cv.boolean,
vol.Exclusive(ATTR_ENTITIES, 'entities'): cv.entity_ids, vol.Exclusive(ATTR_ENTITIES, 'entities'): cv.entity_ids,
vol.Exclusive(ATTR_ADD_ENTITIES, 'entities'): cv.entity_ids, vol.Exclusive(ATTR_ADD_ENTITIES, 'entities'): cv.entity_ids,
}) })
@ -85,6 +88,7 @@ GROUP_SCHEMA = vol.Schema({
CONF_NAME: cv.string, CONF_NAME: cv.string,
CONF_ICON: cv.icon, CONF_ICON: cv.icon,
CONF_CONTROL: CONTROL_TYPES, CONF_CONTROL: CONTROL_TYPES,
CONF_ALL: cv.boolean,
}) })
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
@ -223,6 +227,7 @@ async def async_setup(hass, config):
object_id=object_id, object_id=object_id,
entity_ids=entity_ids, entity_ids=entity_ids,
user_defined=False, user_defined=False,
mode=service.data.get(ATTR_ALL),
**extra_arg **extra_arg
) )
return return
@ -265,6 +270,10 @@ async def async_setup(hass, config):
group.view = service.data[ATTR_VIEW] group.view = service.data[ATTR_VIEW]
need_update = True need_update = True
if ATTR_ALL in service.data:
group.mode = all if service.data[ATTR_ALL] else any
need_update = True
if need_update: if need_update:
await group.async_update_ha_state() await group.async_update_ha_state()
@ -310,19 +319,21 @@ async def _async_process_config(hass, config, component):
icon = conf.get(CONF_ICON) icon = conf.get(CONF_ICON)
view = conf.get(CONF_VIEW) view = conf.get(CONF_VIEW)
control = conf.get(CONF_CONTROL) control = conf.get(CONF_CONTROL)
mode = conf.get(CONF_ALL)
# Don't create tasks and await them all. The order is important as # Don't create tasks and await them all. The order is important as
# groups get a number based on creation order. # groups get a number based on creation order.
await Group.async_create_group( await Group.async_create_group(
hass, name, entity_ids, icon=icon, view=view, hass, name, entity_ids, icon=icon, view=view,
control=control, object_id=object_id) control=control, object_id=object_id, mode=mode)
class Group(Entity): class Group(Entity):
"""Track a group of entity ids.""" """Track a group of entity ids."""
def __init__(self, hass, name, order=None, visible=True, icon=None, def __init__(self, hass, name, order=None, visible=True, icon=None,
view=False, control=None, user_defined=True, entity_ids=None): view=False, control=None, user_defined=True, entity_ids=None,
mode=None):
"""Initialize a group. """Initialize a group.
This Object has factory function for creation. This Object has factory function for creation.
@ -341,6 +352,9 @@ class Group(Entity):
self.visible = visible self.visible = visible
self.control = control self.control = control
self.user_defined = user_defined self.user_defined = user_defined
self.mode = any
if mode:
self.mode = all
self._order = order self._order = order
self._assumed_state = False self._assumed_state = False
self._async_unsub_state_changed = None self._async_unsub_state_changed = None
@ -348,18 +362,19 @@ class Group(Entity):
@staticmethod @staticmethod
def create_group(hass, name, entity_ids=None, user_defined=True, def create_group(hass, name, entity_ids=None, user_defined=True,
visible=True, icon=None, view=False, control=None, visible=True, icon=None, view=False, control=None,
object_id=None): object_id=None, mode=None):
"""Initialize a group.""" """Initialize a group."""
return run_coroutine_threadsafe( return run_coroutine_threadsafe(
Group.async_create_group( Group.async_create_group(
hass, name, entity_ids, user_defined, visible, icon, view, hass, name, entity_ids, user_defined, visible, icon, view,
control, object_id), control, object_id, mode),
hass.loop).result() hass.loop).result()
@staticmethod @staticmethod
async def async_create_group(hass, name, entity_ids=None, async def async_create_group(hass, name, entity_ids=None,
user_defined=True, visible=True, icon=None, user_defined=True, visible=True, icon=None,
view=False, control=None, object_id=None): view=False, control=None, object_id=None,
mode=None):
"""Initialize a group. """Initialize a group.
This method must be run in the event loop. This method must be run in the event loop.
@ -368,7 +383,7 @@ class Group(Entity):
hass, name, hass, name,
order=len(hass.states.async_entity_ids(DOMAIN)), order=len(hass.states.async_entity_ids(DOMAIN)),
visible=visible, icon=icon, view=view, control=control, visible=visible, icon=icon, view=view, control=control,
user_defined=user_defined, entity_ids=entity_ids user_defined=user_defined, entity_ids=entity_ids, mode=mode
) )
group.entity_id = async_generate_entity_id( group.entity_id = async_generate_entity_id(
@ -557,13 +572,16 @@ class Group(Entity):
if gr_on is None: if gr_on is None:
return return
# pylint: disable=too-many-boolean-expressions
if tr_state is None or ((gr_state == gr_on and if tr_state is None or ((gr_state == gr_on and
tr_state.state == gr_off) or tr_state.state == gr_off) or
(gr_state == gr_off and
tr_state.state == gr_on) or
tr_state.state not in (gr_on, gr_off)): tr_state.state not in (gr_on, gr_off)):
if states is None: if states is None:
states = self._tracking_states states = self._tracking_states
if any(state.state == gr_on for state in states): if self.mode(state.state == gr_on for state in states):
self._state = gr_on self._state = gr_on
else: else:
self._state = gr_off self._state = gr_off
@ -576,7 +594,7 @@ class Group(Entity):
if states is None: if states is None:
states = self._tracking_states states = self._tracking_states
self._assumed_state = any( self._assumed_state = self.mode(
state.attributes.get(ATTR_ASSUMED_STATE) for state state.attributes.get(ATTR_ASSUMED_STATE) for state
in states) in states)

View File

@ -40,6 +40,9 @@ set:
add_entities: add_entities:
description: List of members they will change on group listening. description: List of members they will change on group listening.
example: domain.entity_id1, domain.entity_id2 example: domain.entity_id1, domain.entity_id2
all:
description: Enable this option if the group should only turn on when all entities are on.
example: True
remove: remove:
description: Remove a user group. description: Remove a user group.

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