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. - -![Location of button on bridge](/static/images/config_philips_hue.jpg) -""" - 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):