diff --git a/.coveragerc b/.coveragerc index 4fa8e04e694..fe3c598e501 100644 --- a/.coveragerc +++ b/.coveragerc @@ -149,6 +149,9 @@ omit = homeassistant/components/rachio.py homeassistant/components/*/rachio.py + homeassistant/components/raincloud.py + homeassistant/components/*/raincloud.py + homeassistant/components/raspihats.py homeassistant/components/*/raspihats.py @@ -166,6 +169,9 @@ omit = homeassistant/components/scsgate.py homeassistant/components/*/scsgate.py + + homeassistant/components/skybell.py + homeassistant/components/*/skybell.py homeassistant/components/tado.py homeassistant/components/*/tado.py @@ -179,6 +185,9 @@ omit = homeassistant/components/tesla.py homeassistant/components/*/tesla.py + homeassistant/components/thethingsnetwork.py + homeassistant/components/*/thethingsnetwork.py + homeassistant/components/*/thinkingcleaner.py homeassistant/components/tradfri.py @@ -399,6 +408,7 @@ omit = homeassistant/components/notify/aws_sqs.py homeassistant/components/notify/ciscospark.py homeassistant/components/notify/clicksend.py + homeassistant/components/notify/clicksendaudio.py homeassistant/components/notify/discord.py homeassistant/components/notify/facebook.py homeassistant/components/notify/free_mobile.py @@ -420,6 +430,7 @@ omit = homeassistant/components/notify/pushover.py homeassistant/components/notify/pushsafer.py homeassistant/components/notify/rest.py + homeassistant/components/notify/rocketchat.py homeassistant/components/notify/sendgrid.py homeassistant/components/notify/simplepush.py homeassistant/components/notify/slack.py @@ -538,9 +549,11 @@ omit = homeassistant/components/sensor/tank_utility.py homeassistant/components/sensor/ted5000.py homeassistant/components/sensor/temper.py + homeassistant/components/sensor/tibber.py homeassistant/components/sensor/time_date.py homeassistant/components/sensor/torque.py homeassistant/components/sensor/transmission.py + homeassistant/components/sensor/travisci.py homeassistant/components/sensor/twitch.py homeassistant/components/sensor/uber.py homeassistant/components/sensor/upnp.py @@ -578,6 +591,7 @@ omit = homeassistant/components/switch/telnet.py homeassistant/components/switch/transmission.py homeassistant/components/switch/wake_on_lan.py + homeassistant/components/switch/xiaomi_miio.py homeassistant/components/telegram_bot/* homeassistant/components/thingspeak.py homeassistant/components/tts/amazon_polly.py diff --git a/CODEOWNERS b/CODEOWNERS index 3c975ca3862..51791886e6d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -29,6 +29,9 @@ 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 + Dockerfile @home-assistant/docker virtualization/Docker/* @home-assistant/docker @@ -36,6 +39,25 @@ homeassistant/components/zwave/* @home-assistant/z-wave homeassistant/components/*/zwave.py @home-assistant/z-wave # Indiviudal components +homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt +homeassistant/components/climate/eq3btsmart.py @rytilahti +homeassistant/components/climate/sensibo.py @andrey-git homeassistant/components/cover/template.py @PhracturedBlue homeassistant/components/device_tracker/automatic.py @armills +homeassistant/components/history_graph.py @andrey-git +homeassistant/components/light/tplink.py @rytilahti +homeassistant/components/light/yeelight.py @rytilahti homeassistant/components/media_player/kodi.py @armills +homeassistant/components/sensor/airvisual.py @bachya +homeassistant/components/sensor/miflora.py @danielhiversen +homeassistant/components/sensor/tibber.py @danielhiversen +homeassistant/components/sensor/waqi.py @andrey-git +homeassistant/components/switch/rainmachine.py @bachya +homeassistant/components/switch/tplink.py @rytilahti + +homeassistant/components/*/broadlink.py @danielhiversen +homeassistant/components/*/rfxtrx.py @danielhiversen +homeassistant/components/tesla.py @zabuldon +homeassistant/components/*/tesla.py @zabuldon +homeassistant/components/*/xiaomi_aqara.py @danielhiversen +homeassistant/components/*/xiaomi_miio.py @rytilahti diff --git a/Dockerfile b/Dockerfile index f0d5accdf3d..908e8481eee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,9 +11,10 @@ MAINTAINER Paulus Schoutsen #ENV INSTALL_FFMPEG no #ENV INSTALL_LIBCEC no #ENV INSTALL_PHANTOMJS no -#ENV INSTALL_COAP_CLIENT no +#ENV INSTALL_COAP no #ENV INSTALL_SSOCR no + VOLUME /config RUN mkdir -p /usr/src/app @@ -25,7 +26,6 @@ RUN virtualization/Docker/setup_docker_prereqs # Install hass component dependencies COPY requirements_all.txt requirements_all.txt - # Uninstall enum34 because some depenndecies install it but breaks Python 3.4+. # See PR #8103 for more info. RUN pip3 install --no-cache-dir -r requirements_all.txt && \ diff --git a/docs/source/conf.py b/docs/source/conf.py index bcb2699f57b..8ca22e1a126 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -117,7 +117,11 @@ def linkcode_resolve(domain, info): linespec = "#L%d" % (lineno + 1) else: linespec = "" - fn = relpath(fn, start='../') + index = fn.find("/homeassistant/") + if index == -1: + index = 0 + + fn = fn[index:] return '{}/blob/{}/{}{}'.format(GITHUB_URL, code_branch, fn, linespec) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 3ff4d99fb98..4978177a658 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -11,13 +11,11 @@ from typing import Any, Optional, Dict import voluptuous as vol -import homeassistant.components as core_components +from homeassistant import ( + core, config as conf_util, loader, components as core_components) from homeassistant.components import persistent_notification -import homeassistant.config as conf_util -import homeassistant.core as core from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE from homeassistant.setup import async_setup_component -import homeassistant.loader as loader from homeassistant.util.logging import AsyncHandler from homeassistant.util.package import async_get_user_site, get_user_site from homeassistant.util.yaml import clear_secret_cache @@ -83,6 +81,18 @@ def async_from_config_dict(config: Dict[str, Any], This method is a coroutine. """ start = time() + + if enable_log: + async_enable_logging(hass, verbose, log_rotate_days, log_file) + + if sys.version_info[:2] < (3, 5): + _LOGGER.warning( + 'Python 3.4 support has been deprecated and will be removed in ' + 'the begining of 2018. Please upgrade Python or your operating ' + 'system. More info: https://home-assistant.io/blog/2017/10/06/' + 'deprecating-python-3.4-support/' + ) + core_config = config.get(core.DOMAIN, {}) try: @@ -93,9 +103,6 @@ def async_from_config_dict(config: Dict[str, Any], yield from hass.async_add_job(conf_util.process_ha_config_upgrade, hass) - if enable_log: - async_enable_logging(hass, verbose, log_rotate_days, log_file) - hass.config.skip_pip = skip_pip if skip_pip: _LOGGER.warning("Skipping pip installation of required modules. " diff --git a/homeassistant/components/abode.py b/homeassistant/components/abode.py index fe35d7b1b8b..ab13c1534bc 100644 --- a/homeassistant/components/abode.py +++ b/homeassistant/components/abode.py @@ -21,7 +21,7 @@ from homeassistant.const import (ATTR_ATTRIBUTION, ATTR_DATE, ATTR_TIME, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START) -REQUIREMENTS = ['abodepy==0.11.8'] +REQUIREMENTS = ['abodepy==0.12.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/alarm_control_panel/arlo.py b/homeassistant/components/alarm_control_panel/arlo.py new file mode 100644 index 00000000000..2dad3857c4d --- /dev/null +++ b/homeassistant/components/alarm_control_panel/arlo.py @@ -0,0 +1,121 @@ +""" +Support for Arlo Alarm Control Panels. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/alarm_control_panel.arlo/ +""" +import asyncio +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.alarm_control_panel import ( + AlarmControlPanel, PLATFORM_SCHEMA) +from homeassistant.components.arlo import (DATA_ARLO, CONF_ATTRIBUTION) +from homeassistant.const import ( + ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, + STATE_ALARM_DISARMED) + +_LOGGER = logging.getLogger(__name__) + +ARMED = 'armed' + +CONF_HOME_MODE_NAME = 'home_mode_name' + +DEPENDENCIES = ['arlo'] + +DISARMED = 'disarmed' + +ICON = 'mdi:security' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_HOME_MODE_NAME, default=ARMED): cv.string, +}) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up the Arlo Alarm Control Panels.""" + data = hass.data[DATA_ARLO] + + if not data.base_stations: + return + + home_mode_name = config.get(CONF_HOME_MODE_NAME) + base_stations = [] + for base_station in data.base_stations: + base_stations.append(ArloBaseStation(base_station, home_mode_name)) + async_add_devices(base_stations, True) + + +class ArloBaseStation(AlarmControlPanel): + """Representation of an Arlo Alarm Control Panel.""" + + def __init__(self, data, home_mode_name): + """Initialize the alarm control panel.""" + self._base_station = data + self._home_mode_name = home_mode_name + self._state = None + + @property + def icon(self): + """Return icon.""" + return ICON + + @property + def state(self): + """Return the state of the device.""" + return self._state + + def update(self): + """Update the state of the device.""" + # PyArlo sometimes returns None for mode. So retry 3 times before + # returning None. + num_retries = 3 + i = 0 + while i < num_retries: + mode = self._base_station.mode + if mode: + self._state = self._get_state_from_mode(mode) + return + i += 1 + self._state = None + + @asyncio.coroutine + def async_alarm_disarm(self, code=None): + """Send disarm command.""" + self._base_station.mode = DISARMED + + @asyncio.coroutine + def async_alarm_arm_away(self, code=None): + """Send arm away command.""" + self._base_station.mode = ARMED + + @asyncio.coroutine + def async_alarm_arm_home(self, code=None): + """Send arm home command. Uses custom mode.""" + self._base_station.mode = self._home_mode_name + + @property + def name(self): + """Return the name of the base station.""" + return self._base_station.name + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + 'device_id': self._base_station.device_id + } + + def _get_state_from_mode(self, mode): + """Convert Arlo mode to Home Assistant state.""" + if mode == ARMED: + return STATE_ALARM_ARMED_AWAY + elif mode == DISARMED: + return STATE_ALARM_DISARMED + elif mode == self._home_mode_name: + return STATE_ALARM_ARMED_HOME + return None diff --git a/homeassistant/components/alarm_control_panel/egardia.py b/homeassistant/components/alarm_control_panel/egardia.py index fbafe061334..7719ab884bc 100644 --- a/homeassistant/components/alarm_control_panel/egardia.py +++ b/homeassistant/components/alarm_control_panel/egardia.py @@ -18,13 +18,14 @@ from homeassistant.const import ( CONF_NAME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED) -REQUIREMENTS = ['pythonegardia==1.0.20'] +REQUIREMENTS = ['pythonegardia==1.0.22'] _LOGGER = logging.getLogger(__name__) CONF_REPORT_SERVER_CODES = 'report_server_codes' CONF_REPORT_SERVER_ENABLED = 'report_server_enabled' CONF_REPORT_SERVER_PORT = 'report_server_port' +CONF_REPORT_SERVER_CODES_IGNORE = 'ignore' DEFAULT_NAME = 'Egardia' DEFAULT_PORT = 80 @@ -148,9 +149,15 @@ class EgardiaAlarm(alarm.AlarmControlPanel): def parsestatus(self, status): """Parse the status.""" - newstatus = ([v for k, v in STATES.items() - if status.upper() == k][0]) - self._status = newstatus + _LOGGER.debug("Parsing status %s", status) + # Ignore the statuscode if it is IGNORE + if status.lower().strip() != CONF_REPORT_SERVER_CODES_IGNORE: + _LOGGER.debug("Not ignoring status") + newstatus = ([v for k, v in STATES.items() + if status.upper() == k][0]) + self._status = newstatus + else: + _LOGGER.error("Ignoring status") def update(self): """Update the alarm status.""" diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index ae1ecb87f60..61db142ac42 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -6,88 +6,100 @@ from uuid import uuid4 from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF) from homeassistant.components import switch, light +from homeassistant.util.decorator import Registry +HANDLERS = Registry() _LOGGER = logging.getLogger(__name__) -ATTR_HEADER = 'header' -ATTR_NAME = 'name' -ATTR_NAMESPACE = 'namespace' -ATTR_MESSAGE_ID = 'messageId' -ATTR_PAYLOAD = 'payload' -ATTR_PAYLOAD_VERSION = 'payloadVersion' +API_DIRECTIVE = 'directive' +API_EVENT = 'event' +API_HEADER = 'header' +API_PAYLOAD = 'payload' +API_ENDPOINT = 'endpoint' MAPPING_COMPONENT = { - switch.DOMAIN: ['SWITCH', ('turnOff', 'turnOn'), None], + switch.DOMAIN: ['SWITCH', ('Alexa.PowerController',), None], light.DOMAIN: [ - 'LIGHT', ('turnOff', 'turnOn'), { - light.SUPPORT_BRIGHTNESS: 'setPercentage' + 'LIGHT', ('Alexa.PowerController',), { + light.SUPPORT_BRIGHTNESS: 'Alexa.BrightnessController' } ], } -def mapping_api_function(name): - """Return function pointer to api function for name. - - Async friendly. - """ - mapping = { - 'DiscoverAppliancesRequest': async_api_discovery, - 'TurnOnRequest': async_api_turn_on, - 'TurnOffRequest': async_api_turn_off, - 'SetPercentageRequest': async_api_set_percentage, - } - return mapping.get(name, None) - - @asyncio.coroutine def async_handle_message(hass, message): """Handle incoming API messages.""" - assert int(message[ATTR_HEADER][ATTR_PAYLOAD_VERSION]) == 2 + assert message[API_DIRECTIVE][API_HEADER]['payloadVersion'] == '3' + + # Read head data + message = message[API_DIRECTIVE] + namespace = message[API_HEADER]['namespace'] + name = message[API_HEADER]['name'] # Do we support this API request? - funct_ref = mapping_api_function(message[ATTR_HEADER][ATTR_NAME]) + funct_ref = HANDLERS.get((namespace, name)) if not funct_ref: _LOGGER.warning( - "Unsupported API request %s", message[ATTR_HEADER][ATTR_NAME]) + "Unsupported API request %s/%s", namespace, name) return api_error(message) return (yield from funct_ref(hass, message)) -def api_message(name, namespace, payload=None): +def api_message(request, name='Response', namespace='Alexa', payload=None): """Create a API formatted response message. Async friendly. """ payload = payload or {} - return { - ATTR_HEADER: { - ATTR_MESSAGE_ID: uuid4(), - ATTR_NAME: name, - ATTR_NAMESPACE: namespace, - ATTR_PAYLOAD_VERSION: '2', - }, - ATTR_PAYLOAD: payload, + + response = { + API_EVENT: { + API_HEADER: { + 'namespace': namespace, + 'name': name, + 'messageId': str(uuid4()), + 'payloadVersion': '3', + }, + API_PAYLOAD: payload, + } } + # If a correlation token exsits, add it to header / Need by Async requests + token = request[API_HEADER].get('correlationToken') + if token: + response[API_EVENT][API_HEADER]['correlationToken'] = token -def api_error(request, exc='DriverInternalError'): + # Extend event with endpoint object / Need by Async requests + if API_ENDPOINT in request: + response[API_EVENT][API_ENDPOINT] = request[API_ENDPOINT].copy() + + return response + + +def api_error(request, error_type='INTERNAL_ERROR', error_message=""): """Create a API formatted error response. Async friendly. """ - return api_message(exc, request[ATTR_HEADER][ATTR_NAMESPACE]) + payload = { + 'type': error_type, + 'message': error_message, + } + + return api_message(request, name='ErrorResponse', payload=payload) +@HANDLERS.register(('Alexa.Discovery', 'Discover')) @asyncio.coroutine def async_api_discovery(hass, request): """Create a API formatted discovery response. Async friendly. """ - discovered_appliances = [] + discovery_endpoints = [] for entity in hass.states.async_all(): class_data = MAPPING_COMPONENT.get(entity.domain) @@ -95,35 +107,42 @@ def async_api_discovery(hass, request): if not class_data: continue - appliance = { - 'actions': [], - 'applianceTypes': [class_data[0]], + endpoint = { + 'displayCategories': [class_data[0]], 'additionalApplianceDetails': {}, - 'applianceId': entity.entity_id.replace('.', '#'), - 'friendlyDescription': '', + 'endpointId': entity.entity_id.replace('.', '#'), 'friendlyName': entity.name, - 'isReachable': True, + 'description': '', 'manufacturerName': 'Unknown', - 'modelName': 'Unknown', - 'version': 'Unknown', } + actions = set() # static actions if class_data[1]: - appliance['actions'].extend(list(class_data[1])) + actions |= set(class_data[1]) # dynamic actions if class_data[2]: supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) for feature, action_name in class_data[2].items(): if feature & supported > 0: - appliance['actions'].append(action_name) + actions.add(action_name) - discovered_appliances.append(appliance) + # Write action into capabilities + capabilities = [] + for action in actions: + capabilities.append({ + 'type': 'AlexaInterface', + 'interface': action, + 'version': 3, + }) + + endpoint['capabilities'] = capabilities + discovery_endpoints.append(endpoint) return api_message( - 'DiscoverAppliancesResponse', 'Alexa.ConnectedHome.Discovery', - payload={'discoveredAppliances': discovered_appliances}) + request, name='Discover.Response', namespace='Alexa.Discovery', + payload={'endpoints': discovery_endpoints}) def extract_entity(funct): @@ -131,21 +150,21 @@ def extract_entity(funct): @asyncio.coroutine def async_api_entity_wrapper(hass, request): """Process a turn on request.""" - entity_id = \ - request[ATTR_PAYLOAD]['appliance']['applianceId'].replace('#', '.') + entity_id = request[API_ENDPOINT]['endpointId'].replace('#', '.') # extract state object entity = hass.states.get(entity_id) if not entity: _LOGGER.error("Can't process %s for %s", - request[ATTR_HEADER][ATTR_NAME], entity_id) - return api_error(request) + request[API_HEADER]['name'], entity_id) + return api_error(request, error_type='NO_SUCH_ENDPOINT') return (yield from funct(hass, request, entity)) return async_api_entity_wrapper +@HANDLERS.register(('Alexa.PowerController', 'TurnOn')) @extract_entity @asyncio.coroutine def async_api_turn_on(hass, request, entity): @@ -154,9 +173,10 @@ def async_api_turn_on(hass, request, entity): ATTR_ENTITY_ID: entity.entity_id }, blocking=True) - return api_message('TurnOnConfirmation', 'Alexa.ConnectedHome.Control') + return api_message(request) +@HANDLERS.register(('Alexa.PowerController', 'TurnOff')) @extract_entity @asyncio.coroutine def async_api_turn_off(hass, request, entity): @@ -165,21 +185,19 @@ def async_api_turn_off(hass, request, entity): ATTR_ENTITY_ID: entity.entity_id }, blocking=True) - return api_message('TurnOffConfirmation', 'Alexa.ConnectedHome.Control') + return api_message(request) +@HANDLERS.register(('Alexa.BrightnessController', 'SetBrightness')) @extract_entity @asyncio.coroutine -def async_api_set_percentage(hass, request, entity): - """Process a set percentage request.""" - if entity.domain == light.DOMAIN: - brightness = request[ATTR_PAYLOAD]['percentageState']['value'] - yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: entity.entity_id, - light.ATTR_BRIGHTNESS: brightness, - }, blocking=True) - else: - return api_error(request) +def async_api_set_brightness(hass, request, entity): + """Process a set brightness request.""" + brightness = request[API_PAYLOAD]['brightness'] - return api_message( - 'SetPercentageConfirmation', 'Alexa.ConnectedHome.Control') + yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { + ATTR_ENTITY_ID: entity.entity_id, + light.ATTR_BRIGHTNESS: brightness, + }, blocking=True) + + return api_message(request) diff --git a/homeassistant/components/arlo.py b/homeassistant/components/arlo.py index 0ab629cfbd4..f3397a884d1 100644 --- a/homeassistant/components/arlo.py +++ b/homeassistant/components/arlo.py @@ -1,5 +1,5 @@ """ -This component provides basic support for Netgear Arlo IP cameras. +This component provides support for Netgear Arlo IP cameras. For more details about this component, please refer to the documentation at https://home-assistant.io/components/arlo/ @@ -12,7 +12,7 @@ from requests.exceptions import HTTPError, ConnectTimeout from homeassistant.helpers import config_validation as cv from homeassistant.const import CONF_USERNAME, CONF_PASSWORD -REQUIREMENTS = ['pyarlo==0.0.6'] +REQUIREMENTS = ['pyarlo==0.0.7'] _LOGGER = logging.getLogger(__name__) @@ -23,7 +23,7 @@ DEFAULT_BRAND = 'Netgear Arlo' DOMAIN = 'arlo' NOTIFICATION_ID = 'arlo_notification' -NOTIFICATION_TITLE = 'Arlo Camera Setup' +NOTIFICATION_TITLE = 'Arlo Component Setup' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ diff --git a/homeassistant/components/automation/event.py b/homeassistant/components/automation/event.py index 32d2d245bef..90baeaded14 100644 --- a/homeassistant/components/automation/event.py +++ b/homeassistant/components/automation/event.py @@ -21,7 +21,7 @@ _LOGGER = logging.getLogger(__name__) TRIGGER_SCHEMA = vol.Schema({ vol.Required(CONF_PLATFORM): 'event', vol.Required(CONF_EVENT_TYPE): cv.string, - vol.Optional(CONF_EVENT_DATA): dict, + vol.Optional(CONF_EVENT_DATA, default={}): dict, }) @@ -29,18 +29,24 @@ TRIGGER_SCHEMA = vol.Schema({ def async_trigger(hass, config, action): """Listen for events based on configuration.""" event_type = config.get(CONF_EVENT_TYPE) - event_data = config.get(CONF_EVENT_DATA) + event_data_schema = vol.Schema( + config.get(CONF_EVENT_DATA), + extra=vol.ALLOW_EXTRA) @callback def handle_event(event): """Listen for events and calls the action when data matches.""" - if not event_data or all(val == event.data.get(key) for key, val - in event_data.items()): - hass.async_run_job(action, { - 'trigger': { - 'platform': 'event', - 'event': event, - }, - }) + try: + event_data_schema(event.data) + except vol.Invalid: + # If event data doesn't match requested schema, skip event + return + + hass.async_run_job(action, { + 'trigger': { + 'platform': 'event', + 'event': event, + }, + }) return hass.bus.async_listen(event_type, handle_event) diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index 51b2ea89f0f..571888038a6 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -99,8 +99,8 @@ def async_trigger(hass, config, action): return async_remove_track_same = async_track_same_state( - hass, True, time_delta, call_action, entity_ids=entity_id, - async_check_func=check_numeric_state) + hass, time_delta, call_action, entity_ids=entity_id, + async_check_same_func=check_numeric_state) unsub = async_track_state_change( hass, entity_id, state_automation_listener) diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index e7a01cb7115..7ed44761be8 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -65,7 +65,9 @@ def async_trigger(hass, config, action): return async_remove_track_same = async_track_same_state( - hass, to_s.state, time_delta, call_action, entity_ids=entity_id) + hass, time_delta, call_action, + lambda _, _2, to_state: to_state.state == to_s.state, + entity_ids=entity_id) unsub = async_track_state_change( hass, entity_id, state_automation_listener, from_state, to_state) diff --git a/homeassistant/components/binary_sensor/iss.py b/homeassistant/components/binary_sensor/iss.py index 3b927853c00..d35c36a012e 100644 --- a/homeassistant/components/binary_sensor/iss.py +++ b/homeassistant/components/binary_sensor/iss.py @@ -13,7 +13,8 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.binary_sensor import ( BinarySensorDevice, PLATFORM_SCHEMA) -from homeassistant.const import (CONF_NAME, ATTR_LONGITUDE, ATTR_LATITUDE) +from homeassistant.const import ( + CONF_NAME, ATTR_LONGITUDE, ATTR_LATITUDE, CONF_SHOW_ON_MAP) from homeassistant.util import Throttle REQUIREMENTS = ['pyiss==1.0.1'] @@ -23,8 +24,6 @@ _LOGGER = logging.getLogger(__name__) ATTR_ISS_NEXT_RISE = 'next_rise' ATTR_ISS_NUMBER_PEOPLE_SPACE = 'number_of_people_in_space' -CONF_SHOW_ON_MAP = 'show_on_map' - DEFAULT_NAME = 'ISS' DEFAULT_DEVICE_CLASS = 'visible' diff --git a/homeassistant/components/binary_sensor/raincloud.py b/homeassistant/components/binary_sensor/raincloud.py new file mode 100644 index 00000000000..f75f7644c4e --- /dev/null +++ b/homeassistant/components/binary_sensor/raincloud.py @@ -0,0 +1,72 @@ +""" +Support for Melnor RainCloud sprinkler water timer. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.raincloud/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.raincloud import ( + BINARY_SENSORS, DATA_RAINCLOUD, ICON_MAP, RainCloudEntity) +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA) +from homeassistant.const import CONF_MONITORED_CONDITIONS + +DEPENDENCIES = ['raincloud'] + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(BINARY_SENSORS)): + vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)]), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up a sensor for a raincloud device.""" + raincloud = hass.data[DATA_RAINCLOUD].data + + sensors = [] + for sensor_type in config.get(CONF_MONITORED_CONDITIONS): + if sensor_type == 'status': + sensors.append( + RainCloudBinarySensor(raincloud.controller, sensor_type)) + sensors.append( + RainCloudBinarySensor(raincloud.controller.faucet, + sensor_type)) + + else: + # create an sensor for each zone managed by faucet + for zone in raincloud.controller.faucet.zones: + sensors.append(RainCloudBinarySensor(zone, sensor_type)) + + add_devices(sensors, True) + return True + + +class RainCloudBinarySensor(RainCloudEntity, BinarySensorDevice): + """A sensor implementation for raincloud device.""" + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self._state + + def update(self): + """Get the latest data and updates the state.""" + _LOGGER.debug("Updating RainCloud sensor: %s", self._name) + self._state = getattr(self.data, self._sensor_type) + if self._sensor_type == 'status': + self._state = self._state == 'Online' + + @property + def icon(self): + """Return the icon of this device.""" + if self._sensor_type == 'is_watering': + return 'mdi:water' if self.is_on else 'mdi:water-off' + elif self._sensor_type == 'status': + return 'mdi:pipe' if self.is_on else 'mdi:pipe-disconnected' + return ICON_MAP.get(self._sensor_type) diff --git a/homeassistant/components/binary_sensor/skybell.py b/homeassistant/components/binary_sensor/skybell.py new file mode 100644 index 00000000000..734f8e03375 --- /dev/null +++ b/homeassistant/components/binary_sensor/skybell.py @@ -0,0 +1,97 @@ +""" +Binary sensor support for the Skybell HD Doorbell. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.skybell/ +""" +from datetime import timedelta +import logging + +import voluptuous as vol + +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA) +from homeassistant.components.skybell import ( + DEFAULT_ENTITY_NAMESPACE, DOMAIN as SKYBELL_DOMAIN, SkybellDevice) +from homeassistant.const import ( + CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS) +import homeassistant.helpers.config_validation as cv + +DEPENDENCIES = ['skybell'] + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=5) + +# Sensor types: Name, device_class, event +SENSOR_TYPES = { + 'button': ['Button', 'occupancy', 'device:sensor:button'], + 'motion': ['Motion', 'motion', 'device:sensor:motion'], +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_ENTITY_NAMESPACE, default=DEFAULT_ENTITY_NAMESPACE): + cv.string, + vol.Required(CONF_MONITORED_CONDITIONS, default=[]): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the platform for a Skybell device.""" + skybell = hass.data.get(SKYBELL_DOMAIN) + + sensors = [] + for sensor_type in config.get(CONF_MONITORED_CONDITIONS): + for device in skybell.get_devices(): + sensors.append(SkybellBinarySensor(device, sensor_type)) + + add_devices(sensors, True) + + +class SkybellBinarySensor(SkybellDevice, BinarySensorDevice): + """A binary sensor implementation for Skybell devices.""" + + def __init__(self, device, sensor_type): + """Initialize a binary sensor for a Skybell device.""" + super().__init__(device) + self._sensor_type = sensor_type + self._name = "{0} {1}".format(self._device.name, + SENSOR_TYPES[self._sensor_type][0]) + self._device_class = SENSOR_TYPES[self._sensor_type][1] + self._event = {} + self._state = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def is_on(self): + """Return True if the binary sensor is on.""" + return self._state + + @property + def device_class(self): + """Return the class of the binary sensor.""" + return self._device_class + + @property + def device_state_attributes(self): + """Return the state attributes.""" + attrs = super().device_state_attributes + + attrs['event_date'] = self._event.get('createdAt') + + return attrs + + def update(self): + """Get the latest data and updates the state.""" + super().update() + + event = self._device.latest(SENSOR_TYPES[self._sensor_type][2]) + + self._state = bool(event and event.get('id') != self._event.get('id')) + + self._event = event diff --git a/homeassistant/components/binary_sensor/template.py b/homeassistant/components/binary_sensor/template.py index 84afd01303f..9356d87d7ea 100644 --- a/homeassistant/components/binary_sensor/template.py +++ b/homeassistant/components/binary_sensor/template.py @@ -135,7 +135,7 @@ class BinarySensorTemplate(BinarySensorDevice): return False @callback - def _async_render(self, *args): + def _async_render(self): """Get the state of template.""" try: return self._template.async_render().lower() == 'true' @@ -171,5 +171,5 @@ class BinarySensorTemplate(BinarySensorDevice): period = self._delay_on if state else self._delay_off async_track_same_state( - self.hass, state, period, set_state, entity_ids=self._entities, - async_check_func=self._async_render) + self.hass, period, set_state, entity_ids=self._entities, + async_check_same_func=lambda *args: self._async_render() == state) diff --git a/homeassistant/components/binary_sensor/tesla.py b/homeassistant/components/binary_sensor/tesla.py index af7e394b50e..a7cda90b3f6 100644 --- a/homeassistant/components/binary_sensor/tesla.py +++ b/homeassistant/components/binary_sensor/tesla.py @@ -30,7 +30,6 @@ class TeslaBinarySensor(TeslaDevice, BinarySensorDevice): def __init__(self, tesla_device, controller, sensor_type): """Initialisation of binary sensor.""" super().__init__(tesla_device, controller) - self._name = self.tesla_device.name self._state = False self.entity_id = ENTITY_ID_FORMAT.format(self.tesla_id) self._sensor_type = sensor_type diff --git a/homeassistant/components/binary_sensor/threshold.py b/homeassistant/components/binary_sensor/threshold.py index 866e16ecbe2..5ca037767f2 100644 --- a/homeassistant/components/binary_sensor/threshold.py +++ b/homeassistant/components/binary_sensor/threshold.py @@ -20,15 +20,18 @@ from homeassistant.helpers.event import async_track_state_change _LOGGER = logging.getLogger(__name__) +ATTR_HYSTERESIS = 'hysteresis' ATTR_SENSOR_VALUE = 'sensor_value' ATTR_THRESHOLD = 'threshold' ATTR_TYPE = 'type' +CONF_HYSTERESIS = 'hysteresis' CONF_LOWER = 'lower' CONF_THRESHOLD = 'threshold' CONF_UPPER = 'upper' DEFAULT_NAME = 'Threshold' +DEFAULT_HYSTERESIS = 0.0 SENSOR_TYPES = [CONF_LOWER, CONF_UPPER] @@ -36,6 +39,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_THRESHOLD): vol.Coerce(float), vol.Required(CONF_TYPE): vol.In(SENSOR_TYPES), + vol.Optional( + CONF_HYSTERESIS, default=DEFAULT_HYSTERESIS): vol.Coerce(float), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, }) @@ -47,28 +52,32 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): entity_id = config.get(CONF_ENTITY_ID) name = config.get(CONF_NAME) threshold = config.get(CONF_THRESHOLD) + hysteresis = config.get(CONF_HYSTERESIS) limit_type = config.get(CONF_TYPE) device_class = config.get(CONF_DEVICE_CLASS) - async_add_devices( - [ThresholdSensor(hass, entity_id, name, threshold, limit_type, - device_class)], True) + async_add_devices([ThresholdSensor( + hass, entity_id, name, threshold, + hysteresis, limit_type, device_class) + ], True) + return True class ThresholdSensor(BinarySensorDevice): """Representation of a Threshold sensor.""" - def __init__(self, hass, entity_id, name, threshold, limit_type, - device_class): + def __init__(self, hass, entity_id, name, threshold, + hysteresis, limit_type, device_class): """Initialize the Threshold sensor.""" self._hass = hass self._entity_id = entity_id self.is_upper = limit_type == 'upper' self._name = name self._threshold = threshold + self._hysteresis = hysteresis self._device_class = device_class - self._deviation = False + self._state = False self.sensor_value = 0 @callback @@ -97,7 +106,7 @@ class ThresholdSensor(BinarySensorDevice): @property def is_on(self): """Return true if sensor is on.""" - return self._deviation + return self._state @property def should_poll(self): @@ -116,13 +125,16 @@ class ThresholdSensor(BinarySensorDevice): ATTR_ENTITY_ID: self._entity_id, ATTR_SENSOR_VALUE: self.sensor_value, ATTR_THRESHOLD: self._threshold, + ATTR_HYSTERESIS: self._hysteresis, ATTR_TYPE: CONF_UPPER if self.is_upper else CONF_LOWER, } @asyncio.coroutine def async_update(self): """Get the latest data and updates the states.""" - if self.is_upper: - self._deviation = bool(self.sensor_value > self._threshold) - else: - self._deviation = bool(self.sensor_value < self._threshold) + if self._hysteresis == 0 and self.sensor_value == self._threshold: + self._state = False + elif self.sensor_value > (self._threshold + self._hysteresis): + self._state = self.is_upper + elif self.sensor_value < (self._threshold - self._hysteresis): + self._state = not self.is_upper diff --git a/homeassistant/components/camera/arlo.py b/homeassistant/components/camera/arlo.py index d473fa42d9d..be58b61fb8c 100644 --- a/homeassistant/components/camera/arlo.py +++ b/homeassistant/components/camera/arlo.py @@ -1,37 +1,40 @@ """ -This component provides basic support for Netgear Arlo IP cameras. +Support for Netgear Arlo IP cameras. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/camera.arlo/ """ import asyncio import logging +from datetime import timedelta import voluptuous as vol -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream +import homeassistant.helpers.config_validation as cv from homeassistant.components.arlo import DEFAULT_BRAND, DATA_ARLO from homeassistant.components.camera import Camera, PLATFORM_SCHEMA from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.const import ATTR_BATTERY_LEVEL - -DEPENDENCIES = ['arlo', 'ffmpeg'] +from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream _LOGGER = logging.getLogger(__name__) +SCAN_INTERVAL = timedelta(minutes=10) + +ARLO_MODE_ARMED = 'armed' +ARLO_MODE_DISARMED = 'disarmed' + ATTR_BRIGHTNESS = 'brightness' ATTR_FLIPPED = 'flipped' ATTR_MIRRORED = 'mirrored' -ATTR_MOTION_SENSITIVITY = 'motion_detection_sensitivity' -ATTR_POWER_SAVE_MODE = 'power_save_mode' +ATTR_MOTION = 'motion_detection_sensitivity' +ATTR_POWERSAVE = 'power_save_mode' ATTR_SIGNAL_STRENGTH = 'signal_strength' ATTR_UNSEEN_VIDEOS = 'unseen_videos' CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments' -ARLO_MODE_ARMED = 'armed' -ARLO_MODE_DISARMED = 'disarmed' +DEPENDENCIES = ['arlo', 'ffmpeg'] POWERSAVE_MODE_MAPPING = { 1: 'best_battery_life', @@ -40,7 +43,8 @@ POWERSAVE_MODE_MAPPING = { } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string, + vol.Optional(CONF_FFMPEG_ARGUMENTS): + cv.string, }) @@ -69,6 +73,7 @@ class ArloCam(Camera): self._motion_status = False self._ffmpeg = hass.data[DATA_FFMPEG] self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS) + self.attrs = {} def camera_image(self): """Return a still image response from the camera.""" @@ -100,32 +105,24 @@ class ArloCam(Camera): def device_state_attributes(self): """Return the state attributes.""" return { - ATTR_BATTERY_LEVEL: - self._camera.get_battery_level, - ATTR_BRIGHTNESS: - self._camera.get_brightness, - ATTR_FLIPPED: - self._camera.get_flip_state, - ATTR_MIRRORED: - self._camera.get_mirror_state, - ATTR_MOTION_SENSITIVITY: - self._camera.get_motion_detection_sensitivity, - ATTR_POWER_SAVE_MODE: - POWERSAVE_MODE_MAPPING[self._camera.get_powersave_mode], - ATTR_SIGNAL_STRENGTH: - self._camera.get_signal_strength, - ATTR_UNSEEN_VIDEOS: - self._camera.unseen_videos + ATTR_BATTERY_LEVEL: self.attrs.get(ATTR_BATTERY_LEVEL), + ATTR_BRIGHTNESS: self.attrs.get(ATTR_BRIGHTNESS), + ATTR_FLIPPED: self.attrs.get(ATTR_FLIPPED), + ATTR_MIRRORED: self.attrs.get(ATTR_MIRRORED), + ATTR_MOTION: self.attrs.get(ATTR_MOTION), + ATTR_POWERSAVE: self.attrs.get(ATTR_POWERSAVE), + ATTR_SIGNAL_STRENGTH: self.attrs.get(ATTR_SIGNAL_STRENGTH), + ATTR_UNSEEN_VIDEOS: self.attrs.get(ATTR_UNSEEN_VIDEOS), } @property def model(self): - """Camera model.""" + """Return the camera model.""" return self._camera.model_id @property def brand(self): - """Camera brand.""" + """Return the camera brand.""" return DEFAULT_BRAND @property @@ -135,7 +132,7 @@ class ArloCam(Camera): @property def motion_detection_enabled(self): - """Camera Motion Detection Status.""" + """Return the camera motion detection status.""" return self._motion_status def set_base_station_mode(self, mode): @@ -143,7 +140,7 @@ class ArloCam(Camera): # Get the list of base stations identified by library base_stations = self.hass.data[DATA_ARLO].base_stations - # Some Arlo cameras does not have basestation + # Some Arlo cameras does not have base station # So check if there is base station detected first # if yes, then choose the primary base station # Set the mode on the chosen base station @@ -160,3 +157,16 @@ class ArloCam(Camera): """Disable the motion detection in base station (Disarm).""" self._motion_status = False self.set_base_station_mode(ARLO_MODE_DISARMED) + + def update(self): + """Add an attribute-update task to the executor pool.""" + self.attrs[ATTR_BATTERY_LEVEL] = self._camera.get_battery_level + self.attrs[ATTR_BRIGHTNESS] = self._camera.get_battery_level + self.attrs[ATTR_FLIPPED] = self._camera.get_flip_state, + self.attrs[ATTR_MIRRORED] = self._camera.get_mirror_state, + self.attrs[ + ATTR_MOTION] = self._camera.get_motion_detection_sensitivity, + self.attrs[ATTR_POWERSAVE] = POWERSAVE_MODE_MAPPING[ + self._camera.get_powersave_mode], + self.attrs[ATTR_SIGNAL_STRENGTH] = self._camera.get_signal_strength, + self.attrs[ATTR_UNSEEN_VIDEOS] = self._camera.unseen_videos diff --git a/homeassistant/components/camera/skybell.py b/homeassistant/components/camera/skybell.py new file mode 100644 index 00000000000..be3504dab78 --- /dev/null +++ b/homeassistant/components/camera/skybell.py @@ -0,0 +1,67 @@ +""" +Camera support for the Skybell HD Doorbell. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/camera.skybell/ +""" +from datetime import timedelta +import logging + +import requests + +from homeassistant.components.camera import Camera +from homeassistant.components.skybell import ( + DOMAIN as SKYBELL_DOMAIN, SkybellDevice) + +DEPENDENCIES = ['skybell'] + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=90) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the platform for a Skybell device.""" + skybell = hass.data.get(SKYBELL_DOMAIN) + + sensors = [] + for device in skybell.get_devices(): + sensors.append(SkybellCamera(device)) + + add_devices(sensors, True) + + +class SkybellCamera(SkybellDevice, Camera): + """A camera implementation for Skybell devices.""" + + def __init__(self, device): + """Initialize a camera for a Skybell device.""" + SkybellDevice.__init__(self, device) + Camera.__init__(self) + self._name = self._device.name + self._url = None + self._response = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + def camera_image(self): + """Get the latest camera image.""" + super().update() + + if self._url != self._device.image: + self._url = self._device.image + + try: + self._response = requests.get( + self._url, stream=True, timeout=10) + except requests.HTTPError as err: + _LOGGER.warning("Failed to get camera image: %s", err) + self._response = None + + if not self._response: + return None + + return self._response.content diff --git a/homeassistant/components/camera/synology.py b/homeassistant/components/camera/synology.py index 90dfa58d8c5..0b97f55397c 100644 --- a/homeassistant/components/camera/synology.py +++ b/homeassistant/components/camera/synology.py @@ -7,44 +7,25 @@ https://home-assistant.io/components/camera.synology/ import asyncio import logging +import requests import voluptuous as vol -import aiohttp -import async_timeout - from homeassistant.const import ( CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL, CONF_TIMEOUT) from homeassistant.components.camera import ( Camera, PLATFORM_SCHEMA) from homeassistant.helpers.aiohttp_client import ( - async_get_clientsession, async_create_clientsession, + async_create_clientsession, async_aiohttp_proxy_web) import homeassistant.helpers.config_validation as cv -from homeassistant.util.async import run_coroutine_threadsafe + +REQUIREMENTS = ['py-synology==0.1.3'] _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'Synology Camera' -DEFAULT_STREAM_ID = '0' DEFAULT_TIMEOUT = 5 -CONF_CAMERA_NAME = 'camera_name' -CONF_STREAM_ID = 'stream_id' - -QUERY_CGI = 'query.cgi' -QUERY_API = 'SYNO.API.Info' -AUTH_API = 'SYNO.API.Auth' -CAMERA_API = 'SYNO.SurveillanceStation.Camera' -STREAMING_API = 'SYNO.SurveillanceStation.VideoStream' -SESSION_ID = '0' - -WEBAPI_PATH = '/webapi/' -AUTH_PATH = 'auth.cgi' -CAMERA_PATH = 'camera.cgi' -STREAMING_PATH = 'SurveillanceStation/videoStreaming.cgi' -CONTENT_TYPE_HEADER = 'Content-Type' - -SYNO_API_URL = '{0}{1}{2}' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -62,189 +43,89 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up a Synology IP Camera.""" verify_ssl = config.get(CONF_VERIFY_SSL) timeout = config.get(CONF_TIMEOUT) - websession_init = async_get_clientsession(hass, verify_ssl) - # Determine API to use for authentication - syno_api_url = SYNO_API_URL.format( - config.get(CONF_URL), WEBAPI_PATH, QUERY_CGI) - - query_payload = { - 'api': QUERY_API, - 'method': 'Query', - 'version': '1', - 'query': 'SYNO.' - } try: - with async_timeout.timeout(timeout, loop=hass.loop): - query_req = yield from websession_init.get( - syno_api_url, - params=query_payload - ) - - # Skip content type check because Synology doesn't return JSON with - # right content type - query_resp = yield from query_req.json(content_type=None) - auth_path = query_resp['data'][AUTH_API]['path'] - camera_api = query_resp['data'][CAMERA_API]['path'] - camera_path = query_resp['data'][CAMERA_API]['path'] - streaming_path = query_resp['data'][STREAMING_API]['path'] - - except (asyncio.TimeoutError, aiohttp.ClientError): - _LOGGER.exception("Error on %s", syno_api_url) + from synology.surveillance_station import SurveillanceStation + surveillance = SurveillanceStation( + config.get(CONF_URL), + config.get(CONF_USERNAME), + config.get(CONF_PASSWORD), + verify_ssl=verify_ssl, + timeout=timeout + ) + except (requests.exceptions.RequestException, ValueError): + _LOGGER.exception("Error when initializing SurveillanceStation") return False - # Authticate to NAS to get a session id - syno_auth_url = SYNO_API_URL.format( - config.get(CONF_URL), WEBAPI_PATH, auth_path) - - session_id = yield from get_session_id( - hass, - websession_init, - config.get(CONF_USERNAME), - config.get(CONF_PASSWORD), - syno_auth_url, - timeout - ) - - # init websession - websession = async_create_clientsession( - hass, verify_ssl, cookies={'id': session_id}) - - # Use SessionID to get cameras in system - syno_camera_url = SYNO_API_URL.format( - config.get(CONF_URL), WEBAPI_PATH, camera_api) - - camera_payload = { - 'api': CAMERA_API, - 'method': 'List', - 'version': '1' - } - try: - with async_timeout.timeout(timeout, loop=hass.loop): - camera_req = yield from websession.get( - syno_camera_url, - params=camera_payload - ) - except (asyncio.TimeoutError, aiohttp.ClientError): - _LOGGER.exception("Error on %s", syno_camera_url) - return False - - camera_resp = yield from camera_req.json(content_type=None) - cameras = camera_resp['data']['cameras'] + cameras = surveillance.get_all_cameras() + websession = async_create_clientsession(hass, verify_ssl) # add cameras devices = [] for camera in cameras: if not config.get(CONF_WHITELIST): - camera_id = camera['id'] - snapshot_path = camera['snapshot_path'] - - device = SynologyCamera( - hass, websession, config, camera_id, camera['name'], - snapshot_path, streaming_path, camera_path, auth_path, timeout - ) + device = SynologyCamera(websession, surveillance, camera.camera_id) devices.append(device) async_add_devices(devices) -@asyncio.coroutine -def get_session_id(hass, websession, username, password, login_url, timeout): - """Get a session id.""" - auth_payload = { - 'api': AUTH_API, - 'method': 'Login', - 'version': '2', - 'account': username, - 'passwd': password, - 'session': 'SurveillanceStation', - 'format': 'sid' - } - try: - with async_timeout.timeout(timeout, loop=hass.loop): - auth_req = yield from websession.get( - login_url, - params=auth_payload - ) - auth_resp = yield from auth_req.json(content_type=None) - return auth_resp['data']['sid'] - - except (asyncio.TimeoutError, aiohttp.ClientError): - _LOGGER.exception("Error on %s", login_url) - return False - - class SynologyCamera(Camera): """An implementation of a Synology NAS based IP camera.""" - def __init__(self, hass, websession, config, camera_id, - camera_name, snapshot_path, streaming_path, camera_path, - auth_path, timeout): + def __init__(self, websession, surveillance, camera_id): """Initialize a Synology Surveillance Station camera.""" super().__init__() - self.hass = hass self._websession = websession - self._name = camera_name - self._synology_url = config.get(CONF_URL) - self._camera_name = config.get(CONF_CAMERA_NAME) - self._stream_id = config.get(CONF_STREAM_ID) + self._surveillance = surveillance self._camera_id = camera_id - self._snapshot_path = snapshot_path - self._streaming_path = streaming_path - self._camera_path = camera_path - self._auth_path = auth_path - self._timeout = timeout + self._camera = self._surveillance.get_camera(camera_id) + self._motion_setting = self._surveillance.get_motion_setting(camera_id) + self.is_streaming = self._camera.is_enabled def camera_image(self): """Return bytes of camera image.""" - return run_coroutine_threadsafe( - self.async_camera_image(), self.hass.loop).result() - - @asyncio.coroutine - def async_camera_image(self): - """Return a still image response from the camera.""" - image_url = SYNO_API_URL.format( - self._synology_url, WEBAPI_PATH, self._camera_path) - - image_payload = { - 'api': CAMERA_API, - 'method': 'GetSnapshot', - 'version': '1', - 'cameraId': self._camera_id - } - try: - with async_timeout.timeout(self._timeout, loop=self.hass.loop): - response = yield from self._websession.get( - image_url, - params=image_payload - ) - except (asyncio.TimeoutError, aiohttp.ClientError): - _LOGGER.error("Error fetching %s", image_url) - return None - - image = yield from response.read() - - return image + return self._surveillance.get_camera_image(self._camera_id) @asyncio.coroutine def handle_async_mjpeg_stream(self, request): """Return a MJPEG stream image response directly from the camera.""" - streaming_url = SYNO_API_URL.format( - self._synology_url, WEBAPI_PATH, self._streaming_path) - - streaming_payload = { - 'api': STREAMING_API, - 'method': 'Stream', - 'version': '1', - 'cameraId': self._camera_id, - 'format': 'mjpeg' - } - stream_coro = self._websession.get( - streaming_url, params=streaming_payload) + streaming_url = self._camera.video_stream_url + stream_coro = self._websession.get(streaming_url) yield from async_aiohttp_proxy_web(self.hass, request, stream_coro) @property def name(self): """Return the name of this device.""" - return self._name + return self._camera.name + + @property + def is_recording(self): + """Return true if the device is recording.""" + return self._camera.is_recording + + def should_poll(self): + """Update the recording state periodically.""" + return True + + def update(self): + """Update the status of the camera.""" + self._surveillance.update() + self._camera = self._surveillance.get_camera(self._camera.camera_id) + self._motion_setting = self._surveillance.get_motion_setting( + self._camera.camera_id) + self.is_streaming = self._camera.is_enabled + + @property + def motion_detection_enabled(self): + """Return the camera motion detection status.""" + return self._motion_setting.is_enabled + + def enable_motion_detection(self): + """Enable motion detection in the camera.""" + self._surveillance.enable_motion_detection(self._camera_id) + + def disable_motion_detection(self): + """Disable motion detection in camera.""" + self._surveillance.disable_motion_detection(self._camera_id) diff --git a/homeassistant/components/climate/ecobee.py b/homeassistant/components/climate/ecobee.py index 6780d3745f0..d6d92432730 100644 --- a/homeassistant/components/climate/ecobee.py +++ b/homeassistant/components/climate/ecobee.py @@ -27,6 +27,7 @@ ATTR_RESUME_ALL = 'resume_all' DEFAULT_RESUME_ALL = False TEMPERATURE_HOLD = 'temp' VACATION_HOLD = 'vacation' +AWAY_MODE = 'awayMode' DEPENDENCIES = ['ecobee'] @@ -144,20 +145,20 @@ class Thermostat(ClimateDevice): @property def current_temperature(self): """Return the current temperature.""" - return self.thermostat['runtime']['actualTemperature'] / 10 + return self.thermostat['runtime']['actualTemperature'] / 10.0 @property def target_temperature_low(self): """Return the lower bound temperature we try to reach.""" if self.current_operation == STATE_AUTO: - return int(self.thermostat['runtime']['desiredHeat'] / 10) + return self.thermostat['runtime']['desiredHeat'] / 10.0 return None @property def target_temperature_high(self): """Return the upper bound temperature we try to reach.""" if self.current_operation == STATE_AUTO: - return int(self.thermostat['runtime']['desiredCool'] / 10) + return self.thermostat['runtime']['desiredCool'] / 10.0 return None @property @@ -166,9 +167,9 @@ class Thermostat(ClimateDevice): if self.current_operation == STATE_AUTO: return None if self.current_operation == STATE_HEAT: - return int(self.thermostat['runtime']['desiredHeat'] / 10) + return self.thermostat['runtime']['desiredHeat'] / 10.0 elif self.current_operation == STATE_COOL: - return int(self.thermostat['runtime']['desiredCool'] / 10) + return self.thermostat['runtime']['desiredCool'] / 10.0 return None @property @@ -186,6 +187,11 @@ class Thermostat(ClimateDevice): @property def current_hold_mode(self): """Return current hold mode.""" + mode = self._current_hold_mode + return None if mode == AWAY_MODE else mode + + @property + def _current_hold_mode(self): events = self.thermostat['events'] for event in events: if event['running']: @@ -195,8 +201,8 @@ class Thermostat(ClimateDevice): int(event['startDate'][0:4]) <= 1: # A temporary hold from away climate is a hold return 'away' - # A permanent hold from away climate is away_mode - return None + # A permanent hold from away climate + return AWAY_MODE elif event['holdClimateRef'] != "": # Any other hold based on climate return event['holdClimateRef'] @@ -269,7 +275,7 @@ class Thermostat(ClimateDevice): @property def is_away_mode_on(self): """Return true if away mode is on.""" - return self.current_hold_mode == 'away' + return self._current_hold_mode == AWAY_MODE @property def is_aux_heat_on(self): @@ -277,12 +283,17 @@ class Thermostat(ClimateDevice): return 'auxHeat' in self.thermostat['equipmentStatus'] def turn_away_mode_on(self): - """Turn away on.""" - self.set_hold_mode('away') + """Turn away mode on by setting it on away hold indefinitely.""" + if self._current_hold_mode != AWAY_MODE: + self.data.ecobee.set_climate_hold(self.thermostat_index, 'away', + 'indefinite') + self.update_without_throttle = True def turn_away_mode_off(self): """Turn away off.""" - self.set_hold_mode(None) + if self._current_hold_mode == AWAY_MODE: + self.data.ecobee.resume_program(self.thermostat_index) + self.update_without_throttle = True def set_hold_mode(self, hold_mode): """Set hold mode (away, home, temp, sleep, etc.).""" @@ -299,7 +310,7 @@ class Thermostat(ClimateDevice): self.data.ecobee.resume_program(self.thermostat_index) else: if hold_mode == TEMPERATURE_HOLD: - self.set_temp_hold(int(self.current_temperature)) + self.set_temp_hold(self.current_temperature) else: self.data.ecobee.set_climate_hold( self.thermostat_index, hold_mode, self.hold_preference()) @@ -325,15 +336,11 @@ class Thermostat(ClimateDevice): elif self.current_operation == STATE_COOL: heat_temp = temp - 20 cool_temp = temp - - self.data.ecobee.set_hold_temp(self.thermostat_index, cool_temp, - heat_temp, self.hold_preference()) - _LOGGER.debug("Setting ecobee hold_temp to: low=%s, is=%s, " - "cool=%s, is=%s", heat_temp, isinstance( - heat_temp, (int, float)), cool_temp, - isinstance(cool_temp, (int, float))) - - self.update_without_throttle = True + else: + # In auto mode set temperature between + heat_temp = temp - 10 + cool_temp = temp + 10 + self.set_auto_temp_hold(heat_temp, cool_temp) def set_temperature(self, **kwargs): """Set new target temperature.""" @@ -343,9 +350,9 @@ class Thermostat(ClimateDevice): if self.current_operation == STATE_AUTO and low_temp is not None \ and high_temp is not None: - self.set_auto_temp_hold(int(low_temp), int(high_temp)) + self.set_auto_temp_hold(low_temp, high_temp) elif temp is not None: - self.set_temp_hold(int(temp)) + self.set_temp_hold(temp) else: _LOGGER.error( "Missing valid arguments for set_temperature in %s", kwargs) @@ -364,7 +371,7 @@ class Thermostat(ClimateDevice): def resume_program(self, resume_all): """Resume the thermostat schedule program.""" self.data.ecobee.resume_program( - self.thermostat_index, str(resume_all).lower()) + self.thermostat_index, 'true' if resume_all else 'false') self.update_without_throttle = True def hold_preference(self): diff --git a/homeassistant/components/climate/eq3btsmart.py b/homeassistant/components/climate/eq3btsmart.py index ff13dd48cac..d70890317fd 100644 --- a/homeassistant/components/climate/eq3btsmart.py +++ b/homeassistant/components/climate/eq3btsmart.py @@ -17,7 +17,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-eq3bt==0.1.5'] +REQUIREMENTS = ['python-eq3bt==0.1.6'] _LOGGER = logging.getLogger(__name__) @@ -164,4 +164,8 @@ class EQ3BTSmartThermostat(ClimateDevice): def update(self): """Update the data from the thermostat.""" - self._thermostat.update() + from bluepy.btle import BTLEException + try: + self._thermostat.update() + except BTLEException as ex: + _LOGGER.warning("Updating the state failed: %s", ex) diff --git a/homeassistant/components/climate/mqtt.py b/homeassistant/components/climate/mqtt.py new file mode 100644 index 00000000000..2f7bba74185 --- /dev/null +++ b/homeassistant/components/climate/mqtt.py @@ -0,0 +1,483 @@ +""" +Support for MQTT climate devices. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/climate.mqtt/ +""" +import asyncio +import logging + +import voluptuous as vol + +from homeassistant.core import callback +import homeassistant.components.mqtt as mqtt + +from homeassistant.components.climate import ( + STATE_HEAT, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, ClimateDevice, + PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, STATE_AUTO, + ATTR_OPERATION_MODE) +from homeassistant.const import ( + STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME) +from homeassistant.components.mqtt import (CONF_QOS, CONF_RETAIN) +import homeassistant.helpers.config_validation as cv +from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, + SPEED_HIGH) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['mqtt'] + +DEFAULT_NAME = 'MQTT HVAC' + +CONF_POWER_COMMAND_TOPIC = 'power_command_topic' +CONF_POWER_STATE_TOPIC = 'power_state_topic' +CONF_MODE_COMMAND_TOPIC = 'mode_command_topic' +CONF_MODE_STATE_TOPIC = 'mode_state_topic' +CONF_TEMPERATURE_COMMAND_TOPIC = 'temperature_command_topic' +CONF_TEMPERATURE_STATE_TOPIC = 'temperature_state_topic' +CONF_FAN_MODE_COMMAND_TOPIC = 'fan_mode_command_topic' +CONF_FAN_MODE_STATE_TOPIC = 'fan_mode_state_topic' +CONF_SWING_MODE_COMMAND_TOPIC = 'swing_mode_command_topic' +CONF_SWING_MODE_STATE_TOPIC = 'swing_mode_state_topic' +CONF_AWAY_MODE_COMMAND_TOPIC = 'away_mode_command_topic' +CONF_AWAY_MODE_STATE_TOPIC = 'away_mode_state_topic' +CONF_HOLD_COMMAND_TOPIC = 'hold_command_topic' +CONF_HOLD_STATE_TOPIC = 'hold_state_topic' +CONF_AUX_COMMAND_TOPIC = 'aux_command_topic' +CONF_AUX_STATE_TOPIC = 'aux_state_topic' + +CONF_CURRENT_TEMPERATURE_TOPIC = 'current_temperature_topic' + +CONF_PAYLOAD_ON = 'payload_on' +CONF_PAYLOAD_OFF = 'payload_off' + +CONF_FAN_MODE_LIST = 'fan_modes' +CONF_MODE_LIST = 'modes' +CONF_SWING_MODE_LIST = 'swing_modes' +CONF_INITIAL = 'initial' +CONF_SEND_IF_OFF = 'send_if_off' + +PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_POWER_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_TEMPERATURE_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_FAN_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_SWING_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_AWAY_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_HOLD_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_AUX_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_POWER_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_TEMPERATURE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_FAN_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_SWING_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_AWAY_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_HOLD_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_AUX_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_CURRENT_TEMPERATURE_TOPIC): + mqtt.valid_subscribe_topic, + vol.Optional(CONF_FAN_MODE_LIST, + default=[STATE_AUTO, SPEED_LOW, + SPEED_MEDIUM, SPEED_HIGH]): cv.ensure_list, + vol.Optional(CONF_SWING_MODE_LIST, + default=[STATE_ON, STATE_OFF]): cv.ensure_list, + vol.Optional(CONF_MODE_LIST, + default=[STATE_AUTO, STATE_OFF, STATE_COOL, STATE_HEAT, + STATE_DRY, STATE_FAN_ONLY]): cv.ensure_list, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_INITIAL, default=21): cv.positive_int, + vol.Optional(CONF_SEND_IF_OFF, default=True): cv.boolean, + vol.Optional(CONF_PAYLOAD_ON, default="ON"): cv.string, + vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string, +}) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up the MQTT climate devices.""" + async_add_devices([ + MqttClimate( + hass, + config.get(CONF_NAME), + { + key: config.get(key) for key in ( + CONF_POWER_COMMAND_TOPIC, + CONF_MODE_COMMAND_TOPIC, + CONF_TEMPERATURE_COMMAND_TOPIC, + CONF_FAN_MODE_COMMAND_TOPIC, + CONF_SWING_MODE_COMMAND_TOPIC, + CONF_AWAY_MODE_COMMAND_TOPIC, + CONF_HOLD_COMMAND_TOPIC, + CONF_AUX_COMMAND_TOPIC, + CONF_POWER_STATE_TOPIC, + CONF_MODE_STATE_TOPIC, + CONF_TEMPERATURE_STATE_TOPIC, + CONF_FAN_MODE_STATE_TOPIC, + CONF_SWING_MODE_STATE_TOPIC, + CONF_AWAY_MODE_STATE_TOPIC, + CONF_HOLD_STATE_TOPIC, + CONF_AUX_STATE_TOPIC, + CONF_CURRENT_TEMPERATURE_TOPIC + ) + }, + config.get(CONF_QOS), + config.get(CONF_RETAIN), + config.get(CONF_MODE_LIST), + config.get(CONF_FAN_MODE_LIST), + config.get(CONF_SWING_MODE_LIST), + config.get(CONF_INITIAL), + False, None, SPEED_LOW, + STATE_OFF, STATE_OFF, False, + config.get(CONF_SEND_IF_OFF), + config.get(CONF_PAYLOAD_ON), + config.get(CONF_PAYLOAD_OFF)) + ]) + + +class MqttClimate(ClimateDevice): + """Representation of a demo climate device.""" + + def __init__(self, hass, name, topic, qos, retain, mode_list, + fan_mode_list, swing_mode_list, target_temperature, away, + hold, current_fan_mode, current_swing_mode, + current_operation, aux, send_if_off, payload_on, + payload_off): + """Initialize the climate device.""" + self.hass = hass + self._name = name + self._topic = topic + self._qos = qos + self._retain = retain + self._target_temperature = target_temperature + self._unit_of_measurement = hass.config.units.temperature_unit + self._away = away + self._hold = hold + self._current_temperature = None + self._current_fan_mode = current_fan_mode + self._current_operation = current_operation + self._aux = aux + self._current_swing_mode = current_swing_mode + self._fan_list = fan_mode_list + self._operation_list = mode_list + self._swing_list = swing_mode_list + self._target_temperature_step = 1 + self._send_if_off = send_if_off + self._payload_on = payload_on + self._payload_off = payload_off + + def async_added_to_hass(self): + """Handle being added to home assistant.""" + @callback + def handle_current_temp_received(topic, payload, qos): + """Handle current temperature coming via MQTT.""" + try: + self._current_temperature = float(payload) + self.async_schedule_update_ha_state() + except ValueError: + _LOGGER.error("Could not parse temperature from %s", payload) + + if self._topic[CONF_CURRENT_TEMPERATURE_TOPIC] is not None: + yield from mqtt.async_subscribe( + self.hass, self._topic[CONF_CURRENT_TEMPERATURE_TOPIC], + handle_current_temp_received, self._qos) + + @callback + def handle_mode_received(topic, payload, qos): + """Handle receiving mode via MQTT.""" + if payload not in self._operation_list: + _LOGGER.error("Invalid mode: %s", payload) + else: + self._current_operation = payload + self.async_schedule_update_ha_state() + + if self._topic[CONF_MODE_STATE_TOPIC] is not None: + yield from mqtt.async_subscribe( + self.hass, self._topic[CONF_MODE_STATE_TOPIC], + handle_mode_received, self._qos) + + @callback + def handle_temperature_received(topic, payload, qos): + """Handle target temperature coming via MQTT.""" + try: + self._target_temperature = float(payload) + self.async_schedule_update_ha_state() + except ValueError: + _LOGGER.error("Could not parse temperature from %s", payload) + + if self._topic[CONF_TEMPERATURE_STATE_TOPIC] is not None: + yield from mqtt.async_subscribe( + self.hass, self._topic[CONF_TEMPERATURE_STATE_TOPIC], + handle_temperature_received, self._qos) + + @callback + def handle_fan_mode_received(topic, payload, qos): + """Handle receiving fan mode via MQTT.""" + if payload not in self._fan_list: + _LOGGER.error("Invalid fan mode: %s", payload) + else: + self._current_fan_mode = payload + self.async_schedule_update_ha_state() + + if self._topic[CONF_FAN_MODE_STATE_TOPIC] is not None: + yield from mqtt.async_subscribe( + self.hass, self._topic[CONF_FAN_MODE_STATE_TOPIC], + handle_fan_mode_received, self._qos) + + @callback + def handle_swing_mode_received(topic, payload, qos): + """Handle receiving swing mode via MQTT.""" + if payload not in self._swing_list: + _LOGGER.error("Invalid swing mode: %s", payload) + else: + self._current_swing_mode = payload + self.async_schedule_update_ha_state() + + if self._topic[CONF_SWING_MODE_STATE_TOPIC] is not None: + yield from mqtt.async_subscribe( + self.hass, self._topic[CONF_SWING_MODE_STATE_TOPIC], + handle_swing_mode_received, self._qos) + + @callback + def handle_away_mode_received(topic, payload, qos): + """Handle receiving away mode via MQTT.""" + if payload == self._payload_on: + self._away = True + elif payload == self._payload_off: + self._away = False + else: + _LOGGER.error("Invalid away mode: %s", payload) + + self.async_schedule_update_ha_state() + + if self._topic[CONF_AWAY_MODE_STATE_TOPIC] is not None: + yield from mqtt.async_subscribe( + self.hass, self._topic[CONF_AWAY_MODE_STATE_TOPIC], + handle_away_mode_received, self._qos) + + @callback + def handle_aux_mode_received(topic, payload, qos): + """Handle receiving aux mode via MQTT.""" + if payload == self._payload_on: + self._aux = True + elif payload == self._payload_off: + self._aux = False + else: + _LOGGER.error("Invalid aux mode: %s", payload) + + self.async_schedule_update_ha_state() + + if self._topic[CONF_AUX_STATE_TOPIC] is not None: + yield from mqtt.async_subscribe( + self.hass, self._topic[CONF_AUX_STATE_TOPIC], + handle_aux_mode_received, self._qos) + + @callback + def handle_hold_mode_received(topic, payload, qos): + """Handle receiving hold mode via MQTT.""" + self._hold = payload + self.async_schedule_update_ha_state() + + if self._topic[CONF_HOLD_STATE_TOPIC] is not None: + yield from mqtt.async_subscribe( + self.hass, self._topic[CONF_HOLD_STATE_TOPIC], + handle_hold_mode_received, self._qos) + + @property + def should_poll(self): + """Return the polling state.""" + return False + + @property + def name(self): + """Return the name of the climate device.""" + return self._name + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return self._unit_of_measurement + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._current_temperature + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._target_temperature + + @property + def current_operation(self): + """Return current operation ie. heat, cool, idle.""" + return self._current_operation + + @property + def operation_list(self): + """Return the list of available operation modes.""" + return self._operation_list + + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + return self._target_temperature_step + + @property + def is_away_mode_on(self): + """Return if away mode is on.""" + return self._away + + @property + def current_hold_mode(self): + """Return hold mode setting.""" + return self._hold + + @property + def is_aux_heat_on(self): + """Return true if away mode is on.""" + return self._aux + + @property + def current_fan_mode(self): + """Return the fan setting.""" + return self._current_fan_mode + + @property + def fan_list(self): + """Return the list of available fan modes.""" + return self._fan_list + + @asyncio.coroutine + def async_set_temperature(self, **kwargs): + """Set new target temperatures.""" + if kwargs.get(ATTR_OPERATION_MODE) is not None: + operation_mode = kwargs.get(ATTR_OPERATION_MODE) + yield from self.async_set_operation_mode(operation_mode) + + if kwargs.get(ATTR_TEMPERATURE) is not None: + if self._topic[CONF_TEMPERATURE_STATE_TOPIC] is None: + # optimistic mode + self._target_temperature = kwargs.get(ATTR_TEMPERATURE) + + if self._send_if_off or self._current_operation != STATE_OFF: + mqtt.async_publish( + self.hass, self._topic[CONF_TEMPERATURE_COMMAND_TOPIC], + kwargs.get(ATTR_TEMPERATURE), self._qos, self._retain) + + self.async_schedule_update_ha_state() + + @asyncio.coroutine + def async_set_swing_mode(self, swing_mode): + """Set new swing mode.""" + if self._send_if_off or self._current_operation != STATE_OFF: + mqtt.async_publish( + self.hass, self._topic[CONF_SWING_MODE_COMMAND_TOPIC], + swing_mode, self._qos, self._retain) + + if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None: + self._current_swing_mode = swing_mode + self.async_schedule_update_ha_state() + + @asyncio.coroutine + def async_set_fan_mode(self, fan): + """Set new target temperature.""" + if self._send_if_off or self._current_operation != STATE_OFF: + mqtt.async_publish( + self.hass, self._topic[CONF_FAN_MODE_COMMAND_TOPIC], + fan, self._qos, self._retain) + + if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None: + self._current_fan_mode = fan + self.async_schedule_update_ha_state() + + @asyncio.coroutine + def async_set_operation_mode(self, operation_mode) -> None: + """Set new operation mode.""" + if self._topic[CONF_POWER_COMMAND_TOPIC] is not None: + if (self._current_operation == STATE_OFF and + operation_mode != STATE_OFF): + mqtt.async_publish( + self.hass, self._topic[CONF_POWER_COMMAND_TOPIC], + self._payload_on, self._qos, self._retain) + elif (self._current_operation != STATE_OFF and + operation_mode == STATE_OFF): + mqtt.async_publish( + self.hass, self._topic[CONF_POWER_COMMAND_TOPIC], + self._payload_off, self._qos, self._retain) + + if self._topic[CONF_MODE_COMMAND_TOPIC] is not None: + mqtt.async_publish( + self.hass, self._topic[CONF_MODE_COMMAND_TOPIC], + operation_mode, self._qos, self._retain) + + if self._topic[CONF_MODE_STATE_TOPIC] is None: + self._current_operation = operation_mode + self.async_schedule_update_ha_state() + + @property + def current_swing_mode(self): + """Return the swing setting.""" + return self._current_swing_mode + + @property + def swing_list(self): + """List of available swing modes.""" + return self._swing_list + + @asyncio.coroutine + def async_turn_away_mode_on(self): + """Turn away mode on.""" + if self._topic[CONF_AWAY_MODE_COMMAND_TOPIC] is not None: + mqtt.async_publish(self.hass, + self._topic[CONF_AWAY_MODE_COMMAND_TOPIC], + self._payload_on, self._qos, self._retain) + + if self._topic[CONF_AWAY_MODE_STATE_TOPIC] is None: + self._away = True + self.async_schedule_update_ha_state() + + @asyncio.coroutine + def async_turn_away_mode_off(self): + """Turn away mode off.""" + if self._topic[CONF_AWAY_MODE_COMMAND_TOPIC] is not None: + mqtt.async_publish(self.hass, + self._topic[CONF_AWAY_MODE_COMMAND_TOPIC], + self._payload_off, self._qos, self._retain) + + if self._topic[CONF_AWAY_MODE_STATE_TOPIC] is None: + self._away = False + self.async_schedule_update_ha_state() + + @asyncio.coroutine + def async_set_hold_mode(self, hold): + """Update hold mode on.""" + if self._topic[CONF_HOLD_COMMAND_TOPIC] is not None: + mqtt.async_publish(self.hass, + self._topic[CONF_HOLD_COMMAND_TOPIC], + hold, self._qos, self._retain) + + if self._topic[CONF_HOLD_STATE_TOPIC] is None: + self._hold = hold + self.async_schedule_update_ha_state() + + @asyncio.coroutine + def async_turn_aux_heat_on(self): + """Turn auxillary heater on.""" + if self._topic[CONF_AUX_COMMAND_TOPIC] is not None: + mqtt.async_publish(self.hass, self._topic[CONF_AUX_COMMAND_TOPIC], + self._payload_on, self._qos, self._retain) + + if self._topic[CONF_AUX_STATE_TOPIC] is None: + self._aux = True + self.async_schedule_update_ha_state() + + @asyncio.coroutine + def async_turn_aux_heat_off(self): + """Turn auxillary heater off.""" + if self._topic[CONF_AUX_COMMAND_TOPIC] is not None: + mqtt.async_publish(self.hass, self._topic[CONF_AUX_COMMAND_TOPIC], + self._payload_off, self._qos, self._retain) + + if self._topic[CONF_AUX_STATE_TOPIC] is None: + self._aux = False + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/climate/tesla.py b/homeassistant/components/climate/tesla.py index 39d002e72d9..684d131d960 100644 --- a/homeassistant/components/climate/tesla.py +++ b/homeassistant/components/climate/tesla.py @@ -35,7 +35,6 @@ class TeslaThermostat(TeslaDevice, ClimateDevice): self.entity_id = ENTITY_ID_FORMAT.format(self.tesla_id) self._target_temperature = None self._temperature = None - self._name = self.tesla_device.name @property def current_operation(self): diff --git a/homeassistant/components/cover/rflink.py b/homeassistant/components/cover/rflink.py new file mode 100644 index 00000000000..a9b7598159f --- /dev/null +++ b/homeassistant/components/cover/rflink.py @@ -0,0 +1,121 @@ +""" +Support for Rflink Cover devices. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/cover.rflink/ +""" +import asyncio +import logging + +import voluptuous as vol + +from homeassistant.components.rflink import ( + DATA_ENTITY_GROUP_LOOKUP, DATA_ENTITY_LOOKUP, + DEVICE_DEFAULTS_SCHEMA, EVENT_KEY_COMMAND, RflinkCommand) +from homeassistant.components.cover import ( + CoverDevice, PLATFORM_SCHEMA) +import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_NAME + + +DEPENDENCIES = ['rflink'] + +_LOGGER = logging.getLogger(__name__) + + +CONF_ALIASES = 'aliases' +CONF_GROUP_ALIASES = 'group_aliases' +CONF_GROUP = 'group' +CONF_NOGROUP_ALIASES = 'nogroup_aliases' +CONF_DEVICE_DEFAULTS = 'device_defaults' +CONF_DEVICES = 'devices' +CONF_AUTOMATIC_ADD = 'automatic_add' +CONF_FIRE_EVENT = 'fire_event' +CONF_IGNORE_DEVICES = 'ignore_devices' +CONF_RECONNECT_INTERVAL = 'reconnect_interval' +CONF_SIGNAL_REPETITIONS = 'signal_repetitions' +CONF_WAIT_FOR_ACK = 'wait_for_ack' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_DEVICE_DEFAULTS, default=DEVICE_DEFAULTS_SCHEMA({})): + DEVICE_DEFAULTS_SCHEMA, + vol.Optional(CONF_DEVICES, default={}): vol.Schema({ + cv.string: { + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_ALIASES, default=[]): + vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_GROUP_ALIASES, default=[]): + vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_NOGROUP_ALIASES, default=[]): + vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean, + vol.Optional(CONF_SIGNAL_REPETITIONS): vol.Coerce(int), + vol.Optional(CONF_GROUP, default=True): cv.boolean, + }, + }), +}) + + +def devices_from_config(domain_config, hass=None): + """Parse configuration and add Rflink cover devices.""" + devices = [] + for device_id, config in domain_config[CONF_DEVICES].items(): + device_config = dict(domain_config[CONF_DEVICE_DEFAULTS], **config) + device = RflinkCover(device_id, hass, **device_config) + devices.append(device) + + # Register entity (and aliases) to listen to incoming rflink events + # Device id and normal aliases respond to normal and group command + hass.data[DATA_ENTITY_LOOKUP][ + EVENT_KEY_COMMAND][device_id].append(device) + if config[CONF_GROUP]: + hass.data[DATA_ENTITY_GROUP_LOOKUP][ + EVENT_KEY_COMMAND][device_id].append(device) + for _id in config[CONF_ALIASES]: + hass.data[DATA_ENTITY_LOOKUP][ + EVENT_KEY_COMMAND][_id].append(device) + hass.data[DATA_ENTITY_GROUP_LOOKUP][ + EVENT_KEY_COMMAND][_id].append(device) + return devices + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up the Rflink cover platform.""" + async_add_devices(devices_from_config(config, hass)) + + +class RflinkCover(RflinkCommand, CoverDevice): + """Rflink entity which can switch on/stop/off (eg: cover).""" + + def _handle_event(self, event): + """Adjust state if Rflink picks up a remote command for this device.""" + self.cancel_queued_send_commands() + + command = event['command'] + if command in ['on', 'allon']: + self._state = True + elif command in ['off', 'alloff']: + self._state = False + + @property + def should_poll(self): + """No polling available in RFlink cover.""" + return False + + @property + def is_closed(self): + """Return if the cover is closed.""" + return None + + def async_close_cover(self, **kwargs): + """Turn the device close.""" + return self._async_handle_command("close_cover") + + def async_open_cover(self, **kwargs): + """Turn the device open.""" + return self._async_handle_command("open_cover") + + def async_stop_cover(self, **kwargs): + """Turn the device stop.""" + return self._async_handle_command("stop_cover") diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index 2f1dde05bab..b85c2d9a53b 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -87,8 +87,8 @@ def async_setup(hass, config): # Set up input boolean tasks.append(bootstrap.async_setup_component( - hass, 'input_slider', - {'input_slider': { + hass, 'input_number', + {'input_number': { 'noise_allowance': {'icon': 'mdi:bell-ring', 'min': 0, 'max': 10, @@ -163,7 +163,7 @@ def async_setup(hass, config): 'scene.romantic_lights'])) tasks2.append(group.Group.async_create_group(hass, 'Bedroom', [ lights[0], switches[1], media_players[0], - 'input_slider.noise_allowance'])) + 'input_number.noise_allowance'])) tasks2.append(group.Group.async_create_group(hass, 'Kitchen', [ lights[2], 'cover.kitchen_window', 'lock.kitchen_door'])) tasks2.append(group.Group.async_create_group(hass, 'Doors', [ diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 8192dfa751d..9a6dffc6101 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -18,11 +18,10 @@ from homeassistant.setup import async_prepare_setup_platform from homeassistant.core import callback from homeassistant.loader import bind_hass from homeassistant.components import group, zone -from homeassistant.components.discovery import SERVICE_NETGEAR 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 import config_per_platform from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.restore_state import async_get_last_state @@ -89,10 +88,6 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ cv.time_period, cv.positive_timedelta) }) -DISCOVERY_PLATFORMS = { - SERVICE_NETGEAR: 'netgear', -} - @bind_hass def is_on(hass: HomeAssistantType, entity_id: str=None): @@ -180,22 +175,6 @@ def async_setup(hass: HomeAssistantType, config: ConfigType): tracker.async_setup_group() - @callback - def async_device_tracker_discovered(service, info): - """Handle the discovery of device tracker platforms.""" - hass.async_add_job( - async_setup_platform(DISCOVERY_PLATFORMS[service], {}, info)) - - discovery.async_listen( - hass, DISCOVERY_PLATFORMS.keys(), async_device_tracker_discovered) - - @asyncio.coroutine - def async_platform_discovered(platform, info): - """Load a platform.""" - yield from async_setup_platform(platform, {}, disc_info=info) - - discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered) - # Clean up stale devices async_track_utc_time_change( hass, tracker.async_update_stale, second=range(0, 60, 5)) diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index 1c773f97692..f301b2f454e 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -1,5 +1,5 @@ """ -Support the OwnTracks platform. +Device tracker platform that adds support for OwnTracks over MQTT. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.owntracks/ @@ -64,13 +64,7 @@ def get_cipher(): @asyncio.coroutine def async_setup_scanner(hass, config, async_see, discovery_info=None): """Set up an OwnTracks tracker.""" - max_gps_accuracy = config.get(CONF_MAX_GPS_ACCURACY) - waypoint_import = config.get(CONF_WAYPOINT_IMPORT) - waypoint_whitelist = config.get(CONF_WAYPOINT_WHITELIST) - secret = config.get(CONF_SECRET) - - context = OwnTracksContext(async_see, secret, max_gps_accuracy, - waypoint_import, waypoint_whitelist) + context = context_from_config(async_see, config) @asyncio.coroutine def async_handle_mqtt_message(topic, payload, qos): @@ -179,6 +173,17 @@ def _decrypt_payload(secret, topic, ciphertext): return None +def context_from_config(async_see, config): + """Create an async context from Home Assistant config.""" + max_gps_accuracy = config.get(CONF_MAX_GPS_ACCURACY) + waypoint_import = config.get(CONF_WAYPOINT_IMPORT) + waypoint_whitelist = config.get(CONF_WAYPOINT_WHITELIST) + secret = config.get(CONF_SECRET) + + return OwnTracksContext(async_see, secret, max_gps_accuracy, + waypoint_import, waypoint_whitelist) + + class OwnTracksContext: """Hold the current OwnTracks context.""" @@ -402,7 +407,8 @@ def async_handle_message(hass, context, message): handler = HANDLERS.get(msgtype) if handler is None: - error = 'Received unsupported message type: {}.'.format(msgtype) - _LOGGER.warning(error) + _LOGGER.warning( + 'Received unsupported message type: %s.', msgtype) + return yield from handler(hass, context, message) diff --git a/homeassistant/components/device_tracker/owntracks_http.py b/homeassistant/components/device_tracker/owntracks_http.py new file mode 100644 index 00000000000..dcc3300cc12 --- /dev/null +++ b/homeassistant/components/device_tracker/owntracks_http.py @@ -0,0 +1,54 @@ +""" +Device tracker platform that adds support for OwnTracks over HTTP. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.owntracks_http/ +""" +import asyncio + +from aiohttp.web_exceptions import HTTPInternalServerError + +from homeassistant.components.http import HomeAssistantView + +# pylint: disable=unused-import +from .owntracks import ( # NOQA + REQUIREMENTS, PLATFORM_SCHEMA, context_from_config, async_handle_message) + + +DEPENDENCIES = ['http'] + + +@asyncio.coroutine +def async_setup_scanner(hass, config, async_see, discovery_info=None): + """Set up an OwnTracks tracker.""" + context = context_from_config(async_see, config) + + hass.http.register_view(OwnTracksView(context)) + + return True + + +class OwnTracksView(HomeAssistantView): + """View to handle OwnTracks HTTP requests.""" + + url = '/api/owntracks/{user}/{device}' + name = 'api:owntracks' + + def __init__(self, context): + """Initialize OwnTracks URL endpoints.""" + self.context = context + + @asyncio.coroutine + def post(self, request, user, device): + """Handle an OwnTracks message.""" + hass = request.app['hass'] + + message = yield from request.json() + message['topic'] = 'owntracks/{}/{}'.format(user, device) + + try: + yield from async_handle_message(hass, self.context, message) + return self.json([]) + + except ValueError: + raise HTTPInternalServerError diff --git a/homeassistant/components/device_tracker/snmp.py b/homeassistant/components/device_tracker/snmp.py index 25176cd82d0..d0cfcff20ef 100644 --- a/homeassistant/components/device_tracker/snmp.py +++ b/homeassistant/components/device_tracker/snmp.py @@ -16,7 +16,7 @@ from homeassistant.const import CONF_HOST _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pysnmp==4.3.9'] +REQUIREMENTS = ['pysnmp==4.3.10'] CONF_COMMUNITY = 'community' CONF_AUTHKEY = 'authkey' @@ -36,7 +36,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument def get_scanner(hass, config): - """Validate the configuration and return an snmp scanner.""" + """Validate the configuration and return an SNMP scanner.""" scanner = SnmpScanner(config[DOMAIN]) return scanner if scanner.success_init else None diff --git a/homeassistant/components/device_tracker/unifi.py b/homeassistant/components/device_tracker/unifi.py index a471ca5c96a..3ed41b08082 100644 --- a/homeassistant/components/device_tracker/unifi.py +++ b/homeassistant/components/device_tracker/unifi.py @@ -32,7 +32,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): vol.Any( + cv.boolean, cv.isfile) }) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 439b6258bcd..50cc771ffd3 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.discovery import async_load_platform, async_discover import homeassistant.util.dt as dt_util -REQUIREMENTS = ['netdisco==1.2.0'] +REQUIREMENTS = ['netdisco==1.2.2'] DOMAIN = 'discovery' diff --git a/homeassistant/components/ecobee.py b/homeassistant/components/ecobee.py index c4b0f2e9546..0b0c9d1d65a 100644 --- a/homeassistant/components/ecobee.py +++ b/homeassistant/components/ecobee.py @@ -15,7 +15,7 @@ from homeassistant.helpers import discovery from homeassistant.const import CONF_API_KEY from homeassistant.util import Throttle -REQUIREMENTS = ['python-ecobee-api==0.0.9'] +REQUIREMENTS = ['python-ecobee-api==0.0.10'] _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index b5edb751d50..052bd7e86fe 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -3,10 +3,10 @@ FINGERPRINTS = { "compatibility.js": "1686167ff210e001f063f5c606b2e74b", "core.js": "2a7d01e45187c7d4635da05065b5e54e", - "frontend.html": "7e13ce36d3141182a62a5b061e87e77a", - "mdi.html": "89074face5529f5fe6fbae49ecb3e88b", + "frontend.html": "2de1bde3b4a6c6c47dd95504fc098906", + "mdi.html": "2e848b4da029bf73d426d5ba058a088d", "micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a", - "panels/ha-panel-config.html": "61f65e75e39368e07441d7d6a4e36ae3", + "panels/ha-panel-config.html": "52e2e1d477bfd6dc3708d65b8337f0af", "panels/ha-panel-dev-event.html": "d409e7ab537d9fe629126d122345279c", "panels/ha-panel-dev-info.html": "b0e55eb657fd75f21aba2426ac0cedc0", "panels/ha-panel-dev-mqtt.html": "94b222b013a98583842de3e72d5888c6", diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 60713690c44..c873d66777e 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -8,7 +8,7 @@ .flex-1{-ms-flex:1 1 0.000000001px;-webkit-flex:1;flex:1;-webkit-flex-basis:0.000000001px;flex-basis:0.000000001px;}.flex-2{-ms-flex:2;-webkit-flex:2;flex:2;}.flex-3{-ms-flex:3;-webkit-flex:3;flex:3;}.flex-4{-ms-flex:4;-webkit-flex:4;flex:4;}.flex-5{-ms-flex:5;-webkit-flex:5;flex:5;}.flex-6{-ms-flex:6;-webkit-flex:6;flex:6;}.flex-7{-ms-flex:7;-webkit-flex:7;flex:7;}.flex-8{-ms-flex:8;-webkit-flex:8;flex:8;}.flex-9{-ms-flex:9;-webkit-flex:9;flex:9;}.flex-10{-ms-flex:10;-webkit-flex:10;flex:10;}.flex-11{-ms-flex:11;-webkit-flex:11;flex:11;}.flex-12{-ms-flex:12;-webkit-flex:12;flex:12;}