Merge branch 'dev' into nuheat

This commit is contained in:
Derek Brooks 2017-12-06 21:49:13 -06:00
commit 72aa722c33
303 changed files with 12425 additions and 2953 deletions

View File

@ -11,6 +11,9 @@ omit =
homeassistant/components/abode.py homeassistant/components/abode.py
homeassistant/components/*/abode.py homeassistant/components/*/abode.py
homeassistant/components/ads/__init__.py
homeassistant/components/*/ads.py
homeassistant/components/alarmdecoder.py homeassistant/components/alarmdecoder.py
homeassistant/components/*/alarmdecoder.py homeassistant/components/*/alarmdecoder.py
@ -53,6 +56,8 @@ omit =
homeassistant/components/digital_ocean.py homeassistant/components/digital_ocean.py
homeassistant/components/*/digital_ocean.py homeassistant/components/*/digital_ocean.py
homeassistant/components/dominos.py
homeassistant/components/doorbird.py homeassistant/components/doorbird.py
homeassistant/components/*/doorbird.py homeassistant/components/*/doorbird.py
@ -80,6 +85,9 @@ omit =
homeassistant/components/hdmi_cec.py homeassistant/components/hdmi_cec.py
homeassistant/components/*/hdmi_cec.py homeassistant/components/*/hdmi_cec.py
homeassistant/components/hive.py
homeassistant/components/*/hive.py
homeassistant/components/homematic.py homeassistant/components/homematic.py
homeassistant/components/*/homematic.py homeassistant/components/*/homematic.py
@ -182,6 +190,9 @@ omit =
homeassistant/components/tado.py homeassistant/components/tado.py
homeassistant/components/*/tado.py homeassistant/components/*/tado.py
homeassistant/components/tahoma.py
homeassistant/components/*/tahoma.py
homeassistant/components/tellduslive.py homeassistant/components/tellduslive.py
homeassistant/components/*/tellduslive.py homeassistant/components/*/tellduslive.py
@ -255,6 +266,7 @@ omit =
homeassistant/components/alarm_control_panel/alarmdotcom.py homeassistant/components/alarm_control_panel/alarmdotcom.py
homeassistant/components/alarm_control_panel/concord232.py homeassistant/components/alarm_control_panel/concord232.py
homeassistant/components/alarm_control_panel/egardia.py homeassistant/components/alarm_control_panel/egardia.py
homeassistant/components/alarm_control_panel/ialarm.py
homeassistant/components/alarm_control_panel/manual_mqtt.py homeassistant/components/alarm_control_panel/manual_mqtt.py
homeassistant/components/alarm_control_panel/nx584.py homeassistant/components/alarm_control_panel/nx584.py
homeassistant/components/alarm_control_panel/simplisafe.py homeassistant/components/alarm_control_panel/simplisafe.py
@ -276,9 +288,9 @@ omit =
homeassistant/components/camera/ffmpeg.py homeassistant/components/camera/ffmpeg.py
homeassistant/components/camera/foscam.py homeassistant/components/camera/foscam.py
homeassistant/components/camera/mjpeg.py homeassistant/components/camera/mjpeg.py
homeassistant/components/camera/rpi_camera.py
homeassistant/components/camera/onvif.py homeassistant/components/camera/onvif.py
homeassistant/components/camera/ring.py homeassistant/components/camera/ring.py
homeassistant/components/camera/rpi_camera.py
homeassistant/components/camera/synology.py homeassistant/components/camera/synology.py
homeassistant/components/camera/yi.py homeassistant/components/camera/yi.py
homeassistant/components/climate/ephember.py homeassistant/components/climate/ephember.py
@ -286,6 +298,7 @@ omit =
homeassistant/components/climate/flexit.py homeassistant/components/climate/flexit.py
homeassistant/components/climate/heatmiser.py homeassistant/components/climate/heatmiser.py
homeassistant/components/climate/homematic.py homeassistant/components/climate/homematic.py
homeassistant/components/climate/honeywell.py
homeassistant/components/climate/knx.py homeassistant/components/climate/knx.py
homeassistant/components/climate/oem.py homeassistant/components/climate/oem.py
homeassistant/components/climate/proliphix.py homeassistant/components/climate/proliphix.py
@ -309,6 +322,7 @@ omit =
homeassistant/components/device_tracker/cisco_ios.py homeassistant/components/device_tracker/cisco_ios.py
homeassistant/components/device_tracker/fritz.py homeassistant/components/device_tracker/fritz.py
homeassistant/components/device_tracker/gpslogger.py homeassistant/components/device_tracker/gpslogger.py
homeassistant/components/device_tracker/hitron_coda.py
homeassistant/components/device_tracker/huawei_router.py homeassistant/components/device_tracker/huawei_router.py
homeassistant/components/device_tracker/icloud.py homeassistant/components/device_tracker/icloud.py
homeassistant/components/device_tracker/keenetic_ndms2.py homeassistant/components/device_tracker/keenetic_ndms2.py
@ -322,9 +336,10 @@ omit =
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
homeassistant/components/device_tracker/swisscom.py homeassistant/components/device_tracker/swisscom.py
homeassistant/components/device_tracker/thomson.py
homeassistant/components/device_tracker/tomato.py
homeassistant/components/device_tracker/tado.py homeassistant/components/device_tracker/tado.py
homeassistant/components/device_tracker/thomson.py
homeassistant/components/device_tracker/tile.py
homeassistant/components/device_tracker/tomato.py
homeassistant/components/device_tracker/tplink.py homeassistant/components/device_tracker/tplink.py
homeassistant/components/device_tracker/trackr.py homeassistant/components/device_tracker/trackr.py
homeassistant/components/device_tracker/ubus.py homeassistant/components/device_tracker/ubus.py
@ -342,8 +357,8 @@ omit =
homeassistant/components/keyboard.py homeassistant/components/keyboard.py
homeassistant/components/keyboard_remote.py homeassistant/components/keyboard_remote.py
homeassistant/components/light/avion.py homeassistant/components/light/avion.py
homeassistant/components/light/blinkt.py
homeassistant/components/light/blinksticklight.py homeassistant/components/light/blinksticklight.py
homeassistant/components/light/blinkt.py
homeassistant/components/light/decora.py homeassistant/components/light/decora.py
homeassistant/components/light/decora_wifi.py homeassistant/components/light/decora_wifi.py
homeassistant/components/light/flux_led.py homeassistant/components/light/flux_led.py
@ -354,8 +369,8 @@ omit =
homeassistant/components/light/limitlessled.py homeassistant/components/light/limitlessled.py
homeassistant/components/light/mystrom.py homeassistant/components/light/mystrom.py
homeassistant/components/light/osramlightify.py homeassistant/components/light/osramlightify.py
homeassistant/components/light/rpi_gpio_pwm.py
homeassistant/components/light/piglow.py homeassistant/components/light/piglow.py
homeassistant/components/light/rpi_gpio_pwm.py
homeassistant/components/light/sensehat.py homeassistant/components/light/sensehat.py
homeassistant/components/light/tikteck.py homeassistant/components/light/tikteck.py
homeassistant/components/light/tplink.py homeassistant/components/light/tplink.py
@ -366,9 +381,9 @@ omit =
homeassistant/components/light/yeelightsunflower.py homeassistant/components/light/yeelightsunflower.py
homeassistant/components/light/zengge.py homeassistant/components/light/zengge.py
homeassistant/components/lirc.py homeassistant/components/lirc.py
homeassistant/components/lock/lockitron.py
homeassistant/components/lock/nello.py homeassistant/components/lock/nello.py
homeassistant/components/lock/nuki.py homeassistant/components/lock/nuki.py
homeassistant/components/lock/lockitron.py
homeassistant/components/lock/sesame.py homeassistant/components/lock/sesame.py
homeassistant/components/media_extractor.py homeassistant/components/media_extractor.py
homeassistant/components/media_player/anthemav.py homeassistant/components/media_player/anthemav.py
@ -415,6 +430,7 @@ omit =
homeassistant/components/media_player/volumio.py homeassistant/components/media_player/volumio.py
homeassistant/components/media_player/yamaha.py homeassistant/components/media_player/yamaha.py
homeassistant/components/media_player/yamaha_musiccast.py homeassistant/components/media_player/yamaha_musiccast.py
homeassistant/components/media_player/ziggo_mediabox_xl.py
homeassistant/components/mycroft.py homeassistant/components/mycroft.py
homeassistant/components/notify/aws_lambda.py homeassistant/components/notify/aws_lambda.py
homeassistant/components/notify/aws_sns.py homeassistant/components/notify/aws_sns.py
@ -424,7 +440,6 @@ omit =
homeassistant/components/notify/clicksend.py homeassistant/components/notify/clicksend.py
homeassistant/components/notify/clicksend_tts.py homeassistant/components/notify/clicksend_tts.py
homeassistant/components/notify/discord.py homeassistant/components/notify/discord.py
homeassistant/components/notify/facebook.py
homeassistant/components/notify/free_mobile.py homeassistant/components/notify/free_mobile.py
homeassistant/components/notify/gntp.py homeassistant/components/notify/gntp.py
homeassistant/components/notify/group.py homeassistant/components/notify/group.py
@ -463,6 +478,7 @@ omit =
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
homeassistant/components/sensor/alpha_vantage.py
homeassistant/components/sensor/arest.py homeassistant/components/sensor/arest.py
homeassistant/components/sensor/arwn.py homeassistant/components/sensor/arwn.py
homeassistant/components/sensor/bbox.py homeassistant/components/sensor/bbox.py
@ -473,8 +489,8 @@ omit =
homeassistant/components/sensor/bom.py homeassistant/components/sensor/bom.py
homeassistant/components/sensor/broadlink.py homeassistant/components/sensor/broadlink.py
homeassistant/components/sensor/buienradar.py homeassistant/components/sensor/buienradar.py
homeassistant/components/sensor/citybikes.py
homeassistant/components/sensor/cert_expiry.py homeassistant/components/sensor/cert_expiry.py
homeassistant/components/sensor/citybikes.py
homeassistant/components/sensor/comed_hourly_pricing.py homeassistant/components/sensor/comed_hourly_pricing.py
homeassistant/components/sensor/cpuspeed.py homeassistant/components/sensor/cpuspeed.py
homeassistant/components/sensor/crimereports.py homeassistant/components/sensor/crimereports.py
@ -502,6 +518,7 @@ omit =
homeassistant/components/sensor/fixer.py homeassistant/components/sensor/fixer.py
homeassistant/components/sensor/fritzbox_callmonitor.py homeassistant/components/sensor/fritzbox_callmonitor.py
homeassistant/components/sensor/fritzbox_netmonitor.py homeassistant/components/sensor/fritzbox_netmonitor.py
homeassistant/components/sensor/gearbest.py
homeassistant/components/sensor/geizhals.py homeassistant/components/sensor/geizhals.py
homeassistant/components/sensor/gitter.py homeassistant/components/sensor/gitter.py
homeassistant/components/sensor/glances.py homeassistant/components/sensor/glances.py
@ -581,12 +598,12 @@ omit =
homeassistant/components/sensor/upnp.py homeassistant/components/sensor/upnp.py
homeassistant/components/sensor/ups.py homeassistant/components/sensor/ups.py
homeassistant/components/sensor/vasttrafik.py homeassistant/components/sensor/vasttrafik.py
homeassistant/components/sensor/viaggiatreno.py
homeassistant/components/sensor/waqi.py homeassistant/components/sensor/waqi.py
homeassistant/components/sensor/whois.py homeassistant/components/sensor/whois.py
homeassistant/components/sensor/worldtidesinfo.py homeassistant/components/sensor/worldtidesinfo.py
homeassistant/components/sensor/worxlandroid.py homeassistant/components/sensor/worxlandroid.py
homeassistant/components/sensor/xbox_live.py homeassistant/components/sensor/xbox_live.py
homeassistant/components/sensor/yweather.py
homeassistant/components/sensor/zamg.py homeassistant/components/sensor/zamg.py
homeassistant/components/shiftr.py homeassistant/components/shiftr.py
homeassistant/components/spc.py homeassistant/components/spc.py
@ -612,16 +629,19 @@ omit =
homeassistant/components/switch/rest.py homeassistant/components/switch/rest.py
homeassistant/components/switch/rpi_rf.py homeassistant/components/switch/rpi_rf.py
homeassistant/components/switch/snmp.py homeassistant/components/switch/snmp.py
homeassistant/components/switch/tplink.py
homeassistant/components/switch/telnet.py homeassistant/components/switch/telnet.py
homeassistant/components/switch/tplink.py
homeassistant/components/switch/transmission.py homeassistant/components/switch/transmission.py
homeassistant/components/switch/xiaomi_miio.py homeassistant/components/switch/xiaomi_miio.py
homeassistant/components/telegram_bot/* homeassistant/components/telegram_bot/*
homeassistant/components/thingspeak.py homeassistant/components/thingspeak.py
homeassistant/components/tts/amazon_polly.py homeassistant/components/tts/amazon_polly.py
homeassistant/components/tts/baidu.py
homeassistant/components/tts/microsoft.py homeassistant/components/tts/microsoft.py
homeassistant/components/tts/picotts.py homeassistant/components/tts/picotts.py
homeassistant/components/vacuum/mqtt.py
homeassistant/components/vacuum/roomba.py homeassistant/components/vacuum/roomba.py
homeassistant/components/vacuum/xiaomi_miio.py
homeassistant/components/weather/bom.py homeassistant/components/weather/bom.py
homeassistant/components/weather/buienradar.py homeassistant/components/weather/buienradar.py
homeassistant/components/weather/metoffice.py homeassistant/components/weather/metoffice.py
@ -630,8 +650,6 @@ omit =
homeassistant/components/weather/zamg.py homeassistant/components/weather/zamg.py
homeassistant/components/zeroconf.py homeassistant/components/zeroconf.py
homeassistant/components/zwave/util.py homeassistant/components/zwave/util.py
homeassistant/components/vacuum/mqtt.py
[report] [report]
# Regexes for lines to exclude from consideration # Regexes for lines to exclude from consideration

3
.gitattributes vendored Normal file
View File

@ -0,0 +1,3 @@
# Ensure Docker script files uses LF to support Docker for Windows.
setup_docker_prereqs eol=lf
/virtualization/Docker/scripts/* eol=lf

2
.gitignore vendored
View File

@ -96,4 +96,4 @@ docs/build
desktop.ini desktop.ini
/home-assistant.pyproj /home-assistant.pyproj
/home-assistant.sln /home-assistant.sln
/.vs/home-assistant/v14 /.vs/*

View File

@ -8,18 +8,18 @@ matrix:
include: include:
- python: "3.4.2" - python: "3.4.2"
env: TOXENV=lint env: TOXENV=lint
- python: "3.4.2"
env: TOXENV=pylint
- python: "3.4.2" - python: "3.4.2"
env: TOXENV=py34 env: TOXENV=py34
# - python: "3.5" # - python: "3.5"
# env: TOXENV=typing # env: TOXENV=typing
- python: "3.5" - python: "3.5.3"
env: TOXENV=py35 env: TOXENV=py35
- python: "3.6" - python: "3.6"
env: TOXENV=py36 env: TOXENV=py36
# - python: "3.6-dev" # - python: "3.6-dev"
# env: TOXENV=py36 # env: TOXENV=py36
- python: "3.4.2"
env: TOXENV=requirements
# allow_failures: # allow_failures:
# - python: "3.5" # - python: "3.5"
# env: TOXENV=typing # env: TOXENV=typing
@ -29,5 +29,5 @@ cache:
- $HOME/.cache/pip - $HOME/.cache/pip
install: pip install -U tox coveralls install: pip install -U tox coveralls
language: python language: python
script: travis_wait tox script: travis_wait 30 tox --develop
after_success: coveralls after_success: coveralls

View File

@ -46,6 +46,7 @@ homeassistant/components/climate/eq3btsmart.py @rytilahti
homeassistant/components/climate/sensibo.py @andrey-git homeassistant/components/climate/sensibo.py @andrey-git
homeassistant/components/cover/template.py @PhracturedBlue homeassistant/components/cover/template.py @PhracturedBlue
homeassistant/components/device_tracker/automatic.py @armills homeassistant/components/device_tracker/automatic.py @armills
homeassistant/components/device_tracker/tile.py @bachya
homeassistant/components/history_graph.py @andrey-git homeassistant/components/history_graph.py @andrey-git
homeassistant/components/light/tplink.py @rytilahti homeassistant/components/light/tplink.py @rytilahti
homeassistant/components/light/yeelight.py @rytilahti homeassistant/components/light/yeelight.py @rytilahti
@ -53,6 +54,7 @@ homeassistant/components/media_player/kodi.py @armills
homeassistant/components/media_player/monoprice.py @etsinko homeassistant/components/media_player/monoprice.py @etsinko
homeassistant/components/media_player/yamaha_musiccast.py @jalmeroth homeassistant/components/media_player/yamaha_musiccast.py @jalmeroth
homeassistant/components/sensor/airvisual.py @bachya homeassistant/components/sensor/airvisual.py @bachya
homeassistant/components/sensor/gearbest.py @HerrHofrat
homeassistant/components/sensor/irish_rail_transport.py @ttroy50 homeassistant/components/sensor/irish_rail_transport.py @ttroy50
homeassistant/components/sensor/miflora.py @danielhiversen homeassistant/components/sensor/miflora.py @danielhiversen
homeassistant/components/sensor/sytadin.py @gautric homeassistant/components/sensor/sytadin.py @gautric
@ -63,13 +65,19 @@ homeassistant/components/switch/tplink.py @rytilahti
homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
homeassistant/components/*/broadlink.py @danielhiversen homeassistant/components/*/broadlink.py @danielhiversen
homeassistant/components/hive.py @Rendili @KJonline
homeassistant/components/*/hive.py @Rendili @KJonline
homeassistant/components/*/rfxtrx.py @danielhiversen homeassistant/components/*/rfxtrx.py @danielhiversen
homeassistant/components/velux.py @Julius2342 homeassistant/components/velux.py @Julius2342
homeassistant/components/*/velux.py @Julius2342 homeassistant/components/*/velux.py @Julius2342
homeassistant/components/knx.py @Julius2342 homeassistant/components/knx.py @Julius2342
homeassistant/components/*/knx.py @Julius2342 homeassistant/components/*/knx.py @Julius2342
homeassistant/components/tahoma.py @philklei
homeassistant/components/*/tahoma.py @philklei
homeassistant/components/tesla.py @zabuldon homeassistant/components/tesla.py @zabuldon
homeassistant/components/*/tesla.py @zabuldon homeassistant/components/*/tesla.py @zabuldon
homeassistant/components/tellduslive.py @molobrakos @fredrike
homeassistant/components/*/tellduslive.py @molobrakos @fredrike
homeassistant/components/*/tradfri.py @ggravlingen homeassistant/components/*/tradfri.py @ggravlingen
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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 232 KiB

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -30,8 +30,8 @@ ERROR_LOG_FILENAME = 'home-assistant.log'
DATA_LOGGING = 'logging' DATA_LOGGING = 'logging'
FIRST_INIT_COMPONENT = set(( FIRST_INIT_COMPONENT = set((
'recorder', 'mqtt', 'mqtt_eventstream', 'logger', 'introduction', 'system_log', 'recorder', 'mqtt', 'mqtt_eventstream', 'logger',
'frontend', 'history')) 'introduction', 'frontend', 'history'))
def from_config_dict(config: Dict[str, Any], def from_config_dict(config: Dict[str, Any],

View File

@ -0,0 +1,217 @@
"""
ADS Component.
For more details about this component, please refer to the documentation.
https://home-assistant.io/components/ads/
"""
import os
import threading
import struct
import logging
import ctypes
from collections import namedtuple
import voluptuous as vol
from homeassistant.const import CONF_DEVICE, CONF_PORT, CONF_IP_ADDRESS, \
EVENT_HOMEASSISTANT_STOP
from homeassistant.config import load_yaml_config_file
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyads==2.2.6']
_LOGGER = logging.getLogger(__name__)
DATA_ADS = 'data_ads'
# Supported Types
ADSTYPE_INT = 'int'
ADSTYPE_UINT = 'uint'
ADSTYPE_BYTE = 'byte'
ADSTYPE_BOOL = 'bool'
DOMAIN = 'ads'
# config variable names
CONF_ADS_VAR = 'adsvar'
CONF_ADS_VAR_BRIGHTNESS = 'adsvar_brightness'
CONF_ADS_TYPE = 'adstype'
CONF_ADS_FACTOR = 'factor'
CONF_ADS_VALUE = 'value'
SERVICE_WRITE_DATA_BY_NAME = 'write_data_by_name'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_DEVICE): cv.string,
vol.Required(CONF_PORT): cv.port,
vol.Optional(CONF_IP_ADDRESS): cv.string,
})
}, extra=vol.ALLOW_EXTRA)
SCHEMA_SERVICE_WRITE_DATA_BY_NAME = vol.Schema({
vol.Required(CONF_ADS_VAR): cv.string,
vol.Required(CONF_ADS_TYPE): vol.In([ADSTYPE_INT, ADSTYPE_UINT,
ADSTYPE_BYTE]),
vol.Required(CONF_ADS_VALUE): cv.match_all
})
def setup(hass, config):
"""Set up the ADS component."""
import pyads
conf = config[DOMAIN]
# get ads connection parameters from config
net_id = conf.get(CONF_DEVICE)
ip_address = conf.get(CONF_IP_ADDRESS)
port = conf.get(CONF_PORT)
# create a new ads connection
client = pyads.Connection(net_id, port, ip_address)
# add some constants to AdsHub
AdsHub.ADS_TYPEMAP = {
ADSTYPE_BOOL: pyads.PLCTYPE_BOOL,
ADSTYPE_BYTE: pyads.PLCTYPE_BYTE,
ADSTYPE_INT: pyads.PLCTYPE_INT,
ADSTYPE_UINT: pyads.PLCTYPE_UINT,
}
AdsHub.PLCTYPE_BOOL = pyads.PLCTYPE_BOOL
AdsHub.PLCTYPE_BYTE = pyads.PLCTYPE_BYTE
AdsHub.PLCTYPE_INT = pyads.PLCTYPE_INT
AdsHub.PLCTYPE_UINT = pyads.PLCTYPE_UINT
AdsHub.ADSError = pyads.ADSError
# connect to ads client and try to connect
try:
ads = AdsHub(client)
except pyads.pyads.ADSError:
_LOGGER.error(
'Could not connect to ADS host (netid=%s, port=%s)', net_id, port
)
return False
# add ads hub to hass data collection, listen to shutdown
hass.data[DATA_ADS] = ads
hass.bus.listen(EVENT_HOMEASSISTANT_STOP, ads.shutdown)
def handle_write_data_by_name(call):
"""Write a value to the connected ADS device."""
ads_var = call.data.get(CONF_ADS_VAR)
ads_type = call.data.get(CONF_ADS_TYPE)
value = call.data.get(CONF_ADS_VALUE)
try:
ads.write_by_name(ads_var, value, ads.ADS_TYPEMAP[ads_type])
except pyads.ADSError as err:
_LOGGER.error(err)
# load descriptions from services.yaml
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
hass.services.register(
DOMAIN, SERVICE_WRITE_DATA_BY_NAME, handle_write_data_by_name,
descriptions[SERVICE_WRITE_DATA_BY_NAME],
schema=SCHEMA_SERVICE_WRITE_DATA_BY_NAME
)
return True
# tuple to hold data needed for notification
NotificationItem = namedtuple(
'NotificationItem', 'hnotify huser name plc_datatype callback'
)
class AdsHub:
"""Representation of a PyADS connection."""
def __init__(self, ads_client):
"""Initialize the ADS Hub."""
self._client = ads_client
self._client.open()
# all ADS devices are registered here
self._devices = []
self._notification_items = {}
self._lock = threading.Lock()
def shutdown(self, *args, **kwargs):
"""Shutdown ADS connection."""
_LOGGER.debug('Shutting down ADS')
for notification_item in self._notification_items.values():
self._client.del_device_notification(
notification_item.hnotify,
notification_item.huser
)
_LOGGER.debug(
'Deleting device notification %d, %d',
notification_item.hnotify, notification_item.huser
)
self._client.close()
def register_device(self, device):
"""Register a new device."""
self._devices.append(device)
def write_by_name(self, name, value, plc_datatype):
"""Write a value to the device."""
with self._lock:
return self._client.write_by_name(name, value, plc_datatype)
def read_by_name(self, name, plc_datatype):
"""Read a value from the device."""
with self._lock:
return self._client.read_by_name(name, plc_datatype)
def add_device_notification(self, name, plc_datatype, callback):
"""Add a notification to the ADS devices."""
from pyads import NotificationAttrib
attr = NotificationAttrib(ctypes.sizeof(plc_datatype))
with self._lock:
hnotify, huser = self._client.add_device_notification(
name, attr, self._device_notification_callback
)
hnotify = int(hnotify)
_LOGGER.debug(
'Added Device Notification %d for variable %s', hnotify, name
)
self._notification_items[hnotify] = NotificationItem(
hnotify, huser, name, plc_datatype, callback
)
def _device_notification_callback(self, addr, notification, huser):
"""Handle device notifications."""
contents = notification.contents
hnotify = int(contents.hNotification)
_LOGGER.debug('Received Notification %d', hnotify)
data = contents.data
try:
notification_item = self._notification_items[hnotify]
except KeyError:
_LOGGER.debug('Unknown Device Notification handle: %d', hnotify)
return
# parse data to desired datatype
if notification_item.plc_datatype == self.PLCTYPE_BOOL:
value = bool(struct.unpack('<?', bytearray(data)[:1])[0])
elif notification_item.plc_datatype == self.PLCTYPE_INT:
value = struct.unpack('<h', bytearray(data)[:2])[0]
elif notification_item.plc_datatype == self.PLCTYPE_BYTE:
value = struct.unpack('<B', bytearray(data)[:1])[0]
elif notification_item.plc_datatype == self.PLCTYPE_UINT:
value = struct.unpack('<H', bytearray(data)[:2])[0]
else:
value = bytearray(data)
_LOGGER.warning('No callback available for this datatype.')
# execute callback
notification_item.callback(notification_item.name, value)

View File

@ -0,0 +1,15 @@
# Describes the format for available ADS services
write_data_by_name:
description: Write a value to the connected ADS device.
fields:
adsvar:
description: The name of the variable to write to.
example: '.global_var'
adstype:
description: The data type of the variable to write to.
example: 'int'
value:
description: The value to write to the variable.
example: 1

View File

@ -14,7 +14,7 @@ import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER, ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER,
SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY,
SERVICE_ALARM_ARM_NIGHT) SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS)
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
@ -33,6 +33,7 @@ SERVICE_TO_METHOD = {
SERVICE_ALARM_ARM_HOME: 'alarm_arm_home', SERVICE_ALARM_ARM_HOME: 'alarm_arm_home',
SERVICE_ALARM_ARM_AWAY: 'alarm_arm_away', SERVICE_ALARM_ARM_AWAY: 'alarm_arm_away',
SERVICE_ALARM_ARM_NIGHT: 'alarm_arm_night', SERVICE_ALARM_ARM_NIGHT: 'alarm_arm_night',
SERVICE_ALARM_ARM_CUSTOM_BYPASS: 'alarm_arm_custom_bypass',
SERVICE_ALARM_TRIGGER: 'alarm_trigger' SERVICE_ALARM_TRIGGER: 'alarm_trigger'
} }
@ -107,6 +108,18 @@ def alarm_trigger(hass, code=None, entity_id=None):
hass.services.call(DOMAIN, SERVICE_ALARM_TRIGGER, data) hass.services.call(DOMAIN, SERVICE_ALARM_TRIGGER, data)
@bind_hass
def alarm_arm_custom_bypass(hass, code=None, entity_id=None):
"""Send the alarm the command for arm custom bypass."""
data = {}
if code:
data[ATTR_CODE] = code
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_ALARM_ARM_CUSTOM_BYPASS, data)
@asyncio.coroutine @asyncio.coroutine
def async_setup(hass, config): def async_setup(hass, config):
"""Track states and offer events for sensors.""" """Track states and offer events for sensors."""
@ -216,6 +229,17 @@ class AlarmControlPanel(Entity):
""" """
return self.hass.async_add_job(self.alarm_trigger, code) return self.hass.async_add_job(self.alarm_trigger, code)
def alarm_arm_custom_bypass(self, code=None):
"""Send arm custom bypass command."""
raise NotImplementedError()
def async_alarm_arm_custom_bypass(self, code=None):
"""Send arm custom bypass command.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.async_add_job(self.alarm_arm_custom_bypass, code)
@property @property
def state_attributes(self): def state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""

View File

@ -22,6 +22,7 @@ _LOGGER = logging.getLogger(__name__)
ARMED = 'armed' ARMED = 'armed'
CONF_HOME_MODE_NAME = 'home_mode_name' CONF_HOME_MODE_NAME = 'home_mode_name'
CONF_AWAY_MODE_NAME = 'away_mode_name'
DEPENDENCIES = ['arlo'] DEPENDENCIES = ['arlo']
@ -31,6 +32,7 @@ ICON = 'mdi:security'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOME_MODE_NAME, default=ARMED): cv.string, vol.Optional(CONF_HOME_MODE_NAME, default=ARMED): cv.string,
vol.Optional(CONF_AWAY_MODE_NAME, default=ARMED): cv.string,
}) })
@ -43,19 +45,22 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
return return
home_mode_name = config.get(CONF_HOME_MODE_NAME) home_mode_name = config.get(CONF_HOME_MODE_NAME)
away_mode_name = config.get(CONF_AWAY_MODE_NAME)
base_stations = [] base_stations = []
for base_station in data.base_stations: for base_station in data.base_stations:
base_stations.append(ArloBaseStation(base_station, home_mode_name)) base_stations.append(ArloBaseStation(base_station, home_mode_name,
away_mode_name))
async_add_devices(base_stations, True) async_add_devices(base_stations, True)
class ArloBaseStation(AlarmControlPanel): class ArloBaseStation(AlarmControlPanel):
"""Representation of an Arlo Alarm Control Panel.""" """Representation of an Arlo Alarm Control Panel."""
def __init__(self, data, home_mode_name): def __init__(self, data, home_mode_name, away_mode_name):
"""Initialize the alarm control panel.""" """Initialize the alarm control panel."""
self._base_station = data self._base_station = data
self._home_mode_name = home_mode_name self._home_mode_name = home_mode_name
self._away_mode_name = away_mode_name
self._state = None self._state = None
@property @property
@ -89,8 +94,8 @@ class ArloBaseStation(AlarmControlPanel):
@asyncio.coroutine @asyncio.coroutine
def async_alarm_arm_away(self, code=None): def async_alarm_arm_away(self, code=None):
"""Send arm away command.""" """Send arm away command. Uses custom mode."""
self._base_station.mode = ARMED self._base_station.mode = self._away_mode_name
@asyncio.coroutine @asyncio.coroutine
def async_alarm_arm_home(self, code=None): def async_alarm_arm_home(self, code=None):
@ -118,4 +123,6 @@ class ArloBaseStation(AlarmControlPanel):
return STATE_ALARM_DISARMED return STATE_ALARM_DISARMED
elif mode == self._home_mode_name: elif mode == self._home_mode_name:
return STATE_ALARM_ARMED_HOME return STATE_ALARM_ARMED_HOME
elif mode == self._away_mode_name:
return STATE_ALARM_ARMED_AWAY
return None return None

View File

@ -4,27 +4,45 @@ Demo platform that has two fake alarm control panels.
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 datetime
import homeassistant.components.alarm_control_panel.manual as manual import homeassistant.components.alarm_control_panel.manual as manual
from homeassistant.const import ( from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS,
STATE_ALARM_TRIGGERED, CONF_PENDING_TIME) STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, CONF_DELAY_TIME,
CONF_PENDING_TIME, CONF_TRIGGER_TIME)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Demo alarm control panel platform.""" """Set up the Demo alarm control panel platform."""
add_devices([ add_devices([
manual.ManualAlarm(hass, 'Alarm', '1234', 5, 10, False, { manual.ManualAlarm(hass, 'Alarm', '1234', None, False, {
STATE_ALARM_ARMED_AWAY: { STATE_ALARM_ARMED_AWAY: {
CONF_PENDING_TIME: 5 CONF_DELAY_TIME: datetime.timedelta(seconds=0),
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
}, },
STATE_ALARM_ARMED_HOME: { STATE_ALARM_ARMED_HOME: {
CONF_PENDING_TIME: 5 CONF_DELAY_TIME: datetime.timedelta(seconds=0),
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
}, },
STATE_ALARM_ARMED_NIGHT: { STATE_ALARM_ARMED_NIGHT: {
CONF_PENDING_TIME: 5 CONF_DELAY_TIME: datetime.timedelta(seconds=0),
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
},
STATE_ALARM_DISARMED: {
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
},
STATE_ALARM_ARMED_CUSTOM_BYPASS: {
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
}, },
STATE_ALARM_TRIGGERED: { STATE_ALARM_TRIGGERED: {
CONF_PENDING_TIME: 5 CONF_PENDING_TIME: datetime.timedelta(seconds=5),
}, },
}), }),
]) ])

View File

