diff --git a/.coveragerc b/.coveragerc
index a2c0dde77b1..48b45db347b 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -160,9 +160,6 @@ omit =
homeassistant/components/maxcube.py
homeassistant/components/*/maxcube.py
- homeassistant/components/mercedesme.py
- homeassistant/components/*/mercedesme.py
-
homeassistant/components/mochad.py
homeassistant/components/*/mochad.py
@@ -289,11 +286,9 @@ omit =
homeassistant/components/*/wink.py
homeassistant/components/xiaomi_aqara.py
- homeassistant/components/binary_sensor/xiaomi_aqara.py
- homeassistant/components/cover/xiaomi_aqara.py
- homeassistant/components/light/xiaomi_aqara.py
- homeassistant/components/sensor/xiaomi_aqara.py
- homeassistant/components/switch/xiaomi_aqara.py
+ homeassistant/components/*/xiaomi_aqara.py
+
+ homeassistant/components/*/xiaomi_miio.py
homeassistant/components/zabbix.py
homeassistant/components/*/zabbix.py
@@ -357,6 +352,7 @@ omit =
homeassistant/components/climate/touchline.py
homeassistant/components/climate/venstar.py
homeassistant/components/cover/garadget.py
+ homeassistant/components/cover/gogogate2.py
homeassistant/components/cover/homematic.py
homeassistant/components/cover/knx.py
homeassistant/components/cover/myq.py
@@ -374,6 +370,7 @@ omit =
homeassistant/components/device_tracker/cisco_ios.py
homeassistant/components/device_tracker/ddwrt.py
homeassistant/components/device_tracker/fritz.py
+ homeassistant/components/device_tracker/google_maps.py
homeassistant/components/device_tracker/gpslogger.py
homeassistant/components/device_tracker/hitron_coda.py
homeassistant/components/device_tracker/huawei_router.py
@@ -400,8 +397,8 @@ omit =
homeassistant/components/emoncms_history.py
homeassistant/components/emulated_hue/upnp.py
homeassistant/components/fan/mqtt.py
- homeassistant/components/fan/xiaomi_miio.py
homeassistant/components/feedreader.py
+ homeassistant/components/folder_watcher.py
homeassistant/components/foursquare.py
homeassistant/components/goalfeed.py
homeassistant/components/ifttt.py
@@ -424,6 +421,7 @@ omit =
homeassistant/components/light/lifx.py
homeassistant/components/light/limitlessled.py
homeassistant/components/light/mystrom.py
+ homeassistant/components/light/nanoleaf_aurora.py
homeassistant/components/light/osramlightify.py
homeassistant/components/light/piglow.py
homeassistant/components/light/rpi_gpio_pwm.py
@@ -432,7 +430,6 @@ omit =
homeassistant/components/light/tplink.py
homeassistant/components/light/tradfri.py
homeassistant/components/light/x10.py
- homeassistant/components/light/xiaomi_miio.py
homeassistant/components/light/yeelight.py
homeassistant/components/light/yeelightsunflower.py
homeassistant/components/light/zengge.py
@@ -441,6 +438,7 @@ omit =
homeassistant/components/lock/nello.py
homeassistant/components/lock/nuki.py
homeassistant/components/lock/sesame.py
+ homeassistant/components/map.py
homeassistant/components/media_extractor.py
homeassistant/components/media_player/anthemav.py
homeassistant/components/media_player/aquostv.py
@@ -508,6 +506,7 @@ omit =
homeassistant/components/notify/kodi.py
homeassistant/components/notify/lannouncer.py
homeassistant/components/notify/llamalab_automate.py
+ homeassistant/components/notify/mastodon.py
homeassistant/components/notify/matrix.py
homeassistant/components/notify/message_bird.py
homeassistant/components/notify/mycroft.py
@@ -523,8 +522,8 @@ omit =
homeassistant/components/notify/sendgrid.py
homeassistant/components/notify/simplepush.py
homeassistant/components/notify/slack.py
- homeassistant/components/notify/stride.py
homeassistant/components/notify/smtp.py
+ homeassistant/components/notify/stride.py
homeassistant/components/notify/synology_chat.py
homeassistant/components/notify/syslog.py
homeassistant/components/notify/telegram.py
@@ -538,7 +537,6 @@ omit =
homeassistant/components/remember_the_milk/__init__.py
homeassistant/components/remote/harmony.py
homeassistant/components/remote/itach.py
- homeassistant/components/remote/xiaomi_miio.py
homeassistant/components/scene/hunterdouglas_powerview.py
homeassistant/components/scene/lifx_cloud.py
homeassistant/components/sensor/airvisual.py
@@ -674,6 +672,7 @@ omit =
homeassistant/components/sensor/vasttrafik.py
homeassistant/components/sensor/viaggiatreno.py
homeassistant/components/sensor/waqi.py
+ homeassistant/components/sensor/waze_travel_time.py
homeassistant/components/sensor/whois.py
homeassistant/components/sensor/worldtidesinfo.py
homeassistant/components/sensor/worxlandroid.py
@@ -707,7 +706,6 @@ omit =
homeassistant/components/switch/tplink.py
homeassistant/components/switch/transmission.py
homeassistant/components/switch/vesync.py
- homeassistant/components/switch/xiaomi_miio.py
homeassistant/components/telegram_bot/*
homeassistant/components/thingspeak.py
homeassistant/components/tts/amazon_polly.py
@@ -716,7 +714,6 @@ omit =
homeassistant/components/tts/picotts.py
homeassistant/components/vacuum/mqtt.py
homeassistant/components/vacuum/roomba.py
- homeassistant/components/vacuum/xiaomi_miio.py
homeassistant/components/weather/bom.py
homeassistant/components/weather/buienradar.py
homeassistant/components/weather/darksky.py
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
index c570b548360..8772a136eb3 100644
--- a/.github/ISSUE_TEMPLATE.md
+++ b/.github/ISSUE_TEMPLATE.md
@@ -1,35 +1,45 @@
-Make sure you are running the latest version of Home Assistant before reporting an issue.
+
-You should only file an issue if you found a bug. Feature and enhancement requests should go in [the Feature Requests section](https://community.home-assistant.io/c/feature-requests) of our community forum:
-
-**Home Assistant release (`hass --version`):**
+**Home Assistant release with the issue:**
+
-**Python release (`python3 --version`):**
+**Last working Home Assistant release (if known):**
+**Operating environment (Hass.io/Docker/Windows/etc.):**
+
+
**Component/platform:**
+
**Description of problem:**
-**Expected:**
-
-**Problem-relevant `configuration.yaml` entries and steps to reproduce:**
+**Problem-relevant `configuration.yaml` entries and (fill out even if it seems unimportant):**
```yaml
```
-1.
-2.
-3.
-
**Traceback (if applicable):**
-```bash
+```
```
-**Additional info:**
+**Additional information:**
diff --git a/CODEOWNERS b/CODEOWNERS
index d8ebc3cff56..67aef6a248f 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -29,9 +29,6 @@ homeassistant/components/weblink.py @home-assistant/core
homeassistant/components/websocket_api.py @home-assistant/core
homeassistant/components/zone.py @home-assistant/core
-# To monitor non-pypi additions
-requirements_all.txt @andrey-git
-
# HomeAssistant developer Teams
Dockerfile @home-assistant/docker
virtualization/Docker/* @home-assistant/docker
@@ -43,6 +40,7 @@ homeassistant/components/hassio.py @home-assistant/hassio
# Individual components
homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt
+homeassistant/components/alarm_control_panel/manual_mqtt.py @colinodell
homeassistant/components/binary_sensor/hikvision.py @mezz64
homeassistant/components/bmw_connected_drive.py @ChristianKuehnel
homeassistant/components/camera/yi.py @bachya
@@ -69,8 +67,10 @@ homeassistant/components/sensor/gearbest.py @HerrHofrat
homeassistant/components/sensor/irish_rail_transport.py @ttroy50
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
homeassistant/components/sensor/pollen.py @bachya
-homeassistant/components/sensor/sytadin.py @gautric
+homeassistant/components/sensor/qnap.py @colinodell
+homeassistant/components/sensor/sma.py @kellerza
homeassistant/components/sensor/sql.py @dgomes
+homeassistant/components/sensor/sytadin.py @gautric
homeassistant/components/sensor/tibber.py @danielhiversen
homeassistant/components/sensor/waqi.py @andrey-git
homeassistant/components/switch/rainmachine.py @bachya
@@ -80,17 +80,17 @@ homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
homeassistant/components/*/axis.py @kane610
homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel
homeassistant/components/*/broadlink.py @danielhiversen
+homeassistant/components/*/deconz.py @kane610
homeassistant/components/eight_sleep.py @mezz64
homeassistant/components/*/eight_sleep.py @mezz64
homeassistant/components/hive.py @Rendili @KJonline
homeassistant/components/*/hive.py @Rendili @KJonline
homeassistant/components/homekit/* @cdce8p
-homeassistant/components/*/deconz.py @kane610
-homeassistant/components/*/rfxtrx.py @danielhiversen
-homeassistant/components/velux.py @Julius2342
-homeassistant/components/*/velux.py @Julius2342
homeassistant/components/knx.py @Julius2342
homeassistant/components/*/knx.py @Julius2342
+homeassistant/components/qwikswitch.py @kellerza
+homeassistant/components/*/qwikswitch.py @kellerza
+homeassistant/components/*/rfxtrx.py @danielhiversen
homeassistant/components/tahoma.py @philklei
homeassistant/components/*/tahoma.py @philklei
homeassistant/components/tesla.py @zabuldon
@@ -98,5 +98,9 @@ homeassistant/components/*/tesla.py @zabuldon
homeassistant/components/tellduslive.py @molobrakos @fredrike
homeassistant/components/*/tellduslive.py @molobrakos @fredrike
homeassistant/components/*/tradfri.py @ggravlingen
+homeassistant/components/velux.py @Julius2342
+homeassistant/components/*/velux.py @Julius2342
homeassistant/components/*/xiaomi_aqara.py @danielhiversen @syssi
homeassistant/components/*/xiaomi_miio.py @rytilahti @syssi
+
+homeassistant/scripts/check_config.py @kellerza
diff --git a/homeassistant/components/abode.py b/homeassistant/components/abode.py
index fde21a265b0..08918c77f01 100644
--- a/homeassistant/components/abode.py
+++ b/homeassistant/components/abode.py
@@ -19,7 +19,7 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
-REQUIREMENTS = ['abodepy==0.12.2']
+REQUIREMENTS = ['abodepy==0.12.3']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/alarm_control_panel/ifttt.py b/homeassistant/components/alarm_control_panel/ifttt.py
index 5303c24876e..7bdc1ccd9d9 100644
--- a/homeassistant/components/alarm_control_panel/ifttt.py
+++ b/homeassistant/components/alarm_control_panel/ifttt.py
@@ -93,7 +93,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class IFTTTAlarmPanel(alarm.AlarmControlPanel):
- """Representation of an alarm control panel controlled throught IFTTT."""
+ """Representation of an alarm control panel controlled through IFTTT."""
def __init__(self, name, code, event_away, event_home, event_night,
event_disarm, optimistic):
diff --git a/homeassistant/components/alarm_control_panel/totalconnect.py b/homeassistant/components/alarm_control_panel/totalconnect.py
index 5c1323989d4..1f383e32f92 100644
--- a/homeassistant/components/alarm_control_panel/totalconnect.py
+++ b/homeassistant/components/alarm_control_panel/totalconnect.py
@@ -18,7 +18,7 @@ from homeassistant.const import (
STATE_ALARM_ARMED_CUSTOM_BYPASS)
-REQUIREMENTS = ['total_connect_client==0.16']
+REQUIREMENTS = ['total_connect_client==0.17']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py
index 5e5155b3db8..707f8d02958 100644
--- a/homeassistant/components/alexa/smart_home.py
+++ b/homeassistant/components/alexa/smart_home.py
@@ -6,18 +6,20 @@ from datetime import datetime
from uuid import uuid4
from homeassistant.components import (
- alert, automation, cover, fan, group, input_boolean, light, lock,
+ alert, automation, cover, climate, fan, group, input_boolean, light, lock,
media_player, scene, script, switch, http, sensor)
import homeassistant.core as ha
import homeassistant.util.color as color_util
+from homeassistant.util.temperature import convert as convert_temperature
from homeassistant.util.decorator import Registry
from homeassistant.const import (
- ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_NAME, SERVICE_LOCK,
- SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY,
- SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
+ ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, CONF_NAME,
+ SERVICE_LOCK, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE,
+ SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON,
SERVICE_UNLOCK, SERVICE_VOLUME_SET, TEMP_FAHRENHEIT, TEMP_CELSIUS,
CONF_UNIT_OF_MEASUREMENT, STATE_LOCKED, STATE_UNLOCKED, STATE_ON)
+
from .const import CONF_FILTER, CONF_ENTITY_CONFIG
_LOGGER = logging.getLogger(__name__)
@@ -34,6 +36,16 @@ API_TEMP_UNITS = {
TEMP_CELSIUS: 'CELSIUS',
}
+API_THERMOSTAT_MODES = {
+ climate.STATE_HEAT: 'HEAT',
+ climate.STATE_COOL: 'COOL',
+ climate.STATE_AUTO: 'AUTO',
+ climate.STATE_ECO: 'ECO',
+ climate.STATE_IDLE: 'OFF',
+ climate.STATE_FAN_ONLY: 'OFF',
+ climate.STATE_DRY: 'OFF',
+}
+
SMART_HOME_HTTP_ENDPOINT = '/api/alexa/smart_home'
CONF_DESCRIPTION = 'description'
@@ -383,8 +395,60 @@ class _AlexaTemperatureSensor(_AlexaInterface):
raise _UnsupportedProperty(name)
unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT]
+ temp = self.entity.state
+ if self.entity.domain == climate.DOMAIN:
+ temp = self.entity.attributes.get(
+ climate.ATTR_CURRENT_TEMPERATURE)
return {
- 'value': float(self.entity.state),
+ 'value': float(temp),
+ 'scale': API_TEMP_UNITS[unit],
+ }
+
+
+class _AlexaThermostatController(_AlexaInterface):
+ def name(self):
+ return 'Alexa.ThermostatController'
+
+ def properties_supported(self):
+ properties = []
+ supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
+ if supported & climate.SUPPORT_TARGET_TEMPERATURE:
+ properties.append({'name': 'targetSetpoint'})
+ if supported & climate.SUPPORT_TARGET_TEMPERATURE_LOW:
+ properties.append({'name': 'lowerSetpoint'})
+ if supported & climate.SUPPORT_TARGET_TEMPERATURE_HIGH:
+ properties.append({'name': 'upperSetpoint'})
+ if supported & climate.SUPPORT_OPERATION_MODE:
+ properties.append({'name': 'thermostatMode'})
+ return properties
+
+ def properties_retrievable(self):
+ return True
+
+ def get_property(self, name):
+ if name == 'thermostatMode':
+ ha_mode = self.entity.attributes.get(climate.ATTR_OPERATION_MODE)
+ mode = API_THERMOSTAT_MODES.get(ha_mode)
+ if mode is None:
+ _LOGGER.error("%s (%s) has unsupported %s value '%s'",
+ self.entity.entity_id, type(self.entity),
+ climate.ATTR_OPERATION_MODE, ha_mode)
+ raise _UnsupportedProperty(name)
+ return mode
+
+ unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT]
+ temp = None
+ if name == 'targetSetpoint':
+ temp = self.entity.attributes.get(ATTR_TEMPERATURE)
+ elif name == 'lowerSetpoint':
+ temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW)
+ elif name == 'upperSetpoint':
+ temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_HIGH)
+ if temp is None:
+ raise _UnsupportedProperty(name)
+
+ return {
+ 'value': float(temp),
'scale': API_TEMP_UNITS[unit],
}
@@ -415,6 +479,16 @@ class _SwitchCapabilities(_AlexaEntity):
return [_AlexaPowerController(self.entity)]
+@ENTITY_ADAPTERS.register(climate.DOMAIN)
+class _ClimateCapabilities(_AlexaEntity):
+ def default_display_categories(self):
+ return [_DisplayCategory.THERMOSTAT]
+
+ def interfaces(self):
+ yield _AlexaThermostatController(self.entity)
+ yield _AlexaTemperatureSensor(self.entity)
+
+
@ENTITY_ADAPTERS.register(cover.DOMAIN)
class _CoverCapabilities(_AlexaEntity):
def default_display_categories(self):
@@ -682,17 +756,26 @@ def api_message(request,
return response
-def api_error(request, error_type='INTERNAL_ERROR', error_message=""):
+def api_error(request,
+ namespace='Alexa',
+ error_type='INTERNAL_ERROR',
+ error_message="",
+ payload=None):
"""Create a API formatted error response.
Async friendly.
"""
- payload = {
- 'type': error_type,
- 'message': error_message,
- }
+ payload = payload or {}
+ payload['type'] = error_type
+ payload['message'] = error_message
- return api_message(request, name='ErrorResponse', payload=payload)
+ _LOGGER.info("Request %s/%s error %s: %s",
+ request[API_HEADER]['namespace'],
+ request[API_HEADER]['name'],
+ error_type, error_message)
+
+ return api_message(
+ request, name='ErrorResponse', namespace=namespace, payload=payload)
@HANDLERS.register(('Alexa.Discovery', 'Discover'))
@@ -1104,7 +1187,6 @@ def async_api_select_input(hass, config, request, entity):
else:
msg = 'failed to map input {} to a media source on {}'.format(
media_input, entity.entity_id)
- _LOGGER.error(msg)
return api_error(
request, error_type='INVALID_VALUE', error_message=msg)
@@ -1276,6 +1358,149 @@ def async_api_previous(hass, config, request, entity):
return api_message(request)
+def api_error_temp_range(request, temp, min_temp, max_temp, unit):
+ """Create temperature value out of range API error response.
+
+ Async friendly.
+ """
+ temp_range = {
+ 'minimumValue': {
+ 'value': min_temp,
+ 'scale': API_TEMP_UNITS[unit],
+ },
+ 'maximumValue': {
+ 'value': max_temp,
+ 'scale': API_TEMP_UNITS[unit],
+ },
+ }
+
+ msg = 'The requested temperature {} is out of range'.format(temp)
+ return api_error(
+ request,
+ error_type='TEMPERATURE_VALUE_OUT_OF_RANGE',
+ error_message=msg,
+ payload={'validRange': temp_range},
+ )
+
+
+def temperature_from_object(temp_obj, to_unit, interval=False):
+ """Get temperature from Temperature object in requested unit."""
+ from_unit = TEMP_CELSIUS
+ temp = float(temp_obj['value'])
+
+ if temp_obj['scale'] == 'FAHRENHEIT':
+ from_unit = TEMP_FAHRENHEIT
+ elif temp_obj['scale'] == 'KELVIN':
+ # convert to Celsius if absolute temperature
+ if not interval:
+ temp -= 273.15
+
+ return convert_temperature(temp, from_unit, to_unit, interval)
+
+
+@HANDLERS.register(('Alexa.ThermostatController', 'SetTargetTemperature'))
+@extract_entity
+async def async_api_set_target_temp(hass, config, request, entity):
+ """Process a set target temperature request."""
+ unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT]
+ min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP)
+ max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP)
+
+ data = {
+ ATTR_ENTITY_ID: entity.entity_id
+ }
+
+ payload = request[API_PAYLOAD]
+ if 'targetSetpoint' in payload:
+ temp = temperature_from_object(
+ payload['targetSetpoint'], unit)
+ if temp < min_temp or temp > max_temp:
+ return api_error_temp_range(
+ request, temp, min_temp, max_temp, unit)
+ data[ATTR_TEMPERATURE] = temp
+ if 'lowerSetpoint' in payload:
+ temp_low = temperature_from_object(
+ payload['lowerSetpoint'], unit)
+ if temp_low < min_temp or temp_low > max_temp:
+ return api_error_temp_range(
+ request, temp_low, min_temp, max_temp, unit)
+ data[climate.ATTR_TARGET_TEMP_LOW] = temp_low
+ if 'upperSetpoint' in payload:
+ temp_high = temperature_from_object(
+ payload['upperSetpoint'], unit)
+ if temp_high < min_temp or temp_high > max_temp:
+ return api_error_temp_range(
+ request, temp_high, min_temp, max_temp, unit)
+ data[climate.ATTR_TARGET_TEMP_HIGH] = temp_high
+
+ await hass.services.async_call(
+ entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False)
+
+ return api_message(request)
+
+
+@HANDLERS.register(('Alexa.ThermostatController', 'AdjustTargetTemperature'))
+@extract_entity
+async def async_api_adjust_target_temp(hass, config, request, entity):
+ """Process an adjust target temperature request."""
+ unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT]
+ min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP)
+ max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP)
+
+ temp_delta = temperature_from_object(
+ request[API_PAYLOAD]['targetSetpointDelta'], unit, interval=True)
+ target_temp = float(entity.attributes.get(ATTR_TEMPERATURE)) + temp_delta
+
+ if target_temp < min_temp or target_temp > max_temp:
+ return api_error_temp_range(
+ request, target_temp, min_temp, max_temp, unit)
+
+ data = {
+ ATTR_ENTITY_ID: entity.entity_id,
+ ATTR_TEMPERATURE: target_temp,
+ }
+
+ await hass.services.async_call(
+ entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False)
+
+ return api_message(request)
+
+
+@HANDLERS.register(('Alexa.ThermostatController', 'SetThermostatMode'))
+@extract_entity
+async def async_api_set_thermostat_mode(hass, config, request, entity):
+ """Process a set thermostat mode request."""
+ mode = request[API_PAYLOAD]['thermostatMode']
+
+ operation_list = entity.attributes.get(climate.ATTR_OPERATION_LIST)
+ # Work around a pylint false positive due to
+ # https://github.com/PyCQA/pylint/issues/1830
+ # pylint: disable=stop-iteration-return
+ ha_mode = next(
+ (k for k, v in API_THERMOSTAT_MODES.items() if v == mode),
+ None
+ )
+ if ha_mode not in operation_list:
+ msg = 'The requested thermostat mode {} is not supported'.format(mode)
+ return api_error(
+ request,
+ namespace='Alexa.ThermostatController',
+ error_type='UNSUPPORTED_THERMOSTAT_MODE',
+ error_message=msg
+ )
+
+ data = {
+ ATTR_ENTITY_ID: entity.entity_id,
+ climate.ATTR_OPERATION_MODE: ha_mode,
+ }
+
+ await hass.services.async_call(
+ entity.domain, climate.SERVICE_SET_OPERATION_MODE, data,
+ blocking=False)
+
+ return api_message(request)
+
+
@HANDLERS.register(('Alexa', 'ReportState'))
@extract_entity
@asyncio.coroutine
diff --git a/homeassistant/components/amcrest.py b/homeassistant/components/amcrest.py
index b91f1fae565..d0e470e3f8e 100644
--- a/homeassistant/components/amcrest.py
+++ b/homeassistant/components/amcrest.py
@@ -10,14 +10,15 @@ from datetime import timedelta
import aiohttp
import voluptuous as vol
from requests.exceptions import HTTPError, ConnectTimeout
+from requests.exceptions import ConnectionError as ConnectError
from homeassistant.const import (
CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD,
- CONF_SENSORS, CONF_SCAN_INTERVAL, HTTP_BASIC_AUTHENTICATION)
+ CONF_SENSORS, CONF_SWITCHES, CONF_SCAN_INTERVAL, HTTP_BASIC_AUTHENTICATION)
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['amcrest==1.2.1']
+REQUIREMENTS = ['amcrest==1.2.2']
DEPENDENCIES = ['ffmpeg']
_LOGGER = logging.getLogger(__name__)
@@ -63,6 +64,12 @@ SENSORS = {
'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'],
}
+# Switch types are defined like: Name, icon
+SWITCHES = {
+ 'motion_detection': ['Motion Detection', 'mdi:run-fast'],
+ 'motion_recording': ['Motion Recording', 'mdi:record-rec']
+}
+
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
vol.Required(CONF_HOST): cv.string,
@@ -81,6 +88,8 @@ CONFIG_SCHEMA = vol.Schema({
cv.time_period,
vol.Optional(CONF_SENSORS):
vol.All(cv.ensure_list, [vol.In(SENSORS)]),
+ vol.Optional(CONF_SWITCHES):
+ vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
})])
}, extra=vol.ALLOW_EXTRA)
@@ -93,14 +102,15 @@ def setup(hass, config):
amcrest_cams = config[DOMAIN]
for device in amcrest_cams:
- camera = AmcrestCamera(device.get(CONF_HOST),
- device.get(CONF_PORT),
- device.get(CONF_USERNAME),
- device.get(CONF_PASSWORD)).camera
try:
+ camera = AmcrestCamera(device.get(CONF_HOST),
+ device.get(CONF_PORT),
+ device.get(CONF_USERNAME),
+ device.get(CONF_PASSWORD)).camera
+ # pylint: disable=pointless-statement
camera.current_time
- except (ConnectTimeout, HTTPError) as ex:
+ except (ConnectError, ConnectTimeout, HTTPError) as ex:
_LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex))
hass.components.persistent_notification.create(
'Error: {}
'
@@ -108,12 +118,13 @@ def setup(hass, config):
''.format(ex),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
- return False
+ continue
ffmpeg_arguments = device.get(CONF_FFMPEG_ARGUMENTS)
name = device.get(CONF_NAME)
resolution = RESOLUTION_LIST[device.get(CONF_RESOLUTION)]
sensors = device.get(CONF_SENSORS)
+ switches = device.get(CONF_SWITCHES)
stream_source = STREAM_SOURCE_LIST[device.get(CONF_STREAM_SOURCE)]
username = device.get(CONF_USERNAME)
@@ -143,6 +154,13 @@ def setup(hass, config):
CONF_SENSORS: sensors,
}, config)
+ if switches:
+ discovery.load_platform(
+ hass, 'switch', DOMAIN, {
+ CONF_NAME: name,
+ CONF_SWITCHES: switches
+ }, config)
+
return True
diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py
index d272ebcb1c0..6fdf0c027a4 100644
--- a/homeassistant/components/api.py
+++ b/homeassistant/components/api.py
@@ -52,9 +52,8 @@ def setup(hass, config):
hass.http.register_view(APIComponentsView)
hass.http.register_view(APITemplateView)
- log_path = hass.data.get(DATA_LOGGING, None)
- if log_path:
- hass.http.register_static_path(URL_API_ERROR_LOG, log_path, False)
+ if DATA_LOGGING in hass.data:
+ hass.http.register_view(APIErrorLog)
return True
@@ -356,6 +355,17 @@ class APITemplateView(HomeAssistantView):
HTTP_BAD_REQUEST)
+class APIErrorLog(HomeAssistantView):
+ """View to fetch the error log."""
+
+ url = URL_API_ERROR_LOG
+ name = "api:error_log"
+
+ async def get(self, request):
+ """Retrieve API error log."""
+ return await self.file(request, request.app['hass'].data[DATA_LOGGING])
+
+
@asyncio.coroutine
def async_services_json(hass):
"""Generate services data to JSONify."""
diff --git a/homeassistant/components/binary_sensor/bmw_connected_drive.py b/homeassistant/components/binary_sensor/bmw_connected_drive.py
index 0f3edd86dcd..e7af5af988b 100644
--- a/homeassistant/components/binary_sensor/bmw_connected_drive.py
+++ b/homeassistant/components/binary_sensor/bmw_connected_drive.py
@@ -7,8 +7,8 @@ https://home-assistant.io/components/binary_sensor.bmw_connected_drive/
import asyncio
import logging
-from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN
from homeassistant.components.binary_sensor import BinarySensorDevice
+from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN
DEPENDENCIES = ['bmw_connected_drive']
@@ -45,7 +45,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
self._account = account
self._vehicle = vehicle
self._attribute = attribute
- self._name = '{} {}'.format(self._vehicle.modelName, self._attribute)
+ self._name = '{} {}'.format(self._vehicle.name, self._attribute)
self._sensor_name = sensor_name
self._device_class = device_class
self._state = None
@@ -75,7 +75,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
"""Return the state attributes of the binary sensor."""
vehicle_state = self._vehicle.state
result = {
- 'car': self._vehicle.modelName
+ 'car': self._vehicle.name
}
if self._attribute == 'lids':
@@ -91,6 +91,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
def update(self):
"""Read new state data from the library."""
+ from bimmer_connected.state import LockState
vehicle_state = self._vehicle.state
# device class opening: On means open, Off means closed
@@ -101,9 +102,9 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
self._state = not vehicle_state.all_windows_closed
# device class safety: On means unsafe, Off means safe
if self._attribute == 'door_lock_state':
- # Possible values: LOCKED, SECURED, SELECTIVELOCKED, UNLOCKED
- self._state = bool(vehicle_state.door_lock_state.value
- in ('SELECTIVELOCKED', 'UNLOCKED'))
+ # Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED
+ self._state = vehicle_state.door_lock_state not in \
+ [LockState.LOCKED, LockState.SECURED]
def update_callback(self):
"""Schedule a state update."""
diff --git a/homeassistant/components/binary_sensor/mercedesme.py b/homeassistant/components/binary_sensor/mercedesme.py
deleted file mode 100644
index fcf2d7122e2..00000000000
--- a/homeassistant/components/binary_sensor/mercedesme.py
+++ /dev/null
@@ -1,97 +0,0 @@
-"""
-Support for Mercedes cars with Mercedes ME.
-
-For more details about this component, please refer to the documentation at
-https://home-assistant.io/components/binary_sensor.mercedesme/
-"""
-import logging
-import datetime
-
-from homeassistant.components.binary_sensor import (BinarySensorDevice)
-from homeassistant.components.mercedesme import (
- DATA_MME, FEATURE_NOT_AVAILABLE, MercedesMeEntity, BINARY_SENSORS)
-
-DEPENDENCIES = ['mercedesme']
-
-_LOGGER = logging.getLogger(__name__)
-
-
-def setup_platform(hass, config, add_devices, discovery_info=None):
- """Setup the sensor platform."""
- data = hass.data[DATA_MME].data
-
- if not data.cars:
- _LOGGER.error("No cars found. Check component log.")
- return
-
- devices = []
- for car in data.cars:
- for key, value in sorted(BINARY_SENSORS.items()):
- if car['availabilities'].get(key, 'INVALID') == 'VALID':
- devices.append(MercedesMEBinarySensor(
- data, key, value[0], car["vin"], None))
- else:
- _LOGGER.warning(FEATURE_NOT_AVAILABLE, key, car["license"])
-
- add_devices(devices, True)
-
-
-class MercedesMEBinarySensor(MercedesMeEntity, BinarySensorDevice):
- """Representation of a Sensor."""
-
- @property
- def is_on(self):
- """Return the state of the binary sensor."""
- return self._state
-
- @property
- def device_state_attributes(self):
- """Return the state attributes."""
- if self._internal_name == "windowsClosed":
- return {
- "window_front_left": self._car["windowStatusFrontLeft"],
- "window_front_right": self._car["windowStatusFrontRight"],
- "window_rear_left": self._car["windowStatusRearLeft"],
- "window_rear_right": self._car["windowStatusRearRight"],
- "original_value": self._car[self._internal_name],
- "last_update": datetime.datetime.fromtimestamp(
- self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'),
- "car": self._car["license"]
- }
- elif self._internal_name == "tireWarningLight":
- return {
- "front_right_tire_pressure_kpa":
- self._car["frontRightTirePressureKpa"],
- "front_left_tire_pressure_kpa":
- self._car["frontLeftTirePressureKpa"],
- "rear_right_tire_pressure_kpa":
- self._car["rearRightTirePressureKpa"],
- "rear_left_tire_pressure_kpa":
- self._car["rearLeftTirePressureKpa"],
- "original_value": self._car[self._internal_name],
- "last_update": datetime.datetime.fromtimestamp(
- self._car["lastUpdate"]
- ).strftime('%Y-%m-%d %H:%M:%S'),
- "car": self._car["license"],
- }
- return {
- "original_value": self._car[self._internal_name],
- "last_update": datetime.datetime.fromtimestamp(
- self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'),
- "car": self._car["license"]
- }
-
- def update(self):
- """Fetch new state data for the sensor."""
- self._car = next(
- car for car in self._data.cars if car["vin"] == self._vin)
-
- if self._internal_name == "windowsClosed":
- self._state = bool(self._car[self._internal_name] == "CLOSED")
- elif self._internal_name == "tireWarningLight":
- self._state = bool(self._car[self._internal_name] != "INACTIVE")
- else:
- self._state = self._car[self._internal_name] is True
-
- _LOGGER.debug("Updated %s Value: %s IsOn: %s",
- self._internal_name, self._state, self.is_on)
diff --git a/homeassistant/components/binary_sensor/mysensors.py b/homeassistant/components/binary_sensor/mysensors.py
index 1e9359b6902..21443021193 100644
--- a/homeassistant/components/binary_sensor/mysensors.py
+++ b/homeassistant/components/binary_sensor/mysensors.py
@@ -21,11 +21,12 @@ SENSORS = {
}
-def setup_platform(hass, config, add_devices, discovery_info=None):
- """Set up the MySensors platform for binary sensors."""
+async def async_setup_platform(
+ hass, config, async_add_devices, discovery_info=None):
+ """Set up the mysensors platform for binary sensors."""
mysensors.setup_mysensors_platform(
hass, DOMAIN, discovery_info, MySensorsBinarySensor,
- add_devices=add_devices)
+ async_add_devices=async_add_devices)
class MySensorsBinarySensor(mysensors.MySensorsEntity, BinarySensorDevice):
diff --git a/homeassistant/components/binary_sensor/workday.py b/homeassistant/components/binary_sensor/workday.py
index f5a7324d351..8935ad5115d 100644
--- a/homeassistant/components/binary_sensor/workday.py
+++ b/homeassistant/components/binary_sensor/workday.py
@@ -30,8 +30,8 @@ ALL_COUNTRIES = ['Australia', 'AU', 'Austria', 'AT', 'Belgium', 'BE', 'Canada',
'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT',
'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI',
'Slovakia', 'SK', 'South Africa', 'ZA', 'Spain', 'ES',
- 'Sweden', 'SE', 'UnitedKingdom', 'UK', 'UnitedStates', 'US',
- 'Wales']
+ 'Sweden', 'SE', 'Switzerland', 'CH', 'UnitedKingdom', 'UK',
+ 'UnitedStates', 'US', 'Wales']
CONF_COUNTRY = 'country'
CONF_PROVINCE = 'province'
CONF_WORKDAYS = 'workdays'
@@ -47,13 +47,13 @@ DEFAULT_OFFSET = 0
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_COUNTRY): vol.In(ALL_COUNTRIES),
- vol.Optional(CONF_PROVINCE): cv.string,
+ vol.Optional(CONF_EXCLUDES, default=DEFAULT_EXCLUDES):
+ vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_OFFSET, default=DEFAULT_OFFSET): vol.Coerce(int),
+ vol.Optional(CONF_PROVINCE): cv.string,
vol.Optional(CONF_WORKDAYS, default=DEFAULT_WORKDAYS):
vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]),
- vol.Optional(CONF_EXCLUDES, default=DEFAULT_EXCLUDES):
- vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]),
})
@@ -74,14 +74,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if province:
# 'state' and 'prov' are not interchangeable, so need to make
# sure we use the right one
- if (hasattr(obj_holidays, "PROVINCES") and
+ if (hasattr(obj_holidays, 'PROVINCES') and
province in obj_holidays.PROVINCES):
- obj_holidays = getattr(holidays, country)(prov=province,
- years=year)
- elif (hasattr(obj_holidays, "STATES") and
+ obj_holidays = getattr(holidays, country)(
+ prov=province, years=year)
+ elif (hasattr(obj_holidays, 'STATES') and
province in obj_holidays.STATES):
- obj_holidays = getattr(holidays, country)(state=province,
- years=year)
+ obj_holidays = getattr(holidays, country)(
+ state=province, years=year)
else:
_LOGGER.error("There is no province/state %s in country %s",
province, country)
diff --git a/homeassistant/components/bmw_connected_drive.py b/homeassistant/components/bmw_connected_drive.py
index 9e9e2bafac5..48452b6d79b 100644
--- a/homeassistant/components/bmw_connected_drive.py
+++ b/homeassistant/components/bmw_connected_drive.py
@@ -4,30 +4,29 @@ Reads vehicle status from BMW connected drive portal.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/bmw_connected_drive/
"""
-import logging
import datetime
+import logging
import voluptuous as vol
+
+from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
from homeassistant.helpers import discovery
from homeassistant.helpers.event import track_utc_time_change
-
import homeassistant.helpers.config_validation as cv
-from homeassistant.const import (
- CONF_USERNAME, CONF_PASSWORD
-)
-REQUIREMENTS = ['bimmer_connected==0.4.1']
+REQUIREMENTS = ['bimmer_connected==0.5.0']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'bmw_connected_drive'
-CONF_VALUES = 'values'
-CONF_COUNTRY = 'country'
+CONF_REGION = 'region'
+
ACCOUNT_SCHEMA = vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
- vol.Required(CONF_COUNTRY): cv.string,
+ vol.Required(CONF_REGION): vol.Any('north_america', 'china',
+ 'rest_of_world'),
})
CONFIG_SCHEMA = vol.Schema({
@@ -47,9 +46,9 @@ def setup(hass, config):
for name, account_config in config[DOMAIN].items():
username = account_config[CONF_USERNAME]
password = account_config[CONF_PASSWORD]
- country = account_config[CONF_COUNTRY]
+ region = account_config[CONF_REGION]
_LOGGER.debug('Adding new account %s', name)
- bimmer = BMWConnectedDriveAccount(username, password, country, name)
+ bimmer = BMWConnectedDriveAccount(username, password, region, name)
accounts.append(bimmer)
# update every UPDATE_INTERVAL minutes, starting now
@@ -75,12 +74,15 @@ def setup(hass, config):
class BMWConnectedDriveAccount(object):
"""Representation of a BMW vehicle."""
- def __init__(self, username: str, password: str, country: str,
+ def __init__(self, username: str, password: str, region_str: str,
name: str) -> None:
"""Constructor."""
from bimmer_connected.account import ConnectedDriveAccount
+ from bimmer_connected.country_selector import get_region_from_name
- self.account = ConnectedDriveAccount(username, password, country)
+ region = get_region_from_name(region_str)
+
+ self.account = ConnectedDriveAccount(username, password, region)
self.name = name
self._update_listeners = []
diff --git a/homeassistant/components/camera/mqtt.py b/homeassistant/components/camera/mqtt.py
index b7a7510e0eb..b2a27230a02 100644
--- a/homeassistant/components/camera/mqtt.py
+++ b/homeassistant/components/camera/mqtt.py
@@ -19,7 +19,6 @@ from homeassistant.helpers import config_validation as cv
_LOGGER = logging.getLogger(__name__)
CONF_TOPIC = 'topic'
-
DEFAULT_NAME = 'MQTT Camera'
DEPENDENCIES = ['mqtt']
@@ -33,9 +32,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the MQTT Camera."""
- topic = config[CONF_TOPIC]
+ if discovery_info is not None:
+ config = PLATFORM_SCHEMA(discovery_info)
- async_add_devices([MqttCamera(config[CONF_NAME], topic)])
+ async_add_devices([MqttCamera(
+ config.get(CONF_NAME),
+ config.get(CONF_TOPIC)
+ )])
class MqttCamera(Camera):
diff --git a/homeassistant/components/canary.py b/homeassistant/components/canary.py
index 03825bf48a9..4d0fbe617b2 100644
--- a/homeassistant/components/canary.py
+++ b/homeassistant/components/canary.py
@@ -15,7 +15,7 @@ from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT
from homeassistant.helpers import discovery
from homeassistant.util import Throttle
-REQUIREMENTS = ['py-canary==0.4.1']
+REQUIREMENTS = ['py-canary==0.5.0']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/climate/mysensors.py b/homeassistant/components/climate/mysensors.py
index b526d8b066c..2545094ceec 100644
--- a/homeassistant/components/climate/mysensors.py
+++ b/homeassistant/components/climate/mysensors.py
@@ -31,10 +31,12 @@ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_HIGH |
SUPPORT_OPERATION_MODE)
-def setup_platform(hass, config, add_devices, discovery_info=None):
- """Set up the MySensors climate."""
+async def async_setup_platform(
+ hass, config, async_add_devices, discovery_info=None):
+ """Set up the mysensors climate."""
mysensors.setup_mysensors_platform(
- hass, DOMAIN, discovery_info, MySensorsHVAC, add_devices=add_devices)
+ hass, DOMAIN, discovery_info, MySensorsHVAC,
+ async_add_devices=async_add_devices)
class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
@@ -163,8 +165,8 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
self._values[self.value_type] = operation_mode
self.schedule_update_ha_state()
- def update(self):
+ async def async_update(self):
"""Update the controller with the latest value from a sensor."""
- super().update()
+ await super().async_update()
self._values[self.value_type] = DICT_MYS_TO_HA[
self._values[self.value_type]]
diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py
index e5c21158acb..d11f6890a7b 100644
--- a/homeassistant/components/climate/nest.py
+++ b/homeassistant/components/climate/nest.py
@@ -179,7 +179,7 @@ class NestThermostat(ClimateDevice):
try:
self.device.target = temp
except nest.nest.APIError:
- _LOGGER.error("An error occured while setting the temperature")
+ _LOGGER.error("An error occurred while setting the temperature")
def set_operation_mode(self, operation_mode):
"""Set operation mode."""
diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py
index 601b12ffe4a..4d0295c382a 100644
--- a/homeassistant/components/config/__init__.py
+++ b/homeassistant/components/config/__init__.py
@@ -14,24 +14,16 @@ from homeassistant.util.yaml import load_yaml, dump
DOMAIN = 'config'
DEPENDENCIES = ['http']
SECTIONS = ('core', 'customize', 'group', 'hassbian', 'automation', 'script',
- 'entity_registry')
+ 'entity_registry', 'config_entries')
ON_DEMAND = ('zwave',)
-FEATURE_FLAGS = ('config_entries',)
@asyncio.coroutine
def async_setup(hass, config):
"""Set up the config component."""
- global SECTIONS
-
yield from hass.components.frontend.async_register_built_in_panel(
'config', 'config', 'mdi:settings')
- # Temporary way of allowing people to opt-in for unreleased config sections
- for key, value in config.get(DOMAIN, {}).items():
- if key in FEATURE_FLAGS and value:
- SECTIONS += (key,)
-
@asyncio.coroutine
def setup_panel(panel_name):
"""Set up a panel."""
diff --git a/homeassistant/components/config_entry_example/.translations/de.json b/homeassistant/components/config_entry_example/.translations/de.json
deleted file mode 100644
index 75b88f2f822..00000000000
--- a/homeassistant/components/config_entry_example/.translations/de.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "config": {
- "error": {
- "invalid_object_id": "Ung\u00fcltige Objekt-ID"
- },
- "step": {
- "init": {
- "data": {
- "object_id": "Objekt-ID"
- },
- "description": "Bitte gib eine Objekt_ID f\u00fcr das Test-Entity ein.",
- "title": "W\u00e4hle eine Objekt-ID"
- },
- "name": {
- "data": {
- "name": "Name"
- },
- "description": "Bitte gib einen Namen f\u00fcr das Test-Entity ein",
- "title": "Name des Test-Entity"
- }
- },
- "title": "Beispiel Konfig-Eintrag"
- }
-}
\ No newline at end of file
diff --git a/homeassistant/components/config_entry_example/.translations/en.json b/homeassistant/components/config_entry_example/.translations/en.json
deleted file mode 100644
index ec24d01ebc8..00000000000
--- a/homeassistant/components/config_entry_example/.translations/en.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "config": {
- "error": {
- "invalid_object_id": "Invalid object ID"
- },
- "step": {
- "init": {
- "data": {
- "object_id": "Object ID"
- },
- "description": "Please enter an object_id for the test entity.",
- "title": "Pick object id"
- },
- "name": {
- "data": {
- "name": "Name"
- },
- "description": "Please enter a name for the test entity.",
- "title": "Name of the entity"
- }
- },
- "title": "Config Entry Example"
- }
-}
\ No newline at end of file
diff --git a/homeassistant/components/config_entry_example/.translations/fi.json b/homeassistant/components/config_entry_example/.translations/fi.json
deleted file mode 100644
index 054a6f372bc..00000000000
--- a/homeassistant/components/config_entry_example/.translations/fi.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "config": {
- "step": {
- "name": {
- "data": {
- "name": "Nimi"
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/homeassistant/components/config_entry_example/.translations/ko.json b/homeassistant/components/config_entry_example/.translations/ko.json
deleted file mode 100644
index f12e3fc52f1..00000000000
--- a/homeassistant/components/config_entry_example/.translations/ko.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "config": {
- "error": {
- "invalid_object_id": "\uc624\ube0c\uc81d\ud2b8 ID\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
- },
- "step": {
- "init": {
- "data": {
- "object_id": "\uc624\ube0c\uc81d\ud2b8 ID"
- },
- "description": "\ud14c\uc2a4\ud2b8 \uad6c\uc131\uc694\uc18c\uc758 \uc624\ube0c\uc81d\ud2b8 ID \ub97c \uc785\ub825\ud558\uc138\uc694",
- "title": "\uc624\ube0c\uc81d\ud2b8 ID \uc120\ud0dd"
- },
- "name": {
- "data": {
- "name": "\uc774\ub984"
- },
- "description": "\ud14c\uc2a4\ud2b8 \uad6c\uc131\uc694\uc18c\uc758 \uc774\ub984\uc744 \uc785\ub825\ud558\uc138\uc694.",
- "title": "\uad6c\uc131\uc694\uc18c \uc774\ub984"
- }
- },
- "title": "\uc785\ub825 \uc608\uc81c \uad6c\uc131"
- }
-}
\ No newline at end of file
diff --git a/homeassistant/components/config_entry_example/.translations/nl.json b/homeassistant/components/config_entry_example/.translations/nl.json
deleted file mode 100644
index 7b52ac88cf0..00000000000
--- a/homeassistant/components/config_entry_example/.translations/nl.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "config": {
- "error": {
- "invalid_object_id": "Ongeldig object ID"
- },
- "step": {
- "init": {
- "data": {
- "object_id": "Object ID"
- },
- "description": "Voer een object_id in voor het testen van de entiteit.",
- "title": "Kies object id"
- },
- "name": {
- "data": {
- "name": "Naam"
- },
- "description": "Voer een naam in voor het testen van de entiteit.",
- "title": "Naam van de entiteit"
- }
- },
- "title": "Voorbeeld van de config vermelding"
- }
-}
\ No newline at end of file
diff --git a/homeassistant/components/config_entry_example/.translations/no.json b/homeassistant/components/config_entry_example/.translations/no.json
deleted file mode 100644
index 380c539f8af..00000000000
--- a/homeassistant/components/config_entry_example/.translations/no.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "config": {
- "error": {
- "invalid_object_id": "Ugyldig objekt ID"
- },
- "step": {
- "init": {
- "data": {
- "object_id": "Objekt ID"
- },
- "description": "Vennligst skriv inn en object_id for testenheten.",
- "title": "Velg objekt ID"
- },
- "name": {
- "data": {
- "name": "Navn"
- },
- "description": "Vennligst skriv inn et navn for testenheten.",
- "title": "Navn p\u00e5 enheten"
- }
- },
- "title": "Konfigureringseksempel"
- }
-}
\ No newline at end of file
diff --git a/homeassistant/components/config_entry_example/.translations/pl.json b/homeassistant/components/config_entry_example/.translations/pl.json
deleted file mode 100644
index 35cca168249..00000000000
--- a/homeassistant/components/config_entry_example/.translations/pl.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "config": {
- "error": {
- "invalid_object_id": "Nieprawid\u0142owy identyfikator obiektu"
- },
- "step": {
- "init": {
- "data": {
- "object_id": "Identyfikator obiektu"
- },
- "description": "Prosz\u0119 wprowadzi\u0107 identyfikator obiektu (object_id) dla jednostki testowej.",
- "title": "Wybierz identyfikator obiektu"
- },
- "name": {
- "data": {
- "name": "Nazwa"
- },
- "description": "Prosz\u0119 wprowadzi\u0107 nazw\u0119 dla jednostki testowej.",
- "title": "Nazwa jednostki"
- }
- },
- "title": "Przyk\u0142ad wpisu do konfiguracji"
- }
-}
\ No newline at end of file
diff --git a/homeassistant/components/config_entry_example/.translations/ro.json b/homeassistant/components/config_entry_example/.translations/ro.json
deleted file mode 100644
index 1a4cdd6bbb7..00000000000
--- a/homeassistant/components/config_entry_example/.translations/ro.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "config": {
- "step": {
- "init": {
- "description": "Introduce\u021bi un obiect_id pentru entitatea testat\u0103.",
- "title": "Alege\u021bi id-ul obiectului"
- },
- "name": {
- "data": {
- "name": "Nume"
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/homeassistant/components/config_entry_example/.translations/sl.json b/homeassistant/components/config_entry_example/.translations/sl.json
deleted file mode 100644
index 11d2d3f5e80..00000000000
--- a/homeassistant/components/config_entry_example/.translations/sl.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "config": {
- "error": {
- "invalid_object_id": "Neveljaven ID objekta"
- },
- "step": {
- "init": {
- "data": {
- "object_id": "ID objekta"
- },
- "description": "Prosimo, vnesite Id_objekta za testni subjekt.",
- "title": "Izberite ID objekta"
- },
- "name": {
- "data": {
- "name": "Ime"
- },
- "description": "Vnesite ime za testni subjekt.",
- "title": "Ime subjekta"
- }
- },
- "title": "Primer nastavitve"
- }
-}
\ No newline at end of file
diff --git a/homeassistant/components/config_entry_example/.translations/vi.json b/homeassistant/components/config_entry_example/.translations/vi.json
deleted file mode 100644
index e40c4d38e9f..00000000000
--- a/homeassistant/components/config_entry_example/.translations/vi.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "config": {
- "error": {
- "invalid_object_id": "ID \u0111\u1ed1i t\u01b0\u1ee3ng kh\u00f4ng h\u1ee3p l\u1ec7"
- },
- "step": {
- "init": {
- "data": {
- "object_id": "ID \u0111\u1ed1i t\u01b0\u1ee3ng"
- },
- "description": "Xin vui l\u00f2ng nh\u1eadp m\u1ed9t object_id cho th\u1eed nghi\u1ec7m th\u1ef1c th\u1ec3.",
- "title": "Ch\u1ecdn id \u0111\u1ed1i t\u01b0\u1ee3ng"
- },
- "name": {
- "data": {
- "name": "T\u00ean"
- },
- "description": "Xin vui l\u00f2ng nh\u1eadp t\u00ean cho th\u1eed nghi\u1ec7m th\u1ef1c th\u1ec3.",
- "title": "T\u00ean c\u1ee7a th\u1ef1c th\u1ec3"
- }
- },
- "title": "V\u00ed d\u1ee5 v\u1ec1 c\u1ea5u h\u00ecnh th\u1ef1c th\u1ec3"
- }
-}
\ No newline at end of file
diff --git a/homeassistant/components/config_entry_example/.translations/zh-Hans.json b/homeassistant/components/config_entry_example/.translations/zh-Hans.json
deleted file mode 100644
index ee10e6d7b48..00000000000
--- a/homeassistant/components/config_entry_example/.translations/zh-Hans.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "config": {
- "error": {
- "invalid_object_id": "\u65e0\u6548\u7684\u5bf9\u8c61 ID"
- },
- "step": {
- "init": {
- "data": {
- "object_id": "\u5bf9\u8c61 ID"
- },
- "description": "\u8bf7\u4e3a\u6d4b\u8bd5\u8bbe\u5907\u8f93\u5165\u5bf9\u8c61 ID",
- "title": "\u8bf7\u9009\u62e9\u5bf9\u8c61 ID"
- },
- "name": {
- "data": {
- "name": "\u540d\u79f0"
- },
- "description": "\u8bf7\u4e3a\u6d4b\u8bd5\u8bbe\u5907\u8f93\u5165\u540d\u79f0",
- "title": "\u8bbe\u5907\u540d\u79f0"
- }
- },
- "title": "\u6837\u4f8b\u914d\u7f6e\u6761\u76ee"
- }
-}
\ No newline at end of file
diff --git a/homeassistant/components/config_entry_example/__init__.py b/homeassistant/components/config_entry_example/__init__.py
deleted file mode 100644
index 3ebfdc3a183..00000000000
--- a/homeassistant/components/config_entry_example/__init__.py
+++ /dev/null
@@ -1,98 +0,0 @@
-"""Example component to show how config entries work."""
-
-import asyncio
-
-import voluptuous as vol
-
-from homeassistant import config_entries
-from homeassistant.const import ATTR_FRIENDLY_NAME
-from homeassistant.util import slugify
-
-
-DOMAIN = 'config_entry_example'
-
-
-@asyncio.coroutine
-def async_setup(hass, config):
- """Setup for our example component."""
- return True
-
-
-@asyncio.coroutine
-def async_setup_entry(hass, entry):
- """Initialize an entry."""
- entity_id = '{}.{}'.format(DOMAIN, entry.data['object_id'])
- hass.states.async_set(entity_id, 'loaded', {
- ATTR_FRIENDLY_NAME: entry.data['name']
- })
-
- # Indicate setup was successful.
- return True
-
-
-@asyncio.coroutine
-def async_unload_entry(hass, entry):
- """Unload an entry."""
- entity_id = '{}.{}'.format(DOMAIN, entry.data['object_id'])
- hass.states.async_remove(entity_id)
-
- # Indicate unload was successful.
- return True
-
-
-@config_entries.HANDLERS.register(DOMAIN)
-class ExampleConfigFlow(config_entries.ConfigFlowHandler):
- """Handle an example configuration flow."""
-
- VERSION = 1
-
- def __init__(self):
- """Initialize a Hue config handler."""
- self.object_id = None
-
- @asyncio.coroutine
- def async_step_init(self, user_input=None):
- """Start config flow."""
- errors = None
- if user_input is not None:
- object_id = user_input['object_id']
-
- if object_id != '' and object_id == slugify(object_id):
- self.object_id = user_input['object_id']
- return (yield from self.async_step_name())
-
- errors = {
- 'object_id': 'invalid_object_id'
- }
-
- return self.async_show_form(
- step_id='init',
- data_schema=vol.Schema({
- 'object_id': str
- }),
- errors=errors
- )
-
- @asyncio.coroutine
- def async_step_name(self, user_input=None):
- """Ask user to enter the name."""
- errors = None
- if user_input is not None:
- name = user_input['name']
-
- if name != '':
- return self.async_create_entry(
- title=name,
- data={
- 'name': name,
- 'object_id': self.object_id,
- }
- )
-
- return self.async_show_form(
- step_id='name',
- data_schema=vol.Schema({
- 'name': str
- }),
- errors=errors
- )
diff --git a/homeassistant/components/config_entry_example/strings.json b/homeassistant/components/config_entry_example/strings.json
deleted file mode 100644
index a7a8cd4025b..00000000000
--- a/homeassistant/components/config_entry_example/strings.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "config": {
- "title": "Config Entry Example",
- "step": {
- "init": {
- "title": "Pick object id",
- "description": "Please enter an object_id for the test entity.",
- "data": {
- "object_id": "Object ID"
- }
- },
- "name": {
- "title": "Name of the entity",
- "description": "Please enter a name for the test entity.",
- "data": {
- "name": "Name"
- }
- }
- },
- "error": {
- "invalid_object_id": "Invalid object ID"
- }
- }
-}
diff --git a/homeassistant/components/conversation.py b/homeassistant/components/conversation.py
index e96694ce0a3..ddd96c99177 100644
--- a/homeassistant/components/conversation.py
+++ b/homeassistant/components/conversation.py
@@ -13,10 +13,14 @@ from homeassistant import core
from homeassistant.components import http
from homeassistant.components.http.data_validator import (
RequestDataValidator)
+from homeassistant.components.cover import (INTENT_OPEN_COVER,
+ INTENT_CLOSE_COVER)
+from homeassistant.const import EVENT_COMPONENT_LOADED
+from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import intent
-
from homeassistant.loader import bind_hass
+from homeassistant.setup import (ATTR_COMPONENT)
_LOGGER = logging.getLogger(__name__)
@@ -28,6 +32,13 @@ DOMAIN = 'conversation'
REGEX_TURN_COMMAND = re.compile(r'turn (?P(?: |\w)+) (?P\w+)')
REGEX_TYPE = type(re.compile(''))
+UTTERANCES = {
+ 'cover': {
+ INTENT_OPEN_COVER: ['Open [the] [a] [an] {name}[s]'],
+ INTENT_CLOSE_COVER: ['Close [the] [a] [an] {name}[s]']
+ }
+}
+
SERVICE_PROCESS = 'process'
SERVICE_PROCESS_SCHEMA = vol.Schema({
@@ -112,6 +123,25 @@ async def async_setup(hass, config):
'[the] [a] [an] {name}[s] toggle',
])
+ @callback
+ def register_utterances(component):
+ """Register utterances for a component."""
+ if component not in UTTERANCES:
+ return
+ for intent_type, sentences in UTTERANCES[component].items():
+ async_register(hass, intent_type, sentences)
+
+ @callback
+ def component_loaded(event):
+ """Handle a new component loaded."""
+ register_utterances(event.data[ATTR_COMPONENT])
+
+ hass.bus.async_listen(EVENT_COMPONENT_LOADED, component_loaded)
+
+ # Check already loaded components.
+ for component in hass.config.components:
+ register_utterances(component)
+
return True
diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py
index b24361d8293..e4c8f5634cf 100644
--- a/homeassistant/components/cover/__init__.py
+++ b/homeassistant/components/cover/__init__.py
@@ -17,6 +17,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv
from homeassistant.components import group
+from homeassistant.helpers import intent
from homeassistant.const import (
SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_SET_COVER_POSITION,
SERVICE_STOP_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_CLOSE_COVER_TILT,
@@ -55,6 +56,9 @@ ATTR_CURRENT_TILT_POSITION = 'current_tilt_position'
ATTR_POSITION = 'position'
ATTR_TILT_POSITION = 'tilt_position'
+INTENT_OPEN_COVER = 'HassOpenCover'
+INTENT_CLOSE_COVER = 'HassCloseCover'
+
COVER_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
@@ -181,6 +185,12 @@ async def async_setup(hass, config):
hass.services.async_register(
DOMAIN, service_name, async_handle_cover_service,
schema=schema)
+ hass.helpers.intent.async_register(intent.ServiceIntentHandler(
+ INTENT_OPEN_COVER, DOMAIN, SERVICE_OPEN_COVER,
+ "Opened {}"))
+ hass.helpers.intent.async_register(intent.ServiceIntentHandler(
+ INTENT_CLOSE_COVER, DOMAIN, SERVICE_CLOSE_COVER,
+ "Closed {}"))
return True
diff --git a/homeassistant/components/cover/gogogate2.py b/homeassistant/components/cover/gogogate2.py
new file mode 100644
index 00000000000..c2bdc9c5472
--- /dev/null
+++ b/homeassistant/components/cover/gogogate2.py
@@ -0,0 +1,120 @@
+"""
+Support for Gogogate2 Garage Doors.
+
+For more details about this platform, please refer to the documentation
+https://home-assistant.io/components/cover.gogogate2/
+"""
+import logging
+
+import voluptuous as vol
+
+from homeassistant.components.cover import (
+ CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE)
+from homeassistant.const import (
+ CONF_USERNAME, CONF_PASSWORD, STATE_CLOSED, STATE_UNKNOWN,
+ CONF_IP_ADDRESS, CONF_NAME)
+import homeassistant.helpers.config_validation as cv
+
+REQUIREMENTS = ['pygogogate2==0.0.3']
+
+_LOGGER = logging.getLogger(__name__)
+
+DEFAULT_NAME = 'gogogate2'
+
+NOTIFICATION_ID = 'gogogate2_notification'
+NOTIFICATION_TITLE = 'Gogogate2 Cover Setup'
+
+COVER_SCHEMA = vol.Schema({
+ vol.Required(CONF_USERNAME): cv.string,
+ vol.Required(CONF_PASSWORD): cv.string,
+ vol.Required(CONF_IP_ADDRESS): cv.string,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+})
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up the Gogogate2 component."""
+ from pygogogate2 import Gogogate2API as pygogogate2
+
+ username = config.get(CONF_USERNAME)
+ password = config.get(CONF_PASSWORD)
+ ip_address = config.get(CONF_IP_ADDRESS)
+ name = config.get(CONF_NAME)
+ mygogogate2 = pygogogate2(username, password, ip_address)
+
+ try:
+ devices = mygogogate2.get_devices()
+ if devices is False:
+ raise ValueError(
+ "Username or Password is incorrect or no devices found")
+
+ add_devices(MyGogogate2Device(
+ mygogogate2, door, name) for door in devices)
+ return
+
+ except (TypeError, KeyError, NameError, ValueError) as ex:
+ _LOGGER.error("%s", ex)
+ hass.components.persistent_notification.create(
+ 'Error: {}
'
+ 'You will need to restart hass after fixing.'
+ ''.format(ex),
+ title=NOTIFICATION_TITLE,
+ notification_id=NOTIFICATION_ID)
+ return
+
+
+class MyGogogate2Device(CoverDevice):
+ """Representation of a Gogogate2 cover."""
+
+ def __init__(self, mygogogate2, device, name):
+ """Initialize with API object, device id."""
+ self.mygogogate2 = mygogogate2
+ self.device_id = device['door']
+ self._name = name or device['name']
+ self._status = device['status']
+ self.available = None
+
+ @property
+ def name(self):
+ """Return the name of the garage door if any."""
+ return self._name if self._name else DEFAULT_NAME
+
+ @property
+ def is_closed(self):
+ """Return true if cover is closed, else False."""
+ return self._status == STATE_CLOSED
+
+ @property
+ def device_class(self):
+ """Return the class of this device, from component DEVICE_CLASSES."""
+ return 'garage'
+
+ @property
+ def supported_features(self):
+ """Flag supported features."""
+ return SUPPORT_OPEN | SUPPORT_CLOSE
+
+ @property
+ def available(self):
+ """Could the device be accessed during the last update call."""
+ return self.available
+
+ def close_cover(self, **kwargs):
+ """Issue close command to cover."""
+ self.mygogogate2.close_device(self.device_id)
+ self.schedule_update_ha_state(True)
+
+ def open_cover(self, **kwargs):
+ """Issue open command to cover."""
+ self.mygogogate2.open_device(self.device_id)
+ self.schedule_update_ha_state(True)
+
+ def update(self):
+ """Update status of cover."""
+ try:
+ self._status = self.mygogogate2.get_status(self.device_id)
+ self.available = True
+ except (TypeError, KeyError, NameError, ValueError) as ex:
+ _LOGGER.error("%s", ex)
+ self._status = STATE_UNKNOWN
+ self.available = False
diff --git a/homeassistant/components/cover/mysensors.py b/homeassistant/components/cover/mysensors.py
index 391d2a22bda..669a7ce6723 100644
--- a/homeassistant/components/cover/mysensors.py
+++ b/homeassistant/components/cover/mysensors.py
@@ -9,10 +9,12 @@ from homeassistant.components.cover import ATTR_POSITION, DOMAIN, CoverDevice
from homeassistant.const import STATE_OFF, STATE_ON
-def setup_platform(hass, config, add_devices, discovery_info=None):
- """Set up the MySensors platform for covers."""
+async def async_setup_platform(
+ hass, config, async_add_devices, discovery_info=None):
+ """Set up the mysensors platform for covers."""
mysensors.setup_mysensors_platform(
- hass, DOMAIN, discovery_info, MySensorsCover, add_devices=add_devices)
+ hass, DOMAIN, discovery_info, MySensorsCover,
+ async_add_devices=async_add_devices)
class MySensorsCover(mysensors.MySensorsEntity, CoverDevice):
diff --git a/homeassistant/components/cover/rpi_gpio.py b/homeassistant/components/cover/rpi_gpio.py
index 77cd0b0f7e2..49666139330 100644
--- a/homeassistant/components/cover/rpi_gpio.py
+++ b/homeassistant/components/cover/rpi_gpio.py
@@ -87,7 +87,7 @@ class RPiGPIOCover(CoverDevice):
self._invert_relay = invert_relay
rpi_gpio.setup_output(self._relay_pin)
rpi_gpio.setup_input(self._state_pin, self._state_pull_mode)
- rpi_gpio.write_output(self._relay_pin, not self._invert_relay)
+ rpi_gpio.write_output(self._relay_pin, 0 if self._invert_relay else 1)
@property
def name(self):
@@ -105,9 +105,9 @@ class RPiGPIOCover(CoverDevice):
def _trigger(self):
"""Trigger the cover."""
- rpi_gpio.write_output(self._relay_pin, self._invert_relay)
+ rpi_gpio.write_output(self._relay_pin, 1 if self._invert_relay else 0)
sleep(self._relay_time)
- rpi_gpio.write_output(self._relay_pin, not self._invert_relay)
+ rpi_gpio.write_output(self._relay_pin, 0 if self._invert_relay else 1)
def close_cover(self, **kwargs):
"""Close the cover."""
diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json
new file mode 100644
index 00000000000..69165dbbbaf
--- /dev/null
+++ b/homeassistant/components/deconz/.translations/en.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "title": "deCONZ",
+ "step": {
+ "init": {
+ "title": "Define deCONZ gateway",
+ "data": {
+ "host": "Host",
+ "port": "Port (default value: '80')"
+ }
+ },
+ "link": {
+ "title": "Link with deCONZ",
+ "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button"
+ }
+ },
+ "error": {
+ "no_key": "Couldn't get an API key"
+ },
+ "abort": {
+ "no_bridges": "No deCONZ bridges discovered",
+ "one_instance_only": "Component only supports one deCONZ instance"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py
index 26d9fb401e4..85ba271ec3a 100644
--- a/homeassistant/components/deconz/__init__.py
+++ b/homeassistant/components/deconz/__init__.py
@@ -8,16 +8,17 @@ import logging
import voluptuous as vol
+from homeassistant import config_entries
from homeassistant.components.discovery import SERVICE_DECONZ
from homeassistant.const import (
CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP)
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
-from homeassistant.helpers import discovery
+from homeassistant.helpers import discovery, aiohttp_client
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util.json import load_json, save_json
-REQUIREMENTS = ['pydeconz==32']
+REQUIREMENTS = ['pydeconz==35']
_LOGGER = logging.getLogger(__name__)
@@ -160,7 +161,8 @@ async def async_request_configuration(hass, config, deconz_config):
async def async_configuration_callback(data):
"""Set up actions to do when our configuration callback is called."""
from pydeconz.utils import async_get_api_key
- api_key = await async_get_api_key(hass.loop, **deconz_config)
+ websession = async_get_clientsession(hass)
+ api_key = await async_get_api_key(websession, **deconz_config)
if api_key:
deconz_config[CONF_API_KEY] = api_key
result = await async_setup_deconz(hass, config, deconz_config)
@@ -186,3 +188,85 @@ async def async_request_configuration(hass, config, deconz_config):
entity_picture="/static/images/logo_deconz.jpeg",
submit_caption="I have unlocked the gateway",
)
+
+
+@config_entries.HANDLERS.register(DOMAIN)
+class DeconzFlowHandler(config_entries.ConfigFlowHandler):
+ """Handle a deCONZ config flow."""
+
+ VERSION = 1
+
+ def __init__(self):
+ """Initialize the deCONZ flow."""
+ self.bridges = []
+ self.deconz_config = {}
+
+ async def async_step_init(self, user_input=None):
+ """Handle a flow start."""
+ from pydeconz.utils import async_discovery
+
+ if DOMAIN in self.hass.data:
+ return self.async_abort(
+ reason='one_instance_only'
+ )
+
+ if user_input is not None:
+ for bridge in self.bridges:
+ if bridge[CONF_HOST] == user_input[CONF_HOST]:
+ self.deconz_config = bridge
+ return await self.async_step_link()
+
+ session = aiohttp_client.async_get_clientsession(self.hass)
+ self.bridges = await async_discovery(session)
+
+ if len(self.bridges) == 1:
+ self.deconz_config = self.bridges[0]
+ return await self.async_step_link()
+ elif len(self.bridges) > 1:
+ hosts = []
+ for bridge in self.bridges:
+ hosts.append(bridge[CONF_HOST])
+ return self.async_show_form(
+ step_id='init',
+ data_schema=vol.Schema({
+ vol.Required(CONF_HOST): vol.In(hosts)
+ })
+ )
+
+ return self.async_abort(
+ reason='no_bridges'
+ )
+
+ async def async_step_link(self, user_input=None):
+ """Attempt to link with the deCONZ bridge."""
+ from pydeconz.utils import async_get_api_key
+ errors = {}
+
+ if user_input is not None:
+ session = aiohttp_client.async_get_clientsession(self.hass)
+ api_key = await async_get_api_key(session, **self.deconz_config)
+ if api_key:
+ self.deconz_config[CONF_API_KEY] = api_key
+ return self.async_create_entry(
+ title='deCONZ',
+ data=self.deconz_config
+ )
+ else:
+ errors['base'] = 'no_key'
+
+ return self.async_show_form(
+ step_id='link',
+ errors=errors,
+ )
+
+
+async def async_setup_entry(hass, entry):
+ """Set up a bridge for a config entry."""
+ if DOMAIN in hass.data:
+ _LOGGER.error(
+ "Config entry failed since one deCONZ instance already exists")
+ return False
+ result = await async_setup_deconz(hass, None, entry.data)
+ if result:
+ return True
+ return False
diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json
new file mode 100644
index 00000000000..69165dbbbaf
--- /dev/null
+++ b/homeassistant/components/deconz/strings.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "title": "deCONZ",
+ "step": {
+ "init": {
+ "title": "Define deCONZ gateway",
+ "data": {
+ "host": "Host",
+ "port": "Port (default value: '80')"
+ }
+ },
+ "link": {
+ "title": "Link with deCONZ",
+ "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button"
+ }
+ },
+ "error": {
+ "no_key": "Couldn't get an API key"
+ },
+ "abort": {
+ "no_bridges": "No deCONZ bridges discovered",
+ "one_instance_only": "Component only supports one deCONZ instance"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py
index 682496335a0..45f0e51a214 100644
--- a/homeassistant/components/device_tracker/__init__.py
+++ b/homeassistant/components/device_tracker/__init__.py
@@ -9,8 +9,6 @@ from datetime import timedelta
import logging
from typing import Any, List, Sequence, Callable
-import aiohttp
-import async_timeout
import voluptuous as vol
from homeassistant.setup import async_prepare_setup_platform
@@ -19,7 +17,6 @@ from homeassistant.loader import bind_hass
from homeassistant.components import group, zone
from homeassistant.config import load_yaml_config_file, async_log_exception
from homeassistant.exceptions import HomeAssistantError
-from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers import config_per_platform, discovery
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_time_interval
@@ -76,7 +73,6 @@ ATTR_LOCATION_NAME = 'location_name'
ATTR_MAC = 'mac'
ATTR_NAME = 'name'
ATTR_SOURCE_TYPE = 'source_type'
-ATTR_VENDOR = 'vendor'
ATTR_CONSIDER_HOME = 'consider_home'
SOURCE_TYPE_GPS = 'gps'
@@ -328,14 +324,10 @@ class DeviceTracker(object):
self.hass, util.slugify(GROUP_NAME_ALL_DEVICES), visible=False,
name=GROUP_NAME_ALL_DEVICES, add=[device.entity_id])
- # lookup mac vendor string to be stored in config
- 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
@@ -413,7 +405,6 @@ class Device(Entity):
consider_home = None # type: dt_util.dt.timedelta
battery = None # type: int
attributes = None # type: dict
- vendor = None # type: str
icon = None # type: str
# Track if the last update of this device was HOME.
@@ -423,7 +414,7 @@ class Device(Entity):
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
track: bool, dev_id: str, mac: str, name: str = None,
picture: str = None, gravatar: str = None, icon: str = None,
- hide_if_away: bool = False, vendor: str = None) -> None:
+ hide_if_away: bool = False) -> None:
"""Initialize a device."""
self.hass = hass
self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
@@ -451,7 +442,6 @@ class Device(Entity):
self.icon = icon
self.away_hide = hide_if_away
- self.vendor = vendor
self.source_type = None
@@ -567,51 +557,6 @@ class Device(Entity):
self._state = STATE_HOME
self.last_update_home = True
- @asyncio.coroutine
- def set_vendor_for_mac(self):
- """Set vendor string using api.macvendors.com."""
- self.vendor = yield from self.get_vendor_for_mac()
-
- @asyncio.coroutine
- def get_vendor_for_mac(self):
- """Try to find the vendor string for a given MAC address."""
- if not self.mac:
- return None
-
- if '_' in self.mac:
- _, mac = self.mac.split('_', 1)
- else:
- mac = self.mac
-
- if not len(mac.split(':')) == 6:
- return 'unknown'
-
- # We only need the first 3 bytes of the MAC for a lookup
- # this improves somewhat on privacy
- oui_bytes = mac.split(':')[0:3]
- # bytes like 00 get truncates to 0, API needs full bytes
- oui = '{:02x}:{:02x}:{:02x}'.format(*[int(b, 16) for b in oui_bytes])
- url = 'http://api.macvendors.com/' + oui
- try:
- websession = async_get_clientsession(self.hass)
-
- with async_timeout.timeout(5, loop=self.hass.loop):
- resp = yield from websession.get(url)
- # mac vendor found, response is the string
- if resp.status == 200:
- vendor_string = yield from resp.text()
- return vendor_string
- # If vendor is not known to the API (404) or there
- # was a failure during the lookup (500); set vendor
- # to something other then None to prevent retry
- # as the value is only relevant when it is to be stored
- # in the 'known_devices.yaml' file which only happens
- # the first time the device is seen.
- return 'unknown'
- except (asyncio.TimeoutError, aiohttp.ClientError):
- # Same as above
- return 'unknown'
-
@asyncio.coroutine
def async_added_to_hass(self):
"""Add an entity."""
@@ -685,7 +630,6 @@ def async_load_config(path: str, hass: HomeAssistantType,
vol.Optional('picture', default=None): vol.Any(None, cv.string),
vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All(
cv.time_period, cv.positive_timedelta),
- vol.Optional('vendor', default=None): vol.Any(None, cv.string),
})
try:
result = []
@@ -697,6 +641,8 @@ def async_load_config(path: str, hass: HomeAssistantType,
return []
for dev_id, device in devices.items():
+ # Deprecated option. We just ignore it to avoid breaking change
+ device.pop('vendor', None)
try:
device = dev_schema(device)
device['dev_id'] = cv.slugify(dev_id)
@@ -772,7 +718,6 @@ def update_config(path: str, dev_id: str, device: Device):
'picture': device.config_picture,
'track': device.track,
CONF_AWAY_HIDE: device.away_hide,
- 'vendor': device.vendor,
}}
out.write('\n')
out.write(dump(device))
diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py
index 14aea561c8e..7e9b10e9241 100644
--- a/homeassistant/components/device_tracker/asuswrt.py
+++ b/homeassistant/components/device_tracker/asuswrt.py
@@ -25,6 +25,7 @@ _LOGGER = logging.getLogger(__name__)
CONF_PUB_KEY = 'pub_key'
CONF_SSH_KEY = 'ssh_key'
+CONF_REQUIRE_IP = 'require_ip'
DEFAULT_SSH_PORT = 22
SECRET_GROUP = 'Password or SSH Key'
@@ -36,6 +37,7 @@ PLATFORM_SCHEMA = vol.All(
vol.Optional(CONF_PROTOCOL, default='ssh'): vol.In(['ssh', 'telnet']),
vol.Optional(CONF_MODE, default='router'): vol.In(['router', 'ap']),
vol.Optional(CONF_PORT, default=DEFAULT_SSH_PORT): cv.port,
+ vol.Optional(CONF_REQUIRE_IP, default=True): cv.boolean,
vol.Exclusive(CONF_PASSWORD, SECRET_GROUP): cv.string,
vol.Exclusive(CONF_SSH_KEY, SECRET_GROUP): cv.isfile,
vol.Exclusive(CONF_PUB_KEY, SECRET_GROUP): cv.isfile
@@ -115,6 +117,7 @@ class AsusWrtDeviceScanner(DeviceScanner):
self.protocol = config[CONF_PROTOCOL]
self.mode = config[CONF_MODE]
self.port = config[CONF_PORT]
+ self.require_ip = config[CONF_REQUIRE_IP]
if self.protocol == 'ssh':
self.connection = SshConnection(
@@ -172,7 +175,7 @@ class AsusWrtDeviceScanner(DeviceScanner):
ret_devices = {}
for key in devices:
- if devices[key].ip is not None:
+ if not self.require_ip or devices[key].ip is not None:
ret_devices[key] = devices[key]
return ret_devices
diff --git a/homeassistant/components/device_tracker/bluetooth_tracker.py b/homeassistant/components/device_tracker/bluetooth_tracker.py
index 9d41611d9a2..807f6c0d0a4 100644
--- a/homeassistant/components/device_tracker/bluetooth_tracker.py
+++ b/homeassistant/components/device_tracker/bluetooth_tracker.py
@@ -17,12 +17,15 @@ import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
-REQUIREMENTS = ['pybluez==0.22']
+REQUIREMENTS = ['pybluez==0.22', 'bt_proximity==0.1.2']
BT_PREFIX = 'BT_'
+CONF_REQUEST_RSSI = 'request_rssi'
+
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
- vol.Optional(CONF_TRACK_NEW): cv.boolean
+ vol.Optional(CONF_TRACK_NEW): cv.boolean,
+ vol.Optional(CONF_REQUEST_RSSI): cv.boolean
})
@@ -30,11 +33,15 @@ def setup_scanner(hass, config, see, discovery_info=None):
"""Set up the Bluetooth Scanner."""
# pylint: disable=import-error
import bluetooth
+ from bt_proximity import BluetoothRSSI
- def see_device(device):
+ def see_device(mac, name, rssi=None):
"""Mark a device as seen."""
- see(mac=BT_PREFIX + device[0], host_name=device[1],
- source_type=SOURCE_TYPE_BLUETOOTH)
+ attributes = {}
+ if rssi is not None:
+ attributes['rssi'] = rssi
+ see(mac="{}_{}".format(BT_PREFIX, mac), host_name=name,
+ attributes=attributes, source_type=SOURCE_TYPE_BLUETOOTH)
def discover_devices():
"""Discover Bluetooth devices."""
@@ -64,27 +71,32 @@ def setup_scanner(hass, config, see, discovery_info=None):
if track_new:
for dev in discover_devices():
if dev[0] not in devs_to_track and \
- dev[0] not in devs_donot_track:
+ dev[0] not in devs_donot_track:
devs_to_track.append(dev[0])
- see_device(dev)
+ see_device(dev[0], dev[1])
interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
+ request_rssi = config.get(CONF_REQUEST_RSSI, False)
+
def update_bluetooth(now):
"""Lookup Bluetooth device and update status."""
try:
if track_new:
for dev in discover_devices():
if dev[0] not in devs_to_track and \
- dev[0] not in devs_donot_track:
+ dev[0] not in devs_donot_track:
devs_to_track.append(dev[0])
for mac in devs_to_track:
_LOGGER.debug("Scanning %s", mac)
result = bluetooth.lookup_name(mac, timeout=5)
- if not result:
+ rssi = None
+ if request_rssi:
+ rssi = BluetoothRSSI(mac).request_rssi()
+ if result is None:
# Could not lookup device name
continue
- see_device((mac, result))
+ see_device(mac, result, rssi)
except bluetooth.BluetoothError:
_LOGGER.exception("Error looking up Bluetooth device")
track_point_in_utc_time(
diff --git a/homeassistant/components/device_tracker/bmw_connected_drive.py b/homeassistant/components/device_tracker/bmw_connected_drive.py
index 6ba2681e4cd..2267bb51944 100644
--- a/homeassistant/components/device_tracker/bmw_connected_drive.py
+++ b/homeassistant/components/device_tracker/bmw_connected_drive.py
@@ -36,16 +36,20 @@ class BMWDeviceTracker(object):
self.vehicle = vehicle
def update(self) -> None:
- """Update the device info."""
- dev_id = slugify(self.vehicle.modelName)
+ """Update the device info.
+
+ Only update the state in home assistant if tracking in
+ the car is enabled.
+ """
+ dev_id = slugify(self.vehicle.name)
+
+ if not self.vehicle.state.is_vehicle_tracking_enabled:
+ _LOGGER.debug('Tracking is disabled for vehicle %s', dev_id)
+ return
+
_LOGGER.debug('Updating %s', dev_id)
- attrs = {
- 'trackr_id': dev_id,
- 'id': dev_id,
- 'name': self.vehicle.modelName
- }
+
self._see(
- dev_id=dev_id, host_name=self.vehicle.modelName,
- gps=self.vehicle.state.gps_position, attributes=attrs,
- icon='mdi:car'
+ dev_id=dev_id, host_name=self.vehicle.name,
+ gps=self.vehicle.state.gps_position, icon='mdi:car'
)
diff --git a/homeassistant/components/device_tracker/google_maps.py b/homeassistant/components/device_tracker/google_maps.py
new file mode 100644
index 00000000000..9e257616361
--- /dev/null
+++ b/homeassistant/components/device_tracker/google_maps.py
@@ -0,0 +1,83 @@
+"""
+Support for Google Maps location sharing.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/device_tracker.google_maps/
+"""
+import logging
+from datetime import timedelta
+
+import voluptuous as vol
+
+import homeassistant.helpers.config_validation as cv
+from homeassistant.components.device_tracker import (
+ PLATFORM_SCHEMA, SOURCE_TYPE_GPS)
+from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
+from homeassistant.helpers.event import track_time_interval
+from homeassistant.helpers.typing import ConfigType
+from homeassistant.util import slugify
+
+_LOGGER = logging.getLogger(__name__)
+
+REQUIREMENTS = ['locationsharinglib==0.4.0']
+
+CREDENTIALS_FILE = '.google_maps_location_sharing.cookies'
+
+MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30)
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_USERNAME): cv.string,
+ vol.Required(CONF_PASSWORD): cv.string,
+})
+
+
+def setup_scanner(hass, config: ConfigType, see, discovery_info=None):
+ """Set up the scanner."""
+ scanner = GoogleMapsScanner(hass, config, see)
+ return scanner.success_init
+
+
+class GoogleMapsScanner(object):
+ """Representation of an Google Maps location sharing account."""
+
+ def __init__(self, hass, config: ConfigType, see) -> None:
+ """Initialize the scanner."""
+ from locationsharinglib import Service
+ from locationsharinglib.locationsharinglibexceptions import InvalidUser
+
+ self.see = see
+ self.username = config[CONF_USERNAME]
+ self.password = config[CONF_PASSWORD]
+
+ try:
+ self.service = Service(self.username, self.password,
+ hass.config.path(CREDENTIALS_FILE))
+ self._update_info()
+
+ track_time_interval(
+ hass, self._update_info, MIN_TIME_BETWEEN_SCANS)
+
+ self.success_init = True
+
+ except InvalidUser:
+ _LOGGER.error('You have specified invalid login credentials')
+ self.success_init = False
+
+ def _update_info(self, now=None):
+ for person in self.service.get_all_people():
+ dev_id = 'google_maps_{0}'.format(slugify(person.id))
+
+ attrs = {
+ 'id': person.id,
+ 'nickname': person.nickname,
+ 'full_name': person.full_name,
+ 'last_seen': person.datetime,
+ 'address': person.address
+ }
+ self.see(
+ dev_id=dev_id,
+ gps=(person.latitude, person.longitude),
+ picture=person.picture_url,
+ source_type=SOURCE_TYPE_GPS,
+ attributes=attrs
+ )
diff --git a/homeassistant/components/device_tracker/mercedesme.py b/homeassistant/components/device_tracker/mercedesme.py
deleted file mode 100644
index dcc9e3ab2ec..00000000000
--- a/homeassistant/components/device_tracker/mercedesme.py
+++ /dev/null
@@ -1,74 +0,0 @@
-"""
-Support for Mercedes cars with Mercedes ME.
-
-For more details about this component, please refer to the documentation at
-https://home-assistant.io/components/device_tracker.mercedesme/
-"""
-import logging
-from datetime import timedelta
-
-from homeassistant.components.mercedesme import DATA_MME
-from homeassistant.helpers.event import track_time_interval
-from homeassistant.util import Throttle
-
-_LOGGER = logging.getLogger(__name__)
-
-DEPENDENCIES = ['mercedesme']
-
-MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30)
-
-
-def setup_scanner(hass, config, see, discovery_info=None):
- """Set up the Mercedes ME tracker."""
- if discovery_info is None:
- return False
-
- data = hass.data[DATA_MME].data
-
- if not data.cars:
- return False
-
- MercedesMEDeviceTracker(hass, config, see, data)
-
- return True
-
-
-class MercedesMEDeviceTracker(object):
- """A class representing a Mercedes ME device tracker."""
-
- def __init__(self, hass, config, see, data):
- """Initialize the Mercedes ME device tracker."""
- self.see = see
- self.data = data
- self.update_info()
-
- track_time_interval(
- hass, self.update_info, MIN_TIME_BETWEEN_SCANS)
-
- @Throttle(MIN_TIME_BETWEEN_SCANS)
- def update_info(self, now=None):
- """Update the device info."""
- for device in self.data.cars:
- if not device['services'].get('VEHICLE_FINDER', False):
- continue
-
- location = self.data.get_location(device["vin"])
- if location is None:
- continue
-
- dev_id = device["vin"]
- name = device["license"]
-
- lat = location['positionLat']['value']
- lon = location['positionLong']['value']
- attrs = {
- 'trackr_id': dev_id,
- 'id': dev_id,
- 'name': name
- }
- self.see(
- dev_id=dev_id, host_name=name,
- gps=(lat, lon), attributes=attrs
- )
-
- return True
diff --git a/homeassistant/components/device_tracker/mysensors.py b/homeassistant/components/device_tracker/mysensors.py
index f68eb361ca0..b0d29bf0566 100644
--- a/homeassistant/components/device_tracker/mysensors.py
+++ b/homeassistant/components/device_tracker/mysensors.py
@@ -6,15 +6,15 @@ https://home-assistant.io/components/device_tracker.mysensors/
"""
from homeassistant.components import mysensors
from homeassistant.components.device_tracker import DOMAIN
-from homeassistant.helpers.dispatcher import dispatcher_connect
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util import slugify
-def setup_scanner(hass, config, see, discovery_info=None):
+async def async_setup_scanner(hass, config, async_see, discovery_info=None):
"""Set up the MySensors device scanner."""
new_devices = mysensors.setup_mysensors_platform(
hass, DOMAIN, discovery_info, MySensorsDeviceScanner,
- device_args=(see, ))
+ device_args=(async_see, ))
if not new_devices:
return False
@@ -22,9 +22,9 @@ def setup_scanner(hass, config, see, discovery_info=None):
dev_id = (
id(device.gateway), device.node_id, device.child_id,
device.value_type)
- dispatcher_connect(
+ async_dispatcher_connect(
hass, mysensors.SIGNAL_CALLBACK.format(*dev_id),
- device.update_callback)
+ device.async_update_callback)
return True
@@ -32,20 +32,20 @@ def setup_scanner(hass, config, see, discovery_info=None):
class MySensorsDeviceScanner(mysensors.MySensorsDevice):
"""Represent a MySensors scanner."""
- def __init__(self, see, *args):
+ def __init__(self, async_see, *args):
"""Set up instance."""
super().__init__(*args)
- self.see = see
+ self.async_see = async_see
- def update_callback(self):
+ async def async_update_callback(self):
"""Update the device."""
- self.update()
+ await self.async_update()
node = self.gateway.sensors[self.node_id]
child = node.children[self.child_id]
position = child.values[self.value_type]
latitude, longitude, _ = position.split(',')
- self.see(
+ await self.async_see(
dev_id=slugify(self.name),
host_name=self.name,
gps=(latitude, longitude),
diff --git a/homeassistant/components/device_tracker/ubus.py b/homeassistant/components/device_tracker/ubus.py
index c75529655f4..dd12df7b070 100644
--- a/homeassistant/components/device_tracker/ubus.py
+++ b/homeassistant/components/device_tracker/ubus.py
@@ -95,7 +95,7 @@ class UbusDeviceScanner(DeviceScanner):
return self.last_results
def _generate_mac2name(self):
- """Return empty MAC to name dict. Overriden if DHCP server is set."""
+ """Return empty MAC to name dict. Overridden if DHCP server is set."""
self.mac2name = dict()
@_refresh_on_access_denied
diff --git a/homeassistant/components/device_tracker/xiaomi_miio.py b/homeassistant/components/device_tracker/xiaomi_miio.py
new file mode 100644
index 00000000000..61568892388
--- /dev/null
+++ b/homeassistant/components/device_tracker/xiaomi_miio.py
@@ -0,0 +1,77 @@
+"""
+Support for Xiaomi Mi WiFi Repeater 2.
+
+For more details about this platform, please refer to the documentation
+https://home-assistant.io/components/device_tracker.xiaomi_miio/
+"""
+import logging
+
+import voluptuous as vol
+
+import homeassistant.helpers.config_validation as cv
+from homeassistant.components.device_tracker import (DOMAIN, PLATFORM_SCHEMA,
+ DeviceScanner)
+from homeassistant.const import (CONF_HOST, CONF_TOKEN)
+
+_LOGGER = logging.getLogger(__name__)
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_HOST): cv.string,
+ vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)),
+})
+
+REQUIREMENTS = ['python-miio==0.3.9']
+
+
+def get_scanner(hass, config):
+ """Return a Xiaomi MiIO device scanner."""
+ from miio import WifiRepeater, DeviceException
+
+ scanner = None
+ host = config[DOMAIN].get(CONF_HOST)
+ token = config[DOMAIN].get(CONF_TOKEN)
+
+ _LOGGER.info(
+ "Initializing with host %s (token %s...)", host, token[:5])
+
+ try:
+ device = WifiRepeater(host, token)
+ device_info = device.info()
+ _LOGGER.info("%s %s %s detected",
+ device_info.model,
+ device_info.firmware_version,
+ device_info.hardware_version)
+ scanner = XiaomiMiioDeviceScanner(hass, device)
+ except DeviceException as ex:
+ _LOGGER.error("Device unavailable or token incorrect: %s", ex)
+
+ return scanner
+
+
+class XiaomiMiioDeviceScanner(DeviceScanner):
+ """This class queries a Xiaomi Mi WiFi Repeater."""
+
+ def __init__(self, hass, device):
+ """Initialize the scanner."""
+ self.device = device
+
+ async def async_scan_devices(self):
+ """Scan for devices and return a list containing found device ids."""
+ from miio import DeviceException
+
+ devices = []
+ try:
+ station_info = await self.hass.async_add_job(self.device.status)
+ _LOGGER.debug("Got new station info: %s", station_info)
+
+ for device in station_info['mat']:
+ devices.append(device['mac'])
+
+ except DeviceException as ex:
+ _LOGGER.error("Got exception while fetching the state: %s", ex)
+
+ return devices
+
+ async def async_get_device_name(self, device):
+ """The repeater doesn't provide the name of the associated device."""
+ return None
diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py
index eb53782d698..b2aa5b890a8 100644
--- a/homeassistant/components/discovery.py
+++ b/homeassistant/components/discovery.py
@@ -13,6 +13,7 @@ import os
import voluptuous as vol
+from homeassistant import config_entries
from homeassistant.core import callback
from homeassistant.const import EVENT_HOMEASSISTANT_START
import homeassistant.helpers.config_validation as cv
@@ -40,6 +41,10 @@ SERVICE_DECONZ = 'deconz'
SERVICE_DAIKIN = 'daikin'
SERVICE_SAMSUNG_PRINTER = 'samsung_printer'
+CONFIG_ENTRY_HANDLERS = {
+ SERVICE_HUE: 'hue',
+}
+
SERVICE_HANDLERS = {
SERVICE_HASS_IOS_APP: ('ios', None),
SERVICE_NETGEAR: ('device_tracker', None),
@@ -51,7 +56,6 @@ SERVICE_HANDLERS = {
SERVICE_WINK: ('wink', None),
SERVICE_XIAOMI_GW: ('xiaomi_aqara', None),
SERVICE_TELLDUSLIVE: ('tellduslive', None),
- SERVICE_HUE: ('hue', None),
SERVICE_DECONZ: ('deconz', None),
SERVICE_DAIKIN: ('daikin', None),
SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'),
@@ -105,6 +109,20 @@ async def async_setup(hass, config):
logger.info("Ignoring service: %s %s", service, info)
return
+ discovery_hash = json.dumps([service, info], sort_keys=True)
+ if discovery_hash in already_discovered:
+ return
+
+ already_discovered.add(discovery_hash)
+
+ if service in CONFIG_ENTRY_HANDLERS:
+ await hass.config_entries.flow.async_init(
+ CONFIG_ENTRY_HANDLERS[service],
+ source=config_entries.SOURCE_DISCOVERY,
+ data=info
+ )
+ return
+
comp_plat = SERVICE_HANDLERS.get(service)
# We do not know how to handle this service.
@@ -112,12 +130,6 @@ async def async_setup(hass, config):
logger.info("Unknown service discovered: %s %s", service, info)
return
- discovery_hash = json.dumps([service, info], sort_keys=True)
- if discovery_hash in already_discovered:
- return
-
- already_discovered.add(discovery_hash)
-
logger.info("Found new service: %s %s", service, info)
component, platform = comp_plat
diff --git a/homeassistant/components/doorbird.py b/homeassistant/components/doorbird.py
index 34758023f60..48f229b49ca 100644
--- a/homeassistant/components/doorbird.py
+++ b/homeassistant/components/doorbird.py
@@ -22,6 +22,7 @@ DOMAIN = 'doorbird'
API_URL = '/api/{}'.format(DOMAIN)
CONF_DOORBELL_EVENTS = 'doorbell_events'
+CONF_CUSTOM_URL = 'hass_url_override'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
@@ -29,6 +30,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_DOORBELL_EVENTS): cv.boolean,
+ vol.Optional(CONF_CUSTOM_URL): cv.string,
})
}, extra=vol.ALLOW_EXTRA)
@@ -61,9 +63,17 @@ def setup(hass, config):
# Provide an endpoint for the device to call to trigger events
hass.http.register_view(DoorbirdRequestView())
+ # Get the URL of this server
+ hass_url = hass.config.api.base_url
+
+ # Override it if another is specified in the component configuration
+ if config[DOMAIN].get(CONF_CUSTOM_URL):
+ hass_url = config[DOMAIN].get(CONF_CUSTOM_URL)
+ _LOGGER.info("DoorBird will connect to this instance via %s",
+ hass_url)
+
# This will make HA the only service that gets doorbell events
- url = '{}{}/{}'.format(
- hass.config.api.base_url, API_URL, SENSOR_DOORBELL)
+ url = '{}{}/{}'.format(hass_url, API_URL, SENSOR_DOORBELL)
device.reset_notifications()
device.subscribe_notification(SENSOR_DOORBELL, url)
diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py
index 09ce1a57060..fa558cf299f 100644
--- a/homeassistant/components/emulated_hue/__init__.py
+++ b/homeassistant/components/emulated_hue/__init__.py
@@ -158,10 +158,6 @@ class Config(object):
"Listen port not specified, defaulting to %s",
self.listen_port)
- if self.type == TYPE_GOOGLE and self.listen_port != 80:
- _LOGGER.warning("When targeting Google Home, listening port has "
- "to be port 80")
-
# Get whether or not UPNP binds to multicast address (239.255.255.250)
# or to the unicast address (host_ip_addr)
self.upnp_bind_multicast = conf.get(
diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml
index a306cf7767c..a74f67b83fb 100644
--- a/homeassistant/components/fan/services.yaml
+++ b/homeassistant/components/fan/services.yaml
@@ -68,50 +68,50 @@ xiaomi_miio_set_buzzer_on:
description: Turn the buzzer on.
fields:
entity_id:
- description: Name of the air purifier entity.
- example: 'fan.xiaomi_air_purifier'
+ description: Name of the xiaomi miio entity.
+ example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_buzzer_off:
description: Turn the buzzer off.
fields:
entity_id:
- description: Name of the air purifier entity.
- example: 'fan.xiaomi_air_purifier'
+ description: Name of the xiaomi miio entity.
+ example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_led_on:
description: Turn the led on.
fields:
entity_id:
- description: Name of the air purifier entity.
- example: 'fan.xiaomi_air_purifier'
+ description: Name of the xiaomi miio entity.
+ example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_led_off:
description: Turn the led off.
fields:
entity_id:
- description: Name of the air purifier entity.
- example: 'fan.xiaomi_air_purifier'
+ description: Name of the xiaomi miio entity.
+ example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_child_lock_on:
description: Turn the child lock on.
fields:
entity_id:
- description: Name of the air purifier entity.
- example: 'fan.xiaomi_air_purifier'
+ description: Name of the xiaomi miio entity.
+ example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_child_lock_off:
description: Turn the child lock off.
fields:
entity_id:
- description: Name of the air purifier entity.
- example: 'fan.xiaomi_air_purifier'
+ description: Name of the xiaomi miio entity.
+ example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_favorite_level:
description: Set the favorite level.
fields:
entity_id:
- description: Name of the air purifier entity.
- example: 'fan.xiaomi_air_purifier'
+ description: Name of the xiaomi miio entity.
+ example: 'fan.xiaomi_miio_device'
level:
description: Level, between 0 and 16.
example: 1
@@ -120,8 +120,87 @@ xiaomi_miio_set_led_brightness:
description: Set the led brightness.
fields:
entity_id:
- description: Name of the air purifier entity.
- example: 'fan.xiaomi_air_purifier'
+ description: Name of the xiaomi miio entity.
+ example: 'fan.xiaomi_miio_device'
brightness:
description: Brightness (0 = Bright, 1 = Dim, 2 = Off)
example: 1
+
+xiaomi_miio_set_auto_detect_on:
+ description: Turn the auto detect on.
+ fields:
+ entity_id:
+ description: Name of the xiaomi miio entity.
+ example: 'fan.xiaomi_miio_device'
+
+xiaomi_miio_set_auto_detect_off:
+ description: Turn the auto detect off.
+ fields:
+ entity_id:
+ description: Name of the xiaomi miio entity.
+ example: 'fan.xiaomi_miio_device'
+
+xiaomi_miio_set_learn_mode_on:
+ description: Turn the learn mode on.
+ fields:
+ entity_id:
+ description: Name of the xiaomi miio entity.
+ example: 'fan.xiaomi_miio_device'
+
+xiaomi_miio_set_learn_mode_off:
+ description: Turn the learn mode off.
+ fields:
+ entity_id:
+ description: Name of the xiaomi miio entity.
+ example: 'fan.xiaomi_miio_device'
+
+xiaomi_miio_set_volume:
+ description: Set the sound volume.
+ fields:
+ entity_id:
+ description: Name of the xiaomi miio entity.
+ example: 'fan.xiaomi_miio_device'
+ volume:
+ description: Volume, between 0 and 100.
+ example: 50
+
+xiaomi_miio_reset_filter:
+ description: Reset the filter lifetime and usage.
+ fields:
+ entity_id:
+ description: Name of the xiaomi miio entity.
+ example: 'fan.xiaomi_miio_device'
+
+xiaomi_miio_set_extra_features:
+ description: Manipulates a storage register which advertises extra features. The Mi Home app evaluates the value. A feature called "turbo mode" is unlocked in the app on value 1.
+ fields:
+ entity_id:
+ description: Name of the xiaomi miio entity.
+ example: 'fan.xiaomi_miio_device'
+ features:
+ description: Integer, known values are 0 (default) and 1 (turbo mode).
+ example: 1
+
+xiaomi_miio_set_target_humidity:
+ description: Set the target humidity.
+ fields:
+ entity_id:
+ description: Name of the xiaomi miio entity.
+ example: 'fan.xiaomi_miio_device'
+ humidity:
+ description: Target humidity. Allowed values are 30, 40, 50, 60, 70 and 80.
+ example: 50
+
+xiaomi_miio_set_dry_on:
+ description: Turn the dry mode on.
+ fields:
+ entity_id:
+ description: Name of the xiaomi miio entity.
+ example: 'fan.xiaomi_miio_device'
+
+xiaomi_miio_set_dry_off:
+ description: Turn the dry mode off.
+ fields:
+ entity_id:
+ description: Name of the xiaomi miio entity.
+ example: 'fan.xiaomi_miio_device'
diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py
index 4df85711cfd..8dc6bb54bd1 100644
--- a/homeassistant/components/fan/xiaomi_miio.py
+++ b/homeassistant/components/fan/xiaomi_miio.py
@@ -1,16 +1,16 @@
"""
-Support for Xiaomi Mi Air Purifier 2.
+Support for Xiaomi Mi Air Purifier and Xiaomi Mi Air Humidifier.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/fan.xiaomi_miio/
"""
import asyncio
+from enum import Enum
from functools import partial
import logging
import voluptuous as vol
-from homeassistant.helpers.entity import ToggleEntity
from homeassistant.components.fan import (FanEntity, PLATFORM_SCHEMA,
SUPPORT_SET_SPEED, DOMAIN, )
from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN,
@@ -20,17 +20,40 @@ import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
-DEFAULT_NAME = 'Xiaomi Air Purifier'
-PLATFORM = 'xiaomi_miio'
+DEFAULT_NAME = 'Xiaomi Miio Device'
+DATA_KEY = 'fan.xiaomi_miio'
+
+CONF_MODEL = 'model'
+MODEL_AIRPURIFIER_PRO = 'zhimi.airpurifier.v6'
+MODEL_AIRPURIFIER_V3 = 'zhimi.airpurifier.v3'
+MODEL_AIRHUMIDIFIER_V1 = 'zhimi.humidifier.v1'
+MODEL_AIRHUMIDIFIER_CA = 'zhimi.humidifier.ca1'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_MODEL): vol.In(
+ ['zhimi.airpurifier.m1',
+ 'zhimi.airpurifier.m2',
+ 'zhimi.airpurifier.ma1',
+ 'zhimi.airpurifier.ma2',
+ 'zhimi.airpurifier.sa1',
+ 'zhimi.airpurifier.sa2',
+ 'zhimi.airpurifier.v1',
+ 'zhimi.airpurifier.v2',
+ 'zhimi.airpurifier.v3',
+ 'zhimi.airpurifier.v5',
+ 'zhimi.airpurifier.v6',
+ 'zhimi.humidifier.v1',
+ 'zhimi.humidifier.ca1']),
})
REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41']
+ATTR_MODEL = 'model'
+
+# Air Purifier
ATTR_TEMPERATURE = 'temperature'
ATTR_HUMIDITY = 'humidity'
ATTR_AIR_QUALITY_INDEX = 'aqi'
@@ -45,20 +68,190 @@ ATTR_LED_BRIGHTNESS = 'led_brightness'
ATTR_MOTOR_SPEED = 'motor_speed'
ATTR_AVERAGE_AIR_QUALITY_INDEX = 'average_aqi'
ATTR_PURIFY_VOLUME = 'purify_volume'
-
ATTR_BRIGHTNESS = 'brightness'
ATTR_LEVEL = 'level'
+ATTR_MOTOR2_SPEED = 'motor2_speed'
+ATTR_ILLUMINANCE = 'illuminance'
+ATTR_FILTER_RFID_PRODUCT_ID = 'filter_rfid_product_id'
+ATTR_FILTER_RFID_TAG = 'filter_rfid_tag'
+ATTR_FILTER_TYPE = 'filter_type'
+ATTR_LEARN_MODE = 'learn_mode'
+ATTR_SLEEP_TIME = 'sleep_time'
+ATTR_SLEEP_LEARN_COUNT = 'sleep_mode_learn_count'
+ATTR_EXTRA_FEATURES = 'extra_features'
+ATTR_FEATURES = 'features'
+ATTR_TURBO_MODE_SUPPORTED = 'turbo_mode_supported'
+ATTR_AUTO_DETECT = 'auto_detect'
+ATTR_SLEEP_MODE = 'sleep_mode'
+ATTR_VOLUME = 'volume'
+ATTR_USE_TIME = 'use_time'
+ATTR_BUTTON_PRESSED = 'button_pressed'
+
+# Air Humidifier
+ATTR_TARGET_HUMIDITY = 'target_humidity'
+ATTR_TRANS_LEVEL = 'trans_level'
+ATTR_HARDWARE_VERSION = 'hardware_version'
+
+# Air Humidifier CA
+ATTR_SPEED = 'speed'
+ATTR_DEPTH = 'depth'
+ATTR_DRY = 'dry'
+
+# Map attributes to properties of the state object
+AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON = {
+ ATTR_TEMPERATURE: 'temperature',
+ ATTR_HUMIDITY: 'humidity',
+ ATTR_AIR_QUALITY_INDEX: 'aqi',
+ ATTR_MODE: 'mode',
+ ATTR_FILTER_HOURS_USED: 'filter_hours_used',
+ ATTR_FILTER_LIFE: 'filter_life_remaining',
+ ATTR_FAVORITE_LEVEL: 'favorite_level',
+ ATTR_CHILD_LOCK: 'child_lock',
+ ATTR_LED: 'led',
+ ATTR_MOTOR_SPEED: 'motor_speed',
+ ATTR_AVERAGE_AIR_QUALITY_INDEX: 'average_aqi',
+ ATTR_PURIFY_VOLUME: 'purify_volume',
+ ATTR_LEARN_MODE: 'learn_mode',
+ ATTR_SLEEP_TIME: 'sleep_time',
+ ATTR_SLEEP_LEARN_COUNT: 'sleep_mode_learn_count',
+ ATTR_EXTRA_FEATURES: 'extra_features',
+ ATTR_TURBO_MODE_SUPPORTED: 'turbo_mode_supported',
+ ATTR_AUTO_DETECT: 'auto_detect',
+ ATTR_USE_TIME: 'use_time',
+ ATTR_BUTTON_PRESSED: 'button_pressed',
+}
+
+AVAILABLE_ATTRIBUTES_AIRPURIFIER = {
+ **AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON,
+ ATTR_BUZZER: 'buzzer',
+ ATTR_LED_BRIGHTNESS: 'led_brightness',
+ ATTR_SLEEP_MODE: 'sleep_mode',
+}
+
+AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO = {
+ **AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON,
+ ATTR_FILTER_RFID_PRODUCT_ID: 'filter_rfid_product_id',
+ ATTR_FILTER_RFID_TAG: 'filter_rfid_tag',
+ ATTR_FILTER_TYPE: 'filter_type',
+ ATTR_ILLUMINANCE: 'illuminance',
+ ATTR_MOTOR2_SPEED: 'motor2_speed',
+ ATTR_VOLUME: 'volume',
+}
+
+AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3 = {
+ # Common set isn't used here. It's a very basic version of the device.
+ ATTR_AIR_QUALITY_INDEX: 'aqi',
+ ATTR_MODE: 'mode',
+ ATTR_LED: 'led',
+ ATTR_BUZZER: 'buzzer',
+ ATTR_CHILD_LOCK: 'child_lock',
+ ATTR_ILLUMINANCE: 'illuminance',
+ ATTR_FILTER_HOURS_USED: 'filter_hours_used',
+ ATTR_FILTER_LIFE: 'filter_life_remaining',
+ ATTR_MOTOR_SPEED: 'motor_speed',
+ # perhaps supported but unconfirmed
+ ATTR_AVERAGE_AIR_QUALITY_INDEX: 'average_aqi',
+ ATTR_VOLUME: 'volume',
+ ATTR_MOTOR2_SPEED: 'motor2_speed',
+ ATTR_FILTER_RFID_PRODUCT_ID: 'filter_rfid_product_id',
+ ATTR_FILTER_RFID_TAG: 'filter_rfid_tag',
+ ATTR_FILTER_TYPE: 'filter_type',
+ ATTR_PURIFY_VOLUME: 'purify_volume',
+ ATTR_LEARN_MODE: 'learn_mode',
+ ATTR_SLEEP_TIME: 'sleep_time',
+ ATTR_SLEEP_LEARN_COUNT: 'sleep_mode_learn_count',
+ ATTR_EXTRA_FEATURES: 'extra_features',
+ ATTR_AUTO_DETECT: 'auto_detect',
+ ATTR_USE_TIME: 'use_time',
+ ATTR_BUTTON_PRESSED: 'button_pressed',
+}
+
+AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER = {
+ ATTR_TEMPERATURE: 'temperature',
+ ATTR_HUMIDITY: 'humidity',
+ ATTR_MODE: 'mode',
+ ATTR_BUZZER: 'buzzer',
+ ATTR_CHILD_LOCK: 'child_lock',
+ ATTR_TRANS_LEVEL: 'trans_level',
+ ATTR_TARGET_HUMIDITY: 'target_humidity',
+ ATTR_LED_BRIGHTNESS: 'led_brightness',
+ ATTR_BUTTON_PRESSED: 'button_pressed',
+ ATTR_USE_TIME: 'use_time',
+ ATTR_HARDWARE_VERSION: 'hardware_version',
+}
+
+AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA = {
+ **AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER,
+ ATTR_SPEED: 'speed',
+ ATTR_DEPTH: 'depth',
+ ATTR_DRY: 'dry',
+}
+
+OPERATION_MODES_AIRPURIFIER = ['Auto', 'Silent', 'Favorite', 'Idle']
+OPERATION_MODES_AIRPURIFIER_PRO = ['Auto', 'Silent', 'Favorite']
+OPERATION_MODES_AIRPURIFIER_V3 = ['Auto', 'Silent', 'Favorite', 'Idle',
+ 'Medium', 'High', 'Strong']
SUCCESS = ['ok']
+FEATURE_SET_BUZZER = 1
+FEATURE_SET_LED = 2
+FEATURE_SET_CHILD_LOCK = 4
+FEATURE_SET_LED_BRIGHTNESS = 8
+FEATURE_SET_FAVORITE_LEVEL = 16
+FEATURE_SET_AUTO_DETECT = 32
+FEATURE_SET_LEARN_MODE = 64
+FEATURE_SET_VOLUME = 128
+FEATURE_RESET_FILTER = 256
+FEATURE_SET_EXTRA_FEATURES = 512
+FEATURE_SET_TARGET_HUMIDITY = 1024
+FEATURE_SET_DRY = 2048
+
+FEATURE_FLAGS_GENERIC = (FEATURE_SET_BUZZER |
+ FEATURE_SET_CHILD_LOCK)
+
+FEATURE_FLAGS_AIRPURIFIER = (FEATURE_FLAGS_GENERIC |
+ FEATURE_SET_LED |
+ FEATURE_SET_LED_BRIGHTNESS |
+ FEATURE_SET_FAVORITE_LEVEL |
+ FEATURE_SET_LEARN_MODE |
+ FEATURE_RESET_FILTER |
+ FEATURE_SET_EXTRA_FEATURES)
+
+FEATURE_FLAGS_AIRPURIFIER_PRO = (FEATURE_SET_CHILD_LOCK |
+ FEATURE_SET_LED |
+ FEATURE_SET_FAVORITE_LEVEL |
+ FEATURE_SET_AUTO_DETECT |
+ FEATURE_SET_VOLUME)
+
+FEATURE_FLAGS_AIRPURIFIER_V3 = (FEATURE_FLAGS_GENERIC |
+ FEATURE_SET_LED)
+
+FEATURE_FLAGS_AIRHUMIDIFIER = (FEATURE_FLAGS_GENERIC |
+ FEATURE_SET_LED_BRIGHTNESS |
+ FEATURE_SET_TARGET_HUMIDITY)
+
+FEATURE_FLAGS_AIRHUMIDIFIER_CA = (FEATURE_FLAGS_AIRHUMIDIFIER |
+ FEATURE_SET_DRY)
+
SERVICE_SET_BUZZER_ON = 'xiaomi_miio_set_buzzer_on'
SERVICE_SET_BUZZER_OFF = 'xiaomi_miio_set_buzzer_off'
SERVICE_SET_LED_ON = 'xiaomi_miio_set_led_on'
SERVICE_SET_LED_OFF = 'xiaomi_miio_set_led_off'
SERVICE_SET_CHILD_LOCK_ON = 'xiaomi_miio_set_child_lock_on'
SERVICE_SET_CHILD_LOCK_OFF = 'xiaomi_miio_set_child_lock_off'
-SERVICE_SET_FAVORITE_LEVEL = 'xiaomi_miio_set_favorite_level'
SERVICE_SET_LED_BRIGHTNESS = 'xiaomi_miio_set_led_brightness'
+SERVICE_SET_FAVORITE_LEVEL = 'xiaomi_miio_set_favorite_level'
+SERVICE_SET_AUTO_DETECT_ON = 'xiaomi_miio_set_auto_detect_on'
+SERVICE_SET_AUTO_DETECT_OFF = 'xiaomi_miio_set_auto_detect_off'
+SERVICE_SET_LEARN_MODE_ON = 'xiaomi_miio_set_learn_mode_on'
+SERVICE_SET_LEARN_MODE_OFF = 'xiaomi_miio_set_learn_mode_off'
+SERVICE_SET_VOLUME = 'xiaomi_miio_set_volume'
+SERVICE_RESET_FILTER = 'xiaomi_miio_reset_filter'
+SERVICE_SET_EXTRA_FEATURES = 'xiaomi_miio_set_extra_features'
+SERVICE_SET_TARGET_HUMIDITY = 'xiaomi_miio_set_target_humidity'
+SERVICE_SET_DRY_ON = 'xiaomi_miio_set_dry_on'
+SERVICE_SET_DRY_OFF = 'xiaomi_miio_set_dry_off'
AIRPURIFIER_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
@@ -74,6 +267,21 @@ SERVICE_SCHEMA_FAVORITE_LEVEL = AIRPURIFIER_SERVICE_SCHEMA.extend({
vol.All(vol.Coerce(int), vol.Clamp(min=0, max=16))
})
+SERVICE_SCHEMA_VOLUME = AIRPURIFIER_SERVICE_SCHEMA.extend({
+ vol.Required(ATTR_VOLUME):
+ vol.All(vol.Coerce(int), vol.Clamp(min=0, max=100))
+})
+
+SERVICE_SCHEMA_EXTRA_FEATURES = AIRPURIFIER_SERVICE_SCHEMA.extend({
+ vol.Required(ATTR_FEATURES):
+ vol.All(vol.Coerce(int), vol.Range(min=0))
+})
+
+SERVICE_SCHEMA_TARGET_HUMIDITY = AIRPURIFIER_SERVICE_SCHEMA.extend({
+ vol.Required(ATTR_HUMIDITY):
+ vol.All(vol.Coerce(int), vol.In([30, 40, 50, 60, 70, 80]))
+})
+
SERVICE_TO_METHOD = {
SERVICE_SET_BUZZER_ON: {'method': 'async_set_buzzer_on'},
SERVICE_SET_BUZZER_OFF: {'method': 'async_set_buzzer_off'},
@@ -81,59 +289,99 @@ SERVICE_TO_METHOD = {
SERVICE_SET_LED_OFF: {'method': 'async_set_led_off'},
SERVICE_SET_CHILD_LOCK_ON: {'method': 'async_set_child_lock_on'},
SERVICE_SET_CHILD_LOCK_OFF: {'method': 'async_set_child_lock_off'},
- SERVICE_SET_FAVORITE_LEVEL: {
- 'method': 'async_set_favorite_level',
- 'schema': SERVICE_SCHEMA_FAVORITE_LEVEL},
+ SERVICE_SET_AUTO_DETECT_ON: {'method': 'async_set_auto_detect_on'},
+ SERVICE_SET_AUTO_DETECT_OFF: {'method': 'async_set_auto_detect_off'},
+ SERVICE_SET_LEARN_MODE_ON: {'method': 'async_set_learn_mode_on'},
+ SERVICE_SET_LEARN_MODE_OFF: {'method': 'async_set_learn_mode_off'},
+ SERVICE_RESET_FILTER: {'method': 'async_reset_filter'},
SERVICE_SET_LED_BRIGHTNESS: {
'method': 'async_set_led_brightness',
'schema': SERVICE_SCHEMA_LED_BRIGHTNESS},
+ SERVICE_SET_FAVORITE_LEVEL: {
+ 'method': 'async_set_favorite_level',
+ 'schema': SERVICE_SCHEMA_FAVORITE_LEVEL},
+ SERVICE_SET_VOLUME: {
+ 'method': 'async_set_volume',
+ 'schema': SERVICE_SCHEMA_VOLUME},
+ SERVICE_SET_EXTRA_FEATURES: {
+ 'method': 'async_set_extra_features',
+ 'schema': SERVICE_SCHEMA_EXTRA_FEATURES},
+ SERVICE_SET_TARGET_HUMIDITY: {
+ 'method': 'async_set_target_humidity',
+ 'schema': SERVICE_SCHEMA_TARGET_HUMIDITY},
+ SERVICE_SET_DRY_ON: {'method': 'async_set_dry_on'},
+ SERVICE_SET_DRY_OFF: {'method': 'async_set_dry_off'},
}
# pylint: disable=unused-argument
-@asyncio.coroutine
-def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
- """Set up the air purifier from config."""
- from miio import AirPurifier, DeviceException
- if PLATFORM not in hass.data:
- hass.data[PLATFORM] = {}
+async def async_setup_platform(hass, config, async_add_devices,
+ discovery_info=None):
+ """Set up the miio fan device from config."""
+ from miio import Device, DeviceException
+ if DATA_KEY not in hass.data:
+ hass.data[DATA_KEY] = {}
host = config.get(CONF_HOST)
name = config.get(CONF_NAME)
token = config.get(CONF_TOKEN)
+ model = config.get(CONF_MODEL)
_LOGGER.info("Initializing with host %s (token %s...)", host, token[:5])
+ unique_id = None
- try:
+ if model is None:
+ try:
+ miio_device = Device(host, token)
+ device_info = miio_device.info()
+ model = device_info.model
+ unique_id = "{}-{}".format(model, device_info.mac_address)
+ _LOGGER.info("%s %s %s detected",
+ model,
+ device_info.firmware_version,
+ device_info.hardware_version)
+ except DeviceException:
+ raise PlatformNotReady
+
+ if model.startswith('zhimi.airpurifier.'):
+ from miio import AirPurifier
air_purifier = AirPurifier(host, token)
+ device = XiaomiAirPurifier(name, air_purifier, model, unique_id)
+ elif model.startswith('zhimi.humidifier.'):
+ from miio import AirHumidifier
+ air_humidifier = AirHumidifier(host, token)
+ device = XiaomiAirHumidifier(name, air_humidifier, model, unique_id)
+ else:
+ _LOGGER.error(
+ 'Unsupported device found! Please create an issue at '
+ 'https://github.com/syssi/xiaomi_airpurifier/issues '
+ 'and provide the following data: %s', model)
+ return False
- xiaomi_air_purifier = XiaomiAirPurifier(name, air_purifier)
- hass.data[PLATFORM][host] = xiaomi_air_purifier
- except DeviceException:
- raise PlatformNotReady
+ hass.data[DATA_KEY][host] = device
+ async_add_devices([device], update_before_add=True)
- async_add_devices([xiaomi_air_purifier], update_before_add=True)
-
- @asyncio.coroutine
- def async_service_handler(service):
+ async def async_service_handler(service):
"""Map services to methods on XiaomiAirPurifier."""
method = SERVICE_TO_METHOD.get(service.service)
params = {key: value for key, value in service.data.items()
if key != ATTR_ENTITY_ID}
entity_ids = service.data.get(ATTR_ENTITY_ID)
if entity_ids:
- devices = [device for device in hass.data[PLATFORM].values() if
+ devices = [device for device in hass.data[DATA_KEY].values() if
device.entity_id in entity_ids]
else:
- devices = hass.data[PLATFORM].values()
+ devices = hass.data[DATA_KEY].values()
update_tasks = []
for device in devices:
- yield from getattr(device, method['method'])(**params)
+ if not hasattr(device, method['method']):
+ continue
+ await getattr(device, method['method'])(**params)
update_tasks.append(device.async_update_ha_state(True))
if update_tasks:
- yield from asyncio.wait(update_tasks, loop=hass.loop)
+ await asyncio.wait(update_tasks, loop=hass.loop)
for air_purifier_service in SERVICE_TO_METHOD:
schema = SERVICE_TO_METHOD[air_purifier_service].get(
@@ -142,31 +390,22 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
DOMAIN, air_purifier_service, async_service_handler, schema=schema)
-class XiaomiAirPurifier(FanEntity):
- """Representation of a Xiaomi Air Purifier."""
+class XiaomiGenericDevice(FanEntity):
+ """Representation of a generic Xiaomi device."""
- def __init__(self, name, air_purifier):
- """Initialize the air purifier."""
+ def __init__(self, name, device, model, unique_id):
+ """Initialize the generic Xiaomi device."""
self._name = name
+ self._device = device
+ self._model = model
+ self._unique_id = unique_id
- self._air_purifier = air_purifier
+ self._available = False
self._state = None
self._state_attrs = {
- ATTR_AIR_QUALITY_INDEX: None,
- ATTR_TEMPERATURE: None,
- ATTR_HUMIDITY: None,
- ATTR_MODE: None,
- ATTR_FILTER_HOURS_USED: None,
- ATTR_FILTER_LIFE: None,
- ATTR_FAVORITE_LEVEL: None,
- ATTR_BUZZER: None,
- ATTR_CHILD_LOCK: None,
- ATTR_LED: None,
- ATTR_LED_BRIGHTNESS: None,
- ATTR_MOTOR_SPEED: None,
- ATTR_AVERAGE_AIR_QUALITY_INDEX: None,
- ATTR_PURIFY_VOLUME: None,
+ ATTR_MODEL: self._model,
}
+ self._device_features = FEATURE_FLAGS_GENERIC
self._skip_update = False
@property
@@ -176,9 +415,14 @@ class XiaomiAirPurifier(FanEntity):
@property
def should_poll(self):
- """Poll the fan."""
+ """Poll the device."""
return True
+ @property
+ def unique_id(self):
+ """Return an unique ID."""
+ return self._unique_id
+
@property
def name(self):
"""Return the name of the device if any."""
@@ -187,7 +431,7 @@ class XiaomiAirPurifier(FanEntity):
@property
def available(self):
"""Return true when state is known."""
- return self._state is not None
+ return self._available
@property
def device_state_attributes(self):
@@ -196,50 +440,116 @@ class XiaomiAirPurifier(FanEntity):
@property
def is_on(self):
- """Return true if fan is on."""
+ """Return true if device is on."""
return self._state
- @asyncio.coroutine
- def _try_command(self, mask_error, func, *args, **kwargs):
- """Call an air purifier command handling error messages."""
+ @staticmethod
+ def _extract_value_from_attribute(state, attribute):
+ value = getattr(state, attribute)
+ if isinstance(value, Enum):
+ return value.value
+
+ return value
+
+ async def _try_command(self, mask_error, func, *args, **kwargs):
+ """Call a miio device command handling error messages."""
from miio import DeviceException
try:
- result = yield from self.hass.async_add_job(
+ result = await self.hass.async_add_job(
partial(func, *args, **kwargs))
- _LOGGER.debug("Response received from air purifier: %s", result)
+ _LOGGER.debug("Response received from miio device: %s", result)
return result == SUCCESS
except DeviceException as exc:
_LOGGER.error(mask_error, exc)
+ self._available = False
return False
- @asyncio.coroutine
- def async_turn_on(self: ToggleEntity, speed: str = None, **kwargs) -> None:
- """Turn the fan on."""
+ async def async_turn_on(self, speed: str = None,
+ **kwargs) -> None:
+ """Turn the device on."""
if speed:
# If operation mode was set the device must not be turned on.
- result = yield from self.async_set_speed(speed)
+ result = await self.async_set_speed(speed)
else:
- result = yield from self._try_command(
- "Turning the air purifier on failed.", self._air_purifier.on)
+ result = await self._try_command(
+ "Turning the miio device on failed.", self._device.on)
if result:
self._state = True
self._skip_update = True
- @asyncio.coroutine
- def async_turn_off(self: ToggleEntity, **kwargs) -> None:
- """Turn the fan off."""
- result = yield from self._try_command(
- "Turning the air purifier off failed.", self._air_purifier.off)
+ async def async_turn_off(self, **kwargs) -> None:
+ """Turn the device off."""
+ result = await self._try_command(
+ "Turning the miio device off failed.", self._device.off)
if result:
self._state = False
self._skip_update = True
- @asyncio.coroutine
- def async_update(self):
+ async def async_set_buzzer_on(self):
+ """Turn the buzzer on."""
+ if self._device_features & FEATURE_SET_BUZZER == 0:
+ return
+
+ await self._try_command(
+ "Turning the buzzer of the miio device on failed.",
+ self._device.set_buzzer, True)
+
+ async def async_set_buzzer_off(self):
+ """Turn the buzzer off."""
+ if self._device_features & FEATURE_SET_BUZZER == 0:
+ return
+
+ await self._try_command(
+ "Turning the buzzer of the miio device off failed.",
+ self._device.set_buzzer, False)
+
+ async def async_set_child_lock_on(self):
+ """Turn the child lock on."""
+ if self._device_features & FEATURE_SET_CHILD_LOCK == 0:
+ return
+
+ await self._try_command(
+ "Turning the child lock of the miio device on failed.",
+ self._device.set_child_lock, True)
+
+ async def async_set_child_lock_off(self):
+ """Turn the child lock off."""
+ if self._device_features & FEATURE_SET_CHILD_LOCK == 0:
+ return
+
+ await self._try_command(
+ "Turning the child lock of the miio device off failed.",
+ self._device.set_child_lock, False)
+
+
+class XiaomiAirPurifier(XiaomiGenericDevice):
+ """Representation of a Xiaomi Air Purifier."""
+
+ def __init__(self, name, device, model, unique_id):
+ """Initialize the plug switch."""
+ super().__init__(name, device, model, unique_id)
+
+ if self._model == MODEL_AIRPURIFIER_PRO:
+ self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO
+ self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO
+ self._speed_list = OPERATION_MODES_AIRPURIFIER_PRO
+ elif self._model == MODEL_AIRPURIFIER_V3:
+ self._device_features = FEATURE_FLAGS_AIRPURIFIER_V3
+ self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3
+ self._speed_list = OPERATION_MODES_AIRPURIFIER_V3
+ else:
+ self._device_features = FEATURE_FLAGS_AIRPURIFIER
+ self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER
+ self._speed_list = OPERATION_MODES_AIRPURIFIER
+
+ self._state_attrs.update(
+ {attribute: None for attribute in self._available_attributes})
+
+ async def async_update(self):
"""Fetch state from the device."""
from miio import DeviceException
@@ -249,40 +559,24 @@ class XiaomiAirPurifier(FanEntity):
return
try:
- state = yield from self.hass.async_add_job(
- self._air_purifier.status)
+ state = await self.hass.async_add_job(
+ self._device.status)
_LOGGER.debug("Got new state: %s", state)
+ self._available = True
self._state = state.is_on
- self._state_attrs = {
- ATTR_TEMPERATURE: state.temperature,
- ATTR_HUMIDITY: state.humidity,
- ATTR_AIR_QUALITY_INDEX: state.aqi,
- ATTR_MODE: state.mode.value,
- ATTR_FILTER_HOURS_USED: state.filter_hours_used,
- ATTR_FILTER_LIFE: state.filter_life_remaining,
- ATTR_FAVORITE_LEVEL: state.favorite_level,
- ATTR_BUZZER: state.buzzer,
- ATTR_CHILD_LOCK: state.child_lock,
- ATTR_LED: state.led,
- ATTR_MOTOR_SPEED: state.motor_speed,
- ATTR_AVERAGE_AIR_QUALITY_INDEX: state.average_aqi,
- ATTR_PURIFY_VOLUME: state.purify_volume,
- }
-
- if state.led_brightness:
- self._state_attrs[
- ATTR_LED_BRIGHTNESS] = state.led_brightness.value
+ self._state_attrs.update(
+ {key: self._extract_value_from_attribute(state, value) for
+ key, value in self._available_attributes.items()})
except DeviceException as ex:
- self._state = None
+ self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
@property
- def speed_list(self: ToggleEntity) -> list:
+ def speed_list(self) -> list:
"""Get the list of available speeds."""
- from miio.airpurifier import OperationMode
- return [mode.name for mode in OperationMode]
+ return self._speed_list
@property
def speed(self):
@@ -294,70 +588,227 @@ class XiaomiAirPurifier(FanEntity):
return None
- @asyncio.coroutine
- def async_set_speed(self: ToggleEntity, speed: str) -> None:
+ async def async_set_speed(self, speed: str) -> None:
"""Set the speed of the fan."""
- _LOGGER.debug("Setting the operation mode to: %s", speed)
+ if self.supported_features & SUPPORT_SET_SPEED == 0:
+ return
+
from miio.airpurifier import OperationMode
- yield from self._try_command(
- "Setting operation mode of the air purifier failed.",
- self._air_purifier.set_mode, OperationMode[speed.title()])
+ _LOGGER.debug("Setting the operation mode to: %s", speed)
- @asyncio.coroutine
- def async_set_buzzer_on(self):
- """Turn the buzzer on."""
- yield from self._try_command(
- "Turning the buzzer of the air purifier on failed.",
- self._air_purifier.set_buzzer, True)
+ await self._try_command(
+ "Setting operation mode of the miio device failed.",
+ self._device.set_mode, OperationMode[speed.title()])
- @asyncio.coroutine
- def async_set_buzzer_off(self):
- """Turn the buzzer off."""
- yield from self._try_command(
- "Turning the buzzer of the air purifier off failed.",
- self._air_purifier.set_buzzer, False)
-
- @asyncio.coroutine
- def async_set_led_on(self):
+ async def async_set_led_on(self):
"""Turn the led on."""
- yield from self._try_command(
- "Turning the led of the air purifier off failed.",
- self._air_purifier.set_led, True)
+ if self._device_features & FEATURE_SET_LED == 0:
+ return
- @asyncio.coroutine
- def async_set_led_off(self):
+ await self._try_command(
+ "Turning the led of the miio device off failed.",
+ self._device.set_led, True)
+
+ async def async_set_led_off(self):
"""Turn the led off."""
- yield from self._try_command(
- "Turning the led of the air purifier off failed.",
- self._air_purifier.set_led, False)
+ if self._device_features & FEATURE_SET_LED == 0:
+ return
- @asyncio.coroutine
- def async_set_child_lock_on(self):
- """Turn the child lock on."""
- yield from self._try_command(
- "Turning the child lock of the air purifier on failed.",
- self._air_purifier.set_child_lock, True)
+ await self._try_command(
+ "Turning the led of the miio device off failed.",
+ self._device.set_led, False)
- @asyncio.coroutine
- def async_set_child_lock_off(self):
- """Turn the child lock off."""
- yield from self._try_command(
- "Turning the child lock of the air purifier off failed.",
- self._air_purifier.set_child_lock, False)
-
- @asyncio.coroutine
- def async_set_led_brightness(self, brightness: int = 2):
+ async def async_set_led_brightness(self, brightness: int = 2):
"""Set the led brightness."""
+ if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0:
+ return
+
from miio.airpurifier import LedBrightness
- yield from self._try_command(
- "Setting the led brightness of the air purifier failed.",
- self._air_purifier.set_led_brightness, LedBrightness(brightness))
+ await self._try_command(
+ "Setting the led brightness of the miio device failed.",
+ self._device.set_led_brightness, LedBrightness(brightness))
- @asyncio.coroutine
- def async_set_favorite_level(self, level: int = 1):
+ async def async_set_favorite_level(self, level: int = 1):
"""Set the favorite level."""
- yield from self._try_command(
- "Setting the favorite level of the air purifier failed.",
- self._air_purifier.set_favorite_level, level)
+ if self._device_features & FEATURE_SET_FAVORITE_LEVEL == 0:
+ return
+
+ await self._try_command(
+ "Setting the favorite level of the miio device failed.",
+ self._device.set_favorite_level, level)
+
+ async def async_set_auto_detect_on(self):
+ """Turn the auto detect on."""
+ if self._device_features & FEATURE_SET_AUTO_DETECT == 0:
+ return
+
+ await self._try_command(
+ "Turning the auto detect of the miio device on failed.",
+ self._device.set_auto_detect, True)
+
+ async def async_set_auto_detect_off(self):
+ """Turn the auto detect off."""
+ if self._device_features & FEATURE_SET_AUTO_DETECT == 0:
+ return
+
+ await self._try_command(
+ "Turning the auto detect of the miio device off failed.",
+ self._device.set_auto_detect, False)
+
+ async def async_set_learn_mode_on(self):
+ """Turn the learn mode on."""
+ if self._device_features & FEATURE_SET_LEARN_MODE == 0:
+ return
+
+ await self._try_command(
+ "Turning the learn mode of the miio device on failed.",
+ self._device.set_learn_mode, True)
+
+ async def async_set_learn_mode_off(self):
+ """Turn the learn mode off."""
+ if self._device_features & FEATURE_SET_LEARN_MODE == 0:
+ return
+
+ await self._try_command(
+ "Turning the learn mode of the miio device off failed.",
+ self._device.set_learn_mode, False)
+
+ async def async_set_volume(self, volume: int = 50):
+ """Set the sound volume."""
+ if self._device_features & FEATURE_SET_VOLUME == 0:
+ return
+
+ await self._try_command(
+ "Setting the sound volume of the miio device failed.",
+ self._device.set_volume, volume)
+
+ async def async_set_extra_features(self, features: int = 1):
+ """Set the extra features."""
+ if self._device_features & FEATURE_SET_EXTRA_FEATURES == 0:
+ return
+
+ await self._try_command(
+ "Setting the extra features of the miio device failed.",
+ self._device.set_extra_features, features)
+
+ async def async_reset_filter(self):
+ """Reset the filter lifetime and usage."""
+ if self._device_features & FEATURE_RESET_FILTER == 0:
+ return
+
+ await self._try_command(
+ "Resetting the filter lifetime of the miio device failed.",
+ self._device.reset_filter)
+
+
+class XiaomiAirHumidifier(XiaomiGenericDevice):
+ """Representation of a Xiaomi Air Humidifier."""
+
+ def __init__(self, name, device, model, unique_id):
+ """Initialize the plug switch."""
+ from miio.airpurifier import OperationMode
+
+ super().__init__(name, device, model, unique_id)
+
+ if self._model == MODEL_AIRHUMIDIFIER_CA:
+ self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA
+ self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA
+ self._speed_list = [mode.name for mode in OperationMode]
+ else:
+ self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER
+ self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER
+ self._speed_list = [mode.name for mode in OperationMode if
+ mode.name != 'Auto']
+
+ self._state_attrs.update(
+ {attribute: None for attribute in self._available_attributes})
+
+ async def async_update(self):
+ """Fetch state from the device."""
+ from miio import DeviceException
+
+ # On state change the device doesn't provide the new state immediately.
+ if self._skip_update:
+ self._skip_update = False
+ return
+
+ try:
+ state = await self.hass.async_add_job(self._device.status)
+ _LOGGER.debug("Got new state: %s", state)
+
+ self._available = True
+ self._state = state.is_on
+ self._state_attrs.update(
+ {key: self._extract_value_from_attribute(state, value) for
+ key, value in self._available_attributes.items()})
+
+ except DeviceException as ex:
+ self._available = False
+ _LOGGER.error("Got exception while fetching the state: %s", ex)
+
+ def speed_list(self) -> list:
+ """Get the list of available speeds."""
+ return self._speed_list
+
+ @property
+ def speed(self):
+ """Return the current speed."""
+ if self._state:
+ from miio.airhumidifier import OperationMode
+
+ return OperationMode(self._state_attrs[ATTR_MODE]).name
+
+ return None
+
+ async def async_set_speed(self, speed: str) -> None:
+ """Set the speed of the fan."""
+ if self.supported_features & SUPPORT_SET_SPEED == 0:
+ return
+
+ from miio.airhumidifier import OperationMode
+
+ _LOGGER.debug("Setting the operation mode to: %s", speed)
+
+ await self._try_command(
+ "Setting operation mode of the miio device failed.",
+ self._device.set_mode, OperationMode[speed.title()])
+
+ async def async_set_led_brightness(self, brightness: int = 2):
+ """Set the led brightness."""
+ if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0:
+ return
+
+ from miio.airhumidifier import LedBrightness
+
+ await self._try_command(
+ "Setting the led brightness of the miio device failed.",
+ self._device.set_led_brightness, LedBrightness(brightness))
+
+ async def async_set_target_humidity(self, humidity: int = 40):
+ """Set the target humidity."""
+ if self._device_features & FEATURE_SET_TARGET_HUMIDITY == 0:
+ return
+
+ await self._try_command(
+ "Setting the target humidity of the miio device failed.",
+ self._device.set_target_humidity, humidity)
+
+ async def async_set_dry_on(self):
+ """Turn the dry mode on."""
+ if self._device_features & FEATURE_SET_DRY == 0:
+ return
+
+ await self._try_command(
+ "Turning the dry mode of the miio device off failed.",
+ self._device.set_dry, True)
+
+ async def async_set_dry_off(self):
+ """Turn the dry mode off."""
+ if self._device_features & FEATURE_SET_DRY == 0:
+ return
+
+ await self._try_command(
+ "Turning the dry mode of the miio device off failed.",
+ self._device.set_dry, False)
diff --git a/homeassistant/components/folder_watcher.py b/homeassistant/components/folder_watcher.py
new file mode 100644
index 00000000000..44110647632
--- /dev/null
+++ b/homeassistant/components/folder_watcher.py
@@ -0,0 +1,110 @@
+"""
+Component for monitoring activity on a folder.
+
+For more details about this platform, refer to the documentation at
+https://home-assistant.io/components/folder_watcher/
+"""
+import os
+import logging
+import voluptuous as vol
+from homeassistant.const import (
+ EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
+import homeassistant.helpers.config_validation as cv
+
+REQUIREMENTS = ['watchdog==0.8.3']
+_LOGGER = logging.getLogger(__name__)
+
+CONF_FOLDER = 'folder'
+CONF_PATTERNS = 'patterns'
+DEFAULT_PATTERN = '*'
+DOMAIN = "folder_watcher"
+
+CONFIG_SCHEMA = vol.Schema({
+ DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
+ vol.Required(CONF_FOLDER): cv.isdir,
+ vol.Optional(CONF_PATTERNS, default=[DEFAULT_PATTERN]):
+ vol.All(cv.ensure_list, [cv.string]),
+ })])
+}, extra=vol.ALLOW_EXTRA)
+
+
+def setup(hass, config):
+ """Set up the folder watcher."""
+ conf = config[DOMAIN]
+ for watcher in conf:
+ path = watcher[CONF_FOLDER]
+ patterns = watcher[CONF_PATTERNS]
+ if not hass.config.is_allowed_path(path):
+ _LOGGER.error("folder %s is not valid or allowed", path)
+ return False
+ Watcher(path, patterns, hass)
+
+ return True
+
+
+def create_event_handler(patterns, hass):
+ """"Return the Watchdog EventHandler object."""
+ from watchdog.events import PatternMatchingEventHandler
+
+ class EventHandler(PatternMatchingEventHandler):
+ """Class for handling Watcher events."""
+
+ def __init__(self, patterns, hass):
+ """Initialise the EventHandler."""
+ super().__init__(patterns)
+ self.hass = hass
+
+ def process(self, event):
+ """On Watcher event, fire HA event."""
+ _LOGGER.debug("process(%s)", event)
+ if not event.is_directory:
+ folder, file_name = os.path.split(event.src_path)
+ self.hass.bus.fire(
+ DOMAIN, {
+ "event_type": event.event_type,
+ 'path': event.src_path,
+ 'file': file_name,
+ 'folder': folder,
+ })
+
+ def on_modified(self, event):
+ """File modified."""
+ self.process(event)
+
+ def on_moved(self, event):
+ """File moved."""
+ self.process(event)
+
+ def on_created(self, event):
+ """File created."""
+ self.process(event)
+
+ def on_deleted(self, event):
+ """File deleted."""
+ self.process(event)
+
+ return EventHandler(patterns, hass)
+
+
+class Watcher():
+ """Class for starting Watchdog."""
+
+ def __init__(self, path, patterns, hass):
+ """Initialise the watchdog observer."""
+ from watchdog.observers import Observer
+ self._observer = Observer()
+ self._observer.schedule(
+ create_event_handler(patterns, hass),
+ path,
+ recursive=True)
+ hass.bus.listen_once(EVENT_HOMEASSISTANT_START, self.startup)
+ hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.shutdown)
+
+ def startup(self, event):
+ """Start the watcher."""
+ self._observer.start()
+
+ def shutdown(self, event):
+ """Shutdown the watcher."""
+ self._observer.stop()
+ self._observer.join()
diff --git a/homeassistant/components/freedns.py b/homeassistant/components/freedns.py
new file mode 100644
index 00000000000..0512030bdcb
--- /dev/null
+++ b/homeassistant/components/freedns.py
@@ -0,0 +1,103 @@
+"""
+Integrate with FreeDNS Dynamic DNS service at freedns.afraid.org.
+
+For more details about this component, please refer to the documentation at
+https://home-assistant.io/components/freedns/
+"""
+import asyncio
+from datetime import timedelta
+import logging
+
+import aiohttp
+import async_timeout
+import voluptuous as vol
+
+from homeassistant.const import (CONF_URL, CONF_ACCESS_TOKEN)
+import homeassistant.helpers.config_validation as cv
+
+_LOGGER = logging.getLogger(__name__)
+
+DOMAIN = 'freedns'
+
+DEFAULT_INTERVAL = timedelta(minutes=10)
+
+TIMEOUT = 10
+UPDATE_URL = 'https://freedns.afraid.org/dynamic/update.php'
+
+CONF_UPDATE_INTERVAL = 'update_interval'
+
+CONFIG_SCHEMA = vol.Schema({
+ DOMAIN: vol.Schema({
+ vol.Exclusive(CONF_URL, DOMAIN): cv.string,
+ vol.Exclusive(CONF_ACCESS_TOKEN, DOMAIN): cv.string,
+ vol.Optional(CONF_UPDATE_INTERVAL, default=DEFAULT_INTERVAL): vol.All(
+ cv.time_period, cv.positive_timedelta),
+
+ })
+}, extra=vol.ALLOW_EXTRA)
+
+
+@asyncio.coroutine
+def async_setup(hass, config):
+ """Initialize the FreeDNS component."""
+ url = config[DOMAIN].get(CONF_URL)
+ auth_token = config[DOMAIN].get(CONF_ACCESS_TOKEN)
+ update_interval = config[DOMAIN].get(CONF_UPDATE_INTERVAL)
+
+ session = hass.helpers.aiohttp_client.async_get_clientsession()
+
+ result = yield from _update_freedns(
+ hass, session, url, auth_token)
+
+ if result is False:
+ return False
+
+ @asyncio.coroutine
+ def update_domain_callback(now):
+ """Update the FreeDNS entry."""
+ yield from _update_freedns(hass, session, url, auth_token)
+
+ hass.helpers.event.async_track_time_interval(
+ update_domain_callback, update_interval)
+
+ return True
+
+
+@asyncio.coroutine
+def _update_freedns(hass, session, url, auth_token):
+ """Update FreeDNS."""
+ params = None
+
+ if url is None:
+ url = UPDATE_URL
+
+ if auth_token is not None:
+ params = {}
+ params[auth_token] = ""
+
+ try:
+ with async_timeout.timeout(TIMEOUT, loop=hass.loop):
+ resp = yield from session.get(url, params=params)
+ body = yield from resp.text()
+
+ if "has not changed" in body:
+ # IP has not changed.
+ _LOGGER.debug("FreeDNS update skipped: IP has not changed")
+ return True
+
+ if "ERROR" not in body:
+ _LOGGER.debug("Updating FreeDNS was successful: %s", body)
+ return True
+
+ if "Invalid update URL" in body:
+ _LOGGER.error("FreeDNS update token is invalid")
+ else:
+ _LOGGER.warning("Updating FreeDNS failed: %s", body)
+
+ except aiohttp.ClientError:
+ _LOGGER.warning("Can't connect to FreeDNS API")
+
+ except asyncio.TimeoutError:
+ _LOGGER.warning("Timeout from FreeDNS API at %s", url)
+
+ return False
diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py
index 1fbfe94bb0d..3fc3eff0a14 100644
--- a/homeassistant/components/frontend/__init__.py
+++ b/homeassistant/components/frontend/__init__.py
@@ -24,7 +24,7 @@ from homeassistant.core import callback
from homeassistant.helpers.translation import async_get_translations
from homeassistant.loader import bind_hass
-REQUIREMENTS = ['home-assistant-frontend==20180401.0']
+REQUIREMENTS = ['home-assistant-frontend==20180404.0']
DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py
index 8ef8445aa70..948e26be291 100644
--- a/homeassistant/components/homekit/__init__.py
+++ b/homeassistant/components/homekit/__init__.py
@@ -28,7 +28,7 @@ from .util import (
TYPES = Registry()
_LOGGER = logging.getLogger(__name__)
-REQUIREMENTS = ['HAP-python==1.1.7']
+REQUIREMENTS = ['HAP-python==1.1.8']
CONFIG_SCHEMA = vol.Schema({
@@ -102,8 +102,7 @@ def get_accessory(hass, state, aid, config):
aid=aid)
elif state.domain == 'alarm_control_panel':
- _LOGGER.debug('Add "%s" as "%s"', state.entity_id,
- 'SecuritySystem')
+ _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'SecuritySystem')
return TYPES['SecuritySystem'](hass, state.entity_id, state.name,
alarm_code=config.get(ATTR_CODE),
aid=aid)
@@ -120,6 +119,7 @@ def get_accessory(hass, state, aid, config):
state.name, support_auto, aid=aid)
elif state.domain == 'light':
+ _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Light')
return TYPES['Light'](hass, state.entity_id, state.name, aid=aid)
elif state.domain == 'switch' or state.domain == 'remote' \
diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py
index 4c4409e6dfc..da45bee9e90 100644
--- a/homeassistant/components/homekit/accessories.py
+++ b/homeassistant/components/homekit/accessories.py
@@ -8,8 +8,8 @@ from homeassistant.helpers.event import async_track_state_change
from .const import (
ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, BRIDGE_NAME,
- MANUFACTURER, SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE,
- CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER)
+ MANUFACTURER, SERV_ACCESSORY_INFO, CHAR_MANUFACTURER, CHAR_MODEL,
+ CHAR_NAME, CHAR_SERIAL_NUMBER)
from .util import (
show_setup_message, dismiss_setup_message)
@@ -39,15 +39,6 @@ def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER,
service.get_characteristic(CHAR_SERIAL_NUMBER).set_value(serial_number)
-def override_properties(char, properties=None, valid_values=None):
- """Override characteristic property values and valid values."""
- if properties:
- char.properties.update(properties)
-
- if valid_values:
- char.properties['ValidValues'].update(valid_values)
-
-
class HomeAccessory(Accessory):
"""Adapter class for Accessory."""
@@ -65,10 +56,10 @@ class HomeAccessory(Accessory):
def run(self):
"""Method called by accessory after driver is started."""
- state = self._hass.states.get(self._entity_id)
+ state = self.hass.states.get(self.entity_id)
self.update_state(new_state=state)
async_track_state_change(
- self._hass, self._entity_id, self.update_state)
+ self.hass, self.entity_id, self.update_state)
class HomeBridge(Bridge):
@@ -79,11 +70,10 @@ class HomeBridge(Bridge):
"""Initialize a Bridge object."""
super().__init__(name, **kwargs)
set_accessory_info(self, name, model)
- self._hass = hass
+ self.hass = hass
def _set_services(self):
add_preload_service(self, SERV_ACCESSORY_INFO)
- add_preload_service(self, SERV_BRIDGING_STATE)
def setup_message(self):
"""Prevent print of pyhap setup message to terminal."""
@@ -92,12 +82,12 @@ class HomeBridge(Bridge):
def add_paired_client(self, client_uuid, client_public):
"""Override super function to dismiss setup message if paired."""
super().add_paired_client(client_uuid, client_public)
- dismiss_setup_message(self._hass)
+ dismiss_setup_message(self.hass)
def remove_paired_client(self, client_uuid):
"""Override super function to show setup message if unpaired."""
super().remove_paired_client(client_uuid)
- show_setup_message(self, self._hass)
+ show_setup_message(self, self.hass)
class HomeDriver(AccessoryDriver):
diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py
index a45c8298b78..d1c3d84b517 100644
--- a/homeassistant/components/homekit/const.py
+++ b/homeassistant/components/homekit/const.py
@@ -24,13 +24,16 @@ BRIDGE_NAME = 'Home Assistant'
MANUFACTURER = 'HomeAssistant'
# #### Categories ####
+CATEGORY_ALARM_SYSTEM = 'ALARM_SYSTEM'
CATEGORY_LIGHT = 'LIGHTBULB'
CATEGORY_SENSOR = 'SENSOR'
+CATEGORY_SWITCH = 'SWITCH'
+CATEGORY_THERMOSTAT = 'THERMOSTAT'
+CATEGORY_WINDOW_COVERING = 'WINDOW_COVERING'
# #### Services ####
SERV_ACCESSORY_INFO = 'AccessoryInformation'
-SERV_BRIDGING_STATE = 'BridgingState'
SERV_HUMIDITY_SENSOR = 'HumiditySensor'
# CurrentRelativeHumidity | StatusActive, StatusFault, StatusTampered,
# StatusLowBattery, Name
@@ -43,9 +46,8 @@ SERV_WINDOW_COVERING = 'WindowCovering'
# #### Characteristics ####
-CHAR_ACC_IDENTIFIER = 'AccessoryIdentifier'
CHAR_BRIGHTNESS = 'Brightness' # Int | [0, 100]
-CHAR_CATEGORY = 'Category'
+CHAR_COLOR_TEMPERATURE = 'ColorTemperature'
CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature'
CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState'
CHAR_CURRENT_POSITION = 'CurrentPosition'
@@ -54,13 +56,11 @@ CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState'
CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature'
CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature'
CHAR_HUE = 'Hue' # arcdegress | [0, 360]
-CHAR_LINK_QUALITY = 'LinkQuality'
CHAR_MANUFACTURER = 'Manufacturer'
CHAR_MODEL = 'Model'
CHAR_NAME = 'Name'
CHAR_ON = 'On' # boolean
CHAR_POSITION_STATE = 'PositionState'
-CHAR_REACHABLE = 'Reachable'
CHAR_SATURATION = 'Saturation' # percent
CHAR_SERIAL_NUMBER = 'SerialNumber'
CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState'
diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py
index 7616ef05fdf..3650a948f5d 100644
--- a/homeassistant/components/homekit/type_covers.py
+++ b/homeassistant/components/homekit/type_covers.py
@@ -6,8 +6,8 @@ from homeassistant.components.cover import ATTR_CURRENT_POSITION
from . import TYPES
from .accessories import HomeAccessory, add_preload_service
from .const import (
- SERV_WINDOW_COVERING, CHAR_CURRENT_POSITION,
- CHAR_TARGET_POSITION, CHAR_POSITION_STATE)
+ CATEGORY_WINDOW_COVERING, SERV_WINDOW_COVERING,
+ CHAR_CURRENT_POSITION, CHAR_TARGET_POSITION, CHAR_POSITION_STATE)
_LOGGER = logging.getLogger(__name__)
@@ -20,13 +20,13 @@ class WindowCovering(HomeAccessory):
The cover entity must support: set_cover_position.
"""
- def __init__(self, hass, entity_id, display_name, *args, **kwargs):
+ def __init__(self, hass, entity_id, display_name, **kwargs):
"""Initialize a WindowCovering accessory object."""
- super().__init__(display_name, entity_id, 'WINDOW_COVERING',
- *args, **kwargs)
+ super().__init__(display_name, entity_id,
+ CATEGORY_WINDOW_COVERING, **kwargs)
- self._hass = hass
- self._entity_id = entity_id
+ self.hass = hass
+ self.entity_id = entity_id
self.current_position = None
self.homekit_target = None
@@ -48,14 +48,14 @@ class WindowCovering(HomeAccessory):
"""Move cover to value if call came from HomeKit."""
self.char_target_position.set_value(value, should_callback=False)
if value != self.current_position:
- _LOGGER.debug('%s: Set position to %d', self._entity_id, value)
+ _LOGGER.debug('%s: Set position to %d', self.entity_id, value)
self.homekit_target = value
if value > self.current_position:
self.char_position_state.set_value(1)
elif value < self.current_position:
self.char_position_state.set_value(0)
- self._hass.components.cover.set_cover_position(
- value, self._entity_id)
+ self.hass.components.cover.set_cover_position(
+ value, self.entity_id)
def update_state(self, entity_id=None, old_state=None, new_state=None):
"""Update cover position after state changed."""
@@ -63,14 +63,11 @@ class WindowCovering(HomeAccessory):
return
current_position = new_state.attributes.get(ATTR_CURRENT_POSITION)
- if current_position is None:
- return
-
- self.current_position = int(current_position)
- self.char_current_position.set_value(self.current_position)
-
- if self.homekit_target is None or \
- abs(self.current_position - self.homekit_target) < 6:
- self.char_target_position.set_value(self.current_position)
- self.char_position_state.set_value(2)
- self.homekit_target = None
+ if isinstance(current_position, int):
+ self.current_position = current_position
+ self.char_current_position.set_value(self.current_position)
+ if self.homekit_target is None or \
+ abs(self.current_position - self.homekit_target) < 6:
+ self.char_target_position.set_value(self.current_position)
+ self.char_position_state.set_value(2)
+ self.homekit_target = None
diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py
index d88e7100131..018d3cd2e74 100644
--- a/homeassistant/components/homekit/type_lights.py
+++ b/homeassistant/components/homekit/type_lights.py
@@ -2,13 +2,14 @@
import logging
from homeassistant.components.light import (
- ATTR_HS_COLOR, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, SUPPORT_COLOR)
+ ATTR_HS_COLOR, ATTR_COLOR_TEMP, ATTR_BRIGHTNESS, ATTR_MIN_MIREDS,
+ ATTR_MAX_MIREDS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_BRIGHTNESS)
from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF
from . import TYPES
from .accessories import HomeAccessory, add_preload_service
from .const import (
- CATEGORY_LIGHT, SERV_LIGHTBULB,
+ CATEGORY_LIGHT, SERV_LIGHTBULB, CHAR_COLOR_TEMPERATURE,
CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION)
_LOGGER = logging.getLogger(__name__)
@@ -20,25 +21,27 @@ RGB_COLOR = 'rgb_color'
class Light(HomeAccessory):
"""Generate a Light accessory for a light entity.
- Currently supports: state, brightness, rgb_color.
+ Currently supports: state, brightness, color temperature, rgb_color.
"""
- def __init__(self, hass, entity_id, name, *args, **kwargs):
+ def __init__(self, hass, entity_id, name, **kwargs):
"""Initialize a new Light accessory object."""
- super().__init__(name, entity_id, CATEGORY_LIGHT, *args, **kwargs)
+ super().__init__(name, entity_id, CATEGORY_LIGHT, **kwargs)
- self._hass = hass
- self._entity_id = entity_id
+ self.hass = hass
+ self.entity_id = entity_id
self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False,
CHAR_HUE: False, CHAR_SATURATION: False,
- RGB_COLOR: False}
+ CHAR_COLOR_TEMPERATURE: False, RGB_COLOR: False}
self._state = 0
self.chars = []
- self._features = self._hass.states.get(self._entity_id) \
+ self._features = self.hass.states.get(self.entity_id) \
.attributes.get(ATTR_SUPPORTED_FEATURES)
if self._features & SUPPORT_BRIGHTNESS:
self.chars.append(CHAR_BRIGHTNESS)
+ if self._features & SUPPORT_COLOR_TEMP:
+ self.chars.append(CHAR_COLOR_TEMPERATURE)
if self._features & SUPPORT_COLOR:
self.chars.append(CHAR_HUE)
self.chars.append(CHAR_SATURATION)
@@ -55,6 +58,18 @@ class Light(HomeAccessory):
.get_characteristic(CHAR_BRIGHTNESS)
self.char_brightness.setter_callback = self.set_brightness
self.char_brightness.value = 0
+ if CHAR_COLOR_TEMPERATURE in self.chars:
+ self.char_color_temperature = serv_light \
+ .get_characteristic(CHAR_COLOR_TEMPERATURE)
+ self.char_color_temperature.setter_callback = \
+ self.set_color_temperature
+ min_mireds = self.hass.states.get(self.entity_id) \
+ .attributes.get(ATTR_MIN_MIREDS, 153)
+ max_mireds = self.hass.states.get(self.entity_id) \
+ .attributes.get(ATTR_MAX_MIREDS, 500)
+ self.char_color_temperature.override_properties({
+ 'minValue': min_mireds, 'maxValue': max_mireds})
+ self.char_color_temperature.value = min_mireds
if CHAR_HUE in self.chars:
self.char_hue = serv_light.get_characteristic(CHAR_HUE)
self.char_hue.setter_callback = self.set_hue
@@ -70,29 +85,36 @@ class Light(HomeAccessory):
if self._state == value:
return
- _LOGGER.debug('%s: Set state to %d', self._entity_id, value)
+ _LOGGER.debug('%s: Set state to %d', self.entity_id, value)
self._flag[CHAR_ON] = True
self.char_on.set_value(value, should_callback=False)
if value == 1:
- self._hass.components.light.turn_on(self._entity_id)
+ self.hass.components.light.turn_on(self.entity_id)
elif value == 0:
- self._hass.components.light.turn_off(self._entity_id)
+ self.hass.components.light.turn_off(self.entity_id)
def set_brightness(self, value):
"""Set brightness if call came from HomeKit."""
- _LOGGER.debug('%s: Set brightness to %d', self._entity_id, value)
+ _LOGGER.debug('%s: Set brightness to %d', self.entity_id, value)
self._flag[CHAR_BRIGHTNESS] = True
self.char_brightness.set_value(value, should_callback=False)
if value != 0:
- self._hass.components.light.turn_on(
- self._entity_id, brightness_pct=value)
+ self.hass.components.light.turn_on(
+ self.entity_id, brightness_pct=value)
else:
- self._hass.components.light.turn_off(self._entity_id)
+ self.hass.components.light.turn_off(self.entity_id)
+
+ def set_color_temperature(self, value):
+ """Set color temperature if call came from HomeKit."""
+ _LOGGER.debug('%s: Set color temp to %s', self.entity_id, value)
+ self._flag[CHAR_COLOR_TEMPERATURE] = True
+ self.char_color_temperature.set_value(value, should_callback=False)
+ self.hass.components.light.turn_on(self.entity_id, color_temp=value)
def set_saturation(self, value):
"""Set saturation if call came from HomeKit."""
- _LOGGER.debug('%s: Set saturation to %d', self._entity_id, value)
+ _LOGGER.debug('%s: Set saturation to %d', self.entity_id, value)
self._flag[CHAR_SATURATION] = True
self.char_saturation.set_value(value, should_callback=False)
self._saturation = value
@@ -100,7 +122,7 @@ class Light(HomeAccessory):
def set_hue(self, value):
"""Set hue if call came from HomeKit."""
- _LOGGER.debug('%s: Set hue to %d', self._entity_id, value)
+ _LOGGER.debug('%s: Set hue to %d', self.entity_id, value)
self._flag[CHAR_HUE] = True
self.char_hue.set_value(value, should_callback=False)
self._hue = value
@@ -112,11 +134,11 @@ class Light(HomeAccessory):
if self._features & SUPPORT_COLOR and self._flag[CHAR_HUE] and \
self._flag[CHAR_SATURATION]:
color = (self._hue, self._saturation)
- _LOGGER.debug('%s: Set hs_color to %s', self._entity_id, color)
+ _LOGGER.debug('%s: Set hs_color to %s', self.entity_id, color)
self._flag.update({
CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True})
- self._hass.components.light.turn_on(
- self._entity_id, hs_color=color)
+ self.hass.components.light.turn_on(
+ self.entity_id, hs_color=color)
def update_state(self, entity_id=None, old_state=None, new_state=None):
"""Update light after state change."""
@@ -141,13 +163,25 @@ class Light(HomeAccessory):
should_callback=False)
self._flag[CHAR_BRIGHTNESS] = False
+ # Handle color temperature
+ if CHAR_COLOR_TEMPERATURE in self.chars:
+ color_temperature = new_state.attributes.get(ATTR_COLOR_TEMP)
+ if not self._flag[CHAR_COLOR_TEMPERATURE] \
+ and isinstance(color_temperature, int):
+ self.char_color_temperature.set_value(color_temperature,
+ should_callback=False)
+ self._flag[CHAR_COLOR_TEMPERATURE] = False
+
# Handle Color
if CHAR_SATURATION in self.chars and CHAR_HUE in self.chars:
hue, saturation = new_state.attributes.get(
ATTR_HS_COLOR, (None, None))
if not self._flag[RGB_COLOR] and (
- hue != self._hue or saturation != self._saturation):
+ hue != self._hue or saturation != self._saturation) and \
+ isinstance(hue, (int, float)) and \
+ isinstance(saturation, (int, float)):
self.char_hue.set_value(hue, should_callback=False)
self.char_saturation.set_value(saturation,
should_callback=False)
+ self._hue, self._saturation = (hue, saturation)
self._flag[RGB_COLOR] = False
diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py
index b23522f0ea2..2cce6653db3 100644
--- a/homeassistant/components/homekit/type_security_systems.py
+++ b/homeassistant/components/homekit/type_security_systems.py
@@ -9,8 +9,8 @@ from homeassistant.const import (
from . import TYPES
from .accessories import HomeAccessory, add_preload_service
from .const import (
- SERV_SECURITY_SYSTEM, CHAR_CURRENT_SECURITY_STATE,
- CHAR_TARGET_SECURITY_STATE)
+ CATEGORY_ALARM_SYSTEM, SERV_SECURITY_SYSTEM,
+ CHAR_CURRENT_SECURITY_STATE, CHAR_TARGET_SECURITY_STATE)
_LOGGER = logging.getLogger(__name__)
@@ -27,14 +27,13 @@ STATE_TO_SERVICE = {STATE_ALARM_DISARMED: 'alarm_disarm',
class SecuritySystem(HomeAccessory):
"""Generate an SecuritySystem accessory for an alarm control panel."""
- def __init__(self, hass, entity_id, display_name,
- alarm_code, *args, **kwargs):
+ def __init__(self, hass, entity_id, display_name, alarm_code, **kwargs):
"""Initialize a SecuritySystem accessory object."""
- super().__init__(display_name, entity_id, 'ALARM_SYSTEM',
- *args, **kwargs)
+ super().__init__(display_name, entity_id,
+ CATEGORY_ALARM_SYSTEM, **kwargs)
- self._hass = hass
- self._entity_id = entity_id
+ self.hass = hass
+ self.entity_id = entity_id
self._alarm_code = alarm_code
self.flag_target_state = False
@@ -52,16 +51,16 @@ class SecuritySystem(HomeAccessory):
def set_security_state(self, value):
"""Move security state to value if call came from HomeKit."""
_LOGGER.debug('%s: Set security state to %d',
- self._entity_id, value)
+ self.entity_id, value)
self.flag_target_state = True
self.char_target_state.set_value(value, should_callback=False)
hass_value = HOMEKIT_TO_HASS[value]
service = STATE_TO_SERVICE[hass_value]
- params = {ATTR_ENTITY_ID: self._entity_id}
+ params = {ATTR_ENTITY_ID: self.entity_id}
if self._alarm_code:
params[ATTR_CODE] = self._alarm_code
- self._hass.services.call('alarm_control_panel', service, params)
+ self.hass.services.call('alarm_control_panel', service, params)
def update_state(self, entity_id=None, old_state=None, new_state=None):
"""Update security state after state changed."""
@@ -76,7 +75,7 @@ class SecuritySystem(HomeAccessory):
self.char_current_state.set_value(current_security_state,
should_callback=False)
_LOGGER.debug('%s: Updated current state to %s (%d)',
- self._entity_id, hass_state, current_security_state)
+ self.entity_id, hass_state, current_security_state)
if not self.flag_target_state:
self.char_target_state.set_value(current_security_state,
diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py
index e980ce4a316..80521df5991 100644
--- a/homeassistant/components/homekit/type_sensors.py
+++ b/homeassistant/components/homekit/type_sensors.py
@@ -5,8 +5,7 @@ from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS)
from . import TYPES
-from .accessories import (
- HomeAccessory, add_preload_service, override_properties)
+from .accessories import HomeAccessory, add_preload_service
from .const import (
CATEGORY_SENSOR, SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR,
CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS)
@@ -23,16 +22,16 @@ class TemperatureSensor(HomeAccessory):
Sensor entity must return temperature in °C, °F.
"""
- def __init__(self, hass, entity_id, name, *args, **kwargs):
+ def __init__(self, hass, entity_id, name, **kwargs):
"""Initialize a TemperatureSensor accessory object."""
- super().__init__(name, entity_id, CATEGORY_SENSOR, *args, **kwargs)
+ super().__init__(name, entity_id, CATEGORY_SENSOR, **kwargs)
- self._hass = hass
- self._entity_id = entity_id
+ self.hass = hass
+ self.entity_id = entity_id
serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR)
self.char_temp = serv_temp.get_characteristic(CHAR_CURRENT_TEMPERATURE)
- override_properties(self.char_temp, PROP_CELSIUS)
+ self.char_temp.override_properties(properties=PROP_CELSIUS)
self.char_temp.value = 0
self.unit = None
@@ -47,7 +46,7 @@ class TemperatureSensor(HomeAccessory):
temperature = temperature_to_homekit(temperature, unit)
self.char_temp.set_value(temperature, should_callback=False)
_LOGGER.debug('%s: Current temperature set to %d°C',
- self._entity_id, temperature)
+ self.entity_id, temperature)
@TYPES.register('HumiditySensor')
@@ -58,8 +57,8 @@ class HumiditySensor(HomeAccessory):
"""Initialize a HumiditySensor accessory object."""
super().__init__(name, entity_id, CATEGORY_SENSOR, *args, **kwargs)
- self._hass = hass
- self._entity_id = entity_id
+ self.hass = hass
+ self.entity_id = entity_id
serv_humidity = add_preload_service(self, SERV_HUMIDITY_SENSOR)
self.char_humidity = serv_humidity \
@@ -75,4 +74,4 @@ class HumiditySensor(HomeAccessory):
if humidity:
self.char_humidity.set_value(humidity, should_callback=False)
_LOGGER.debug('%s: Percent set to %d%%',
- self._entity_id, humidity)
+ self.entity_id, humidity)
diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py
index 1f19893d0be..689edde6f37 100644
--- a/homeassistant/components/homekit/type_switches.py
+++ b/homeassistant/components/homekit/type_switches.py
@@ -7,7 +7,7 @@ from homeassistant.core import split_entity_id
from . import TYPES
from .accessories import HomeAccessory, add_preload_service
-from .const import SERV_SWITCH, CHAR_ON
+from .const import CATEGORY_SWITCH, SERV_SWITCH, CHAR_ON
_LOGGER = logging.getLogger(__name__)
@@ -16,12 +16,12 @@ _LOGGER = logging.getLogger(__name__)
class Switch(HomeAccessory):
"""Generate a Switch accessory."""
- def __init__(self, hass, entity_id, display_name, *args, **kwargs):
+ def __init__(self, hass, entity_id, display_name, **kwargs):
"""Initialize a Switch accessory object to represent a remote."""
- super().__init__(display_name, entity_id, 'SWITCH', *args, **kwargs)
+ super().__init__(display_name, entity_id, CATEGORY_SWITCH, **kwargs)
- self._hass = hass
- self._entity_id = entity_id
+ self.hass = hass
+ self.entity_id = entity_id
self._domain = split_entity_id(entity_id)[0]
self.flag_target_state = False
@@ -34,12 +34,12 @@ class Switch(HomeAccessory):
def set_state(self, value):
"""Move switch state to value if call came from HomeKit."""
_LOGGER.debug('%s: Set switch state to %s',
- self._entity_id, value)
+ self.entity_id, value)
self.flag_target_state = True
self.char_on.set_value(value, should_callback=False)
service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
- self._hass.services.call(self._domain, service,
- {ATTR_ENTITY_ID: self._entity_id})
+ self.hass.services.call(self._domain, service,
+ {ATTR_ENTITY_ID: self.entity_id})
def update_state(self, entity_id=None, old_state=None, new_state=None):
"""Update switch state after state changed."""
@@ -49,7 +49,7 @@ class Switch(HomeAccessory):
current_state = (new_state.state == STATE_ON)
if not self.flag_target_state:
_LOGGER.debug('%s: Set current state to %s',
- self._entity_id, current_state)
+ self.entity_id, current_state)
self.char_on.set_value(current_state, should_callback=False)
self.flag_target_state = False
diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py
index d49c1ca626b..69b61062791 100644
--- a/homeassistant/components/homekit/type_thermostats.py
+++ b/homeassistant/components/homekit/type_thermostats.py
@@ -7,12 +7,12 @@ from homeassistant.components.climate import (
ATTR_OPERATION_MODE, ATTR_OPERATION_LIST,
STATE_HEAT, STATE_COOL, STATE_AUTO)
from homeassistant.const import (
- ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT)
+ ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT)
from . import TYPES
from .accessories import HomeAccessory, add_preload_service
from .const import (
- SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING,
+ CATEGORY_THERMOSTAT, SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING,
CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE,
CHAR_TARGET_TEMPERATURE, CHAR_TEMP_DISPLAY_UNITS,
CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE)
@@ -20,7 +20,6 @@ from .util import temperature_to_homekit, temperature_to_states
_LOGGER = logging.getLogger(__name__)
-STATE_OFF = 'off'
UNIT_HASS_TO_HOMEKIT = {TEMP_CELSIUS: 0, TEMP_FAHRENHEIT: 1}
UNIT_HOMEKIT_TO_HASS = {c: s for s, c in UNIT_HASS_TO_HOMEKIT.items()}
HC_HASS_TO_HOMEKIT = {STATE_OFF: 0, STATE_HEAT: 1,
@@ -32,14 +31,13 @@ HC_HOMEKIT_TO_HASS = {c: s for s, c in HC_HASS_TO_HOMEKIT.items()}
class Thermostat(HomeAccessory):
"""Generate a Thermostat accessory for a climate."""
- def __init__(self, hass, entity_id, display_name,
- support_auto, *args, **kwargs):
+ def __init__(self, hass, entity_id, display_name, support_auto, **kwargs):
"""Initialize a Thermostat accessory object."""
- super().__init__(display_name, entity_id, 'THERMOSTAT',
- *args, **kwargs)
+ super().__init__(display_name, entity_id,
+ CATEGORY_THERMOSTAT, **kwargs)
- self._hass = hass
- self._entity_id = entity_id
+ self.hass = hass
+ self.entity_id = entity_id
self._call_timer = None
self._unit = TEMP_CELSIUS
@@ -101,48 +99,48 @@ class Thermostat(HomeAccessory):
"""Move operation mode to value if call came from HomeKit."""
self.char_target_heat_cool.set_value(value, should_callback=False)
if value in HC_HOMEKIT_TO_HASS:
- _LOGGER.debug('%s: Set heat-cool to %d', self._entity_id, value)
+ _LOGGER.debug('%s: Set heat-cool to %d', self.entity_id, value)
self.heat_cool_flag_target_state = True
hass_value = HC_HOMEKIT_TO_HASS[value]
- self._hass.components.climate.set_operation_mode(
- operation_mode=hass_value, entity_id=self._entity_id)
+ self.hass.components.climate.set_operation_mode(
+ operation_mode=hass_value, entity_id=self.entity_id)
def set_cooling_threshold(self, value):
"""Set cooling threshold temp to value if call came from HomeKit."""
_LOGGER.debug('%s: Set cooling threshold temperature to %.2f°C',
- self._entity_id, value)
+ self.entity_id, value)
self.coolingthresh_flag_target_state = True
self.char_cooling_thresh_temp.set_value(value, should_callback=False)
low = self.char_heating_thresh_temp.value
low = temperature_to_states(low, self._unit)
value = temperature_to_states(value, self._unit)
- self._hass.components.climate.set_temperature(
- entity_id=self._entity_id, target_temp_high=value,
+ self.hass.components.climate.set_temperature(
+ entity_id=self.entity_id, target_temp_high=value,
target_temp_low=low)
def set_heating_threshold(self, value):
"""Set heating threshold temp to value if call came from HomeKit."""
_LOGGER.debug('%s: Set heating threshold temperature to %.2f°C',
- self._entity_id, value)
+ self.entity_id, value)
self.heatingthresh_flag_target_state = True
self.char_heating_thresh_temp.set_value(value, should_callback=False)
# Home assistant always wants to set low and high at the same time
high = self.char_cooling_thresh_temp.value
high = temperature_to_states(high, self._unit)
value = temperature_to_states(value, self._unit)
- self._hass.components.climate.set_temperature(
- entity_id=self._entity_id, target_temp_high=high,
+ self.hass.components.climate.set_temperature(
+ entity_id=self.entity_id, target_temp_high=high,
target_temp_low=value)
def set_target_temperature(self, value):
"""Set target temperature to value if call came from HomeKit."""
_LOGGER.debug('%s: Set target temperature to %.2f°C',
- self._entity_id, value)
+ self.entity_id, value)
self.temperature_flag_target_state = True
self.char_target_temp.set_value(value, should_callback=False)
value = temperature_to_states(value, self._unit)
- self._hass.components.climate.set_temperature(
- temperature=value, entity_id=self._entity_id)
+ self.hass.components.climate.set_temperature(
+ temperature=value, entity_id=self.entity_id)
def update_state(self, entity_id=None, old_state=None, new_state=None):
"""Update security state after state changed."""
diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py
index b70021e0304..557a47f3e05 100644
--- a/homeassistant/components/hue/__init__.py
+++ b/homeassistant/components/hue/__init__.py
@@ -4,31 +4,23 @@ This component provides basic support for the Philips Hue system.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/hue/
"""
-import asyncio
-import json
import ipaddress
import logging
-import os
-import async_timeout
import voluptuous as vol
-from homeassistant.core import callback
-from homeassistant.components.discovery import SERVICE_HUE
from homeassistant.const import CONF_FILENAME, CONF_HOST
-import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers import discovery, aiohttp_client
-from homeassistant import config_entries
-from homeassistant.util.json import save_json
+from homeassistant.helpers import aiohttp_client, config_validation as cv
+
+from .const import DOMAIN, API_NUPNP
+from .bridge import HueBridge
+# Loading the config flow file will register the flow
+from .config_flow import configured_hosts
REQUIREMENTS = ['aiohue==1.3.0']
_LOGGER = logging.getLogger(__name__)
-DOMAIN = "hue"
-SERVICE_HUE_SCENE = "hue_activate_scene"
-API_NUPNP = 'https://www.meethue.com/api/nupnp'
-
CONF_BRIDGES = "bridges"
CONF_ALLOW_UNREACHABLE = 'allow_unreachable'
@@ -42,6 +34,7 @@ DEFAULT_ALLOW_HUE_GROUPS = True
BRIDGE_CONFIG_SCHEMA = vol.Schema({
# Validate as IP address and then convert back to a string.
vol.Required(CONF_HOST): vol.All(ipaddress.ip_address, cv.string),
+ # This is for legacy reasons and is only used for importing auth.
vol.Optional(CONF_FILENAME, default=PHUE_CONFIG_FILE): cv.string,
vol.Optional(CONF_ALLOW_UNREACHABLE,
default=DEFAULT_ALLOW_UNREACHABLE): cv.boolean,
@@ -56,19 +49,6 @@ CONFIG_SCHEMA = vol.Schema({
}),
}, extra=vol.ALLOW_EXTRA)
-ATTR_GROUP_NAME = "group_name"
-ATTR_SCENE_NAME = "scene_name"
-SCENE_SCHEMA = vol.Schema({
- vol.Required(ATTR_GROUP_NAME): cv.string,
- vol.Required(ATTR_SCENE_NAME): cv.string,
-})
-
-CONFIG_INSTRUCTIONS = """
-Press the button on the bridge to register Philips Hue with Home Assistant.
-
-
-"""
-
async def async_setup(hass, config):
"""Set up the Hue platform."""
@@ -76,20 +56,8 @@ async def async_setup(hass, config):
if conf is None:
conf = {}
- if DOMAIN not in hass.data:
- hass.data[DOMAIN] = {}
-
- async def async_bridge_discovered(service, discovery_info):
- """Dispatcher for Hue discovery events."""
- # Ignore emulated hue
- if "HASS Bridge" in discovery_info.get('name', ''):
- return
-
- await async_setup_bridge(
- hass, discovery_info['host'],
- 'phue-{}.conf'.format(discovery_info['serial']))
-
- discovery.async_listen(hass, SERVICE_HUE, async_bridge_discovered)
+ hass.data[DOMAIN] = {}
+ configured = configured_hosts(hass)
# User has configured bridges
if CONF_BRIDGES in conf:
@@ -103,12 +71,19 @@ async def async_setup(hass, config):
async with websession.get(API_NUPNP) as req:
hosts = await req.json()
- # Run through config schema to populate defaults
- bridges = [BRIDGE_CONFIG_SCHEMA({
- CONF_HOST: entry['internalipaddress'],
- CONF_FILENAME: '.hue_{}.conf'.format(entry['id']),
- }) for entry in hosts]
+ bridges = []
+ for entry in hosts:
+ # Filter out already configured hosts
+ if entry['internalipaddress'] in configured:
+ continue
+ # Run through config schema to populate defaults
+ bridges.append(BRIDGE_CONFIG_SCHEMA({
+ CONF_HOST: entry['internalipaddress'],
+ # Careful with using entry['id'] for other reasons. The
+ # value is in lowercase but is returned uppercase from hub.
+ CONF_FILENAME: '.hue_{}.conf'.format(entry['id']),
+ }))
else:
# Component not specified in config, we're loaded via discovery
bridges = []
@@ -116,277 +91,43 @@ async def async_setup(hass, config):
if not bridges:
return True
- await asyncio.wait([
- async_setup_bridge(
- hass, bridge[CONF_HOST], bridge[CONF_FILENAME],
- bridge[CONF_ALLOW_UNREACHABLE], bridge[CONF_ALLOW_HUE_GROUPS]
- ) for bridge in bridges
- ])
+ for bridge_conf in bridges:
+ host = bridge_conf[CONF_HOST]
+
+ # Store config in hass.data so the config entry can find it
+ hass.data[DOMAIN][host] = bridge_conf
+
+ # If configured, the bridge will be set up during config entry phase
+ if host in configured:
+ continue
+
+ # No existing config entry found, try importing it or trigger link
+ # config flow if no existing auth. Because we're inside the setup of
+ # this component we'll have to use hass.async_add_job to avoid a
+ # deadlock: creating a config entry will set up the component but the
+ # setup would block till the entry is created!
+ hass.async_add_job(hass.config_entries.flow.async_init(
+ DOMAIN, source='import', data={
+ 'host': bridge_conf[CONF_HOST],
+ 'path': bridge_conf[CONF_FILENAME],
+ }
+ ))
return True
-async def async_setup_bridge(
- hass, host, filename=None,
- allow_unreachable=DEFAULT_ALLOW_UNREACHABLE,
- allow_hue_groups=DEFAULT_ALLOW_HUE_GROUPS,
- username=None):
- """Set up a given Hue bridge."""
- assert filename or username, 'Need to pass at least a username or filename'
-
- # Only register a device once
- if host in hass.data[DOMAIN]:
- return
-
- if username is None:
- username = await hass.async_add_job(
- _find_username_from_config, hass, filename)
-
- bridge = HueBridge(host, hass, filename, username, allow_unreachable,
- allow_hue_groups)
- await bridge.async_setup()
-
-
-def _find_username_from_config(hass, filename):
- """Load username from config."""
- path = hass.config.path(filename)
-
- if not os.path.isfile(path):
- return None
-
- with open(path) as inp:
- return list(json.load(inp).values())[0]['username']
-
-
-class HueBridge(object):
- """Manages a single Hue bridge."""
-
- def __init__(self, host, hass, filename, username,
- allow_unreachable=False, allow_groups=True):
- """Initialize the system."""
- self.host = host
- self.hass = hass
- self.filename = filename
- self.username = username
- self.allow_unreachable = allow_unreachable
- self.allow_groups = allow_groups
- self.available = True
- self.config_request_id = None
- self.api = None
-
- async def async_setup(self):
- """Set up a phue bridge based on host parameter."""
- import aiohue
-
- api = aiohue.Bridge(
- self.host,
- username=self.username,
- websession=aiohttp_client.async_get_clientsession(self.hass)
- )
-
- try:
- with async_timeout.timeout(5):
- # Initialize bridge and validate our username
- if not self.username:
- await api.create_user('home-assistant')
- await api.initialize()
- except (aiohue.LinkButtonNotPressed, aiohue.Unauthorized):
- _LOGGER.warning("Connected to Hue at %s but not registered.",
- self.host)
- self.async_request_configuration()
- return
- except (asyncio.TimeoutError, aiohue.RequestError):
- _LOGGER.error("Error connecting to the Hue bridge at %s",
- self.host)
- return
- except aiohue.AiohueException:
- _LOGGER.exception('Unknown Hue linking error occurred')
- self.async_request_configuration()
- return
- except Exception: # pylint: disable=broad-except
- _LOGGER.exception("Unknown error connecting with Hue bridge at %s",
- self.host)
- return
-
- self.hass.data[DOMAIN][self.host] = self
-
- # If we came here and configuring this host, mark as done
- if self.config_request_id:
- request_id = self.config_request_id
- self.config_request_id = None
- self.hass.components.configurator.async_request_done(request_id)
-
- self.username = api.username
-
- # Save config file
- await self.hass.async_add_job(
- save_json, self.hass.config.path(self.filename),
- {self.host: {'username': api.username}})
-
- self.api = api
-
- self.hass.async_add_job(discovery.async_load_platform(
- self.hass, 'light', DOMAIN,
- {'host': self.host}))
-
- self.hass.services.async_register(
- DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene,
- schema=SCENE_SCHEMA)
-
- @callback
- def async_request_configuration(self):
- """Request configuration steps from the user."""
- configurator = self.hass.components.configurator
-
- # We got an error if this method is called while we are configuring
- if self.config_request_id:
- configurator.async_notify_errors(
- self.config_request_id,
- "Failed to register, please try again.")
- return
-
- async def config_callback(data):
- """Callback for configurator data."""
- await self.async_setup()
-
- self.config_request_id = configurator.async_request_config(
- "Philips Hue", config_callback,
- description=CONFIG_INSTRUCTIONS,
- entity_picture="/static/images/logo_philips_hue.png",
- submit_caption="I have pressed the button"
- )
-
- async def hue_activate_scene(self, call, updated=False):
- """Service to call directly into bridge to set scenes."""
- group_name = call.data[ATTR_GROUP_NAME]
- scene_name = call.data[ATTR_SCENE_NAME]
-
- group = next(
- (group for group in self.api.groups.values()
- if group.name == group_name), None)
-
- scene_id = next(
- (scene.id for scene in self.api.scenes.values()
- if scene.name == scene_name), None)
-
- # If we can't find it, fetch latest info.
- if not updated and (group is None or scene_id is None):
- await self.api.groups.update()
- await self.api.scenes.update()
- await self.hue_activate_scene(call, updated=True)
- return
-
- if group is None:
- _LOGGER.warning('Unable to find group %s', group_name)
- return
-
- if scene_id is None:
- _LOGGER.warning('Unable to find scene %s', scene_name)
- return
-
- await group.set_action(scene=scene_id)
-
-
-@config_entries.HANDLERS.register(DOMAIN)
-class HueFlowHandler(config_entries.ConfigFlowHandler):
- """Handle a Hue config flow."""
-
- VERSION = 1
-
- def __init__(self):
- """Initialize the Hue flow."""
- self.host = None
-
- @property
- def _websession(self):
- """Return a websession.
-
- Cannot assign in init because hass variable is not set yet.
- """
- return aiohttp_client.async_get_clientsession(self.hass)
-
- async def async_step_init(self, user_input=None):
- """Handle a flow start."""
- from aiohue.discovery import discover_nupnp
-
- if user_input is not None:
- self.host = user_input['host']
- return await self.async_step_link()
-
- try:
- with async_timeout.timeout(5):
- bridges = await discover_nupnp(websession=self._websession)
- except asyncio.TimeoutError:
- return self.async_abort(
- reason='discover_timeout'
- )
-
- if not bridges:
- return self.async_abort(
- reason='no_bridges'
- )
-
- # Find already configured hosts
- configured_hosts = set(
- entry.data['host'] for entry
- in self.hass.config_entries.async_entries(DOMAIN))
-
- hosts = [bridge.host for bridge in bridges
- if bridge.host not in configured_hosts]
-
- if not hosts:
- return self.async_abort(
- reason='all_configured'
- )
-
- elif len(hosts) == 1:
- self.host = hosts[0]
- return await self.async_step_link()
-
- return self.async_show_form(
- step_id='init',
- data_schema=vol.Schema({
- vol.Required('host'): vol.In(hosts)
- })
- )
-
- async def async_step_link(self, user_input=None):
- """Attempt to link with the Hue bridge."""
- import aiohue
- errors = {}
-
- if user_input is not None:
- bridge = aiohue.Bridge(self.host, websession=self._websession)
- try:
- with async_timeout.timeout(5):
- # Create auth token
- await bridge.create_user('home-assistant')
- # Fetches name and id
- await bridge.initialize()
- except (asyncio.TimeoutError, aiohue.RequestError,
- aiohue.LinkButtonNotPressed):
- errors['base'] = 'register_failed'
- except aiohue.AiohueException:
- errors['base'] = 'linking'
- _LOGGER.exception('Unknown Hue linking error occurred')
- else:
- return self.async_create_entry(
- title=bridge.config.name,
- data={
- 'host': bridge.host,
- 'bridge_id': bridge.config.bridgeid,
- 'username': bridge.username,
- }
- )
-
- return self.async_show_form(
- step_id='link',
- errors=errors,
- )
-
-
async def async_setup_entry(hass, entry):
- """Set up a bridge for a config entry."""
- await async_setup_bridge(hass, entry.data['host'],
- username=entry.data['username'])
- return True
+ """Set up a bridge from a config entry."""
+ host = entry.data['host']
+ config = hass.data[DOMAIN].get(host)
+
+ if config is None:
+ allow_unreachable = DEFAULT_ALLOW_UNREACHABLE
+ allow_groups = DEFAULT_ALLOW_HUE_GROUPS
+ else:
+ allow_unreachable = config[CONF_ALLOW_UNREACHABLE]
+ allow_groups = config[CONF_ALLOW_HUE_GROUPS]
+
+ bridge = HueBridge(hass, entry, allow_unreachable, allow_groups)
+ hass.data[DOMAIN][host] = bridge
+ return await bridge.async_setup()
diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py
new file mode 100644
index 00000000000..8093c84971e
--- /dev/null
+++ b/homeassistant/components/hue/bridge.py
@@ -0,0 +1,148 @@
+"""Code to handle a Hue bridge."""
+import asyncio
+
+import async_timeout
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.helpers import aiohttp_client, config_validation as cv
+
+from .const import DOMAIN, LOGGER
+from .errors import AuthenticationRequired, CannotConnect
+
+SERVICE_HUE_SCENE = "hue_activate_scene"
+ATTR_GROUP_NAME = "group_name"
+ATTR_SCENE_NAME = "scene_name"
+SCENE_SCHEMA = vol.Schema({
+ vol.Required(ATTR_GROUP_NAME): cv.string,
+ vol.Required(ATTR_SCENE_NAME): cv.string,
+})
+
+
+class HueBridge(object):
+ """Manages a single Hue bridge."""
+
+ def __init__(self, hass, config_entry, allow_unreachable, allow_groups):
+ """Initialize the system."""
+ self.config_entry = config_entry
+ self.hass = hass
+ self.allow_unreachable = allow_unreachable
+ self.allow_groups = allow_groups
+ self.available = True
+ self.api = None
+
+ @property
+ def host(self):
+ """Return the host of this bridge."""
+ return self.config_entry.data['host']
+
+ async def async_setup(self, tries=0):
+ """Set up a phue bridge based on host parameter."""
+ host = self.host
+
+ try:
+ self.api = await get_bridge(
+ self.hass, host,
+ self.config_entry.data['username']
+ )
+ except AuthenticationRequired:
+ # usernames can become invalid if hub is reset or user removed.
+ # We are going to fail the config entry setup and initiate a new
+ # linking procedure. When linking succeeds, it will remove the
+ # old config entry.
+ self.hass.async_add_job(self.hass.config_entries.flow.async_init(
+ DOMAIN, source='import', data={
+ 'host': host,
+ }
+ ))
+ return False
+
+ except CannotConnect:
+ retry_delay = 2 ** (tries + 1)
+ LOGGER.error("Error connecting to the Hue bridge at %s. Retrying "
+ "in %d seconds", host, retry_delay)
+
+ async def retry_setup(_now):
+ """Retry setup."""
+ if await self.async_setup(tries + 1):
+ # This feels hacky, we should find a better way to do this
+ self.config_entry.state = config_entries.ENTRY_STATE_LOADED
+
+ # Unhandled edge case: cancel this if we discover bridge on new IP
+ self.hass.helpers.event.async_call_later(retry_delay, retry_setup)
+
+ return False
+
+ except Exception: # pylint: disable=broad-except
+ LOGGER.exception('Unknown error connecting with Hue bridge at %s',
+ host)
+ return False
+
+ self.hass.async_add_job(
+ self.hass.helpers.discovery.async_load_platform(
+ 'light', DOMAIN, {'host': host}))
+
+ self.hass.services.async_register(
+ DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene,
+ schema=SCENE_SCHEMA)
+
+ return True
+
+ async def hue_activate_scene(self, call, updated=False):
+ """Service to call directly into bridge to set scenes."""
+ group_name = call.data[ATTR_GROUP_NAME]
+ scene_name = call.data[ATTR_SCENE_NAME]
+
+ group = next(
+ (group for group in self.api.groups.values()
+ if group.name == group_name), None)
+
+ scene_id = next(
+ (scene.id for scene in self.api.scenes.values()
+ if scene.name == scene_name), None)
+
+ # If we can't find it, fetch latest info.
+ if not updated and (group is None or scene_id is None):
+ await self.api.groups.update()
+ await self.api.scenes.update()
+ await self.hue_activate_scene(call, updated=True)
+ return
+
+ if group is None:
+ LOGGER.warning('Unable to find group %s', group_name)
+ return
+
+ if scene_id is None:
+ LOGGER.warning('Unable to find scene %s', scene_name)
+ return
+
+ await group.set_action(scene=scene_id)
+
+
+async def get_bridge(hass, host, username=None):
+ """Create a bridge object and verify authentication."""
+ import aiohue
+
+ bridge = aiohue.Bridge(
+ host, username=username,
+ websession=aiohttp_client.async_get_clientsession(hass)
+ )
+
+ try:
+ with async_timeout.timeout(5):
+ # Create username if we don't have one
+ if not username:
+ await bridge.create_user('home-assistant')
+ # Initialize bridge (and validate our username)
+ await bridge.initialize()
+
+ return bridge
+ except (aiohue.LinkButtonNotPressed, aiohue.Unauthorized):
+ LOGGER.warning("Connected to Hue at %s but not registered.", host)
+ raise AuthenticationRequired
+ except (asyncio.TimeoutError, aiohue.RequestError):
+ LOGGER.error("Error connecting to the Hue bridge at %s", host)
+ raise CannotConnect
+ except aiohue.AiohueException:
+ LOGGER.exception('Unknown Hue linking error occurred')
+ raise AuthenticationRequired
diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py
new file mode 100644
index 00000000000..11e399c984d
--- /dev/null
+++ b/homeassistant/components/hue/config_flow.py
@@ -0,0 +1,235 @@
+"""Config flow to configure Philips Hue."""
+import asyncio
+import json
+import os
+
+import async_timeout
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.core import callback
+from homeassistant.helpers import aiohttp_client
+
+from .bridge import get_bridge
+from .const import DOMAIN, LOGGER
+from .errors import AuthenticationRequired, CannotConnect
+
+
+@callback
+def configured_hosts(hass):
+ """Return a set of the configured hosts."""
+ return set(entry.data['host'] for entry
+ in hass.config_entries.async_entries(DOMAIN))
+
+
+def _find_username_from_config(hass, filename):
+ """Load username from config.
+
+ This was a legacy way of configuring Hue until Home Assistant 0.67.
+ """
+ path = hass.config.path(filename)
+
+ if not os.path.isfile(path):
+ return None
+
+ with open(path) as inp:
+ try:
+ return list(json.load(inp).values())[0]['username']
+ except ValueError:
+ # If we get invalid JSON
+ return None
+
+
+@config_entries.HANDLERS.register(DOMAIN)
+class HueFlowHandler(config_entries.ConfigFlowHandler):
+ """Handle a Hue config flow."""
+
+ VERSION = 1
+
+ def __init__(self):
+ """Initialize the Hue flow."""
+ self.host = None
+
+ async def async_step_init(self, user_input=None):
+ """Handle a flow start."""
+ from aiohue.discovery import discover_nupnp
+
+ if user_input is not None:
+ self.host = user_input['host']
+ return await self.async_step_link()
+
+ websession = aiohttp_client.async_get_clientsession(self.hass)
+
+ try:
+ with async_timeout.timeout(5):
+ bridges = await discover_nupnp(websession=websession)
+ except asyncio.TimeoutError:
+ return self.async_abort(
+ reason='discover_timeout'
+ )
+
+ if not bridges:
+ return self.async_abort(
+ reason='no_bridges'
+ )
+
+ # Find already configured hosts
+ configured = configured_hosts(self.hass)
+
+ hosts = [bridge.host for bridge in bridges
+ if bridge.host not in configured]
+
+ if not hosts:
+ return self.async_abort(
+ reason='all_configured'
+ )
+
+ elif len(hosts) == 1:
+ self.host = hosts[0]
+ return await self.async_step_link()
+
+ return self.async_show_form(
+ step_id='init',
+ data_schema=vol.Schema({
+ vol.Required('host'): vol.In(hosts)
+ })
+ )
+
+ async def async_step_link(self, user_input=None):
+ """Attempt to link with the Hue bridge.
+
+ Given a configured host, will ask the user to press the link button
+ to connect to the bridge.
+ """
+ errors = {}
+
+ # We will always try linking in case the user has already pressed
+ # the link button.
+ try:
+ bridge = await get_bridge(
+ self.hass, self.host, username=None
+ )
+
+ return await self._entry_from_bridge(bridge)
+ except AuthenticationRequired:
+ errors['base'] = 'register_failed'
+
+ except CannotConnect:
+ LOGGER.error("Error connecting to the Hue bridge at %s", self.host)
+ errors['base'] = 'linking'
+
+ except Exception: # pylint: disable=broad-except
+ LOGGER.exception(
+ 'Unknown error connecting with Hue bridge at %s',
+ self.host)
+ errors['base'] = 'linking'
+
+ # If there was no user input, do not show the errors.
+ if user_input is None:
+ errors = {}
+
+ return self.async_show_form(
+ step_id='link',
+ errors=errors,
+ )
+
+ async def async_step_discovery(self, discovery_info):
+ """Handle a discovered Hue bridge.
+
+ This flow is triggered by the discovery component. It will check if the
+ host is already configured and delegate to the import step if not.
+ """
+ # Filter out emulated Hue
+ if "HASS Bridge" in discovery_info.get('name', ''):
+ return self.async_abort(reason='already_configured')
+
+ host = discovery_info.get('host')
+
+ if host in configured_hosts(self.hass):
+ return self.async_abort(reason='already_configured')
+
+ # This value is based off host/description.xml and is, weirdly, missing
+ # 4 characters in the middle of the serial compared to results returned
+ # from the NUPNP API or when querying the bridge API for bridgeid.
+ # (on first gen Hue hub)
+ serial = discovery_info.get('serial')
+
+ return await self.async_step_import({
+ 'host': host,
+ # This format is the legacy format that Hue used for discovery
+ 'path': 'phue-{}.conf'.format(serial)
+ })
+
+ async def async_step_import(self, import_info):
+ """Import a new bridge as a config entry.
+
+ Will read authentication from Phue config file if available.
+
+ This flow is triggered by `async_setup` for both configured and
+ discovered bridges. Triggered for any bridge that does not have a
+ config entry yet (based on host).
+
+ This flow is also triggered by `async_step_discovery`.
+
+ If an existing config file is found, we will validate the credentials
+ and create an entry. Otherwise we will delegate to `link` step which
+ will ask user to link the bridge.
+ """
+ host = import_info['host']
+ path = import_info.get('path')
+
+ if path is not None:
+ username = await self.hass.async_add_job(
+ _find_username_from_config, self.hass,
+ self.hass.config.path(path))
+ else:
+ username = None
+
+ try:
+ bridge = await get_bridge(
+ self.hass, host, username
+ )
+
+ LOGGER.info('Imported authentication for %s from %s', host, path)
+
+ return await self._entry_from_bridge(bridge)
+ except AuthenticationRequired:
+ self.host = host
+
+ LOGGER.info('Invalid authentication for %s, requesting link.',
+ host)
+
+ return await self.async_step_link()
+
+ except CannotConnect:
+ LOGGER.error("Error connecting to the Hue bridge at %s", host)
+ return self.async_abort(reason='cannot_connect')
+
+ except Exception: # pylint: disable=broad-except
+ LOGGER.exception('Unknown error connecting with Hue bridge at %s',
+ host)
+ return self.async_abort(reason='unknown')
+
+ async def _entry_from_bridge(self, bridge):
+ """Return a config entry from an initialized bridge."""
+ # Remove all other entries of hubs with same ID or host
+ host = bridge.host
+ bridge_id = bridge.config.bridgeid
+
+ same_hub_entries = [entry.entry_id for entry
+ in self.hass.config_entries.async_entries(DOMAIN)
+ if entry.data['bridge_id'] == bridge_id or
+ entry.data['host'] == host]
+
+ if same_hub_entries:
+ await asyncio.wait([self.hass.config_entries.async_remove(entry_id)
+ for entry_id in same_hub_entries])
+
+ return self.async_create_entry(
+ title=bridge.config.name,
+ data={
+ 'host': host,
+ 'bridge_id': bridge_id,
+ 'username': bridge.username,
+ }
+ )
diff --git a/homeassistant/components/hue/const.py b/homeassistant/components/hue/const.py
new file mode 100644
index 00000000000..2eb30d47804
--- /dev/null
+++ b/homeassistant/components/hue/const.py
@@ -0,0 +1,6 @@
+"""Constants for the Hue component."""
+import logging
+
+LOGGER = logging.getLogger('homeassistant.components.hue')
+DOMAIN = "hue"
+API_NUPNP = 'https://www.meethue.com/api/nupnp'
diff --git a/homeassistant/components/hue/errors.py b/homeassistant/components/hue/errors.py
new file mode 100644
index 00000000000..dd217c3bc26
--- /dev/null
+++ b/homeassistant/components/hue/errors.py
@@ -0,0 +1,14 @@
+"""Errors for the Hue component."""
+from homeassistant.exceptions import HomeAssistantError
+
+
+class HueException(HomeAssistantError):
+ """Base class for Hue exceptions."""
+
+
+class CannotConnect(HueException):
+ """Unable to connect to the bridge."""
+
+
+class AuthenticationRequired(HueException):
+ """Unknown error occurred."""
diff --git a/homeassistant/components/hue/strings.json b/homeassistant/components/hue/strings.json
index 59b1ecd3cd1..fc9e91c93d7 100644
--- a/homeassistant/components/hue/strings.json
+++ b/homeassistant/components/hue/strings.json
@@ -20,7 +20,10 @@
"abort": {
"discover_timeout": "Unable to discover Hue bridges",
"no_bridges": "No Philips Hue bridges discovered",
- "all_configured": "All Philips Hue bridges are already configured"
+ "all_configured": "All Philips Hue bridges are already configured",
+ "unknown": "Unknown error occurred",
+ "cannot_connect": "Unable to connect to the bridge",
+ "already_configured": "Bridge is already configured"
}
}
}
diff --git a/homeassistant/components/insteon_plm.py b/homeassistant/components/insteon_plm.py
index 6f5c5223ea0..d867f0c3d28 100644
--- a/homeassistant/components/insteon_plm.py
+++ b/homeassistant/components/insteon_plm.py
@@ -16,7 +16,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
-REQUIREMENTS = ['insteonplm==0.8.3']
+REQUIREMENTS = ['insteonplm==0.8.6']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py
index eea6c821fc0..39d3203795e 100644
--- a/homeassistant/components/light/__init__.py
+++ b/homeassistant/components/light/__init__.py
@@ -457,12 +457,14 @@ class Light(ToggleEntity):
def min_mireds(self):
"""Return the coldest color_temp that this light supports."""
# Default to the Philips Hue value that HA has always assumed
- return 154
+ # https://developers.meethue.com/documentation/core-concepts
+ return 153
@property
def max_mireds(self):
"""Return the warmest color_temp that this light supports."""
# Default to the Philips Hue value that HA has always assumed
+ # https://developers.meethue.com/documentation/core-concepts
return 500
@property
diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py
index 4a54f0a337d..1701b886b68 100644
--- a/homeassistant/components/light/hue.py
+++ b/homeassistant/components/light/hue.py
@@ -300,8 +300,14 @@ class HueLight(Light):
command['transitiontime'] = int(kwargs[ATTR_TRANSITION] * 10)
if ATTR_HS_COLOR in kwargs:
- command['hue'] = int(kwargs[ATTR_HS_COLOR][0] / 360 * 65535)
- command['sat'] = int(kwargs[ATTR_HS_COLOR][1] / 100 * 255)
+ if self.is_osram:
+ command['hue'] = int(kwargs[ATTR_HS_COLOR][0] / 360 * 65535)
+ command['sat'] = int(kwargs[ATTR_HS_COLOR][1] / 100 * 255)
+ else:
+ # Philips hue bulb models respond differently to hue/sat
+ # requests, so we convert to XY first to ensure a consistent
+ # color.
+ command['xy'] = color.color_hs_to_xy(*kwargs[ATTR_HS_COLOR])
elif ATTR_COLOR_TEMP in kwargs:
temp = kwargs[ATTR_COLOR_TEMP]
command['ct'] = max(self.min_mireds, min(temp, self.max_mireds))
diff --git a/homeassistant/components/light/iglo.py b/homeassistant/components/light/iglo.py
index 77e3972968c..f40dc2ce84e 100644
--- a/homeassistant/components/light/iglo.py
+++ b/homeassistant/components/light/iglo.py
@@ -79,7 +79,7 @@ class IGloLamp(Light):
@property
def hs_color(self):
"""Return the hs value."""
- return color_util.color_RGB_to_hsv(*self._lamp.state()['rgb'])
+ return color_util.color_RGB_to_hs(*self._lamp.state()['rgb'])
@property
def effect(self):
diff --git a/homeassistant/components/light/mysensors.py b/homeassistant/components/light/mysensors.py
index 7aa1e754c43..6e41e0f5693 100644
--- a/homeassistant/components/light/mysensors.py
+++ b/homeassistant/components/light/mysensors.py
@@ -15,8 +15,9 @@ import homeassistant.util.color as color_util
SUPPORT_MYSENSORS_RGBW = SUPPORT_COLOR | SUPPORT_WHITE_VALUE
-def setup_platform(hass, config, add_devices, discovery_info=None):
- """Set up the MySensors platform for lights."""
+async def async_setup_platform(
+ hass, config, async_add_devices, discovery_info=None):
+ """Set up the mysensors platform for lights."""
device_class_map = {
'S_DIMMER': MySensorsLightDimmer,
'S_RGB_LIGHT': MySensorsLightRGB,
@@ -24,7 +25,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
}
mysensors.setup_mysensors_platform(
hass, DOMAIN, discovery_info, device_class_map,
- add_devices=add_devices)
+ async_add_devices=async_add_devices)
class MySensorsLight(mysensors.MySensorsEntity, Light):
@@ -140,12 +141,12 @@ class MySensorsLight(mysensors.MySensorsEntity, Light):
self._values[value_type] = STATE_OFF
self.schedule_update_ha_state()
- def _update_light(self):
+ def _async_update_light(self):
"""Update the controller with values from light child."""
value_type = self.gateway.const.SetReq.V_LIGHT
self._state = self._values[value_type] == STATE_ON
- def _update_dimmer(self):
+ def _async_update_dimmer(self):
"""Update the controller with values from dimmer child."""
value_type = self.gateway.const.SetReq.V_DIMMER
if value_type in self._values:
@@ -153,7 +154,7 @@ class MySensorsLight(mysensors.MySensorsEntity, Light):
if self._brightness == 0:
self._state = False
- def _update_rgb_or_w(self):
+ def _async_update_rgb_or_w(self):
"""Update the controller with values from RGB or RGBW child."""
value = self._values[self.value_type]
color_list = rgb_hex_to_rgb_list(value)
@@ -177,11 +178,11 @@ class MySensorsLightDimmer(MySensorsLight):
if self.gateway.optimistic:
self.schedule_update_ha_state()
- def update(self):
+ async def async_update(self):
"""Update the controller with the latest value from a sensor."""
- super().update()
- self._update_light()
- self._update_dimmer()
+ await super().async_update()
+ self._async_update_light()
+ self._async_update_dimmer()
class MySensorsLightRGB(MySensorsLight):
@@ -203,12 +204,12 @@ class MySensorsLightRGB(MySensorsLight):
if self.gateway.optimistic:
self.schedule_update_ha_state()
- def update(self):
+ async def async_update(self):
"""Update the controller with the latest value from a sensor."""
- super().update()
- self._update_light()
- self._update_dimmer()
- self._update_rgb_or_w()
+ await super().async_update()
+ self._async_update_light()
+ self._async_update_dimmer()
+ self._async_update_rgb_or_w()
class MySensorsLightRGBW(MySensorsLightRGB):
diff --git a/homeassistant/components/light/mystrom.py b/homeassistant/components/light/mystrom.py
index d9312e6aadc..8d7fb807c6d 100644
--- a/homeassistant/components/light/mystrom.py
+++ b/homeassistant/components/light/mystrom.py
@@ -15,7 +15,7 @@ from homeassistant.components.light import (
ATTR_HS_COLOR)
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, STATE_UNKNOWN
-REQUIREMENTS = ['python-mystrom==0.3.8']
+REQUIREMENTS = ['python-mystrom==0.4.2']
_LOGGER = logging.getLogger(__name__)
@@ -43,7 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the myStrom Light platform."""
- from pymystrom import MyStromBulb
+ from pymystrom.bulb import MyStromBulb
from pymystrom.exceptions import MyStromConnectionError
host = config.get(CONF_HOST)
diff --git a/homeassistant/components/light/nanoleaf_aurora.py b/homeassistant/components/light/nanoleaf_aurora.py
new file mode 100644
index 00000000000..2a9066bd55f
--- /dev/null
+++ b/homeassistant/components/light/nanoleaf_aurora.py
@@ -0,0 +1,153 @@
+"""
+Support for Nanoleaf Aurora platform.
+
+Based in large parts upon Software-2's ha-aurora and fully
+reliant on Software-2's nanoleaf-aurora Python Library, see
+https://github.com/software-2/ha-aurora as well as
+https://github.com/software-2/nanoleaf
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/light.nanoleaf_aurora/
+"""
+import logging
+
+import voluptuous as vol
+
+from homeassistant.components.light import (
+ ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR,
+ SUPPORT_EFFECT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP,
+ SUPPORT_COLOR, PLATFORM_SCHEMA, Light)
+from homeassistant.const import CONF_HOST, CONF_TOKEN, CONF_NAME
+import homeassistant.helpers.config_validation as cv
+from homeassistant.util import color as color_util
+from homeassistant.util.color import \
+ color_temperature_mired_to_kelvin as mired_to_kelvin
+
+REQUIREMENTS = ['nanoleaf==0.4.1']
+
+SUPPORT_AURORA = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT |
+ SUPPORT_COLOR)
+
+_LOGGER = logging.getLogger(__name__)
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_HOST): cv.string,
+ vol.Required(CONF_TOKEN): cv.string,
+ vol.Optional(CONF_NAME, default='Aurora'): cv.string,
+})
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Setup Nanoleaf Aurora device."""
+ import nanoleaf
+ host = config.get(CONF_HOST)
+ name = config.get(CONF_NAME)
+ token = config.get(CONF_TOKEN)
+ aurora_light = nanoleaf.Aurora(host, token)
+ aurora_light.hass_name = name
+
+ if aurora_light.on is None:
+ _LOGGER.error("Could not connect to \
+ Nanoleaf Aurora: %s on %s", name, host)
+ add_devices([AuroraLight(aurora_light)], True)
+
+
+class AuroraLight(Light):
+ """Representation of a Nanoleaf Aurora."""
+
+ def __init__(self, light):
+ """Initialize an Aurora."""
+ self._brightness = None
+ self._color_temp = None
+ self._effect = None
+ self._effects_list = None
+ self._light = light
+ self._name = light.hass_name
+ self._hs_color = None
+ self._state = None
+
+ @property
+ def brightness(self):
+ """Return the brightness of the light."""
+ if self._brightness is not None:
+ return int(self._brightness * 2.55)
+ return None
+
+ @property
+ def color_temp(self):
+ """Return the current color temperature."""
+ if self._color_temp is not None:
+ return color_util.color_temperature_kelvin_to_mired(
+ self._color_temp)
+ return None
+
+ @property
+ def effect(self):
+ """Return the current effect."""
+ return self._effect
+
+ @property
+ def effect_list(self):
+ """Return the list of supported effects."""
+ return self._effects_list
+
+ @property
+ def name(self):
+ """Return the display name of this light."""
+ return self._name
+
+ @property
+ def icon(self):
+ """Return the icon to use in the frontend, if any."""
+ return "mdi:triangle-outline"
+
+ @property
+ def is_on(self):
+ """Return true if light is on."""
+ return self._state
+
+ @property
+ def hs_color(self):
+ """Return the color in HS."""
+ return self._hs_color
+
+ @property
+ def supported_features(self):
+ """Flag supported features."""
+ return SUPPORT_AURORA
+
+ def turn_on(self, **kwargs):
+ """Instruct the light to turn on."""
+ self._light.on = True
+ brightness = kwargs.get(ATTR_BRIGHTNESS)
+ hs_color = kwargs.get(ATTR_HS_COLOR)
+ color_temp_mired = kwargs.get(ATTR_COLOR_TEMP)
+ effect = kwargs.get(ATTR_EFFECT)
+
+ if hs_color:
+ hue, saturation = hs_color
+ self._light.hue = int(hue)
+ self._light.saturation = int(saturation)
+
+ if color_temp_mired:
+ self._light.color_temperature = mired_to_kelvin(color_temp_mired)
+ if brightness:
+ self._light.brightness = int(brightness / 2.55)
+ if effect:
+ self._light.effect = effect
+
+ def turn_off(self, **kwargs):
+ """Instruct the light to turn off."""
+ self._light.on = False
+
+ def update(self):
+ """Fetch new state data for this light.
+
+ This is the only method that should fetch new data for Home Assistant.
+ """
+ self._brightness = self._light.brightness
+ self._color_temp = self._light.color_temperature
+ self._effect = self._light.effect
+ self._effects_list = self._light.effects_list
+ self._hs_color = self._light.hue, self._light.saturation
+ self._state = self._light.on
diff --git a/homeassistant/components/light/qwikswitch.py b/homeassistant/components/light/qwikswitch.py
index 63051d2ea8c..26741525b8f 100644
--- a/homeassistant/components/light/qwikswitch.py
+++ b/homeassistant/components/light/qwikswitch.py
@@ -4,21 +4,32 @@ Support for Qwikswitch Relays and Dimmers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.qwikswitch/
"""
-import logging
+from homeassistant.components.qwikswitch import (
+ QSToggleEntity, DOMAIN as QWIKSWITCH)
+from homeassistant.components.light import SUPPORT_BRIGHTNESS, Light
-import homeassistant.components.qwikswitch as qwikswitch
-
-_LOGGER = logging.getLogger(__name__)
-
-DEPENDENCIES = ['qwikswitch']
+DEPENDENCIES = [QWIKSWITCH]
-# pylint: disable=unused-argument
-def setup_platform(hass, config, add_devices, discovery_info=None):
- """Set up the lights from the main Qwikswitch component."""
+async def async_setup_platform(hass, _, add_devices, discovery_info=None):
+ """Add lights from the main Qwikswitch component."""
if discovery_info is None:
- _LOGGER.error("Configure Qwikswitch component failed")
- return False
+ return
- add_devices(qwikswitch.QSUSB['light'])
- return True
+ qsusb = hass.data[QWIKSWITCH]
+ devs = [QSLight(qsid, qsusb) for qsid in discovery_info[QWIKSWITCH]]
+ add_devices(devs)
+
+
+class QSLight(QSToggleEntity, Light):
+ """Light based on a Qwikswitch relay/dimmer module."""
+
+ @property
+ def brightness(self):
+ """Return the brightness of this light (0-255)."""
+ return self._qsusb[self.qsid, 1] if self._dim else None
+
+ @property
+ def supported_features(self):
+ """Flag supported features."""
+ return SUPPORT_BRIGHTNESS if self._dim else 0
diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml
index 44e887e62c4..3507c6d2cda 100644
--- a/homeassistant/components/light/services.yaml
+++ b/homeassistant/components/light/services.yaml
@@ -15,6 +15,9 @@ turn_on:
color_name:
description: A human readable color name.
example: 'red'
+ hs_color:
+ description: Color for the light in hue/sat format. Hue is 0-360 and Sat is 0-100.
+ example: '[300, 70]'
xy_color:
description: Color for the light in XY-format.
example: '[0.52, 0.43]'
@@ -179,3 +182,13 @@ xiaomi_miio_set_delayed_turn_off:
time_period:
description: Time period for the delayed turn off.
example: "5, '0:05', {'minutes': 5}"
+
+yeelight_set_mode:
+ description: Set a operation mode.
+ fields:
+ entity_id:
+ description: Name of the light entity.
+ example: 'light.yeelight'
+ mode:
+ description: Operation mode. Valid values are 'last', 'normal', 'rgb', 'hsv', 'color_flow', 'moonlight'.
+ example: 'moonlight'
diff --git a/homeassistant/components/light/tradfri.py b/homeassistant/components/light/tradfri.py
index 1851579a172..95082bb4d19 100644
--- a/homeassistant/components/light/tradfri.py
+++ b/homeassistant/components/light/tradfri.py
@@ -4,11 +4,9 @@ Support for the IKEA Tradfri platform.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.tradfri/
"""
-import asyncio
import logging
from homeassistant.core import callback
-from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION,
SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, SUPPORT_COLOR_TEMP,
@@ -17,20 +15,20 @@ from homeassistant.components.light import \
PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA
from homeassistant.components.tradfri import KEY_GATEWAY, KEY_TRADFRI_GROUPS, \
KEY_API
-from homeassistant.util import color as color_util
+import homeassistant.util.color as color_util
_LOGGER = logging.getLogger(__name__)
+ATTR_TRANSITION_TIME = 'transition_time'
DEPENDENCIES = ['tradfri']
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA
IKEA = 'IKEA of Sweden'
TRADFRI_LIGHT_MANAGER = 'Tradfri Light Manager'
SUPPORTED_FEATURES = (SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION)
-ALLOWED_TEMPERATURES = {IKEA}
-@asyncio.coroutine
-def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
+async def async_setup_platform(hass, config,
+ async_add_devices, discovery_info=None):
"""Set up the IKEA Tradfri Light platform."""
if discovery_info is None:
return
@@ -40,41 +38,43 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
gateway = hass.data[KEY_GATEWAY][gateway_id]
devices_command = gateway.get_devices()
- devices_commands = yield from api(devices_command)
- devices = yield from api(devices_commands)
+ devices_commands = await api(devices_command)
+ devices = await api(devices_commands)
lights = [dev for dev in devices if dev.has_light_control]
if lights:
- async_add_devices(TradfriLight(light, api) for light in lights)
+ async_add_devices(
+ TradfriLight(light, api, gateway_id) for light in lights)
allow_tradfri_groups = hass.data[KEY_TRADFRI_GROUPS][gateway_id]
if allow_tradfri_groups:
groups_command = gateway.get_groups()
- groups_commands = yield from api(groups_command)
- groups = yield from api(groups_commands)
+ groups_commands = await api(groups_command)
+ groups = await api(groups_commands)
if groups:
- async_add_devices(TradfriGroup(group, api) for group in groups)
+ async_add_devices(
+ TradfriGroup(group, api, gateway_id) for group in groups)
class TradfriGroup(Light):
"""The platform class required by hass."""
- def __init__(self, light, api):
+ def __init__(self, group, api, gateway_id):
"""Initialize a Group."""
self._api = api
- self._group = light
- self._name = light.name
+ self._unique_id = "group-{}-{}".format(gateway_id, group.id)
+ self._group = group
+ self._name = group.name
- self._refresh(light)
+ self._refresh(group)
- @asyncio.coroutine
- def async_added_to_hass(self):
+ async def async_added_to_hass(self):
"""Start thread when added to hass."""
self._async_start_observe()
@property
- def should_poll(self):
- """No polling needed for tradfri group."""
- return False
+ def unique_id(self):
+ """Return unique ID for this group."""
+ return self._unique_id
@property
def supported_features(self):
@@ -96,13 +96,11 @@ class TradfriGroup(Light):
"""Return the brightness of the group lights."""
return self._group.dimmer
- @asyncio.coroutine
- def async_turn_off(self, **kwargs):
+ async def async_turn_off(self, **kwargs):
"""Instruct the group lights to turn off."""
- yield from self._api(self._group.set_state(0))
+ await self._api(self._group.set_state(0))
- @asyncio.coroutine
- def async_turn_on(self, **kwargs):
+ async def async_turn_on(self, **kwargs):
"""Instruct the group lights to turn on, or dim."""
keys = {}
if ATTR_TRANSITION in kwargs:
@@ -112,16 +110,16 @@ class TradfriGroup(Light):
if kwargs[ATTR_BRIGHTNESS] == 255:
kwargs[ATTR_BRIGHTNESS] = 254
- yield from self._api(
+ await self._api(
self._group.set_dimmer(kwargs[ATTR_BRIGHTNESS], **keys))
else:
- yield from self._api(self._group.set_state(1))
+ await self._api(self._group.set_state(1))
@callback
def _async_start_observe(self, exc=None):
"""Start observation of light."""
# pylint: disable=import-error
- from pytradfri.error import PyTradFriError
+ from pytradfri.error import PytradfriError
if exc:
_LOGGER.warning("Observation failed for %s", self._name,
exc_info=exc)
@@ -131,7 +129,7 @@ class TradfriGroup(Light):
err_callback=self._async_start_observe,
duration=0)
self.hass.async_add_job(self._api(cmd))
- except PyTradFriError as err:
+ except PytradfriError as err:
_LOGGER.warning("Observation failed, trying again", exc_info=err)
self._async_start_observe()
@@ -146,54 +144,44 @@ class TradfriGroup(Light):
self._refresh(tradfri_device)
self.async_schedule_update_ha_state()
+ async def async_update(self):
+ """Fetch new state data for the group."""
+ await self._api(self._group.update())
+
class TradfriLight(Light):
"""The platform class required by Home Assistant."""
- def __init__(self, light, api):
+ def __init__(self, light, api, gateway_id):
"""Initialize a Light."""
self._api = api
+ self._unique_id = "light-{}-{}".format(gateway_id, light.id)
self._light = None
self._light_control = None
self._light_data = None
self._name = None
self._hs_color = None
self._features = SUPPORTED_FEATURES
- self._temp_supported = False
self._available = True
self._refresh(light)
+ @property
+ def unique_id(self):
+ """Return unique ID for light."""
+ return self._unique_id
+
@property
def min_mireds(self):
"""Return the coldest color_temp that this light supports."""
- if self._light_control.max_kelvin is not None:
- return color_util.color_temperature_kelvin_to_mired(
- self._light_control.max_kelvin
- )
+ return self._light_control.min_mireds
@property
def max_mireds(self):
"""Return the warmest color_temp that this light supports."""
- if self._light_control.min_kelvin is not None:
- return color_util.color_temperature_kelvin_to_mired(
- self._light_control.min_kelvin
- )
+ return self._light_control.max_mireds
- @property
- def device_state_attributes(self):
- """Return the devices' state attributes."""
- info = self._light.device_info
-
- attrs = {}
-
- if info.battery_level is not None:
- attrs[ATTR_BATTERY_LEVEL] = info.battery_level
-
- return attrs
-
- @asyncio.coroutine
- def async_added_to_hass(self):
+ async def async_added_to_hass(self):
"""Start thread when added to hass."""
self._async_start_observe()
@@ -229,64 +217,87 @@ class TradfriLight(Light):
@property
def color_temp(self):
- """Return the CT color value in mireds."""
- kelvin_color = self._light_data.kelvin_color_inferred
- if kelvin_color is not None:
- return color_util.color_temperature_kelvin_to_mired(
- kelvin_color
- )
+ """Return the color temp value in mireds."""
+ return self._light_data.color_temp
@property
def hs_color(self):
"""HS color of the light."""
- return self._hs_color
+ if self._light_control.can_set_color:
+ hsbxy = self._light_data.hsb_xy_color
+ hue = hsbxy[0] / (65535 / 360)
+ sat = hsbxy[1] / (65279 / 100)
+ if hue is not None and sat is not None:
+ return hue, sat
- @asyncio.coroutine
- def async_turn_off(self, **kwargs):
+ async def async_turn_off(self, **kwargs):
"""Instruct the light to turn off."""
- yield from self._api(self._light_control.set_state(False))
+ await self._api(self._light_control.set_state(False))
- @asyncio.coroutine
- def async_turn_on(self, **kwargs):
- """
- Instruct the light to turn on.
-
- After adding "self._light_data.hexcolor is not None"
- for ATTR_HS_COLOR, this also supports Philips Hue bulbs.
- """
- if ATTR_HS_COLOR in kwargs and self._light_data.hex_color is not None:
- rgb = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR])
- yield from self._api(
- self._light.light_control.set_rgb_color(*rgb))
-
- elif ATTR_COLOR_TEMP in kwargs and \
- self._light_data.hex_color is not None and \
- self._temp_supported:
- kelvin = color_util.color_temperature_mired_to_kelvin(
- kwargs[ATTR_COLOR_TEMP])
- yield from self._api(
- self._light_control.set_kelvin_color(kelvin))
-
- keys = {}
+ async def async_turn_on(self, **kwargs):
+ """Instruct the light to turn on."""
+ params = {}
+ transition_time = None
if ATTR_TRANSITION in kwargs:
- keys['transition_time'] = int(kwargs[ATTR_TRANSITION]) * 10
+ transition_time = int(kwargs[ATTR_TRANSITION]) * 10
- if ATTR_BRIGHTNESS in kwargs:
- if kwargs[ATTR_BRIGHTNESS] == 255:
- kwargs[ATTR_BRIGHTNESS] = 254
+ brightness = kwargs.get(ATTR_BRIGHTNESS)
- yield from self._api(
- self._light_control.set_dimmer(kwargs[ATTR_BRIGHTNESS],
- **keys))
+ if brightness is not None:
+ if brightness > 254:
+ brightness = 254
+ elif brightness < 0:
+ brightness = 0
+
+ if ATTR_HS_COLOR in kwargs and self._light_control.can_set_color:
+ params[ATTR_BRIGHTNESS] = brightness
+ hue = int(kwargs[ATTR_HS_COLOR][0] * (65535 / 360))
+ sat = int(kwargs[ATTR_HS_COLOR][1] * (65279 / 100))
+ await self._api(
+ self._light_control.set_hsb(hue, sat, **params))
+ return
+
+ if ATTR_COLOR_TEMP in kwargs and (self._light_control.can_set_temp or
+ self._light_control.can_set_color):
+ temp = kwargs[ATTR_COLOR_TEMP]
+ if temp > self.max_mireds:
+ temp = self.max_mireds
+ elif temp < self.min_mireds:
+ temp = self.min_mireds
+
+ if brightness is None:
+ params[ATTR_TRANSITION_TIME] = transition_time
+ # White Spectrum bulb
+ if (self._light_control.can_set_temp and
+ not self._light_control.can_set_color):
+ await self._api(
+ self._light_control.set_color_temp(temp, **params))
+ # Color bulb (CWS)
+ # color_temp needs to be set with hue/saturation
+ if self._light_control.can_set_color:
+ params[ATTR_BRIGHTNESS] = brightness
+ temp_k = color_util.color_temperature_mired_to_kelvin(temp)
+ hs_color = color_util.color_temperature_to_hs(temp_k)
+ hue = int(hs_color[0] * (65535 / 360))
+ sat = int(hs_color[1] * (65279 / 100))
+ await self._api(
+ self._light_control.set_hsb(hue, sat,
+ **params))
+
+ if brightness is not None:
+ params[ATTR_TRANSITION_TIME] = transition_time
+ await self._api(
+ self._light_control.set_dimmer(brightness,
+ **params))
else:
- yield from self._api(
+ await self._api(
self._light_control.set_state(True))
@callback
def _async_start_observe(self, exc=None):
"""Start observation of light."""
# pylint: disable=import-error
- from pytradfri.error import PyTradFriError
+ from pytradfri.error import PytradfriError
if exc:
_LOGGER.warning("Observation failed for %s", self._name,
exc_info=exc)
@@ -296,7 +307,7 @@ class TradfriLight(Light):
err_callback=self._async_start_observe,
duration=0)
self.hass.async_add_job(self._api(cmd))
- except PyTradFriError as err:
+ except PytradfriError as err:
_LOGGER.warning("Observation failed, trying again", exc_info=err)
self._async_start_observe()
@@ -309,27 +320,15 @@ class TradfriLight(Light):
self._light_control = light.light_control
self._light_data = light.light_control.lights[0]
self._name = light.name
- self._hs_color = None
self._features = SUPPORTED_FEATURES
- if self._light.device_info.manufacturer == IKEA:
- if self._light_control.can_set_kelvin:
- self._features |= SUPPORT_COLOR_TEMP
- if self._light_control.can_set_color:
- self._features |= SUPPORT_COLOR
- else:
- if self._light_data.hex_color is not None:
- self._features |= SUPPORT_COLOR
-
- self._temp_supported = self._light.device_info.manufacturer \
- in ALLOWED_TEMPERATURES
+ if light.light_control.can_set_color:
+ self._features |= SUPPORT_COLOR
+ if light.light_control.can_set_temp:
+ self._features |= SUPPORT_COLOR_TEMP
@callback
def _observe_update(self, tradfri_device):
"""Receive new state data for this light."""
self._refresh(tradfri_device)
- rgb = color_util.rgb_hex_to_rgb_list(
- self._light_data.hex_color_inferred
- )
- self._hs_color = color_util.color_RGB_to_hs(*rgb)
self.async_schedule_update_ha_state()
diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py
index 21a27c33203..24eab7ebd4a 100644
--- a/homeassistant/components/light/xiaomi_miio.py
+++ b/homeassistant/components/light/xiaomi_miio.py
@@ -38,6 +38,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
'philips.light.ceiling',
'philips.light.zyceiling',
'philips.light.bulb',
+ 'philips.light.candle',
'philips.light.candle2']),
})
@@ -149,7 +150,9 @@ async def async_setup_platform(hass, config, async_add_devices,
device = XiaomiPhilipsCeilingLamp(name, light, model, unique_id)
devices.append(device)
hass.data[DATA_KEY][host] = device
- elif model in ['philips.light.bulb', 'philips.light.candle2']:
+ elif model in ['philips.light.bulb',
+ 'philips.light.candle',
+ 'philips.light.candle2']:
from miio import PhilipsBulb
light = PhilipsBulb(host, token)
device = XiaomiPhilipsBulb(name, light, model, unique_id)
diff --git a/homeassistant/components/light/yeelight.py b/homeassistant/components/light/yeelight.py
index 585db950efc..7061c24aac6 100644
--- a/homeassistant/components/light/yeelight.py
+++ b/homeassistant/components/light/yeelight.py
@@ -16,7 +16,7 @@ from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_TRANSITION, ATTR_COLOR_TEMP,
ATTR_FLASH, FLASH_SHORT, FLASH_LONG, ATTR_EFFECT, SUPPORT_BRIGHTNESS,
SUPPORT_COLOR, SUPPORT_TRANSITION, SUPPORT_COLOR_TEMP, SUPPORT_FLASH,
- SUPPORT_EFFECT, Light, PLATFORM_SCHEMA)
+ SUPPORT_EFFECT, Light, PLATFORM_SCHEMA, ATTR_ENTITY_ID, DOMAIN)
import homeassistant.helpers.config_validation as cv
import homeassistant.util.color as color_util
@@ -30,7 +30,7 @@ DEFAULT_TRANSITION = 350
CONF_SAVE_ON_CHANGE = 'save_on_change'
CONF_MODE_MUSIC = 'use_music_mode'
-DOMAIN = 'yeelight'
+DATA_KEY = 'light.yeelight'
DEVICE_SCHEMA = vol.Schema({
vol.Optional(CONF_NAME): cv.string,
@@ -90,6 +90,13 @@ YEELIGHT_EFFECT_LIST = [
EFFECT_TWITTER,
EFFECT_STOP]
+SERVICE_SET_MODE = 'yeelight_set_mode'
+ATTR_MODE = 'mode'
+
+YEELIGHT_SERVICE_SCHEMA = vol.Schema({
+ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
+})
+
def _cmd(func):
"""Define a wrapper to catch exceptions from the bulb."""
@@ -106,6 +113,11 @@ def _cmd(func):
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Yeelight bulbs."""
+ from yeelight.enums import PowerMode
+
+ if DATA_KEY not in hass.data:
+ hass.data[DATA_KEY] = {}
+
lights = []
if discovery_info is not None:
_LOGGER.debug("Adding autodetected %s", discovery_info['hostname'])
@@ -115,16 +127,44 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
discovery_info['properties']['mac'])
device = {'name': name, 'ipaddr': discovery_info['host']}
- lights.append(YeelightLight(device, DEVICE_SCHEMA({})))
+ light = YeelightLight(device, DEVICE_SCHEMA({}))
+ lights.append(light)
+ hass.data[DATA_KEY][name] = light
else:
for ipaddr, device_config in config[CONF_DEVICES].items():
- _LOGGER.debug("Adding configured %s", device_config[CONF_NAME])
+ name = device_config[CONF_NAME]
+ _LOGGER.debug("Adding configured %s", name)
- device = {'name': device_config[CONF_NAME], 'ipaddr': ipaddr}
- lights.append(YeelightLight(device, device_config))
+ device = {'name': name, 'ipaddr': ipaddr}
+ light = YeelightLight(device, device_config)
+ lights.append(light)
+ hass.data[DATA_KEY][name] = light
add_devices(lights, True)
+ def service_handler(service):
+ """Dispatch service calls to target entities."""
+ params = {key: value for key, value in service.data.items()
+ if key != ATTR_ENTITY_ID}
+ entity_ids = service.data.get(ATTR_ENTITY_ID)
+ if entity_ids:
+ target_devices = [dev for dev in hass.data[DATA_KEY].values()
+ if dev.entity_id in entity_ids]
+ else:
+ target_devices = hass.data[DATA_KEY].values()
+
+ for target_device in target_devices:
+ if service.service == SERVICE_SET_MODE:
+ target_device.set_mode(**params)
+
+ service_schema_set_mode = YEELIGHT_SERVICE_SCHEMA.extend({
+ vol.Required(ATTR_MODE):
+ vol.In([mode.name.lower() for mode in PowerMode])
+ })
+ hass.services.register(
+ DOMAIN, SERVICE_SET_MODE, service_handler,
+ schema=service_schema_set_mode)
+
class YeelightLight(Light):
"""Representation of a Yeelight light."""
@@ -444,3 +484,11 @@ class YeelightLight(Light):
self._bulb.turn_off(duration=duration)
except yeelight.BulbException as ex:
_LOGGER.error("Unable to turn the bulb off: %s", ex)
+
+ def set_mode(self, mode: str):
+ """Set a power mode."""
+ import yeelight
+ try:
+ self._bulb.set_power_mode(yeelight.enums.PowerMode[mode.upper()])
+ except yeelight.BulbException as ex:
+ _LOGGER.error("Unable to set the power mode: %s", ex)
diff --git a/homeassistant/components/light/yeelightsunflower.py b/homeassistant/components/light/yeelightsunflower.py
index 88f86063c13..96cce67b1bb 100644
--- a/homeassistant/components/light/yeelightsunflower.py
+++ b/homeassistant/components/light/yeelightsunflower.py
@@ -15,7 +15,7 @@ from homeassistant.components.light import (
from homeassistant.const import CONF_HOST
import homeassistant.util.color as color_util
-REQUIREMENTS = ['yeelightsunflower==0.0.8']
+REQUIREMENTS = ['yeelightsunflower==0.0.10']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/lock/bmw_connected_drive.py b/homeassistant/components/lock/bmw_connected_drive.py
index c500e02b2f7..c992bf1225a 100644
--- a/homeassistant/components/lock/bmw_connected_drive.py
+++ b/homeassistant/components/lock/bmw_connected_drive.py
@@ -37,7 +37,7 @@ class BMWLock(LockDevice):
self._account = account
self._vehicle = vehicle
self._attribute = attribute
- self._name = '{} {}'.format(self._vehicle.modelName, self._attribute)
+ self._name = '{} {}'.format(self._vehicle.name, self._attribute)
self._sensor_name = sensor_name
self._state = None
@@ -59,7 +59,7 @@ class BMWLock(LockDevice):
"""Return the state attributes of the lock."""
vehicle_state = self._vehicle.state
return {
- 'car': self._vehicle.modelName,
+ 'car': self._vehicle.name,
'door_lock_state': vehicle_state.door_lock_state.value
}
@@ -70,7 +70,7 @@ class BMWLock(LockDevice):
def lock(self, **kwargs):
"""Lock the car."""
- _LOGGER.debug("%s: locking doors", self._vehicle.modelName)
+ _LOGGER.debug("%s: locking doors", self._vehicle.name)
# Optimistic state set here because it takes some time before the
# update callback response
self._state = STATE_LOCKED
@@ -79,7 +79,7 @@ class BMWLock(LockDevice):
def unlock(self, **kwargs):
"""Unlock the car."""
- _LOGGER.debug("%s: unlocking doors", self._vehicle.modelName)
+ _LOGGER.debug("%s: unlocking doors", self._vehicle.name)
# Optimistic state set here because it takes some time before the
# update callback response
self._state = STATE_UNLOCKED
@@ -88,13 +88,17 @@ class BMWLock(LockDevice):
def update(self):
"""Update state of the lock."""
- _LOGGER.debug("%s: updating data for %s", self._vehicle.modelName,
+ from bimmer_connected.state import LockState
+
+ _LOGGER.debug("%s: updating data for %s", self._vehicle.name,
self._attribute)
vehicle_state = self._vehicle.state
- # Possible values: LOCKED, SECURED, SELECTIVELOCKED, UNLOCKED
- self._state = (STATE_LOCKED if vehicle_state.door_lock_state.value
- in ('LOCKED', 'SECURED') else STATE_UNLOCKED)
+ # Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED
+ self._state = STATE_LOCKED \
+ if vehicle_state.door_lock_state \
+ in [LockState.LOCKED, LockState.SECURED] \
+ else STATE_UNLOCKED
def update_callback(self):
"""Schedule a state update."""
diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py
index e10a713995b..85c569789a2 100644
--- a/homeassistant/components/media_extractor.py
+++ b/homeassistant/components/media_extractor.py
@@ -14,7 +14,7 @@ from homeassistant.components.media_player import (
SERVICE_PLAY_MEDIA)
from homeassistant.helpers import config_validation as cv
-REQUIREMENTS = ['youtube_dl==2018.03.10']
+REQUIREMENTS = ['youtube_dl==2018.04.03']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py
index 37536bf5586..615c758cd1a 100644
--- a/homeassistant/components/media_player/__init__.py
+++ b/homeassistant/components/media_player/__init__.py
@@ -83,7 +83,8 @@ ATTR_MEDIA_SHUFFLE = 'shuffle'
MEDIA_TYPE_MUSIC = 'music'
MEDIA_TYPE_TVSHOW = 'tvshow'
-MEDIA_TYPE_VIDEO = 'movie'
+MEDIA_TYPE_MOVIE = 'movie'
+MEDIA_TYPE_VIDEO = 'video'
MEDIA_TYPE_EPISODE = 'episode'
MEDIA_TYPE_CHANNEL = 'channel'
MEDIA_TYPE_PLAYLIST = 'playlist'
diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py
index 91b8d362c43..2edda0645b0 100644
--- a/homeassistant/components/media_player/cast.py
+++ b/homeassistant/components/media_player/cast.py
@@ -18,7 +18,7 @@ from homeassistant.core import callback
from homeassistant.helpers.dispatcher import (dispatcher_send,
async_dispatcher_connect)
from homeassistant.components.media_player import (
- MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK,
+ MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK,
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_STOP, SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA)
@@ -517,7 +517,7 @@ class CastDevice(MediaPlayerDevice):
elif self.media_status.media_is_tvshow:
return MEDIA_TYPE_TVSHOW
elif self.media_status.media_is_movie:
- return MEDIA_TYPE_VIDEO
+ return MEDIA_TYPE_MOVIE
elif self.media_status.media_is_musictrack:
return MEDIA_TYPE_MUSIC
return None
diff --git a/homeassistant/components/media_player/channels.py b/homeassistant/components/media_player/channels.py
index 480e5152c8e..6b41ace6ce2 100644
--- a/homeassistant/components/media_player/channels.py
+++ b/homeassistant/components/media_player/channels.py
@@ -10,7 +10,7 @@ import voluptuous as vol
from homeassistant.components.media_player import (
MEDIA_TYPE_CHANNEL, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_EPISODE,
- MEDIA_TYPE_VIDEO, SUPPORT_PLAY, SUPPORT_PAUSE, SUPPORT_STOP,
+ MEDIA_TYPE_MOVIE, SUPPORT_PLAY, SUPPORT_PAUSE, SUPPORT_STOP,
SUPPORT_VOLUME_MUTE, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK,
SUPPORT_PLAY_MEDIA, SUPPORT_SELECT_SOURCE, DOMAIN, PLATFORM_SCHEMA,
MediaPlayerDevice)
@@ -281,7 +281,7 @@ class ChannelsPlayer(MediaPlayerDevice):
if media_type == MEDIA_TYPE_CHANNEL:
response = self.client.play_channel(media_id)
self.update_state(response)
- elif media_type in [MEDIA_TYPE_VIDEO, MEDIA_TYPE_EPISODE,
+ elif media_type in [MEDIA_TYPE_MOVIE, MEDIA_TYPE_EPISODE,
MEDIA_TYPE_TVSHOW]:
response = self.client.play_recording(media_id)
self.update_state(response)
diff --git a/homeassistant/components/media_player/demo.py b/homeassistant/components/media_player/demo.py
index 2be7ad431cf..22fe1d005f7 100644
--- a/homeassistant/components/media_player/demo.py
+++ b/homeassistant/components/media_player/demo.py
@@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/
"""
from homeassistant.components.media_player import (
- MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK,
+ MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK,
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST, SUPPORT_PLAY,
@@ -147,7 +147,7 @@ class DemoYoutubePlayer(AbstractDemoPlayer):
@property
def media_content_type(self):
"""Return the content type of current playing media."""
- return MEDIA_TYPE_VIDEO
+ return MEDIA_TYPE_MOVIE
@property
def media_duration(self):
diff --git a/homeassistant/components/media_player/directv.py b/homeassistant/components/media_player/directv.py
index fae18f03cde..25d13e3017a 100644
--- a/homeassistant/components/media_player/directv.py
+++ b/homeassistant/components/media_player/directv.py
@@ -8,7 +8,7 @@ import voluptuous as vol
import requests
from homeassistant.components.media_player import (
- MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA,
+ MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA,
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_STOP, PLATFORM_SCHEMA,
SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_PLAY,
MediaPlayerDevice)
@@ -154,7 +154,7 @@ class DirecTvDevice(MediaPlayerDevice):
"""Return the content type of current playing media."""
if 'episodeTitle' in self._current:
return MEDIA_TYPE_TVSHOW
- return MEDIA_TYPE_VIDEO
+ return MEDIA_TYPE_MOVIE
@property
def media_channel(self):
diff --git a/homeassistant/components/media_player/emby.py b/homeassistant/components/media_player/emby.py
index 7b5658c56d9..4f9a4019268 100644
--- a/homeassistant/components/media_player/emby.py
+++ b/homeassistant/components/media_player/emby.py
@@ -10,7 +10,7 @@ import logging
import voluptuous as vol
from homeassistant.components.media_player import (
- MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
+ MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE, SUPPORT_SEEK, SUPPORT_STOP, SUPPORT_PREVIOUS_TRACK,
MediaPlayerDevice, SUPPORT_PLAY, PLATFORM_SCHEMA)
from homeassistant.const import (
@@ -231,7 +231,7 @@ class EmbyDevice(MediaPlayerDevice):
if media_type == 'Episode':
return MEDIA_TYPE_TVSHOW
elif media_type == 'Movie':
- return MEDIA_TYPE_VIDEO
+ return MEDIA_TYPE_MOVIE
elif media_type == 'Trailer':
return MEDIA_TYPE_TRAILER
elif media_type == 'Music':
diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py
index 33116258978..9f2a653b8ee 100644
--- a/homeassistant/components/media_player/kodi.py
+++ b/homeassistant/components/media_player/kodi.py
@@ -19,8 +19,8 @@ from homeassistant.components.media_player import (
SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP,
SUPPORT_TURN_OFF, SUPPORT_PLAY, SUPPORT_VOLUME_STEP, SUPPORT_SHUFFLE_SET,
MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW,
- MEDIA_TYPE_VIDEO, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_PLAYLIST,
- MEDIA_PLAYER_SCHEMA, DOMAIN, SUPPORT_TURN_ON)
+ MEDIA_TYPE_MOVIE, MEDIA_TYPE_VIDEO, MEDIA_TYPE_CHANNEL,
+ MEDIA_TYPE_PLAYLIST, MEDIA_PLAYER_SCHEMA, DOMAIN, SUPPORT_TURN_ON)
from homeassistant.const import (
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_HOST, CONF_NAME,
CONF_PORT, CONF_PROXY_SSL, CONF_USERNAME, CONF_PASSWORD,
@@ -67,7 +67,7 @@ MEDIA_TYPES = {
'video': MEDIA_TYPE_VIDEO,
'set': MEDIA_TYPE_PLAYLIST,
'musicvideo': MEDIA_TYPE_VIDEO,
- 'movie': MEDIA_TYPE_VIDEO,
+ 'movie': MEDIA_TYPE_MOVIE,
'tvshow': MEDIA_TYPE_TVSHOW,
'season': MEDIA_TYPE_TVSHOW,
'episode': MEDIA_TYPE_TVSHOW,
diff --git a/homeassistant/components/media_player/liveboxplaytv.py b/homeassistant/components/media_player/liveboxplaytv.py
index 8093f0d3dbe..4fe4da5a942 100644
--- a/homeassistant/components/media_player/liveboxplaytv.py
+++ b/homeassistant/components/media_player/liveboxplaytv.py
@@ -22,7 +22,7 @@ from homeassistant.const import (
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
-REQUIREMENTS = ['liveboxplaytv==2.0.2', 'pyteleloisirs==3.3']
+REQUIREMENTS = ['liveboxplaytv==2.0.2', 'pyteleloisirs==3.4']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/media_player/philips_js.py b/homeassistant/components/media_player/philips_js.py
index 29d336e4d7a..d526fbb0387 100644
--- a/homeassistant/components/media_player/philips_js.py
+++ b/homeassistant/components/media_player/philips_js.py
@@ -19,7 +19,7 @@ from homeassistant.const import (
from homeassistant.helpers.script import Script
from homeassistant.util import Throttle
-REQUIREMENTS = ['ha-philipsjs==0.0.2']
+REQUIREMENTS = ['ha-philipsjs==0.0.3']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py
index edb8aa147fb..6690382846f 100644
--- a/homeassistant/components/media_player/plex.py
+++ b/homeassistant/components/media_player/plex.py
@@ -14,7 +14,7 @@ import voluptuous as vol
from homeassistant import util
from homeassistant.components.media_player import (
- MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, PLATFORM_SCHEMA,
+ MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, PLATFORM_SCHEMA,
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK,
SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
MediaPlayerDevice)
@@ -480,7 +480,7 @@ class PlexClient(MediaPlayerDevice):
self._media_episode = str(self._session.index).zfill(2)
elif self._session_type == 'movie':
- self._media_content_type = MEDIA_TYPE_VIDEO
+ self._media_content_type = MEDIA_TYPE_MOVIE
if self._session.year is not None and \
self._media_title is not None:
self._media_title += ' (' + str(self._session.year) + ')'
@@ -576,7 +576,7 @@ class PlexClient(MediaPlayerDevice):
elif self._session_type == 'episode':
return MEDIA_TYPE_TVSHOW
elif self._session_type == 'movie':
- return MEDIA_TYPE_VIDEO
+ return MEDIA_TYPE_MOVIE
elif self._session_type == 'track':
return MEDIA_TYPE_MUSIC
diff --git a/homeassistant/components/media_player/roku.py b/homeassistant/components/media_player/roku.py
index 15b16eec11b..87129f30db5 100644
--- a/homeassistant/components/media_player/roku.py
+++ b/homeassistant/components/media_player/roku.py
@@ -9,7 +9,7 @@ import logging
import voluptuous as vol
from homeassistant.components.media_player import (
- MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK, SUPPORT_PLAY_MEDIA,
+ MEDIA_TYPE_MOVIE, SUPPORT_NEXT_TRACK, SUPPORT_PLAY_MEDIA,
SUPPORT_PREVIOUS_TRACK, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_SELECT_SOURCE, SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA)
from homeassistant.const import (
@@ -155,7 +155,7 @@ class RokuDevice(MediaPlayerDevice):
return None
elif self.current_app.name == "Roku":
return None
- return MEDIA_TYPE_VIDEO
+ return MEDIA_TYPE_MOVIE
@property
def media_image_url(self):
diff --git a/homeassistant/components/media_player/songpal.py b/homeassistant/components/media_player/songpal.py
index e43f5951db7..955456f2465 100644
--- a/homeassistant/components/media_player/songpal.py
+++ b/homeassistant/components/media_player/songpal.py
@@ -17,7 +17,7 @@ from homeassistant.const import (
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['python-songpal==0.0.6']
+REQUIREMENTS = ['python-songpal==0.0.7']
SUPPORT_SONGPAL = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_STEP | \
SUPPORT_VOLUME_MUTE | SUPPORT_SELECT_SOURCE | \
@@ -101,7 +101,7 @@ class SongpalDevice(MediaPlayerDevice):
import songpal
self._name = name
self.endpoint = endpoint
- self.dev = songpal.Protocol(self.endpoint)
+ self.dev = songpal.Device(self.endpoint)
self._sysinfo = None
self._state = False
diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py
index acd1ffad6eb..ae9d259a47c 100644
--- a/homeassistant/components/media_player/webostv.py
+++ b/homeassistant/components/media_player/webostv.py
@@ -35,6 +35,7 @@ CONF_SOURCES = 'sources'
CONF_ON_ACTION = 'turn_on_action'
DEFAULT_NAME = 'LG webOS Smart TV'
+LIVETV_APP_ID = 'com.webos.app.livetv'
WEBOSTV_CONFIG_FILE = 'webostv.conf'
@@ -357,8 +358,16 @@ class LgWebOSDevice(MediaPlayerDevice):
def media_next_track(self):
"""Send next track command."""
- self._client.fast_forward()
+ current_input = self._client.get_input()
+ if current_input == LIVETV_APP_ID:
+ self._client.channel_up()
+ else:
+ self._client.fast_forward()
def media_previous_track(self):
"""Send the previous track command."""
- self._client.rewind()
+ current_input = self._client.get_input()
+ if current_input == LIVETV_APP_ID:
+ self._client.channel_down()
+ else:
+ self._client.rewind()
diff --git a/homeassistant/components/mercedesme.py b/homeassistant/components/mercedesme.py
deleted file mode 100644
index b809e46ec64..00000000000
--- a/homeassistant/components/mercedesme.py
+++ /dev/null
@@ -1,156 +0,0 @@
-"""
-Support for MercedesME System.
-
-For more details about this component, please refer to the documentation at
-https://home-assistant.io/components/mercedesme/
-"""
-import asyncio
-import logging
-from datetime import timedelta
-
-import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
-
-from homeassistant.const import (
- CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL, LENGTH_KILOMETERS)
-from homeassistant.helpers import discovery
-from homeassistant.helpers.dispatcher import (
- async_dispatcher_connect, dispatcher_send)
-from homeassistant.helpers.entity import Entity
-from homeassistant.helpers.event import track_time_interval
-
-REQUIREMENTS = ['mercedesmejsonpy==0.1.2']
-
-_LOGGER = logging.getLogger(__name__)
-
-BINARY_SENSORS = {
- 'doorsClosed': ['Doors closed'],
- 'windowsClosed': ['Windows closed'],
- 'locked': ['Doors locked'],
- 'tireWarningLight': ['Tire Warning']
-}
-
-SENSORS = {
- 'fuelLevelPercent': ['Fuel Level', '%'],
- 'fuelRangeKm': ['Fuel Range', LENGTH_KILOMETERS],
- 'latestTrip': ['Latest Trip', None],
- 'odometerKm': ['Odometer', LENGTH_KILOMETERS],
- 'serviceIntervalDays': ['Next Service', 'days']
-}
-
-DATA_MME = 'mercedesme'
-DOMAIN = 'mercedesme'
-
-FEATURE_NOT_AVAILABLE = "The feature %s is not available for your car %s"
-
-NOTIFICATION_ID = 'mercedesme_integration_notification'
-NOTIFICATION_TITLE = 'Mercedes me integration setup'
-
-SIGNAL_UPDATE_MERCEDESME = "mercedesme_update"
-
-
-CONFIG_SCHEMA = vol.Schema({
- DOMAIN: vol.Schema({
- vol.Required(CONF_USERNAME): cv.string,
- vol.Required(CONF_PASSWORD): cv.string,
- vol.Optional(CONF_SCAN_INTERVAL, default=30):
- vol.All(cv.positive_int, vol.Clamp(min=10))
- })
-}, extra=vol.ALLOW_EXTRA)
-
-
-def setup(hass, config):
- """Set up MercedesMe System."""
- from mercedesmejsonpy.controller import Controller
- from mercedesmejsonpy import Exceptions
-
- conf = config[DOMAIN]
- username = conf.get(CONF_USERNAME)
- password = conf.get(CONF_PASSWORD)
- scan_interval = conf.get(CONF_SCAN_INTERVAL)
-
- try:
- mercedesme_api = Controller(username, password, scan_interval)
- if not mercedesme_api.is_valid_session:
- raise Exceptions.MercedesMeException(500)
- hass.data[DATA_MME] = MercedesMeHub(mercedesme_api)
- except Exceptions.MercedesMeException as ex:
- if ex.code == 401:
- hass.components.persistent_notification.create(
- "Error:
Please check username and password."
- "You will need to restart Home Assistant after fixing.",
- title=NOTIFICATION_TITLE,
- notification_id=NOTIFICATION_ID)
- else:
- hass.components.persistent_notification.create(
- "Error:
Can't communicate with Mercedes me API.
"
- "Error code: {} Reason: {}"
- "You will need to restart Home Assistant after fixing."
- "".format(ex.code, ex.message),
- title=NOTIFICATION_TITLE,
- notification_id=NOTIFICATION_ID)
-
- _LOGGER.error("Unable to communicate with Mercedes me API: %s",
- ex.message)
- return False
-
- discovery.load_platform(hass, 'sensor', DOMAIN, {}, config)
- discovery.load_platform(hass, 'device_tracker', DOMAIN, {}, config)
- discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config)
-
- def hub_refresh(event_time):
- """Call Mercedes me API to refresh information."""
- _LOGGER.info("Updating Mercedes me component.")
- hass.data[DATA_MME].data.update()
- dispatcher_send(hass, SIGNAL_UPDATE_MERCEDESME)
-
- track_time_interval(
- hass,
- hub_refresh,
- timedelta(seconds=scan_interval))
-
- return True
-
-
-class MercedesMeHub(object):
- """Representation of a base MercedesMe device."""
-
- def __init__(self, data):
- """Initialize the entity."""
- self.data = data
-
-
-class MercedesMeEntity(Entity):
- """Entity class for MercedesMe devices."""
-
- def __init__(self, data, internal_name, sensor_name, vin, unit):
- """Initialize the MercedesMe entity."""
- self._car = None
- self._data = data
- self._state = False
- self._name = sensor_name
- self._internal_name = internal_name
- self._unit = unit
- self._vin = vin
-
- @property
- def name(self):
- """Return the name of the sensor."""
- return self._name
-
- @asyncio.coroutine
- def async_added_to_hass(self):
- """Register callbacks."""
- async_dispatcher_connect(
- self.hass, SIGNAL_UPDATE_MERCEDESME, self._update_callback)
-
- def _update_callback(self):
- """Callback update method."""
- # If the method is made a callback this should be changed
- # to the async version. Check core.callback
- self.schedule_update_ha_state(True)
-
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement."""
- return self._unit
diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py
index 3263521f3f1..d5a3b4a2efb 100644
--- a/homeassistant/components/mqtt/discovery.py
+++ b/homeassistant/components/mqtt/discovery.py
@@ -20,10 +20,12 @@ TOPIC_MATCHER = re.compile(
r'(?:(?P[a-zA-Z0-9_-]+)/)?(?P[a-zA-Z0-9_-]+)/config')
SUPPORTED_COMPONENTS = [
- 'binary_sensor', 'cover', 'fan', 'light', 'sensor', 'switch', 'lock']
+ 'binary_sensor', 'camera', 'cover', 'fan',
+ 'light', 'sensor', 'switch', 'lock']
ALLOWED_PLATFORMS = {
'binary_sensor': ['mqtt'],
+ 'camera': ['mqtt'],
'cover': ['mqtt'],
'fan': ['mqtt'],
'light': ['mqtt', 'mqtt_json', 'mqtt_template'],
diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py
index a560b49648f..17c9129a31d 100644
--- a/homeassistant/components/mysensors.py
+++ b/homeassistant/components/mysensors.py
@@ -4,7 +4,6 @@ Connect to a MySensors gateway via pymysensors API.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/mysensors/
"""
-import asyncio
from collections import defaultdict
import logging
import os
@@ -19,6 +18,7 @@ from homeassistant.components.mqtt import (
from homeassistant.const import (
ATTR_BATTERY_LEVEL, CONF_NAME, CONF_OPTIMISTIC, EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP, STATE_OFF, STATE_ON)
+from homeassistant.core import callback
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import (
@@ -301,9 +301,9 @@ def setup(hass, config):
"""Call MQTT publish function."""
mqtt.publish(hass, topic, payload, qos, retain)
- def sub_callback(topic, callback, qos):
+ def sub_callback(topic, sub_cb, qos):
"""Call MQTT subscribe function."""
- mqtt.subscribe(hass, topic, callback, qos)
+ mqtt.subscribe(hass, topic, sub_cb, qos)
gateway = mysensors.MQTTGateway(
pub_callback, sub_callback,
event_callback=None, persistence=persistence,
@@ -518,11 +518,12 @@ def get_mysensors_gateway(hass, gateway_id):
return gateways.get(gateway_id)
+@callback
def setup_mysensors_platform(
hass, domain, discovery_info, device_class, device_args=None,
- add_devices=None):
+ async_add_devices=None):
"""Set up a MySensors platform."""
- # Only act if called via MySensors by discovery event.
+ # Only act if called via mysensors by discovery event.
# Otherwise gateway is not setup.
if not discovery_info:
return
@@ -551,8 +552,8 @@ def setup_mysensors_platform(
new_devices.append(devices[dev_id])
if new_devices:
_LOGGER.info("Adding new devices: %s", new_devices)
- if add_devices is not None:
- add_devices(new_devices, True)
+ if async_add_devices is not None:
+ async_add_devices(new_devices, True)
return new_devices
@@ -595,7 +596,7 @@ class MySensorsDevice(object):
return attr
- def update(self):
+ async def async_update(self):
"""Update the controller with the latest value from a sensor."""
node = self.gateway.sensors[self.node_id]
child = node.children[self.child_id]
@@ -627,14 +628,14 @@ class MySensorsEntity(MySensorsDevice, Entity):
"""Return true if entity is available."""
return self.value_type in self._values
- def _async_update_callback(self):
+ @callback
+ def async_update_callback(self):
"""Update the entity."""
self.async_schedule_update_ha_state(True)
- @asyncio.coroutine
- def async_added_to_hass(self):
+ async def async_added_to_hass(self):
"""Register update callback."""
dev_id = id(self.gateway), self.node_id, self.child_id, self.value_type
async_dispatcher_connect(
self.hass, SIGNAL_CALLBACK.format(*dev_id),
- self._async_update_callback)
+ self.async_update_callback)
diff --git a/homeassistant/components/notify/mastodon.py b/homeassistant/components/notify/mastodon.py
new file mode 100644
index 00000000000..3ba95407fec
--- /dev/null
+++ b/homeassistant/components/notify/mastodon.py
@@ -0,0 +1,70 @@
+"""
+Mastodon platform for notify component.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/notify.mastodon/
+"""
+import logging
+
+import voluptuous as vol
+
+from homeassistant.components.notify import (
+ PLATFORM_SCHEMA, BaseNotificationService)
+from homeassistant.const import CONF_ACCESS_TOKEN
+import homeassistant.helpers.config_validation as cv
+
+REQUIREMENTS = ['Mastodon.py==1.2.2']
+
+_LOGGER = logging.getLogger(__name__)
+
+CONF_BASE_URL = 'base_url'
+CONF_CLIENT_ID = 'client_id'
+CONF_CLIENT_SECRET = 'client_secret'
+
+DEFAULT_URL = 'https://mastodon.social'
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_ACCESS_TOKEN): cv.string,
+ vol.Required(CONF_CLIENT_ID): cv.string,
+ vol.Required(CONF_CLIENT_SECRET): cv.string,
+ vol.Optional(CONF_BASE_URL, default=DEFAULT_URL): cv.string,
+})
+
+
+def get_service(hass, config, discovery_info=None):
+ """Get the Mastodon notification service."""
+ from mastodon import Mastodon
+ from mastodon.Mastodon import MastodonUnauthorizedError
+
+ client_id = config.get(CONF_CLIENT_ID)
+ client_secret = config.get(CONF_CLIENT_SECRET)
+ access_token = config.get(CONF_ACCESS_TOKEN)
+ base_url = config.get(CONF_BASE_URL)
+
+ try:
+ mastodon = Mastodon(
+ client_id=client_id, client_secret=client_secret,
+ access_token=access_token, api_base_url=base_url)
+ mastodon.account_verify_credentials()
+ except MastodonUnauthorizedError:
+ _LOGGER.warning("Authentication failed")
+ return None
+
+ return MastodonNotificationService(mastodon)
+
+
+class MastodonNotificationService(BaseNotificationService):
+ """Implement the notification service for Mastodon."""
+
+ def __init__(self, api):
+ """Initialize the service."""
+ self._api = api
+
+ def send_message(self, message="", **kwargs):
+ """Send a message to a user."""
+ from mastodon.Mastodon import MastodonAPIError
+
+ try:
+ self._api.toot(message)
+ except MastodonAPIError:
+ _LOGGER.error("Unable to send message")
diff --git a/homeassistant/components/notify/mysensors.py b/homeassistant/components/notify/mysensors.py
index 8ae697048f5..257b5995446 100644
--- a/homeassistant/components/notify/mysensors.py
+++ b/homeassistant/components/notify/mysensors.py
@@ -9,12 +9,12 @@ from homeassistant.components.notify import (
ATTR_TARGET, DOMAIN, BaseNotificationService)
-def get_service(hass, config, discovery_info=None):
+async def async_get_service(hass, config, discovery_info=None):
"""Get the MySensors notification service."""
new_devices = mysensors.setup_mysensors_platform(
hass, DOMAIN, discovery_info, MySensorsNotificationDevice)
if not new_devices:
- return
+ return None
return MySensorsNotificationService(hass)
diff --git a/homeassistant/components/notify/rest.py b/homeassistant/components/notify/rest.py
index 73618c19502..40b09dc3c72 100644
--- a/homeassistant/components/notify/rest.py
+++ b/homeassistant/components/notify/rest.py
@@ -12,7 +12,8 @@ import voluptuous as vol
from homeassistant.components.notify import (
ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, BaseNotificationService,
PLATFORM_SCHEMA)
-from homeassistant.const import (CONF_RESOURCE, CONF_METHOD, CONF_NAME)
+from homeassistant.const import (CONF_RESOURCE, CONF_METHOD, CONF_NAME,
+ CONF_HEADERS)
import homeassistant.helpers.config_validation as cv
CONF_DATA = 'data'
@@ -29,6 +30,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
default=DEFAULT_MESSAGE_PARAM_NAME): cv.string,
vol.Optional(CONF_METHOD, default=DEFAULT_METHOD):
vol.In(['POST', 'GET', 'POST_JSON']),
+ vol.Optional(CONF_HEADERS): vol.Schema({cv.string: cv.string}),
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_TARGET_PARAMETER_NAME): cv.string,
vol.Optional(CONF_TITLE_PARAMETER_NAME): cv.string,
@@ -43,6 +45,7 @@ def get_service(hass, config, discovery_info=None):
"""Get the RESTful notification service."""
resource = config.get(CONF_RESOURCE)
method = config.get(CONF_METHOD)
+ headers = config.get(CONF_HEADERS)
message_param_name = config.get(CONF_MESSAGE_PARAMETER_NAME)
title_param_name = config.get(CONF_TITLE_PARAMETER_NAME)
target_param_name = config.get(CONF_TARGET_PARAMETER_NAME)
@@ -50,19 +53,20 @@ def get_service(hass, config, discovery_info=None):
data_template = config.get(CONF_DATA_TEMPLATE)
return RestNotificationService(
- hass, resource, method, message_param_name,
+ hass, resource, method, headers, message_param_name,
title_param_name, target_param_name, data, data_template)
class RestNotificationService(BaseNotificationService):
"""Implementation of a notification service for REST."""
- def __init__(self, hass, resource, method, message_param_name,
+ def __init__(self, hass, resource, method, headers, message_param_name,
title_param_name, target_param_name, data, data_template):
"""Initialize the service."""
self._resource = resource
self._hass = hass
self._method = method.upper()
+ self._headers = headers
self._message_param_name = message_param_name
self._title_param_name = title_param_name
self._target_param_name = target_param_name
@@ -99,11 +103,14 @@ class RestNotificationService(BaseNotificationService):
data.update(_data_template_creator(self._data_template))
if self._method == 'POST':
- response = requests.post(self._resource, data=data, timeout=10)
+ response = requests.post(self._resource, headers=self._headers,
+ data=data, timeout=10)
elif self._method == 'POST_JSON':
- response = requests.post(self._resource, json=data, timeout=10)
+ response = requests.post(self._resource, headers=self._headers,
+ json=data, timeout=10)
else: # default GET
- response = requests.get(self._resource, params=data, timeout=10)
+ response = requests.get(self._resource, headers=self._headers,
+ params=data, timeout=10)
if response.status_code not in (200, 201):
_LOGGER.exception(
diff --git a/homeassistant/components/notify/slack.py b/homeassistant/components/notify/slack.py
index 30aadfc8297..b50260e4c61 100644
--- a/homeassistant/components/notify/slack.py
+++ b/homeassistant/components/notify/slack.py
@@ -17,7 +17,7 @@ from homeassistant.components.notify import (
BaseNotificationService)
from homeassistant.const import (CONF_API_KEY, CONF_USERNAME, CONF_ICON)
-REQUIREMENTS = ['slacker==0.9.60']
+REQUIREMENTS = ['slacker==0.9.65']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/pilight.py b/homeassistant/components/pilight.py
index 71e8232e8c2..344c750c0ec 100644
--- a/homeassistant/components/pilight.py
+++ b/homeassistant/components/pilight.py
@@ -26,7 +26,7 @@ _LOGGER = logging.getLogger(__name__)
CONF_SEND_DELAY = 'send_delay'
DEFAULT_HOST = '127.0.0.1'
-DEFAULT_PORT = 5000
+DEFAULT_PORT = 5001
DEFAULT_SEND_DELAY = 0.0
DOMAIN = 'pilight'
diff --git a/homeassistant/components/qwikswitch.py b/homeassistant/components/qwikswitch.py
index 4d5f27082de..708eff7cf11 100644
--- a/homeassistant/components/qwikswitch.py
+++ b/homeassistant/components/qwikswitch.py
@@ -9,13 +9,16 @@ import logging
import voluptuous as vol
from homeassistant.const import (
- EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_URL)
+ EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_URL,
+ CONF_SENSORS, CONF_SWITCHES)
+from homeassistant.core import callback
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.discovery import load_platform
-from homeassistant.components.light import (
- ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light)
-from homeassistant.components.switch import SwitchDevice
+from homeassistant.helpers.entity import Entity
+from homeassistant.components.light import ATTR_BRIGHTNESS
+import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['pyqwikswitch==0.4']
+REQUIREMENTS = ['pyqwikswitch==0.6']
_LOGGER = logging.getLogger(__name__)
@@ -30,15 +33,14 @@ CONFIG_SCHEMA = vol.Schema({
vol.Required(CONF_URL, default='http://127.0.0.1:2020'):
vol.Coerce(str),
vol.Optional(CONF_DIMMER_ADJUST, default=1): CV_DIM_VALUE,
- vol.Optional(CONF_BUTTON_EVENTS): vol.Coerce(str)
+ vol.Optional(CONF_BUTTON_EVENTS, default=[]): cv.ensure_list_csv,
+ vol.Optional(CONF_SENSORS, default={}): vol.Schema({cv.slug: str}),
+ vol.Optional(CONF_SWITCHES, default=[]): vol.All(
+ cv.ensure_list, [str])
})}, extra=vol.ALLOW_EXTRA)
-QSUSB = {}
-SUPPORT_QWIKSWITCH = SUPPORT_BRIGHTNESS
-
-
-class QSToggleEntity(object):
+class QSToggleEntity(Entity):
"""Representation of a Qwikswitch Entity.
Implement base QS methods. Modeled around HA ToggleEntity[1] & should only
@@ -53,22 +55,15 @@ class QSToggleEntity(object):
[3] /components/switch/__init__.py
"""
- def __init__(self, qsitem, qsusb):
+ def __init__(self, qsid, qsusb):
"""Initialize the ToggleEntity."""
- from pyqwikswitch import (QS_ID, QS_NAME, QSType, PQS_VALUE, PQS_TYPE)
- self._id = qsitem[QS_ID]
- self._name = qsitem[QS_NAME]
- self._value = qsitem[PQS_VALUE]
- self._qsusb = qsusb
- self._dim = qsitem[PQS_TYPE] == QSType.dimmer
- QSUSB[self._id] = self
+ from pyqwikswitch import (QS_NAME, QSDATA, QS_TYPE, QSType)
+ self.qsid = qsid
+ self._qsusb = qsusb.devices
+ dev = qsusb.devices[qsid]
+ self._dim = dev[QS_TYPE] == QSType.dimmer
+ self._name = dev[QSDATA][QS_NAME]
- @property
- def brightness(self):
- """Return the brightness of this light between 0..100."""
- return self._value if self._dim else None
-
- # pylint: disable=no-self-use
@property
def should_poll(self):
"""No polling needed."""
@@ -82,115 +77,113 @@ class QSToggleEntity(object):
@property
def is_on(self):
"""Check if device is on (non-zero)."""
- return self._value > 0
+ return self._qsusb[self.qsid, 1] > 0
- def update_value(self, value):
- """Decode the QSUSB value and update the Home assistant state."""
- if value != self._value:
- self._value = value
- # pylint: disable=no-member
- super().schedule_update_ha_state() # Part of Entity/ToggleEntity
- return self._value
-
- def turn_on(self, **kwargs):
+ async def async_turn_on(self, **kwargs):
"""Turn the device on."""
- newvalue = 255
- if ATTR_BRIGHTNESS in kwargs:
- newvalue = kwargs[ATTR_BRIGHTNESS]
- if self._qsusb.set(self._id, round(min(newvalue, 255)/2.55)) >= 0:
- self.update_value(newvalue)
+ new = kwargs.get(ATTR_BRIGHTNESS, 255)
+ self._qsusb.set_value(self.qsid, new)
- # pylint: disable=unused-argument
- def turn_off(self, **kwargs):
+ async def async_turn_off(self, **_):
"""Turn the device off."""
- if self._qsusb.set(self._id, 0) >= 0:
- self.update_value(0)
+ self._qsusb.set_value(self.qsid, 0)
+
+ def _update(self, _packet=None):
+ """Schedule an update - match dispather_send signature."""
+ self.async_schedule_update_ha_state()
+
+ async def async_added_to_hass(self):
+ """Listen for updates from QSUSb via dispatcher."""
+ self.hass.helpers.dispatcher.async_dispatcher_connect(
+ self.qsid, self._update)
-class QSSwitch(QSToggleEntity, SwitchDevice):
- """Switch based on a Qwikswitch relay module."""
-
- pass
-
-
-class QSLight(QSToggleEntity, Light):
- """Light based on a Qwikswitch relay/dimmer module."""
-
- @property
- def supported_features(self):
- """Flag supported features."""
- return SUPPORT_QWIKSWITCH
-
-
-def setup(hass, config):
- """Set up the QSUSB component."""
+async def async_setup(hass, config):
+ """Qwiskswitch component setup."""
+ from pyqwikswitch.async_ import QSUsb
from pyqwikswitch import (
- QSUsb, CMD_BUTTONS, QS_NAME, QS_ID, QS_CMD, PQS_VALUE, PQS_TYPE,
- QSType)
+ CMD_BUTTONS, QS_CMD, QS_ID, QS_TYPE, QSType)
- # Override which cmd's in /&listen packets will fire events
+ # Add cmd's to in /&listen packets will fire events
# By default only buttons of type [TOGGLE,SCENE EXE,LEVEL]
- cmd_buttons = config[DOMAIN].get(CONF_BUTTON_EVENTS, ','.join(CMD_BUTTONS))
- cmd_buttons = cmd_buttons.split(',')
+ cmd_buttons = set(CMD_BUTTONS)
+ for btn in config[DOMAIN][CONF_BUTTON_EVENTS]:
+ cmd_buttons.add(btn)
url = config[DOMAIN][CONF_URL]
dimmer_adjust = config[DOMAIN][CONF_DIMMER_ADJUST]
+ sensors = config[DOMAIN]['sensors']
+ switches = config[DOMAIN]['switches']
- qsusb = QSUsb(url, _LOGGER, dimmer_adjust)
+ def callback_value_changed(_qsd, qsid, _val):
+ """Update entity values based on device change."""
+ _LOGGER.debug("Dispatch %s (update from devices)", qsid)
+ hass.helpers.dispatcher.async_dispatcher_send(qsid, None)
- def _stop(event):
- """Stop the listener queue and clean up."""
- nonlocal qsusb
- qsusb.stop()
- qsusb = None
- global QSUSB
- QSUSB = {}
- _LOGGER.info("Waiting for long poll to QSUSB to time out")
-
- hass.bus.listen(EVENT_HOMEASSISTANT_STOP, _stop)
+ session = async_get_clientsession(hass)
+ qsusb = QSUsb(url=url, dim_adj=dimmer_adjust, session=session,
+ callback_value_changed=callback_value_changed)
# Discover all devices in QSUSB
- devices = qsusb.devices()
- QSUSB['switch'] = []
- QSUSB['light'] = []
- for item in devices:
- if item[PQS_TYPE] == QSType.relay and (item[QS_NAME].lower()
- .endswith(' switch')):
- item[QS_NAME] = item[QS_NAME][:-7] # Remove ' switch' postfix
- QSUSB['switch'].append(QSSwitch(item, qsusb))
- elif item[PQS_TYPE] in [QSType.relay, QSType.dimmer]:
- QSUSB['light'].append(QSLight(item, qsusb))
+ if not await qsusb.update_from_devices():
+ return False
+
+ hass.data[DOMAIN] = qsusb
+
+ _new = {'switch': [], 'light': [], 'sensor': sensors}
+ for _id, item in qsusb.devices:
+ if _id in switches:
+ if item[QS_TYPE] != QSType.relay:
+ _LOGGER.warning(
+ "You specified a switch that is not a relay %s", _id)
+ continue
+ _new['switch'].append(_id)
+ elif item[QS_TYPE] in [QSType.relay, QSType.dimmer]:
+ _new['light'].append(_id)
else:
_LOGGER.warning("Ignored unknown QSUSB device: %s", item)
+ continue
# Load platforms
- for comp_name in ('switch', 'light'):
- if QSUSB[comp_name]:
- load_platform(hass, comp_name, 'qwikswitch', {}, config)
+ for comp_name, comp_conf in _new.items():
+ if comp_conf:
+ load_platform(hass, comp_name, DOMAIN, {DOMAIN: comp_conf}, config)
- def qs_callback(item):
+ def callback_qs_listen(item):
"""Typically a button press or update signal."""
- if qsusb is None: # Shutting down
- _LOGGER.info("Button press or updating signal done")
- return
-
# If button pressed, fire a hass event
- if item.get(QS_CMD, '') in cmd_buttons:
- hass.bus.fire('qwikswitch.button.' + item.get(QS_ID, '@no_id'))
- return
+ if QS_ID in item:
+ if item.get(QS_CMD, '') in cmd_buttons:
+ hass.bus.async_fire(
+ 'qwikswitch.button.{}'.format(item[QS_ID]), item)
+ return
+
+ # Private method due to bad __iter__ design in qsusb
+ # qsusb.devices returns a list of tuples
+ if item[QS_ID] not in \
+ qsusb.devices._data: # pylint: disable=protected-access
+ # Not a standard device in, component can handle packet
+ # i.e. sensors
+ _LOGGER.debug("Dispatch %s ((%s))", item[QS_ID], item)
+ hass.helpers.dispatcher.async_dispatcher_send(
+ item[QS_ID], item)
# Update all ha_objects
- qsreply = qsusb.devices()
- if qsreply is False:
- return
- for itm in qsreply:
- if itm[QS_ID] in QSUSB:
- QSUSB[itm[QS_ID]].update_value(
- round(min(itm[PQS_VALUE], 100) * 2.55))
+ hass.async_add_job(qsusb.update_from_devices)
- def _start(event):
+ @callback
+ def async_start(_):
"""Start listening."""
- qsusb.listen(callback=qs_callback, timeout=30)
- hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start)
+ hass.async_add_job(qsusb.listen, callback_qs_listen)
+
+ hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, async_start)
+
+ @callback
+ def async_stop(_):
+ """Stop the listener queue and clean up."""
+ hass.data[DOMAIN].stop()
+ _LOGGER.info("Waiting for long poll to QSUSB to time out (max 30sec)")
+
+ hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, async_stop)
return True
diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py
index b71eb2cb447..e731d421e69 100644
--- a/homeassistant/components/remote/xiaomi_miio.py
+++ b/homeassistant/components/remote/xiaomi_miio.py
@@ -138,7 +138,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
while (utcnow() - start_time) < timedelta(seconds=timeout):
message = yield from hass.async_add_job(
device.read, slot)
- _LOGGER.debug("Message recieved from device: '%s'", message)
+ _LOGGER.debug("Message received from device: '%s'", message)
if 'code' in message and message['code']:
log_msg = "Received command is: {}".format(message['code'])
diff --git a/homeassistant/components/rflink.py b/homeassistant/components/rflink.py
index 439f938beb3..87e2a7a2331 100644
--- a/homeassistant/components/rflink.py
+++ b/homeassistant/components/rflink.py
@@ -22,7 +22,7 @@ from homeassistant.helpers.deprecation import get_deprecated
from homeassistant.helpers.entity import Entity
-REQUIREMENTS = ['rflink==0.0.34']
+REQUIREMENTS = ['rflink==0.0.37']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py
index e7301836d7e..d6873a0bd91 100644
--- a/homeassistant/components/rfxtrx.py
+++ b/homeassistant/components/rfxtrx.py
@@ -20,7 +20,7 @@ from homeassistant.const import (
)
from homeassistant.helpers.entity import Entity
-REQUIREMENTS = ['pyRFXtrx==0.21.1']
+REQUIREMENTS = ['pyRFXtrx==0.22.0']
DOMAIN = 'rfxtrx'
diff --git a/homeassistant/components/sensor/bmw_connected_drive.py b/homeassistant/components/sensor/bmw_connected_drive.py
index 3208c7377df..bd582da1ef4 100644
--- a/homeassistant/components/sensor/bmw_connected_drive.py
+++ b/homeassistant/components/sensor/bmw_connected_drive.py
@@ -4,8 +4,8 @@ Reads vehicle status from BMW connected drive portal.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.bmw_connected_drive/
"""
-import logging
import asyncio
+import logging
from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN
from homeassistant.helpers.entity import Entity
@@ -51,7 +51,7 @@ class BMWConnectedDriveSensor(Entity):
self._attribute = attribute
self._state = None
self._unit_of_measurement = None
- self._name = '{} {}'.format(self._vehicle.modelName, self._attribute)
+ self._name = '{} {}'.format(self._vehicle.name, self._attribute)
self._sensor_name = sensor_name
self._icon = icon
@@ -88,19 +88,19 @@ class BMWConnectedDriveSensor(Entity):
def device_state_attributes(self):
"""Return the state attributes of the binary sensor."""
return {
- 'car': self._vehicle.modelName
+ 'car': self._vehicle.name
}
def update(self) -> None:
"""Read new state data from the library."""
- _LOGGER.debug('Updating %s', self._vehicle.modelName)
+ _LOGGER.debug('Updating %s', self._vehicle.name)
vehicle_state = self._vehicle.state
self._state = getattr(vehicle_state, self._attribute)
if self._attribute in LENGTH_ATTRIBUTES:
- self._unit_of_measurement = vehicle_state.unit_of_length
+ self._unit_of_measurement = 'km'
elif self._attribute == 'remaining_fuel':
- self._unit_of_measurement = vehicle_state.unit_of_volume
+ self._unit_of_measurement = 'l'
else:
self._unit_of_measurement = None
diff --git a/homeassistant/components/sensor/broadlink.py b/homeassistant/components/sensor/broadlink.py
index 47cefe50aec..044b77ebfe8 100644
--- a/homeassistant/components/sensor/broadlink.py
+++ b/homeassistant/components/sensor/broadlink.py
@@ -19,9 +19,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = [
- 'https://github.com/balloob/python-broadlink/archive/'
- '3580ff2eaccd267846f14246d6ede6e30671f7c6.zip#broadlink==0.5.1']
+REQUIREMENTS = ['broadlink==0.8.0']
_LOGGER = logging.getLogger(__name__)
@@ -108,7 +106,7 @@ class BroadlinkData(object):
"""Initialize the data object."""
import broadlink
self.data = None
- self._device = broadlink.a1((ip_addr, 80), mac_addr)
+ self._device = broadlink.a1((ip_addr, 80), mac_addr, None)
self._device.timeout = timeout
self._schema = vol.Schema({
vol.Optional('temperature'): vol.Range(min=-50, max=150),
diff --git a/homeassistant/components/sensor/canary.py b/homeassistant/components/sensor/canary.py
index ded8f36203e..51fe1d4dd7a 100644
--- a/homeassistant/components/sensor/canary.py
+++ b/homeassistant/components/sensor/canary.py
@@ -8,6 +8,7 @@ https://home-assistant.io/components/sensor.canary/
from homeassistant.components.canary import DATA_CANARY
from homeassistant.const import TEMP_CELSIUS
from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.icon import icon_for_battery_level
DEPENDENCIES = ['canary']
@@ -17,9 +18,11 @@ ATTR_AIR_QUALITY = "air_quality"
# Sensor types are defined like so:
# sensor type name, unit_of_measurement, icon
SENSOR_TYPES = [
- ["temperature", TEMP_CELSIUS, "mdi:thermometer"],
- ["humidity", "%", "mdi:water-percent"],
- ["air_quality", None, "mdi:weather-windy"],
+ ["temperature", TEMP_CELSIUS, "mdi:thermometer", ["Canary"]],
+ ["humidity", "%", "mdi:water-percent", ["Canary"]],
+ ["air_quality", None, "mdi:weather-windy", ["Canary"]],
+ ["wifi", "dBm", "mdi:wifi", ["Canary Flex"]],
+ ["battery", "%", "mdi:battery-50", ["Canary Flex"]],
]
STATE_AIR_QUALITY_NORMAL = "normal"
@@ -35,9 +38,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for location in data.locations:
for device in location.devices:
if device.is_online:
+ device_type = device.device_type
for sensor_type in SENSOR_TYPES:
- devices.append(CanarySensor(data, sensor_type, location,
- device))
+ if device_type.get("name") in sensor_type[3]:
+ devices.append(CanarySensor(data, sensor_type,
+ location, device))
add_devices(devices, True)
@@ -80,6 +85,9 @@ class CanarySensor(Entity):
@property
def icon(self):
"""Icon for the sensor."""
+ if self.state is not None and self._sensor_type[0] == "battery":
+ return icon_for_battery_level(battery_level=self.state)
+
return self._sensor_type[2]
@property
@@ -113,6 +121,10 @@ class CanarySensor(Entity):
canary_sensor_type = SensorType.TEMPERATURE
elif self._sensor_type[0] == "humidity":
canary_sensor_type = SensorType.HUMIDITY
+ elif self._sensor_type[0] == "wifi":
+ canary_sensor_type = SensorType.WIFI
+ elif self._sensor_type[0] == "battery":
+ canary_sensor_type = SensorType.BATTERY
value = self._data.get_reading(self._device_id, canary_sensor_type)
diff --git a/homeassistant/components/sensor/cpuspeed.py b/homeassistant/components/sensor/cpuspeed.py
index 25b7bba506c..c39ae43aef0 100644
--- a/homeassistant/components/sensor/cpuspeed.py
+++ b/homeassistant/components/sensor/cpuspeed.py
@@ -13,7 +13,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import CONF_NAME
from homeassistant.helpers.entity import Entity
-REQUIREMENTS = ['py-cpuinfo==3.3.0']
+REQUIREMENTS = ['py-cpuinfo==4.0.0']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/sensor/file.py b/homeassistant/components/sensor/file.py
index afa305a0fb0..cbdd4eef227 100644
--- a/homeassistant/components/sensor/file.py
+++ b/homeassistant/components/sensor/file.py
@@ -25,7 +25,7 @@ DEFAULT_NAME = 'File'
ICON = 'mdi:file'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
- vol.Required(CONF_FILE_PATH): cv.string,
+ vol.Required(CONF_FILE_PATH): cv.isfile,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
@@ -43,8 +43,11 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
if value_template is not None:
value_template.hass = hass
- async_add_devices(
- [FileSensor(name, file_path, unit, value_template)], True)
+ if hass.config.is_allowed_path(file_path):
+ async_add_devices(
+ [FileSensor(name, file_path, unit, value_template)], True)
+ else:
+ _LOGGER.error("'%s' is not a whitelisted directory", file_path)
class FileSensor(Entity):
diff --git a/homeassistant/components/sensor/hydroquebec.py b/homeassistant/components/sensor/hydroquebec.py
index 3678ac9268f..9129ee17d80 100644
--- a/homeassistant/components/sensor/hydroquebec.py
+++ b/homeassistant/components/sensor/hydroquebec.py
@@ -21,7 +21,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['pyhydroquebec==2.1.0']
+REQUIREMENTS = ['pyhydroquebec==2.2.1']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/sensor/imap_email_content.py b/homeassistant/components/sensor/imap_email_content.py
index 1f04cd606d6..c0c9bf62efd 100644
--- a/homeassistant/components/sensor/imap_email_content.py
+++ b/homeassistant/components/sensor/imap_email_content.py
@@ -87,6 +87,8 @@ class EmailReader(object):
_, message_data = self.connection.uid(
'fetch', message_uid, '(RFC822)')
+ if message_data is None:
+ return None
raw_email = message_data[0][1]
email_message = email.message_from_bytes(raw_email)
return email_message
diff --git a/homeassistant/components/sensor/mercedesme.py b/homeassistant/components/sensor/mercedesme.py
deleted file mode 100644
index bb7212678a7..00000000000
--- a/homeassistant/components/sensor/mercedesme.py
+++ /dev/null
@@ -1,87 +0,0 @@
-"""
-Support for Mercedes cars with Mercedes ME.
-
-For more details about this component, please refer to the documentation at
-https://home-assistant.io/components/sensor.mercedesme/
-"""
-import logging
-import datetime
-
-from homeassistant.components.mercedesme import (
- DATA_MME, FEATURE_NOT_AVAILABLE, MercedesMeEntity, SENSORS)
-
-
-DEPENDENCIES = ['mercedesme']
-
-_LOGGER = logging.getLogger(__name__)
-
-
-def setup_platform(hass, config, add_devices, discovery_info=None):
- """Setup the sensor platform."""
- if discovery_info is None:
- return
-
- data = hass.data[DATA_MME].data
-
- if not data.cars:
- return
-
- devices = []
- for car in data.cars:
- for key, value in sorted(SENSORS.items()):
- if car['availabilities'].get(key, 'INVALID') == 'VALID':
- devices.append(
- MercedesMESensor(
- data, key, value[0], car["vin"], value[1]))
- else:
- _LOGGER.warning(FEATURE_NOT_AVAILABLE, key, car["license"])
-
- add_devices(devices, True)
-
-
-class MercedesMESensor(MercedesMeEntity):
- """Representation of a Sensor."""
-
- @property
- def state(self):
- """Return the state of the sensor."""
- return self._state
-
- def update(self):
- """Get the latest data and updates the states."""
- _LOGGER.debug("Updating %s", self._internal_name)
-
- self._car = next(
- car for car in self._data.cars if car["vin"] == self._vin)
-
- if self._internal_name == "latestTrip":
- self._state = self._car["latestTrip"]["id"]
- else:
- self._state = self._car[self._internal_name]
-
- @property
- def device_state_attributes(self):
- """Return the state attributes."""
- if self._internal_name == "latestTrip":
- return {
- "duration_seconds":
- self._car["latestTrip"]["durationSeconds"],
- "distance_traveled_km":
- self._car["latestTrip"]["distanceTraveledKm"],
- "started_at": datetime.datetime.fromtimestamp(
- self._car["latestTrip"]["startedAt"]
- ).strftime('%Y-%m-%d %H:%M:%S'),
- "average_speed_km_per_hr":
- self._car["latestTrip"]["averageSpeedKmPerHr"],
- "finished": self._car["latestTrip"]["finished"],
- "last_update": datetime.datetime.fromtimestamp(
- self._car["lastUpdate"]
- ).strftime('%Y-%m-%d %H:%M:%S'),
- "car": self._car["license"]
- }
-
- return {
- "last_update": datetime.datetime.fromtimestamp(
- self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'),
- "car": self._car["license"]
- }
diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py
index 66c36a8d9b1..669ef3998de 100644
--- a/homeassistant/components/sensor/mysensors.py
+++ b/homeassistant/components/sensor/mysensors.py
@@ -34,10 +34,12 @@ SENSORS = {
}
-def setup_platform(hass, config, add_devices, discovery_info=None):
+async def async_setup_platform(
+ hass, config, async_add_devices, discovery_info=None):
"""Set up the MySensors platform for sensors."""
mysensors.setup_mysensors_platform(
- hass, DOMAIN, discovery_info, MySensorsSensor, add_devices=add_devices)
+ hass, DOMAIN, discovery_info, MySensorsSensor,
+ async_add_devices=async_add_devices)
class MySensorsSensor(mysensors.MySensorsEntity):
diff --git a/homeassistant/components/sensor/plex.py b/homeassistant/components/sensor/plex.py
index 505983cb3a7..b61e1bce0da 100644
--- a/homeassistant/components/sensor/plex.py
+++ b/homeassistant/components/sensor/plex.py
@@ -128,7 +128,7 @@ class PlexSensor(Entity):
season_title += " ({0})".format(sess.show().year)
season_episode = "S{0}".format(sess.parentIndex)
if sess.index is not None:
- season_episode += " · E{1}".format(sess.index)
+ season_episode += " · E{0}".format(sess.index)
episode_title = sess.title
now_playing_title = "{0} - {1} - {2}".format(season_title,
season_episode,
diff --git a/homeassistant/components/sensor/qnap.py b/homeassistant/components/sensor/qnap.py
index 09c9938f1c1..629a5f6a0ee 100644
--- a/homeassistant/components/sensor/qnap.py
+++ b/homeassistant/components/sensor/qnap.py
@@ -17,7 +17,7 @@ from homeassistant.const import (
from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['qnapstats==0.2.4']
+REQUIREMENTS = ['qnapstats==0.2.5']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/sensor/qwikswitch.py b/homeassistant/components/sensor/qwikswitch.py
new file mode 100644
index 00000000000..19b32e93670
--- /dev/null
+++ b/homeassistant/components/sensor/qwikswitch.py
@@ -0,0 +1,69 @@
+"""
+Support for Qwikswitch Sensors.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/sensor.qwikswitch/
+"""
+import logging
+
+from homeassistant.components.qwikswitch import DOMAIN as QWIKSWITCH
+from homeassistant.helpers.entity import Entity
+
+DEPENDENCIES = [QWIKSWITCH]
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_platform(hass, _, add_devices, discovery_info=None):
+ """Add lights from the main Qwikswitch component."""
+ if discovery_info is None:
+ return
+
+ qsusb = hass.data[QWIKSWITCH]
+ _LOGGER.debug("Setup qwikswitch.sensor %s, %s", qsusb, discovery_info)
+ devs = [QSSensor(name, qsid)
+ for name, qsid in discovery_info[QWIKSWITCH].items()]
+ add_devices(devs)
+
+
+class QSSensor(Entity):
+ """Sensor based on a Qwikswitch relay/dimmer module."""
+
+ _val = {}
+
+ def __init__(self, sensor_name, sensor_id):
+ """Initialize the sensor."""
+ self._name = sensor_name
+ self.qsid = sensor_id
+
+ def update_packet(self, packet):
+ """Receive update packet from QSUSB."""
+ _LOGGER.debug("Update %s (%s): %s", self.entity_id, self.qsid, packet)
+ self._val = packet
+ self.async_schedule_update_ha_state()
+
+ @property
+ def state(self):
+ """Return the value of the sensor."""
+ return self._val.get('data', 0)
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes of the sensor."""
+ return self._val
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit the value is expressed in."""
+ return None
+
+ @property
+ def poll(self):
+ """QS sensors gets packets in update_packet."""
+ return False
+
+ async def async_added_to_hass(self):
+ """Listen for updates from QSUSb via dispatcher."""
+ # Part of Entity/ToggleEntity
+ self.hass.helpers.dispatcher.async_dispatcher_connect(
+ self.qsid, self.update_packet)
diff --git a/homeassistant/components/sensor/tesla.py b/homeassistant/components/sensor/tesla.py
index 1ffc97bb137..3233ebb1780 100644
--- a/homeassistant/components/sensor/tesla.py
+++ b/homeassistant/components/sensor/tesla.py
@@ -86,6 +86,8 @@ class TeslaSensor(TeslaDevice, Entity):
self._unit = LENGTH_MILES
else:
self._unit = LENGTH_KILOMETERS
+ self.current_value /= 0.621371
+ self.current_value = round(self.current_value, 2)
else:
self.current_value = self.tesla_device.get_value()
if self.tesla_device.bin_type == 0x5:
@@ -95,3 +97,5 @@ class TeslaSensor(TeslaDevice, Entity):
self._unit = LENGTH_MILES
else:
self._unit = LENGTH_KILOMETERS
+ self.current_value /= 0.621371
+ self.current_value = round(self.current_value, 2)
diff --git a/homeassistant/components/sensor/tradfri.py b/homeassistant/components/sensor/tradfri.py
index d087fdda9f6..df931770cf2 100644
--- a/homeassistant/components/sensor/tradfri.py
+++ b/homeassistant/components/sensor/tradfri.py
@@ -4,7 +4,6 @@ Support for the IKEA Tradfri platform.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.tradfri/
"""
-import asyncio
import logging
from datetime import timedelta
@@ -20,8 +19,8 @@ DEPENDENCIES = ['tradfri']
SCAN_INTERVAL = timedelta(minutes=5)
-@asyncio.coroutine
-def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
+async def async_setup_platform(hass, config, async_add_devices,
+ discovery_info=None):
"""Set up the IKEA Tradfri device platform."""
if discovery_info is None:
return
@@ -31,8 +30,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
gateway = hass.data[KEY_GATEWAY][gateway_id]
devices_command = gateway.get_devices()
- devices_commands = yield from api(devices_command)
- all_devices = yield from api(devices_commands)
+ devices_commands = await api(devices_command)
+ all_devices = await api(devices_commands)
devices = [dev for dev in all_devices if not dev.has_light_control]
async_add_devices(TradfriDevice(device, api) for device in devices)
@@ -48,8 +47,7 @@ class TradfriDevice(Entity):
self._refresh(device)
- @asyncio.coroutine
- def async_added_to_hass(self):
+ async def async_added_to_hass(self):
"""Start thread when added to hass."""
self._async_start_observe()
@@ -91,7 +89,7 @@ class TradfriDevice(Entity):
def _async_start_observe(self, exc=None):
"""Start observation of light."""
# pylint: disable=import-error
- from pytradfri.error import PyTradFriError
+ from pytradfri.error import PytradfriError
if exc:
_LOGGER.warning("Observation failed for %s", self._name,
exc_info=exc)
@@ -101,7 +99,7 @@ class TradfriDevice(Entity):
err_callback=self._async_start_observe,
duration=0)
self.hass.async_add_job(self._api(cmd))
- except PyTradFriError as err:
+ except PytradfriError as err:
_LOGGER.warning("Observation failed, trying again", exc_info=err)
self._async_start_observe()
diff --git a/homeassistant/components/sensor/waze_travel_time.py b/homeassistant/components/sensor/waze_travel_time.py
new file mode 100644
index 00000000000..47589f33530
--- /dev/null
+++ b/homeassistant/components/sensor/waze_travel_time.py
@@ -0,0 +1,136 @@
+"""
+Support for Waze travel time sensor.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/sensor.waze_travel_time/
+"""
+from datetime import timedelta
+import logging
+
+import requests
+import voluptuous as vol
+
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, CONF_REGION
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import Entity
+from homeassistant.util import Throttle
+
+REQUIREMENTS = ['WazeRouteCalculator==0.5']
+
+_LOGGER = logging.getLogger(__name__)
+
+ATTR_DISTANCE = 'distance'
+ATTR_ROUTE = 'route'
+
+CONF_ATTRIBUTION = "Data provided by the Waze.com"
+CONF_DESTINATION = 'destination'
+CONF_ORIGIN = 'origin'
+
+DEFAULT_NAME = 'Waze Travel Time'
+
+ICON = 'mdi:car'
+
+REGIONS = ['US', 'NA', 'EU', 'IL']
+
+SCAN_INTERVAL = timedelta(minutes=5)
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_ORIGIN): cv.string,
+ vol.Required(CONF_DESTINATION): cv.string,
+ vol.Required(CONF_REGION): vol.In(REGIONS),
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+})
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up the Waze travel time sensor platform."""
+ destination = config.get(CONF_DESTINATION)
+ name = config.get(CONF_NAME)
+ origin = config.get(CONF_ORIGIN)
+ region = config.get(CONF_REGION)
+
+ try:
+ waze_data = WazeRouteData(origin, destination, region)
+ except requests.exceptions.HTTPError as error:
+ _LOGGER.error("%s", error)
+ return
+
+ add_devices([WazeTravelTime(waze_data, name)], True)
+
+
+class WazeTravelTime(Entity):
+ """Representation of a Waze travel time sensor."""
+
+ def __init__(self, waze_data, name):
+ """Initialize the Waze travel time sensor."""
+ self._name = name
+ self._state = None
+ self.waze_data = waze_data
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return self._name
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return round(self._state['duration'])
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement."""
+ return 'min'
+
+ @property
+ def icon(self):
+ """Icon to use in the frontend, if any."""
+ return ICON
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes of the last update."""
+ return {
+ ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
+ ATTR_DISTANCE: round(self._state['distance']),
+ ATTR_ROUTE: self._state['route'],
+ }
+
+ def update(self):
+ """Fetch new state data for the sensor."""
+ try:
+ self.waze_data.update()
+ self._state = self.waze_data.data
+ except KeyError:
+ _LOGGER.error("Error retrieving data from server")
+
+
+class WazeRouteData(object):
+ """Get data from Waze."""
+
+ def __init__(self, origin, destination, region):
+ """Initialize the data object."""
+ self._destination = destination
+ self._origin = origin
+ self._region = region
+ self.data = {}
+
+ @Throttle(SCAN_INTERVAL)
+ def update(self):
+ """Fetch latest data from Waze."""
+ import WazeRouteCalculator
+ _LOGGER.debug("Update in progress...")
+ try:
+ params = WazeRouteCalculator.WazeRouteCalculator(
+ self._origin, self._destination, self._region, None)
+ results = params.calc_all_routes_info()
+ best_route = next(iter(results))
+ (duration, distance) = results[best_route]
+ best_route_str = bytes(best_route, 'ISO-8859-1').decode('UTF-8')
+ self.data['duration'] = duration
+ self.data['distance'] = distance
+ self.data['route'] = best_route_str
+ except WazeRouteCalculator.WRCError as exp:
+ _LOGGER.error("Error on retrieving data: %s", exp)
+ return
diff --git a/homeassistant/components/smappee.py b/homeassistant/components/smappee.py
index 0111e0437fb..1241679770b 100644
--- a/homeassistant/components/smappee.py
+++ b/homeassistant/components/smappee.py
@@ -264,7 +264,7 @@ class Smappee(object):
return True
def active_power(self):
- """Get sum of all instantanious active power values from local hub."""
+ """Get sum of all instantaneous active power values from local hub."""
if not self.is_local_active:
return
diff --git a/homeassistant/components/switch/amcrest.py b/homeassistant/components/switch/amcrest.py
new file mode 100755
index 00000000000..0b93bc98b10
--- /dev/null
+++ b/homeassistant/components/switch/amcrest.py
@@ -0,0 +1,92 @@
+"""
+Support for toggling Amcrest IP camera settings.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/switch.amcrest/
+"""
+import asyncio
+import logging
+
+from homeassistant.components.amcrest import DATA_AMCREST, SWITCHES
+from homeassistant.const import (
+ CONF_NAME, CONF_SWITCHES, STATE_OFF, STATE_ON)
+from homeassistant.helpers.entity import ToggleEntity
+
+_LOGGER = logging.getLogger(__name__)
+
+DEPENDENCIES = ['amcrest']
+
+
+@asyncio.coroutine
+def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
+ """Set up the IP Amcrest camera switch platform."""
+ if discovery_info is None:
+ return
+
+ name = discovery_info[CONF_NAME]
+ switches = discovery_info[CONF_SWITCHES]
+ camera = hass.data[DATA_AMCREST][name].device
+
+ all_switches = []
+
+ for setting in switches:
+ all_switches.append(AmcrestSwitch(setting, camera))
+
+ async_add_devices(all_switches, True)
+
+
+class AmcrestSwitch(ToggleEntity):
+ """Representation of an Amcrest IP camera switch."""
+
+ def __init__(self, setting, camera):
+ """Initialize the Amcrest switch."""
+ self._setting = setting
+ self._camera = camera
+ self._name = SWITCHES[setting][0]
+ self._icon = SWITCHES[setting][1]
+ self._state = None
+
+ @property
+ def name(self):
+ """Return the name of the switch if any."""
+ return self._name
+
+ @property
+ def state(self):
+ """Return the state of the switch."""
+ return self._state
+
+ @property
+ def is_on(self):
+ """Return true if switch is on."""
+ return self._state == STATE_ON
+
+ def turn_on(self, **kwargs):
+ """Turn setting on."""
+ if self._setting == 'motion_detection':
+ self._camera.motion_detection = 'true'
+ elif self._setting == 'motion_recording':
+ self._camera.motion_recording = 'true'
+
+ def turn_off(self, **kwargs):
+ """Turn setting off."""
+ if self._setting == 'motion_detection':
+ self._camera.motion_detection = 'false'
+ elif self._setting == 'motion_recording':
+ self._camera.motion_recording = 'false'
+
+ def update(self):
+ """Update setting state."""
+ _LOGGER.debug("Polling state for setting: %s ", self._name)
+
+ if self._setting == 'motion_detection':
+ detection = self._camera.is_motion_detector_on()
+ elif self._setting == 'motion_recording':
+ detection = self._camera.is_record_on_motion_detection()
+
+ self._state = STATE_ON if detection else STATE_OFF
+
+ @property
+ def icon(self):
+ """Return the icon for the switch."""
+ return self._icon
diff --git a/homeassistant/components/switch/broadlink.py b/homeassistant/components/switch/broadlink.py
index 38888733ba6..3e620a6a25b 100644
--- a/homeassistant/components/switch/broadlink.py
+++ b/homeassistant/components/switch/broadlink.py
@@ -14,7 +14,7 @@ import socket
import voluptuous as vol
from homeassistant.components.switch import (
- DOMAIN, PLATFORM_SCHEMA, SwitchDevice)
+ DOMAIN, PLATFORM_SCHEMA, SwitchDevice, ENTITY_ID_FORMAT)
from homeassistant.const import (
CONF_COMMAND_OFF, CONF_COMMAND_ON, CONF_FRIENDLY_NAME, CONF_HOST, CONF_MAC,
CONF_SWITCHES, CONF_TIMEOUT, CONF_TYPE)
@@ -22,9 +22,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
from homeassistant.util.dt import utcnow
-REQUIREMENTS = [
- 'https://github.com/balloob/python-broadlink/archive/'
- '3580ff2eaccd267846f14246d6ede6e30671f7c6.zip#broadlink==0.5.1']
+REQUIREMENTS = ['broadlink==0.8.0']
_LOGGER = logging.getLogger(__name__)
@@ -142,7 +140,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return slots['slot_{}'.format(slot)]
if switch_type in RM_TYPES:
- broadlink_device = broadlink.rm((ip_addr, 80), mac_addr)
+ broadlink_device = broadlink.rm((ip_addr, 80), mac_addr, None)
hass.services.register(DOMAIN, SERVICE_LEARN + '_' +
ip_addr.replace('.', '_'), _learn_command)
hass.services.register(DOMAIN, SERVICE_SEND + '_' +
@@ -152,6 +150,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for object_id, device_config in devices.items():
switches.append(
BroadlinkRMSwitch(
+ object_id,
device_config.get(CONF_FRIENDLY_NAME, object_id),
broadlink_device,
device_config.get(CONF_COMMAND_ON),
@@ -159,14 +158,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
)
)
elif switch_type in SP1_TYPES:
- broadlink_device = broadlink.sp1((ip_addr, 80), mac_addr)
+ broadlink_device = broadlink.sp1((ip_addr, 80), mac_addr, None)
switches = [BroadlinkSP1Switch(friendly_name, broadlink_device)]
elif switch_type in SP2_TYPES:
- broadlink_device = broadlink.sp2((ip_addr, 80), mac_addr)
+ broadlink_device = broadlink.sp2((ip_addr, 80), mac_addr, None)
switches = [BroadlinkSP2Switch(friendly_name, broadlink_device)]
elif switch_type in MP1_TYPES:
switches = []
- broadlink_device = broadlink.mp1((ip_addr, 80), mac_addr)
+ broadlink_device = broadlink.mp1((ip_addr, 80), mac_addr, None)
parent_device = BroadlinkMP1Switch(broadlink_device)
for i in range(1, 5):
slot = BroadlinkMP1Slot(
@@ -186,8 +185,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class BroadlinkRMSwitch(SwitchDevice):
"""Representation of an Broadlink switch."""
- def __init__(self, friendly_name, device, command_on, command_off):
+ def __init__(self, name, friendly_name, device, command_on, command_off):
"""Initialize the switch."""
+ self.entity_id = ENTITY_ID_FORMAT.format(name)
self._name = friendly_name
self._state = False
self._command_on = b64decode(command_on) if command_on else None
diff --git a/homeassistant/components/switch/mysensors.py b/homeassistant/components/switch/mysensors.py
index b4a1dcde3e6..c0f45cad861 100644
--- a/homeassistant/components/switch/mysensors.py
+++ b/homeassistant/components/switch/mysensors.py
@@ -20,7 +20,8 @@ SEND_IR_CODE_SERVICE_SCHEMA = vol.Schema({
})
-def setup_platform(hass, config, add_devices, discovery_info=None):
+async def async_setup_platform(
+ hass, config, async_add_devices, discovery_info=None):
"""Set up the mysensors platform for switches."""
device_class_map = {
'S_DOOR': MySensorsSwitch,
@@ -39,7 +40,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
}
mysensors.setup_mysensors_platform(
hass, DOMAIN, discovery_info, device_class_map,
- add_devices=add_devices)
+ async_add_devices=async_add_devices)
def send_ir_code_service(service):
"""Set IR code as device state attribute."""
@@ -59,9 +60,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for device in _devices:
device.turn_on(**kwargs)
- hass.services.register(DOMAIN, SERVICE_SEND_IR_CODE,
- send_ir_code_service,
- schema=SEND_IR_CODE_SERVICE_SCHEMA)
+ hass.services.async_register(
+ DOMAIN, SERVICE_SEND_IR_CODE, send_ir_code_service,
+ schema=SEND_IR_CODE_SERVICE_SCHEMA)
class MySensorsSwitch(mysensors.MySensorsEntity, SwitchDevice):
@@ -143,7 +144,7 @@ class MySensorsIRSwitch(MySensorsSwitch):
self._values[set_req.V_LIGHT] = STATE_OFF
self.schedule_update_ha_state()
- def update(self):
+ async def async_update(self):
"""Update the controller with the latest value from a sensor."""
- super().update()
+ await super().async_update()
self._ir_code = self._values.get(self.value_type)
diff --git a/homeassistant/components/switch/mystrom.py b/homeassistant/components/switch/mystrom.py
index e813da43dfa..0a87d41d2fe 100644
--- a/homeassistant/components/switch/mystrom.py
+++ b/homeassistant/components/switch/mystrom.py
@@ -12,7 +12,7 @@ from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA)
from homeassistant.const import (CONF_NAME, CONF_HOST)
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['python-mystrom==0.3.8']
+REQUIREMENTS = ['python-mystrom==0.4.2']
DEFAULT_NAME = 'myStrom Switch'
@@ -26,7 +26,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Find and return myStrom switch."""
- from pymystrom import MyStromPlug, exceptions
+ from pymystrom.switch import MyStromPlug, exceptions
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
@@ -45,7 +45,7 @@ class MyStromSwitch(SwitchDevice):
def __init__(self, name, resource):
"""Initialize the myStrom switch."""
- from pymystrom import MyStromPlug
+ from pymystrom.switch import MyStromPlug
self._name = name
self._resource = resource
diff --git a/homeassistant/components/switch/qwikswitch.py b/homeassistant/components/switch/qwikswitch.py
index 7aea1dea1e1..193c2722534 100644
--- a/homeassistant/components/switch/qwikswitch.py
+++ b/homeassistant/components/switch/qwikswitch.py
@@ -4,21 +4,22 @@ Support for Qwikswitch relays.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/switch.qwikswitch/
"""
-import logging
+from homeassistant.components.qwikswitch import (
+ QSToggleEntity, DOMAIN as QWIKSWITCH)
+from homeassistant.components.switch import SwitchDevice
-import homeassistant.components.qwikswitch as qwikswitch
-
-_LOGGER = logging.getLogger(__name__)
-
-DEPENDENCIES = ['qwikswitch']
+DEPENDENCIES = [QWIKSWITCH]
-# pylint: disable=unused-argument
-def setup_platform(hass, config, add_devices, discovery_info=None):
- """Add switched from the main Qwikswitch component."""
+async def async_setup_platform(hass, _, add_devices, discovery_info=None):
+ """Add switches from the main Qwikswitch component."""
if discovery_info is None:
- _LOGGER.error("Configure Qwikswitch component")
- return False
+ return
- add_devices(qwikswitch.QSUSB['switch'])
- return True
+ qsusb = hass.data[QWIKSWITCH]
+ devs = [QSSwitch(qsid, qsusb) for qsid in discovery_info[QWIKSWITCH]]
+ add_devices(devs)
+
+
+class QSSwitch(QSToggleEntity, SwitchDevice):
+ """Switch based on a Qwikswitch relay module."""
diff --git a/homeassistant/components/switch/tahoma.py b/homeassistant/components/switch/tahoma.py
new file mode 100644
index 00000000000..339a0c39386
--- /dev/null
+++ b/homeassistant/components/switch/tahoma.py
@@ -0,0 +1,51 @@
+"""
+Support for Tahoma Switch - those are push buttons for garage door etc.
+
+Those buttons are implemented as switchs that are never on. They only
+receive the turn_on action, perform the relay click, and stay in OFF state
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/switch.tahoma/
+"""
+import logging
+
+from homeassistant.components.switch import SwitchDevice
+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 switchs."""
+ controller = hass.data[TAHOMA_DOMAIN]['controller']
+ devices = []
+ for switch in hass.data[TAHOMA_DOMAIN]['devices']['switch']:
+ devices.append(TahomaSwitch(switch, controller))
+ add_devices(devices, True)
+
+
+class TahomaSwitch(TahomaDevice, SwitchDevice):
+ """Representation a Tahoma Switch."""
+
+ @property
+ def device_class(self):
+ """Return the class of the device."""
+ if self.tahoma_device.type == 'rts:GarageDoor4TRTSComponent':
+ return 'garage'
+ return None
+
+ def turn_on(self, **kwargs):
+ """Send the on command."""
+ self.toggle()
+
+ def toggle(self, **kwargs):
+ """Click the switch."""
+ self.apply_action('cycle')
+
+ @property
+ def is_on(self):
+ """Get whether the switch is in on state."""
+ return False
diff --git a/homeassistant/components/switch/telnet.py b/homeassistant/components/switch/telnet.py
index 7c69b31aa00..c3a608b9692 100644
--- a/homeassistant/components/switch/telnet.py
+++ b/homeassistant/components/switch/telnet.py
@@ -25,7 +25,7 @@ SWITCH_SCHEMA = vol.Schema({
vol.Required(CONF_COMMAND_OFF): cv.string,
vol.Required(CONF_COMMAND_ON): cv.string,
vol.Required(CONF_RESOURCE): cv.string,
- vol.Required(CONF_VALUE_TEMPLATE): cv.template,
+ vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_COMMAND_STATE): cv.string,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
diff --git a/homeassistant/components/switch/xiaomi_miio.py b/homeassistant/components/switch/xiaomi_miio.py
index 6110b6dc469..149acd76c07 100644
--- a/homeassistant/components/switch/xiaomi_miio.py
+++ b/homeassistant/components/switch/xiaomi_miio.py
@@ -24,6 +24,7 @@ DATA_KEY = 'switch.xiaomi_miio'
CONF_MODEL = 'model'
MODEL_POWER_STRIP_V2 = 'zimi.powerstrip.v2'
+MODEL_PLUG_V3 = 'chuangmi.plug.v3'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
@@ -34,7 +35,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
'qmi.powerstrip.v1',
'zimi.powerstrip.v2',
'chuangmi.plug.m1',
- 'chuangmi.plug.v2']),
+ 'chuangmi.plug.v2',
+ 'chuangmi.plug.v3']),
})
REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41']
@@ -51,18 +53,20 @@ ATTR_PRICE = 'price'
SUCCESS = ['ok']
-SUPPORT_SET_POWER_MODE = 1
-SUPPORT_SET_WIFI_LED = 2
-SUPPORT_SET_POWER_PRICE = 4
+FEATURE_SET_POWER_MODE = 1
+FEATURE_SET_WIFI_LED = 2
+FEATURE_SET_POWER_PRICE = 4
-ADDITIONAL_SUPPORT_FLAGS_GENERIC = 0
+FEATURE_FLAGS_GENERIC = 0
-ADDITIONAL_SUPPORT_FLAGS_POWER_STRIP_V1 = (SUPPORT_SET_POWER_MODE |
- SUPPORT_SET_WIFI_LED |
- SUPPORT_SET_POWER_PRICE)
+FEATURE_FLAGS_POWER_STRIP_V1 = (FEATURE_SET_POWER_MODE |
+ FEATURE_SET_WIFI_LED |
+ FEATURE_SET_POWER_PRICE)
-ADDITIONAL_SUPPORT_FLAGS_POWER_STRIP_V2 = (SUPPORT_SET_WIFI_LED |
- SUPPORT_SET_POWER_PRICE)
+FEATURE_FLAGS_POWER_STRIP_V2 = (FEATURE_SET_WIFI_LED |
+ FEATURE_SET_POWER_PRICE)
+
+FEATURE_FLAGS_PLUG_V3 = (FEATURE_SET_WIFI_LED)
SERVICE_SET_WIFI_LED_ON = 'xiaomi_miio_set_wifi_led_on'
SERVICE_SET_WIFI_LED_OFF = 'xiaomi_miio_set_wifi_led_off'
@@ -124,29 +128,27 @@ async def async_setup_platform(hass, config, async_add_devices,
except DeviceException:
raise PlatformNotReady
- if model in ['chuangmi.plug.v1']:
- from miio import PlugV1
- plug = PlugV1(host, token)
+ if model in ['chuangmi.plug.v1', 'chuangmi.plug.v3']:
+ from miio import ChuangmiPlug
+ plug = ChuangmiPlug(host, token, model=model)
# The device has two switchable channels (mains and a USB port).
# A switch device per channel will be created.
for channel_usb in [True, False]:
- device = ChuangMiPlugV1Switch(
+ device = ChuangMiPlugSwitch(
name, plug, model, unique_id, channel_usb)
devices.append(device)
hass.data[DATA_KEY][host] = device
- elif model in ['qmi.powerstrip.v1',
- 'zimi.powerstrip.v2']:
+ elif model in ['qmi.powerstrip.v1', 'zimi.powerstrip.v2']:
from miio import PowerStrip
plug = PowerStrip(host, token)
device = XiaomiPowerStripSwitch(name, plug, model, unique_id)
devices.append(device)
hass.data[DATA_KEY][host] = device
- elif model in ['chuangmi.plug.m1',
- 'chuangmi.plug.v2']:
- from miio import Plug
- plug = Plug(host, token)
+ elif model in ['chuangmi.plug.m1', 'chuangmi.plug.v2']:
+ from miio import ChuangmiPlug
+ plug = ChuangmiPlug(host, token, model=model)
device = XiaomiPlugGenericSwitch(name, plug, model, unique_id)
devices.append(device)
hass.data[DATA_KEY][host] = device
@@ -204,7 +206,7 @@ class XiaomiPlugGenericSwitch(SwitchDevice):
ATTR_TEMPERATURE: None,
ATTR_MODEL: self._model,
}
- self._additional_supported_features = ADDITIONAL_SUPPORT_FLAGS_GENERIC
+ self._device_features = FEATURE_FLAGS_GENERIC
self._skip_update = False
@property
@@ -251,6 +253,10 @@ class XiaomiPlugGenericSwitch(SwitchDevice):
_LOGGER.debug("Response received from plug: %s", result)
+ # The Chuangmi Plug V3 returns 0 on success on usb_on/usb_off.
+ if func in ['usb_on', 'usb_off'] and result == 0:
+ return True
+
return result == SUCCESS
except DeviceException as exc:
_LOGGER.error(mask_error, exc)
@@ -300,7 +306,7 @@ class XiaomiPlugGenericSwitch(SwitchDevice):
async def async_set_wifi_led_on(self):
"""Turn the wifi led on."""
- if self._additional_supported_features & SUPPORT_SET_WIFI_LED == 0:
+ if self._device_features & FEATURE_SET_WIFI_LED == 0:
return
await self._try_command(
@@ -309,7 +315,7 @@ class XiaomiPlugGenericSwitch(SwitchDevice):
async def async_set_wifi_led_off(self):
"""Turn the wifi led on."""
- if self._additional_supported_features & SUPPORT_SET_WIFI_LED == 0:
+ if self._device_features & FEATURE_SET_WIFI_LED == 0:
return
await self._try_command(
@@ -318,7 +324,7 @@ class XiaomiPlugGenericSwitch(SwitchDevice):
async def async_set_power_price(self, price: int):
"""Set the power price."""
- if self._additional_supported_features & SUPPORT_SET_POWER_PRICE == 0:
+ if self._device_features & FEATURE_SET_POWER_PRICE == 0:
return
await self._try_command(
@@ -331,26 +337,24 @@ class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch):
def __init__(self, name, plug, model, unique_id):
"""Initialize the plug switch."""
- XiaomiPlugGenericSwitch.__init__(self, name, plug, model, unique_id)
+ super().__init__(name, plug, model, unique_id)
if self._model == MODEL_POWER_STRIP_V2:
- self._additional_supported_features = \
- ADDITIONAL_SUPPORT_FLAGS_POWER_STRIP_V2
+ self._device_features = FEATURE_FLAGS_POWER_STRIP_V2
else:
- self._additional_supported_features = \
- ADDITIONAL_SUPPORT_FLAGS_POWER_STRIP_V1
+ self._device_features = FEATURE_FLAGS_POWER_STRIP_V1
self._state_attrs.update({
ATTR_LOAD_POWER: None,
})
- if self._additional_supported_features & SUPPORT_SET_POWER_MODE == 1:
+ if self._device_features & FEATURE_SET_POWER_MODE == 1:
self._state_attrs[ATTR_POWER_MODE] = None
- if self._additional_supported_features & SUPPORT_SET_WIFI_LED == 1:
+ if self._device_features & FEATURE_SET_WIFI_LED == 1:
self._state_attrs[ATTR_WIFI_LED] = None
- if self._additional_supported_features & SUPPORT_SET_POWER_PRICE == 1:
+ if self._device_features & FEATURE_SET_POWER_PRICE == 1:
self._state_attrs[ATTR_POWER_PRICE] = None
async def async_update(self):
@@ -373,16 +377,16 @@ class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch):
ATTR_LOAD_POWER: state.load_power,
})
- if self._additional_supported_features & \
- SUPPORT_SET_POWER_MODE == 1 and state.mode:
+ if self._device_features & FEATURE_SET_POWER_MODE == 1 and \
+ state.mode:
self._state_attrs[ATTR_POWER_MODE] = state.mode.value
- if self._additional_supported_features & \
- SUPPORT_SET_WIFI_LED == 1 and state.wifi_led:
+ if self._device_features & FEATURE_SET_WIFI_LED == 1 and \
+ state.wifi_led:
self._state_attrs[ATTR_WIFI_LED] = state.wifi_led
- if self._additional_supported_features & \
- SUPPORT_SET_POWER_PRICE == 1 and state.power_price:
+ if self._device_features & FEATURE_SET_POWER_PRICE == 1 and \
+ state.power_price:
self._state_attrs[ATTR_POWER_PRICE] = state.power_price
except DeviceException as ex:
@@ -391,7 +395,7 @@ class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch):
async def async_set_power_mode(self, mode: str):
"""Set the power mode."""
- if self._additional_supported_features & SUPPORT_SET_POWER_MODE == 0:
+ if self._device_features & FEATURE_SET_POWER_MODE == 0:
return
from miio.powerstrip import PowerMode
@@ -401,8 +405,8 @@ class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch):
self._plug.set_power_mode, PowerMode(mode))
-class ChuangMiPlugV1Switch(XiaomiPlugGenericSwitch):
- """Representation of a Chuang Mi Plug V1."""
+class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch):
+ """Representation of a Chuang Mi Plug V1 and V3."""
def __init__(self, name, plug, model, unique_id, channel_usb):
"""Initialize the plug switch."""
@@ -411,9 +415,16 @@ class ChuangMiPlugV1Switch(XiaomiPlugGenericSwitch):
if unique_id is not None and channel_usb:
unique_id = "{}-{}".format(unique_id, 'usb')
- XiaomiPlugGenericSwitch.__init__(self, name, plug, model, unique_id)
+ super().__init__(name, plug, model, unique_id)
self._channel_usb = channel_usb
+ if self._model == MODEL_PLUG_V3:
+ self._device_features = FEATURE_FLAGS_PLUG_V3
+ self._state_attrs.update({
+ ATTR_WIFI_LED: None,
+ ATTR_LOAD_POWER: None,
+ })
+
async def async_turn_on(self, **kwargs):
"""Turn a channel on."""
if self._channel_usb:
@@ -463,6 +474,12 @@ class ChuangMiPlugV1Switch(XiaomiPlugGenericSwitch):
ATTR_TEMPERATURE: state.temperature
})
+ if state.wifi_led:
+ self._state_attrs[ATTR_WIFI_LED] = state.wifi_led
+
+ if state.load_power:
+ self._state_attrs[ATTR_LOAD_POWER] = state.load_power
+
except DeviceException as ex:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
diff --git a/homeassistant/components/tado.py b/homeassistant/components/tado.py
index cfba0a5c0c4..7c045518132 100644
--- a/homeassistant/components/tado.py
+++ b/homeassistant/components/tado.py
@@ -15,7 +15,7 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
from homeassistant.util import Throttle
-REQUIREMENTS = ['python-tado==0.2.2']
+REQUIREMENTS = ['python-tado==0.2.3']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/tahoma.py b/homeassistant/components/tahoma.py
index 7c8d047fbcf..055e3f410ea 100644
--- a/homeassistant/components/tahoma.py
+++ b/homeassistant/components/tahoma.py
@@ -32,7 +32,7 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA)
TAHOMA_COMPONENTS = [
- 'scene', 'sensor', 'cover'
+ 'scene', 'sensor', 'cover', 'switch'
]
TAHOMA_TYPES = {
@@ -43,6 +43,7 @@ TAHOMA_TYPES = {
'io:RollerShutterGenericIOComponent': 'cover',
'io:WindowOpenerVeluxIOComponent': 'cover',
'io:LightIOSystemSensor': 'sensor',
+ 'rts:GarageDoor4TRTSComponent': 'switch',
}
diff --git a/homeassistant/components/tradfri.py b/homeassistant/components/tradfri.py
index 5ac4d2a4eb1..72d1b4c769f 100644
--- a/homeassistant/components/tradfri.py
+++ b/homeassistant/components/tradfri.py
@@ -1,10 +1,9 @@
"""
-Support for Ikea Tradfri.
+Support for IKEA Tradfri.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/ikea_tradfri/
"""
-import asyncio
import logging
from uuid import uuid4
@@ -16,7 +15,7 @@ from homeassistant.const import CONF_HOST
from homeassistant.components.discovery import SERVICE_IKEA_TRADFRI
from homeassistant.util.json import load_json, save_json
-REQUIREMENTS = ['pytradfri[async]==4.1.0']
+REQUIREMENTS = ['pytradfri[async]==5.4.2']
DOMAIN = 'tradfri'
GATEWAY_IDENTITY = 'homeassistant'
@@ -49,8 +48,7 @@ def request_configuration(hass, config, host):
if instance:
return
- @asyncio.coroutine
- def configuration_callback(callback_data):
+ async def configuration_callback(callback_data):
"""Handle the submitted configuration."""
try:
from pytradfri.api.aiocoap_api import APIFactory
@@ -67,14 +65,14 @@ def request_configuration(hass, config, host):
# pytradfri aiocoap API into an endless loop.
# Should just raise a requestError or something.
try:
- key = yield from api_factory.generate_psk(security_code)
+ key = await api_factory.generate_psk(security_code)
except RequestError:
configurator.async_notify_errors(hass, instance,
"Security Code not accepted.")
return
- res = yield from _setup_gateway(hass, config, host, identity, key,
- DEFAULT_ALLOW_TRADFRI_GROUPS)
+ res = await _setup_gateway(hass, config, host, identity, key,
+ DEFAULT_ALLOW_TRADFRI_GROUPS)
if not res:
configurator.async_notify_errors(hass, instance,
@@ -101,18 +99,16 @@ def request_configuration(hass, config, host):
)
-@asyncio.coroutine
-def async_setup(hass, config):
+async def async_setup(hass, config):
"""Set up the Tradfri component."""
conf = config.get(DOMAIN, {})
host = conf.get(CONF_HOST)
allow_tradfri_groups = conf.get(CONF_ALLOW_TRADFRI_GROUPS)
- known_hosts = yield from hass.async_add_job(load_json,
- hass.config.path(CONFIG_FILE))
+ known_hosts = await hass.async_add_job(load_json,
+ hass.config.path(CONFIG_FILE))
- @asyncio.coroutine
- def gateway_discovered(service, info,
- allow_tradfri_groups=DEFAULT_ALLOW_TRADFRI_GROUPS):
+ async def gateway_discovered(service, info,
+ allow_groups=DEFAULT_ALLOW_TRADFRI_GROUPS):
"""Run when a gateway is discovered."""
host = info['host']
@@ -121,23 +117,22 @@ def async_setup(hass, config):
# identity was hard coded as 'homeassistant'
identity = known_hosts[host].get('identity', 'homeassistant')
key = known_hosts[host].get('key')
- yield from _setup_gateway(hass, config, host, identity, key,
- allow_tradfri_groups)
+ await _setup_gateway(hass, config, host, identity, key,
+ allow_groups)
else:
hass.async_add_job(request_configuration, hass, config, host)
discovery.async_listen(hass, SERVICE_IKEA_TRADFRI, gateway_discovered)
if host:
- yield from gateway_discovered(None,
- {'host': host},
- allow_tradfri_groups)
+ await gateway_discovered(None,
+ {'host': host},
+ allow_tradfri_groups)
return True
-@asyncio.coroutine
-def _setup_gateway(hass, hass_config, host, identity, key,
- allow_tradfri_groups):
+async def _setup_gateway(hass, hass_config, host, identity, key,
+ allow_tradfri_groups):
"""Create a gateway."""
from pytradfri import Gateway, RequestError # pylint: disable=import-error
try:
@@ -151,7 +146,7 @@ def _setup_gateway(hass, hass_config, host, identity, key,
loop=hass.loop)
api = factory.request
gateway = Gateway()
- gateway_info_result = yield from api(gateway.get_gateway_info())
+ gateway_info_result = await api(gateway.get_gateway_info())
except RequestError:
_LOGGER.exception("Tradfri setup failed.")
return False
diff --git a/homeassistant/components/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara.py
index 244605a7b97..48c54cdecff 100644
--- a/homeassistant/components/xiaomi_aqara.py
+++ b/homeassistant/components/xiaomi_aqara.py
@@ -23,7 +23,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util.dt import utcnow
from homeassistant.util import slugify
-REQUIREMENTS = ['PyXiaomiGateway==0.8.3']
+REQUIREMENTS = ['PyXiaomiGateway==0.9.0']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py
index a85160e8bde..02d2b574592 100644
--- a/homeassistant/components/zwave/__init__.py
+++ b/homeassistant/components/zwave/__init__.py
@@ -182,10 +182,8 @@ def nice_print_node(node):
node_dict['values'] = {value_id: _obj_to_dict(value)
for value_id, value in node.values.items()}
- print("\n\n\n")
- print("FOUND NODE", node.product_name)
- pprint(node_dict)
- print("\n\n\n")
+ _LOGGER.info("FOUND NODE %s \n"
+ "%s", node.product_name, node_dict)
def get_config_value(node, value_index, tries=5):
diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py
index eb05e800683..69491af1aad 100644
--- a/homeassistant/config_entries.py
+++ b/homeassistant/config_entries.py
@@ -126,7 +126,7 @@ _LOGGER = logging.getLogger(__name__)
HANDLERS = Registry()
# Components that have config flows. In future we will auto-generate this list.
FLOWS = [
- 'config_entry_example',
+ 'deconz',
'hue',
]
@@ -384,7 +384,7 @@ class FlowManager:
handler = HANDLERS.get(domain)
if handler is None:
- raise self.hass.helpers.UnknownHandler
+ raise UnknownHandler
# Make sure requirements and dependencies of component are resolved
await async_process_deps_reqs(
diff --git a/homeassistant/const.py b/homeassistant/const.py
index a597c25d094..5364fe6951e 100644
--- a/homeassistant/const.py
+++ b/homeassistant/const.py
@@ -1,8 +1,8 @@
# coding: utf-8
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 0
-MINOR_VERSION = 66
-PATCH_VERSION = '1'
+MINOR_VERSION = 67
+PATCH_VERSION = '0'
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
REQUIRED_PYTHON_VER = (3, 5, 3)
diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py
index 28ab4e9bfa0..353fda28875 100644
--- a/homeassistant/helpers/template.py
+++ b/homeassistant/helpers/template.py
@@ -28,7 +28,7 @@ DATE_STR_FORMAT = "%Y-%m-%d %H:%M:%S"
_RE_NONE_ENTITIES = re.compile(r"distance\(|closest\(", re.I | re.M)
_RE_GET_ENTITIES = re.compile(
- r"(?:(?:states\.|(?:is_state|is_state_attr|states)"
+ r"(?:(?:states\.|(?:is_state|is_state_attr|state_attr|states)"
r"\((?:[\ \'\"]?))([\w]+\.[\w]+)|([\w]+))", re.I | re.M
)
@@ -182,6 +182,7 @@ class Template(object):
'distance': template_methods.distance,
'is_state': self.hass.states.is_state,
'is_state_attr': template_methods.is_state_attr,
+ 'state_attr': template_methods.state_attr,
'states': AllStates(self.hass),
})
@@ -405,9 +406,15 @@ class TemplateMethods(object):
def is_state_attr(self, entity_id, name, value):
"""Test if a state is a specific attribute."""
+ state_attr = self.state_attr(entity_id, name)
+ return state_attr is not None and state_attr == value
+
+ def state_attr(self, entity_id, name):
+ """Get a specific attribute from a state."""
state_obj = self._hass.states.get(entity_id)
- return state_obj is not None and \
- state_obj.attributes.get(name) == value
+ if state_obj is not None:
+ return state_obj.attributes.get(name)
+ return None
def _resolve_state(self, entity_id_or_state):
"""Return state or entity_id if given."""
@@ -509,6 +516,39 @@ def forgiving_float(value):
return value
+def regex_match(value, find='', ignorecase=False):
+ """Match value using regex."""
+ if not isinstance(value, str):
+ value = str(value)
+ flags = re.I if ignorecase else 0
+ return bool(re.match(find, value, flags))
+
+
+def regex_replace(value='', find='', replace='', ignorecase=False):
+ """Replace using regex."""
+ if not isinstance(value, str):
+ value = str(value)
+ flags = re.I if ignorecase else 0
+ regex = re.compile(find, flags)
+ return regex.sub(replace, value)
+
+
+def regex_search(value, find='', ignorecase=False):
+ """Search using regex."""
+ if not isinstance(value, str):
+ value = str(value)
+ flags = re.I if ignorecase else 0
+ return bool(re.search(find, value, flags))
+
+
+def regex_findall_index(value, find='', index=0, ignorecase=False):
+ """Find all matches using regex and then pick specific match index."""
+ if not isinstance(value, str):
+ value = str(value)
+ flags = re.I if ignorecase else 0
+ return re.findall(find, value, flags)[index]
+
+
@contextfilter
def random_every_time(context, values):
"""Choose a random value.
@@ -538,6 +578,10 @@ ENV.filters['is_defined'] = fail_when_undefined
ENV.filters['max'] = max
ENV.filters['min'] = min
ENV.filters['random'] = random_every_time
+ENV.filters['regex_match'] = regex_match
+ENV.filters['regex_replace'] = regex_replace
+ENV.filters['regex_search'] = regex_search
+ENV.filters['regex_findall_index'] = regex_findall_index
ENV.globals['log'] = logarithm
ENV.globals['float'] = forgiving_float
ENV.globals['now'] = dt_util.now
diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt
index e43e1f3dafe..85f8d5dcf12 100644
--- a/homeassistant/package_constraints.txt
+++ b/homeassistant/package_constraints.txt
@@ -5,7 +5,7 @@ pip>=8.0.3
jinja2>=2.10
voluptuous==0.11.1
typing>=3,<4
-aiohttp==3.0.9
+aiohttp==3.1.1
async_timeout==2.0.1
astral==1.6
certifi>=2017.4.17
diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py
index ac3ac62e82d..8c78602f3d0 100644
--- a/homeassistant/scripts/check_config.py
+++ b/homeassistant/scripts/check_config.py
@@ -252,7 +252,7 @@ def dump_dict(layer, indent_count=3, listi=False, **kwargs):
"""
def sort_dict_key(val):
"""Return the dict key for sorting."""
- key = str.lower(val[0])
+ key = str(val[0]).lower()
return '0' if key == 'platform' else key
indent_str = indent_count * ' '
@@ -261,10 +261,10 @@ def dump_dict(layer, indent_count=3, listi=False, **kwargs):
if isinstance(layer, Dict):
for key, value in sorted(layer.items(), key=sort_dict_key):
if isinstance(value, (dict, list)):
- print(indent_str, key + ':', line_info(value, **kwargs))
+ print(indent_str, str(key) + ':', line_info(value, **kwargs))
dump_dict(value, indent_count + 2)
else:
- print(indent_str, key + ':', value)
+ print(indent_str, str(key) + ':', value)
indent_str = indent_count * ' '
if isinstance(layer, Sequence):
for i in layer:
diff --git a/homeassistant/scripts/keyring.py b/homeassistant/scripts/keyring.py
index 64ad09bcd70..82a57c90263 100644
--- a/homeassistant/scripts/keyring.py
+++ b/homeassistant/scripts/keyring.py
@@ -5,7 +5,7 @@ import os
from homeassistant.util.yaml import _SECRET_NAMESPACE
-REQUIREMENTS = ['keyring==11.0.0', 'keyrings.alt==2.3']
+REQUIREMENTS = ['keyring==12.0.0', 'keyrings.alt==3.0']
def run(args):
diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py
index c2e4ac737e8..32e9df70a03 100644
--- a/homeassistant/util/color.py
+++ b/homeassistant/util/color.py
@@ -203,7 +203,7 @@ def color_RGB_to_xy_brightness(
# Wide RGB D65 conversion formula
X = R * 0.664511 + G * 0.154324 + B * 0.162028
- Y = R * 0.313881 + G * 0.668433 + B * 0.047685
+ Y = R * 0.283881 + G * 0.668433 + B * 0.047685
Z = R * 0.000088 + G * 0.072310 + B * 0.986039
# Convert XYZ to xy
diff --git a/homeassistant/util/temperature.py b/homeassistant/util/temperature.py
index b7e2412f293..913d6456906 100644
--- a/homeassistant/util/temperature.py
+++ b/homeassistant/util/temperature.py
@@ -3,17 +3,22 @@ from homeassistant.const import (
TEMP_CELSIUS, TEMP_FAHRENHEIT, UNIT_NOT_RECOGNIZED_TEMPLATE, TEMPERATURE)
-def fahrenheit_to_celsius(fahrenheit: float) -> float:
+def fahrenheit_to_celsius(fahrenheit: float, interval: bool = False) -> float:
"""Convert a temperature in Fahrenheit to Celsius."""
+ if interval:
+ return fahrenheit / 1.8
return (fahrenheit - 32.0) / 1.8
-def celsius_to_fahrenheit(celsius: float) -> float:
+def celsius_to_fahrenheit(celsius: float, interval: bool = False) -> float:
"""Convert a temperature in Celsius to Fahrenheit."""
+ if interval:
+ return celsius * 1.8
return celsius * 1.8 + 32.0
-def convert(temperature: float, from_unit: str, to_unit: str) -> float:
+def convert(temperature: float, from_unit: str, to_unit: str,
+ interval: bool = False) -> float:
"""Convert a temperature from one unit to another."""
if from_unit not in (TEMP_CELSIUS, TEMP_FAHRENHEIT):
raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(
@@ -25,5 +30,5 @@ def convert(temperature: float, from_unit: str, to_unit: str) -> float:
if from_unit == to_unit:
return temperature
elif from_unit == TEMP_CELSIUS:
- return celsius_to_fahrenheit(temperature)
- return fahrenheit_to_celsius(temperature)
+ return celsius_to_fahrenheit(temperature, interval)
+ return fahrenheit_to_celsius(temperature, interval)
diff --git a/requirements_all.txt b/requirements_all.txt
index b2232eeb9e2..8fe9c7e1c13 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -6,7 +6,7 @@ pip>=8.0.3
jinja2>=2.10
voluptuous==0.11.1
typing>=3,<4
-aiohttp==3.0.9
+aiohttp==3.1.1
async_timeout==2.0.1
astral==1.6
certifi>=2017.4.17
@@ -22,7 +22,10 @@ attrs==17.4.0
DoorBirdPy==0.1.3
# homeassistant.components.homekit
-HAP-python==1.1.7
+HAP-python==1.1.8
+
+# homeassistant.components.notify.mastodon
+Mastodon.py==1.2.2
# homeassistant.components.isy994
PyISY==1.1.0
@@ -37,7 +40,7 @@ PyMVGLive==1.1.4
PyMata==2.14
# homeassistant.components.xiaomi_aqara
-PyXiaomiGateway==0.8.3
+PyXiaomiGateway==0.9.0
# homeassistant.components.rpi_gpio
# RPi.GPIO==0.6.1
@@ -54,11 +57,14 @@ TravisPy==0.3.5
# homeassistant.components.notify.twitter
TwitterAPI==2.5.0
+# homeassistant.components.sensor.waze_travel_time
+WazeRouteCalculator==0.5
+
# homeassistant.components.notify.yessssms
YesssSMS==0.1.1b3
# homeassistant.components.abode
-abodepy==0.12.2
+abodepy==0.12.3
# homeassistant.components.media_player.frontier_silicon
afsapi==0.0.3
@@ -95,7 +101,7 @@ alarmdecoder==1.13.2
alpha_vantage==1.9.0
# homeassistant.components.amcrest
-amcrest==1.2.1
+amcrest==1.2.2
# homeassistant.components.media_player.anthemav
anthemav==1.1.8
@@ -137,7 +143,7 @@ beautifulsoup4==4.6.0
bellows==0.5.1
# homeassistant.components.bmw_connected_drive
-bimmer_connected==0.4.1
+bimmer_connected==0.5.0
# homeassistant.components.blink
blinkpy==0.6.0
@@ -166,6 +172,13 @@ boto3==1.4.7
# homeassistant.scripts.credstash
botocore==1.7.34
+# homeassistant.components.sensor.broadlink
+# homeassistant.components.switch.broadlink
+broadlink==0.8.0
+
+# homeassistant.components.device_tracker.bluetooth_tracker
+bt_proximity==0.1.2
+
# homeassistant.components.sensor.buienradar
# homeassistant.components.weather.buienradar
buienradar==0.91
@@ -345,7 +358,7 @@ gstreamer-player==1.1.0
ha-ffmpeg==1.9
# homeassistant.components.media_player.philips_js
-ha-philipsjs==0.0.2
+ha-philipsjs==0.0.3
# homeassistant.components.sensor.geo_rss_events
haversine==0.4.5
@@ -366,7 +379,7 @@ hipnotify==1.0.8
holidays==0.9.4
# homeassistant.components.frontend
-home-assistant-frontend==20180401.0
+home-assistant-frontend==20180404.0
# homeassistant.components.homematicip_cloud
homematicip==0.8
@@ -383,10 +396,6 @@ httplib2==0.10.3
# homeassistant.components.media_player.braviatv
https://github.com/aparraga/braviarc/archive/0.3.7.zip#braviarc==0.3.7
-# homeassistant.components.sensor.broadlink
-# homeassistant.components.switch.broadlink
-https://github.com/balloob/python-broadlink/archive/3580ff2eaccd267846f14246d6ede6e30671f7c6.zip#broadlink==0.5.1
-
# homeassistant.components.media_player.spotify
https://github.com/happyleavesaoc/spotipy/archive/544614f4b1d508201d363e84e871f86c90aa26b2.zip#spotipy==2.4.4
@@ -427,7 +436,7 @@ influxdb==5.0.0
insteonlocal==0.53
# homeassistant.components.insteon_plm
-insteonplm==0.8.3
+insteonplm==0.8.6
# homeassistant.components.verisure
jsonpath==0.75
@@ -440,10 +449,10 @@ jsonrpc-async==0.6
jsonrpc-websocket==0.6
# homeassistant.scripts.keyring
-keyring==11.0.0
+keyring==12.0.0
# homeassistant.scripts.keyring
-keyrings.alt==2.3
+keyrings.alt==3.0
# homeassistant.components.device_tracker.owntracks
# homeassistant.components.device_tracker.owntracks_http
@@ -480,6 +489,9 @@ liveboxplaytv==2.0.2
# homeassistant.components.notify.lametric
lmnotify==0.0.4
+# homeassistant.components.device_tracker.google_maps
+locationsharinglib==0.4.0
+
# homeassistant.components.sensor.luftdaten
luftdaten==0.1.3
@@ -492,9 +504,6 @@ matrix-client==0.0.6
# homeassistant.components.maxcube
maxcube-api==0.1.0
-# homeassistant.components.mercedesme
-mercedesmejsonpy==0.1.2
-
# homeassistant.components.notify.message_bird
messagebird==1.2.0
@@ -527,6 +536,9 @@ myusps==1.3.2
# homeassistant.components.media_player.nadtcp
nad_receiver==0.0.9
+# homeassistant.components.light.nanoleaf_aurora
+nanoleaf==0.4.1
+
# homeassistant.components.discovery
netdisco==1.3.0
@@ -636,10 +648,10 @@ pwmled==1.2.1
py-august==0.4.0
# homeassistant.components.canary
-py-canary==0.4.1
+py-canary==0.5.0
# homeassistant.components.sensor.cpuspeed
-py-cpuinfo==3.3.0
+py-cpuinfo==4.0.0
# homeassistant.components.melissa
py-melissa-climate==1.0.6
@@ -655,7 +667,7 @@ pyCEC==0.4.13
pyHS100==0.3.0
# homeassistant.components.rfxtrx
-pyRFXtrx==0.21.1
+pyRFXtrx==0.22.0
# homeassistant.components.sensor.tibber
pyTibber==0.4.0
@@ -714,7 +726,7 @@ pycsspeechtts==1.0.2
pydaikin==0.4
# homeassistant.components.deconz
-pydeconz==32
+pydeconz==35
# homeassistant.components.zwave
pydispatcher==2.0.5
@@ -752,6 +764,9 @@ pyflexit==0.3
# homeassistant.components.ifttt
pyfttt==0.3
+# homeassistant.components.cover.gogogate2
+pygogogate2==0.0.3
+
# homeassistant.components.remote.harmony
pyharmony==1.0.20
@@ -765,7 +780,7 @@ pyhiveapi==0.2.11
pyhomematic==0.1.40
# homeassistant.components.sensor.hydroquebec
-pyhydroquebec==2.1.0
+pyhydroquebec==2.2.1
# homeassistant.components.alarm_control_panel.ialarm
pyialarm==0.2
@@ -870,7 +885,7 @@ pyowm==2.8.0
pypollencom==1.1.1
# homeassistant.components.qwikswitch
-pyqwikswitch==0.4
+pyqwikswitch==0.6
# homeassistant.components.rainbird
pyrainbird==0.1.3
@@ -908,7 +923,7 @@ pystride==0.1.7
pysyncthru==0.3.1
# homeassistant.components.media_player.liveboxplaytv
-pyteleloisirs==3.3
+pyteleloisirs==3.4
# homeassistant.components.sensor.thinkingcleaner
# homeassistant.components.switch.thinkingcleaner
@@ -952,6 +967,7 @@ python-juicenet==0.0.5
# homeassistant.components.lirc
# python-lirc==1.2.3
+# homeassistant.components.device_tracker.xiaomi_miio
# homeassistant.components.fan.xiaomi_miio
# homeassistant.components.light.xiaomi_miio
# homeassistant.components.remote.xiaomi_miio
@@ -965,7 +981,7 @@ python-mpd2==0.5.5
# homeassistant.components.light.mystrom
# homeassistant.components.switch.mystrom
-python-mystrom==0.3.8
+python-mystrom==0.4.2
# homeassistant.components.nest
python-nest==3.7.0
@@ -986,13 +1002,13 @@ python-roku==3.1.5
python-sochain-api==0.0.2
# homeassistant.components.media_player.songpal
-python-songpal==0.0.6
+python-songpal==0.0.7
# homeassistant.components.sensor.synologydsm
python-synology==0.1.0
# homeassistant.components.tado
-python-tado==0.2.2
+python-tado==0.2.3
# homeassistant.components.telegram_bot
python-telegram-bot==10.0.1
@@ -1031,7 +1047,7 @@ pytouchline==0.7
pytrackr==0.0.5
# homeassistant.components.tradfri
-pytradfri[async]==4.1.0
+pytradfri[async]==5.4.2
# homeassistant.components.device_tracker.unifi
pyunifi==2.13
@@ -1064,7 +1080,7 @@ pyxeoma==1.4.0
pyzabbix==0.7.4
# homeassistant.components.sensor.qnap
-qnapstats==0.2.4
+qnapstats==0.2.5
# homeassistant.components.switch.rachio
rachiopy==0.1.2
@@ -1085,7 +1101,7 @@ regenmaschine==0.4.1
restrictedpython==4.0b2
# homeassistant.components.rflink
-rflink==0.0.34
+rflink==0.0.37
# homeassistant.components.ring
ring_doorbell==0.1.8
@@ -1146,7 +1162,7 @@ simplisafe-python==1.0.5
skybellpy==0.1.1
# homeassistant.components.notify.slack
-slacker==0.9.60
+slacker==0.9.65
# homeassistant.components.notify.xmpp
sleekxmpp==1.3.2
@@ -1228,7 +1244,7 @@ todoist-python==7.0.17
toonlib==1.0.2
# homeassistant.components.alarm_control_panel.totalconnect
-total_connect_client==0.16
+total_connect_client==0.17
# homeassistant.components.sensor.transmission
# homeassistant.components.switch.transmission
@@ -1279,6 +1295,9 @@ waqiasync==1.0.0
# homeassistant.components.cloud
warrant==0.6.1
+# homeassistant.components.folder_watcher
+watchdog==0.8.3
+
# homeassistant.components.waterfurnace
waterfurnace==0.4.0
@@ -1317,10 +1336,10 @@ yahooweather==0.10
yeelight==0.4.0
# homeassistant.components.light.yeelightsunflower
-yeelightsunflower==0.0.8
+yeelightsunflower==0.0.10
# homeassistant.components.media_extractor
-youtube_dl==2018.03.10
+youtube_dl==2018.04.03
# homeassistant.components.light.zengge
zengge==0.2
diff --git a/requirements_test.txt b/requirements_test.txt
index fc9e113e97c..38b716406fd 100644
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -6,9 +6,9 @@ coveralls==1.2.0
flake8-docstrings==1.0.3
flake8==3.5
mock-open==1.3.1
-mypy==0.570
+mypy==0.580
pydocstyle==1.1.1
-pylint==1.8.2
+pylint==1.8.3
pytest-aiohttp==0.3.0
pytest-cov==2.5.1
pytest-sugar==0.9.1
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 8c01400f79e..7c5467f7608 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -7,9 +7,9 @@ coveralls==1.2.0
flake8-docstrings==1.0.3
flake8==3.5
mock-open==1.3.1
-mypy==0.570
+mypy==0.580
pydocstyle==1.1.1
-pylint==1.8.2
+pylint==1.8.3
pytest-aiohttp==0.3.0
pytest-cov==2.5.1
pytest-sugar==0.9.1
@@ -19,7 +19,7 @@ requests_mock==1.4
# homeassistant.components.homekit
-HAP-python==1.1.7
+HAP-python==1.1.8
# homeassistant.components.notify.html5
PyJWT==1.6.0
@@ -81,7 +81,7 @@ hbmqtt==0.9.1
holidays==0.9.4
# homeassistant.components.frontend
-home-assistant-frontend==20180401.0
+home-assistant-frontend==20180404.0
# homeassistant.components.influxdb
# homeassistant.components.sensor.influxdb
@@ -127,7 +127,10 @@ prometheus_client==0.1.0
pushbullet.py==0.11.0
# homeassistant.components.canary
-py-canary==0.4.1
+py-canary==0.5.0
+
+# homeassistant.components.deconz
+pydeconz==35
# homeassistant.components.zwave
pydispatcher==2.0.5
@@ -159,7 +162,7 @@ pywebpush==1.6.0
restrictedpython==4.0b2
# homeassistant.components.rflink
-rflink==0.0.34
+rflink==0.0.37
# homeassistant.components.ring
ring_doorbell==0.1.8
diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py
index d8fc7b1ed60..d5bb2701e9b 100755
--- a/script/gen_requirements_all.py
+++ b/script/gen_requirements_all.py
@@ -67,6 +67,7 @@ TEST_REQUIREMENTS = (
'prometheus_client',
'pushbullet.py',
'py-canary',
+ 'pydeconz',
'pydispatcher',
'PyJWT',
'pylitejet',
diff --git a/setup.py b/setup.py
index a317aeb18f1..db4b1f8df92 100755
--- a/setup.py
+++ b/setup.py
@@ -49,7 +49,7 @@ REQUIRES = [
'jinja2>=2.10',
'voluptuous==0.11.1',
'typing>=3,<4',
- 'aiohttp==3.0.9',
+ 'aiohttp==3.1.1',
'async_timeout==2.0.1',
'astral==1.6',
'certifi>=2017.4.17',
diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py
index 8199652d09e..dd404b7d57a 100644
--- a/tests/components/alexa/test_smart_home.py
+++ b/tests/components/alexa/test_smart_home.py
@@ -693,6 +693,133 @@ def test_unknown_sensor(hass):
yield from discovery_test(device, hass, expected_endpoints=0)
+async def test_thermostat(hass):
+ """Test thermostat discovery."""
+ device = (
+ 'climate.test_thermostat',
+ 'cool',
+ {
+ 'operation_mode': 'cool',
+ 'temperature': 70.0,
+ 'target_temp_high': 80.0,
+ 'target_temp_low': 60.0,
+ 'current_temperature': 75.0,
+ 'friendly_name': "Test Thermostat",
+ 'supported_features': 1 | 2 | 4 | 128,
+ 'operation_list': ['heat', 'cool', 'auto', 'off'],
+ 'min_temp': 50,
+ 'max_temp': 90,
+ 'unit_of_measurement': TEMP_FAHRENHEIT,
+ }
+ )
+ appliance = await discovery_test(device, hass)
+
+ assert appliance['endpointId'] == 'climate#test_thermostat'
+ assert appliance['displayCategories'][0] == 'THERMOSTAT'
+ assert appliance['friendlyName'] == "Test Thermostat"
+
+ assert_endpoint_capabilities(
+ appliance,
+ 'Alexa.ThermostatController',
+ 'Alexa.TemperatureSensor',
+ )
+
+ properties = await reported_properties(
+ hass, 'climate#test_thermostat')
+ properties.assert_equal(
+ 'Alexa.ThermostatController', 'thermostatMode', 'COOL')
+ properties.assert_equal(
+ 'Alexa.ThermostatController', 'targetSetpoint',
+ {'value': 70.0, 'scale': 'FAHRENHEIT'})
+ properties.assert_equal(
+ 'Alexa.TemperatureSensor', 'temperature',
+ {'value': 75.0, 'scale': 'FAHRENHEIT'})
+
+ call, _ = await assert_request_calls_service(
+ 'Alexa.ThermostatController', 'SetTargetTemperature',
+ 'climate#test_thermostat', 'climate.set_temperature',
+ hass,
+ payload={'targetSetpoint': {'value': 69.0, 'scale': 'FAHRENHEIT'}}
+ )
+ assert call.data['temperature'] == 69.0
+
+ msg = await assert_request_fails(
+ 'Alexa.ThermostatController', 'SetTargetTemperature',
+ 'climate#test_thermostat', 'climate.set_temperature',
+ hass,
+ payload={'targetSetpoint': {'value': 0.0, 'scale': 'CELSIUS'}}
+ )
+ assert msg['event']['payload']['type'] == 'TEMPERATURE_VALUE_OUT_OF_RANGE'
+
+ call, _ = await assert_request_calls_service(
+ 'Alexa.ThermostatController', 'SetTargetTemperature',
+ 'climate#test_thermostat', 'climate.set_temperature',
+ hass,
+ payload={
+ 'targetSetpoint': {'value': 70.0, 'scale': 'FAHRENHEIT'},
+ 'lowerSetpoint': {'value': 293.15, 'scale': 'KELVIN'},
+ 'upperSetpoint': {'value': 30.0, 'scale': 'CELSIUS'},
+ }
+ )
+ assert call.data['temperature'] == 70.0
+ assert call.data['target_temp_low'] == 68.0
+ assert call.data['target_temp_high'] == 86.0
+
+ msg = await assert_request_fails(
+ 'Alexa.ThermostatController', 'SetTargetTemperature',
+ 'climate#test_thermostat', 'climate.set_temperature',
+ hass,
+ payload={
+ 'lowerSetpoint': {'value': 273.15, 'scale': 'KELVIN'},
+ 'upperSetpoint': {'value': 75.0, 'scale': 'FAHRENHEIT'},
+ }
+ )
+ assert msg['event']['payload']['type'] == 'TEMPERATURE_VALUE_OUT_OF_RANGE'
+
+ msg = await assert_request_fails(
+ 'Alexa.ThermostatController', 'SetTargetTemperature',
+ 'climate#test_thermostat', 'climate.set_temperature',
+ hass,
+ payload={
+ 'lowerSetpoint': {'value': 293.15, 'scale': 'FAHRENHEIT'},
+ 'upperSetpoint': {'value': 75.0, 'scale': 'CELSIUS'},
+ }
+ )
+ assert msg['event']['payload']['type'] == 'TEMPERATURE_VALUE_OUT_OF_RANGE'
+
+ call, _ = await assert_request_calls_service(
+ 'Alexa.ThermostatController', 'AdjustTargetTemperature',
+ 'climate#test_thermostat', 'climate.set_temperature',
+ hass,
+ payload={'targetSetpointDelta': {'value': -10.0, 'scale': 'KELVIN'}}
+ )
+ assert call.data['temperature'] == 52.0
+
+ msg = await assert_request_fails(
+ 'Alexa.ThermostatController', 'AdjustTargetTemperature',
+ 'climate#test_thermostat', 'climate.set_temperature',
+ hass,
+ payload={'targetSetpointDelta': {'value': 20.0, 'scale': 'CELSIUS'}}
+ )
+ assert msg['event']['payload']['type'] == 'TEMPERATURE_VALUE_OUT_OF_RANGE'
+
+ call, _ = await assert_request_calls_service(
+ 'Alexa.ThermostatController', 'SetThermostatMode',
+ 'climate#test_thermostat', 'climate.set_operation_mode',
+ hass,
+ payload={'thermostatMode': 'HEAT'}
+ )
+ assert call.data['operation_mode'] == 'heat'
+
+ msg = await assert_request_fails(
+ 'Alexa.ThermostatController', 'SetThermostatMode',
+ 'climate#test_thermostat', 'climate.set_operation_mode',
+ hass,
+ payload={'thermostatMode': 'INVALID'}
+ )
+ assert msg['event']['payload']['type'] == 'UNSUPPORTED_THERMOSTAT_MODE'
+
+
@asyncio.coroutine
def test_exclude_filters(hass):
"""Test exclusion filters."""
diff --git a/tests/components/cover/test_init.py b/tests/components/cover/test_init.py
new file mode 100755
index 00000000000..5df492d3d47
--- /dev/null
+++ b/tests/components/cover/test_init.py
@@ -0,0 +1,49 @@
+"""The tests for the cover platform."""
+
+from homeassistant.components.cover import (SERVICE_OPEN_COVER,
+ SERVICE_CLOSE_COVER)
+from homeassistant.components import intent
+import homeassistant.components as comps
+from tests.common import async_mock_service
+
+
+async def test_open_cover_intent(hass):
+ """Test HassOpenCover intent."""
+ result = await comps.cover.async_setup(hass, {})
+ assert result
+
+ hass.states.async_set('cover.garage_door', 'closed')
+ calls = async_mock_service(hass, 'cover', SERVICE_OPEN_COVER)
+
+ response = await intent.async_handle(
+ hass, 'test', 'HassOpenCover', {'name': {'value': 'garage door'}}
+ )
+ await hass.async_block_till_done()
+
+ assert response.speech['plain']['speech'] == 'Opened garage door'
+ assert len(calls) == 1
+ call = calls[0]
+ assert call.domain == 'cover'
+ assert call.service == 'open_cover'
+ assert call.data == {'entity_id': 'cover.garage_door'}
+
+
+async def test_close_cover_intent(hass):
+ """Test HassCloseCover intent."""
+ result = await comps.cover.async_setup(hass, {})
+ assert result
+
+ hass.states.async_set('cover.garage_door', 'open')
+ calls = async_mock_service(hass, 'cover', SERVICE_CLOSE_COVER)
+
+ response = await intent.async_handle(
+ hass, 'test', 'HassCloseCover', {'name': {'value': 'garage door'}}
+ )
+ await hass.async_block_till_done()
+
+ assert response.speech['plain']['speech'] == 'Closed garage door'
+ assert len(calls) == 1
+ call = calls[0]
+ assert call.domain == 'cover'
+ assert call.service == 'close_cover'
+ assert call.data == {'entity_id': 'cover.garage_door'}
diff --git a/tests/components/device_tracker/test_asuswrt.py b/tests/components/device_tracker/test_asuswrt.py
index 27f28412561..d2ae8965668 100644
--- a/tests/components/device_tracker/test_asuswrt.py
+++ b/tests/components/device_tracker/test_asuswrt.py
@@ -15,7 +15,7 @@ from homeassistant.components.device_tracker import (
from homeassistant.components.device_tracker.asuswrt import (
CONF_PROTOCOL, CONF_MODE, CONF_PUB_KEY, DOMAIN, _ARP_REGEX,
CONF_PORT, PLATFORM_SCHEMA, Device, get_scanner, AsusWrtDeviceScanner,
- _parse_lines, SshConnection, TelnetConnection)
+ _parse_lines, SshConnection, TelnetConnection, CONF_REQUIRE_IP)
from homeassistant.const import (CONF_PLATFORM, CONF_PASSWORD, CONF_USERNAME,
CONF_HOST)
@@ -105,6 +105,15 @@ WAKE_DEVICES_AP = {
mac='08:09:10:11:12:14', ip='123.123.123.126', name=None)
}
+WAKE_DEVICES_NO_IP = {
+ '01:02:03:04:06:08': Device(
+ mac='01:02:03:04:06:08', ip='123.123.123.125', name=None),
+ '08:09:10:11:12:14': Device(
+ mac='08:09:10:11:12:14', ip='123.123.123.126', name=None),
+ '08:09:10:11:12:15': Device(
+ mac='08:09:10:11:12:15', ip=None, name=None)
+}
+
def setup_module():
"""Setup the test module."""
@@ -411,6 +420,21 @@ class TestComponentsDeviceTrackerASUSWRT(unittest.TestCase):
scanner._get_leases.return_value = LEASES_DEVICES
self.assertEqual(WAKE_DEVICES_AP, scanner.get_asuswrt_data())
+ def test_get_asuswrt_data_no_ip(self):
+ """Test for get asuswrt_data and not requiring ip."""
+ conf = VALID_CONFIG_ROUTER_SSH.copy()[DOMAIN]
+ conf[CONF_REQUIRE_IP] = False
+ scanner = AsusWrtDeviceScanner(conf)
+ scanner._get_wl = mock.Mock()
+ scanner._get_arp = mock.Mock()
+ scanner._get_neigh = mock.Mock()
+ scanner._get_leases = mock.Mock()
+ scanner._get_wl.return_value = WL_DEVICES
+ scanner._get_arp.return_value = ARP_DEVICES
+ scanner._get_neigh.return_value = NEIGH_DEVICES
+ scanner._get_leases.return_value = LEASES_DEVICES
+ self.assertEqual(WAKE_DEVICES_NO_IP, scanner.get_asuswrt_data())
+
def test_update_info(self):
"""Test for update info."""
scanner = get_scanner(self.hass, VALID_CONFIG_ROUTER_SSH)
diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py
index c051983d8fa..912bd315ecd 100644
--- a/tests/components/device_tracker/test_init.py
+++ b/tests/components/device_tracker/test_init.py
@@ -24,9 +24,7 @@ from homeassistant.remote import JSONEncoder
from tests.common import (
get_test_home_assistant, fire_time_changed,
- patch_yaml_files, assert_setup_component, mock_restore_cache, mock_coro)
-
-from ...test_util.aiohttp import mock_aiohttp_client
+ patch_yaml_files, assert_setup_component, mock_restore_cache)
TEST_PLATFORM = {device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}}
@@ -111,7 +109,6 @@ class TestComponentsDeviceTracker(unittest.TestCase):
self.assertEqual(device.config_picture, config.config_picture)
self.assertEqual(device.away_hide, config.away_hide)
self.assertEqual(device.consider_home, config.consider_home)
- self.assertEqual(device.vendor, config.vendor)
self.assertEqual(device.icon, config.icon)
# pylint: disable=invalid-name
@@ -173,124 +170,6 @@ class TestComponentsDeviceTracker(unittest.TestCase):
"55502f40dc8b7c769880b10874abc9d0.jpg?s=80&d=wavatar")
self.assertEqual(device.config_picture, gravatar_url)
- def test_mac_vendor_lookup(self):
- """Test if vendor string is lookup on macvendors API."""
- mac = 'B8:27:EB:00:00:00'
- vendor_string = 'Raspberry Pi Foundation'
-
- device = device_tracker.Device(
- self.hass, timedelta(seconds=180), True, 'test', mac, 'Test name')
-
- with mock_aiohttp_client() as aioclient_mock:
- aioclient_mock.get('http://api.macvendors.com/b8:27:eb',
- text=vendor_string)
-
- run_coroutine_threadsafe(device.set_vendor_for_mac(),
- self.hass.loop).result()
- assert aioclient_mock.call_count == 1
-
- self.assertEqual(device.vendor, vendor_string)
-
- def test_mac_vendor_mac_formats(self):
- """Verify all variations of MAC addresses are handled correctly."""
- vendor_string = 'Raspberry Pi Foundation'
-
- with mock_aiohttp_client() as aioclient_mock:
- aioclient_mock.get('http://api.macvendors.com/b8:27:eb',
- text=vendor_string)
- aioclient_mock.get('http://api.macvendors.com/00:27:eb',
- text=vendor_string)
-
- mac = 'B8:27:EB:00:00:00'
- device = device_tracker.Device(
- self.hass, timedelta(seconds=180),
- True, 'test', mac, 'Test name')
- run_coroutine_threadsafe(device.set_vendor_for_mac(),
- self.hass.loop).result()
- self.assertEqual(device.vendor, vendor_string)
-
- mac = '0:27:EB:00:00:00'
- device = device_tracker.Device(
- self.hass, timedelta(seconds=180),
- True, 'test', mac, 'Test name')
- run_coroutine_threadsafe(device.set_vendor_for_mac(),
- self.hass.loop).result()
- self.assertEqual(device.vendor, vendor_string)
-
- mac = 'PREFIXED_B8:27:EB:00:00:00'
- device = device_tracker.Device(
- self.hass, timedelta(seconds=180),
- True, 'test', mac, 'Test name')
- run_coroutine_threadsafe(device.set_vendor_for_mac(),
- self.hass.loop).result()
- self.assertEqual(device.vendor, vendor_string)
-
- def test_mac_vendor_lookup_unknown(self):
- """Prevent another mac vendor lookup if was not found first time."""
- mac = 'B8:27:EB:00:00:00'
-
- device = device_tracker.Device(
- self.hass, timedelta(seconds=180), True, 'test', mac, 'Test name')
-
- with mock_aiohttp_client() as aioclient_mock:
- aioclient_mock.get('http://api.macvendors.com/b8:27:eb',
- status=404)
-
- run_coroutine_threadsafe(device.set_vendor_for_mac(),
- self.hass.loop).result()
-
- self.assertEqual(device.vendor, 'unknown')
-
- def test_mac_vendor_lookup_error(self):
- """Prevent another lookup if failure during API call."""
- mac = 'B8:27:EB:00:00:00'
-
- device = device_tracker.Device(
- self.hass, timedelta(seconds=180), True, 'test', mac, 'Test name')
-
- with mock_aiohttp_client() as aioclient_mock:
- aioclient_mock.get('http://api.macvendors.com/b8:27:eb',
- status=500)
-
- run_coroutine_threadsafe(device.set_vendor_for_mac(),
- self.hass.loop).result()
-
- self.assertEqual(device.vendor, 'unknown')
-
- def test_mac_vendor_lookup_exception(self):
- """Prevent another lookup if exception during API call."""
- mac = 'B8:27:EB:00:00:00'
-
- device = device_tracker.Device(
- self.hass, timedelta(seconds=180), True, 'test', mac, 'Test name')
-
- with mock_aiohttp_client() as aioclient_mock:
- aioclient_mock.get('http://api.macvendors.com/b8:27:eb',
- exc=asyncio.TimeoutError())
-
- run_coroutine_threadsafe(device.set_vendor_for_mac(),
- self.hass.loop).result()
-
- self.assertEqual(device.vendor, 'unknown')
-
- def test_mac_vendor_lookup_on_see(self):
- """Test if macvendor is looked up when device is seen."""
- mac = 'B8:27:EB:00:00:00'
- vendor_string = 'Raspberry Pi Foundation'
-
- tracker = device_tracker.DeviceTracker(
- self.hass, timedelta(seconds=60), 0, {}, [])
-
- with mock_aiohttp_client() as aioclient_mock:
- aioclient_mock.get('http://api.macvendors.com/b8:27:eb',
- text=vendor_string)
-
- run_coroutine_threadsafe(
- tracker.async_see(mac=mac), self.hass.loop).result()
- assert aioclient_mock.call_count == 1, \
- 'No http request for macvendor made!'
- self.assertEqual(tracker.devices['b827eb000000'].vendor, vendor_string)
-
@patch(
'homeassistant.components.device_tracker.DeviceTracker.see')
@patch(
@@ -463,7 +342,6 @@ class TestComponentsDeviceTracker(unittest.TestCase):
'entity_id': 'device_tracker.hello',
'host_name': 'hello',
'mac': 'MAC_1',
- 'vendor': 'unknown',
}
# pylint: disable=invalid-name
@@ -495,9 +373,7 @@ class TestComponentsDeviceTracker(unittest.TestCase):
timedelta(seconds=0))
assert len(config) == 0
- @patch('homeassistant.components.device_tracker.Device'
- '.set_vendor_for_mac', return_value=mock_coro())
- def test_see_state(self, mock_set_vendor):
+ def test_see_state(self):
"""Test device tracker see records state correctly."""
self.assertTrue(setup_component(self.hass, device_tracker.DOMAIN,
TEST_PLATFORM))
diff --git a/tests/components/emulated_hue/test_init.py b/tests/components/emulated_hue/test_init.py
index 06613f1336a..2f443eb5d6e 100644
--- a/tests/components/emulated_hue/test_init.py
+++ b/tests/components/emulated_hue/test_init.py
@@ -3,7 +3,7 @@ import json
from unittest.mock import patch, Mock, mock_open
-from homeassistant.components.emulated_hue import Config, _LOGGER
+from homeassistant.components.emulated_hue import Config
def test_config_google_home_entity_id_to_number():
@@ -112,17 +112,3 @@ def test_config_alexa_entity_id_to_number():
entity_id = conf.number_to_entity_id('light.test')
assert entity_id == 'light.test'
-
-
-def test_warning_config_google_home_listen_port():
- """Test we warn when non-default port is used for Google Home."""
- with patch.object(_LOGGER, 'warning') as mock_warn:
- Config(None, {
- 'type': 'google_home',
- 'host_ip': '123.123.123.123',
- 'listen_port': 8300
- })
-
- assert mock_warn.called
- assert mock_warn.mock_calls[0][1][0] == \
- "When targeting Google Home, listening port has to be port 80"
diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py
index dd9373c782a..e284b026ad8 100644
--- a/tests/components/google_assistant/test_smart_home.py
+++ b/tests/components/google_assistant/test_smart_home.py
@@ -74,7 +74,7 @@ async def test_sync_message(hass):
'willReportState': False,
'attributes': {
'colorModel': 'rgb',
- 'temperatureMinK': 6493,
+ 'temperatureMinK': 6535,
'temperatureMaxK': 2000,
},
'roomHint': 'Living Room'
diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py
index 4d230b81686..a2facd826e4 100644
--- a/tests/components/homekit/test_accessories.py
+++ b/tests/components/homekit/test_accessories.py
@@ -6,12 +6,12 @@ import unittest
from unittest.mock import call, patch, Mock
from homeassistant.components.homekit.accessories import (
- add_preload_service, set_accessory_info, override_properties,
+ add_preload_service, set_accessory_info,
HomeAccessory, HomeBridge, HomeDriver)
from homeassistant.components.homekit.const import (
ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, BRIDGE_NAME,
- SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE,
- CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER)
+ SERV_ACCESSORY_INFO, CHAR_MANUFACTURER, CHAR_MODEL,
+ CHAR_NAME, CHAR_SERIAL_NUMBER)
class TestAccessories(unittest.TestCase):
@@ -22,7 +22,7 @@ class TestAccessories(unittest.TestCase):
acc = Mock()
serv = add_preload_service(acc, 'AirPurifier')
self.assertEqual(acc.mock_calls, [call.add_service(serv)])
- with self.assertRaises(AssertionError):
+ with self.assertRaises(ValueError):
serv.get_characteristic('Name')
# Test with typo in service name
@@ -68,24 +68,6 @@ class TestAccessories(unittest.TestCase):
self.assertEqual(
serv.get_characteristic(CHAR_SERIAL_NUMBER).value, '0000')
- def test_override_properties(self):
- """Test overriding property values."""
- serv = add_preload_service(Mock(), 'AirPurifier', 'RotationSpeed')
-
- char_active = serv.get_characteristic('Active')
- char_rotation_speed = serv.get_characteristic('RotationSpeed')
-
- self.assertTrue(
- char_active.properties['ValidValues'].get('State') is None)
- self.assertEqual(char_rotation_speed.properties['maxValue'], 100)
-
- override_properties(char_active, valid_values={'State': 'On'})
- override_properties(char_rotation_speed, properties={'maxValue': 200})
-
- self.assertFalse(
- char_active.properties['ValidValues'].get('State') is None)
- self.assertEqual(char_rotation_speed.properties['maxValue'], 200)
-
def test_home_accessory(self):
"""Test HomeAccessory class."""
acc = HomeAccessory()
@@ -110,17 +92,15 @@ class TestAccessories(unittest.TestCase):
bridge = HomeBridge(None)
self.assertEqual(bridge.display_name, BRIDGE_NAME)
self.assertEqual(bridge.category, 2) # Category.BRIDGE
- self.assertEqual(len(bridge.services), 2)
+ self.assertEqual(len(bridge.services), 1)
serv = bridge.services[0] # SERV_ACCESSORY_INFO
self.assertEqual(serv.display_name, SERV_ACCESSORY_INFO)
self.assertEqual(
serv.get_characteristic(CHAR_MODEL).value, BRIDGE_MODEL)
- serv = bridge.services[1] # SERV_BRIDGING_STATE
- self.assertEqual(serv.display_name, SERV_BRIDGING_STATE)
bridge = HomeBridge('hass', 'test_name', 'test_model')
self.assertEqual(bridge.display_name, 'test_name')
- self.assertEqual(len(bridge.services), 2)
+ self.assertEqual(len(bridge.services), 1)
serv = bridge.services[0] # SERV_ACCESSORY_INFO
self.assertEqual(
serv.get_characteristic(CHAR_MODEL).value, 'test_model')
@@ -153,13 +133,13 @@ class TestAccessories(unittest.TestCase):
def test_home_driver(self):
"""Test HomeDriver class."""
bridge = HomeBridge(None)
- ip_adress = '127.0.0.1'
+ ip_address = '127.0.0.1'
port = 51826
path = '.homekit.state'
with patch('pyhap.accessory_driver.AccessoryDriver.__init__') \
as mock_driver:
- HomeDriver(bridge, ip_adress, port, path)
+ HomeDriver(bridge, ip_address, port, path)
self.assertEqual(
- mock_driver.call_args, call(bridge, ip_adress, port, path))
+ mock_driver.call_args, call(bridge, ip_address, port, path))
diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py
index ee1900fd7c5..1cfb926c4ce 100644
--- a/tests/components/homekit/test_type_lights.py
+++ b/tests/components/homekit/test_type_lights.py
@@ -4,8 +4,8 @@ import unittest
from homeassistant.core import callback
from homeassistant.components.homekit.type_lights import Light
from homeassistant.components.light import (
- DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_HS_COLOR,
- SUPPORT_BRIGHTNESS, SUPPORT_COLOR)
+ DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP,
+ ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR)
from homeassistant.const import (
ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_SERVICE, ATTR_SERVICE_DATA,
ATTR_SUPPORTED_FEATURES, EVENT_CALL_SERVICE, SERVICE_TURN_ON,
@@ -118,6 +118,28 @@ class TestHomekitLights(unittest.TestCase):
self.assertEqual(self.events[2].data[ATTR_DOMAIN], DOMAIN)
self.assertEqual(self.events[2].data[ATTR_SERVICE], SERVICE_TURN_OFF)
+ def test_light_color_temperature(self):
+ """Test light with color temperature."""
+ entity_id = 'light.demo'
+ self.hass.states.set(entity_id, STATE_ON, {
+ ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR_TEMP,
+ ATTR_COLOR_TEMP: 190})
+ acc = Light(self.hass, entity_id, 'Light', aid=2)
+ self.assertEqual(acc.char_color_temperature.value, 153)
+
+ acc.run()
+ self.hass.block_till_done()
+ self.assertEqual(acc.char_color_temperature.value, 190)
+
+ # Set from HomeKit
+ acc.char_color_temperature.set_value(250)
+ self.hass.block_till_done()
+ self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN)
+ self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON)
+ self.assertEqual(
+ self.events[0].data[ATTR_SERVICE_DATA], {
+ ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 250})
+
def test_light_rgb_color(self):
"""Test light with rgb_color."""
entity_id = 'light.demo'
diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py
index 011fe73377d..e1511163f2f 100644
--- a/tests/components/homekit/test_type_thermostats.py
+++ b/tests/components/homekit/test_type_thermostats.py
@@ -6,11 +6,10 @@ from homeassistant.components.climate import (
ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE,
ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE,
ATTR_OPERATION_LIST, STATE_COOL, STATE_HEAT, STATE_AUTO)
-from homeassistant.components.homekit.type_thermostats import (
- Thermostat, STATE_OFF)
+from homeassistant.components.homekit.type_thermostats import Thermostat
from homeassistant.const import (
ATTR_SERVICE, EVENT_CALL_SERVICE, ATTR_SERVICE_DATA,
- ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT)
+ ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT)
from tests.common import get_test_home_assistant
diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py
deleted file mode 100644
index 7ccc202b31b..00000000000
--- a/tests/components/hue/conftest.py
+++ /dev/null
@@ -1,17 +0,0 @@
-"""Fixtures for Hue tests."""
-from unittest.mock import patch
-
-import pytest
-
-from tests.common import mock_coro_func
-
-
-@pytest.fixture
-def mock_bridge():
- """Mock the HueBridge from initializing."""
- with patch('homeassistant.components.hue._find_username_from_config',
- return_value=None), \
- patch('homeassistant.components.hue.HueBridge') as mock_bridge:
- mock_bridge().async_setup = mock_coro_func()
- mock_bridge.reset_mock()
- yield mock_bridge
diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py
index 39351699df5..0845aa2f077 100644
--- a/tests/components/hue/test_bridge.py
+++ b/tests/components/hue/test_bridge.py
@@ -1,99 +1,57 @@
"""Test Hue bridge."""
-import asyncio
from unittest.mock import Mock, patch
-import aiohue
-import pytest
-
-from homeassistant.components import hue
+from homeassistant.components.hue import bridge, errors
from tests.common import mock_coro
-class MockBridge(hue.HueBridge):
- """Class that sets default for constructor."""
+async def test_bridge_setup():
+ """Test a successful setup."""
+ hass = Mock()
+ entry = Mock()
+ api = Mock()
+ entry.data = {'host': '1.2.3.4', 'username': 'mock-username'}
+ hue_bridge = bridge.HueBridge(hass, entry, False, False)
- def __init__(self, hass, host='1.2.3.4', filename='mock-bridge.conf',
- username=None, **kwargs):
- """Initialize a mock bridge."""
- super().__init__(host, hass, filename, username, **kwargs)
+ with patch.object(bridge, 'get_bridge', return_value=mock_coro(api)):
+ assert await hue_bridge.async_setup() is True
-
-@pytest.fixture
-def mock_request():
- """Mock configurator.async_request_config."""
- with patch('homeassistant.components.configurator.'
- 'async_request_config') as mock_request:
- yield mock_request
-
-
-async def test_setup_request_config_button_not_pressed(hass, mock_request):
- """Test we request config if link button has not been pressed."""
- with patch('aiohue.Bridge.create_user',
- side_effect=aiohue.LinkButtonNotPressed):
- await MockBridge(hass).async_setup()
-
- assert len(mock_request.mock_calls) == 1
-
-
-async def test_setup_request_config_invalid_username(hass, mock_request):
- """Test we request config if username is no longer whitelisted."""
- with patch('aiohue.Bridge.create_user',
- side_effect=aiohue.Unauthorized):
- await MockBridge(hass).async_setup()
-
- assert len(mock_request.mock_calls) == 1
-
-
-async def test_setup_timeout(hass, mock_request):
- """Test we give up when there is a timeout."""
- with patch('aiohue.Bridge.create_user',
- side_effect=asyncio.TimeoutError):
- await MockBridge(hass).async_setup()
-
- assert len(mock_request.mock_calls) == 0
-
-
-async def test_only_create_no_username(hass):
- """."""
- with patch('aiohue.Bridge.create_user') as mock_create, \
- patch('aiohue.Bridge.initialize') as mock_init:
- await MockBridge(hass, username='bla').async_setup()
-
- assert len(mock_create.mock_calls) == 0
- assert len(mock_init.mock_calls) == 1
-
-
-async def test_configurator_callback(hass, mock_request):
- """."""
- hass.data[hue.DOMAIN] = {}
- with patch('aiohue.Bridge.create_user',
- side_effect=aiohue.LinkButtonNotPressed):
- await MockBridge(hass).async_setup()
-
- assert len(mock_request.mock_calls) == 1
-
- callback = mock_request.mock_calls[0][1][2]
-
- mock_init = Mock(return_value=mock_coro())
- mock_create = Mock(return_value=mock_coro())
-
- with patch('aiohue.Bridge') as mock_bridge, \
- patch('homeassistant.helpers.discovery.async_load_platform',
- return_value=mock_coro()) as mock_load_platform, \
- patch('homeassistant.components.hue.save_json') as mock_save:
- inst = mock_bridge()
- inst.username = 'mock-user'
- inst.create_user = mock_create
- inst.initialize = mock_init
- await callback(None)
-
- assert len(mock_create.mock_calls) == 1
- assert len(mock_init.mock_calls) == 1
- assert len(mock_save.mock_calls) == 1
- assert mock_save.mock_calls[0][1][1] == {
- '1.2.3.4': {
- 'username': 'mock-user'
- }
+ assert hue_bridge.api is api
+ assert len(hass.helpers.discovery.async_load_platform.mock_calls) == 1
+ assert hass.helpers.discovery.async_load_platform.mock_calls[0][1][2] == {
+ 'host': '1.2.3.4'
}
- assert len(mock_load_platform.mock_calls) == 1
+
+
+async def test_bridge_setup_invalid_username():
+ """Test we start config flow if username is no longer whitelisted."""
+ hass = Mock()
+ entry = Mock()
+ entry.data = {'host': '1.2.3.4', 'username': 'mock-username'}
+ hue_bridge = bridge.HueBridge(hass, entry, False, False)
+
+ with patch.object(bridge, 'get_bridge',
+ side_effect=errors.AuthenticationRequired):
+ assert await hue_bridge.async_setup() is False
+
+ assert len(hass.async_add_job.mock_calls) == 1
+ assert len(hass.config_entries.flow.async_init.mock_calls) == 1
+ assert hass.config_entries.flow.async_init.mock_calls[0][2]['data'] == {
+ 'host': '1.2.3.4'
+ }
+
+
+async def test_bridge_setup_timeout(hass):
+ """Test we retry to connect if we cannot connect."""
+ hass = Mock()
+ entry = Mock()
+ entry.data = {'host': '1.2.3.4', 'username': 'mock-username'}
+ hue_bridge = bridge.HueBridge(hass, entry, False, False)
+
+ with patch.object(bridge, 'get_bridge', side_effect=errors.CannotConnect):
+ assert await hue_bridge.async_setup() is False
+
+ assert len(hass.helpers.event.async_call_later.mock_calls) == 1
+ # Assert we are going to wait 2 seconds
+ assert hass.helpers.event.async_call_later.mock_calls[0][1][0] == 2
diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py
index 959e3c6241b..fe3bffe5357 100644
--- a/tests/components/hue/test_config_flow.py
+++ b/tests/components/hue/test_config_flow.py
@@ -1,28 +1,29 @@
"""Tests for Philips Hue config flow."""
import asyncio
-from unittest.mock import patch
+from unittest.mock import Mock, patch
import aiohue
import pytest
import voluptuous as vol
-from homeassistant.components import hue
+from homeassistant.components.hue import config_flow, const, errors
from tests.common import MockConfigEntry, mock_coro
async def test_flow_works(hass, aioclient_mock):
"""Test config flow ."""
- aioclient_mock.get(hue.API_NUPNP, json=[
+ aioclient_mock.get(const.API_NUPNP, json=[
{'internalipaddress': '1.2.3.4', 'id': 'bla'}
])
- flow = hue.HueFlowHandler()
+ flow = config_flow.HueFlowHandler()
flow.hass = hass
await flow.async_step_init()
with patch('aiohue.Bridge') as mock_bridge:
- def mock_constructor(host, websession):
+ def mock_constructor(host, websession, username=None):
+ """Fake the bridge constructor."""
mock_bridge.host = host
return mock_bridge
@@ -50,8 +51,8 @@ async def test_flow_works(hass, aioclient_mock):
async def test_flow_no_discovered_bridges(hass, aioclient_mock):
"""Test config flow discovers no bridges."""
- aioclient_mock.get(hue.API_NUPNP, json=[])
- flow = hue.HueFlowHandler()
+ aioclient_mock.get(const.API_NUPNP, json=[])
+ flow = config_flow.HueFlowHandler()
flow.hass = hass
result = await flow.async_step_init()
@@ -60,13 +61,13 @@ async def test_flow_no_discovered_bridges(hass, aioclient_mock):
async def test_flow_all_discovered_bridges_exist(hass, aioclient_mock):
"""Test config flow discovers only already configured bridges."""
- aioclient_mock.get(hue.API_NUPNP, json=[
+ aioclient_mock.get(const.API_NUPNP, json=[
{'internalipaddress': '1.2.3.4', 'id': 'bla'}
])
MockConfigEntry(domain='hue', data={
'host': '1.2.3.4'
}).add_to_hass(hass)
- flow = hue.HueFlowHandler()
+ flow = config_flow.HueFlowHandler()
flow.hass = hass
result = await flow.async_step_init()
@@ -75,10 +76,10 @@ async def test_flow_all_discovered_bridges_exist(hass, aioclient_mock):
async def test_flow_one_bridge_discovered(hass, aioclient_mock):
"""Test config flow discovers one bridge."""
- aioclient_mock.get(hue.API_NUPNP, json=[
+ aioclient_mock.get(const.API_NUPNP, json=[
{'internalipaddress': '1.2.3.4', 'id': 'bla'}
])
- flow = hue.HueFlowHandler()
+ flow = config_flow.HueFlowHandler()
flow.hass = hass
result = await flow.async_step_init()
@@ -88,11 +89,11 @@ async def test_flow_one_bridge_discovered(hass, aioclient_mock):
async def test_flow_two_bridges_discovered(hass, aioclient_mock):
"""Test config flow discovers two bridges."""
- aioclient_mock.get(hue.API_NUPNP, json=[
+ aioclient_mock.get(const.API_NUPNP, json=[
{'internalipaddress': '1.2.3.4', 'id': 'bla'},
{'internalipaddress': '5.6.7.8', 'id': 'beer'}
])
- flow = hue.HueFlowHandler()
+ flow = config_flow.HueFlowHandler()
flow.hass = hass
result = await flow.async_step_init()
@@ -108,14 +109,14 @@ async def test_flow_two_bridges_discovered(hass, aioclient_mock):
async def test_flow_two_bridges_discovered_one_new(hass, aioclient_mock):
"""Test config flow discovers two bridges."""
- aioclient_mock.get(hue.API_NUPNP, json=[
+ aioclient_mock.get(const.API_NUPNP, json=[
{'internalipaddress': '1.2.3.4', 'id': 'bla'},
{'internalipaddress': '5.6.7.8', 'id': 'beer'}
])
MockConfigEntry(domain='hue', data={
'host': '1.2.3.4'
}).add_to_hass(hass)
- flow = hue.HueFlowHandler()
+ flow = config_flow.HueFlowHandler()
flow.hass = hass
result = await flow.async_step_init()
@@ -126,7 +127,7 @@ async def test_flow_two_bridges_discovered_one_new(hass, aioclient_mock):
async def test_flow_timeout_discovery(hass):
"""Test config flow ."""
- flow = hue.HueFlowHandler()
+ flow = config_flow.HueFlowHandler()
flow.hass = hass
with patch('aiohue.discovery.discover_nupnp',
@@ -138,7 +139,7 @@ async def test_flow_timeout_discovery(hass):
async def test_flow_link_timeout(hass):
"""Test config flow ."""
- flow = hue.HueFlowHandler()
+ flow = config_flow.HueFlowHandler()
flow.hass = hass
with patch('aiohue.Bridge.create_user',
@@ -148,13 +149,13 @@ async def test_flow_link_timeout(hass):
assert result['type'] == 'form'
assert result['step_id'] == 'link'
assert result['errors'] == {
- 'base': 'register_failed'
+ 'base': 'linking'
}
async def test_flow_link_button_not_pressed(hass):
"""Test config flow ."""
- flow = hue.HueFlowHandler()
+ flow = config_flow.HueFlowHandler()
flow.hass = hass
with patch('aiohue.Bridge.create_user',
@@ -170,7 +171,7 @@ async def test_flow_link_button_not_pressed(hass):
async def test_flow_link_unknown_host(hass):
"""Test config flow ."""
- flow = hue.HueFlowHandler()
+ flow = config_flow.HueFlowHandler()
flow.hass = hass
with patch('aiohue.Bridge.create_user',
@@ -180,5 +181,175 @@ async def test_flow_link_unknown_host(hass):
assert result['type'] == 'form'
assert result['step_id'] == 'link'
assert result['errors'] == {
- 'base': 'register_failed'
+ 'base': 'linking'
}
+
+
+async def test_bridge_discovery(hass):
+ """Test a bridge being discovered."""
+ flow = config_flow.HueFlowHandler()
+ flow.hass = hass
+
+ with patch.object(config_flow, 'get_bridge',
+ side_effect=errors.AuthenticationRequired):
+ result = await flow.async_step_discovery({
+ 'host': '0.0.0.0',
+ 'serial': '1234'
+ })
+
+ assert result['type'] == 'form'
+ assert result['step_id'] == 'link'
+
+
+async def test_bridge_discovery_emulated_hue(hass):
+ """Test if discovery info is from an emulated hue instance."""
+ flow = config_flow.HueFlowHandler()
+ flow.hass = hass
+
+ result = await flow.async_step_discovery({
+ 'name': 'HASS Bridge',
+ 'host': '0.0.0.0',
+ 'serial': '1234'
+ })
+
+ assert result['type'] == 'abort'
+
+
+async def test_bridge_discovery_already_configured(hass):
+ """Test if a discovered bridge has already been configured."""
+ MockConfigEntry(domain='hue', data={
+ 'host': '0.0.0.0'
+ }).add_to_hass(hass)
+
+ flow = config_flow.HueFlowHandler()
+ flow.hass = hass
+
+ result = await flow.async_step_discovery({
+ 'host': '0.0.0.0',
+ 'serial': '1234'
+ })
+
+ assert result['type'] == 'abort'
+
+
+async def test_import_with_existing_config(hass):
+ """Test importing a host with an existing config file."""
+ flow = config_flow.HueFlowHandler()
+ flow.hass = hass
+
+ bridge = Mock()
+ bridge.username = 'username-abc'
+ bridge.config.bridgeid = 'bridge-id-1234'
+ bridge.config.name = 'Mock Bridge'
+ bridge.host = '0.0.0.0'
+
+ with patch.object(config_flow, '_find_username_from_config',
+ return_value='mock-user'), \
+ patch.object(config_flow, 'get_bridge',
+ return_value=mock_coro(bridge)):
+ result = await flow.async_step_import({
+ 'host': '0.0.0.0',
+ 'path': 'bla.conf'
+ })
+
+ assert result['type'] == 'create_entry'
+ assert result['title'] == 'Mock Bridge'
+ assert result['data'] == {
+ 'host': '0.0.0.0',
+ 'bridge_id': 'bridge-id-1234',
+ 'username': 'username-abc'
+ }
+
+
+async def test_import_with_no_config(hass):
+ """Test importing a host without an existing config file."""
+ flow = config_flow.HueFlowHandler()
+ flow.hass = hass
+
+ with patch.object(config_flow, 'get_bridge',
+ side_effect=errors.AuthenticationRequired):
+ result = await flow.async_step_import({
+ 'host': '0.0.0.0',
+ })
+
+ assert result['type'] == 'form'
+ assert result['step_id'] == 'link'
+
+
+async def test_import_with_existing_but_invalid_config(hass):
+ """Test importing a host with a config file with invalid username."""
+ flow = config_flow.HueFlowHandler()
+ flow.hass = hass
+
+ with patch.object(config_flow, '_find_username_from_config',
+ return_value='mock-user'), \
+ patch.object(config_flow, 'get_bridge',
+ side_effect=errors.AuthenticationRequired):
+ result = await flow.async_step_import({
+ 'host': '0.0.0.0',
+ 'path': 'bla.conf'
+ })
+
+ assert result['type'] == 'form'
+ assert result['step_id'] == 'link'
+
+
+async def test_import_cannot_connect(hass):
+ """Test importing a host that we cannot conncet to."""
+ flow = config_flow.HueFlowHandler()
+ flow.hass = hass
+
+ with patch.object(config_flow, 'get_bridge',
+ side_effect=errors.CannotConnect):
+ result = await flow.async_step_import({
+ 'host': '0.0.0.0',
+ })
+
+ assert result['type'] == 'abort'
+ assert result['reason'] == 'cannot_connect'
+
+
+async def test_creating_entry_removes_entries_for_same_host_or_bridge(hass):
+ """Test that we clean up entries for same host and bridge.
+
+ An IP can only hold a single bridge and a single bridge can only be
+ accessible via a single IP. So when we create a new entry, we'll remove
+ all existing entries that either have same IP or same bridge_id.
+ """
+ MockConfigEntry(domain='hue', data={
+ 'host': '0.0.0.0',
+ 'bridge_id': 'id-1234'
+ }).add_to_hass(hass)
+
+ MockConfigEntry(domain='hue', data={
+ 'host': '1.2.3.4',
+ 'bridge_id': 'id-1234'
+ }).add_to_hass(hass)
+
+ assert len(hass.config_entries.async_entries('hue')) == 2
+
+ flow = config_flow.HueFlowHandler()
+ flow.hass = hass
+
+ bridge = Mock()
+ bridge.username = 'username-abc'
+ bridge.config.bridgeid = 'id-1234'
+ bridge.config.name = 'Mock Bridge'
+ bridge.host = '0.0.0.0'
+
+ with patch.object(config_flow, 'get_bridge',
+ return_value=mock_coro(bridge)):
+ result = await flow.async_step_import({
+ 'host': '0.0.0.0',
+ })
+
+ assert result['type'] == 'create_entry'
+ assert result['title'] == 'Mock Bridge'
+ assert result['data'] == {
+ 'host': '0.0.0.0',
+ 'bridge_id': 'id-1234',
+ 'username': 'username-abc'
+ }
+ # We did not process the result of this entry but already removed the old
+ # ones. So we should have 0 entries.
+ assert len(hass.config_entries.async_entries('hue')) == 0
diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py
new file mode 100644
index 00000000000..47e74b70e83
--- /dev/null
+++ b/tests/components/hue/test_init.py
@@ -0,0 +1,169 @@
+"""Test Hue setup process."""
+from unittest.mock import patch
+
+from homeassistant.setup import async_setup_component
+from homeassistant.components import hue
+
+from tests.common import mock_coro, MockConfigEntry
+
+
+async def test_setup_with_no_config(hass):
+ """Test that we do not discover anything or try to setup a bridge."""
+ with patch.object(hass, 'config_entries') as mock_config_entries, \
+ patch.object(hue, 'configured_hosts', return_value=[]):
+ assert await async_setup_component(hass, hue.DOMAIN, {}) is True
+
+ # No flows started
+ assert len(mock_config_entries.flow.mock_calls) == 0
+
+ # No configs stored
+ assert hass.data[hue.DOMAIN] == {}
+
+
+async def test_setup_with_discovery_no_known_auth(hass, aioclient_mock):
+ """Test discovering a bridge and not having known auth."""
+ aioclient_mock.get(hue.API_NUPNP, json=[
+ {
+ 'internalipaddress': '0.0.0.0',
+ 'id': 'abcd1234'
+ }
+ ])
+
+ with patch.object(hass, 'config_entries') as mock_config_entries, \
+ patch.object(hue, 'configured_hosts', return_value=[]):
+ mock_config_entries.flow.async_init.return_value = mock_coro()
+ assert await async_setup_component(hass, hue.DOMAIN, {
+ hue.DOMAIN: {}
+ }) is True
+
+ # Flow started for discovered bridge
+ assert len(mock_config_entries.flow.mock_calls) == 1
+ assert mock_config_entries.flow.mock_calls[0][2]['data'] == {
+ 'host': '0.0.0.0',
+ 'path': '.hue_abcd1234.conf',
+ }
+
+ # Config stored for domain.
+ assert hass.data[hue.DOMAIN] == {
+ '0.0.0.0': {
+ hue.CONF_HOST: '0.0.0.0',
+ hue.CONF_FILENAME: '.hue_abcd1234.conf',
+ hue.CONF_ALLOW_HUE_GROUPS: hue.DEFAULT_ALLOW_HUE_GROUPS,
+ hue.CONF_ALLOW_UNREACHABLE: hue.DEFAULT_ALLOW_UNREACHABLE,
+ }
+ }
+
+
+async def test_setup_with_discovery_known_auth(hass, aioclient_mock):
+ """Test we don't do anything if we discover already configured hub."""
+ aioclient_mock.get(hue.API_NUPNP, json=[
+ {
+ 'internalipaddress': '0.0.0.0',
+ 'id': 'abcd1234'
+ }
+ ])
+
+ with patch.object(hass, 'config_entries') as mock_config_entries, \
+ patch.object(hue, 'configured_hosts', return_value=['0.0.0.0']):
+ assert await async_setup_component(hass, hue.DOMAIN, {
+ hue.DOMAIN: {}
+ }) is True
+
+ # Flow started for discovered bridge
+ assert len(mock_config_entries.flow.mock_calls) == 0
+
+ # Config stored for domain.
+ assert hass.data[hue.DOMAIN] == {}
+
+
+async def test_setup_defined_hosts_known_auth(hass):
+ """Test we don't initiate a config entry if config bridge is known."""
+ with patch.object(hass, 'config_entries') as mock_config_entries, \
+ patch.object(hue, 'configured_hosts', return_value=['0.0.0.0']):
+ assert await async_setup_component(hass, hue.DOMAIN, {
+ hue.DOMAIN: {
+ hue.CONF_BRIDGES: {
+ hue.CONF_HOST: '0.0.0.0',
+ hue.CONF_FILENAME: 'bla.conf',
+ hue.CONF_ALLOW_HUE_GROUPS: False,
+ hue.CONF_ALLOW_UNREACHABLE: True
+ }
+ }
+ }) is True
+
+ # Flow started for discovered bridge
+ assert len(mock_config_entries.flow.mock_calls) == 0
+
+ # Config stored for domain.
+ assert hass.data[hue.DOMAIN] == {
+ '0.0.0.0': {
+ hue.CONF_HOST: '0.0.0.0',
+ hue.CONF_FILENAME: 'bla.conf',
+ hue.CONF_ALLOW_HUE_GROUPS: False,
+ hue.CONF_ALLOW_UNREACHABLE: True
+ }
+ }
+
+
+async def test_setup_defined_hosts_no_known_auth(hass):
+ """Test we initiate config entry if config bridge is not known."""
+ with patch.object(hass, 'config_entries') as mock_config_entries, \
+ patch.object(hue, 'configured_hosts', return_value=[]):
+ mock_config_entries.flow.async_init.return_value = mock_coro()
+ assert await async_setup_component(hass, hue.DOMAIN, {
+ hue.DOMAIN: {
+ hue.CONF_BRIDGES: {
+ hue.CONF_HOST: '0.0.0.0',
+ hue.CONF_FILENAME: 'bla.conf',
+ hue.CONF_ALLOW_HUE_GROUPS: False,
+ hue.CONF_ALLOW_UNREACHABLE: True
+ }
+ }
+ }) is True
+
+ # Flow started for discovered bridge
+ assert len(mock_config_entries.flow.mock_calls) == 1
+ assert mock_config_entries.flow.mock_calls[0][2]['data'] == {
+ 'host': '0.0.0.0',
+ 'path': 'bla.conf',
+ }
+
+ # Config stored for domain.
+ assert hass.data[hue.DOMAIN] == {
+ '0.0.0.0': {
+ hue.CONF_HOST: '0.0.0.0',
+ hue.CONF_FILENAME: 'bla.conf',
+ hue.CONF_ALLOW_HUE_GROUPS: False,
+ hue.CONF_ALLOW_UNREACHABLE: True
+ }
+ }
+
+
+async def test_config_passed_to_config_entry(hass):
+ """Test that configured options for a host are loaded via config entry."""
+ entry = MockConfigEntry(domain=hue.DOMAIN, data={
+ 'host': '0.0.0.0',
+ })
+ entry.add_to_hass(hass)
+
+ with patch.object(hue, 'HueBridge') as mock_bridge:
+ mock_bridge.return_value.async_setup.return_value = mock_coro(True)
+ assert await async_setup_component(hass, hue.DOMAIN, {
+ hue.DOMAIN: {
+ hue.CONF_BRIDGES: {
+ hue.CONF_HOST: '0.0.0.0',
+ hue.CONF_FILENAME: 'bla.conf',
+ hue.CONF_ALLOW_HUE_GROUPS: False,
+ hue.CONF_ALLOW_UNREACHABLE: True
+ }
+ }
+ }) is True
+
+ assert len(mock_bridge.mock_calls) == 2
+ p_hass, p_entry, p_allow_unreachable, p_allow_groups = \
+ mock_bridge.mock_calls[0][1]
+
+ assert p_hass is hass
+ assert p_entry is entry
+ assert p_allow_unreachable is True
+ assert p_allow_groups is False
diff --git a/tests/components/hue/test_setup.py b/tests/components/hue/test_setup.py
deleted file mode 100644
index f90f58a50c3..00000000000
--- a/tests/components/hue/test_setup.py
+++ /dev/null
@@ -1,70 +0,0 @@
-"""Test Hue setup process."""
-from homeassistant.setup import async_setup_component
-from homeassistant.components import hue
-from homeassistant.components.discovery import SERVICE_HUE
-
-
-async def test_setup_with_multiple_hosts(hass, mock_bridge):
- """Multiple hosts specified in the config file."""
- assert await async_setup_component(hass, hue.DOMAIN, {
- hue.DOMAIN: {
- hue.CONF_BRIDGES: [
- {hue.CONF_HOST: '127.0.0.1'},
- {hue.CONF_HOST: '192.168.1.10'},
- ]
- }
- })
-
- assert len(mock_bridge.mock_calls) == 2
- hosts = sorted(mock_call[1][0] for mock_call in mock_bridge.mock_calls)
- assert hosts == ['127.0.0.1', '192.168.1.10']
-
-
-async def test_bridge_discovered(hass, mock_bridge):
- """Bridge discovery."""
- assert await async_setup_component(hass, hue.DOMAIN, {})
-
- await hass.helpers.discovery.async_discover(SERVICE_HUE, {
- 'host': '192.168.1.10',
- 'serial': '1234567',
- })
- await hass.async_block_till_done()
-
- assert len(mock_bridge.mock_calls) == 1
- assert mock_bridge.mock_calls[0][1][0] == '192.168.1.10'
-
-
-async def test_bridge_configure_and_discovered(hass, mock_bridge):
- """Bridge is in the config file, then we discover it."""
- assert await async_setup_component(hass, hue.DOMAIN, {
- hue.DOMAIN: {
- hue.CONF_BRIDGES: {
- hue.CONF_HOST: '192.168.1.10'
- }
- }
- })
-
- assert len(mock_bridge.mock_calls) == 1
- assert mock_bridge.mock_calls[0][1][0] == '192.168.1.10'
- hass.data[hue.DOMAIN] = {'192.168.1.10': {}}
-
- mock_bridge.reset_mock()
-
- await hass.helpers.discovery.async_discover(SERVICE_HUE, {
- 'host': '192.168.1.10',
- 'serial': '1234567',
- })
- await hass.async_block_till_done()
-
- assert len(mock_bridge.mock_calls) == 0
-
-
-async def test_setup_no_host(hass, aioclient_mock):
- """Check we call discovery if domain specified but no bridges."""
- aioclient_mock.get(hue.API_NUPNP, json=[])
-
- result = await async_setup_component(
- hass, hue.DOMAIN, {hue.DOMAIN: {}})
- assert result
-
- assert len(aioclient_mock.mock_calls) == 1
diff --git a/tests/components/light/test_demo.py b/tests/components/light/test_demo.py
index ff984aff221..8ba6385166b 100644
--- a/tests/components/light/test_demo.py
+++ b/tests/components/light/test_demo.py
@@ -29,15 +29,15 @@ class TestDemoLight(unittest.TestCase):
def test_state_attributes(self):
"""Test light state attributes."""
light.turn_on(
- self.hass, ENTITY_LIGHT, xy_color=(.4, .6), brightness=25)
+ self.hass, ENTITY_LIGHT, xy_color=(.4, .4), brightness=25)
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_LIGHT)
self.assertTrue(light.is_on(self.hass, ENTITY_LIGHT))
- self.assertEqual((0.378, 0.574), state.attributes.get(
+ self.assertEqual((0.4, 0.4), state.attributes.get(
light.ATTR_XY_COLOR))
self.assertEqual(25, state.attributes.get(light.ATTR_BRIGHTNESS))
self.assertEqual(
- (207, 255, 0), state.attributes.get(light.ATTR_RGB_COLOR))
+ (255, 234, 164), state.attributes.get(light.ATTR_RGB_COLOR))
self.assertEqual('rainbow', state.attributes.get(light.ATTR_EFFECT))
light.turn_on(
self.hass, ENTITY_LIGHT, rgb_color=(251, 253, 255),
@@ -48,12 +48,12 @@ class TestDemoLight(unittest.TestCase):
self.assertEqual(
(250, 252, 255), state.attributes.get(light.ATTR_RGB_COLOR))
self.assertEqual(
- (0.316, 0.333), state.attributes.get(light.ATTR_XY_COLOR))
+ (0.319, 0.326), state.attributes.get(light.ATTR_XY_COLOR))
light.turn_on(self.hass, ENTITY_LIGHT, color_temp=400, effect='none')
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_LIGHT)
self.assertEqual(400, state.attributes.get(light.ATTR_COLOR_TEMP))
- self.assertEqual(154, state.attributes.get(light.ATTR_MIN_MIREDS))
+ self.assertEqual(153, state.attributes.get(light.ATTR_MIN_MIREDS))
self.assertEqual(500, state.attributes.get(light.ATTR_MAX_MIREDS))
self.assertEqual('none', state.attributes.get(light.ATTR_EFFECT))
light.turn_on(self.hass, ENTITY_LIGHT, kelvin=3000, brightness_pct=50)
diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py
index d73531b1b9a..7b6c3a21a79 100644
--- a/tests/components/light/test_hue.py
+++ b/tests/components/light/test_hue.py
@@ -160,7 +160,13 @@ LIGHT_RESPONSE = {
@pytest.fixture
def mock_bridge(hass):
"""Mock a Hue bridge."""
- bridge = Mock(available=True, allow_groups=False, host='1.1.1.1')
+ bridge = Mock(
+ available=True,
+ allow_unreachable=False,
+ allow_groups=False,
+ api=Mock(),
+ spec=hue.HueBridge
+ )
bridge.mock_requests = []
# We're using a deque so we can schedule multiple responses
# and also means that `popleft()` will blow up if we get more updates
diff --git a/tests/components/light/test_mqtt.py b/tests/components/light/test_mqtt.py
index 71fe77ef6be..7f7841b1a69 100644
--- a/tests/components/light/test_mqtt.py
+++ b/tests/components/light/test_mqtt.py
@@ -255,7 +255,7 @@ class TestLightMQTT(unittest.TestCase):
self.assertEqual(150, state.attributes.get('color_temp'))
self.assertEqual('none', state.attributes.get('effect'))
self.assertEqual(255, state.attributes.get('white_value'))
- self.assertEqual((0.32, 0.336), state.attributes.get('xy_color'))
+ self.assertEqual((0.323, 0.329), state.attributes.get('xy_color'))
fire_mqtt_message(self.hass, 'test_light_rgb/status', '0')
self.hass.block_till_done()
@@ -311,7 +311,7 @@ class TestLightMQTT(unittest.TestCase):
self.hass.block_till_done()
light_state = self.hass.states.get('light.test')
- self.assertEqual((0.652, 0.343),
+ self.assertEqual((0.672, 0.324),
light_state.attributes.get('xy_color'))
def test_brightness_controlling_scale(self):
@@ -519,7 +519,7 @@ class TestLightMQTT(unittest.TestCase):
mock.call('test_light_rgb/rgb/set', '50,50,50', 2, False),
mock.call('test_light_rgb/brightness/set', 50, 2, False),
mock.call('test_light_rgb/white_value/set', 80, 2, False),
- mock.call('test_light_rgb/xy/set', '0.32,0.336', 2, False),
+ mock.call('test_light_rgb/xy/set', '0.323,0.329', 2, False),
], any_order=True)
state = self.hass.states.get('light.test')
@@ -527,7 +527,7 @@ class TestLightMQTT(unittest.TestCase):
self.assertEqual((255, 255, 255), state.attributes['rgb_color'])
self.assertEqual(50, state.attributes['brightness'])
self.assertEqual(80, state.attributes['white_value'])
- self.assertEqual((0.32, 0.336), state.attributes['xy_color'])
+ self.assertEqual((0.323, 0.329), state.attributes['xy_color'])
def test_sending_mqtt_rgb_command_with_template(self):
"""Test the sending of RGB command with template."""
@@ -679,7 +679,7 @@ class TestLightMQTT(unittest.TestCase):
state = self.hass.states.get('light.test')
self.assertEqual(STATE_ON, state.state)
- self.assertEqual((0.32, 0.336), state.attributes.get('xy_color'))
+ self.assertEqual((0.323, 0.329), state.attributes.get('xy_color'))
def test_on_command_first(self):
"""Test on command being sent before brightness."""
diff --git a/tests/components/light/test_mqtt_json.py b/tests/components/light/test_mqtt_json.py
index a183355fbb3..d6835b00be0 100644
--- a/tests/components/light/test_mqtt_json.py
+++ b/tests/components/light/test_mqtt_json.py
@@ -206,7 +206,7 @@ class TestLightMQTTJSON(unittest.TestCase):
self.assertEqual(155, state.attributes.get('color_temp'))
self.assertEqual('colorloop', state.attributes.get('effect'))
self.assertEqual(150, state.attributes.get('white_value'))
- self.assertEqual((0.32, 0.336), state.attributes.get('xy_color'))
+ self.assertEqual((0.323, 0.329), state.attributes.get('xy_color'))
# Turn the light off
fire_mqtt_message(self.hass, 'test_light_rgb', '{"state":"OFF"}')
diff --git a/tests/components/sensor/test_canary.py b/tests/components/sensor/test_canary.py
index 79e2bf4ee35..346929a4685 100644
--- a/tests/components/sensor/test_canary.py
+++ b/tests/components/sensor/test_canary.py
@@ -40,9 +40,9 @@ class TestCanarySensorSetup(unittest.TestCase):
def test_setup_sensors(self):
"""Test the sensor setup."""
- online_device_at_home = mock_device(20, "Dining Room", True)
- offline_device_at_home = mock_device(21, "Front Yard", False)
- online_device_at_work = mock_device(22, "Office", True)
+ online_device_at_home = mock_device(20, "Dining Room", True, "Canary")
+ offline_device_at_home = mock_device(21, "Front Yard", False, "Canary")
+ online_device_at_work = mock_device(22, "Office", True, "Canary")
self.hass.data[DATA_CANARY] = Mock()
self.hass.data[DATA_CANARY].locations = [
@@ -57,7 +57,7 @@ class TestCanarySensorSetup(unittest.TestCase):
def test_temperature_sensor(self):
"""Test temperature sensor with fahrenheit."""
- device = mock_device(10, "Family Room")
+ device = mock_device(10, "Family Room", "Canary")
location = mock_location("Home", False)
data = Mock()
@@ -69,10 +69,11 @@ class TestCanarySensorSetup(unittest.TestCase):
self.assertEqual("Home Family Room Temperature", sensor.name)
self.assertEqual("°C", sensor.unit_of_measurement)
self.assertEqual(21.12, sensor.state)
+ self.assertEqual("mdi:thermometer", sensor.icon)
def test_temperature_sensor_with_none_sensor_value(self):
"""Test temperature sensor with fahrenheit."""
- device = mock_device(10, "Family Room")
+ device = mock_device(10, "Family Room", "Canary")
location = mock_location("Home", False)
data = Mock()
@@ -85,7 +86,7 @@ class TestCanarySensorSetup(unittest.TestCase):
def test_humidity_sensor(self):
"""Test humidity sensor."""
- device = mock_device(10, "Family Room")
+ device = mock_device(10, "Family Room", "Canary")
location = mock_location("Home")
data = Mock()
@@ -97,10 +98,11 @@ class TestCanarySensorSetup(unittest.TestCase):
self.assertEqual("Home Family Room Humidity", sensor.name)
self.assertEqual("%", sensor.unit_of_measurement)
self.assertEqual(50.46, sensor.state)
+ self.assertEqual("mdi:water-percent", sensor.icon)
def test_air_quality_sensor_with_very_abnormal_reading(self):
"""Test air quality sensor."""
- device = mock_device(10, "Family Room")
+ device = mock_device(10, "Family Room", "Canary")
location = mock_location("Home")
data = Mock()
@@ -112,13 +114,14 @@ class TestCanarySensorSetup(unittest.TestCase):
self.assertEqual("Home Family Room Air Quality", sensor.name)
self.assertEqual(None, sensor.unit_of_measurement)
self.assertEqual(0.4, sensor.state)
+ self.assertEqual("mdi:weather-windy", sensor.icon)
air_quality = sensor.device_state_attributes[ATTR_AIR_QUALITY]
self.assertEqual(STATE_AIR_QUALITY_VERY_ABNORMAL, air_quality)
def test_air_quality_sensor_with_abnormal_reading(self):
"""Test air quality sensor."""
- device = mock_device(10, "Family Room")
+ device = mock_device(10, "Family Room", "Canary")
location = mock_location("Home")
data = Mock()
@@ -130,13 +133,14 @@ class TestCanarySensorSetup(unittest.TestCase):
self.assertEqual("Home Family Room Air Quality", sensor.name)
self.assertEqual(None, sensor.unit_of_measurement)
self.assertEqual(0.59, sensor.state)
+ self.assertEqual("mdi:weather-windy", sensor.icon)
air_quality = sensor.device_state_attributes[ATTR_AIR_QUALITY]
self.assertEqual(STATE_AIR_QUALITY_ABNORMAL, air_quality)
def test_air_quality_sensor_with_normal_reading(self):
"""Test air quality sensor."""
- device = mock_device(10, "Family Room")
+ device = mock_device(10, "Family Room", "Canary")
location = mock_location("Home")
data = Mock()
@@ -148,13 +152,14 @@ class TestCanarySensorSetup(unittest.TestCase):
self.assertEqual("Home Family Room Air Quality", sensor.name)
self.assertEqual(None, sensor.unit_of_measurement)
self.assertEqual(1.0, sensor.state)
+ self.assertEqual("mdi:weather-windy", sensor.icon)
air_quality = sensor.device_state_attributes[ATTR_AIR_QUALITY]
self.assertEqual(STATE_AIR_QUALITY_NORMAL, air_quality)
def test_air_quality_sensor_with_none_sensor_value(self):
"""Test air quality sensor."""
- device = mock_device(10, "Family Room")
+ device = mock_device(10, "Family Room", "Canary")
location = mock_location("Home")
data = Mock()
@@ -165,3 +170,35 @@ class TestCanarySensorSetup(unittest.TestCase):
self.assertEqual(None, sensor.state)
self.assertEqual(None, sensor.device_state_attributes)
+
+ def test_battery_sensor(self):
+ """Test battery sensor."""
+ device = mock_device(10, "Family Room", "Canary Flex")
+ location = mock_location("Home")
+
+ data = Mock()
+ data.get_reading.return_value = 70.4567
+
+ sensor = CanarySensor(data, SENSOR_TYPES[4], location, device)
+ sensor.update()
+
+ self.assertEqual("Home Family Room Battery", sensor.name)
+ self.assertEqual("%", sensor.unit_of_measurement)
+ self.assertEqual(70.46, sensor.state)
+ self.assertEqual("mdi:battery-70", sensor.icon)
+
+ def test_wifi_sensor(self):
+ """Test battery sensor."""
+ device = mock_device(10, "Family Room", "Canary Flex")
+ location = mock_location("Home")
+
+ data = Mock()
+ data.get_reading.return_value = -57
+
+ sensor = CanarySensor(data, SENSOR_TYPES[3], location, device)
+ sensor.update()
+
+ self.assertEqual("Home Family Room Wifi", sensor.name)
+ self.assertEqual("dBm", sensor.unit_of_measurement)
+ self.assertEqual(-57, sensor.state)
+ self.assertEqual("mdi:wifi", sensor.icon)
diff --git a/tests/components/sensor/test_file.py b/tests/components/sensor/test_file.py
index aa048f7a62e..7171289de69 100644
--- a/tests/components/sensor/test_file.py
+++ b/tests/components/sensor/test_file.py
@@ -18,6 +18,8 @@ class TestFileSensor(unittest.TestCase):
def setup_method(self, method):
"""Set up things to be run when tests are started."""
self.hass = get_test_home_assistant()
+ # Patch out 'is_allowed_path' as the mock files aren't allowed
+ self.hass.config.is_allowed_path = Mock(return_value=True)
mock_registry(self.hass)
def teardown_method(self, method):
diff --git a/tests/components/switch/test_flux.py b/tests/components/switch/test_flux.py
index a1e600860f9..c42061db958 100644
--- a/tests/components/switch/test_flux.py
+++ b/tests/components/switch/test_flux.py
@@ -154,8 +154,8 @@ class TestSwitchFlux(unittest.TestCase):
fire_time_changed(self.hass, test_time)
self.hass.block_till_done()
call = turn_on_calls[-1]
- self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 119)
- self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.591, 0.395])
+ self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 112)
+ self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.606, 0.379])
# pylint: disable=invalid-name
def test_flux_after_sunrise_before_sunset(self):
@@ -201,8 +201,8 @@ class TestSwitchFlux(unittest.TestCase):
fire_time_changed(self.hass, test_time)
self.hass.block_till_done()
call = turn_on_calls[-1]
- self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 180)
- self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.431, 0.38])
+ self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 173)
+ self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.439, 0.37])
# pylint: disable=invalid-name
def test_flux_after_sunset_before_stop(self):
@@ -249,8 +249,8 @@ class TestSwitchFlux(unittest.TestCase):
fire_time_changed(self.hass, test_time)
self.hass.block_till_done()
call = turn_on_calls[-1]
- self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 153)
- self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.496, 0.397])
+ self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 146)
+ self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.506, 0.385])
# pylint: disable=invalid-name
def test_flux_after_stop_before_sunrise(self):
@@ -296,8 +296,8 @@ class TestSwitchFlux(unittest.TestCase):
fire_time_changed(self.hass, test_time)
self.hass.block_till_done()
call = turn_on_calls[-1]
- self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 119)
- self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.591, 0.395])
+ self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 112)
+ self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.606, 0.379])
# pylint: disable=invalid-name
def test_flux_with_custom_start_stop_times(self):
@@ -345,8 +345,8 @@ class TestSwitchFlux(unittest.TestCase):
fire_time_changed(self.hass, test_time)
self.hass.block_till_done()
call = turn_on_calls[-1]
- self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 154)
- self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.494, 0.397])
+ self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 147)
+ self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.504, 0.385])
def test_flux_before_sunrise_stop_next_day(self):
"""Test the flux switch before sunrise.
@@ -395,8 +395,8 @@ class TestSwitchFlux(unittest.TestCase):
fire_time_changed(self.hass, test_time)
self.hass.block_till_done()
call = turn_on_calls[-1]
- self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 119)
- self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.591, 0.395])
+ self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 112)
+ self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.606, 0.379])
# pylint: disable=invalid-name
def test_flux_after_sunrise_before_sunset_stop_next_day(self):
@@ -447,8 +447,8 @@ class TestSwitchFlux(unittest.TestCase):
fire_time_changed(self.hass, test_time)
self.hass.block_till_done()
call = turn_on_calls[-1]
- self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 180)
- self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.431, 0.38])
+ self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 173)
+ self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.439, 0.37])
# pylint: disable=invalid-name
def test_flux_after_sunset_before_midnight_stop_next_day(self):
@@ -498,8 +498,8 @@ class TestSwitchFlux(unittest.TestCase):
fire_time_changed(self.hass, test_time)
self.hass.block_till_done()
call = turn_on_calls[-1]
- self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 126)
- self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.574, 0.401])
+ self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 119)
+ self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.588, 0.386])
# pylint: disable=invalid-name
def test_flux_after_sunset_after_midnight_stop_next_day(self):
@@ -549,8 +549,8 @@ class TestSwitchFlux(unittest.TestCase):
fire_time_changed(self.hass, test_time)
self.hass.block_till_done()
call = turn_on_calls[-1]
- self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 122)
- self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.586, 0.397])
+ self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 114)
+ self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.601, 0.382])
# pylint: disable=invalid-name
def test_flux_after_stop_before_sunrise_stop_next_day(self):
@@ -600,8 +600,8 @@ class TestSwitchFlux(unittest.TestCase):
fire_time_changed(self.hass, test_time)
self.hass.block_till_done()
call = turn_on_calls[-1]
- self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 119)
- self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.591, 0.395])
+ self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 112)
+ self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.606, 0.379])
# pylint: disable=invalid-name
def test_flux_with_custom_colortemps(self):
@@ -650,8 +650,8 @@ class TestSwitchFlux(unittest.TestCase):
fire_time_changed(self.hass, test_time)
self.hass.block_till_done()
call = turn_on_calls[-1]
- self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 167)
- self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.461, 0.389])
+ self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 159)
+ self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.469, 0.378])
# pylint: disable=invalid-name
def test_flux_with_custom_brightness(self):
@@ -700,7 +700,7 @@ class TestSwitchFlux(unittest.TestCase):
self.hass.block_till_done()
call = turn_on_calls[-1]
self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 255)
- self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.496, 0.397])
+ self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.506, 0.385])
def test_flux_with_multiple_lights(self):
"""Test the flux switch with multiple light entities."""
@@ -762,14 +762,14 @@ class TestSwitchFlux(unittest.TestCase):
fire_time_changed(self.hass, test_time)
self.hass.block_till_done()
call = turn_on_calls[-1]
- self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 171)
- self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.452, 0.386])
+ self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 163)
+ self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.46, 0.376])
call = turn_on_calls[-2]
- self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 171)
- self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.452, 0.386])
+ self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 163)
+ self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.46, 0.376])
call = turn_on_calls[-3]
- self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 171)
- self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.452, 0.386])
+ self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 163)
+ self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.46, 0.376])
def test_flux_with_mired(self):
"""Test the flux switch´s mode mired."""
diff --git a/tests/components/test_api.py b/tests/components/test_api.py
index 6d5bec046f1..c9dae27d14c 100644
--- a/tests/components/test_api.py
+++ b/tests/components/test_api.py
@@ -2,13 +2,18 @@
# pylint: disable=protected-access
import asyncio
import json
+from unittest.mock import patch
+from aiohttp import web
import pytest
from homeassistant import const
+from homeassistant.bootstrap import DATA_LOGGING
import homeassistant.core as ha
from homeassistant.setup import async_setup_component
+from tests.common import mock_coro
+
@pytest.fixture
def mock_api_client(hass, aiohttp_client):
@@ -398,3 +403,31 @@ def _stream_next_event(stream):
def _listen_count(hass):
"""Return number of event listeners."""
return sum(hass.bus.async_listeners().values())
+
+
+async def test_api_error_log(hass, aiohttp_client):
+ """Test if we can fetch the error log."""
+ hass.data[DATA_LOGGING] = '/some/path'
+ await async_setup_component(hass, 'api', {
+ 'http': {
+ 'api_password': 'yolo'
+ }
+ })
+ client = await aiohttp_client(hass.http.app)
+
+ resp = await client.get(const.URL_API_ERROR_LOG)
+ # Verufy auth required
+ assert resp.status == 401
+
+ with patch(
+ 'homeassistant.components.http.view.HomeAssistantView.file',
+ return_value=mock_coro(web.Response(status=200, text='Hello'))
+ ) as mock_file:
+ resp = await client.get(const.URL_API_ERROR_LOG, headers={
+ 'x-ha-access': 'yolo'
+ })
+
+ assert len(mock_file.mock_calls) == 1
+ assert mock_file.mock_calls[0][1][1] == hass.data[DATA_LOGGING]
+ assert resp.status == 200
+ assert await resp.text() == 'Hello'
diff --git a/tests/components/test_canary.py b/tests/components/test_canary.py
index 2c496c26e11..310f3be9f05 100644
--- a/tests/components/test_canary.py
+++ b/tests/components/test_canary.py
@@ -8,12 +8,16 @@ from tests.common import (
get_test_home_assistant)
-def mock_device(device_id, name, is_online=True):
+def mock_device(device_id, name, is_online=True, device_type_name=None):
"""Mock Canary Device class."""
device = MagicMock()
type(device).device_id = PropertyMock(return_value=device_id)
type(device).name = PropertyMock(return_value=name)
type(device).is_online = PropertyMock(return_value=is_online)
+ type(device).device_type = PropertyMock(return_value={
+ "id": 1,
+ "name": device_type_name,
+ })
return device
diff --git a/tests/components/test_config_entry_example.py b/tests/components/test_config_entry_example.py
deleted file mode 100644
index 31084384c31..00000000000
--- a/tests/components/test_config_entry_example.py
+++ /dev/null
@@ -1,38 +0,0 @@
-"""Test the config entry example component."""
-import asyncio
-
-from homeassistant import config_entries
-
-
-@asyncio.coroutine
-def test_flow_works(hass):
- """Test that the config flow works."""
- result = yield from hass.config_entries.flow.async_init(
- 'config_entry_example')
-
- assert result['type'] == config_entries.RESULT_TYPE_FORM
-
- result = yield from hass.config_entries.flow.async_configure(
- result['flow_id'], {
- 'object_id': 'bla'
- })
-
- assert result['type'] == config_entries.RESULT_TYPE_FORM
-
- result = yield from hass.config_entries.flow.async_configure(
- result['flow_id'], {
- 'name': 'Hello'
- })
-
- assert result['type'] == config_entries.RESULT_TYPE_CREATE_ENTRY
- state = hass.states.get('config_entry_example.bla')
- assert state is not None
- assert state.name == 'Hello'
- assert 'config_entry_example' in hass.config.components
- assert len(hass.config_entries.async_entries()) == 1
-
- # Test removing entry.
- entry = hass.config_entries.async_entries()[0]
- yield from hass.config_entries.async_remove(entry.entry_id)
- state = hass.states.get('config_entry_example.bla')
- assert state is None
diff --git a/tests/components/test_conversation.py b/tests/components/test_conversation.py
index bde00e10928..d9c29cdae83 100644
--- a/tests/components/test_conversation.py
+++ b/tests/components/test_conversation.py
@@ -1,26 +1,24 @@
"""The tests for the Conversation component."""
# pylint: disable=protected-access
-import asyncio
-
import pytest
from homeassistant.setup import async_setup_component
from homeassistant.components import conversation
import homeassistant.components as component
+from homeassistant.components.cover import (SERVICE_OPEN_COVER)
from homeassistant.helpers import intent
from tests.common import async_mock_intent, async_mock_service
-@asyncio.coroutine
-def test_calling_intent(hass):
+async def test_calling_intent(hass):
"""Test calling an intent from a conversation."""
intents = async_mock_intent(hass, 'OrderBeer')
- result = yield from component.async_setup(hass, {})
+ result = await component.async_setup(hass, {})
assert result
- result = yield from async_setup_component(hass, 'conversation', {
+ result = await async_setup_component(hass, 'conversation', {
'conversation': {
'intents': {
'OrderBeer': [
@@ -31,11 +29,11 @@ def test_calling_intent(hass):
})
assert result
- yield from hass.services.async_call(
+ await hass.services.async_call(
'conversation', 'process', {
conversation.ATTR_TEXT: 'I would like the Grolsch beer'
})
- yield from hass.async_block_till_done()
+ await hass.async_block_till_done()
assert len(intents) == 1
intent = intents[0]
@@ -45,8 +43,7 @@ def test_calling_intent(hass):
assert intent.text_input == 'I would like the Grolsch beer'
-@asyncio.coroutine
-def test_register_before_setup(hass):
+async def test_register_before_setup(hass):
"""Test calling an intent from a conversation."""
intents = async_mock_intent(hass, 'OrderBeer')
@@ -54,7 +51,7 @@ def test_register_before_setup(hass):
'A {type} beer, please'
])
- result = yield from async_setup_component(hass, 'conversation', {
+ result = await async_setup_component(hass, 'conversation', {
'conversation': {
'intents': {
'OrderBeer': [
@@ -65,11 +62,11 @@ def test_register_before_setup(hass):
})
assert result
- yield from hass.services.async_call(
+ await hass.services.async_call(
'conversation', 'process', {
conversation.ATTR_TEXT: 'A Grolsch beer, please'
})
- yield from hass.async_block_till_done()
+ await hass.async_block_till_done()
assert len(intents) == 1
intent = intents[0]
@@ -78,11 +75,11 @@ def test_register_before_setup(hass):
assert intent.slots == {'type': {'value': 'Grolsch'}}
assert intent.text_input == 'A Grolsch beer, please'
- yield from hass.services.async_call(
+ await hass.services.async_call(
'conversation', 'process', {
conversation.ATTR_TEXT: 'I would like the Grolsch beer'
})
- yield from hass.async_block_till_done()
+ await hass.async_block_till_done()
assert len(intents) == 2
intent = intents[1]
@@ -92,14 +89,14 @@ def test_register_before_setup(hass):
assert intent.text_input == 'I would like the Grolsch beer'
-@asyncio.coroutine
-def test_http_processing_intent(hass, aiohttp_client):
+async def test_http_processing_intent(hass, test_client):
"""Test processing intent via HTTP API."""
class TestIntentHandler(intent.IntentHandler):
+ """Test Intent Handler."""
+
intent_type = 'OrderBeer'
- @asyncio.coroutine
- def async_handle(self, intent):
+ async def async_handle(self, intent):
"""Handle the intent."""
response = intent.create_response()
response.async_set_speech(
@@ -111,7 +108,7 @@ def test_http_processing_intent(hass, aiohttp_client):
intent.async_register(hass, TestIntentHandler())
- result = yield from async_setup_component(hass, 'conversation', {
+ result = await async_setup_component(hass, 'conversation', {
'conversation': {
'intents': {
'OrderBeer': [
@@ -122,13 +119,13 @@ def test_http_processing_intent(hass, aiohttp_client):
})
assert result
- client = yield from aiohttp_client(hass.http.app)
- resp = yield from client.post('/api/conversation/process', json={
+ client = await test_client(hass.http.app)
+ resp = await client.post('/api/conversation/process', json={
'text': 'I would like the Grolsch beer'
})
assert resp.status == 200
- data = yield from resp.json()
+ data = await resp.json()
assert data == {
'card': {
@@ -145,24 +142,23 @@ def test_http_processing_intent(hass, aiohttp_client):
}
-@asyncio.coroutine
@pytest.mark.parametrize('sentence', ('turn on kitchen', 'turn kitchen on'))
-def test_turn_on_intent(hass, sentence):
+async def test_turn_on_intent(hass, sentence):
"""Test calling the turn on intent."""
- result = yield from component.async_setup(hass, {})
+ result = await component.async_setup(hass, {})
assert result
- result = yield from async_setup_component(hass, 'conversation', {})
+ result = await async_setup_component(hass, 'conversation', {})
assert result
hass.states.async_set('light.kitchen', 'off')
calls = async_mock_service(hass, 'homeassistant', 'turn_on')
- yield from hass.services.async_call(
+ await hass.services.async_call(
'conversation', 'process', {
conversation.ATTR_TEXT: sentence
})
- yield from hass.async_block_till_done()
+ await hass.async_block_till_done()
assert len(calls) == 1
call = calls[0]
@@ -171,24 +167,49 @@ def test_turn_on_intent(hass, sentence):
assert call.data == {'entity_id': 'light.kitchen'}
-@asyncio.coroutine
-@pytest.mark.parametrize('sentence', ('turn off kitchen', 'turn kitchen off'))
-def test_turn_off_intent(hass, sentence):
- """Test calling the turn on intent."""
- result = yield from component.async_setup(hass, {})
+async def test_cover_intents_loading(hass):
+ """Test Cover Intents Loading."""
+ with pytest.raises(intent.UnknownIntent):
+ await intent.async_handle(
+ hass, 'test', 'HassOpenCover', {'name': {'value': 'garage door'}}
+ )
+
+ result = await async_setup_component(hass, 'cover', {})
assert result
- result = yield from async_setup_component(hass, 'conversation', {})
+ hass.states.async_set('cover.garage_door', 'closed')
+ calls = async_mock_service(hass, 'cover', SERVICE_OPEN_COVER)
+
+ response = await intent.async_handle(
+ hass, 'test', 'HassOpenCover', {'name': {'value': 'garage door'}}
+ )
+ await hass.async_block_till_done()
+
+ assert response.speech['plain']['speech'] == 'Opened garage door'
+ assert len(calls) == 1
+ call = calls[0]
+ assert call.domain == 'cover'
+ assert call.service == 'open_cover'
+ assert call.data == {'entity_id': 'cover.garage_door'}
+
+
+@pytest.mark.parametrize('sentence', ('turn off kitchen', 'turn kitchen off'))
+async def test_turn_off_intent(hass, sentence):
+ """Test calling the turn on intent."""
+ result = await component.async_setup(hass, {})
+ assert result
+
+ result = await async_setup_component(hass, 'conversation', {})
assert result
hass.states.async_set('light.kitchen', 'on')
calls = async_mock_service(hass, 'homeassistant', 'turn_off')
- yield from hass.services.async_call(
+ await hass.services.async_call(
'conversation', 'process', {
conversation.ATTR_TEXT: sentence
})
- yield from hass.async_block_till_done()
+ await hass.async_block_till_done()
assert len(calls) == 1
call = calls[0]
@@ -197,24 +218,23 @@ def test_turn_off_intent(hass, sentence):
assert call.data == {'entity_id': 'light.kitchen'}
-@asyncio.coroutine
@pytest.mark.parametrize('sentence', ('toggle kitchen', 'kitchen toggle'))
-def test_toggle_intent(hass, sentence):
+async def test_toggle_intent(hass, sentence):
"""Test calling the turn on intent."""
- result = yield from component.async_setup(hass, {})
+ result = await component.async_setup(hass, {})
assert result
- result = yield from async_setup_component(hass, 'conversation', {})
+ result = await async_setup_component(hass, 'conversation', {})
assert result
hass.states.async_set('light.kitchen', 'on')
calls = async_mock_service(hass, 'homeassistant', 'toggle')
- yield from hass.services.async_call(
+ await hass.services.async_call(
'conversation', 'process', {
conversation.ATTR_TEXT: sentence
})
- yield from hass.async_block_till_done()
+ await hass.async_block_till_done()
assert len(calls) == 1
call = calls[0]
@@ -223,20 +243,19 @@ def test_toggle_intent(hass, sentence):
assert call.data == {'entity_id': 'light.kitchen'}
-@asyncio.coroutine
-def test_http_api(hass, aiohttp_client):
+async def test_http_api(hass, test_client):
"""Test the HTTP conversation API."""
- result = yield from component.async_setup(hass, {})
+ result = await component.async_setup(hass, {})
assert result
- result = yield from async_setup_component(hass, 'conversation', {})
+ result = await async_setup_component(hass, 'conversation', {})
assert result
- client = yield from aiohttp_client(hass.http.app)
+ client = await test_client(hass.http.app)
hass.states.async_set('light.kitchen', 'off')
calls = async_mock_service(hass, 'homeassistant', 'turn_on')
- resp = yield from client.post('/api/conversation/process', json={
+ resp = await client.post('/api/conversation/process', json={
'text': 'Turn the kitchen on'
})
assert resp.status == 200
@@ -248,23 +267,22 @@ def test_http_api(hass, aiohttp_client):
assert call.data == {'entity_id': 'light.kitchen'}
-@asyncio.coroutine
-def test_http_api_wrong_data(hass, aiohttp_client):
+async def test_http_api_wrong_data(hass, test_client):
"""Test the HTTP conversation API."""
- result = yield from component.async_setup(hass, {})
+ result = await component.async_setup(hass, {})
assert result
- result = yield from async_setup_component(hass, 'conversation', {})
+ result = await async_setup_component(hass, 'conversation', {})
assert result
- client = yield from aiohttp_client(hass.http.app)
+ client = await test_client(hass.http.app)
- resp = yield from client.post('/api/conversation/process', json={
+ resp = await client.post('/api/conversation/process', json={
'text': 123
})
assert resp.status == 400
- resp = yield from client.post('/api/conversation/process', json={
+ resp = await client.post('/api/conversation/process', json={
})
assert resp.status == 400
diff --git a/tests/components/test_deconz.py b/tests/components/test_deconz.py
new file mode 100644
index 00000000000..2c7c656d560
--- /dev/null
+++ b/tests/components/test_deconz.py
@@ -0,0 +1,97 @@
+"""Tests for deCONZ config flow."""
+import pytest
+
+import voluptuous as vol
+
+import homeassistant.components.deconz as deconz
+import pydeconz
+
+
+async def test_flow_works(hass, aioclient_mock):
+ """Test config flow."""
+ aioclient_mock.get(pydeconz.utils.URL_DISCOVER, json=[
+ {'id': 'id', 'internalipaddress': '1.2.3.4', 'internalport': '80'}
+ ])
+ aioclient_mock.post('http://1.2.3.4:80/api', json=[
+ {"success": {"username": "1234567890ABCDEF"}}
+ ])
+
+ flow = deconz.DeconzFlowHandler()
+ flow.hass = hass
+ await flow.async_step_init()
+ result = await flow.async_step_link(user_input={})
+
+ assert result['type'] == 'create_entry'
+ assert result['title'] == 'deCONZ'
+ assert result['data'] == {
+ 'bridgeid': 'id',
+ 'host': '1.2.3.4',
+ 'port': '80',
+ 'api_key': '1234567890ABCDEF'
+ }
+
+
+async def test_flow_already_registered_bridge(hass, aioclient_mock):
+ """Test config flow don't allow more than one bridge to be registered."""
+ flow = deconz.DeconzFlowHandler()
+ flow.hass = hass
+ flow.hass.data[deconz.DOMAIN] = True
+
+ result = await flow.async_step_init()
+ assert result['type'] == 'abort'
+
+
+async def test_flow_no_discovered_bridges(hass, aioclient_mock):
+ """Test config flow discovers no bridges."""
+ aioclient_mock.get(pydeconz.utils.URL_DISCOVER, json=[])
+ flow = deconz.DeconzFlowHandler()
+ flow.hass = hass
+
+ result = await flow.async_step_init()
+ assert result['type'] == 'abort'
+
+
+async def test_flow_one_bridge_discovered(hass, aioclient_mock):
+ """Test config flow discovers one bridge."""
+ aioclient_mock.get(pydeconz.utils.URL_DISCOVER, json=[
+ {'id': 'id', 'internalipaddress': '1.2.3.4', 'internalport': '80'}
+ ])
+ flow = deconz.DeconzFlowHandler()
+ flow.hass = hass
+
+ result = await flow.async_step_init()
+ assert result['type'] == 'form'
+ assert result['step_id'] == 'link'
+
+
+async def test_flow_two_bridges_discovered(hass, aioclient_mock):
+ """Test config flow discovers two bridges."""
+ aioclient_mock.get(pydeconz.utils.URL_DISCOVER, json=[
+ {'id': 'id1', 'internalipaddress': '1.2.3.4', 'internalport': '80'},
+ {'id': 'id2', 'internalipaddress': '5.6.7.8', 'internalport': '80'}
+ ])
+ flow = deconz.DeconzFlowHandler()
+ flow.hass = hass
+
+ result = await flow.async_step_init()
+ assert result['type'] == 'form'
+ assert result['step_id'] == 'init'
+
+ with pytest.raises(vol.Invalid):
+ assert result['data_schema']({'host': '0.0.0.0'})
+
+ result['data_schema']({'host': '1.2.3.4'})
+ result['data_schema']({'host': '5.6.7.8'})
+
+
+async def test_flow_no_api_key(hass, aioclient_mock):
+ """Test config flow discovers no bridges."""
+ aioclient_mock.post('http://1.2.3.4:80/api', json=[])
+ flow = deconz.DeconzFlowHandler()
+ flow.hass = hass
+ flow.deconz_config = {'host': '1.2.3.4', 'port': 80}
+
+ result = await flow.async_step_link(user_input={})
+ assert result['type'] == 'form'
+ assert result['step_id'] == 'link'
+ assert result['errors'] == {'base': 'no_key'}
diff --git a/tests/components/test_discovery.py b/tests/components/test_discovery.py
index 580d876982d..b4c80bf3210 100644
--- a/tests/components/test_discovery.py
+++ b/tests/components/test_discovery.py
@@ -5,6 +5,7 @@ from unittest.mock import patch, MagicMock
import pytest
+from homeassistant import config_entries
from homeassistant.bootstrap import async_setup_component
from homeassistant.components import discovery
from homeassistant.util.dt import utcnow
@@ -44,13 +45,12 @@ def netdisco_mock():
yield
-@asyncio.coroutine
-def mock_discovery(hass, discoveries, config=BASE_CONFIG):
+async def mock_discovery(hass, discoveries, config=BASE_CONFIG):
"""Helper to mock discoveries."""
- result = yield from async_setup_component(hass, 'discovery', config)
+ result = await async_setup_component(hass, 'discovery', config)
assert result
- yield from hass.async_start()
+ await hass.async_start()
with patch.object(discovery, '_discover', discoveries), \
patch('homeassistant.components.discovery.async_discover',
@@ -59,8 +59,8 @@ def mock_discovery(hass, discoveries, config=BASE_CONFIG):
return_value=mock_coro()) as mock_platform:
async_fire_time_changed(hass, utcnow())
# Work around an issue where our loop.call_soon not get caught
- yield from hass.async_block_till_done()
- yield from hass.async_block_till_done()
+ await hass.async_block_till_done()
+ await hass.async_block_till_done()
return mock_discover, mock_platform
@@ -154,3 +154,25 @@ def test_load_component_hassio(hass):
yield from mock_discovery(hass, discover)
assert mock_hassio.called
+
+
+async def test_discover_config_flow(hass):
+ """Test discovery triggering a config flow."""
+ discovery_info = {
+ 'hello': 'world'
+ }
+
+ def discover(netdisco):
+ """Fake discovery."""
+ return [('mock-service', discovery_info)]
+
+ with patch.dict(discovery.CONFIG_ENTRY_HANDLERS, {
+ 'mock-service': 'mock-component'}), patch(
+ 'homeassistant.config_entries.FlowManager.async_init') as m_init:
+ await mock_discovery(hass, discover)
+
+ assert len(m_init.mock_calls) == 1
+ args, kwargs = m_init.mock_calls[0][1:]
+ assert args == ('mock-component',)
+ assert kwargs['source'] == config_entries.SOURCE_DISCOVERY
+ assert kwargs['data'] == discovery_info
diff --git a/tests/components/test_folder_watcher.py b/tests/components/test_folder_watcher.py
new file mode 100644
index 00000000000..16ec7a58a02
--- /dev/null
+++ b/tests/components/test_folder_watcher.py
@@ -0,0 +1,56 @@
+"""The tests for the folder_watcher component."""
+from unittest.mock import Mock, patch
+import os
+
+from homeassistant.components import folder_watcher
+from homeassistant.setup import async_setup_component
+from tests.common import MockDependency
+
+
+async def test_invalid_path_setup(hass):
+ """Test that a invalid path is not setup."""
+ assert not await async_setup_component(
+ hass, folder_watcher.DOMAIN, {
+ folder_watcher.DOMAIN: {
+ folder_watcher.CONF_FOLDER: 'invalid_path'
+ }
+ })
+
+
+async def test_valid_path_setup(hass):
+ """Test that a valid path is setup."""
+ cwd = os.path.join(os.path.dirname(__file__))
+ hass.config.whitelist_external_dirs = set((cwd))
+ with patch.object(folder_watcher, 'Watcher'):
+ assert await async_setup_component(
+ hass, folder_watcher.DOMAIN, {
+ folder_watcher.DOMAIN: {folder_watcher.CONF_FOLDER: cwd}
+ })
+
+
+@MockDependency('watchdog', 'events')
+def test_event(mock_watchdog):
+ """Check that HASS events are fired correctly on watchdog event."""
+ class MockPatternMatchingEventHandler:
+ """Mock base class for the pattern matcher event handler."""
+
+ def __init__(self, patterns):
+ pass
+
+ mock_watchdog.events.PatternMatchingEventHandler = \
+ MockPatternMatchingEventHandler
+ hass = Mock()
+ handler = folder_watcher.create_event_handler(['*'], hass)
+ handler.on_created(Mock(
+ is_directory=False,
+ src_path='/hello/world.txt',
+ event_type='created'
+ ))
+ assert hass.bus.fire.called
+ assert hass.bus.fire.mock_calls[0][1][0] == folder_watcher.DOMAIN
+ assert hass.bus.fire.mock_calls[0][1][1] == {
+ 'event_type': 'created',
+ 'path': '/hello/world.txt',
+ 'file': 'world.txt',
+ 'folder': '/hello',
+ }
diff --git a/tests/components/test_freedns.py b/tests/components/test_freedns.py
new file mode 100644
index 00000000000..b8e38e9c3a8
--- /dev/null
+++ b/tests/components/test_freedns.py
@@ -0,0 +1,69 @@
+"""Test the FreeDNS component."""
+import asyncio
+import pytest
+
+from homeassistant.setup import async_setup_component
+from homeassistant.components import freedns
+from homeassistant.util.dt import utcnow
+
+from tests.common import async_fire_time_changed
+
+ACCESS_TOKEN = 'test_token'
+UPDATE_INTERVAL = freedns.DEFAULT_INTERVAL
+UPDATE_URL = freedns.UPDATE_URL
+
+
+@pytest.fixture
+def setup_freedns(hass, aioclient_mock):
+ """Fixture that sets up FreeDNS."""
+ params = {}
+ params[ACCESS_TOKEN] = ""
+ aioclient_mock.get(
+ UPDATE_URL, params=params, text='Successfully updated 1 domains.')
+
+ hass.loop.run_until_complete(async_setup_component(hass, freedns.DOMAIN, {
+ freedns.DOMAIN: {
+ 'access_token': ACCESS_TOKEN,
+ 'update_interval': UPDATE_INTERVAL,
+ }
+ }))
+
+
+@asyncio.coroutine
+def test_setup(hass, aioclient_mock):
+ """Test setup works if update passes."""
+ params = {}
+ params[ACCESS_TOKEN] = ""
+ aioclient_mock.get(
+ UPDATE_URL, params=params, text='ERROR: Address has not changed.')
+
+ result = yield from async_setup_component(hass, freedns.DOMAIN, {
+ freedns.DOMAIN: {
+ 'access_token': ACCESS_TOKEN,
+ 'update_interval': UPDATE_INTERVAL,
+ }
+ })
+ assert result
+ assert aioclient_mock.call_count == 1
+
+ async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL)
+ yield from hass.async_block_till_done()
+ assert aioclient_mock.call_count == 2
+
+
+@asyncio.coroutine
+def test_setup_fails_if_wrong_token(hass, aioclient_mock):
+ """Test setup fails if first update fails through wrong token."""
+ params = {}
+ params[ACCESS_TOKEN] = ""
+ aioclient_mock.get(
+ UPDATE_URL, params=params, text='ERROR: Invalid update URL (2)')
+
+ result = yield from async_setup_component(hass, freedns.DOMAIN, {
+ freedns.DOMAIN: {
+ 'access_token': ACCESS_TOKEN,
+ 'update_interval': UPDATE_INTERVAL,
+ }
+ })
+ assert not result
+ assert aioclient_mock.call_count == 1
diff --git a/tests/components/test_init.py b/tests/components/test_init.py
index 991982af9b2..c8c7e0d809b 100644
--- a/tests/components/test_init.py
+++ b/tests/components/test_init.py
@@ -1,6 +1,5 @@
"""The tests for Core components."""
# pylint: disable=protected-access
-import asyncio
import unittest
from unittest.mock import patch, Mock
@@ -75,9 +74,9 @@ class TestComponentsCore(unittest.TestCase):
self.hass.block_till_done()
self.assertEqual(1, len(calls))
- @asyncio.coroutine
@patch('homeassistant.core.ServiceRegistry.call')
- def test_turn_on_to_not_block_for_domains_without_service(self, mock_call):
+ async def test_turn_on_to_not_block_for_domains_without_service(self,
+ mock_call):
"""Test if turn_on is blocking domain with no service."""
async_mock_service(self.hass, 'light', SERVICE_TURN_ON)
@@ -88,7 +87,7 @@ class TestComponentsCore(unittest.TestCase):
'entity_id': ['light.test', 'sensor.bla', 'light.bla']
})
service = self.hass.services._services['homeassistant']['turn_on']
- yield from service.func(service_call)
+ await service.func(service_call)
self.assertEqual(2, mock_call.call_count)
self.assertEqual(
@@ -130,8 +129,8 @@ class TestComponentsCore(unittest.TestCase):
comps.reload_core_config(self.hass)
self.hass.block_till_done()
- assert 10 == self.hass.config.latitude
- assert 20 == self.hass.config.longitude
+ assert self.hass.config.latitude == 10
+ assert self.hass.config.longitude == 20
ent.schedule_update_ha_state()
self.hass.block_till_done()
@@ -198,19 +197,18 @@ class TestComponentsCore(unittest.TestCase):
assert not mock_stop.called
-@asyncio.coroutine
-def test_turn_on_intent(hass):
+async def test_turn_on_intent(hass):
"""Test HassTurnOn intent."""
- result = yield from comps.async_setup(hass, {})
+ result = await comps.async_setup(hass, {})
assert result
hass.states.async_set('light.test_light', 'off')
calls = async_mock_service(hass, 'light', SERVICE_TURN_ON)
- response = yield from intent.async_handle(
+ response = await intent.async_handle(
hass, 'test', 'HassTurnOn', {'name': {'value': 'test light'}}
)
- yield from hass.async_block_till_done()
+ await hass.async_block_till_done()
assert response.speech['plain']['speech'] == 'Turned test light on'
assert len(calls) == 1
@@ -220,19 +218,18 @@ def test_turn_on_intent(hass):
assert call.data == {'entity_id': ['light.test_light']}
-@asyncio.coroutine
-def test_turn_off_intent(hass):
+async def test_turn_off_intent(hass):
"""Test HassTurnOff intent."""
- result = yield from comps.async_setup(hass, {})
+ result = await comps.async_setup(hass, {})
assert result
hass.states.async_set('light.test_light', 'on')
calls = async_mock_service(hass, 'light', SERVICE_TURN_OFF)
- response = yield from intent.async_handle(
+ response = await intent.async_handle(
hass, 'test', 'HassTurnOff', {'name': {'value': 'test light'}}
)
- yield from hass.async_block_till_done()
+ await hass.async_block_till_done()
assert response.speech['plain']['speech'] == 'Turned test light off'
assert len(calls) == 1
@@ -242,19 +239,18 @@ def test_turn_off_intent(hass):
assert call.data == {'entity_id': ['light.test_light']}
-@asyncio.coroutine
-def test_toggle_intent(hass):
+async def test_toggle_intent(hass):
"""Test HassToggle intent."""
- result = yield from comps.async_setup(hass, {})
+ result = await comps.async_setup(hass, {})
assert result
hass.states.async_set('light.test_light', 'off')
calls = async_mock_service(hass, 'light', SERVICE_TOGGLE)
- response = yield from intent.async_handle(
+ response = await intent.async_handle(
hass, 'test', 'HassToggle', {'name': {'value': 'test light'}}
)
- yield from hass.async_block_till_done()
+ await hass.async_block_till_done()
assert response.speech['plain']['speech'] == 'Toggled test light'
assert len(calls) == 1
@@ -264,13 +260,12 @@ def test_toggle_intent(hass):
assert call.data == {'entity_id': ['light.test_light']}
-@asyncio.coroutine
-def test_turn_on_multiple_intent(hass):
+async def test_turn_on_multiple_intent(hass):
"""Test HassTurnOn intent with multiple similar entities.
This tests that matching finds the proper entity among similar names.
"""
- result = yield from comps.async_setup(hass, {})
+ result = await comps.async_setup(hass, {})
assert result
hass.states.async_set('light.test_light', 'off')
@@ -278,10 +273,10 @@ def test_turn_on_multiple_intent(hass):
hass.states.async_set('light.test_lighter', 'off')
calls = async_mock_service(hass, 'light', SERVICE_TURN_ON)
- response = yield from intent.async_handle(
+ response = await intent.async_handle(
hass, 'test', 'HassTurnOn', {'name': {'value': 'test lights'}}
)
- yield from hass.async_block_till_done()
+ await hass.async_block_till_done()
assert response.speech['plain']['speech'] == 'Turned test lights 2 on'
assert len(calls) == 1
diff --git a/tests/components/test_pilight.py b/tests/components/test_pilight.py
index 06ad84e7a34..24052a56839 100644
--- a/tests/components/test_pilight.py
+++ b/tests/components/test_pilight.py
@@ -81,7 +81,7 @@ class TestPilight(unittest.TestCase):
@patch('homeassistant.components.pilight._LOGGER.error')
def test_connection_failed_error(self, mock_error):
- """Try to connect at 127.0.0.1:5000 with socket error."""
+ """Try to connect at 127.0.0.1:5001 with socket error."""
with assert_setup_component(4):
with patch('pilight.pilight.Client',
side_effect=socket.error) as mock_client:
@@ -93,7 +93,7 @@ class TestPilight(unittest.TestCase):
@patch('homeassistant.components.pilight._LOGGER.error')
def test_connection_timeout_error(self, mock_error):
- """Try to connect at 127.0.0.1:5000 with socket timeout."""
+ """Try to connect at 127.0.0.1:5001 with socket timeout."""
with assert_setup_component(4):
with patch('pilight.pilight.Client',
side_effect=socket.timeout) as mock_client:
diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py
index bb073459b48..004e5e95ca0 100644
--- a/tests/components/zwave/test_init.py
+++ b/tests/components/zwave/test_init.py
@@ -1094,20 +1094,18 @@ class TestZWaveServices(unittest.TestCase):
assert mock_logger.info.mock_calls[0][1][3] == 2345
def test_print_node(self):
- """Test zwave print_config_parameter service."""
- node1 = MockNode(node_id=14)
- node2 = MockNode(node_id=15)
- self.zwave_network.nodes = {14: node1, 15: node2}
+ """Test zwave print_node_parameter service."""
+ node = MockNode(node_id=14)
- with patch.object(zwave, 'pprint') as mock_pprint:
+ self.zwave_network.nodes = {14: node}
+
+ with self.assertLogs(level='INFO') as mock_logger:
self.hass.services.call('zwave', 'print_node', {
- const.ATTR_NODE_ID: 15,
+ const.ATTR_NODE_ID: 14
})
self.hass.block_till_done()
- assert mock_pprint.called
- assert len(mock_pprint.mock_calls) == 1
- assert mock_pprint.mock_calls[0][1][0]['node_id'] == 15
+ self.assertIn("FOUND NODE ", mock_logger.output[1])
def test_set_wakeup(self):
"""Test zwave set_wakeup service."""
diff --git a/tests/conftest.py b/tests/conftest.py
index 8f0ca787721..269d460ebb6 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -123,7 +123,5 @@ def mock_device_tracker_conf():
), patch(
'homeassistant.components.device_tracker.async_load_config',
side_effect=lambda *args: mock_coro(devices)
- ), patch('homeassistant.components.device_tracker'
- '.Device.set_vendor_for_mac'):
-
+ ):
yield devices
diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py
index def06ea9284..650b98509d0 100644
--- a/tests/helpers/test_template.py
+++ b/tests/helpers/test_template.py
@@ -397,6 +397,19 @@ class TestHelpersTemplate(unittest.TestCase):
""", self.hass)
self.assertEqual('False', tpl.render())
+ def test_state_attr(self):
+ """Test state_attr method."""
+ self.hass.states.set('test.object', 'available', {'mode': 'on'})
+ tpl = template.Template("""
+{% if state_attr("test.object", "mode") == "on" %}yes{% else %}no{% endif %}
+ """, self.hass)
+ self.assertEqual('yes', tpl.render())
+
+ tpl = template.Template("""
+{{ state_attr("test.noobject", "mode") == None }}
+ """, self.hass)
+ self.assertEqual('True', tpl.render())
+
def test_states_function(self):
"""Test using states as a function."""
self.hass.states.set('test.object', 'available')
@@ -428,6 +441,59 @@ class TestHelpersTemplate(unittest.TestCase):
template.Template('{{ utcnow().isoformat() }}',
self.hass).render())
+ def test_regex_match(self):
+ """Test regex_match method."""
+ tpl = template.Template("""
+{{ '123-456-7890' | regex_match('(\d{3})-(\d{3})-(\d{4})') }}
+ """, self.hass)
+ self.assertEqual('True', tpl.render())
+
+ tpl = template.Template("""
+{{ 'home assistant test' | regex_match('Home', True) }}
+ """, self.hass)
+ self.assertEqual('True', tpl.render())
+
+ tpl = template.Template("""
+ {{ 'Another home assistant test' | regex_match('home') }}
+ """, self.hass)
+ self.assertEqual('False', tpl.render())
+
+ def test_regex_search(self):
+ """Test regex_search method."""
+ tpl = template.Template("""
+{{ '123-456-7890' | regex_search('(\d{3})-(\d{3})-(\d{4})') }}
+ """, self.hass)
+ self.assertEqual('True', tpl.render())
+
+ tpl = template.Template("""
+{{ 'home assistant test' | regex_search('Home', True) }}
+ """, self.hass)
+ self.assertEqual('True', tpl.render())
+
+ tpl = template.Template("""
+ {{ 'Another home assistant test' | regex_search('home') }}
+ """, self.hass)
+ self.assertEqual('True', tpl.render())
+
+ def test_regex_replace(self):
+ """Test regex_replace method."""
+ tpl = template.Template("""
+{{ 'Hello World' | regex_replace('(Hello\s)',) }}
+ """, self.hass)
+ self.assertEqual('World', tpl.render())
+
+ def test_regex_findall_index(self):
+ """Test regex_findall_index method."""
+ tpl = template.Template("""
+{{ 'Flight from JFK to LHR' | regex_findall_index('([A-Z]{3})', 0) }}
+ """, self.hass)
+ self.assertEqual('JFK', tpl.render())
+
+ tpl = template.Template("""
+{{ 'Flight from JFK to LHR' | regex_findall_index('([A-Z]{3})', 1) }}
+ """, self.hass)
+ self.assertEqual('LHR', tpl.render())
+
def test_distance_function_with_1_state(self):
"""Test distance function with 1 state."""
self.hass.states.set('test.object', 'happy', {
diff --git a/tests/util/test_color.py b/tests/util/test_color.py
index b64cf0acf80..74ba72cd3d1 100644
--- a/tests/util/test_color.py
+++ b/tests/util/test_color.py
@@ -14,7 +14,7 @@ class TestColorUtil(unittest.TestCase):
"""Test color_RGB_to_xy_brightness."""
self.assertEqual((0, 0, 0),
color_util.color_RGB_to_xy_brightness(0, 0, 0))
- self.assertEqual((0.32, 0.336, 255),
+ self.assertEqual((0.323, 0.329, 255),
color_util.color_RGB_to_xy_brightness(255, 255, 255))
self.assertEqual((0.136, 0.04, 12),
@@ -23,17 +23,17 @@ class TestColorUtil(unittest.TestCase):
self.assertEqual((0.172, 0.747, 170),
color_util.color_RGB_to_xy_brightness(0, 255, 0))
- self.assertEqual((0.679, 0.321, 80),
+ self.assertEqual((0.701, 0.299, 72),
color_util.color_RGB_to_xy_brightness(255, 0, 0))
- self.assertEqual((0.679, 0.321, 17),
+ self.assertEqual((0.701, 0.299, 16),
color_util.color_RGB_to_xy_brightness(128, 0, 0))
def test_color_RGB_to_xy(self):
"""Test color_RGB_to_xy."""
self.assertEqual((0, 0),
color_util.color_RGB_to_xy(0, 0, 0))
- self.assertEqual((0.32, 0.336),
+ self.assertEqual((0.323, 0.329),
color_util.color_RGB_to_xy(255, 255, 255))
self.assertEqual((0.136, 0.04),
@@ -42,10 +42,10 @@ class TestColorUtil(unittest.TestCase):
self.assertEqual((0.172, 0.747),
color_util.color_RGB_to_xy(0, 255, 0))
- self.assertEqual((0.679, 0.321),
+ self.assertEqual((0.701, 0.299),
color_util.color_RGB_to_xy(255, 0, 0))
- self.assertEqual((0.679, 0.321),
+ self.assertEqual((0.701, 0.299),
color_util.color_RGB_to_xy(128, 0, 0))
def test_color_xy_brightness_to_RGB(self):
@@ -155,16 +155,16 @@ class TestColorUtil(unittest.TestCase):
self.assertEqual((0.151, 0.343),
color_util.color_hs_to_xy(180, 100))
- self.assertEqual((0.352, 0.329),
+ self.assertEqual((0.356, 0.321),
color_util.color_hs_to_xy(350, 12.5))
- self.assertEqual((0.228, 0.476),
+ self.assertEqual((0.229, 0.474),
color_util.color_hs_to_xy(140, 50))
- self.assertEqual((0.465, 0.33),
+ self.assertEqual((0.474, 0.317),
color_util.color_hs_to_xy(0, 40))
- self.assertEqual((0.32, 0.336),
+ self.assertEqual((0.323, 0.329),
color_util.color_hs_to_xy(360, 0))
def test_rgb_hex_to_rgb_list(self):