@ -0,0 +1,107 @@
"""
Interfaces with iAlarm control panels.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.ialarm/
"""
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, CONF_HOST, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, CONF_NAME)
REQUIREMENTS = ['pyialarm==0.2']
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'iAlarm'
def no_application_protocol(value):
"""Validate that value is without the application protocol."""
protocol_separator = "://"
if not value or protocol_separator in value:
raise vol.Invalid(
'Invalid host, {} is not allowed'.format(protocol_separator))
return value
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_HOST): vol.All(cv.string, no_application_protocol),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up an iAlarm control panel."""
name = config.get(CONF_NAME)
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
host = config.get(CONF_HOST)
url = 'http://{}'.format(host)
ialarm = IAlarmPanel(name, username, password, url)
add_devices([ialarm], True)
class IAlarmPanel(alarm.AlarmControlPanel):
"""Represent an iAlarm status."""
def __init__(self, name, username, password, url):
"""Initialize the iAlarm status."""
from pyialarm import IAlarm
self._name = name
self._username = username
self._password = password
self._url = url
self._state = None
self._client = IAlarm(username, password, url)
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def state(self):
"""Return the state of the device."""
return self._state
def update(self):
"""Return the state of the device."""
status = self._client.get_status()
_LOGGER.debug('iAlarm status: %s', status)
if status:
status = int(status)
if status == self._client.DISARMED:
state = STATE_ALARM_DISARMED
elif status == self._client.ARMED_AWAY:
state = STATE_ALARM_ARMED_AWAY
elif status == self._client.ARMED_STAY:
state = STATE_ALARM_ARMED_HOME
else:
state = None
self._state = state
def alarm_disarm(self, code=None):
"""Send disarm command."""
self._client.disarm()
def alarm_arm_away(self, code=None):
"""Send arm away command."""
self._client.arm_away()
def alarm_arm_home(self, code=None):
"""Send arm home command."""
self._client.arm_stay()

View File

@ -14,25 +14,42 @@ import homeassistant.components.alarm_control_panel as alarm
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.const import ( from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_DISARMED, STATE_ALARM_PENDING,
CONF_PLATFORM, CONF_NAME, CONF_CODE, CONF_PENDING_TIME, CONF_TRIGGER_TIME, STATE_ALARM_TRIGGERED, CONF_PLATFORM, CONF_NAME, CONF_CODE,
CONF_DELAY_TIME, CONF_PENDING_TIME, CONF_TRIGGER_TIME,
CONF_DISARM_AFTER_TRIGGER) CONF_DISARM_AFTER_TRIGGER)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import track_point_in_time from homeassistant.helpers.event import track_point_in_time
CONF_CODE_TEMPLATE = 'code_template'
DEFAULT_ALARM_NAME = 'HA Alarm' DEFAULT_ALARM_NAME = 'HA Alarm'
DEFAULT_PENDING_TIME = 60 DEFAULT_DELAY_TIME = datetime.timedelta(seconds=0)
DEFAULT_TRIGGER_TIME = 120 DEFAULT_PENDING_TIME = datetime.timedelta(seconds=60)
DEFAULT_TRIGGER_TIME = datetime.timedelta(seconds=120)
DEFAULT_DISARM_AFTER_TRIGGER = False DEFAULT_DISARM_AFTER_TRIGGER = False
SUPPORTED_PENDING_STATES = [STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, SUPPORTED_STATES = [STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED] STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_TRIGGERED]
SUPPORTED_PRETRIGGER_STATES = [state for state in SUPPORTED_STATES
if state != STATE_ALARM_TRIGGERED]
SUPPORTED_PENDING_STATES = [state for state in SUPPORTED_STATES
if state != STATE_ALARM_DISARMED]
ATTR_PRE_PENDING_STATE = 'pre_pending_state'
ATTR_POST_PENDING_STATE = 'post_pending_state' ATTR_POST_PENDING_STATE = 'post_pending_state'
def _state_validator(config): def _state_validator(config):
config = copy.deepcopy(config) config = copy.deepcopy(config)
for state in SUPPORTED_PRETRIGGER_STATES:
if CONF_DELAY_TIME not in config[state]:
config[state][CONF_DELAY_TIME] = config[CONF_DELAY_TIME]
if CONF_TRIGGER_TIME not in config[state]:
config[state][CONF_TRIGGER_TIME] = config[CONF_TRIGGER_TIME]
for state in SUPPORTED_PENDING_STATES: for state in SUPPORTED_PENDING_STATES:
if CONF_PENDING_TIME not in config[state]: if CONF_PENDING_TIME not in config[state]:
config[state][CONF_PENDING_TIME] = config[CONF_PENDING_TIME] config[state][CONF_PENDING_TIME] = config[CONF_PENDING_TIME]
@ -40,26 +57,44 @@ def _state_validator(config):
return config return config
STATE_SETTING_SCHEMA = vol.Schema({ def _state_schema(state):
vol.Optional(CONF_PENDING_TIME): schema = {}
vol.All(vol.Coerce(int), vol.Range(min=0)) if state in SUPPORTED_PRETRIGGER_STATES:
}) schema[vol.Optional(CONF_DELAY_TIME)] = vol.All(
cv.time_period, cv.positive_timedelta)
schema[vol.Optional(CONF_TRIGGER_TIME)] = vol.All(
cv.time_period, cv.positive_timedelta)
if state in SUPPORTED_PENDING_STATES:
schema[vol.Optional(CONF_PENDING_TIME)] = vol.All(
cv.time_period, cv.positive_timedelta)
return vol.Schema(schema)
PLATFORM_SCHEMA = vol.Schema(vol.All({ PLATFORM_SCHEMA = vol.Schema(vol.All({
vol.Required(CONF_PLATFORM): 'manual', vol.Required(CONF_PLATFORM): 'manual',
vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
vol.Optional(CONF_CODE): cv.string, vol.Exclusive(CONF_CODE, 'code validation'): cv.string,
vol.Exclusive(CONF_CODE_TEMPLATE, 'code validation'): cv.template,
vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME):
vol.All(cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME): vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME):
vol.All(vol.Coerce(int), vol.Range(min=0)), vol.All(cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME): vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME):
vol.All(vol.Coerce(int), vol.Range(min=1)), vol.All(cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_DISARM_AFTER_TRIGGER, vol.Optional(CONF_DISARM_AFTER_TRIGGER,
default=DEFAULT_DISARM_AFTER_TRIGGER): cv.boolean, default=DEFAULT_DISARM_AFTER_TRIGGER): cv.boolean,
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): STATE_SETTING_SCHEMA, vol.Optional(STATE_ALARM_ARMED_AWAY, default={}):
vol.Optional(STATE_ALARM_ARMED_HOME, default={}): STATE_SETTING_SCHEMA, _state_schema(STATE_ALARM_ARMED_AWAY),
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}): STATE_SETTING_SCHEMA, vol.Optional(STATE_ALARM_ARMED_HOME, default={}):
vol.Optional(STATE_ALARM_TRIGGERED, default={}): STATE_SETTING_SCHEMA, _state_schema(STATE_ALARM_ARMED_HOME),
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}):
_state_schema(STATE_ALARM_ARMED_NIGHT),
vol.Optional(STATE_ALARM_ARMED_CUSTOM_BYPASS, default={}):
_state_schema(STATE_ALARM_ARMED_CUSTOM_BYPASS),
vol.Optional(STATE_ALARM_DISARMED, default={}):
_state_schema(STATE_ALARM_DISARMED),
vol.Optional(STATE_ALARM_TRIGGERED, default={}):
_state_schema(STATE_ALARM_TRIGGERED),
}, _state_validator)) }, _state_validator))
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -71,8 +106,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
hass, hass,
config[CONF_NAME], config[CONF_NAME],
config.get(CONF_CODE), config.get(CONF_CODE),
config.get(CONF_PENDING_TIME, DEFAULT_PENDING_TIME), config.get(CONF_CODE_TEMPLATE),
config.get(CONF_TRIGGER_TIME, DEFAULT_TRIGGER_TIME),
config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER), config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER),
config config
)]) )])
@ -83,27 +117,37 @@ class ManualAlarm(alarm.AlarmControlPanel):
Representation of an alarm status. Representation of an alarm status.
When armed, will be pending for 'pending_time', after that armed. When armed, will be pending for 'pending_time', after that armed.
When triggered, will be pending for 'trigger_time'. After that will be When triggered, will be pending for the triggering state's 'delay_time'
triggered for 'trigger_time', after that we return to the previous state plus the triggered state's 'pending_time'.
or disarm if `disarm_after_trigger` is true. After that will be triggered for 'trigger_time', after that we return to
the previous state or disarm if `disarm_after_trigger` is true.
A trigger_time of zero disables the alarm_trigger service.
""" """
def __init__(self, hass, name, code, pending_time, trigger_time, def __init__(self, hass, name, code, code_template,
disarm_after_trigger, config): disarm_after_trigger, config):
"""Init the manual alarm panel.""" """Init the manual alarm panel."""
self._state = STATE_ALARM_DISARMED self._state = STATE_ALARM_DISARMED
self._hass = hass self._hass = hass
self._name = name self._name = name
self._code = str(code) if code else None if code_template:
self._trigger_time = datetime.timedelta(seconds=trigger_time) self._code = code_template
self._code.hass = hass
else:
self._code = code or None
self._disarm_after_trigger = disarm_after_trigger self._disarm_after_trigger = disarm_after_trigger
self._pre_trigger_state = self._state self._previous_state = self._state
self._state_ts = None self._state_ts = None
self._pending_time_by_state = {} self._delay_time_by_state = {
for state in SUPPORTED_PENDING_STATES: state: config[state][CONF_DELAY_TIME]
self._pending_time_by_state[state] = datetime.timedelta( for state in SUPPORTED_PRETRIGGER_STATES}
seconds=config[state][CONF_PENDING_TIME]) self._trigger_time_by_state = {
state: config[state][CONF_TRIGGER_TIME]
for state in SUPPORTED_PRETRIGGER_STATES}
self._pending_time_by_state = {
state: config[state][CONF_PENDING_TIME]
for state in SUPPORTED_PENDING_STATES}
@property @property
def should_poll(self): def should_poll(self):
@ -118,15 +162,16 @@ class ManualAlarm(alarm.AlarmControlPanel):
@property @property
def state(self): def state(self):
"""Return the state of the device.""" """Return the state of the device."""
if self._state == STATE_ALARM_TRIGGERED and self._trigger_time: if self._state == STATE_ALARM_TRIGGERED:
if self._within_pending_time(self._state): if self._within_pending_time(self._state):
return STATE_ALARM_PENDING return STATE_ALARM_PENDING
elif (self._state_ts + self._pending_time_by_state[self._state] + trigger_time = self._trigger_time_by_state[self._previous_state]
self._trigger_time) < dt_util.utcnow(): if (self._state_ts + self._pending_time(self._state) +
trigger_time) < dt_util.utcnow():
if self._disarm_after_trigger: if self._disarm_after_trigger:
return STATE_ALARM_DISARMED return STATE_ALARM_DISARMED
else: else:
self._state = self._pre_trigger_state self._state = self._previous_state
return self._state return self._state
if self._state in SUPPORTED_PENDING_STATES and \ if self._state in SUPPORTED_PENDING_STATES and \
@ -135,9 +180,21 @@ class ManualAlarm(alarm.AlarmControlPanel):
return self._state return self._state
def _within_pending_time(self, state): @property
def _active_state(self):
if self.state == STATE_ALARM_PENDING:
return self._previous_state
else:
return self._state
def _pending_time(self, state):
pending_time = self._pending_time_by_state[state] pending_time = self._pending_time_by_state[state]
return self._state_ts + pending_time > dt_util.utcnow() if state == STATE_ALARM_TRIGGERED:
pending_time += self._delay_time_by_state[self._previous_state]
return pending_time
def _within_pending_time(self, state):
return self._state_ts + self._pending_time(state) > dt_util.utcnow()
@property @property
def code_format(self): def code_format(self):
@ -174,27 +231,43 @@ class ManualAlarm(alarm.AlarmControlPanel):
self._update_state(STATE_ALARM_ARMED_NIGHT) self._update_state(STATE_ALARM_ARMED_NIGHT)
def alarm_trigger(self, code=None): def alarm_arm_custom_bypass(self, code=None):
"""Send alarm trigger command. No code needed.""" """Send arm custom bypass command."""
self._pre_trigger_state = self._state if not self._validate_code(code, STATE_ALARM_ARMED_CUSTOM_BYPASS):
return
self._update_state(STATE_ALARM_ARMED_CUSTOM_BYPASS)
def alarm_trigger(self, code=None):
"""
Send alarm trigger command.
No code needed, a trigger time of zero for the current state
disables the alarm.
"""
if not self._trigger_time_by_state[self._active_state]:
return
self._update_state(STATE_ALARM_TRIGGERED) self._update_state(STATE_ALARM_TRIGGERED)
def _update_state(self, state): def _update_state(self, state):
if self._state == state:
return
self._previous_state = self._state
self._state = state self._state = state
self._state_ts = dt_util.utcnow() self._state_ts = dt_util.utcnow()
self.schedule_update_ha_state() self.schedule_update_ha_state()
pending_time = self._pending_time_by_state[state] pending_time = self._pending_time(state)
if state == STATE_ALARM_TRIGGERED:
if state == STATE_ALARM_TRIGGERED and self._trigger_time:
track_point_in_time( track_point_in_time(
self._hass, self.async_update_ha_state, self._hass, self.async_update_ha_state,
self._state_ts + pending_time) self._state_ts + pending_time)
trigger_time = self._trigger_time_by_state[self._previous_state]
track_point_in_time( track_point_in_time(
self._hass, self.async_update_ha_state, self._hass, self.async_update_ha_state,
self._state_ts + self._trigger_time + pending_time) self._state_ts + pending_time + trigger_time)
elif state in SUPPORTED_PENDING_STATES and pending_time: elif state in SUPPORTED_PENDING_STATES and pending_time:
track_point_in_time( track_point_in_time(
self._hass, self.async_update_ha_state, self._hass, self.async_update_ha_state,
@ -202,7 +275,14 @@ class ManualAlarm(alarm.AlarmControlPanel):
def _validate_code(self, code, state): def _validate_code(self, code, state):
"""Validate given code.""" """Validate given code."""
check = self._code is None or code == self._code if self._code is None:
return True
if isinstance(self._code, str):
alarm_code = self._code
else:
alarm_code = self._code.render(from_state=self._state,
to_state=state)
check = not alarm_code or code == alarm_code
if not check: if not check:
_LOGGER.warning("Invalid code given for %s", state) _LOGGER.warning("Invalid code given for %s", state)
return check return check
@ -213,6 +293,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
state_attr = {} state_attr = {}
if self.state == STATE_ALARM_PENDING: if self.state == STATE_ALARM_PENDING:
state_attr[ATTR_PRE_PENDING_STATE] = self._previous_state
state_attr[ATTR_POST_PENDING_STATE] = self._state state_attr[ATTR_POST_PENDING_STATE] = self._state
return state_attr return state_attr

View File

@ -16,8 +16,8 @@ import homeassistant.util.dt as dt_util
from homeassistant.const import ( from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED,
CONF_PLATFORM, CONF_NAME, CONF_CODE, CONF_PENDING_TIME, CONF_TRIGGER_TIME, CONF_PLATFORM, CONF_NAME, CONF_CODE, CONF_DELAY_TIME, CONF_PENDING_TIME,
CONF_DISARM_AFTER_TRIGGER) CONF_TRIGGER_TIME, CONF_DISARM_AFTER_TRIGGER)
import homeassistant.components.mqtt as mqtt import homeassistant.components.mqtt as mqtt
from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.event import async_track_state_change
@ -26,28 +26,44 @@ from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import track_point_in_time from homeassistant.helpers.event import track_point_in_time
CONF_CODE_TEMPLATE = 'code_template'
CONF_PAYLOAD_DISARM = 'payload_disarm' CONF_PAYLOAD_DISARM = 'payload_disarm'
CONF_PAYLOAD_ARM_HOME = 'payload_arm_home' CONF_PAYLOAD_ARM_HOME = 'payload_arm_home'
CONF_PAYLOAD_ARM_AWAY = 'payload_arm_away' CONF_PAYLOAD_ARM_AWAY = 'payload_arm_away'
CONF_PAYLOAD_ARM_NIGHT = 'payload_arm_night' CONF_PAYLOAD_ARM_NIGHT = 'payload_arm_night'
DEFAULT_ALARM_NAME = 'HA Alarm' DEFAULT_ALARM_NAME = 'HA Alarm'
DEFAULT_PENDING_TIME = 60 DEFAULT_DELAY_TIME = datetime.timedelta(seconds=0)
DEFAULT_TRIGGER_TIME = 120 DEFAULT_PENDING_TIME = datetime.timedelta(seconds=60)
DEFAULT_TRIGGER_TIME = datetime.timedelta(seconds=120)
DEFAULT_DISARM_AFTER_TRIGGER = False DEFAULT_DISARM_AFTER_TRIGGER = False
DEFAULT_ARM_AWAY = 'ARM_AWAY' DEFAULT_ARM_AWAY = 'ARM_AWAY'
DEFAULT_ARM_HOME = 'ARM_HOME' DEFAULT_ARM_HOME = 'ARM_HOME'
DEFAULT_ARM_NIGHT = 'ARM_NIGHT' DEFAULT_ARM_NIGHT = 'ARM_NIGHT'
DEFAULT_DISARM = 'DISARM' DEFAULT_DISARM = 'DISARM'
SUPPORTED_PENDING_STATES = [STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, SUPPORTED_STATES = [STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED] STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_TRIGGERED]
SUPPORTED_PRETRIGGER_STATES = [state for state in SUPPORTED_STATES
if state != STATE_ALARM_TRIGGERED]
SUPPORTED_PENDING_STATES = [state for state in SUPPORTED_STATES
if state != STATE_ALARM_DISARMED]
ATTR_PRE_PENDING_STATE = 'pre_pending_state'
ATTR_POST_PENDING_STATE = 'post_pending_state' ATTR_POST_PENDING_STATE = 'post_pending_state'
def _state_validator(config): def _state_validator(config):
config = copy.deepcopy(config) config = copy.deepcopy(config)
for state in SUPPORTED_PRETRIGGER_STATES:
if CONF_DELAY_TIME not in config[state]:
config[state][CONF_DELAY_TIME] = config[CONF_DELAY_TIME]
if CONF_TRIGGER_TIME not in config[state]:
config[state][CONF_TRIGGER_TIME] = config[CONF_TRIGGER_TIME]
for state in SUPPORTED_PENDING_STATES: for state in SUPPORTED_PENDING_STATES:
if CONF_PENDING_TIME not in config[state]: if CONF_PENDING_TIME not in config[state]:
config[state][CONF_PENDING_TIME] = config[CONF_PENDING_TIME] config[state][CONF_PENDING_TIME] = config[CONF_PENDING_TIME]
@ -55,27 +71,44 @@ def _state_validator(config):
return config return config
STATE_SETTING_SCHEMA = vol.Schema({ def _state_schema(state):
vol.Optional(CONF_PENDING_TIME): schema = {}
vol.All(vol.Coerce(int), vol.Range(min=0)) if state in SUPPORTED_PRETRIGGER_STATES:
}) schema[vol.Optional(CONF_DELAY_TIME)] = vol.All(
cv.time_period, cv.positive_timedelta)
schema[vol.Optional(CONF_TRIGGER_TIME)] = vol.All(
cv.time_period, cv.positive_timedelta)
if state in SUPPORTED_PENDING_STATES:
schema[vol.Optional(CONF_PENDING_TIME)] = vol.All(
cv.time_period, cv.positive_timedelta)
return vol.Schema(schema)
DEPENDENCIES = ['mqtt'] DEPENDENCIES = ['mqtt']
PLATFORM_SCHEMA = vol.Schema(vol.All(mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = vol.Schema(vol.All(mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
vol.Required(CONF_PLATFORM): 'manual_mqtt', vol.Required(CONF_PLATFORM): 'manual_mqtt',
vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
vol.Optional(CONF_CODE): cv.string, vol.Exclusive(CONF_CODE, 'code validation'): cv.string,
vol.Exclusive(CONF_CODE_TEMPLATE, 'code validation'): cv.template,
vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME):
vol.All(cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME): vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME):
vol.All(vol.Coerce(int), vol.Range(min=0)), vol.All(cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME): vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME):
vol.All(vol.Coerce(int), vol.Range(min=1)), vol.All(cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_DISARM_AFTER_TRIGGER, vol.Optional(CONF_DISARM_AFTER_TRIGGER,
default=DEFAULT_DISARM_AFTER_TRIGGER): cv.boolean, default=DEFAULT_DISARM_AFTER_TRIGGER): cv.boolean,
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): STATE_SETTING_SCHEMA, vol.Optional(STATE_ALARM_ARMED_AWAY, default={}):
vol.Optional(STATE_ALARM_ARMED_HOME, default={}): STATE_SETTING_SCHEMA, _state_schema(STATE_ALARM_ARMED_AWAY),
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}): STATE_SETTING_SCHEMA, vol.Optional(STATE_ALARM_ARMED_HOME, default={}):
vol.Optional(STATE_ALARM_TRIGGERED, default={}): STATE_SETTING_SCHEMA, _state_schema(STATE_ALARM_ARMED_HOME),
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}):
_state_schema(STATE_ALARM_ARMED_NIGHT),
vol.Optional(STATE_ALARM_DISARMED, default={}):
_state_schema(STATE_ALARM_DISARMED),
vol.Optional(STATE_ALARM_TRIGGERED, default={}):
_state_schema(STATE_ALARM_TRIGGERED),
vol.Required(mqtt.CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Required(mqtt.CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Required(mqtt.CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Required(mqtt.CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string, vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string,
@ -93,8 +126,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
hass, hass,
config[CONF_NAME], config[CONF_NAME],
config.get(CONF_CODE), config.get(CONF_CODE),
config.get(CONF_PENDING_TIME, DEFAULT_PENDING_TIME), config.get(CONF_CODE_TEMPLATE),
config.get(CONF_TRIGGER_TIME, DEFAULT_TRIGGER_TIME),
config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER), config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER),
config.get(mqtt.CONF_STATE_TOPIC), config.get(mqtt.CONF_STATE_TOPIC),
config.get(mqtt.CONF_COMMAND_TOPIC), config.get(mqtt.CONF_COMMAND_TOPIC),
@ -111,13 +143,15 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
Representation of an alarm status. Representation of an alarm status.
When armed, will be pending for 'pending_time', after that armed. When armed, will be pending for 'pending_time', after that armed.
When triggered, will be pending for 'trigger_time'. After that will be When triggered, will be pending for the triggering state's 'delay_time'
triggered for 'trigger_time', after that we return to the previous state plus the triggered state's 'pending_time'.
or disarm if `disarm_after_trigger` is true. After that will be triggered for 'trigger_time', after that we return to
the previous state or disarm if `disarm_after_trigger` is true.
A trigger_time of zero disables the alarm_trigger service.
""" """
def __init__(self, hass, name, code, pending_time, def __init__(self, hass, name, code, code_template,
trigger_time, disarm_after_trigger, disarm_after_trigger,
state_topic, command_topic, qos, state_topic, command_topic, qos,
payload_disarm, payload_arm_home, payload_arm_away, payload_disarm, payload_arm_home, payload_arm_away,
payload_arm_night, config): payload_arm_night, config):
@ -125,17 +159,24 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
self._state = STATE_ALARM_DISARMED self._state = STATE_ALARM_DISARMED
self._hass = hass self._hass = hass
self._name = name self._name = name
self._code = str(code) if code else None if code_template:
self._pending_time = datetime.timedelta(seconds=pending_time) self._code = code_template
self._trigger_time = datetime.timedelta(seconds=trigger_time) self._code.hass = hass
else:
self._code = code or None
self._disarm_after_trigger = disarm_after_trigger self._disarm_after_trigger = disarm_after_trigger
self._pre_trigger_state = self._state self._previous_state = self._state
self._state_ts = None self._state_ts = None
self._pending_time_by_state = {} self._delay_time_by_state = {
for state in SUPPORTED_PENDING_STATES: state: config[state][CONF_DELAY_TIME]
self._pending_time_by_state[state] = datetime.timedelta( for state in SUPPORTED_PRETRIGGER_STATES}
seconds=config[state][CONF_PENDING_TIME]) self._trigger_time_by_state = {
state: config[state][CONF_TRIGGER_TIME]
for state in SUPPORTED_PRETRIGGER_STATES}
self._pending_time_by_state = {
state: config[state][CONF_PENDING_TIME]
for state in SUPPORTED_PENDING_STATES}
self._state_topic = state_topic self._state_topic = state_topic
self._command_topic = command_topic self._command_topic = command_topic
@ -158,15 +199,16 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
@property @property
def state(self): def state(self):
"""Return the state of the device.""" """Return the state of the device."""
if self._state == STATE_ALARM_TRIGGERED and self._trigger_time: if self._state == STATE_ALARM_TRIGGERED:
if self._within_pending_time(self._state): if self._within_pending_time(self._state):
return STATE_ALARM_PENDING return STATE_ALARM_PENDING
elif (self._state_ts + self._pending_time_by_state[self._state] + trigger_time = self._trigger_time_by_state[self._previous_state]
self._trigger_time) < dt_util.utcnow(): if (self._state_ts + self._pending_time(self._state) +
trigger_time) < dt_util.utcnow():
if self._disarm_after_trigger: if self._disarm_after_trigger:
return STATE_ALARM_DISARMED return STATE_ALARM_DISARMED
else: else:
self._state = self._pre_trigger_state self._state = self._previous_state
return self._state return self._state
if self._state in SUPPORTED_PENDING_STATES and \ if self._state in SUPPORTED_PENDING_STATES and \
@ -175,9 +217,21 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
return self._state return self._state
def _within_pending_time(self, state): @property
def _active_state(self):
if self.state == STATE_ALARM_PENDING:
return self._previous_state
else:
return self._state
def _pending_time(self, state):
pending_time = self._pending_time_by_state[state] pending_time = self._pending_time_by_state[state]
return self._state_ts + pending_time > dt_util.utcnow() if state == STATE_ALARM_TRIGGERED:
pending_time += self._delay_time_by_state[self._previous_state]
return pending_time
def _within_pending_time(self, state):
return self._state_ts + self._pending_time(state) > dt_util.utcnow()
@property @property
def code_format(self): def code_format(self):
@ -215,26 +269,35 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
self._update_state(STATE_ALARM_ARMED_NIGHT) self._update_state(STATE_ALARM_ARMED_NIGHT)
def alarm_trigger(self, code=None): def alarm_trigger(self, code=None):
"""Send alarm trigger command. No code needed.""" """
self._pre_trigger_state = self._state Send alarm trigger command.
No code needed, a trigger time of zero for the current state
disables the alarm.
"""
if not self._trigger_time_by_state[self._active_state]:
return
self._update_state(STATE_ALARM_TRIGGERED) self._update_state(STATE_ALARM_TRIGGERED)
def _update_state(self, state): def _update_state(self, state):
if self._state == state:
return
self._previous_state = self._state
self._state = state self._state = state
self._state_ts = dt_util.utcnow() self._state_ts = dt_util.utcnow()
self.schedule_update_ha_state() self.schedule_update_ha_state()
pending_time = self._pending_time_by_state[state] pending_time = self._pending_time(state)
if state == STATE_ALARM_TRIGGERED:
if state == STATE_ALARM_TRIGGERED and self._trigger_time:
track_point_in_time( track_point_in_time(
self._hass, self.async_update_ha_state, self._hass, self.async_update_ha_state,
self._state_ts + pending_time) self._state_ts + pending_time)
trigger_time = self._trigger_time_by_state[self._previous_state]
track_point_in_time( track_point_in_time(
self._hass, self.async_update_ha_state, self._hass, self.async_update_ha_state,
self._state_ts + self._trigger_time + pending_time) self._state_ts + pending_time + trigger_time)
elif state in SUPPORTED_PENDING_STATES and pending_time: elif state in SUPPORTED_PENDING_STATES and pending_time:
track_point_in_time( track_point_in_time(
self._hass, self.async_update_ha_state, self._hass, self.async_update_ha_state,
@ -242,7 +305,14 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
def _validate_code(self, code, state): def _validate_code(self, code, state):
"""Validate given code.""" """Validate given code."""
check = self._code is None or code == self._code if self._code is None:
return True
if isinstance(self._code, str):
alarm_code = self._code
else:
alarm_code = self._code.render(from_state=self._state,
to_state=state)
check = not alarm_code or code == alarm_code
if not check: if not check:
_LOGGER.warning("Invalid code given for %s", state) _LOGGER.warning("Invalid code given for %s", state)
return check return check
@ -253,6 +323,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
state_attr = {} state_attr = {}
if self.state == STATE_ALARM_PENDING: if self.state == STATE_ALARM_PENDING:
state_attr[ATTR_PRE_PENDING_STATE] = self._previous_state
state_attr[ATTR_POST_PENDING_STATE] = self._state state_attr[ATTR_POST_PENDING_STATE] = self._state
return state_attr return state_attr

View File

@ -34,10 +34,8 @@ def async_setup_platform(hass, config, async_add_devices,
discovery_info[ATTR_DISCOVER_AREAS] is None): discovery_info[ATTR_DISCOVER_AREAS] is None):
return return
devices = [SpcAlarm(hass=hass, api = hass.data[DATA_API]
area_id=area['id'], devices = [SpcAlarm(api, area)
name=area['name'],
state=_get_alarm_state(area['mode']))
for area in discovery_info[ATTR_DISCOVER_AREAS]] for area in discovery_info[ATTR_DISCOVER_AREAS]]
async_add_devices(devices) async_add_devices(devices)
@ -46,21 +44,29 @@ def async_setup_platform(hass, config, async_add_devices,
class SpcAlarm(alarm.AlarmControlPanel): class SpcAlarm(alarm.AlarmControlPanel):
"""Represents the SPC alarm panel.""" """Represents the SPC alarm panel."""
def __init__(self, hass, area_id, name, state): def __init__(self, api, area):
"""Initialize the SPC alarm panel.""" """Initialize the SPC alarm panel."""
self._hass = hass self._area_id = area['id']
self._area_id = area_id self._name = area['name']
self._name = name self._state = _get_alarm_state(area['mode'])
self._state = state if self._state == STATE_ALARM_DISARMED:
self._api = hass.data[DATA_API] self._changed_by = area.get('last_unset_user_name', 'unknown')
else:
hass.data[DATA_REGISTRY].register_alarm_device(area_id, self) self._changed_by = area.get('last_set_user_name', 'unknown')
self._api = api
@asyncio.coroutine @asyncio.coroutine
def async_update_from_spc(self, state): def async_added_to_hass(self):
"""Calbback for init handlers."""
self.hass.data[DATA_REGISTRY].register_alarm_device(
self._area_id, self)
@asyncio.coroutine
def async_update_from_spc(self, state, extra):
"""Update the alarm panel with a new state.""" """Update the alarm panel with a new state."""
self._state = state self._state = state
yield from self.async_update_ha_state() self._changed_by = extra.get('changed_by', 'unknown')
self.async_schedule_update_ha_state()
@property @property
def should_poll(self): def should_poll(self):
@ -72,6 +78,11 @@ class SpcAlarm(alarm.AlarmControlPanel):
"""Return the name of the device.""" """Return the name of the device."""
return self._name return self._name
@property
def changed_by(self):
"""Return the user the last change was triggered by."""
return self._changed_by
@property @property
def state(self): def state(self):
"""Return the state of the device.""" """Return the state of the device."""

View File

@ -16,7 +16,7 @@ from homeassistant.const import (
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
STATE_ALARM_ARMING, STATE_ALARM_DISARMING, STATE_UNKNOWN, CONF_NAME) STATE_ALARM_ARMING, STATE_ALARM_DISARMING, STATE_UNKNOWN, CONF_NAME)
REQUIREMENTS = ['total_connect_client==0.12'] REQUIREMENTS = ['total_connect_client==0.16']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -15,4 +15,6 @@ ATTR_STREAM_URL = 'streamUrl'
ATTR_MAIN_TEXT = 'mainText' ATTR_MAIN_TEXT = 'mainText'
ATTR_REDIRECTION_URL = 'redirectionURL' ATTR_REDIRECTION_URL = 'redirectionURL'
SYN_RESOLUTION_MATCH = 'ER_SUCCESS_MATCH'
DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.0Z' DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.0Z'

View File

@ -3,6 +3,7 @@ Support for Alexa skill service end point.
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/alexa/ https://home-assistant.io/components/alexa/
""" """
import asyncio import asyncio
import enum import enum
@ -13,7 +14,7 @@ from homeassistant.const import HTTP_BAD_REQUEST
from homeassistant.helpers import intent from homeassistant.helpers import intent
from homeassistant.components import http from homeassistant.components import http
from .const import DOMAIN from .const import DOMAIN, SYN_RESOLUTION_MATCH
INTENTS_API_ENDPOINT = '/api/alexa' INTENTS_API_ENDPOINT = '/api/alexa'
@ -123,6 +124,43 @@ class AlexaIntentsView(http.HomeAssistantView):
return self.json(alexa_response) return self.json(alexa_response)
def resolve_slot_synonyms(key, request):
"""Check slot request for synonym resolutions."""
# Default to the spoken slot value if more than one or none are found. For
# reference to the request object structure, see the Alexa docs:
# https://tinyurl.com/ybvm7jhs
resolved_value = request['value']
if ('resolutions' in request and
'resolutionsPerAuthority' in request['resolutions'] and
len(request['resolutions']['resolutionsPerAuthority']) >= 1):
# Extract all of the possible values from each authority with a
# successful match
possible_values = []
for entry in request['resolutions']['resolutionsPerAuthority']:
if entry['status']['code'] != SYN_RESOLUTION_MATCH:
continue
possible_values.extend([item['value']['name']
for item
in entry['values']])
# If there is only one match use the resolved value, otherwise the
# resolution cannot be determined, so use the spoken slot value
if len(possible_values) == 1:
resolved_value = possible_values[0]
else:
_LOGGER.debug(
'Found multiple synonym resolutions for slot value: {%s: %s}',
key,
request['value']
)
return resolved_value
class AlexaResponse(object): class AlexaResponse(object):
"""Help generating the response for Alexa.""" """Help generating the response for Alexa."""
@ -135,12 +173,17 @@ class AlexaResponse(object):
self.session_attributes = {} self.session_attributes = {}
self.should_end_session = True self.should_end_session = True
self.variables = {} self.variables = {}
# Intent is None if request was a LaunchRequest or SessionEndedRequest # Intent is None if request was a LaunchRequest or SessionEndedRequest
if intent_info is not None: if intent_info is not None:
for key, value in intent_info.get('slots', {}).items(): for key, value in intent_info.get('slots', {}).items():
if 'value' in value: # Only include slots with values
underscored_key = key.replace('.', '_') if 'value' not in value:
self.variables[underscored_key] = value['value'] continue
_key = key.replace('.', '_')
self.variables[_key] = resolve_slot_synonyms(key, value)
def add_card(self, card_type, title, content): def add_card(self, card_type, title, content):
"""Add a card to the response.""" """Add a card to the response."""

View File

@ -1,12 +1,20 @@
"""Support for alexa Smart Home Skill API.""" """Support for alexa Smart Home Skill API."""
import asyncio import asyncio
from collections import namedtuple
import logging import logging
import math import math
from uuid import uuid4 from uuid import uuid4
import homeassistant.core as ha
from homeassistant.const import ( from homeassistant.const import (
ATTR_SUPPORTED_FEATURES, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF) ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_LOCK,
from homeassistant.components import switch, light SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY,
SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON,
SERVICE_UNLOCK, SERVICE_VOLUME_SET)
from homeassistant.components import (
alert, automation, cover, fan, group, input_boolean, light, lock,
media_player, scene, script, switch)
import homeassistant.util.color as color_util import homeassistant.util.color as color_util
from homeassistant.util.decorator import Registry from homeassistant.util.decorator import Registry
@ -14,14 +22,32 @@ HANDLERS = Registry()
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
API_DIRECTIVE = 'directive' API_DIRECTIVE = 'directive'
API_ENDPOINT = 'endpoint'
API_EVENT = 'event' API_EVENT = 'event'
API_HEADER = 'header' API_HEADER = 'header'
API_PAYLOAD = 'payload' API_PAYLOAD = 'payload'
API_ENDPOINT = 'endpoint'
ATTR_ALEXA_DESCRIPTION = 'alexa_description'
ATTR_ALEXA_DISPLAY_CATEGORIES = 'alexa_display_categories'
ATTR_ALEXA_HIDDEN = 'alexa_hidden'
ATTR_ALEXA_NAME = 'alexa_name'
MAPPING_COMPONENT = { MAPPING_COMPONENT = {
switch.DOMAIN: ['SWITCH', ('Alexa.PowerController',), None], alert.DOMAIN: ['OTHER', ('Alexa.PowerController',), None],
automation.DOMAIN: ['OTHER', ('Alexa.PowerController',), None],
cover.DOMAIN: [
'DOOR', ('Alexa.PowerController',), {
cover.SUPPORT_SET_POSITION: 'Alexa.PercentageController',
}
],
fan.DOMAIN: [
'OTHER', ('Alexa.PowerController',), {
fan.SUPPORT_SET_SPEED: 'Alexa.PercentageController',
}
],
group.DOMAIN: ['OTHER', ('Alexa.PowerController',), None],
input_boolean.DOMAIN: ['OTHER', ('Alexa.PowerController',), None],
light.DOMAIN: [ light.DOMAIN: [
'LIGHT', ('Alexa.PowerController',), { 'LIGHT', ('Alexa.PowerController',), {
light.SUPPORT_BRIGHTNESS: 'Alexa.BrightnessController', light.SUPPORT_BRIGHTNESS: 'Alexa.BrightnessController',
@ -30,11 +56,28 @@ MAPPING_COMPONENT = {
light.SUPPORT_COLOR_TEMP: 'Alexa.ColorTemperatureController', light.SUPPORT_COLOR_TEMP: 'Alexa.ColorTemperatureController',
} }
], ],
lock.DOMAIN: ['SMARTLOCK', ('Alexa.LockController',), None],
media_player.DOMAIN: [
'TV', ('Alexa.PowerController',), {
media_player.SUPPORT_VOLUME_SET: 'Alexa.Speaker',
media_player.SUPPORT_PLAY: 'Alexa.PlaybackController',
media_player.SUPPORT_PAUSE: 'Alexa.PlaybackController',
media_player.SUPPORT_STOP: 'Alexa.PlaybackController',
media_player.SUPPORT_NEXT_TRACK: 'Alexa.PlaybackController',
media_player.SUPPORT_PREVIOUS_TRACK: 'Alexa.PlaybackController',
}
],
scene.DOMAIN: ['ACTIVITY_TRIGGER', ('Alexa.SceneController',), None],
script.DOMAIN: ['OTHER', ('Alexa.PowerController',), None],
switch.DOMAIN: ['SWITCH', ('Alexa.PowerController',), None],
} }
Config = namedtuple('AlexaConfig', 'filter')
@asyncio.coroutine @asyncio.coroutine
def async_handle_message(hass, message): def async_handle_message(hass, config, message):
"""Handle incoming API messages.""" """Handle incoming API messages."""
assert message[API_DIRECTIVE][API_HEADER]['payloadVersion'] == '3' assert message[API_DIRECTIVE][API_HEADER]['payloadVersion'] == '3'
@ -50,7 +93,7 @@ def async_handle_message(hass, message):
"Unsupported API request %s/%s", namespace, name) "Unsupported API request %s/%s", namespace, name)
return api_error(message) return api_error(message)
return (yield from funct_ref(hass, message)) return (yield from funct_ref(hass, config, message))
def api_message(request, name='Response', namespace='Alexa', payload=None): def api_message(request, name='Response', namespace='Alexa', payload=None):
@ -99,7 +142,7 @@ def api_error(request, error_type='INTERNAL_ERROR', error_message=""):
@HANDLERS.register(('Alexa.Discovery', 'Discover')) @HANDLERS.register(('Alexa.Discovery', 'Discover'))
@asyncio.coroutine @asyncio.coroutine
def async_api_discovery(hass, request): def async_api_discovery(hass, config, request):
"""Create a API formatted discovery response. """Create a API formatted discovery response.
Async friendly. Async friendly.
@ -107,18 +150,40 @@ def async_api_discovery(hass, request):
discovery_endpoints = [] discovery_endpoints = []
for entity in hass.states.async_all(): for entity in hass.states.async_all():
if not config.filter(entity.entity_id):
_LOGGER.debug("Not exposing %s because filtered by config",
entity.entity_id)
continue
if entity.attributes.get(ATTR_ALEXA_HIDDEN, False):
_LOGGER.debug("Not exposing %s because alexa_hidden is true",
entity.entity_id)
continue
class_data = MAPPING_COMPONENT.get(entity.domain) class_data = MAPPING_COMPONENT.get(entity.domain)
if not class_data: if not class_data:
continue continue
friendly_name = entity.attributes.get(ATTR_ALEXA_NAME, entity.name)
description = entity.attributes.get(ATTR_ALEXA_DESCRIPTION,
entity.entity_id)
# Required description as per Amazon Scene docs
if entity.domain == scene.DOMAIN:
scene_fmt = '{} (Scene connected via Home Assistant)'
description = scene_fmt.format(description)
cat_key = ATTR_ALEXA_DISPLAY_CATEGORIES
display_categories = entity.attributes.get(cat_key, class_data[0])
endpoint = { endpoint = {
'displayCategories': [class_data[0]], 'displayCategories': [display_categories],
'additionalApplianceDetails': {}, 'additionalApplianceDetails': {},
'endpointId': entity.entity_id.replace('.', '#'), 'endpointId': entity.entity_id.replace('.', '#'),
'friendlyName': entity.name, 'friendlyName': friendly_name,
'description': '', 'description': description,
'manufacturerName': 'Unknown', 'manufacturerName': 'Home Assistant',
} }
actions = set() actions = set()
@ -153,7 +218,7 @@ def async_api_discovery(hass, request):
def extract_entity(funct): def extract_entity(funct):
"""Decorator for extract entity object from request.""" """Decorator for extract entity object from request."""
@asyncio.coroutine @asyncio.coroutine
def async_api_entity_wrapper(hass, request): def async_api_entity_wrapper(hass, config, request):
"""Process a turn on request.""" """Process a turn on request."""
entity_id = request[API_ENDPOINT]['endpointId'].replace('#', '.') entity_id = request[API_ENDPOINT]['endpointId'].replace('#', '.')
@ -164,7 +229,7 @@ def extract_entity(funct):
request[API_HEADER]['name'], entity_id) request[API_HEADER]['name'], entity_id)
return api_error(request, error_type='NO_SUCH_ENDPOINT') return api_error(request, error_type='NO_SUCH_ENDPOINT')
return (yield from funct(hass, request, entity)) return (yield from funct(hass, config, request, entity))
return async_api_entity_wrapper return async_api_entity_wrapper
@ -172,9 +237,13 @@ def extract_entity(funct):
@HANDLERS.register(('Alexa.PowerController', 'TurnOn')) @HANDLERS.register(('Alexa.PowerController', 'TurnOn'))
@extract_entity @extract_entity
@asyncio.coroutine @asyncio.coroutine
def async_api_turn_on(hass, request, entity): def async_api_turn_on(hass, config, request, entity):
"""Process a turn on request.""" """Process a turn on request."""
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { domain = entity.domain
if entity.domain == group.DOMAIN:
domain = ha.DOMAIN
yield from hass.services.async_call(domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id ATTR_ENTITY_ID: entity.entity_id
}, blocking=True) }, blocking=True)
@ -184,9 +253,13 @@ def async_api_turn_on(hass, request, entity):
@HANDLERS.register(('Alexa.PowerController', 'TurnOff')) @HANDLERS.register(('Alexa.PowerController', 'TurnOff'))
@extract_entity @extract_entity
@asyncio.coroutine @asyncio.coroutine
def async_api_turn_off(hass, request, entity): def async_api_turn_off(hass, config, request, entity):
"""Process a turn off request.""" """Process a turn off request."""
yield from hass.services.async_call(entity.domain, SERVICE_TURN_OFF, { domain = entity.domain
if entity.domain == group.DOMAIN:
domain = ha.DOMAIN
yield from hass.services.async_call(domain, SERVICE_TURN_OFF, {
ATTR_ENTITY_ID: entity.entity_id ATTR_ENTITY_ID: entity.entity_id
}, blocking=True) }, blocking=True)
@ -196,7 +269,7 @@ def async_api_turn_off(hass, request, entity):
@HANDLERS.register(('Alexa.BrightnessController', 'SetBrightness')) @HANDLERS.register(('Alexa.BrightnessController', 'SetBrightness'))
@extract_entity @extract_entity
@asyncio.coroutine @asyncio.coroutine
def async_api_set_brightness(hass, request, entity): def async_api_set_brightness(hass, config, request, entity):
"""Process a set brightness request.""" """Process a set brightness request."""
brightness = int(request[API_PAYLOAD]['brightness']) brightness = int(request[API_PAYLOAD]['brightness'])
@ -211,7 +284,7 @@ def async_api_set_brightness(hass, request, entity):
@HANDLERS.register(('Alexa.BrightnessController', 'AdjustBrightness')) @HANDLERS.register(('Alexa.BrightnessController', 'AdjustBrightness'))
@extract_entity @extract_entity
@asyncio.coroutine @asyncio.coroutine
def async_api_adjust_brightness(hass, request, entity): def async_api_adjust_brightness(hass, config, request, entity):
"""Process a adjust brightness request.""" """Process a adjust brightness request."""
brightness_delta = int(request[API_PAYLOAD]['brightnessDelta']) brightness_delta = int(request[API_PAYLOAD]['brightnessDelta'])
@ -235,7 +308,7 @@ def async_api_adjust_brightness(hass, request, entity):
@HANDLERS.register(('Alexa.ColorController', 'SetColor')) @HANDLERS.register(('Alexa.ColorController', 'SetColor'))
@extract_entity @extract_entity
@asyncio.coroutine @asyncio.coroutine
def async_api_set_color(hass, request, entity): def async_api_set_color(hass, config, request, entity):
"""Process a set color request.""" """Process a set color request."""
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES) supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES)
rgb = color_util.color_hsb_to_RGB( rgb = color_util.color_hsb_to_RGB(
@ -263,7 +336,7 @@ def async_api_set_color(hass, request, entity):
@HANDLERS.register(('Alexa.ColorTemperatureController', 'SetColorTemperature')) @HANDLERS.register(('Alexa.ColorTemperatureController', 'SetColorTemperature'))
@extract_entity @extract_entity
@asyncio.coroutine @asyncio.coroutine
def async_api_set_color_temperature(hass, request, entity): def async_api_set_color_temperature(hass, config, request, entity):
"""Process a set color temperature request.""" """Process a set color temperature request."""
kelvin = int(request[API_PAYLOAD]['colorTemperatureInKelvin']) kelvin = int(request[API_PAYLOAD]['colorTemperatureInKelvin'])
@ -279,7 +352,7 @@ def async_api_set_color_temperature(hass, request, entity):
('Alexa.ColorTemperatureController', 'DecreaseColorTemperature')) ('Alexa.ColorTemperatureController', 'DecreaseColorTemperature'))
@extract_entity @extract_entity
@asyncio.coroutine @asyncio.coroutine
def async_api_decrease_color_temp(hass, request, entity): def async_api_decrease_color_temp(hass, config, request, entity):
"""Process a decrease color temperature request.""" """Process a decrease color temperature request."""
current = int(entity.attributes.get(light.ATTR_COLOR_TEMP)) current = int(entity.attributes.get(light.ATTR_COLOR_TEMP))
max_mireds = int(entity.attributes.get(light.ATTR_MAX_MIREDS)) max_mireds = int(entity.attributes.get(light.ATTR_MAX_MIREDS))
@ -297,7 +370,7 @@ def async_api_decrease_color_temp(hass, request, entity):
('Alexa.ColorTemperatureController', 'IncreaseColorTemperature')) ('Alexa.ColorTemperatureController', 'IncreaseColorTemperature'))
@extract_entity @extract_entity
@asyncio.coroutine @asyncio.coroutine
def async_api_increase_color_temp(hass, request, entity): def async_api_increase_color_temp(hass, config, request, entity):
"""Process a increase color temperature request.""" """Process a increase color temperature request."""
current = int(entity.attributes.get(light.ATTR_COLOR_TEMP)) current = int(entity.attributes.get(light.ATTR_COLOR_TEMP))
min_mireds = int(entity.attributes.get(light.ATTR_MIN_MIREDS)) min_mireds = int(entity.attributes.get(light.ATTR_MIN_MIREDS))
@ -309,3 +382,262 @@ def async_api_increase_color_temp(hass, request, entity):
}, blocking=True) }, blocking=True)
return api_message(request) return api_message(request)
@HANDLERS.register(('Alexa.SceneController', 'Activate'))
@extract_entity
@asyncio.coroutine
def async_api_activate(hass, config, request, entity):
"""Process a activate request."""
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=True)
return api_message(request)
@HANDLERS.register(('Alexa.PercentageController', 'SetPercentage'))
@extract_entity
@asyncio.coroutine
def async_api_set_percentage(hass, config, request, entity):
"""Process a set percentage request."""
percentage = int(request[API_PAYLOAD]['percentage'])
service = None
data = {ATTR_ENTITY_ID: entity.entity_id}
if entity.domain == fan.DOMAIN:
service = fan.SERVICE_SET_SPEED
speed = "off"
if percentage <= 33:
speed = "low"
elif percentage <= 66:
speed = "medium"
elif percentage <= 100:
speed = "high"
data[fan.ATTR_SPEED] = speed
elif entity.domain == cover.DOMAIN:
service = SERVICE_SET_COVER_POSITION
data[cover.ATTR_POSITION] = percentage
yield from hass.services.async_call(entity.domain, service,
data, blocking=True)
return api_message(request)
@HANDLERS.register(('Alexa.PercentageController', 'AdjustPercentage'))
@extract_entity
@asyncio.coroutine
def async_api_adjust_percentage(hass, config, request, entity):
"""Process a adjust percentage request."""
percentage_delta = int(request[API_PAYLOAD]['percentageDelta'])
service = None
data = {ATTR_ENTITY_ID: entity.entity_id}
if entity.domain == fan.DOMAIN:
service = fan.SERVICE_SET_SPEED
speed = entity.attributes.get(fan.ATTR_SPEED)
if speed == "off":
current = 0
elif speed == "low":
current = 33
elif speed == "medium":
current = 66
elif speed == "high":
current = 100
# set percentage
percentage = max(0, percentage_delta + current)
speed = "off"
if percentage <= 33:
speed = "low"
elif percentage <= 66:
speed = "medium"
elif percentage <= 100:
speed = "high"
data[fan.ATTR_SPEED] = speed
elif entity.domain == cover.DOMAIN:
service = SERVICE_SET_COVER_POSITION
current = entity.attributes.get(cover.ATTR_POSITION)
data[cover.ATTR_POSITION] = max(0, percentage_delta + current)
yield from hass.services.async_call(entity.domain, service,
data, blocking=True)
return api_message(request)
@HANDLERS.register(('Alexa.LockController', 'Lock'))
@extract_entity
@asyncio.coroutine
def async_api_lock(hass, config, request, entity):
"""Process a lock request."""
yield from hass.services.async_call(entity.domain, SERVICE_LOCK, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=True)
return api_message(request)
# Not supported by Alexa yet
@HANDLERS.register(('Alexa.LockController', 'Unlock'))
@extract_entity
@asyncio.coroutine
def async_api_unlock(hass, config, request, entity):
"""Process a unlock request."""
yield from hass.services.async_call(entity.domain, SERVICE_UNLOCK, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=True)
return api_message(request)
@HANDLERS.register(('Alexa.Speaker', 'SetVolume'))
@extract_entity
@asyncio.coroutine
def async_api_set_volume(hass, config, request, entity):
"""Process a set volume request."""
volume = round(float(request[API_PAYLOAD]['volume'] / 100), 2)
data = {
ATTR_ENTITY_ID: entity.entity_id,
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
}
yield from hass.services.async_call(entity.domain, SERVICE_VOLUME_SET,
data, blocking=True)
return api_message(request)
@HANDLERS.register(('Alexa.Speaker', 'AdjustVolume'))
@extract_entity
@asyncio.coroutine
def async_api_adjust_volume(hass, config, request, entity):
"""Process a adjust volume request."""
volume_delta = int(request[API_PAYLOAD]['volume'])
current_level = entity.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL)
# read current state
try:
current = math.floor(int(current_level * 100))
except ZeroDivisionError:
current = 0
volume = float(max(0, volume_delta + current) / 100)
data = {
ATTR_ENTITY_ID: entity.entity_id,
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
}
yield from hass.services.async_call(entity.domain,
media_player.SERVICE_VOLUME_SET,
data, blocking=True)
return api_message(request)
@HANDLERS.register(('Alexa.Speaker', 'SetMute'))
@extract_entity
@asyncio.coroutine
def async_api_set_mute(hass, config, request, entity):
"""Process a set mute request."""
mute = bool(request[API_PAYLOAD]['mute'])
data = {
ATTR_ENTITY_ID: entity.entity_id,
media_player.ATTR_MEDIA_VOLUME_MUTED: mute,
}
yield from hass.services.async_call(entity.domain,
media_player.SERVICE_VOLUME_MUTE,
data, blocking=True)
return api_message(request)
@HANDLERS.register(('Alexa.PlaybackController', 'Play'))
@extract_entity
@asyncio.coroutine
def async_api_play(hass, config, request, entity):
"""Process a play request."""
data = {
ATTR_ENTITY_ID: entity.entity_id
}
yield from hass.services.async_call(entity.domain, SERVICE_MEDIA_PLAY,
data, blocking=True)
return api_message(request)
@HANDLERS.register(('Alexa.PlaybackController', 'Pause'))
@extract_entity
@asyncio.coroutine
def async_api_pause(hass, config, request, entity):
"""Process a pause request."""
data = {
ATTR_ENTITY_ID: entity.entity_id
}
yield from hass.services.async_call(entity.domain, SERVICE_MEDIA_PAUSE,
data, blocking=True)
return api_message(request)
@HANDLERS.register(('Alexa.PlaybackController', 'Stop'))
@extract_entity
@asyncio.coroutine
def async_api_stop(hass, config, request, entity):
"""Process a stop request."""
data = {
ATTR_ENTITY_ID: entity.entity_id
}
yield from hass.services.async_call(entity.domain, SERVICE_MEDIA_STOP,
data, blocking=True)
return api_message(request)
@HANDLERS.register(('Alexa.PlaybackController', 'Next'))
@extract_entity
@asyncio.coroutine
def async_api_next(hass, config, request, entity):
"""Process a next request."""
data = {
ATTR_ENTITY_ID: entity.entity_id
}
yield from hass.services.async_call(entity.domain,
SERVICE_MEDIA_NEXT_TRACK,
data, blocking=True)
return api_message(request)
@HANDLERS.register(('Alexa.PlaybackController', 'Previous'))
@extract_entity
@asyncio.coroutine
def async_api_previous(hass, config, request, entity):
"""Process a previous request."""
data = {
ATTR_ENTITY_ID: entity.entity_id
}
yield from hass.services.async_call(entity.domain,
SERVICE_MEDIA_PREVIOUS_TRACK,
data, blocking=True)
return api_message(request)

View File

@ -89,6 +89,7 @@ def setup(hass, config):
"""Set up the Amcrest IP Camera component.""" """Set up the Amcrest IP Camera component."""
from amcrest import AmcrestCamera from amcrest import AmcrestCamera
hass.data[DATA_AMCREST] = {}
amcrest_cams = config[DOMAIN] amcrest_cams = config[DOMAIN]
for device in amcrest_cams: for device in amcrest_cams:
@ -126,22 +127,34 @@ def setup(hass, config):
else: else:
authentication = None authentication = None
hass.data[DATA_AMCREST][name] = AmcrestDevice(
camera, name, authentication, ffmpeg_arguments, stream_source,
resolution)
discovery.load_platform( discovery.load_platform(
hass, 'camera', DOMAIN, { hass, 'camera', DOMAIN, {
'device': camera,
CONF_AUTHENTICATION: authentication,
CONF_FFMPEG_ARGUMENTS: ffmpeg_arguments,
CONF_NAME: name, CONF_NAME: name,
CONF_RESOLUTION: resolution,
CONF_STREAM_SOURCE: stream_source,
}, config) }, config)
if sensors: if sensors:
discovery.load_platform( discovery.load_platform(
hass, 'sensor', DOMAIN, { hass, 'sensor', DOMAIN, {
'device': camera,
CONF_NAME: name, CONF_NAME: name,
CONF_SENSORS: sensors, CONF_SENSORS: sensors,
}, config) }, config)
return True return True
class AmcrestDevice(object):
"""Representation of a base Amcrest discovery device."""
def __init__(self, camera, name, authentication, ffmpeg_arguments,
stream_source, resolution):
"""Initialize the entity."""
self.device = camera
self.name = name
self.authentication = authentication
self.ffmpeg_arguments = ffmpeg_arguments
self.stream_source = stream_source
self.resolution = resolution

View File

@ -18,7 +18,7 @@ from homeassistant.helpers import discovery
from homeassistant.components.discovery import SERVICE_APPLE_TV from homeassistant.components.discovery import SERVICE_APPLE_TV
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyatv==0.3.6'] REQUIREMENTS = ['pyatv==0.3.8']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -12,7 +12,7 @@ from requests.exceptions import HTTPError, ConnectTimeout
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
REQUIREMENTS = ['pyarlo==0.0.7'] REQUIREMENTS = ['pyarlo==0.1.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -37,8 +37,8 @@ def async_trigger(hass, config, action):
above = config.get(CONF_ABOVE) above = config.get(CONF_ABOVE)
time_delta = config.get(CONF_FOR) time_delta = config.get(CONF_FOR)
value_template = config.get(CONF_VALUE_TEMPLATE) value_template = config.get(CONF_VALUE_TEMPLATE)
async_remove_track_same = None unsub_track_same = {}
already_triggered = False entities_triggered = set()
if value_template is not None: if value_template is not None:
value_template.hass = hass value_template.hass = hass
@ -63,8 +63,6 @@ def async_trigger(hass, config, action):
@callback @callback
def state_automation_listener(entity, from_s, to_s): def state_automation_listener(entity, from_s, to_s):
"""Listen for state changes and calls action.""" """Listen for state changes and calls action."""
nonlocal already_triggered, async_remove_track_same
@callback @callback
def call_action(): def call_action():
"""Call action with right context.""" """Call action with right context."""
@ -81,16 +79,18 @@ def async_trigger(hass, config, action):
matching = check_numeric_state(entity, from_s, to_s) matching = check_numeric_state(entity, from_s, to_s)
if matching and not already_triggered: if not matching:
entities_triggered.discard(entity)
elif entity not in entities_triggered:
entities_triggered.add(entity)
if time_delta: if time_delta:
async_remove_track_same = async_track_same_state( unsub_track_same[entity] = async_track_same_state(
hass, time_delta, call_action, entity_ids=entity_id, hass, time_delta, call_action, entity_ids=entity_id,
async_check_same_func=check_numeric_state) async_check_same_func=check_numeric_state)
else: else:
call_action() call_action()
already_triggered = matching
unsub = async_track_state_change( unsub = async_track_state_change(
hass, entity_id, state_automation_listener) hass, entity_id, state_automation_listener)
@ -98,7 +98,8 @@ def async_trigger(hass, config, action):
def async_remove(): def async_remove():
"""Remove state listeners async.""" """Remove state listeners async."""
unsub() unsub()
if async_remove_track_same: for async_remove in unsub_track_same.values():
async_remove_track_same() # pylint: disable=not-callable async_remove()
unsub_track_same.clear()
return async_remove return async_remove

View File

@ -35,13 +35,11 @@ def async_trigger(hass, config, action):
to_state = config.get(CONF_TO, MATCH_ALL) to_state = config.get(CONF_TO, MATCH_ALL)
time_delta = config.get(CONF_FOR) time_delta = config.get(CONF_FOR)
match_all = (from_state == MATCH_ALL and to_state == MATCH_ALL) match_all = (from_state == MATCH_ALL and to_state == MATCH_ALL)
async_remove_track_same = None unsub_track_same = {}
@callback @callback
def state_automation_listener(entity, from_s, to_s): def state_automation_listener(entity, from_s, to_s):
"""Listen for state changes and calls action.""" """Listen for state changes and calls action."""
nonlocal async_remove_track_same
@callback @callback
def call_action(): def call_action():
"""Call action with right context.""" """Call action with right context."""
@ -64,7 +62,7 @@ def async_trigger(hass, config, action):
call_action() call_action()
return return
async_remove_track_same = async_track_same_state( unsub_track_same[entity] = async_track_same_state(
hass, time_delta, call_action, hass, time_delta, call_action,
lambda _, _2, to_state: to_state.state == to_s.state, lambda _, _2, to_state: to_state.state == to_s.state,
entity_ids=entity_id) entity_ids=entity_id)
@ -76,7 +74,8 @@ def async_trigger(hass, config, action):
def async_remove(): def async_remove():
"""Remove state listeners async.""" """Remove state listeners async."""
unsub() unsub()
if async_remove_track_same: for async_remove in unsub_track_same.values():
async_remove_track_same() # pylint: disable=not-callable async_remove()
unsub_track_same.clear()
return async_remove return async_remove

View File

@ -5,7 +5,6 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/axis/ https://home-assistant.io/components/axis/
""" """
import json
import logging import logging
import os import os
@ -22,6 +21,7 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.dispatcher import dispatcher_send
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.util.json import load_json, save_json
REQUIREMENTS = ['axis==14'] REQUIREMENTS = ['axis==14']
@ -103,9 +103,9 @@ def request_configuration(hass, config, name, host, serialnumber):
return False return False
if setup_device(hass, config, device_config): if setup_device(hass, config, device_config):
config_file = _read_config(hass) config_file = load_json(hass.config.path(CONFIG_FILE))
config_file[serialnumber] = dict(device_config) config_file[serialnumber] = dict(device_config)
_write_config(hass, config_file) save_json(hass.config.path(CONFIG_FILE), config_file)
configurator.request_done(request_id) configurator.request_done(request_id)
else: else:
configurator.notify_errors(request_id, configurator.notify_errors(request_id,
@ -163,7 +163,7 @@ def setup(hass, config):
serialnumber = discovery_info['properties']['macaddress'] serialnumber = discovery_info['properties']['macaddress']
if serialnumber not in AXIS_DEVICES: if serialnumber not in AXIS_DEVICES:
config_file = _read_config(hass) config_file = load_json(hass.config.path(CONFIG_FILE))
if serialnumber in config_file: if serialnumber in config_file:
# Device config previously saved to file # Device config previously saved to file
try: try:
@ -269,29 +269,11 @@ def setup_device(hass, config, device_config):
config) config)
AXIS_DEVICES[device.serial_number] = device AXIS_DEVICES[device.serial_number] = device
hass.add_job(device.start) if event_types:
hass.add_job(device.start)
return True return True
def _read_config(hass):
"""Read Axis config."""
path = hass.config.path(CONFIG_FILE)
if not os.path.isfile(path):
return {}
with open(path) as f_handle:
# Guard against empty file
return json.loads(f_handle.read() or '{}')
def _write_config(hass, config):
"""Write Axis config."""
data = json.dumps(config)
with open(hass.config.path(CONFIG_FILE), 'w', encoding='utf-8') as outfile:
outfile.write(data)
class AxisDeviceEvent(Entity): class AxisDeviceEvent(Entity):
"""Representation of a Axis device event.""" """Representation of a Axis device event."""

View File

@ -20,6 +20,7 @@ SCAN_INTERVAL = timedelta(seconds=30)
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'
DEVICE_CLASSES = [ DEVICE_CLASSES = [
'battery', # On means low, Off means normal
'cold', # On means cold (or too cold) 'cold', # On means cold (or too cold)
'connectivity', # On means connection present, Off = no connection 'connectivity', # On means connection present, Off = no connection
'gas', # CO, CO2, etc. 'gas', # CO, CO2, etc.
@ -32,6 +33,7 @@ DEVICE_CLASSES = [
'opening', # Door, window, etc. 'opening', # Door, window, etc.
'plug', # On means plugged in, Off means unplugged 'plug', # On means plugged in, Off means unplugged
'power', # Power, over-current, etc 'power', # Power, over-current, etc
'presence', # On means home, Off means away
'safety', # Generic on=unsafe, off=safe 'safety', # Generic on=unsafe, off=safe
'smoke', # Smoke detector 'smoke', # Smoke detector
'sound', # On means sound detected, Off means no sound 'sound', # On means sound detected, Off means no sound

View File

@ -0,0 +1,87 @@
"""
Support for ADS binary sensors.
For more details about this platform, please refer to the documentation.
https://home-assistant.io/components/binary_sensor.ads/
"""
import asyncio
import logging
import voluptuous as vol
from homeassistant.components.binary_sensor import BinarySensorDevice, \
PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA
from homeassistant.components.ads import DATA_ADS, CONF_ADS_VAR
from homeassistant.const import CONF_NAME, CONF_DEVICE_CLASS
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['ads']
DEFAULT_NAME = 'ADS binary sensor'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ADS_VAR): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Binary Sensor platform for ADS."""
ads_hub = hass.data.get(DATA_ADS)
ads_var = config.get(CONF_ADS_VAR)
name = config.get(CONF_NAME)
device_class = config.get(CONF_DEVICE_CLASS)
ads_sensor = AdsBinarySensor(ads_hub, name, ads_var, device_class)
add_devices([ads_sensor])
class AdsBinarySensor(BinarySensorDevice):
"""Representation of ADS binary sensors."""
def __init__(self, ads_hub, name, ads_var, device_class):
"""Initialize AdsBinarySensor entity."""
self._name = name
self._state = False
self._device_class = device_class or 'moving'
self._ads_hub = ads_hub
self.ads_var = ads_var
@asyncio.coroutine
def async_added_to_hass(self):
"""Register device notification."""
def update(name, value):
"""Handle device notifications."""
_LOGGER.debug('Variable %s changed its value to %d',
name, value)
self._state = value
self.schedule_update_ha_state()
self.hass.async_add_job(
self._ads_hub.add_device_notification,
self.ads_var, self._ads_hub.PLCTYPE_BOOL, update
)
@property
def name(self):
"""Return the default name of the binary sensor."""
return self._name
@property
def device_class(self):
"""Return the device class."""
return self._device_class
@property
def is_on(self):
"""Return if the binary sensor is on."""
return self._state
@property
def should_poll(self):
"""Return False because entity pushes its state to HA."""
return False

View File

@ -0,0 +1,63 @@
"""
Support for the Hive devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.hive/
"""
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.hive import DATA_HIVE
DEPENDENCIES = ['hive']
DEVICETYPE_DEVICE_CLASS = {'motionsensor': 'motion',
'contactsensor': 'opening'}
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up Hive sensor devices."""
if discovery_info is None:
return
session = hass.data.get(DATA_HIVE)
add_devices([HiveBinarySensorEntity(session, discovery_info)])
class HiveBinarySensorEntity(BinarySensorDevice):
"""Representation of a Hive binary sensor."""
def __init__(self, hivesession, hivedevice):
"""Initialize the hive sensor."""
self.node_id = hivedevice["Hive_NodeID"]
self.node_name = hivedevice["Hive_NodeName"]
self.device_type = hivedevice["HA_DeviceType"]
self.node_device_type = hivedevice["Hive_DeviceType"]
self.session = hivesession
self.data_updatesource = '{}.{}'.format(self.device_type,
self.node_id)
self.session.entities.append(self)
def handle_update(self, updatesource):
"""Handle the new update request."""
if '{}.{}'.format(self.device_type, self.node_id) not in updatesource:
self.schedule_update_ha_state()
@property
def device_class(self):
"""Return the class of this sensor."""
return DEVICETYPE_DEVICE_CLASS.get(self.node_device_type)
@property
def name(self):
"""Return the name of the binary sensor."""
return self.node_name
@property
def is_on(self):
"""Return true if the binary sensor is on."""
return self.session.sensor.get_state(self.node_id,
self.node_device_type)
def update(self):
"""Update all Node data frome Hive."""
self.session.core.update_data(self.node_id)

View File

@ -25,6 +25,7 @@ SENSOR_TYPES_CLASS = {
'RemoteMotion': None, 'RemoteMotion': None,
'WeatherSensor': None, 'WeatherSensor': None,
'TiltSensor': None, 'TiltSensor': None,
'PresenceIP': 'motion',
} }

View File

@ -67,7 +67,7 @@ class SpcBinarySensor(BinarySensorDevice):
spc_registry.register_sensor_device(zone_id, self) spc_registry.register_sensor_device(zone_id, self)
@asyncio.coroutine @asyncio.coroutine
def async_update_from_spc(self, state): def async_update_from_spc(self, state, extra):
"""Update the state of the device.""" """Update the state of the device."""
self._state = state self._state = state
yield from self.async_update_ha_state() yield from self.async_update_ha_state()

View File

@ -8,9 +8,10 @@ import asyncio
import logging import logging
from homeassistant.components.amcrest import ( from homeassistant.components.amcrest import (
STREAM_SOURCE_LIST, TIMEOUT) DATA_AMCREST, STREAM_SOURCE_LIST, TIMEOUT)
from homeassistant.components.camera import Camera from homeassistant.components.camera import Camera
from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.components.ffmpeg import DATA_FFMPEG
from homeassistant.const import CONF_NAME
from homeassistant.helpers.aiohttp_client import ( from homeassistant.helpers.aiohttp_client import (
async_get_clientsession, async_aiohttp_proxy_web, async_get_clientsession, async_aiohttp_proxy_web,
async_aiohttp_proxy_stream) async_aiohttp_proxy_stream)
@ -26,21 +27,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
if discovery_info is None: if discovery_info is None:
return return
device = discovery_info['device'] device_name = discovery_info[CONF_NAME]
authentication = discovery_info['authentication'] amcrest = hass.data[DATA_AMCREST][device_name]
ffmpeg_arguments = discovery_info['ffmpeg_arguments']
name = discovery_info['name']
resolution = discovery_info['resolution']
stream_source = discovery_info['stream_source']
async_add_devices([ async_add_devices([AmcrestCam(hass, amcrest)], True)
AmcrestCam(hass,
name,
device,
authentication,
ffmpeg_arguments,
stream_source,
resolution)], True)
return True return True
@ -48,18 +38,17 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
class AmcrestCam(Camera): class AmcrestCam(Camera):
"""An implementation of an Amcrest IP camera.""" """An implementation of an Amcrest IP camera."""
def __init__(self, hass, name, camera, authentication, def __init__(self, hass, amcrest):
ffmpeg_arguments, stream_source, resolution):
"""Initialize an Amcrest camera.""" """Initialize an Amcrest camera."""
super(AmcrestCam, self).__init__() super(AmcrestCam, self).__init__()
self._name = name self._name = amcrest.name
self._camera = camera self._camera = amcrest.device
self._base_url = self._camera.get_base_url() self._base_url = self._camera.get_base_url()
self._ffmpeg = hass.data[DATA_FFMPEG] self._ffmpeg = hass.data[DATA_FFMPEG]
self._ffmpeg_arguments = ffmpeg_arguments self._ffmpeg_arguments = amcrest.ffmpeg_arguments
self._stream_source = stream_source self._stream_source = amcrest.stream_source
self._resolution = resolution self._resolution = amcrest.resolution
self._token = self._auth = authentication self._token = self._auth = amcrest.authentication
def camera_image(self): def camera_image(self):
"""Return a still image response from the camera.""" """Return a still image response from the camera."""

View File

@ -19,7 +19,7 @@ from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(minutes=10) SCAN_INTERVAL = timedelta(seconds=90)
ARLO_MODE_ARMED = 'armed' ARLO_MODE_ARMED = 'armed'
ARLO_MODE_DISARMED = 'disarmed' ARLO_MODE_DISARMED = 'disarmed'
@ -31,6 +31,7 @@ ATTR_MOTION = 'motion_detection_sensitivity'
ATTR_POWERSAVE = 'power_save_mode' ATTR_POWERSAVE = 'power_save_mode'
ATTR_SIGNAL_STRENGTH = 'signal_strength' ATTR_SIGNAL_STRENGTH = 'signal_strength'
ATTR_UNSEEN_VIDEOS = 'unseen_videos' ATTR_UNSEEN_VIDEOS = 'unseen_videos'
ATTR_LAST_REFRESH = 'last_refresh'
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments' CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
@ -73,6 +74,8 @@ class ArloCam(Camera):
self._motion_status = False self._motion_status = False
self._ffmpeg = hass.data[DATA_FFMPEG] self._ffmpeg = hass.data[DATA_FFMPEG]
self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS) self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS)
self._last_refresh = None
self._camera.base_station.refresh_rate = SCAN_INTERVAL.total_seconds()
self.attrs = {} self.attrs = {}
def camera_image(self): def camera_image(self):
@ -105,14 +108,17 @@ class ArloCam(Camera):
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""
return { return {
ATTR_BATTERY_LEVEL: self.attrs.get(ATTR_BATTERY_LEVEL), name: value for name, value in (
ATTR_BRIGHTNESS: self.attrs.get(ATTR_BRIGHTNESS), (ATTR_BATTERY_LEVEL, self._camera.battery_level),
ATTR_FLIPPED: self.attrs.get(ATTR_FLIPPED), (ATTR_BRIGHTNESS, self._camera.brightness),
ATTR_MIRRORED: self.attrs.get(ATTR_MIRRORED), (ATTR_FLIPPED, self._camera.flip_state),
ATTR_MOTION: self.attrs.get(ATTR_MOTION), (ATTR_MIRRORED, self._camera.mirror_state),
ATTR_POWERSAVE: self.attrs.get(ATTR_POWERSAVE), (ATTR_MOTION, self._camera.motion_detection_sensitivity),
ATTR_SIGNAL_STRENGTH: self.attrs.get(ATTR_SIGNAL_STRENGTH), (ATTR_POWERSAVE, POWERSAVE_MODE_MAPPING.get(
ATTR_UNSEEN_VIDEOS: self.attrs.get(ATTR_UNSEEN_VIDEOS), self._camera.powersave_mode)),
(ATTR_SIGNAL_STRENGTH, self._camera.signal_strength),
(ATTR_UNSEEN_VIDEOS, self._camera.unseen_videos),
) if value is not None
} }
@property @property
@ -160,13 +166,4 @@ class ArloCam(Camera):
def update(self): def update(self):
"""Add an attribute-update task to the executor pool.""" """Add an attribute-update task to the executor pool."""
self.attrs[ATTR_BATTERY_LEVEL] = self._camera.get_battery_level self._camera.update()
self.attrs[ATTR_BRIGHTNESS] = self._camera.get_battery_level
self.attrs[ATTR_FLIPPED] = self._camera.get_flip_state,
self.attrs[ATTR_MIRRORED] = self._camera.get_mirror_state,
self.attrs[
ATTR_MOTION] = self._camera.get_motion_detection_sensitivity,
self.attrs[ATTR_POWERSAVE] = POWERSAVE_MODE_MAPPING[
self._camera.get_powersave_mode],
self.attrs[ATTR_SIGNAL_STRENGTH] = self._camera.get_signal_strength,
self.attrs[ATTR_UNSEEN_VIDEOS] = self._camera.unseen_videos

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

@ -12,7 +12,8 @@ from datetime import timedelta
import voluptuous as vol import voluptuous as vol
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.components.ring import DATA_RING, CONF_ATTRIBUTION from homeassistant.components.ring import (
DATA_RING, CONF_ATTRIBUTION, NOTIFICATION_ID)
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.components.ffmpeg import DATA_FFMPEG
from homeassistant.const import ATTR_ATTRIBUTION, CONF_SCAN_INTERVAL from homeassistant.const import ATTR_ATTRIBUTION, CONF_SCAN_INTERVAL
@ -27,6 +28,8 @@ FORCE_REFRESH_INTERVAL = timedelta(minutes=45)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
NOTIFICATION_TITLE = 'Ring Camera Setup'
SCAN_INTERVAL = timedelta(seconds=90) SCAN_INTERVAL = timedelta(seconds=90)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@ -42,11 +45,33 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
ring = hass.data[DATA_RING] ring = hass.data[DATA_RING]
cams = [] cams = []
cams_no_plan = []
for camera in ring.doorbells: for camera in ring.doorbells:
cams.append(RingCam(hass, camera, config)) if camera.has_subscription:
cams.append(RingCam(hass, camera, config))
else:
cams_no_plan.append(camera)
for camera in ring.stickup_cams: for camera in ring.stickup_cams:
cams.append(RingCam(hass, camera, config)) if camera.has_subscription:
cams.append(RingCam(hass, camera, config))
else:
cams_no_plan.append(camera)
# show notification for all cameras without an active subscription
if cams_no_plan:
cameras = str(', '.join([camera.name for camera in cams_no_plan]))
err_msg = '''A Ring Protect Plan is required for the''' \
''' following cameras: {}.'''.format(cameras)
_LOGGER.error(err_msg)
hass.components.persistent_notification.async_create(
'Error: {}<br />'
'You will need to restart hass after fixing.'
''.format(err_msg),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
async_add_devices(cams, True) async_add_devices(cams, True)
return True return True
@ -84,7 +109,6 @@ class RingCam(Camera):
'timezone': self._camera.timezone, 'timezone': self._camera.timezone,
'type': self._camera.family, 'type': self._camera.family,
'video_url': self._video_url, 'video_url': self._video_url,
'video_id': self._last_video_id
} }
@asyncio.coroutine @asyncio.coroutine

View File

@ -9,12 +9,12 @@ from datetime import timedelta
import logging import logging
import os import os
import functools as ft import functools as ft
from numbers import Number
import voluptuous as vol import voluptuous as vol
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
from homeassistant.helpers.temperature import display_temp as show_temp
from homeassistant.util.temperature import convert as convert_temperature from homeassistant.util.temperature import convert as convert_temperature
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
@ -22,7 +22,7 @@ from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN, ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN,
TEMP_CELSIUS) TEMP_CELSIUS, PRECISION_WHOLE, PRECISION_TENTHS)
DOMAIN = 'climate' DOMAIN = 'climate'
@ -51,6 +51,19 @@ STATE_HIGH_DEMAND = 'high_demand'
STATE_HEAT_PUMP = 'heat_pump' STATE_HEAT_PUMP = 'heat_pump'
STATE_GAS = 'gas' STATE_GAS = 'gas'
SUPPORT_TARGET_TEMPERATURE = 1
SUPPORT_TARGET_TEMPERATURE_HIGH = 2
SUPPORT_TARGET_TEMPERATURE_LOW = 4
SUPPORT_TARGET_HUMIDITY = 8
SUPPORT_TARGET_HUMIDITY_HIGH = 16
SUPPORT_TARGET_HUMIDITY_LOW = 32
SUPPORT_FAN_MODE = 64
SUPPORT_OPERATION_MODE = 128
SUPPORT_HOLD_MODE = 256
SUPPORT_SWING_MODE = 512
SUPPORT_AWAY_MODE = 1024
SUPPORT_AUX_HEAT = 2048
ATTR_CURRENT_TEMPERATURE = 'current_temperature' ATTR_CURRENT_TEMPERATURE = 'current_temperature'
ATTR_MAX_TEMP = 'max_temp' ATTR_MAX_TEMP = 'max_temp'
ATTR_MIN_TEMP = 'min_temp' ATTR_MIN_TEMP = 'min_temp'
@ -71,11 +84,6 @@ ATTR_OPERATION_LIST = 'operation_list'
ATTR_SWING_MODE = 'swing_mode' ATTR_SWING_MODE = 'swing_mode'
ATTR_SWING_LIST = 'swing_list' ATTR_SWING_LIST = 'swing_list'
# The degree of precision for each platform
PRECISION_WHOLE = 1
PRECISION_HALVES = 0.5
PRECISION_TENTHS = 0.1
CONVERTIBLE_ATTRIBUTE = [ CONVERTIBLE_ATTRIBUTE = [
ATTR_TEMPERATURE, ATTR_TEMPERATURE,
ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_LOW,
@ -456,12 +464,18 @@ class ClimateDevice(Entity):
def state_attributes(self): def state_attributes(self):
"""Return the optional state attributes.""" """Return the optional state attributes."""
data = { data = {
ATTR_CURRENT_TEMPERATURE: ATTR_CURRENT_TEMPERATURE: show_temp(
self._convert_for_display(self.current_temperature), self.hass, self.current_temperature, self.temperature_unit,
ATTR_MIN_TEMP: self._convert_for_display(self.min_temp), self.precision),
ATTR_MAX_TEMP: self._convert_for_display(self.max_temp), ATTR_MIN_TEMP: show_temp(
ATTR_TEMPERATURE: self.hass, self.min_temp, self.temperature_unit,
self._convert_for_display(self.target_temperature), self.precision),
ATTR_MAX_TEMP: show_temp(
self.hass, self.max_temp, self.temperature_unit,
self.precision),
ATTR_TEMPERATURE: show_temp(
self.hass, self.target_temperature, self.temperature_unit,
self.precision),
} }
if self.target_temperature_step is not None: if self.target_temperature_step is not None:
@ -469,10 +483,12 @@ class ClimateDevice(Entity):
target_temp_high = self.target_temperature_high target_temp_high = self.target_temperature_high
if target_temp_high is not None: if target_temp_high is not None:
data[ATTR_TARGET_TEMP_HIGH] = self._convert_for_display( data[ATTR_TARGET_TEMP_HIGH] = show_temp(
self.target_temperature_high) self.hass, self.target_temperature_high, self.temperature_unit,
data[ATTR_TARGET_TEMP_LOW] = self._convert_for_display( self.precision)
self.target_temperature_low) data[ATTR_TARGET_TEMP_LOW] = show_temp(
self.hass, self.target_temperature_low, self.temperature_unit,
self.precision)
humidity = self.target_humidity humidity = self.target_humidity
if humidity is not None: if humidity is not None:
@ -714,6 +730,11 @@ class ClimateDevice(Entity):
""" """
return self.hass.async_add_job(self.turn_aux_heat_off) return self.hass.async_add_job(self.turn_aux_heat_off)
@property
def supported_features(self):
"""Return the list of supported features."""
raise NotImplementedError()
@property @property
def min_temp(self): def min_temp(self):
"""Return the minimum temperature.""" """Return the minimum temperature."""
@ -733,24 +754,3 @@ class ClimateDevice(Entity):
def max_humidity(self): def max_humidity(self):
"""Return the maximum humidity.""" """Return the maximum humidity."""
return 99 return 99
def _convert_for_display(self, temp):
"""Convert temperature into preferred units for display purposes."""
if temp is None:
return temp
# if the temperature is not a number this can cause issues
# with polymer components, so bail early there.
if not isinstance(temp, Number):
raise TypeError("Temperature is not a number: %s" % temp)
if self.temperature_unit != self.unit_of_measurement:
temp = convert_temperature(
temp, self.temperature_unit, self.unit_of_measurement)
# Round in the units appropriate
if self.precision == PRECISION_HALVES:
return round(temp * 2) / 2.0
elif self.precision == PRECISION_TENTHS:
return round(temp, 1)
# PRECISION_WHOLE as a fall back
return round(temp)

View File

@ -5,9 +5,19 @@ For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/ https://home-assistant.io/components/demo/
""" """
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ClimateDevice, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW) ClimateDevice, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_HUMIDITY,
SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_FAN_MODE,
SUPPORT_OPERATION_MODE, SUPPORT_AUX_HEAT, SUPPORT_SWING_MODE,
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW)
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_HUMIDITY |
SUPPORT_AWAY_MODE | SUPPORT_HOLD_MODE | SUPPORT_FAN_MODE |
SUPPORT_OPERATION_MODE | SUPPORT_AUX_HEAT |
SUPPORT_SWING_MODE | SUPPORT_TARGET_TEMPERATURE_HIGH |
SUPPORT_TARGET_TEMPERATURE_LOW)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Demo climate devices.""" """Set up the Demo climate devices."""
@ -47,6 +57,11 @@ class DemoClimate(ClimateDevice):
self._target_temperature_high = target_temp_high self._target_temperature_high = target_temp_high
self._target_temperature_low = target_temp_low self._target_temperature_low = target_temp_low
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property @property
def should_poll(self): def should_poll(self):
"""Return the polling state.""" """Return the polling state."""

View File

@ -12,7 +12,9 @@ import voluptuous as vol
from homeassistant.components import ecobee from homeassistant.components import ecobee
from homeassistant.components.climate import ( from homeassistant.components.climate import (
DOMAIN, STATE_COOL, STATE_HEAT, STATE_AUTO, STATE_IDLE, ClimateDevice, DOMAIN, STATE_COOL, STATE_HEAT, STATE_AUTO, STATE_IDLE, ClimateDevice,
ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH) ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE,
SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_TARGET_HUMIDITY_HIGH)
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, STATE_OFF, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT) ATTR_ENTITY_ID, STATE_OFF, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT)
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
@ -44,6 +46,10 @@ RESUME_PROGRAM_SCHEMA = vol.Schema({
vol.Optional(ATTR_RESUME_ALL, default=DEFAULT_RESUME_ALL): cv.boolean, vol.Optional(ATTR_RESUME_ALL, default=DEFAULT_RESUME_ALL): cv.boolean,
}) })
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE |
SUPPORT_HOLD_MODE | SUPPORT_OPERATION_MODE |
SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Ecobee Thermostat Platform.""" """Set up the Ecobee Thermostat Platform."""
@ -132,6 +138,11 @@ class Thermostat(ClimateDevice):
self.thermostat = self.data.ecobee.get_thermostat( self.thermostat = self.data.ecobee.get_thermostat(
self.thermostat_index) self.thermostat_index)
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property @property
def name(self): def name(self):
"""Return the name of the Ecobee Thermostat.""" """Return the name of the Ecobee Thermostat."""
@ -318,8 +329,21 @@ class Thermostat(ClimateDevice):
def set_auto_temp_hold(self, heat_temp, cool_temp): def set_auto_temp_hold(self, heat_temp, cool_temp):
"""Set temperature hold in auto mode.""" """Set temperature hold in auto mode."""
self.data.ecobee.set_hold_temp(self.thermostat_index, cool_temp, if cool_temp is not None:
heat_temp, self.hold_preference()) cool_temp_setpoint = cool_temp
else:
cool_temp_setpoint = (
self.thermostat['runtime']['desiredCool'] / 10.0)
if heat_temp is not None:
heat_temp_setpoint = heat_temp
else:
heat_temp_setpoint = (
self.thermostat['runtime']['desiredCool'] / 10.0)
self.data.ecobee.set_hold_temp(self.thermostat_index,
cool_temp_setpoint, heat_temp_setpoint,
self.hold_preference())
_LOGGER.debug("Setting ecobee hold_temp to: heat=%s, is=%s, " _LOGGER.debug("Setting ecobee hold_temp to: heat=%s, is=%s, "
"cool=%s, is=%s", heat_temp, isinstance( "cool=%s, is=%s", heat_temp, isinstance(
heat_temp, (int, float)), cool_temp, heat_temp, (int, float)), cool_temp,
@ -348,8 +372,8 @@ class Thermostat(ClimateDevice):
high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH)
temp = kwargs.get(ATTR_TEMPERATURE) temp = kwargs.get(ATTR_TEMPERATURE)
if self.current_operation == STATE_AUTO and low_temp is not None \ if self.current_operation == STATE_AUTO and (low_temp is not None or
and high_temp is not None: high_temp is not None):
self.set_auto_temp_hold(low_temp, high_temp) self.set_auto_temp_hold(low_temp, high_temp)
elif temp is not None: elif temp is not None:
self.set_temp_hold(temp) self.set_temp_hold(temp)
@ -357,6 +381,10 @@ class Thermostat(ClimateDevice):
_LOGGER.error( _LOGGER.error(
"Missing valid arguments for set_temperature in %s", kwargs) "Missing valid arguments for set_temperature in %s", kwargs)
def set_humidity(self, humidity):
"""Set the humidity level."""
self.data.ecobee.set_humidity(self.thermostat_index, humidity)
def set_operation_mode(self, operation_mode): def set_operation_mode(self, operation_mode):
"""Set HVAC mode (auto, auxHeatOnly, cool, heat, off).""" """Set HVAC mode (auto, auxHeatOnly, cool, heat, off)."""
self.data.ecobee.set_hvac_mode(self.thermostat_index, operation_mode) self.data.ecobee.set_hvac_mode(self.thermostat_index, operation_mode)

View File

@ -9,7 +9,7 @@ from datetime import timedelta
import voluptuous as vol import voluptuous as vol
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ClimateDevice, PLATFORM_SCHEMA, STATE_HEAT, STATE_IDLE) ClimateDevice, PLATFORM_SCHEMA, STATE_HEAT, STATE_IDLE, SUPPORT_AUX_HEAT)
from homeassistant.const import ( from homeassistant.const import (
TEMP_CELSIUS, CONF_USERNAME, CONF_PASSWORD) TEMP_CELSIUS, CONF_USERNAME, CONF_PASSWORD)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -56,6 +56,11 @@ class EphEmberThermostat(ClimateDevice):
self._zone = zone self._zone = zone
self._hot_water = zone['isHotWater'] self._hot_water = zone['isHotWater']
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_AUX_HEAT
@property @property
def name(self): def name(self):
"""Return the name of the thermostat, if any.""" """Return the name of the thermostat, if any."""

View File

@ -9,12 +9,10 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ClimateDevice, PLATFORM_SCHEMA, PRECISION_HALVES, STATE_ON, STATE_OFF, STATE_AUTO, PLATFORM_SCHEMA, ClimateDevice,
STATE_AUTO, STATE_ON, STATE_OFF, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE)
)
from homeassistant.const import ( from homeassistant.const import (
CONF_MAC, TEMP_CELSIUS, CONF_DEVICES, ATTR_TEMPERATURE) 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.6'] REQUIREMENTS = ['python-eq3bt==0.1.6']
@ -40,6 +38,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Schema({cv.string: DEVICE_SCHEMA}), vol.Schema({cv.string: DEVICE_SCHEMA}),
}) })
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
SUPPORT_AWAY_MODE)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the eQ-3 BLE thermostats.""" """Set up the eQ-3 BLE thermostats."""
@ -58,21 +59,28 @@ class EQ3BTSmartThermostat(ClimateDevice):
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
self.modes = {eq3.Mode.Open: STATE_ON, self.modes = {
eq3.Mode.Closed: STATE_OFF, eq3.Mode.Open: STATE_ON,
eq3.Mode.Auto: STATE_AUTO, eq3.Mode.Closed: STATE_OFF,
eq3.Mode.Manual: STATE_MANUAL, eq3.Mode.Auto: STATE_AUTO,
eq3.Mode.Boost: STATE_BOOST, eq3.Mode.Manual: STATE_MANUAL,
eq3.Mode.Away: STATE_AWAY} eq3.Mode.Boost: STATE_BOOST,
eq3.Mode.Away: STATE_AWAY,
}
self.reverse_modes = {v: k for k, v in self.modes.items()} self.reverse_modes = {v: k for k, v in self.modes.items()}
self._name = _name self._name = _name
self._thermostat = eq3.Thermostat(_mac) self._thermostat = eq3.Thermostat(_mac)
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property @property
def available(self) -> bool: def available(self) -> bool:
"""Return if thermostat is available.""" """Return if thermostat is available."""
@ -153,11 +161,11 @@ class EQ3BTSmartThermostat(ClimateDevice):
def device_state_attributes(self): def device_state_attributes(self):
"""Return the device specific state attributes.""" """Return the device specific state attributes."""
dev_specific = { dev_specific = {
ATTR_STATE_AWAY_END: self._thermostat.away_end,
ATTR_STATE_LOCKED: self._thermostat.locked, ATTR_STATE_LOCKED: self._thermostat.locked,
ATTR_STATE_LOW_BAT: self._thermostat.low_battery, ATTR_STATE_LOW_BAT: self._thermostat.low_battery,
ATTR_STATE_VALVE: self._thermostat.valve_state, ATTR_STATE_VALVE: self._thermostat.valve_state,
ATTR_STATE_WINDOW_OPEN: self._thermostat.window_open, ATTR_STATE_WINDOW_OPEN: self._thermostat.window_open,
ATTR_STATE_AWAY_END: self._thermostat.away_end,
} }
return dev_specific return dev_specific

View File

@ -17,7 +17,9 @@ import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_SLAVE, TEMP_CELSIUS, CONF_NAME, CONF_SLAVE, TEMP_CELSIUS,
ATTR_TEMPERATURE, DEVICE_DEFAULT_NAME) ATTR_TEMPERATURE, DEVICE_DEFAULT_NAME)
from homeassistant.components.climate import (ClimateDevice, PLATFORM_SCHEMA) from homeassistant.components.climate import (
ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_FAN_MODE)
import homeassistant.components.modbus as modbus import homeassistant.components.modbus as modbus
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -31,6 +33,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Flexit Platform.""" """Set up the Flexit Platform."""
@ -62,6 +66,11 @@ class Flexit(ClimateDevice):
self._alarm = False self._alarm = False
self.unit = pyflexit.pyflexit(modbus.HUB, modbus_slave) self.unit = pyflexit.pyflexit(modbus.HUB, modbus_slave)
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
def update(self): def update(self):
"""Update unit attributes.""" """Update unit attributes."""
if not self.unit.update(): if not self.unit.update():

View File

@ -10,17 +10,19 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.components import switch from homeassistant.core import DOMAIN as HA_DOMAIN
from homeassistant.components.climate import ( from homeassistant.components.climate import (
STATE_HEAT, STATE_COOL, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA, STATE_HEAT, STATE_COOL, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA,
STATE_AUTO) STATE_AUTO, ATTR_OPERATION_MODE, SUPPORT_OPERATION_MODE,
SUPPORT_TARGET_TEMPERATURE)
from homeassistant.const import ( from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE,
CONF_NAME) CONF_NAME, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF)
from homeassistant.helpers import condition from homeassistant.helpers import condition
from homeassistant.helpers.event import ( from homeassistant.helpers.event import (
async_track_state_change, async_track_time_interval) async_track_state_change, async_track_time_interval)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.restore_state import async_get_last_state
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -39,7 +41,8 @@ CONF_MIN_DUR = 'min_cycle_duration'
CONF_COLD_TOLERANCE = 'cold_tolerance' CONF_COLD_TOLERANCE = 'cold_tolerance'
CONF_HOT_TOLERANCE = 'hot_tolerance' CONF_HOT_TOLERANCE = 'hot_tolerance'
CONF_KEEP_ALIVE = 'keep_alive' CONF_KEEP_ALIVE = 'keep_alive'
CONF_INITIAL_OPERATION_MODE = 'initial_operation_mode'
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HEATER): cv.entity_id, vol.Required(CONF_HEATER): cv.entity_id,
@ -56,6 +59,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_TARGET_TEMP): vol.Coerce(float), vol.Optional(CONF_TARGET_TEMP): vol.Coerce(float),
vol.Optional(CONF_KEEP_ALIVE): vol.All( vol.Optional(CONF_KEEP_ALIVE): vol.All(
cv.time_period, cv.positive_timedelta), cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_INITIAL_OPERATION_MODE):
vol.In([STATE_AUTO, STATE_OFF])
}) })
@ -73,11 +78,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
cold_tolerance = config.get(CONF_COLD_TOLERANCE) cold_tolerance = config.get(CONF_COLD_TOLERANCE)
hot_tolerance = config.get(CONF_HOT_TOLERANCE) hot_tolerance = config.get(CONF_HOT_TOLERANCE)
keep_alive = config.get(CONF_KEEP_ALIVE) keep_alive = config.get(CONF_KEEP_ALIVE)
initial_operation_mode = config.get(CONF_INITIAL_OPERATION_MODE)
async_add_devices([GenericThermostat( async_add_devices([GenericThermostat(
hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp, hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp,
target_temp, ac_mode, min_cycle_duration, cold_tolerance, target_temp, ac_mode, min_cycle_duration, cold_tolerance,
hot_tolerance, keep_alive)]) hot_tolerance, keep_alive, initial_operation_mode)])
class GenericThermostat(ClimateDevice): class GenericThermostat(ClimateDevice):
@ -85,7 +91,8 @@ class GenericThermostat(ClimateDevice):
def __init__(self, hass, name, heater_entity_id, sensor_entity_id, def __init__(self, hass, name, heater_entity_id, sensor_entity_id,
min_temp, max_temp, target_temp, ac_mode, min_cycle_duration, min_temp, max_temp, target_temp, ac_mode, min_cycle_duration,
cold_tolerance, hot_tolerance, keep_alive): cold_tolerance, hot_tolerance, keep_alive,
initial_operation_mode):
"""Initialize the thermostat.""" """Initialize the thermostat."""
self.hass = hass self.hass = hass
self._name = name self._name = name
@ -95,7 +102,11 @@ class GenericThermostat(ClimateDevice):
self._cold_tolerance = cold_tolerance self._cold_tolerance = cold_tolerance
self._hot_tolerance = hot_tolerance self._hot_tolerance = hot_tolerance
self._keep_alive = keep_alive self._keep_alive = keep_alive
self._enabled = True self._initial_operation_mode = initial_operation_mode
if initial_operation_mode == STATE_OFF:
self._enabled = False
else:
self._enabled = True
self._active = False self._active = False
self._cur_temp = None self._cur_temp = None
@ -117,6 +128,23 @@ class GenericThermostat(ClimateDevice):
if sensor_state: if sensor_state:
self._async_update_temp(sensor_state) self._async_update_temp(sensor_state)
@asyncio.coroutine
def async_added_to_hass(self):
"""Run when entity about to be added."""
# Check If we have an old state
old_state = yield from async_get_last_state(self.hass,
self.entity_id)
if old_state is not None:
# If we have no initial temperature, restore
if self._target_temp is None:
self._target_temp = float(
old_state.attributes[ATTR_TEMPERATURE])
# If we have no initial operation mode, restore
if self._initial_operation_mode is None:
if old_state.attributes[ATTR_OPERATION_MODE] == STATE_OFF:
self._enabled = False
@property @property
def should_poll(self): def should_poll(self):
"""Return the polling state.""" """Return the polling state."""
@ -163,10 +191,11 @@ class GenericThermostat(ClimateDevice):
"""Set operation mode.""" """Set operation mode."""
if operation_mode == STATE_AUTO: if operation_mode == STATE_AUTO:
self._enabled = True self._enabled = True
self._async_control_heating()
elif operation_mode == STATE_OFF: elif operation_mode == STATE_OFF:
self._enabled = False self._enabled = False
if self._is_device_active: if self._is_device_active:
switch.async_turn_off(self.hass, self.heater_entity_id) self._heater_turn_off()
else: else:
_LOGGER.error('Unrecognized operation mode: %s', operation_mode) _LOGGER.error('Unrecognized operation mode: %s', operation_mode)
return return
@ -224,9 +253,9 @@ class GenericThermostat(ClimateDevice):
def _async_keep_alive(self, time): def _async_keep_alive(self, time):
"""Call at constant intervals for keep-alive purposes.""" """Call at constant intervals for keep-alive purposes."""
if self.current_operation in [STATE_COOL, STATE_HEAT]: if self.current_operation in [STATE_COOL, STATE_HEAT]:
switch.async_turn_on(self.hass, self.heater_entity_id) self._heater_turn_on()
else: else:
switch.async_turn_off(self.hass, self.heater_entity_id) self._heater_turn_off()
@callback @callback
def _async_update_temp(self, state): def _async_update_temp(self, state):
@ -272,13 +301,13 @@ class GenericThermostat(ClimateDevice):
self._cold_tolerance self._cold_tolerance
if too_cold: if too_cold:
_LOGGER.info('Turning off AC %s', self.heater_entity_id) _LOGGER.info('Turning off AC %s', self.heater_entity_id)
switch.async_turn_off(self.hass, self.heater_entity_id) self._heater_turn_off()
else: else:
too_hot = self._cur_temp - self._target_temp >= \ too_hot = self._cur_temp - self._target_temp >= \
self._hot_tolerance self._hot_tolerance
if too_hot: if too_hot:
_LOGGER.info('Turning on AC %s', self.heater_entity_id) _LOGGER.info('Turning on AC %s', self.heater_entity_id)
switch.async_turn_on(self.hass, self.heater_entity_id) self._heater_turn_on()
else: else:
is_heating = self._is_device_active is_heating = self._is_device_active
if is_heating: if is_heating:
@ -287,15 +316,34 @@ class GenericThermostat(ClimateDevice):
if too_hot: if too_hot:
_LOGGER.info('Turning off heater %s', _LOGGER.info('Turning off heater %s',
self.heater_entity_id) self.heater_entity_id)
switch.async_turn_off(self.hass, self.heater_entity_id) self._heater_turn_off()
else: else:
too_cold = self._target_temp - self._cur_temp >= \ too_cold = self._target_temp - self._cur_temp >= \
self._cold_tolerance self._cold_tolerance
if too_cold: if too_cold:
_LOGGER.info('Turning on heater %s', self.heater_entity_id) _LOGGER.info('Turning on heater %s', self.heater_entity_id)
switch.async_turn_on(self.hass, self.heater_entity_id) self._heater_turn_on()
@property @property
def _is_device_active(self): def _is_device_active(self):
"""If the toggleable device is currently active.""" """If the toggleable device is currently active."""
return switch.is_on(self.hass, self.heater_entity_id) return self.hass.states.is_state(self.heater_entity_id, STATE_ON)
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@callback
def _heater_turn_on(self):
"""Turn heater toggleable device on."""
data = {ATTR_ENTITY_ID: self.heater_entity_id}
self.hass.async_add_job(
self.hass.services.async_call(HA_DOMAIN, SERVICE_TURN_ON, data))
@callback
def _heater_turn_off(self):
"""Turn heater toggleable device off."""
data = {ATTR_ENTITY_ID: self.heater_entity_id}
self.hass.async_add_job(
self.hass.services.async_call(HA_DOMAIN, SERVICE_TURN_OFF, data))

View File

@ -8,7 +8,8 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA from homeassistant.components.climate import (
ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE)
from homeassistant.const import ( from homeassistant.const import (
TEMP_CELSIUS, ATTR_TEMPERATURE, CONF_PORT, CONF_NAME, CONF_ID) TEMP_CELSIUS, ATTR_TEMPERATURE, CONF_PORT, CONF_NAME, CONF_ID)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -68,6 +69,11 @@ class HeatmiserV3Thermostat(ClimateDevice):
self.update() self.update()
self._target_temperature = int(self.dcb.get('roomset')) self._target_temperature = int(self.dcb.get('roomset'))
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_TARGET_TEMPERATURE
@property @property
def name(self): def name(self):
"""Return the name of the thermostat, if any.""" """Return the name of the thermostat, if any."""

View File

@ -0,0 +1,139 @@
"""
Support for the Hive devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.hive/
"""
from homeassistant.components.climate import (
ClimateDevice, STATE_AUTO, STATE_HEAT, STATE_OFF, STATE_ON,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE)
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
from homeassistant.components.hive import DATA_HIVE
DEPENDENCIES = ['hive']
HIVE_TO_HASS_STATE = {'SCHEDULE': STATE_AUTO, 'MANUAL': STATE_HEAT,
'ON': STATE_ON, 'OFF': STATE_OFF}
HASS_TO_HIVE_STATE = {STATE_AUTO: 'SCHEDULE', STATE_HEAT: 'MANUAL',
STATE_ON: 'ON', STATE_OFF: 'OFF'}
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up Hive climate devices."""
if discovery_info is None:
return
session = hass.data.get(DATA_HIVE)
add_devices([HiveClimateEntity(session, discovery_info)])
class HiveClimateEntity(ClimateDevice):
"""Hive Climate Device."""
def __init__(self, hivesession, hivedevice):
"""Initialize the Climate device."""
self.node_id = hivedevice["Hive_NodeID"]
self.node_name = hivedevice["Hive_NodeName"]
self.device_type = hivedevice["HA_DeviceType"]
self.session = hivesession
self.data_updatesource = '{}.{}'.format(self.device_type,
self.node_id)
if self.device_type == "Heating":
self.modes = [STATE_AUTO, STATE_HEAT, STATE_OFF]
elif self.device_type == "HotWater":
self.modes = [STATE_AUTO, STATE_ON, STATE_OFF]
self.session.entities.append(self)
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
def handle_update(self, updatesource):
"""Handle the new update request."""
if '{}.{}'.format(self.device_type, self.node_id) not in updatesource:
self.schedule_update_ha_state()
@property
def name(self):
"""Return the name of the Climate device."""
friendly_name = "Climate Device"
if self.device_type == "Heating":
friendly_name = "Heating"
if self.node_name is not None:
friendly_name = '{} {}'.format(self.node_name, friendly_name)
elif self.device_type == "HotWater":
friendly_name = "Hot Water"
return friendly_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_type == "Heating":
return self.session.heating.current_temperature(self.node_id)
@property
def target_temperature(self):
"""Return the target temperature."""
if self.device_type == "Heating":
return self.session.heating.get_target_temperature(self.node_id)
@property
def min_temp(self):
"""Return minimum temperature."""
if self.device_type == "Heating":
return self.session.heating.min_temperature(self.node_id)
@property
def max_temp(self):
"""Return the maximum temperature."""
if self.device_type == "Heating":
return self.session.heating.max_temperature(self.node_id)
@property
def operation_list(self):
"""List of the operation modes."""
return self.modes
@property
def current_operation(self):
"""Return current mode."""
if self.device_type == "Heating":
currentmode = self.session.heating.get_mode(self.node_id)
elif self.device_type == "HotWater":
currentmode = self.session.hotwater.get_mode(self.node_id)
return HIVE_TO_HASS_STATE.get(currentmode)
def set_operation_mode(self, operation_mode):
"""Set new Heating mode."""
new_mode = HASS_TO_HIVE_STATE.get(operation_mode)
if self.device_type == "Heating":
self.session.heating.set_mode(self.node_id, new_mode)
elif self.device_type == "HotWater":
self.session.hotwater.set_mode(self.node_id, new_mode)
for entity in self.session.entities:
entity.handle_update(self.data_updatesource)
def set_temperature(self, **kwargs):
"""Set new target temperature."""
new_temperature = kwargs.get(ATTR_TEMPERATURE)
if new_temperature is not None:
if self.device_type == "Heating":
self.session.heating.set_target_temperature(self.node_id,
new_temperature)
for entity in self.session.entities:
entity.handle_update(self.data_updatesource)
def update(self):
"""Update all Node data frome Hive."""
self.session.core.update_data(self.node_id)

View File

@ -5,7 +5,9 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.homematic/ https://home-assistant.io/components/climate.homematic/
""" """
import logging import logging
from homeassistant.components.climate import ClimateDevice, STATE_AUTO from homeassistant.components.climate import (
ClimateDevice, STATE_AUTO, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_OPERATION_MODE)
from homeassistant.components.homematic import HMDevice, ATTR_DISCOVER_DEVICES from homeassistant.components.homematic import HMDevice, ATTR_DISCOVER_DEVICES
from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN, ATTR_TEMPERATURE from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN, ATTR_TEMPERATURE
@ -38,6 +40,8 @@ HM_HUMI_MAP = [
HM_CONTROL_MODE = 'CONTROL_MODE' HM_CONTROL_MODE = 'CONTROL_MODE'
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Homematic thermostat platform.""" """Set up the Homematic thermostat platform."""
@ -55,6 +59,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class HMThermostat(HMDevice, ClimateDevice): class HMThermostat(HMDevice, ClimateDevice):
"""Representation of a Homematic thermostat.""" """Representation of a Homematic thermostat."""
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property @property
def temperature_unit(self): def temperature_unit(self):
"""Return the unit of measurement that is used.""" """Return the unit of measurement that is used."""

View File

@ -14,12 +14,13 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ClimateDevice, PLATFORM_SCHEMA, ATTR_FAN_MODE, ATTR_FAN_LIST, ClimateDevice, PLATFORM_SCHEMA, ATTR_FAN_MODE, ATTR_FAN_LIST,
ATTR_OPERATION_MODE, ATTR_OPERATION_LIST) ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_AWAY_MODE, SUPPORT_OPERATION_MODE)
from homeassistant.const import ( from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT,
ATTR_TEMPERATURE, CONF_REGION) ATTR_TEMPERATURE, CONF_REGION)
REQUIREMENTS = ['evohomeclient==0.2.5', 'somecomfort==0.4.1'] REQUIREMENTS = ['evohomeclient==0.2.5', 'somecomfort==0.5.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -126,6 +127,14 @@ class RoundThermostat(ClimateDevice):
self._away_temp = away_temp self._away_temp = away_temp
self._away = False self._away = False
@property
def supported_features(self):
"""Return the list of supported features."""
supported = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE)
if hasattr(self.client, ATTR_SYSTEM_MODE):
supported |= SUPPORT_OPERATION_MODE
return supported
@property @property
def name(self): def name(self):
"""Return the name of the honeywell, if any.""" """Return the name of the honeywell, if any."""
@ -234,6 +243,14 @@ class HoneywellUSThermostat(ClimateDevice):
self._username = username self._username = username
self._password = password self._password = password
@property
def supported_features(self):
"""Return the list of supported features."""
supported = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE)
if hasattr(self._device, ATTR_SYSTEM_MODE):
supported |= SUPPORT_OPERATION_MODE
return supported
@property @property
def is_fan_on(self): def is_fan_on(self):
"""Return true if fan is on.""" """Return true if fan is on."""

View File

@ -8,7 +8,9 @@ import asyncio
import voluptuous as vol import voluptuous as vol
from homeassistant.components.knx import DATA_KNX, ATTR_DISCOVER_DEVICES from homeassistant.components.knx import DATA_KNX, ATTR_DISCOVER_DEVICES
from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate import (
PLATFORM_SCHEMA, ClimateDevice, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_OPERATION_MODE)
from homeassistant.const import CONF_NAME, TEMP_CELSIUS, ATTR_TEMPERATURE from homeassistant.const import CONF_NAME, TEMP_CELSIUS, ATTR_TEMPERATURE
from homeassistant.core import callback from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -135,6 +137,14 @@ class KNXClimate(ClimateDevice):
self._unit_of_measurement = TEMP_CELSIUS self._unit_of_measurement = TEMP_CELSIUS
@property
def supported_features(self):
"""Return the list of supported features."""
support = SUPPORT_TARGET_TEMPERATURE
if self.device.supports_operation_mode:
support |= SUPPORT_OPERATION_MODE
return support
def async_register_callbacks(self): def async_register_callbacks(self):
"""Register callbacks to update hass after device was changed.""" """Register callbacks to update hass after device was changed."""
@asyncio.coroutine @asyncio.coroutine

View File

@ -7,7 +7,9 @@ https://home-assistant.io/components/maxcube/
import socket import socket
import logging import logging
from homeassistant.components.climate import ClimateDevice, STATE_AUTO from homeassistant.components.climate import (
ClimateDevice, STATE_AUTO, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_OPERATION_MODE)
from homeassistant.components.maxcube import MAXCUBE_HANDLE from homeassistant.components.maxcube import MAXCUBE_HANDLE
from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE
@ -17,6 +19,8 @@ STATE_MANUAL = 'manual'
STATE_BOOST = 'boost' STATE_BOOST = 'boost'
STATE_VACATION = 'vacation' STATE_VACATION = 'vacation'
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Iterate through all MAX! Devices and add thermostats.""" """Iterate through all MAX! Devices and add thermostats."""
@ -47,6 +51,11 @@ class MaxCubeClimate(ClimateDevice):
self._rf_address = rf_address self._rf_address = rf_address
self._cubehandle = hass.data[MAXCUBE_HANDLE] self._cubehandle = hass.data[MAXCUBE_HANDLE]
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property @property
def should_poll(self): def should_poll(self):
"""Return the polling state.""" """Return the polling state."""

View File

@ -15,7 +15,9 @@ import homeassistant.components.mqtt as mqtt
from homeassistant.components.climate import ( from homeassistant.components.climate import (
STATE_HEAT, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, ClimateDevice, STATE_HEAT, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, ClimateDevice,
PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, STATE_AUTO, PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, STATE_AUTO,
ATTR_OPERATION_MODE) ATTR_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE,
SUPPORT_SWING_MODE, SUPPORT_FAN_MODE, SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE,
SUPPORT_AUX_HEAT)
from homeassistant.const import ( from homeassistant.const import (
STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME) STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME)
from homeassistant.components.mqtt import (CONF_QOS, CONF_RETAIN, from homeassistant.components.mqtt import (CONF_QOS, CONF_RETAIN,
@ -483,3 +485,38 @@ class MqttClimate(ClimateDevice):
if self._topic[CONF_AUX_STATE_TOPIC] is None: if self._topic[CONF_AUX_STATE_TOPIC] is None:
self._aux = False self._aux = False
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
@property
def supported_features(self):
"""Return the list of supported features."""
support = 0
if (self._topic[CONF_TEMPERATURE_STATE_TOPIC] is not None) or \
(self._topic[CONF_TEMPERATURE_COMMAND_TOPIC] is not None):
support |= SUPPORT_TARGET_TEMPERATURE
if (self._topic[CONF_MODE_COMMAND_TOPIC] is not None) or \
(self._topic[CONF_MODE_STATE_TOPIC] is not None):
support |= SUPPORT_OPERATION_MODE
if (self._topic[CONF_FAN_MODE_STATE_TOPIC] is not None) or \
(self._topic[CONF_FAN_MODE_COMMAND_TOPIC] is not None):
support |= SUPPORT_FAN_MODE
if (self._topic[CONF_SWING_MODE_STATE_TOPIC] is not None) or \
(self._topic[CONF_SWING_MODE_COMMAND_TOPIC] is not None):
support |= SUPPORT_SWING_MODE
if (self._topic[CONF_AWAY_MODE_STATE_TOPIC] is not None) or \
(self._topic[CONF_AWAY_MODE_COMMAND_TOPIC] is not None):
support |= SUPPORT_AWAY_MODE
if (self._topic[CONF_HOLD_STATE_TOPIC] is not None) or \
(self._topic[CONF_HOLD_COMMAND_TOPIC] is not None):
support |= SUPPORT_HOLD_MODE
if (self._topic[CONF_AUX_STATE_TOPIC] is not None) or \
(self._topic[CONF_AUX_COMMAND_TOPIC] is not None):
support |= SUPPORT_AUX_HEAT
return support

View File

@ -7,7 +7,9 @@ https://home-assistant.io/components/climate.mysensors/
from homeassistant.components import mysensors from homeassistant.components import mysensors
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DOMAIN, STATE_AUTO, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DOMAIN, STATE_AUTO,
STATE_COOL, STATE_HEAT, STATE_OFF, ClimateDevice) STATE_COOL, STATE_HEAT, STATE_OFF, ClimateDevice,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_HIGH,
SUPPORT_TARGET_TEMPERATURE_LOW, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE)
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
DICT_HA_TO_MYS = { DICT_HA_TO_MYS = {
@ -23,6 +25,10 @@ DICT_MYS_TO_HA = {
'Off': STATE_OFF, 'Off': STATE_OFF,
} }
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_HIGH |
SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_FAN_MODE |
SUPPORT_OPERATION_MODE)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the mysensors climate.""" """Setup the mysensors climate."""
@ -33,6 +39,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice): class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
"""Representation of a MySensors HVAC.""" """Representation of a MySensors HVAC."""
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property @property
def assumed_state(self): def assumed_state(self):
"""Return True if unable to access real state of entity.""" """Return True if unable to access real state of entity."""

View File

@ -12,7 +12,9 @@ from homeassistant.components.nest import DATA_NEST
from homeassistant.components.climate import ( from homeassistant.components.climate import (
STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice, STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice,
PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
ATTR_TEMPERATURE) ATTR_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW,
SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE, SUPPORT_FAN_MODE)
from homeassistant.const import ( from homeassistant.const import (
TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_CELSIUS, TEMP_FAHRENHEIT,
CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF, STATE_UNKNOWN) CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF, STATE_UNKNOWN)
@ -28,6 +30,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
STATE_ECO = 'eco' STATE_ECO = 'eco'
STATE_HEAT_COOL = 'heat-cool' STATE_HEAT_COOL = 'heat-cool'
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_HIGH |
SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_OPERATION_MODE |
SUPPORT_AWAY_MODE | SUPPORT_FAN_MODE)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Nest thermostat.""" """Set up the Nest thermostat."""
@ -87,6 +93,11 @@ class NestThermostat(ClimateDevice):
self._min_temperature = None self._min_temperature = None
self._max_temperature = None self._max_temperature = None
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property @property
def name(self): def name(self):
"""Return the name of the nest, if any.""" """Return the name of the nest, if any."""

View File

@ -10,7 +10,8 @@ import voluptuous as vol
from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE
from homeassistant.components.climate import ( from homeassistant.components.climate import (
STATE_HEAT, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA) STATE_HEAT, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE)
from homeassistant.util import Throttle from homeassistant.util import Throttle
from homeassistant.loader import get_component from homeassistant.loader import get_component
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -35,6 +36,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.All(cv.ensure_list, [cv.string]), vol.All(cv.ensure_list, [cv.string]),
}) })
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
SUPPORT_AWAY_MODE)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the NetAtmo Thermostat.""" """Set up the NetAtmo Thermostat."""
@ -65,6 +69,11 @@ class NetatmoThermostat(ClimateDevice):
self._target_temperature = None self._target_temperature = None
self._away = None self._away = None
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property @property
def name(self): def name(self):
"""Return the name of the sensor.""" """Return the name of the sensor."""

View File

@ -14,7 +14,8 @@ import voluptuous as vol
# Import the device class from the component that you want to support # Import the device class from the component that you want to support
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ClimateDevice, PLATFORM_SCHEMA, STATE_HEAT, STATE_IDLE, ATTR_TEMPERATURE) ClimateDevice, PLATFORM_SCHEMA, STATE_HEAT, STATE_IDLE, ATTR_TEMPERATURE,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_AWAY_MODE)
from homeassistant.const import (CONF_HOST, CONF_USERNAME, CONF_PASSWORD, from homeassistant.const import (CONF_HOST, CONF_USERNAME, CONF_PASSWORD,
CONF_PORT, TEMP_CELSIUS, CONF_NAME) CONF_PORT, TEMP_CELSIUS, CONF_NAME)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -34,6 +35,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_AWAY_TEMP, default=14): vol.Coerce(float) vol.Optional(CONF_AWAY_TEMP, default=14): vol.Coerce(float)
}) })
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the oemthermostat platform.""" """Set up the oemthermostat platform."""
@ -77,6 +80,11 @@ class ThermostatDevice(ClimateDevice):
self._temperature = None self._temperature = None
self._setpoint = None self._setpoint = None
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property @property
def name(self): def name(self):
"""Return the name of this Thermostat.""" """Return the name of this Thermostat."""

View File

@ -8,7 +8,7 @@ import voluptuous as vol
from homeassistant.components.climate import ( from homeassistant.components.climate import (
PRECISION_TENTHS, STATE_COOL, STATE_HEAT, STATE_IDLE, PRECISION_TENTHS, STATE_COOL, STATE_HEAT, STATE_IDLE,
ClimateDevice, PLATFORM_SCHEMA) ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE)
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) CONF_HOST, CONF_PASSWORD, CONF_USERNAME, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -46,6 +46,11 @@ class ProliphixThermostat(ClimateDevice):
self._pdp.update() self._pdp.update()
self._name = self._pdp.name self._name = self._pdp.name
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_TARGET_TEMPERATURE
@property @property
def should_poll(self): def should_poll(self):
"""Set up polling needed for thermostat.""" """Set up polling needed for thermostat."""

View File

@ -4,15 +4,18 @@ Support for Radio Thermostat wifi-enabled home thermostats.
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.radiotherm/ https://home-assistant.io/components/climate.radiotherm/
""" """
import asyncio
import datetime import datetime
import logging import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.climate import ( from homeassistant.components.climate import (
STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_IDLE, STATE_OFF, STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_IDLE, STATE_ON, STATE_OFF,
ClimateDevice, PLATFORM_SCHEMA) ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE,
from homeassistant.const import CONF_HOST, TEMP_FAHRENHEIT, ATTR_TEMPERATURE SUPPORT_OPERATION_MODE, SUPPORT_FAN_MODE, SUPPORT_AWAY_MODE)
from homeassistant.const import (
CONF_HOST, TEMP_FAHRENHEIT, ATTR_TEMPERATURE, PRECISION_HALVES)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['radiotherm==1.3'] REQUIREMENTS = ['radiotherm==1.3']
@ -29,15 +32,56 @@ CONF_AWAY_TEMPERATURE_COOL = 'away_temperature_cool'
DEFAULT_AWAY_TEMPERATURE_HEAT = 60 DEFAULT_AWAY_TEMPERATURE_HEAT = 60
DEFAULT_AWAY_TEMPERATURE_COOL = 85 DEFAULT_AWAY_TEMPERATURE_COOL = 85
STATE_CIRCULATE = "circulate"
OPERATION_LIST = [STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_OFF]
CT30_FAN_OPERATION_LIST = [STATE_ON, STATE_AUTO]
CT80_FAN_OPERATION_LIST = [STATE_ON, STATE_CIRCULATE, STATE_AUTO]
# Mappings from radiotherm json data codes to and from HASS state
# flags. CODE is the thermostat integer code and these map to and
# from HASS state flags.
# Programmed temperature mode of the thermostat.
CODE_TO_TEMP_MODE = {0: STATE_OFF, 1: STATE_HEAT, 2: STATE_COOL, 3: STATE_AUTO}
TEMP_MODE_TO_CODE = {v: k for k, v in CODE_TO_TEMP_MODE.items()}
# Programmed fan mode (circulate is supported by CT80 models)
CODE_TO_FAN_MODE = {0: STATE_AUTO, 1: STATE_CIRCULATE, 2: STATE_ON}
FAN_MODE_TO_CODE = {v: k for k, v in CODE_TO_FAN_MODE.items()}
# Active thermostat state (is it heating or cooling?). In the future
# this should probably made into heat and cool binary sensors.
CODE_TO_TEMP_STATE = {0: STATE_IDLE, 1: STATE_HEAT, 2: STATE_COOL}
# Active fan state. This is if the fan is actually on or not. In the
# future this should probably made into a binary sensor for the fan.
CODE_TO_FAN_STATE = {0: STATE_OFF, 1: STATE_ON}
def round_temp(temperature):
"""Round a temperature to the resolution of the thermostat.
RadioThermostats can handle 0.5 degree temps so the input
temperature is rounded to that value and returned.
"""
return round(temperature * 2.0) / 2.0
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_HOST): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_HOLD_TEMP, default=False): cv.boolean, vol.Optional(CONF_HOLD_TEMP, default=False): cv.boolean,
vol.Optional(CONF_AWAY_TEMPERATURE_HEAT, vol.Optional(CONF_AWAY_TEMPERATURE_HEAT,
default=DEFAULT_AWAY_TEMPERATURE_HEAT): vol.Coerce(float), default=DEFAULT_AWAY_TEMPERATURE_HEAT):
vol.All(vol.Coerce(float), round_temp),
vol.Optional(CONF_AWAY_TEMPERATURE_COOL, vol.Optional(CONF_AWAY_TEMPERATURE_COOL,
default=DEFAULT_AWAY_TEMPERATURE_COOL): vol.Coerce(float), default=DEFAULT_AWAY_TEMPERATURE_COOL):
vol.All(vol.Coerce(float), round_temp),
}) })
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
SUPPORT_FAN_MODE | SUPPORT_AWAY_MODE)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Radio Thermostat.""" """Set up the Radio Thermostat."""
@ -77,19 +121,39 @@ class RadioThermostat(ClimateDevice):
def __init__(self, device, hold_temp, away_temps): def __init__(self, device, hold_temp, away_temps):
"""Initialize the thermostat.""" """Initialize the thermostat."""
self.device = device self.device = device
self.set_time()
self._target_temperature = None self._target_temperature = None
self._current_temperature = None self._current_temperature = None
self._current_operation = STATE_IDLE self._current_operation = STATE_IDLE
self._name = None self._name = None
self._fmode = None self._fmode = None
self._fstate = None
self._tmode = None self._tmode = None
self._tstate = None self._tstate = None
self._hold_temp = hold_temp self._hold_temp = hold_temp
self._hold_set = False
self._away = False self._away = False
self._away_temps = away_temps self._away_temps = away_temps
self._prev_temp = None self._prev_temp = None
self._operation_list = [STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_OFF]
# Fan circulate mode is only supported by the CT80 models.
import radiotherm
self._is_model_ct80 = isinstance(self.device,
radiotherm.thermostat.CT80)
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@asyncio.coroutine
def async_added_to_hass(self):
"""Register callbacks."""
# Set the time on the device. This shouldn't be in the
# constructor because it's a network call. We can't put it in
# update() because calling it will clear any temporary mode or
# temperature in the thermostat. So add it as a future job
# for the event loop to run.
self.hass.async_add_job(self.set_time)
@property @property
def name(self): def name(self):
@ -101,6 +165,11 @@ class RadioThermostat(ClimateDevice):
"""Return the unit of measurement.""" """Return the unit of measurement."""
return TEMP_FAHRENHEIT return TEMP_FAHRENHEIT
@property
def precision(self):
"""Return the precision of the system."""
return PRECISION_HALVES
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the device specific state attributes.""" """Return the device specific state attributes."""
@ -109,6 +178,25 @@ class RadioThermostat(ClimateDevice):
ATTR_MODE: self._tmode, ATTR_MODE: self._tmode,
} }
@property
def fan_list(self):
"""List of available fan modes."""
if self._is_model_ct80:
return CT80_FAN_OPERATION_LIST
else:
return CT30_FAN_OPERATION_LIST
@property
def current_fan_mode(self):
"""Return whether the fan is on."""
return self._fmode
def set_fan_mode(self, fan):
"""Turn fan on/off."""
code = FAN_MODE_TO_CODE.get(fan, None)
if code is not None:
self.device.fmode = code
@property @property
def current_temperature(self): def current_temperature(self):
"""Return the current temperature.""" """Return the current temperature."""
@ -122,7 +210,7 @@ class RadioThermostat(ClimateDevice):
@property @property
def operation_list(self): def operation_list(self):
"""Return the operation modes list.""" """Return the operation modes list."""
return self._operation_list return OPERATION_LIST
@property @property
def target_temperature(self): def target_temperature(self):
@ -136,53 +224,48 @@ class RadioThermostat(ClimateDevice):
def update(self): def update(self):
"""Update and validate the data from the thermostat.""" """Update and validate the data from the thermostat."""
current_temp = self.device.temp['raw'] # Radio thermostats are very slow, and sometimes don't respond
if current_temp == -1: # very quickly. So we need to keep the number of calls to them
_LOGGER.error("Couldn't get valid temperature reading") # to a bare minimum or we'll hit the HASS 10 sec warning. We
return # have to make one call to /tstat to get temps but we'll try and
self._current_temperature = current_temp # keep the other calls to a minimum. Even with this, these
self._name = self.device.name['raw'] # thermostats tend to time out sometimes when they're actively
try: # heating or cooling.
self._fmode = self.device.fmode['human']
except AttributeError:
_LOGGER.error("Couldn't get valid fan mode reading")
try:
self._tmode = self.device.tmode['human']
except AttributeError:
_LOGGER.error("Couldn't get valid thermostat mode reading")
try:
self._tstate = self.device.tstate['human']
except AttributeError:
_LOGGER.error("Couldn't get valid thermostat state reading")
if self._tmode == 'Cool': # First time - get the name from the thermostat. This is
target_temp = self.device.t_cool['raw'] # normally set in the radio thermostat web app.
if target_temp == -1: if self._name is None:
_LOGGER.error("Couldn't get target reading") self._name = self.device.name['raw']
return
self._target_temperature = target_temp # Request the current state from the thermostat.
self._current_operation = STATE_COOL data = self.device.tstat['raw']
elif self._tmode == 'Heat':
target_temp = self.device.t_heat['raw'] current_temp = data['temp']
if target_temp == -1: if current_temp == -1:
_LOGGER.error("Couldn't get valid target reading") _LOGGER.error('%s (%s) was busy (temp == -1)', self._name,
return self.device.host)
self._target_temperature = target_temp return
self._current_operation = STATE_HEAT
elif self._tmode == 'Auto': # Map thermostat values into various STATE_ flags.
if self._tstate == 'Cool': self._current_temperature = current_temp
target_temp = self.device.t_cool['raw'] self._fmode = CODE_TO_FAN_MODE[data['fmode']]
if target_temp == -1: self._fstate = CODE_TO_FAN_STATE[data['fstate']]
_LOGGER.error("Couldn't get valid target reading") self._tmode = CODE_TO_TEMP_MODE[data['tmode']]
return self._tstate = CODE_TO_TEMP_STATE[data['tstate']]
self._target_temperature = target_temp
elif self._tstate == 'Heat': self._current_operation = self._tmode
target_temp = self.device.t_heat['raw'] if self._tmode == STATE_COOL:
if target_temp == -1: self._target_temperature = data['t_cool']
_LOGGER.error("Couldn't get valid target reading") elif self._tmode == STATE_HEAT:
return self._target_temperature = data['t_heat']
self._target_temperature = target_temp elif self._tmode == STATE_AUTO:
self._current_operation = STATE_AUTO # This doesn't really work - tstate is only set if the HVAC is
# active. If it's idle, we don't know what to do with the target
# temperature.
if self._tstate == STATE_COOL:
self._target_temperature = data['t_cool']
elif self._tstate == STATE_HEAT:
self._target_temperature = data['t_heat']
else: else:
self._current_operation = STATE_IDLE self._current_operation = STATE_IDLE
@ -191,23 +274,32 @@ class RadioThermostat(ClimateDevice):
temperature = kwargs.get(ATTR_TEMPERATURE) temperature = kwargs.get(ATTR_TEMPERATURE)
if temperature is None: if temperature is None:
return return
if self._current_operation == STATE_COOL:
self.device.t_cool = round(temperature * 2.0) / 2.0
elif self._current_operation == STATE_HEAT:
self.device.t_heat = round(temperature * 2.0) / 2.0
elif self._current_operation == STATE_AUTO:
if self._tstate == 'Cool':
self.device.t_cool = round(temperature * 2.0) / 2.0
elif self._tstate == 'Heat':
self.device.t_heat = round(temperature * 2.0) / 2.0
if self._hold_temp or self._away: temperature = round_temp(temperature)
self.device.hold = 1
else: if self._current_operation == STATE_COOL:
self.device.hold = 0 self.device.t_cool = temperature
elif self._current_operation == STATE_HEAT:
self.device.t_heat = temperature
elif self._current_operation == STATE_AUTO:
if self._tstate == STATE_COOL:
self.device.t_cool = temperature
elif self._tstate == STATE_HEAT:
self.device.t_heat = temperature
# Only change the hold if requested or if hold mode was turned
# on and we haven't set it yet.
if kwargs.get('hold_changed', False) or not self._hold_set:
if self._hold_temp or self._away:
self.device.hold = 1
self._hold_set = True
else:
self.device.hold = 0
def set_time(self): def set_time(self):
"""Set device time.""" """Set device time."""
# Calling this clears any local temperature override and
# reverts to the scheduled temperature.
now = datetime.datetime.now() now = datetime.datetime.now()
self.device.time = { self.device.time = {
'day': now.weekday(), 'day': now.weekday(),
@ -217,14 +309,14 @@ class RadioThermostat(ClimateDevice):
def set_operation_mode(self, operation_mode): def set_operation_mode(self, operation_mode):
"""Set operation mode (auto, cool, heat, off).""" """Set operation mode (auto, cool, heat, off)."""
if operation_mode == STATE_OFF: if operation_mode == STATE_OFF or operation_mode == STATE_AUTO:
self.device.tmode = 0 self.device.tmode = TEMP_MODE_TO_CODE[operation_mode]
elif operation_mode == STATE_AUTO:
self.device.tmode = 3 # Setting t_cool or t_heat automatically changes tmode.
elif operation_mode == STATE_COOL: elif operation_mode == STATE_COOL:
self.device.t_cool = round(self._target_temperature * 2.0) / 2.0 self.device.t_cool = self._target_temperature
elif operation_mode == STATE_HEAT: elif operation_mode == STATE_HEAT:
self.device.t_heat = round(self._target_temperature * 2.0) / 2.0 self.device.t_heat = self._target_temperature
def turn_away_mode_on(self): def turn_away_mode_on(self):
"""Turn away on. """Turn away on.
@ -238,10 +330,11 @@ class RadioThermostat(ClimateDevice):
away_temp = self._away_temps[0] away_temp = self._away_temps[0]
elif self._current_operation == STATE_COOL: elif self._current_operation == STATE_COOL:
away_temp = self._away_temps[1] away_temp = self._away_temps[1]
self._away = True self._away = True
self.set_temperature(temperature=away_temp) self.set_temperature(temperature=away_temp, hold_changed=True)
def turn_away_mode_off(self): def turn_away_mode_off(self):
"""Turn away off.""" """Turn away off."""
self._away = False self._away = False
self.set_temperature(temperature=self._prev_temp) self.set_temperature(temperature=self._prev_temp, hold_changed=True)

View File

@ -15,7 +15,10 @@ import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
ATTR_TEMPERATURE, CONF_API_KEY, CONF_ID, TEMP_CELSIUS, TEMP_FAHRENHEIT) ATTR_TEMPERATURE, CONF_API_KEY, CONF_ID, TEMP_CELSIUS, TEMP_FAHRENHEIT)
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ATTR_CURRENT_HUMIDITY, ClimateDevice, PLATFORM_SCHEMA) ATTR_CURRENT_HUMIDITY, ClimateDevice, PLATFORM_SCHEMA,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE,
SUPPORT_FAN_MODE, SUPPORT_AWAY_MODE, SUPPORT_SWING_MODE,
SUPPORT_AUX_HEAT)
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
@ -35,9 +38,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
_FETCH_FIELDS = ','.join([ _FETCH_FIELDS = ','.join([
'room{name}', 'measurements', 'remoteCapabilities', 'room{name}', 'measurements', 'remoteCapabilities',
'acState', 'connectionStatus{isAlive}']) 'acState', 'connectionStatus{isAlive}', 'temperatureUnit'])
_INITIAL_FETCH_FIELDS = 'id,' + _FETCH_FIELDS _INITIAL_FETCH_FIELDS = 'id,' + _FETCH_FIELDS
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
SUPPORT_FAN_MODE | SUPPORT_AWAY_MODE | SUPPORT_SWING_MODE |
SUPPORT_AUX_HEAT)
@asyncio.coroutine @asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
@ -55,7 +62,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
devices.append(SensiboClimate(client, dev)) devices.append(SensiboClimate(client, dev))
except (aiohttp.client_exceptions.ClientConnectorError, except (aiohttp.client_exceptions.ClientConnectorError,
asyncio.TimeoutError): asyncio.TimeoutError):
_LOGGER.exception('Failed to connct to Sensibo servers.') _LOGGER.exception('Failed to connect to Sensibo servers.')
raise PlatformNotReady raise PlatformNotReady
if devices: if devices:
@ -63,7 +70,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
class SensiboClimate(ClimateDevice): class SensiboClimate(ClimateDevice):
"""Representation os a Sensibo device.""" """Representation of a Sensibo device."""
def __init__(self, client, data): def __init__(self, client, data):
"""Build SensiboClimate. """Build SensiboClimate.
@ -75,6 +82,11 @@ class SensiboClimate(ClimateDevice):
self._id = data['id'] self._id = data['id']
self._do_update(data) self._do_update(data)
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
def _do_update(self, data): def _do_update(self, data):
self._name = data['room']['name'] self._name = data['room']['name']
self._measurements = data['measurements'] self._measurements = data['measurements']
@ -84,11 +96,16 @@ class SensiboClimate(ClimateDevice):
self._operations = sorted(capabilities['modes'].keys()) self._operations = sorted(capabilities['modes'].keys())
self._current_capabilities = capabilities[ self._current_capabilities = capabilities[
'modes'][self.current_operation] 'modes'][self.current_operation]
temperature_unit_key = self._ac_states['temperatureUnit'] temperature_unit_key = data.get('temperatureUnit') or \
self._temperature_unit = \ self._ac_states.get('temperatureUnit')
TEMP_CELSIUS if temperature_unit_key == 'C' else TEMP_FAHRENHEIT if temperature_unit_key:
self._temperatures_list = self._current_capabilities[ self._temperature_unit = TEMP_CELSIUS if \
'temperatures'][temperature_unit_key]['values'] temperature_unit_key == 'C' else TEMP_FAHRENHEIT
self._temperatures_list = self._current_capabilities[
'temperatures'].get(temperature_unit_key, {}).get('values', [])
else:
self._temperature_unit = self.unit_of_measurement
self._temperatures_list = []
@property @property
def device_state_attributes(self): def device_state_attributes(self):
@ -108,7 +125,7 @@ class SensiboClimate(ClimateDevice):
@property @property
def target_temperature(self): def target_temperature(self):
"""Return the temperature we try to reach.""" """Return the temperature we try to reach."""
return self._ac_states['targetTemperature'] return self._ac_states.get('targetTemperature')
@property @property
def target_temperature_step(self): def target_temperature_step(self):
@ -133,10 +150,8 @@ class SensiboClimate(ClimateDevice):
@property @property
def current_temperature(self): def current_temperature(self):
"""Return the current temperature.""" """Return the current temperature."""
# This field is not affected by temperature_unit. # This field is not affected by temperatureUnit.
# It is always in C / nativeTemperatureUnit # It is always in C
if 'nativeTemperatureUnit' not in self._ac_states:
return self._measurements['temperature']
return convert_temperature( return convert_temperature(
self._measurements['temperature'], self._measurements['temperature'],
TEMP_CELSIUS, TEMP_CELSIUS,
@ -180,12 +195,14 @@ class SensiboClimate(ClimateDevice):
@property @property
def min_temp(self): def min_temp(self):
"""Return the minimum temperature.""" """Return the minimum temperature."""
return self._temperatures_list[0] return self._temperatures_list[0] \
if len(self._temperatures_list) else super.min_temp()
@property @property
def max_temp(self): def max_temp(self):
"""Return the maximum temperature.""" """Return the maximum temperature."""
return self._temperatures_list[-1] return self._temperatures_list[-1] \
if len(self._temperatures_list) else super.max_temp()
@asyncio.coroutine @asyncio.coroutine
def async_set_temperature(self, **kwargs): def async_set_temperature(self, **kwargs):

View File

@ -7,7 +7,8 @@ https://home-assistant.io/components/climate.tado/
import logging import logging
from homeassistant.const import TEMP_CELSIUS from homeassistant.const import TEMP_CELSIUS
from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate import (
ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE)
from homeassistant.const import ATTR_TEMPERATURE from homeassistant.const import ATTR_TEMPERATURE
from homeassistant.components.tado import DATA_TADO from homeassistant.components.tado import DATA_TADO
@ -43,6 +44,8 @@ OPERATION_LIST = {
CONST_MODE_OFF: 'Off', CONST_MODE_OFF: 'Off',
} }
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Tado climate platform.""" """Set up the Tado climate platform."""
@ -56,8 +59,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
climate_devices = [] climate_devices = []
for zone in zones: for zone in zones:
climate_devices.append(create_climate_device( device = create_climate_device(
tado, hass, zone, zone['name'], zone['id'])) tado, hass, zone, zone['name'], zone['id'])
if not device:
continue
climate_devices.append(device)
if climate_devices: if climate_devices:
add_devices(climate_devices, True) add_devices(climate_devices, True)
@ -72,8 +78,11 @@ def create_climate_device(tado, hass, zone, name, zone_id):
if ac_mode: if ac_mode:
temperatures = capabilities['HEAT']['temperatures'] temperatures = capabilities['HEAT']['temperatures']
else: elif 'temperatures' in capabilities:
temperatures = capabilities['temperatures'] temperatures = capabilities['temperatures']
else:
_LOGGER.debug("Received zone %s has no temperature; not adding", name)
return
min_temp = float(temperatures['celsius']['min']) min_temp = float(temperatures['celsius']['min'])
max_temp = float(temperatures['celsius']['max']) max_temp = float(temperatures['celsius']['max'])
@ -127,6 +136,11 @@ class TadoClimate(ClimateDevice):
self._current_operation = CONST_MODE_SMART_SCHEDULE self._current_operation = CONST_MODE_SMART_SCHEDULE
self._overlay_mode = CONST_MODE_SMART_SCHEDULE self._overlay_mode = CONST_MODE_SMART_SCHEDULE
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property @property
def name(self): def name(self):
"""Return the name of the device.""" """Return the name of the device."""

View File

@ -7,7 +7,9 @@ https://home-assistant.io/components/climate.tesla/
import logging import logging
from homeassistant.const import STATE_ON, STATE_OFF from homeassistant.const import STATE_ON, STATE_OFF
from homeassistant.components.climate import ClimateDevice, ENTITY_ID_FORMAT from homeassistant.components.climate import (
ClimateDevice, ENTITY_ID_FORMAT, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_OPERATION_MODE)
from homeassistant.components.tesla import DOMAIN as TESLA_DOMAIN, TeslaDevice from homeassistant.components.tesla import DOMAIN as TESLA_DOMAIN, TeslaDevice
from homeassistant.const import ( from homeassistant.const import (
TEMP_FAHRENHEIT, TEMP_CELSIUS, ATTR_TEMPERATURE) TEMP_FAHRENHEIT, TEMP_CELSIUS, ATTR_TEMPERATURE)
@ -18,6 +20,8 @@ DEPENDENCIES = ['tesla']
OPERATION_LIST = [STATE_ON, STATE_OFF] OPERATION_LIST = [STATE_ON, STATE_OFF]
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Tesla climate platform.""" """Set up the Tesla climate platform."""
@ -36,6 +40,11 @@ class TeslaThermostat(TeslaDevice, ClimateDevice):
self._target_temperature = None self._target_temperature = None
self._temperature = None self._temperature = None
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property @property
def current_operation(self): def current_operation(self):
"""Return current operation ie. On or Off.""" """Return current operation ie. On or Off."""

View File

@ -10,9 +10,11 @@ https://home-assistant.io/components/climate.toon/
import homeassistant.components.toon as toon_main import homeassistant.components.toon as toon_main
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ClimateDevice, ATTR_TEMPERATURE, STATE_PERFORMANCE, STATE_HEAT, STATE_ECO, ClimateDevice, ATTR_TEMPERATURE, STATE_PERFORMANCE, STATE_HEAT, STATE_ECO,
STATE_COOL) STATE_COOL, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE)
from homeassistant.const import TEMP_CELSIUS from homeassistant.const import TEMP_CELSIUS
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Toon thermostat.""" """Set up the Toon thermostat."""
@ -38,6 +40,11 @@ class ThermostatDevice(ClimateDevice):
STATE_COOL, STATE_COOL,
] ]
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property @property
def name(self): def name(self):
"""Name of this Thermostat.""" """Name of this Thermostat."""

View File

@ -7,7 +7,9 @@ https://home-assistant.io/components/switch.vera/
import logging import logging
from homeassistant.util import convert from homeassistant.util import convert
from homeassistant.components.climate import ClimateDevice, ENTITY_ID_FORMAT from homeassistant.components.climate import (
ClimateDevice, ENTITY_ID_FORMAT, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_OPERATION_MODE, SUPPORT_FAN_MODE)
from homeassistant.const import ( from homeassistant.const import (
TEMP_FAHRENHEIT, TEMP_FAHRENHEIT,
TEMP_CELSIUS, TEMP_CELSIUS,
@ -23,6 +25,9 @@ _LOGGER = logging.getLogger(__name__)
OPERATION_LIST = ['Heat', 'Cool', 'Auto Changeover', 'Off'] OPERATION_LIST = ['Heat', 'Cool', 'Auto Changeover', 'Off']
FAN_OPERATION_LIST = ['On', 'Auto', 'Cycle'] FAN_OPERATION_LIST = ['On', 'Auto', 'Cycle']
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
SUPPORT_FAN_MODE)
def setup_platform(hass, config, add_devices_callback, discovery_info=None): def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Set up of Vera thermostats.""" """Set up of Vera thermostats."""
@ -39,6 +44,11 @@ class VeraThermostat(VeraDevice, ClimateDevice):
VeraDevice.__init__(self, vera_device, controller) VeraDevice.__init__(self, vera_device, controller)
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property @property
def current_operation(self): def current_operation(self):
"""Return current operation ie. heat, cool, idle.""" """Return current operation ie. heat, cool, idle."""

View File

@ -4,46 +4,65 @@ Support for Wink thermostats, Air Conditioners, and Water Heaters.
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/
""" """
import logging
import asyncio import asyncio
import logging
from homeassistant.components.wink import WinkDevice, DOMAIN
from homeassistant.components.climate import ( from homeassistant.components.climate import (
STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice, STATE_ECO, STATE_GAS, STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_ELECTRIC,
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, STATE_FAN_ONLY, STATE_HEAT_PUMP, ATTR_TEMPERATURE, STATE_HIGH_DEMAND,
ATTR_TEMPERATURE, STATE_FAN_ONLY, STATE_PERFORMANCE, ATTR_TARGET_TEMP_LOW, ATTR_CURRENT_HUMIDITY,
ATTR_CURRENT_HUMIDITY, STATE_ECO, STATE_ELECTRIC, ATTR_TARGET_TEMP_HIGH, ClimateDevice, SUPPORT_TARGET_TEMPERATURE,
STATE_PERFORMANCE, STATE_HIGH_DEMAND, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW,
STATE_HEAT_PUMP, STATE_GAS) SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE, SUPPORT_FAN_MODE,
SUPPORT_AUX_HEAT)
from homeassistant.components.wink import DOMAIN, WinkDevice
from homeassistant.const import ( from homeassistant.const import (
TEMP_CELSIUS, STATE_ON, STATE_ON, STATE_OFF, TEMP_CELSIUS, STATE_UNKNOWN, PRECISION_TENTHS)
STATE_OFF, STATE_UNKNOWN) from homeassistant.helpers.temperature import display_temp as show_temp
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_ECO_TARGET = 'eco_target'
ATTR_EXTERNAL_TEMPERATURE = 'external_temperature'
ATTR_OCCUPIED = 'occupied'
ATTR_RHEEM_TYPE = 'rheem_type'
ATTR_SCHEDULE_ENABLED = 'schedule_enabled'
ATTR_SMART_TEMPERATURE = 'smart_temperature'
ATTR_TOTAL_CONSUMPTION = 'total_consumption'
ATTR_VACATION_MODE = 'vacation_mode'
DEPENDENCIES = ['wink'] DEPENDENCIES = ['wink']
SPEED_LOW = 'low' SPEED_LOW = 'low'
SPEED_MEDIUM = 'medium' SPEED_MEDIUM = 'medium'
SPEED_HIGH = 'high' SPEED_HIGH = 'high'
HA_STATE_TO_WINK = {STATE_AUTO: 'auto', HA_STATE_TO_WINK = {
STATE_ECO: 'eco', STATE_AUTO: 'auto',
STATE_FAN_ONLY: 'fan_only', STATE_COOL: 'cool_only',
STATE_HEAT: 'heat_only', STATE_ECO: 'eco',
STATE_COOL: 'cool_only', STATE_ELECTRIC: 'electric_only',
STATE_PERFORMANCE: 'performance', STATE_FAN_ONLY: 'fan_only',
STATE_HIGH_DEMAND: 'high_demand', STATE_GAS: 'gas',
STATE_HEAT_PUMP: 'heat_pump', STATE_HEAT: 'heat_only',
STATE_ELECTRIC: 'electric_only', STATE_HEAT_PUMP: 'heat_pump',
STATE_GAS: 'gas', 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()}
ATTR_EXTERNAL_TEMPERATURE = "external_temperature" SUPPORT_FLAGS_THERMOSTAT = (
ATTR_SMART_TEMPERATURE = "smart_temperature" SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_HIGH |
ATTR_ECO_TARGET = "eco_target" SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_OPERATION_MODE |
ATTR_OCCUPIED = "occupied" SUPPORT_AWAY_MODE | SUPPORT_FAN_MODE | SUPPORT_AUX_HEAT)
SUPPORT_FLAGS_AC = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
SUPPORT_FAN_MODE)
SUPPORT_FLAGS_HEATER = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
SUPPORT_AWAY_MODE)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
@ -67,6 +86,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class WinkThermostat(WinkDevice, ClimateDevice): class WinkThermostat(WinkDevice, ClimateDevice):
"""Representation of a Wink thermostat.""" """Representation of a Wink thermostat."""
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS_THERMOSTAT
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
"""Callback when entity is added to hass.""" """Callback when entity is added to hass."""
@ -85,15 +109,18 @@ class WinkThermostat(WinkDevice, ClimateDevice):
target_temp_high = self.target_temperature_high target_temp_high = self.target_temperature_high
target_temp_low = self.target_temperature_low target_temp_low = self.target_temperature_low
if target_temp_high is not None: if target_temp_high is not None:
data[ATTR_TARGET_TEMP_HIGH] = self._convert_for_display( data[ATTR_TARGET_TEMP_HIGH] = show_temp(
self.target_temperature_high) self.hass, self.target_temperature_high, self.temperature_unit,
PRECISION_TENTHS)
if target_temp_low is not None: if target_temp_low is not None:
data[ATTR_TARGET_TEMP_LOW] = self._convert_for_display( data[ATTR_TARGET_TEMP_LOW] = show_temp(
self.target_temperature_low) self.hass, self.target_temperature_low, self.temperature_unit,
PRECISION_TENTHS)
if self.external_temperature: if self.external_temperature:
data[ATTR_EXTERNAL_TEMPERATURE] = self._convert_for_display( data[ATTR_EXTERNAL_TEMPERATURE] = show_temp(
self.external_temperature) self.hass, self.external_temperature, self.temperature_unit,
PRECISION_TENTHS)
if self.smart_temperature: if self.smart_temperature:
data[ATTR_SMART_TEMPERATURE] = self.smart_temperature data[ATTR_SMART_TEMPERATURE] = self.smart_temperature
@ -139,7 +166,7 @@ class WinkThermostat(WinkDevice, ClimateDevice):
@property @property
def eco_target(self): def eco_target(self):
"""Return status of eco target (Is the termostat in eco mode).""" """Return status of eco target (Is the thermostat in eco mode)."""
return self.wink.eco_target() return self.wink.eco_target()
@property @property
@ -249,7 +276,7 @@ class WinkThermostat(WinkDevice, ClimateDevice):
if ha_mode is not None: if ha_mode is not None:
op_list.append(ha_mode) op_list.append(ha_mode)
else: else:
error = "Invaid operation mode mapping. " + mode + \ error = "Invalid operation mode mapping. " + mode + \
" doesn't map. Please report this." " doesn't map. Please report this."
_LOGGER.error(error) _LOGGER.error(error)
return op_list return op_list
@ -297,7 +324,6 @@ class WinkThermostat(WinkDevice, ClimateDevice):
minimum = 7 # Default minimum minimum = 7 # Default minimum
min_min = self.wink.min_min_set_point() min_min = self.wink.min_min_set_point()
min_max = self.wink.min_max_set_point() min_max = self.wink.min_max_set_point()
return_value = minimum
if self.current_operation == STATE_HEAT: if self.current_operation == STATE_HEAT:
if min_min: if min_min:
return_value = min_min return_value = min_min
@ -323,7 +349,6 @@ class WinkThermostat(WinkDevice, ClimateDevice):
maximum = 35 # Default maximum maximum = 35 # Default maximum
max_min = self.wink.max_min_set_point() max_min = self.wink.max_min_set_point()
max_max = self.wink.max_max_set_point() max_max = self.wink.max_max_set_point()
return_value = maximum
if self.current_operation == STATE_HEAT: if self.current_operation == STATE_HEAT:
if max_min: if max_min:
return_value = max_min return_value = max_min
@ -347,6 +372,11 @@ class WinkThermostat(WinkDevice, ClimateDevice):
class WinkAC(WinkDevice, ClimateDevice): class WinkAC(WinkDevice, ClimateDevice):
"""Representation of a Wink air conditioner.""" """Representation of a Wink air conditioner."""
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS_AC
@property @property
def temperature_unit(self): def temperature_unit(self):
"""Return the unit of measurement.""" """Return the unit of measurement."""
@ -360,13 +390,15 @@ class WinkAC(WinkDevice, ClimateDevice):
target_temp_high = self.target_temperature_high target_temp_high = self.target_temperature_high
target_temp_low = self.target_temperature_low target_temp_low = self.target_temperature_low
if target_temp_high is not None: if target_temp_high is not None:
data[ATTR_TARGET_TEMP_HIGH] = self._convert_for_display( data[ATTR_TARGET_TEMP_HIGH] = show_temp(
self.target_temperature_high) self.hass, self.target_temperature_high, self.temperature_unit,
PRECISION_TENTHS)
if target_temp_low is not None: if target_temp_low is not None:
data[ATTR_TARGET_TEMP_LOW] = self._convert_for_display( data[ATTR_TARGET_TEMP_LOW] = show_temp(
self.target_temperature_low) self.hass, self.target_temperature_low, self.temperature_unit,
data["total_consumption"] = self.wink.total_consumption() PRECISION_TENTHS)
data["schedule_enabled"] = self.wink.schedule_enabled() data[ATTR_TOTAL_CONSUMPTION] = self.wink.total_consumption()
data[ATTR_SCHEDULE_ENABLED] = self.wink.schedule_enabled()
return data return data
@ -377,11 +409,14 @@ class WinkAC(WinkDevice, ClimateDevice):
@property @property
def current_operation(self): def current_operation(self):
"""Return current operation ie. heat, cool, idle.""" """Return current operation ie. auto_eco, cool_only, fan_only."""
if not self.wink.is_on(): if not self.wink.is_on():
current_op = STATE_OFF current_op = STATE_OFF
else: else:
current_op = WINK_STATE_TO_HA.get(self.wink.current_hvac_mode()) wink_mode = self.wink.current_mode()
if wink_mode == "auto_eco":
wink_mode = "eco"
current_op = WINK_STATE_TO_HA.get(wink_mode)
if current_op is None: if current_op is None:
current_op = STATE_UNKNOWN current_op = STATE_UNKNOWN
return current_op return current_op
@ -392,11 +427,13 @@ class WinkAC(WinkDevice, ClimateDevice):
op_list = ['off'] op_list = ['off']
modes = self.wink.modes() modes = self.wink.modes()
for mode in modes: for mode in modes:
if mode == "auto_eco":
mode = "eco"
ha_mode = WINK_STATE_TO_HA.get(mode) ha_mode = WINK_STATE_TO_HA.get(mode)
if ha_mode is not None: if ha_mode is not None:
op_list.append(ha_mode) op_list.append(ha_mode)
else: else:
error = "Invaid operation mode mapping. " + mode + \ error = "Invalid operation mode mapping. " + mode + \
" doesn't map. Please report this." " doesn't map. Please report this."
_LOGGER.error(error) _LOGGER.error(error)
return op_list return op_list
@ -420,15 +457,19 @@ class WinkAC(WinkDevice, ClimateDevice):
@property @property
def current_fan_mode(self): def current_fan_mode(self):
"""Return the current fan mode.""" """
Return the current fan mode.
The official Wink app only supports 3 modes [low, medium, high]
which are equal to [0.33, 0.66, 1.0] respectively.
"""
speed = self.wink.current_fan_speed() speed = self.wink.current_fan_speed()
if speed <= 0.4 and speed > 0.3: if speed <= 0.33:
return SPEED_LOW return SPEED_LOW
elif speed <= 0.8 and speed > 0.5: elif speed <= 0.66:
return SPEED_MEDIUM return SPEED_MEDIUM
elif speed <= 1.0 and speed > 0.8: else:
return SPEED_HIGH return SPEED_HIGH
return STATE_UNKNOWN
@property @property
def fan_list(self): def fan_list(self):
@ -436,11 +477,16 @@ class WinkAC(WinkDevice, ClimateDevice):
return [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] return [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
def set_fan_mode(self, fan): def set_fan_mode(self, fan):
"""Set fan speed.""" """
Set fan speed.
The official Wink app only supports 3 modes [low, medium, high]
which are equal to [0.33, 0.66, 1.0] respectively.
"""
if fan == SPEED_LOW: if fan == SPEED_LOW:
speed = 0.4 speed = 0.33
elif fan == SPEED_MEDIUM: elif fan == SPEED_MEDIUM:
speed = 0.8 speed = 0.66
elif fan == SPEED_HIGH: elif fan == SPEED_HIGH:
speed = 1.0 speed = 1.0
self.wink.set_ac_fan_speed(speed) self.wink.set_ac_fan_speed(speed)
@ -449,6 +495,11 @@ class WinkAC(WinkDevice, ClimateDevice):
class WinkWaterHeater(WinkDevice, ClimateDevice): class WinkWaterHeater(WinkDevice, ClimateDevice):
"""Representation of a Wink water heater.""" """Representation of a Wink water heater."""
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS_HEATER
@property @property
def temperature_unit(self): def temperature_unit(self):
"""Return the unit of measurement.""" """Return the unit of measurement."""
@ -459,8 +510,8 @@ class WinkWaterHeater(WinkDevice, ClimateDevice):
def device_state_attributes(self): def device_state_attributes(self):
"""Return the optional state attributes.""" """Return the optional state attributes."""
data = {} data = {}
data["vacation_mode"] = self.wink.vacation_mode_enabled() data[ATTR_VACATION_MODE] = self.wink.vacation_mode_enabled()
data["rheem_type"] = self.wink.rheem_type() data[ATTR_RHEEM_TYPE] = self.wink.rheem_type()
return data return data
@ -492,7 +543,7 @@ class WinkWaterHeater(WinkDevice, ClimateDevice):
if ha_mode is not None: if ha_mode is not None:
op_list.append(ha_mode) op_list.append(ha_mode)
else: else:
error = "Invaid operation mode mapping. " + mode + \ error = "Invalid operation mode mapping. " + mode + \
" doesn't map. Please report this." " doesn't map. Please report this."
_LOGGER.error(error) _LOGGER.error(error)
return op_list return op_list

View File

@ -7,8 +7,9 @@ https://home-assistant.io/components/climate.zwave/
# Because we do not compile openzwave on CI # Because we do not compile openzwave on CI
# pylint: disable=import-error # pylint: disable=import-error
import logging import logging
from homeassistant.components.climate import DOMAIN from homeassistant.components.climate import (
from homeassistant.components.climate import ClimateDevice DOMAIN, ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE,
SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE)
from homeassistant.components.zwave import ZWaveDeviceEntity from homeassistant.components.zwave import ZWaveDeviceEntity
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
from homeassistant.const import ( from homeassistant.const import (
@ -70,6 +71,18 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
self._zxt_120 = 1 self._zxt_120 = 1
self.update_properties() self.update_properties()
@property
def supported_features(self):
"""Return the list of supported features."""
support = SUPPORT_TARGET_TEMPERATURE
if self.values.fan_mode:
support |= SUPPORT_FAN_MODE
if self.values.mode:
support |= SUPPORT_OPERATION_MODE
if self._zxt_120 == 1 and self.values.zxt_120_swing_mode:
support |= SUPPORT_SWING_MODE
return support
def update_properties(self): def update_properties(self):
"""Handle the data changes for node values.""" """Handle the data changes for node values."""
# Operation Mode # Operation Mode

View File

@ -1,5 +1,6 @@
"""Component to integrate the Home Assistant cloud.""" """Component to integrate the Home Assistant cloud."""
import asyncio import asyncio
from datetime import datetime
import json import json
import logging import logging
import os import os
@ -8,6 +9,9 @@ import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE) EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE)
from homeassistant.helpers import entityfilter
from homeassistant.util import dt as dt_util
from homeassistant.components.alexa import smart_home
from . import http_api, iot from . import http_api, iot
from .const import CONFIG_DIR, DOMAIN, SERVERS from .const import CONFIG_DIR, DOMAIN, SERVERS
@ -16,6 +20,8 @@ REQUIREMENTS = ['warrant==0.5.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_ALEXA = 'alexa'
CONF_ALEXA_FILTER = 'filter'
CONF_COGNITO_CLIENT_ID = 'cognito_client_id' CONF_COGNITO_CLIENT_ID = 'cognito_client_id'
CONF_RELAYER = 'relayer' CONF_RELAYER = 'relayer'
CONF_USER_POOL_ID = 'user_pool_id' CONF_USER_POOL_ID = 'user_pool_id'
@ -24,6 +30,13 @@ MODE_DEV = 'development'
DEFAULT_MODE = MODE_DEV DEFAULT_MODE = MODE_DEV
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']
ALEXA_SCHEMA = vol.Schema({
vol.Optional(
CONF_ALEXA_FILTER,
default=lambda: entityfilter.generate_filter([], [], [], [])
): entityfilter.FILTER_SCHEMA,
})
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Optional(CONF_MODE, default=DEFAULT_MODE): vol.Optional(CONF_MODE, default=DEFAULT_MODE):
@ -33,6 +46,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.Required(CONF_USER_POOL_ID): str, vol.Required(CONF_USER_POOL_ID): str,
vol.Required(CONF_REGION): str, vol.Required(CONF_REGION): str,
vol.Required(CONF_RELAYER): str, vol.Required(CONF_RELAYER): str,
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA
}), }),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@ -45,6 +59,10 @@ def async_setup(hass, config):
else: else:
kwargs = {CONF_MODE: DEFAULT_MODE} kwargs = {CONF_MODE: DEFAULT_MODE}
if CONF_ALEXA not in kwargs:
kwargs[CONF_ALEXA] = ALEXA_SCHEMA({})
kwargs[CONF_ALEXA] = smart_home.Config(**kwargs[CONF_ALEXA])
cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs) cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs)
@asyncio.coroutine @asyncio.coroutine
@ -62,11 +80,11 @@ class Cloud:
"""Store the configuration of the cloud connection.""" """Store the configuration of the cloud connection."""
def __init__(self, hass, mode, cognito_client_id=None, user_pool_id=None, def __init__(self, hass, mode, cognito_client_id=None, user_pool_id=None,
region=None, relayer=None): region=None, relayer=None, alexa=None):
"""Create an instance of Cloud.""" """Create an instance of Cloud."""
self.hass = hass self.hass = hass
self.mode = mode self.mode = mode
self.email = None self.alexa_config = alexa
self.id_token = None self.id_token = None
self.access_token = None self.access_token = None
self.refresh_token = None self.refresh_token = None
@ -86,10 +104,37 @@ class Cloud:
self.region = info['region'] self.region = info['region']
self.relayer = info['relayer'] self.relayer = info['relayer']
@property
def cognito_email_based(self):
"""Return if cognito is email based."""
return not self.user_pool_id.endswith('GmV')
@property @property
def is_logged_in(self): def is_logged_in(self):
"""Get if cloud is logged in.""" """Get if cloud is logged in."""
return self.email is not None return self.id_token is not None
@property
def subscription_expired(self):
"""Return a boolen if the subscription has expired."""
# For now, don't enforce subscriptions to exist
if 'custom:sub-exp' not in self.claims:
return False
return dt_util.utcnow() > self.expiration_date
@property
def expiration_date(self):
"""Return the subscription expiration as a UTC datetime object."""
return datetime.combine(
dt_util.parse_date(self.claims['custom:sub-exp']),
datetime.min.time()).replace(tzinfo=dt_util.UTC)
@property
def claims(self):
"""Get the claims from the id token."""
from jose import jwt
return jwt.get_unverified_claims(self.id_token)
@property @property
def user_info_path(self): def user_info_path(self):
@ -110,18 +155,20 @@ class Cloud:
if os.path.isfile(user_info): if os.path.isfile(user_info):
with open(user_info, 'rt') as file: with open(user_info, 'rt') as file:
info = json.loads(file.read()) info = json.loads(file.read())
self.email = info['email']
self.id_token = info['id_token'] self.id_token = info['id_token']
self.access_token = info['access_token'] self.access_token = info['access_token']
self.refresh_token = info['refresh_token'] self.refresh_token = info['refresh_token']
yield from self.hass.async_add_job(load_config) yield from self.hass.async_add_job(load_config)
if self.email is not None: if self.id_token is not None:
yield from self.iot.connect() yield from self.iot.connect()
def path(self, *parts): def path(self, *parts):
"""Get config path inside cloud dir.""" """Get config path inside cloud dir.
Async friendly.
"""
return self.hass.config.path(CONFIG_DIR, *parts) return self.hass.config.path(CONFIG_DIR, *parts)
@asyncio.coroutine @asyncio.coroutine
@ -129,7 +176,6 @@ class Cloud:
"""Close connection and remove all credentials.""" """Close connection and remove all credentials."""
yield from self.iot.disconnect() yield from self.iot.disconnect()
self.email = None
self.id_token = None self.id_token = None
self.access_token = None self.access_token = None
self.refresh_token = None self.refresh_token = None
@ -141,7 +187,6 @@ class Cloud:
"""Write user info to a file.""" """Write user info to a file."""
with open(self.user_info_path, 'wt') as file: with open(self.user_info_path, 'wt') as file:
file.write(json.dumps({ file.write(json.dumps({
'email': self.email,
'id_token': self.id_token, 'id_token': self.id_token,
'access_token': self.access_token, 'access_token': self.access_token,
'refresh_token': self.refresh_token, 'refresh_token': self.refresh_token,

View File

@ -69,7 +69,10 @@ def register(cloud, email, password):
cognito = _cognito(cloud) cognito = _cognito(cloud)
try: try:
cognito.register(_generate_username(email), password, email=email) if cloud.cognito_email_based:
cognito.register(email, password, email=email)
else:
cognito.register(_generate_username(email), password, email=email)
except ClientError as err: except ClientError as err:
raise _map_aws_exception(err) raise _map_aws_exception(err)
@ -80,7 +83,11 @@ def confirm_register(cloud, confirmation_code, email):
cognito = _cognito(cloud) cognito = _cognito(cloud)
try: try:
cognito.confirm_sign_up(confirmation_code, _generate_username(email)) if cloud.cognito_email_based:
cognito.confirm_sign_up(confirmation_code, email)
else:
cognito.confirm_sign_up(confirmation_code,
_generate_username(email))
except ClientError as err: except ClientError as err:
raise _map_aws_exception(err) raise _map_aws_exception(err)
@ -89,7 +96,11 @@ def forgot_password(cloud, email):
"""Initiate forgotten password flow.""" """Initiate forgotten password flow."""
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
cognito = _cognito(cloud, username=_generate_username(email)) if cloud.cognito_email_based:
cognito = _cognito(cloud, username=email)
else:
cognito = _cognito(cloud, username=_generate_username(email))
try: try:
cognito.initiate_forgot_password() cognito.initiate_forgot_password()
except ClientError as err: except ClientError as err:
@ -100,7 +111,11 @@ def confirm_forgot_password(cloud, confirmation_code, email, new_password):
"""Confirm forgotten password code and change password.""" """Confirm forgotten password code and change password."""
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
cognito = _cognito(cloud, username=_generate_username(email)) if cloud.cognito_email_based:
cognito = _cognito(cloud, username=email)
else:
cognito = _cognito(cloud, username=_generate_username(email))
try: try:
cognito.confirm_forgot_password(confirmation_code, new_password) cognito.confirm_forgot_password(confirmation_code, new_password)
except ClientError as err: except ClientError as err:
@ -113,7 +128,6 @@ def login(cloud, email, password):
cloud.id_token = cognito.id_token cloud.id_token = cognito.id_token
cloud.access_token = cognito.access_token cloud.access_token = cognito.access_token
cloud.refresh_token = cognito.refresh_token cloud.refresh_token = cognito.refresh_token
cloud.email = email
cloud.write_user_info() cloud.write_user_info()

View File

@ -12,3 +12,8 @@ SERVERS = {
# 'relayer': '' # 'relayer': ''
# } # }
} }
MESSAGE_EXPIRATION = """
It looks like your Home Assistant Cloud subscription has expired. Please check
your [account page](/config/cloud/account) to continue using the service.
"""

View File

@ -65,12 +65,12 @@ class CloudLoginView(HomeAssistantView):
url = '/api/cloud/login' url = '/api/cloud/login'
name = 'api:cloud:login' name = 'api:cloud:login'
@asyncio.coroutine
@_handle_cloud_errors @_handle_cloud_errors
@RequestDataValidator(vol.Schema({ @RequestDataValidator(vol.Schema({
vol.Required('email'): str, vol.Required('email'): str,
vol.Required('password'): str, vol.Required('password'): str,
})) }))
@asyncio.coroutine
def post(self, request, data): def post(self, request, data):
"""Handle login request.""" """Handle login request."""
hass = request.app['hass'] hass = request.app['hass']
@ -79,8 +79,10 @@ class CloudLoginView(HomeAssistantView):
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
yield from hass.async_add_job(auth_api.login, cloud, data['email'], yield from hass.async_add_job(auth_api.login, cloud, data['email'],
data['password']) data['password'])
hass.async_add_job(cloud.iot.connect)
hass.async_add_job(cloud.iot.connect)
# Allow cloud to start connecting.
yield from asyncio.sleep(0, loop=hass.loop)
return self.json(_account_data(cloud)) return self.json(_account_data(cloud))
@ -90,8 +92,8 @@ class CloudLogoutView(HomeAssistantView):
url = '/api/cloud/logout' url = '/api/cloud/logout'
name = 'api:cloud:logout' name = 'api:cloud:logout'
@asyncio.coroutine
@_handle_cloud_errors @_handle_cloud_errors
@asyncio.coroutine
def post(self, request): def post(self, request):
"""Handle logout request.""" """Handle logout request."""
hass = request.app['hass'] hass = request.app['hass']
@ -127,12 +129,12 @@ class CloudRegisterView(HomeAssistantView):
url = '/api/cloud/register' url = '/api/cloud/register'
name = 'api:cloud:register' name = 'api:cloud:register'
@asyncio.coroutine
@_handle_cloud_errors @_handle_cloud_errors
@RequestDataValidator(vol.Schema({ @RequestDataValidator(vol.Schema({
vol.Required('email'): str, vol.Required('email'): str,
vol.Required('password'): vol.All(str, vol.Length(min=6)), vol.Required('password'): vol.All(str, vol.Length(min=6)),
})) }))
@asyncio.coroutine
def post(self, request, data): def post(self, request, data):
"""Handle registration request.""" """Handle registration request."""
hass = request.app['hass'] hass = request.app['hass']
@ -151,12 +153,12 @@ class CloudConfirmRegisterView(HomeAssistantView):
url = '/api/cloud/confirm_register' url = '/api/cloud/confirm_register'
name = 'api:cloud:confirm_register' name = 'api:cloud:confirm_register'
@asyncio.coroutine
@_handle_cloud_errors @_handle_cloud_errors
@RequestDataValidator(vol.Schema({ @RequestDataValidator(vol.Schema({
vol.Required('confirmation_code'): str, vol.Required('confirmation_code'): str,
vol.Required('email'): str, vol.Required('email'): str,
})) }))
@asyncio.coroutine
def post(self, request, data): def post(self, request, data):
"""Handle registration confirmation request.""" """Handle registration confirmation request."""
hass = request.app['hass'] hass = request.app['hass']
@ -176,11 +178,11 @@ class CloudForgotPasswordView(HomeAssistantView):
url = '/api/cloud/forgot_password' url = '/api/cloud/forgot_password'
name = 'api:cloud:forgot_password' name = 'api:cloud:forgot_password'
@asyncio.coroutine
@_handle_cloud_errors @_handle_cloud_errors
@RequestDataValidator(vol.Schema({ @RequestDataValidator(vol.Schema({
vol.Required('email'): str, vol.Required('email'): str,
})) }))
@asyncio.coroutine
def post(self, request, data): def post(self, request, data):
"""Handle forgot password request.""" """Handle forgot password request."""
hass = request.app['hass'] hass = request.app['hass']
@ -199,13 +201,13 @@ class CloudConfirmForgotPasswordView(HomeAssistantView):
url = '/api/cloud/confirm_forgot_password' url = '/api/cloud/confirm_forgot_password'
name = 'api:cloud:confirm_forgot_password' name = 'api:cloud:confirm_forgot_password'
@asyncio.coroutine
@_handle_cloud_errors @_handle_cloud_errors
@RequestDataValidator(vol.Schema({ @RequestDataValidator(vol.Schema({
vol.Required('confirmation_code'): str, vol.Required('confirmation_code'): str,
vol.Required('email'): str, vol.Required('email'): str,
vol.Required('new_password'): vol.All(str, vol.Length(min=6)) vol.Required('new_password'): vol.All(str, vol.Length(min=6))
})) }))
@asyncio.coroutine
def post(self, request, data): def post(self, request, data):
"""Handle forgot password confirm request.""" """Handle forgot password confirm request."""
hass = request.app['hass'] hass = request.app['hass']
@ -222,6 +224,10 @@ class CloudConfirmForgotPasswordView(HomeAssistantView):
def _account_data(cloud): def _account_data(cloud):
"""Generate the auth data JSON response.""" """Generate the auth data JSON response."""
claims = cloud.claims
return { return {
'email': cloud.email 'email': claims['email'],
'sub_exp': claims.get('custom:sub-exp'),
'cloud': cloud.iot.state,
} }

View File

@ -9,11 +9,16 @@ from homeassistant.components.alexa import smart_home
from homeassistant.util.decorator import Registry from homeassistant.util.decorator import Registry
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from . import auth_api from . import auth_api
from .const import MESSAGE_EXPIRATION
HANDLERS = Registry() HANDLERS = Registry()
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
STATE_CONNECTING = 'connecting'
STATE_CONNECTED = 'connected'
STATE_DISCONNECTED = 'disconnected'
class UnknownHandler(Exception): class UnknownHandler(Exception):
"""Exception raised when trying to handle unknown handler.""" """Exception raised when trying to handle unknown handler."""
@ -25,27 +30,34 @@ class CloudIoT:
def __init__(self, cloud): def __init__(self, cloud):
"""Initialize the CloudIoT class.""" """Initialize the CloudIoT class."""
self.cloud = cloud self.cloud = cloud
# The WebSocket client
self.client = None self.client = None
# Scheduled sleep task till next connection retry
self.retry_task = None
# Boolean to indicate if we wanted the connection to close
self.close_requested = False self.close_requested = False
# The current number of attempts to connect, impacts wait time
self.tries = 0 self.tries = 0
# Current state of the connection
@property self.state = STATE_DISCONNECTED
def is_connected(self):
"""Return if connected to the cloud."""
return self.client is not None
@asyncio.coroutine @asyncio.coroutine
def connect(self): def connect(self):
"""Connect to the IoT broker.""" """Connect to the IoT broker."""
if self.client is not None:
raise RuntimeError('Cannot connect while already connected')
self.close_requested = False
hass = self.cloud.hass hass = self.cloud.hass
remove_hass_stop_listener = None if self.cloud.subscription_expired:
# Try refreshing the token to see if it is still expired.
yield from hass.async_add_job(auth_api.check_token, self.cloud)
session = async_get_clientsession(self.cloud.hass) if self.cloud.subscription_expired:
hass.components.persistent_notification.async_create(
MESSAGE_EXPIRATION, 'Subscription expired',
'cloud_subscription_expired')
self.state = STATE_DISCONNECTED
return
if self.state == STATE_CONNECTED:
raise RuntimeError('Already connected')
@asyncio.coroutine @asyncio.coroutine
def _handle_hass_stop(event): def _handle_hass_stop(event):
@ -54,8 +66,14 @@ class CloudIoT:
remove_hass_stop_listener = None remove_hass_stop_listener = None
yield from self.disconnect() yield from self.disconnect()
self.state = STATE_CONNECTING
self.close_requested = False
remove_hass_stop_listener = hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, _handle_hass_stop)
session = async_get_clientsession(self.cloud.hass)
client = None client = None
disconnect_warn = None disconnect_warn = None
try: try:
yield from hass.async_add_job(auth_api.check_token, self.cloud) yield from hass.async_add_job(auth_api.check_token, self.cloud)
@ -66,17 +84,15 @@ class CloudIoT:
}) })
self.tries = 0 self.tries = 0
remove_hass_stop_listener = hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, _handle_hass_stop)
_LOGGER.info('Connected') _LOGGER.info('Connected')
self.state = STATE_CONNECTED
while not client.closed: while not client.closed:
msg = yield from client.receive() msg = yield from client.receive()
if msg.type in (WSMsgType.ERROR, WSMsgType.CLOSED, if msg.type in (WSMsgType.ERROR, WSMsgType.CLOSED,
WSMsgType.CLOSING): WSMsgType.CLOSING):
disconnect_warn = 'Closed by server' disconnect_warn = 'Connection cancelled.'
break break
elif msg.type != WSMsgType.TEXT: elif msg.type != WSMsgType.TEXT:
@ -144,20 +160,33 @@ class CloudIoT:
self.client = None self.client = None
yield from client.close() yield from client.close()
if not self.close_requested: if self.close_requested:
self.state = STATE_DISCONNECTED
else:
self.state = STATE_CONNECTING
self.tries += 1 self.tries += 1
# Sleep 0, 5, 10, 15 … up to 30 seconds between retries try:
yield from asyncio.sleep( # Sleep 0, 5, 10, 15 … up to 30 seconds between retries
min(30, (self.tries - 1) * 5), loop=hass.loop) self.retry_task = hass.async_add_job(asyncio.sleep(
min(30, (self.tries - 1) * 5), loop=hass.loop))
hass.async_add_job(self.connect()) yield from self.retry_task
self.retry_task = None
hass.async_add_job(self.connect())
except asyncio.CancelledError:
# Happens if disconnect called
pass
@asyncio.coroutine @asyncio.coroutine
def disconnect(self): def disconnect(self):
"""Disconnect the client.""" """Disconnect the client."""
self.close_requested = True self.close_requested = True
yield from self.client.close()
if self.client is not None:
yield from self.client.close()
elif self.retry_task is not None:
self.retry_task.cancel()
@asyncio.coroutine @asyncio.coroutine
@ -175,7 +204,9 @@ def async_handle_message(hass, cloud, handler_name, payload):
@asyncio.coroutine @asyncio.coroutine
def async_handle_alexa(hass, cloud, payload): def async_handle_alexa(hass, cloud, payload):
"""Handle an incoming IoT message for Alexa.""" """Handle an incoming IoT message for Alexa."""
return (yield from smart_home.async_handle_message(hass, payload)) return (yield from smart_home.async_handle_message(hass,
cloud.alexa_config,
payload))
@HANDLERS.register('cloud') @HANDLERS.register('cloud')

View File

@ -1,8 +1,8 @@
"""Provide configuration end points for Groups.""" """Provide configuration end points for Groups."""
import asyncio import asyncio
from homeassistant.const import SERVICE_RELOAD
from homeassistant.components.config import EditKeyBasedConfigView from homeassistant.components.config import EditKeyBasedConfigView
from homeassistant.components.group import GROUP_SCHEMA from homeassistant.components.group import DOMAIN, GROUP_SCHEMA
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -12,7 +12,13 @@ CONFIG_PATH = 'groups.yaml'
@asyncio.coroutine @asyncio.coroutine
def async_setup(hass): def async_setup(hass):
"""Set up the Group config API.""" """Set up the Group config API."""
@asyncio.coroutine
def hook(hass):
"""post_write_hook for Config View that reloads groups."""
yield from hass.services.async_call(DOMAIN, SERVICE_RELOAD)
hass.http.register_view(EditKeyBasedConfigView( hass.http.register_view(EditKeyBasedConfigView(
'group', 'config', CONFIG_PATH, cv.slug, GROUP_SCHEMA 'group', 'config', CONFIG_PATH, cv.slug, GROUP_SCHEMA,
post_write_hook=hook
)) ))
return True return True

View File

@ -2,6 +2,8 @@
import asyncio import asyncio
import logging import logging
from collections import deque
from aiohttp.web import Response
import homeassistant.core as ha import homeassistant.core as ha
from homeassistant.const import HTTP_NOT_FOUND, HTTP_OK from homeassistant.const import HTTP_NOT_FOUND, HTTP_OK
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
@ -12,7 +14,6 @@ import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONFIG_PATH = 'zwave_device_config.yaml' CONFIG_PATH = 'zwave_device_config.yaml'
OZW_LOG_FILENAME = 'OZW_Log.txt' OZW_LOG_FILENAME = 'OZW_Log.txt'
URL_API_OZW_LOG = '/api/zwave/ozwlog'
@asyncio.coroutine @asyncio.coroutine
@ -26,13 +27,44 @@ def async_setup(hass):
hass.http.register_view(ZWaveNodeGroupView) hass.http.register_view(ZWaveNodeGroupView)
hass.http.register_view(ZWaveNodeConfigView) hass.http.register_view(ZWaveNodeConfigView)
hass.http.register_view(ZWaveUserCodeView) hass.http.register_view(ZWaveUserCodeView)
hass.http.register_static_path( hass.http.register_view(ZWaveLogView)
URL_API_OZW_LOG, hass.config.path(OZW_LOG_FILENAME), False)
hass.http.register_view(ZWaveConfigWriteView) hass.http.register_view(ZWaveConfigWriteView)
return True return True
class ZWaveLogView(HomeAssistantView):
"""View to read the ZWave log file."""
url = "/api/zwave/ozwlog"
name = "api:zwave:ozwlog"
# pylint: disable=no-self-use
@asyncio.coroutine
def get(self, request):
"""Retrieve the lines from ZWave log."""
try:
lines = int(request.query.get('lines', 0))
except ValueError:
return Response(text='Invalid datetime', status=400)
hass = request.app['hass']
response = yield from hass.async_add_job(self._get_log, hass, lines)
return Response(text='\n'.join(response))
def _get_log(self, hass, lines):
"""Retrieve the logfile content."""
logfilepath = hass.config.path(OZW_LOG_FILENAME)
with open(logfilepath, 'r') as logfile:
data = (line.rstrip() for line in logfile)
if lines == 0:
loglines = list(data)
else:
loglines = deque(data, lines)
return loglines
class ZWaveConfigWriteView(HomeAssistantView): class ZWaveConfigWriteView(HomeAssistantView):
"""View to save the ZWave configuration to zwcfg_xxxxx.xml.""" """View to save the ZWave configuration to zwcfg_xxxxx.xml."""

View File

@ -50,15 +50,19 @@ def async_request_config(
Will return an ID to be used for sequent calls. Will return an ID to be used for sequent calls.
""" """
if link_name is not None and link_url is not None:
description += '\n\n[{}]({})'.format(link_name, link_url)
if description_image is not None:
description += '\n\n![Description image]({})'.format(description_image)
instance = hass.data.get(_KEY_INSTANCE) instance = hass.data.get(_KEY_INSTANCE)
if instance is None: if instance is None:
instance = hass.data[_KEY_INSTANCE] = Configurator(hass) instance = hass.data[_KEY_INSTANCE] = Configurator(hass)
request_id = instance.async_request_config( request_id = instance.async_request_config(
name, callback, name, callback, description, submit_caption, fields, entity_picture)
description, description_image, submit_caption,
fields, link_name, link_url, entity_picture)
if DATA_REQUESTS not in hass.data: if DATA_REQUESTS not in hass.data:
hass.data[DATA_REQUESTS] = {} hass.data[DATA_REQUESTS] = {}
@ -137,9 +141,8 @@ class Configurator(object):
@async_callback @async_callback
def async_request_config( def async_request_config(
self, name, callback, self, name, callback, description, submit_caption, fields,
description, description_image, submit_caption, entity_picture):
fields, link_name, link_url, entity_picture):
"""Set up a request for configuration.""" """Set up a request for configuration."""
entity_id = async_generate_entity_id( entity_id = async_generate_entity_id(
ENTITY_ID_FORMAT, name, hass=self.hass) ENTITY_ID_FORMAT, name, hass=self.hass)
@ -161,10 +164,7 @@ class Configurator(object):
data.update({ data.update({
key: value for key, value in [ key: value for key, value in [
(ATTR_DESCRIPTION, description), (ATTR_DESCRIPTION, description),
(ATTR_DESCRIPTION_IMAGE, description_image),
(ATTR_SUBMIT_CAPTION, submit_caption), (ATTR_SUBMIT_CAPTION, submit_caption),
(ATTR_LINK_NAME, link_name),
(ATTR_LINK_URL, link_url),
] if value is not None ] if value is not None
}) })
@ -207,7 +207,7 @@ class Configurator(object):
self.hass.bus.async_listen_once(EVENT_TIME_CHANGED, deferred_remove) self.hass.bus.async_listen_once(EVENT_TIME_CHANGED, deferred_remove)
@async_callback @asyncio.coroutine
def async_handle_service_call(self, call): def async_handle_service_call(self, call):
"""Handle a configure service call.""" """Handle a configure service call."""
request_id = call.data.get(ATTR_CONFIGURE_ID) request_id = call.data.get(ATTR_CONFIGURE_ID)
@ -220,7 +220,8 @@ class Configurator(object):
# field validation goes here? # field validation goes here?
if callback: if callback:
self.hass.async_add_job(callback, call.data.get(ATTR_FIELDS, {})) yield from self.hass.async_add_job(callback,
call.data.get(ATTR_FIELDS, {}))
def _generate_unique_id(self): def _generate_unique_id(self):
"""Generate a unique configurator ID.""" """Generate a unique configurator ID."""

View File

@ -14,7 +14,7 @@ import voluptuous as vol
from homeassistant import core from homeassistant import core
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, HTTP_BAD_REQUEST) ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON)
from homeassistant.helpers import intent, config_validation as cv from homeassistant.helpers import intent, config_validation as cv
from homeassistant.components import http from homeassistant.components import http
@ -39,6 +39,10 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({
}) })
})}, extra=vol.ALLOW_EXTRA) })}, extra=vol.ALLOW_EXTRA)
INTENT_TURN_ON = 'HassTurnOn'
INTENT_TURN_OFF = 'HassTurnOff'
REGEX_TYPE = type(re.compile(''))
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -60,7 +64,11 @@ def async_register(hass, intent_type, utterances):
if conf is None: if conf is None:
conf = intents[intent_type] = [] conf = intents[intent_type] = []
conf.extend(_create_matcher(utterance) for utterance in utterances) for utterance in utterances:
if isinstance(utterance, REGEX_TYPE):
conf.append(utterance)
else:
conf.append(_create_matcher(utterance))
@asyncio.coroutine @asyncio.coroutine
@ -93,6 +101,13 @@ def async_setup(hass, config):
hass.http.register_view(ConversationProcessView) hass.http.register_view(ConversationProcessView)
hass.helpers.intent.async_register(TurnOnIntent())
hass.helpers.intent.async_register(TurnOffIntent())
async_register(hass, INTENT_TURN_ON,
['Turn {name} on', 'Turn on {name}'])
async_register(hass, INTENT_TURN_OFF, [
'Turn {name} off', 'Turn off {name}'])
return True return True
@ -128,48 +143,84 @@ def _process(hass, text):
if not match: if not match:
continue continue
response = yield from intent.async_handle( response = yield from hass.helpers.intent.async_handle(
hass, DOMAIN, intent_type, DOMAIN, intent_type,
{key: {'value': value} for key, value {key: {'value': value} for key, value
in match.groupdict().items()}, text) in match.groupdict().items()}, text)
return response return response
@core.callback
def _match_entity(hass, name):
"""Match a name to an entity."""
from fuzzywuzzy import process as fuzzyExtract from fuzzywuzzy import process as fuzzyExtract
text = text.lower()
match = REGEX_TURN_COMMAND.match(text)
if not match:
_LOGGER.error("Unable to process: %s", text)
return None
name, command = match.groups()
entities = {state.entity_id: state.name for state entities = {state.entity_id: state.name for state
in hass.states.async_all()} in hass.states.async_all()}
entity_ids = fuzzyExtract.extractOne( entity_id = fuzzyExtract.extractOne(
name, entities, score_cutoff=65)[2] name, entities, score_cutoff=65)[2]
return hass.states.get(entity_id) if entity_id else None
if not entity_ids:
_LOGGER.error(
"Could not find entity id %s from text %s", name, text)
return None
if command == 'on': class TurnOnIntent(intent.IntentHandler):
"""Handle turning item on intents."""
intent_type = INTENT_TURN_ON
slot_schema = {
'name': cv.string,
}
@asyncio.coroutine
def async_handle(self, intent_obj):
"""Handle turn on intent."""
hass = intent_obj.hass
slots = self.async_validate_slots(intent_obj.slots)
name = slots['name']['value']
entity = _match_entity(hass, name)
if not entity:
_LOGGER.error("Could not find entity id for %s", name)
return None
yield from hass.services.async_call( yield from hass.services.async_call(
core.DOMAIN, SERVICE_TURN_ON, { core.DOMAIN, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity_ids, ATTR_ENTITY_ID: entity.entity_id,
}, blocking=True) }, blocking=True)
elif command == 'off': response = intent_obj.create_response()
response.async_set_speech(
'Turned on {}'.format(entity.name))
return response
class TurnOffIntent(intent.IntentHandler):
"""Handle turning item off intents."""
intent_type = INTENT_TURN_OFF
slot_schema = {
'name': cv.string,
}
@asyncio.coroutine
def async_handle(self, intent_obj):
"""Handle turn off intent."""
hass = intent_obj.hass
slots = self.async_validate_slots(intent_obj.slots)
name = slots['name']['value']
entity = _match_entity(hass, name)
if not entity:
_LOGGER.error("Could not find entity id for %s", name)
return None
yield from hass.services.async_call( yield from hass.services.async_call(
core.DOMAIN, SERVICE_TURN_OFF, { core.DOMAIN, SERVICE_TURN_OFF, {
ATTR_ENTITY_ID: entity_ids, ATTR_ENTITY_ID: entity.entity_id,
}, blocking=True) }, blocking=True)
else: response = intent_obj.create_response()
_LOGGER.error('Got unsupported command %s from text %s', response.async_set_speech(
command, text) 'Turned off {}'.format(entity.name))
return response
return None
class ConversationProcessView(http.HomeAssistantView): class ConversationProcessView(http.HomeAssistantView):
@ -178,23 +229,15 @@ class ConversationProcessView(http.HomeAssistantView):
url = '/api/conversation/process' url = '/api/conversation/process'
name = "api:conversation:process" name = "api:conversation:process"
@http.RequestDataValidator(vol.Schema({
vol.Required('text'): str,
}))
@asyncio.coroutine @asyncio.coroutine
def post(self, request): def post(self, request, data):
"""Send a request for processing.""" """Send a request for processing."""
hass = request.app['hass'] hass = request.app['hass']
try:
data = yield from request.json()
except ValueError:
return self.json_message('Invalid JSON specified',
HTTP_BAD_REQUEST)
text = data.get('text') intent_result = yield from _process(hass, data['text'])
if text is None:
return self.json_message('Missing "text" key in JSON.',
HTTP_BAD_REQUEST)
intent_result = yield from _process(hass, text)
if intent_result is None: if intent_result is None:
intent_result = intent.IntentResponse() intent_result = intent.IntentResponse()

View File

@ -104,6 +104,9 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
@asyncio.coroutine @asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the MQTT Cover.""" """Set up the MQTT Cover."""
if discovery_info is not None:
config = PLATFORM_SCHEMA(discovery_info)
value_template = config.get(CONF_VALUE_TEMPLATE) value_template = config.get(CONF_VALUE_TEMPLATE)
if value_template is not None: if value_template is not None:
value_template.hass = hass value_template.hass = hass

View File

@ -0,0 +1,73 @@
"""
Support for Tahoma cover - shutters etc.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.tahoma/
"""
import logging
from homeassistant.components.cover import CoverDevice, ENTITY_ID_FORMAT
from homeassistant.components.tahoma import (
DOMAIN as TAHOMA_DOMAIN, TahomaDevice)
DEPENDENCIES = ['tahoma']
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up Tahoma covers."""
controller = hass.data[TAHOMA_DOMAIN]['controller']
devices = []
for device in hass.data[TAHOMA_DOMAIN]['devices']['cover']:
devices.append(TahomaCover(device, controller))
add_devices(devices, True)
class TahomaCover(TahomaDevice, CoverDevice):
"""Representation a Tahoma Cover."""
def __init__(self, tahoma_device, controller):
"""Initialize the Tahoma device."""
super().__init__(tahoma_device, controller)
self.entity_id = ENTITY_ID_FORMAT.format(self.unique_id)
def update(self):
"""Update method."""
self.controller.get_states([self.tahoma_device])
@property
def current_cover_position(self):
"""
Return current position of cover.
0 is closed, 100 is fully open.
"""
position = 100 - self.tahoma_device.active_states['core:ClosureState']
if position <= 5:
return 0
if position >= 95:
return 100
return position
def set_cover_position(self, position, **kwargs):
"""Move the cover to a specific position."""
self.apply_action('setPosition', 100 - position)
@property
def is_closed(self):
"""Return if the cover is closed."""
if self.current_cover_position is not None:
return self.current_cover_position == 0
def open_cover(self, **kwargs):
"""Open the cover."""
self.apply_action('open')
def close_cover(self, **kwargs):
"""Close the cover."""
self.apply_action('close')
def stop_cover(self, **kwargs):
"""Stop the cover."""
self.apply_action('stopIdentify')

View File

@ -53,6 +53,7 @@ YAML_DEVICES = 'known_devices.yaml'
CONF_TRACK_NEW = 'track_new_devices' CONF_TRACK_NEW = 'track_new_devices'
DEFAULT_TRACK_NEW = True DEFAULT_TRACK_NEW = True
CONF_NEW_DEVICE_DEFAULTS = 'new_device_defaults'
CONF_CONSIDER_HOME = 'consider_home' CONF_CONSIDER_HOME = 'consider_home'
DEFAULT_CONSIDER_HOME = timedelta(seconds=180) DEFAULT_CONSIDER_HOME = timedelta(seconds=180)
@ -76,16 +77,23 @@ ATTR_LOCATION_NAME = 'location_name'
ATTR_MAC = 'mac' ATTR_MAC = 'mac'
ATTR_NAME = 'name' ATTR_NAME = 'name'
ATTR_SOURCE_TYPE = 'source_type' ATTR_SOURCE_TYPE = 'source_type'
ATTR_VENDOR = 'vendor'
SOURCE_TYPE_GPS = 'gps' SOURCE_TYPE_GPS = 'gps'
SOURCE_TYPE_ROUTER = 'router' SOURCE_TYPE_ROUTER = 'router'
NEW_DEVICE_DEFAULTS_SCHEMA = vol.Any(None, vol.Schema({
vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean,
vol.Optional(CONF_AWAY_HIDE, default=DEFAULT_AWAY_HIDE): cv.boolean,
}))
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({
vol.Optional(CONF_SCAN_INTERVAL): cv.time_period, vol.Optional(CONF_SCAN_INTERVAL): cv.time_period,
vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean, vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean,
vol.Optional(CONF_CONSIDER_HOME, vol.Optional(CONF_CONSIDER_HOME,
default=DEFAULT_CONSIDER_HOME): vol.All( default=DEFAULT_CONSIDER_HOME): vol.All(
cv.time_period, cv.positive_timedelta) cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_NEW_DEVICE_DEFAULTS,
default={}): NEW_DEVICE_DEFAULTS_SCHEMA
}) })
@ -124,9 +132,11 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
conf = conf[0] if conf else {} conf = conf[0] if conf else {}
consider_home = conf.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME) consider_home = conf.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME)
track_new = conf.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW) track_new = conf.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
defaults = conf.get(CONF_NEW_DEVICE_DEFAULTS, {})
devices = yield from async_load_config(yaml_path, hass, consider_home) devices = yield from async_load_config(yaml_path, hass, consider_home)
tracker = DeviceTracker(hass, consider_home, track_new, devices) tracker = DeviceTracker(
hass, consider_home, track_new, defaults, devices)
@asyncio.coroutine @asyncio.coroutine
def async_setup_platform(p_type, p_config, disc_info=None): def async_setup_platform(p_type, p_config, disc_info=None):
@ -210,13 +220,15 @@ class DeviceTracker(object):
"""Representation of a device tracker.""" """Representation of a device tracker."""
def __init__(self, hass: HomeAssistantType, consider_home: timedelta, def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
track_new: bool, devices: Sequence) -> None: track_new: bool, defaults: dict,
devices: Sequence) -> None:
"""Initialize a device tracker.""" """Initialize a device tracker."""
self.hass = hass self.hass = hass
self.devices = {dev.dev_id: dev for dev in devices} self.devices = {dev.dev_id: dev for dev in devices}
self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac} self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac}
self.consider_home = consider_home self.consider_home = consider_home
self.track_new = track_new self.track_new = defaults.get(CONF_TRACK_NEW, track_new)
self.defaults = defaults
self.group = None self.group = None
self._is_updating = asyncio.Lock(loop=hass.loop) self._is_updating = asyncio.Lock(loop=hass.loop)
@ -273,7 +285,8 @@ class DeviceTracker(object):
device = Device( device = Device(
self.hass, self.consider_home, self.track_new, self.hass, self.consider_home, self.track_new,
dev_id, mac, (host_name or dev_id).replace('_', ' '), dev_id, mac, (host_name or dev_id).replace('_', ' '),
picture=picture, icon=icon) picture=picture, icon=icon,
hide_if_away=self.defaults.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE))
self.devices[dev_id] = device self.devices[dev_id] = device
if mac is not None: if mac is not None:
self.mac_to_dev[mac] = device self.mac_to_dev[mac] = device
@ -285,11 +298,6 @@ class DeviceTracker(object):
if device.track: if device.track:
yield from device.async_update_ha_state() yield from device.async_update_ha_state()
self.hass.bus.async_fire(EVENT_NEW_DEVICE, {
ATTR_ENTITY_ID: device.entity_id,
ATTR_HOST_NAME: device.host_name,
})
# During init, we ignore the group # During init, we ignore the group
if self.group and self.track_new: if self.group and self.track_new:
self.group.async_set_group( self.group.async_set_group(
@ -299,6 +307,13 @@ class DeviceTracker(object):
# lookup mac vendor string to be stored in config # lookup mac vendor string to be stored in config
yield from device.set_vendor_for_mac() yield from device.set_vendor_for_mac()
self.hass.bus.async_fire(EVENT_NEW_DEVICE, {
ATTR_ENTITY_ID: device.entity_id,
ATTR_HOST_NAME: device.host_name,
ATTR_MAC: device.mac,
ATTR_VENDOR: device.vendor,
})
# update known_devices.yaml # update known_devices.yaml
self.hass.async_add_job( self.hass.async_add_job(
self.async_update_config( self.async_update_config(

View File

@ -0,0 +1,138 @@
"""
Support for the Hitron CODA-4582U, provided by Rogers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.hitron_coda/
"""
import logging
from collections import namedtuple
import requests
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME
)
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string
})
def get_scanner(_hass, config):
"""Validate the configuration and return a Nmap scanner."""
scanner = HitronCODADeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None
Device = namedtuple('Device', ['mac', 'name'])
class HitronCODADeviceScanner(DeviceScanner):
"""This class scans for devices using the CODA's web interface."""
def __init__(self, config):
"""Initialize the scanner."""
self.last_results = []
host = config[CONF_HOST]
self._url = 'http://{}/data/getConnectInfo.asp'.format(host)
self._loginurl = 'http://{}/goform/login'.format(host)
self._username = config.get(CONF_USERNAME)
self._password = config.get(CONF_PASSWORD)
self._userid = None
self.success_init = self._update_info()
_LOGGER.info("Scanner initialized")
def scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
self._update_info()
return [device.mac for device in self.last_results]
def get_device_name(self, mac):
"""Return the name of the device with the given MAC address."""
name = next((
device.name for device in self.last_results
if device.mac == mac), None)
return name
def _login(self):
"""Log in to the router. This is required for subsequent api calls."""
_LOGGER.info("Logging in to CODA...")
try:
data = [
('user', self._username),
('pws', self._password),
]
res = requests.post(self._loginurl, data=data, timeout=10)
except requests.exceptions.Timeout:
_LOGGER.error(
"Connection to the router timed out at URL %s", self._url)
return False
if res.status_code != 200:
_LOGGER.error(
"Connection failed with http code %s", res.status_code)
return False
try:
self._userid = res.cookies['userid']
return True
except KeyError:
_LOGGER.error("Failed to log in to router")
return False
def _update_info(self):
"""Get ARP from router."""
_LOGGER.info("Fetching...")
if self._userid is None:
if not self._login():
_LOGGER.error("Could not obtain a user ID from the router")
return False
last_results = []
# doing a request
try:
res = requests.get(self._url, timeout=10, cookies={
'userid': self._userid
})
except requests.exceptions.Timeout:
_LOGGER.error(
"Connection to the router timed out at URL %s", self._url)
return False
if res.status_code != 200:
_LOGGER.error(
"Connection failed with http code %s", res.status_code)
return False
try:
result = res.json()
except ValueError:
# If json decoder could not parse the response
_LOGGER.error("Failed to parse response from router")
return False
# parsing response
for info in result:
mac = info['macAddr']
name = info['hostName']
# No address = no item :)
if mac is None:
continue
last_results.append(Device(mac.upper(), name))
self.last_results = last_results
_LOGGER.info("Request successful")
return True

View File

@ -11,7 +11,8 @@ import requests
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL) CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL)
@ -38,7 +39,7 @@ def get_scanner(hass, config):
return None return None
class LinksysAPDeviceScanner(object): class LinksysAPDeviceScanner(DeviceScanner):
"""This class queries a Linksys Access Point.""" """This class queries a Linksys Access Point."""
def __init__(self, config): def __init__(self, config):

View File

@ -0,0 +1,116 @@
"""
Support for the Meraki CMX location service.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.meraki/
"""
import asyncio
import logging
import json
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (HTTP_BAD_REQUEST, HTTP_UNPROCESSABLE_ENTITY)
from homeassistant.core import callback
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.device_tracker import (
PLATFORM_SCHEMA, SOURCE_TYPE_ROUTER)
CONF_VALIDATOR = 'validator'
CONF_SECRET = 'secret'
DEPENDENCIES = ['http']
URL = '/api/meraki'
VERSION = '2.0'
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_VALIDATOR): cv.string,
vol.Required(CONF_SECRET): cv.string
})
@asyncio.coroutine
def async_setup_scanner(hass, config, async_see, discovery_info=None):
"""Set up an endpoint for the Meraki tracker."""
hass.http.register_view(
MerakiView(config, async_see))
return True
class MerakiView(HomeAssistantView):
"""View to handle Meraki requests."""
url = URL
name = 'api:meraki'
def __init__(self, config, async_see):
"""Initialize Meraki URL endpoints."""
self.async_see = async_see
self.validator = config[CONF_VALIDATOR]
self.secret = config[CONF_SECRET]
@asyncio.coroutine
def get(self, request):
"""Meraki message received as GET."""
return self.validator
@asyncio.coroutine
def post(self, request):
"""Meraki CMX message received."""
try:
data = yield from request.json()
except ValueError:
return self.json_message('Invalid JSON', HTTP_BAD_REQUEST)
_LOGGER.debug("Meraki Data from Post: %s", json.dumps(data))
if not data.get('secret', False):
_LOGGER.error("secret invalid")
return self.json_message('No secret', HTTP_UNPROCESSABLE_ENTITY)
if data['secret'] != self.secret:
_LOGGER.error("Invalid Secret received from Meraki")
return self.json_message('Invalid secret',
HTTP_UNPROCESSABLE_ENTITY)
elif data['version'] != VERSION:
_LOGGER.error("Invalid API version: %s", data['version'])
return self.json_message('Invalid version',
HTTP_UNPROCESSABLE_ENTITY)
else:
_LOGGER.debug('Valid Secret')
if data['type'] not in ('DevicesSeen', 'BluetoothDevicesSeen'):
_LOGGER.error("Unknown Device %s", data['type'])
return self.json_message('Invalid device type',
HTTP_UNPROCESSABLE_ENTITY)
_LOGGER.debug("Processing %s", data['type'])
if len(data["data"]["observations"]) == 0:
_LOGGER.debug("No observations found")
return
self._handle(request.app['hass'], data)
@callback
def _handle(self, hass, data):
for i in data["data"]["observations"]:
data["data"]["secret"] = "hidden"
mac = i["clientMac"]
_LOGGER.debug("clientMac: %s", mac)
attrs = {}
if i.get('os', False):
attrs['os'] = i['os']
if i.get('manufacturer', False):
attrs['manufacturer'] = i['manufacturer']
if i.get('ipv4', False):
attrs['ipv4'] = i['ipv4']
if i.get('ipv6', False):
attrs['ipv6'] = i['ipv6']
if i.get('seenTime', False):
attrs['seenTime'] = i['seenTime']
if i.get('ssid', False):
attrs['ssid'] = i['ssid']
hass.async_add_job(self.async_see(
mac=mac,
source_type=SOURCE_TYPE_ROUTER,
attributes=attrs
))

View File

@ -367,6 +367,29 @@ def async_handle_transition_message(hass, context, message):
message['event']) message['event'])
@asyncio.coroutine
def async_handle_waypoint(hass, name_base, waypoint):
"""Handle a waypoint."""
name = waypoint['desc']
pretty_name = '{} - {}'.format(name_base, name)
lat = waypoint['lat']
lon = waypoint['lon']
rad = waypoint['rad']
# check zone exists
entity_id = zone_comp.ENTITY_ID_FORMAT.format(slugify(pretty_name))
# Check if state already exists
if hass.states.get(entity_id) is not None:
return
zone = zone_comp.Zone(hass, pretty_name, lat, lon, rad,
zone_comp.ICON_IMPORT, False)
zone.entity_id = entity_id
yield from zone.async_update_ha_state()
@HANDLERS.register('waypoint')
@HANDLERS.register('waypoints') @HANDLERS.register('waypoints')
@asyncio.coroutine @asyncio.coroutine
def async_handle_waypoints_message(hass, context, message): def async_handle_waypoints_message(hass, context, message):
@ -380,30 +403,17 @@ def async_handle_waypoints_message(hass, context, message):
if user not in context.waypoint_whitelist: if user not in context.waypoint_whitelist:
return return
wayps = message['waypoints'] if 'waypoints' in message:
wayps = message['waypoints']
else:
wayps = [message]
_LOGGER.info("Got %d waypoints from %s", len(wayps), message['topic']) _LOGGER.info("Got %d waypoints from %s", len(wayps), message['topic'])
name_base = ' '.join(_parse_topic(message['topic'])) name_base = ' '.join(_parse_topic(message['topic']))
for wayp in wayps: for wayp in wayps:
name = wayp['desc'] yield from async_handle_waypoint(hass, name_base, wayp)
pretty_name = '{} - {}'.format(name_base, name)
lat = wayp['lat']
lon = wayp['lon']
rad = wayp['rad']
# check zone exists
entity_id = zone_comp.ENTITY_ID_FORMAT.format(slugify(pretty_name))
# Check if state already exists
if hass.states.get(entity_id) is not None:
continue
zone = zone_comp.Zone(hass, pretty_name, lat, lon, rad,
zone_comp.ICON_IMPORT, False)
zone.entity_id = entity_id
yield from zone.async_update_ha_state()
@HANDLERS.register('encrypted') @HANDLERS.register('encrypted')
@ -423,10 +433,22 @@ def async_handle_encrypted_message(hass, context, message):
@HANDLERS.register('lwt') @HANDLERS.register('lwt')
@HANDLERS.register('configuration')
@HANDLERS.register('beacon')
@HANDLERS.register('cmd')
@HANDLERS.register('steps')
@HANDLERS.register('card')
@asyncio.coroutine @asyncio.coroutine
def async_handle_lwt_message(hass, context, message): def async_handle_not_impl_msg(hass, context, message):
"""Handle an lwt message.""" """Handle valid but not implemented message types."""
_LOGGER.debug('Not handling lwt message: %s', message) _LOGGER.debug('Not handling %s message: %s', message.get("_type"), message)
@asyncio.coroutine
def async_handle_unsupported_msg(hass, context, message):
"""Handle an unsupported or invalid message type."""
_LOGGER.warning('Received unsupported message type: %s.',
message.get('_type'))
@asyncio.coroutine @asyncio.coroutine
@ -434,11 +456,6 @@ def async_handle_message(hass, context, message):
"""Handle an OwnTracks message.""" """Handle an OwnTracks message."""
msgtype = message.get('_type') msgtype = message.get('_type')
handler = HANDLERS.get(msgtype) handler = HANDLERS.get(msgtype, async_handle_unsupported_msg)
if handler is None:
_LOGGER.warning(
'Received unsupported message type: %s.', msgtype)
return
yield from handler(hass, context, message) yield from handler(hass, context, message)

View File

@ -14,14 +14,14 @@ from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner) DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST from homeassistant.const import CONF_HOST
REQUIREMENTS = ['pysnmp==4.4.2']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pysnmp==4.4.1']
CONF_COMMUNITY = 'community'
CONF_AUTHKEY = 'authkey' CONF_AUTHKEY = 'authkey'
CONF_PRIVKEY = 'privkey'
CONF_BASEOID = 'baseoid' CONF_BASEOID = 'baseoid'
CONF_COMMUNITY = 'community'
CONF_PRIVKEY = 'privkey'
DEFAULT_COMMUNITY = 'public' DEFAULT_COMMUNITY = 'public'

View File

@ -0,0 +1,124 @@
"""
Support for Tile® Bluetooth trackers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.tile/
"""
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import (
CONF_USERNAME, CONF_MONITORED_VARIABLES, CONF_PASSWORD)
from homeassistant.helpers.event import track_utc_time_change
from homeassistant.util import slugify
from homeassistant.util.json import load_json, save_json
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pytile==1.0.0']
CLIENT_UUID_CONFIG_FILE = '.tile.conf'
DEFAULT_ICON = 'mdi:bluetooth'
DEVICE_TYPES = ['PHONE', 'TILE']
ATTR_ALTITUDE = 'altitude'
ATTR_CONNECTION_STATE = 'connection_state'
ATTR_IS_DEAD = 'is_dead'
ATTR_IS_LOST = 'is_lost'
ATTR_LAST_SEEN = 'last_seen'
ATTR_LAST_UPDATED = 'last_updated'
ATTR_RING_STATE = 'ring_state'
ATTR_VOIP_STATE = 'voip_state'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_MONITORED_VARIABLES):
vol.All(cv.ensure_list, [vol.In(DEVICE_TYPES)]),
})
def setup_scanner(hass, config: dict, see, discovery_info=None):
"""Validate the configuration and return a Tile scanner."""
TileDeviceScanner(hass, config, see)
return True
class TileDeviceScanner(DeviceScanner):
"""Define a device scanner for Tiles."""
def __init__(self, hass, config, see):
"""Initialize."""
from pytile import Client
_LOGGER.debug('Received configuration data: %s', config)
# Load the client UUID (if it exists):
config_data = load_json(hass.config.path(CLIENT_UUID_CONFIG_FILE))
if config_data:
_LOGGER.debug('Using existing client UUID')
self._client = Client(
config[CONF_USERNAME],
config[CONF_PASSWORD],
config_data['client_uuid'])
else:
_LOGGER.debug('Generating new client UUID')
self._client = Client(
config[CONF_USERNAME],
config[CONF_PASSWORD])
if not save_json(
hass.config.path(CLIENT_UUID_CONFIG_FILE),
{'client_uuid': self._client.client_uuid}):
_LOGGER.error("Failed to save configuration file")
_LOGGER.debug('Client UUID: %s', self._client.client_uuid)
_LOGGER.debug('User UUID: %s', self._client.user_uuid)
self._types = config.get(CONF_MONITORED_VARIABLES)
self.devices = {}
self.see = see
track_utc_time_change(
hass, self._update_info, second=range(0, 60, 30))
self._update_info()
def _update_info(self, now=None) -> None:
"""Update the device info."""
device_data = self._client.get_tiles(type_whitelist=self._types)
try:
self.devices = device_data['result']
except KeyError:
_LOGGER.warning('No Tiles found')
_LOGGER.debug(device_data)
return
for info in self.devices.values():
dev_id = 'tile_{0}'.format(slugify(info['name']))
lat = info['tileState']['latitude']
lon = info['tileState']['longitude']
attrs = {
ATTR_ALTITUDE: info['tileState']['altitude'],
ATTR_CONNECTION_STATE: info['tileState']['connection_state'],
ATTR_IS_DEAD: info['is_dead'],
ATTR_IS_LOST: info['tileState']['is_lost'],
ATTR_LAST_SEEN: info['tileState']['timestamp'],
ATTR_LAST_UPDATED: device_data['timestamp_ms'],
ATTR_RING_STATE: info['tileState']['ring_state'],
ATTR_VOIP_STATE: info['tileState']['voip_state'],
}
self.see(
dev_id=dev_id,
gps=(lat, lon),
attributes=attrs,
icon=DEFAULT_ICON
)

View File

@ -0,0 +1,134 @@
"""
Support for Unifi AP direct access.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.unifi_direct/
"""
import logging
import json
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME,
CONF_PORT)
REQUIREMENTS = ['pexpect==4.0.1']
_LOGGER = logging.getLogger(__name__)
DEFAULT_SSH_PORT = 22
UNIFI_COMMAND = 'mca-dump | tr -d "\n"'
UNIFI_SSID_TABLE = "vap_table"
UNIFI_CLIENT_TABLE = "sta_table"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_SSH_PORT): cv.port
})
# pylint: disable=unused-argument
def get_scanner(hass, config):
"""Validate the configuration and return a Unifi direct scanner."""
scanner = UnifiDeviceScanner(config[DOMAIN])
if not scanner.connected:
return False
return scanner
class UnifiDeviceScanner(DeviceScanner):
"""This class queries Unifi wireless access point."""
def __init__(self, config):
"""Initialize the scanner."""
self.host = config[CONF_HOST]
self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD]
self.port = config[CONF_PORT]
self.ssh = None
self.connected = False
self.last_results = {}
self._connect()
def scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
result = _response_to_json(self._get_update())
if result:
self.last_results = result
return self.last_results.keys()
def get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
hostname = next((
value.get('hostname') for key, value in self.last_results.items()
if key.upper() == device.upper()), None)
if hostname is not None:
hostname = str(hostname)
return hostname
def _connect(self):
"""Connect to the Unifi AP SSH server."""
from pexpect import pxssh, exceptions
self.ssh = pxssh.pxssh()
try:
self.ssh.login(self.host, self.username,
password=self.password, port=self.port)
self.connected = True
except exceptions.EOF:
_LOGGER.error("Connection refused. SSH enabled?")
self._disconnect()
def _disconnect(self):
"""Disconnect the current SSH connection."""
# pylint: disable=broad-except
try:
self.ssh.logout()
except Exception:
pass
finally:
self.ssh = None
self.connected = False
def _get_update(self):
from pexpect import pxssh
try:
if not self.connected:
self._connect()
self.ssh.sendline(UNIFI_COMMAND)
self.ssh.prompt()
return self.ssh.before
except pxssh.ExceptionPxssh as err:
_LOGGER.error("Unexpected SSH error: %s", str(err))
self._disconnect()
return None
except AssertionError as err:
_LOGGER.error("Connection to AP unavailable: %s", str(err))
self._disconnect()
return None
def _response_to_json(response):
try:
json_response = json.loads(str(response)[31:-1].replace("\\", ""))
_LOGGER.debug(str(json_response))
ssid_table = json_response.get(UNIFI_SSID_TABLE)
active_clients = {}
for ssid in ssid_table:
client_table = ssid.get(UNIFI_CLIENT_TABLE)
for client in client_table:
active_clients[client.get("mac")] = client
return active_clients
except ValueError:
_LOGGER.error("Failed to decode response from AP.")
return {}

View File

@ -35,6 +35,7 @@ SERVICE_AXIS = 'axis'
SERVICE_APPLE_TV = 'apple_tv' SERVICE_APPLE_TV = 'apple_tv'
SERVICE_WINK = 'wink' SERVICE_WINK = 'wink'
SERVICE_XIAOMI_GW = 'xiaomi_gw' SERVICE_XIAOMI_GW = 'xiaomi_gw'
SERVICE_TELLDUSLIVE = 'tellstick'
SERVICE_HANDLERS = { SERVICE_HANDLERS = {
SERVICE_HASS_IOS_APP: ('ios', None), SERVICE_HASS_IOS_APP: ('ios', None),
@ -46,6 +47,7 @@ SERVICE_HANDLERS = {
SERVICE_APPLE_TV: ('apple_tv', None), SERVICE_APPLE_TV: ('apple_tv', None),
SERVICE_WINK: ('wink', None), SERVICE_WINK: ('wink', None),
SERVICE_XIAOMI_GW: ('xiaomi_aqara', None), SERVICE_XIAOMI_GW: ('xiaomi_aqara', None),
SERVICE_TELLDUSLIVE: ('tellduslive', None),
'philips_hue': ('light', 'hue'), 'philips_hue': ('light', 'hue'),
'google_cast': ('media_player', 'cast'), 'google_cast': ('media_player', 'cast'),
'panasonic_viera': ('media_player', 'panasonic_viera'), 'panasonic_viera': ('media_player', 'panasonic_viera'),

View File

@ -0,0 +1,254 @@
"""
Support for Dominos Pizza ordering.
The Dominos Pizza component ceates a service which can be invoked to order
from their menu
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/dominos/.
"""
import logging
from datetime import timedelta
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components import http
from homeassistant.core import callback
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
# The domain of your component. Should be equal to the name of your component.
DOMAIN = 'dominos'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
ATTR_COUNTRY = 'country_code'
ATTR_FIRST_NAME = 'first_name'
ATTR_LAST_NAME = 'last_name'
ATTR_EMAIL = 'email'
ATTR_PHONE = 'phone'
ATTR_ADDRESS = 'address'
ATTR_ORDERS = 'orders'
ATTR_SHOW_MENU = 'show_menu'
ATTR_ORDER_ENTITY = 'order_entity_id'
ATTR_ORDER_NAME = 'name'
ATTR_ORDER_CODES = 'codes'
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10)
MIN_TIME_BETWEEN_STORE_UPDATES = timedelta(minutes=3330)
REQUIREMENTS = ['pizzapi==0.0.3']
DEPENDENCIES = ['http']
_ORDERS_SCHEMA = vol.Schema({
vol.Required(ATTR_ORDER_NAME): cv.string,
vol.Required(ATTR_ORDER_CODES): vol.All(cv.ensure_list, [cv.string]),
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(ATTR_COUNTRY): cv.string,
vol.Required(ATTR_FIRST_NAME): cv.string,
vol.Required(ATTR_LAST_NAME): cv.string,
vol.Required(ATTR_EMAIL): cv.string,
vol.Required(ATTR_PHONE): cv.string,
vol.Required(ATTR_ADDRESS): cv.string,
vol.Optional(ATTR_SHOW_MENU): cv.boolean,
vol.Optional(ATTR_ORDERS, default=[]): vol.All(
cv.ensure_list, [_ORDERS_SCHEMA]),
}),
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Set up is called when Home Assistant is loading our component."""
dominos = Dominos(hass, config)
component = EntityComponent(_LOGGER, DOMAIN, hass)
hass.data[DOMAIN] = {}
entities = []
conf = config[DOMAIN]
hass.services.register(DOMAIN, 'order', dominos.handle_order)
if conf.get(ATTR_SHOW_MENU):
hass.http.register_view(DominosProductListView(dominos))
for order_info in conf.get(ATTR_ORDERS):
order = DominosOrder(order_info, dominos)
entities.append(order)
if entities:
component.add_entities(entities)
# Return boolean to indicate that initialization was successfully.
return True
class Dominos():
"""Main Dominos service."""
def __init__(self, hass, config):
"""Set up main service."""
conf = config[DOMAIN]
from pizzapi import Address, Customer
from pizzapi.address import StoreException
self.hass = hass
self.customer = Customer(
conf.get(ATTR_FIRST_NAME),
conf.get(ATTR_LAST_NAME),
conf.get(ATTR_EMAIL),
conf.get(ATTR_PHONE),
conf.get(ATTR_ADDRESS))
self.address = Address(
*self.customer.address.split(','),
country=conf.get(ATTR_COUNTRY))
self.country = conf.get(ATTR_COUNTRY)
try:
self.closest_store = self.address.closest_store()
except StoreException:
self.closest_store = None
def handle_order(self, call):
"""Handle ordering pizza."""
entity_ids = call.data.get(ATTR_ORDER_ENTITY, None)
target_orders = [order for order in self.hass.data[DOMAIN]['entities']
if order.entity_id in entity_ids]
for order in target_orders:
order.place()
@Throttle(MIN_TIME_BETWEEN_STORE_UPDATES)
def update_closest_store(self):
"""Update the shared closest store (if open)."""
from pizzapi.address import StoreException
try:
self.closest_store = self.address.closest_store()
return True
except StoreException:
self.closest_store = None
return False
def get_menu(self):
"""Return the products from the closest stores menu."""
self.update_closest_store()
if self.closest_store is None:
_LOGGER.warning('Cannot get menu. Store may be closed')
return []
else:
menu = self.closest_store.get_menu()
product_entries = []
for product in menu.products:
item = {}
if isinstance(product.menu_data['Variants'], list):
variants = ', '.join(product.menu_data['Variants'])
else:
variants = product.menu_data['Variants']
item['name'] = product.name
item['variants'] = variants
product_entries.append(item)
return product_entries
class DominosProductListView(http.HomeAssistantView):
"""View to retrieve product list content."""
url = '/api/dominos'
name = "api:dominos"
def __init__(self, dominos):
"""Initialize suite view."""
self.dominos = dominos
@callback
def get(self, request):
"""Retrieve if API is running."""
return self.json(self.dominos.get_menu())
class DominosOrder(Entity):
"""Represents a Dominos order entity."""
def __init__(self, order_info, dominos):
"""Set up the entity."""
self._name = order_info['name']
self._product_codes = order_info['codes']
self._orderable = False
self.dominos = dominos
@property
def name(self):
"""Return the orders name."""
return self._name
@property
def product_codes(self):
"""Return the orders product codes."""
return self._product_codes
@property
def orderable(self):
"""Return the true if orderable."""
return self._orderable
@property
def state(self):
"""Return the state either closed, orderable or unorderable."""
if self.dominos.closest_store is None:
return 'closed'
else:
return 'orderable' if self._orderable else 'unorderable'
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Update the order state and refreshes the store."""
from pizzapi.address import StoreException
try:
self.dominos.update_closest_store()
except StoreException:
self._orderable = False
return
try:
order = self.order()
order.pay_with()
self._orderable = True
except StoreException:
self._orderable = False
def order(self):
"""Create the order object."""
from pizzapi import Order
from pizzapi.address import StoreException
if self.dominos.closest_store is None:
raise StoreException
order = Order(
self.dominos.closest_store,
self.dominos.customer,
self.dominos.address,
self.dominos.country)
for code in self._product_codes:
order.add_item(code)
return order
def place(self):
"""Place the order."""
from pizzapi.address import StoreException
try:
order = self.order()
order.place()
except StoreException:
self._orderable = False
_LOGGER.warning(
'Attempted to order Dominos - Order invalid or store closed')

View File

@ -6,7 +6,7 @@ import voluptuous as vol
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['DoorBirdPy==0.0.4'] REQUIREMENTS = ['DoorBirdPy==0.1.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -77,8 +77,13 @@ def setup(hass, config):
req = requests.get(url, stream=True, timeout=10) req = requests.get(url, stream=True, timeout=10)
if req.status_code == 200: if req.status_code != 200:
_LOGGER.warning(
"downloading '%s' failed, stauts_code=%d",
url,
req.status_code)
else:
if filename is None and \ if filename is None and \
'content-disposition' in req.headers: 'content-disposition' in req.headers:
match = re.findall(r"filename=(\S+)", match = re.findall(r"filename=(\S+)",
@ -121,13 +126,13 @@ def setup(hass, config):
final_path = "{}_{}.{}".format(path, tries, ext) final_path = "{}_{}.{}".format(path, tries, ext)
_LOGGER.info("%s -> %s", url, final_path) _LOGGER.debug("%s -> %s", url, final_path)
with open(final_path, 'wb') as fil: with open(final_path, 'wb') as fil:
for chunk in req.iter_content(1024): for chunk in req.iter_content(1024):
fil.write(chunk) fil.write(chunk)
_LOGGER.info("Downloading of %s done", url) _LOGGER.debug("Downloading of %s done", url)
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
_LOGGER.exception("ConnectionError occurred for %s", url) _LOGGER.exception("ConnectionError occurred for %s", url)

View File

@ -14,8 +14,9 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
from homeassistant.const import CONF_API_KEY from homeassistant.const import CONF_API_KEY
from homeassistant.util import Throttle from homeassistant.util import Throttle
from homeassistant.util.json import save_json
REQUIREMENTS = ['python-ecobee-api==0.0.10'] REQUIREMENTS = ['python-ecobee-api==0.0.14']
_CONFIGURING = {} _CONFIGURING = {}
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -81,6 +82,7 @@ def setup_ecobee(hass, network, config):
hass, 'climate', DOMAIN, {'hold_temp': hold_temp}, config) hass, 'climate', DOMAIN, {'hold_temp': hold_temp}, config)
discovery.load_platform(hass, 'sensor', DOMAIN, {}, config) discovery.load_platform(hass, 'sensor', DOMAIN, {}, config)
discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config) discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config)
discovery.load_platform(hass, 'weather', DOMAIN, {}, config)
class EcobeeData(object): class EcobeeData(object):
@ -110,12 +112,10 @@ def setup(hass, config):
if 'ecobee' in _CONFIGURING: if 'ecobee' in _CONFIGURING:
return return
from pyecobee import config_from_file
# Create ecobee.conf if it doesn't exist # Create ecobee.conf if it doesn't exist
if not os.path.isfile(hass.config.path(ECOBEE_CONFIG_FILE)): if not os.path.isfile(hass.config.path(ECOBEE_CONFIG_FILE)):
jsonconfig = {"API_KEY": config[DOMAIN].get(CONF_API_KEY)} jsonconfig = {"API_KEY": config[DOMAIN].get(CONF_API_KEY)}
config_from_file(hass.config.path(ECOBEE_CONFIG_FILE), jsonconfig) save_json(hass.config.path(ECOBEE_CONFIG_FILE), jsonconfig)
NETWORK = EcobeeData(hass.config.path(ECOBEE_CONFIG_FILE)) NETWORK = EcobeeData(hass.config.path(ECOBEE_CONFIG_FILE))

View File

@ -5,7 +5,6 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/emulated_hue/ https://home-assistant.io/components/emulated_hue/
""" """
import asyncio import asyncio
import json
import logging import logging
import voluptuous as vol import voluptuous as vol
@ -16,8 +15,10 @@ from homeassistant.const import (
) )
from homeassistant.components.http import REQUIREMENTS # NOQA from homeassistant.components.http import REQUIREMENTS # NOQA
from homeassistant.components.http import HomeAssistantWSGI from homeassistant.components.http import HomeAssistantWSGI
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.deprecation import get_deprecated from homeassistant.helpers.deprecation import get_deprecated
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util.json import load_json, save_json
from .hue_api import ( from .hue_api import (
HueUsernameView, HueAllLightsStateView, HueOneLightStateView, HueUsernameView, HueAllLightsStateView, HueOneLightStateView,
HueOneLightChangeView) HueOneLightChangeView)
@ -136,7 +137,7 @@ class Config(object):
self.host_ip_addr = conf.get(CONF_HOST_IP) self.host_ip_addr = conf.get(CONF_HOST_IP)
if self.host_ip_addr is None: if self.host_ip_addr is None:
self.host_ip_addr = util.get_local_ip() self.host_ip_addr = util.get_local_ip()
_LOGGER.warning( _LOGGER.info(
"Listen IP address not specified, auto-detected address is %s", "Listen IP address not specified, auto-detected address is %s",
self.host_ip_addr) self.host_ip_addr)
@ -144,7 +145,7 @@ class Config(object):
self.listen_port = conf.get(CONF_LISTEN_PORT) self.listen_port = conf.get(CONF_LISTEN_PORT)
if not isinstance(self.listen_port, int): if not isinstance(self.listen_port, int):
self.listen_port = DEFAULT_LISTEN_PORT self.listen_port = DEFAULT_LISTEN_PORT
_LOGGER.warning( _LOGGER.info(
"Listen port not specified, defaulting to %s", "Listen port not specified, defaulting to %s",
self.listen_port) self.listen_port)
@ -187,7 +188,7 @@ class Config(object):
return entity_id return entity_id
if self.numbers is None: if self.numbers is None:
self.numbers = self._load_numbers_json() self.numbers = _load_json(self.hass.config.path(NUMBERS_FILE))
# Google Home # Google Home
for number, ent_id in self.numbers.items(): for number, ent_id in self.numbers.items():
@ -198,7 +199,7 @@ class Config(object):
if self.numbers: if self.numbers:
number = str(max(int(k) for k in self.numbers) + 1) number = str(max(int(k) for k in self.numbers) + 1)
self.numbers[number] = entity_id self.numbers[number] = entity_id
self._save_numbers_json() save_json(self.hass.config.path(NUMBERS_FILE), self.numbers)
return number return number
def number_to_entity_id(self, number): def number_to_entity_id(self, number):
@ -207,7 +208,7 @@ class Config(object):
return number return number
if self.numbers is None: if self.numbers is None:
self.numbers = self._load_numbers_json() self.numbers = _load_json(self.hass.config.path(NUMBERS_FILE))
# Google Home # Google Home
assert isinstance(number, str) assert isinstance(number, str)
@ -244,25 +245,11 @@ class Config(object):
return is_default_exposed or expose return is_default_exposed or expose
def _load_numbers_json(self):
"""Set up helper method to load numbers json."""
try:
with open(self.hass.config.path(NUMBERS_FILE),
encoding='utf-8') as fil:
return json.loads(fil.read())
except (OSError, ValueError) as err:
# OSError if file not found or unaccessible/no permissions
# ValueError if could not parse JSON
if not isinstance(err, FileNotFoundError):
_LOGGER.warning("Failed to open %s: %s", NUMBERS_FILE, err)
return {}
def _save_numbers_json(self): def _load_json(filename):
"""Set up helper method to save numbers json.""" """Wrapper, because we actually want to handle invalid json."""
try: try:
with open(self.hass.config.path(NUMBERS_FILE), 'w', return load_json(filename)
encoding='utf-8') as fil: except HomeAssistantError:
fil.write(json.dumps(self.numbers)) pass
except OSError as err: return {}
# OSError if file write permissions
_LOGGER.warning("Failed to write %s: %s", NUMBERS_FILE, err)

View File

@ -4,9 +4,7 @@ Support for Insteon fans via local hub control.
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/fan.insteon_local/ https://home-assistant.io/components/fan.insteon_local/
""" """
import json
import logging import logging
import os
from datetime import timedelta from datetime import timedelta
from homeassistant.components.fan import ( from homeassistant.components.fan import (
@ -14,6 +12,7 @@ from homeassistant.components.fan import (
SUPPORT_SET_SPEED, FanEntity) SUPPORT_SET_SPEED, FanEntity)
from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity import ToggleEntity
import homeassistant.util as util import homeassistant.util as util
from homeassistant.util.json import load_json, save_json
_CONFIGURING = {} _CONFIGURING = {}
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -33,7 +32,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Insteon local fan platform.""" """Set up the Insteon local fan platform."""
insteonhub = hass.data['insteon_local'] insteonhub = hass.data['insteon_local']
conf_fans = config_from_file(hass.config.path(INSTEON_LOCAL_FANS_CONF)) conf_fans = load_json(hass.config.path(INSTEON_LOCAL_FANS_CONF))
if conf_fans: if conf_fans:
for device_id in conf_fans: for device_id in conf_fans:
setup_fan(device_id, conf_fans[device_id], insteonhub, hass, setup_fan(device_id, conf_fans[device_id], insteonhub, hass,
@ -88,44 +87,16 @@ def setup_fan(device_id, name, insteonhub, hass, add_devices_callback):
configurator.request_done(request_id) configurator.request_done(request_id)
_LOGGER.info("Device configuration done!") _LOGGER.info("Device configuration done!")
conf_fans = config_from_file(hass.config.path(INSTEON_LOCAL_FANS_CONF)) conf_fans = load_json(hass.config.path(INSTEON_LOCAL_FANS_CONF))
if device_id not in conf_fans: if device_id not in conf_fans:
conf_fans[device_id] = name conf_fans[device_id] = name
if not config_from_file( save_json(hass.config.path(INSTEON_LOCAL_FANS_CONF), conf_fans)
hass.config.path(INSTEON_LOCAL_FANS_CONF),
conf_fans):
_LOGGER.error("Failed to save configuration file")
device = insteonhub.fan(device_id) device = insteonhub.fan(device_id)
add_devices_callback([InsteonLocalFanDevice(device, name)]) add_devices_callback([InsteonLocalFanDevice(device, name)])
def config_from_file(filename, config=None):
"""Small configuration file management function."""
if config:
# We're writing configuration
try:
with open(filename, 'w') as fdesc:
fdesc.write(json.dumps(config))
except IOError as error:
_LOGGER.error('Saving config file failed: %s', error)
return False
return True
else:
# We're reading config
if os.path.isfile(filename):
try:
with open(filename, 'r') as fdesc:
return json.loads(fdesc.read())
except IOError as error:
_LOGGER.error("Reading configuration file failed: %s", error)
# This won't work yet
return False
else:
return {}
class InsteonLocalFanDevice(FanEntity): class InsteonLocalFanDevice(FanEntity):
"""An abstract Class for an Insteon node.""" """An abstract Class for an Insteon node."""

View File

@ -31,7 +31,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
}) })
REQUIREMENTS = ['python-miio==0.3.1'] REQUIREMENTS = ['python-miio==0.3.2']
ATTR_TEMPERATURE = 'temperature' ATTR_TEMPERATURE = 'temperature'
ATTR_HUMIDITY = 'humidity' ATTR_HUMIDITY = 'humidity'

View File

@ -9,9 +9,11 @@ import hashlib
import json import json
import logging import logging
import os import os
from urllib.parse import urlparse
from aiohttp import web from aiohttp import web
import voluptuous as vol import voluptuous as vol
import jinja2
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
@ -21,21 +23,20 @@ from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
REQUIREMENTS = ['home-assistant-frontend==20171106.0'] REQUIREMENTS = ['home-assistant-frontend==20171206.0', 'user-agents==1.1.0']
DOMAIN = 'frontend' DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api'] DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
URL_PANEL_COMPONENT = '/frontend/panels/{}.html'
URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html' URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html'
POLYMER_PATH = os.path.join(os.path.dirname(__file__),
'home-assistant-polymer/')
FINAL_PATH = os.path.join(POLYMER_PATH, 'final')
CONF_THEMES = 'themes' CONF_THEMES = 'themes'
CONF_EXTRA_HTML_URL = 'extra_html_url' CONF_EXTRA_HTML_URL = 'extra_html_url'
CONF_EXTRA_HTML_URL_ES5 = 'extra_html_url_es5'
CONF_FRONTEND_REPO = 'development_repo' CONF_FRONTEND_REPO = 'development_repo'
CONF_JS_VERSION = 'javascript_version'
JS_DEFAULT_OPTION = 'es5'
JS_OPTIONS = ['es5', 'latest', 'auto']
DEFAULT_THEME_COLOR = '#03A9F4' DEFAULT_THEME_COLOR = '#03A9F4'
@ -61,15 +62,15 @@ for size in (192, 384, 512, 1024):
DATA_FINALIZE_PANEL = 'frontend_finalize_panel' DATA_FINALIZE_PANEL = 'frontend_finalize_panel'
DATA_PANELS = 'frontend_panels' DATA_PANELS = 'frontend_panels'
DATA_JS_VERSION = 'frontend_js_version'
DATA_EXTRA_HTML_URL = 'frontend_extra_html_url' DATA_EXTRA_HTML_URL = 'frontend_extra_html_url'
DATA_EXTRA_HTML_URL_ES5 = 'frontend_extra_html_url_es5'
DATA_THEMES = 'frontend_themes' DATA_THEMES = 'frontend_themes'
DATA_DEFAULT_THEME = 'frontend_default_theme' DATA_DEFAULT_THEME = 'frontend_default_theme'
DEFAULT_THEME = 'default' DEFAULT_THEME = 'default'
PRIMARY_COLOR = 'primary-color' PRIMARY_COLOR = 'primary-color'
# To keep track we don't register a component twice (gives a warning)
# _REGISTERED_COMPONENTS = set()
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
@ -80,6 +81,10 @@ CONFIG_SCHEMA = vol.Schema({
}), }),
vol.Optional(CONF_EXTRA_HTML_URL): vol.Optional(CONF_EXTRA_HTML_URL):
vol.All(cv.ensure_list, [cv.string]), vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_EXTRA_HTML_URL_ES5):
vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_JS_VERSION, default=JS_DEFAULT_OPTION):
vol.In(JS_OPTIONS)
}), }),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@ -102,8 +107,9 @@ class AbstractPanel:
# Title to show in the sidebar (optional) # Title to show in the sidebar (optional)
sidebar_title = None sidebar_title = None
# Url to the webcomponent # Url to the webcomponent (depending on JS version)
webcomponent_url = None webcomponent_url_es5 = None
webcomponent_url_latest = None
# Url to show the panel in the frontend # Url to show the panel in the frontend
frontend_url_path = None frontend_url_path = None
@ -135,16 +141,20 @@ class AbstractPanel:
'get', '/{}/{{extra:.+}}'.format(self.frontend_url_path), 'get', '/{}/{{extra:.+}}'.format(self.frontend_url_path),
index_view.get) index_view.get)
def as_dict(self): def to_response(self, hass, request):
"""Panel as dictionary.""" """Panel as dictionary."""
return { result = {
'component_name': self.component_name, 'component_name': self.component_name,
'icon': self.sidebar_icon, 'icon': self.sidebar_icon,
'title': self.sidebar_title, 'title': self.sidebar_title,
'url': self.webcomponent_url,
'url_path': self.frontend_url_path, 'url_path': self.frontend_url_path,
'config': self.config, 'config': self.config,
} }
if _is_latest(hass.data[DATA_JS_VERSION], request):
result['url'] = self.webcomponent_url_latest
else:
result['url'] = self.webcomponent_url_es5
return result
class BuiltInPanel(AbstractPanel): class BuiltInPanel(AbstractPanel):
@ -166,19 +176,21 @@ class BuiltInPanel(AbstractPanel):
If frontend_repository_path is set, will be prepended to path of If frontend_repository_path is set, will be prepended to path of
built-in components. built-in components.
""" """
panel_path = 'panels/ha-panel-{}.html'.format(self.component_name)
if frontend_repository_path is None: if frontend_repository_path is None:
import hass_frontend import hass_frontend
import hass_frontend_es5
self.webcomponent_url = \ self.webcomponent_url_latest = \
'/static/panels/ha-panel-{}-{}.html'.format( '/frontend_latest/panels/ha-panel-{}-{}.html'.format(
self.component_name, self.component_name,
hass_frontend.FINGERPRINTS[panel_path]) hass_frontend.FINGERPRINTS[self.component_name])
self.webcomponent_url_es5 = \
'/frontend_es5/panels/ha-panel-{}-{}.html'.format(
self.component_name,
hass_frontend_es5.FINGERPRINTS[self.component_name])
else: else:
# Dev mode # Dev mode
self.webcomponent_url = \ self.webcomponent_url_es5 = self.webcomponent_url_latest = \
'/home-assistant-polymer/panels/{}/ha-panel-{}.html'.format( '/home-assistant-polymer/panels/{}/ha-panel-{}.html'.format(
self.component_name, self.component_name) self.component_name, self.component_name)
@ -208,18 +220,20 @@ class ExternalPanel(AbstractPanel):
""" """
try: try:
if self.md5 is None: if self.md5 is None:
yield from hass.async_add_job(_fingerprint, self.path) self.md5 = yield from hass.async_add_job(
_fingerprint, self.path)
except OSError: except OSError:
_LOGGER.error('Cannot find or access %s at %s', _LOGGER.error('Cannot find or access %s at %s',
self.component_name, self.path) self.component_name, self.path)
hass.data[DATA_PANELS].pop(self.frontend_url_path) hass.data[DATA_PANELS].pop(self.frontend_url_path)
return
self.webcomponent_url = \ self.webcomponent_url_es5 = self.webcomponent_url_latest = \
URL_PANEL_COMPONENT_FP.format(self.component_name, self.md5) URL_PANEL_COMPONENT_FP.format(self.component_name, self.md5)
if self.component_name not in self.REGISTERED_COMPONENTS: if self.component_name not in self.REGISTERED_COMPONENTS:
hass.http.register_static_path( hass.http.register_static_path(
self.webcomponent_url, self.path, self.webcomponent_url_latest, self.path,
# if path is None, we're in prod mode, so cache static assets # if path is None, we're in prod mode, so cache static assets
frontend_repository_path is None) frontend_repository_path is None)
self.REGISTERED_COMPONENTS.add(self.component_name) self.REGISTERED_COMPONENTS.add(self.component_name)
@ -259,11 +273,12 @@ def async_register_panel(hass, component_name, path, md5=None,
@bind_hass @bind_hass
@callback @callback
def add_extra_html_url(hass, url): def add_extra_html_url(hass, url, es5=False):
"""Register extra html url to load.""" """Register extra html url to load."""
url_set = hass.data.get(DATA_EXTRA_HTML_URL) key = DATA_EXTRA_HTML_URL_ES5 if es5 else DATA_EXTRA_HTML_URL
url_set = hass.data.get(key)
if url_set is None: if url_set is None:
url_set = hass.data[DATA_EXTRA_HTML_URL] = set() url_set = hass.data[key] = set()
url_set.add(url) url_set.add(url)
@ -281,31 +296,50 @@ def async_setup(hass, config):
repo_path = conf.get(CONF_FRONTEND_REPO) repo_path = conf.get(CONF_FRONTEND_REPO)
is_dev = repo_path is not None is_dev = repo_path is not None
hass.data[DATA_JS_VERSION] = js_version = conf.get(CONF_JS_VERSION)
if is_dev: if is_dev:
hass.http.register_static_path( hass.http.register_static_path(
"/home-assistant-polymer", repo_path, False) "/home-assistant-polymer", repo_path, False)
hass.http.register_static_path( hass.http.register_static_path(
"/static/translations", "/static/translations",
os.path.join(repo_path, "build/translations"), False) os.path.join(repo_path, "build-translations/output"), False)
sw_path = os.path.join(repo_path, "build/service_worker.js") sw_path_es5 = os.path.join(repo_path, "build-es5/service_worker.js")
sw_path_latest = os.path.join(repo_path, "build/service_worker.js")
static_path = os.path.join(repo_path, 'hass_frontend') static_path = os.path.join(repo_path, 'hass_frontend')
frontend_es5_path = os.path.join(repo_path, 'build-es5')
frontend_latest_path = os.path.join(repo_path, 'build')
else: else:
import hass_frontend import hass_frontend
frontend_path = hass_frontend.where() import hass_frontend_es5
sw_path = os.path.join(frontend_path, "service_worker.js") sw_path_es5 = os.path.join(hass_frontend_es5.where(),
static_path = frontend_path "service_worker.js")
sw_path_latest = os.path.join(hass_frontend.where(),
"service_worker.js")
# /static points to dir with files that are JS-type agnostic.
# ES5 files are served from /frontend_es5.
# ES6 files are served from /frontend_latest.
static_path = hass_frontend.where()
frontend_es5_path = hass_frontend_es5.where()
frontend_latest_path = static_path
hass.http.register_static_path("/service_worker.js", sw_path, False) hass.http.register_static_path(
"/service_worker_es5.js", sw_path_es5, False)
hass.http.register_static_path(
"/service_worker.js", sw_path_latest, False)
hass.http.register_static_path( hass.http.register_static_path(
"/robots.txt", os.path.join(static_path, "robots.txt"), not is_dev) "/robots.txt", os.path.join(static_path, "robots.txt"), not is_dev)
hass.http.register_static_path("/static", static_path, not is_dev) hass.http.register_static_path("/static", static_path, not is_dev)
hass.http.register_static_path(
"/frontend_latest", frontend_latest_path, not is_dev)
hass.http.register_static_path(
"/frontend_es5", frontend_es5_path, not is_dev)
local = hass.config.path('www') local = hass.config.path('www')
if os.path.isdir(local): if os.path.isdir(local):
hass.http.register_static_path("/local", local, not is_dev) hass.http.register_static_path("/local", local, not is_dev)
index_view = IndexView(is_dev) index_view = IndexView(repo_path, js_version)
hass.http.register_view(index_view) hass.http.register_view(index_view)
@asyncio.coroutine @asyncio.coroutine
@ -329,9 +363,13 @@ def async_setup(hass, config):
if DATA_EXTRA_HTML_URL not in hass.data: if DATA_EXTRA_HTML_URL not in hass.data:
hass.data[DATA_EXTRA_HTML_URL] = set() hass.data[DATA_EXTRA_HTML_URL] = set()
if DATA_EXTRA_HTML_URL_ES5 not in hass.data:
hass.data[DATA_EXTRA_HTML_URL_ES5] = set()
for url in conf.get(CONF_EXTRA_HTML_URL, []): for url in conf.get(CONF_EXTRA_HTML_URL, []):
add_extra_html_url(hass, url) add_extra_html_url(hass, url, False)
for url in conf.get(CONF_EXTRA_HTML_URL_ES5, []):
add_extra_html_url(hass, url, True)
yield from async_setup_themes(hass, conf.get(CONF_THEMES)) yield from async_setup_themes(hass, conf.get(CONF_THEMES))
@ -405,40 +443,41 @@ class IndexView(HomeAssistantView):
requires_auth = False requires_auth = False
extra_urls = ['/states', '/states/{extra}'] extra_urls = ['/states', '/states/{extra}']
def __init__(self, use_repo): def __init__(self, repo_path, js_option):
"""Initialize the frontend view.""" """Initialize the frontend view."""
from jinja2 import FileSystemLoader, Environment self.repo_path = repo_path
self.js_option = js_option
self._template_cache = {}
self.use_repo = use_repo def get_template(self, latest):
self.templates = Environment( """Get template."""
autoescape=True, if self.repo_path is not None:
loader=FileSystemLoader( root = self.repo_path
os.path.join(os.path.dirname(__file__), 'templates/') elif latest:
) import hass_frontend
) root = hass_frontend.where()
else:
import hass_frontend_es5
root = hass_frontend_es5.where()
tpl = self._template_cache.get(root)
if tpl is None:
with open(os.path.join(root, 'index.html')) as file:
tpl = jinja2.Template(file.read())
# Cache template if not running from repository
if self.repo_path is None:
self._template_cache[root] = tpl
return tpl
@asyncio.coroutine @asyncio.coroutine
def get(self, request, extra=None): def get(self, request, extra=None):
"""Serve the index view.""" """Serve the index view."""
hass = request.app['hass'] hass = request.app['hass']
latest = self.repo_path is not None or \
if self.use_repo: _is_latest(self.js_option, request)
core_url = '/home-assistant-polymer/build/core.js'
compatibility_url = \
'/home-assistant-polymer/build/compatibility.js'
ui_url = '/home-assistant-polymer/src/home-assistant.html'
icons_fp = ''
icons_url = '/static/mdi.html'
else:
import hass_frontend
core_url = '/static/core-{}.js'.format(
hass_frontend.FINGERPRINTS['core.js'])
compatibility_url = '/static/compatibility-{}.js'.format(
hass_frontend.FINGERPRINTS['compatibility.js'])
ui_url = '/static/frontend-{}.html'.format(
hass_frontend.FINGERPRINTS['frontend.html'])
icons_fp = '-{}'.format(hass_frontend.FINGERPRINTS['mdi.html'])
icons_url = '/static/mdi{}.html'.format(icons_fp)
if request.path == '/': if request.path == '/':
panel = 'states' panel = 'states'
@ -447,28 +486,27 @@ class IndexView(HomeAssistantView):
if panel == 'states': if panel == 'states':
panel_url = '' panel_url = ''
elif latest:
panel_url = hass.data[DATA_PANELS][panel].webcomponent_url_latest
else: else:
panel_url = hass.data[DATA_PANELS][panel].webcomponent_url panel_url = hass.data[DATA_PANELS][panel].webcomponent_url_es5
no_auth = 'true' no_auth = '1'
if hass.config.api.api_password and not is_trusted_ip(request): if hass.config.api.api_password and not is_trusted_ip(request):
# do not try to auto connect on load # do not try to auto connect on load
no_auth = 'false' no_auth = '0'
template = yield from hass.async_add_job( template = yield from hass.async_add_job(self.get_template, latest)
self.templates.get_template, 'index.html')
extra_key = DATA_EXTRA_HTML_URL if latest else DATA_EXTRA_HTML_URL_ES5
# pylint is wrong
# pylint: disable=no-member
# This is a jinja2 template, not a HA template so we call 'render'.
resp = template.render( resp = template.render(
core_url=core_url, ui_url=ui_url, no_auth=no_auth,
compatibility_url=compatibility_url, no_auth=no_auth, panel_url=panel_url,
icons_url=icons_url, icons=icons_fp, panels=hass.data[DATA_PANELS],
panel_url=panel_url, panels=hass.data[DATA_PANELS],
dev_mode=self.use_repo,
theme_color=MANIFEST_JSON['theme_color'], theme_color=MANIFEST_JSON['theme_color'],
extra_urls=hass.data[DATA_EXTRA_HTML_URL]) extra_urls=hass.data[extra_key],
)
return web.Response(text=resp, content_type='text/html') return web.Response(text=resp, content_type='text/html')
@ -483,8 +521,8 @@ class ManifestJSONView(HomeAssistantView):
@asyncio.coroutine @asyncio.coroutine
def get(self, request): # pylint: disable=no-self-use def get(self, request): # pylint: disable=no-self-use
"""Return the manifest.json.""" """Return the manifest.json."""
msg = json.dumps(MANIFEST_JSON, sort_keys=True).encode('UTF-8') msg = json.dumps(MANIFEST_JSON, sort_keys=True)
return web.Response(body=msg, content_type="application/manifest+json") return web.Response(text=msg, content_type="application/manifest+json")
class ThemesView(HomeAssistantView): class ThemesView(HomeAssistantView):
@ -509,3 +547,46 @@ def _fingerprint(path):
"""Fingerprint a file.""" """Fingerprint a file."""
with open(path) as fil: with open(path) as fil:
return hashlib.md5(fil.read().encode('utf-8')).hexdigest() return hashlib.md5(fil.read().encode('utf-8')).hexdigest()
def _is_latest(js_option, request):
"""
Return whether we should serve latest untranspiled code.
Set according to user's preference and URL override.
"""
if request is None:
return js_option == 'latest'
# latest in query
if 'latest' in request.query or (
request.headers.get('Referer') and
'latest' in urlparse(request.headers['Referer']).query):
return True
# es5 in query
if 'es5' in request.query or (
request.headers.get('Referer') and
'es5' in urlparse(request.headers['Referer']).query):
return False
# non-auto option in config
if js_option != 'auto':
return js_option == 'latest'
from user_agents import parse
useragent = parse(request.headers.get('User-Agent'))
# on iOS every browser is a Safari which we support from version 10.
if useragent.os.family == 'iOS':
return useragent.os.version[0] >= 10
family_min_version = {
'Chrome': 50, # Probably can reduce this
'Firefox': 43, # Array.protopype.includes added in 43
'Opera': 40, # Probably can reduce this
'Edge': 14, # Array.protopype.includes added in 14
'Safari': 10, # many features not supported by 9
}
version = family_min_version.get(useragent.browser.family)
return version and useragent.browser.version[0] >= version

View File

@ -1,118 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Home Assistant</title>
<link rel='manifest' href='/manifest.json'>
<link rel='icon' href='/static/icons/favicon.ico'>
<link rel='apple-touch-icon' sizes='180x180'
href='/static/icons/favicon-apple-180x180.png'>
<link rel="mask-icon" href="/static/icons/mask-icon.svg" color="#3fbbf4">
{% if not dev_mode %}
<link rel='preload' href='{{ core_url }}' as='script'/>
{% for panel in panels.values() -%}
<link rel='prefetch' href='{{ panel.webcomponent_url }}'>
{% endfor -%}
{% endif %}
<meta name='apple-mobile-web-app-capable' content='yes'>
<meta name="msapplication-square70x70logo" content="/static/icons/tile-win-70x70.png"/>
<meta name="msapplication-square150x150logo" content="/static/icons/tile-win-150x150.png"/>
<meta name="msapplication-wide310x150logo" content="/static/icons/tile-win-310x150.png"/>
<meta name="msapplication-square310x310logo" content="/static/icons/tile-win-310x310.png"/>
<meta name="msapplication-TileColor" content="#3fbbf4ff"/>
<meta name='mobile-web-app-capable' content='yes'>
<meta name='viewport' content='width=device-width, user-scalable=no'>
<meta name='theme-color' content='{{ theme_color }}'>
<style>
body {
font-family: 'Roboto', 'Noto', sans-serif;
font-weight: 400;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
margin: 0;
padding: 0;
}
#ha-init-skeleton::before {
display: block;
content: "";
height: 48px;
background-color: {{ theme_color }};
}
#ha-init-skeleton .message {
transition: font-size 2s;
font-size: 0;
padding: 24px;
}
#ha-init-skeleton.error .message {
font-size: 16px;
}
#ha-init-skeleton a {
color: {{ theme_color }};
text-decoration: none;
font-weight: bold;
}
</style>
<script>
function initError() {
document.getElementById('ha-init-skeleton').classList.add('error');
};
window.noAuth = {{ no_auth }};
window.Polymer = {
lazyRegister: true,
useNativeCSSProperties: true,
dom: 'shadow',
suppressTemplateNotifications: true,
suppressBindingNotifications: true,
};
</script>
</head>
<body>
<div id='ha-init-skeleton'>
<div class='message'>
Home Assistant had trouble<br>connecting to the server.<br><br>
<a href='/'>TRY AGAIN</a>
</div>
</div>
<home-assistant icons='{{ icons }}'></home-assistant>
{# <script src='/static/home-assistant-polymer/build/_demo_data_compiled.js'></script> #}
<script>
var compatibilityRequired = (
typeof Object.assign != 'function');
if (compatibilityRequired) {
var e = document.createElement('script');
e.onerror = initError;
e.src = '{{ compatibility_url }}';
document.head.appendChild(e);
}
</script>
<script src='{{ core_url }}'></script>
{% if not dev_mode %}
<script src='/static/custom-elements-es5-adapter.js'></script>
{% endif %}
<script>
var webComponentsSupported = (
'customElements' in window &&
'import' in document.createElement('link') &&
'content' in document.createElement('template'));
if (!webComponentsSupported) {
var e = document.createElement('script');
e.onerror = initError;
e.src = '/static/webcomponents-lite.js';
document.head.appendChild(e);
}
</script>
<link rel='import' href='{{ ui_url }}' onerror='initError()'>
{% if panel_url -%}
<link rel='import' href='{{ panel_url }}' onerror='initError()' async>
{% endif -%}
<link rel='import' href='{{ icons_url }}' async>
{% for extra_url in extra_urls -%}
<link rel='import' href='{{ extra_url }}' async>
{% endfor -%}
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@ -4,9 +4,13 @@ Support for Actions on Google Assistant Smart Home Control.
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/google_assistant/ https://home-assistant.io/components/google_assistant/
""" """
import os
import asyncio import asyncio
import logging import logging
import aiohttp
import async_timeout
import voluptuous as vol import voluptuous as vol
# Typing imports # Typing imports
@ -15,11 +19,16 @@ import voluptuous as vol
from homeassistant.core import HomeAssistant # NOQA from homeassistant.core import HomeAssistant # NOQA
from typing import Dict, Any # NOQA from typing import Dict, Any # NOQA
from homeassistant import config as conf_util
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.loader import bind_hass
from .const import ( from .const import (
DOMAIN, CONF_PROJECT_ID, CONF_CLIENT_ID, CONF_ACCESS_TOKEN, DOMAIN, CONF_PROJECT_ID, CONF_CLIENT_ID, CONF_ACCESS_TOKEN,
CONF_EXPOSE_BY_DEFAULT, CONF_EXPOSED_DOMAINS CONF_EXPOSE_BY_DEFAULT, CONF_EXPOSED_DOMAINS,
CONF_AGENT_USER_ID, CONF_API_KEY,
SERVICE_REQUEST_SYNC, REQUEST_SYNC_BASE_URL
) )
from .auth import GoogleAssistantAuthView from .auth import GoogleAssistantAuthView
from .http import GoogleAssistantView from .http import GoogleAssistantView
@ -28,6 +37,8 @@ _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']
DEFAULT_AGENT_USER_ID = 'home-assistant'
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
DOMAIN: { DOMAIN: {
@ -36,17 +47,57 @@ CONFIG_SCHEMA = vol.Schema(
vol.Required(CONF_ACCESS_TOKEN): cv.string, vol.Required(CONF_ACCESS_TOKEN): cv.string,
vol.Optional(CONF_EXPOSE_BY_DEFAULT): cv.boolean, vol.Optional(CONF_EXPOSE_BY_DEFAULT): cv.boolean,
vol.Optional(CONF_EXPOSED_DOMAINS): cv.ensure_list, vol.Optional(CONF_EXPOSED_DOMAINS): cv.ensure_list,
vol.Optional(CONF_AGENT_USER_ID,
default=DEFAULT_AGENT_USER_ID): cv.string,
vol.Optional(CONF_API_KEY): cv.string
} }
}, },
extra=vol.ALLOW_EXTRA) extra=vol.ALLOW_EXTRA)
@bind_hass
def request_sync(hass):
"""Request sync."""
hass.services.call(DOMAIN, SERVICE_REQUEST_SYNC)
@asyncio.coroutine @asyncio.coroutine
def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]):
"""Activate Google Actions component.""" """Activate Google Actions component."""
config = yaml_config.get(DOMAIN, {}) config = yaml_config.get(DOMAIN, {})
agent_user_id = config.get(CONF_AGENT_USER_ID)
api_key = config.get(CONF_API_KEY)
if api_key is not None:
descriptions = yield from hass.async_add_job(
conf_util.load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml')
)
hass.http.register_view(GoogleAssistantAuthView(hass, config)) hass.http.register_view(GoogleAssistantAuthView(hass, config))
hass.http.register_view(GoogleAssistantView(hass, config)) hass.http.register_view(GoogleAssistantView(hass, config))
@asyncio.coroutine
def request_sync_service_handler(call):
"""Handle request sync service calls."""
websession = async_get_clientsession(hass)
try:
with async_timeout.timeout(5, loop=hass.loop):
res = yield from websession.post(
REQUEST_SYNC_BASE_URL,
params={'key': api_key},
json={'agent_user_id': agent_user_id})
_LOGGER.info("Submitted request_sync request to Google")
res.raise_for_status()
except aiohttp.ClientResponseError:
body = yield from res.read()
_LOGGER.error(
'request_sync request failed: %d %s', res.status, body)
except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.error("Could not contact Google for request_sync")
# Register service only if api key is provided
if api_key is not None:
hass.services.async_register(
DOMAIN, SERVICE_REQUEST_SYNC, request_sync_service_handler,
descriptions.get(SERVICE_REQUEST_SYNC))
return True return True

View File

@ -13,6 +13,8 @@ CONF_PROJECT_ID = 'project_id'
CONF_ACCESS_TOKEN = 'access_token' CONF_ACCESS_TOKEN = 'access_token'
CONF_CLIENT_ID = 'client_id' CONF_CLIENT_ID = 'client_id'
CONF_ALIASES = 'aliases' CONF_ALIASES = 'aliases'
CONF_AGENT_USER_ID = 'agent_user_id'
CONF_API_KEY = 'api_key'
DEFAULT_EXPOSE_BY_DEFAULT = True DEFAULT_EXPOSE_BY_DEFAULT = True
DEFAULT_EXPOSED_DOMAINS = [ DEFAULT_EXPOSED_DOMAINS = [
@ -44,3 +46,7 @@ TYPE_LIGHT = PREFIX_TYPES + 'LIGHT'
TYPE_SWITCH = PREFIX_TYPES + 'SWITCH' TYPE_SWITCH = PREFIX_TYPES + 'SWITCH'
TYPE_SCENE = PREFIX_TYPES + 'SCENE' TYPE_SCENE = PREFIX_TYPES + 'SCENE'
TYPE_THERMOSTAT = PREFIX_TYPES + 'THERMOSTAT' TYPE_THERMOSTAT = PREFIX_TYPES + 'THERMOSTAT'
SERVICE_REQUEST_SYNC = 'request_sync'
HOMEGRAPH_URL = 'https://homegraph.googleapis.com/'
REQUEST_SYNC_BASE_URL = HOMEGRAPH_URL + 'v1/devices:requestSync'

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