diff --git a/.coveragerc b/.coveragerc index b4f05ee01a3..9de3b6f8cf6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -5,6 +5,7 @@ omit = homeassistant/__main__.py homeassistant/scripts/*.py homeassistant/helpers/typing.py + homeassistant/helpers/signal.py # omit pieces of code that rely on external devices being present homeassistant/components/apcupsd.py @@ -116,9 +117,6 @@ omit = homeassistant/components/knx.py homeassistant/components/*/knx.py - homeassistant/components/ffmpeg.py - homeassistant/components/*/ffmpeg.py - homeassistant/components/zoneminder.py homeassistant/components/*/zoneminder.py @@ -132,6 +130,7 @@ omit = homeassistant/components/alarm_control_panel/concord232.py homeassistant/components/alarm_control_panel/nx584.py homeassistant/components/alarm_control_panel/simplisafe.py + homeassistant/components/apiai.py homeassistant/components/binary_sensor/arest.py homeassistant/components/binary_sensor/concord232.py homeassistant/components/binary_sensor/flic.py @@ -141,6 +140,7 @@ omit = homeassistant/components/browser.py homeassistant/components/camera/amcrest.py homeassistant/components/camera/bloomsky.py + homeassistant/components/camera/ffmpeg.py homeassistant/components/camera/foscam.py homeassistant/components/camera/mjpeg.py homeassistant/components/camera/rpi_camera.py @@ -210,7 +210,9 @@ omit = homeassistant/components/light/piglow.py homeassistant/components/light/zengge.py homeassistant/components/lirc.py + homeassistant/components/lock/nuki.py homeassistant/components/media_player/anthemav.py + homeassistant/components/media_player/apple_tv.py homeassistant/components/media_player/aquostv.py homeassistant/components/media_player/braviatv.py homeassistant/components/media_player/cast.py @@ -226,6 +228,7 @@ omit = homeassistant/components/media_player/itunes.py homeassistant/components/media_player/kodi.py homeassistant/components/media_player/lg_netcast.py + homeassistant/components/media_player/liveboxplaytv.py homeassistant/components/media_player/mpchc.py homeassistant/components/media_player/mpd.py homeassistant/components/media_player/nad.py @@ -256,6 +259,7 @@ omit = homeassistant/components/notify/kodi.py homeassistant/components/notify/lannouncer.py homeassistant/components/notify/llamalab_automate.py + homeassistant/components/notify/mailgun.py homeassistant/components/notify/matrix.py homeassistant/components/notify/message_bird.py homeassistant/components/notify/nfandroidtv.py @@ -325,11 +329,13 @@ omit = homeassistant/components/sensor/nzbget.py homeassistant/components/sensor/ohmconnect.py homeassistant/components/sensor/onewire.py + homeassistant/components/sensor/openevse.py homeassistant/components/sensor/openexchangerates.py homeassistant/components/sensor/openweathermap.py homeassistant/components/sensor/pi_hole.py homeassistant/components/sensor/plex.py homeassistant/components/sensor/pvoutput.py + homeassistant/components/sensor/qnap.py homeassistant/components/sensor/sabnzbd.py homeassistant/components/sensor/scrape.py homeassistant/components/sensor/sensehat.py @@ -365,6 +371,7 @@ omit = homeassistant/components/switch/digitalloggers.py homeassistant/components/switch/dlink.py homeassistant/components/switch/edimax.py + homeassistant/components/switch/fritzdect.py homeassistant/components/switch/hdmi_cec.py homeassistant/components/switch/hikvisioncam.py homeassistant/components/switch/hook.py diff --git a/Dockerfile b/Dockerfile index ecdbbafba66..8c7ab8a9039 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,22 @@ FROM python:3.5 MAINTAINER Paulus Schoutsen +# Uncomment any of the following lines to disable the installation. +#ENV INSTALL_TELLSTICK no +#ENV INSTALL_OPENALPR no +#ENV INSTALL_FFMPEG no +#ENV INSTALL_OPENZWAVE no +#ENV INSTALL_LIBCEC no +#ENV INSTALL_PHANTOMJS no + VOLUME /config RUN mkdir -p /usr/src/app WORKDIR /usr/src/app # Copy build scripts -COPY script/setup_docker_prereqs script/build_python_openzwave script/build_libcec script/install_phantomjs script/ -RUN script/setup_docker_prereqs +COPY virtualization/Docker/ virtualization/Docker/ +RUN virtualization/Docker/setup_docker_prereqs # Install hass component dependencies COPY requirements_all.txt requirements_all.txt @@ -18,4 +26,4 @@ RUN pip3 install --no-cache-dir -r requirements_all.txt && \ # Copy source COPY . . -CMD [ "python", "-m", "homeassistant", "--config", "/config" ] +CMD [ "python", "-m", "homeassistant", "--config", "/config" ] \ No newline at end of file diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 0c8b0bc688e..87fd9f7d9e5 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -26,6 +26,7 @@ from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import ( event_decorators, service, config_per_platform, extract_domain_configs) +from homeassistant.helpers.signal import async_register_signal_handling _LOGGER = logging.getLogger(__name__) @@ -435,6 +436,7 @@ def async_from_config_dict(config: Dict[str, Any], yield from hass.async_stop_track_tasks() + async_register_signal_handling(hass) return hass diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index a4f18250d17..0e9d554a579 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -12,14 +12,19 @@ import itertools as it import logging import homeassistant.core as ha +import homeassistant.config as conf_util +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.service import extract_entity_ids from homeassistant.loader import get_component from homeassistant.const import ( - ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE) + ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, + SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART, + RESTART_EXIT_CODE) _LOGGER = logging.getLogger(__name__) SERVICE_RELOAD_CORE_CONFIG = 'reload_core_config' +SERVICE_CHECK_CONFIG = 'check_config' def is_on(hass, entity_id=None): @@ -75,6 +80,21 @@ def toggle(hass, entity_id=None, **service_data): hass.services.call(ha.DOMAIN, SERVICE_TOGGLE, service_data) +def stop(hass): + """Stop Home Assistant.""" + hass.services.call(ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP) + + +def restart(hass): + """Stop Home Assistant.""" + hass.services.call(ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART) + + +def check_config(hass): + """Check the config files.""" + hass.services.call(ha.DOMAIN, SERVICE_CHECK_CONFIG) + + def reload_core_config(hass): """Reload the core config.""" hass.services.call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG) @@ -84,7 +104,7 @@ def reload_core_config(hass): def async_setup(hass, config): """Setup general services related to Home Assistant.""" @asyncio.coroutine - def handle_turn_service(service): + def async_handle_turn_service(service): """Method to handle calls to homeassistant.turn_on/off.""" entity_ids = extract_entity_ids(hass, service) @@ -122,18 +142,37 @@ def async_setup(hass, config): yield from asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( - ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service) + ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service) hass.services.async_register( - ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service) + ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service) hass.services.async_register( - ha.DOMAIN, SERVICE_TOGGLE, handle_turn_service) + ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service) @asyncio.coroutine - def handle_reload_config(call): - """Service handler for reloading core config.""" - from homeassistant.exceptions import HomeAssistantError - from homeassistant import config as conf_util + def async_handle_core_service(call): + """Service handler for handling core services.""" + if call.service == SERVICE_HOMEASSISTANT_STOP: + hass.async_add_job(hass.async_stop()) + return + try: + yield from conf_util.async_check_ha_config_file(hass) + except HomeAssistantError: + return + + if call.service == SERVICE_HOMEASSISTANT_RESTART: + hass.async_add_job(hass.async_stop(RESTART_EXIT_CODE)) + + hass.services.async_register( + ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service) + hass.services.async_register( + ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service) + hass.services.async_register( + ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service) + + @asyncio.coroutine + def async_handle_reload_config(call): + """Service handler for reloading core config.""" try: conf = yield from conf_util.async_hass_config_yaml(hass) except HomeAssistantError as err: @@ -144,6 +183,6 @@ def async_setup(hass, config): hass, conf.get(ha.DOMAIN) or {}) hass.services.async_register( - ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, handle_reload_config) + ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, async_handle_reload_config) return True diff --git a/homeassistant/components/alarm_control_panel/wink.py b/homeassistant/components/alarm_control_panel/wink.py index 2a600fe70a9..c489b53c9c0 100644 --- a/homeassistant/components/alarm_control_panel/wink.py +++ b/homeassistant/components/alarm_control_panel/wink.py @@ -11,7 +11,7 @@ from homeassistant.const import (STATE_UNKNOWN, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY) -from homeassistant.components.wink import WinkDevice +from homeassistant.components.wink import WinkDevice, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -24,7 +24,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): import pywink for camera in pywink.get_cameras(): - add_devices([WinkCameraDevice(camera, hass)]) + # get_cameras returns multiple device types. + # Only add those that aren't sensors. + try: + camera.capability() + except AttributeError: + _id = camera.object_id() + camera.name() + if _id not in hass.data[DOMAIN]['unique_ids']: + add_devices([WinkCameraDevice(camera, hass)]) class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel): @@ -32,7 +39,7 @@ class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel): def __init__(self, wink, hass): """Initialize the Wink alarm.""" - WinkDevice.__init__(self, wink, hass) + super().__init__(wink, hass) @property def state(self): diff --git a/homeassistant/components/alert.py b/homeassistant/components/alert.py new file mode 100644 index 00000000000..dc0774edef2 --- /dev/null +++ b/homeassistant/components/alert.py @@ -0,0 +1,275 @@ +""" +Support for repeating alerts when conditions are met. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/alert/ +""" +import asyncio +from datetime import datetime, timedelta +import logging +import os + +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.config import load_yaml_config_file +from homeassistant.const import (CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, + CONF_STATE, STATE_ON, STATE_OFF, + SERVICE_TURN_ON, SERVICE_TURN_OFF, + SERVICE_TOGGLE, ATTR_ENTITY_ID) +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers import service, event +from homeassistant.util.async import run_callback_threadsafe +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'alert' +ENTITY_ID_FORMAT = DOMAIN + '.{}' + +CONF_CAN_ACK = 'can_acknowledge' +CONF_NOTIFIERS = 'notifiers' +CONF_REPEAT = 'repeat' +CONF_SKIP_FIRST = 'skip_first' + +ALERT_SCHEMA = vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_STATE, default=STATE_ON): cv.string, + vol.Required(CONF_REPEAT): vol.All(cv.ensure_list, [vol.Coerce(float)]), + vol.Required(CONF_CAN_ACK, default=True): cv.boolean, + vol.Required(CONF_SKIP_FIRST, default=False): cv.boolean, + vol.Required(CONF_NOTIFIERS): cv.ensure_list}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + cv.slug: ALERT_SCHEMA, + }), +}, extra=vol.ALLOW_EXTRA) + + +ALERT_SERVICE_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, +}) + + +def is_on(hass, entity_id): + """Return if the alert is firing and not acknowledged.""" + return hass.states.is_state(entity_id, STATE_ON) + + +def turn_on(hass, entity_id): + """Reset the alert.""" + run_callback_threadsafe(hass.loop, async_turn_on, hass, entity_id) + + +@callback +def async_turn_on(hass, entity_id): + """Async reset the alert.""" + data = {ATTR_ENTITY_ID: entity_id} + hass.async_add_job( + hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data)) + + +def turn_off(hass, entity_id): + """Acknowledge alert.""" + run_callback_threadsafe(hass.loop, async_turn_off, hass, entity_id) + + +@callback +def async_turn_off(hass, entity_id): + """Async acknowledge the alert.""" + data = {ATTR_ENTITY_ID: entity_id} + hass.async_add_job( + hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data)) + + +def toggle(hass, entity_id): + """Toggle acknowledgement of alert.""" + run_callback_threadsafe(hass.loop, async_toggle, hass, entity_id) + + +@callback +def async_toggle(hass, entity_id): + """Async toggle acknowledgement of alert.""" + data = {ATTR_ENTITY_ID: entity_id} + hass.async_add_job( + hass.services.async_call(DOMAIN, SERVICE_TOGGLE, data)) + + +@asyncio.coroutine +def async_setup(hass, config): + """Setup alert component.""" + alerts = config.get(DOMAIN) + all_alerts = {} + + @asyncio.coroutine + def async_handle_alert_service(service_call): + """Handle calls to alert services.""" + alert_ids = service.extract_entity_ids(hass, service_call) + + for alert_id in alert_ids: + alert = all_alerts[alert_id] + if service_call.service == SERVICE_TURN_ON: + yield from alert.async_turn_on() + elif service_call.service == SERVICE_TOGGLE: + yield from alert.async_toggle() + else: + yield from alert.async_turn_off() + + # setup alerts + for entity_id, alert in alerts.items(): + entity = Alert(hass, entity_id, + alert[CONF_NAME], alert[CONF_ENTITY_ID], + alert[CONF_STATE], alert[CONF_REPEAT], + alert[CONF_SKIP_FIRST], alert[CONF_NOTIFIERS], + alert[CONF_CAN_ACK]) + all_alerts[entity.entity_id] = entity + + # read descriptions + descriptions = yield from hass.loop.run_in_executor( + None, load_yaml_config_file, os.path.join( + os.path.dirname(__file__), 'services.yaml')) + descriptions = descriptions.get(DOMAIN, {}) + + # setup service calls + hass.services.async_register( + DOMAIN, SERVICE_TURN_OFF, async_handle_alert_service, + descriptions.get(SERVICE_TURN_OFF), schema=ALERT_SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_TURN_ON, async_handle_alert_service, + descriptions.get(SERVICE_TURN_ON), schema=ALERT_SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_TOGGLE, async_handle_alert_service, + descriptions.get(SERVICE_TOGGLE), schema=ALERT_SERVICE_SCHEMA) + + tasks = [alert.async_update_ha_state() for alert in all_alerts.values()] + if tasks: + yield from asyncio.wait(tasks, loop=hass.loop) + + return True + + +class Alert(ToggleEntity): + """Representation of an alert.""" + + def __init__(self, hass, entity_id, name, watched_entity_id, state, + repeat, skip_first, notifiers, can_ack): + """Initialize the alert.""" + self.hass = hass + self._name = name + self._alert_state = state + self._skip_first = skip_first + self._notifiers = notifiers + self._can_ack = can_ack + + self._delay = [timedelta(minutes=val) for val in repeat] + self._next_delay = 0 + + self._firing = False + self._ack = False + self._cancel = None + self.entity_id = ENTITY_ID_FORMAT.format(entity_id) + + event.async_track_state_change(hass, watched_entity_id, + self.watched_entity_change) + + @property + def name(self): + """Return the name of the alert.""" + return self._name + + @property + def should_poll(self): + """HASS need not poll these entities.""" + return False + + @property + def state(self): + """Return the alert status.""" + if self._firing: + if self._ack: + return STATE_OFF + return STATE_ON + return STATE_IDLE + + @property + def hidden(self): + """Hide the alert when it is not firing.""" + return not self._can_ack or not self._firing + + @asyncio.coroutine + def watched_entity_change(self, entity, from_state, to_state): + """Determine if the alert should start or stop.""" + _LOGGER.debug('Watched entity (%s) has changed.', entity) + if to_state.state == self._alert_state and not self._firing: + yield from self.begin_alerting() + if to_state.state != self._alert_state and self._firing: + yield from self.end_alerting() + + @asyncio.coroutine + def begin_alerting(self): + """Begin the alert procedures.""" + _LOGGER.debug('Beginning Alert: %s', self._name) + self._ack = False + self._firing = True + self._next_delay = 0 + + if not self._skip_first: + yield from self._notify() + else: + yield from self._schedule_notify() + + self.hass.async_add_job(self.async_update_ha_state) + + @asyncio.coroutine + def end_alerting(self): + """End the alert procedures.""" + _LOGGER.debug('Ending Alert: %s', self._name) + self._cancel() + self._ack = False + self._firing = False + self.hass.async_add_job(self.async_update_ha_state) + + @asyncio.coroutine + def _schedule_notify(self): + """Schedule a notification.""" + delay = self._delay[self._next_delay] + next_msg = datetime.now() + delay + self._cancel = \ + event.async_track_point_in_time(self.hass, self._notify, next_msg) + self._next_delay = min(self._next_delay + 1, len(self._delay) - 1) + + @asyncio.coroutine + def _notify(self, *args): + """Send the alert notification.""" + if not self._firing: + return + + if not self._ack: + _LOGGER.info('Alerting: %s', self._name) + for target in self._notifiers: + yield from self.hass.services.async_call( + 'notify', target, {'message': self._name}) + yield from self._schedule_notify() + + @asyncio.coroutine + def async_turn_on(self): + """Async Unacknowledge alert.""" + _LOGGER.debug('Reset Alert: %s', self._name) + self._ack = False + yield from self.async_update_ha_state() + + @asyncio.coroutine + def async_turn_off(self): + """Async Acknowledge alert.""" + _LOGGER.debug('Acknowledged Alert: %s', self._name) + self._ack = True + yield from self.async_update_ha_state() + + @asyncio.coroutine + def async_toggle(self): + """Async toggle alert.""" + if self._ack: + return self.async_turn_on() + return self.async_turn_off() diff --git a/homeassistant/components/apiai.py b/homeassistant/components/apiai.py new file mode 100644 index 00000000000..769283fa5d9 --- /dev/null +++ b/homeassistant/components/apiai.py @@ -0,0 +1,172 @@ +""" +Support for API.AI webhook. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/apiai/ +""" +import asyncio +import copy +import logging + +import voluptuous as vol + +from homeassistant.const import PROJECT_NAME, HTTP_BAD_REQUEST +from homeassistant.helpers import template, script, config_validation as cv +from homeassistant.components.http import HomeAssistantView + +_LOGGER = logging.getLogger(__name__) + +INTENTS_API_ENDPOINT = '/api/apiai' + +CONF_INTENTS = 'intents' +CONF_SPEECH = 'speech' +CONF_ACTION = 'action' +CONF_ASYNC_ACTION = 'async_action' + +DEFAULT_CONF_ASYNC_ACTION = False + +DOMAIN = 'apiai' +DEPENDENCIES = ['http'] + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: { + CONF_INTENTS: { + cv.string: { + vol.Optional(CONF_SPEECH): cv.template, + vol.Optional(CONF_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_ASYNC_ACTION, + default=DEFAULT_CONF_ASYNC_ACTION): cv.boolean + } + } + } +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Activate API.AI component.""" + intents = config[DOMAIN].get(CONF_INTENTS, {}) + + hass.http.register_view(ApiaiIntentsView(hass, intents)) + + return True + + +class ApiaiIntentsView(HomeAssistantView): + """Handle API.AI requests.""" + + url = INTENTS_API_ENDPOINT + name = 'api:apiai' + + def __init__(self, hass, intents): + """Initialize API.AI view.""" + super().__init__() + + self.hass = hass + intents = copy.deepcopy(intents) + template.attach(hass, intents) + + for name, intent in intents.items(): + if CONF_ACTION in intent: + intent[CONF_ACTION] = script.Script( + hass, intent[CONF_ACTION], "Apiai intent {}".format(name)) + + self.intents = intents + + @asyncio.coroutine + def post(self, request): + """Handle API.AI.""" + data = yield from request.json() + + _LOGGER.debug('Received Apiai request: %s', data) + + req = data.get('result') + + if req is None: + _LOGGER.error('Received invalid data from Apiai: %s', data) + return self.json_message('Expected result value not received', + HTTP_BAD_REQUEST) + + action_incomplete = req['actionIncomplete'] + + if action_incomplete: + return None + + # use intent to no mix HASS actions with this parameter + intent = req.get('action') + parameters = req.get('parameters') + # contexts = req.get('contexts') + response = ApiaiResponse(parameters) + + # Default Welcome Intent + # Maybe is better to handle this in api.ai directly? + # + # if intent == 'input.welcome': + # response.add_speech( + # "Hello, and welcome to the future. How may I help?") + # return self.json(response) + + if intent == "": + _LOGGER.warning('Received intent with empty action') + response.add_speech( + "You have not defined an action in your api.ai intent.") + return self.json(response) + + config = self.intents.get(intent) + + if config is None: + _LOGGER.warning('Received unknown intent %s', intent) + response.add_speech( + "Intent '%s' is not yet configured within Home Assistant." % + intent) + return self.json(response) + + speech = config.get(CONF_SPEECH) + action = config.get(CONF_ACTION) + async_action = config.get(CONF_ASYNC_ACTION) + + if action is not None: + # API.AI expects a response in less than 5s + if async_action: + # Do not wait for the action to be executed. + # Needed if the action will take longer than 5s to execute + self.hass.async_add_job(action.async_run(response.parameters)) + else: + # Wait for the action to be executed so we can use results to + # render the answer + yield from action.async_run(response.parameters) + + # pylint: disable=unsubscriptable-object + if speech is not None: + response.add_speech(speech) + + return self.json(response) + + +class ApiaiResponse(object): + """Help generating the response for API.AI.""" + + def __init__(self, parameters): + """Initialize the response.""" + self.speech = None + self.parameters = {} + # Parameter names replace '.' and '-' for '_' + for key, value in parameters.items(): + underscored_key = key.replace('.', '_').replace('-', '_') + self.parameters[underscored_key] = value + + def add_speech(self, text): + """Add speech to the response.""" + assert self.speech is None + + if isinstance(text, template.Template): + text = text.async_render(self.parameters) + + self.speech = text + + def as_dict(self): + """Return response in an API.AI valid dict.""" + return { + 'speech': self.speech, + 'displayText': self.speech, + 'source': PROJECT_NAME, + } diff --git a/homeassistant/components/binary_sensor/ffmpeg.py b/homeassistant/components/binary_sensor/ffmpeg.py deleted file mode 100644 index ea89ff7c743..00000000000 --- a/homeassistant/components/binary_sensor/ffmpeg.py +++ /dev/null @@ -1,279 +0,0 @@ -""" -Provides a binary sensor which is a collection of ffmpeg tools. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.ffmpeg/ -""" -import asyncio -import logging -import os - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components.binary_sensor import ( - BinarySensorDevice, PLATFORM_SCHEMA, DOMAIN) -from homeassistant.components.ffmpeg import ( - DATA_FFMPEG, CONF_INPUT, CONF_OUTPUT, CONF_EXTRA_ARGUMENTS) -from homeassistant.config import load_yaml_config_file -from homeassistant.const import ( - EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START, CONF_NAME, - ATTR_ENTITY_ID) - -DEPENDENCIES = ['ffmpeg'] - -_LOGGER = logging.getLogger(__name__) - -SERVICE_START = 'ffmpeg_start' -SERVICE_STOP = 'ffmpeg_stop' -SERVICE_RESTART = 'ffmpeg_restart' - -DATA_FFMPEG_DEVICE = 'ffmpeg_binary_sensor' - -FFMPEG_SENSOR_NOISE = 'noise' -FFMPEG_SENSOR_MOTION = 'motion' - -MAP_FFMPEG_BIN = [ - FFMPEG_SENSOR_NOISE, - FFMPEG_SENSOR_MOTION -] - -CONF_INITIAL_STATE = 'initial_state' -CONF_TOOL = 'tool' -CONF_PEAK = 'peak' -CONF_DURATION = 'duration' -CONF_RESET = 'reset' -CONF_CHANGES = 'changes' -CONF_REPEAT = 'repeat' -CONF_REPEAT_TIME = 'repeat_time' - -DEFAULT_NAME = 'FFmpeg' -DEFAULT_INIT_STATE = True - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_TOOL): vol.In(MAP_FFMPEG_BIN), - vol.Required(CONF_INPUT): cv.string, - vol.Optional(CONF_INITIAL_STATE, default=DEFAULT_INIT_STATE): cv.boolean, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_EXTRA_ARGUMENTS): cv.string, - vol.Optional(CONF_OUTPUT): cv.string, - vol.Optional(CONF_PEAK, default=-30): vol.Coerce(int), - vol.Optional(CONF_DURATION, default=1): - vol.All(vol.Coerce(int), vol.Range(min=1)), - vol.Optional(CONF_RESET, default=10): - vol.All(vol.Coerce(int), vol.Range(min=1)), - vol.Optional(CONF_CHANGES, default=10): - vol.All(vol.Coerce(float), vol.Range(min=0, max=99)), - vol.Optional(CONF_REPEAT, default=0): - vol.All(vol.Coerce(int), vol.Range(min=0)), - vol.Optional(CONF_REPEAT_TIME, default=0): - vol.All(vol.Coerce(int), vol.Range(min=0)), -}) - -SERVICE_FFMPEG_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, -}) - - -def restart(hass, entity_id=None): - """Restart a ffmpeg process on entity.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_RESTART, data) - - -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Create the binary sensor.""" - from haffmpeg import SensorNoise, SensorMotion - - # check source - if not hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_INPUT)): - return - - # generate sensor object - if config.get(CONF_TOOL) == FFMPEG_SENSOR_NOISE: - entity = FFmpegNoise(hass, SensorNoise, config) - else: - entity = FFmpegMotion(hass, SensorMotion, config) - - @asyncio.coroutine - def async_shutdown(event): - """Stop ffmpeg.""" - yield from entity.async_shutdown_ffmpeg() - - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, async_shutdown) - - # start on startup - if config.get(CONF_INITIAL_STATE): - @asyncio.coroutine - def async_start(event): - """Start ffmpeg.""" - yield from entity.async_start_ffmpeg() - yield from entity.async_update_ha_state() - - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, async_start) - - # add to system - yield from async_add_devices([entity]) - - # exists service? - if hass.services.has_service(DOMAIN, SERVICE_RESTART): - hass.data[DATA_FFMPEG_DEVICE].append(entity) - return - hass.data[DATA_FFMPEG_DEVICE] = [entity] - - descriptions = yield from hass.loop.run_in_executor( - None, load_yaml_config_file, - os.path.join(os.path.dirname(__file__), 'services.yaml')) - - # register service - @asyncio.coroutine - def async_service_handle(service): - """Handle service binary_sensor.ffmpeg_restart.""" - entity_ids = service.data.get('entity_id') - - if entity_ids: - _devices = [device for device in hass.data[DATA_FFMPEG_DEVICE] - if device.entity_id in entity_ids] - else: - _devices = hass.data[DATA_FFMPEG_DEVICE] - - tasks = [] - for device in _devices: - if service.service == SERVICE_START: - tasks.append(device.async_start_ffmpeg()) - elif service.service == SERVICE_STOP: - tasks.append(device.async_shutdown_ffmpeg()) - else: - tasks.append(device.async_restart_ffmpeg()) - - if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) - - hass.services.async_register( - DOMAIN, SERVICE_START, async_service_handle, - descriptions.get(SERVICE_START), schema=SERVICE_FFMPEG_SCHEMA) - - hass.services.async_register( - DOMAIN, SERVICE_STOP, async_service_handle, - descriptions.get(SERVICE_STOP), schema=SERVICE_FFMPEG_SCHEMA) - - hass.services.async_register( - DOMAIN, SERVICE_RESTART, async_service_handle, - descriptions.get(SERVICE_RESTART), schema=SERVICE_FFMPEG_SCHEMA) - - -class FFmpegBinarySensor(BinarySensorDevice): - """A binary sensor which use ffmpeg for noise detection.""" - - def __init__(self, hass, ffobj, config): - """Constructor for binary sensor noise detection.""" - self._manager = hass.data[DATA_FFMPEG] - self._state = False - self._config = config - self._name = config.get(CONF_NAME) - self._ffmpeg = ffobj( - self._manager.binary, hass.loop, self._async_callback) - - def _async_callback(self, state): - """HA-FFmpeg callback for noise detection.""" - self._state = state - self.hass.async_add_job(self.async_update_ha_state()) - - def async_start_ffmpeg(self): - """Start a FFmpeg instance. - - This method must be run in the event loop and returns a coroutine. - """ - raise NotImplementedError() - - def async_shutdown_ffmpeg(self): - """For STOP event to shutdown ffmpeg. - - This method must be run in the event loop and returns a coroutine. - """ - return self._ffmpeg.close() - - @asyncio.coroutine - def async_restart_ffmpeg(self): - """Restart processing.""" - yield from self.async_shutdown_ffmpeg() - yield from self.async_start_ffmpeg() - - @property - def is_on(self): - """True if the binary sensor is on.""" - return self._state - - @property - def should_poll(self): - """Return True if entity has to be polled for state.""" - return False - - @property - def name(self): - """Return the name of the entity.""" - return self._name - - @property - def available(self): - """Return True if entity is available.""" - return self._ffmpeg.is_running - - -class FFmpegNoise(FFmpegBinarySensor): - """A binary sensor which use ffmpeg for noise detection.""" - - def async_start_ffmpeg(self): - """Start a FFmpeg instance. - - This method must be run in the event loop and returns a coroutine. - """ - # init config - self._ffmpeg.set_options( - time_duration=self._config.get(CONF_DURATION), - time_reset=self._config.get(CONF_RESET), - peak=self._config.get(CONF_PEAK), - ) - - # run - return self._ffmpeg.open_sensor( - input_source=self._config.get(CONF_INPUT), - output_dest=self._config.get(CONF_OUTPUT), - extra_cmd=self._config.get(CONF_EXTRA_ARGUMENTS), - ) - - @property - def sensor_class(self): - """Return the class of this sensor, from SENSOR_CLASSES.""" - return "sound" - - -class FFmpegMotion(FFmpegBinarySensor): - """A binary sensor which use ffmpeg for noise detection.""" - - def async_start_ffmpeg(self): - """Start a FFmpeg instance. - - This method must be run in the event loop and returns a coroutine. - """ - # init config - self._ffmpeg.set_options( - time_reset=self._config.get(CONF_RESET), - time_repeat=self._config.get(CONF_REPEAT_TIME), - repeat=self._config.get(CONF_REPEAT), - changes=self._config.get(CONF_CHANGES), - ) - - # run - return self._ffmpeg.open_sensor( - input_source=self._config.get(CONF_INPUT), - extra_cmd=self._config.get(CONF_EXTRA_ARGUMENTS), - ) - - @property - def sensor_class(self): - """Return the class of this sensor, from SENSOR_CLASSES.""" - return "motion" diff --git a/homeassistant/components/binary_sensor/ffmpeg_motion.py b/homeassistant/components/binary_sensor/ffmpeg_motion.py new file mode 100644 index 00000000000..70e13722b85 --- /dev/null +++ b/homeassistant/components/binary_sensor/ffmpeg_motion.py @@ -0,0 +1,127 @@ +""" +Provides a binary sensor which is a collection of ffmpeg tools. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.ffmpeg_motion/ +""" +import asyncio +import logging + +import voluptuous as vol + +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA) +from homeassistant.components.ffmpeg import ( + FFmpegBase, DATA_FFMPEG, CONF_INPUT, CONF_EXTRA_ARGUMENTS, + CONF_INITIAL_STATE) +from homeassistant.const import CONF_NAME + +DEPENDENCIES = ['ffmpeg'] + +_LOGGER = logging.getLogger(__name__) + +CONF_RESET = 'reset' +CONF_CHANGES = 'changes' +CONF_REPEAT = 'repeat' +CONF_REPEAT_TIME = 'repeat_time' + +DEFAULT_NAME = 'FFmpeg Motion' +DEFAULT_INIT_STATE = True + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_INPUT): cv.string, + vol.Optional(CONF_INITIAL_STATE, default=DEFAULT_INIT_STATE): cv.boolean, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_EXTRA_ARGUMENTS): cv.string, + vol.Optional(CONF_RESET, default=10): + vol.All(vol.Coerce(int), vol.Range(min=1)), + vol.Optional(CONF_CHANGES, default=10): + vol.All(vol.Coerce(float), vol.Range(min=0, max=99)), + vol.Inclusive(CONF_REPEAT, 'repeat'): + vol.All(vol.Coerce(int), vol.Range(min=1)), + vol.Inclusive(CONF_REPEAT_TIME, 'repeat'): + vol.All(vol.Coerce(int), vol.Range(min=1)), +}) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Create the binary sensor.""" + manager = hass.data[DATA_FFMPEG] + + # check source + if not manager.async_run_test(config.get(CONF_INPUT)): + return + + # generate sensor object + entity = FFmpegMotion(hass, manager, config) + + # add to system + manager.async_register_device(entity) + yield from async_add_devices([entity]) + + +class FFmpegBinarySensor(FFmpegBase, BinarySensorDevice): + """A binary sensor which use ffmpeg for noise detection.""" + + def __init__(self, hass, config): + """Constructor for binary sensor noise detection.""" + super().__init__(config.get(CONF_INITIAL_STATE)) + + self._state = False + self._config = config + self._name = config.get(CONF_NAME) + + @callback + def _async_callback(self, state): + """HA-FFmpeg callback for noise detection.""" + self._state = state + self.hass.async_add_job(self.async_update_ha_state()) + + @property + def is_on(self): + """True if the binary sensor is on.""" + return self._state + + @property + def name(self): + """Return the name of the entity.""" + return self._name + + +class FFmpegMotion(FFmpegBinarySensor): + """A binary sensor which use ffmpeg for noise detection.""" + + def __init__(self, hass, manager, config): + """Initialize ffmpeg motion binary sensor.""" + from haffmpeg import SensorMotion + + super().__init__(hass, config) + self.ffmpeg = SensorMotion( + manager.binary, hass.loop, self._async_callback) + + def async_start_ffmpeg(self): + """Start a FFmpeg instance. + + This method must be run in the event loop and returns a coroutine. + """ + # init config + self.ffmpeg.set_options( + time_reset=self._config.get(CONF_RESET), + time_repeat=self._config.get(CONF_REPEAT_TIME, 0), + repeat=self._config.get(CONF_REPEAT, 0), + changes=self._config.get(CONF_CHANGES), + ) + + # run + return self.ffmpeg.open_sensor( + input_source=self._config.get(CONF_INPUT), + extra_cmd=self._config.get(CONF_EXTRA_ARGUMENTS), + ) + + @property + def sensor_class(self): + """Return the class of this sensor, from SENSOR_CLASSES.""" + return "motion" diff --git a/homeassistant/components/binary_sensor/ffmpeg_noise.py b/homeassistant/components/binary_sensor/ffmpeg_noise.py new file mode 100644 index 00000000000..b4592fceefd --- /dev/null +++ b/homeassistant/components/binary_sensor/ffmpeg_noise.py @@ -0,0 +1,96 @@ +""" +Provides a binary sensor which is a collection of ffmpeg tools. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.ffmpeg_noise/ +""" +import asyncio +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA +from homeassistant.components.binary_sensor.ffmpeg_motion import ( + FFmpegBinarySensor) +from homeassistant.components.ffmpeg import ( + DATA_FFMPEG, CONF_INPUT, CONF_OUTPUT, CONF_EXTRA_ARGUMENTS, + CONF_INITIAL_STATE) +from homeassistant.const import CONF_NAME + +DEPENDENCIES = ['ffmpeg'] + +_LOGGER = logging.getLogger(__name__) + +CONF_PEAK = 'peak' +CONF_DURATION = 'duration' +CONF_RESET = 'reset' + +DEFAULT_NAME = 'FFmpeg Noise' +DEFAULT_INIT_STATE = True + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_INPUT): cv.string, + vol.Optional(CONF_INITIAL_STATE, default=DEFAULT_INIT_STATE): cv.boolean, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_EXTRA_ARGUMENTS): cv.string, + vol.Optional(CONF_OUTPUT): cv.string, + vol.Optional(CONF_PEAK, default=-30): vol.Coerce(int), + vol.Optional(CONF_DURATION, default=1): + vol.All(vol.Coerce(int), vol.Range(min=1)), + vol.Optional(CONF_RESET, default=10): + vol.All(vol.Coerce(int), vol.Range(min=1)), +}) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Create the binary sensor.""" + manager = hass.data[DATA_FFMPEG] + + # check source + if not manager.async_run_test(config.get(CONF_INPUT)): + return + + # generate sensor object + entity = FFmpegNoise(hass, manager, config) + + # add to system + manager.async_register_device(entity) + yield from async_add_devices([entity]) + + +class FFmpegNoise(FFmpegBinarySensor): + """A binary sensor which use ffmpeg for noise detection.""" + + def __init__(self, hass, manager, config): + """Initialize ffmpeg noise binary sensor.""" + from haffmpeg import SensorNoise + + super().__init__(hass, config) + self.ffmpeg = SensorNoise( + manager.binary, hass.loop, self._async_callback) + + def async_start_ffmpeg(self): + """Start a FFmpeg instance. + + This method must be run in the event loop and returns a coroutine. + """ + # init config + self.ffmpeg.set_options( + time_duration=self._config.get(CONF_DURATION), + time_reset=self._config.get(CONF_RESET), + peak=self._config.get(CONF_PEAK), + ) + + # run + return self.ffmpeg.open_sensor( + input_source=self._config.get(CONF_INPUT), + output_dest=self._config.get(CONF_OUTPUT), + extra_cmd=self._config.get(CONF_EXTRA_ARGUMENTS), + ) + + @property + def sensor_class(self): + """Return the class of this sensor, from SENSOR_CLASSES.""" + return "sound" diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/binary_sensor/mqtt.py index 28d9566b2ab..8c8beaddb6e 100644 --- a/homeassistant/components/binary_sensor/mqtt.py +++ b/homeassistant/components/binary_sensor/mqtt.py @@ -36,7 +36,10 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the MQTT binary sensor.""" + """Set up the MQTT binary sensor.""" + if discovery_info is not None: + config = PLATFORM_SCHEMA(discovery_info) + value_template = config.get(CONF_VALUE_TEMPLATE) if value_template is not None: value_template.hass = hass diff --git a/homeassistant/components/binary_sensor/services.yaml b/homeassistant/components/binary_sensor/services.yaml deleted file mode 100644 index a1ac8cf8b5d..00000000000 --- a/homeassistant/components/binary_sensor/services.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# Describes the format for available binary_sensor services - -ffmpeg_start: - description: Send a start command to a ffmpeg based sensor. - - fields: - entity_id: - description: Name(s) of entites that will start. Platform dependent. - example: 'binary_sensor.ffmpeg_noise' - -ffmpeg_stop: - description: Send a stop command to a ffmpeg based sensor. - - fields: - entity_id: - description: Name(s) of entites that will stop. Platform dependent. - example: 'binary_sensor.ffmpeg_noise' - -ffmpeg_restart: - description: Send a restart command to a ffmpeg based sensor. - - fields: - entity_id: - description: Name(s) of entites that will restart. Platform dependent. - example: 'binary_sensor.ffmpeg_noise' diff --git a/homeassistant/components/binary_sensor/template.py b/homeassistant/components/binary_sensor/template.py index e097c7c0ea4..fa60412c77f 100644 --- a/homeassistant/components/binary_sensor/template.py +++ b/homeassistant/components/binary_sensor/template.py @@ -118,7 +118,8 @@ class BinarySensorTemplate(BinarySensorDevice): if ex.args and ex.args[0].startswith( "UndefinedError: 'None' has no attribute"): # Common during HA startup - so just a warning - _LOGGER.warning(ex) + _LOGGER.warning('Could not render template %s,' + ' the state is unknown.', self._name) return - _LOGGER.error(ex) + _LOGGER.error('Could not render template %s: %s', self._name, ex) self._state = False diff --git a/homeassistant/components/binary_sensor/wink.py b/homeassistant/components/binary_sensor/wink.py index 19ecb853536..4e1969de858 100644 --- a/homeassistant/components/binary_sensor/wink.py +++ b/homeassistant/components/binary_sensor/wink.py @@ -4,11 +4,14 @@ Support for Wink binary sensors. For more details about this platform, please refer to the documentation at at https://home-assistant.io/components/binary_sensor.wink/ """ +import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.sensor.wink import WinkDevice +from homeassistant.components.wink import WinkDevice, DOMAIN from homeassistant.helpers.entity import Entity +_LOGGER = logging.getLogger(__name__) + DEPENDENCIES = ['wink'] # These are the available sensors mapped to binary_sensor class @@ -17,11 +20,14 @@ SENSOR_TYPES = { "brightness": "light", "vibration": "vibration", "loudness": "sound", + "noise": "sound", + "capturing_audio": "sound", "liquid_detected": "moisture", "motion": "motion", "presence": "occupancy", "co_detected": "gas", - "smoke_detected": "smoke" + "smoke_detected": "smoke", + "capturing_video": None } @@ -30,26 +36,54 @@ def setup_platform(hass, config, add_devices, discovery_info=None): import pywink for sensor in pywink.get_sensors(): - if sensor.capability() in SENSOR_TYPES: - add_devices([WinkBinarySensorDevice(sensor, hass)]) + _id = sensor.object_id() + sensor.name() + if _id not in hass.data[DOMAIN]['unique_ids']: + if sensor.capability() in SENSOR_TYPES: + add_devices([WinkBinarySensorDevice(sensor, hass)]) for key in pywink.get_keys(): - add_devices([WinkBinarySensorDevice(key, hass)]) + _id = key.object_id() + key.name() + if _id not in hass.data[DOMAIN]['unique_ids']: + add_devices([WinkBinarySensorDevice(key, hass)]) for sensor in pywink.get_smoke_and_co_detectors(): - add_devices([WinkBinarySensorDevice(sensor, hass)]) + _id = sensor.object_id() + sensor.name() + if _id not in hass.data[DOMAIN]['unique_ids']: + add_devices([WinkSmokeDetector(sensor, hass)]) for hub in pywink.get_hubs(): - add_devices([WinkHub(hub, hass)]) + _id = hub.object_id() + hub.name() + if _id not in hass.data[DOMAIN]['unique_ids']: + add_devices([WinkHub(hub, hass)]) for remote in pywink.get_remotes(): - add_devices([WinkRemote(remote, hass)]) + _id = remote.object_id() + remote.name() + if _id not in hass.data[DOMAIN]['unique_ids']: + add_devices([WinkRemote(remote, hass)]) for button in pywink.get_buttons(): - add_devices([WinkButton(button, hass)]) + _id = button.object_id() + button.name() + if _id not in hass.data[DOMAIN]['unique_ids']: + add_devices([WinkButton(button, hass)]) for gang in pywink.get_gangs(): - add_devices([WinkGang(gang, hass)]) + _id = gang.object_id() + gang.name() + if _id not in hass.data[DOMAIN]['unique_ids']: + add_devices([WinkGang(gang, hass)]) + + for door_bell_sensor in pywink.get_door_bells(): + _id = door_bell_sensor.object_id() + door_bell_sensor.name() + if _id not in hass.data[DOMAIN]['unique_ids']: + add_devices([WinkBinarySensorDevice(door_bell_sensor, hass)]) + + for camera_sensor in pywink.get_cameras(): + _id = camera_sensor.object_id() + camera_sensor.name() + if _id not in hass.data[DOMAIN]['unique_ids']: + try: + if camera_sensor.capability() in SENSOR_TYPES: + add_devices([WinkBinarySensorDevice(camera_sensor, hass)]) + except AttributeError: + _LOGGER.info("Device isn't a sensor, skipping.") class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity): @@ -58,8 +92,14 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity): def __init__(self, wink, hass): """Initialize the Wink binary sensor.""" super().__init__(wink, hass) - self._unit_of_measurement = self.wink.unit() - self.capability = self.wink.capability() + try: + self._unit_of_measurement = self.wink.unit() + except AttributeError: + self._unit_of_measurement = None + try: + self.capability = self.wink.capability() + except AttributeError: + self.capability = None @property def is_on(self): @@ -72,17 +112,27 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity): return SENSOR_TYPES.get(self.capability) -class WinkHub(WinkDevice, BinarySensorDevice, Entity): - """Representation of a Wink Hub.""" +class WinkSmokeDetector(WinkBinarySensorDevice): + """Representation of a Wink Smoke detector.""" - def __init(self, wink, hass): - """Initialize the hub sensor.""" - WinkDevice.__init__(self, wink, hass) + def __init__(self, wink, hass): + """Initialize the Wink binary sensor.""" + super().__init__(wink, hass) @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self.wink.state() + def device_state_attributes(self): + """Return the state attributes.""" + return { + 'test_activated': self.wink.test_activated() + } + + +class WinkHub(WinkBinarySensorDevice): + """Representation of a Wink Hub.""" + + def __init__(self, wink, hass): + """Initialize the Wink binary sensor.""" + super().__init__(wink, hass) @property def device_state_attributes(self): @@ -93,17 +143,12 @@ class WinkHub(WinkDevice, BinarySensorDevice, Entity): } -class WinkRemote(WinkDevice, BinarySensorDevice, Entity): +class WinkRemote(WinkBinarySensorDevice): """Representation of a Wink Lutron Connected bulb remote.""" - def __init(self, wink, hass): - """Initialize the hub sensor.""" - WinkDevice.__init__(self, wink, hass) - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self.wink.state() + def __init__(self, wink, hass): + """Initialize the Wink binary sensor.""" + super().__init__(wink, hass) @property def device_state_attributes(self): @@ -115,18 +160,18 @@ class WinkRemote(WinkDevice, BinarySensorDevice, Entity): 'button_down_pressed': self.wink.button_down_pressed() } + @property + def sensor_class(self): + """Return the class of this sensor, from SENSOR_CLASSES.""" + return None -class WinkButton(WinkDevice, BinarySensorDevice, Entity): + +class WinkButton(WinkBinarySensorDevice): """Representation of a Wink Relay button.""" - def __init(self, wink, hass): - """Initialize the hub sensor.""" - WinkDevice.__init__(self, wink, hass) - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self.wink.state() + def __init__(self, wink, hass): + """Initialize the Wink binary sensor.""" + super().__init__(wink, hass) @property def device_state_attributes(self): @@ -137,12 +182,12 @@ class WinkButton(WinkDevice, BinarySensorDevice, Entity): } -class WinkGang(WinkDevice, BinarySensorDevice, Entity): +class WinkGang(WinkBinarySensorDevice): """Representation of a Wink Relay gang.""" - def __init(self, wink, hass): - """Initialize the gang sensor.""" - WinkDevice.__init__(self, wink, hass) + def __init__(self, wink, hass): + """Initialize the Wink binary sensor.""" + super().__init__(wink, hass) @property def is_on(self): diff --git a/homeassistant/components/binary_sensor/zwave.py b/homeassistant/components/binary_sensor/zwave.py index b0054d7b00f..8dd02cc4b43 100644 --- a/homeassistant/components/binary_sensor/zwave.py +++ b/homeassistant/components/binary_sensor/zwave.py @@ -9,6 +9,7 @@ import datetime import homeassistant.util.dt as dt_util from homeassistant.helpers.event import track_point_in_time from homeassistant.components import zwave +from homeassistant.components.zwave import workaround from homeassistant.components.binary_sensor import ( DOMAIN, BinarySensorDevice) @@ -16,22 +17,6 @@ from homeassistant.components.binary_sensor import ( _LOGGER = logging.getLogger(__name__) DEPENDENCIES = [] -PHILIO = 0x013c -PHILIO_SLIM_SENSOR = 0x0002 -PHILIO_SLIM_SENSOR_MOTION = (PHILIO, PHILIO_SLIM_SENSOR, 0) -PHILIO_3_IN_1_SENSOR_GEN_4 = 0x000d -PHILIO_3_IN_1_SENSOR_GEN_4_MOTION = (PHILIO, PHILIO_3_IN_1_SENSOR_GEN_4, 0) -WENZHOU = 0x0118 -WENZHOU_SLIM_SENSOR_MOTION = (WENZHOU, PHILIO_SLIM_SENSOR, 0) - -WORKAROUND_NO_OFF_EVENT = 'trigger_no_off_event' - -DEVICE_MAPPINGS = { - PHILIO_SLIM_SENSOR_MOTION: WORKAROUND_NO_OFF_EVENT, - PHILIO_3_IN_1_SENSOR_GEN_4_MOTION: WORKAROUND_NO_OFF_EVENT, - WENZHOU_SLIM_SENSOR_MOTION: WORKAROUND_NO_OFF_EVENT, -} - def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Z-Wave platform for binary sensors.""" @@ -42,23 +27,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None): value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]] value.set_change_verified(False) - # Make sure that we have values for the key before converting to int - if (value.node.manufacturer_id.strip() and - value.node.product_id.strip()): - specific_sensor_key = (int(value.node.manufacturer_id, 16), - int(value.node.product_id, 16), - value.index) + device_mapping = workaround.get_device_mapping(value) + if device_mapping == workaround.WORKAROUND_NO_OFF_EVENT: + # Default the multiplier to 4 + re_arm_multiplier = (zwave.get_config_value(value.node, 9) or 4) + add_devices([ + ZWaveTriggerSensor(value, "motion", + hass, re_arm_multiplier * 8) + ]) + return - if specific_sensor_key in DEVICE_MAPPINGS: - if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_NO_OFF_EVENT: - # Default the multiplier to 4 - re_arm_multiplier = (zwave.get_config_value(value.node, - 9) or 4) - add_devices([ - ZWaveTriggerSensor(value, "motion", - hass, re_arm_multiplier * 8) - ]) - return + if workaround.get_device_component_mapping(value) == DOMAIN: + add_devices([ZWaveBinarySensor(value, None)]) + return if value.command_class == zwave.const.COMMAND_CLASS_SENSOR_BINARY: add_devices([ZWaveBinarySensor(value, None)]) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 0cd9bbe17d3..cb684785207 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -122,8 +122,6 @@ def set_away_mode(hass, away_mode, entity_id=None): if entity_id: data[ATTR_ENTITY_ID] = entity_id - _LOGGER.warning( - 'This service has been deprecated; use climate.set_hold_mode') hass.services.call(DOMAIN, SERVICE_SET_AWAY_MODE, data) @@ -243,14 +241,6 @@ def async_setup(hass, config): away_mode = service.data.get(ATTR_AWAY_MODE) - if away_mode is None: - _LOGGER.error( - "Received call to %s without attribute %s", - SERVICE_SET_AWAY_MODE, ATTR_AWAY_MODE) - return - - _LOGGER.warning( - 'This service has been deprecated; use climate.set_hold_mode') for climate in target_climate: if away_mode: yield from climate.async_turn_away_mode_on() @@ -288,12 +278,6 @@ def async_setup(hass, config): aux_heat = service.data.get(ATTR_AUX_HEAT) - if aux_heat is None: - _LOGGER.error( - "Received call to %s without attribute %s", - SERVICE_SET_AUX_HEAT, ATTR_AUX_HEAT) - return - for climate in target_climate: if aux_heat: yield from climate.async_turn_aux_heat_on() @@ -340,12 +324,6 @@ def async_setup(hass, config): humidity = service.data.get(ATTR_HUMIDITY) - if humidity is None: - _LOGGER.error( - "Received call to %s without attribute %s", - SERVICE_SET_HUMIDITY, ATTR_HUMIDITY) - return - for climate in target_climate: yield from climate.async_set_humidity(humidity) @@ -363,12 +341,6 @@ def async_setup(hass, config): fan = service.data.get(ATTR_FAN_MODE) - if fan is None: - _LOGGER.error( - "Received call to %s without attribute %s", - SERVICE_SET_FAN_MODE, ATTR_FAN_MODE) - return - for climate in target_climate: yield from climate.async_set_fan_mode(fan) @@ -386,12 +358,6 @@ def async_setup(hass, config): operation_mode = service.data.get(ATTR_OPERATION_MODE) - if operation_mode is None: - _LOGGER.error( - "Received call to %s without attribute %s", - SERVICE_SET_OPERATION_MODE, ATTR_OPERATION_MODE) - return - for climate in target_climate: yield from climate.async_set_operation_mode(operation_mode) @@ -409,12 +375,6 @@ def async_setup(hass, config): swing_mode = service.data.get(ATTR_SWING_MODE) - if swing_mode is None: - _LOGGER.error( - "Received call to %s without attribute %s", - SERVICE_SET_SWING_MODE, ATTR_SWING_MODE) - return - for climate in target_climate: yield from climate.async_set_swing_mode(swing_mode) diff --git a/homeassistant/components/climate/demo.py b/homeassistant/components/climate/demo.py index a66873cbc63..9830daff69c 100644 --- a/homeassistant/components/climate/demo.py +++ b/homeassistant/components/climate/demo.py @@ -135,27 +135,27 @@ class DemoClimate(ClimateDevice): kwargs.get(ATTR_TARGET_TEMP_LOW) is not None: self._target_temperature_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) self._target_temperature_low = kwargs.get(ATTR_TARGET_TEMP_LOW) - self.update_ha_state() + self.schedule_update_ha_state() def set_humidity(self, humidity): """Set new target temperature.""" self._target_humidity = humidity - self.update_ha_state() + self.schedule_update_ha_state() def set_swing_mode(self, swing_mode): """Set new target temperature.""" self._current_swing_mode = swing_mode - self.update_ha_state() + self.schedule_update_ha_state() def set_fan_mode(self, fan): """Set new target temperature.""" self._current_fan_mode = fan - self.update_ha_state() + self.schedule_update_ha_state() def set_operation_mode(self, operation_mode): """Set new target temperature.""" self._current_operation = operation_mode - self.update_ha_state() + self.schedule_update_ha_state() @property def current_swing_mode(self): @@ -170,24 +170,24 @@ class DemoClimate(ClimateDevice): def turn_away_mode_on(self): """Turn away mode on.""" self._away = True - self.update_ha_state() + self.schedule_update_ha_state() def turn_away_mode_off(self): """Turn away mode off.""" self._away = False - self.update_ha_state() + self.schedule_update_ha_state() def set_hold_mode(self, hold): """Update hold mode on.""" self._hold = hold - self.update_ha_state() + self.schedule_update_ha_state() def turn_aux_heat_on(self): """Turn away auxillary heater on.""" self._aux = True - self.update_ha_state() + self.schedule_update_ha_state() def turn_aux_heat_off(self): """Turn auxillary heater off.""" self._aux = False - self.update_ha_state() + self.schedule_update_ha_state() diff --git a/homeassistant/components/climate/ecobee.py b/homeassistant/components/climate/ecobee.py index dcee6d9ce31..18ccff459b0 100644 --- a/homeassistant/components/climate/ecobee.py +++ b/homeassistant/components/climate/ecobee.py @@ -69,7 +69,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for thermostat in target_thermostats: thermostat.set_fan_min_on_time(str(fan_min_on_time)) - thermostat.update_ha_state(True) + thermostat.schedule_update_ha_state(True) def resume_program_set_service(service): """Resume the program on the target thermostats.""" @@ -85,7 +85,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for thermostat in target_thermostats: thermostat.resume_program(resume_all) - thermostat.update_ha_state(True) + thermostat.schedule_update_ha_state(True) descriptions = load_yaml_config_file( path.join(path.dirname(__file__), 'services.yaml')) @@ -186,12 +186,27 @@ class Thermostat(ClimateDevice): @property def current_hold_mode(self): """Return current hold mode.""" - if self.is_away_mode_on: + events = self.thermostat['events'] + if any((event['holdClimateRef'] == 'away' and + int(event['endDate'][0:4])-int(event['startDate'][0:4]) <= 1) + or event['type'] == 'autoAway' + for event in events): + # away hold is auto away or a temporary hold from away climate hold = 'away' - elif self.is_home_mode_on: + elif any(event['holdClimateRef'] == 'away' and + int(event['endDate'][0:4])-int(event['startDate'][0:4]) > 1 + for event in events): + # a permanent away is not considered a hold, but away_mode + hold = None + elif any(event['holdClimateRef'] == 'home' or + event['type'] == 'autoHome' + for event in events): + # home mode is auto home or any home hold hold = 'home' - elif self.is_temp_hold_on(): + elif any(event['type'] == 'hold' and event['running'] + for event in events): hold = 'temp' + # temperature hold is any other hold not based on climate else: hold = None return hold @@ -255,42 +270,23 @@ class Thermostat(ClimateDevice): return any(event['type'] == 'vacation' and event['running'] for event in events) - def is_temp_hold_on(self): - """Return true if temperature hold is on.""" - events = self.thermostat['events'] - return any(event['type'] == 'hold' and event['running'] - for event in events) - @property def is_away_mode_on(self): """Return true if away mode is on.""" events = self.thermostat['events'] - return any(event['holdClimateRef'] == 'away' or - event['type'] == 'autoAway' + return any(event['holdClimateRef'] == 'away' and + int(event['endDate'][0:4])-int(event['startDate'][0:4]) > 1 for event in events) def turn_away_mode_on(self): """Turn away on.""" self.data.ecobee.set_climate_hold(self.thermostat_index, - "away", self.hold_preference()) + "away", 'indefinite') self.update_without_throttle = True def turn_away_mode_off(self): """Turn away off.""" - self.set_hold_mode(None) - - @property - def is_home_mode_on(self): - """Return true if home mode is on.""" - events = self.thermostat['events'] - return any(event['holdClimateRef'] == 'home' or - event['type'] == 'autoHome' - for event in events) - - def turn_home_mode_on(self): - """Turn home on.""" - self.data.ecobee.set_climate_hold(self.thermostat_index, - "home", self.hold_preference()) + self.data.ecobee.resume_program(self.thermostat_index) self.update_without_throttle = True def set_hold_mode(self, hold_mode): @@ -298,11 +294,14 @@ class Thermostat(ClimateDevice): hold = self.current_hold_mode if hold == hold_mode: + # no change, so no action required return elif hold_mode == 'away': - self.turn_away_mode_on() + self.data.ecobee.set_climate_hold(self.thermostat_index, + "away", self.hold_preference()) elif hold_mode == 'home': - self.turn_home_mode_on() + self.data.ecobee.set_climate_hold(self.thermostat_index, + "home", self.hold_preference()) elif hold_mode == 'temp': self.set_temp_hold(int(self.current_temperature)) else: @@ -378,17 +377,8 @@ class Thermostat(ClimateDevice): default = self.thermostat['settings']['holdAction'] if default == 'nextTransition': return default - elif default == 'indefinite': - return default + # add further conditions if other hold durations should be + # supported; note that this should not include 'indefinite' + # as an indefinite away hold is interpreted as away_mode else: return 'nextTransition' - - # Sleep mode isn't used in UI yet: - - # def turn_sleep_mode_on(self): - # """ Turns sleep mode on. """ - # self.data.ecobee.set_climate_hold(self.thermostat_index, "sleep") - - # def turn_sleep_mode_off(self): - # """ Turns sleep mode off. """ - # self.data.ecobee.resume_program(self.thermostat_index) diff --git a/homeassistant/components/climate/eq3btsmart.py b/homeassistant/components/climate/eq3btsmart.py index 6587ad86300..7e0712647ea 100644 --- a/homeassistant/components/climate/eq3btsmart.py +++ b/homeassistant/components/climate/eq3btsmart.py @@ -10,14 +10,14 @@ import voluptuous as vol from homeassistant.components.climate import ( ClimateDevice, PLATFORM_SCHEMA, PRECISION_HALVES, - STATE_UNKNOWN, STATE_AUTO, STATE_ON, STATE_OFF, + STATE_AUTO, STATE_ON, STATE_OFF, ) from homeassistant.const import ( CONF_MAC, TEMP_CELSIUS, CONF_DEVICES, ATTR_TEMPERATURE) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-eq3bt==0.1.4'] +REQUIREMENTS = ['python-eq3bt==0.1.5'] _LOGGER = logging.getLogger(__name__) @@ -61,15 +61,12 @@ class EQ3BTSmartThermostat(ClimateDevice): # we want to avoid name clash with this module.. import eq3bt as eq3 - self.modes = {None: STATE_UNKNOWN, # When not yet connected. - eq3.Mode.Unknown: STATE_UNKNOWN, - eq3.Mode.Auto: STATE_AUTO, - # away handled separately, here just for reverse mapping - eq3.Mode.Away: STATE_AWAY, + self.modes = {eq3.Mode.Open: STATE_ON, eq3.Mode.Closed: STATE_OFF, - eq3.Mode.Open: STATE_ON, + eq3.Mode.Auto: STATE_AUTO, eq3.Mode.Manual: STATE_MANUAL, - eq3.Mode.Boost: STATE_BOOST} + eq3.Mode.Boost: STATE_BOOST, + eq3.Mode.Away: STATE_AWAY} self.reverse_modes = {v: k for k, v in self.modes.items()} @@ -79,7 +76,7 @@ class EQ3BTSmartThermostat(ClimateDevice): @property def available(self) -> bool: """Return if thermostat is available.""" - return self.current_operation != STATE_UNKNOWN + return self.current_operation is not None @property def name(self): @@ -116,6 +113,8 @@ class EQ3BTSmartThermostat(ClimateDevice): @property def current_operation(self): """Current mode.""" + if self._thermostat.mode < 0: + return None return self.modes[self._thermostat.mode] @property diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index 562847567a3..da746270197 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -4,17 +4,19 @@ Adds support for generic thermostat units. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/climate.generic_thermostat/ """ +import asyncio import logging import voluptuous as vol +from homeassistant.core import callback from homeassistant.components import switch from homeassistant.components.climate import ( STATE_HEAT, STATE_COOL, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA) from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE) from homeassistant.helpers import condition -from homeassistant.helpers.event import track_state_change +from homeassistant.helpers.event import async_track_state_change import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -48,7 +50,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -def setup_platform(hass, config, add_devices, discovery_info=None): +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup the generic thermostat.""" name = config.get(CONF_NAME) heater_entity_id = config.get(CONF_HEATER) @@ -60,7 +63,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): min_cycle_duration = config.get(CONF_MIN_DUR) tolerance = config.get(CONF_TOLERANCE) - add_devices([GenericThermostat( + yield from async_add_devices([GenericThermostat( hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp, target_temp, ac_mode, min_cycle_duration, tolerance)]) @@ -86,12 +89,14 @@ class GenericThermostat(ClimateDevice): self._target_temp = target_temp self._unit = hass.config.units.temperature_unit - track_state_change(hass, sensor_entity_id, self._sensor_changed) - track_state_change(hass, heater_entity_id, self._switch_changed) + async_track_state_change( + hass, sensor_entity_id, self._async_sensor_changed) + async_track_state_change( + hass, heater_entity_id, self._async_switch_changed) sensor_state = hass.states.get(sensor_entity_id) if sensor_state: - self._update_temp(sensor_state) + self._async_update_temp(sensor_state) @property def should_poll(self): @@ -128,14 +133,15 @@ class GenericThermostat(ClimateDevice): """Return the temperature we try to reach.""" return self._target_temp - def set_temperature(self, **kwargs): + @asyncio.coroutine + def async_set_temperature(self, **kwargs): """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) if temperature is None: return self._target_temp = temperature - self._control_heating() - self.schedule_update_ha_state() + self._async_control_heating() + yield from self.async_update_ha_state() @property def min_temp(self): @@ -157,22 +163,25 @@ class GenericThermostat(ClimateDevice): # Get default temp from super class return ClimateDevice.max_temp.fget(self) - def _sensor_changed(self, entity_id, old_state, new_state): + @asyncio.coroutine + def _async_sensor_changed(self, entity_id, old_state, new_state): """Called when temperature changes.""" if new_state is None: return - self._update_temp(new_state) - self._control_heating() - self.schedule_update_ha_state() + self._async_update_temp(new_state) + self._async_control_heating() + yield from self.async_update_ha_state() - def _switch_changed(self, entity_id, old_state, new_state): + @callback + def _async_switch_changed(self, entity_id, old_state, new_state): """Called when heater switch changes state.""" if new_state is None: return - self.schedule_update_ha_state() + self.hass.async_add_job(self.async_update_ha_state()) - def _update_temp(self, state): + @callback + def _async_update_temp(self, state): """Update thermostat with latest state from sensor.""" unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -182,7 +191,8 @@ class GenericThermostat(ClimateDevice): except ValueError as ex: _LOGGER.error('Unable to update from sensor: %s', ex) - def _control_heating(self): + @callback + def _async_control_heating(self): """Check if we need to turn heating on or off.""" if not self._active and None not in (self._cur_temp, self._target_temp): @@ -198,9 +208,9 @@ class GenericThermostat(ClimateDevice): current_state = STATE_ON else: current_state = STATE_OFF - long_enough = condition.state(self.hass, self.heater_entity_id, - current_state, - self.min_cycle_duration) + long_enough = condition.state( + self.hass, self.heater_entity_id, current_state, + self.min_cycle_duration) if not long_enough: return @@ -210,12 +220,12 @@ class GenericThermostat(ClimateDevice): too_cold = self._target_temp - self._cur_temp > self._tolerance if too_cold: _LOGGER.info('Turning off AC %s', self.heater_entity_id) - switch.turn_off(self.hass, self.heater_entity_id) + switch.async_turn_off(self.hass, self.heater_entity_id) else: too_hot = self._cur_temp - self._target_temp > self._tolerance if too_hot: _LOGGER.info('Turning on AC %s', self.heater_entity_id) - switch.turn_on(self.hass, self.heater_entity_id) + switch.async_turn_on(self.hass, self.heater_entity_id) else: is_heating = self._is_device_active if is_heating: @@ -223,12 +233,12 @@ class GenericThermostat(ClimateDevice): if too_hot: _LOGGER.info('Turning off heater %s', self.heater_entity_id) - switch.turn_off(self.hass, self.heater_entity_id) + switch.async_turn_off(self.hass, self.heater_entity_id) else: too_cold = self._target_temp - self._cur_temp > self._tolerance if too_cold: _LOGGER.info('Turning on heater %s', self.heater_entity_id) - switch.turn_on(self.hass, self.heater_entity_id) + switch.async_turn_on(self.hass, self.heater_entity_id) @property def _is_device_active(self): diff --git a/homeassistant/components/climate/mysensors.py b/homeassistant/components/climate/mysensors.py index 6c55b3b4451..02979e75f5f 100755 --- a/homeassistant/components/climate/mysensors.py +++ b/homeassistant/components/climate/mysensors.py @@ -135,7 +135,7 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice): if self.gateway.optimistic: # optimistically assume that switch has changed state self._values[value_type] = value - self.update_ha_state() + self.schedule_update_ha_state() def set_fan_mode(self, fan): """Set new target temperature.""" @@ -145,7 +145,7 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice): if self.gateway.optimistic: # optimistically assume that switch has changed state self._values[set_req.V_HVAC_SPEED] = fan - self.update_ha_state() + self.schedule_update_ha_state() def set_operation_mode(self, operation_mode): """Set new target temperature.""" @@ -156,7 +156,7 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice): if self.gateway.optimistic: # optimistically assume that switch has changed state self._values[set_req.V_HVAC_FLOW_STATE] = operation_mode - self.update_ha_state() + self.schedule_update_ha_state() def update(self): """Update the controller with the latest value from a sensor.""" diff --git a/homeassistant/components/climate/netatmo.py b/homeassistant/components/climate/netatmo.py index 163054cd121..0afc8c29bd9 100755 --- a/homeassistant/components/climate/netatmo.py +++ b/homeassistant/components/climate/netatmo.py @@ -111,7 +111,6 @@ class NetatmoThermostat(ClimateDevice): temp = None self._data.thermostatdata.setthermpoint(mode, temp, endTimeOffset=None) self._away = True - self.update_ha_state() def turn_away_mode_off(self): """Turn away off.""" @@ -119,7 +118,6 @@ class NetatmoThermostat(ClimateDevice): temp = None self._data.thermostatdata.setthermpoint(mode, temp, endTimeOffset=None) self._away = False - self.update_ha_state() def set_temperature(self, endTimeOffset=DEFAULT_TIME_OFFSET, **kwargs): """Set new target temperature for 2 hours.""" @@ -131,7 +129,6 @@ class NetatmoThermostat(ClimateDevice): mode, temperature, endTimeOffset) self._target_temperature = temperature self._away = False - self.update_ha_state() @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): diff --git a/homeassistant/components/climate/services.yaml b/homeassistant/components/climate/services.yaml index 899a3dcfe33..4aebb1c85c9 100644 --- a/homeassistant/components/climate/services.yaml +++ b/homeassistant/components/climate/services.yaml @@ -78,7 +78,7 @@ set_fan_mode: description: Name(s) of entities to change example: 'climate.nest' - fan: + fan_mode: description: New value of fan mode example: On Low diff --git a/homeassistant/components/climate/wink.py b/homeassistant/components/climate/wink.py index 733d2baddf7..d4785d4a13e 100644 --- a/homeassistant/components/climate/wink.py +++ b/homeassistant/components/climate/wink.py @@ -4,7 +4,7 @@ Support for Wink thermostats. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/climate.wink/ """ -from homeassistant.components.wink import WinkDevice +from homeassistant.components.wink import WinkDevice, DOMAIN from homeassistant.components.climate import ( STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, @@ -13,12 +13,16 @@ from homeassistant.components.climate import ( from homeassistant.const import ( TEMP_CELSIUS, STATE_ON, STATE_OFF, STATE_UNKNOWN) -from homeassistant.loader import get_component DEPENDENCIES = ['wink'] STATE_AUX = 'aux' STATE_ECO = 'eco' +STATE_FAN = 'fan' +SPEED_LOWEST = 'lowest' +SPEED_LOW = 'low' +SPEED_MEDIUM = 'medium' +SPEED_HIGH = 'high' ATTR_EXTERNAL_TEMPERATURE = "external_temperature" ATTR_SMART_TEMPERATURE = "smart_temperature" @@ -30,8 +34,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Wink thermostat.""" import pywink temp_unit = hass.config.units.temperature_unit - add_devices(WinkThermostat(thermostat, hass, temp_unit) - for thermostat in pywink.get_thermostats()) + for climate in pywink.get_thermostats(): + _id = climate.object_id() + climate.name() + if _id not in hass.data[DOMAIN]['unique_ids']: + add_devices([WinkThermostat(climate, hass, temp_unit)]) + for climate in pywink.get_air_conditioners(): + _id = climate.object_id() + climate.name() + if _id not in hass.data[DOMAIN]['unique_ids']: + add_devices([WinkAC(climate, hass, temp_unit)]) # pylint: disable=abstract-method,too-many-public-methods, too-many-branches @@ -41,7 +51,6 @@ class WinkThermostat(WinkDevice, ClimateDevice): def __init__(self, wink, hass, temp_unit): """Initialize the Wink device.""" super().__init__(wink, hass) - wink = get_component('wink') self._config_temp_unit = temp_unit @property @@ -329,3 +338,131 @@ class WinkThermostat(WinkDevice, ClimateDevice): else: return_value = maximum return return_value + + +class WinkAC(WinkDevice, ClimateDevice): + """Representation of a Wink air conditioner.""" + + def __init__(self, wink, hass, temp_unit): + """Initialize the Wink device.""" + super().__init__(wink, hass) + self._config_temp_unit = temp_unit + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + # The Wink API always returns temp in Celsius + return TEMP_CELSIUS + + @property + def device_state_attributes(self): + """Return the optional state attributes.""" + data = {} + target_temp_high = self.target_temperature_high + target_temp_low = self.target_temperature_low + if target_temp_high is not None: + data[ATTR_TARGET_TEMP_HIGH] = self._convert_for_display( + self.target_temperature_high) + if target_temp_low is not None: + data[ATTR_TARGET_TEMP_LOW] = self._convert_for_display( + self.target_temperature_low) + data["total_consumption"] = self.wink.toatl_consumption() + data["schedule_enabled"] = self.wink.toatl_consumption() + + return data + + @property + def current_temperature(self): + """Return the current temperature.""" + return self.wink.current_temperature() + + @property + def current_operation(self): + """Return current operation ie. heat, cool, idle.""" + if not self.wink.is_on(): + current_op = STATE_OFF + elif self.wink.current_mode() == 'cool_only': + current_op = STATE_COOL + elif self.wink.current_mode() == 'auto_eco': + current_op = STATE_ECO + elif self.wink.current_mode() == 'fan_only': + current_op = STATE_FAN + else: + current_op = STATE_UNKNOWN + return current_op + + @property + def operation_list(self): + """List of available operation modes.""" + op_list = ['off'] + modes = self.wink.modes() + if 'cool_only' in modes: + op_list.append(STATE_COOL) + if 'auto_eco' in modes: + op_list.append(STATE_ECO) + if 'fan_eco' in modes: + op_list.append(STATE_FAN) + return op_list + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + target_temp = kwargs.get(ATTR_TEMPERATURE) + self.wink.set_temperature(target_temp) + + def set_operation_mode(self, operation_mode): + """Set operation mode.""" + if operation_mode == STATE_COOL: + self.wink.set_operation_mode('cool_only') + elif operation_mode == STATE_ECO: + self.wink.set_operation_mode('auto_eco') + elif operation_mode == STATE_OFF: + self.wink.set_operation_mode('off') + elif operation_mode == STATE_FAN: + self.wink.set_operation_mode('fan_only') + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self.wink.current_max_set_point() + + @property + def target_temperature_low(self): + """Only supports cool.""" + return None + + @property + def target_temperature_high(self): + """Only supports cool.""" + return None + + @property + def current_fan_mode(self): + """Return the current fan mode.""" + speed = self.wink.current_fan_speed() + if speed <= 0.3 and speed >= 0.0: + return SPEED_LOWEST + elif speed <= 0.5 and speed > 0.3: + return SPEED_LOW + elif speed <= 0.8 and speed > 0.5: + return SPEED_MEDIUM + elif speed <= 1.0 and speed > 0.8: + return SPEED_HIGH + else: + return STATE_UNKNOWN + + @property + def fan_list(self): + """List of available fan modes.""" + return [SPEED_LOWEST, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + + def set_fan_mode(self, mode): + """Set fan speed.""" + if mode == SPEED_LOWEST: + speed = 0.3 + elif mode == SPEED_LOW: + speed = 0.5 + elif mode == SPEED_MEDIUM: + speed = 0.8 + elif mode == SPEED_HIGH: + speed = 1.0 + self.wink.set_ac_fan_speed(speed) diff --git a/homeassistant/components/climate/zwave.py b/homeassistant/components/climate/zwave.py index fc2e8736ee9..545d3b41432 100755 --- a/homeassistant/components/climate/zwave.py +++ b/homeassistant/components/climate/zwave.py @@ -83,45 +83,52 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): def update_properties(self): """Callback on data changes for node values.""" # Operation Mode - for value in self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE).values(): - self._current_operation = value.data - self._operation_list = list(value.data_items) - _LOGGER.debug("self._operation_list=%s", self._operation_list) - _LOGGER.debug("self._current_operation=%s", - self._current_operation) + self._current_operation = self.get_value( + class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE, member='data') + operation_list = self.get_value( + class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE, + member='data_items') + if operation_list: + self._operation_list = list(operation_list) + _LOGGER.debug("self._operation_list=%s", self._operation_list) + _LOGGER.debug("self._current_operation=%s", self._current_operation) + # Current Temp - for value in ( - self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL) - .values()): - if value.label == 'Temperature': - self._current_temperature = round((float(value.data)), 1) - self._unit = value.units + self._current_temperature = self.get_value( + class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL, + label=['Temperature'], member='data') + self._unit = self.get_value( + class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL, + label=['Temperature'], member='units') + # Fan Mode - for value in ( - self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE) - .values()): - self._current_fan_mode = value.data - self._fan_list = list(value.data_items) - _LOGGER.debug("self._fan_list=%s", self._fan_list) - _LOGGER.debug("self._current_fan_mode=%s", - self._current_fan_mode) + self._current_fan_mode = self.get_value( + class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE, + member='data') + fan_list = self.get_value( + class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE, + member='data_items') + if fan_list: + self._fan_list = list(fan_list) + _LOGGER.debug("self._fan_list=%s", self._fan_list) + _LOGGER.debug("self._current_fan_mode=%s", + self._current_fan_mode) # Swing mode if self._zxt_120 == 1: - for value in ( - self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_CONFIGURATION) - .values()): - if value.command_class == \ - zwave.const.COMMAND_CLASS_CONFIGURATION and \ - value.index == 33: - self._current_swing_mode = value.data - self._swing_list = list(value.data_items) - _LOGGER.debug("self._swing_list=%s", self._swing_list) - _LOGGER.debug("self._current_swing_mode=%s", - self._current_swing_mode) + self._current_swing_mode = ( + self.get_value( + class_id=zwave.const.COMMAND_CLASS_CONFIGURATION, + index=33, + member='data')) + swing_list = self.get_value(class_id=zwave.const + .COMMAND_CLASS_CONFIGURATION, + index=33, + member='data_items') + if swing_list: + self._swing_list = list(swing_list) + _LOGGER.debug("self._swing_list=%s", self._swing_list) + _LOGGER.debug("self._current_swing_mode=%s", + self._current_swing_mode) # Set point temps = [] for value in ( @@ -139,19 +146,16 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): break else: self._target_temperature = round((float(value.data)), 1) + # Operating state - for value in ( - self._node.get_values( - class_id=zwave.const - .COMMAND_CLASS_THERMOSTAT_OPERATING_STATE).values()): - self._operating_state = value.data + self._operating_state = self.get_value( + class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_OPERATING_STATE, + member='data') # Fan operating state - for value in ( - self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_STATE) - .values()): - self._fan_state = value.data + self._fan_state = self.get_value( + class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_STATE, + member='data') @property def should_poll(self): @@ -215,50 +219,29 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): else: return - for value in (self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT) - .values()): - if value.index == self._index: - if self._zxt_120: - # ZXT-120 responds only to whole int - value.data = round(temperature, 0) - self._target_temperature = temperature - self.update_ha_state() - else: - value.data = temperature - self.update_ha_state() - break + self.set_value( + class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT, + index=self._index, data=temperature) + self.update_ha_state() def set_fan_mode(self, fan): """Set new target fan mode.""" - for value in (self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE). - values()): - if value.command_class == \ - zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE and \ - value.index == 0: - value.data = bytes(fan, 'utf-8') - break + self.set_value( + class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE, + index=0, data=bytes(fan, 'utf-8')) def set_operation_mode(self, operation_mode): """Set new target operation mode.""" - for value in self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE).values(): - if value.command_class == \ - zwave.const.COMMAND_CLASS_THERMOSTAT_MODE and value.index == 0: - value.data = bytes(operation_mode, 'utf-8') - break + self.set_value( + class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE, + index=0, data=bytes(operation_mode, 'utf-8')) def set_swing_mode(self, swing_mode): """Set new target swing mode.""" if self._zxt_120 == 1: - for value in self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_CONFIGURATION).values(): - if value.command_class == \ - zwave.const.COMMAND_CLASS_CONFIGURATION and \ - value.index == 33: - value.data = bytes(swing_mode, 'utf-8') - break + self.set_value( + class_id=zwave.const.COMMAND_CLASS_CONFIGURATION, + index=33, data=bytes(swing_mode, 'utf-8')) @property def device_state_attributes(self): diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index da473df111e..1dc6101f1e9 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -4,9 +4,11 @@ Support for Cover devices. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/cover/ """ -import os +import asyncio from datetime import timedelta +import functools as ft import logging +import os import voluptuous as vol @@ -53,17 +55,17 @@ COVER_SET_COVER_TILT_POSITION_SCHEMA = COVER_SERVICE_SCHEMA.extend({ }) SERVICE_TO_METHOD = { - SERVICE_OPEN_COVER: {'method': 'open_cover'}, - SERVICE_CLOSE_COVER: {'method': 'close_cover'}, + SERVICE_OPEN_COVER: {'method': 'async_open_cover'}, + SERVICE_CLOSE_COVER: {'method': 'async_close_cover'}, SERVICE_SET_COVER_POSITION: { - 'method': 'set_cover_position', + 'method': 'async_set_cover_position', 'schema': COVER_SET_COVER_POSITION_SCHEMA}, - SERVICE_STOP_COVER: {'method': 'stop_cover'}, - SERVICE_OPEN_COVER_TILT: {'method': 'open_cover_tilt'}, - SERVICE_CLOSE_COVER_TILT: {'method': 'close_cover_tilt'}, - SERVICE_STOP_COVER_TILT: {'method': 'stop_cover_tilt'}, + SERVICE_STOP_COVER: {'method': 'async_stop_cover'}, + SERVICE_OPEN_COVER_TILT: {'method': 'async_open_cover_tilt'}, + SERVICE_CLOSE_COVER_TILT: {'method': 'async_close_cover_tilt'}, + SERVICE_STOP_COVER_TILT: {'method': 'async_stop_cover_tilt'}, SERVICE_SET_COVER_TILT_POSITION: { - 'method': 'set_cover_tilt_position', + 'method': 'async_set_cover_tilt_position', 'schema': COVER_SET_COVER_TILT_POSITION_SCHEMA}, } @@ -124,40 +126,53 @@ def stop_cover_tilt(hass, entity_id=None): hass.services.call(DOMAIN, SERVICE_STOP_COVER_TILT, data) -def setup(hass, config): +@asyncio.coroutine +def async_setup(hass, config): """Track states and offer events for covers.""" component = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_COVERS) - component.setup(config) - def handle_cover_service(service): + yield from component.async_setup(config) + + @asyncio.coroutine + def async_handle_cover_service(service): """Handle calls to the cover services.""" + covers = component.async_extract_from_service(service) method = SERVICE_TO_METHOD.get(service.service) params = service.data.copy() params.pop(ATTR_ENTITY_ID, None) - if not method: - return - - covers = component.extract_from_service(service) - + # call method for cover in covers: - getattr(cover, method['method'])(**params) + yield from getattr(cover, method['method'])(**params) + + update_tasks = [] for cover in covers: if not cover.should_poll: continue - cover.update_ha_state(True) + update_coro = hass.loop.create_task( + cover.async_update_ha_state(True)) + if hasattr(cover, 'async_update'): + update_tasks.append(update_coro) + else: + yield from update_coro - descriptions = load_yaml_config_file( - os.path.join(os.path.dirname(__file__), 'services.yaml')) + if update_tasks: + yield from asyncio.wait(update_tasks, loop=hass.loop) + + descriptions = yield from hass.loop.run_in_executor( + None, load_yaml_config_file, os.path.join( + os.path.dirname(__file__), 'services.yaml')) for service_name in SERVICE_TO_METHOD: schema = SERVICE_TO_METHOD[service_name].get( 'schema', COVER_SERVICE_SCHEMA) - hass.services.register(DOMAIN, service_name, handle_cover_service, - descriptions.get(service_name), schema=schema) + hass.services.async_register( + DOMAIN, service_name, async_handle_cover_service, + descriptions.get(service_name), schema=schema) + return True @@ -215,30 +230,94 @@ class CoverDevice(Entity): """Open the cover.""" raise NotImplementedError() + def async_open_cover(self, **kwargs): + """Open the cover. + + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.loop.run_in_executor( + None, ft.partial(self.open_cover, **kwargs)) + def close_cover(self, **kwargs): """Close cover.""" raise NotImplementedError() + def async_close_cover(self, **kwargs): + """Close cover. + + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.loop.run_in_executor( + None, ft.partial(self.close_cover, **kwargs)) + def set_cover_position(self, **kwargs): """Move the cover to a specific position.""" pass + def async_set_cover_position(self, **kwargs): + """Move the cover to a specific position. + + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.loop.run_in_executor( + None, ft.partial(self.set_cover_position, **kwargs)) + def stop_cover(self, **kwargs): """Stop the cover.""" pass + def async_stop_cover(self, **kwargs): + """Stop the cover. + + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.loop.run_in_executor( + None, ft.partial(self.stop_cover, **kwargs)) + def open_cover_tilt(self, **kwargs): """Open the cover tilt.""" pass + def async_open_cover_tilt(self, **kwargs): + """Open the cover tilt. + + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.loop.run_in_executor( + None, ft.partial(self.open_cover_tilt, **kwargs)) + def close_cover_tilt(self, **kwargs): """Close the cover tilt.""" pass + def async_close_cover_tilt(self, **kwargs): + """Close the cover tilt. + + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.loop.run_in_executor( + None, ft.partial(self.close_cover_tilt, **kwargs)) + def set_cover_tilt_position(self, **kwargs): """Move the cover tilt to a specific position.""" pass + def async_set_cover_tilt_position(self, **kwargs): + """Move the cover tilt to a specific position. + + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.loop.run_in_executor( + None, ft.partial(self.set_cover_tilt_position, **kwargs)) + def stop_cover_tilt(self, **kwargs): """Stop the cover.""" pass + + def async_stop_cover_tilt(self, **kwargs): + """Stop the cover. + + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.loop.run_in_executor( + None, ft.partial(self.stop_cover_tilt, **kwargs)) diff --git a/homeassistant/components/cover/demo.py b/homeassistant/components/cover/demo.py index 5929ab1851a..070c37a0e3c 100644 --- a/homeassistant/components/cover/demo.py +++ b/homeassistant/components/cover/demo.py @@ -149,7 +149,7 @@ class DemoCover(CoverDevice): if self._position in (100, 0, self._set_position): self.stop_cover() - self.update_ha_state() + self.schedule_update_ha_state() def _listen_cover_tilt(self): """Listen for changes in cover tilt.""" @@ -167,4 +167,4 @@ class DemoCover(CoverDevice): if self._tilt_position in (100, 0, self._set_tilt_position): self.stop_cover_tilt() - self.update_ha_state() + self.schedule_update_ha_state() diff --git a/homeassistant/components/cover/garadget.py b/homeassistant/components/cover/garadget.py index 813ddea7170..1847e58c4a5 100644 --- a/homeassistant/components/cover/garadget.py +++ b/homeassistant/components/cover/garadget.py @@ -199,8 +199,7 @@ class GaradgetCover(CoverDevice): def _check_state(self, now): """Check the state of the service during an operation.""" - self.update() - self.update_ha_state() + self.schedule_update_ha_state(True) def close_cover(self): """Close the cover.""" diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py index 44b59133d21..aa549986533 100644 --- a/homeassistant/components/cover/mqtt.py +++ b/homeassistant/components/cover/mqtt.py @@ -151,7 +151,7 @@ class MqttCover(CoverDevice): if self._optimistic: # Optimistically assume that cover has changed state. self._state = False - self.update_ha_state() + self.schedule_update_ha_state() def close_cover(self, **kwargs): """Move the cover down.""" @@ -160,7 +160,7 @@ class MqttCover(CoverDevice): if self._optimistic: # Optimistically assume that cover has changed state. self._state = True - self.update_ha_state() + self.schedule_update_ha_state() def stop_cover(self, **kwargs): """Stop the device.""" diff --git a/homeassistant/components/cover/mysensors.py b/homeassistant/components/cover/mysensors.py index a75ad36354b..7daadebadad 100644 --- a/homeassistant/components/cover/mysensors.py +++ b/homeassistant/components/cover/mysensors.py @@ -75,7 +75,7 @@ class MySensorsCover(mysensors.MySensorsDeviceEntity, CoverDevice): self._values[set_req.V_DIMMER] = 100 else: self._values[set_req.V_LIGHT] = STATE_ON - self.update_ha_state() + self.schedule_update_ha_state() def close_cover(self, **kwargs): """Move the cover down.""" @@ -88,7 +88,7 @@ class MySensorsCover(mysensors.MySensorsDeviceEntity, CoverDevice): self._values[set_req.V_DIMMER] = 0 else: self._values[set_req.V_LIGHT] = STATE_OFF - self.update_ha_state() + self.schedule_update_ha_state() def set_cover_position(self, **kwargs): """Move the cover to a specific position.""" @@ -99,7 +99,7 @@ class MySensorsCover(mysensors.MySensorsDeviceEntity, CoverDevice): if self.gateway.optimistic: # Optimistically assume that cover has changed state. self._values[set_req.V_DIMMER] = position - self.update_ha_state() + self.schedule_update_ha_state() def stop_cover(self, **kwargs): """Stop the device.""" diff --git a/homeassistant/components/cover/wink.py b/homeassistant/components/cover/wink.py index 264cec70a7e..79e2ee334dc 100644 --- a/homeassistant/components/cover/wink.py +++ b/homeassistant/components/cover/wink.py @@ -6,7 +6,7 @@ https://home-assistant.io/components/cover.wink/ """ from homeassistant.components.cover import CoverDevice -from homeassistant.components.wink import WinkDevice +from homeassistant.components.wink import WinkDevice, DOMAIN DEPENDENCIES = ['wink'] @@ -15,10 +15,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Wink cover platform.""" import pywink - add_devices(WinkCoverDevice(shade, hass) for shade in - pywink.get_shades()) - add_devices(WinkCoverDevice(door, hass) for door in - pywink.get_garage_doors()) + for shade in pywink.get_shades(): + _id = shade.object_id() + shade.name() + if _id not in hass.data[DOMAIN]['unique_ids']: + add_devices([WinkCoverDevice(shade, hass)]) + for door in pywink.get_garage_doors(): + _id = door.object_id() + door.name() + if _id not in hass.data[DOMAIN]['unique_ids']: + add_devices([WinkCoverDevice(door, hass)]) class WinkCoverDevice(WinkDevice, CoverDevice): @@ -26,7 +30,7 @@ class WinkCoverDevice(WinkDevice, CoverDevice): def __init__(self, wink, hass): """Initialize the cover.""" - WinkDevice.__init__(self, wink, hass) + super().__init__(wink, hass) def close_cover(self): """Close the shade.""" @@ -36,13 +40,17 @@ class WinkCoverDevice(WinkDevice, CoverDevice): """Open the shade.""" self.wink.set_state(1) + def set_cover_position(self, position, **kwargs): + """Move the roller shutter to a specific position.""" + self.wink.set_state(float(position)/100) + + @property + def current_cover_position(self): + """Return the current position of roller shutter.""" + return int(self.wink.state()*100) + @property def is_closed(self): """Return if the cover is closed.""" state = self.wink.state() - if state == 0: - return True - elif state == 1: - return False - else: - return None + return bool(state == 0) diff --git a/homeassistant/components/cover/zwave.py b/homeassistant/components/cover/zwave.py index d9d33942e15..0e40b46a3f9 100644 --- a/homeassistant/components/cover/zwave.py +++ b/homeassistant/components/cover/zwave.py @@ -52,12 +52,11 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice): def __init__(self, value): """Initialize the zwave rollershutter.""" - import libopenzwave ZWaveDeviceEntity.__init__(self, value, DOMAIN) # pylint: disable=no-member - self._lozwmgr = libopenzwave.PyManager() - self._lozwmgr.create() self._node = value.node + self._open_id = None + self._close_id = None self._current_position = None self._workaround = None if (value.node.manufacturer_id.strip() and @@ -73,12 +72,15 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice): def update_properties(self): """Callback on data changes for node values.""" # Position value - for value in self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values(): - if value.command_class == \ - zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and \ - value.label == 'Level': - self._current_position = value.data + self._current_position = self.get_value( + class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL, + label=['Level'], member='data') + self._open_id = self.get_value( + class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL, + label=['Open', 'Up'], member='value_id') + self._close_id = self.get_value( + class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL, + label=['Close', 'Down'], member='value_id') @property def is_closed(self): @@ -104,27 +106,11 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice): def open_cover(self, **kwargs): """Move the roller shutter up.""" - for value in self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values(): - if value.command_class == \ - zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \ - 'Open' or value.command_class == \ - zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \ - 'Up': - self._lozwmgr.pressButton(value.value_id) - break + zwave.NETWORK.manager.pressButton(self._open_id) def close_cover(self, **kwargs): """Move the roller shutter down.""" - for value in self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values(): - if value.command_class == \ - zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \ - 'Down' or value.command_class == \ - zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \ - 'Close': - self._lozwmgr.pressButton(value.value_id) - break + zwave.NETWORK.manager.pressButton(self._close_id) def set_cover_position(self, position, **kwargs): """Move the roller shutter to a specific position.""" @@ -132,15 +118,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice): def stop_cover(self, **kwargs): """Stop the roller shutter.""" - for value in self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values(): - if value.command_class == \ - zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \ - 'Open' or value.command_class == \ - zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \ - 'Down': - self._lozwmgr.releaseButton(value.value_id) - break + zwave.NETWORK.manager.releaseButton(self._open_id) class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, CoverDevice): diff --git a/homeassistant/components/device_sun_light_trigger.py b/homeassistant/components/device_sun_light_trigger.py index 9da4348362d..0a4d7173c20 100644 --- a/homeassistant/components/device_sun_light_trigger.py +++ b/homeassistant/components/device_sun_light_trigger.py @@ -4,6 +4,7 @@ Provides functionality to turn on lights based on the states. For more details about this component, please refer to the documentation at https://home-assistant.io/components/device_sun_light_trigger/ """ +import asyncio import logging from datetime import timedelta @@ -12,8 +13,8 @@ import voluptuous as vol from homeassistant.core import callback import homeassistant.util.dt as dt_util from homeassistant.const import STATE_HOME, STATE_NOT_HOME -from homeassistant.helpers.event import track_point_in_time -from homeassistant.helpers.event_decorators import track_state_change +from homeassistant.helpers.event import ( + async_track_point_in_time, async_track_state_change) from homeassistant.loader import get_component import homeassistant.helpers.config_validation as cv @@ -42,20 +43,20 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -def setup(hass, config): +@asyncio.coroutine +def async_setup(hass, config): """The triggers to turn lights on or off based on device presence.""" logger = logging.getLogger(__name__) device_tracker = get_component('device_tracker') group = get_component('group') light = get_component('light') sun = get_component('sun') - - disable_turn_off = config[DOMAIN].get(CONF_DISABLE_TURN_OFF) - light_group = config[DOMAIN].get(CONF_LIGHT_GROUP, - light.ENTITY_ID_ALL_LIGHTS) - light_profile = config[DOMAIN].get(CONF_LIGHT_PROFILE) - device_group = config[DOMAIN].get(CONF_DEVICE_GROUP, - device_tracker.ENTITY_ID_ALL_DEVICES) + conf = config[DOMAIN] + disable_turn_off = conf.get(CONF_DISABLE_TURN_OFF) + light_group = conf.get(CONF_LIGHT_GROUP, light.ENTITY_ID_ALL_LIGHTS) + light_profile = conf.get(CONF_LIGHT_PROFILE) + device_group = conf.get(CONF_DEVICE_GROUP, + device_tracker.ENTITY_ID_ALL_DEVICES) device_entity_ids = group.get_entity_ids(hass, device_group, device_tracker.DOMAIN) @@ -74,6 +75,8 @@ def setup(hass, config): """Calculate the time when to start fading lights in when sun sets. Returns None if no next_setting data available. + + Async friendly. """ next_setting = sun.next_setting(hass) if not next_setting: @@ -81,22 +84,26 @@ def setup(hass, config): return next_setting - LIGHT_TRANSITION_TIME * len(light_ids) def async_turn_on_before_sunset(light_id): - """Helper function to turn on lights. - - Speed is slow if there are devices home and the light is not on yet. - """ + """Helper function to turn on lights.""" if not device_tracker.is_on(hass) or light.is_on(hass, light_id): return light.async_turn_on(hass, light_id, transition=LIGHT_TRANSITION_TIME.seconds, profile=light_profile) + def async_turn_on_factory(light_id): + """Factory to generate turn on callbacks.""" + @callback + def async_turn_on_light(now): + """Turn on specific light.""" + async_turn_on_before_sunset(light_id) + + return async_turn_on_light + # Track every time sun rises so we can schedule a time-based # pre-sun set event - @track_state_change(sun.ENTITY_ID, sun.STATE_BELOW_HORIZON, - sun.STATE_ABOVE_HORIZON) @callback - def schedule_lights_at_sun_set(hass, entity, old_state, new_state): + def schedule_light_turn_on(entity, old_state, new_state): """The moment sun sets we want to have all the lights on. We will schedule to have each light start after one another @@ -106,35 +113,23 @@ def setup(hass, config): if not start_point: return - def async_turn_on_factory(light_id): - """Lambda can keep track of function parameters. - - No local parameters. If we put the lambda directly in the below - statement only the last light will be turned on. - """ - @callback - def async_turn_on_light(now): - """Turn on specific light.""" - async_turn_on_before_sunset(light_id) - - return async_turn_on_light - for index, light_id in enumerate(light_ids): - track_point_in_time(hass, async_turn_on_factory(light_id), - start_point + index * LIGHT_TRANSITION_TIME) + async_track_point_in_time( + hass, async_turn_on_factory(light_id), + start_point + index * LIGHT_TRANSITION_TIME) + + async_track_state_change(hass, sun.ENTITY_ID, schedule_light_turn_on, + sun.STATE_BELOW_HORIZON, sun.STATE_ABOVE_HORIZON) # If the sun is already above horizon schedule the time-based pre-sun set # event. if sun.is_on(hass): - schedule_lights_at_sun_set(hass, None, None, None) + schedule_light_turn_on(None, None, None) - @track_state_change(device_entity_ids, STATE_NOT_HOME, STATE_HOME) @callback - def check_light_on_dev_state_change(hass, entity, old_state, new_state): + def check_light_on_dev_state_change(entity, old_state, new_state): """Handle tracked device state changes.""" - # pylint: disable=unused-variable lights_are_on = group.is_on(hass, light_group) - light_needed = not (lights_are_on or sun.is_on(hass)) # These variables are needed for the elif check @@ -164,17 +159,25 @@ def setup(hass, config): # will all the following then, break. break - if not disable_turn_off: - @track_state_change(device_group, STATE_HOME, STATE_NOT_HOME) - @callback - def turn_off_lights_when_all_leave(hass, entity, old_state, new_state): - """Handle device group state change.""" - # pylint: disable=unused-variable - if not group.is_on(hass, light_group): - return + async_track_state_change( + hass, device_entity_ids, check_light_on_dev_state_change, + STATE_NOT_HOME, STATE_HOME) - logger.info( - "Everyone has left but there are lights on. Turning them off") - light.async_turn_off(hass, light_ids) + if disable_turn_off: + return True + + @callback + def turn_off_lights_when_all_leave(entity, old_state, new_state): + """Handle device group state change.""" + if not group.is_on(hass, light_group): + return + + logger.info( + "Everyone has left but there are lights on. Turning them off") + light.async_turn_off(hass, light_ids) + + async_track_state_change( + hass, device_group, turn_off_lights_when_all_leave, + STATE_HOME, STATE_NOT_HOME) return True diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 21e7c7b0da1..bb6730a562f 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -158,10 +158,11 @@ def async_setup(hass: HomeAssistantType, config: ConfigType): None, platform.get_scanner, hass, {DOMAIN: p_config}) elif hasattr(platform, 'async_setup_scanner'): setup = yield from platform.async_setup_scanner( - hass, p_config, tracker.async_see) + hass, p_config, tracker.async_see, disc_info) elif hasattr(platform, 'setup_scanner'): setup = yield from hass.loop.run_in_executor( - None, platform.setup_scanner, hass, p_config, tracker.see) + None, platform.setup_scanner, hass, p_config, tracker.see, + disc_info) else: raise HomeAssistantError("Invalid device_tracker platform.") @@ -193,6 +194,13 @@ def async_setup(hass: HomeAssistantType, config: ConfigType): discovery.async_listen( hass, DISCOVERY_PLATFORMS.keys(), async_device_tracker_discovered) + @asyncio.coroutine + def async_platform_discovered(platform, info): + """Callback to 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/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py index 512ccba0b74..e18d560e653 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -76,6 +76,7 @@ _IP_NEIGH_REGEX = re.compile( r'\w+\s' r'\w+\s' r'(\w+\s(?P(([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))))?\s' + r'\s?(router)?' r'(?P(\w+))') _NVRAM_CMD = 'nvram get client_info_tmp' diff --git a/homeassistant/components/device_tracker/automatic.py b/homeassistant/components/device_tracker/automatic.py index d47aa818673..3b4612edf6c 100644 --- a/homeassistant/components/device_tracker/automatic.py +++ b/homeassistant/components/device_tracker/automatic.py @@ -49,7 +49,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -def setup_scanner(hass, config: dict, see): +def setup_scanner(hass, config: dict, see, discovery_info=None): """Validate the configuration and return an Automatic scanner.""" try: AutomaticDeviceScanner(hass, config, see) diff --git a/homeassistant/components/device_tracker/bluetooth_le_tracker.py b/homeassistant/components/device_tracker/bluetooth_le_tracker.py index 454ab127af0..a4a933fe778 100644 --- a/homeassistant/components/device_tracker/bluetooth_le_tracker.py +++ b/homeassistant/components/device_tracker/bluetooth_le_tracker.py @@ -25,7 +25,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -def setup_scanner(hass, config, see): +def setup_scanner(hass, config, see, discovery_info=None): """Setup the Bluetooth LE Scanner.""" # pylint: disable=import-error from gattlib import DiscoveryService diff --git a/homeassistant/components/device_tracker/bluetooth_tracker.py b/homeassistant/components/device_tracker/bluetooth_tracker.py index a8b3861cdc5..1de0629c7c5 100644 --- a/homeassistant/components/device_tracker/bluetooth_tracker.py +++ b/homeassistant/components/device_tracker/bluetooth_tracker.py @@ -21,7 +21,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -def setup_scanner(hass, config, see): +def setup_scanner(hass, config, see, discovery_info=None): """Setup the Bluetooth Scanner.""" # pylint: disable=import-error import bluetooth diff --git a/homeassistant/components/device_tracker/demo.py b/homeassistant/components/device_tracker/demo.py index 08242c2034d..dfd50a2b991 100644 --- a/homeassistant/components/device_tracker/demo.py +++ b/homeassistant/components/device_tracker/demo.py @@ -4,7 +4,7 @@ import random from homeassistant.components.device_tracker import DOMAIN -def setup_scanner(hass, config, see): +def setup_scanner(hass, config, see, discovery_info=None): """Setup the demo tracker.""" def offset(): """Return random offset.""" diff --git a/homeassistant/components/device_tracker/gpslogger.py b/homeassistant/components/device_tracker/gpslogger.py index 22099630bd1..c76c8fdd51b 100644 --- a/homeassistant/components/device_tracker/gpslogger.py +++ b/homeassistant/components/device_tracker/gpslogger.py @@ -19,7 +19,7 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['http'] -def setup_scanner(hass, config, see): +def setup_scanner(hass, config, see, discovery_info=None): """Setup an endpoint for the GPSLogger application.""" hass.http.register_view(GPSLoggerView(see)) diff --git a/homeassistant/components/device_tracker/icloud.py b/homeassistant/components/device_tracker/icloud.py index 0878f8b005b..f6396ba7c34 100644 --- a/homeassistant/components/device_tracker/icloud.py +++ b/homeassistant/components/device_tracker/icloud.py @@ -71,7 +71,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -def setup_scanner(hass, config: dict, see): +def setup_scanner(hass, config: dict, see, discovery_info=None): """Set up the iCloud Scanner.""" username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) diff --git a/homeassistant/components/device_tracker/locative.py b/homeassistant/components/device_tracker/locative.py index 32eb033a284..75cebbd95e7 100644 --- a/homeassistant/components/device_tracker/locative.py +++ b/homeassistant/components/device_tracker/locative.py @@ -20,7 +20,7 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['http'] -def setup_scanner(hass, config, see): +def setup_scanner(hass, config, see, discovery_info=None): """Setup an endpoint for the Locative application.""" hass.http.register_view(LocativeView(see)) diff --git a/homeassistant/components/device_tracker/mqtt.py b/homeassistant/components/device_tracker/mqtt.py index f9a85da98b2..a93263fada9 100644 --- a/homeassistant/components/device_tracker/mqtt.py +++ b/homeassistant/components/device_tracker/mqtt.py @@ -23,7 +23,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend({ }) -def setup_scanner(hass, config, see): +def setup_scanner(hass, config, see, discovery_info=None): """Setup the MQTT tracker.""" devices = config[CONF_DEVICES] qos = config[CONF_QOS] diff --git a/homeassistant/components/device_tracker/mysensors.py b/homeassistant/components/device_tracker/mysensors.py new file mode 100644 index 00000000000..2ba19924089 --- /dev/null +++ b/homeassistant/components/device_tracker/mysensors.py @@ -0,0 +1,60 @@ +""" +Support for tracking MySensors devices. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.mysensors/ +""" +import logging + +from homeassistant.components import mysensors +from homeassistant.util import slugify + +DEPENDENCIES = ['mysensors'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_scanner(hass, config, see, discovery_info=None): + """Setup the MySensors tracker.""" + def mysensors_callback(gateway, node_id): + """Callback for mysensors platform.""" + node = gateway.sensors[node_id] + if node.sketch_name is None: + _LOGGER.info('No sketch_name: node %s', node_id) + return + + pres = gateway.const.Presentation + set_req = gateway.const.SetReq + + for child in node.children.values(): + position = child.values.get(set_req.V_POSITION) + if child.type != pres.S_GPS or position is None: + continue + try: + latitude, longitude, _ = position.split(',') + except ValueError: + _LOGGER.error('Payload for V_POSITION %s is not of format ' + 'latitude,longitude,altitude', position) + continue + name = '{} {} {}'.format( + node.sketch_name, node_id, child.id) + attr = { + mysensors.ATTR_CHILD_ID: child.id, + mysensors.ATTR_DESCRIPTION: child.description, + mysensors.ATTR_DEVICE: gateway.device, + mysensors.ATTR_NODE_ID: node_id, + } + see( + dev_id=slugify(name), + host_name=name, + gps=(latitude, longitude), + battery=node.battery_level, + attributes=attr + ) + + gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS) + + for gateway in gateways: + gateway.platform_callbacks.append(mysensors_callback) + + return True diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index 44ae0359ff6..c03041b6317 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -71,7 +71,7 @@ def get_cipher(): return (KEYLEN, decrypt) -def setup_scanner(hass, config, see): +def setup_scanner(hass, config, 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) @@ -190,7 +190,7 @@ def setup_scanner(hass, config, see): return # OwnTracks uses - at the start of a beacon zone # to switch on 'hold mode' - ignore this - location = slugify(data['desc'].lstrip("-")) + location = data['desc'].lstrip("-") if location.lower() == 'home': location = STATE_HOME @@ -198,7 +198,7 @@ def setup_scanner(hass, config, see): def enter_event(): """Execute enter event.""" - zone = hass.states.get("zone.{}".format(location)) + zone = hass.states.get("zone.{}".format(slugify(location))) with LOCK: if zone is None and data.get('t') == 'b': # Not a HA zone, and a beacon so assume mobile @@ -227,7 +227,8 @@ def setup_scanner(hass, config, see): if new_region: # Exit to previous region - zone = hass.states.get("zone.{}".format(new_region)) + zone = hass.states.get( + "zone.{}".format(slugify(new_region))) _set_gps_from_zone(kwargs, new_region, zone) _LOGGER.info("Exit to %s", new_region) see(**kwargs) diff --git a/homeassistant/components/device_tracker/ping.py b/homeassistant/components/device_tracker/ping.py index 9c64d37f820..2af400ba89c 100644 --- a/homeassistant/components/device_tracker/ping.py +++ b/homeassistant/components/device_tracker/ping.py @@ -19,7 +19,7 @@ from datetime import timedelta import voluptuous as vol from homeassistant.components.device_tracker import ( - PLATFORM_SCHEMA, DEFAULT_SCAN_INTERVAL) + PLATFORM_SCHEMA, DEFAULT_SCAN_INTERVAL, SOURCE_TYPE_ROUTER) from homeassistant.helpers.event import track_point_in_utc_time from homeassistant import util from homeassistant import const @@ -66,14 +66,14 @@ class Host: failed = 0 while failed < self._count: # check more times if host in unreachable if self.ping(): - see(dev_id=self.dev_id) + see(dev_id=self.dev_id, source_type=SOURCE_TYPE_ROUTER) return True failed += 1 _LOGGER.debug("ping KO on ip=%s failed=%d", self.ip_address, failed) -def setup_scanner(hass, config, see): +def setup_scanner(hass, config, see, discovery_info=None): """Setup the Host objects and return the update function.""" hosts = [Host(ip, dev_id, hass, config) for (dev_id, ip) in config[const.CONF_HOSTS].items()] diff --git a/homeassistant/components/device_tracker/sky_hub.py b/homeassistant/components/device_tracker/sky_hub.py index 647731d8485..ef58c50991c 100644 --- a/homeassistant/components/device_tracker/sky_hub.py +++ b/homeassistant/components/device_tracker/sky_hub.py @@ -30,7 +30,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument def get_scanner(hass, config): - """Return a Sky Hub 5 scanner if successful.""" + """Return a Sky Hub scanner if successful.""" scanner = SkyHubDeviceScanner(config[DOMAIN]) return scanner if scanner.success_init else None @@ -111,6 +111,9 @@ def _get_skyhub_data(url): def _parse_skyhub_response(data_str): """Parse the Sky Hub data format.""" pattmatch = re.search('attach_dev = \'(.*)\'', data_str) + if pattmatch is None: + raise IOError('Error: Impossible to fetch data from' + + ' Sky Hub. Try to reboot the router.') patt = pattmatch.group(1) dev = [patt1.split(',') for patt1 in patt.split('')] diff --git a/homeassistant/components/device_tracker/snmp.py b/homeassistant/components/device_tracker/snmp.py index 43c0662e568..4cbaa557517 100644 --- a/homeassistant/components/device_tracker/snmp.py +++ b/homeassistant/components/device_tracker/snmp.py @@ -17,24 +17,24 @@ from homeassistant.components.device_tracker import ( from homeassistant.const import CONF_HOST from homeassistant.util import Throttle -# Return cached results if last scan was less then this time ago. -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) - _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pysnmp==4.3.2'] -CONF_COMMUNITY = "community" -CONF_AUTHKEY = "authkey" -CONF_PRIVKEY = "privkey" -CONF_BASEOID = "baseoid" +REQUIREMENTS = ['pysnmp==4.3.3'] -DEFAULT_COMMUNITY = "public" +CONF_COMMUNITY = 'community' +CONF_AUTHKEY = 'authkey' +CONF_PRIVKEY = 'privkey' +CONF_BASEOID = 'baseoid' + +DEFAULT_COMMUNITY = 'public' + +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_COMMUNITY, default=DEFAULT_COMMUNITY): cv.string, - vol.Inclusive(CONF_AUTHKEY, "keys"): cv.string, - vol.Inclusive(CONF_PRIVKEY, "keys"): cv.string, + vol.Inclusive(CONF_AUTHKEY, 'keys'): cv.string, + vol.Inclusive(CONF_PRIVKEY, 'keys'): cv.string, vol.Required(CONF_BASEOID): cv.string }) @@ -119,14 +119,14 @@ class SnmpScanner(DeviceScanner): return # pylint: disable=no-member if errstatus: - _LOGGER.error('SNMP error: %s at %s', errstatus.prettyPrint(), + _LOGGER.error("SNMP error: %s at %s", errstatus.prettyPrint(), errindex and restable[int(errindex) - 1][0] or '?') return for resrow in restable: for _, val in resrow: mac = binascii.hexlify(val.asOctets()).decode('utf-8') - _LOGGER.debug('Found mac %s', mac) + _LOGGER.debug("Found MAC %s", mac) mac = ':'.join([mac[i:i+2] for i in range(0, len(mac), 2)]) devices.append({'mac': mac}) return devices diff --git a/homeassistant/components/device_tracker/trackr.py b/homeassistant/components/device_tracker/trackr.py index 2eb0def278f..cf66fd33272 100644 --- a/homeassistant/components/device_tracker/trackr.py +++ b/homeassistant/components/device_tracker/trackr.py @@ -23,7 +23,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -def setup_scanner(hass, config: dict, see): +def setup_scanner(hass, config: dict, see, discovery_info=None): """Validate the configuration and return a TrackR scanner.""" TrackRDeviceScanner(hass, config, see) return True diff --git a/homeassistant/components/device_tracker/volvooncall.py b/homeassistant/components/device_tracker/volvooncall.py index 1be76d6139c..834ec7e55bd 100644 --- a/homeassistant/components/device_tracker/volvooncall.py +++ b/homeassistant/components/device_tracker/volvooncall.py @@ -30,7 +30,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -def setup_scanner(hass, config, see): +def setup_scanner(hass, config, see, discovery_info=None): """Validate the configuration and return a scanner.""" from volvooncall import Connection connection = Connection( diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 32d57b4bf85..45a3944e6fe 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -14,7 +14,7 @@ import voluptuous as vol from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.helpers.discovery import load_platform, discover -REQUIREMENTS = ['netdisco==0.8.1'] +REQUIREMENTS = ['netdisco==0.8.2'] DOMAIN = 'discovery' @@ -39,6 +39,8 @@ SERVICE_HANDLERS = { 'denonavr': ('media_player', 'denonavr'), 'samsung_tv': ('media_player', 'samsungtv'), 'yeelight': ('light', 'yeelight'), + 'flux_led': ('light', 'flux_led'), + 'apple_tv': ('media_player', 'apple_tv'), } CONFIG_SCHEMA = vol.Schema({ diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index b56be3484fe..7ff174b32b6 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -8,14 +8,13 @@ from homeassistant import core from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_SET, SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, STATE_ON, STATE_OFF, - HTTP_BAD_REQUEST, HTTP_NOT_FOUND, + HTTP_BAD_REQUEST, HTTP_NOT_FOUND, ATTR_SUPPORTED_FEATURES, ) from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS + ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS ) from homeassistant.components.media_player import ( - ATTR_MEDIA_VOLUME_LEVEL, ATTR_SUPPORTED_MEDIA_COMMANDS, - SUPPORT_VOLUME_SET, + ATTR_MEDIA_VOLUME_LEVEL, SUPPORT_VOLUME_SET, ) from homeassistant.components.fan import ( ATTR_SPEED, SUPPORT_SET_SPEED, SPEED_OFF, SPEED_LOW, @@ -178,11 +177,10 @@ class HueOneLightChangeView(HomeAssistantView): # Make sure the entity actually supports brightness entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - if (entity_features & - SUPPORT_BRIGHTNESS & - (entity.domain == "light")) == SUPPORT_BRIGHTNESS: - if brightness is not None: - data[ATTR_BRIGHTNESS] = brightness + if entity.domain == "light": + if entity_features & SUPPORT_BRIGHTNESS: + if brightness is not None: + data[ATTR_BRIGHTNESS] = brightness # If the requested entity is a script add some variables elif entity.domain == "script": @@ -195,9 +193,7 @@ class HueOneLightChangeView(HomeAssistantView): # If the requested entity is a media player, convert to volume elif entity.domain == "media_player": - media_commands = entity.attributes.get( - ATTR_SUPPORTED_MEDIA_COMMANDS, 0) - if media_commands & SUPPORT_VOLUME_SET == SUPPORT_VOLUME_SET: + if entity_features & SUPPORT_VOLUME_SET: if brightness is not None: turn_on_needed = True domain = entity.domain @@ -215,9 +211,7 @@ class HueOneLightChangeView(HomeAssistantView): # If the requested entity is a fan, convert to speed elif entity.domain == "fan": - functions = entity.attributes.get( - ATTR_SUPPORTED_FEATURES, 0) - if (functions & SUPPORT_SET_SPEED) == SUPPORT_SET_SPEED: + if entity_features & SUPPORT_SET_SPEED: if brightness is not None: domain = entity.domain # Convert 0-100 to a fan speed @@ -288,9 +282,10 @@ def parse_hue_api_put_light_body(request_json, entity): # Make sure the entity actually supports brightness entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - if (entity_features & SUPPORT_BRIGHTNESS) == SUPPORT_BRIGHTNESS: - report_brightness = True - result = (brightness > 0) + if entity.domain == "light": + if entity_features & SUPPORT_BRIGHTNESS: + report_brightness = True + result = (brightness > 0) elif (entity.domain == "script" or entity.domain == "media_player" or @@ -316,8 +311,9 @@ def get_entity_state(config, entity): # Make sure the entity actually supports brightness entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - if (entity_features & SUPPORT_BRIGHTNESS) == SUPPORT_BRIGHTNESS: - pass + if entity.domain == "light": + if entity_features & SUPPORT_BRIGHTNESS: + pass elif entity.domain == "media_player": level = entity.attributes.get( diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index e6da2ff0fd7..dd8dda36121 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -4,7 +4,9 @@ Provides functionality to interact with fans. For more details about this component, please refer to the documentation at https://home-assistant.io/components/fan/ """ +import asyncio from datetime import timedelta +import functools as ft import logging import os @@ -24,13 +26,14 @@ import homeassistant.helpers.config_validation as cv DOMAIN = 'fan' SCAN_INTERVAL = timedelta(seconds=30) +_LOGGER = logging.getLogger(__name__) + GROUP_NAME_ALL_FANS = 'all fans' ENTITY_ID_ALL_FANS = group.ENTITY_ID_FORMAT.format(GROUP_NAME_ALL_FANS) ENTITY_ID_FORMAT = DOMAIN + '.{}' # Bitfield of features supported by the fan entity -ATTR_SUPPORTED_FEATURES = 'supported_features' SUPPORT_SET_SPEED = 1 SUPPORT_OSCILLATE = 2 SUPPORT_DIRECTION = 4 @@ -56,7 +59,6 @@ PROP_TO_ATTR = { 'speed': ATTR_SPEED, 'speed_list': ATTR_SPEED_LIST, 'oscillating': ATTR_OSCILLATING, - 'supported_features': ATTR_SUPPORTED_FEATURES, 'direction': ATTR_DIRECTION, } # type: dict @@ -88,7 +90,32 @@ FAN_SET_DIRECTION_SCHEMA = vol.Schema({ vol.Optional(ATTR_DIRECTION): cv.string }) # type: dict -_LOGGER = logging.getLogger(__name__) +SERVICE_TO_METHOD = { + SERVICE_TURN_ON: { + 'method': 'async_turn_on', + 'schema': FAN_TURN_ON_SCHEMA, + }, + SERVICE_TURN_OFF: { + 'method': 'async_turn_off', + 'schema': FAN_TURN_OFF_SCHEMA, + }, + SERVICE_TOGGLE: { + 'method': 'async_toggle', + 'schema': FAN_TOGGLE_SCHEMA, + }, + SERVICE_SET_SPEED: { + 'method': 'async_set_speed', + 'schema': FAN_SET_SPEED_SCHEMA, + }, + SERVICE_OSCILLATE: { + 'method': 'async_oscillate', + 'schema': FAN_OSCILLATE_SCHEMA, + }, + SERVICE_SET_DIRECTION: { + 'method': 'async_set_direction', + 'schema': FAN_SET_DIRECTION_SCHEMA, + }, +} def is_on(hass, entity_id: str=None) -> bool: @@ -164,60 +191,53 @@ def set_direction(hass, entity_id: str=None, direction: str=None) -> None: hass.services.call(DOMAIN, SERVICE_SET_DIRECTION, data) -def setup(hass, config: dict) -> None: +@asyncio.coroutine +def async_setup(hass, config: dict): """Expose fan control via statemachine and services.""" component = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_FANS) - component.setup(config) - def handle_fan_service(service: str) -> None: + yield from component.async_setup(config) + + @asyncio.coroutine + def async_handle_fan_service(service): """Hande service call for fans.""" - # Get the validated data + method = SERVICE_TO_METHOD.get(service.service) params = service.data.copy() # Convert the entity ids to valid fan ids - target_fans = component.extract_from_service(service) + target_fans = component.async_extract_from_service(service) params.pop(ATTR_ENTITY_ID, None) - service_fun = None - for service_def in [SERVICE_TURN_ON, SERVICE_TURN_OFF, - SERVICE_SET_SPEED, SERVICE_OSCILLATE, - SERVICE_SET_DIRECTION]: - if service_def == service.service: - service_fun = service_def - break + for fan in target_fans: + yield from getattr(fan, method['method'])(**params) - if service_fun: - for fan in target_fans: - getattr(fan, service_fun)(**params) + update_tasks = [] - for fan in target_fans: - if fan.should_poll: - fan.update_ha_state(True) - return + for fan in target_fans: + if not fan.should_poll: + continue + + update_coro = hass.loop.create_task( + fan.async_update_ha_state(True)) + if hasattr(fan, 'async_update'): + update_tasks.append(update_coro) + else: + yield from update_coro + + if update_tasks: + yield from asyncio.wait(update_tasks, loop=hass.loop) # Listen for fan service calls. - descriptions = load_yaml_config_file( - os.path.join(os.path.dirname(__file__), 'services.yaml')) - hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_fan_service, - descriptions.get(SERVICE_TURN_ON), - schema=FAN_TURN_ON_SCHEMA) + descriptions = yield from hass.loop.run_in_executor( + None, load_yaml_config_file, os.path.join( + os.path.dirname(__file__), 'services.yaml')) - hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_fan_service, - descriptions.get(SERVICE_TURN_OFF), - schema=FAN_TURN_OFF_SCHEMA) - - hass.services.register(DOMAIN, SERVICE_SET_SPEED, handle_fan_service, - descriptions.get(SERVICE_SET_SPEED), - schema=FAN_SET_SPEED_SCHEMA) - - hass.services.register(DOMAIN, SERVICE_OSCILLATE, handle_fan_service, - descriptions.get(SERVICE_OSCILLATE), - schema=FAN_OSCILLATE_SCHEMA) - - hass.services.register(DOMAIN, SERVICE_SET_DIRECTION, handle_fan_service, - descriptions.get(SERVICE_SET_DIRECTION), - schema=FAN_SET_DIRECTION_SCHEMA) + for service_name in SERVICE_TO_METHOD: + schema = SERVICE_TO_METHOD[service_name].get('schema') + hass.services.async_register( + DOMAIN, service_name, async_handle_fan_service, + descriptions.get(service_name), schema=schema) return True @@ -225,34 +245,57 @@ def setup(hass, config: dict) -> None: class FanEntity(ToggleEntity): """Representation of a fan.""" - # pylint: disable=no-self-use - def set_speed(self: ToggleEntity, speed: str) -> None: """Set the speed of the fan.""" - if speed is SPEED_OFF: - self.turn_off() - return raise NotImplementedError() + def async_set_speed(self: ToggleEntity, speed: str): + """Set the speed of the fan. + + This method must be run in the event loop and returns a coroutine. + """ + if speed is SPEED_OFF: + return self.async_turn_off() + return self.hass.loop.run_in_executor(None, self.set_speed, speed) + def set_direction(self: ToggleEntity, direction: str) -> None: """Set the direction of the fan.""" raise NotImplementedError() + def async_set_direction(self: ToggleEntity, direction: str): + """Set the direction of the fan. + + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.loop.run_in_executor( + None, self.set_direction, direction) + def turn_on(self: ToggleEntity, speed: str=None, **kwargs) -> None: """Turn on the fan.""" - if speed is SPEED_OFF: - self.turn_off() - return raise NotImplementedError() - def turn_off(self: ToggleEntity, **kwargs) -> None: - """Turn off the fan.""" - raise NotImplementedError() + def async_turn_on(self: ToggleEntity, speed: str=None, **kwargs): + """Turn on the fan. + + This method must be run in the event loop and returns a coroutine. + """ + if speed is SPEED_OFF: + return self.async_turn_off() + return self.hass.loop.run_in_executor( + None, ft.partial(self.turn_on, speed, **kwargs)) def oscillate(self: ToggleEntity, oscillating: bool) -> None: """Oscillate the fan.""" pass + def async_oscillate(self: ToggleEntity, oscillating: bool): + """Oscillate the fan. + + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.loop.run_in_executor( + None, self.oscillate, oscillating) + @property def is_on(self): """Return true if the entity is on.""" diff --git a/homeassistant/components/fan/demo.py b/homeassistant/components/fan/demo.py index 6d24f8d3048..931f4914552 100644 --- a/homeassistant/components/fan/demo.py +++ b/homeassistant/components/fan/demo.py @@ -56,8 +56,10 @@ class DemoFan(FanEntity): """Get the list of available speeds.""" return [STATE_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] - def turn_on(self, speed: str=SPEED_MEDIUM) -> None: + def turn_on(self, speed: str=None) -> None: """Turn on the entity.""" + if speed is None: + speed = SPEED_MEDIUM self.set_speed(speed) def turn_off(self) -> None: @@ -68,17 +70,17 @@ class DemoFan(FanEntity): def set_speed(self, speed: str) -> None: """Set the speed of the fan.""" self._speed = speed - self.update_ha_state() + self.schedule_update_ha_state() def set_direction(self, direction: str) -> None: """Set the direction of the fan.""" self.direction = direction - self.update_ha_state() + self.schedule_update_ha_state() def oscillate(self, oscillating: bool) -> None: """Set oscillation.""" self.oscillating = oscillating - self.update_ha_state() + self.schedule_update_ha_state() @property def current_direction(self) -> str: diff --git a/homeassistant/components/fan/isy994.py b/homeassistant/components/fan/isy994.py index 30c1d2ed2a3..f8c7b2e867d 100644 --- a/homeassistant/components/fan/isy994.py +++ b/homeassistant/components/fan/isy994.py @@ -29,7 +29,7 @@ STATE_TO_VALUE = {} for key in VALUE_TO_STATE: STATE_TO_VALUE[VALUE_TO_STATE[key]] = key -STATES = [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] +STATES = [SPEED_OFF, SPEED_LOW, 'med', SPEED_HIGH] # pylint: disable=unused-argument diff --git a/homeassistant/components/fan/mqtt.py b/homeassistant/components/fan/mqtt.py index 4ca1fc8bae4..1d5b7609897 100644 --- a/homeassistant/components/fan/mqtt.py +++ b/homeassistant/components/fan/mqtt.py @@ -150,7 +150,7 @@ class MqttFan(FanEntity): elif payload == self._payload[STATE_OFF]: self._state = False - self.update_ha_state() + self.schedule_update_ha_state() if self._topic[CONF_STATE_TOPIC] is not None: mqtt.subscribe(self._hass, self._topic[CONF_STATE_TOPIC], @@ -165,7 +165,7 @@ class MqttFan(FanEntity): self._speed = SPEED_MEDIUM elif payload == self._payload[SPEED_HIGH]: self._speed = SPEED_HIGH - self.update_ha_state() + self.schedule_update_ha_state() if self._topic[CONF_SPEED_STATE_TOPIC] is not None: mqtt.subscribe(self._hass, self._topic[CONF_SPEED_STATE_TOPIC], @@ -183,7 +183,7 @@ class MqttFan(FanEntity): self._oscillation = True elif payload == self._payload[OSCILLATE_OFF_PAYLOAD]: self._oscillation = False - self.update_ha_state() + self.schedule_update_ha_state() if self._topic[CONF_OSCILLATION_STATE_TOPIC] is not None: mqtt.subscribe(self._hass, @@ -262,7 +262,7 @@ class MqttFan(FanEntity): self._speed = speed mqtt.publish(self._hass, self._topic[CONF_SPEED_COMMAND_TOPIC], mqtt_payload, self._qos, self._retain) - self.update_ha_state() + self.schedule_update_ha_state() def oscillate(self, oscillating: bool) -> None: """Set oscillation.""" @@ -274,4 +274,4 @@ class MqttFan(FanEntity): mqtt.publish(self._hass, self._topic[CONF_OSCILLATION_COMMAND_TOPIC], payload, self._qos, self._retain) - self.update_ha_state() + self.schedule_update_ha_state() diff --git a/homeassistant/components/fan/wink.py b/homeassistant/components/fan/wink.py index 74fd06e5516..00eb082212d 100644 --- a/homeassistant/components/fan/wink.py +++ b/homeassistant/components/fan/wink.py @@ -10,7 +10,7 @@ from homeassistant.components.fan import (FanEntity, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, STATE_UNKNOWN) from homeassistant.helpers.entity import ToggleEntity -from homeassistant.components.wink import WinkDevice +from homeassistant.components.wink import WinkDevice, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -22,7 +22,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Wink platform.""" import pywink - add_devices(WinkFanDevice(fan, hass) for fan in pywink.get_fans()) + for fan in pywink.get_fans(): + if fan.object_id() + fan.name() not in hass.data[DOMAIN]['unique_ids']: + add_devices([WinkFanDevice(fan, hass)]) class WinkFanDevice(WinkDevice, FanEntity): @@ -30,7 +32,7 @@ class WinkFanDevice(WinkDevice, FanEntity): def __init__(self, wink, hass): """Initialize the fan.""" - WinkDevice.__init__(self, wink, hass) + super().__init__(wink, hass) def set_direction(self: ToggleEntity, direction: str) -> None: """Set the direction of the fan.""" diff --git a/homeassistant/components/ffmpeg.py b/homeassistant/components/ffmpeg.py index 2a498198e3c..64a81687fd3 100644 --- a/homeassistant/components/ffmpeg.py +++ b/homeassistant/components/ffmpeg.py @@ -6,18 +6,29 @@ https://home-assistant.io/components/ffmpeg/ """ import asyncio import logging +import os import voluptuous as vol +from homeassistant.core import callback +from homeassistant.const import ( + ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) +from homeassistant.config import load_yaml_config_file import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity DOMAIN = 'ffmpeg' -REQUIREMENTS = ["ha-ffmpeg==1.2"] +REQUIREMENTS = ["ha-ffmpeg==1.4"] _LOGGER = logging.getLogger(__name__) +SERVICE_START = 'start' +SERVICE_STOP = 'stop' +SERVICE_RESTART = 'restart' + DATA_FFMPEG = 'ffmpeg' +CONF_INITIAL_STATE = 'initial_state' CONF_INPUT = 'input' CONF_FFMPEG_BIN = 'ffmpeg_bin' CONF_EXTRA_ARGUMENTS = 'extra_arguments' @@ -34,18 +45,89 @@ CONFIG_SCHEMA = vol.Schema({ }), }, extra=vol.ALLOW_EXTRA) +SERVICE_FFMPEG_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, +}) + + +def start(hass, entity_id=None): + """Start a ffmpeg process on entity.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_START, data) + + +def stop(hass, entity_id=None): + """Stop a ffmpeg process on entity.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_STOP, data) + + +def restart(hass, entity_id=None): + """Restart a ffmpeg process on entity.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_RESTART, data) + @asyncio.coroutine def async_setup(hass, config): """Setup the FFmpeg component.""" conf = config.get(DOMAIN, {}) - hass.data[DATA_FFMPEG] = FFmpegManager( + manager = FFmpegManager( hass, conf.get(CONF_FFMPEG_BIN, DEFAULT_BINARY), conf.get(CONF_RUN_TEST, DEFAULT_RUN_TEST) ) + descriptions = yield from hass.loop.run_in_executor( + None, load_yaml_config_file, + os.path.join(os.path.dirname(__file__), 'services.yaml')) + + # register service + @asyncio.coroutine + def async_service_handle(service): + """Handle service ffmpeg process.""" + entity_ids = service.data.get(ATTR_ENTITY_ID) + + if entity_ids: + devices = [device for device in manager.entities + if device.entity_id in entity_ids] + else: + devices = manager.entities + + tasks = [] + for device in devices: + if service.service == SERVICE_START: + tasks.append(device.async_start_ffmpeg()) + elif service.service == SERVICE_STOP: + tasks.append(device.async_stop_ffmpeg()) + else: + tasks.append(device.async_restart_ffmpeg()) + + if tasks: + yield from asyncio.wait(tasks, loop=hass.loop) + + tasks.clear() + for device in devices: + tasks.append(device.async_update_ha_state()) + + if tasks: + yield from asyncio.wait(tasks, loop=hass.loop) + + hass.services.async_register( + DOMAIN, SERVICE_START, async_service_handle, + descriptions[DOMAIN].get(SERVICE_START), schema=SERVICE_FFMPEG_SCHEMA) + + hass.services.async_register( + DOMAIN, SERVICE_STOP, async_service_handle, + descriptions[DOMAIN].get(SERVICE_STOP), schema=SERVICE_FFMPEG_SCHEMA) + + hass.services.async_register( + DOMAIN, SERVICE_RESTART, async_service_handle, + descriptions[DOMAIN].get(SERVICE_RESTART), + schema=SERVICE_FFMPEG_SCHEMA) + + hass.data[DATA_FFMPEG] = manager return True @@ -58,12 +140,42 @@ class FFmpegManager(object): self._cache = {} self._bin = ffmpeg_bin self._run_test = run_test + self._entities = [] @property def binary(self): """Return ffmpeg binary from config.""" return self._bin + @property + def entities(self): + """Return ffmpeg entities for services.""" + return self._entities + + @callback + def async_register_device(self, device): + """Register a ffmpeg process/device.""" + self._entities.append(device) + + @asyncio.coroutine + def async_shutdown(event): + """Stop ffmpeg process.""" + yield from device.async_stop_ffmpeg() + + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, async_shutdown) + + # start on startup + if device.initial_state: + @asyncio.coroutine + def async_start(event): + """Start ffmpeg process.""" + yield from device.async_start_ffmpeg() + yield from device.async_update_ha_state() + + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, async_start) + @asyncio.coroutine def async_run_test(self, input_source): """Run test on this input. TRUE is deactivate or run correct. @@ -86,3 +198,42 @@ class FFmpegManager(object): return False self._cache[input_source] = True return True + + +class FFmpegBase(Entity): + """Interface object for ffmpeg.""" + + def __init__(self, initial_state=True): + """Initialize ffmpeg base object.""" + self.ffmpeg = None + self.initial_state = initial_state + + @property + def available(self): + """Return True if entity is available.""" + return self.ffmpeg.is_running + + @property + def should_poll(self): + """Return True if entity has to be polled for state.""" + return False + + def async_start_ffmpeg(self): + """Start a ffmpeg process. + + This method must be run in the event loop and returns a coroutine. + """ + raise NotImplementedError() + + def async_stop_ffmpeg(self): + """Stop a ffmpeg process. + + This method must be run in the event loop and returns a coroutine. + """ + return self.ffmpeg.close() + + @asyncio.coroutine + def async_restart_ffmpeg(self): + """Stop a ffmpeg process.""" + yield from self.async_stop_ffmpeg() + yield from self.async_start_ffmpeg() diff --git a/homeassistant/components/frontend/templates/index.html b/homeassistant/components/frontend/templates/index.html index a13d640a579..858afc696b6 100644 --- a/homeassistant/components/frontend/templates/index.html +++ b/homeassistant/components/frontend/templates/index.html @@ -9,6 +9,7 @@ + {% for panel in panels.values() -%} {% endfor -%} @@ -24,35 +25,28 @@
- - - Home Assistant had trouble
connecting to the server.

TRY AGAIN +
+ Home Assistant had trouble
connecting to the server.

+ TRY AGAIN +
{# #} diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 12566881be8..40cda09ae24 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -1,18 +1,18 @@ """DO NOT MODIFY. Auto-generated by script/fingerprint_frontend.""" FINGERPRINTS = { - "core.js": "769f3fdd4e04b34bd66c7415743cf7b5", - "frontend.html": "d48d9a13f7d677e59b1d22c6db051207", - "mdi.html": "7a0f14bbf3822449f9060b9c53bd7376", + "core.js": "adfeb513cf650acf763e284d76a48d6b", + "frontend.html": "43340b2369646b779e04a9925c225ab4", + "mdi.html": "c1dde43ccf5667f687c418fc8daf9668", "micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a", - "panels/ha-panel-dev-event.html": "f19840b9a6a46f57cb064b384e1353f5", - "panels/ha-panel-dev-info.html": "3765a371478cc66d677cf6dcc35267c6", - "panels/ha-panel-dev-service.html": "1d223225c1c75083738033895ea3e4b5", - "panels/ha-panel-dev-state.html": "8257d99a38358a150eafdb23fa6727e0", - "panels/ha-panel-dev-template.html": "cbb251acabd5e7431058ed507b70522b", - "panels/ha-panel-history.html": "9f2c72574fb6135beb1b381a4b8b7703", + "panels/ha-panel-dev-event.html": "5c82300b3cf543a92cf4297506e450e7", + "panels/ha-panel-dev-info.html": "0469024d94d6270a8680df2be44ba916", + "panels/ha-panel-dev-service.html": "9f749635e518a4ca7991975bdefdb10a", + "panels/ha-panel-dev-state.html": "7d069ba8fd5379fa8f59858b8c0a7473", + "panels/ha-panel-dev-template.html": "2b618508510afa5281c9ecae0c3a3dbd", + "panels/ha-panel-history.html": "8955c1d093a2c417c89ed90dd627c7d3", "panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab", - "panels/ha-panel-logbook.html": "313f2ac57aaa5ad55933c9bbf8d8a1e5", - "panels/ha-panel-map.html": "13f120066c0b5faa2ce1db2c3f3cc486", + "panels/ha-panel-logbook.html": "f36297a894524518fa70883f264492b0", + "panels/ha-panel-map.html": "9c8c7924ba8f731560c9f4093835cc26", "websocket_test.html": "575de64b431fe11c3785bf96d7813450" } diff --git a/homeassistant/components/frontend/www_static/core.js b/homeassistant/components/frontend/www_static/core.js index f3679c981d7..53db7214c5a 100644 --- a/homeassistant/components/frontend/www_static/core.js +++ b/homeassistant/components/frontend/www_static/core.js @@ -1,4 +1 @@ -!(function(){"use strict";function t(t){return t&&t.__esModule?t.default:t}function e(t,e){return e={exports:{}},t(e,e.exports),e.exports}function n(t,e){var n=e.authToken,r=e.host;return xe({authToken:n,host:r,isValidating:!0,isInvalid:!1,errorMessage:""})}function r(){return Ve.getInitialState()}function i(t,e){var n=e.errorMessage;return t.withMutations((function(t){return t.set("isValidating",!1).set("isInvalid",!0).set("errorMessage",n)}))}function o(t,e){var n=e.authToken,r=e.host;return Fe({authToken:n,host:r})}function u(){return Ge.getInitialState()}function a(t,e){var n=e.rememberAuth;return n}function s(t){return t.withMutations((function(t){t.set("isStreaming",!0).set("hasError",!1)}))}function c(t){return t.withMutations((function(t){t.set("isStreaming",!1).set("hasError",!0)}))}function f(){return Xe.getInitialState()}function h(t){return{type:"auth",api_password:t}}function l(){return{type:"get_states"}}function p(){return{type:"get_config"}}function _(){return{type:"get_services"}}function d(){return{type:"get_panels"}}function v(t,e,n){var r={type:"call_service",domain:t,service:e};return n&&(r.service_data=n),r}function y(t){var e={type:"subscribe_events"};return t&&(e.event_type=t),e}function g(t){return{type:"unsubscribe_events",subscription:t}}function m(){return{type:"ping"}}function S(t,e){return{type:"result",success:!1,error:{code:t,message:e}}}function E(t){return t.result}function b(t,e){var n=new tn(t,e);return n.connect()}function I(t,e,n,r){void 0===r&&(r=null);var i=t.evaluate(Lo.authInfo),o=i.host+"/api/"+n;return new Promise(function(t,n){var u=new XMLHttpRequest;u.open(e,o,!0),u.setRequestHeader("X-HA-access",i.authToken),u.onload=function(){var e;try{e="application/json"===u.getResponseHeader("content-type")?JSON.parse(u.responseText):u.responseText}catch(t){e=u.responseText}u.status>199&&u.status<300?t(e):n(e)},u.onerror=function(){return n({})},r?(u.setRequestHeader("Content-Type","application/json;charset=UTF-8"),u.send(JSON.stringify(r))):u.send()})}function O(t,e){var n=e.model,r=e.result,i=e.params,o=n.entity;if(!r)return t;var u=i.replace?sn({}):t.get(o),a=Array.isArray(r)?r:[r],s=n.fromJSON||sn;return t.set(o,u.withMutations((function(t){for(var e=0;e6e4}function gt(t,e){var n=e.date;return n.toISOString()}function mt(){return Zr.getInitialState()}function St(t,e){var n=e.date,r=e.stateHistory;return 0===r.length?t.set(n,ti({})):t.withMutations((function(t){r.forEach((function(e){return t.setIn([n,e[0].entity_id],ti(e.map(On.fromJSON)))}))}))}function Et(){return ei.getInitialState()}function bt(t,e){var n=e.stateHistory;return t.withMutations((function(t){n.forEach((function(e){return t.set(e[0].entity_id,oi(e.map(On.fromJSON)))}))}))}function It(){return ui.getInitialState()}function Ot(t,e){var n=e.stateHistory,r=(new Date).getTime();return t.withMutations((function(t){n.forEach((function(e){return t.set(e[0].entity_id,r)})),history.length>1&&t.set(ci,r)}))}function wt(){return fi.getInitialState()}function Tt(t,e){t.dispatch(Xr.ENTITY_HISTORY_DATE_SELECTED,{date:e})}function At(t,e){void 0===e&&(e=null),t.dispatch(Xr.RECENT_ENTITY_HISTORY_FETCH_START,{});var n="history/period";return null!==e&&(n+="?filter_entity_id="+e),on(t,"GET",n).then((function(e){return t.dispatch(Xr.RECENT_ENTITY_HISTORY_FETCH_SUCCESS,{stateHistory:e})}),(function(){return t.dispatch(Xr.RECENT_ENTITY_HISTORY_FETCH_ERROR,{})}))}function Ct(t,e){return t.dispatch(Xr.ENTITY_HISTORY_FETCH_START,{date:e}),on(t,"GET","history/period/"+e).then((function(n){return t.dispatch(Xr.ENTITY_HISTORY_FETCH_SUCCESS,{date:e,stateHistory:n})}),(function(){return t.dispatch(Xr.ENTITY_HISTORY_FETCH_ERROR,{})}))}function Dt(t){var e=t.evaluate(pi);return Ct(t,e)}function zt(t){t.registerStores({currentEntityHistoryDate:Zr,entityHistory:ei,isLoadingEntityHistory:ri,recentEntityHistory:ui,recentEntityHistoryUpdated:fi})}function Rt(t){t.registerStores({moreInfoEntityId:Jr})}function Mt(t,e){var n=e.model,r=e.result,i=e.params;if(null===t||"entity"!==n.entity||!i.replace)return t;for(var o=0;o0?i=setTimeout(r,e-c):(i=null,n||(s=t.apply(u,o),i||(u=o=null)))}var i,o,u,a,s;null==e&&(e=100);var c=function(){u=this,o=arguments,a=(new Date).getTime();var c=n&&!i;return i||(i=setTimeout(r,e)),c&&(s=t.apply(u,o),u=o=null),s};return c.clear=function(){i&&(clearTimeout(i),i=null)},c}function Yt(t){var e=ho[t.hassId];e&&(e.scheduleHealthCheck.clear(),e.conn.close(),ho[t.hassId]=!1)}function Jt(t,e){void 0===e&&(e={});var n=e.syncOnInitialConnect;void 0===n&&(n=!0),Yt(t);var r=t.evaluate(Lo.authToken),i="https:"===document.location.protocol?"wss://":"ws://";i+=document.location.hostname,document.location.port&&(i+=":"+document.location.port),i+="/api/websocket",b(i,{authToken:r}).then((function(e){var r=Bt((function(){return e.ping()}),co);r(),e.socket.addEventListener("message",r),ho[t.hassId]={conn:e,scheduleHealthCheck:r},fo.forEach((function(n){return e.subscribeEvents(so.bind(null,t),n)})),t.batch((function(){t.dispatch(Ye.STREAM_START),n&&oo.fetchAll(t)})),e.addEventListener("disconnected",(function(){t.dispatch(Ye.STREAM_ERROR)})),e.addEventListener("ready",(function(){t.batch((function(){t.dispatch(Ye.STREAM_START),oo.fetchAll(t)}))}))}))}function Wt(t){t.registerStores({streamStatus:Xe})}function Xt(t,e,n){void 0===n&&(n={});var r=n.rememberAuth;void 0===r&&(r=!1);var i=n.host;void 0===i&&(i=""),t.dispatch(Ue.VALIDATING_AUTH_TOKEN,{authToken:e,host:i}),oo.fetchAll(t).then((function(){t.dispatch(Ue.VALID_AUTH_TOKEN,{authToken:e,host:i,rememberAuth:r}),yo.start(t,{syncOnInitialConnect:!1})}),(function(e){void 0===e&&(e={});var n=e.message;void 0===n&&(n=So),t.dispatch(Ue.INVALID_AUTH_TOKEN,{errorMessage:n})}))}function Qt(t){t.dispatch(Ue.LOG_OUT,{})}function Zt(t){t.registerStores({authAttempt:Ve,authCurrent:Ge,rememberAuth:Be})}function $t(){if(!("localStorage"in window))return{};var t=window.localStorage,e="___test";try{return t.setItem(e,e),t.removeItem(e),t}catch(t){return{}}}function te(){var t=new Ho({debug:!1});return t.hassId=xo++,t}function ee(t,e,n){Object.keys(n).forEach((function(r){var i=n[r];if("register"in i&&i.register(e),"getters"in i&&Object.defineProperty(t,r+"Getters",{value:i.getters,enumerable:!0}),"actions"in i){var o={};Object.getOwnPropertyNames(i.actions).forEach((function(t){"function"==typeof i.actions[t]&&Object.defineProperty(o,t,{value:i.actions[t].bind(null,e),enumerable:!0})})),Object.defineProperty(t,r+"Actions",{value:o,enumerable:!0})}}))}function ne(t,e){return Vo(t.attributes.entity_id.map((function(t){return e.get(t)})).filter((function(t){return!!t})))}function re(t){return on(t,"GET","error_log")}function ie(t,e){var n=e.date;return n.toISOString()}function oe(){return Wo.getInitialState()}function ue(t,e){var n=e.date,r=e.entries;return t.set(n,nu(r.map(tu.fromJSON)))}function ae(){return ru.getInitialState()}function se(t,e){var n=e.date;return t.set(n,(new Date).getTime())}function ce(){return uu.getInitialState()}function fe(t,e){t.dispatch(Yo.LOGBOOK_DATE_SELECTED,{date:e})}function he(t,e){t.dispatch(Yo.LOGBOOK_ENTRIES_FETCH_START,{date:e}),on(t,"GET","logbook/"+e).then((function(n){return t.dispatch(Yo.LOGBOOK_ENTRIES_FETCH_SUCCESS,{date:e,entries:n})}),(function(){return t.dispatch(Yo.LOGBOOK_ENTRIES_FETCH_ERROR,{})}))}function le(t){return!t||(new Date).getTime()-t>cu}function pe(t){t.registerStores({currentLogbookDate:Wo,isLoadingLogbookEntries:Qo,logbookEntries:ru,logbookEntriesUpdated:uu})}function _e(t){return t.set("active",!0)}function de(t){return t.set("active",!1)}function ve(){return Eu.getInitialState()}function ye(t){return navigator.serviceWorker.getRegistration().then((function(t){if(!t)throw new Error("No service worker registered.");return t.pushManager.subscribe({userVisibleOnly:!0})})).then((function(e){var n;return n=navigator.userAgent.toLowerCase().indexOf("firefox")>-1?"firefox":"chrome",on(t,"POST","notify.html5",{subscription:e,browser:n}).then((function(){return t.dispatch(gu.PUSH_NOTIFICATIONS_SUBSCRIBE,{})})).then((function(){return!0}))})).catch((function(e){var n;return n=e.message&&e.message.indexOf("gcm_sender_id")!==-1?"Please setup the notify.html5 platform.":"Notification registration failed.",console.error(e),qn.createNotification(t,n),!1}))}function ge(t){return navigator.serviceWorker.getRegistration().then((function(t){if(!t)throw new Error("No service worker registered");return t.pushManager.subscribe({userVisibleOnly:!0})})).then((function(e){return on(t,"DELETE","notify.html5",{subscription:e}).then((function(){return e.unsubscribe()})).then((function(){return t.dispatch(gu.PUSH_NOTIFICATIONS_UNSUBSCRIBE,{})})).then((function(){return!0}))})).catch((function(e){var n="Failed unsubscribing for push notifications.";return console.error(e),qn.createNotification(t,n),!1}))}function me(t){t.registerStores({pushNotifications:Eu})}function Se(t,e){return on(t,"POST","template",{template:e})}function Ee(t){return t.set("isListening",!0)}function be(t,e){var n=e.interimTranscript,r=e.finalTranscript;return t.withMutations((function(t){return t.set("isListening",!0).set("isTransmitting",!1).set("interimTranscript",n).set("finalTranscript",r)}))}function Ie(t,e){var n=e.finalTranscript;return t.withMutations((function(t){return t.set("isListening",!1).set("isTransmitting",!0).set("interimTranscript","").set("finalTranscript",n)}))}function Oe(){return Pu.getInitialState()}function we(){return Pu.getInitialState()}function Te(){return Pu.getInitialState()}function Ae(t){return Uu[t.hassId]}function Ce(t){var e=Ae(t);if(e){var n=e.finalTranscript||e.interimTranscript;t.dispatch(ju.VOICE_TRANSMITTING,{finalTranscript:n}),er.callService(t,"conversation","process",{text:n}).then((function(){t.dispatch(ju.VOICE_DONE)}),(function(){t.dispatch(ju.VOICE_ERROR)}))}}function De(t){var e=Ae(t);e&&(e.recognition.stop(),Uu[t.hassId]=!1)}function ze(t){Ce(t),De(t)}function Re(t){var e=ze.bind(null,t);e();var n=new webkitSpeechRecognition;Uu[t.hassId]={recognition:n,interimTranscript:"",finalTranscript:""},n.interimResults=!0,n.onstart=function(){return t.dispatch(ju.VOICE_START)},n.onerror=function(){return t.dispatch(ju.VOICE_ERROR)},n.onend=e,n.onresult=function(e){var n=Ae(t);if(n){for(var r="",i="",o=e.resultIndex;o>>0;if(""+n!==e||4294967295===n)return NaN;e=n}return e<0?_(t)+e:e}function v(){return!0}function y(t,e,n){return(0===t||void 0!==n&&t<=-n)&&(void 0===e||void 0!==n&&e>=n)}function g(t,e){return S(t,e,0)}function m(t,e){return S(t,e,e)}function S(t,e,n){return void 0===t?n:t<0?Math.max(0,e+t):void 0===e?t:Math.min(e,t)}function E(t){this.next=t}function b(t,e,n,r){var i=0===t?e:1===t?n:[e,n];return r?r.value=i:r={value:i,done:!1},r}function I(){return{value:void 0,done:!0}}function O(t){return!!A(t)}function w(t){return t&&"function"==typeof t.next}function T(t){var e=A(t);return e&&e.call(t)}function A(t){var e=t&&(In&&t[In]||t[On]);if("function"==typeof e)return e}function C(t){return t&&"number"==typeof t.length}function D(t){return null===t||void 0===t?U():o(t)?t.toSeq():V(t)}function z(t){return null===t||void 0===t?U().toKeyedSeq():o(t)?u(t)?t.toSeq():t.fromEntrySeq():H(t)}function R(t){return null===t||void 0===t?U():o(t)?u(t)?t.entrySeq():t.toIndexedSeq():x(t)}function M(t){return(null===t||void 0===t?U():o(t)?u(t)?t.entrySeq():t:x(t)).toSetSeq()}function L(t){this._array=t,this.size=t.length}function j(t){var e=Object.keys(t);this._object=t,this._keys=e,this.size=e.length}function k(t){this._iterable=t,this.size=t.length||t.size}function N(t){this._iterator=t,this._iteratorCache=[]}function P(t){return!(!t||!t[Tn])}function U(){return An||(An=new L([]))}function H(t){var e=Array.isArray(t)?new L(t).fromEntrySeq():w(t)?new N(t).fromEntrySeq():O(t)?new k(t).fromEntrySeq():"object"==typeof t?new j(t):void 0;if(!e)throw new TypeError("Expected Array or iterable object of [k, v] entries, or keyed object: "+t);return e}function x(t){var e=q(t);if(!e)throw new TypeError("Expected Array or iterable object of values: "+t);return e}function V(t){var e=q(t)||"object"==typeof t&&new j(t);if(!e)throw new TypeError("Expected Array or iterable object of values, or keyed object: "+t);return e}function q(t){return C(t)?new L(t):w(t)?new N(t):O(t)?new k(t):void 0}function F(t,e,n,r){var i=t._cache;if(i){for(var o=i.length-1,u=0;u<=o;u++){var a=i[n?o-u:u];if(e(a[1],r?a[0]:u,t)===!1)return u+1}return u}return t.__iterateUncached(e,n)}function G(t,e,n,r){var i=t._cache;if(i){var o=i.length-1,u=0;return new E(function(){var t=i[n?o-u:u];return u++>o?I():b(e,r?t[0]:u-1,t[1])})}return t.__iteratorUncached(e,n)}function K(t,e){return e?B(e,t,"",{"":t}):Y(t)}function B(t,e,n,r){return Array.isArray(e)?t.call(r,n,R(e).map((function(n,r){return B(t,n,r,e)}))):J(e)?t.call(r,n,z(e).map((function(n,r){return B(t,n,r,e)}))):e}function Y(t){return Array.isArray(t)?R(t).map(Y).toList():J(t)?z(t).map(Y).toMap():t}function J(t){return t&&(t.constructor===Object||void 0===t.constructor)}function W(t,e){if(t===e||t!==t&&e!==e)return!0;if(!t||!e)return!1;if("function"==typeof t.valueOf&&"function"==typeof e.valueOf){if(t=t.valueOf(),e=e.valueOf(),t===e||t!==t&&e!==e)return!0;if(!t||!e)return!1}return!("function"!=typeof t.equals||"function"!=typeof e.equals||!t.equals(e))}function X(t,e){if(t===e)return!0;if(!o(e)||void 0!==t.size&&void 0!==e.size&&t.size!==e.size||void 0!==t.__hash&&void 0!==e.__hash&&t.__hash!==e.__hash||u(t)!==u(e)||a(t)!==a(e)||c(t)!==c(e))return!1;if(0===t.size&&0===e.size)return!0;var n=!s(t);if(c(t)){var r=t.entries();return e.every((function(t,e){var i=r.next().value;return i&&W(i[1],t)&&(n||W(i[0],e))}))&&r.next().done}var i=!1;if(void 0===t.size)if(void 0===e.size)"function"==typeof t.cacheResult&&t.cacheResult();else{i=!0;var f=t;t=e,e=f}var h=!0,l=e.__iterate((function(e,r){if(n?!t.has(e):i?!W(e,t.get(r,yn)):!W(t.get(r,yn),e))return h=!1,!1}));return h&&t.size===l}function Q(t,e){if(!(this instanceof Q))return new Q(t,e);if(this._value=t,this.size=void 0===e?1/0:Math.max(0,e),0===this.size){if(Cn)return Cn;Cn=this}}function Z(t,e){if(!t)throw new Error(e)}function $(t,e,n){if(!(this instanceof $))return new $(t,e,n);if(Z(0!==n,"Cannot step a Range by 0"),t=t||0,void 0===e&&(e=1/0),n=void 0===n?1:Math.abs(n),e>>1&1073741824|3221225471&t}function ot(t){if(t===!1||null===t||void 0===t)return 0;if("function"==typeof t.valueOf&&(t=t.valueOf(),t===!1||null===t||void 0===t))return 0;if(t===!0)return 1;var e=typeof t;if("number"===e){if(t!==t||t===1/0)return 0;var n=0|t;for(n!==t&&(n^=4294967295*t);t>4294967295;)t/=4294967295,n^=t;return it(n)}if("string"===e)return t.length>Pn?ut(t):at(t);if("function"==typeof t.hashCode)return t.hashCode();if("object"===e)return st(t);if("function"==typeof t.toString)return at(t.toString());throw new Error("Value type "+e+" cannot be hashed.")}function ut(t){var e=xn[t];return void 0===e&&(e=at(t),Hn===Un&&(Hn=0,xn={}),Hn++,xn[t]=e),e}function at(t){for(var e=0,n=0;n0)switch(t.nodeType){case 1:return t.uniqueID;case 9:return t.documentElement&&t.documentElement.uniqueID}}function ft(t){Z(t!==1/0,"Cannot perform this action with an infinite size.")}function ht(t){return null===t||void 0===t?bt():lt(t)&&!c(t)?t:bt().withMutations((function(e){var r=n(t);ft(r.size),r.forEach((function(t,n){return e.set(n,t)}))}))}function lt(t){return!(!t||!t[Vn])}function pt(t,e){this.ownerID=t,this.entries=e}function _t(t,e,n){this.ownerID=t,this.bitmap=e,this.nodes=n}function dt(t,e,n){this.ownerID=t,this.count=e,this.nodes=n}function vt(t,e,n){this.ownerID=t,this.keyHash=e,this.entries=n}function yt(t,e,n){this.ownerID=t,this.keyHash=e,this.entry=n}function gt(t,e,n){this._type=e,this._reverse=n,this._stack=t._root&&St(t._root)}function mt(t,e){return b(t,e[0],e[1])}function St(t,e){return{node:t,index:0,__prev:e}}function Et(t,e,n,r){var i=Object.create(qn);return i.size=t,i._root=e,i.__ownerID=n,i.__hash=r,i.__altered=!1,i}function bt(){return Fn||(Fn=Et(0))}function It(t,e,n){var r,i;if(t._root){var o=f(gn),u=f(mn);if(r=Ot(t._root,t.__ownerID,0,void 0,e,n,o,u),!u.value)return t;i=t.size+(o.value?n===yn?-1:1:0)}else{if(n===yn)return t;i=1,r=new pt(t.__ownerID,[[e,n]])}return t.__ownerID?(t.size=i,t._root=r,t.__hash=void 0,t.__altered=!0,t):r?Et(i,r):bt()}function Ot(t,e,n,r,i,o,u,a){return t?t.update(e,n,r,i,o,u,a):o===yn?t:(h(a),h(u),new yt(e,r,[i,o]))}function wt(t){return t.constructor===yt||t.constructor===vt}function Tt(t,e,n,r,i){if(t.keyHash===r)return new vt(e,r,[t.entry,i]);var o,u=(0===n?t.keyHash:t.keyHash>>>n)&vn,a=(0===n?r:r>>>n)&vn,s=u===a?[Tt(t,e,n+_n,r,i)]:(o=new yt(e,r,i),u>>=1)u[a]=1&n?e[o++]:void 0;return u[r]=i,new dt(t,o+1,u)}function zt(t,e,r){for(var i=[],u=0;u>1&1431655765,t=(858993459&t)+(t>>2&858993459),t=t+(t>>4)&252645135,t+=t>>8,t+=t>>16,127&t}function Nt(t,e,n,r){var i=r?t:p(t);return i[e]=n,i}function Pt(t,e,n,r){var i=t.length+1;if(r&&e+1===i)return t[e]=n,t;for(var o=new Array(i),u=0,a=0;a0&&io?0:o-n,c=u-n;return c>dn&&(c=dn),function(){if(i===c)return Xn;var t=e?--c:i++;return r&&r[t]}}function i(t,r,i){var a,s=t&&t.array,c=i>o?0:o-i>>r,f=(u-i>>r)+1;return f>dn&&(f=dn),function(){for(;;){if(a){var t=a();if(t!==Xn)return t;a=null}if(c===f)return Xn;var o=e?--f:c++;a=n(s&&s[o],r-_n,i+(o<=t.size||e<0)return t.withMutations((function(t){e<0?Wt(t,e).set(0,n):Wt(t,0,e+1).set(e,n)}));e+=t._origin;var r=t._tail,i=t._root,o=f(mn);return e>=Qt(t._capacity)?r=Bt(r,t.__ownerID,0,e,n,o):i=Bt(i,t.__ownerID,t._level,e,n,o),o.value?t.__ownerID?(t._root=i,t._tail=r,t.__hash=void 0,t.__altered=!0,t):Ft(t._origin,t._capacity,t._level,i,r):t}function Bt(t,e,n,r,i,o){var u=r>>>n&vn,a=t&&u0){var c=t&&t.array[u],f=Bt(c,e,n-_n,r,i,o);return f===c?t:(s=Yt(t,e),s.array[u]=f,s)}return a&&t.array[u]===i?t:(h(o),s=Yt(t,e),void 0===i&&u===s.array.length-1?s.array.pop():s.array[u]=i,s)}function Yt(t,e){return e&&t&&e===t.ownerID?t:new Vt(t?t.array.slice():[],e)}function Jt(t,e){if(e>=Qt(t._capacity))return t._tail;if(e<1<0;)n=n.array[e>>>r&vn],r-=_n;return n}}function Wt(t,e,n){void 0!==e&&(e|=0),void 0!==n&&(n|=0);var r=t.__ownerID||new l,i=t._origin,o=t._capacity,u=i+e,a=void 0===n?o:n<0?o+n:i+n;if(u===i&&a===o)return t;if(u>=a)return t.clear();for(var s=t._level,c=t._root,f=0;u+f<0;)c=new Vt(c&&c.array.length?[void 0,c]:[],r),s+=_n,f+=1<=1<h?new Vt([],r):_;if(_&&p>h&&u_n;y-=_n){var g=h>>>y&vn;v=v.array[g]=Yt(v.array[g],r)}v.array[h>>>_n&vn]=_}if(a=p)u-=p,a-=p,s=_n,c=null,d=d&&d.removeBefore(r,0,u);else if(u>i||p>>s&vn;if(m!==p>>>s&vn)break;m&&(f+=(1<i&&(c=c.removeBefore(r,s,u-f)),c&&pu&&(u=c.size),o(s)||(c=c.map((function(t){return K(t)}))),i.push(c)}return u>t.size&&(t=t.setSize(u)),Lt(t,e,i)}function Qt(t){return t>>_n<<_n}function Zt(t){return null===t||void 0===t?ee():$t(t)?t:ee().withMutations((function(e){var r=n(t);ft(r.size),r.forEach((function(t,n){return e.set(n,t)}))}))}function $t(t){return lt(t)&&c(t)}function te(t,e,n,r){var i=Object.create(Zt.prototype);return i.size=t?t.size:0,i._map=t,i._list=e,i.__ownerID=n,i.__hash=r,i}function ee(){return Qn||(Qn=te(bt(),Gt()))}function ne(t,e,n){var r,i,o=t._map,u=t._list,a=o.get(e),s=void 0!==a;if(n===yn){if(!s)return t;u.size>=dn&&u.size>=2*o.size?(i=u.filter((function(t,e){return void 0!==t&&a!==e})),r=i.toKeyedSeq().map((function(t){return t[0]})).flip().toMap(),t.__ownerID&&(r.__ownerID=i.__ownerID=t.__ownerID)):(r=o.remove(e),i=a===u.size-1?u.pop():u.set(a,void 0))}else if(s){if(n===u.get(a)[1])return t;r=o,i=u.set(a,[e,n])}else r=o.set(e,u.size),i=u.set(u.size,[e,n]);return t.__ownerID?(t.size=r.size,t._map=r,t._list=i,t.__hash=void 0,t):te(r,i)}function re(t,e){this._iter=t,this._useKeys=e,this.size=t.size}function ie(t){this._iter=t,this.size=t.size}function oe(t){this._iter=t,this.size=t.size}function ue(t){this._iter=t,this.size=t.size}function ae(t){var e=Ce(t);return e._iter=t,e.size=t.size,e.flip=function(){return t},e.reverse=function(){var e=t.reverse.apply(this);return e.flip=function(){return t.reverse()},e},e.has=function(e){return t.includes(e)},e.includes=function(e){return t.has(e)},e.cacheResult=De,e.__iterateUncached=function(e,n){var r=this;return t.__iterate((function(t,n){return e(n,t,r)!==!1}),n)},e.__iteratorUncached=function(e,n){if(e===bn){var r=t.__iterator(e,n);return new E(function(){var t=r.next();if(!t.done){var e=t.value[0];t.value[0]=t.value[1],t.value[1]=e}return t})}return t.__iterator(e===En?Sn:En,n)},e}function se(t,e,n){var r=Ce(t);return r.size=t.size,r.has=function(e){return t.has(e)},r.get=function(r,i){var o=t.get(r,yn);return o===yn?i:e.call(n,o,r,t)},r.__iterateUncached=function(r,i){var o=this;return t.__iterate((function(t,i,u){return r(e.call(n,t,i,u),i,o)!==!1}),i)},r.__iteratorUncached=function(r,i){var o=t.__iterator(bn,i);return new E(function(){var i=o.next();if(i.done)return i;var u=i.value,a=u[0];return b(r,a,e.call(n,u[1],a,t),i)})},r}function ce(t,e){var n=Ce(t);return n._iter=t,n.size=t.size,n.reverse=function(){return t},t.flip&&(n.flip=function(){var e=ae(t);return e.reverse=function(){return t.flip()},e}),n.get=function(n,r){return t.get(e?n:-1-n,r)},n.has=function(n){return t.has(e?n:-1-n)},n.includes=function(e){return t.includes(e)},n.cacheResult=De,n.__iterate=function(e,n){var r=this;return t.__iterate((function(t,n){return e(t,n,r)}),!n)},n.__iterator=function(e,n){return t.__iterator(e,!n)},n}function fe(t,e,n,r){var i=Ce(t);return r&&(i.has=function(r){var i=t.get(r,yn);return i!==yn&&!!e.call(n,i,r,t)},i.get=function(r,i){var o=t.get(r,yn);return o!==yn&&e.call(n,o,r,t)?o:i}),i.__iterateUncached=function(i,o){var u=this,a=0;return t.__iterate((function(t,o,s){if(e.call(n,t,o,s))return a++,i(t,r?o:a-1,u)}),o),a},i.__iteratorUncached=function(i,o){var u=t.__iterator(bn,o),a=0;return new E(function(){for(;;){var o=u.next();if(o.done)return o;var s=o.value,c=s[0],f=s[1];if(e.call(n,f,c,t))return b(i,r?c:a++,f,o)}})},i}function he(t,e,n){var r=ht().asMutable();return t.__iterate((function(i,o){r.update(e.call(n,i,o,t),0,(function(t){return t+1}))})),r.asImmutable()}function le(t,e,n){var r=u(t),i=(c(t)?Zt():ht()).asMutable();t.__iterate((function(o,u){i.update(e.call(n,o,u,t),(function(t){return t=t||[],t.push(r?[u,o]:o),t}))}));var o=Ae(t);return i.map((function(e){return Oe(t,o(e))}))}function pe(t,e,n,r){var i=t.size;if(void 0!==e&&(e|=0),void 0!==n&&(n===1/0?n=i:n|=0),y(e,n,i))return t;var o=g(e,i),u=m(n,i);if(o!==o||u!==u)return pe(t.toSeq().cacheResult(),e,n,r);var a,s=u-o;s===s&&(a=s<0?0:s);var c=Ce(t);return c.size=0===a?a:t.size&&a||void 0,!r&&P(t)&&a>=0&&(c.get=function(e,n){return e=d(this,e),e>=0&&ea)return I();var t=i.next();return r||e===En?t:e===Sn?b(e,s-1,void 0,t):b(e,s-1,t.value[1],t)})},c}function _e(t,e,n){var r=Ce(t);return r.__iterateUncached=function(r,i){var o=this;if(i)return this.cacheResult().__iterate(r,i);var u=0;return t.__iterate((function(t,i,a){return e.call(n,t,i,a)&&++u&&r(t,i,o)})),u},r.__iteratorUncached=function(r,i){var o=this;if(i)return this.cacheResult().__iterator(r,i);var u=t.__iterator(bn,i),a=!0;return new E(function(){if(!a)return I();var t=u.next();if(t.done)return t;var i=t.value,s=i[0],c=i[1];return e.call(n,c,s,o)?r===bn?t:b(r,s,c,t):(a=!1,I())})},r}function de(t,e,n,r){var i=Ce(t);return i.__iterateUncached=function(i,o){var u=this;if(o)return this.cacheResult().__iterate(i,o);var a=!0,s=0;return t.__iterate((function(t,o,c){if(!a||!(a=e.call(n,t,o,c)))return s++,i(t,r?o:s-1,u)})),s},i.__iteratorUncached=function(i,o){var u=this;if(o)return this.cacheResult().__iterator(i,o);var a=t.__iterator(bn,o),s=!0,c=0;return new E(function(){var t,o,f;do{if(t=a.next(),t.done)return r||i===En?t:i===Sn?b(i,c++,void 0,t):b(i,c++,t.value[1],t);var h=t.value;o=h[0],f=h[1],s&&(s=e.call(n,f,o,u))}while(s);return i===bn?t:b(i,o,f,t)})},i}function ve(t,e){var r=u(t),i=[t].concat(e).map((function(t){return o(t)?r&&(t=n(t)):t=r?H(t):x(Array.isArray(t)?t:[t]),t})).filter((function(t){return 0!==t.size}));if(0===i.length)return t;if(1===i.length){var s=i[0];if(s===t||r&&u(s)||a(t)&&a(s))return s}var c=new L(i);return r?c=c.toKeyedSeq():a(t)||(c=c.toSetSeq()),c=c.flatten(!0),c.size=i.reduce((function(t,e){if(void 0!==t){var n=e.size;if(void 0!==n)return t+n}}),0),c}function ye(t,e,n){var r=Ce(t);return r.__iterateUncached=function(r,i){function u(t,c){var f=this;t.__iterate((function(t,i){return(!e||c0}function Ie(t,n,r){var i=Ce(t);return i.size=new L(r).map((function(t){return t.size})).min(),i.__iterate=function(t,e){for(var n,r=this,i=this.__iterator(En,e),o=0;!(n=i.next()).done&&t(n.value,o++,r)!==!1;);return o},i.__iteratorUncached=function(t,i){var o=r.map((function(t){return t=e(t),T(i?t.reverse():t)})),u=0,a=!1;return new E(function(){var e;return a||(e=o.map((function(t){return t.next()})),a=e.some((function(t){return t.done}))),a?I():b(t,u++,n.apply(null,e.map((function(t){return t.value}))))})},i}function Oe(t,e){return P(t)?e:t.constructor(e)}function we(t){if(t!==Object(t))throw new TypeError("Expected [K, V] tuple: "+t)}function Te(t){return ft(t.size),_(t)}function Ae(t){return u(t)?n:a(t)?r:i}function Ce(t){return Object.create((u(t)?z:a(t)?R:M).prototype)}function De(){return this._iter.cacheResult?(this._iter.cacheResult(),this.size=this._iter.size,this):D.prototype.cacheResult.call(this)}function ze(t,e){return t>e?1:te?-1:0}function on(t){if(t.size===1/0)return 0;var e=c(t),n=u(t),r=e?1:0,i=t.__iterate(n?e?function(t,e){r=31*r+an(ot(t),ot(e))|0}:function(t,e){r=r+an(ot(t),ot(e))|0}:e?function(t){r=31*r+ot(t)|0}:function(t){r=r+ot(t)|0});return un(i,r)}function un(t,e){return e=Rn(e,3432918353),e=Rn(e<<15|e>>>-15,461845907),e=Rn(e<<13|e>>>-13,5),e=(e+3864292196|0)^t,e=Rn(e^e>>>16,2246822507),e=Rn(e^e>>>13,3266489909),e=it(e^e>>>16)}function an(t,e){return t^e+2654435769+(t<<6)+(t>>2)|0}var sn=Array.prototype.slice;t(n,e),t(r,e),t(i,e),e.isIterable=o,e.isKeyed=u,e.isIndexed=a,e.isAssociative=s,e.isOrdered=c,e.Keyed=n,e.Indexed=r,e.Set=i;var cn="@@__IMMUTABLE_ITERABLE__@@",fn="@@__IMMUTABLE_KEYED__@@",hn="@@__IMMUTABLE_INDEXED__@@",ln="@@__IMMUTABLE_ORDERED__@@",pn="delete",_n=5,dn=1<<_n,vn=dn-1,yn={},gn={value:!1},mn={value:!1},Sn=0,En=1,bn=2,In="function"==typeof Symbol&&Symbol.iterator,On="@@iterator",wn=In||On;E.prototype.toString=function(){return"[Iterator]"},E.KEYS=Sn,E.VALUES=En,E.ENTRIES=bn,E.prototype.inspect=E.prototype.toSource=function(){return this.toString()},E.prototype[wn]=function(){return this},t(D,e),D.of=function(){return D(arguments)},D.prototype.toSeq=function(){return this},D.prototype.toString=function(){return this.__toString("Seq {","}")},D.prototype.cacheResult=function(){return!this._cache&&this.__iterateUncached&&(this._cache=this.entrySeq().toArray(),this.size=this._cache.length),this},D.prototype.__iterate=function(t,e){return F(this,t,e,!0)},D.prototype.__iterator=function(t,e){return G(this,t,e,!0)},t(z,D),z.prototype.toKeyedSeq=function(){return this},t(R,D),R.of=function(){return R(arguments)},R.prototype.toIndexedSeq=function(){return this},R.prototype.toString=function(){return this.__toString("Seq [","]")},R.prototype.__iterate=function(t,e){return F(this,t,e,!1)},R.prototype.__iterator=function(t,e){return G(this,t,e,!1)},t(M,D),M.of=function(){return M(arguments)},M.prototype.toSetSeq=function(){return this},D.isSeq=P,D.Keyed=z,D.Set=M,D.Indexed=R;var Tn="@@__IMMUTABLE_SEQ__@@";D.prototype[Tn]=!0,t(L,R),L.prototype.get=function(t,e){return this.has(t)?this._array[d(this,t)]:e},L.prototype.__iterate=function(t,e){for(var n=this,r=this._array,i=r.length-1,o=0;o<=i;o++)if(t(r[e?i-o:o],o,n)===!1)return o+1;return o},L.prototype.__iterator=function(t,e){var n=this._array,r=n.length-1,i=0;return new E(function(){return i>r?I():b(t,i,n[e?r-i++:i++])})},t(j,z),j.prototype.get=function(t,e){return void 0===e||this.has(t)?this._object[t]:e},j.prototype.has=function(t){return this._object.hasOwnProperty(t)},j.prototype.__iterate=function(t,e){for(var n=this,r=this._object,i=this._keys,o=i.length-1,u=0;u<=o;u++){var a=i[e?o-u:u];if(t(r[a],a,n)===!1)return u+1}return u},j.prototype.__iterator=function(t,e){var n=this._object,r=this._keys,i=r.length-1,o=0;return new E(function(){var u=r[e?i-o:o];return o++>i?I():b(t,u,n[u])})},j.prototype[ln]=!0,t(k,R),k.prototype.__iterateUncached=function(t,e){var n=this;if(e)return this.cacheResult().__iterate(t,e);var r=this._iterable,i=T(r),o=0;if(w(i))for(var u;!(u=i.next()).done&&t(u.value,o++,n)!==!1;);return o},k.prototype.__iteratorUncached=function(t,e){if(e)return this.cacheResult().__iterator(t,e);var n=this._iterable,r=T(n);if(!w(r))return new E(I);var i=0;return new E(function(){var e=r.next();return e.done?e:b(t,i++,e.value)})},t(N,R),N.prototype.__iterateUncached=function(t,e){var n=this;if(e)return this.cacheResult().__iterate(t,e);for(var r=this._iterator,i=this._iteratorCache,o=0;o=r.length){var e=n.next();if(e.done)return e;r[i]=e.value}return b(t,i,r[i++])})};var An;t(Q,R),Q.prototype.toString=function(){return 0===this.size?"Repeat []":"Repeat [ "+this._value+" "+this.size+" times ]"},Q.prototype.get=function(t,e){return this.has(t)?this._value:e},Q.prototype.includes=function(t){return W(this._value,t)},Q.prototype.slice=function(t,e){var n=this.size;return y(t,e,n)?this:new Q(this._value,m(e,n)-g(t,n))},Q.prototype.reverse=function(){return this},Q.prototype.indexOf=function(t){return W(this._value,t)?0:-1},Q.prototype.lastIndexOf=function(t){return W(this._value,t)?this.size:-1},Q.prototype.__iterate=function(t,e){for(var n=this,r=0;r=0&&e=0&&nn?I():b(t,o++,u)})},$.prototype.equals=function(t){return t instanceof $?this._start===t._start&&this._end===t._end&&this._step===t._step:X(this,t)};var Dn;t(tt,e),t(et,tt),t(nt,tt),t(rt,tt),tt.Keyed=et,tt.Indexed=nt,tt.Set=rt;var zn,Rn="function"==typeof Math.imul&&Math.imul(4294967295,2)===-2?Math.imul:function(t,e){t|=0,e|=0;var n=65535&t,r=65535&e;return n*r+((t>>>16)*r+n*(e>>>16)<<16>>>0)|0},Mn=Object.isExtensible,Ln=(function(){try{return Object.defineProperty({},"@",{}),!0}catch(t){return!1}})(),jn="function"==typeof WeakMap;jn&&(zn=new WeakMap);var kn=0,Nn="__immutablehash__";"function"==typeof Symbol&&(Nn=Symbol(Nn));var Pn=16,Un=255,Hn=0,xn={};t(ht,et),ht.of=function(){var t=sn.call(arguments,0);return bt().withMutations((function(e){for(var n=0;n=t.length)throw new Error("Missing value for key: "+t[n]);e.set(t[n],t[n+1])}}))},ht.prototype.toString=function(){return this.__toString("Map {","}")},ht.prototype.get=function(t,e){return this._root?this._root.get(0,void 0,t,e):e},ht.prototype.set=function(t,e){return It(this,t,e)},ht.prototype.setIn=function(t,e){return this.updateIn(t,yn,(function(){return e}))},ht.prototype.remove=function(t){return It(this,t,yn)},ht.prototype.deleteIn=function(t){return this.updateIn(t,(function(){return yn}))},ht.prototype.update=function(t,e,n){return 1===arguments.length?t(this):this.updateIn([t],e,n)},ht.prototype.updateIn=function(t,e,n){n||(n=e,e=void 0);var r=jt(this,Re(t),e,n);return r===yn?void 0:r},ht.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._root=null,this.__hash=void 0,this.__altered=!0,this):bt()},ht.prototype.merge=function(){return zt(this,void 0,arguments)},ht.prototype.mergeWith=function(t){var e=sn.call(arguments,1);return zt(this,t,e)},ht.prototype.mergeIn=function(t){var e=sn.call(arguments,1);return this.updateIn(t,bt(),(function(t){return"function"==typeof t.merge?t.merge.apply(t,e):e[e.length-1]}))},ht.prototype.mergeDeep=function(){return zt(this,Rt,arguments)},ht.prototype.mergeDeepWith=function(t){var e=sn.call(arguments,1);return zt(this,Mt(t),e)},ht.prototype.mergeDeepIn=function(t){var e=sn.call(arguments,1);return this.updateIn(t,bt(),(function(t){return"function"==typeof t.mergeDeep?t.mergeDeep.apply(t,e):e[e.length-1]}))},ht.prototype.sort=function(t){return Zt(Se(this,t))},ht.prototype.sortBy=function(t,e){return Zt(Se(this,e,t))},ht.prototype.withMutations=function(t){var e=this.asMutable();return t(e),e.wasAltered()?e.__ensureOwner(this.__ownerID):this},ht.prototype.asMutable=function(){return this.__ownerID?this:this.__ensureOwner(new l)},ht.prototype.asImmutable=function(){return this.__ensureOwner()},ht.prototype.wasAltered=function(){return this.__altered},ht.prototype.__iterator=function(t,e){return new gt(this,t,e)},ht.prototype.__iterate=function(t,e){var n=this,r=0;return this._root&&this._root.iterate((function(e){return r++,t(e[1],e[0],n)}),e),r},ht.prototype.__ensureOwner=function(t){return t===this.__ownerID?this:t?Et(this.size,this._root,t,this.__hash):(this.__ownerID=t,this.__altered=!1,this)},ht.isMap=lt;var Vn="@@__IMMUTABLE_MAP__@@",qn=ht.prototype;qn[Vn]=!0,qn[pn]=qn.remove,qn.removeIn=qn.deleteIn,pt.prototype.get=function(t,e,n,r){for(var i=this.entries,o=0,u=i.length;o=Gn)return At(t,s,r,i);var _=t&&t===this.ownerID,d=_?s:p(s);return l?a?c===f-1?d.pop():d[c]=d.pop():d[c]=[r,i]:d.push([r,i]),_?(this.entries=d,this):new pt(t,d)}},_t.prototype.get=function(t,e,n,r){void 0===e&&(e=ot(n));var i=1<<((0===t?e:e>>>t)&vn),o=this.bitmap;return 0===(o&i)?r:this.nodes[kt(o&i-1)].get(t+_n,e,n,r)},_t.prototype.update=function(t,e,n,r,i,o,u){void 0===n&&(n=ot(r));var a=(0===e?n:n>>>e)&vn,s=1<=Kn)return Dt(t,l,c,a,_);if(f&&!_&&2===l.length&&wt(l[1^h]))return l[1^h];if(f&&_&&1===l.length&&wt(_))return _;var d=t&&t===this.ownerID,v=f?_?c:c^s:c|s,y=f?_?Nt(l,h,_,d):Ut(l,h,d):Pt(l,h,_,d);return d?(this.bitmap=v,this.nodes=y,this):new _t(t,v,y)},dt.prototype.get=function(t,e,n,r){void 0===e&&(e=ot(n));var i=(0===t?e:e>>>t)&vn,o=this.nodes[i];return o?o.get(t+_n,e,n,r):r},dt.prototype.update=function(t,e,n,r,i,o,u){void 0===n&&(n=ot(r));var a=(0===e?n:n>>>e)&vn,s=i===yn,c=this.nodes,f=c[a];if(s&&!f)return this;var h=Ot(f,t,e+_n,n,r,i,o,u);if(h===f)return this;var l=this.count;if(f){if(!h&&(l--,l=0&&t>>e&vn;if(r>=this.array.length)return new Vt([],t);var i,o=0===r;if(e>0){var u=this.array[r];if(i=u&&u.removeBefore(t,e-_n,n),i===u&&o)return this}if(o&&!i)return this;var a=Yt(this,t);if(!o)for(var s=0;s>>e&vn;if(r>=this.array.length)return this;var i;if(e>0){var o=this.array[r];if(i=o&&o.removeAfter(t,e-_n,n),i===o&&r===this.array.length-1)return this}var u=Yt(this,t);return u.array.splice(r+1),i&&(u.array[r]=i),u};var Wn,Xn={};t(Zt,ht),Zt.of=function(){return this(arguments)},Zt.prototype.toString=function(){return this.__toString("OrderedMap {","}")},Zt.prototype.get=function(t,e){var n=this._map.get(t);return void 0!==n?this._list.get(n)[1]:e},Zt.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._map.clear(),this._list.clear(),this):ee()},Zt.prototype.set=function(t,e){return ne(this,t,e)},Zt.prototype.remove=function(t){return ne(this,t,yn)},Zt.prototype.wasAltered=function(){return this._map.wasAltered()||this._list.wasAltered()},Zt.prototype.__iterate=function(t,e){var n=this;return this._list.__iterate((function(e){return e&&t(e[1],e[0],n)}),e)},Zt.prototype.__iterator=function(t,e){return this._list.fromEntrySeq().__iterator(t,e)},Zt.prototype.__ensureOwner=function(t){if(t===this.__ownerID)return this;var e=this._map.__ensureOwner(t),n=this._list.__ensureOwner(t);return t?te(e,n,t,this.__hash):(this.__ownerID=t,this._map=e,this._list=n,this)},Zt.isOrderedMap=$t,Zt.prototype[ln]=!0,Zt.prototype[pn]=Zt.prototype.remove;var Qn;t(re,z),re.prototype.get=function(t,e){return this._iter.get(t,e)},re.prototype.has=function(t){return this._iter.has(t)},re.prototype.valueSeq=function(){return this._iter.valueSeq()},re.prototype.reverse=function(){var t=this,e=ce(this,!0);return this._useKeys||(e.valueSeq=function(){return t._iter.toSeq().reverse()}),e},re.prototype.map=function(t,e){var n=this,r=se(this,t,e);return this._useKeys||(r.valueSeq=function(){return n._iter.toSeq().map(t,e)}),r},re.prototype.__iterate=function(t,e){var n,r=this;return this._iter.__iterate(this._useKeys?function(e,n){return t(e,n,r)}:(n=e?Te(this):0,function(i){return t(i,e?--n:n++,r)}),e)},re.prototype.__iterator=function(t,e){if(this._useKeys)return this._iter.__iterator(t,e);var n=this._iter.__iterator(En,e),r=e?Te(this):0;return new E(function(){var i=n.next();return i.done?i:b(t,e?--r:r++,i.value,i)})},re.prototype[ln]=!0,t(ie,R),ie.prototype.includes=function(t){return this._iter.includes(t)},ie.prototype.__iterate=function(t,e){var n=this,r=0;return this._iter.__iterate((function(e){return t(e,r++,n)}),e)},ie.prototype.__iterator=function(t,e){var n=this._iter.__iterator(En,e),r=0;return new E(function(){var e=n.next();return e.done?e:b(t,r++,e.value,e)})},t(oe,M),oe.prototype.has=function(t){return this._iter.includes(t)},oe.prototype.__iterate=function(t,e){var n=this;return this._iter.__iterate((function(e){return t(e,e,n)}),e)},oe.prototype.__iterator=function(t,e){var n=this._iter.__iterator(En,e);return new E(function(){var e=n.next();return e.done?e:b(t,e.value,e.value,e)})},t(ue,z),ue.prototype.entrySeq=function(){return this._iter.toSeq()},ue.prototype.__iterate=function(t,e){var n=this;return this._iter.__iterate((function(e){if(e){we(e);var r=o(e);return t(r?e.get(1):e[1],r?e.get(0):e[0],n)}}),e)},ue.prototype.__iterator=function(t,e){var n=this._iter.__iterator(En,e);return new E(function(){for(;;){var e=n.next();if(e.done)return e;var r=e.value;if(r){we(r);var i=o(r);return b(t,i?r.get(0):r[0],i?r.get(1):r[1],e)}}})},ie.prototype.cacheResult=re.prototype.cacheResult=oe.prototype.cacheResult=ue.prototype.cacheResult=De,t(Me,et),Me.prototype.toString=function(){return this.__toString(je(this)+" {","}")},Me.prototype.has=function(t){return this._defaultValues.hasOwnProperty(t)},Me.prototype.get=function(t,e){if(!this.has(t))return e;var n=this._defaultValues[t];return this._map?this._map.get(t,n):n},Me.prototype.clear=function(){if(this.__ownerID)return this._map&&this._map.clear(),this;var t=this.constructor;return t._empty||(t._empty=Le(this,bt()))},Me.prototype.set=function(t,e){if(!this.has(t))throw new Error('Cannot set unknown key "'+t+'" on '+je(this));if(this._map&&!this._map.has(t)){var n=this._defaultValues[t];if(e===n)return this}var r=this._map&&this._map.set(t,e);return this.__ownerID||r===this._map?this:Le(this,r)},Me.prototype.remove=function(t){if(!this.has(t))return this;var e=this._map&&this._map.remove(t);return this.__ownerID||e===this._map?this:Le(this,e)},Me.prototype.wasAltered=function(){return this._map.wasAltered()},Me.prototype.__iterator=function(t,e){var r=this;return n(this._defaultValues).map((function(t,e){return r.get(e)})).__iterator(t,e)},Me.prototype.__iterate=function(t,e){var r=this;return n(this._defaultValues).map((function(t,e){return r.get(e)})).__iterate(t,e)},Me.prototype.__ensureOwner=function(t){if(t===this.__ownerID)return this;var e=this._map&&this._map.__ensureOwner(t);return t?Le(this,e,t):(this.__ownerID=t,this._map=e,this)};var Zn=Me.prototype;Zn[pn]=Zn.remove,Zn.deleteIn=Zn.removeIn=qn.removeIn,Zn.merge=qn.merge,Zn.mergeWith=qn.mergeWith,Zn.mergeIn=qn.mergeIn,Zn.mergeDeep=qn.mergeDeep,Zn.mergeDeepWith=qn.mergeDeepWith,Zn.mergeDeepIn=qn.mergeDeepIn,Zn.setIn=qn.setIn,Zn.update=qn.update,Zn.updateIn=qn.updateIn,Zn.withMutations=qn.withMutations,Zn.asMutable=qn.asMutable,Zn.asImmutable=qn.asImmutable,t(Pe,rt),Pe.of=function(){return this(arguments)},Pe.fromKeys=function(t){return this(n(t).keySeq())},Pe.prototype.toString=function(){return this.__toString("Set {","}")},Pe.prototype.has=function(t){return this._map.has(t)},Pe.prototype.add=function(t){return He(this,this._map.set(t,!0))},Pe.prototype.remove=function(t){return He(this,this._map.remove(t)); -},Pe.prototype.clear=function(){return He(this,this._map.clear())},Pe.prototype.union=function(){var t=sn.call(arguments,0);return t=t.filter((function(t){return 0!==t.size})),0===t.length?this:0!==this.size||this.__ownerID||1!==t.length?this.withMutations((function(e){for(var n=0;n=0;r--)n={value:t[r],next:n};return this.__ownerID?(this.size=e,this._head=n,this.__hash=void 0,this.__altered=!0,this):Je(e,n)},Be.prototype.pushAll=function(t){if(t=r(t),0===t.size)return this;ft(t.size);var e=this.size,n=this._head;return t.reverse().forEach((function(t){e++,n={value:t,next:n}})),this.__ownerID?(this.size=e,this._head=n,this.__hash=void 0,this.__altered=!0,this):Je(e,n)},Be.prototype.pop=function(){return this.slice(1)},Be.prototype.unshift=function(){return this.push.apply(this,arguments)},Be.prototype.unshiftAll=function(t){return this.pushAll(t)},Be.prototype.shift=function(){return this.pop.apply(this,arguments)},Be.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._head=void 0,this.__hash=void 0,this.__altered=!0,this):We()},Be.prototype.slice=function(t,e){if(y(t,e,this.size))return this;var n=g(t,this.size),r=m(e,this.size);if(r!==this.size)return nt.prototype.slice.call(this,t,e);for(var i=this.size-n,o=this._head;n--;)o=o.next;return this.__ownerID?(this.size=i,this._head=o,this.__hash=void 0,this.__altered=!0,this):Je(i,o)},Be.prototype.__ensureOwner=function(t){return t===this.__ownerID?this:t?Je(this.size,this._head,t,this.__hash):(this.__ownerID=t,this.__altered=!1,this)},Be.prototype.__iterate=function(t,e){var n=this;if(e)return this.reverse().__iterate(t);for(var r=0,i=this._head;i&&t(i.value,r++,n)!==!1;)i=i.next;return r},Be.prototype.__iterator=function(t,e){if(e)return this.reverse().__iterator(t);var n=0,r=this._head;return new E(function(){if(r){var e=r.value;return r=r.next,b(t,n++,e)}return I()})},Be.isStack=Ye;var ir="@@__IMMUTABLE_STACK__@@",or=Be.prototype;or[ir]=!0,or.withMutations=qn.withMutations,or.asMutable=qn.asMutable,or.asImmutable=qn.asImmutable,or.wasAltered=qn.wasAltered;var ur;e.Iterator=E,Xe(e,{toArray:function(){ft(this.size);var t=new Array(this.size||0);return this.valueSeq().__iterate((function(e,n){t[n]=e})),t},toIndexedSeq:function(){return new ie(this)},toJS:function(){return this.toSeq().map((function(t){return t&&"function"==typeof t.toJS?t.toJS():t})).__toJS()},toJSON:function(){return this.toSeq().map((function(t){return t&&"function"==typeof t.toJSON?t.toJSON():t})).__toJS()},toKeyedSeq:function(){return new re(this,!0)},toMap:function(){return ht(this.toKeyedSeq())},toObject:function(){ft(this.size);var t={};return this.__iterate((function(e,n){t[n]=e})),t},toOrderedMap:function(){return Zt(this.toKeyedSeq())},toOrderedSet:function(){return qe(u(this)?this.valueSeq():this)},toSet:function(){return Pe(u(this)?this.valueSeq():this)},toSetSeq:function(){return new oe(this)},toSeq:function(){return a(this)?this.toIndexedSeq():u(this)?this.toKeyedSeq():this.toSetSeq()},toStack:function(){return Be(u(this)?this.valueSeq():this)},toList:function(){return Ht(u(this)?this.valueSeq():this)},toString:function(){return"[Iterable]"},__toString:function(t,e){return 0===this.size?t+e:t+" "+this.toSeq().map(this.__toStringMapper).join(", ")+" "+e},concat:function(){var t=sn.call(arguments,0);return Oe(this,ve(this,t))},includes:function(t){return this.some((function(e){return W(e,t)}))},entries:function(){return this.__iterator(bn)},every:function(t,e){ft(this.size);var n=!0;return this.__iterate((function(r,i,o){if(!t.call(e,r,i,o))return n=!1,!1})),n},filter:function(t,e){return Oe(this,fe(this,t,e,!0))},find:function(t,e,n){var r=this.findEntry(t,e);return r?r[1]:n},forEach:function(t,e){return ft(this.size),this.__iterate(e?t.bind(e):t)},join:function(t){ft(this.size),t=void 0!==t?""+t:",";var e="",n=!0;return this.__iterate((function(r){n?n=!1:e+=t,e+=null!==r&&void 0!==r?r.toString():""})),e},keys:function(){return this.__iterator(Sn)},map:function(t,e){return Oe(this,se(this,t,e))},reduce:function(t,e,n){ft(this.size);var r,i;return arguments.length<2?i=!0:r=e,this.__iterate((function(e,o,u){i?(i=!1,r=e):r=t.call(n,r,e,o,u)})),r},reduceRight:function(t,e,n){var r=this.toKeyedSeq().reverse();return r.reduce.apply(r,arguments)},reverse:function(){return Oe(this,ce(this,!0))},slice:function(t,e){return Oe(this,pe(this,t,e,!0))},some:function(t,e){return!this.every($e(t),e)},sort:function(t){return Oe(this,Se(this,t))},values:function(){return this.__iterator(En)},butLast:function(){return this.slice(0,-1)},isEmpty:function(){return void 0!==this.size?0===this.size:!this.some((function(){return!0}))},count:function(t,e){return _(t?this.toSeq().filter(t,e):this)},countBy:function(t,e){return he(this,t,e)},equals:function(t){return X(this,t)},entrySeq:function(){var t=this;if(t._cache)return new L(t._cache);var e=t.toSeq().map(Ze).toIndexedSeq();return e.fromEntrySeq=function(){return t.toSeq()},e},filterNot:function(t,e){return this.filter($e(t),e)},findEntry:function(t,e,n){var r=n;return this.__iterate((function(n,i,o){if(t.call(e,n,i,o))return r=[i,n],!1})),r},findKey:function(t,e){var n=this.findEntry(t,e);return n&&n[0]},findLast:function(t,e,n){return this.toKeyedSeq().reverse().find(t,e,n)},findLastEntry:function(t,e,n){return this.toKeyedSeq().reverse().findEntry(t,e,n)},findLastKey:function(t,e){return this.toKeyedSeq().reverse().findKey(t,e)},first:function(){return this.find(v)},flatMap:function(t,e){return Oe(this,ge(this,t,e))},flatten:function(t){return Oe(this,ye(this,t,!0))},fromEntrySeq:function(){return new ue(this)},get:function(t,e){return this.find((function(e,n){return W(n,t)}),void 0,e)},getIn:function(t,e){for(var n,r=this,i=Re(t);!(n=i.next()).done;){var o=n.value;if(r=r&&r.get?r.get(o,yn):yn,r===yn)return e}return r},groupBy:function(t,e){return le(this,t,e)},has:function(t){return this.get(t,yn)!==yn},hasIn:function(t){return this.getIn(t,yn)!==yn},isSubset:function(t){return t="function"==typeof t.includes?t:e(t),this.every((function(e){return t.includes(e)}))},isSuperset:function(t){return t="function"==typeof t.isSubset?t:e(t),t.isSubset(this)},keyOf:function(t){return this.findKey((function(e){return W(e,t)}))},keySeq:function(){return this.toSeq().map(Qe).toIndexedSeq()},last:function(){return this.toSeq().reverse().first()},lastKeyOf:function(t){return this.toKeyedSeq().reverse().keyOf(t)},max:function(t){return Ee(this,t)},maxBy:function(t,e){return Ee(this,e,t)},min:function(t){return Ee(this,t?tn(t):rn)},minBy:function(t,e){return Ee(this,e?tn(e):rn,t)},rest:function(){return this.slice(1)},skip:function(t){return this.slice(Math.max(0,t))},skipLast:function(t){return Oe(this,this.toSeq().reverse().skip(t).reverse())},skipWhile:function(t,e){return Oe(this,de(this,t,e,!0))},skipUntil:function(t,e){return this.skipWhile($e(t),e)},sortBy:function(t,e){return Oe(this,Se(this,e,t))},take:function(t){return this.slice(0,Math.max(0,t))},takeLast:function(t){return Oe(this,this.toSeq().reverse().take(t).reverse())},takeWhile:function(t,e){return Oe(this,_e(this,t,e))},takeUntil:function(t,e){return this.takeWhile($e(t),e)},valueSeq:function(){return this.toIndexedSeq()},hashCode:function(){return this.__hash||(this.__hash=on(this))}});var ar=e.prototype;ar[cn]=!0,ar[wn]=ar.values,ar.__toJS=ar.toArray,ar.__toStringMapper=en,ar.inspect=ar.toSource=function(){return this.toString()},ar.chain=ar.flatMap,ar.contains=ar.includes,Xe(n,{flip:function(){return Oe(this,ae(this))},mapEntries:function(t,e){var n=this,r=0;return Oe(this,this.toSeq().map((function(i,o){return t.call(e,[o,i],r++,n)})).fromEntrySeq())},mapKeys:function(t,e){var n=this;return Oe(this,this.toSeq().flip().map((function(r,i){return t.call(e,r,i,n)})).flip())}});var sr=n.prototype;sr[fn]=!0,sr[wn]=ar.entries,sr.__toJS=ar.toObject,sr.__toStringMapper=function(t,e){return JSON.stringify(e)+": "+en(t)},Xe(r,{toKeyedSeq:function(){return new re(this,!1)},filter:function(t,e){return Oe(this,fe(this,t,e,!1))},findIndex:function(t,e){var n=this.findEntry(t,e);return n?n[0]:-1},indexOf:function(t){var e=this.keyOf(t);return void 0===e?-1:e},lastIndexOf:function(t){var e=this.lastKeyOf(t);return void 0===e?-1:e},reverse:function(){return Oe(this,ce(this,!1))},slice:function(t,e){return Oe(this,pe(this,t,e,!1))},splice:function(t,e){var n=arguments.length;if(e=Math.max(0|e,0),0===n||2===n&&!e)return this;t=g(t,t<0?this.count():this.size);var r=this.slice(0,t);return Oe(this,1===n?r:r.concat(p(arguments,2),this.slice(t+e)))},findLastIndex:function(t,e){var n=this.findLastEntry(t,e);return n?n[0]:-1},first:function(){return this.get(0)},flatten:function(t){return Oe(this,ye(this,t,!1))},get:function(t,e){return t=d(this,t),t<0||this.size===1/0||void 0!==this.size&&t>this.size?e:this.find((function(e,n){return n===t}),void 0,e)},has:function(t){return t=d(this,t),t>=0&&(void 0!==this.size?this.size===1/0||t-1&&t%1===0&&t<=Number.MAX_VALUE}var i=Function.prototype.bind;e.isString=function(t){return"string"==typeof t||"[object String]"===n(t)},e.isArray=Array.isArray||function(t){return"[object Array]"===n(t)},"function"!=typeof/./&&"object"!=typeof Int8Array?e.isFunction=function(t){return"function"==typeof t||!1}:e.isFunction=function(t){return"[object Function]"===toString.call(t)},e.isObject=function(t){var e=typeof t;return"function"===e||"object"===e&&!!t},e.extend=function(t){var e=arguments,n=arguments.length;if(!t||n<2)return t||{};for(var r=1;r0)){var e=this.reactorState.get("dirtyStores");if(0!==e.size){var n=c.default.Set().withMutations((function(n){n.union(t.observerState.get("any")),e.forEach((function(e){var r=t.observerState.getIn(["stores",e]);r&&n.union(r)}))}));n.forEach((function(e){var n=t.observerState.getIn(["observersMap",e]);if(n){var r=n.get("getter"),i=n.get("handler"),o=p.evaluate(t.prevReactorState,r),u=p.evaluate(t.reactorState,r);t.prevReactorState=o.reactorState,t.reactorState=u.reactorState;var a=o.result,s=u.result;c.default.is(a,s)||i.call(null,s)}}));var r=p.resetDirtyStores(this.reactorState);this.prevReactorState=r,this.reactorState=r}}}},{key:"batchStart",value:function(){this.__batchDepth++}},{key:"batchEnd",value:function(){if(this.__batchDepth--,this.__batchDepth<=0){this.__isDispatching=!0;try{this.__notify()}catch(t){throw this.__isDispatching=!1,t}this.__isDispatching=!1}}}]),t})();e.default=(0,m.toFactory)(E),t.exports=e.default}),(function(t,e,n){function r(t,e,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function i(t,e){var n={};return(0,o.each)(e,(function(e,r){n[r]=t.evaluate(e)})),n}Object.defineProperty(e,"__esModule",{value:!0});var o=n(4);e.default=function(t){return{getInitialState:function(){return i(t,this.getDataBindings())},componentDidMount:function(){var e=this;this.__unwatchFns=[],(0,o.each)(this.getDataBindings(),(function(n,i){var o=t.observe(n,(function(t){e.setState(r({},i,t))}));e.__unwatchFns.push(o)}))},componentWillUnmount:function(){for(var t=this;this.__unwatchFns.length;)t.__unwatchFns.shift()()}}},t.exports=e.default}),(function(t,e,n){function r(t){return t&&t.__esModule?t:{default:t}}function i(t,e){return new C({result:t,reactorState:e})}function o(t,e){return t.withMutations((function(t){(0,A.each)(e,(function(e,n){t.getIn(["stores",n])&&console.warn("Store already defined for id = "+n);var r=e.getInitialState();if(void 0===r&&f(t,"throwOnUndefinedStoreReturnValue"))throw new Error("Store getInitialState() must return a value, did you forget a return statement");if(f(t,"throwOnNonImmutableStore")&&!(0,O.isImmutableValue)(r))throw new Error("Store getInitialState() must return an immutable value, did you forget to call toImmutable");t.update("stores",(function(t){return t.set(n,e)})).update("state",(function(t){return t.set(n,r)})).update("dirtyStores",(function(t){return t.add(n)})).update("storeStates",(function(t){return S(t,[n])}))})),m(t)}))}function u(t,e){return t.withMutations((function(t){(0,A.each)(e,(function(e,n){t.update("stores",(function(t){return t.set(n,e)}))}))}))}function a(t,e,n){var r=t.get("logger");if(void 0===e&&f(t,"throwOnUndefinedActionType"))throw new Error("`dispatch` cannot be called with an `undefined` action type.");var i=t.get("state"),o=t.get("dirtyStores"),u=i.withMutations((function(u){r.dispatchStart(t,e,n),t.get("stores").forEach((function(i,a){var s=u.get(a),c=void 0;try{c=i.handle(s,e,n)}catch(e){throw r.dispatchError(t,e.message),e}if(void 0===c&&f(t,"throwOnUndefinedStoreReturnValue")){var h="Store handler must return a value, did you forget a return statement";throw r.dispatchError(t,h),new Error(h)}u.set(a,c),s!==c&&(o=o.add(a))})),r.dispatchEnd(t,u,o,i)})),a=t.set("state",u).set("dirtyStores",o).update("storeStates",(function(t){return S(t,o)}));return m(a)}function s(t,e){var n=[],r=(0,O.toImmutable)({}).withMutations((function(r){(0,A.each)(e,(function(e,i){var o=t.getIn(["stores",i]);if(o){var u=o.deserialize(e);void 0!==u&&(r.set(i,u),n.push(i))}}))})),i=b.default.Set(n);return t.update("state",(function(t){return t.merge(r)})).update("dirtyStores",(function(t){return t.union(i)})).update("storeStates",(function(t){return S(t,n)}))}function c(t,e,n){var r=e;(0,T.isKeyPath)(e)&&(e=(0,w.fromKeyPath)(e));var i=t.get("nextId"),o=(0,w.getStoreDeps)(e),u=b.default.Map({id:i,storeDeps:o,getterKey:r,getter:e,handler:n}),a=void 0;return a=0===o.size?t.update("any",(function(t){return t.add(i)})):t.withMutations((function(t){o.forEach((function(e){var n=["stores",e];t.hasIn(n)||t.setIn(n,b.default.Set()),t.updateIn(["stores",e],(function(t){return t.add(i)}))}))})),a=a.set("nextId",i+1).setIn(["observersMap",i],u),{observerState:a,entry:u}}function f(t,e){var n=t.getIn(["options",e]);if(void 0===n)throw new Error("Invalid option: "+e);return n}function h(t,e,n){var r=t.get("observersMap").filter((function(t){var r=t.get("getterKey"),i=!n||t.get("handler")===n;return!!i&&((0,T.isKeyPath)(e)&&(0,T.isKeyPath)(r)?(0,T.isEqual)(e,r):e===r)}));return t.withMutations((function(t){r.forEach((function(e){return l(t,e)}))}))}function l(t,e){return t.withMutations((function(t){var n=e.get("id"),r=e.get("storeDeps");0===r.size?t.update("any",(function(t){return t.remove(n)})):r.forEach((function(e){t.updateIn(["stores",e],(function(t){return t?t.remove(n):t}))})),t.removeIn(["observersMap",n])}))}function p(t){var e=t.get("state");return t.withMutations((function(t){var n=t.get("stores"),r=n.keySeq().toJS();n.forEach((function(n,r){var i=e.get(r),o=n.handleReset(i);if(void 0===o&&f(t,"throwOnUndefinedStoreReturnValue"))throw new Error("Store handleReset() must return a value, did you forget a return statement");if(f(t,"throwOnNonImmutableStore")&&!(0,O.isImmutableValue)(o))throw new Error("Store reset state must be an immutable value, did you forget to call toImmutable");t.setIn(["state",r],o)})),t.update("storeStates",(function(t){return S(t,r)})),v(t)}))}function _(t,e){var n=t.get("state");if((0,T.isKeyPath)(e))return i(n.getIn(e),t);if(!(0,w.isGetter)(e))throw new Error("evaluate must be passed a keyPath or Getter");var r=t.get("cache"),o=r.lookup(e),u=!o||y(t,o);return u&&(o=g(t,e)),i(o.get("value"),t.update("cache",(function(t){return u?t.miss(e,o):t.hit(e)})))}function d(t){var e={};return t.get("stores").forEach((function(n,r){var i=t.getIn(["state",r]),o=n.serialize(i);void 0!==o&&(e[r]=o)})),e}function v(t){return t.set("dirtyStores",b.default.Set())}function y(t,e){var n=e.get("storeStates");return!n.size||n.some((function(e,n){return t.getIn(["storeStates",n])!==e}))}function g(t,e){var n=(0,w.getDeps)(e).map((function(e){return _(t,e).result})),r=(0,w.getComputeFn)(e).apply(null,n),i=(0,w.getStoreDeps)(e),o=(0,O.toImmutable)({}).withMutations((function(e){i.forEach((function(n){var r=t.getIn(["storeStates",n]);e.set(n,r)}))}));return(0,I.CacheEntry)({value:r,storeStates:o,dispatchId:t.get("dispatchId")})}function m(t){return t.update("dispatchId",(function(t){return t+1}))}function S(t,e){return t.withMutations((function(t){e.forEach((function(e){var n=t.has(e)?t.get(e)+1:1;t.set(e,n)}))}))}Object.defineProperty(e,"__esModule",{value:!0}),e.registerStores=o,e.replaceStores=u,e.dispatch=a,e.loadState=s,e.addObserver=c,e.getOption=f,e.removeObserver=h,e.removeObserverByEntry=l,e.reset=p,e.evaluate=_,e.serialize=d,e.resetDirtyStores=v;var E=n(3),b=r(E),I=n(9),O=n(5),w=n(10),T=n(11),A=n(4),C=b.default.Record({result:null,reactorState:null})}),(function(t,e,n){function r(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(){return new s}Object.defineProperty(e,"__esModule",{value:!0});var o=(function(){function t(t,e){for(var n=0;nn.dispatchId)throw new Error("Refusing to cache older value");return n})))}},{key:"evict",value:function(e){return new t(this.cache.remove(e))}}]),t})();e.BasicCache=s;var c=1e3,f=1,h=(function(){function t(){var e=arguments.length<=0||void 0===arguments[0]?c:arguments[0],n=arguments.length<=1||void 0===arguments[1]?f:arguments[1],i=arguments.length<=2||void 0===arguments[2]?new s:arguments[2],o=arguments.length<=3||void 0===arguments[3]?(0,u.OrderedSet)():arguments[3];r(this,t),console.log("using LRU"),this.limit=e,this.evictCount=n,this.cache=i,this.lru=o}return o(t,[{key:"lookup",value:function(t,e){return this.cache.lookup(t,e)}},{key:"has",value:function(t){return this.cache.has(t)}},{key:"asMap",value:function(){return this.cache.asMap()}},{key:"hit",value:function(e){return this.cache.has(e)?new t(this.limit,this.evictCount,this.cache,this.lru.remove(e).add(e)):this}},{key:"miss",value:function(e,n){var r;if(this.lru.size>=this.limit){if(this.has(e))return new t(this.limit,this.evictCount,this.cache.miss(e,n),this.lru.remove(e).add(e));var i=this.lru.take(this.evictCount).reduce((function(t,e){return t.evict(e)}),this.cache).miss(e,n);r=new t(this.limit,this.evictCount,i,this.lru.skip(this.evictCount).add(e))}else r=new t(this.limit,this.evictCount,this.cache.miss(e,n),this.lru.add(e));return r}},{key:"evict",value:function(e){return this.cache.has(e)?new t(this.limit,this.evictCount,this.cache.evict(e),this.lru.remove(e)):this}}]),t})();e.LRUCache=h}),(function(t,e,n){function r(t){return t&&t.__esModule?t:{default:t}}function i(t){return(0,l.isArray)(t)&&(0,l.isFunction)(t[t.length-1])}function o(t){return t[t.length-1]}function u(t){return t.slice(0,t.length-1)}function a(t,e){e||(e=h.default.Set());var n=h.default.Set().withMutations((function(e){if(!i(t))throw new Error("getFlattenedDeps must be passed a Getter");u(t).forEach((function(t){if((0,p.isKeyPath)(t))e.add((0,f.List)(t));else{if(!i(t))throw new Error("Invalid getter, each dependency must be a KeyPath or Getter");e.union(a(t))}}))}));return e.union(n)}function s(t){if(!(0,p.isKeyPath)(t))throw new Error("Cannot create Getter from KeyPath: "+t);return[t,_]}function c(t){if(t.hasOwnProperty("__storeDeps"))return t.__storeDeps;var e=a(t).map((function(t){return t.first()})).filter((function(t){return!!t}));return Object.defineProperty(t,"__storeDeps",{enumerable:!1,configurable:!1,writable:!1,value:e}),e}Object.defineProperty(e,"__esModule",{value:!0});var f=n(3),h=r(f),l=n(4),p=n(11),_=function(t){return t};e.default={isGetter:i,getComputeFn:o,getFlattenedDeps:a,getStoreDeps:c,getDeps:u,fromKeyPath:s},t.exports=e.default}),(function(t,e,n){function r(t){return t&&t.__esModule?t:{default:t}}function i(t){return(0,s.isArray)(t)&&!(0,s.isFunction)(t[t.length-1])}function o(t,e){var n=a.default.List(t),r=a.default.List(e);return a.default.is(n,r)}Object.defineProperty(e,"__esModule",{value:!0}),e.isKeyPath=i,e.isEqual=o;var u=n(3),a=r(u),s=n(4)}),(function(t,e,n){Object.defineProperty(e,"__esModule",{value:!0});var r=n(8),i={dispatchStart:function(t,e,n){(0,r.getOption)(t,"logDispatches")&&console.group&&(console.groupCollapsed("Dispatch: %s",e),console.group("payload"),console.debug(n),console.groupEnd())},dispatchError:function(t,e){(0,r.getOption)(t,"logDispatches")&&console.group&&(console.debug("Dispatch error: "+e),console.groupEnd())},dispatchEnd:function(t,e,n,i){(0,r.getOption)(t,"logDispatches")&&console.group&&((0,r.getOption)(t,"logDirtyStores")&&console.log("Stores updated:",n.toList().toJS()),(0,r.getOption)(t,"logAppState")&&console.debug("Dispatch done, new state: ",e.toJS()),console.groupEnd())}};e.ConsoleGroupLogger=i;var o={dispatchStart:function(t,e,n){},dispatchError:function(t,e){},dispatchEnd:function(t,e,n){}};e.NoopLogger=o}),(function(t,e,n){Object.defineProperty(e,"__esModule",{value:!0});var r=n(3),i=n(9),o=n(12),u=(0,r.Map)({logDispatches:!1,logAppState:!1,logDirtyStores:!1,throwOnUndefinedActionType:!1,throwOnUndefinedStoreReturnValue:!1,throwOnNonImmutableStore:!1,throwOnDispatchInDispatch:!1});e.PROD_OPTIONS=u;var a=(0,r.Map)({logDispatches:!0,logAppState:!0,logDirtyStores:!0,throwOnUndefinedActionType:!0,throwOnUndefinedStoreReturnValue:!0,throwOnNonImmutableStore:!0,throwOnDispatchInDispatch:!0});e.DEBUG_OPTIONS=a;var s=(0,r.Record)({dispatchId:0,state:(0,r.Map)(),stores:(0,r.Map)(),cache:(0,i.DefaultCache)(),logger:o.NoopLogger,storeStates:(0,r.Map)(),dirtyStores:(0,r.Set)(),debug:!1,options:u});e.ReactorState=s;var c=(0,r.Record)({any:(0,r.Set)(),stores:(0,r.Map)({}),observersMap:(0,r.Map)({}),nextId:1});e.ObserverState=c})])}))})),ke=t(je),Ne=function(t){var e,n={};if(!(t instanceof Object)||Array.isArray(t))throw new Error("keyMirror(...): Argument must be an object.");for(e in t)t.hasOwnProperty(e)&&(n[e]=e);return n},Pe=Ne,Ue=Pe({VALIDATING_AUTH_TOKEN:null,VALID_AUTH_TOKEN:null,INVALID_AUTH_TOKEN:null,LOG_OUT:null}),He=ke.Store,xe=ke.toImmutable,Ve=new He({getInitialState:function(){return xe({isValidating:!1,authToken:!1,host:null,isInvalid:!1,errorMessage:""})},initialize:function(){this.on(Ue.VALIDATING_AUTH_TOKEN,n),this.on(Ue.VALID_AUTH_TOKEN,r),this.on(Ue.INVALID_AUTH_TOKEN,i)}}),qe=ke.Store,Fe=ke.toImmutable,Ge=new qe({getInitialState:function(){return Fe({authToken:null,host:""})},initialize:function(){this.on(Ue.VALID_AUTH_TOKEN,o),this.on(Ue.LOG_OUT,u)}}),Ke=ke.Store,Be=new Ke({getInitialState:function(){return!0},initialize:function(){this.on(Ue.VALID_AUTH_TOKEN,a)}}),Ye=Pe({STREAM_START:null,STREAM_STOP:null,STREAM_ERROR:null}),Je=ke.Store,We=ke.toImmutable,Xe=new Je({getInitialState:function(){return We({isStreaming:!1,hasError:!1})},initialize:function(){this.on(Ye.STREAM_START,s),this.on(Ye.STREAM_ERROR,c),this.on(Ye.LOG_OUT,f)}}),Qe=1,Ze=2,$e=3,tn=function(t,e){this.url=t,this.options=e||{},this.commandId=1,this.commands={},this.connectionTries=0,this.eventListeners={},this.closeRequested=!1};tn.prototype.addEventListener=function(t,e){var n=this.eventListeners[t];n||(n=this.eventListeners[t]=[]),n.push(e)},tn.prototype.fireEvent=function(t){var e=this;(this.eventListeners[t]||[]).forEach((function(t){return t(e)}))},tn.prototype.connect=function(){var t=this;return new Promise(function(e,n){var r=t.commands;Object.keys(r).forEach((function(t){var e=r[t];e.reject&&e.reject(S($e,"Connection lost"))}));var i=!1;t.connectionTries+=1,t.socket=new WebSocket(t.url),t.socket.addEventListener("open",(function(){t.connectionTries=0})),t.socket.addEventListener("message",(function(o){var u=JSON.parse(o.data);switch(u.type){case"event":t.commands[u.id].eventCallback(u.event);break;case"result":u.success?t.commands[u.id].resolve(u):t.commands[u.id].reject(u.error),delete t.commands[u.id];break;case"pong":break; -case"auth_required":t.sendMessage(h(t.options.authToken));break;case"auth_invalid":n(Ze),i=!0;break;case"auth_ok":e(t),t.fireEvent("ready"),t.commandId=1,t.commands={},Object.keys(r).forEach((function(e){var n=r[e];n.eventType&&t.subscribeEvents(n.eventCallback,n.eventType).then((function(t){n.unsubscribe=t}))}))}})),t.socket.addEventListener("close",(function(){if(!i&&!t.closeRequested){0===t.connectionTries?t.fireEvent("disconnected"):n(Qe);var e=1e3*Math.min(t.connectionTries,5);setTimeout((function(){return t.connect()}),e)}}))})},tn.prototype.close=function(){this.closeRequested=!0,this.socket.close()},tn.prototype.getStates=function(){return this.sendMessagePromise(l()).then(E)},tn.prototype.getServices=function(){return this.sendMessagePromise(_()).then(E)},tn.prototype.getPanels=function(){return this.sendMessagePromise(d()).then(E)},tn.prototype.getConfig=function(){return this.sendMessagePromise(p()).then(E)},tn.prototype.callService=function(t,e,n){return this.sendMessagePromise(v(t,e,n))},tn.prototype.subscribeEvents=function(t,e){var n=this;return this.sendMessagePromise(y(e)).then((function(r){var i={eventCallback:t,eventType:e,unsubscribe:function(){return n.sendMessagePromise(g(r.id)).then((function(){delete n.commands[r.id]}))}};return n.commands[r.id]=i,function(){return i.unsubscribe()}}))},tn.prototype.ping=function(){return this.sendMessagePromise(m())},tn.prototype.sendMessage=function(t){this.socket.send(JSON.stringify(t))},tn.prototype.sendMessagePromise=function(t){var e=this;return new Promise(function(n,r){e.commandId+=1;var i=e.commandId;t.id=i,e.commands[i]={resolve:n,reject:r},e.sendMessage(t)})};var en=Pe({API_FETCH_ALL_START:null,API_FETCH_ALL_SUCCESS:null,API_FETCH_ALL_FAIL:null,SYNC_SCHEDULED:null,SYNC_SCHEDULE_CANCELLED:null}),nn=ke.Store,rn=new nn({getInitialState:function(){return!0},initialize:function(){this.on(en.API_FETCH_ALL_START,(function(){return!0})),this.on(en.API_FETCH_ALL_SUCCESS,(function(){return!1})),this.on(en.API_FETCH_ALL_FAIL,(function(){return!1})),this.on(en.LOG_OUT,(function(){return!1}))}}),on=I,un=Pe({API_FETCH_SUCCESS:null,API_FETCH_START:null,API_FETCH_FAIL:null,API_SAVE_SUCCESS:null,API_SAVE_START:null,API_SAVE_FAIL:null,API_DELETE_SUCCESS:null,API_DELETE_START:null,API_DELETE_FAIL:null,LOG_OUT:null}),an=ke.Store,sn=ke.toImmutable,cn=new an({getInitialState:function(){return sn({})},initialize:function(){var t=this;this.on(un.API_FETCH_SUCCESS,O),this.on(un.API_SAVE_SUCCESS,O),this.on(un.API_DELETE_SUCCESS,w),this.on(un.LOG_OUT,(function(){return t.getInitialState()}))}}),fn=Object.getOwnPropertySymbols,hn=Object.prototype.hasOwnProperty,ln=Object.prototype.propertyIsEnumerable,pn=A()?Object.assign:function(t,e){for(var n,r,i=arguments,o=T(t),u=1;u \ No newline at end of file + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/frontend.html.gz b/homeassistant/components/frontend/www_static/frontend.html.gz index 6c318038174..2790cc1ddf4 100644 Binary files a/homeassistant/components/frontend/www_static/frontend.html.gz and b/homeassistant/components/frontend/www_static/frontend.html.gz differ diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index 5159326a7b3..f3808ff4d44 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit 5159326a7b3d1ba29ae17a7861fa2eaa8c2c95f6 +Subproject commit f3808ff4d44733f5810e21131e0daa1425bf5f22 diff --git a/homeassistant/components/frontend/www_static/mdi.html b/homeassistant/components/frontend/www_static/mdi.html index 44dfedbfb39..438a682efaf 100644 --- a/homeassistant/components/frontend/www_static/mdi.html +++ b/homeassistant/components/frontend/www_static/mdi.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/mdi.html.gz b/homeassistant/components/frontend/www_static/mdi.html.gz index b540bf33f7d..50c4efec9e0 100644 Binary files a/homeassistant/components/frontend/www_static/mdi.html.gz and b/homeassistant/components/frontend/www_static/mdi.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-event.html b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-event.html index 531167d98a8..bbe752c7018 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-event.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-event.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-event.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-event.html.gz index 22fde6f1a5f..c4be6a82b4d 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-event.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-event.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-info.html b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-info.html index 0ecddfec1f3..5a7a699e237 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-info.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-info.html @@ -1,2 +1,2 @@ \ No newline at end of file + clear: both;white-space:pre-wrap}
About


Home Assistant
[[hass.config.core.version]]

Path to configuration.yaml: [[hass.config.core.config_dir]]

Developed by a bunch of awesome people.

Published under the MIT license
Source: serverfrontend-ui

Built using Python 3, Polymer [[polymerVersion]], Icons by Google and MaterialDesignIcons.com.

The following errors have been logged this session:

[[errorLog]]
\ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-info.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-info.html.gz index 8c840c85985..2a297bf2c7e 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-info.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-info.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html index c3068300cd2..bd9b5163382 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html.gz index 5bfed6cbac9..e9b1d4676fc 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-state.html b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-state.html index da96f77d3d4..52ea7fff18e 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-state.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-state.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-state.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-state.html.gz index ee640b9c7cb..0b8a61e0426 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-state.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-state.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html index e122c075f9d..abc6dd7c0b6 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html @@ -1,2 +1,2 @@ \ No newline at end of file + clear: both;white-space:pre-wrap}.rendered.error{color:red}
Templates

Templates are rendered using the Jinja2 template engine with some Home Assistant specific extensions.

[[processed]]
\ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html.gz index fcfef9681b5..1e281bb5322 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-history.html b/homeassistant/components/frontend/www_static/panels/ha-panel-history.html index ac1979c3cb1..71b823295a6 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-history.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-history.html @@ -1,4 +1,4 @@ \ No newline at end of file + */.pika-single{z-index:9999;display:block;position:relative;color:#333;background:#fff;border:1px solid #ccc;border-bottom-color:#bbb;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif}.pika-single:after,.pika-single:before{content:" ";display:table}.pika-single:after{clear:both}.pika-single.is-hidden{display:none}.pika-single.is-bound{position:absolute;box-shadow:0 5px 15px -5px rgba(0,0,0,.5)}.pika-lendar{float:left;width:240px;margin:8px}.pika-title{position:relative;text-align:center}.pika-label{display:inline-block;position:relative;z-index:9999;overflow:hidden;margin:0;padding:5px 3px;font-size:14px;line-height:20px;font-weight:700;background-color:#fff}.pika-title select{cursor:pointer;position:absolute;z-index:9998;margin:0;left:0;top:5px;filter:alpha(opacity=0);opacity:0}.pika-next,.pika-prev{display:block;cursor:pointer;position:relative;outline:0;border:0;padding:0;width:20px;height:30px;text-indent:20px;white-space:nowrap;overflow:hidden;background-color:transparent;background-position:center center;background-repeat:no-repeat;background-size:75% 75%;opacity:.5}.pika-next:hover,.pika-prev:hover{opacity:1}.is-rtl .pika-next,.pika-prev{float:left;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAYAAAAsEj5rAAAAUklEQVR42u3VMQoAIBADQf8Pgj+OD9hG2CtONJB2ymQkKe0HbwAP0xucDiQWARITIDEBEnMgMQ8S8+AqBIl6kKgHiXqQqAeJepBo/z38J/U0uAHlaBkBl9I4GwAAAABJRU5ErkJggg==)}.is-rtl .pika-prev,.pika-next{float:right;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAYAAAAsEj5rAAAAU0lEQVR42u3VOwoAMAgE0dwfAnNjU26bYkBCFGwfiL9VVWoO+BJ4Gf3gtsEKKoFBNTCoCAYVwaAiGNQGMUHMkjGbgjk2mIONuXo0nC8XnCf1JXgArVIZAQh5TKYAAAAASUVORK5CYII=)}.pika-next.is-disabled,.pika-prev.is-disabled{cursor:default;opacity:.2}.pika-select{display:inline-block}.pika-table{width:100%;border-collapse:collapse;border-spacing:0;border:0}.pika-table td,.pika-table th{width:14.285714285714286%;padding:0}.pika-table th{color:#999;font-size:12px;line-height:25px;font-weight:700;text-align:center}.pika-button{cursor:pointer;display:block;box-sizing:border-box;-moz-box-sizing:border-box;outline:0;border:0;margin:0;width:100%;padding:5px;color:#666;font-size:12px;line-height:15px;text-align:right;background:#f5f5f5}.pika-week{font-size:11px;color:#999}.is-today .pika-button{color:#3af;font-weight:700}.is-selected .pika-button{color:#fff;font-weight:700;background:#3af;box-shadow:inset 0 1px 3px #178fe5;border-radius:3px}.is-inrange .pika-button{background:#D5E9F7}.is-startrange .pika-button{color:#fff;background:#6CB31D;box-shadow:none;border-radius:3px}.is-endrange .pika-button{color:#fff;background:#3af;box-shadow:none;border-radius:3px}.is-disabled .pika-button,.is-outside-current-month .pika-button{pointer-events:none;cursor:default;color:#999;opacity:.3}.pika-button:hover{color:#fff;background:#ff8000;box-shadow:none;border-radius:3px}.pika-table abbr{border-bottom:none;cursor:help}} \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-history.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-history.html.gz index 589cf52692c..0c3d57acb41 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-history.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-history.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-iframe.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-iframe.html.gz index 974200ba1f7..95751cabbf8 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-iframe.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-iframe.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-logbook.html b/homeassistant/components/frontend/www_static/panels/ha-panel-logbook.html index c3fd949967a..7b5c0f6d33c 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-logbook.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-logbook.html @@ -1,4 +1,4 @@ \ No newline at end of file + */.pika-single{z-index:9999;display:block;position:relative;color:#333;background:#fff;border:1px solid #ccc;border-bottom-color:#bbb;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif}.pika-single:after,.pika-single:before{content:" ";display:table}.pika-single:after{clear:both}.pika-single.is-hidden{display:none}.pika-single.is-bound{position:absolute;box-shadow:0 5px 15px -5px rgba(0,0,0,.5)}.pika-lendar{float:left;width:240px;margin:8px}.pika-title{position:relative;text-align:center}.pika-label{display:inline-block;position:relative;z-index:9999;overflow:hidden;margin:0;padding:5px 3px;font-size:14px;line-height:20px;font-weight:700;background-color:#fff}.pika-title select{cursor:pointer;position:absolute;z-index:9998;margin:0;left:0;top:5px;filter:alpha(opacity=0);opacity:0}.pika-next,.pika-prev{display:block;cursor:pointer;position:relative;outline:0;border:0;padding:0;width:20px;height:30px;text-indent:20px;white-space:nowrap;overflow:hidden;background-color:transparent;background-position:center center;background-repeat:no-repeat;background-size:75% 75%;opacity:.5}.pika-next:hover,.pika-prev:hover{opacity:1}.is-rtl .pika-next,.pika-prev{float:left;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAYAAAAsEj5rAAAAUklEQVR42u3VMQoAIBADQf8Pgj+OD9hG2CtONJB2ymQkKe0HbwAP0xucDiQWARITIDEBEnMgMQ8S8+AqBIl6kKgHiXqQqAeJepBo/z38J/U0uAHlaBkBl9I4GwAAAABJRU5ErkJggg==)}.is-rtl .pika-prev,.pika-next{float:right;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAYAAAAsEj5rAAAAU0lEQVR42u3VOwoAMAgE0dwfAnNjU26bYkBCFGwfiL9VVWoO+BJ4Gf3gtsEKKoFBNTCoCAYVwaAiGNQGMUHMkjGbgjk2mIONuXo0nC8XnCf1JXgArVIZAQh5TKYAAAAASUVORK5CYII=)}.pika-next.is-disabled,.pika-prev.is-disabled{cursor:default;opacity:.2}.pika-select{display:inline-block}.pika-table{width:100%;border-collapse:collapse;border-spacing:0;border:0}.pika-table td,.pika-table th{width:14.285714285714286%;padding:0}.pika-table th{color:#999;font-size:12px;line-height:25px;font-weight:700;text-align:center}.pika-button{cursor:pointer;display:block;box-sizing:border-box;-moz-box-sizing:border-box;outline:0;border:0;margin:0;width:100%;padding:5px;color:#666;font-size:12px;line-height:15px;text-align:right;background:#f5f5f5}.pika-week{font-size:11px;color:#999}.is-today .pika-button{color:#3af;font-weight:700}.is-selected .pika-button{color:#fff;font-weight:700;background:#3af;box-shadow:inset 0 1px 3px #178fe5;border-radius:3px}.is-inrange .pika-button{background:#D5E9F7}.is-startrange .pika-button{color:#fff;background:#6CB31D;box-shadow:none;border-radius:3px}.is-endrange .pika-button{color:#fff;background:#3af;box-shadow:none;border-radius:3px}.is-disabled .pika-button,.is-outside-current-month .pika-button{pointer-events:none;cursor:default;color:#999;opacity:.3}.pika-button:hover{color:#fff;background:#ff8000;box-shadow:none;border-radius:3px}.pika-table abbr{border-bottom:none;cursor:help}} \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-logbook.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-logbook.html.gz index 2844f1ce8e0..c81ae0bc454 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-logbook.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-logbook.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-map.html b/homeassistant/components/frontend/www_static/panels/ha-panel-map.html index 42097a123a2..6af9a28c48b 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-map.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-map.html @@ -2,46 +2,14 @@ },_initPanes:function(){var t=this._panes={};this._paneRenderers={},this._mapPane=this.createPane("mapPane",this._container),o.DomUtil.setPosition(this._mapPane,new o.Point(0,0)),this.createPane("tilePane"),this.createPane("shadowPane"),this.createPane("overlayPane"),this.createPane("markerPane"),this.createPane("tooltipPane"),this.createPane("popupPane"),this.options.markerZoomAnimation||(o.DomUtil.addClass(t.markerPane,"leaflet-zoom-hide"),o.DomUtil.addClass(t.shadowPane,"leaflet-zoom-hide"))},_resetView:function(t,e){o.DomUtil.setPosition(this._mapPane,new o.Point(0,0));var i=!this._loaded;this._loaded=!0,e=this._limitZoom(e),this.fire("viewprereset");var n=this._zoom!==e;this._moveStart(n)._move(t,e)._moveEnd(n),this.fire("viewreset"),i&&this.fire("load")},_moveStart:function(t){return t&&this.fire("zoomstart"),this.fire("movestart")},_move:function(t,e,n){e===i&&(e=this._zoom);var o=this._zoom!==e;return this._zoom=e,this._lastCenter=t,this._pixelOrigin=this._getNewPixelOrigin(t),(o||n&&n.pinch)&&this.fire("zoom",n),this.fire("move",n)},_moveEnd:function(t){return t&&this.fire("zoomend"),this.fire("moveend")},_stop:function(){return o.Util.cancelAnimFrame(this._flyToFrame),this._panAnim&&this._panAnim.stop(),this},_rawPanBy:function(t){o.DomUtil.setPosition(this._mapPane,this._getMapPanePos().subtract(t))},_getZoomSpan:function(){return this.getMaxZoom()-this.getMinZoom()},_panInsideMaxBounds:function(){this._enforcingBounds||this.panInsideBounds(this.options.maxBounds)},_checkIfLoaded:function(){if(!this._loaded)throw new Error("Set map center and zoom first.")},_initEvents:function(e){if(o.DomEvent){this._targets={},this._targets[o.stamp(this._container)]=this;var i=e?"off":"on";o.DomEvent[i](this._container,"click dblclick mousedown mouseup mouseover mouseout mousemove contextmenu keypress",this._handleDOMEvent,this),this.options.trackResize&&o.DomEvent[i](t,"resize",this._onResize,this),o.Browser.any3d&&this.options.transform3DLimit&&this[i]("moveend",this._onMoveEnd)}},_onResize:function(){o.Util.cancelAnimFrame(this._resizeRequest),this._resizeRequest=o.Util.requestAnimFrame(function(){this.invalidateSize({debounceMoveend:!0})},this)},_onScroll:function(){this._container.scrollTop=0,this._container.scrollLeft=0},_onMoveEnd:function(){var t=this._getMapPanePos();Math.max(Math.abs(t.x),Math.abs(t.y))>=this.options.transform3DLimit&&this._resetView(this.getCenter(),this.getZoom())},_findEventTargets:function(t,e){for(var i,n=[],s="mouseout"===e||"mouseover"===e,r=t.target||t.srcElement,a=!1;r;){if(i=this._targets[o.stamp(r)],i&&("click"===e||"preclick"===e)&&!t._simulated&&this._draggableMoved(i)){a=!0;break}if(i&&i.listens(e,!0)){if(s&&!o.DomEvent._isExternalTarget(r,t))break;if(n.push(i),s)break}if(r===this._container)break;r=r.parentNode}return n.length||a||s||!o.DomEvent._isExternalTarget(r,t)||(n=[this]),n},_handleDOMEvent:function(t){if(this._loaded&&!o.DomEvent._skipped(t)){var e="keypress"===t.type&&13===t.keyCode?"click":t.type;"mousedown"===e&&o.DomUtil.preventOutline(t.target||t.srcElement),this._fireDOMEvent(t,e)}},_fireDOMEvent:function(t,e,i){if("click"===t.type){var n=o.Util.extend({},t);n.type="preclick",this._fireDOMEvent(n,n.type,i)}if(!t._stopped&&(i=(i||[]).concat(this._findEventTargets(t,e)),i.length)){var s=i[0];"contextmenu"===e&&s.listens(e,!0)&&o.DomEvent.preventDefault(t);var r={originalEvent:t};if("keypress"!==t.type){var a=s instanceof o.Marker;r.containerPoint=a?this.latLngToContainerPoint(s.getLatLng()):this.mouseEventToContainerPoint(t),r.layerPoint=this.containerPointToLayerPoint(r.containerPoint),r.latlng=a?s.getLatLng():this.layerPointToLatLng(r.layerPoint)}for(var h=0;h0?Math.round(t-e)/2:Math.max(0,Math.ceil(t))-Math.max(0,Math.floor(e))},_limitZoom:function(t){var e=this.getMinZoom(),i=this.getMaxZoom(),n=o.Browser.any3d?this.options.zoomSnap:1;return n&&(t=Math.round(t/n)*n),Math.max(e,Math.min(i,t))},_onPanTransitionStep:function(){this.fire("move")},_onPanTransitionEnd:function(){o.DomUtil.removeClass(this._mapPane,"leaflet-pan-anim"),this.fire("moveend")},_tryAnimatedPan:function(t,e){var i=this._getCenterOffset(t)._floor();return!((e&&e.animate)!==!0&&!this.getSize().contains(i)||(this.panBy(i,e),0))},_createAnimProxy:function(){var t=this._proxy=o.DomUtil.create("div","leaflet-proxy leaflet-zoom-animated");this._panes.mapPane.appendChild(t),this.on("zoomanim",function(e){var i=o.DomUtil.TRANSFORM,n=t.style[i];o.DomUtil.setTransform(t,this.project(e.center,e.zoom),this.getZoomScale(e.zoom,1)),n===t.style[i]&&this._animatingZoom&&this._onZoomTransitionEnd()},this),this.on("load moveend",function(){var e=this.getCenter(),i=this.getZoom();o.DomUtil.setTransform(t,this.project(e,i),this.getZoomScale(i,1))},this)},_catchTransitionEnd:function(t){this._animatingZoom&&t.propertyName.indexOf("transform")>=0&&this._onZoomTransitionEnd()},_nothingToAnimate:function(){return!this._container.getElementsByClassName("leaflet-zoom-animated").length},_tryAnimatedZoom:function(t,e,i){if(this._animatingZoom)return!0;if(i=i||{},!this._zoomAnimated||i.animate===!1||this._nothingToAnimate()||Math.abs(e-this._zoom)>this.options.zoomAnimationThreshold)return!1;var n=this.getZoomScale(e),s=this._getCenterOffset(t)._divideBy(1-1/n);return!(i.animate!==!0&&!this.getSize().contains(s)||(o.Util.requestAnimFrame(function(){this._moveStart(!0)._animateZoom(t,e,!0)},this),0))},_animateZoom:function(t,e,i,n){i&&(this._animatingZoom=!0,this._animateToCenter=t,this._animateToZoom=e,o.DomUtil.addClass(this._mapPane,"leaflet-zoom-anim")),this.fire("zoomanim",{center:t,zoom:e,noUpdate:n}),setTimeout(o.bind(this._onZoomTransitionEnd,this),250)},_onZoomTransitionEnd:function(){this._animatingZoom&&(o.DomUtil.removeClass(this._mapPane,"leaflet-zoom-anim"),this._animatingZoom=!1,this._move(this._animateToCenter,this._animateToZoom),o.Util.requestAnimFrame(function(){this._moveEnd(!0)},this))}}),o.map=function(t,e){return new o.Map(t,e)},o.Layer=o.Evented.extend({options:{pane:"overlayPane",nonBubblingEvents:[],attribution:null},addTo:function(t){return t.addLayer(this),this},remove:function(){return this.removeFrom(this._map||this._mapToAdd)},removeFrom:function(t){return t&&t.removeLayer(this),this},getPane:function(t){return this._map.getPane(t?this.options[t]||t:this.options.pane)},addInteractiveTarget:function(t){return this._map._targets[o.stamp(t)]=this,this},removeInteractiveTarget:function(t){return delete this._map._targets[o.stamp(t)],this},getAttribution:function(){return this.options.attribution},_layerAdd:function(t){var e=t.target;if(e.hasLayer(this)){if(this._map=e,this._zoomAnimated=e._zoomAnimated,this.getEvents){var i=this.getEvents();e.on(i,this),this.once("remove",function(){e.off(i,this)},this)}this.onAdd(e),this.getAttribution&&this._map.attributionControl&&this._map.attributionControl.addAttribution(this.getAttribution()),this.fire("add"),e.fire("layeradd",{layer:this})}}}),o.Map.include({addLayer:function(t){var e=o.stamp(t);return this._layers[e]?this:(this._layers[e]=t,t._mapToAdd=this,t.beforeAdd&&t.beforeAdd(this),this.whenReady(t._layerAdd,t),this)},removeLayer:function(t){var e=o.stamp(t);return this._layers[e]?(this._loaded&&t.onRemove(this),t.getAttribution&&this.attributionControl&&this.attributionControl.removeAttribution(t.getAttribution()),delete this._layers[e],this._loaded&&(this.fire("layerremove",{layer:t}),t.fire("remove")),t._map=t._mapToAdd=null,this):this},hasLayer:function(t){return!!t&&o.stamp(t)in this._layers},eachLayer:function(t,e){for(var i in this._layers)t.call(e,this._layers[i]);return this},_addLayers:function(t){t=t?o.Util.isArray(t)?t:[t]:[];for(var e=0,i=t.length;ethis._layersMaxZoom&&this.setZoom(this._layersMaxZoom),this.options.minZoom===i&&this._layersMinZoom&&this.getZoom()100&&n<500||t.target._simulatedClick&&!t._simulated?void o.DomEvent.stop(t):(o.DomEvent._lastClick=i,void e(t))}},o.DomEvent.addListener=o.DomEvent.on,o.DomEvent.removeListener=o.DomEvent.off,o.PosAnimation=o.Evented.extend({run:function(t,e,i,n){this.stop(),this._el=t,this._inProgress=!0,this._duration=i||.25,this._easeOutPower=1/Math.max(n||.5,.2),this._startPos=o.DomUtil.getPosition(t),this._offset=e.subtract(this._startPos),this._startTime=+new Date,this.fire("start"),this._animate()},stop:function(){this._inProgress&&(this._step(!0),this._complete())},_animate:function(){this._animId=o.Util.requestAnimFrame(this._animate,this),this._step()},_step:function(t){var e=+new Date-this._startTime,i=1e3*this._duration;e1e-7;l++)e=r*Math.sin(h),e=Math.pow((1-e)/(1+e),r/2),u=Math.PI/2-2*Math.atan(a*e)-h,h+=u;return new o.LatLng(h*i,t.x*i/n)}},o.CRS.EPSG3395=o.extend({},o.CRS.Earth,{code:"EPSG:3395",projection:o.Projection.Mercator,transformation:function(){var t=.5/(Math.PI*o.Projection.Mercator.R);return new o.Transformation(t,.5,-t,.5)}()}),o.GridLayer=o.Layer.extend({options:{tileSize:256,opacity:1,updateWhenIdle:o.Browser.mobile,updateWhenZooming:!0,updateInterval:200,zIndex:1,bounds:null,minZoom:0,maxZoom:i,noWrap:!1,pane:"tilePane",className:"",keepBuffer:2},initialize:function(t){o.setOptions(this,t)},onAdd:function(){this._initContainer(),this._levels={},this._tiles={},this._resetView(),this._update()},beforeAdd:function(t){t._addZoomLimit(this)},onRemove:function(t){this._removeAllTiles(),o.DomUtil.remove(this._container),t._removeZoomLimit(this),this._container=null,this._tileZoom=null},bringToFront:function(){return this._map&&(o.DomUtil.toFront(this._container),this._setAutoZIndex(Math.max)),this},bringToBack:function(){return this._map&&(o.DomUtil.toBack(this._container),this._setAutoZIndex(Math.min)),this},getContainer:function(){return this._container},setOpacity:function(t){return this.options.opacity=t,this._updateOpacity(),this},setZIndex:function(t){return this.options.zIndex=t,this._updateZIndex(),this},isLoading:function(){return this._loading},redraw:function(){return this._map&&(this._removeAllTiles(),this._update()),this},getEvents:function(){var t={viewprereset:this._invalidateAll,viewreset:this._resetView,zoom:this._resetView,moveend:this._onMoveEnd};return this.options.updateWhenIdle||(this._onMove||(this._onMove=o.Util.throttle(this._onMoveEnd,this.options.updateInterval,this)),t.move=this._onMove),this._zoomAnimated&&(t.zoomanim=this._animateZoom),t},createTile:function(){return e.createElement("div")},getTileSize:function(){var t=this.options.tileSize;return t instanceof o.Point?t:new o.Point(t,t)},_updateZIndex:function(){this._container&&this.options.zIndex!==i&&null!==this.options.zIndex&&(this._container.style.zIndex=this.options.zIndex)},_setAutoZIndex:function(t){for(var e,i=this.getPane().children,n=-t(-(1/0),1/0),o=0,s=i.length;othis.options.maxZoom||in&&this._retainParent(s,r,a,n))},_retainChildren:function(t,e,i,n){for(var s=2*t;s<2*t+2;s++)for(var r=2*e;r<2*e+2;r++){var a=new o.Point(s,r);a.z=i+1;var h=this._tileCoordsToKey(a),l=this._tiles[h];l&&l.active?l.retain=!0:(l&&l.loaded&&(l.retain=!0),i+1this.options.maxZoom||this.options.minZoom!==i&&s1)return void this._setView(t,s);for(var m=a.min.y;m<=a.max.y;m++)for(var p=a.min.x;p<=a.max.x;p++){var f=new o.Point(p,m);if(f.z=this._tileZoom,this._isValidTile(f)){var g=this._tiles[this._tileCoordsToKey(f)];g?g.current=!0:l.push(f)}}if(l.sort(function(t,e){return t.distanceTo(h)-e.distanceTo(h)}),0!==l.length){this._loading||(this._loading=!0,this.fire("loading"));var v=e.createDocumentFragment();for(p=0;pi.max.x)||!e.wrapLat&&(t.yi.max.y))return!1}if(!this.options.bounds)return!0;var n=this._tileCoordsToBounds(t);return o.latLngBounds(this.options.bounds).overlaps(n)},_keyToBounds:function(t){return this._tileCoordsToBounds(this._keyToTileCoords(t))},_tileCoordsToBounds:function(t){var e=this._map,i=this.getTileSize(),n=t.scaleBy(i),s=n.add(i),r=e.unproject(n,t.z),a=e.unproject(s,t.z);return this.options.noWrap||(r=e.wrapLatLng(r),a=e.wrapLatLng(a)),new o.LatLngBounds(r,a)},_tileCoordsToKey:function(t){return t.x+":"+t.y+":"+t.z},_keyToTileCoords:function(t){var e=t.split(":"),i=new o.Point(+e[0],+e[1]);return i.z=+e[2],i},_removeTile:function(t){var e=this._tiles[t];e&&(o.DomUtil.remove(e.el),delete this._tiles[t],this.fire("tileunload",{tile:e.el,coords:this._keyToTileCoords(t)}))},_initTile:function(t){o.DomUtil.addClass(t,"leaflet-tile");var e=this.getTileSize();t.style.width=e.x+"px",t.style.height=e.y+"px",t.onselectstart=o.Util.falseFn,t.onmousemove=o.Util.falseFn,o.Browser.ielt9&&this.options.opacity<1&&o.DomUtil.setOpacity(t,this.options.opacity),o.Browser.android&&!o.Browser.android23&&(t.style.WebkitBackfaceVisibility="hidden")},_addTile:function(t,e){var i=this._getTilePos(t),n=this._tileCoordsToKey(t),s=this.createTile(this._wrapCoords(t),o.bind(this._tileReady,this,t));this._initTile(s),this.createTile.length<2&&o.Util.requestAnimFrame(o.bind(this._tileReady,this,t,null,s)),o.DomUtil.setPosition(s,i),this._tiles[n]={el:s,coords:t,current:!0},e.appendChild(s),this.fire("tileloadstart",{tile:s,coords:t})},_tileReady:function(t,e,i){if(this._map){e&&this.fire("tileerror",{error:e,tile:i,coords:t});var n=this._tileCoordsToKey(t);i=this._tiles[n],i&&(i.loaded=+new Date,this._map._fadeAnimated?(o.DomUtil.setOpacity(i.el,0),o.Util.cancelAnimFrame(this._fadeFrame),this._fadeFrame=o.Util.requestAnimFrame(this._updateOpacity,this)):(i.active=!0,this._pruneTiles()),e||(o.DomUtil.addClass(i.el,"leaflet-tile-loaded"),this.fire("tileload",{tile:i.el,coords:t})),this._noTilesToLoad()&&(this._loading=!1,this.fire("load"),o.Browser.ielt9||!this._map._fadeAnimated?o.Util.requestAnimFrame(this._pruneTiles,this):setTimeout(o.bind(this._pruneTiles,this),250)))}},_getTilePos:function(t){return t.scaleBy(this.getTileSize()).subtract(this._level.origin)},_wrapCoords:function(t){var e=new o.Point(this._wrapX?o.Util.wrapNum(t.x,this._wrapX):t.x,this._wrapY?o.Util.wrapNum(t.y,this._wrapY):t.y);return e.z=t.z,e},_pxBoundsToTileRange:function(t){var e=this.getTileSize();return new o.Bounds(t.min.unscaleBy(e).floor(),t.max.unscaleBy(e).ceil().subtract([1,1]))},_noTilesToLoad:function(){for(var t in this._tiles)if(!this._tiles[t].loaded)return!1;return!0}}),o.gridLayer=function(t){return new o.GridLayer(t)},o.TileLayer=o.GridLayer.extend({options:{minZoom:0,maxZoom:18,maxNativeZoom:null,minNativeZoom:null,subdomains:"abc",errorTileUrl:"",zoomOffset:0,tms:!1,zoomReverse:!1,detectRetina:!1,crossOrigin:!1},initialize:function(t,e){this._url=t,e=o.setOptions(this,e),e.detectRetina&&o.Browser.retina&&e.maxZoom>0&&(e.tileSize=Math.floor(e.tileSize/2),e.zoomReverse?(e.zoomOffset--,e.minZoom++):(e.zoomOffset++,e.maxZoom--),e.minZoom=Math.max(0,e.minZoom)),"string"==typeof e.subdomains&&(e.subdomains=e.subdomains.split("")),o.Browser.android||this.on("tileunload",this._onTileRemove)},setUrl:function(t,e){return this._url=t,e||this.redraw(),this},createTile:function(t,i){var n=e.createElement("img");return o.DomEvent.on(n,"load",o.bind(this._tileOnLoad,this,i,n)),o.DomEvent.on(n,"error",o.bind(this._tileOnError,this,i,n)),this.options.crossOrigin&&(n.crossOrigin=""),n.alt="",n.setAttribute("role","presentation"),n.src=this.getTileUrl(t),n},getTileUrl:function(t){var e={r:o.Browser.retina?"@2x":"",s:this._getSubdomain(t),x:t.x,y:t.y,z:this._getZoomForUrl()};if(this._map&&!this._map.options.crs.infinite){var i=this._globalTileRange.max.y-t.y;this.options.tms&&(e.y=i),e["-y"]=i}return o.Util.template(this._url,o.extend(e,this.options))},_tileOnLoad:function(t,e){o.Browser.ielt9?setTimeout(o.bind(t,this,null,e),0):t(null,e)},_tileOnError:function(t,e,i){var n=this.options.errorTileUrl;n&&(e.src=n),t(i,e)},getTileSize:function(){var t=this._map,e=o.GridLayer.prototype.getTileSize.call(this),i=this._tileZoom+this.options.zoomOffset,n=this.options.minNativeZoom,s=this.options.maxNativeZoom;return null!==n&&is?e.divideBy(t.getZoomScale(s,i)).round():e},_onTileRemove:function(t){t.tile.onload=null},_getZoomForUrl:function(){var t=this._tileZoom,e=this.options.maxZoom,i=this.options.zoomReverse,n=this.options.zoomOffset,o=this.options.minNativeZoom,s=this.options.maxNativeZoom;return i&&(t=e-t),t+=n,null!==o&&ts?s:t},_getSubdomain:function(t){var e=Math.abs(t.x+t.y)%this.options.subdomains.length;return this.options.subdomains[e]},_abortLoading:function(){var t,e;for(t in this._tiles)this._tiles[t].coords.z!==this._tileZoom&&(e=this._tiles[t].el,e.onload=o.Util.falseFn,e.onerror=o.Util.falseFn,e.complete||(e.src=o.Util.emptyImageUrl,o.DomUtil.remove(e)))}}),o.tileLayer=function(t,e){return new o.TileLayer(t,e)},o.TileLayer.WMS=o.TileLayer.extend({defaultWmsParams:{service:"WMS",request:"GetMap",layers:"",styles:"",format:"image/jpeg",transparent:!1,version:"1.1.1"},options:{crs:null,uppercase:!1},initialize:function(t,e){this._url=t;var i=o.extend({},this.defaultWmsParams);for(var n in e)n in this.options||(i[n]=e[n]);e=o.setOptions(this,e),i.width=i.height=e.tileSize*(e.detectRetina&&o.Browser.retina?2:1),this.wmsParams=i},onAdd:function(t){this._crs=this.options.crs||t.options.crs,this._wmsVersion=parseFloat(this.wmsParams.version);var e=this._wmsVersion>=1.3?"crs":"srs";this.wmsParams[e]=this._crs.code,o.TileLayer.prototype.onAdd.call(this,t)},getTileUrl:function(t){var e=this._tileCoordsToBounds(t),i=this._crs.project(e.getNorthWest()),n=this._crs.project(e.getSouthEast()),s=(this._wmsVersion>=1.3&&this._crs===o.CRS.EPSG4326?[n.y,i.x,i.y,n.x]:[i.x,n.y,n.x,i.y]).join(","),r=o.TileLayer.prototype.getTileUrl.call(this,t);return r+o.Util.getParamString(this.wmsParams,r,this.options.uppercase)+(this.options.uppercase?"&BBOX=":"&bbox=")+s},setParams:function(t,e){return o.extend(this.wmsParams,t),e||this.redraw(),this}}),o.tileLayer.wms=function(t,e){return new o.TileLayer.WMS(t,e)},o.ImageOverlay=o.Layer.extend({options:{opacity:1,alt:"",interactive:!1,crossOrigin:!1},initialize:function(t,e,i){this._url=t,this._bounds=o.latLngBounds(e),o.setOptions(this,i)},onAdd:function(){this._image||(this._initImage(),this.options.opacity<1&&this._updateOpacity()),this.options.interactive&&(o.DomUtil.addClass(this._image,"leaflet-interactive"),this.addInteractiveTarget(this._image)),this.getPane().appendChild(this._image),this._reset()},onRemove:function(){o.DomUtil.remove(this._image),this.options.interactive&&this.removeInteractiveTarget(this._image)},setOpacity:function(t){return this.options.opacity=t,this._image&&this._updateOpacity(),this},setStyle:function(t){return t.opacity&&this.setOpacity(t.opacity),this},bringToFront:function(){return this._map&&o.DomUtil.toFront(this._image),this},bringToBack:function(){return this._map&&o.DomUtil.toBack(this._image),this},setUrl:function(t){return this._url=t,this._image&&(this._image.src=t),this},setBounds:function(t){return this._bounds=t,this._map&&this._reset(),this},getEvents:function(){var t={zoom:this._reset,viewreset:this._reset};return this._zoomAnimated&&(t.zoomanim=this._animateZoom),t},getBounds:function(){return this._bounds},getElement:function(){return this._image},_initImage:function(){var t=this._image=o.DomUtil.create("img","leaflet-image-layer "+(this._zoomAnimated?"leaflet-zoom-animated":""));t.onselectstart=o.Util.falseFn,t.onmousemove=o.Util.falseFn,t.onload=o.bind(this.fire,this,"load"),this.options.crossOrigin&&(t.crossOrigin=""),t.src=this._url,t.alt=this.options.alt},_animateZoom:function(t){var e=this._map.getZoomScale(t.zoom),i=this._map._latLngBoundsToNewLayerBounds(this._bounds,t.zoom,t.center).min;o.DomUtil.setTransform(this._image,i,e)},_reset:function(){var t=this._image,e=new o.Bounds(this._map.latLngToLayerPoint(this._bounds.getNorthWest()),this._map.latLngToLayerPoint(this._bounds.getSouthEast())),i=e.getSize();o.DomUtil.setPosition(t,e.min),t.style.width=i.x+"px",t.style.height=i.y+"px"},_updateOpacity:function(){o.DomUtil.setOpacity(this._image,this.options.opacity); }}),o.imageOverlay=function(t,e,i){return new o.ImageOverlay(t,e,i)},o.Icon=o.Class.extend({initialize:function(t){o.setOptions(this,t)},createIcon:function(t){return this._createIcon("icon",t)},createShadow:function(t){return this._createIcon("shadow",t)},_createIcon:function(t,e){var i=this._getIconUrl(t);if(!i){if("icon"===t)throw new Error("iconUrl not set in Icon options (see the docs).");return null}var n=this._createImg(i,e&&"IMG"===e.tagName?e:null);return this._setIconStyles(n,t),n},_setIconStyles:function(t,e){var i=this.options,n=i[e+"Size"];"number"==typeof n&&(n=[n,n]);var s=o.point(n),r=o.point("shadow"===e&&i.shadowAnchor||i.iconAnchor||s&&s.divideBy(2,!0));t.className="leaflet-marker-"+e+" "+(i.className||""),r&&(t.style.marginLeft=-r.x+"px",t.style.marginTop=-r.y+"px"),s&&(t.style.width=s.x+"px",t.style.height=s.y+"px")},_createImg:function(t,i){return i=i||e.createElement("img"),i.src=t,i},_getIconUrl:function(t){return o.Browser.retina&&this.options[t+"RetinaUrl"]||this.options[t+"Url"]}}),o.icon=function(t){return new o.Icon(t)},o.Icon.Default=o.Icon.extend({options:{iconUrl:"marker-icon.png",iconRetinaUrl:"marker-icon-2x.png",shadowUrl:"marker-shadow.png",iconSize:[25,41],iconAnchor:[12,41],popupAnchor:[1,-34],tooltipAnchor:[16,-28],shadowSize:[41,41]},_getIconUrl:function(t){return o.Icon.Default.imagePath||(o.Icon.Default.imagePath=this._detectIconPath()),(this.options.imagePath||o.Icon.Default.imagePath)+o.Icon.prototype._getIconUrl.call(this,t)},_detectIconPath:function(){var t=o.DomUtil.create("div","leaflet-default-icon-path",e.body),i=o.DomUtil.getStyle(t,"background-image")||o.DomUtil.getStyle(t,"backgroundImage");return e.body.removeChild(t),0===i.indexOf("url")?i.replace(/^url\([\"\']?/,"").replace(/marker-icon\.png[\"\']?\)$/,""):""}}),o.Marker=o.Layer.extend({options:{icon:new o.Icon.Default,interactive:!0,draggable:!1,keyboard:!0,title:"",alt:"",zIndexOffset:0,opacity:1,riseOnHover:!1,riseOffset:250,pane:"markerPane",nonBubblingEvents:["click","dblclick","mouseover","mouseout","contextmenu"]},initialize:function(t,e){o.setOptions(this,e),this._latlng=o.latLng(t)},onAdd:function(t){this._zoomAnimated=this._zoomAnimated&&t.options.markerZoomAnimation,this._zoomAnimated&&t.on("zoomanim",this._animateZoom,this),this._initIcon(),this.update()},onRemove:function(t){this.dragging&&this.dragging.enabled()&&(this.options.draggable=!0,this.dragging.removeHooks()),this._zoomAnimated&&t.off("zoomanim",this._animateZoom,this),this._removeIcon(),this._removeShadow()},getEvents:function(){return{zoom:this.update,viewreset:this.update}},getLatLng:function(){return this._latlng},setLatLng:function(t){var e=this._latlng;return this._latlng=o.latLng(t),this.update(),this.fire("move",{oldLatLng:e,latlng:this._latlng})},setZIndexOffset:function(t){return this.options.zIndexOffset=t,this.update()},setIcon:function(t){return this.options.icon=t,this._map&&(this._initIcon(),this.update()),this._popup&&this.bindPopup(this._popup,this._popup.options),this},getElement:function(){return this._icon},update:function(){if(this._icon){var t=this._map.latLngToLayerPoint(this._latlng).round();this._setPos(t)}return this},_initIcon:function(){var t=this.options,e="leaflet-zoom-"+(this._zoomAnimated?"animated":"hide"),i=t.icon.createIcon(this._icon),n=!1;i!==this._icon&&(this._icon&&this._removeIcon(),n=!0,t.title&&(i.title=t.title),t.alt&&(i.alt=t.alt)),o.DomUtil.addClass(i,e),t.keyboard&&(i.tabIndex="0"),this._icon=i,t.riseOnHover&&this.on({mouseover:this._bringToFront,mouseout:this._resetZIndex});var s=t.icon.createShadow(this._shadow),r=!1;s!==this._shadow&&(this._removeShadow(),r=!0),s&&o.DomUtil.addClass(s,e),this._shadow=s,t.opacity<1&&this._updateOpacity(),n&&this.getPane().appendChild(this._icon),this._initInteraction(),s&&r&&this.getPane("shadowPane").appendChild(this._shadow)},_removeIcon:function(){this.options.riseOnHover&&this.off({mouseover:this._bringToFront,mouseout:this._resetZIndex}),o.DomUtil.remove(this._icon),this.removeInteractiveTarget(this._icon),this._icon=null},_removeShadow:function(){this._shadow&&o.DomUtil.remove(this._shadow),this._shadow=null},_setPos:function(t){o.DomUtil.setPosition(this._icon,t),this._shadow&&o.DomUtil.setPosition(this._shadow,t),this._zIndex=t.y+this.options.zIndexOffset,this._resetZIndex()},_updateZIndex:function(t){this._icon.style.zIndex=this._zIndex+t},_animateZoom:function(t){var e=this._map._latLngToNewLayerPoint(this._latlng,t.zoom,t.center).round();this._setPos(e)},_initInteraction:function(){if(this.options.interactive&&(o.DomUtil.addClass(this._icon,"leaflet-interactive"),this.addInteractiveTarget(this._icon),o.Handler.MarkerDrag)){var t=this.options.draggable;this.dragging&&(t=this.dragging.enabled(),this.dragging.disable()),this.dragging=new o.Handler.MarkerDrag(this),t&&this.dragging.enable()}},setOpacity:function(t){return this.options.opacity=t,this._map&&this._updateOpacity(),this},_updateOpacity:function(){var t=this.options.opacity;o.DomUtil.setOpacity(this._icon,t),this._shadow&&o.DomUtil.setOpacity(this._shadow,t)},_bringToFront:function(){this._updateZIndex(this.options.riseOffset)},_resetZIndex:function(){this._updateZIndex(0)},_getPopupAnchor:function(){return this.options.icon.options.popupAnchor||[0,0]},_getTooltipAnchor:function(){return this.options.icon.options.tooltipAnchor||[0,0]}}),o.marker=function(t,e){return new o.Marker(t,e)},o.DivIcon=o.Icon.extend({options:{iconSize:[12,12],html:!1,bgPos:null,className:"leaflet-div-icon"},createIcon:function(t){var i=t&&"DIV"===t.tagName?t:e.createElement("div"),n=this.options;if(i.innerHTML=n.html!==!1?n.html:"",n.bgPos){var s=o.point(n.bgPos);i.style.backgroundPosition=-s.x+"px "+-s.y+"px"}return this._setIconStyles(i,"icon"),i},createShadow:function(){return null}}),o.divIcon=function(t){return new o.DivIcon(t)},o.DivOverlay=o.Layer.extend({options:{offset:[0,7],className:"",pane:"popupPane"},initialize:function(t,e){o.setOptions(this,t),this._source=e},onAdd:function(t){this._zoomAnimated=t._zoomAnimated,this._container||this._initLayout(),t._fadeAnimated&&o.DomUtil.setOpacity(this._container,0),clearTimeout(this._removeTimeout),this.getPane().appendChild(this._container),this.update(),t._fadeAnimated&&o.DomUtil.setOpacity(this._container,1),this.bringToFront()},onRemove:function(t){t._fadeAnimated?(o.DomUtil.setOpacity(this._container,0),this._removeTimeout=setTimeout(o.bind(o.DomUtil.remove,o.DomUtil,this._container),200)):o.DomUtil.remove(this._container)},getLatLng:function(){return this._latlng},setLatLng:function(t){return this._latlng=o.latLng(t),this._map&&(this._updatePosition(),this._adjustPan()),this},getContent:function(){return this._content},setContent:function(t){return this._content=t,this.update(),this},getElement:function(){return this._container},update:function(){this._map&&(this._container.style.visibility="hidden",this._updateContent(),this._updateLayout(),this._updatePosition(),this._container.style.visibility="",this._adjustPan())},getEvents:function(){var t={zoom:this._updatePosition,viewreset:this._updatePosition};return this._zoomAnimated&&(t.zoomanim=this._animateZoom),t},isOpen:function(){return!!this._map&&this._map.hasLayer(this)},bringToFront:function(){return this._map&&o.DomUtil.toFront(this._container),this},bringToBack:function(){return this._map&&o.DomUtil.toBack(this._container),this},_updateContent:function(){if(this._content){var t=this._contentNode,e="function"==typeof this._content?this._content(this._source||this):this._content;if("string"==typeof e)t.innerHTML=e;else{for(;t.hasChildNodes();)t.removeChild(t.firstChild);t.appendChild(e)}this.fire("contentupdate")}},_updatePosition:function(){if(this._map){var t=this._map.latLngToLayerPoint(this._latlng),e=o.point(this.options.offset),i=this._getAnchor();this._zoomAnimated?o.DomUtil.setPosition(this._container,t.add(i)):e=e.add(t).add(i);var n=this._containerBottom=-e.y,s=this._containerLeft=-Math.round(this._containerWidth/2)+e.x;this._container.style.bottom=n+"px",this._container.style.left=s+"px"}},_getAnchor:function(){return[0,0]}}),o.Popup=o.DivOverlay.extend({options:{maxWidth:300,minWidth:50,maxHeight:null,autoPan:!0,autoPanPaddingTopLeft:null,autoPanPaddingBottomRight:null,autoPanPadding:[5,5],keepInView:!1,closeButton:!0,autoClose:!0,className:""},openOn:function(t){return t.openPopup(this),this},onAdd:function(t){o.DivOverlay.prototype.onAdd.call(this,t),t.fire("popupopen",{popup:this}),this._source&&(this._source.fire("popupopen",{popup:this},!0),this._source instanceof o.Path||this._source.on("preclick",o.DomEvent.stopPropagation))},onRemove:function(t){o.DivOverlay.prototype.onRemove.call(this,t),t.fire("popupclose",{popup:this}),this._source&&(this._source.fire("popupclose",{popup:this},!0),this._source instanceof o.Path||this._source.off("preclick",o.DomEvent.stopPropagation))},getEvents:function(){var t=o.DivOverlay.prototype.getEvents.call(this);return("closeOnClick"in this.options?this.options.closeOnClick:this._map.options.closePopupOnClick)&&(t.preclick=this._close),this.options.keepInView&&(t.moveend=this._adjustPan),t},_close:function(){this._map&&this._map.closePopup(this)},_initLayout:function(){var t="leaflet-popup",e=this._container=o.DomUtil.create("div",t+" "+(this.options.className||"")+" leaflet-zoom-animated");if(this.options.closeButton){var i=this._closeButton=o.DomUtil.create("a",t+"-close-button",e);i.href="#close",i.innerHTML="×",o.DomEvent.on(i,"click",this._onCloseButtonClick,this)}var n=this._wrapper=o.DomUtil.create("div",t+"-content-wrapper",e);this._contentNode=o.DomUtil.create("div",t+"-content",n),o.DomEvent.disableClickPropagation(n).disableScrollPropagation(this._contentNode).on(n,"contextmenu",o.DomEvent.stopPropagation),this._tipContainer=o.DomUtil.create("div",t+"-tip-container",e),this._tip=o.DomUtil.create("div",t+"-tip",this._tipContainer)},_updateLayout:function(){var t=this._contentNode,e=t.style;e.width="",e.whiteSpace="nowrap";var i=t.offsetWidth;i=Math.min(i,this.options.maxWidth),i=Math.max(i,this.options.minWidth),e.width=i+1+"px",e.whiteSpace="",e.height="";var n=t.offsetHeight,s=this.options.maxHeight,r="leaflet-popup-scrolled";s&&n>s?(e.height=s+"px",o.DomUtil.addClass(t,r)):o.DomUtil.removeClass(t,r),this._containerWidth=this._container.offsetWidth},_animateZoom:function(t){var e=this._map._latLngToNewLayerPoint(this._latlng,t.zoom,t.center),i=this._getAnchor();o.DomUtil.setPosition(this._container,e.add(i))},_adjustPan:function(){if(!(!this.options.autoPan||this._map._panAnim&&this._map._panAnim._inProgress)){var t=this._map,e=parseInt(o.DomUtil.getStyle(this._container,"marginBottom"),10)||0,i=this._container.offsetHeight+e,n=this._containerWidth,s=new o.Point(this._containerLeft,-i-this._containerBottom);s._add(o.DomUtil.getPosition(this._container));var r=t.layerPointToContainerPoint(s),a=o.point(this.options.autoPanPadding),h=o.point(this.options.autoPanPaddingTopLeft||a),l=o.point(this.options.autoPanPaddingBottomRight||a),u=t.getSize(),c=0,d=0;r.x+n+l.x>u.x&&(c=r.x+n-u.x+l.x),r.x-c-h.x<0&&(c=r.x-h.x),r.y+i+l.y>u.y&&(d=r.y+i-u.y+l.y),r.y-d-h.y<0&&(d=r.y-h.y),(c||d)&&t.fire("autopanstart").panBy([c,d])}},_onCloseButtonClick:function(t){this._close(),o.DomEvent.stop(t)},_getAnchor:function(){return o.point(this._source&&this._source._getPopupAnchor?this._source._getPopupAnchor():[0,0])}}),o.popup=function(t,e){return new o.Popup(t,e)},o.Map.mergeOptions({closePopupOnClick:!0}),o.Map.include({openPopup:function(t,e,i){return t instanceof o.Popup||(t=new o.Popup(i).setContent(t)),e&&t.setLatLng(e),this.hasLayer(t)?this:(this._popup&&this._popup.options.autoClose&&this.closePopup(),this._popup=t,this.addLayer(t))},closePopup:function(t){return t&&t!==this._popup||(t=this._popup,this._popup=null),t&&this.removeLayer(t),this}}),o.Layer.include({bindPopup:function(t,e){return t instanceof o.Popup?(o.setOptions(t,e),this._popup=t,t._source=this):(this._popup&&!e||(this._popup=new o.Popup(e,this)),this._popup.setContent(t)),this._popupHandlersAdded||(this.on({click:this._openPopup,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!0),this},unbindPopup:function(){return this._popup&&(this.off({click:this._openPopup,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!1,this._popup=null),this},openPopup:function(t,e){if(t instanceof o.Layer||(e=t,t=this),t instanceof o.FeatureGroup)for(var i in this._layers){t=this._layers[i];break}return e||(e=t.getCenter?t.getCenter():t.getLatLng()),this._popup&&this._map&&(this._popup._source=t,this._popup.update(),this._map.openPopup(this._popup,e)),this},closePopup:function(){return this._popup&&this._popup._close(),this},togglePopup:function(t){return this._popup&&(this._popup._map?this.closePopup():this.openPopup(t)),this},isPopupOpen:function(){return this._popup.isOpen()},setPopupContent:function(t){return this._popup&&this._popup.setContent(t),this},getPopup:function(){return this._popup},_openPopup:function(t){var e=t.layer||t.target;if(this._popup&&this._map)return o.DomEvent.stop(t),e instanceof o.Path?void this.openPopup(t.layer||t.target,t.latlng):void(this._map.hasLayer(this._popup)&&this._popup._source===e?this.closePopup():this.openPopup(e,t.latlng))},_movePopup:function(t){this._popup.setLatLng(t.latlng)}}),o.Tooltip=o.DivOverlay.extend({options:{pane:"tooltipPane",offset:[0,0],direction:"auto",permanent:!1,sticky:!1,interactive:!1,opacity:.9},onAdd:function(t){o.DivOverlay.prototype.onAdd.call(this,t),this.setOpacity(this.options.opacity),t.fire("tooltipopen",{tooltip:this}),this._source&&this._source.fire("tooltipopen",{tooltip:this},!0)},onRemove:function(t){o.DivOverlay.prototype.onRemove.call(this,t),t.fire("tooltipclose",{tooltip:this}),this._source&&this._source.fire("tooltipclose",{tooltip:this},!0)},getEvents:function(){var t=o.DivOverlay.prototype.getEvents.call(this);return o.Browser.touch&&!this.options.permanent&&(t.preclick=this._close),t},_close:function(){this._map&&this._map.closeTooltip(this)},_initLayout:function(){var t="leaflet-tooltip",e=t+" "+(this.options.className||"")+" leaflet-zoom-"+(this._zoomAnimated?"animated":"hide");this._contentNode=this._container=o.DomUtil.create("div",e)},_updateLayout:function(){},_adjustPan:function(){},_setPosition:function(t){var e=this._map,i=this._container,n=e.latLngToContainerPoint(e.getCenter()),s=e.layerPointToContainerPoint(t),r=this.options.direction,a=i.offsetWidth,h=i.offsetHeight,l=o.point(this.options.offset),u=this._getAnchor();"top"===r?t=t.add(o.point(-a/2+l.x,-h+l.y+u.y,!0)):"bottom"===r?t=t.subtract(o.point(a/2-l.x,-l.y,!0)):"center"===r?t=t.subtract(o.point(a/2+l.x,h/2-u.y+l.y,!0)):"right"===r||"auto"===r&&s.xh&&(s=r,h=a);h>i&&(e[s]=1,this._simplifyDPStep(t,e,i,n,s),this._simplifyDPStep(t,e,i,s,o))},_reducePoints:function(t,e){for(var i=[t[0]],n=1,o=0,s=t.length;ne&&(i.push(t[n]),o=n);return oe.max.x&&(i|=2),t.ye.max.y&&(i|=8),i},_sqDist:function(t,e){var i=e.x-t.x,n=e.y-t.y;return i*i+n*n},_sqClosestPointOnSegment:function(t,e,i,n){var s,r=e.x,a=e.y,h=i.x-r,l=i.y-a,u=h*h+l*l;return u>0&&(s=((t.x-r)*h+(t.y-a)*l)/u,s>1?(r=i.x,a=i.y):s>0&&(r+=h*s,a+=l*s)),h=t.x-r,l=t.y-a,n?h*h+l*l:new o.Point(r,a)}},o.Polyline=o.Path.extend({options:{smoothFactor:1,noClip:!1},initialize:function(t,e){o.setOptions(this,e),this._setLatLngs(t)},getLatLngs:function(){return this._latlngs},setLatLngs:function(t){return this._setLatLngs(t),this.redraw()},isEmpty:function(){return!this._latlngs.length},closestLayerPoint:function(t){for(var e,i,n=1/0,s=null,r=o.LineUtil._sqClosestPointOnSegment,a=0,h=this._parts.length;ae)return r=(n-e)/i,this._map.layerPointToLatLng([s.x-r*(s.x-o.x),s.y-r*(s.y-o.y)])},getBounds:function(){return this._bounds},addLatLng:function(t,e){return e=e||this._defaultShape(),t=o.latLng(t),e.push(t),this._bounds.extend(t),this.redraw()},_setLatLngs:function(t){this._bounds=new o.LatLngBounds,this._latlngs=this._convertLatLngs(t)},_defaultShape:function(){return o.Polyline._flat(this._latlngs)?this._latlngs:this._latlngs[0]},_convertLatLngs:function(t){for(var e=[],i=o.Polyline._flat(t),n=0,s=t.length;n=2&&e[0]instanceof o.LatLng&&e[0].equals(e[i-1])&&e.pop(),e},_setLatLngs:function(t){o.Polyline.prototype._setLatLngs.call(this,t),o.Polyline._flat(this._latlngs)&&(this._latlngs=[this._latlngs])},_defaultShape:function(){return o.Polyline._flat(this._latlngs[0])?this._latlngs[0]:this._latlngs[0][0]},_clipPoints:function(){var t=this._renderer._bounds,e=this.options.weight,i=new o.Point(e,e);if(t=new o.Bounds(t.min.subtract(i),t.max.add(i)),this._parts=[],this._pxBounds&&this._pxBounds.intersects(t)){if(this.options.noClip)return void(this._parts=this._rings);for(var n,s=0,r=this._rings.length;s';var i=t.firstChild;return i.style.behavior="url(#default#VML)",i&&"object"==typeof i.adj}catch(t){return!1}}(),o.SVG.include(o.Browser.vml?{_initContainer:function(){this._container=o.DomUtil.create("div","leaflet-vml-container")},_update:function(){this._map._animatingZoom||(o.Renderer.prototype._update.call(this),this.fire("update"))},_initPath:function(t){var e=t._container=o.SVG.create("shape");o.DomUtil.addClass(e,"leaflet-vml-shape "+(this.options.className||"")),e.coordsize="1 1",t._path=o.SVG.create("path"),e.appendChild(t._path),this._updateStyle(t)},_addPath:function(t){var e=t._container;this._container.appendChild(e),t.options.interactive&&t.addInteractiveTarget(e)},_removePath:function(t){var e=t._container;o.DomUtil.remove(e),t.removeInteractiveTarget(e)},_updateStyle:function(t){var e=t._stroke,i=t._fill,n=t.options,s=t._container;s.stroked=!!n.stroke,s.filled=!!n.fill,n.stroke?(e||(e=t._stroke=o.SVG.create("stroke")),s.appendChild(e),e.weight=n.weight+"px",e.color=n.color,e.opacity=n.opacity,n.dashArray?e.dashStyle=o.Util.isArray(n.dashArray)?n.dashArray.join(" "):n.dashArray.replace(/( *, *)/g," "):e.dashStyle="",e.endcap=n.lineCap.replace("butt","flat"),e.joinstyle=n.lineJoin):e&&(s.removeChild(e),t._stroke=null),n.fill?(i||(i=t._fill=o.SVG.create("fill")),s.appendChild(i),i.color=n.fillColor||n.color,i.opacity=n.fillOpacity):i&&(s.removeChild(i),t._fill=null)},_updateCircle:function(t){var e=t._point.round(),i=Math.round(t._radius),n=Math.round(t._radiusY||i);this._setPath(t,t._empty()?"M0 0":"AL "+e.x+","+e.y+" "+i+","+n+" 0,23592600")},_setPath:function(t,e){t._path.v=e},_bringToFront:function(t){o.DomUtil.toFront(t._container)},_bringToBack:function(t){o.DomUtil.toBack(t._container)}}:{}),o.Browser.vml&&(o.SVG.create=function(){try{return e.namespaces.add("lvml","urn:schemas-microsoft-com:vml"),function(t){return e.createElement("')}}catch(t){return function(t){return e.createElement("<"+t+' xmlns="urn:schemas-microsoft.com:vml" class="lvml">')}}}()),o.Canvas=o.Renderer.extend({onAdd:function(){o.Renderer.prototype.onAdd.call(this),this._draw()},_initContainer:function(){var t=this._container=e.createElement("canvas");o.DomEvent.on(t,"mousemove",o.Util.throttle(this._onMouseMove,32,this),this).on(t,"click dblclick mousedown mouseup contextmenu",this._onClick,this).on(t,"mouseout",this._handleMouseOut,this),this._ctx=t.getContext("2d")},_updatePaths:function(){var t;this._redrawBounds=null;for(var e in this._layers)t=this._layers[e],t._update();this._redraw()},_update:function(){if(!this._map._animatingZoom||!this._bounds){this._drawnLayers={},o.Renderer.prototype._update.call(this);var t=this._bounds,e=this._container,i=t.getSize(),n=o.Browser.retina?2:1;o.DomUtil.setPosition(e,t.min),e.width=n*i.x,e.height=n*i.y,e.style.width=i.x+"px",e.style.height=i.y+"px",o.Browser.retina&&this._ctx.scale(2,2),this._ctx.translate(-t.min.x,-t.min.y),this.fire("update")}},_initPath:function(t){this._updateDashArray(t),this._layers[o.stamp(t)]=t;var e=t._order={layer:t,prev:this._drawLast,next:null};this._drawLast&&(this._drawLast.next=e),this._drawLast=e,this._drawFirst=this._drawFirst||this._drawLast},_addPath:function(t){this._requestRedraw(t)},_removePath:function(t){var e=t._order,i=e.next,n=e.prev;i?i.prev=n:this._drawLast=n,n?n.next=i:this._drawFirst=i,delete t._order,delete this._layers[o.stamp(t)],this._requestRedraw(t)},_updatePath:function(t){this._extendRedrawBounds(t),t._project(),t._update(),this._requestRedraw(t)},_updateStyle:function(t){this._updateDashArray(t),this._requestRedraw(t)},_updateDashArray:function(t){if(t.options.dashArray){var e,i=t.options.dashArray.split(","),n=[];for(e=0;et.y!=n.y>t.y&&t.x<(n.x-i.x)*(t.y-i.y)/(n.y-i.y)+i.x&&(u=!u);return u||o.Polyline.prototype._containsPoint.call(this,t,!0)},o.CircleMarker.prototype._containsPoint=function(t){return t.distanceTo(this._point)<=this._radius+this._clickTolerance()},o.GeoJSON=o.FeatureGroup.extend({initialize:function(t,e){o.setOptions(this,e),this._layers={},t&&this.addData(t)},addData:function(t){var e,i,n,s=o.Util.isArray(t)?t:t.features;if(s){for(e=0,i=s.length;e1)return void(this._moved=!0);var n=i.touches&&1===i.touches.length?i.touches[0]:i,s=new o.Point(n.clientX,n.clientY),r=s.subtract(this._startPoint);(r.x||r.y)&&(Math.abs(r.x)+Math.abs(r.y)50&&(this._positions.shift(),this._times.shift())}this._map.fire("move",t).fire("drag",t)},_onZoomEnd:function(){var t=this._map.getSize().divideBy(2),e=this._map.latLngToLayerPoint([0,0]);this._initialWorldOffset=e.subtract(t).x,this._worldWidth=this._map.getPixelWorldBounds().getSize().x},_viscousLimit:function(t,e){return t-(t-e)*this._viscosity},_onPreDragLimit:function(){if(this._viscosity&&this._offsetLimit){var t=this._draggable._newPos.subtract(this._draggable._startPos),e=this._offsetLimit;t.xe.max.x&&(t.x=this._viscousLimit(t.x,e.max.x)),t.y>e.max.y&&(t.y=this._viscousLimit(t.y,e.max.y)),this._draggable._newPos=this._draggable._startPos.add(t)}},_onPreDragWrap:function(){var t=this._worldWidth,e=Math.round(t/2),i=this._initialWorldOffset,n=this._draggable._newPos.x,o=(n-e+i)%t+e-i,s=(n+e+i)%t-e-i,r=Math.abs(o+i)0?s:-s))-e;this._delta=0,this._startTime=null,r&&("center"===t.options.scrollWheelZoom?t.setZoom(e+r):t.setZoomAround(this._lastMousePos,e+r))}}),o.Map.addInitHook("addHandler","scrollWheelZoom",o.Map.ScrollWheelZoom),o.extend(o.DomEvent,{_touchstart:o.Browser.msPointer?"MSPointerDown":o.Browser.pointer?"pointerdown":"touchstart",_touchend:o.Browser.msPointer?"MSPointerUp":o.Browser.pointer?"pointerup":"touchend",addDoubleTapListener:function(t,e,i){function n(t){var e;if(e=o.Browser.pointer?o.DomEvent._pointersCount:t.touches.length,!(e>1)){var i=Date.now(),n=i-(r||i);a=t.touches?t.touches[0]:t,h=n>0&&n<=l,r=i}}function s(){if(h&&!a.cancelBubble){if(o.Browser.pointer){var t,i,n={};for(i in a)t=a[i],n[i]=t&&t.bind?t.bind(a):t;a=n}a.type="dblclick",e(a),r=null}}var r,a,h=!1,l=250,u="_leaflet_",c=this._touchstart,d=this._touchend;return t[u+c+i]=n,t[u+d+i]=s,t[u+"dblclick"+i]=e,t.addEventListener(c,n,!1),t.addEventListener(d,s,!1),o.Browser.edge||t.addEventListener("dblclick",e,!1),this},removeDoubleTapListener:function(t,e){var i="_leaflet_",n=t[i+this._touchstart+e],s=t[i+this._touchend+e],r=t[i+"dblclick"+e];return t.removeEventListener(this._touchstart,n,!1),t.removeEventListener(this._touchend,s,!1),o.Browser.edge||t.removeEventListener("dblclick",r,!1),this}}),o.extend(o.DomEvent,{POINTER_DOWN:o.Browser.msPointer?"MSPointerDown":"pointerdown",POINTER_MOVE:o.Browser.msPointer?"MSPointerMove":"pointermove",POINTER_UP:o.Browser.msPointer?"MSPointerUp":"pointerup",POINTER_CANCEL:o.Browser.msPointer?"MSPointerCancel":"pointercancel",TAG_WHITE_LIST:["INPUT","SELECT","OPTION"],_pointers:{},_pointersCount:0,addPointerListener:function(t,e,i,n){return"touchstart"===e?this._addPointerStart(t,i,n):"touchmove"===e?this._addPointerMove(t,i,n):"touchend"===e&&this._addPointerEnd(t,i,n),this},removePointerListener:function(t,e,i){var n=t["_leaflet_"+e+i];return"touchstart"===e?t.removeEventListener(this.POINTER_DOWN,n,!1):"touchmove"===e?t.removeEventListener(this.POINTER_MOVE,n,!1):"touchend"===e&&(t.removeEventListener(this.POINTER_UP,n,!1),t.removeEventListener(this.POINTER_CANCEL,n,!1)),this},_addPointerStart:function(t,i,n){var s=o.bind(function(t){if("mouse"!==t.pointerType&&t.pointerType!==t.MSPOINTER_TYPE_MOUSE){if(!(this.TAG_WHITE_LIST.indexOf(t.target.tagName)<0))return;o.DomEvent.preventDefault(t)}this._handlePointer(t,i)},this);if(t["_leaflet_touchstart"+n]=s,t.addEventListener(this.POINTER_DOWN,s,!1),!this._pointerDocListener){var r=o.bind(this._globalPointerUp,this);e.documentElement.addEventListener(this.POINTER_DOWN,o.bind(this._globalPointerDown,this),!0),e.documentElement.addEventListener(this.POINTER_MOVE,o.bind(this._globalPointerMove,this),!0),e.documentElement.addEventListener(this.POINTER_UP,r,!0),e.documentElement.addEventListener(this.POINTER_CANCEL,r,!0),this._pointerDocListener=!0}},_globalPointerDown:function(t){this._pointers[t.pointerId]=t,this._pointersCount++},_globalPointerMove:function(t){this._pointers[t.pointerId]&&(this._pointers[t.pointerId]=t)},_globalPointerUp:function(t){delete this._pointers[t.pointerId],this._pointersCount--},_handlePointer:function(t,e){t.touches=[];for(var i in this._pointers)t.touches.push(this._pointers[i]);t.changedTouches=[t],e(t)},_addPointerMove:function(t,e,i){var n=o.bind(function(t){(t.pointerType!==t.MSPOINTER_TYPE_MOUSE&&"mouse"!==t.pointerType||0!==t.buttons)&&this._handlePointer(t,e)},this);t["_leaflet_touchmove"+i]=n,t.addEventListener(this.POINTER_MOVE,n,!1)},_addPointerEnd:function(t,e,i){var n=o.bind(function(t){this._handlePointer(t,e)},this);t["_leaflet_touchend"+i]=n,t.addEventListener(this.POINTER_UP,n,!1),t.addEventListener(this.POINTER_CANCEL,n,!1)}}),o.Map.mergeOptions({touchZoom:o.Browser.touch&&!o.Browser.android23,bounceAtZoomLimits:!0}),o.Map.TouchZoom=o.Handler.extend({addHooks:function(){o.DomUtil.addClass(this._map._container,"leaflet-touch-zoom"),o.DomEvent.on(this._map._container,"touchstart",this._onTouchStart,this)},removeHooks:function(){o.DomUtil.removeClass(this._map._container,"leaflet-touch-zoom"),o.DomEvent.off(this._map._container,"touchstart",this._onTouchStart,this)},_onTouchStart:function(t){var i=this._map;if(t.touches&&2===t.touches.length&&!i._animatingZoom&&!this._zooming){var n=i.mouseEventToContainerPoint(t.touches[0]),s=i.mouseEventToContainerPoint(t.touches[1]);this._centerPoint=i.getSize()._divideBy(2),this._startLatLng=i.containerPointToLatLng(this._centerPoint),"center"!==i.options.touchZoom&&(this._pinchStartLatLng=i.containerPointToLatLng(n.add(s)._divideBy(2))),this._startDist=n.distanceTo(s),this._startZoom=i.getZoom(),this._moved=!1,this._zooming=!0,i._stop(),o.DomEvent.on(e,"touchmove",this._onTouchMove,this).on(e,"touchend",this._onTouchEnd,this),o.DomEvent.preventDefault(t)}},_onTouchMove:function(t){if(t.touches&&2===t.touches.length&&this._zooming){var e=this._map,i=e.mouseEventToContainerPoint(t.touches[0]),n=e.mouseEventToContainerPoint(t.touches[1]),s=i.distanceTo(n)/this._startDist;if(this._zoom=e.getScaleZoom(s,this._startZoom),!e.options.bounceAtZoomLimits&&(this._zoome.getMaxZoom()&&s>1)&&(this._zoom=e._limitZoom(this._zoom)),"center"===e.options.touchZoom){if(this._center=this._startLatLng,1===s)return}else{var r=i._add(n)._divideBy(2)._subtract(this._centerPoint);if(1===s&&0===r.x&&0===r.y)return;this._center=e.unproject(e.project(this._pinchStartLatLng,this._zoom).subtract(r),this._zoom)}this._moved||(e._moveStart(!0),this._moved=!0),o.Util.cancelAnimFrame(this._animRequest);var a=o.bind(e._move,e,this._center,this._zoom,{pinch:!0,round:!1});this._animRequest=o.Util.requestAnimFrame(a,this,!0),o.DomEvent.preventDefault(t)}},_onTouchEnd:function(){return this._moved&&this._zooming?(this._zooming=!1,o.Util.cancelAnimFrame(this._animRequest),o.DomEvent.off(e,"touchmove",this._onTouchMove).off(e,"touchend",this._onTouchEnd),void(this._map.options.zoomAnimation?this._map._animateZoom(this._center,this._map._limitZoom(this._zoom),!0,this._map.options.zoomSnap):this._map._resetView(this._center,this._map._limitZoom(this._zoom)))):void(this._zooming=!1)}}),o.Map.addInitHook("addHandler","touchZoom",o.Map.TouchZoom),o.Map.mergeOptions({tap:!0,tapTolerance:15}),o.Map.Tap=o.Handler.extend({addHooks:function(){o.DomEvent.on(this._map._container,"touchstart",this._onDown,this)},removeHooks:function(){o.DomEvent.off(this._map._container,"touchstart",this._onDown,this)},_onDown:function(t){if(t.touches){if(o.DomEvent.preventDefault(t),this._fireClick=!0,t.touches.length>1)return this._fireClick=!1,void clearTimeout(this._holdTimeout);var i=t.touches[0],n=i.target;this._startPos=this._newPos=new o.Point(i.clientX,i.clientY),n.tagName&&"a"===n.tagName.toLowerCase()&&o.DomUtil.addClass(n,"leaflet-active"),this._holdTimeout=setTimeout(o.bind(function(){this._isTapValid()&&(this._fireClick=!1,this._onUp(),this._simulateEvent("contextmenu",i))},this),1e3),this._simulateEvent("mousedown",i),o.DomEvent.on(e,{touchmove:this._onMove,touchend:this._onUp},this)}},_onUp:function(t){if(clearTimeout(this._holdTimeout),o.DomEvent.off(e,{touchmove:this._onMove,touchend:this._onUp},this),this._fireClick&&t&&t.changedTouches){var i=t.changedTouches[0],n=i.target;n&&n.tagName&&"a"===n.tagName.toLowerCase()&&o.DomUtil.removeClass(n,"leaflet-active"),this._simulateEvent("mouseup",i),this._isTapValid()&&this._simulateEvent("click",i)}},_isTapValid:function(){return this._newPos.distanceTo(this._startPos)<=this._map.options.tapTolerance},_onMove:function(t){var e=t.touches[0];this._newPos=new o.Point(e.clientX,e.clientY),this._simulateEvent("mousemove",e)},_simulateEvent:function(i,n){var o=e.createEvent("MouseEvents");o._simulated=!0,n.target._simulatedClick=!0,o.initMouseEvent(i,!0,!0,t,1,n.screenX,n.screenY,n.clientX,n.clientY,!1,!1,!1,!1,0,null),n.target.dispatchEvent(o)}}),o.Browser.touch&&!o.Browser.pointer&&o.Map.addInitHook("addHandler","tap",o.Map.Tap),o.Map.mergeOptions({boxZoom:!0}),o.Map.BoxZoom=o.Handler.extend({initialize:function(t){this._map=t,this._container=t._container,this._pane=t._panes.overlayPane},addHooks:function(){o.DomEvent.on(this._container,"mousedown",this._onMouseDown,this)},removeHooks:function(){o.DomEvent.off(this._container,"mousedown",this._onMouseDown,this)},moved:function(){return this._moved},_resetState:function(){this._moved=!1},_onMouseDown:function(t){ -return!(!t.shiftKey||1!==t.which&&1!==t.button)&&(this._resetState(),o.DomUtil.disableTextSelection(),o.DomUtil.disableImageDrag(),this._startPoint=this._map.mouseEventToContainerPoint(t),void o.DomEvent.on(e,{contextmenu:o.DomEvent.stop,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this))},_onMouseMove:function(t){this._moved||(this._moved=!0,this._box=o.DomUtil.create("div","leaflet-zoom-box",this._container),o.DomUtil.addClass(this._container,"leaflet-crosshair"),this._map.fire("boxzoomstart")),this._point=this._map.mouseEventToContainerPoint(t);var e=new o.Bounds(this._point,this._startPoint),i=e.getSize();o.DomUtil.setPosition(this._box,e.min),this._box.style.width=i.x+"px",this._box.style.height=i.y+"px"},_finish:function(){this._moved&&(o.DomUtil.remove(this._box),o.DomUtil.removeClass(this._container,"leaflet-crosshair")),o.DomUtil.enableTextSelection(),o.DomUtil.enableImageDrag(),o.DomEvent.off(e,{contextmenu:o.DomEvent.stop,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseUp:function(t){if((1===t.which||1===t.button)&&(this._finish(),this._moved)){setTimeout(o.bind(this._resetState,this),0);var e=new o.LatLngBounds(this._map.containerPointToLatLng(this._startPoint),this._map.containerPointToLatLng(this._point));this._map.fitBounds(e).fire("boxzoomend",{boxZoomBounds:e})}},_onKeyDown:function(t){27===t.keyCode&&this._finish()}}),o.Map.addInitHook("addHandler","boxZoom",o.Map.BoxZoom),o.Map.mergeOptions({keyboard:!0,keyboardPanDelta:80}),o.Map.Keyboard=o.Handler.extend({keyCodes:{left:[37],right:[39],down:[40],up:[38],zoomIn:[187,107,61,171],zoomOut:[189,109,54,173]},initialize:function(t){this._map=t,this._setPanDelta(t.options.keyboardPanDelta),this._setZoomDelta(t.options.zoomDelta)},addHooks:function(){var t=this._map._container;t.tabIndex<=0&&(t.tabIndex="0"),o.DomEvent.on(t,{focus:this._onFocus,blur:this._onBlur,mousedown:this._onMouseDown},this),this._map.on({focus:this._addHooks,blur:this._removeHooks},this)},removeHooks:function(){this._removeHooks(),o.DomEvent.off(this._map._container,{focus:this._onFocus,blur:this._onBlur,mousedown:this._onMouseDown},this),this._map.off({focus:this._addHooks,blur:this._removeHooks},this)},_onMouseDown:function(){if(!this._focused){var i=e.body,n=e.documentElement,o=i.scrollTop||n.scrollTop,s=i.scrollLeft||n.scrollLeft;this._map._container.focus(),t.scrollTo(s,o)}},_onFocus:function(){this._focused=!0,this._map.fire("focus")},_onBlur:function(){this._focused=!1,this._map.fire("blur")},_setPanDelta:function(t){var e,i,n=this._panKeys={},o=this.keyCodes;for(e=0,i=o.left.length;e0&&t.screenY>0&&this._map.getContainer().focus()}}),o.control=function(t){return new o.Control(t)},o.Map.include({addControl:function(t){return t.addTo(this),this},removeControl:function(t){return t.remove(),this},_initControlPos:function(){function t(t,s){var r=i+t+" "+i+s;e[t+s]=o.DomUtil.create("div",r,n)}var e=this._controlCorners={},i="leaflet-",n=this._controlContainer=o.DomUtil.create("div",i+"control-container",this._container);t("top","left"),t("top","right"),t("bottom","left"),t("bottom","right")},_clearControlPos:function(){o.DomUtil.remove(this._controlContainer)}}),o.Control.Zoom=o.Control.extend({options:{position:"topleft",zoomInText:"+",zoomInTitle:"Zoom in",zoomOutText:"-",zoomOutTitle:"Zoom out"},onAdd:function(t){var e="leaflet-control-zoom",i=o.DomUtil.create("div",e+" leaflet-bar"),n=this.options;return this._zoomInButton=this._createButton(n.zoomInText,n.zoomInTitle,e+"-in",i,this._zoomIn),this._zoomOutButton=this._createButton(n.zoomOutText,n.zoomOutTitle,e+"-out",i,this._zoomOut),this._updateDisabled(),t.on("zoomend zoomlevelschange",this._updateDisabled,this),i},onRemove:function(t){t.off("zoomend zoomlevelschange",this._updateDisabled,this)},disable:function(){return this._disabled=!0,this._updateDisabled(),this},enable:function(){return this._disabled=!1,this._updateDisabled(),this},_zoomIn:function(t){!this._disabled&&this._map._zoomthis._map.getMinZoom()&&this._map.zoomOut(this._map.options.zoomDelta*(t.shiftKey?3:1))},_createButton:function(t,e,i,n,s){var r=o.DomUtil.create("a",i,n);return r.innerHTML=t,r.href="#",r.title=e,r.setAttribute("role","button"),r.setAttribute("aria-label",e),o.DomEvent.on(r,"mousedown dblclick",o.DomEvent.stopPropagation).on(r,"click",o.DomEvent.stop).on(r,"click",s,this).on(r,"click",this._refocusOnMap,this),r},_updateDisabled:function(){var t=this._map,e="leaflet-disabled";o.DomUtil.removeClass(this._zoomInButton,e),o.DomUtil.removeClass(this._zoomOutButton,e),(this._disabled||t._zoom===t.getMinZoom())&&o.DomUtil.addClass(this._zoomOutButton,e),(this._disabled||t._zoom===t.getMaxZoom())&&o.DomUtil.addClass(this._zoomInButton,e)}}),o.Map.mergeOptions({zoomControl:!0}),o.Map.addInitHook(function(){this.options.zoomControl&&(this.zoomControl=new o.Control.Zoom,this.addControl(this.zoomControl))}),o.control.zoom=function(t){return new o.Control.Zoom(t)},o.Control.Attribution=o.Control.extend({options:{position:"bottomright",prefix:'Leaflet'},initialize:function(t){o.setOptions(this,t),this._attributions={}},onAdd:function(t){t.attributionControl=this,this._container=o.DomUtil.create("div","leaflet-control-attribution"),o.DomEvent&&o.DomEvent.disableClickPropagation(this._container);for(var e in t._layers)t._layers[e].getAttribution&&this.addAttribution(t._layers[e].getAttribution());return this._update(),this._container},setPrefix:function(t){return this.options.prefix=t,this._update(),this},addAttribution:function(t){return t?(this._attributions[t]||(this._attributions[t]=0),this._attributions[t]++,this._update(),this):this},removeAttribution:function(t){return t?(this._attributions[t]&&(this._attributions[t]--,this._update()),this):this},_update:function(){if(this._map){var t=[];for(var e in this._attributions)this._attributions[e]&&t.push(e);var i=[];this.options.prefix&&i.push(this.options.prefix),t.length&&i.push(t.join(", ")),this._container.innerHTML=i.join(" | ")}}}),o.Map.mergeOptions({attributionControl:!0}),o.Map.addInitHook(function(){this.options.attributionControl&&(new o.Control.Attribution).addTo(this)}),o.control.attribution=function(t){return new o.Control.Attribution(t)},o.Control.Scale=o.Control.extend({options:{position:"bottomleft",maxWidth:100,metric:!0,imperial:!0},onAdd:function(t){var e="leaflet-control-scale",i=o.DomUtil.create("div",e),n=this.options;return this._addScales(n,e+"-line",i),t.on(n.updateWhenIdle?"moveend":"move",this._update,this),t.whenReady(this._update,this),i},onRemove:function(t){t.off(this.options.updateWhenIdle?"moveend":"move",this._update,this)},_addScales:function(t,e,i){t.metric&&(this._mScale=o.DomUtil.create("div",e,i)),t.imperial&&(this._iScale=o.DomUtil.create("div",e,i))},_update:function(){var t=this._map,e=t.getSize().y/2,i=t.distance(t.containerPointToLatLng([0,e]),t.containerPointToLatLng([this.options.maxWidth,e]));this._updateScales(i)},_updateScales:function(t){this.options.metric&&t&&this._updateMetric(t),this.options.imperial&&t&&this._updateImperial(t)},_updateMetric:function(t){var e=this._getRoundNum(t),i=e<1e3?e+" m":e/1e3+" km";this._updateScale(this._mScale,i,e/t)},_updateImperial:function(t){var e,i,n,o=3.2808399*t;o>5280?(e=o/5280,i=this._getRoundNum(e),this._updateScale(this._iScale,i+" mi",i/e)):(n=this._getRoundNum(o),this._updateScale(this._iScale,n+" ft",n/o))},_updateScale:function(t,e,i){t.style.width=Math.round(this.options.maxWidth*i)+"px",t.innerHTML=e},_getRoundNum:function(t){var e=Math.pow(10,(Math.floor(t)+"").length-1),i=t/e;return i=i>=10?10:i>=5?5:i>=3?3:i>=2?2:1,e*i}}),o.control.scale=function(t){return new o.Control.Scale(t)},o.Control.Layers=o.Control.extend({options:{collapsed:!0,position:"topright",autoZIndex:!0,hideSingleBase:!1,sortLayers:!1,sortFunction:function(t,e,i,n){return i1,this._baseLayersList.style.display=t?"":"none"),this._separator.style.display=e&&t?"":"none",this},_onLayerChange:function(t){this._handlingClick||this._update();var e=this._getLayer(o.stamp(t.target)),i=e.overlay?"add"===t.type?"overlayadd":"overlayremove":"add"===t.type?"baselayerchange":null;i&&this._map.fire(i,e)},_createRadioElement:function(t,i){var n='",o=e.createElement("div");return o.innerHTML=n,o.firstChild},_addItem:function(t){var i,n=e.createElement("label"),s=this._map.hasLayer(t.layer);t.overlay?(i=e.createElement("input"),i.type="checkbox",i.className="leaflet-control-layers-selector",i.defaultChecked=s):i=this._createRadioElement("leaflet-base-layers",s),i.layerId=o.stamp(t.layer),o.DomEvent.on(i,"click",this._onInputClick,this);var r=e.createElement("span");r.innerHTML=" "+t.name;var a=e.createElement("div");n.appendChild(a),a.appendChild(i),a.appendChild(r);var h=t.overlay?this._overlaysList:this._baseLayersList;return h.appendChild(n),this._checkDisabledLayers(),n},_onInputClick:function(){var t,e,i,n=this._form.getElementsByTagName("input"),o=[],s=[];this._handlingClick=!0;for(var r=n.length-1;r>=0;r--)t=n[r],e=this._getLayer(t.layerId).layer,i=this._map.hasLayer(e),t.checked&&!i?o.push(e):!t.checked&&i&&s.push(e);for(r=0;r=0;s--)t=n[s],e=this._getLayer(t.layerId).layer,t.disabled=e.options.minZoom!==i&&oe.options.maxZoom},_expand:function(){return this.expand()},_collapse:function(){return this.collapse()}}),o.control.layers=function(t,e,i){return new o.Control.Layers(t,e,i)}}(window,document) \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-map.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-map.html.gz index f542d52730d..1a7bc76bc0d 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-map.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-map.html.gz differ diff --git a/homeassistant/components/frontend/www_static/service_worker.js b/homeassistant/components/frontend/www_static/service_worker.js index 3c0d0ab8b0d..334d21443f1 100644 --- a/homeassistant/components/frontend/www_static/service_worker.js +++ b/homeassistant/components/frontend/www_static/service_worker.js @@ -1 +1 @@ -"use strict";function setOfCachedUrls(e){return e.keys().then(function(e){return e.map(function(e){return e.url})}).then(function(e){return new Set(e)})}function notificationEventCallback(e,t){firePushCallback({action:t.action,data:t.notification.data,tag:t.notification.tag,type:e},t.notification.data.jwt)}function firePushCallback(e,t){delete e.data.jwt,0===Object.keys(e.data).length&&e.data.constructor===Object&&delete e.data,fetch("/api/notify.html5/callback",{method:"POST",headers:new Headers({"Content-Type":"application/json",Authorization:"Bearer "+t}),body:JSON.stringify(e)})}var precacheConfig=[["/","23137dada0d5b1e147f988eb63607368"],["/frontend/panels/dev-event-f19840b9a6a46f57cb064b384e1353f5.html","21cf247351b95fdd451c304e308a726c"],["/frontend/panels/dev-info-3765a371478cc66d677cf6dcc35267c6.html","dd614f2ee5e09a9dfd7f98822a55893d"],["/frontend/panels/dev-service-1d223225c1c75083738033895ea3e4b5.html","3c6d75bd5b2e38bb73391f71ea338496"],["/frontend/panels/dev-state-8257d99a38358a150eafdb23fa6727e0.html","3cf24bb7e92c759b35a74cf641ed80cb"],["/frontend/panels/dev-template-cbb251acabd5e7431058ed507b70522b.html","edd6ef67f4ab763f9d3dd7d3aa6f4007"],["/frontend/panels/map-13f120066c0b5faa2ce1db2c3f3cc486.html","e14d872bfdc0372b5f04cefe1ed3b7a6"],["/static/core-769f3fdd4e04b34bd66c7415743cf7b5.js","08e08915eed6697b16c07725fd9b43cb"],["/static/frontend-d48d9a13f7d677e59b1d22c6db051207.html","2d297277e12f77a102ad2694dff4adfd"],["/static/mdi-5bb2f1717206bad0d187c2633062c575.html","fd915b78a66e34026eb1eebfe58157d8"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/icons/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/icons/favicon.ico","04235bda7843ec2fceb1cbe2bc696cf4"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"],["static/webcomponents-lite.min.js","89313f9f2126ddea722150f8154aca03"]],cacheName="sw-precache-v2--"+(self.registration?self.registration.scope:""),ignoreUrlParametersMatching=[/^utm_/],addDirectoryIndex=function(e,t){var n=new URL(e);return"/"===n.pathname.slice(-1)&&(n.pathname+=t),n.toString()},createCacheKey=function(e,t,n,a){var c=new URL(e);return a&&c.toString().match(a)||(c.search+=(c.search?"&":"")+encodeURIComponent(t)+"="+encodeURIComponent(n)),c.toString()},isPathWhitelisted=function(e,t){if(0===e.length)return!0;var n=new URL(t).pathname;return e.some(function(e){return n.match(e)})},stripIgnoredUrlParameters=function(e,t){var n=new URL(e);return n.search=n.search.slice(1).split("&").map(function(e){return e.split("=")}).filter(function(e){return t.every(function(t){return!t.test(e[0])})}).map(function(e){return e.join("=")}).join("&"),n.toString()},hashParamName="_sw-precache",urlsToCacheKeys=new Map(precacheConfig.map(function(e){var t=e[0],n=e[1],a=new URL(t,self.location),c=createCacheKey(a,hashParamName,n,!1);return[a.toString(),c]}));self.addEventListener("install",function(e){e.waitUntil(caches.open(cacheName).then(function(e){return setOfCachedUrls(e).then(function(t){return Promise.all(Array.from(urlsToCacheKeys.values()).map(function(n){if(!t.has(n))return e.add(new Request(n,{credentials:"same-origin",redirect:"follow"}))}))})}).then(function(){return self.skipWaiting()}))}),self.addEventListener("activate",function(e){var t=new Set(urlsToCacheKeys.values());e.waitUntil(caches.open(cacheName).then(function(e){return e.keys().then(function(n){return Promise.all(n.map(function(n){if(!t.has(n.url))return e.delete(n)}))})}).then(function(){return self.clients.claim()}))}),self.addEventListener("fetch",function(e){if("GET"===e.request.method){var t,n=stripIgnoredUrlParameters(e.request.url,ignoreUrlParametersMatching);t=urlsToCacheKeys.has(n);var a="index.html";!t&&a&&(n=addDirectoryIndex(n,a),t=urlsToCacheKeys.has(n));var c="/";!t&&c&&"navigate"===e.request.mode&&isPathWhitelisted(["^((?!(static|api|local|service_worker.js|manifest.json)).)*$"],e.request.url)&&(n=new URL(c,self.location).toString(),t=urlsToCacheKeys.has(n)),t&&e.respondWith(caches.open(cacheName).then(function(e){return e.match(urlsToCacheKeys.get(n)).then(function(e){if(e)return e;throw Error("The cached response that was expected is missing.")})}).catch(function(t){return console.warn('Couldn\'t serve response for "%s" from cache: %O',e.request.url,t),fetch(e.request)}))}}),self.addEventListener("push",function(e){var t;e.data&&(t=e.data.json(),e.waitUntil(self.registration.showNotification(t.title,t).then(function(e){firePushCallback({type:"received",tag:t.tag,data:t.data},t.data.jwt)})))}),self.addEventListener("notificationclick",function(e){var t;notificationEventCallback("clicked",e),e.notification.close(),e.notification.data&&e.notification.data.url&&(t=e.notification.data.url,t&&e.waitUntil(clients.matchAll({type:"window"}).then(function(e){var n,a;for(n=0;n32&&t<127&&[34,35,60,62,63,96].indexOf(t)==-1?e:encodeURIComponent(e)}function i(e){var t=e.charCodeAt(0);return t>32&&t<127&&[34,35,60,62,96].indexOf(t)==-1?e:encodeURIComponent(e)}function a(e,a,s){function c(e){g.push(e)}var d=a||"scheme start",l=0,u="",w=!1,_=!1,g=[];e:for(;(e[l-1]!=p||0==l)&&!this._isInvalid;){var b=e[l];switch(d){case"scheme start":if(!b||!m.test(b)){if(a){c("Invalid scheme.");break e}u="",d="no scheme";continue}u+=b.toLowerCase(),d="scheme";break;case"scheme":if(b&&v.test(b))u+=b.toLowerCase();else{if(":"!=b){if(a){if(p==b)break e;c("Code point not allowed in scheme: "+b);break e}u="",l=0,d="no scheme";continue}if(this._scheme=u,u="",a)break e;t(this._scheme)&&(this._isRelative=!0),d="file"==this._scheme?"relative":this._isRelative&&s&&s._scheme==this._scheme?"relative or authority":this._isRelative?"authority first slash":"scheme data"}break;case"scheme data":"?"==b?(this._query="?",d="query"):"#"==b?(this._fragment="#",d="fragment"):p!=b&&"\t"!=b&&"\n"!=b&&"\r"!=b&&(this._schemeData+=r(b));break;case"no scheme":if(s&&t(s._scheme)){d="relative";continue}c("Missing scheme."),n.call(this);break;case"relative or authority":if("/"!=b||"/"!=e[l+1]){c("Expected /, got: "+b),d="relative";continue}d="authority ignore slashes";break;case"relative":if(this._isRelative=!0,"file"!=this._scheme&&(this._scheme=s._scheme),p==b){this._host=s._host,this._port=s._port,this._path=s._path.slice(),this._query=s._query,this._username=s._username,this._password=s._password;break e}if("/"==b||"\\"==b)"\\"==b&&c("\\ is an invalid code point."),d="relative slash";else if("?"==b)this._host=s._host,this._port=s._port,this._path=s._path.slice(),this._query="?",this._username=s._username,this._password=s._password,d="query";else{if("#"!=b){var y=e[l+1],E=e[l+2];("file"!=this._scheme||!m.test(b)||":"!=y&&"|"!=y||p!=E&&"/"!=E&&"\\"!=E&&"?"!=E&&"#"!=E)&&(this._host=s._host,this._port=s._port,this._username=s._username,this._password=s._password,this._path=s._path.slice(),this._path.pop()),d="relative path";continue}this._host=s._host,this._port=s._port,this._path=s._path.slice(),this._query=s._query,this._fragment="#",this._username=s._username,this._password=s._password,d="fragment"}break;case"relative slash":if("/"!=b&&"\\"!=b){"file"!=this._scheme&&(this._host=s._host,this._port=s._port,this._username=s._username,this._password=s._password),d="relative path";continue}"\\"==b&&c("\\ is an invalid code point."),d="file"==this._scheme?"file host":"authority ignore slashes";break;case"authority first slash":if("/"!=b){c("Expected '/', got: "+b),d="authority ignore slashes";continue}d="authority second slash";break;case"authority second slash":if(d="authority ignore slashes","/"!=b){c("Expected '/', got: "+b);continue}break;case"authority ignore slashes":if("/"!=b&&"\\"!=b){d="authority";continue}c("Expected authority, got: "+b);break;case"authority":if("@"==b){w&&(c("@ already seen."),u+="%40"),w=!0;for(var L=0;L>>0)+(t++ +"__")};n.prototype={set:function(t,n){var o=t[this.name];return o&&o[0]===t?o[1]=n:e(t,this.name,{value:[t,n],writable:!0}),this},get:function(e){var t;return(t=e[this.name])&&t[0]===e?t[1]:void 0},"delete":function(e){var t=e[this.name];return!(!t||t[0]!==e)&&(t[0]=t[1]=void 0,!0)},has:function(e){var t=e[this.name];return!!t&&t[0]===e}},window.WeakMap=n}(),function(e){function t(e){b.push(e),g||(g=!0,m(o))}function n(e){return window.ShadowDOMPolyfill&&window.ShadowDOMPolyfill.wrapIfNeeded(e)||e}function o(){g=!1;var e=b;b=[],e.sort(function(e,t){return e.uid_-t.uid_});var t=!1;e.forEach(function(e){var n=e.takeRecords();r(e),n.length&&(e.callback_(n,e),t=!0)}),t&&o()}function r(e){e.nodes_.forEach(function(t){var n=v.get(t);n&&n.forEach(function(t){t.observer===e&&t.removeTransientObservers()})})}function i(e,t){for(var n=e;n;n=n.parentNode){var o=v.get(n);if(o)for(var r=0;r0){var r=n[o-1],i=f(r,e);if(i)return void(n[o-1]=i)}else t(this.observer);n[o]=e},addListeners:function(){this.addListeners_(this.target)},addListeners_:function(e){var t=this.options;t.attributes&&e.addEventListener("DOMAttrModified",this,!0),t.characterData&&e.addEventListener("DOMCharacterDataModified",this,!0),t.childList&&e.addEventListener("DOMNodeInserted",this,!0),(t.childList||t.subtree)&&e.addEventListener("DOMNodeRemoved",this,!0)},removeListeners:function(){this.removeListeners_(this.target)},removeListeners_:function(e){var t=this.options;t.attributes&&e.removeEventListener("DOMAttrModified",this,!0),t.characterData&&e.removeEventListener("DOMCharacterDataModified",this,!0),t.childList&&e.removeEventListener("DOMNodeInserted",this,!0),(t.childList||t.subtree)&&e.removeEventListener("DOMNodeRemoved",this,!0)},addTransientObserver:function(e){if(e!==this.target){this.addListeners_(e),this.transientObservedNodes.push(e);var t=v.get(e);t||v.set(e,t=[]),t.push(this)}},removeTransientObservers:function(){var e=this.transientObservedNodes;this.transientObservedNodes=[],e.forEach(function(e){this.removeListeners_(e);for(var t=v.get(e),n=0;n":return">";case" ":return" "}}function t(t){return t.replace(u,e)}var n="undefined"==typeof HTMLTemplateElement;/Trident/.test(navigator.userAgent)&&!function(){var e=document.importNode;document.importNode=function(){var t=e.apply(document,arguments);if(t.nodeType===Node.DOCUMENT_FRAGMENT_NODE){var n=document.createDocumentFragment();return n.appendChild(t),n}return t}}();var o=function(){if(!n){var e=document.createElement("template"),t=document.createElement("template");t.content.appendChild(document.createElement("div")),e.content.appendChild(t);var o=e.cloneNode(!0);return 0===o.content.childNodes.length||0===o.content.firstChild.content.childNodes.length}}(),r="template",i=function(){};if(n){var a=document.implementation.createHTMLDocument("template"),s=!0,c=document.createElement("style");c.textContent=r+"{display:none;}";var d=document.head;d.insertBefore(c,d.firstElementChild),i.prototype=Object.create(HTMLElement.prototype),i.decorate=function(e){if(!e.content){e.content=a.createDocumentFragment();for(var n;n=e.firstChild;)e.content.appendChild(n);if(e.cloneNode=function(e){return i.cloneNode(this,e)},s)try{Object.defineProperty(e,"innerHTML",{get:function(){for(var e="",n=this.content.firstChild;n;n=n.nextSibling)e+=n.outerHTML||t(n.data);return e},set:function(e){for(a.body.innerHTML=e,i.bootstrap(a);this.content.firstChild;)this.content.removeChild(this.content.firstChild);for(;a.body.firstChild;)this.content.appendChild(a.body.firstChild)},configurable:!0})}catch(o){s=!1}i.bootstrap(e.content)}},i.bootstrap=function(e){for(var t,n=e.querySelectorAll(r),o=0,a=n.length;o]/g}if(n||o){var h=Node.prototype.cloneNode;i.cloneNode=function(e,t){var n=h.call(e,!1);return this.decorate&&this.decorate(n),t&&(n.content.appendChild(h.call(e.content,!0)),this.fixClonedDom(n.content,e.content)),n},i.fixClonedDom=function(e,t){if(t.querySelectorAll)for(var n,o,i=t.querySelectorAll(r),a=e.querySelectorAll(r),s=0,c=a.length;s=200&&e.status<300||304===e.status||0===e.status},load:function(n,o,r){var i=new XMLHttpRequest;return(e.flags.debug||e.flags.bust)&&(n+="?"+Math.random()),i.open("GET",n,t.async),i.addEventListener("readystatechange",function(e){if(4===i.readyState){var n=null;try{var a=i.getResponseHeader("Location");a&&(n="/"===a.substr(0,1)?location.origin+a:a)}catch(e){console.error(e.message)}o.call(r,!t.ok(i)&&i,i.response||i.responseText,n)}}),i.send(),i},loadDocument:function(e,t,n){this.load(e,t,n).responseType="document"}};e.xhr=t}),window.HTMLImports.addModule(function(e){var t=e.xhr,n=e.flags,o=function(e,t){this.cache={},this.onload=e,this.oncomplete=t,this.inflight=0,this.pending={}};o.prototype={addNodes:function(e){this.inflight+=e.length;for(var t,n=0,o=e.length;n-1?atob(a):decodeURIComponent(a),setTimeout(function(){this.receive(e,o,null,a)}.bind(this),0)}else{var s=function(t,n,r){this.receive(e,o,t,n,r)}.bind(this);t.load(e,s)}else setTimeout(function(){this.receive(e,o,{error:"href must be specified"},null)}.bind(this),0)},receive:function(e,t,n,o,r){this.cache[e]=o;for(var i,a=this.pending[e],s=0,c=a.length;s=0&&this.dynamicElements.splice(t,1)},parseImport:function(e){if(e["import"]=e.__doc,window.HTMLImports.__importsParsingHook&&window.HTMLImports.__importsParsingHook(e),e["import"]&&(e["import"].__importParsed=!0),this.markParsingComplete(e),e.__resource&&!e.__error?e.dispatchEvent(new CustomEvent("load",{bubbles:!1})):e.dispatchEvent(new CustomEvent("error",{bubbles:!1})),e.__pending)for(var t;e.__pending.length;)t=e.__pending.shift(),t&&t({target:e});this.parseNext()},parseLink:function(e){t(e)?this.parseImport(e):(e.href=e.href,this.parseGeneric(e))},parseStyle:function(e){var t=e;e=i(e),t.__appliedElement=e,e.__importElement=t,this.parseGeneric(e)},parseGeneric:function(e){this.trackElement(e),this.addElementToDocument(e)},rootImportForElement:function(e){for(var t=e;t.ownerDocument.__importLink;)t=t.ownerDocument.__importLink;return t},addElementToDocument:function(e){var t=this.rootImportForElement(e.__importElement||e);t.parentNode.insertBefore(e,t)},trackElement:function(e,t){var n=this,o=function(r){e.removeEventListener("load",o),e.removeEventListener("error",o),t&&t(r),n.markParsingComplete(e),n.parseNext()};if(e.addEventListener("load",o),e.addEventListener("error",o),d&&"style"===e.localName){var r=!1;if(e.textContent.indexOf("@import")==-1)r=!0;else if(e.sheet){r=!0;for(var i,a=e.sheet.cssRules,s=a?a.length:0,c=0;c=0},hasResource:function(e){return!t(e)||void 0!==e.__doc}};e.parser=h,e.IMPORT_SELECTOR=u}),window.HTMLImports.addModule(function(e){function t(e){return n(e,a)}function n(e,t){return"link"===e.localName&&e.getAttribute("rel")===t}function o(e){return!!Object.getOwnPropertyDescriptor(e,"baseURI")}function r(e,t){var n=document.implementation.createHTMLDocument(a);n._URL=t;var r=n.createElement("base");r.setAttribute("href",t),n.baseURI||o(n)||Object.defineProperty(n,"baseURI",{value:t});var i=n.createElement("meta");return i.setAttribute("charset","utf-8"),n.head.appendChild(i),n.head.appendChild(r),n.body.innerHTML=e,window.HTMLTemplateElement&&HTMLTemplateElement.bootstrap&&HTMLTemplateElement.bootstrap(n),n}var i=e.flags,a=e.IMPORT_LINK_TYPE,s=e.IMPORT_SELECTOR,c=e.rootDocument,d=e.Loader,l=e.Observer,u=e.parser,h={documents:{},documentPreloadSelectors:s,importsPreloadSelectors:[s].join(","),loadNode:function(e){f.addNode(e)},loadSubtree:function(e){var t=this.marshalNodes(e);f.addNodes(t)},marshalNodes:function(e){return e.querySelectorAll(this.loadSelectorsForNode(e))},loadSelectorsForNode:function(e){var t=e.ownerDocument||e;return t===c?this.documentPreloadSelectors:this.importsPreloadSelectors},loaded:function(e,n,o,a,s){if(i.load&&console.log("loaded",e,n),n.__resource=o,n.__error=a,t(n)){var c=this.documents[e];void 0===c&&(c=a?null:r(o,s||e),c&&(c.__importLink=n,this.bootDocument(c)),this.documents[e]=c),n.__doc=c}u.parseNext()},bootDocument:function(e){this.loadSubtree(e),this.observer.observe(e),u.parseNext()},loadedAll:function(){u.parseNext()}},f=new d(h.loaded.bind(h),h.loadedAll.bind(h));if(h.observer=new l,!document.baseURI){var p={get:function(){var e=document.querySelector("base");return e?e.href:window.location.href},configurable:!0};Object.defineProperty(document,"baseURI",p),Object.defineProperty(c,"baseURI",p)}e.importer=h,e.importLoader=f}),window.HTMLImports.addModule(function(e){var t=e.parser,n=e.importer,o={added:function(e){for(var o,r,i,a,s=0,c=e.length;s=0)){n.push(e);for(var o,r=e.querySelectorAll("link[rel="+a+"]"),s=0,c=r.length;s=0&&g(o,HTMLElement),o)}function p(e,t){var n=e[t];e[t]=function(){var e=n.apply(this,arguments);return w(e),e}}var m,v=(e.isIE,e.upgradeDocumentTree),w=e.upgradeAll,_=e.upgradeWithDefinition,g=e.implementPrototype,b=e.useNative,y=["annotation-xml","color-profile","font-face","font-face-src","font-face-uri","font-face-format","font-face-name","missing-glyph"],E={},L="http://www.w3.org/1999/xhtml",N=document.createElement.bind(document),M=document.createElementNS.bind(document);m=Object.__proto__||b?function(e,t){return e instanceof t}:function(e,t){if(e instanceof t)return!0;for(var n=e;n;){if(n===t.prototype)return!0;n=n.__proto__}return!1},p(Node.prototype,"cloneNode"),p(document,"importNode"),document.registerElement=t,document.createElement=f,document.createElementNS=h,e.registry=E,e["instanceof"]=m,e.reservedTagList=y,e.getRegisteredDefinition=d,document.register=document.registerElement}),function(e){function t(){i(window.wrap(document)),window.CustomElements.ready=!0;var e=window.requestAnimationFrame||function(e){setTimeout(e,16)};e(function(){setTimeout(function(){window.CustomElements.readyTime=Date.now(),window.HTMLImports&&(window.CustomElements.elapsed=window.CustomElements.readyTime-window.HTMLImports.readyTime),document.dispatchEvent(new CustomEvent("WebComponentsReady",{bubbles:!0}))})})}var n=e.useNative,o=e.initializeModules;e.isIE;if(n){var r=function(){};e.watchShadow=r,e.upgrade=r,e.upgradeAll=r,e.upgradeDocumentTree=r,e.upgradeSubtree=r,e.takeRecords=r,e["instanceof"]=function(e,t){return e instanceof t}}else o();var i=e.upgradeDocumentTree,a=e.upgradeDocument;if(window.wrap||(window.ShadowDOMPolyfill?(window.wrap=window.ShadowDOMPolyfill.wrapIfNeeded,window.unwrap=window.ShadowDOMPolyfill.unwrapIfNeeded):window.wrap=window.unwrap=function(e){return e}),window.HTMLImports&&(window.HTMLImports.__importsParsingHook=function(e){e["import"]&&a(wrap(e["import"]))}),"complete"===document.readyState||e.flags.eager)t();else if("interactive"!==document.readyState||window.attachEvent||window.HTMLImports&&!window.HTMLImports.ready){var s=window.HTMLImports&&!window.HTMLImports.ready?"HTMLImportsLoaded":"DOMContentLoaded";window.addEventListener(s,t)}else t()}(window.CustomElements),function(e){var t=document.createElement("style");t.textContent="body {transition: opacity ease-in 0.2s; } \nbody[unresolved] {opacity: 0; display: block; overflow: hidden; position: relative; } \n";var n=document.querySelector("head");n.insertBefore(t,n.firstChild)}(window.WebComponents); \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/webcomponents-lite.min.js.gz b/homeassistant/components/frontend/www_static/webcomponents-lite.min.js.gz index c5fe12d2303..28fff2c7c34 100644 Binary files a/homeassistant/components/frontend/www_static/webcomponents-lite.min.js.gz and b/homeassistant/components/frontend/www_static/webcomponents-lite.min.js.gz differ diff --git a/homeassistant/components/hdmi_cec.py b/homeassistant/components/hdmi_cec.py index 11a3f0f2d02..a154bdf609e 100644 --- a/homeassistant/components/hdmi_cec.py +++ b/homeassistant/components/hdmi_cec.py @@ -26,7 +26,7 @@ from homeassistant.const import (EVENT_HOMEASSISTANT_START, STATE_UNKNOWN, from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyCEC==0.4.12'] +REQUIREMENTS = ['pyCEC==0.4.13'] DOMAIN = 'hdmi_cec' diff --git a/homeassistant/components/history.py b/homeassistant/components/history.py index a077ad09ec7..69ed528f661 100644 --- a/homeassistant/components/history.py +++ b/homeassistant/components/history.py @@ -64,7 +64,7 @@ def get_significant_states(start_time, end_time=None, entity_id=None, """ entity_ids = (entity_id.lower(), ) if entity_id is not None else None states = recorder.get_model('States') - query = recorder.query('States').filter( + query = recorder.query(states).filter( (states.domain.in_(SIGNIFICANT_DOMAINS) | (states.last_changed == states.last_updated)) & (states.last_updated > start_time)) @@ -221,12 +221,17 @@ class HistoryPeriodView(HomeAssistantView): if datetime is None: return self.json_message('Invalid datetime', HTTP_BAD_REQUEST) + now = dt_util.utcnow() + one_day = timedelta(days=1) if datetime: start_time = dt_util.as_utc(datetime) else: - start_time = dt_util.utcnow() - one_day + start_time = now - one_day + + if start_time > now: + return self.json([]) end_time = start_time + one_day entity_id = request.GET.get('filter_entity_id') diff --git a/homeassistant/components/image_processing/demo.py b/homeassistant/components/image_processing/demo.py index 62b1f8bee9b..3cc2c17654c 100644 --- a/homeassistant/components/image_processing/demo.py +++ b/homeassistant/components/image_processing/demo.py @@ -4,53 +4,22 @@ Support for the demo image processing. For more details about this component, please refer to the documentation at https://home-assistant.io/components/demo/ """ - -from homeassistant.components.image_processing import ImageProcessingEntity +from homeassistant.components.image_processing import ATTR_CONFIDENCE from homeassistant.components.image_processing.openalpr_local import ( ImageProcessingAlprEntity) from homeassistant.components.image_processing.microsoft_face_identify import ( - ImageProcessingFaceIdentifyEntity) + ImageProcessingFaceEntity, ATTR_NAME, ATTR_AGE, ATTR_GENDER) def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the demo image_processing platform.""" add_devices([ - DemoImageProcessing('camera.demo_camera', "Demo"), DemoImageProcessingAlpr('camera.demo_camera', "Demo Alpr"), - DemoImageProcessingFaceIdentify( - 'camera.demo_camera', "Demo Face Identify") + DemoImageProcessingFace( + 'camera.demo_camera', "Demo Face") ]) -class DemoImageProcessing(ImageProcessingEntity): - """Demo alpr image processing entity.""" - - def __init__(self, camera_entity, name): - """Initialize demo alpr.""" - self._name = name - self._camera = camera_entity - self._count = 0 - - @property - def camera_entity(self): - """Return camera entity id from process pictures.""" - return self._camera - - @property - def name(self): - """Return the name of the entity.""" - return self._name - - @property - def state(self): - """Return the state of the entity.""" - return self._count - - def process_image(self, image): - """Process image.""" - self._count += 1 - - class DemoImageProcessingAlpr(ImageProcessingAlprEntity): """Demo alpr image processing entity.""" @@ -88,7 +57,7 @@ class DemoImageProcessingAlpr(ImageProcessingAlprEntity): self.process_plates(demo_data, 1) -class DemoImageProcessingFaceIdentify(ImageProcessingFaceIdentifyEntity): +class DemoImageProcessingFace(ImageProcessingFaceEntity): """Demo face identify image processing entity.""" def __init__(self, camera_entity, name): @@ -115,10 +84,22 @@ class DemoImageProcessingFaceIdentify(ImageProcessingFaceIdentifyEntity): def process_image(self, image): """Process image.""" - demo_data = { - 'Hans': 98.34, - 'Helena': 82.53, - 'Luna': 62.53, - } + demo_data = [ + { + ATTR_CONFIDENCE: 98.34, + ATTR_NAME: 'Hans', + ATTR_AGE: 16.0, + ATTR_GENDER: 'male', + }, + { + ATTR_NAME: 'Helena', + ATTR_AGE: 28.0, + ATTR_GENDER: 'female', + }, + { + ATTR_CONFIDENCE: 62.53, + ATTR_NAME: 'Luna', + }, + ] self.process_faces(demo_data, 4) diff --git a/homeassistant/components/image_processing/microsoft_face_detect.py b/homeassistant/components/image_processing/microsoft_face_detect.py new file mode 100644 index 00000000000..43c5c9dd7f0 --- /dev/null +++ b/homeassistant/components/image_processing/microsoft_face_detect.py @@ -0,0 +1,122 @@ +""" +Component that will help set the microsoft face detect processing. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/image_processing.microsoft_face_detect/ +""" +import asyncio +import logging + +import voluptuous as vol + +from homeassistant.core import split_entity_id +from homeassistant.exceptions import HomeAssistantError +from homeassistant.components.microsoft_face import DATA_MICROSOFT_FACE +from homeassistant.components.image_processing import ( + PLATFORM_SCHEMA, CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME) +from homeassistant.components.image_processing.microsoft_face_identify import ( + ImageProcessingFaceEntity, ATTR_GENDER, ATTR_AGE, ATTR_GLASSES) +import homeassistant.helpers.config_validation as cv + +DEPENDENCIES = ['microsoft_face'] + +_LOGGER = logging.getLogger(__name__) + +EVENT_IDENTIFY_FACE = 'detect_face' + +SUPPORTED_ATTRIBUTES = [ + ATTR_AGE, + ATTR_GENDER, + ATTR_GLASSES +] + +CONF_ATTRIBUTES = 'attributes' +DEFAULT_ATTRIBUTES = [ATTR_AGE, ATTR_GENDER] + + +def validate_attributes(list_attributes): + """Validate face attributes.""" + for attr in list_attributes: + if attr not in SUPPORTED_ATTRIBUTES: + raise vol.Invalid("Invalid attribtue {0}".format(attr)) + return list_attributes + + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_ATTRIBUTES, default=DEFAULT_ATTRIBUTES): + vol.All(cv.ensure_list, validate_attributes), +}) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up the microsoft face detection platform.""" + api = hass.data[DATA_MICROSOFT_FACE] + attributes = config[CONF_ATTRIBUTES] + + entities = [] + for camera in config[CONF_SOURCE]: + entities.append(MicrosoftFaceDetectEntity( + camera[CONF_ENTITY_ID], api, attributes, camera.get(CONF_NAME) + )) + + yield from async_add_devices(entities) + + +class MicrosoftFaceDetectEntity(ImageProcessingFaceEntity): + """Microsoft face api entity for identify.""" + + def __init__(self, camera_entity, api, attributes, name=None): + """Initialize openalpr local api.""" + super().__init__() + + self._api = api + self._camera = camera_entity + self._attributes = attributes + + if name: + self._name = name + else: + self._name = "MicrosoftFace {0}".format( + split_entity_id(camera_entity)[1]) + + @property + def camera_entity(self): + """Return camera entity id from process pictures.""" + return self._camera + + @property + def name(self): + """Return the name of the entity.""" + return self._name + + @asyncio.coroutine + def async_process_image(self, image): + """Process image. + + This method is a coroutine. + """ + face_data = None + try: + face_data = yield from self._api.call_api( + 'post', 'detect', image, binary=True, + params={'returnFaceAttributes': ",".join(self._attributes)}) + + except HomeAssistantError as err: + _LOGGER.error("Can't process image on microsoft face: %s", err) + return + + if face_data is None or len(face_data) < 1: + return + + faces = [] + for face in face_data: + face_attr = {} + for attr in self._attributes: + if attr in face['faceAttributes']: + face_attr[attr] = face['faceAttributes'][attr] + + if face_attr: + faces.append(face_attr) + + self.async_process_faces(faces, len(face_data)) diff --git a/homeassistant/components/image_processing/microsoft_face_identify.py b/homeassistant/components/image_processing/microsoft_face_identify.py index 0402f272eeb..1cc17f1443b 100644 --- a/homeassistant/components/image_processing/microsoft_face_identify.py +++ b/homeassistant/components/image_processing/microsoft_face_identify.py @@ -23,11 +23,16 @@ DEPENDENCIES = ['microsoft_face'] _LOGGER = logging.getLogger(__name__) -EVENT_IDENTIFY_FACE = 'identify_face' +EVENT_DETECT_FACE = 'image_processing.detect_face' ATTR_NAME = 'name' ATTR_TOTAL_FACES = 'total_faces' -ATTR_KNOWN_FACES = 'known_faces' +ATTR_AGE = 'age' +ATTR_GENDER = 'gender' +ATTR_MOTION = 'motion' +ATTR_GLASSES = 'glasses' +ATTR_FACES = 'faces' + CONF_GROUP = 'group' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -52,71 +57,90 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): yield from async_add_devices(entities) -class ImageProcessingFaceIdentifyEntity(ImageProcessingEntity): - """Base entity class for face identify/verify image processing.""" +class ImageProcessingFaceEntity(ImageProcessingEntity): + """Base entity class for face image processing.""" def __init__(self): """Initialize base face identify/verify entity.""" - self.known_faces = {} # last scan data + self.faces = [] # last scan data self.total_faces = 0 # face count @property def state(self): """Return the state of the entity.""" confidence = 0 - face_name = STATE_UNKNOWN + state = STATE_UNKNOWN - # search high verify face - for i_name, i_co in self.known_faces.items(): - if i_co > confidence: - confidence = i_co - face_name = i_name - return face_name + # no confidence support + if not self.confidence: + return self.total_faces + + # search high confidence + for face in self.faces: + if ATTR_CONFIDENCE not in face: + continue + + f_co = face[ATTR_CONFIDENCE] + if f_co > confidence: + confidence = f_co + for attr in [ATTR_NAME, ATTR_MOTION]: + if attr in face: + state = face[attr] + break + + return state @property def state_attributes(self): """Return device specific state attributes.""" attr = { - ATTR_KNOWN_FACES: self.known_faces, + ATTR_FACES: self.faces, ATTR_TOTAL_FACES: self.total_faces, } return attr - def process_faces(self, known, total): + def process_faces(self, faces, total): """Send event with detected faces and store data.""" run_callback_threadsafe( - self.hass.loop, self.async_process_faces, known, total + self.hass.loop, self.async_process_faces, faces, total ).result() @callback - def async_process_faces(self, known, total): + def async_process_faces(self, faces, total): """Send event with detected faces and store data. known are a dict in follow format: - { 'name': confidence } + [ + { + ATTR_CONFIDENCE: 80, + ATTR_NAME: 'Name', + ATTR_AGE: 12.0, + ATTR_GENDER: 'man', + ATTR_MOTION: 'smile', + ATTR_GLASSES: 'sunglasses' + }, + ] This method must be run in the event loop. """ - detect = {name: confidence for name, confidence in known.items() - if confidence >= self.confidence} - # send events - for name, confidence in detect.items(): + for face in faces: + if ATTR_CONFIDENCE in face and self.confidence: + if face[ATTR_CONFIDENCE] < self.confidence: + continue + + face.update({ATTR_ENTITY_ID: self.entity_id}) self.hass.async_add_job( - self.hass.bus.async_fire, EVENT_IDENTIFY_FACE, { - ATTR_NAME: name, - ATTR_ENTITY_ID: self.entity_id, - ATTR_CONFIDENCE: confidence, - } + self.hass.bus.async_fire, EVENT_DETECT_FACE, face ) # update entity store - self.known_faces = detect + self.faces = faces self.total_faces = total -class MicrosoftFaceIdentifyEntity(ImageProcessingFaceIdentifyEntity): +class MicrosoftFaceIdentifyEntity(ImageProcessingFaceEntity): """Microsoft face api entity for identify.""" def __init__(self, camera_entity, api, face_group, confidence, name=None): @@ -173,7 +197,7 @@ class MicrosoftFaceIdentifyEntity(ImageProcessingFaceIdentifyEntity): return # parse data - knwon_faces = {} + knwon_faces = [] total = 0 for face in detect: total += 1 @@ -187,7 +211,10 @@ class MicrosoftFaceIdentifyEntity(ImageProcessingFaceIdentifyEntity): name = s_name break - knwon_faces[name] = data['confidence'] * 100 + knwon_faces.append({ + ATTR_NAME: name, + ATTR_CONFIDENCE: data['confidence'] * 100, + }) # process data self.async_process_faces(knwon_faces, total) diff --git a/homeassistant/components/image_processing/openalpr_local.py b/homeassistant/components/image_processing/openalpr_local.py index 319f14c1f3d..dbd404dd04c 100644 --- a/homeassistant/components/image_processing/openalpr_local.py +++ b/homeassistant/components/image_processing/openalpr_local.py @@ -24,7 +24,7 @@ _LOGGER = logging.getLogger(__name__) RE_ALPR_PLATE = re.compile(r"^plate\d*:") RE_ALPR_RESULT = re.compile(r"- (\w*)\s*confidence: (\d*.\d*)") -EVENT_FOUND_PLATE = 'found_plate' +EVENT_FOUND_PLATE = 'image_processing.found_plate' ATTR_PLATE = 'plate' ATTR_PLATES = 'plates' diff --git a/homeassistant/components/input_select.py b/homeassistant/components/input_select.py index bd25a74ae3f..a099e6eb8c3 100644 --- a/homeassistant/components/input_select.py +++ b/homeassistant/components/input_select.py @@ -45,6 +45,15 @@ SERVICE_SELECT_PREVIOUS_SCHEMA = vol.Schema({ }) +SERVICE_SET_OPTIONS = 'set_options' + +SERVICE_SET_OPTIONS_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_OPTIONS): + vol.All(cv.ensure_list, vol.Length(min=1), [cv.string]), +}) + + def _cv_input_select(cfg): """Config validation helper for input select (Voluptuous).""" options = cfg[CONF_OPTIONS] @@ -89,6 +98,14 @@ def select_previous(hass, entity_id): }) +def set_options(hass, entity_id, options): + """Set options of input_select.""" + hass.services.call(DOMAIN, SERVICE_SET_OPTIONS, { + ATTR_ENTITY_ID: entity_id, + ATTR_OPTIONS: options, + }) + + @asyncio.coroutine def async_setup(hass, config): """Setup input select.""" @@ -148,6 +165,20 @@ def async_setup(hass, config): DOMAIN, SERVICE_SELECT_PREVIOUS, async_select_previous_service, schema=SERVICE_SELECT_PREVIOUS_SCHEMA) + @asyncio.coroutine + def async_set_options_service(call): + """Handle a calls to the set options service.""" + target_inputs = component.async_extract_from_service(call) + + tasks = [input_select.async_set_options(call.data[ATTR_OPTIONS]) + for input_select in target_inputs] + if tasks: + yield from asyncio.wait(tasks, loop=hass.loop) + + hass.services.async_register( + DOMAIN, SERVICE_SET_OPTIONS, async_set_options_service, + schema=SERVICE_SET_OPTIONS_SCHEMA) + yield from component.async_add_entities(entities) return True @@ -207,3 +238,10 @@ class InputSelect(Entity): new_index = (current_index + offset) % len(self._options) self._current_option = self._options[new_index] yield from self.async_update_ha_state() + + @asyncio.coroutine + def async_set_options(self, options): + """Set options.""" + self._current_option = options[0] + self._options = options + yield from self.async_update_ha_state() diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index efbb9447fcf..5c3e7f4d177 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -35,7 +35,6 @@ ENTITY_ID_ALL_LIGHTS = group.ENTITY_ID_FORMAT.format('all_lights') ENTITY_ID_FORMAT = DOMAIN + ".{}" # Bitfield of features supported by the light entity -ATTR_SUPPORTED_FEATURES = 'supported_features' SUPPORT_BRIGHTNESS = 1 SUPPORT_COLOR_TEMP = 2 SUPPORT_EFFECT = 4 @@ -85,7 +84,6 @@ PROP_TO_ATTR = { 'white_value': ATTR_WHITE_VALUE, 'effect_list': ATTR_EFFECT_LIST, 'effect': ATTR_EFFECT, - 'supported_features': ATTR_SUPPORTED_FEATURES, } # Service call validation schemas @@ -364,8 +362,6 @@ class Light(ToggleEntity): data[ATTR_RGB_COLOR] = color_util.color_xy_brightness_to_RGB( data[ATTR_XY_COLOR][0], data[ATTR_XY_COLOR][1], data[ATTR_BRIGHTNESS]) - else: - data[ATTR_SUPPORTED_FEATURES] = self.supported_features return data diff --git a/homeassistant/components/light/demo.py b/homeassistant/components/light/demo.py index 614374ce65f..068efbbfe5f 100644 --- a/homeassistant/components/light/demo.py +++ b/homeassistant/components/light/demo.py @@ -28,19 +28,21 @@ SUPPORT_DEMO = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT | def setup_platform(hass, config, add_devices_callback, discovery_info=None): """Setup the demo light platform.""" add_devices_callback([ - DemoLight("Bed Light", False, effect_list=LIGHT_EFFECT_LIST, + DemoLight("Bed Light", False, True, effect_list=LIGHT_EFFECT_LIST, effect=LIGHT_EFFECT_LIST[0]), - DemoLight("Ceiling Lights", True, LIGHT_COLORS[0], LIGHT_TEMPS[1]), - DemoLight("Kitchen Lights", True, LIGHT_COLORS[1], LIGHT_TEMPS[0]) + DemoLight("Ceiling Lights", True, True, + LIGHT_COLORS[0], LIGHT_TEMPS[1]), + DemoLight("Kitchen Lights", True, True, + LIGHT_COLORS[1], LIGHT_TEMPS[0]) ]) class DemoLight(Light): - """Represenation of a demo light.""" + """Representation of a demo light.""" - def __init__( - self, name, state, rgb=None, ct=None, brightness=180, - xy_color=(.5, .5), white=200, effect_list=None, effect=None): + def __init__(self, name, state, available=False, rgb=None, ct=None, + brightness=180, xy_color=(.5, .5), white=200, + effect_list=None, effect=None): """Initialize the light.""" self._name = name self._state = state @@ -53,61 +55,68 @@ class DemoLight(Light): self._effect = effect @property - def should_poll(self): + def should_poll(self) -> bool: """No polling needed for a demo light.""" return False @property - def name(self): + def name(self) -> str: """Return the name of the light if any.""" return self._name @property - def brightness(self): + def available(self) -> bool: + """Return availability.""" + # This demo light is always available, but well-behaving components + # should implement this to inform Home Assistant accordingly. + return True + + @property + def brightness(self) -> int: """Return the brightness of this light between 0..255.""" return self._brightness @property - def xy_color(self): + def xy_color(self) -> tuple: """Return the XY color value [float, float].""" return self._xy_color @property - def rgb_color(self): + def rgb_color(self) -> tuple: """Return the RBG color value.""" return self._rgb @property - def color_temp(self): + def color_temp(self) -> int: """Return the CT color temperature.""" return self._ct @property - def white_value(self): + def white_value(self) -> int: """Return the white value of this light between 0..255.""" return self._white @property - def effect_list(self): + def effect_list(self) -> list: """Return the list of supported effects.""" return self._effect_list @property - def effect(self): + def effect(self) -> str: """Return the current effect.""" return self._effect @property - def is_on(self): + def is_on(self) -> bool: """Return true if light is on.""" return self._state @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" return SUPPORT_DEMO - def turn_on(self, **kwargs): + def turn_on(self, **kwargs) -> None: """Turn the light on.""" self._state = True @@ -129,9 +138,14 @@ class DemoLight(Light): if ATTR_EFFECT in kwargs: self._effect = kwargs[ATTR_EFFECT] + # As we have disabled polling, we need to inform + # Home Assistant about updates in our state ourselves. self.schedule_update_ha_state() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs) -> None: """Turn the light off.""" self._state = False + + # As we have disabled polling, we need to inform + # Home Assistant about updates in our state ourselves. self.schedule_update_ha_state() diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index 46eec35724a..11acdac6bc7 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -17,7 +17,7 @@ from homeassistant.components.light import ( PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['flux_led==0.12'] +REQUIREMENTS = ['flux_led==0.13'] _LOGGER = logging.getLogger(__name__) @@ -29,10 +29,13 @@ DOMAIN = 'flux_led' SUPPORT_FLUX_LED = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | SUPPORT_RGB_COLOR) +MODE_RGB = 'rgb' +MODE_RGBW = 'rgbw' + DEVICE_SCHEMA = vol.Schema({ vol.Optional(CONF_NAME): cv.string, - vol.Optional(ATTR_MODE, default='rgbw'): - vol.All(cv.string, vol.In(['rgbw', 'rgb'])), + vol.Optional(ATTR_MODE, default=MODE_RGBW): + vol.All(cv.string, vol.In([MODE_RGBW, MODE_RGB])), vol.Optional(CONF_PROTOCOL, default=None): vol.All(cv.string, vol.In(['ledenet'])), }) @@ -48,7 +51,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): import flux_led lights = [] light_ips = [] - for ipaddr, device_config in config[CONF_DEVICES].items(): + + for ipaddr, device_config in config.get(CONF_DEVICES, {}).items(): device = {} device['name'] = device_config[CONF_NAME] device['ipaddr'] = ipaddr @@ -59,7 +63,21 @@ def setup_platform(hass, config, add_devices, discovery_info=None): lights.append(light) light_ips.append(ipaddr) - if not config[CONF_AUTOMATIC_ADD]: + if discovery_info: + device = {} + # discovery_info: ip address,device id,device type + device['ipaddr'] = discovery_info[0] + device['name'] = discovery_info[1] + # As we don't know protocol and mode set to none to autodetect. + device[CONF_PROTOCOL] = None + device[ATTR_MODE] = None + + light = FluxLight(device) + if light.is_valid: + lights.append(light) + light_ips.append(device['ipaddr']) + + if not config.get(CONF_AUTOMATIC_ADD, False): add_devices(lights) return @@ -94,10 +112,20 @@ class FluxLight(Light): self._mode = device[ATTR_MODE] self.is_valid = True self._bulb = None + try: self._bulb = flux_led.WifiLedBulb(self._ipaddr) if self._protocol: self._bulb.setProtocol(self._protocol) + + # After bulb object is created the status is updated. We can + # now set the correct mode if it was not explicitly defined. + if not self._mode: + if self._bulb.rgbwcapable: + self._mode = MODE_RGBW + else: + self._mode = MODE_RGB + except socket.error: self.is_valid = False _LOGGER.error( @@ -121,7 +149,7 @@ class FluxLight(Light): @property def brightness(self): """Return the brightness of this light between 0..255.""" - return self._bulb.getWarmWhite255() + return self._bulb.brightness @property def rgb_color(self): diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 65ae9f30cf4..372dec04fbc 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -200,8 +200,7 @@ def setup_bridge(host, hass, add_devices, filename, allow_unreachable, for light_id, info in api_lights.items(): if light_id not in lights: - lights[light_id] = HueLight(hass, - int(light_id), info, + lights[light_id] = HueLight(int(light_id), info, bridge, update_lights, bridge_type, allow_unreachable, allow_in_emulated_hue) @@ -219,7 +218,6 @@ def setup_bridge(host, hass, add_devices, filename, allow_unreachable, if lightgroup_id not in lightgroups: lightgroups[lightgroup_id] = HueLight( - hass, int(lightgroup_id), info, bridge, update_lights, bridge_type, allow_unreachable, allow_in_emulated_hue, True) @@ -282,11 +280,10 @@ def request_configuration(host, hass, add_devices, filename, class HueLight(Light): """Representation of a Hue light.""" - def __init__(self, hass, light_id, info, bridge, update_lights, + def __init__(self, light_id, info, bridge, update_lights, bridge_type, allow_unreachable, allow_in_emulated_hue, is_group=False): """Initialize the light.""" - self.hass = hass self.light_id = light_id self.info = info self.bridge = bridge @@ -304,8 +301,14 @@ class HueLight(Light): @property def unique_id(self): """Return the ID of this Hue light.""" - return "{}.{}".format( - self.__class__, self.info.get('uniqueid', self.name)) + lid = self.info.get('uniqueid') + + if lid is None: + default_type = 'Group' if self.is_group else 'Light' + ltype = self.info.get('type', default_type) + lid = '{}.{}.{}'.format(self.name, ltype, self.light_id) + + return '{}.{}'.format(self.__class__, lid) @property def name(self): diff --git a/homeassistant/components/light/hyperion.py b/homeassistant/components/light/hyperion.py index 385cc43717f..cfa9a64580e 100644 --- a/homeassistant/components/light/hyperion.py +++ b/homeassistant/components/light/hyperion.py @@ -28,7 +28,9 @@ SUPPORT_HYPERION = SUPPORT_RGB_COLOR PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_DEFAULT_COLOR, default=DEFAULT_COLOR): cv.string, + vol.Optional(CONF_DEFAULT_COLOR, default=DEFAULT_COLOR): + vol.All(list, vol.Length(min=3, max=3), + [vol.All(vol.Coerce(int), vol.Range(min=0, max=255))]), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) diff --git a/homeassistant/components/light/mqtt.py b/homeassistant/components/light/mqtt.py index 54fa6b30598..5f227af97b6 100644 --- a/homeassistant/components/light/mqtt.py +++ b/homeassistant/components/light/mqtt.py @@ -121,12 +121,12 @@ class MqttLight(Light): self._state = False self._supported_features = 0 self._supported_features |= ( - topic[CONF_RGB_STATE_TOPIC] is not None and SUPPORT_RGB_COLOR) + topic[CONF_RGB_COMMAND_TOPIC] is not None and SUPPORT_RGB_COLOR) self._supported_features |= ( - topic[CONF_BRIGHTNESS_STATE_TOPIC] is not None and + topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None and SUPPORT_BRIGHTNESS) self._supported_features |= ( - topic[CONF_COLOR_TEMP_STATE_TOPIC] is not None and + topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None and SUPPORT_COLOR_TEMP) for key, tpl in list(templates.items()): diff --git a/homeassistant/components/light/mqtt_template.py b/homeassistant/components/light/mqtt_template.py index 55d4afac231..b7520fce682 100755 --- a/homeassistant/components/light/mqtt_template.py +++ b/homeassistant/components/light/mqtt_template.py @@ -284,3 +284,16 @@ class MqttTemplate(Light): if self._optimistic: self.schedule_update_ha_state() + + @property + def supported_features(self): + """Flag supported features.""" + features = 0 + if self._brightness is not None: + features = features | SUPPORT_BRIGHTNESS + if self._rgb is not None: + features = features | SUPPORT_RGB_COLOR + if self._effect_list is not None: + features = features | SUPPORT_EFFECT + + return features diff --git a/homeassistant/components/light/mysensors.py b/homeassistant/components/light/mysensors.py index 9a018192f63..7cb978bc10c 100644 --- a/homeassistant/components/light/mysensors.py +++ b/homeassistant/components/light/mysensors.py @@ -151,8 +151,14 @@ class MySensorsLight(mysensors.MySensorsDeviceEntity, Light): rgb = list(new_rgb) if rgb is None: return - if new_white is not None and hex_template == '%02x%02x%02x%02x': - rgb.append(new_white) + if hex_template == '%02x%02x%02x%02x': + if new_white is not None: + rgb.append(new_white) + elif white is not None: + rgb.append(white) + else: + _LOGGER.error('White value is not updated for RGBW light') + return hex_color = hex_template % tuple(rgb) if len(rgb) > 3: white = rgb.pop() @@ -236,11 +242,20 @@ class MySensorsLight(mysensors.MySensorsDeviceEntity, Light): """Update the controller with values from RGB or RGBW child.""" set_req = self.gateway.const.SetReq value = self._values[self.value_type] + if len(value) != 6 and len(value) != 8: + _LOGGER.error( + 'Wrong value %s for %s', value, set_req(self.value_type).name) + return color_list = rgb_hex_to_rgb_list(value) if set_req.V_LIGHT not in self._values and \ set_req.V_DIMMER not in self._values: self._state = max(color_list) > 0 if len(color_list) > 3: + if set_req.V_RGBW != self.value_type: + _LOGGER.error( + 'Wrong value %s for %s', + value, set_req(self.value_type).name) + return self._white = color_list.pop() self._rgb = color_list diff --git a/homeassistant/components/light/rflink.py b/homeassistant/components/light/rflink.py new file mode 100644 index 00000000000..ab532e2368a --- /dev/null +++ b/homeassistant/components/light/rflink.py @@ -0,0 +1,234 @@ +"""Support for Rflink lights. + +For more details about this platform, please refer to the documentation +at https://home-assistant.io/components/light.rflink/ + +""" +import asyncio +import logging + +from homeassistant.components import group +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) +from homeassistant.components.rflink import ( + CONF_ALIASSES, CONF_DEVICE_DEFAULTS, CONF_DEVICES, CONF_FIRE_EVENT, + CONF_IGNORE_DEVICES, CONF_NEW_DEVICES_GROUP, CONF_SIGNAL_REPETITIONS, + DATA_DEVICE_REGISTER, DATA_ENTITY_LOOKUP, DEVICE_DEFAULTS_SCHEMA, DOMAIN, + EVENT_KEY_COMMAND, EVENT_KEY_ID, SwitchableRflinkDevice, cv, vol) +from homeassistant.const import CONF_NAME, CONF_PLATFORM, CONF_TYPE + +DEPENDENCIES = ['rflink'] + +_LOGGER = logging.getLogger(__name__) + +TYPE_DIMMABLE = 'dimmable' +TYPE_SWITCHABLE = 'switchable' +TYPE_HYBRID = 'hybrid' + +PLATFORM_SCHEMA = vol.Schema({ + vol.Required(CONF_PLATFORM): DOMAIN, + vol.Optional(CONF_NEW_DEVICES_GROUP, default=None): cv.string, + vol.Optional(CONF_IGNORE_DEVICES): vol.All(cv.ensure_list, [cv.string]), + 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_TYPE): + vol.Any(TYPE_DIMMABLE, TYPE_SWITCHABLE, TYPE_HYBRID), + vol.Optional(CONF_ALIASSES, 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), + }, + }), +}) + + +def entity_type_for_device_id(device_id): + """Return entity class for procotol of a given device_id. + + Async friendly. + + """ + entity_type_mapping = { + # KlikAanKlikUit support both dimmers and on/off switches on the same + # protocol + 'newkaku': TYPE_HYBRID, + } + protocol = device_id.split('_')[0] + return entity_type_mapping.get(protocol, None) + + +def entity_class_for_type(entity_type): + """Translate entity type to entity class. + + Async friendly. + + """ + entity_device_mapping = { + # sends only 'dim' commands not compatible with on/off switches + TYPE_DIMMABLE: DimmableRflinkLight, + # sends only 'on/off' commands not advices with dimmers and signal + # repetition + TYPE_SWITCHABLE: RflinkLight, + # sends 'dim' and 'on' command to support both dimmers and on/off + # switches. Not compatible with signal repetition. + TYPE_HYBRID: HybridRflinkLight, + } + + return entity_device_mapping.get(entity_type, RflinkLight) + + +def devices_from_config(domain_config, hass=None): + """Parse config and add rflink switch devices.""" + devices = [] + for device_id, config in domain_config[CONF_DEVICES].items(): + # determine which kind of entity to create + if CONF_TYPE in config: + # remove type from config to not pass it as and argument to entity + # instantiation + entity_type = config.pop(CONF_TYPE) + else: + entity_type = entity_type_for_device_id(device_id) + entity_class = entity_class_for_type(entity_type) + + device_config = dict(domain_config[CONF_DEVICE_DEFAULTS], **config) + + is_hybrid = entity_class is HybridRflinkLight + + # make user aware this can cause problems + repetitions_enabled = device_config[CONF_SIGNAL_REPETITIONS] != 1 + if is_hybrid and repetitions_enabled: + _LOGGER.warning( + "Hybrid type for %s not compatible with signal " + "repetitions. Please set 'dimmable' or 'switchable' " + "type explicity in configuration.", + device_id) + + device = entity_class(device_id, hass, **device_config) + devices.append(device) + + # register entity (and aliasses) to listen to incoming rflink events + for _id in [device_id] + config[CONF_ALIASSES]: + hass.data[DATA_ENTITY_LOOKUP][ + EVENT_KEY_COMMAND][_id].append(device) + + return devices + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Setup the Rflink platform.""" + # add devices from config + yield from async_add_devices(devices_from_config(config, hass)) + + # add new (unconfigured) devices to user desired group + if config[CONF_NEW_DEVICES_GROUP]: + new_devices_group = yield from group.Group.async_create_group( + hass, config[CONF_NEW_DEVICES_GROUP], [], True) + else: + new_devices_group = None + + @asyncio.coroutine + def add_new_device(event): + """Check if device is known, otherwise add to list of known devices.""" + device_id = event[EVENT_KEY_ID] + + entity_type = entity_type_for_device_id(event[EVENT_KEY_ID]) + entity_class = entity_class_for_type(entity_type) + + device_config = config[CONF_DEVICE_DEFAULTS] + device = entity_class(device_id, hass, **device_config) + yield from async_add_devices([device]) + + # register entity to listen to incoming rflink events + hass.data[DATA_ENTITY_LOOKUP][ + EVENT_KEY_COMMAND][device_id].append(device) + + # make sure the event is processed by the new entity + device.handle_event(event) + + # maybe add to new devices group + if new_devices_group: + yield from new_devices_group.async_update_tracked_entity_ids( + list(new_devices_group.tracking) + [device.entity_id]) + + hass.data[DATA_DEVICE_REGISTER][EVENT_KEY_COMMAND] = add_new_device + + +class RflinkLight(SwitchableRflinkDevice, Light): + """Representation of a Rflink light.""" + + pass + + +class DimmableRflinkLight(SwitchableRflinkDevice, Light): + """Rflink light device that support dimming.""" + + _brightness = 255 + + @asyncio.coroutine + def async_turn_on(self, **kwargs): + """Turn the device on.""" + if ATTR_BRIGHTNESS in kwargs: + # rflink only support 16 brightness levels + self._brightness = int(kwargs[ATTR_BRIGHTNESS] / 17) * 17 + + # turn on light at the requested dim level + yield from self._async_handle_command('dim', self._brightness) + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + return self._brightness + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS + + +class HybridRflinkLight(SwitchableRflinkDevice, Light): + """Rflink light device that sends out both dim and on/off commands. + + Used for protocols which support lights that are not exclusively on/off + style. For example KlikAanKlikUit supports both on/off and dimmable light + switches using the same protocol. This type allows unconfigured + KlikAanKlikUit devices to support dimming without breaking support for + on/off switches. + + This type is not compatible with signal repetitions as the 'dim' and 'on' + command are send sequential and multiple 'on' commands to a dimmable + device can cause the dimmer to switch into a pulsating brightness mode. + Which results in a nice house disco :) + + """ + + _brightness = 255 + + @asyncio.coroutine + def async_turn_on(self, **kwargs): + """Turn the device on and set dim level.""" + if ATTR_BRIGHTNESS in kwargs: + # rflink only support 16 brightness levels + self._brightness = int(kwargs[ATTR_BRIGHTNESS] / 17) * 17 + + # if receiver supports dimming this will turn on the light + # at the requested dim level + yield from self._async_handle_command('dim', self._brightness) + + # if the receiving device does not support dimlevel this + # will ensure it is turned on when full brightness is set + if self._brightness == 255: + yield from self._async_handle_command("turn_on") + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + return self._brightness + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py index dcff4b31a5c..c89139171bf 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/light/wink.py @@ -9,7 +9,7 @@ import colorsys from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, Light) -from homeassistant.components.wink import WinkDevice +from homeassistant.components.wink import WinkDevice, DOMAIN from homeassistant.util import color as color_util from homeassistant.util.color import \ color_temperature_mired_to_kelvin as mired_to_kelvin @@ -23,7 +23,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Wink lights.""" import pywink - add_devices(WinkLight(light, hass) for light in pywink.get_light_bulbs()) + for light in pywink.get_light_bulbs(): + _id = light.object_id() + light.name() + if _id not in hass.data[DOMAIN]['unique_ids']: + add_devices([WinkLight(light, hass)]) class WinkLight(WinkDevice, Light): @@ -31,7 +34,7 @@ class WinkLight(WinkDevice, Light): def __init__(self, wink, hass): """Initialize the Wink device.""" - WinkDevice.__init__(self, wink, hass) + super().__init__(wink, hass) @property def is_on(self): diff --git a/homeassistant/components/light/yeelight.py b/homeassistant/components/light/yeelight.py index a8a2ec9b3fc..5eae4c66bb6 100644 --- a/homeassistant/components/light/yeelight.py +++ b/homeassistant/components/light/yeelight.py @@ -5,158 +5,309 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.yeelight/ """ import logging -import socket +import colorsys import voluptuous as vol +from homeassistant.util.color import ( + color_temperature_mired_to_kelvin as mired_to_kelvin, + color_temperature_kelvin_to_mired as kelvin_to_mired, + color_temperature_to_rgb) from homeassistant.const import CONF_DEVICES, CONF_NAME -from homeassistant.components.light import (ATTR_BRIGHTNESS, ATTR_RGB_COLOR, - ATTR_COLOR_TEMP, - SUPPORT_BRIGHTNESS, - SUPPORT_RGB_COLOR, - SUPPORT_COLOR_TEMP, - Light, PLATFORM_SCHEMA) +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_TRANSITION, ATTR_COLOR_TEMP, + ATTR_FLASH, FLASH_SHORT, FLASH_LONG, + SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, + SUPPORT_COLOR_TEMP, SUPPORT_FLASH, + Light, PLATFORM_SCHEMA) 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 = ['pyyeelight==1.0-beta'] +REQUIREMENTS = ['yeelight==0.2.2'] _LOGGER = logging.getLogger(__name__) +CONF_TRANSITION = "transition" +DEFAULT_TRANSITION = 350 + +CONF_SAVE_ON_CHANGE = "save_on_change" +CONF_MODE_MUSIC = "use_music_mode" + DOMAIN = 'yeelight' -SUPPORT_YEELIGHT = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR | - SUPPORT_COLOR_TEMP) - -DEVICE_SCHEMA = vol.Schema({vol.Optional(CONF_NAME): cv.string, }) +DEVICE_SCHEMA = vol.Schema({ + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_TRANSITION, default=DEFAULT_TRANSITION): cv.positive_int, + vol.Optional(CONF_MODE_MUSIC, default=False): cv.boolean, + vol.Optional(CONF_SAVE_ON_CHANGE, default=True): cv.boolean, +}) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( {vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}, }) +SUPPORT_YEELIGHT_RGB = (SUPPORT_RGB_COLOR | + SUPPORT_COLOR_TEMP) + +SUPPORT_YEELIGHT = (SUPPORT_BRIGHTNESS | + SUPPORT_TRANSITION | + SUPPORT_FLASH) + + +def _cmd(func): + """A wrapper to catch exceptions from the bulb.""" + def _wrap(self, *args, **kwargs): + import yeelight + try: + _LOGGER.debug("Calling %s with %s %s", func, args, kwargs) + return func(self, *args, **kwargs) + except yeelight.BulbException as ex: + _LOGGER.error("Error when calling %s: %s", func, ex) + + return _wrap + def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Yeelight bulbs.""" lights = [] if discovery_info is not None: - device = {'name': discovery_info['hostname'], - 'ipaddr': discovery_info['host']} - lights.append(YeelightLight(device)) + _LOGGER.debug("Adding autodetected %s", discovery_info['hostname']) + + # not using hostname, as it seems to vary. + name = "yeelight_%s_%s" % (discovery_info["device_type"], + discovery_info["properties"]["mac"]) + device = {'name': name, 'ipaddr': discovery_info['host']} + + lights.append(YeelightLight(device, DEVICE_SCHEMA({}))) else: for ipaddr, device_config in config[CONF_DEVICES].items(): - device = {'name': device_config[CONF_NAME], 'ipaddr': ipaddr} - lights.append(YeelightLight(device)) + _LOGGER.debug("Adding configured %s", device_config[CONF_NAME]) - add_devices(lights) + device = {'name': device_config[CONF_NAME], 'ipaddr': ipaddr} + lights.append(YeelightLight(device, device_config)) + + add_devices(lights, True) # true to request an update before adding. class YeelightLight(Light): """Representation of a Yeelight light.""" - def __init__(self, device): + def __init__(self, device, config): """Initialize the light.""" - import pyyeelight - + self.config = config self._name = device['name'] self._ipaddr = device['ipaddr'] - self.is_valid = True - self._bulb = None - self._state = None - self._bright = None + + self._supported_features = SUPPORT_YEELIGHT + self._available = False + self._bulb_device = None + + self._brightness = None + self._color_temp = None + self._is_on = None self._rgb = None - self._ct = None - try: - self._bulb = pyyeelight.YeelightBulb(self._ipaddr) - except socket.error: - self.is_valid = False - _LOGGER.error("Failed to connect to bulb %s, %s", self._ipaddr, - self._name) @property - def unique_id(self): + def available(self) -> bool: + """Return if bulb is available.""" + return self._available + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return self._supported_features + + @property + def unique_id(self) -> str: """Return the ID of this light.""" return "{}.{}".format(self.__class__, self._ipaddr) @property - def name(self): + def color_temp(self) -> int: + """Return the color temperature.""" + return self._color_temp + + @property + def name(self) -> str: """Return the name of the device if any.""" return self._name @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" - return self._state == self._bulb.POWER_ON + return self._is_on @property - def brightness(self): + def brightness(self) -> int: """Return the brightness of this light between 1..255.""" - return self._bright + return self._brightness + + def _get_rgb_from_properties(self): + rgb = self._properties.get("rgb", None) + color_mode = self._properties.get("color_mode", None) + if not rgb or not color_mode: + return rgb + + color_mode = int(color_mode) + if color_mode == 2: # color temperature + return color_temperature_to_rgb(self.color_temp) + if color_mode == 3: # hsv + hue = self._properties.get("hue") + sat = self._properties.get("sat") + val = self._properties.get("bright") + return colorsys.hsv_to_rgb(hue, sat, val) + + rgb = int(rgb) + blue = rgb & 0xff + green = (rgb >> 8) & 0xff + red = (rgb >> 16) & 0xff + + return red, green, blue @property - def rgb_color(self): + def rgb_color(self) -> tuple: """Return the color property.""" return self._rgb @property - def color_temp(self): - """Return the color temperature.""" - return color_util.color_temperature_kelvin_to_mired(self._ct) + def _properties(self) -> dict: + return self._bulb.last_properties @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_YEELIGHT + def _bulb(self) -> object: + import yeelight + if self._bulb_device is None: + try: + self._bulb_device = yeelight.Bulb(self._ipaddr) + self._bulb_device.get_properties() # force init for type - def turn_on(self, **kwargs): - """Turn the specified or all lights on.""" - if not self.is_on: - self._bulb.turn_on() + btype = self._bulb_device.bulb_type + if btype == yeelight.BulbType.Color: + self._supported_features |= SUPPORT_YEELIGHT_RGB + self._available = True + except yeelight.BulbException as ex: + self._available = False + _LOGGER.error("Failed to connect to bulb %s, %s: %s", + self._ipaddr, self._name, ex) - if ATTR_RGB_COLOR in kwargs: - rgb = kwargs[ATTR_RGB_COLOR] - self._bulb.set_rgb_color(rgb[0], rgb[1], rgb[2]) - self._rgb = [rgb[0], rgb[1], rgb[2]] + return self._bulb_device - if ATTR_COLOR_TEMP in kwargs: - kelvin = int(mired_to_kelvin(kwargs[ATTR_COLOR_TEMP])) - self._bulb.set_color_temperature(kelvin) - self._ct = kelvin - - if ATTR_BRIGHTNESS in kwargs: - bright = int(kwargs[ATTR_BRIGHTNESS] * 100 / 255) - self._bulb.set_brightness(bright) - self._bright = kwargs[ATTR_BRIGHTNESS] - - def turn_off(self, **kwargs): - """Turn the specified or all lights off.""" - self._bulb.turn_off() - - def update(self): - """Synchronize state with bulb.""" - self._bulb.refresh_property() - - # Update power state - self._state = self._bulb.get_property(self._bulb.PROPERTY_NAME_POWER) - - # Update Brightness value - bright_percent = self._bulb.get_property( - self._bulb.PROPERTY_NAME_BRIGHTNESS) - bright = int(bright_percent) * 255 / 100 - # Handle 0 - if int(bright) == 0: - self._bright = 1 + def set_music_mode(self, mode) -> None: + """Set the music mode on or off.""" + if mode: + self._bulb.start_music() else: - self._bright = int(bright) + self._bulb.stop_music() - # Update RGB Value - raw_rgb = int( - self._bulb.get_property(self._bulb.PROPERTY_NAME_RGB_COLOR)) - red = int(raw_rgb / 65536) - green = int((raw_rgb - (red * 65536)) / 256) - blue = raw_rgb - (red * 65536) - (green * 256) - self._rgb = [red, green, blue] + def update(self) -> None: + """Update properties from the bulb.""" + import yeelight + try: + self._bulb.get_properties() - # Update CT value - self._ct = int(self._bulb.get_property( - self._bulb.PROPERTY_NAME_COLOR_TEMPERATURE)) + self._is_on = self._properties.get("power") == "on" + + bright = self._properties.get("bright", None) + if bright: + self._brightness = 255 * (int(bright) / 100) + + temp_in_k = self._properties.get("ct", None) + if temp_in_k: + self._color_temp = kelvin_to_mired(int(temp_in_k)) + + self._rgb = self._get_rgb_from_properties() + + self._available = True + except yeelight.BulbException as ex: + if self._available: # just inform once + _LOGGER.error("Unable to update bulb status: %s", ex) + self._available = False + + @_cmd + def set_brightness(self, brightness, duration) -> None: + """Set bulb brightness.""" + if brightness: + _LOGGER.debug("Setting brightness: %s", brightness) + self._bulb.set_brightness(brightness / 255 * 100, + duration=duration) + + @_cmd + def set_rgb(self, rgb, duration) -> None: + """Set bulb's color.""" + if rgb and self.supported_features & SUPPORT_RGB_COLOR: + _LOGGER.debug("Setting RGB: %s", rgb) + self._bulb.set_rgb(rgb[0], rgb[1], rgb[2], duration=duration) + + @_cmd + def set_colortemp(self, colortemp, duration) -> None: + """Set bulb's color temperature.""" + if colortemp and self.supported_features & SUPPORT_COLOR_TEMP: + temp_in_k = mired_to_kelvin(colortemp) + _LOGGER.debug("Setting color temp: %s K", temp_in_k) + + self._bulb.set_color_temp(temp_in_k, duration=duration) + + @_cmd + def set_default(self) -> None: + """Set current options as default.""" + self._bulb.set_default() + + @_cmd + def set_flash(self, flash) -> None: + """Activate flash.""" + if flash: + from yeelight import RGBTransition, SleepTransition, Flow + if self._bulb.last_properties["color_mode"] != 1: + _LOGGER.error("Flash supported currently only in RGB mode.") + return + + transition = self.config[CONF_TRANSITION] + if flash == FLASH_LONG: + count = 1 + duration = transition * 5 + if flash == FLASH_SHORT: + count = 1 + duration = transition * 2 + + red, green, blue = self.rgb_color + + transitions = list() + transitions.append( + RGBTransition(255, 0, 0, brightness=10, duration=duration)) + transitions.append(SleepTransition( + duration=transition)) + transitions.append( + RGBTransition(red, green, blue, brightness=self.brightness, + duration=duration)) + + flow = Flow(count=count, transitions=transitions) + self._bulb.start_flow(flow) + + def turn_on(self, **kwargs) -> None: + """Turn the bulb on.""" + brightness = kwargs.get(ATTR_BRIGHTNESS) + colortemp = kwargs.get(ATTR_COLOR_TEMP) + rgb = kwargs.get(ATTR_RGB_COLOR) + flash = kwargs.get(ATTR_FLASH) + + duration = self.config[CONF_TRANSITION] # in ms + if ATTR_TRANSITION in kwargs: # passed kwarg overrides config + duration = kwargs.get(ATTR_TRANSITION) * 1000 # kwarg in s + + self._bulb.turn_on(duration=duration) + + if self.config[CONF_MODE_MUSIC] and not self._bulb.music_mode: + self.set_music_mode(self.config[CONF_MODE_MUSIC]) + + # values checked for none in methods + self.set_rgb(rgb, duration) + self.set_colortemp(colortemp, duration) + self.set_brightness(brightness, duration) + self.set_flash(flash) + + # save the current state if we had a manual change. + if self.config[CONF_SAVE_ON_CHANGE]: + if brightness or colortemp or rgb: + self.set_default() + + def turn_off(self, **kwargs) -> None: + """Turn off.""" + self._bulb.turn_off() diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py index 5bab9ace4c6..a0804097830 100644 --- a/homeassistant/components/light/zwave.py +++ b/homeassistant/components/light/zwave.py @@ -217,11 +217,9 @@ class ZwaveColorLight(ZwaveDimmer): self._value_color = value_color if self._value_color_channels is None: - for value_color_channels in self._value.node.get_values( - class_id=zwave.const.COMMAND_CLASS_SWITCH_COLOR, - genre=zwave.const.GENRE_SYSTEM, - type=zwave.const.TYPE_INT).values(): - self._value_color_channels = value_color_channels + self._value_color_channels = self.get_value( + class_id=zwave.const.COMMAND_CLASS_SWITCH_COLOR, + genre=zwave.const.GENRE_SYSTEM, type=zwave.const.TYPE_INT) if self._value_color and self._value_color_channels: _LOGGER.debug("Zwave node color values found.") diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index a7d392b321e..790f0783a9a 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -4,7 +4,9 @@ Component to interface with various locks that can be controlled remotely. For more details about this component, please refer to the documentation at https://home-assistant.io/components/lock/ """ +import asyncio from datetime import timedelta +import functools as ft import logging import os @@ -67,38 +69,54 @@ def unlock(hass, entity_id=None, code=None): hass.services.call(DOMAIN, SERVICE_UNLOCK, data) -def setup(hass, config): +@asyncio.coroutine +def async_setup(hass, config): """Track states and offer events for locks.""" component = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_LOCKS) - component.setup(config) - def handle_lock_service(service): + yield from component.async_setup(config) + + @asyncio.coroutine + def async_handle_lock_service(service): """Handle calls to the lock services.""" - target_locks = component.extract_from_service(service) + target_locks = component.async_extract_from_service(service) code = service.data.get(ATTR_CODE) - for item in target_locks: + for entity in target_locks: if service.service == SERVICE_LOCK: - item.lock(code=code) + yield from entity.async_lock(code=code) else: - item.unlock(code=code) + yield from entity.async_unlock(code=code) - for item in target_locks: - if not item.should_poll: + update_tasks = [] + + for entity in target_locks: + if not entity.should_poll: continue - item.update_ha_state(True) + update_coro = hass.loop.create_task( + entity.async_update_ha_state(True)) + if hasattr(entity, 'async_update'): + update_tasks.append(update_coro) + else: + yield from update_coro + + if update_tasks: + yield from asyncio.wait(update_tasks, loop=hass.loop) + + descriptions = yield from hass.loop.run_in_executor( + None, load_yaml_config_file, os.path.join( + os.path.dirname(__file__), 'services.yaml')) + + hass.services.async_register( + DOMAIN, SERVICE_UNLOCK, async_handle_lock_service, + descriptions.get(SERVICE_UNLOCK), schema=LOCK_SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_LOCK, async_handle_lock_service, + descriptions.get(SERVICE_LOCK), schema=LOCK_SERVICE_SCHEMA) - descriptions = load_yaml_config_file( - os.path.join(os.path.dirname(__file__), 'services.yaml')) - hass.services.register(DOMAIN, SERVICE_UNLOCK, handle_lock_service, - descriptions.get(SERVICE_UNLOCK), - schema=LOCK_SERVICE_SCHEMA) - hass.services.register(DOMAIN, SERVICE_LOCK, handle_lock_service, - descriptions.get(SERVICE_LOCK), - schema=LOCK_SERVICE_SCHEMA) return True @@ -125,10 +143,26 @@ class LockDevice(Entity): """Lock the lock.""" raise NotImplementedError() + def async_lock(self, **kwargs): + """Lock the lock. + + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.loop.run_in_executor( + None, ft.partial(self.lock, **kwargs)) + def unlock(self, **kwargs): """Unlock the lock.""" raise NotImplementedError() + def async_unlock(self, **kwargs): + """Unlock the lock. + + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.loop.run_in_executor( + None, ft.partial(self.unlock, **kwargs)) + @property def state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/lock/demo.py b/homeassistant/components/lock/demo.py index 55929227039..fca922b11e2 100644 --- a/homeassistant/components/lock/demo.py +++ b/homeassistant/components/lock/demo.py @@ -43,9 +43,9 @@ class DemoLock(LockDevice): def lock(self, **kwargs): """Lock the device.""" self._state = STATE_LOCKED - self.update_ha_state() + self.schedule_update_ha_state() def unlock(self, **kwargs): """Unlock the device.""" self._state = STATE_UNLOCKED - self.update_ha_state() + self.schedule_update_ha_state() diff --git a/homeassistant/components/lock/mqtt.py b/homeassistant/components/lock/mqtt.py index da6e595914b..fde62c8695e 100644 --- a/homeassistant/components/lock/mqtt.py +++ b/homeassistant/components/lock/mqtt.py @@ -82,10 +82,10 @@ class MqttLock(LockDevice): payload) if payload == self._payload_lock: self._state = True - self.update_ha_state() + self.schedule_update_ha_state() elif payload == self._payload_unlock: self._state = False - self.update_ha_state() + self.schedule_update_ha_state() if self._state_topic is None: # Force into optimistic mode. @@ -121,7 +121,7 @@ class MqttLock(LockDevice): if self._optimistic: # Optimistically assume that switch has changed state. self._state = True - self.update_ha_state() + self.schedule_update_ha_state() def unlock(self, **kwargs): """Unlock the device.""" @@ -130,4 +130,4 @@ class MqttLock(LockDevice): if self._optimistic: # Optimistically assume that switch has changed state. self._state = False - self.update_ha_state() + self.schedule_update_ha_state() diff --git a/homeassistant/components/lock/nuki.py b/homeassistant/components/lock/nuki.py new file mode 100644 index 00000000000..b167c00b9bc --- /dev/null +++ b/homeassistant/components/lock/nuki.py @@ -0,0 +1,74 @@ +""" +Nuki.io lock platform. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/lock.nuki/ +""" +from datetime import timedelta +import logging +import voluptuous as vol + +from homeassistant.components.lock import (LockDevice, PLATFORM_SCHEMA) +from homeassistant.const import (CONF_HOST, CONF_PORT, CONF_TOKEN) +from homeassistant.util import Throttle +import homeassistant.helpers.config_validation as cv + + +REQUIREMENTS = ['pynuki==1.2'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_PORT = 8080 + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Required(CONF_TOKEN): cv.string +}) + + +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30) +MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=5) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Demo lock platform.""" + from pynuki import NukiBridge + bridge = NukiBridge(config.get(CONF_HOST), config.get(CONF_TOKEN)) + add_devices([NukiLock(lock) for lock in bridge.locks]) + + +class NukiLock(LockDevice): + """Representation of a Nuki lock.""" + + def __init__(self, nuki_lock): + """Initialize the lock.""" + self._nuki_lock = nuki_lock + self._locked = nuki_lock.is_locked + self._name = nuki_lock.name + + @property + def name(self): + """Return the name of the lock.""" + return self._name + + @property + def is_locked(self): + """Return true if lock is locked.""" + return self._locked + + @Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) + def update(self): + """Update the nuki lock properties.""" + self._nuki_lock.update() + self._name = self._nuki_lock.name + self._locked = self._nuki_lock.is_locked + + def lock(self, **kwargs): + """Lock the device.""" + self._nuki_lock.lock() + + def unlock(self, **kwargs): + """Unlock the device.""" + self._nuki_lock.unlock() diff --git a/homeassistant/components/lock/vera.py b/homeassistant/components/lock/vera.py index 0307bbf4312..14606c0853c 100644 --- a/homeassistant/components/lock/vera.py +++ b/homeassistant/components/lock/vera.py @@ -35,13 +35,11 @@ class VeraLock(VeraDevice, LockDevice): """Lock the device.""" self.vera_device.lock() self._state = STATE_LOCKED - self.update_ha_state() def unlock(self, **kwargs): """Unlock the device.""" self.vera_device.unlock() self._state = STATE_UNLOCKED - self.update_ha_state() @property def is_locked(self): diff --git a/homeassistant/components/lock/wink.py b/homeassistant/components/lock/wink.py index 4536387e4ac..76922cf9d62 100644 --- a/homeassistant/components/lock/wink.py +++ b/homeassistant/components/lock/wink.py @@ -6,7 +6,7 @@ https://home-assistant.io/components/lock.wink/ """ from homeassistant.components.lock import LockDevice -from homeassistant.components.wink import WinkDevice +from homeassistant.components.wink import WinkDevice, DOMAIN DEPENDENCIES = ['wink'] @@ -15,7 +15,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Wink platform.""" import pywink - add_devices(WinkLockDevice(lock, hass) for lock in pywink.get_locks()) + for lock in pywink.get_locks(): + _id = lock.object_id() + lock.name() + if _id not in hass.data[DOMAIN]['unique_ids']: + add_devices([WinkLockDevice(lock, hass)]) class WinkLockDevice(WinkDevice, LockDevice): @@ -23,7 +26,7 @@ class WinkLockDevice(WinkDevice, LockDevice): def __init__(self, wink, hass): """Initialize the lock.""" - WinkDevice.__init__(self, wink, hass) + super().__init__(wink, hass) @property def is_locked(self): diff --git a/homeassistant/components/lock/zwave.py b/homeassistant/components/lock/zwave.py index 1f5f2ca8b15..6501c7d1b74 100644 --- a/homeassistant/components/lock/zwave.py +++ b/homeassistant/components/lock/zwave.py @@ -14,6 +14,7 @@ import voluptuous as vol from homeassistant.components.lock import DOMAIN, LockDevice from homeassistant.components import zwave from homeassistant.config import load_yaml_config_file +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -37,74 +38,74 @@ DEVICE_MAPPINGS = { } LOCK_NOTIFICATION = { - 1: 'Manual Lock', - 2: 'Manual Unlock', - 3: 'RF Lock', - 4: 'RF Unlock', - 5: 'Keypad Lock', - 6: 'Keypad Unlock', - 11: 'Lock Jammed', - 254: 'Unknown Event' + '1': 'Manual Lock', + '2': 'Manual Unlock', + '3': 'RF Lock', + '4': 'RF Unlock', + '5': 'Keypad Lock', + '6': 'Keypad Unlock', + '11': 'Lock Jammed', + '254': 'Unknown Event' } LOCK_ALARM_TYPE = { - 9: 'Deadbolt Jammed', - 18: 'Locked with Keypad by user', - 19: 'Unlocked with Keypad by user ', - 21: 'Manually Locked by', - 22: 'Manually Unlocked by Key or Inside thumb turn', - 24: 'Locked by RF', - 25: 'Unlocked by RF', - 27: 'Auto re-lock', - 33: 'User deleted: ', - 112: 'Master code changed or User added: ', - 113: 'Duplicate Pin-code: ', - 130: 'RF module, power restored', - 161: 'Tamper Alarm: ', - 167: 'Low Battery', - 168: 'Critical Battery Level', - 169: 'Battery too low to operate' + '9': 'Deadbolt Jammed', + '18': 'Locked with Keypad by user ', + '19': 'Unlocked with Keypad by user ', + '21': 'Manually Locked by', + '22': 'Manually Unlocked by Key or Inside thumb turn', + '24': 'Locked by RF', + '25': 'Unlocked by RF', + '27': 'Auto re-lock', + '33': 'User deleted: ', + '112': 'Master code changed or User added: ', + '113': 'Duplicate Pin-code: ', + '130': 'RF module, power restored', + '161': 'Tamper Alarm: ', + '167': 'Low Battery', + '168': 'Critical Battery Level', + '169': 'Battery too low to operate' } MANUAL_LOCK_ALARM_LEVEL = { - 1: 'Key Cylinder or Inside thumb turn', - 2: 'Touch function (lock and leave)' + '1': 'Key Cylinder or Inside thumb turn', + '2': 'Touch function (lock and leave)' } TAMPER_ALARM_LEVEL = { - 1: 'Too many keypresses', - 2: 'Cover removed' + '1': 'Too many keypresses', + '2': 'Cover removed' } LOCK_STATUS = { - 1: True, - 2: False, - 3: True, - 4: False, - 5: True, - 6: False, - 9: False, - 18: True, - 19: False, - 21: True, - 22: False, - 24: True, - 25: False, - 27: True + '1': True, + '2': False, + '3': True, + '4': False, + '5': True, + '6': False, + '9': False, + '18': True, + '19': False, + '21': True, + '22': False, + '24': True, + '25': False, + '27': True } ALARM_TYPE_STD = [ - 18, - 19, - 33, - 112, - 113 + '18', + '19', + '33', + '112', + '113' ] SET_USERCODE_SCHEMA = vol.Schema({ vol.Required(zwave.const.ATTR_NODE_ID): vol.Coerce(int), vol.Required(ATTR_CODE_SLOT): vol.Coerce(int), - vol.Required(ATTR_USERCODE): vol.Coerce(int), + vol.Required(ATTR_USERCODE): cv.string, }) GET_USERCODE_SCHEMA = vol.Schema({ @@ -233,66 +234,58 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice): def update_properties(self): """Callback on data changes for node values.""" - for value in self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_DOOR_LOCK).values(): - if value.type != zwave.const.TYPE_BOOL: - continue - if value.genre != zwave.const.GENRE_USER: - continue - self._state = value.data - _LOGGER.debug('Lock state set from Bool value and' - ' is %s', value.data) - break + self._state = self.get_value(class_id=zwave + .const.COMMAND_CLASS_DOOR_LOCK, + type=zwave.const.TYPE_BOOL, + genre=zwave.const.GENRE_USER, + member='data') + _LOGGER.debug('Lock state set from Bool value and' + ' is %s', self._state) + notification_data = self.get_value(class_id=zwave.const + .COMMAND_CLASS_ALARM, + label=['Access Control'], + member='data') + if notification_data: + self._notification = LOCK_NOTIFICATION.get(str(notification_data)) + if self._v2btze: + advanced_config = self.get_value(class_id=zwave.const + .COMMAND_CLASS_CONFIGURATION, + index=12, + data=CONFIG_ADVANCED, + member='data') + if advanced_config: + self._state = LOCK_STATUS.get(str(notification_data)) + _LOGGER.debug('Lock state set from Access Control ' + 'value and is %s, get=%s', + str(notification_data), + self.state) - for value in self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_ALARM).values(): - if value.label != "Access Control": - continue - self._notification = LOCK_NOTIFICATION.get(value.data) - notification_data = value.data - if self._v2btze: - for value in (self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_CONFIGURATION) - .values()): - if value.index != 12: - continue - if value.data == CONFIG_ADVANCED: - self._state = LOCK_STATUS.get(notification_data) - _LOGGER.debug('Lock state set from Access Control ' - 'value and is %s', notification_data) - break - - break - - for value in self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_ALARM).values(): - if value.label != "Alarm Type": - continue - alarm_type = LOCK_ALARM_TYPE.get(value.data) - break - - for value in self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_ALARM).values(): - if value.label != "Alarm Level": - continue - alarm_level = value.data - _LOGGER.debug('Lock alarm_level is %s', alarm_level) - if alarm_type is 21: - self._lock_status = '{}{}'.format( - LOCK_ALARM_TYPE.get(alarm_type), - MANUAL_LOCK_ALARM_LEVEL.get(alarm_level)) - if alarm_type in ALARM_TYPE_STD: - self._lock_status = '{}{}'.format( - LOCK_ALARM_TYPE.get(alarm_type), alarm_level) - break - if alarm_type is 161: - self._lock_status = '{}{}'.format( - LOCK_ALARM_TYPE.get(alarm_type), - TAMPER_ALARM_LEVEL.get(alarm_level)) - break - if alarm_type != 0: - self._lock_status = LOCK_ALARM_TYPE.get(alarm_type) - break + alarm_type = self.get_value(class_id=zwave.const + .COMMAND_CLASS_ALARM, + label=['Alarm Type'], member='data') + _LOGGER.debug('Lock alarm_type is %s', str(alarm_type)) + alarm_level = self.get_value(class_id=zwave.const + .COMMAND_CLASS_ALARM, + label=['Alarm Level'], member='data') + _LOGGER.debug('Lock alarm_level is %s', str(alarm_level)) + if not alarm_type: + return + if alarm_type is 21: + self._lock_status = '{}{}'.format( + LOCK_ALARM_TYPE.get(str(alarm_type)), + MANUAL_LOCK_ALARM_LEVEL.get(str(alarm_level))) + if alarm_type in ALARM_TYPE_STD: + self._lock_status = '{}{}'.format( + LOCK_ALARM_TYPE.get(str(alarm_type)), str(alarm_level)) + return + if alarm_type is 161: + self._lock_status = '{}{}'.format( + LOCK_ALARM_TYPE.get(str(alarm_type)), + TAMPER_ALARM_LEVEL.get(str(alarm_level))) + return + if alarm_type != 0: + self._lock_status = LOCK_ALARM_TYPE.get(str(alarm_type)) + return @property def is_locked(self): diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 71901b6256a..a1077b1f132 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -77,7 +77,6 @@ ATTR_MEDIA_CHANNEL = 'media_channel' ATTR_MEDIA_PLAYLIST = 'media_playlist' ATTR_APP_ID = 'app_id' ATTR_APP_NAME = 'app_name' -ATTR_SUPPORTED_MEDIA_COMMANDS = 'supported_media_commands' ATTR_INPUT_SOURCE = 'source' ATTR_INPUT_SOURCE_LIST = 'source_list' ATTR_MEDIA_ENQUEUE = 'enqueue' @@ -183,7 +182,6 @@ ATTR_TO_PROPERTY = [ ATTR_MEDIA_PLAYLIST, ATTR_APP_ID, ATTR_APP_NAME, - ATTR_SUPPORTED_MEDIA_COMMANDS, ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE_LIST, ] @@ -523,7 +521,39 @@ class MediaPlayerDevice(Entity): @property def supported_media_commands(self): - """Flag media commands that are supported.""" + """Flag media commands that are supported. + + DEPRECATED: Included for temporary custom platform compatibility. + """ + return None + + @property + def supported_features(self): + """Flag media player features that are supported.""" + # Begin temporary transition logic + + if self.supported_media_commands is not None: + # If this platform is still using supported_media_commands, issue + # a logger warning once with instructions on how to fix it. + if not getattr(self, '_supported_features_warned', False): + def show_warning(): + """Show a deprecation warning in the log for this class.""" + import inspect + _LOGGER.warning( + "supported_media_commands is deprecated. Please " + "rename supported_media_commands to " + "supported_features in '%s' to ensure future support.", + inspect.getfile(self.__class__)) + # This is a temporary attribute. We don't want to pollute + # __init__ so it can be easily removed. + # pylint: disable=attribute-defined-outside-init + self._supported_features_warned = True + self.hass.add_job(show_warning) + + # Return the old property + return self.supported_media_commands + + # End temporary transition logic return 0 def turn_on(self): @@ -686,57 +716,57 @@ class MediaPlayerDevice(Entity): @property def support_play(self): """Boolean if play is supported.""" - return bool(self.supported_media_commands & SUPPORT_PLAY) + return bool(self.supported_features & SUPPORT_PLAY) @property def support_pause(self): """Boolean if pause is supported.""" - return bool(self.supported_media_commands & SUPPORT_PAUSE) + return bool(self.supported_features & SUPPORT_PAUSE) @property def support_stop(self): """Boolean if stop is supported.""" - return bool(self.supported_media_commands & SUPPORT_STOP) + return bool(self.supported_features & SUPPORT_STOP) @property def support_seek(self): """Boolean if seek is supported.""" - return bool(self.supported_media_commands & SUPPORT_SEEK) + return bool(self.supported_features & SUPPORT_SEEK) @property def support_volume_set(self): """Boolean if setting volume is supported.""" - return bool(self.supported_media_commands & SUPPORT_VOLUME_SET) + return bool(self.supported_features & SUPPORT_VOLUME_SET) @property def support_volume_mute(self): """Boolean if muting volume is supported.""" - return bool(self.supported_media_commands & SUPPORT_VOLUME_MUTE) + return bool(self.supported_features & SUPPORT_VOLUME_MUTE) @property def support_previous_track(self): """Boolean if previous track command supported.""" - return bool(self.supported_media_commands & SUPPORT_PREVIOUS_TRACK) + return bool(self.supported_features & SUPPORT_PREVIOUS_TRACK) @property def support_next_track(self): """Boolean if next track command supported.""" - return bool(self.supported_media_commands & SUPPORT_NEXT_TRACK) + return bool(self.supported_features & SUPPORT_NEXT_TRACK) @property def support_play_media(self): """Boolean if play media command supported.""" - return bool(self.supported_media_commands & SUPPORT_PLAY_MEDIA) + return bool(self.supported_features & SUPPORT_PLAY_MEDIA) @property def support_select_source(self): """Boolean if select source command supported.""" - return bool(self.supported_media_commands & SUPPORT_SELECT_SOURCE) + return bool(self.supported_features & SUPPORT_SELECT_SOURCE) @property def support_clear_playlist(self): """Boolean if clear playlist command supported.""" - return bool(self.supported_media_commands & SUPPORT_CLEAR_PLAYLIST) + return bool(self.supported_features & SUPPORT_CLEAR_PLAYLIST) def toggle(self): """Toggle the power on the media player.""" @@ -821,14 +851,12 @@ class MediaPlayerDevice(Entity): def state_attributes(self): """Return the state attributes.""" if self.state == STATE_OFF: - state_attr = { - ATTR_SUPPORTED_MEDIA_COMMANDS: self.supported_media_commands, - } - else: - state_attr = { - attr: getattr(self, attr) for attr - in ATTR_TO_PROPERTY if getattr(self, attr) is not None - } + return None + + state_attr = { + attr: getattr(self, attr) for attr + in ATTR_TO_PROPERTY if getattr(self, attr) is not None + } return state_attr diff --git a/homeassistant/components/media_player/anthemav.py b/homeassistant/components/media_player/anthemav.py index f53f5cf9264..01b4b32deb2 100644 --- a/homeassistant/components/media_player/anthemav.py +++ b/homeassistant/components/media_player/anthemav.py @@ -79,8 +79,8 @@ class AnthemAVR(MediaPlayerDevice): return getattr(self.avr.protocol, propname, dval) @property - def supported_media_commands(self): - """Return flag of media commands that are supported.""" + def supported_features(self): + """Flag media player features that are supported.""" return SUPPORT_ANTHEMAV @property diff --git a/homeassistant/components/media_player/apple_tv.py b/homeassistant/components/media_player/apple_tv.py new file mode 100644 index 00000000000..2f47b1ba0a8 --- /dev/null +++ b/homeassistant/components/media_player/apple_tv.py @@ -0,0 +1,255 @@ +""" +Support for Apple TV. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.apple_tv/ +""" +import asyncio +import logging +import hashlib + +import aiohttp +import voluptuous as vol + +from homeassistant.components.media_player import ( + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, + SUPPORT_STOP, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, MediaPlayerDevice, + PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC, MEDIA_TYPE_VIDEO, MEDIA_TYPE_TVSHOW) +from homeassistant.const import ( + STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_STANDBY, CONF_HOST, + CONF_NAME, EVENT_HOMEASSISTANT_STOP) +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util + + +REQUIREMENTS = ['pyatv==0.1.3'] + +_LOGGER = logging.getLogger(__name__) + +CONF_LOGIN_ID = 'login_id' + +DEFAULT_NAME = 'Apple TV' + +DATA_APPLE_TV = 'apple_tv' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_LOGIN_ID): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Setup the Apple TV platform.""" + import pyatv + + if discovery_info is not None: + name = discovery_info['name'] + host = discovery_info['host'] + login_id = discovery_info['hsgid'] + else: + name = config.get(CONF_NAME) + host = config.get(CONF_HOST) + login_id = config.get(CONF_LOGIN_ID) + + if DATA_APPLE_TV not in hass.data: + hass.data[DATA_APPLE_TV] = [] + + if host in hass.data[DATA_APPLE_TV]: + return False + hass.data[DATA_APPLE_TV].append(host) + + details = pyatv.AppleTVDevice(name, host, login_id) + session = async_get_clientsession(hass) + atv = pyatv.connect_to_apple_tv(details, hass.loop, session=session) + entity = AppleTvDevice(atv, name) + + @asyncio.coroutine + def async_stop_subscription(event): + """Logout device to close its session.""" + _LOGGER.info("Closing Apple TV session") + yield from atv.logout() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, + async_stop_subscription) + + yield from async_add_entities([entity], update_before_add=True) + + +class AppleTvDevice(MediaPlayerDevice): + """Representation of an Apple TV device.""" + + def __init__(self, atv, name): + """Initialize the Apple TV device.""" + self._name = name + self._atv = atv + self._playing = None + self._artwork = None + self._artwork_hash = None + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + if self._playing is not None: + from pyatv import const + state = self._playing.play_state + if state == const.PLAY_STATE_NO_MEDIA: + return STATE_IDLE + elif state == const.PLAY_STATE_PLAYING or \ + state == const.PLAY_STATE_LOADING: + return STATE_PLAYING + elif state == const.PLAY_STATE_PAUSED or \ + state == const.PLAY_STATE_FAST_FORWARD or \ + state == const.PLAY_STATE_FAST_BACKWARD: + # Catch fast forward/backward here so "play" is default action + return STATE_PAUSED + else: + return STATE_STANDBY # Bad or unknown state? + + @asyncio.coroutine + def async_update(self): + """Retrieve latest state.""" + from pyatv import exceptions + try: + playing = yield from self._atv.metadata.playing() + + if self._should_download_artwork(playing): + self._artwork = None + self._artwork_hash = None + self._artwork = yield from self._atv.metadata.artwork() + if self._artwork: + self._artwork_hash = hashlib.md5(self._artwork).hexdigest() + + self._playing = playing + except exceptions.AuthenticationError as ex: + _LOGGER.warning('%s (bad login id?)', str(ex)) + except aiohttp.errors.ClientOSError as ex: + _LOGGER.error('failed to connect to Apple TV (%s)', str(ex)) + except asyncio.TimeoutError: + _LOGGER.warning('timed out while connecting to Apple TV') + + def _should_download_artwork(self, new_playing): + if self._playing is None: + return True + old_playing = self._playing + return new_playing.media_type != old_playing.media_type or \ + new_playing.title != old_playing.title + + @property + def media_content_type(self): + """Content type of current playing media.""" + if self._playing is not None: + from pyatv import const + media_type = self._playing.media_type + if media_type == const.MEDIA_TYPE_VIDEO: + return MEDIA_TYPE_VIDEO + elif media_type == const.MEDIA_TYPE_MUSIC: + return MEDIA_TYPE_MUSIC + elif media_type == const.MEDIA_TYPE_TV: + return MEDIA_TYPE_TVSHOW + + @property + def media_duration(self): + """Duration of current playing media in seconds.""" + if self._playing is not None: + return self._playing.total_time + + @property + def media_position(self): + """Position of current playing media in seconds.""" + if self._playing is not None: + return self._playing.position + + @property + def media_position_updated_at(self): + """Last valid time of media position.""" + state = self.state + if state == STATE_PLAYING or state == STATE_PAUSED: + return dt_util.utcnow() + + @asyncio.coroutine + def async_play_media(self, media_type, media_id, **kwargs): + """Send the play_media command to the media player.""" + yield from self._atv.remote_control.play_url(media_id, 0) + + @property + def media_image(self): + """Artwork for what is currently playing.""" + return self._artwork, 'image/png', self._artwork_hash + + @property + def media_title(self): + """Title of current playing media.""" + if self._playing is not None: + title = self._playing.title + return title if title else "No title" + + @property + def supported_features(self): + """Flag media player features that are supported.""" + if self._playing is not None: + if self.state != STATE_IDLE: + return SUPPORT_PAUSE | SUPPORT_PLAY | \ + SUPPORT_SEEK | SUPPORT_STOP | \ + SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK | \ + SUPPORT_PLAY_MEDIA + else: + return SUPPORT_PLAY_MEDIA + + def async_media_play_pause(self): + """Pause media on media player. + + This method must be run in the event loop and returns a coroutine. + """ + if self._playing is not None: + state = self.state + if state == STATE_PAUSED: + return self._atv.remote_control.play() + elif state == STATE_PLAYING: + return self._atv.remote_control.pause() + + def async_media_play(self): + """Play media. + + This method must be run in the event loop and returns a coroutine. + """ + if self._playing is not None: + return self._atv.remote_control.play() + + def async_media_pause(self): + """Pause the media player. + + This method must be run in the event loop and returns a coroutine. + """ + if self._playing is not None: + return self._atv.remote_control.pause() + + def async_media_next_track(self): + """Send next track command. + + This method must be run in the event loop and returns a coroutine. + """ + if self._playing is not None: + return self._atv.remote_control.next() + + def async_media_previous_track(self): + """Send previous track command. + + This method must be run in the event loop and returns a coroutine. + """ + if self._playing is not None: + return self._atv.remote_control.previous() + + @asyncio.coroutine + def async_media_seek(self, position): + """Send seek command.""" + if self._playing is not None: + yield from self._atv.remote_control.set_position(position) diff --git a/homeassistant/components/media_player/aquostv.py b/homeassistant/components/media_player/aquostv.py index 0654e393780..65d3e2c93fd 100644 --- a/homeassistant/components/media_player/aquostv.py +++ b/homeassistant/components/media_player/aquostv.py @@ -191,8 +191,8 @@ class SharpAquosTVDevice(MediaPlayerDevice): return self._muted @property - def supported_media_commands(self): - """Flag of media commands that are supported.""" + def supported_features(self): + """Flag media player features that are supported.""" return SUPPORT_SHARPTV @_retry diff --git a/homeassistant/components/media_player/braviatv.py b/homeassistant/components/media_player/braviatv.py index 20eb4fc7cca..d6e7261ec4f 100644 --- a/homeassistant/components/media_player/braviatv.py +++ b/homeassistant/components/media_player/braviatv.py @@ -316,8 +316,8 @@ class BraviaTVDevice(MediaPlayerDevice): return self._muted @property - def supported_media_commands(self): - """Flag of media commands that are supported.""" + def supported_features(self): + """Flag media player features that are supported.""" return SUPPORT_BRAVIA @property diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 202c877c2b1..b7e7504b2f8 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -20,7 +20,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -REQUIREMENTS = ['pychromecast==0.7.6'] +REQUIREMENTS = ['pychromecast==0.8.0'] _LOGGER = logging.getLogger(__name__) @@ -226,8 +226,8 @@ class CastDevice(MediaPlayerDevice): return self.cast.app_display_name @property - def supported_media_commands(self): - """Flag of media commands that are supported.""" + def supported_features(self): + """Flag media player features that are supported.""" return SUPPORT_CAST @property diff --git a/homeassistant/components/media_player/cmus.py b/homeassistant/components/media_player/cmus.py index ac6885e3450..de946b3e349 100644 --- a/homeassistant/components/media_player/cmus.py +++ b/homeassistant/components/media_player/cmus.py @@ -151,8 +151,8 @@ class CmusDevice(MediaPlayerDevice): return int(volume)/100 @property - def supported_media_commands(self): - """Flag of media commands that are supported.""" + def supported_features(self): + """Flag media player features that are supported.""" return SUPPORT_CMUS def turn_off(self): diff --git a/homeassistant/components/media_player/demo.py b/homeassistant/components/media_player/demo.py index 4e9867b49e9..aab75f702d4 100644 --- a/homeassistant/components/media_player/demo.py +++ b/homeassistant/components/media_player/demo.py @@ -155,8 +155,8 @@ class DemoYoutubePlayer(AbstractDemoPlayer): return "YouTube" @property - def supported_media_commands(self): - """Flag of media commands that are supported.""" + def supported_features(self): + """Flag media player features that are supported.""" return YOUTUBE_PLAYER_SUPPORT @property @@ -269,8 +269,8 @@ class DemoMusicPlayer(AbstractDemoPlayer): return self._cur_track + 1 @property - def supported_media_commands(self): - """Flag of media commands that are supported.""" + def supported_features(self): + """Flag media player features that are supported.""" support = MUSIC_PLAYER_SUPPORT if self._cur_track > 0: @@ -364,8 +364,8 @@ class DemoTVShowPlayer(AbstractDemoPlayer): return self._source @property - def supported_media_commands(self): - """Flag of media commands that are supported.""" + def supported_features(self): + """Flag media player features that are supported.""" support = NETFLIX_PLAYER_SUPPORT if self._cur_episode > 1: diff --git a/homeassistant/components/media_player/denon.py b/homeassistant/components/media_player/denon.py index 22ccd2f0d56..4b3347a832a 100755 --- a/homeassistant/components/media_player/denon.py +++ b/homeassistant/components/media_player/denon.py @@ -193,8 +193,8 @@ class DenonDevice(MediaPlayerDevice): return self._mediainfo @property - def supported_media_commands(self): - """Flag of media commands that are supported.""" + def supported_features(self): + """Flag media player features that are supported.""" if self._mediasource in MEDIA_MODES.values(): return SUPPORT_DENON | SUPPORT_MEDIA_MODES else: diff --git a/homeassistant/components/media_player/denonavr.py b/homeassistant/components/media_player/denonavr.py index e6f0bf99d42..eba2d031158 100644 --- a/homeassistant/components/media_player/denonavr.py +++ b/homeassistant/components/media_player/denonavr.py @@ -167,8 +167,8 @@ class DenonDevice(MediaPlayerDevice): return self._source_list @property - def supported_media_commands(self): - """Flag of media commands that are supported.""" + def supported_features(self): + """Flag media player features that are supported.""" if self._current_source in self._receiver.netaudio_func_list: return SUPPORT_DENON | SUPPORT_MEDIA_MODES else: diff --git a/homeassistant/components/media_player/directv.py b/homeassistant/components/media_player/directv.py index e63301db4d9..035fe6d9cd6 100644 --- a/homeassistant/components/media_player/directv.py +++ b/homeassistant/components/media_player/directv.py @@ -133,8 +133,8 @@ class DirecTvDevice(MediaPlayerDevice): return None @property - def supported_media_commands(self): - """Flag of media commands that are supported.""" + def supported_features(self): + """Flag media player features that are supported.""" return SUPPORT_DTV @property diff --git a/homeassistant/components/media_player/dunehd.py b/homeassistant/components/media_player/dunehd.py index 9deab4bcdff..1facb523da6 100644 --- a/homeassistant/components/media_player/dunehd.py +++ b/homeassistant/components/media_player/dunehd.py @@ -97,8 +97,8 @@ class DuneHDPlayerEntity(MediaPlayerDevice): return list(self._sources.keys()) @property - def supported_media_commands(self): - """Flag of media commands that are supported.""" + def supported_features(self): + """Flag media player features that are supported.""" return DUNEHD_PLAYER_SUPPORT def volume_up(self): diff --git a/homeassistant/components/media_player/emby.py b/homeassistant/components/media_player/emby.py index 4aac93c42d9..3ff3a40e5cc 100644 --- a/homeassistant/components/media_player/emby.py +++ b/homeassistant/components/media_player/emby.py @@ -309,8 +309,8 @@ class EmbyClient(MediaPlayerDevice): return self.now_playing_item['IndexNumber'] @property - def supported_media_commands(self): - """Flag of media commands that are supported.""" + def supported_features(self): + """Flag media player features that are supported.""" if self.supports_remote_control: return SUPPORT_EMBY else: diff --git a/homeassistant/components/media_player/firetv.py b/homeassistant/components/media_player/firetv.py index 1b0a9b02b63..237faddaaea 100644 --- a/homeassistant/components/media_player/firetv.py +++ b/homeassistant/components/media_player/firetv.py @@ -122,8 +122,8 @@ class FireTVDevice(MediaPlayerDevice): return True @property - def supported_media_commands(self): - """Flag of media commands that are supported.""" + def supported_features(self): + """Flag media player features that are supported.""" return SUPPORT_FIRETV @property diff --git a/homeassistant/components/media_player/gpmdp.py b/homeassistant/components/media_player/gpmdp.py index c98b32137c7..c3283aca382 100644 --- a/homeassistant/components/media_player/gpmdp.py +++ b/homeassistant/components/media_player/gpmdp.py @@ -295,8 +295,8 @@ class GPMDP(MediaPlayerDevice): return self._name @property - def supported_media_commands(self): - """Flag of media commands that are supported.""" + def supported_features(self): + """Flag media player features that are supported.""" return SUPPORT_GPMDP def media_next_track(self): @@ -311,13 +311,13 @@ class GPMDP(MediaPlayerDevice): """Send media_play command to media player.""" self.send_gpmdp_msg('playback', 'playPause', False) self._status = STATE_PLAYING - self.update_ha_state() + self.schedule_update_ha_state() def media_pause(self): """Send media_pause command to media player.""" self.send_gpmdp_msg('playback', 'playPause', False) self._status = STATE_PAUSED - self.update_ha_state() + self.schedule_update_ha_state() def media_seek(self, position): """Send media_seek command to media player.""" @@ -327,7 +327,7 @@ class GPMDP(MediaPlayerDevice): websocket.send(json.dumps({'namespace': 'playback', 'method': 'setCurrentTime', 'arguments': [position*1000]})) - self.update_ha_state() + self.schedule_update_ha_state() def volume_up(self): """Send volume_up command to media player.""" @@ -335,7 +335,7 @@ class GPMDP(MediaPlayerDevice): if websocket is None: return websocket.send('{"namespace": "volume", "method": "increaseVolume"}') - self.update_ha_state() + self.schedule_update_ha_state() def volume_down(self): """Send volume_down command to media player.""" @@ -343,7 +343,7 @@ class GPMDP(MediaPlayerDevice): if websocket is None: return websocket.send('{"namespace": "volume", "method": "decreaseVolume"}') - self.update_ha_state() + self.schedule_update_ha_state() def set_volume_level(self, volume): """Set volume on media player, range(0..1).""" @@ -353,4 +353,4 @@ class GPMDP(MediaPlayerDevice): websocket.send(json.dumps({'namespace': 'volume', 'method': 'setVolume', 'arguments': [volume*100]})) - self.update_ha_state() + self.schedule_update_ha_state() diff --git a/homeassistant/components/media_player/hdmi_cec.py b/homeassistant/components/media_player/hdmi_cec.py index c7e9be562cc..aa3b44e9e90 100644 --- a/homeassistant/components/media_player/hdmi_cec.py +++ b/homeassistant/components/media_player/hdmi_cec.py @@ -158,8 +158,8 @@ class CecPlayerDevice(CecDevice, MediaPlayerDevice): self.schedule_update_ha_state() @property - def supported_media_commands(self): - """Flag media commands that are supported.""" + def supported_features(self): + """Flag media player features that are supported.""" from pycec.const import TYPE_RECORDER, TYPE_PLAYBACK, TYPE_TUNER, \ TYPE_AUDIO if self.type_id == TYPE_RECORDER or self.type == TYPE_PLAYBACK: diff --git a/homeassistant/components/media_player/itunes.py b/homeassistant/components/media_player/itunes.py index 4dc41c7e085..5d53518256e 100644 --- a/homeassistant/components/media_player/itunes.py +++ b/homeassistant/components/media_player/itunes.py @@ -306,8 +306,8 @@ class ItunesDevice(MediaPlayerDevice): return self.current_playlist @property - def supported_media_commands(self): - """Flag of media commands that are supported.""" + def supported_features(self): + """Flag media player features that are supported.""" return SUPPORT_ITUNES def set_volume_level(self, volume): @@ -425,8 +425,8 @@ class AirPlayDevice(MediaPlayerDevice): return MEDIA_TYPE_MUSIC @property - def supported_media_commands(self): - """Flag of media commands that are supported.""" + def supported_features(self): + """Flag media player features that are supported.""" return SUPPORT_AIRPLAY def set_volume_level(self, volume): @@ -438,13 +438,13 @@ class AirPlayDevice(MediaPlayerDevice): def turn_on(self): """Select AirPlay.""" self.update_state({"selected": True}) - self.update_ha_state() + self.schedule_update_ha_state() response = self.client.toggle_airplay_device(self._id, True) self.update_state(response) def turn_off(self): """Deselect AirPlay.""" self.update_state({"selected": False}) - self.update_ha_state() + self.schedule_update_ha_state() response = self.client.toggle_airplay_device(self._id, False) self.update_state(response) diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index acb6a6f45db..75a54c6fac2 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -228,14 +228,14 @@ class KodiDevice(MediaPlayerDevice): self._item.get('label', self._item.get('file', 'unknown'))) @property - def supported_media_commands(self): - """Flag of media commands that are supported.""" - supported_media_commands = SUPPORT_KODI + def supported_features(self): + """Flag media player features that are supported.""" + supported_features = SUPPORT_KODI if self._turn_off_action in TURN_OFF_ACTION: - supported_media_commands |= SUPPORT_TURN_OFF + supported_features |= SUPPORT_TURN_OFF - return supported_media_commands + return supported_features @asyncio.coroutine def async_turn_off(self): diff --git a/homeassistant/components/media_player/lg_netcast.py b/homeassistant/components/media_player/lg_netcast.py index ab6cfa701ca..00e405c17b2 100644 --- a/homeassistant/components/media_player/lg_netcast.py +++ b/homeassistant/components/media_player/lg_netcast.py @@ -163,8 +163,8 @@ class LgTVDevice(MediaPlayerDevice): return self._program_name @property - def supported_media_commands(self): - """Flag of media commands that are supported.""" + def supported_features(self): + """Flag media player features that are supported.""" return SUPPORT_LGTV @property diff --git a/homeassistant/components/media_player/liveboxplaytv.py b/homeassistant/components/media_player/liveboxplaytv.py new file mode 100644 index 00000000000..b36d52a161c --- /dev/null +++ b/homeassistant/components/media_player/liveboxplaytv.py @@ -0,0 +1,207 @@ +""" +Support for interface with an Orange Livebox Play TV appliance. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.liveboxplaytv/ +""" +import logging +from datetime import timedelta + +import requests +import voluptuous as vol + +import homeassistant.util as util +from homeassistant.components.media_player import ( + SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_PLAY, + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, + SUPPORT_VOLUME_STEP, SUPPORT_VOLUME_MUTE, SUPPORT_SELECT_SOURCE, + MEDIA_TYPE_CHANNEL, MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.const import ( + CONF_HOST, CONF_PORT, STATE_ON, STATE_OFF, STATE_PLAYING, + STATE_PAUSED, STATE_UNKNOWN, CONF_NAME) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['liveboxplaytv==1.4.7'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'Livebox Play TV' +DEFAULT_PORT = 8080 + +SUPPORT_LIVEBOXPLAYTV = SUPPORT_TURN_OFF | SUPPORT_TURN_ON | \ + SUPPORT_NEXT_TRACK | SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | \ + SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | SUPPORT_SELECT_SOURCE | \ + SUPPORT_PLAY + +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) +MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string +}) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Orange Livebox Play TV platform.""" + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + name = config.get(CONF_NAME) + + add_devices([LiveboxPlayTvDevice(host, port, name)], True) + + +class LiveboxPlayTvDevice(MediaPlayerDevice): + """Representation of an Orange Livebox Play TV.""" + + def __init__(self, host, port, name): + """Initialize the Livebox Play TV device.""" + from liveboxplaytv import LiveboxPlayTv + self._client = LiveboxPlayTv(host, port) + # Assume that the appliance is not muted + self._muted = False + # Assume that the TV is in Play mode + self._name = name + self._current_source = None + self._state = STATE_UNKNOWN + self._channel_list = {} + self._current_channel = None + self._current_program = None + self._media_image_url = None + + @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) + def update(self): + """Retrieve the latest data.""" + try: + self._state = self.refresh_state() + # Update current channel + channel = self._client.get_current_channel() + if channel is not None: + self._current_program = self._client.program + self._current_channel = channel.get('name', None) + self._media_image_url = \ + self._client.get_current_channel_image(img_size=300) + self.refresh_channel_list() + except requests.ConnectionError: + self._state = STATE_OFF + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def is_volume_muted(self): + """Boolean if volume is currently muted.""" + return self._muted + + @property + def source(self): + """Return the current input source.""" + return self._current_channel + + @property + def source_list(self): + """List of available input sources.""" + # Sort channels by tvIndex + return [self._channel_list[c] for c in + sorted(self._channel_list.keys())] + + @property + def media_content_type(self): + """Content type of current playing media.""" + # return self._client.media_type + return MEDIA_TYPE_CHANNEL + + @property + def media_image_url(self): + """Image url of current playing media.""" + return self._media_image_url + + @property + def media_title(self): + """Title of current playing media.""" + if self._current_channel: + return '{}: {}'.format(self._current_channel, + self._current_program) + + @property + def supported_features(self): + """Flag media player features that are supported.""" + return SUPPORT_LIVEBOXPLAYTV + + def refresh_channel_list(self): + """Refresh the list of available channels.""" + new_channel_list = {} + # update channels + for channel in self._client.get_channels(): + new_channel_list[int(channel['index'])] = channel['name'] + self._channel_list = new_channel_list + + def refresh_state(self): + """Refresh the current media state.""" + state = self._client.media_state + if state == 'PLAY': + return STATE_PLAYING + elif state == 'PAUSE': + return STATE_PAUSED + else: + return STATE_ON if self._client.is_on else STATE_OFF + return STATE_UNKNOWN + + def turn_off(self): + """Turn off media player.""" + self._state = STATE_OFF + self._client.turn_off() + + def turn_on(self): + """Turn on the media player.""" + self._state = STATE_ON + self._client.turn_on() + + def volume_up(self): + """Volume up the media player.""" + self._client.volume_up() + + def volume_down(self): + """Volume down media player.""" + self._client.volume_down() + + def mute_volume(self, mute): + """Send mute command.""" + self._muted = mute + self._client.mute() + + def media_play_pause(self): + """Simulate play pause media player.""" + self._client.play_pause() + + def select_source(self, source): + """Select input source.""" + self._current_source = source + self._client.set_channel(source) + + def media_play(self): + """Send play command.""" + self._state = STATE_PLAYING + self._client.play() + + def media_pause(self): + """Send media pause command to media player.""" + self._state = STATE_PAUSED + self._client.pause() + + def media_next_track(self): + """Send next track command.""" + self._client.channel_up() + + def media_previous_track(self): + """Send the previous track command.""" + self._client.channel_down() diff --git a/homeassistant/components/media_player/mpchc.py b/homeassistant/components/media_player/mpchc.py index e51d91aa95c..168497c4052 100644 --- a/homeassistant/components/media_player/mpchc.py +++ b/homeassistant/components/media_player/mpchc.py @@ -126,8 +126,8 @@ class MpcHcDevice(MediaPlayerDevice): int(duration[2]) @property - def supported_media_commands(self): - """Flag of media commands that are supported.""" + def supported_features(self): + """Flag media player features that are supported.""" return SUPPORT_MPCHC def volume_up(self): diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py index 2f16410e783..48d22dab021 100644 --- a/homeassistant/components/media_player/mpd.py +++ b/homeassistant/components/media_player/mpd.py @@ -184,8 +184,8 @@ class MpdDevice(MediaPlayerDevice): return int(self.status['volume'])/100 @property - def supported_media_commands(self): - """Flag of media commands that are supported.""" + def supported_features(self): + """Flag media player features that are supported.""" return SUPPORT_MPD @property diff --git a/homeassistant/components/media_player/nad.py b/homeassistant/components/media_player/nad.py index 27122fcfc93..332aed4a839 100644 --- a/homeassistant/components/media_player/nad.py +++ b/homeassistant/components/media_player/nad.py @@ -136,8 +136,8 @@ class NAD(MediaPlayerDevice): return self._mute @property - def supported_media_commands(self): - """Flag of media commands that are supported.""" + def supported_features(self): + """Flag media player features that are supported.""" return SUPPORT_NAD def turn_off(self): diff --git a/homeassistant/components/media_player/onkyo.py b/homeassistant/components/media_player/onkyo.py index 3db9413490f..017581f8186 100644 --- a/homeassistant/components/media_player/onkyo.py +++ b/homeassistant/components/media_player/onkyo.py @@ -150,8 +150,8 @@ class OnkyoDevice(MediaPlayerDevice): return self._muted @property - def supported_media_commands(self): - """Flag of media commands that are supported.""" + def supported_features(self): + """Flag media player features that are supported.""" return SUPPORT_ONKYO @property diff --git a/homeassistant/components/media_player/panasonic_viera.py b/homeassistant/components/media_player/panasonic_viera.py index 09f4845effe..30672069558 100644 --- a/homeassistant/components/media_player/panasonic_viera.py +++ b/homeassistant/components/media_player/panasonic_viera.py @@ -123,8 +123,8 @@ class PanasonicVieraTVDevice(MediaPlayerDevice): return self._muted @property - def supported_media_commands(self): - """Flag of media commands that are supported.""" + def supported_features(self): + """Flag media player features that are supported.""" if self._mac: return SUPPORT_VIERATV | SUPPORT_TURN_ON return SUPPORT_VIERATV diff --git a/homeassistant/components/media_player/pandora.py b/homeassistant/components/media_player/pandora.py index 0d554093be2..f742618822a 100644 --- a/homeassistant/components/media_player/pandora.py +++ b/homeassistant/components/media_player/pandora.py @@ -158,8 +158,8 @@ class PandoraMediaPlayer(MediaPlayerDevice): self.schedule_update_ha_state() @property - def supported_media_commands(self): - """Show what this supports.""" + def supported_features(self): + """Flag media player features that are supported.""" return PANDORA_SUPPORT @property diff --git a/homeassistant/components/media_player/philips_js.py b/homeassistant/components/media_player/philips_js.py index 22f257f31dc..a4d3b02381a 100644 --- a/homeassistant/components/media_player/philips_js.py +++ b/homeassistant/components/media_player/philips_js.py @@ -86,8 +86,8 @@ class PhilipsTV(MediaPlayerDevice): return True @property - def supported_media_commands(self): - """Flag of media commands that are supported.""" + def supported_features(self): + """Flag media player features that are supported.""" if self._watching_tv: return SUPPORT_PHILIPS_JS_TV else: diff --git a/homeassistant/components/media_player/pioneer.py b/homeassistant/components/media_player/pioneer.py index dec8bd12bb2..73f0d18f597 100644 --- a/homeassistant/components/media_player/pioneer.py +++ b/homeassistant/components/media_player/pioneer.py @@ -176,8 +176,8 @@ class PioneerDevice(MediaPlayerDevice): return self._muted @property - def supported_media_commands(self): - """Flag of media commands that are supported.""" + def supported_features(self): + """Flag media player features that are supported.""" return SUPPORT_PIONEER @property diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index 409a480443a..27e61a7863b 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -334,8 +334,8 @@ class PlexClient(MediaPlayerDevice): return self._convert_na_to_none(self.session.index) @property - def supported_media_commands(self): - """Flag of media commands that are supported.""" + def supported_features(self): + """Flag media player features that are supported.""" return SUPPORT_PLEX def set_volume_level(self, volume): diff --git a/homeassistant/components/media_player/roku.py b/homeassistant/components/media_player/roku.py index 26649d0be96..08a3eec17e8 100644 --- a/homeassistant/components/media_player/roku.py +++ b/homeassistant/components/media_player/roku.py @@ -15,6 +15,7 @@ from homeassistant.components.media_player import ( from homeassistant.const import ( CONF_HOST, STATE_IDLE, STATE_PLAYING, STATE_UNKNOWN, STATE_HOME) import homeassistant.helpers.config_validation as cv +import homeassistant.loader as loader REQUIREMENTS = [ 'https://github.com/bah2830/python-roku/archive/3.1.3.zip' @@ -23,6 +24,9 @@ REQUIREMENTS = [ KNOWN_HOSTS = [] DEFAULT_PORT = 8060 +NOTIFICATION_ID = 'roku_notification' +NOTIFICATION_TITLE = 'Roku Media Player Setup' + _LOGGER = logging.getLogger(__name__) SUPPORT_ROKU = SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK |\ @@ -48,15 +52,28 @@ def setup_platform(hass, config, add_devices, discovery_info=None): elif CONF_HOST in config: hosts.append(config.get(CONF_HOST)) + persistent_notification = loader.get_component('persistent_notification') rokus = [] for host in hosts: new_roku = RokuDevice(host) - if new_roku.name is None: + try: + if new_roku.name is not None: + rokus.append(RokuDevice(host)) + KNOWN_HOSTS.append(host) + else: + _LOGGER.error("Unable to initialize roku at %s", host) + + except AttributeError: _LOGGER.error("Unable to initialize roku at %s", host) - else: - rokus.append(RokuDevice(host)) - KNOWN_HOSTS.append(host) + persistent_notification.create( + hass, 'Error: Unable to initialize roku at {}
' + 'Check its network connection or consider ' + 'using auto discovery.
' + 'You will need to restart hass after fixing.' + ''.format(config.get(CONF_HOST)), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) add_devices(rokus) @@ -125,8 +142,8 @@ class RokuDevice(MediaPlayerDevice): return STATE_UNKNOWN @property - def supported_media_commands(self): - """Flag of media commands that are supported.""" + def supported_features(self): + """Flag media player features that are supported.""" return SUPPORT_ROKU @property diff --git a/homeassistant/components/media_player/russound_rnet.py b/homeassistant/components/media_player/russound_rnet.py index b8f79c45cec..7ae16adcce3 100644 --- a/homeassistant/components/media_player/russound_rnet.py +++ b/homeassistant/components/media_player/russound_rnet.py @@ -4,20 +4,20 @@ Support for interfacing with Russound via RNET Protocol. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.russound_rnet/ """ -import logging +import logging import voluptuous as vol from homeassistant.components.media_player import ( - SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_SELECT_SOURCE, SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA) + SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA) from homeassistant.const import ( CONF_HOST, CONF_PORT, STATE_OFF, STATE_ON, CONF_NAME) import homeassistant.helpers.config_validation as cv REQUIREMENTS = [ - 'https://github.com/laf/russound/archive/0.1.6.zip' - '#russound==0.1.6'] + 'https://github.com/laf/russound/archive/0.1.7.zip' + '#russound==0.1.7'] _LOGGER = logging.getLogger(__name__) @@ -25,7 +25,7 @@ CONF_ZONES = 'zones' CONF_SOURCES = 'sources' SUPPORT_RUSSOUND = SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | \ - SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE | SUPPORT_PLAY + SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE ZONE_SCHEMA = vol.Schema({ vol.Required(CONF_NAME): cv.string, @@ -48,7 +48,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Russound RNET platform.""" host = config.get(CONF_HOST) port = config.get(CONF_PORT) - keypad = config.get('keypad', '70') if host is None or port is None: _LOGGER.error("Invalid config. Expected %s and %s", @@ -58,7 +57,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): from russound import russound russ = russound.Russound(host, port) - russ.connect(keypad) + russ.connect() sources = [] for source in config[CONF_SOURCES]: @@ -67,7 +66,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if russ.is_connected(): for zone_id, extra in config[CONF_ZONES].items(): add_devices([RussoundRNETDevice( - hass, russ, sources, zone_id, extra)]) + hass, russ, sources, zone_id, extra)], True) else: _LOGGER.error('Not connected to %s:%s', host, port) @@ -79,10 +78,32 @@ class RussoundRNETDevice(MediaPlayerDevice): """Initialise the Russound RNET device.""" self._name = extra['name'] self._russ = russ - self._state = STATE_OFF self._sources = sources self._zone_id = zone_id - self._volume = 0 + + self._state = None + self._volume = None + self._source = None + + def update(self): + """Retrieve latest state.""" + if self._russ.get_power('1', self._zone_id) == 0: + self._state = STATE_OFF + else: + self._state = STATE_ON + + self._volume = self._russ.get_volume('1', self._zone_id) / 100.0 + + # Returns 0 based index for source. + index = self._russ.get_source('1', self._zone_id) + # Possibility exists that user has defined list of all sources. + # If a source is set externally that is beyond the defined list then + # an exception will be thrown. + # In this case return and unknown source (None) + try: + self._source = self._sources[index] + except IndexError: + self._source = None @property def name(self): @@ -95,29 +116,39 @@ class RussoundRNETDevice(MediaPlayerDevice): return self._state @property - def supported_media_commands(self): - """Flag of media commands that are supported.""" + def supported_features(self): + """Flag media player features that are supported.""" return SUPPORT_RUSSOUND + @property + def source(self): + """Get the currently selected source.""" + return self._source + @property def volume_level(self): - """Volume level of the media player (0..1).""" + """Volume level of the media player (0..1). + + Value is returned based on a range (0..100). + Therefore float divide by 100 to get to the required range. + """ return self._volume def set_volume_level(self, volume): - """Set volume level, range 0..1.""" - self._volume = volume * 100 - self._russ.set_volume('1', self._zone_id, self._volume) + """Set volume level. Volume has a range (0..1). + + Translate this to a range of (0..100) as expected expected + by _russ.set_volume() + """ + self._russ.set_volume('1', self._zone_id, volume * 100) def turn_on(self): """Turn the media player on.""" self._russ.set_power('1', self._zone_id, '1') - self._state = STATE_ON def turn_off(self): """Turn off media player.""" self._russ.set_power('1', self._zone_id, '0') - self._state = STATE_OFF def mute_volume(self, mute): """Send mute command.""" @@ -126,7 +157,8 @@ class RussoundRNETDevice(MediaPlayerDevice): def select_source(self, source): """Set the input source.""" if source in self._sources: - index = self._sources.index(source)+1 + index = self._sources.index(source) + # 0 based value for source self._russ.set_source('1', self._zone_id, index) @property diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py index d2d3d2e25a6..794fefffecc 100644 --- a/homeassistant/components/media_player/samsungtv.py +++ b/homeassistant/components/media_player/samsungtv.py @@ -156,8 +156,8 @@ class SamsungTVDevice(MediaPlayerDevice): return self._muted @property - def supported_media_commands(self): - """Flag of media commands that are supported.""" + def supported_features(self): + """Flag media player features that are supported.""" return SUPPORT_SAMSUNGTV def turn_off(self): diff --git a/homeassistant/components/media_player/snapcast.py b/homeassistant/components/media_player/snapcast.py index 98dad3486a2..3e06cca38a5 100644 --- a/homeassistant/components/media_player/snapcast.py +++ b/homeassistant/components/media_player/snapcast.py @@ -72,8 +72,8 @@ class SnapcastDevice(MediaPlayerDevice): return self._client.muted @property - def supported_media_commands(self): - """Flag of media commands that are supported.""" + def supported_features(self): + """Flag media player features that are supported.""" return SUPPORT_SNAPCAST @property diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 0bb34eee598..e052cfcaea3 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -334,271 +334,15 @@ class SonosDevice(MediaPlayerDevice): self._speaker_info = self._player.get_speaker_info(True) self._name = self._speaker_info['zone_name'].replace( ' (R)', '').replace(' (L)', '') + self._favorite_sources = \ + self._player.get_sonos_favorites()['favorites'] if self._last_avtransport_event: is_available = True else: is_available = self._is_available() - if is_available: - - # set group coordinator - if self._player.is_coordinator: - self._coordinator = None - else: - try: - self._coordinator = _get_entity_from_soco( - self.hass, self._player.group.coordinator) - - # protect for loop - if not self._coordinator.is_coordinator: - # pylint: disable=protected-access - self._coordinator._coordinator = None - except ValueError: - self._coordinator = None - - track_info = None - if self._last_avtransport_event: - variables = self._last_avtransport_event.variables - current_track_metadata = variables.get( - 'current_track_meta_data', {} - ) - - self._status = variables.get('transport_state') - - if current_track_metadata: - # no need to ask speaker for information we already have - current_track_metadata = current_track_metadata.__dict__ - - track_info = { - 'uri': variables.get('current_track_uri'), - 'artist': current_track_metadata.get('creator'), - 'album': current_track_metadata.get('album'), - 'title': current_track_metadata.get('title'), - 'playlist_position': variables.get('current_track'), - 'duration': variables.get('current_track_duration') - } - else: - self._player_volume = self._player.volume - self._player_volume_muted = self._player.mute - transport_info = self._player.get_current_transport_info() - self._status = transport_info.get('current_transport_state') - - if not track_info: - track_info = self._player.get_current_track_info() - - if not self._coordinator: - - is_playing_tv = self._player.is_playing_tv - is_playing_line_in = self._player.is_playing_line_in - - media_info = self._player.avTransport.GetMediaInfo( - [('InstanceID', 0)] - ) - - current_media_uri = media_info['CurrentURI'] - media_artist = track_info.get('artist') - media_album_name = track_info.get('album') - media_title = track_info.get('title') - media_image_url = track_info.get('album_art', None) - - media_position = None - media_position_updated_at = None - source_name = None - - is_radio_stream = \ - current_media_uri.startswith('x-sonosapi-stream:') or \ - current_media_uri.startswith('x-rincon-mp3radio:') - - if is_playing_tv or is_playing_line_in: - # playing from line-in/tv. - - support_previous_track = False - support_next_track = False - support_stop = False - support_pause = False - - if is_playing_tv: - media_artist = SUPPORT_SOURCE_TV - else: - media_artist = SUPPORT_SOURCE_LINEIN - - source_name = media_artist - - media_album_name = None - media_title = None - media_image_url = None - - elif is_radio_stream: - media_image_url = self._format_media_image_url( - media_image_url, - current_media_uri - ) - support_previous_track = False - support_next_track = False - support_stop = False - support_pause = False - - source_name = 'Radio' - # Check if currently playing radio station is in favorites - favs = self._player.get_sonos_favorites()['favorites'] - favc = [ - fav for fav in favs if fav['uri'] == current_media_uri - ] - if len(favc) == 1: - src = favc.pop() - source_name = src['title'] - - # for radio streams we set the radio station name as the - # title. - if media_artist and media_title: - # artist and album name are in the data, concatenate - # that do display as artist. - # "Information" field in the sonos pc app - - media_artist = '{artist} - {title}'.format( - artist=media_artist, - title=media_title - ) - else: - # "On Now" field in the sonos pc app - media_artist = self._media_radio_show - - current_uri_metadata = media_info["CurrentURIMetaData"] - if current_uri_metadata not in \ - ('', 'NOT_IMPLEMENTED', None): - - # currently soco does not have an API for this - import soco - current_uri_metadata = soco.xml.XML.fromstring( - soco.utils.really_utf8(current_uri_metadata)) - - md_title = current_uri_metadata.findtext( - './/{http://purl.org/dc/elements/1.1/}title') - - if md_title not in ('', 'NOT_IMPLEMENTED', None): - media_title = md_title - - if media_artist and media_title: - # some radio stations put their name into the artist - # name, e.g.: - # media_title = "Station" - # media_artist = "Station - Artist - Title" - # detect this case and trim from the front of - # media_artist for cosmetics - str_to_trim = '{title} - '.format( - title=media_title - ) - chars = min(len(media_artist), len(str_to_trim)) - - if media_artist[:chars].upper() == \ - str_to_trim[:chars].upper(): - - media_artist = media_artist[chars:] - - else: - # not a radio stream - media_image_url = self._format_media_image_url( - media_image_url, - track_info['uri'] - ) - support_previous_track = True - support_next_track = True - support_stop = True - support_pause = True - - position_info = self._player.avTransport.GetPositionInfo( - [('InstanceID', 0), - ('Channel', 'Master')] - ) - rel_time = _parse_timespan( - position_info.get("RelTime") - ) - - # player no longer reports position? - update_media_position = rel_time is None and \ - self._media_position is not None - - # player started reporting position? - update_media_position |= rel_time is not None and \ - self._media_position is None - - # position changed? - if rel_time is not None and \ - self._media_position is not None: - - time_diff = utcnow() - self._media_position_updated_at - time_diff = time_diff.total_seconds() - - calculated_position = \ - self._media_position + \ - time_diff - - update_media_position = \ - abs(calculated_position - rel_time) > 1.5 - - if update_media_position and self.state == STATE_PLAYING: - media_position = rel_time - media_position_updated_at = utcnow() - else: - # don't update media_position (don't want unneeded - # state transitions) - media_position = self._media_position - media_position_updated_at = \ - self._media_position_updated_at - - playlist_position = track_info.get('playlist_position') - if playlist_position in ('', 'NOT_IMPLEMENTED', None): - playlist_position = None - else: - playlist_position = int(playlist_position) - - playlist_size = media_info.get('NrTracks') - if playlist_size in ('', 'NOT_IMPLEMENTED', None): - playlist_size = None - else: - playlist_size = int(playlist_size) - - if playlist_position is not None and \ - playlist_size is not None: - - if playlist_position == 1: - support_previous_track = False - - if playlist_position == playlist_size: - support_next_track = False - - self._media_content_id = track_info.get('title') - self._media_duration = _parse_timespan( - track_info.get('duration') - ) - self._media_position = media_position - self._media_position_updated_at = media_position_updated_at - self._media_image_url = media_image_url - self._media_artist = media_artist - self._media_album_name = media_album_name - self._media_title = media_title - self._current_track_uri = track_info['uri'] - self._current_track_is_radio_stream = is_radio_stream - self._support_previous_track = support_previous_track - self._support_next_track = support_next_track - self._support_stop = support_stop - self._support_pause = support_pause - self._is_playing_tv = is_playing_tv - self._is_playing_line_in = is_playing_line_in - self._source_name = source_name - - # update state of the whole group - for device in [x for x in self.hass.data[DATA_SONOS] - if x.coordinator == self]: - if device.entity_id is not self.entity_id: - self.schedule_update_ha_state() - - if self._queue is None and self.entity_id is not None: - self._subscribe_to_player_events() - favs = self._player.get_sonos_favorites().get('favorites', []) - self._favorite_sources = [fav['title'] for fav in favs] - else: + if not is_available: self._player_volume = None self._player_volume_muted = None self._status = 'OFF' @@ -623,6 +367,255 @@ class SonosDevice(MediaPlayerDevice): self._is_playing_line_in = False self._favorite_sources = None self._source_name = None + self._last_avtransport_event = None + return + + # set group coordinator + if self._player.is_coordinator: + self._coordinator = None + else: + try: + self._coordinator = _get_entity_from_soco( + self.hass, self._player.group.coordinator) + + # protect for loop + if not self._coordinator.is_coordinator: + # pylint: disable=protected-access + self._coordinator._coordinator = None + except ValueError: + self._coordinator = None + + track_info = None + if self._last_avtransport_event: + variables = self._last_avtransport_event.variables + current_track_metadata = variables.get( + 'current_track_meta_data', {} + ) + + self._status = variables.get('transport_state') + + if current_track_metadata: + # no need to ask speaker for information we already have + current_track_metadata = current_track_metadata.__dict__ + + track_info = { + 'uri': variables.get('current_track_uri'), + 'artist': current_track_metadata.get('creator'), + 'album': current_track_metadata.get('album'), + 'title': current_track_metadata.get('title'), + 'playlist_position': variables.get('current_track'), + 'duration': variables.get('current_track_duration') + } + else: + self._player_volume = self._player.volume + self._player_volume_muted = self._player.mute + transport_info = self._player.get_current_transport_info() + self._status = transport_info.get('current_transport_state') + + if not track_info: + track_info = self._player.get_current_track_info() + + if self._coordinator: + self._last_avtransport_event = None + return + + is_playing_tv = self._player.is_playing_tv + is_playing_line_in = self._player.is_playing_line_in + + media_info = self._player.avTransport.GetMediaInfo( + [('InstanceID', 0)] + ) + + current_media_uri = media_info['CurrentURI'] + media_artist = track_info.get('artist') + media_album_name = track_info.get('album') + media_title = track_info.get('title') + media_image_url = track_info.get('album_art', None) + + media_position = None + media_position_updated_at = None + source_name = None + + is_radio_stream = \ + current_media_uri.startswith('x-sonosapi-stream:') or \ + current_media_uri.startswith('x-rincon-mp3radio:') + + if is_playing_tv or is_playing_line_in: + # playing from line-in/tv. + + support_previous_track = False + support_next_track = False + support_stop = False + support_pause = False + + if is_playing_tv: + media_artist = SUPPORT_SOURCE_TV + else: + media_artist = SUPPORT_SOURCE_LINEIN + + source_name = media_artist + + media_album_name = None + media_title = None + media_image_url = None + + elif is_radio_stream: + media_image_url = self._format_media_image_url( + media_image_url, + current_media_uri + ) + support_previous_track = False + support_next_track = False + support_stop = False + support_pause = False + + source_name = 'Radio' + # Check if currently playing radio station is in favorites + favc = [fav for fav in self._favorite_sources + if fav['uri'] == current_media_uri] + if len(favc) == 1: + src = favc.pop() + source_name = src['title'] + + # for radio streams we set the radio station name as the + # title. + if media_artist and media_title: + # artist and album name are in the data, concatenate + # that do display as artist. + # "Information" field in the sonos pc app + + media_artist = '{artist} - {title}'.format( + artist=media_artist, + title=media_title + ) + else: + # "On Now" field in the sonos pc app + media_artist = self._media_radio_show + + current_uri_metadata = media_info["CurrentURIMetaData"] + if current_uri_metadata not in ('', 'NOT_IMPLEMENTED', None): + + # currently soco does not have an API for this + import soco + current_uri_metadata = soco.xml.XML.fromstring( + soco.utils.really_utf8(current_uri_metadata)) + + md_title = current_uri_metadata.findtext( + './/{http://purl.org/dc/elements/1.1/}title') + + if md_title not in ('', 'NOT_IMPLEMENTED', None): + media_title = md_title + + if media_artist and media_title: + # some radio stations put their name into the artist + # name, e.g.: + # media_title = "Station" + # media_artist = "Station - Artist - Title" + # detect this case and trim from the front of + # media_artist for cosmetics + str_to_trim = '{title} - '.format( + title=media_title + ) + chars = min(len(media_artist), len(str_to_trim)) + + if media_artist[:chars].upper() == str_to_trim[:chars].upper(): + media_artist = media_artist[chars:] + + else: + # not a radio stream + media_image_url = self._format_media_image_url( + media_image_url, + track_info['uri'] + ) + support_previous_track = True + support_next_track = True + support_stop = True + support_pause = True + + position_info = self._player.avTransport.GetPositionInfo( + [('InstanceID', 0), + ('Channel', 'Master')] + ) + rel_time = _parse_timespan( + position_info.get("RelTime") + ) + + # player no longer reports position? + update_media_position = rel_time is None and \ + self._media_position is not None + + # player started reporting position? + update_media_position |= rel_time is not None and \ + self._media_position is None + + # position changed? + if rel_time is not None and self._media_position is not None: + + time_diff = utcnow() - self._media_position_updated_at + time_diff = time_diff.total_seconds() + + calculated_position = self._media_position + time_diff + + update_media_position = \ + abs(calculated_position - rel_time) > 1.5 + + if update_media_position and self.state == STATE_PLAYING: + media_position = rel_time + media_position_updated_at = utcnow() + else: + # don't update media_position (don't want unneeded + # state transitions) + media_position = self._media_position + media_position_updated_at = self._media_position_updated_at + + playlist_position = track_info.get('playlist_position') + if playlist_position in ('', 'NOT_IMPLEMENTED', None): + playlist_position = None + else: + playlist_position = int(playlist_position) + + playlist_size = media_info.get('NrTracks') + if playlist_size in ('', 'NOT_IMPLEMENTED', None): + playlist_size = None + else: + playlist_size = int(playlist_size) + + if playlist_position is not None and playlist_size is not None: + + if playlist_position == 1: + support_previous_track = False + + if playlist_position == playlist_size: + support_next_track = False + + self._media_content_id = track_info.get('title') + self._media_duration = _parse_timespan( + track_info.get('duration') + ) + self._media_position = media_position + self._media_position_updated_at = media_position_updated_at + self._media_image_url = media_image_url + self._media_artist = media_artist + self._media_album_name = media_album_name + self._media_title = media_title + self._current_track_uri = track_info['uri'] + self._current_track_is_radio_stream = is_radio_stream + self._support_previous_track = support_previous_track + self._support_next_track = support_next_track + self._support_stop = support_stop + self._support_pause = support_pause + self._is_playing_tv = is_playing_tv + self._is_playing_line_in = is_playing_line_in + self._source_name = source_name + + # update state of the whole group + for device in [x for x in self.hass.data[DATA_SONOS] + if x.coordinator == self]: + if device.entity_id is not self.entity_id: + self.schedule_update_ha_state() + + if self._queue is None and self.entity_id is not None: + self._subscribe_to_player_events() self._last_avtransport_event = None @@ -768,10 +761,10 @@ class SonosDevice(MediaPlayerDevice): return self._media_title @property - def supported_media_commands(self): - """Flag of media commands that are supported.""" + def supported_features(self): + """Flag media player features that are supported.""" if self._coordinator: - return self._coordinator.supported_media_commands + return self._coordinator.supported_features supported = SUPPORT_SONOS @@ -807,15 +800,17 @@ class SonosDevice(MediaPlayerDevice): def select_source(self, source): """Select input source.""" - if source == SUPPORT_SOURCE_LINEIN: + if self._coordinator: + self._coordinator.select_source(source) + elif source == SUPPORT_SOURCE_LINEIN: self._source_name = SUPPORT_SOURCE_LINEIN self._player.switch_to_line_in() elif source == SUPPORT_SOURCE_TV: self._source_name = SUPPORT_SOURCE_TV self._player.switch_to_tv() else: - favorites = self._player.get_sonos_favorites()['favorites'] - fav = [fav for fav in favorites if fav['title'] == source] + fav = [fav for fav in self._favorite_sources + if fav['title'] == source] if len(fav) == 1: src = fav.pop() self._source_name = src['title'] @@ -824,9 +819,15 @@ class SonosDevice(MediaPlayerDevice): @property def source_list(self): """List of available input sources.""" - model_name = self._speaker_info['model_name'] + if self._coordinator: + return self._coordinator.source_list - sources = self._favorite_sources.copy() + model_name = self._speaker_info['model_name'] + sources = [] + + if self._favorite_sources: + for fav in self._favorite_sources: + sources.append(fav['title']) if 'PLAY:5' in model_name: sources += [SUPPORT_SOURCE_LINEIN] @@ -940,7 +941,15 @@ class SonosDevice(MediaPlayerDevice): def snapshot(self, with_group=True): """Snapshot the player.""" - self.soco_snapshot.snapshot() + from soco.exceptions import SoCoException + try: + self.soco_snapshot.is_playing_queue = False + self.soco_snapshot.is_coordinator = False + self.soco_snapshot.snapshot() + except SoCoException: + _LOGGER.debug("Error on snapshot %s", self.entity_id) + self._snapshot_group = None + return if with_group: self._snapshot_group = self._player.group diff --git a/homeassistant/components/media_player/soundtouch.py b/homeassistant/components/media_player/soundtouch.py index 22191a0ee17..90a870a8c65 100644 --- a/homeassistant/components/media_player/soundtouch.py +++ b/homeassistant/components/media_player/soundtouch.py @@ -287,8 +287,8 @@ class SoundTouchDevice(MediaPlayerDevice): return self._volume.muted @property - def supported_media_commands(self): - """Flag of media commands that are supported.""" + def supported_features(self): + """Flag media player features that are supported.""" return SUPPORT_SOUNDTOUCH def turn_off(self): diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index 852ce522559..efab17a61a9 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -201,10 +201,6 @@ class SqueezeBoxDevice(MediaPlayerDevice): return self._lms.async_query( *parameters, player=self._id) - def query(self, *parameters): - """Queue up a command to send the LMS.""" - self.hass.loop.create_task(self.async_query(*parameters)) - @asyncio.coroutine def async_update(self): """Retrieve the current state of the player.""" @@ -306,89 +302,112 @@ class SqueezeBoxDevice(MediaPlayerDevice): return self._status['album'] @property - def supported_media_commands(self): - """Flag of media commands that are supported.""" + def supported_features(self): + """Flag media player features that are supported.""" return SUPPORT_SQUEEZEBOX - def turn_off(self): - """Turn off media player.""" - self.query('power', '0') - self.update_ha_state() + def async_turn_off(self): + """Turn off media player. - def volume_up(self): - """Volume up media player.""" - self.query('mixer', 'volume', '+5') - self.update_ha_state() + This method must be run in the event loop and returns a coroutine. + """ + return self.async_query('power', '0') - def volume_down(self): - """Volume down media player.""" - self.query('mixer', 'volume', '-5') - self.update_ha_state() + def async_volume_up(self): + """Volume up media player. - def set_volume_level(self, volume): - """Set volume level, range 0..1.""" + This method must be run in the event loop and returns a coroutine. + """ + return self.async_query('mixer', 'volume', '+5') + + def async_volume_down(self): + """Volume down media player. + + This method must be run in the event loop and returns a coroutine. + """ + return self.async_query('mixer', 'volume', '-5') + + def async_set_volume_level(self, volume): + """Set volume level, range 0..1. + + This method must be run in the event loop and returns a coroutine. + """ volume_percent = str(int(volume*100)) - self.query('mixer', 'volume', volume_percent) - self.update_ha_state() + return self.async_query('mixer', 'volume', volume_percent) - def mute_volume(self, mute): - """Mute (true) or unmute (false) media player.""" + def async_mute_volume(self, mute): + """Mute (true) or unmute (false) media player. + + This method must be run in the event loop and returns a coroutine. + """ mute_numeric = '1' if mute else '0' - self.query('mixer', 'muting', mute_numeric) - self.update_ha_state() + return self.async_query('mixer', 'muting', mute_numeric) - def media_play_pause(self): - """Send pause command to media player.""" - self.query('pause') - self.update_ha_state() + def async_media_play_pause(self): + """Send pause command to media player. - def media_play(self): - """Send play command to media player.""" - self.query('play') - self.update_ha_state() + This method must be run in the event loop and returns a coroutine. + """ + return self.async_query('pause') - def media_pause(self): - """Send pause command to media player.""" - self.query('pause', '1') - self.update_ha_state() + def async_media_play(self): + """Send play command to media player. - def media_next_track(self): - """Send next track command.""" - self.query('playlist', 'index', '+1') - self.update_ha_state() + This method must be run in the event loop and returns a coroutine. + """ + return self.async_query('play') - def media_previous_track(self): - """Send next track command.""" - self.query('playlist', 'index', '-1') - self.update_ha_state() + def async_media_pause(self): + """Send pause command to media player. - def media_seek(self, position): - """Send seek command.""" - self.query('time', position) - self.update_ha_state() + This method must be run in the event loop and returns a coroutine. + """ + return self.async_query('pause', '1') - def turn_on(self): - """Turn the media player on.""" - self.query('power', '1') - self.update_ha_state() + def async_media_next_track(self): + """Send next track command. - def play_media(self, media_type, media_id, **kwargs): + This method must be run in the event loop and returns a coroutine. + """ + return self.async_query('playlist', 'index', '+1') + + def async_media_previous_track(self): + """Send next track command. + + This method must be run in the event loop and returns a coroutine. + """ + return self.async_query('playlist', 'index', '-1') + + def async_media_seek(self, position): + """Send seek command. + + This method must be run in the event loop and returns a coroutine. + """ + return self.async_query('time', position) + + def async_turn_on(self): + """Turn the media player on. + + This method must be run in the event loop and returns a coroutine. + """ + return self.async_query('power', '1') + + def async_play_media(self, media_type, media_id, **kwargs): """ Send the play_media command to the media player. If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the current playlist. + This method must be run in the event loop and returns a coroutine. """ if kwargs.get(ATTR_MEDIA_ENQUEUE): - self._add_uri_to_playlist(media_id) - else: - self._play_uri(media_id) + return self._add_uri_to_playlist(media_id) + + return self._play_uri(media_id) def _play_uri(self, media_id): """Replace the current play list with the uri.""" - self.query('playlist', 'play', media_id) - self.update_ha_state() + return self.async_query('playlist', 'play', media_id) def _add_uri_to_playlist(self, media_id): """Add a items to the existing playlist.""" - self.query('playlist', 'add', media_id) - self.update_ha_state() + return self.async_query('playlist', 'add', media_id) diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index e01717f5693..3eccafba20c 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -4,10 +4,12 @@ Combination of multiple media players into one for a universal controller. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.universal/ """ +import asyncio import logging # pylint: disable=import-error from copy import copy +from homeassistant.core import callback from homeassistant.components.media_player import ( ATTR_APP_ID, ATTR_APP_NAME, ATTR_MEDIA_ALBUM_ARTIST, ATTR_MEDIA_ALBUM_NAME, ATTR_MEDIA_ARTIST, ATTR_MEDIA_CHANNEL, ATTR_MEDIA_CONTENT_ID, @@ -15,7 +17,7 @@ from homeassistant.components.media_player import ( ATTR_MEDIA_PLAYLIST, ATTR_MEDIA_SEASON, ATTR_MEDIA_SEEK_POSITION, ATTR_MEDIA_SERIES_TITLE, ATTR_MEDIA_TITLE, ATTR_MEDIA_TRACK, ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, ATTR_INPUT_SOURCE_LIST, - ATTR_SUPPORTED_MEDIA_COMMANDS, ATTR_MEDIA_POSITION, + ATTR_MEDIA_POSITION, ATTR_MEDIA_POSITION_UPDATED_AT, DOMAIN, SERVICE_PLAY_MEDIA, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST, @@ -27,9 +29,9 @@ from homeassistant.const import ( SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, STATE_IDLE, STATE_OFF, STATE_ON, - SERVICE_MEDIA_STOP) -from homeassistant.helpers.event import track_state_change -from homeassistant.helpers.service import call_from_config + SERVICE_MEDIA_STOP, ATTR_SUPPORTED_FEATURES) +from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.service import async_call_from_config ATTR_ACTIVE_CHILD = 'active_child' @@ -47,18 +49,21 @@ REQUIREMENTS = [] _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_devices, discovery_info=None): +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup the universal media players.""" if not validate_config(config): return - player = UniversalMediaPlayer(hass, - config[CONF_NAME], - config[CONF_CHILDREN], - config[CONF_COMMANDS], - config[CONF_ATTRS]) + player = UniversalMediaPlayer( + hass, + config[CONF_NAME], + config[CONF_CHILDREN], + config[CONF_COMMANDS], + config[CONF_ATTRS] + ) - add_devices([player]) + yield from async_add_devices([player]) def validate_config(config): @@ -143,15 +148,16 @@ class UniversalMediaPlayer(MediaPlayerDevice): self._attrs = attributes self._child_state = None - def on_dependency_update(*_): + @callback + def async_on_dependency_update(*_): """Update ha state when dependencies update.""" - self.update_ha_state(True) + self.hass.add_job(self.async_update_ha_state(True)) depend = copy(children) for entity in attributes.values(): depend.append(entity[0]) - track_state_change(hass, depend, on_dependency_update) + async_track_state_change(hass, depend, async_on_dependency_update) def _entity_lkp(self, entity_id, state_attr=None): """Look up an entity state.""" @@ -177,14 +183,15 @@ class UniversalMediaPlayer(MediaPlayerDevice): active_child = self._child_state return active_child.attributes.get(attr_name) if active_child else None - def _call_service(self, service_name, service_data=None, - allow_override=False): + @asyncio.coroutine + def _async_call_service(self, service_name, service_data=None, + allow_override=False): """Call either a specified or active child's service.""" if service_data is None: service_data = {} if allow_override and service_name in self._cmds: - call_from_config( + yield from async_call_from_config( self.hass, self._cmds[service_name], variables=service_data, blocking=True) return @@ -196,8 +203,8 @@ class UniversalMediaPlayer(MediaPlayerDevice): service_data[ATTR_ENTITY_ID] = active_child.entity_id - self.hass.services.call(DOMAIN, service_name, service_data, - blocking=True) + yield from self.hass.services.async_call( + DOMAIN, service_name, service_data, blocking=True) @property def should_poll(self): @@ -350,9 +357,9 @@ class UniversalMediaPlayer(MediaPlayerDevice): return self._override_or_child_attr(ATTR_INPUT_SOURCE_LIST) @property - def supported_media_commands(self): - """Flag media commands that are supported.""" - flags = self._child_attr(ATTR_SUPPORTED_MEDIA_COMMANDS) or 0 + def supported_features(self): + """Flag media player features that are supported.""" + flags = self._child_attr(ATTR_SUPPORTED_FEATURES) or 0 if SERVICE_TURN_ON in self._cmds: flags |= SUPPORT_TURN_ON @@ -395,77 +402,130 @@ class UniversalMediaPlayer(MediaPlayerDevice): """When was the position of the current playing media valid.""" return self._child_attr(ATTR_MEDIA_POSITION_UPDATED_AT) - def turn_on(self): - """Turn the media player on.""" - self._call_service(SERVICE_TURN_ON, allow_override=True) + def async_turn_on(self): + """Turn the media player on. - def turn_off(self): - """Turn the media player off.""" - self._call_service(SERVICE_TURN_OFF, allow_override=True) + This method must be run in the event loop and returns a coroutine. + """ + return self._async_call_service(SERVICE_TURN_ON, allow_override=True) - def mute_volume(self, is_volume_muted): - """Mute the volume.""" + def async_turn_off(self): + """Turn the media player off. + + This method must be run in the event loop and returns a coroutine. + """ + return self._async_call_service(SERVICE_TURN_OFF, allow_override=True) + + def async_mute_volume(self, is_volume_muted): + """Mute the volume. + + This method must be run in the event loop and returns a coroutine. + """ data = {ATTR_MEDIA_VOLUME_MUTED: is_volume_muted} - self._call_service(SERVICE_VOLUME_MUTE, data, allow_override=True) + return self._async_call_service( + SERVICE_VOLUME_MUTE, data, allow_override=True) - def set_volume_level(self, volume_level): - """Set volume level, range 0..1.""" + def async_set_volume_level(self, volume_level): + """Set volume level, range 0..1. + + This method must be run in the event loop and returns a coroutine. + """ data = {ATTR_MEDIA_VOLUME_LEVEL: volume_level} - self._call_service(SERVICE_VOLUME_SET, data, allow_override=True) + return self._async_call_service( + SERVICE_VOLUME_SET, data, allow_override=True) - def media_play(self): - """Send play commmand.""" - self._call_service(SERVICE_MEDIA_PLAY) + def async_media_play(self): + """Send play commmand. - def media_pause(self): - """Send pause command.""" - self._call_service(SERVICE_MEDIA_PAUSE) + This method must be run in the event loop and returns a coroutine. + """ + return self._async_call_service(SERVICE_MEDIA_PLAY) - def media_stop(self): - """Send stop command.""" - self._call_service(SERVICE_MEDIA_STOP) + def async_media_pause(self): + """Send pause command. - def media_previous_track(self): - """Send previous track command.""" - self._call_service(SERVICE_MEDIA_PREVIOUS_TRACK) + This method must be run in the event loop and returns a coroutine. + """ + return self._async_call_service(SERVICE_MEDIA_PAUSE) - def media_next_track(self): - """Send next track command.""" - self._call_service(SERVICE_MEDIA_NEXT_TRACK) + def async_media_stop(self): + """Send stop command. - def media_seek(self, position): - """Send seek command.""" + This method must be run in the event loop and returns a coroutine. + """ + return self._async_call_service(SERVICE_MEDIA_STOP) + + def async_media_previous_track(self): + """Send previous track command. + + This method must be run in the event loop and returns a coroutine. + """ + return self._async_call_service(SERVICE_MEDIA_PREVIOUS_TRACK) + + def async_media_next_track(self): + """Send next track command. + + This method must be run in the event loop and returns a coroutine. + """ + return self._async_call_service(SERVICE_MEDIA_NEXT_TRACK) + + def async_media_seek(self, position): + """Send seek command. + + This method must be run in the event loop and returns a coroutine. + """ data = {ATTR_MEDIA_SEEK_POSITION: position} - self._call_service(SERVICE_MEDIA_SEEK, data) + return self._async_call_service(SERVICE_MEDIA_SEEK, data) - def play_media(self, media_type, media_id, **kwargs): - """Play a piece of media.""" + def async_play_media(self, media_type, media_id, **kwargs): + """Play a piece of media. + + This method must be run in the event loop and returns a coroutine. + """ data = {ATTR_MEDIA_CONTENT_TYPE: media_type, ATTR_MEDIA_CONTENT_ID: media_id} - self._call_service(SERVICE_PLAY_MEDIA, data) + return self._async_call_service(SERVICE_PLAY_MEDIA, data) - def volume_up(self): - """Turn volume up for media player.""" - self._call_service(SERVICE_VOLUME_UP, allow_override=True) + def async_volume_up(self): + """Turn volume up for media player. - def volume_down(self): - """Turn volume down for media player.""" - self._call_service(SERVICE_VOLUME_DOWN, allow_override=True) + This method must be run in the event loop and returns a coroutine. + """ + return self._async_call_service(SERVICE_VOLUME_UP, allow_override=True) - def media_play_pause(self): - """Play or pause the media player.""" - self._call_service(SERVICE_MEDIA_PLAY_PAUSE) + def async_volume_down(self): + """Turn volume down for media player. - def select_source(self, source): - """Set the input source.""" + This method must be run in the event loop and returns a coroutine. + """ + return self._async_call_service( + SERVICE_VOLUME_DOWN, allow_override=True) + + def async_media_play_pause(self): + """Play or pause the media player. + + This method must be run in the event loop and returns a coroutine. + """ + return self._async_call_service(SERVICE_MEDIA_PLAY_PAUSE) + + def async_select_source(self, source): + """Set the input source. + + This method must be run in the event loop and returns a coroutine. + """ data = {ATTR_INPUT_SOURCE: source} - self._call_service(SERVICE_SELECT_SOURCE, data, allow_override=True) + return self._async_call_service( + SERVICE_SELECT_SOURCE, data, allow_override=True) - def clear_playlist(self): - """Clear players playlist.""" - self._call_service(SERVICE_CLEAR_PLAYLIST) + def async_clear_playlist(self): + """Clear players playlist. - def update(self): + This method must be run in the event loop and returns a coroutine. + """ + return self._async_call_service(SERVICE_CLEAR_PLAYLIST) + + @asyncio.coroutine + def async_update(self): """Update state in HA.""" for child_name in self._children: child_state = self.hass.states.get(child_name) diff --git a/homeassistant/components/media_player/vlc.py b/homeassistant/components/media_player/vlc.py index 711c8c74422..9439e6da5ad 100644 --- a/homeassistant/components/media_player/vlc.py +++ b/homeassistant/components/media_player/vlc.py @@ -21,6 +21,7 @@ REQUIREMENTS = ['python-vlc==1.1.2'] _LOGGER = logging.getLogger(__name__) CONF_ARGUMENTS = 'arguments' +DEFAULT_NAME = 'Vlc' SUPPORT_VLC = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_PLAY_MEDIA | SUPPORT_PLAY @@ -34,7 +35,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the vlc platform.""" - add_devices([VlcDevice(config.get(CONF_NAME), + add_devices([VlcDevice(config.get(CONF_NAME, DEFAULT_NAME), config.get(CONF_ARGUMENTS))]) @@ -96,8 +97,8 @@ class VlcDevice(MediaPlayerDevice): return self._muted @property - def supported_media_commands(self): - """Flag of media commands that are supported.""" + def supported_features(self): + """Flag media player features that are supported.""" return SUPPORT_VLC @property diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py index fbd3bb14e74..da498dc3d5b 100644 --- a/homeassistant/components/media_player/webostv.py +++ b/homeassistant/components/media_player/webostv.py @@ -20,13 +20,13 @@ from homeassistant.components.media_player import ( from homeassistant.const import ( CONF_HOST, CONF_MAC, CONF_CUSTOMIZE, STATE_OFF, STATE_PLAYING, STATE_PAUSED, - STATE_UNKNOWN, CONF_NAME) + STATE_UNKNOWN, CONF_NAME, CONF_FILENAME) from homeassistant.loader import get_component import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['https://github.com/TheRealLink/pylgtv' - '/archive/v0.1.2.zip' - '#pylgtv==0.1.2', + '/archive/v0.1.3.zip' + '#pylgtv==0.1.3', 'websockets==3.2', 'wakeonlan==0.2.2'] @@ -37,6 +37,8 @@ CONF_SOURCES = 'sources' DEFAULT_NAME = 'LG webOS Smart TV' +WEBOSTV_CONFIG_FILE = 'webostv.conf' + SUPPORT_WEBOSTV = SUPPORT_TURN_OFF | \ SUPPORT_NEXT_TRACK | SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | \ SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_STEP | \ @@ -55,6 +57,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_MAC): cv.string, vol.Optional(CONF_CUSTOMIZE, default={}): CUSTOMIZE_SCHEMA, + vol.Optional(CONF_FILENAME, default=WEBOSTV_CONFIG_FILE): cv.string }) @@ -77,16 +80,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None): mac = config.get(CONF_MAC) name = config.get(CONF_NAME) customize = config.get(CONF_CUSTOMIZE) - setup_tv(host, mac, name, customize, hass, add_devices) + config = hass.config.path(config.get(CONF_FILENAME)) + setup_tv(host, mac, name, customize, config, hass, add_devices) -def setup_tv(host, mac, name, customize, hass, add_devices): +def setup_tv(host, mac, name, customize, config, hass, add_devices): """Setup a LG WebOS TV based on host parameter.""" from pylgtv import WebOsClient from pylgtv import PyLGTVPairException from websockets.exceptions import ConnectionClosed - client = WebOsClient(host) + client = WebOsClient(host, config) if not client.is_registered(): if host in _CONFIGURING: @@ -104,7 +108,7 @@ def setup_tv(host, mac, name, customize, hass, add_devices): # Not registered, request configuration. _LOGGER.warning("LG webOS TV %s needs to be paired", host) request_configuration( - host, mac, name, customize, hass, add_devices) + host, mac, name, customize, config, hass, add_devices) return # If we came here and configuring this host, mark as done. @@ -113,11 +117,11 @@ def setup_tv(host, mac, name, customize, hass, add_devices): configurator = get_component('configurator') configurator.request_done(request_id) - add_devices([LgWebOSDevice(host, mac, name, customize)], True) + add_devices([LgWebOSDevice(host, mac, name, customize, config)], True) def request_configuration( - host, mac, name, customize, hass, add_devices): + host, mac, name, customize, config, hass, add_devices): """Request configuration steps from the user.""" configurator = get_component('configurator') @@ -130,7 +134,7 @@ def request_configuration( # pylint: disable=unused-argument def lgtv_configuration_callback(data): """The actions to do when our configuration callback is called.""" - setup_tv(host, mac, name, customize, hass, add_devices) + setup_tv(host, mac, name, customize, config, hass, add_devices) _CONFIGURING[host] = configurator.request_config( hass, name, lgtv_configuration_callback, @@ -143,11 +147,11 @@ def request_configuration( class LgWebOSDevice(MediaPlayerDevice): """Representation of a LG WebOS TV.""" - def __init__(self, host, mac, name, customize): + def __init__(self, host, mac, name, customize, config): """Initialize the webos device.""" from pylgtv import WebOsClient from wakeonlan import wol - self._client = WebOsClient(host) + self._client = WebOsClient(host, config) self._wol = wol self._mac = mac self._customize = customize @@ -240,8 +244,8 @@ class LgWebOSDevice(MediaPlayerDevice): return None @property - def supported_media_commands(self): - """Flag of media commands that are supported.""" + def supported_features(self): + """Flag media player features that are supported.""" if self._mac: return SUPPORT_WEBOSTV | SUPPORT_TURN_ON return SUPPORT_WEBOSTV diff --git a/homeassistant/components/media_player/yamaha.py b/homeassistant/components/media_player/yamaha.py index 84778cef2d5..928d15b5950 100644 --- a/homeassistant/components/media_player/yamaha.py +++ b/homeassistant/components/media_player/yamaha.py @@ -181,9 +181,9 @@ class YamahaDevice(MediaPlayerDevice): return self._source_list @property - def supported_media_commands(self): - """Flag of media commands that are supported.""" - supported_commands = SUPPORT_YAMAHA + def supported_features(self): + """Flag media player features that are supported.""" + supported_features = SUPPORT_YAMAHA supports = self._playback_support mapping = {'play': (SUPPORT_PLAY | SUPPORT_PLAY_MEDIA), @@ -193,8 +193,8 @@ class YamahaDevice(MediaPlayerDevice): 'skip_r': SUPPORT_PREVIOUS_TRACK} for attr, feature in mapping.items(): if getattr(supports, attr, False): - supported_commands |= feature - return supported_commands + supported_features |= feature + return supported_features def turn_off(self): """Turn off media player.""" diff --git a/homeassistant/components/modbus.py b/homeassistant/components/modbus.py index 3bf6cbf031a..1f1363f2060 100644 --- a/homeassistant/components/modbus.py +++ b/homeassistant/components/modbus.py @@ -59,7 +59,7 @@ ATTR_VALUE = "value" SERVICE_WRITE_REGISTER_SCHEMA = vol.Schema({ vol.Required(ATTR_UNIT): cv.positive_int, vol.Required(ATTR_ADDRESS): cv.positive_int, - vol.Required(ATTR_VALUE): cv.positive_int + vol.Required(ATTR_VALUE): vol.All(cv.ensure_list, [cv.positive_int]) }) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index ad4cce15cf3..e880be177e8 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -4,7 +4,6 @@ Support for MQTT message handling. For more details about this component, please refer to the documentation at https://home-assistant.io/components/mqtt/ """ -import asyncio import logging import os import socket @@ -12,6 +11,7 @@ import time import voluptuous as vol +from homeassistant.core import callback from homeassistant.bootstrap import prepare_setup_platform from homeassistant.config import load_yaml_config_file from homeassistant.exceptions import HomeAssistantError @@ -36,6 +36,8 @@ REQUIREMENTS = ['paho-mqtt==1.2'] CONF_EMBEDDED = 'embedded' CONF_BROKER = 'broker' CONF_CLIENT_ID = 'client_id' +CONF_DISCOVERY = 'discovery' +CONF_DISCOVERY_PREFIX = 'discovery_prefix' CONF_KEEPALIVE = 'keepalive' CONF_CERTIFICATE = 'certificate' CONF_CLIENT_KEY = 'client_key' @@ -58,6 +60,8 @@ DEFAULT_KEEPALIVE = 60 DEFAULT_QOS = 0 DEFAULT_RETAIN = False DEFAULT_PROTOCOL = PROTOCOL_311 +DEFAULT_DISCOVERY = False +DEFAULT_DISCOVERY_PREFIX = 'homeassistant' ATTR_TOPIC = 'topic' ATTR_PAYLOAD = 'payload' @@ -70,7 +74,8 @@ MAX_RECONNECT_WAIT = 300 # seconds def valid_subscribe_topic(value, invalid_chars='\0'): """Validate that we can subscribe using this MQTT topic.""" - if isinstance(value, str) and all(c not in value for c in invalid_chars): + value = cv.string(value) + if all(c not in value for c in invalid_chars): return vol.Length(min=1, max=65535)(value) raise vol.Invalid('Invalid MQTT topic name') @@ -80,6 +85,11 @@ def valid_publish_topic(value): return valid_subscribe_topic(value, invalid_chars='#+\0') +def valid_discovery_topic(value): + """Validate a discovery topic.""" + return valid_subscribe_topic(value, invalid_chars='#+\0/') + + _VALID_QOS_SCHEMA = vol.All(vol.Coerce(int), vol.In([0, 1, 2])) CLIENT_KEY_AUTH_MSG = 'client_key and client_cert must both be present in ' \ @@ -111,7 +121,10 @@ CONFIG_SCHEMA = vol.Schema({ vol.All(cv.string, vol.In([PROTOCOL_31, PROTOCOL_311])), vol.Optional(CONF_EMBEDDED): HBMQTT_CONFIG_SCHEMA, vol.Optional(CONF_WILL_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, - vol.Optional(CONF_BIRTH_MESSAGE): MQTT_WILL_BIRTH_SCHEMA + vol.Optional(CONF_BIRTH_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, + vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY): cv.boolean, + vol.Optional(CONF_DISCOVERY_PREFIX, + default=DEFAULT_DISCOVERY_PREFIX): valid_discovery_topic, }), }, extra=vol.ALLOW_EXTRA) @@ -170,15 +183,16 @@ def publish_template(hass, topic, payload_template, qos=None, retain=None): hass.services.call(DOMAIN, SERVICE_PUBLISH, data) -def async_subscribe(hass, topic, callback, qos=DEFAULT_QOS): +@callback +def async_subscribe(hass, topic, msg_callback, qos=DEFAULT_QOS): """Subscribe to an MQTT topic.""" - @asyncio.coroutine + @callback def mqtt_topic_subscriber(event): """Match subscribed MQTT topic.""" if not _match_topic(topic, event.data[ATTR_TOPIC]): return - hass.async_run_job(callback, event.data[ATTR_TOPIC], + hass.async_run_job(msg_callback, event.data[ATTR_TOPIC], event.data[ATTR_PAYLOAD], event.data[ATTR_QOS]) async_remove = hass.bus.async_listen(EVENT_MQTT_MESSAGE_RECEIVED, @@ -213,6 +227,21 @@ def _setup_server(hass, config): return success and broker_config +def _setup_discovery(hass, config): + """Try to start the discovery of MQTT devices.""" + conf = config.get(DOMAIN, {}) + + discovery = prepare_setup_platform(hass, config, DOMAIN, 'discovery') + + if discovery is None: + _LOGGER.error("Unable to load MQTT discovery") + return None + + success = discovery.start(hass, conf[CONF_DISCOVERY_PREFIX], config) + + return success + + def setup(hass, config): """Start the MQTT protocol service.""" conf = config.get(DOMAIN, {}) @@ -301,6 +330,9 @@ def setup(hass, config): descriptions.get(SERVICE_PUBLISH), schema=MQTT_PUBLISH_SCHEMA) + if conf.get(CONF_DISCOVERY): + _setup_discovery(hass, config) + return True diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py new file mode 100644 index 00000000000..ca2d37bbbba --- /dev/null +++ b/homeassistant/components/mqtt/discovery.py @@ -0,0 +1,62 @@ +""" +Support for MQTT discovery. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/mqtt/#discovery +""" +import asyncio +import json +import logging +import re + +from homeassistant.core import callback +import homeassistant.components.mqtt as mqtt +from homeassistant.components.mqtt import DOMAIN +from homeassistant.helpers.discovery import async_load_platform +from homeassistant.const import CONF_PLATFORM +from homeassistant.components.mqtt import CONF_STATE_TOPIC + +_LOGGER = logging.getLogger(__name__) + +TOPIC_MATCHER = re.compile( + r'homeassistant/(?P\w+)/(?P\w+)/config') +SUPPORTED_COMPONENTS = ['binary_sensor'] + + +@callback +def async_start(hass, discovery_topic, hass_config): + """Initialization of MQTT Discovery.""" + @asyncio.coroutine + def async_device_message_received(topic, payload, qos): + """Process the received message.""" + match = TOPIC_MATCHER.match(topic) + + if not match: + return + + component, object_id = match.groups() + + try: + payload = json.loads(payload) + except ValueError: + _LOGGER.warning( + "Unable to parse JSON %s: %s", object_id, payload) + return + + if component not in SUPPORTED_COMPONENTS: + _LOGGER.warning("Component %s is not supported", component) + return + + payload = dict(payload) + payload[CONF_PLATFORM] = 'mqtt' + if CONF_STATE_TOPIC not in payload: + payload[CONF_STATE_TOPIC] = '{}/{}/{}/state'.format( + discovery_topic, component, object_id) + + yield from async_load_platform( + hass, component, DOMAIN, payload, hass_config) + + mqtt.async_subscribe(hass, discovery_topic + '/#', + async_device_message_received, 0) + + return True diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index 52b851d1983..d9c8584a5e9 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -7,6 +7,7 @@ https://home-assistant.io/components/sensor.mysensors/ import logging import os import socket +import sys import voluptuous as vol @@ -81,15 +82,37 @@ def has_all_unique_files(value): return value +def is_persistence_file(value): + """Validate that persistence file path ends in either .pickle or .json.""" + if value.endswith(('.json', '.pickle')): + return value + else: + raise vol.Invalid( + '{} does not end in either `.json` or `.pickle`'.format(value)) + + +def is_serial_port(value): + """Validate that value is a windows serial port or a unix device.""" + if sys.platform.startswith('win'): + ports = ('COM{}'.format(idx + 1) for idx in range(256)) + if value in ports: + return value + else: + raise vol.Invalid( + '{} is not a serial port'.format(value)) + else: + return cv.isdevice(value) + + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_GATEWAYS): vol.All( cv.ensure_list, has_all_unique_files, [{ vol.Required(CONF_DEVICE): - vol.Any(cv.isdevice, MQTT_COMPONENT, is_socket_address), + vol.Any(MQTT_COMPONENT, is_socket_address, is_serial_port), vol.Optional(CONF_PERSISTENCE_FILE): - vol.All(cv.string, has_parent_dir), + vol.All(cv.string, is_persistence_file, has_parent_dir), vol.Optional( CONF_BAUD_RATE, default=DEFAULT_BAUD_RATE): cv.positive_int, @@ -211,6 +234,9 @@ def setup(hass, config): 'cover']: discovery.load_platform(hass, component, DOMAIN, {}, config) + discovery.load_platform( + hass, 'device_tracker', DOMAIN, {}, config) + discovery.load_platform( hass, 'notify', DOMAIN, {CONF_NAME: DOMAIN}, config) diff --git a/homeassistant/components/nest.py b/homeassistant/components/nest.py index 13c2ddc7bed..94a0db35f4d 100644 --- a/homeassistant/components/nest.py +++ b/homeassistant/components/nest.py @@ -19,10 +19,7 @@ from homeassistant.loader import get_component _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = [ - 'http://github.com/technicalpickles/python-nest' - '/archive/e6c9d56a8df455d4d7746389811f2c1387e8cb33.zip' # nest-cam branch - '#python-nest==3.0.2'] +REQUIREMENTS = ['python-nest==3.1.0'] DOMAIN = 'nest' diff --git a/homeassistant/components/notify/facebook.py b/homeassistant/components/notify/facebook.py index e598b0e818b..8f8bb98bbe1 100644 --- a/homeassistant/components/notify/facebook.py +++ b/homeassistant/components/notify/facebook.py @@ -12,7 +12,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( - ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService) + ATTR_TARGET, ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService) from homeassistant.const import CONTENT_TYPE_JSON _LOGGER = logging.getLogger(__name__) @@ -41,6 +41,15 @@ class FacebookNotificationService(BaseNotificationService): """Send some message.""" payload = {'access_token': self.page_access_token} targets = kwargs.get(ATTR_TARGET) + data = kwargs.get(ATTR_DATA) + + body_message = {"text": message} + + if data is not None: + body_message.update(data) + # Only one of text or attachment can be specified + if 'attachment' in body_message: + body_message.pop('text') if not targets: _LOGGER.error("At least 1 target is required") @@ -49,7 +58,7 @@ class FacebookNotificationService(BaseNotificationService): for target in targets: body = { "recipient": {"phone_number": target}, - "message": {"text": message} + "message": body_message } import json resp = requests.post(BASE_URL, data=json.dumps(body), diff --git a/homeassistant/components/notify/mailgun.py b/homeassistant/components/notify/mailgun.py new file mode 100644 index 00000000000..4765bd6893a --- /dev/null +++ b/homeassistant/components/notify/mailgun.py @@ -0,0 +1,109 @@ +""" +Support for the Mailgun mail service. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.mailgun/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.notify import ( + PLATFORM_SCHEMA, BaseNotificationService, + ATTR_TITLE, ATTR_TITLE_DEFAULT, ATTR_DATA) +from homeassistant.const import (CONF_TOKEN, CONF_DOMAIN, + CONF_RECIPIENT, CONF_SENDER) +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) +REQUIREMENTS = ['https://github.com/pschmitt/pymailgun/' + 'archive/1.3.zip#' + 'pymailgun==1.3'] + +# Images to attach to notification +ATTR_IMAGES = 'images' + +# Configuration item for the domain to use. +CONF_SANDBOX = 'sandbox' + +# Default sender name +DEFAULT_SENDER = 'hass@{domain}' +# Default sandbox value +DEFAULT_SANDBOX = False + +# pylint: disable=no-value-for-parameter +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_TOKEN): cv.string, + vol.Required(CONF_RECIPIENT): vol.Email(), + vol.Optional(CONF_DOMAIN): cv.string, + vol.Optional(CONF_SENDER): vol.Email(), + vol.Optional(CONF_SANDBOX, default=DEFAULT_SANDBOX): cv.boolean, +}) + + +def get_service(hass, config, discovery_info=None): + """Get the Mailgun notification service.""" + mailgun_service = MailgunNotificationService(config.get(CONF_DOMAIN), + config.get(CONF_SANDBOX), + config.get(CONF_TOKEN), + config.get(CONF_SENDER), + config.get(CONF_RECIPIENT)) + if mailgun_service.connection_is_valid(): + return mailgun_service + else: + return None + + +class MailgunNotificationService(BaseNotificationService): + """Implement a notification service for the Mailgun mail service.""" + + def __init__(self, domain, sandbox, token, sender, recipient): + """Initialize the service.""" + self._client = None # Mailgun API client + self._domain = domain + self._sandbox = sandbox + self._token = token + self._sender = sender + self._recipient = recipient + + def initialize_client(self): + """Initialize the connection to Mailgun.""" + from pymailgun import Client + self._client = Client(self._token, self._domain, self._sandbox) + _LOGGER.debug('Mailgun domain: %s', self._client.domain) + self._domain = self._client.domain + if not self._sender: + self._sender = DEFAULT_SENDER.format(domain=self._domain) + + def connection_is_valid(self): + """Check whether the provided credentials are valid.""" + from pymailgun import (MailgunCredentialsError, MailgunDomainError) + try: + self.initialize_client() + except MailgunCredentialsError: + _LOGGER.exception('Invalid credentials') + return False + except MailgunDomainError as mailgun_error: + _LOGGER.exception(mailgun_error) + return False + return True + + def send_message(self, message="", **kwargs): + """Send a mail to the recipient.""" + from pymailgun import MailgunError + subject = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) + data = kwargs.get(ATTR_DATA) + files = data.get(ATTR_IMAGES) if data else None + + try: + # Initialize the client in case it was not. + if self._client is None: + self.initialize_client() + resp = self._client.send_mail(sender=self._sender, + to=self._recipient, + subject=subject, + text=message, + files=files) + _LOGGER.debug('Message sent: %s', resp) + except MailgunError as mailgun_error: + _LOGGER.exception('Failed to send message: %s', mailgun_error) diff --git a/homeassistant/components/notify/slack.py b/homeassistant/components/notify/slack.py index 90944cbd308..ea21d2b2afc 100644 --- a/homeassistant/components/notify/slack.py +++ b/homeassistant/components/notify/slack.py @@ -14,7 +14,7 @@ from homeassistant.const import ( CONF_API_KEY, CONF_USERNAME, CONF_ICON) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['slacker==0.9.30'] +REQUIREMENTS = ['slacker==0.9.40'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/notify/smtp.py b/homeassistant/components/notify/smtp.py index 45b477cdfb8..89a71db3cf4 100644 --- a/homeassistant/components/notify/smtp.py +++ b/homeassistant/components/notify/smtp.py @@ -10,6 +10,7 @@ from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.mime.image import MIMEImage import email.utils +from email.mime.application import MIMEApplication import voluptuous as vol @@ -179,9 +180,19 @@ def _build_multipart_msg(message, images): body_text.append('
'.format(cid)) try: with open(atch_name, 'rb') as attachment_file: - attachment = MIMEImage(attachment_file.read()) - msg.attach(attachment) - attachment.add_header('Content-ID', '<{}>'.format(cid)) + file_bytes = attachment_file.read() + try: + attachment = MIMEImage(file_bytes) + msg.attach(attachment) + attachment.add_header('Content-ID', '<{}>'.format(cid)) + except TypeError: + _LOGGER.warning('Attachment %s has an unkown MIME type.' + ' Falling back to file', atch_name) + attachment = MIMEApplication(file_bytes, Name=atch_name) + attachment['Content-Disposition'] = ('attachment; ' + 'filename="%s"' % + atch_name) + msg.attach(attachment) except FileNotFoundError: _LOGGER.warning('Attachment %s not found. Skipping', atch_name) diff --git a/homeassistant/components/notify/webostv.py b/homeassistant/components/notify/webostv.py index 815e2ff750a..edaab1b5521 100644 --- a/homeassistant/components/notify/webostv.py +++ b/homeassistant/components/notify/webostv.py @@ -11,16 +11,18 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( BaseNotificationService, PLATFORM_SCHEMA) -from homeassistant.const import CONF_HOST +from homeassistant.const import (CONF_FILENAME, CONF_HOST) -REQUIREMENTS = ['https://github.com/TheRealLink/pylgtv/archive/v0.1.2.zip' - '#pylgtv==0.1.2'] +REQUIREMENTS = ['https://github.com/TheRealLink/pylgtv/archive/v0.1.3.zip' + '#pylgtv==0.1.3'] _LOGGER = logging.getLogger(__name__) +WEBOSTV_CONFIG_FILE = 'webostv.conf' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_FILENAME, default=WEBOSTV_CONFIG_FILE): cv.string }) @@ -29,7 +31,8 @@ def get_service(hass, config, discovery_info=None): from pylgtv import WebOsClient from pylgtv import PyLGTVPairException - client = WebOsClient(config.get(CONF_HOST)) + path = hass.config.path(config.get(CONF_FILENAME)) + client = WebOsClient(config.get(CONF_HOST), key_file_path=path) try: client.register() diff --git a/homeassistant/components/notify/xmpp.py b/homeassistant/components/notify/xmpp.py index 27bfd2aa607..2b72dfc4f8c 100644 --- a/homeassistant/components/notify/xmpp.py +++ b/homeassistant/components/notify/xmpp.py @@ -15,7 +15,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_SENDER, CONF_RECIPIENT REQUIREMENTS = ['sleekxmpp==1.3.1', 'dnspython3==1.15.0', - 'pyasn1==0.1.9', + 'pyasn1==0.2.2', 'pyasn1-modules==0.0.8'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 3d8d1357b0f..b227a8ce76a 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -13,6 +13,7 @@ import threading import time from datetime import timedelta, datetime from typing import Any, Union, Optional, List, Dict +from contextlib import contextmanager import voluptuous as vol @@ -22,7 +23,7 @@ from homeassistant.const import ( CONF_INCLUDE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import track_point_in_utc_time +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType, QueryType import homeassistant.util.dt as dt_util @@ -39,6 +40,7 @@ CONF_PURGE_DAYS = 'purge_days' RETRIES = 3 CONNECT_RETRY_WAIT = 10 QUERY_RETRY_WAIT = 0.1 +ERROR_QUERY = "Error during query: %s" CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -62,28 +64,45 @@ _INSTANCE = None # type: Any _LOGGER = logging.getLogger(__name__) # These classes will be populated during setup() -# pylint: disable=invalid-name,no-member -Session = None # pylint: disable=no-member +# scoped_session, in the same thread session_scope() stays the same +_SESSION = None + + +@contextmanager +def session_scope(): + """Provide a transactional scope around a series of operations.""" + session = _SESSION() + try: + yield session + session.commit() + except Exception as err: # pylint: disable=broad-except + _LOGGER.error(ERROR_QUERY, err) + session.rollback() + raise + finally: + session.close() # pylint: disable=invalid-sequence-index -def execute(q: QueryType) -> List[Any]: +def execute(qry: QueryType) -> List[Any]: """Query the database and convert the objects to HA native form. This method also retries a few times in the case of stale connections. """ + _verify_instance() + import sqlalchemy.exc - try: + with session_scope() as session: for _ in range(0, RETRIES): try: return [ row for row in - (row.to_native() for row in q) + (row.to_native() for row in qry) if row is not None] - except sqlalchemy.exc.SQLAlchemyError as e: - log_error(e, retry_wait=QUERY_RETRY_WAIT, rollback=True) - finally: - Session.close() + except sqlalchemy.exc.SQLAlchemyError as err: + _LOGGER.error(ERROR_QUERY, err) + session.rollback() + time.sleep(QUERY_RETRY_WAIT) return [] @@ -101,9 +120,13 @@ def run_information(point_in_time: Optional[datetime]=None): start=_INSTANCE.recording_start, closed_incorrect=False) - return query('RecorderRuns').filter( - (recorder_runs.start < point_in_time) & - (recorder_runs.end > point_in_time)).first() + with session_scope() as session: + res = query(recorder_runs).filter( + (recorder_runs.start < point_in_time) & + (recorder_runs.end > point_in_time)).first() + if res: + session.expunge(res) + return res def setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -134,8 +157,8 @@ def query(model_name: Union[str, Any], *args) -> QueryType: _verify_instance() if isinstance(model_name, str): - return Session.query(get_model(model_name), *args) - return Session.query(model_name, *args) + return _SESSION().query(get_model(model_name), *args) + return _SESSION().query(model_name, *args) def get_model(model_name: str) -> Any: @@ -148,22 +171,6 @@ def get_model(model_name: str) -> Any: return None -def log_error(e: Exception, retry_wait: Optional[float]=0, - rollback: Optional[bool]=True, - message: Optional[str]="Error during query: %s") -> None: - """Log about SQLAlchemy errors in a sane manner.""" - import sqlalchemy.exc - if not isinstance(e, sqlalchemy.exc.OperationalError): - _LOGGER.exception(str(e)) - else: - _LOGGER.error(message, str(e)) - if rollback: - Session.rollback() - if retry_wait: - _LOGGER.info("Retrying in %s seconds", retry_wait) - time.sleep(retry_wait) - - class Recorder(threading.Thread): """A threaded recorder class.""" @@ -204,18 +211,14 @@ class Recorder(threading.Thread): self._setup_connection() self._setup_run() break - except sqlalchemy.exc.SQLAlchemyError as e: - log_error(e, retry_wait=CONNECT_RETRY_WAIT, rollback=False, - message="Error during connection setup: %s") + except sqlalchemy.exc.SQLAlchemyError as err: + _LOGGER.error("Error during connection setup: %s (retrying " + "in %s seconds)", err, CONNECT_RETRY_WAIT) + time.sleep(CONNECT_RETRY_WAIT) if self.purge_days is not None: - def purge_ticker(event): - """Rerun purge every second day.""" - self._purge_old_data() - track_point_in_utc_time(self.hass, purge_ticker, - dt_util.utcnow() + timedelta(days=2)) - track_point_in_utc_time(self.hass, purge_ticker, - dt_util.utcnow() + timedelta(minutes=5)) + async_track_time_interval( + self.hass, self._purge_old_data, timedelta(days=2)) while True: event = self.queue.get() @@ -250,16 +253,17 @@ class Recorder(threading.Thread): self.queue.task_done() continue - dbevent = Events.from_event(event) - self._commit(dbevent) + with session_scope() as session: + dbevent = Events.from_event(event) + self._commit(session, dbevent) - if event.event_type != EVENT_STATE_CHANGED: - self.queue.task_done() - continue + if event.event_type != EVENT_STATE_CHANGED: + self.queue.task_done() + continue - dbstate = States.from_event(event) - dbstate.event_id = dbevent.event_id - self._commit(dbstate) + dbstate = States.from_event(event) + dbstate.event_id = dbevent.event_id + self._commit(session, dbstate) self.queue.task_done() @@ -282,11 +286,14 @@ class Recorder(threading.Thread): def block_till_db_ready(self): """Block until the database session is ready.""" - self.db_ready.wait() + self.db_ready.wait(10) + while not self.db_ready.is_set(): + _LOGGER.warning('Database not ready, waiting another 10 seconds.') + self.db_ready.wait(10) def _setup_connection(self): """Ensure database is ready to fly.""" - global Session # pylint: disable=global-statement + global _SESSION # pylint: disable=invalid-name,global-statement import homeassistant.components.recorder.models as models from sqlalchemy import create_engine @@ -298,47 +305,129 @@ class Recorder(threading.Thread): self.engine = create_engine( 'sqlite://', connect_args={'check_same_thread': False}, - poolclass=StaticPool) + poolclass=StaticPool, + pool_reset_on_return=None) else: self.engine = create_engine(self.db_url, echo=False) models.Base.metadata.create_all(self.engine) session_factory = sessionmaker(bind=self.engine) - Session = scoped_session(session_factory) + _SESSION = scoped_session(session_factory) + self._migrate_schema() self.db_ready.set() + def _migrate_schema(self): + """Check if the schema needs to be upgraded.""" + from homeassistant.components.recorder.models import SCHEMA_VERSION + schema_changes = get_model('SchemaChanges') + with session_scope() as session: + res = session.query(schema_changes).order_by( + schema_changes.change_id.desc()).first() + current_version = getattr(res, 'schema_version', None) + + if current_version == SCHEMA_VERSION: + return + _LOGGER.debug("Schema version incorrect: %s", current_version) + + if current_version is None: + current_version = self._inspect_schema_version() + _LOGGER.debug("No schema version found. Inspected version: %s", + current_version) + + for version in range(current_version, SCHEMA_VERSION): + new_version = version + 1 + _LOGGER.info("Upgrading recorder db schema to version %s", + new_version) + self._apply_update(new_version) + self._commit(session, + schema_changes(schema_version=new_version)) + _LOGGER.info("Upgraded recorder db schema to version %s", + new_version) + + def _apply_update(self, new_version): + """Perform operations to bring schema up to date.""" + from sqlalchemy import Table + import homeassistant.components.recorder.models as models + + if new_version == 1: + def create_index(table_name, column_name): + """Create an index for the specified table and column.""" + table = Table(table_name, models.Base.metadata) + name = "_".join(("ix", table_name, column_name)) + # Look up the index object that was created from the models + index = next(idx for idx in table.indexes if idx.name == name) + _LOGGER.debug("Creating index for table %s column %s", + table_name, column_name) + index.create(self.engine) + _LOGGER.debug("Index creation done for table %s column %s", + table_name, column_name) + + create_index("events", "time_fired") + else: + raise ValueError("No schema migration defined for version {}" + .format(new_version)) + + def _inspect_schema_version(self): + """Determine the schema version by inspecting the db structure. + + When the schema verison is not present in the db, either db was just + created with the correct schema, or this is a db created before schema + versions were tracked. For now, we'll test if the changes for schema + version 1 are present to make the determination. Eventually this logic + can be removed and we can assume a new db is being created. + """ + from sqlalchemy.engine import reflection + import homeassistant.components.recorder.models as models + inspector = reflection.Inspector.from_engine(self.engine) + indexes = inspector.get_indexes("events") + with session_scope() as session: + for index in indexes: + if index['column_names'] == ["time_fired"]: + # Schema addition from version 1 detected. New DB. + current_version = models.SchemaChanges( + schema_version=models.SCHEMA_VERSION) + self._commit(session, current_version) + return models.SCHEMA_VERSION + + # Version 1 schema changes not found, this db needs to be migrated. + current_version = models.SchemaChanges(schema_version=0) + self._commit(session, current_version) + return current_version.schema_version + def _close_connection(self): """Close the connection.""" - global Session # pylint: disable=global-statement + global _SESSION # pylint: disable=invalid-name,global-statement self.engine.dispose() self.engine = None - Session = None + _SESSION = None def _setup_run(self): """Log the start of the current run.""" recorder_runs = get_model('RecorderRuns') - for run in query('RecorderRuns').filter_by(end=None): - run.closed_incorrect = True - run.end = self.recording_start - _LOGGER.warning("Ended unfinished session (id=%s from %s)", - run.run_id, run.start) - Session.add(run) + with session_scope() as session: + for run in query('RecorderRuns').filter_by(end=None): + run.closed_incorrect = True + run.end = self.recording_start + _LOGGER.warning("Ended unfinished session (id=%s from %s)", + run.run_id, run.start) + session.add(run) - _LOGGER.warning("Found unfinished sessions") + _LOGGER.warning("Found unfinished sessions") - self._run = recorder_runs( - start=self.recording_start, - created=dt_util.utcnow() - ) - self._commit(self._run) + self._run = recorder_runs( + start=self.recording_start, + created=dt_util.utcnow() + ) + self._commit(session, self._run) def _close_run(self): """Save end time for current run.""" - self._run.end = dt_util.utcnow() - self._commit(self._run) + with session_scope() as session: + self._run.end = dt_util.utcnow() + self._commit(session, self._run) self._run = None - def _purge_old_data(self): + def _purge_old_data(self, _=None): """Purge events and states older than purge_days ago.""" from homeassistant.components.recorder.models import Events, States @@ -355,8 +444,9 @@ class Recorder(threading.Thread): .delete(synchronize_session=False) _LOGGER.debug("Deleted %s states", deleted_rows) - if self._commit(_purge_states): - _LOGGER.info("Purged states created before %s", purge_before) + with session_scope() as session: + if self._commit(session, _purge_states): + _LOGGER.info("Purged states created before %s", purge_before) def _purge_events(session): deleted_rows = session.query(Events) \ @@ -364,10 +454,9 @@ class Recorder(threading.Thread): .delete(synchronize_session=False) _LOGGER.debug("Deleted %s events", deleted_rows) - if self._commit(_purge_events): - _LOGGER.info("Purged events created before %s", purge_before) - - Session.expire_all() + with session_scope() as session: + if self._commit(session, _purge_events): + _LOGGER.info("Purged events created before %s", purge_before) # Execute sqlite vacuum command to free up space on disk if self.engine.driver == 'sqlite': @@ -375,10 +464,9 @@ class Recorder(threading.Thread): self.engine.execute("VACUUM") @staticmethod - def _commit(work): + def _commit(session, work): """Commit & retry work: Either a model or in a function.""" import sqlalchemy.exc - session = Session() for _ in range(0, RETRIES): try: if callable(work): @@ -387,8 +475,10 @@ class Recorder(threading.Thread): session.add(work) session.commit() return True - except sqlalchemy.exc.OperationalError as e: - log_error(e, retry_wait=QUERY_RETRY_WAIT, rollback=True) + except sqlalchemy.exc.OperationalError as err: + _LOGGER.error(ERROR_QUERY, err) + session.rollback() + time.sleep(QUERY_RETRY_WAIT) return False @@ -396,4 +486,9 @@ def _verify_instance() -> None: """Throw error if recorder not initialized.""" if _INSTANCE is None: raise RuntimeError("Recorder not initialized.") + + ident = _INSTANCE.hass.loop.__dict__.get("_thread_ident") + if ident is not None and ident == threading.get_ident(): + raise RuntimeError('Cannot be called from within the event loop') + _INSTANCE.block_till_db_ready() diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 3b7b5aca1cb..4bc044a51bd 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -16,6 +16,8 @@ from homeassistant.remote import JSONEncoder # pylint: disable=invalid-name Base = declarative_base() +SCHEMA_VERSION = 1 + _LOGGER = logging.getLogger(__name__) @@ -27,7 +29,7 @@ class Events(Base): # type: ignore event_type = Column(String(32), index=True) event_data = Column(Text) origin = Column(String(32)) - time_fired = Column(DateTime(timezone=True)) + time_fired = Column(DateTime(timezone=True), index=True) created = Column(DateTime(timezone=True), default=datetime.utcnow) @staticmethod @@ -149,6 +151,15 @@ class RecorderRuns(Base): # type: ignore return self +class SchemaChanges(Base): # type: ignore + """Representation of schema version changes.""" + + __tablename__ = 'schema_changes' + change_id = Column(Integer, primary_key=True) + schema_version = Column(Integer) + changed = Column(DateTime(timezone=True), default=datetime.utcnow) + + def _process_timestamp(ts): """Process a timestamp into datetime object.""" if ts is None: diff --git a/homeassistant/components/rflink.py b/homeassistant/components/rflink.py new file mode 100644 index 00000000000..9a9e6d1145f --- /dev/null +++ b/homeassistant/components/rflink.py @@ -0,0 +1,402 @@ +"""Support for Rflink components. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/rflink/ + +Technical overview: + +The Rflink gateway is a USB serial device (Arduino with Rflink firwmare) +connected to a 433Mhz transceiver module. + +The the `rflink` Python module a asyncio transport/protocol is setup that +fires an callback for every (valid/supported) packet received by the Rflink +gateway. + +This component uses this callback to distribute 'rflink packet events' over +the HASS bus which can be subscribed to by entities/platform implementations. + +The platform implementions take care of creating new devices (if enabled) for +unsees incoming packet id's. + +Device Entities take care of matching to the packet id, interpreting and +performing actions based on the packet contents. Common entitiy logic is +maintained in this file. + +""" +import asyncio +from collections import defaultdict +import functools as ft +import logging + +from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, + STATE_UNKNOWN) +from homeassistant.core import CoreState, callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +import voluptuous as vol + +REQUIREMENTS = ['rflink==0.0.28'] + +DOMAIN = 'rflink' + +CONF_ALIASSES = 'aliasses' +CONF_DEVICES = 'devices' +CONF_DEVICE_DEFAULTS = 'device_defaults' +CONF_FIRE_EVENT = 'fire_event' +CONF_IGNORE_DEVICES = 'ignore_devices' +CONF_NEW_DEVICES_GROUP = 'new_devices_group' +CONF_RECONNECT_INTERVAL = 'reconnect_interval' +CONF_SIGNAL_REPETITIONS = 'signal_repetitions' +CONF_WAIT_FOR_ACK = 'wait_for_ack' + +DEFAULT_SIGNAL_REPETITIONS = 1 +DEFAULT_RECONNECT_INTERVAL = 10 + +DEVICE_DEFAULTS_SCHEMA = vol.Schema({ + vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean, + vol.Optional(CONF_SIGNAL_REPETITIONS, + default=DEFAULT_SIGNAL_REPETITIONS): vol.Coerce(int), +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_PORT): vol.Any(cv.port, cv.string), + vol.Optional(CONF_HOST, default=None): cv.string, + vol.Optional(CONF_WAIT_FOR_ACK, default=True): cv.boolean, + vol.Optional(CONF_RECONNECT_INTERVAL, + default=DEFAULT_RECONNECT_INTERVAL): int, + vol.Optional(CONF_IGNORE_DEVICES, default=[]): + vol.All(cv.ensure_list, [cv.string]), + }), +}, extra=vol.ALLOW_EXTRA) + +ATTR_EVENT = 'event' +ATTR_STATE = 'state' + +DATA_DEVICE_REGISTER = 'rflink_device_register' +DATA_ENTITY_LOOKUP = 'rflink_entity_lookup' + +EVENT_BUTTON_PRESSED = 'button_pressed' + +EVENT_KEY_COMMAND = 'command' +EVENT_KEY_ID = 'id' +EVENT_KEY_SENSOR = 'sensor' +EVENT_KEY_UNIT = 'unit' + +_LOGGER = logging.getLogger(__name__) + + +def identify_event_type(event): + """Look at event to determine type of device. + + Async friendly. + + """ + if EVENT_KEY_COMMAND in event: + return EVENT_KEY_COMMAND + elif EVENT_KEY_SENSOR in event: + return EVENT_KEY_SENSOR + else: + return 'unknown' + + +@asyncio.coroutine +def async_setup(hass, config): + """Setup the Rflink component.""" + from rflink.protocol import create_rflink_connection + import serial + + # allow entities to register themselves by device_id to be looked up when + # new rflink events arrive to be handled + hass.data[DATA_ENTITY_LOOKUP] = { + EVENT_KEY_COMMAND: defaultdict(list), + EVENT_KEY_SENSOR: defaultdict(list), + } + + # allow platform to specify function to register new unknown devices + hass.data[DATA_DEVICE_REGISTER] = {} + + @callback + def event_callback(event): + """Handle incoming rflink events. + + Rflink events arrive as dictionaries of varying content + depending on their type. Identify the events and distribute + accordingly. + + """ + event_type = identify_event_type(event) + _LOGGER.debug('event of type %s: %s', event_type, event) + + # don't propagate non entity events (eg: version string, ack response) + if event_type not in hass.data[DATA_ENTITY_LOOKUP]: + _LOGGER.debug('unhandled event of type: %s', event_type) + return + + # lookup entities who registered this device id as device id or alias + event_id = event.get('id', None) + entities = hass.data[DATA_ENTITY_LOOKUP][event_type][event_id] + + if entities: + # propagate event to every entity matching the device id + for entity in entities: + _LOGGER.debug('passing event to %s', entities) + entity.handle_event(event) + else: + _LOGGER.debug('device_id not known, adding new device') + + # if device is not yet known, register with platform (if loaded) + if event_type in hass.data[DATA_DEVICE_REGISTER]: + hass.async_run_job( + hass.data[DATA_DEVICE_REGISTER][event_type], event) + + # when connecting to tcp host instead of serial port (optional) + host = config[DOMAIN][CONF_HOST] + # tcp port when host configured, otherwise serial port + port = config[DOMAIN][CONF_PORT] + + @callback + def reconnect(exc=None): + """Schedule reconnect after connection has been unexpectedly lost.""" + # reset protocol binding before starting reconnect + RflinkCommand.set_rflink_protocol(None) + + # if HA is not stopping, initiate new connection + if hass.state != CoreState.stopping: + _LOGGER.warning('disconnected from Rflink, reconnecting') + hass.async_add_job(connect) + + @asyncio.coroutine + def connect(): + """Setup connection and hook it into HA for reconnect/shutdown.""" + _LOGGER.info('initiating Rflink connection') + + # rflink create_rflink_connection decides based on the value of host + # (string or None) if serial or tcp mode should be used + + # initiate serial/tcp connection to Rflink gateway + connection = create_rflink_connection( + port=port, + host=host, + event_callback=event_callback, + disconnect_callback=reconnect, + loop=hass.loop, + ignore=config[DOMAIN][CONF_IGNORE_DEVICES] + ) + + try: + transport, protocol = yield from connection + except (serial.serialutil.SerialException, ConnectionRefusedError, + TimeoutError) as exc: + reconnect_interval = config[DOMAIN][CONF_RECONNECT_INTERVAL] + _LOGGER.exception( + 'error connecting to Rflink, reconnecting in %s', + reconnect_interval) + hass.loop.call_later(reconnect_interval, reconnect, exc) + return + + # bind protocol to command class to allow entities to send commands + RflinkCommand.set_rflink_protocol( + protocol, config[DOMAIN][CONF_WAIT_FOR_ACK]) + + # handle shutdown of rflink asyncio transport + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, + lambda x: transport.close()) + + _LOGGER.info('connected to Rflink') + + # make initial connection + yield from connect() + + # whoo + return True + + +class RflinkDevice(Entity): + """Represents a Rflink device. + + Contains the common logic for Rflink entities. + + """ + + # should be set by component implementation + platform = None + # default state + _state = STATE_UNKNOWN + + def __init__(self, device_id, hass, name=None, + aliasses=None, fire_event=False, + signal_repetitions=DEFAULT_SIGNAL_REPETITIONS): + """Initialize the device.""" + self.hass = hass + + # rflink specific attributes for every component type + self._device_id = device_id + if name: + self._name = name + else: + self._name = device_id + + # generate list of device_ids to match against + if aliasses: + self._aliasses = aliasses + else: + self._aliasses = [] + + self._should_fire_event = fire_event + self._signal_repetitions = signal_repetitions + + def handle_event(self, event): + """Handle incoming event for device type.""" + # call platform specific event handler + self._handle_event(event) + + # propagate changes through ha + self.hass.async_add_job(self.async_update_ha_state()) + + # put command onto bus for user to subscribe to + if self._should_fire_event and identify_event_type( + event) == EVENT_KEY_COMMAND: + self.hass.bus.fire(EVENT_BUTTON_PRESSED, { + ATTR_ENTITY_ID: self.entity_id, + ATTR_STATE: event[EVENT_KEY_COMMAND], + }) + _LOGGER.debug( + 'fired bus event for %s: %s', + self.entity_id, + event[EVENT_KEY_COMMAND]) + + def _handle_event(self, event): + """Platform specific event handler.""" + raise NotImplementedError() + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return a name for the device.""" + return self._name + + @property + def is_on(self): + """Return true if device is on.""" + if self.assumed_state: + return False + return self._state + + @property + def assumed_state(self): + """Assume device state until first device event sets state.""" + return self._state is STATE_UNKNOWN + + +class RflinkCommand(RflinkDevice): + """Singleton class to make Rflink command interface available to entities. + + This class is to be inherited by every Entity class that is actionable + (switches/lights). It exposes the Rflink command interface for these + entities. + + The Rflink interface is managed as a class level and set during setup (and + reset on reconnect). + + """ + + # keep repetition tasks to cancel if state is changed before repetitions + # are sent + _repetition_task = None + + @classmethod + def set_rflink_protocol(cls, protocol, wait_ack=None): + """Set the Rflink asyncio protocol as a class variable.""" + cls._protocol = protocol + if wait_ack is not None: + cls._wait_ack = wait_ack + + @asyncio.coroutine + def _async_handle_command(self, command, *args): + """Do bookkeeping for command, send it to rflink and update state.""" + self.cancel_queued_send_commands() + + if command == "turn_on": + cmd = 'on' + self._state = True + + elif command == 'turn_off': + cmd = 'off' + self._state = False + + elif command == 'dim': + # convert brightness to rflink dim level + cmd = str(int(args[0] / 17)) + self._state = True + + # send initial command and queue repetitions + # this allows the entity state to be updated quickly and not having to + # wait for all repetitions to be sent + yield from self._async_send_command(cmd, self._signal_repetitions) + + # Update state of entity + yield from self.async_update_ha_state() + + def cancel_queued_send_commands(self): + """Cancel queued signal repetition commands. + + For example when user changed state while repetitions are still + queued for broadcast. Or when a incoming Rflink command (remote + switch) changes the state. + + """ + # cancel any outstanding tasks from the previous state change + if self._repetition_task: + self._repetition_task.cancel() + + @asyncio.coroutine + def _async_send_command(self, cmd, repetitions): + """Send a command for device to Rflink gateway.""" + _LOGGER.debug('sending command: %s to rflink device: %s', + cmd, self._device_id) + + if self._wait_ack: + # Puts command on outgoing buffer then waits for Rflink to confirm + # the command has been send out in the ether. + yield from self._protocol.send_command_ack(self._device_id, cmd) + else: + # Puts command on outgoing buffer and returns straight away. + # Rflink protocol/transport handles asynchronous writing of buffer + # to serial/tcp device. Does not wait for command send + # confirmation. + self.hass.loop.run_in_executor(None, ft.partial( + self._protocol.send_command, self._device_id, cmd)) + + if repetitions > 1: + self._repetition_task = self.hass.loop.create_task( + self._async_send_command(cmd, repetitions - 1)) + + +class SwitchableRflinkDevice(RflinkCommand): + """Rflink entity which can switch on/off (eg: light, switch).""" + + 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 == 'on': + self._state = True + elif command == 'off': + self._state = False + + @asyncio.coroutine + def async_turn_on(self, **kwargs): + """Turn the device on.""" + yield from self._async_handle_command("turn_on") + + @asyncio.coroutine + def async_turn_off(self, **kwargs): + """Turn the device off.""" + yield from self._async_handle_command("turn_off") diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py index 6918a596988..caa44d5a8f2 100644 --- a/homeassistant/components/rfxtrx.py +++ b/homeassistant/components/rfxtrx.py @@ -14,7 +14,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.helpers.entity import Entity from homeassistant.const import (ATTR_ENTITY_ID, TEMP_CELSIUS) -REQUIREMENTS = ['pyRFXtrx==0.15.0'] +REQUIREMENTS = ['pyRFXtrx==0.16.1'] DOMAIN = "rfxtrx" diff --git a/homeassistant/components/sensor/amcrest.py b/homeassistant/components/sensor/amcrest.py index 08d551b8fde..f250905e952 100644 --- a/homeassistant/components/sensor/amcrest.py +++ b/homeassistant/components/sensor/amcrest.py @@ -122,10 +122,18 @@ class AmcrestSensor(Entity): def update(self): """Get the latest data and updates the state.""" - version, build_date = self._camera.software_information - self._attrs['Build Date'] = build_date.split('=')[-1] - self._attrs['Serial Number'] = self._camera.serial_number - self._attrs['Version'] = version.split('=')[-1] + try: + version, build_date = self._camera.software_information + self._attrs['Build Date'] = build_date.split('=')[-1] + self._attrs['Version'] = version.split('=')[-1] + except ValueError: + self._attrs['Build Date'] = 'Not Available' + self._attrs['Version'] = 'Not Available' + + try: + self._attrs['Serial Number'] = self._camera.serial_number + except ValueError: + self._attrs['Serial Number'] = 'Not Available' if self._sensor_type == 'motion_detector': self._state = self._camera.is_motion_detected diff --git a/homeassistant/components/sensor/arwn.py b/homeassistant/components/sensor/arwn.py index c0012b4ac92..834efa1b415 100644 --- a/homeassistant/components/sensor/arwn.py +++ b/homeassistant/components/sensor/arwn.py @@ -35,11 +35,12 @@ def discover_sensors(topic, payload): unit = TEMP_CELSIUS return ArwnSensor(name, 'temp', unit) if domain == 'barometer': - return ArwnSensor('Barometer', 'pressure', unit) + return ArwnSensor('Barometer', 'pressure', unit, + "mdi:thermometer-lines") if domain == 'wind': - return (ArwnSensor('Wind Speed', 'speed', unit), - ArwnSensor('Wind Gust', 'gust', unit), - ArwnSensor('Wind Direction', 'direction', '°')) + return (ArwnSensor('Wind Speed', 'speed', unit, "mdi:speedometer"), + ArwnSensor('Wind Gust', 'gust', unit, "mdi:speedometer"), + ArwnSensor('Wind Direction', 'direction', '°', "mdi:compass")) def _slug(name): @@ -66,6 +67,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if not sensors: return + if isinstance(sensors, ArwnSensor): + sensors = (sensors, ) + if 'timestamp' in event: del event['timestamp'] @@ -88,7 +92,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class ArwnSensor(Entity): """Representation of an ARWN sensor.""" - def __init__(self, name, state_key, units): + def __init__(self, name, state_key, units, icon=None): """Initialize the sensor.""" self.hass = None self.entity_id = _slug(name) @@ -96,6 +100,7 @@ class ArwnSensor(Entity): self._state_key = state_key self.event = {} self._unit_of_measurement = units + self._icon = icon def set_event(self, event): """Update the sensor with the most recent event.""" @@ -126,3 +131,11 @@ class ArwnSensor(Entity): def should_poll(self): """Should we poll.""" return False + + @property + def icon(self): + """Icon of device based on its type.""" + if self._icon: + return self._icon + else: + return super().icon diff --git a/homeassistant/components/sensor/deutsche_bahn.py b/homeassistant/components/sensor/deutsche_bahn.py index 51394e0f3ac..34be6ba078c 100644 --- a/homeassistant/components/sensor/deutsche_bahn.py +++ b/homeassistant/components/sensor/deutsche_bahn.py @@ -65,7 +65,7 @@ class DeutscheBahnSensor(Entity): return self._state @property - def state_attributes(self): + def device_state_attributes(self): """Return the state attributes.""" connections = self.data.connections[0] connections['next'] = self.data.connections[1]['departure'] diff --git a/homeassistant/components/sensor/gpsd.py b/homeassistant/components/sensor/gpsd.py index 0fb24c96283..f6f9e75df31 100644 --- a/homeassistant/components/sensor/gpsd.py +++ b/homeassistant/components/sensor/gpsd.py @@ -98,7 +98,7 @@ class GpsdSensor(Entity): return STATE_UNKNOWN @property - def state_attributes(self): + def device_state_attributes(self): """Return the state attributes of the GPS.""" return { ATTR_LATITUDE: self.agps_thread.data_stream.lat, diff --git a/homeassistant/components/sensor/hddtemp.py b/homeassistant/components/sensor/hddtemp.py index 1a964a458e2..c0e3dc32d4d 100644 --- a/homeassistant/components/sensor/hddtemp.py +++ b/homeassistant/components/sensor/hddtemp.py @@ -83,7 +83,7 @@ class HddTempSensor(Entity): return TEMP_FAHRENHEIT @property - def state_attributes(self): + def device_state_attributes(self): """Return the state attributes of the sensor.""" return { ATTR_DEVICE: self.details[1], diff --git a/homeassistant/components/sensor/hp_ilo.py b/homeassistant/components/sensor/hp_ilo.py index 17a58dd9862..675db6400a0 100644 --- a/homeassistant/components/sensor/hp_ilo.py +++ b/homeassistant/components/sensor/hp_ilo.py @@ -110,7 +110,7 @@ class HpIloSensor(Entity): return self._state @property - def state_attributes(self): + def device_state_attributes(self): """Return the state attributes.""" return self._data diff --git a/homeassistant/components/sensor/imap_email_content.py b/homeassistant/components/sensor/imap_email_content.py index b5845ab78ed..5f9a7e7f8e7 100644 --- a/homeassistant/components/sensor/imap_email_content.py +++ b/homeassistant/components/sensor/imap_email_content.py @@ -159,7 +159,7 @@ class EmailContentSensor(Entity): return self._message @property - def state_attributes(self): + def device_state_attributes(self): """Return other state attributes for the message.""" return self._state_attributes diff --git a/homeassistant/components/sensor/linux_battery.py b/homeassistant/components/sensor/linux_battery.py index 79f485d80dd..ddfb12f008b 100644 --- a/homeassistant/components/sensor/linux_battery.py +++ b/homeassistant/components/sensor/linux_battery.py @@ -98,7 +98,7 @@ class LinuxBatterySensor(Entity): return ICON @property - def state_attributes(self): + def device_state_attributes(self): """Return the state attributes of the sensor.""" return { ATTR_NAME: self._battery_stat.name, diff --git a/homeassistant/components/sensor/miflora.py b/homeassistant/components/sensor/miflora.py index 1922d4832ee..2536bcd2bcc 100644 --- a/homeassistant/components/sensor/miflora.py +++ b/homeassistant/components/sensor/miflora.py @@ -14,22 +14,25 @@ import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_MAC) -REQUIREMENTS = ['miflora==0.1.14'] +REQUIREMENTS = ['miflora==0.1.15'] _LOGGER = logging.getLogger(__name__) +CONF_ADAPTER = 'adapter' CONF_CACHE = 'cache_value' CONF_FORCE_UPDATE = 'force_update' CONF_MEDIAN = 'median' CONF_RETRIES = 'retries' CONF_TIMEOUT = 'timeout' +DEFAULT_ADAPTER = 'hci0' +DEFAULT_UPDATE_INTERVAL = 1200 DEFAULT_FORCE_UPDATE = False DEFAULT_MEDIAN = 3 DEFAULT_NAME = 'Mi Flora' DEFAULT_RETRIES = 2 DEFAULT_TIMEOUT = 10 -DEFAULT_UPDATE_INTERVAL = 1200 + # Sensor types are defined like: Name, units SENSOR_TYPES = { @@ -50,6 +53,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, vol.Optional(CONF_RETRIES, default=DEFAULT_RETRIES): cv.positive_int, vol.Optional(CONF_CACHE, default=DEFAULT_UPDATE_INTERVAL): cv.positive_int, + vol.Optional(CONF_ADAPTER, default=DEFAULT_ADAPTER): cv.string, }) @@ -59,7 +63,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): cache = config.get(CONF_CACHE) poller = miflora_poller.MiFloraPoller( - config.get(CONF_MAC), cache_timeout=cache) + config.get(CONF_MAC), cache_timeout=cache, + adapter=config.get(CONF_ADAPTER)) force_update = config.get(CONF_FORCE_UPDATE) median = config.get(CONF_MEDIAN) poller.ble_timeout = config.get(CONF_TIMEOUT) diff --git a/homeassistant/components/sensor/mold_indicator.py b/homeassistant/components/sensor/mold_indicator.py index 61cdae5fb36..102b4620410 100644 --- a/homeassistant/components/sensor/mold_indicator.py +++ b/homeassistant/components/sensor/mold_indicator.py @@ -238,7 +238,7 @@ class MoldIndicator(Entity): return self._state @property - def state_attributes(self): + def device_state_attributes(self): """Return the state attributes.""" if self._is_metric: return { diff --git a/homeassistant/components/sensor/moon.py b/homeassistant/components/sensor/moon.py new file mode 100644 index 00000000000..3b13e625bb4 --- /dev/null +++ b/homeassistant/components/sensor/moon.py @@ -0,0 +1,81 @@ +""" +Support for tracking the moon phases. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.moon/ +""" +import asyncio +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import (CONF_NAME) +import homeassistant.util.dt as dt_util +from homeassistant.helpers.entity import Entity +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['astral==1.3.4'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'Moon' + +ICON = 'mdi:brightness-3' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up the Moon sensor.""" + name = config.get(CONF_NAME) + + yield from async_add_devices([MoonSensor(name)], True) + return True + + +class MoonSensor(Entity): + """Representation of a Moon sensor.""" + + def __init__(self, name): + """Initialize the sensor.""" + self._name = name + self._state = None + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + if self._state >= 21: + return 'Last quarter' + elif self._state >= 14: + return 'Full moon' + elif self._state >= 7: + return 'First quarter' + else: + return 'New moon' + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return 'Phase' + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return ICON + + @asyncio.coroutine + def async_update(self): + """Get the time and updates the states.""" + from astral import Astral + + today = dt_util.as_local(dt_util.utcnow()).date() + self._state = Astral().moon_phase(today) diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index 6c1f65606da..9f750e3c10b 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -33,7 +33,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): pres.S_TEMP: [set_req.V_TEMP], pres.S_HUM: [set_req.V_HUM], pres.S_BARO: [set_req.V_PRESSURE, set_req.V_FORECAST], - pres.S_WIND: [set_req.V_WIND, set_req.V_GUST], + pres.S_WIND: [set_req.V_WIND, set_req.V_GUST, set_req.V_DIRECTION], pres.S_RAIN: [set_req.V_RAIN, set_req.V_RAINRATE], pres.S_UV: [set_req.V_UV], pres.S_WEIGHT: [set_req.V_WEIGHT, set_req.V_IMPEDANCE], @@ -106,6 +106,7 @@ class MySensorsSensor(mysensors.MySensorsDeviceEntity, Entity): @property def unit_of_measurement(self): """Return the unit of measurement of this entity.""" + pres = self.gateway.const.Presentation set_req = self.gateway.const.SetReq unit_map = { set_req.V_TEMP: (TEMP_CELSIUS @@ -113,13 +114,14 @@ class MySensorsSensor(mysensors.MySensorsDeviceEntity, Entity): set_req.V_HUM: '%', set_req.V_DIMMER: '%', set_req.V_LIGHT_LEVEL: '%', + set_req.V_DIRECTION: '°', set_req.V_WEIGHT: 'kg', set_req.V_DISTANCE: 'm', set_req.V_IMPEDANCE: 'ohm', set_req.V_WATT: 'W', set_req.V_KWH: 'kWh', set_req.V_FLOW: 'm', - set_req.V_VOLUME: 'm3', + set_req.V_VOLUME: 'm³', set_req.V_VOLTAGE: 'V', set_req.V_CURRENT: 'A', } @@ -127,7 +129,11 @@ class MySensorsSensor(mysensors.MySensorsDeviceEntity, Entity): if set_req.V_UNIT_PREFIX in self._values: return self._values[ set_req.V_UNIT_PREFIX] - unit_map.update({set_req.V_PERCENTAGE: '%'}) + unit_map.update({ + set_req.V_PERCENTAGE: '%', + set_req.V_LEVEL: { + pres.S_SOUND: 'dB', pres.S_VIBRATION: 'Hz', + pres.S_LIGHT_LEVEL: 'lux'}}) if float(self.gateway.protocol_version) >= 2.0: unit_map.update({ set_req.V_ORP: 'mV', @@ -135,4 +141,7 @@ class MySensorsSensor(mysensors.MySensorsDeviceEntity, Entity): set_req.V_VAR: 'var', set_req.V_VA: 'VA', }) - return unit_map.get(self.value_type) + unit = unit_map.get(self.value_type) + if isinstance(unit, dict): + unit = unit.get(self.child_type) + return unit diff --git a/homeassistant/components/sensor/openevse.py b/homeassistant/components/sensor/openevse.py new file mode 100644 index 00000000000..4d1fad49fb0 --- /dev/null +++ b/homeassistant/components/sensor/openevse.py @@ -0,0 +1,102 @@ +""" +Support for monitoring an OpenEVSE Charger. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.openevse/ +""" + +import logging + +from requests import RequestException + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import TEMP_CELSIUS, CONF_HOST +from homeassistant.const import CONF_MONITORED_VARIABLES +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) +REQUIREMENTS = ['openevsewifi==0.4'] +SENSOR_TYPES = { + 'status': ['Charging Status', None], + 'charge_time': ['Charge Time Elapsed', 'minutes'], + 'ambient_temp': ['Ambient Termperature', TEMP_CELSIUS], + 'ir_temp': ['IR Temperature', TEMP_CELSIUS], + 'rtc_temp': ['RTC Temperature', TEMP_CELSIUS], + 'usage_session': ['Usage this Session', 'kWh'], + 'usage_total': ['Total Usage', 'kWh'] +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_MONITORED_VARIABLES, default=['status']): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the OpenEVSE sensor.""" + import openevsewifi + + host = config.get(CONF_HOST) + monitored_variables = config.get(CONF_MONITORED_VARIABLES) + + charger = openevsewifi.Charger(host) + + dev = [] + for variable in monitored_variables: + dev.append(OpenEVSESensor(variable, charger)) + + add_devices(dev) + + +class OpenEVSESensor(Entity): + """Implementation of an OpenEVSE sensor.""" + + # pylint: disable=too-many-arguments + def __init__(self, sensor_type, charger): + """Initialize the sensor.""" + self._name = SENSOR_TYPES[sensor_type][0] + self.type = sensor_type + self._state = None + self.charger = charger + self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this sensor.""" + return self._unit_of_measurement + + def update(self): + """Get the monitored data from the charger.""" + try: + if self.type == 'status': + self._state = self.charger.getStatus() + elif self.type == 'charge_time': + self._state = self.charger.getChargeTimeElapsed()/60 + elif self.type == 'ambient_temp': + self._state = self.charger.getAmbientTemperature() + elif self.type == 'ir_temp': + self._state = self.charger.getIRTemperature() + elif self.type == 'rtc_temp': + self._state = self.charger.getRTCTemperature() + elif self.type == 'usage_session': + self._state = float(self.charger.getUsageSession())/1000 + elif self.type == 'usage_total': + self._state = float(self.charger.getUsageTotal())/1000 + else: + self._state = 'Unknown' + except (RequestException, ValueError, KeyError): + _LOGGER.warning('Could not update status for %s', self.name) diff --git a/homeassistant/components/sensor/openweathermap.py b/homeassistant/components/sensor/openweathermap.py index 08b2a4c0f65..4c556e61ae2 100755 --- a/homeassistant/components/sensor/openweathermap.py +++ b/homeassistant/components/sensor/openweathermap.py @@ -17,7 +17,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['pyowm==2.6.0'] +REQUIREMENTS = ['pyowm==2.6.1'] _LOGGER = logging.getLogger(__name__) @@ -32,6 +32,7 @@ SENSOR_TYPES = { 'weather': ['Condition', None], 'temperature': ['Temperature', None], 'wind_speed': ['Wind speed', 'm/s'], + 'wind_bearing': ['Wind bearing', '°'], 'humidity': ['Humidity', '%'], 'pressure': ['Pressure', 'mbar'], 'clouds': ['Cloud coverage', '%'], @@ -64,9 +65,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): owm = OWM(config.get(CONF_API_KEY)) if not owm: - _LOGGER.error( - "Connection error " - "Please check your settings for OpenWeatherMap") + _LOGGER.error("Unable to connect to OpenWeatherMap") return False data = WeatherData(owm, forecast, hass.config.latitude, @@ -130,8 +129,7 @@ class OpenWeatherMapSensor(Entity): self._state = data.get_detailed_status() elif self.type == 'temperature': if self.temp_unit == TEMP_CELSIUS: - self._state = round(data.get_temperature('celsius')['temp'], - 1) + self._state = round(data.get_temperature('celsius')['temp'], 1) elif self.temp_unit == TEMP_FAHRENHEIT: self._state = round(data.get_temperature('fahrenheit')['temp'], 1) @@ -139,6 +137,8 @@ class OpenWeatherMapSensor(Entity): self._state = round(data.get_temperature()['temp'], 1) elif self.type == 'wind_speed': self._state = round(data.get_wind()['speed'], 1) + elif self.type == 'wind_bearing': + self._state = round(data.get_wind()['deg'], 1) elif self.type == 'humidity': self._state = round(data.get_humidity(), 1) elif self.type == 'pressure': diff --git a/homeassistant/components/sensor/qnap.py b/homeassistant/components/sensor/qnap.py new file mode 100644 index 00000000000..51a83c9371b --- /dev/null +++ b/homeassistant/components/sensor/qnap.py @@ -0,0 +1,406 @@ +""" +Support for QNAP NAS Sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.qnap/ +""" + +import logging +from datetime import timedelta + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.helpers.entity import Entity +from homeassistant.const import ( + CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_SSL, + CONF_MONITORED_CONDITIONS, TEMP_CELSIUS) +from homeassistant.util import Throttle +import homeassistant.helpers.config_validation as cv + +import voluptuous as vol + +REQUIREMENTS = ['qnapstats==0.2.1'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_DRIVE = 'Drive' +ATTR_DRIVE_SIZE = 'Drive Size' +ATTR_IP = 'IP Address' +ATTR_MAC = 'MAC Address' +ATTR_MASK = 'Mask' +ATTR_MAX_SPEED = 'Max Speed' +ATTR_MEMORY_SIZE = 'Memory Size' +ATTR_MODEL = 'Model' +ATTR_NAME = 'Name' +ATTR_PACKETS_TX = 'Packets (TX)' +ATTR_PACKETS_RX = 'Packets (RX)' +ATTR_PACKETS_ERR = 'Packets (Err)' +ATTR_SERIAL = 'Serial #' +ATTR_TYPE = 'Type' +ATTR_UPTIME = 'Uptime' +ATTR_VOLUME_SIZE = 'Volume Size' + +CONF_DRIVES = 'drives' +CONF_NICS = 'nics' +CONF_VOLUMES = 'volumes' +DEFAULT_NAME = 'QNAP' +DEFAULT_PORT = 8080 + +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) + +_HEALTH_MON_COND = { + 'status': ['Status', None, 'mdi:checkbox-marked-circle-outline'], +} +_CPU_MON_COND = { + 'cpu_temp': ['CPU Temperature', TEMP_CELSIUS, 'mdi:thermometer'], + 'cpu_usage': ['CPU Usage', '%', 'mdi:chip'], +} +_MEMORY_MON_COND = { + 'memory_free': ['Memory Available', 'GB', 'mdi:memory'], + 'memory_used': ['Memory Used', 'GB', 'mdi:memory'], + 'memory_percent_used': ['Memory Usage', '%', 'mdi:memory'], +} +_NETWORK_MON_COND = { + 'network_link_status': ['Network Link', None, + 'mdi:checkbox-marked-circle-outline'], + 'network_tx': ['Network Up', 'MB/s', 'mdi:upload'], + 'network_rx': ['Network Down', 'MB/s', 'mdi:download'], +} +_DRIVE_MON_COND = { + 'drive_smart_status': ['SMART Status', None, + 'mdi:checkbox-marked-circle-outline'], + 'drive_temp': ['Temperature', TEMP_CELSIUS, 'mdi:thermometer'], +} +_VOLUME_MON_COND = { + 'volume_size_used': ['Used Space', 'GB', 'mdi:chart-pie'], + 'volume_size_free': ['Free Space', 'GB', 'mdi:chart-pie'], + 'volume_percentage_used': ['Volume Used', '%', 'mdi:chart-pie'], +} + +_MONITORED_CONDITIONS = list(_HEALTH_MON_COND.keys()) + \ + list(_CPU_MON_COND.keys()) + \ + list(_MEMORY_MON_COND.keys()) + \ + list(_NETWORK_MON_COND.keys()) + \ + list(_DRIVE_MON_COND.keys()) + \ + list(_VOLUME_MON_COND.keys()) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_SSL, default=False): cv.boolean, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_MONITORED_CONDITIONS): + vol.All(cv.ensure_list, [vol.In(_MONITORED_CONDITIONS)]), + vol.Optional(CONF_NICS, default=None): cv.ensure_list, + vol.Optional(CONF_DRIVES, default=None): cv.ensure_list, + vol.Optional(CONF_VOLUMES, default=None): cv.ensure_list, +}) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the QNAP NAS sensor.""" + api = QNAPStatsAPI(config) + api.update() + + sensors = [] + + # Basic sensors + for variable in config[CONF_MONITORED_CONDITIONS]: + if variable in _HEALTH_MON_COND: + sensors.append(QNAPHealthStatus(api, variable, + _HEALTH_MON_COND[variable])) + if variable in _CPU_MON_COND: + sensors.append(QNAPCPUSensor(api, variable, + _CPU_MON_COND[variable])) + if variable in _MEMORY_MON_COND: + sensors.append(QNAPMemorySensor(api, variable, + _MEMORY_MON_COND[variable])) + + # Network sensors + nics = config[CONF_NICS] + if nics is None: + nics = api.data["system_stats"]["nics"].keys() + + for nic in nics: + sensors += [QNAPNetworkSensor(api, variable, + _NETWORK_MON_COND[variable], nic) + for variable in config[CONF_MONITORED_CONDITIONS] + if variable in _NETWORK_MON_COND] + + # Drive sensors + drives = config[CONF_DRIVES] + if drives is None: + drives = api.data["smart_drive_health"].keys() + + for drive in drives: + sensors += [QNAPDriveSensor(api, variable, + _DRIVE_MON_COND[variable], drive) + for variable in config[CONF_MONITORED_CONDITIONS] + if variable in _DRIVE_MON_COND] + + # Volume sensors + volumes = config[CONF_VOLUMES] + if volumes is None: + volumes = api.data["volumes"].keys() + + for volume in volumes: + sensors += [QNAPVolumeSensor(api, variable, + _VOLUME_MON_COND[variable], volume) + for variable in config[CONF_MONITORED_CONDITIONS] + if variable in _VOLUME_MON_COND] + + add_devices(sensors) + + +def round_nicely(number): + """Round a number based on its size (so it looks nice).""" + if number < 10: + return round(number, 2) + if number < 100: + return round(number, 1) + + return round(number) + + +class QNAPStatsAPI(object): + """Class to interface with the API.""" + + def __init__(self, config): + """Initialize the API wrapper.""" + from qnapstats import QNAPStats + + protocol = "https" if config.get(CONF_SSL) else "http" + self._api = QNAPStats( + protocol + "://" + config.get(CONF_HOST), + config.get(CONF_PORT), + config.get(CONF_USERNAME), + config.get(CONF_PASSWORD)) + + self.data = {} + + # pylint: disable=bare-except + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Update API information and store locally.""" + try: + self.data["system_stats"] = self._api.get_system_stats() + self.data["system_health"] = self._api.get_system_health() + self.data["smart_drive_health"] = self._api.get_smart_disk_health() + self.data["volumes"] = self._api.get_volumes() + self.data["bandwidth"] = self._api.get_bandwidth() + except: + _LOGGER.exception("Failed to fetch QNAP stats from the NAS.") + + +class QNAPSensor(Entity): + """Base class for a QNAP sensor.""" + + def __init__(self, api, variable, variable_info, monitor_device=None): + """Initialize the sensor.""" + self.var_id = variable + self.var_name = variable_info[0] + self.var_units = variable_info[1] + self.var_icon = variable_info[2] + self.monitor_device = monitor_device + self._api = api + + @property + def name(self): + """Return the name of the sensor, if any.""" + server_name = self._api.data["system_stats"]["system"]["name"] + + if self.monitor_device is not None: + return "{} {} ({})".format(server_name, + self.var_name, + self.monitor_device) + else: + return "{} {}".format(server_name, + self.var_name) + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self.var_icon + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self.var_units + + def update(self): + """Get the latest data for the states.""" + self._api.update() + + +class QNAPCPUSensor(QNAPSensor): + """A QNAP sensor that monitors CPU stats.""" + + @property + def state(self): + """Return the state of the sensor.""" + if self.var_id == "cpu_temp": + return self._api.data["system_stats"]["cpu"]["temp_c"] + elif self.var_id == "cpu_usage": + return self._api.data["system_stats"]["cpu"]["usage_percent"] + + +class QNAPMemorySensor(QNAPSensor): + """A QNAP sensor that monitors memory stats.""" + + @property + def state(self): + """Return the state of the sensor.""" + free = float(self._api.data["system_stats"]["memory"]["free"]) / 1024 + if self.var_id == "memory_free": + return round_nicely(free) + + total = float(self._api.data["system_stats"]["memory"]["total"]) / 1024 + + used = total - free + if self.var_id == "memory_used": + return round_nicely(used) + + if self.var_id == "memory_percent_used": + return round(used / total * 100) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self._api.data: + data = self._api.data["system_stats"]["memory"] + size = round_nicely(float(data["total"]) / 1024) + return { + ATTR_MEMORY_SIZE: "{} GB".format(size), + } + + +class QNAPNetworkSensor(QNAPSensor): + """A QNAP sensor that monitors network stats.""" + + @property + def state(self): + """Return the state of the sensor.""" + if self.var_id == "network_link_status": + nic = self._api.data["system_stats"]["nics"][self.monitor_device] + return nic["link_status"] + + data = self._api.data["bandwidth"][self.monitor_device] + if self.var_id == "network_tx": + return round_nicely(data["tx"] / 1024 / 1024) + + if self.var_id == "network_rx": + return round_nicely(data["rx"] / 1024 / 1024) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self._api.data: + data = self._api.data["system_stats"]["nics"][self.monitor_device] + return { + ATTR_IP: data["ip"], + ATTR_MASK: data["mask"], + ATTR_MAC: data["mac"], + ATTR_MAX_SPEED: data["max_speed"], + ATTR_PACKETS_TX: data["tx_packets"], + ATTR_PACKETS_RX: data["rx_packets"], + ATTR_PACKETS_ERR: data["err_packets"] + } + + +class QNAPHealthStatus(QNAPSensor): + """A QNAP sensor that monitors overall system health.""" + + @property + def state(self): + """Return the state of the sensor.""" + return self._api.data["system_health"] + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self._api.data: + data = self._api.data["system_stats"] + days = int(data["uptime"]["days"]) + hours = int(data["uptime"]["hours"]) + minutes = int(data["uptime"]["minutes"]) + + return { + ATTR_NAME: data["system"]["name"], + ATTR_MODEL: data["system"]["model"], + ATTR_SERIAL: data["system"]["serial_number"], + ATTR_UPTIME: "{:0>2d}d {:0>2d}h {:0>2d}m".format(days, + hours, + minutes) + } + + +class QNAPDriveSensor(QNAPSensor): + """A QNAP sensor that monitors HDD/SSD drive stats.""" + + @property + def state(self): + """Return the state of the sensor.""" + data = self._api.data["smart_drive_health"][self.monitor_device] + + if self.var_id == "drive_smart_status": + return data["health"] + + if self.var_id == "drive_temp": + return int(data["temp_c"]) + + @property + def name(self): + """Return the name of the sensor, if any.""" + server_name = self._api.data["system_stats"]["system"]["name"] + + return "{} {} (Drive {})".format( + server_name, + self.var_name, + self.monitor_device + ) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self._api.data: + data = self._api.data["smart_drive_health"][self.monitor_device] + return { + ATTR_DRIVE: data["drive_number"], + ATTR_MODEL: data["model"], + ATTR_SERIAL: data["serial"], + ATTR_TYPE: data["type"], + } + + +class QNAPVolumeSensor(QNAPSensor): + """A QNAP sensor that monitors storage volume stats.""" + + @property + def state(self): + """Return the state of the sensor.""" + data = self._api.data["volumes"][self.monitor_device] + + free_gb = int(data["free_size"]) / 1024 / 1024 / 1024 + if self.var_id == "volume_size_free": + return round_nicely(free_gb) + + total_gb = int(data["total_size"]) / 1024 / 1024 / 1024 + + used_gb = total_gb - free_gb + if self.var_id == "volume_size_used": + return round_nicely(used_gb) + + if self.var_id == "volume_percentage_used": + return round(used_gb / total_gb * 100) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self._api.data: + data = self._api.data["volumes"][self.monitor_device] + total_gb = int(data["total_size"]) / 1024 / 1024 / 1024 + + return { + ATTR_VOLUME_SIZE: "{} GB".format(round_nicely(total_gb)), + } diff --git a/homeassistant/components/sensor/rflink.py b/homeassistant/components/sensor/rflink.py new file mode 100644 index 00000000000..9f6496149b3 --- /dev/null +++ b/homeassistant/components/sensor/rflink.py @@ -0,0 +1,141 @@ +"""Support for Rflink sensors. + +For more details about this platform, please refer to the documentation +at https://home-assistant.io/components/light.rflink/ + +""" +import asyncio +from functools import partial +import logging + +from homeassistant.components import group +from homeassistant.components.rflink import ( + CONF_ALIASSES, CONF_DEVICES, CONF_NEW_DEVICES_GROUP, DATA_DEVICE_REGISTER, + DATA_ENTITY_LOOKUP, DOMAIN, EVENT_KEY_ID, EVENT_KEY_SENSOR, EVENT_KEY_UNIT, + RflinkDevice, cv, vol) +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, CONF_PLATFORM, + CONF_UNIT_OF_MEASUREMENT) + +DEPENDENCIES = ['rflink'] + +_LOGGER = logging.getLogger(__name__) + +SENSOR_ICONS = { + 'humidity': 'mdi:water-percent', + 'battery': 'mdi:battery', + 'temperature': 'mdi:thermometer', +} + +CONF_SENSOR_TYPE = 'sensor_type' + +PLATFORM_SCHEMA = vol.Schema({ + vol.Required(CONF_PLATFORM): DOMAIN, + vol.Optional(CONF_NEW_DEVICES_GROUP, default=None): cv.string, + vol.Optional(CONF_DEVICES, default={}): vol.Schema({ + cv.string: { + vol.Optional(CONF_NAME): cv.string, + vol.Required(CONF_SENSOR_TYPE): cv.string, + vol.Optional(CONF_UNIT_OF_MEASUREMENT, default=None): cv.string, + vol.Optional(CONF_ALIASSES, default=[]): + vol.All(cv.ensure_list, [cv.string]), + }, + }), +}) + + +def lookup_unit_for_sensor_type(sensor_type): + """Get unit for sensor type. + + Async friendly. + + """ + from rflink.parser import UNITS, PACKET_FIELDS + field_abbrev = {v: k for k, v in PACKET_FIELDS.items()} + + return UNITS.get(field_abbrev.get(sensor_type)) + + +def devices_from_config(domain_config, hass=None): + """Parse config and add rflink sensor devices.""" + devices = [] + for device_id, config in domain_config[CONF_DEVICES].items(): + if not config[ATTR_UNIT_OF_MEASUREMENT]: + config[ATTR_UNIT_OF_MEASUREMENT] = lookup_unit_for_sensor_type( + config[CONF_SENSOR_TYPE]) + device = RflinkSensor(device_id, hass, **config) + devices.append(device) + + # register entity to listen to incoming rflink events + hass.data[DATA_ENTITY_LOOKUP][ + EVENT_KEY_SENSOR][device_id].append(device) + return devices + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Setup the Rflink platform.""" + # add devices from config + yield from async_add_devices(devices_from_config(config, hass)) + + # add new (unconfigured) devices to user desired group + if config[CONF_NEW_DEVICES_GROUP]: + new_devices_group = yield from group.Group.async_create_group( + hass, config[CONF_NEW_DEVICES_GROUP], [], True) + else: + new_devices_group = None + + @asyncio.coroutine + def add_new_device(event): + """Check if device is known, otherwise create device entity.""" + device_id = event[EVENT_KEY_ID] + + rflinksensor = partial(RflinkSensor, device_id, hass) + device = rflinksensor(event[EVENT_KEY_SENSOR], event[EVENT_KEY_UNIT]) + # add device entity + yield from async_add_devices([device]) + + # register entity to listen to incoming rflink events + hass.data[DATA_ENTITY_LOOKUP][ + EVENT_KEY_SENSOR][device_id].append(device) + + # make sure the event is processed by the new entity + device.handle_event(event) + + # maybe add to new devices group + if new_devices_group: + yield from new_devices_group.async_update_tracked_entity_ids( + list(new_devices_group.tracking) + [device.entity_id]) + + hass.data[DATA_DEVICE_REGISTER][EVENT_KEY_SENSOR] = add_new_device + + +class RflinkSensor(RflinkDevice): + """Representation of a Rflink sensor.""" + + def __init__(self, device_id, hass, sensor_type, + unit_of_measurement, **kwargs): + """Handle sensor specific args and super init.""" + self._sensor_type = sensor_type + self._unit_of_measurement = unit_of_measurement + super().__init__(device_id, hass, **kwargs) + + def _handle_event(self, event): + """Domain specific event handler.""" + self._state = event['value'] + + @property + def unit_of_measurement(self): + """Return measurement unit.""" + return self._unit_of_measurement + + @property + def state(self): + """Return value.""" + return self._state + + @property + def icon(self): + """Return possible sensor specific icon.""" + if self._sensor_type in SENSOR_ICONS: + return SENSOR_ICONS[self._sensor_type] diff --git a/homeassistant/components/sensor/sma.py b/homeassistant/components/sensor/sma.py index fc074a8defe..bbd0d92018f 100644 --- a/homeassistant/components/sensor/sma.py +++ b/homeassistant/components/sensor/sma.py @@ -54,7 +54,8 @@ PLATFORM_SCHEMA = vol.All(PLATFORM_SCHEMA.extend({ vol.Optional(CONF_CUSTOM, default={}): vol.Schema({ cv.slug: { vol.Required('key'): vol.All(str, vol.Length(min=13, max=13)), - vol.Required('unit'): str + vol.Required('unit'): str, + vol.Optional('factor', default=1): vol.Coerce(float), }}) }, extra=vol.PREVENT_EXTRA), _check_sensor_schema) @@ -63,16 +64,18 @@ def async_setup_platform(hass, config, add_devices, discovery_info=None): """Set up SMA WebConnect sensor.""" import pysma - # Combine sensor_defs from the library and custom config + # sensor_defs from the library sensor_defs = dict(zip(SENSOR_OPTIONS, [ - (pysma.KEY_CURRENT_CONSUMPTION_W, 'W'), - (pysma.KEY_CURRENT_POWER_W, 'W'), - (pysma.KEY_TOTAL_CONSUMPTION_KWH, 'kW/h'), - (pysma.KEY_TOTAL_YIELD_KWH, 'kW/h')])) + (pysma.KEY_CURRENT_CONSUMPTION_W, 'W', 1), + (pysma.KEY_CURRENT_POWER_W, 'W', 1), + (pysma.KEY_TOTAL_CONSUMPTION_KWH, 'kWh', 1000), + (pysma.KEY_TOTAL_YIELD_KWH, 'kWh', 1000)])) + + # sensor_defs from the custom config for name, prop in config[CONF_CUSTOM].items(): if name in sensor_defs: _LOGGER.warning("Custom sensor %s replace built-in sensor", name) - sensor_defs[name] = (prop['key'], prop['unit']) + sensor_defs[name] = (prop['key'], prop['unit'], prop['factor']) # Prepare all HASS sensor entities hass_sensors = [] @@ -121,7 +124,9 @@ def async_setup_platform(hass, config, add_devices, discovery_info=None): if values is None: backoff = 3 return + values = [0 if val is None else val for val in values] res = dict(zip(names_to_query, values)) + res = {key: val // sensor_defs[key][2] for key, val in res.items()} _LOGGER.debug("Update sensors %s %s %s", keys_to_query, values, res) tasks = [] for sensor in hass_sensors: @@ -141,7 +146,7 @@ class SMAsensor(Entity): def __init__(self, sensor_name, attr, sensor_defs): """Initialize the sensor.""" self._name = sensor_name - self._key, self._unit_of_measurement = sensor_defs[sensor_name] + self._key, self._unit_of_measurement, _ = sensor_defs[sensor_name] self._state = None self._sensor_defs = sensor_defs self._attr = {att: "" for att in attr} @@ -176,10 +181,10 @@ class SMAsensor(Entity): update = False for key, val in self._attr.items(): - if val.partition(' ')[0] != key_values[key]: + newval = '{} {}'.format(key_values[key], self._sensor_defs[key][1]) + if val != newval: update = True - self._attr[key] = '{} {}'.format(key_values[key], - self._sensor_defs[key][1]) + self._attr[key] = newval new_state = key_values[self._name] if new_state != self._state: diff --git a/homeassistant/components/sensor/snmp.py b/homeassistant/components/sensor/snmp.py index b5e3b16b5d1..6a991f28898 100644 --- a/homeassistant/components/sensor/snmp.py +++ b/homeassistant/components/sensor/snmp.py @@ -16,7 +16,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle -REQUIREMENTS = ['pysnmp==4.3.2'] +REQUIREMENTS = ['pysnmp==4.3.3'] _LOGGER = logging.getLogger(__name__) @@ -41,7 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the SNMP sensor.""" + """Set up the SNMP sensor.""" from pysnmp.hlapi import (getCmd, CommunityData, SnmpEngine, UdpTransportTarget, ContextData, ObjectType, ObjectIdentity) @@ -61,7 +61,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ObjectType(ObjectIdentity(baseoid)))) if errindication: - _LOGGER.error('Please check the details in the configuration file') + _LOGGER.error("Please check the details in the configuration file") return False else: data = SnmpData(host, port, community, baseoid) @@ -128,7 +128,7 @@ class SnmpData(object): if errindication: _LOGGER.error("SNMP error: %s", errindication) elif errstatus: - _LOGGER.error('SNMP error: %s at %s', errstatus.prettyPrint(), + _LOGGER.error("SNMP error: %s at %s", errstatus.prettyPrint(), errindex and restable[-1][int(errindex) - 1] or '?') else: for resrow in restable: diff --git a/homeassistant/components/sensor/statistics.py b/homeassistant/components/sensor/statistics.py index 15184fbbc11..ff2df5ef893 100644 --- a/homeassistant/components/sensor/statistics.py +++ b/homeassistant/components/sensor/statistics.py @@ -116,7 +116,7 @@ class StatisticsSensor(Entity): return False @property - def state_attributes(self): + def device_state_attributes(self): """Return the state attributes of the sensor.""" if not self.is_binary: return { diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index 4feb5ed3a59..da9c9457aa0 100755 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.systemmonitor/ """ import logging +import os import voluptuous as vol @@ -15,30 +16,33 @@ from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -REQUIREMENTS = ['psutil==5.0.1'] +REQUIREMENTS = ['psutil==5.1.2'] _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { - 'disk_use_percent': ['Disk Use', '%', 'mdi:harddisk'], - 'disk_use': ['Disk Use', 'GiB', 'mdi:harddisk'], 'disk_free': ['Disk Free', 'GiB', 'mdi:harddisk'], - 'memory_use_percent': ['RAM Use', '%', 'mdi:memory'], - 'memory_use': ['RAM Use', 'MiB', 'mdi:memory'], - 'memory_free': ['RAM Free', 'MiB', 'mdi:memory'], - 'processor_use': ['CPU Use', '%', 'mdi:memory'], - 'process': ['Process', ' ', 'mdi:memory'], - 'swap_use_percent': ['Swap Use', '%', 'mdi:harddisk'], - 'swap_use': ['Swap Use', 'GiB', 'mdi:harddisk'], - 'swap_free': ['Swap Free', 'GiB', 'mdi:harddisk'], - 'network_out': ['Sent', 'MiB', 'mdi:server-network'], - 'network_in': ['Received', 'MiB', 'mdi:server-network'], - 'packets_out': ['Packets sent', ' ', 'mdi:server-network'], - 'packets_in': ['Packets received', ' ', 'mdi:server-network'], + 'disk_use': ['Disk Use', 'GiB', 'mdi:harddisk'], + 'disk_use_percent': ['Disk Use', '%', 'mdi:harddisk'], 'ipv4_address': ['IPv4 address', '', 'mdi:server-network'], 'ipv6_address': ['IPv6 address', '', 'mdi:server-network'], 'last_boot': ['Last Boot', '', 'mdi:clock'], - 'since_last_boot': ['Since Last Boot', '', 'mdi:clock'] + 'load_15m': ['Average Load (15m)', '', 'mdi:memory'], + 'load_1m': ['Average Load (1m)', '', 'mdi:memory'], + 'load_5m': ['Average Load (5m)', '', 'mdi:memory'], + 'memory_free': ['RAM Free', 'MiB', 'mdi:memory'], + 'memory_use': ['RAM Use', 'MiB', 'mdi:memory'], + 'memory_use_percent': ['RAM Use', '%', 'mdi:memory'], + 'network_in': ['Received', 'MiB', 'mdi:server-network'], + 'network_out': ['Sent', 'MiB', 'mdi:server-network'], + 'packets_in': ['Packets received', ' ', 'mdi:server-network'], + 'packets_out': ['Packets sent', ' ', 'mdi:server-network'], + 'process': ['Process', ' ', 'mdi:memory'], + 'processor_use': ['CPU Use', '%', 'mdi:memory'], + 'since_last_boot': ['Since Last Boot', '', 'mdi:clock'], + 'swap_free': ['Swap Free', 'GiB', 'mdi:harddisk'], + 'swap_use': ['Swap Use', 'GiB', 'mdi:harddisk'], + 'swap_use_percent': ['Swap Use', '%', 'mdi:harddisk'], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -164,3 +168,9 @@ class SystemMonitorSensor(Entity): elif self.type == 'since_last_boot': self._state = dt_util.utcnow() - dt_util.utc_from_timestamp( psutil.boot_time()) + elif self.type == 'load_1m': + self._state = os.getloadavg()[0] + elif self.type == 'load_5m': + self._state = os.getloadavg()[1] + elif self.type == 'load_15m': + self._state = os.getloadavg()[2] diff --git a/homeassistant/components/sensor/tellduslive.py b/homeassistant/components/sensor/tellduslive.py index b7f3cf60892..9bebbe6e3dc 100644 --- a/homeassistant/components/sensor/tellduslive.py +++ b/homeassistant/components/sensor/tellduslive.py @@ -85,7 +85,9 @@ class TelldusLiveSensor(TelldusLiveEntity): @property def state(self): """Return the state of the sensor.""" - if self._type == SENSOR_TYPE_TEMP: + if not self.available: + return None + elif self._type == SENSOR_TYPE_TEMP: return self._value_as_temperature elif self._type == SENSOR_TYPE_HUMIDITY: return self._value_as_humidity diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py index 44c6d2e08ea..aba42519e60 100644 --- a/homeassistant/components/sensor/template.py +++ b/homeassistant/components/sensor/template.py @@ -21,8 +21,11 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) +CONF_ICON_TEMPLATE = 'icon_template' + SENSOR_SCHEMA = vol.Schema({ vol.Required(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_ICON_TEMPLATE): cv.template, vol.Optional(ATTR_FRIENDLY_NAME): cv.string, vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids @@ -41,6 +44,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): for device, device_config in config[CONF_SENSORS].items(): state_template = device_config[CONF_VALUE_TEMPLATE] + icon_template = device_config.get(CONF_ICON_TEMPLATE) entity_ids = (device_config.get(ATTR_ENTITY_ID) or state_template.extract_entities()) friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device) @@ -48,6 +52,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): state_template.hass = hass + if icon_template is not None: + icon_template.hass = hass + sensors.append( SensorTemplate( hass, @@ -55,6 +62,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): friendly_name, unit_of_measurement, state_template, + icon_template, entity_ids) ) if not sensors: @@ -69,7 +77,7 @@ class SensorTemplate(Entity): """Representation of a Template Sensor.""" def __init__(self, hass, device_id, friendly_name, unit_of_measurement, - state_template, entity_ids): + state_template, icon_template, entity_ids): """Initialize the sensor.""" self.hass = hass self.entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, device_id, @@ -78,6 +86,8 @@ class SensorTemplate(Entity): self._unit_of_measurement = unit_of_measurement self._template = state_template self._state = None + self._icon_template = icon_template + self._icon = None @callback def template_sensor_state_listener(entity, old_state, new_state): @@ -97,6 +107,11 @@ class SensorTemplate(Entity): """Return the state of the sensor.""" return self._state + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return self._icon + @property def unit_of_measurement(self): """Return the unit_of_measurement of the device.""" @@ -116,7 +131,22 @@ class SensorTemplate(Entity): if ex.args and ex.args[0].startswith( "UndefinedError: 'None' has no attribute"): # Common during HA startup - so just a warning - _LOGGER.warning(ex) + _LOGGER.warning('Could not render template %s,' + ' the state is unknown.', self._name) return self._state = None - _LOGGER.error(ex) + _LOGGER.error('Could not render template %s: %s', self._name, ex) + + if self._icon_template is not None: + try: + self._icon = self._icon_template.async_render() + except TemplateError as ex: + if ex.args and ex.args[0].startswith( + "UndefinedError: 'None' has no attribute"): + # Common during HA startup - so just a warning + _LOGGER.warning('Could not render icon template %s,' + ' the state is unknown.', self._name) + return + self._icon = super().icon + _LOGGER.error('Could not render icon template %s: %s', + self._name, ex) diff --git a/homeassistant/components/sensor/waqi.py b/homeassistant/components/sensor/waqi.py index 73de98c0168..699b617b7ee 100644 --- a/homeassistant/components/sensor/waqi.py +++ b/homeassistant/components/sensor/waqi.py @@ -16,7 +16,7 @@ from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['pwaqi==1.3'] +REQUIREMENTS = ['pwaqi==1.4'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/wink.py b/homeassistant/components/sensor/wink.py index b43952e6330..27cfbd691ad 100644 --- a/homeassistant/components/sensor/wink.py +++ b/homeassistant/components/sensor/wink.py @@ -8,10 +8,10 @@ import logging from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.entity import Entity -from homeassistant.components.wink import WinkDevice -from homeassistant.loader import get_component +from homeassistant.components.wink import WinkDevice, DOMAIN DEPENDENCIES = ['wink'] +_LOGGER = logging.getLogger(__name__) SENSOR_TYPES = ['temperature', 'humidity', 'balance', 'proximity'] @@ -21,18 +21,29 @@ def setup_platform(hass, config, add_devices, discovery_info=None): import pywink for sensor in pywink.get_sensors(): - if sensor.capability() in SENSOR_TYPES: - add_devices([WinkSensorDevice(sensor, hass)]) + _id = sensor.object_id() + sensor.name() + if _id not in hass.data[DOMAIN]['unique_ids']: + if sensor.capability() in SENSOR_TYPES: + add_devices([WinkSensorDevice(sensor, hass)]) for eggtray in pywink.get_eggtrays(): - add_devices([WinkSensorDevice(eggtray, hass)]) + _id = eggtray.object_id() + eggtray.name() + if _id not in hass.data[DOMAIN]['unique_ids']: + add_devices([WinkSensorDevice(eggtray, hass)]) + + for tank in pywink.get_propane_tanks(): + _id = tank.object_id() + tank.name() + if _id not in hass.data[DOMAIN]['unique_ids']: + add_devices([WinkSensorDevice(tank, hass)]) for piggy_bank in pywink.get_piggy_banks(): - try: - if piggy_bank.capability() in SENSOR_TYPES: - add_devices([WinkSensorDevice(piggy_bank, hass)]) - except AttributeError: - logging.getLogger(__name__).info("Device is not a sensor") + _id = piggy_bank.object_id() + piggy_bank.name() + if _id not in hass.data[DOMAIN]['unique_ids']: + try: + if piggy_bank.capability() in SENSOR_TYPES: + add_devices([WinkSensorDevice(piggy_bank, hass)]) + except AttributeError: + _LOGGER.info("Device is not a sensor") class WinkSensorDevice(WinkDevice, Entity): @@ -41,7 +52,6 @@ class WinkSensorDevice(WinkDevice, Entity): def __init__(self, wink, hass): """Initialize the Wink device.""" super().__init__(wink, hass) - wink = get_component('wink') self.capability = self.wink.capability() if self.wink.unit() == '°': self._unit_of_measurement = TEMP_CELSIUS diff --git a/homeassistant/components/sensor/zamg.py b/homeassistant/components/sensor/zamg.py index 6f621b683b6..6b500460d7b 100644 --- a/homeassistant/components/sensor/zamg.py +++ b/homeassistant/components/sensor/zamg.py @@ -112,7 +112,7 @@ class ZamgSensor(Entity): return SENSOR_TYPES[self.variable][1] @property - def state_attributes(self): + def device_state_attributes(self): """Return the state attributes.""" return { ATTR_WEATHER_ATTRIBUTION: ATTRIBUTION, diff --git a/homeassistant/components/services.yaml b/homeassistant/components/services.yaml index 9ab64c89571..661f8be8dab 100644 --- a/homeassistant/components/services.yaml +++ b/homeassistant/components/services.yaml @@ -211,6 +211,31 @@ verisure: description: The serial number of the smartcam you want to capture an image from. example: '2DEU AT5Z' +alert: + turn_off: + description: Silence alert's notifications. + + fields: + entity_id: + description: Name of the alert to silence. + example: 'alert.garage_door_open' + + turn_on: + description: Reset alert's notifications. + + fields: + entity_id: + description: Name of the alert to reset. + example: 'alert.garage_door_open' + + toggle: + description: Toggle alert's notifications. + + fields: + entity_id: + description: Name of the alert to toggle. + example: 'alert.garage_door_open' + hdmi_cec: send_command: description: Sends CEC command into HDMI CEC capable adapter. @@ -265,3 +290,25 @@ hdmi_cec: standby: description: Standby all devices which supports it. + +ffmpeg: + start: + description: Send a start command to a ffmpeg based sensor. + fields: + entity_id: + description: Name(s) of entites that will start. Platform dependent. + example: 'binary_sensor.ffmpeg_noise' + + stop: + description: Send a stop command to a ffmpeg based sensor. + fields: + entity_id: + description: Name(s) of entites that will stop. Platform dependent. + example: 'binary_sensor.ffmpeg_noise' + + restart: + description: Send a restart command to a ffmpeg based sensor. + fields: + entity_id: + description: Name(s) of entites that will restart. Platform dependent. + example: 'binary_sensor.ffmpeg_noise' diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py index 92349be6376..00a9370a446 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -17,7 +17,7 @@ from homeassistant.util import dt as dt_util import homeassistant.helpers.config_validation as cv import homeassistant.util as util -REQUIREMENTS = ['astral==1.3.3'] +REQUIREMENTS = ['astral==1.3.4'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 56ad5ea8966..a5712fcbcbe 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -11,6 +11,7 @@ import os import voluptuous as vol +from homeassistant.core import callback from homeassistant.config import load_yaml_config_file from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import ToggleEntity @@ -20,6 +21,7 @@ from homeassistant.const import ( STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID) from homeassistant.components import group +from homeassistant.util.async import run_callback_threadsafe DOMAIN = 'switch' SCAN_INTERVAL = timedelta(seconds=30) @@ -47,21 +49,39 @@ _LOGGER = logging.getLogger(__name__) def is_on(hass, entity_id=None): - """Return if the switch is on based on the statemachine.""" + """Return if the switch is on based on the statemachine. + + Async friendly. + """ entity_id = entity_id or ENTITY_ID_ALL_SWITCHES return hass.states.is_state(entity_id, STATE_ON) def turn_on(hass, entity_id=None): + """Turn all or specified switch on.""" + run_callback_threadsafe( + hass.loop, async_turn_on, hass, entity_id).result() + + +@callback +def async_turn_on(hass, entity_id=None): """Turn all or specified switch on.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_TURN_ON, data) + hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data)) def turn_off(hass, entity_id=None): + """Turn all or specified switch off.""" + run_callback_threadsafe( + hass.loop, async_turn_off, hass, entity_id).result() + + +@callback +def async_turn_off(hass, entity_id=None): """Turn all or specified switch off.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) + hass.async_add_job( + hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data)) def toggle(hass, entity_id=None): diff --git a/homeassistant/components/switch/broadlink.py b/homeassistant/components/switch/broadlink.py index 7c561a3eb1f..52a5c1e4e80 100644 --- a/homeassistant/components/switch/broadlink.py +++ b/homeassistant/components/switch/broadlink.py @@ -28,7 +28,9 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "broadlink" DEFAULT_NAME = 'Broadlink switch' DEFAULT_TIMEOUT = 10 +DEFAULT_RETRY = 3 SERVICE_LEARN = "learn_command" +SERVICE_SEND = "send_packet" RM_TYPES = ["rm", "rm2", "rm_mini", "rm_pro_phicomm", "rm2_home_plus", "rm2_home_plus_gdt", "rm2_pro_plus", "rm2_pro_plus2", @@ -101,10 +103,30 @@ def setup_platform(hass, config, add_devices, discovery_info=None): "Did not received any signal", title='Broadlink switch') + @asyncio.coroutine + def _send_packet(call): + packets = call.data.get('packet', []) + for packet in packets: + for retry in range(DEFAULT_RETRY): + try: + payload = b64decode(packet) + yield from hass.loop.run_in_executor( + None, broadlink_device.send_data, payload) + break + except (socket.timeout, ValueError): + try: + yield from hass.loop.run_in_executor( + None, broadlink_device.auth) + except socket.timeout: + if retry == DEFAULT_RETRY-1: + _LOGGER.error("Failed to send packet to device.") + if switch_type in RM_TYPES: broadlink_device = broadlink.rm((ip_addr, 80), mac_addr) - hass.services.register(DOMAIN, SERVICE_LEARN + '_' + ip_addr, - _learn_command) + hass.services.register(DOMAIN, SERVICE_LEARN + '_' + + ip_addr.replace('.', '_'), _learn_command) + hass.services.register(DOMAIN, SERVICE_SEND + '_' + + ip_addr.replace('.', '_'), _send_packet) switches = [] for object_id, device_config in devices.items(): switches.append( diff --git a/homeassistant/components/switch/dlink.py b/homeassistant/components/switch/dlink.py index 3e1f7db3ddb..11aff81a0d5 100644 --- a/homeassistant/components/switch/dlink.py +++ b/homeassistant/components/switch/dlink.py @@ -15,7 +15,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN REQUIREMENTS = ['https://github.com/LinuxChristian/pyW215/archive/' - 'v0.3.7.zip#pyW215==0.3.7'] + 'v0.4.zip#pyW215==0.4'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/fritzdect.py b/homeassistant/components/switch/fritzdect.py new file mode 100644 index 00000000000..fc185c9f6a3 --- /dev/null +++ b/homeassistant/components/switch/fritzdect.py @@ -0,0 +1,165 @@ +""" +Support for FRITZ!DECT Switches. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.fritzdect/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) +from homeassistant.const import ( + CONF_HOST, CONF_PASSWORD, CONF_USERNAME) +import homeassistant.helpers.config_validation as cv +from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN + +REQUIREMENTS = ['fritzhome==1.0.2'] + +_LOGGER = logging.getLogger(__name__) + +# Standard Fritz Box IP +DEFAULT_HOST = 'fritz.box' + +ATTR_CURRENT_CONSUMPTION = 'Current Consumption' +ATTR_CURRENT_CONSUMPTION_UNIT = 'W' + +ATTR_TOTAL_CONSUMPTION = 'Total Consumption' +ATTR_TOTAL_CONSUMPTION_UNIT = 'kWh' + +ATTR_TEMPERATURE = 'Temperature' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Add all switches connected to Fritz Box.""" + from fritzhome.fritz import FritzBox + + host = config.get(CONF_HOST) + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + + # Hack: fritzhome only throws Exception. To prevent pylint from + # complaining, we disable the warning here: + # pylint: disable=W0703 + + # Log into Fritz Box + fritz = FritzBox(host, username, password) + try: + fritz.login() + except Exception: + _LOGGER.error("Login to Fritz!Box failed") + return + + # Add all actors to hass + for actor in fritz.get_actors(): + # Only add devices that support switching + if actor.has_switch: + data = FritzDectSwitchData(fritz, actor.actor_id) + add_devices([FritzDectSwitch(hass, data, actor.name)], True) + + +class FritzDectSwitch(SwitchDevice): + """Representation of a FRITZ!DECT switch.""" + + def __init__(self, hass, data, name): + """Initialize the switch.""" + self.units = hass.config.units + self.data = data + self._name = name + + @property + def name(self): + """Return the name of the FRITZ!DECT switch, if any.""" + return self._name + + @property + def device_state_attributes(self): + """Return the state attributes of the device.""" + attrs = {} + + if self.data.has_powermeter and \ + self.data.current_consumption != STATE_UNKNOWN and \ + self.data.total_consumption != STATE_UNKNOWN: + attrs[ATTR_CURRENT_CONSUMPTION] = "%.1f %s" % \ + (self.data.current_consumption, ATTR_CURRENT_CONSUMPTION_UNIT) + attrs[ATTR_TOTAL_CONSUMPTION] = "%.3f %s" % \ + (self.data.total_consumption, ATTR_TOTAL_CONSUMPTION_UNIT) + + if self.data.has_temperature and \ + self.data.temperature != STATE_UNKNOWN: + attrs[ATTR_TEMPERATURE] = "%.1f %s" % \ + (self.units.temperature(self.data.temperature, TEMP_CELSIUS), + self.units.temperature_unit) + + return attrs + + @property + def current_power_watt(self): + """Return the current power usage in Watt.""" + try: + return float(self.data.current_consumption) + except ValueError: + return None + + @property + def is_on(self): + """Return true if switch is on.""" + return self.data.state + + def turn_on(self, **kwargs): + """Turn the switch on.""" + actor = self.data.fritz.get_actor_by_ain(self.data.ain) + actor.switch_on() + + def turn_off(self): + """Turn the switch off.""" + actor = self.data.fritz.get_actor_by_ain(self.data.ain) + actor.switch_off() + + def update(self): + """Get the latest data from the fritz box and updates the states.""" + self.data.update() + + +class FritzDectSwitchData(object): + """Get the latest data from the fritz box.""" + + def __init__(self, fritz, ain): + """Initialize the data object.""" + self.fritz = fritz + self.ain = ain + self.state = STATE_UNKNOWN + self.temperature = STATE_UNKNOWN + self.current_consumption = STATE_UNKNOWN + self.total_consumption = STATE_UNKNOWN + self.has_switch = STATE_UNKNOWN + self.has_temperature = STATE_UNKNOWN + self.has_powermeter = STATE_UNKNOWN + + def update(self): + """Get the latest data from the fritz box.""" + from requests.exceptions import RequestException + + try: + actor = self.fritz.get_actor_by_ain(self.ain) + self.state = actor.get_state() + except RequestException: + _LOGGER.error("Request to actor failed") + self.state = STATE_UNKNOWN + self.temperature = STATE_UNKNOWN + self.current_consumption = STATE_UNKNOWN + self.total_consumption = STATE_UNKNOWN + return + + self.temperature = actor.temperature + self.current_consumption = (actor.get_power() or 0.0) / 1000 + self.total_consumption = (actor.get_energy() or 0.0) / 100000 + self.has_switch = actor.has_switch + self.has_temperature = actor.has_temperature + self.has_powermeter = actor.has_powermeter diff --git a/homeassistant/components/switch/rflink.py b/homeassistant/components/switch/rflink.py new file mode 100644 index 00000000000..aa61987c3dd --- /dev/null +++ b/homeassistant/components/switch/rflink.py @@ -0,0 +1,62 @@ +"""Support for Rflink switches. + +For more details about this platform, please refer to the documentation +at https://home-assistant.io/components/switch.rflink/ + +""" +import asyncio +import logging + +from homeassistant.components.rflink import ( + CONF_ALIASSES, CONF_DEVICE_DEFAULTS, CONF_DEVICES, CONF_FIRE_EVENT, + CONF_SIGNAL_REPETITIONS, DATA_ENTITY_LOOKUP, DEVICE_DEFAULTS_SCHEMA, + DOMAIN, EVENT_KEY_COMMAND, SwitchableRflinkDevice, cv, vol) +from homeassistant.components.switch import SwitchDevice +from homeassistant.const import CONF_NAME, CONF_PLATFORM + +DEPENDENCIES = ['rflink'] + +_LOGGER = logging.getLogger(__name__) + + +PLATFORM_SCHEMA = vol.Schema({ + vol.Required(CONF_PLATFORM): DOMAIN, + 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_ALIASSES, 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), + }, + }), +}) + + +def devices_from_config(domain_config, hass=None): + """Parse config and add rflink switch devices.""" + devices = [] + for device_id, config in domain_config[CONF_DEVICES].items(): + device_config = dict(domain_config[CONF_DEVICE_DEFAULTS], **config) + device = RflinkSwitch(device_id, hass, **device_config) + devices.append(device) + + # register entity (and aliasses) to listen to incoming rflink events + for _id in config[CONF_ALIASSES] + [device_id]: + hass.data[DATA_ENTITY_LOOKUP][ + EVENT_KEY_COMMAND][_id].append(device) + return devices + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Setup the Rflink platform.""" + yield from async_add_devices(devices_from_config(config, hass)) + + +class RflinkSwitch(SwitchableRflinkDevice, SwitchDevice): + """Representation of a Rflink switch.""" + + pass diff --git a/homeassistant/components/switch/rpi_rf.py b/homeassistant/components/switch/rpi_rf.py index 361f4e0e934..866fea0df0b 100644 --- a/homeassistant/components/switch/rpi_rf.py +++ b/homeassistant/components/switch/rpi_rf.py @@ -12,7 +12,7 @@ from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) from homeassistant.const import (CONF_NAME, CONF_SWITCHES) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['rpi-rf==0.9.5'] +REQUIREMENTS = ['rpi-rf==0.9.6'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/wink.py b/homeassistant/components/switch/wink.py index 5df37d87b53..1f534f7290f 100644 --- a/homeassistant/components/switch/wink.py +++ b/homeassistant/components/switch/wink.py @@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.wink/ """ -from homeassistant.components.wink import WinkDevice +from homeassistant.components.wink import WinkDevice, DOMAIN from homeassistant.helpers.entity import ToggleEntity DEPENDENCIES = ['wink'] @@ -16,13 +16,21 @@ def setup_platform(hass, config, add_devices, discovery_info=None): import pywink for switch in pywink.get_switches(): - add_devices([WinkToggleDevice(switch, hass)]) + _id = switch.object_id() + switch.name() + if _id not in hass.data[DOMAIN]['unique_ids']: + add_devices([WinkToggleDevice(switch, hass)]) for switch in pywink.get_powerstrips(): - add_devices([WinkToggleDevice(switch, hass)]) + _id = switch.object_id() + switch.name() + if _id not in hass.data[DOMAIN]['unique_ids']: + add_devices([WinkToggleDevice(switch, hass)]) for switch in pywink.get_sirens(): - add_devices([WinkToggleDevice(switch, hass)]) + _id = switch.object_id() + switch.name() + if _id not in hass.data[DOMAIN]['unique_ids']: + add_devices([WinkToggleDevice(switch, hass)]) for sprinkler in pywink.get_sprinklers(): - add_devices([WinkToggleDevice(sprinkler, hass)]) + _id = sprinkler.object_id() + sprinkler.name() + if _id not in hass.data[DOMAIN]['unique_ids']: + add_devices([WinkToggleDevice(sprinkler, hass)]) class WinkToggleDevice(WinkDevice, ToggleEntity): @@ -30,7 +38,7 @@ class WinkToggleDevice(WinkDevice, ToggleEntity): def __init__(self, wink, hass): """Initialize the Wink device.""" - WinkDevice.__init__(self, wink, hass) + super().__init__(wink, hass) @property def is_on(self): @@ -44,3 +52,14 @@ class WinkToggleDevice(WinkDevice, ToggleEntity): def turn_off(self): """Turn the device off.""" self.wink.set_state(False) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + try: + event = self.wink.last_event() + except AttributeError: + event = None + return { + 'last_event': event + } diff --git a/homeassistant/components/tellduslive.py b/homeassistant/components/tellduslive.py index 259d4e1becb..84ab96841d9 100644 --- a/homeassistant/components/tellduslive.py +++ b/homeassistant/components/tellduslive.py @@ -17,7 +17,7 @@ import voluptuous as vol DOMAIN = 'tellduslive' -REQUIREMENTS = ['tellduslive==0.3.0'] +REQUIREMENTS = ['tellduslive==0.3.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 9b4df2749c0..9d31667cc14 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -12,6 +12,7 @@ import logging import mimetypes import os import re +import io from aiohttp import web import voluptuous as vol @@ -30,6 +31,7 @@ import homeassistant.helpers.config_validation as cv DOMAIN = 'tts' DEPENDENCIES = ['http'] +REQUIREMENTS = ["mutagen==1.36.2"] _LOGGER = logging.getLogger(__name__) @@ -255,6 +257,8 @@ class SpeechManager(object): def async_register_engine(self, engine, provider, config): """Register a TTS provider.""" provider.hass = self.hass + if provider.name is None: + provider.name = engine self.providers[engine] = provider @asyncio.coroutine @@ -276,6 +280,8 @@ class SpeechManager(object): language)) # options + if provider.default_options and options: + options = provider.default_options.copy().update(options) options = options or provider.default_options if options is not None: invalid_opts = [opt_name for opt_name in options.keys() @@ -323,6 +329,9 @@ class SpeechManager(object): # create file infos filename = ("{}.{}".format(key, extension)).lower() + data = self.write_tags( + filename, data, provider, message, language, options) + # save to memory self._async_store_to_memcache(key, filename, data) @@ -412,11 +421,43 @@ class SpeechManager(object): content, _ = mimetypes.guess_type(filename) return (content, self.mem_cache[key][MEM_CACHE_VOICE]) + @staticmethod + def write_tags(filename, data, provider, message, language, options): + """Write ID3 tags to file. + + Async friendly. + """ + import mutagen + + data_bytes = io.BytesIO(data) + data_bytes.name = filename + data_bytes.seek(0) + + album = provider.name + artist = language + + if options is not None: + if options.get('voice') is not None: + artist = options.get('voice') + + try: + tts_file = mutagen.File(data_bytes, easy=True) + if tts_file is not None: + tts_file['artist'] = artist + tts_file['album'] = album + tts_file['title'] = message + tts_file.save(data_bytes) + except mutagen.MutagenError as err: + _LOGGER.error("ID3 tag error: %s", err) + + return data_bytes.getvalue() + class Provider(object): """Represent a single provider.""" hass = None + name = None @property def default_language(self): diff --git a/homeassistant/components/tts/amazon_polly.py b/homeassistant/components/tts/amazon_polly.py index e40c10f5e14..7dab49482ed 100644 --- a/homeassistant/components/tts/amazon_polly.py +++ b/homeassistant/components/tts/amazon_polly.py @@ -138,6 +138,7 @@ class AmazonPollyProvider(Provider): self.supported_langs = supported_languages self.all_voices = all_voices self.default_voice = self.config.get(CONF_VOICE) + self.name = 'Amazon Polly' @property def supported_languages(self): diff --git a/homeassistant/components/tts/demo.py b/homeassistant/components/tts/demo.py index 95362b49db9..d9d1eccec8d 100644 --- a/homeassistant/components/tts/demo.py +++ b/homeassistant/components/tts/demo.py @@ -32,6 +32,7 @@ class DemoProvider(Provider): def __init__(self, lang): """Initialize demo provider.""" self._lang = lang + self.name = 'Demo' @property def default_language(self): diff --git a/homeassistant/components/tts/google.py b/homeassistant/components/tts/google.py index 32c9663eedc..be84e0e029b 100644 --- a/homeassistant/components/tts/google.py +++ b/homeassistant/components/tts/google.py @@ -58,6 +58,7 @@ class GoogleProvider(Provider): "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/47.0.2526.106 Safari/537.36") } + self.name = 'Google' @property def default_language(self): diff --git a/homeassistant/components/tts/picotts.py b/homeassistant/components/tts/picotts.py index 49addd9b177..a22196cfbe0 100644 --- a/homeassistant/components/tts/picotts.py +++ b/homeassistant/components/tts/picotts.py @@ -38,6 +38,7 @@ class PicoProvider(Provider): def __init__(self, lang): """Initialize Pico TTS provider.""" self._lang = lang + self.name = 'PicoTTS' @property def default_language(self): diff --git a/homeassistant/components/tts/voicerss.py b/homeassistant/components/tts/voicerss.py index b0c74d1de30..ee50cc30cca 100644 --- a/homeassistant/components/tts/voicerss.py +++ b/homeassistant/components/tts/voicerss.py @@ -95,6 +95,7 @@ class VoiceRSSProvider(Provider): self.hass = hass self._extension = conf[CONF_CODEC] self._lang = conf[CONF_LANG] + self.name = 'VoiceRSS' self._form_data = { 'key': conf[CONF_API_KEY], diff --git a/homeassistant/components/tts/yandextts.py b/homeassistant/components/tts/yandextts.py index 824ca6ca38f..b60f9cab61e 100644 --- a/homeassistant/components/tts/yandextts.py +++ b/homeassistant/components/tts/yandextts.py @@ -82,6 +82,7 @@ class YandexSpeechKitProvider(Provider): self._language = conf.get(CONF_LANG) self._emotion = conf.get(CONF_EMOTION) self._speed = str(conf.get(CONF_SPEED)) + self.name = 'YandexTTS' @property def default_language(self): diff --git a/homeassistant/components/weather/openweathermap.py b/homeassistant/components/weather/openweathermap.py index 931ce7bdf6b..c08666881f3 100644 --- a/homeassistant/components/weather/openweathermap.py +++ b/homeassistant/components/weather/openweathermap.py @@ -15,7 +15,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle -REQUIREMENTS = ['pyowm==2.6.0'] +REQUIREMENTS = ['pyowm==2.6.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py index c1de7e340c1..569def22401 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -15,7 +15,7 @@ from homeassistant.const import ( from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-wink==1.0.0', 'pubnubsub-handler==1.0.0'] +REQUIREMENTS = ['python-wink==1.1.1', 'pubnubsub-handler==1.0.0'] _LOGGER = logging.getLogger(__name__) @@ -87,11 +87,16 @@ def setup(hass, config): password = config[DOMAIN][CONF_PASSWORD] payload = {'username': email, 'password': password} token_response = requests.post(CONF_TOKEN_URL, data=payload) - token = token_response.text.split(':')[1].split()[0].rstrip(' None: + def async_stop(self, exit_code=0) -> None: """Stop Home Assistant and shuts down all threads. This method is a coroutine. @@ -306,6 +283,7 @@ class HomeAssistant(object): logging.getLogger('').removeHandler(handler) yield from handler.async_close(blocking=True) + self.exit_code = exit_code self.loop.stop() # pylint: disable=no-self-use @@ -324,18 +302,6 @@ class HomeAssistant(object): _LOGGER.error("Error doing job: %s", context['message'], **kwargs) - @callback - def _async_stop_handler(self, *args): - """Stop Home Assistant.""" - self.exit_code = 0 - self.loop.create_task(self.async_stop()) - - @callback - def _async_restart_handler(self, *args): - """Restart Home Assistant.""" - self.exit_code = RESTART_EXIT_CODE - self.loop.create_task(self.async_stop()) - class EventOrigin(enum.Enum): """Represent the origin of an event.""" diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 781ef37dc9d..24af8a26351 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -199,7 +199,10 @@ numeric_state_from_config = _threaded_factory(async_numeric_state_from_config) def state(hass, entity, req_state, for_period=None): - """Test if state matches requirements.""" + """Test if state matches requirements. + + Async friendly. + """ if isinstance(entity, str): entity = hass.states.get(entity) @@ -357,7 +360,7 @@ def time_from_config(config, config_validation=True): def zone(hass, zone_ent, entity): """Test if zone-condition matches. - Can be run async. + Async friendly. """ if isinstance(zone_ent, str): zone_ent = hass.states.get(zone_ent) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index ac124b3abf3..854d1bc169d 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -10,7 +10,7 @@ from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, DEVICE_DEFAULT_NAME, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, - ATTR_ENTITY_PICTURE) + ATTR_ENTITY_PICTURE, ATTR_SUPPORTED_FEATURES) from homeassistant.core import HomeAssistant, DOMAIN as CORE_DOMAIN from homeassistant.exceptions import NoEntitySpecifiedError from homeassistant.util import ensure_unique_string, slugify @@ -148,6 +148,11 @@ class Entity(object): """ return False + @property + def supported_features(self) -> int: + """Flag supported features.""" + return None + def update(self): """Retrieve latest state. @@ -231,6 +236,8 @@ class Entity(object): self._attr_setter('entity_picture', str, ATTR_ENTITY_PICTURE, attr) self._attr_setter('hidden', bool, ATTR_HIDDEN, attr) self._attr_setter('assumed_state', bool, ATTR_ASSUMED_STATE, attr) + self._attr_setter('supported_features', int, ATTR_SUPPORTED_FEATURES, + attr) end = timer() diff --git a/homeassistant/helpers/event_decorators.py b/homeassistant/helpers/event_decorators.py index 90a85628e59..d8c24544b7c 100644 --- a/homeassistant/helpers/event_decorators.py +++ b/homeassistant/helpers/event_decorators.py @@ -1,5 +1,6 @@ """Event Decorators for custom components.""" import functools +import logging # pylint: disable=unused-import from typing import Optional # NOQA @@ -8,10 +9,14 @@ from homeassistant.core import HomeAssistant # NOQA from homeassistant.helpers import event HASS = None # type: Optional[HomeAssistant] +_LOGGER = logging.getLogger(__name__) +_MSG = 'Event decorators are deprecated. Support will be removed in 0.40.' def track_state_change(entity_ids, from_state=None, to_state=None): """Decorator factory to track state changes for entity id.""" + _LOGGER.warning(_MSG) + def track_state_change_decorator(action): """Decorator to track state changes.""" event.track_state_change(HASS, entity_ids, @@ -24,6 +29,8 @@ def track_state_change(entity_ids, from_state=None, to_state=None): def track_sunrise(offset=None): """Decorator factory to track sunrise events.""" + _LOGGER.warning(_MSG) + def track_sunrise_decorator(action): """Decorator to track sunrise events.""" event.track_sunrise(HASS, @@ -36,6 +43,8 @@ def track_sunrise(offset=None): def track_sunset(offset=None): """Decorator factory to track sunset events.""" + _LOGGER.warning(_MSG) + def track_sunset_decorator(action): """Decorator to track sunset events.""" event.track_sunset(HASS, @@ -49,6 +58,8 @@ def track_sunset(offset=None): def track_time_change(year=None, month=None, day=None, hour=None, minute=None, second=None): """Decorator factory to track time changes.""" + _LOGGER.warning(_MSG) + def track_time_change_decorator(action): """Decorator to track time changes.""" event.track_time_change(HASS, @@ -62,6 +73,8 @@ def track_time_change(year=None, month=None, day=None, hour=None, minute=None, def track_utc_time_change(year=None, month=None, day=None, hour=None, minute=None, second=None): """Decorator factory to track time changes.""" + _LOGGER.warning(_MSG) + def track_utc_time_change_decorator(action): """Decorator to track time changes.""" event.track_utc_time_change(HASS, diff --git a/homeassistant/helpers/signal.py b/homeassistant/helpers/signal.py new file mode 100644 index 00000000000..de4c344d375 --- /dev/null +++ b/homeassistant/helpers/signal.py @@ -0,0 +1,31 @@ +"""Signal handling related helpers.""" +import logging +import signal +import sys + +from homeassistant.core import callback +from homeassistant.const import RESTART_EXIT_CODE + +_LOGGER = logging.getLogger(__name__) + + +@callback +def async_register_signal_handling(hass): + """Register system signal handler for core.""" + if sys.platform != 'win32': + @callback + def async_signal_handle(exit_code): + """Wrap signal handling.""" + hass.async_add_job(hass.async_stop(exit_code)) + + try: + hass.loop.add_signal_handler( + signal.SIGTERM, async_signal_handle, 0) + except ValueError: + _LOGGER.warning("Could not bind to SIGTERM") + + try: + hass.loop.add_signal_handler( + signal.SIGHUP, async_signal_handle, RESTART_EXIT_CODE) + except ValueError: + _LOGGER.warning("Could not bind to SIGHUP") diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index d3f49c003c8..8d55615f661 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -425,6 +425,8 @@ ENV.filters['timestamp_custom'] = timestamp_custom ENV.filters['timestamp_local'] = timestamp_local ENV.filters['timestamp_utc'] = timestamp_utc ENV.filters['is_defined'] = fail_when_undefined +ENV.filters['max'] = max +ENV.filters['min'] = min ENV.globals['float'] = forgiving_float ENV.globals['now'] = dt_util.now ENV.globals['utcnow'] = dt_util.utcnow diff --git a/homeassistant/loader.py b/homeassistant/loader.py index dc68d3f1d46..77b4da00f09 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -177,6 +177,8 @@ def load_order_components(components: Sequence[str]) -> OrderedSet: - Will ensure that all components that do not directly depend on the group component will be loaded before the group component. - returns an OrderedSet load order. + - Makes sure MQTT eventstream is available for publish before + components start updating states. Async friendly. """ @@ -193,7 +195,8 @@ def load_order_components(components: Sequence[str]) -> OrderedSet: load_order.update(comp_load_order) # Push some to first place in load order - for comp in ('logger', 'recorder', 'introduction'): + for comp in ('mqtt_eventstream', 'mqtt', 'logger', + 'recorder', 'introduction'): if comp in load_order: load_order.promote(comp) diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index cb825ad44c8..154754c667a 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -33,6 +33,7 @@ MOCKS = { } SILENCE = ( 'homeassistant.bootstrap.clear_secret_cache', + 'homeassistant.bootstrap.async_register_signal_handling', 'homeassistant.core._LOGGER.info', 'homeassistant.loader._LOGGER.info', 'homeassistant.bootstrap._LOGGER.info', @@ -96,7 +97,6 @@ def run(script_args: List) -> int: domain_info = args.info.split(',') res = check(config_path) - if args.files: print(color(C_HEAD, 'yaml files'), '(used /', color('red', 'not used') + ')') @@ -247,6 +247,7 @@ def check(config_path): res['secret_cache'] = dict(yaml.__SECRET_CACHE) except Exception as err: # pylint: disable=broad-except print(color('red', 'Fatal error while loading config:'), str(err)) + res['except'].setdefault(ERROR_STR, []).append(err) finally: # Stop all patches for pat in PATCHES.values(): diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index cf65e319552..6f632f47dbe 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -35,7 +35,7 @@ def install_package(package: str, upgrade: bool=True, try: return subprocess.call(args) == 0 except subprocess.SubprocessError: - _LOGGER.exception('Unable to install pacakge %s', package) + _LOGGER.exception('Unable to install package %s', package) return False diff --git a/requirements_all.txt b/requirements_all.txt index f1eadf1a51d..fff2eaef756 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -6,7 +6,7 @@ pip>=7.0.0 jinja2>=2.8 voluptuous==0.9.3 typing>=3,<4 -aiohttp==1.2 +aiohttp==1.3.1 async_timeout==1.1.0 # homeassistant.components.nuimo_controller @@ -51,7 +51,8 @@ apcaccess==0.0.4 apns2==0.1.1 # homeassistant.components.sun -astral==1.3.3 +# homeassistant.components.sensor.moon +astral==1.3.4 # homeassistant.components.light.avion # avion==0.5 @@ -146,7 +147,7 @@ fitbit==0.2.3 fixerio==0.1.1 # homeassistant.components.light.flux_led -flux_led==0.12 +flux_led==0.13 # homeassistant.components.notify.free_mobile freesms==0.1.1 @@ -154,6 +155,9 @@ freesms==0.1.1 # homeassistant.components.device_tracker.fritz # fritzconnection==0.6 +# homeassistant.components.switch.fritzdect +fritzhome==1.0.2 + # homeassistant.components.conversation fuzzywuzzy==0.14.0 @@ -176,7 +180,7 @@ googlemaps==2.4.4 gps3==0.33.3 # homeassistant.components.ffmpeg -ha-ffmpeg==1.2 +ha-ffmpeg==1.4 # homeassistant.components.media_player.philips_js ha-philipsjs==0.0.1 @@ -193,15 +197,12 @@ hikvision==0.4 # homeassistant.components.sensor.dht # http://github.com/adafruit/Adafruit_Python_DHT/archive/310c59b0293354d07d94375f1365f7b9b9110c7d.zip#Adafruit_DHT==1.3.0 -# homeassistant.components.nest -http://github.com/technicalpickles/python-nest/archive/e6c9d56a8df455d4d7746389811f2c1387e8cb33.zip#python-nest==3.0.2 - # homeassistant.components.switch.dlink -https://github.com/LinuxChristian/pyW215/archive/v0.3.7.zip#pyW215==0.3.7 +https://github.com/LinuxChristian/pyW215/archive/v0.4.zip#pyW215==0.4 # homeassistant.components.media_player.webostv # homeassistant.components.notify.webostv -https://github.com/TheRealLink/pylgtv/archive/v0.1.2.zip#pylgtv==0.1.2 +https://github.com/TheRealLink/pylgtv/archive/v0.1.3.zip#pylgtv==0.1.3 # homeassistant.components.sensor.thinkingcleaner # homeassistant.components.switch.thinkingcleaner @@ -235,7 +236,7 @@ https://github.com/jamespcole/home-assistant-nzb-clients/archive/616cad591540925 https://github.com/joopert/nad_receiver/archive/0.0.3.zip#nad_receiver==0.0.3 # homeassistant.components.media_player.russound_rnet -https://github.com/laf/russound/archive/0.1.6.zip#russound==0.1.6 +https://github.com/laf/russound/archive/0.1.7.zip#russound==0.1.7 # homeassistant.components.switch.anel_pwrctrl https://github.com/mweinelt/anel-pwrctrl/archive/ed26e8830e28a2bfa4260a9002db23ce3e7e63d7.zip#anel_pwrctrl==0.0.1 @@ -247,6 +248,9 @@ https://github.com/nkgilley/python-ecobee-api/archive/4856a704670c53afe1882178a8 # homeassistant.components.notify.joaoapps_join https://github.com/nkgilley/python-join-api/archive/3e1e849f1af0b4080f551b62270c6d244d5fbcbd.zip#python-join-api==0.0.1 +# homeassistant.components.notify.mailgun +https://github.com/pschmitt/pymailgun/archive/1.3.zip#pymailgun==1.3 + # homeassistant.components.switch.edimax https://github.com/rkabadi/pyedimax/archive/365301ce3ff26129a7910c501ead09ea625f3700.zip#pyedimax==0.1 @@ -308,6 +312,9 @@ liffylights==0.9.4 # homeassistant.components.light.limitlessled limitlessled==1.0.2 +# homeassistant.components.media_player.liveboxplaytv +liveboxplaytv==1.4.7 + # homeassistant.components.notify.matrix matrix-client==0.0.5 @@ -319,13 +326,16 @@ messagebird==1.2.0 mficlient==0.3.0 # homeassistant.components.sensor.miflora -miflora==0.1.14 +miflora==0.1.15 + +# homeassistant.components.tts +mutagen==1.36.2 # homeassistant.components.sensor.usps myusps==1.0.2 # homeassistant.components.discovery -netdisco==0.8.1 +netdisco==0.8.2 # homeassistant.components.sensor.neurio_energy neurio==0.3.1 @@ -333,6 +343,9 @@ neurio==0.3.1 # homeassistant.components.google oauth2client==3.0.0 +# homeassistant.components.sensor.openevse +openevsewifi==0.4 + # homeassistant.components.switch.orvibo orvibo==1.1.1 @@ -372,7 +385,7 @@ pmsensor==0.3 proliphix==0.4.1 # homeassistant.components.sensor.systemmonitor -psutil==5.0.1 +psutil==5.1.2 # homeassistant.components.wink pubnubsub-handler==1.0.0 @@ -384,25 +397,28 @@ pushbullet.py==0.10.0 pushetta==1.0.15 # homeassistant.components.sensor.waqi -pwaqi==1.3 +pwaqi==1.4 # homeassistant.components.sensor.cpuspeed py-cpuinfo==0.2.3 # homeassistant.components.hdmi_cec -pyCEC==0.4.12 +pyCEC==0.4.13 # homeassistant.components.switch.tplink pyHS100==0.2.3 # homeassistant.components.rfxtrx -pyRFXtrx==0.15.0 +pyRFXtrx==0.16.1 # homeassistant.components.notify.xmpp pyasn1-modules==0.0.8 # homeassistant.components.notify.xmpp -pyasn1==0.1.9 +pyasn1==0.2.2 + +# homeassistant.components.media_player.apple_tv +pyatv==0.1.3 # homeassistant.components.device_tracker.bbox # homeassistant.components.sensor.bbox @@ -412,7 +428,7 @@ pybbox==0.0.5-alpha # pybluez==0.22 # homeassistant.components.media_player.cast -pychromecast==0.7.6 +pychromecast==0.8.0 # homeassistant.components.media_player.cmus pycmus==0.1.0 @@ -470,6 +486,9 @@ pynetgear==0.3.3 # homeassistant.components.switch.netio pynetio==0.1.6 +# homeassistant.components.lock.nuki +pynuki==1.2 + # homeassistant.components.sensor.nut pynut2==2.1.2 @@ -479,7 +498,7 @@ pynx584==0.4 # homeassistant.components.sensor.openweathermap # homeassistant.components.weather.openweathermap -pyowm==2.6.0 +pyowm==2.6.1 # homeassistant.components.qwikswitch pyqwikswitch==0.4 @@ -492,13 +511,13 @@ pysma==0.1.3 # homeassistant.components.device_tracker.snmp # homeassistant.components.sensor.snmp -pysnmp==4.3.2 +pysnmp==4.3.3 # homeassistant.components.digital_ocean python-digitalocean==1.10.1 # homeassistant.components.climate.eq3btsmart -# python-eq3bt==0.1.4 +# python-eq3bt==0.1.5 # homeassistant.components.sensor.darksky python-forecastio==1.3.5 @@ -515,6 +534,9 @@ python-mpd2==0.5.5 # homeassistant.components.switch.mystrom python-mystrom==0.3.6 +# homeassistant.components.nest +python-nest==3.1.0 + # homeassistant.components.device_tracker.nmap_tracker python-nmap==0.6.1 @@ -534,7 +556,7 @@ python-twitch==1.3.0 python-vlc==1.1.2 # homeassistant.components.wink -python-wink==1.0.0 +python-wink==1.1.1 # homeassistant.components.device_tracker.trackr pytrackr==0.0.5 @@ -554,17 +576,20 @@ pywebpush==0.6.1 # homeassistant.components.wemo pywemo==0.4.11 -# homeassistant.components.light.yeelight -pyyeelight==1.0-beta - # homeassistant.components.zabbix pyzabbix==0.7.4 +# homeassistant.components.sensor.qnap +qnapstats==0.2.1 + # homeassistant.components.climate.radiotherm radiotherm==1.2 +# homeassistant.components.rflink +rflink==0.0.28 + # homeassistant.components.switch.rpi_rf -# rpi-rf==0.9.5 +# rpi-rf==0.9.6 # homeassistant.components.media_player.yamaha rxv==0.4.0 @@ -588,7 +613,7 @@ sense-hat==2.2.0 sharp_aquos_rc==0.3.2 # homeassistant.components.notify.slack -slacker==0.9.30 +slacker==0.9.40 # homeassistant.components.notify.xmpp sleekxmpp==1.3.1 @@ -620,7 +645,7 @@ steamodd==4.21 tellcore-py==1.1.2 # homeassistant.components.tellduslive -tellduslive==0.3.0 +tellduslive==0.3.2 # homeassistant.components.sensor.temper temperusb==1.5.1 @@ -685,8 +710,11 @@ yahoo-finance==1.4.0 # homeassistant.components.sensor.yweather yahooweather==0.8 +# homeassistant.components.light.yeelight +yeelight==0.2.2 + # homeassistant.components.light.zengge zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.17.6 +zeroconf==0.18.0 diff --git a/script/bootstrap_frontend b/script/bootstrap_frontend index ed3321b1d93..e1d4ef887be 100755 --- a/script/bootstrap_frontend +++ b/script/bootstrap_frontend @@ -17,5 +17,4 @@ yarn install # Install bower web components. Allow to download the components as root since the user in docker is root. ./node_modules/.bin/bower install --allow-root -yarn run setup_js_dev cd ../../../../.. diff --git a/script/setup_docker_prereqs b/script/setup_docker_prereqs deleted file mode 100755 index a482d9a0ec7..00000000000 --- a/script/setup_docker_prereqs +++ /dev/null @@ -1,63 +0,0 @@ -#!/bin/bash -# Install requirements and build dependencies for Home Assinstant in Docker. - -# Required debian packages for running hass or components -PACKAGES=( - # homeassistant.components.device_tracker.nmap_tracker - nmap net-tools - # homeassistant.components.device_tracker.bluetooth_tracker - bluetooth libglib2.0-dev libbluetooth-dev - # homeassistant.components.tellstick - libtelldus-core2 -) - -# Required debian packages for running hass or components from jessie-backports -PACKAGES_BACKPORTS=( - # homeassistant.components.ffmpeg - ffmpeg -) - -# Required debian packages for building dependencies -PACKAGES_DEV=( - # python-openzwave - cython3 libudev-dev - # libcec - cmake swig libxrandr-dev -) - -# Stop on errors -set -e - -cd "$(dirname "$0")/.." - -# Add Tellstick repository -echo "deb http://download.telldus.com/debian/ stable main" >> /etc/apt/sources.list.d/telldus.list -wget -qO - http://download.telldus.se/debian/telldus-public.key | apt-key add - - -# Add jessie-backports -echo "deb http://deb.debian.org/debian jessie-backports main" >> /etc/apt/sources.list - -# Install packages -apt-get update -apt-get install -y --no-install-recommends ${PACKAGES[@]} ${PACKAGES_DEV[@]} -apt-get install -y --no-install-recommends -t jessie-backports ${PACKAGES_BACKPORTS[@]} - -# Build and install openzwave -script/build_python_openzwave -mkdir -p /usr/local/share/python-openzwave -cp -R /usr/src/app/build/python-openzwave/openzwave/config /usr/local/share/python-openzwave/config - -# Build and install libcec -script/build_libcec - -# Install phantomjs -script/install_phantomjs - -# Remove packages -apt-get remove -y --purge ${PACKAGES_DEV[@]} -apt-get -y --purge autoremove - -# Cleanup -apt-get clean -rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* build/ - diff --git a/setup.py b/setup.py index 4f223eb9b8a..b6b679315e1 100755 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ REQUIRES = [ 'jinja2>=2.8', 'voluptuous==0.9.3', 'typing>=3,<4', - 'aiohttp==1.2', + 'aiohttp==1.3.1', 'async_timeout==1.1.0', ] diff --git a/tests/common.py b/tests/common.py index b602edbd717..e384a4f1b37 100644 --- a/tests/common.py +++ b/tests/common.py @@ -26,6 +26,7 @@ from homeassistant.components import sun, mqtt from homeassistant.components.http.auth import auth_middleware from homeassistant.components.http.const import ( KEY_USE_X_FORWARDED_FOR, KEY_BANS_ENABLED, KEY_TRUSTED_NETWORKS) +from homeassistant.util.async import run_callback_threadsafe _TEST_INSTANCE_PORT = SERVER_PORT _LOGGER = logging.getLogger(__name__) @@ -107,8 +108,7 @@ def async_test_home_assistant(loop): @asyncio.coroutine def mock_async_start(): """Start the mocking.""" - with patch.object(loop, 'add_signal_handler'), \ - patch('homeassistant.core._async_create_timer'): + with patch('homeassistant.core._async_create_timer'): yield from orig_start() hass.async_start = mock_async_start @@ -147,15 +147,22 @@ def mock_service(hass, domain, service): return calls -def fire_mqtt_message(hass, topic, payload, qos=0): +@ha.callback +def async_fire_mqtt_message(hass, topic, payload, qos=0): """Fire the MQTT message.""" - hass.bus.fire(mqtt.EVENT_MQTT_MESSAGE_RECEIVED, { + hass.bus.async_fire(mqtt.EVENT_MQTT_MESSAGE_RECEIVED, { mqtt.ATTR_TOPIC: topic, mqtt.ATTR_PAYLOAD: payload, mqtt.ATTR_QOS: qos, }) +def fire_mqtt_message(hass, topic, payload, qos=0): + """Fire the MQTT message.""" + run_callback_threadsafe( + hass.loop, async_fire_mqtt_message, hass, topic, payload, qos).result() + + def fire_time_changed(hass, time): """Fire a time changes event.""" hass.bus.fire(EVENT_TIME_CHANGED, {'now': time}) @@ -388,6 +395,11 @@ def mock_coro(return_value=None): return coro +def mock_generator(return_value=None): + """Helper method to return a coro generator that returns a value.""" + return mock_coro(return_value)() + + @contextmanager def assert_setup_component(count, domain=None): """Collect valid configuration from setup_component. diff --git a/tests/components/binary_sensor/test_ffmpeg.py b/tests/components/binary_sensor/test_ffmpeg.py new file mode 100644 index 00000000000..ec06d025b8e --- /dev/null +++ b/tests/components/binary_sensor/test_ffmpeg.py @@ -0,0 +1,104 @@ +"""The tests for Home Assistant ffmpeg binary sensor.""" +from unittest.mock import patch + +from homeassistant.bootstrap import setup_component +from homeassistant.util.async import run_callback_threadsafe + +from tests.common import ( + get_test_home_assistant, assert_setup_component, mock_coro) + + +class TestFFmpegNoiseSetup(object): + """Test class for ffmpeg.""" + + def setup_method(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + self.config = { + 'ffmpeg': { + 'run_test': False, + }, + 'binary_sensor': { + 'platform': 'ffmpeg_noise', + 'input': 'testinputvideo', + }, + } + + def teardown_method(self): + """Stop everything that was started.""" + self.hass.stop() + + def test_setup_component(self): + """Setup ffmpeg component.""" + with assert_setup_component(1, 'binary_sensor'): + setup_component(self.hass, 'binary_sensor', self.config) + + assert self.hass.data['ffmpeg'].binary == 'ffmpeg' + assert len(self.hass.data['ffmpeg'].entities) == 1 + + @patch('haffmpeg.SensorNoise.open_sensor', return_value=mock_coro()()) + def test_setup_component_start(self, mock_start): + """Setup ffmpeg component.""" + with assert_setup_component(1, 'binary_sensor'): + setup_component(self.hass, 'binary_sensor', self.config) + + assert self.hass.data['ffmpeg'].binary == 'ffmpeg' + assert len(self.hass.data['ffmpeg'].entities) == 1 + + entity = self.hass.data['ffmpeg'].entities[0] + self.hass.start() + assert mock_start.called + + assert entity.state == 'off' + run_callback_threadsafe( + self.hass.loop, entity._async_callback, True).result() + assert entity.state == 'on' + + +class TestFFmpegMotionSetup(object): + """Test class for ffmpeg.""" + + def setup_method(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + self.config = { + 'ffmpeg': { + 'run_test': False, + }, + 'binary_sensor': { + 'platform': 'ffmpeg_motion', + 'input': 'testinputvideo', + }, + } + + def teardown_method(self): + """Stop everything that was started.""" + self.hass.stop() + + def test_setup_component(self): + """Setup ffmpeg component.""" + with assert_setup_component(1, 'binary_sensor'): + setup_component(self.hass, 'binary_sensor', self.config) + + assert self.hass.data['ffmpeg'].binary == 'ffmpeg' + assert len(self.hass.data['ffmpeg'].entities) == 1 + + @patch('haffmpeg.SensorMotion.open_sensor', return_value=mock_coro()()) + def test_setup_component_start(self, mock_start): + """Setup ffmpeg component.""" + with assert_setup_component(1, 'binary_sensor'): + setup_component(self.hass, 'binary_sensor', self.config) + + assert self.hass.data['ffmpeg'].binary == 'ffmpeg' + assert len(self.hass.data['ffmpeg'].entities) == 1 + + entity = self.hass.data['ffmpeg'].entities[0] + self.hass.start() + assert mock_start.called + + assert entity.state == 'off' + run_callback_threadsafe( + self.hass.loop, entity._async_callback, True).result() + assert entity.state == 'on' diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index c083557294b..524eda22634 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -11,6 +11,7 @@ import os from homeassistant.components import zone from homeassistant.core import callback from homeassistant.bootstrap import setup_component +from homeassistant.helpers import discovery from homeassistant.loader import get_component from homeassistant.util.async import run_coroutine_threadsafe import homeassistant.util.dt as dt_util @@ -324,6 +325,23 @@ class TestComponentsDeviceTracker(unittest.TestCase): fire_service_discovered(self.hass, 'test', {}) self.assertTrue(mock_scan.called) + @patch( + 'homeassistant.components.device_tracker.DeviceTracker.see') + @patch( + 'homeassistant.components.device_tracker.demo.setup_scanner', + autospec=True) + def test_discover_platform(self, mock_demo_setup_scanner, mock_see): + """Test discovery of device_tracker demo platform.""" + assert device_tracker.DOMAIN not in self.hass.config.components + discovery.load_platform( + self.hass, device_tracker.DOMAIN, 'demo', {'test_key': 'test_val'}, + {}) + self.hass.block_till_done() + assert device_tracker.DOMAIN in self.hass.config.components + assert mock_demo_setup_scanner.called + assert mock_demo_setup_scanner.call_args[0] == ( + self.hass, {}, mock_see, {'test_key': 'test_val'}) + def test_update_stale(self): """Test stalled update.""" scanner = get_component('device_tracker.test').SCANNER diff --git a/tests/components/device_tracker/test_mqtt.py b/tests/components/device_tracker/test_mqtt.py index 6eb5ba2381c..9405c944552 100644 --- a/tests/components/device_tracker/test_mqtt.py +++ b/tests/components/device_tracker/test_mqtt.py @@ -33,7 +33,7 @@ class TestComponentsDeviceTrackerMQTT(unittest.TestCase): def test_ensure_device_tracker_platform_validation(self): \ # pylint: disable=invalid-name """Test if platform validation was done.""" - def mock_setup_scanner(hass, config, see): + def mock_setup_scanner(hass, config, see, discovery_info=None): """Check that Qos was added by validation.""" self.assertTrue('qos' in config) diff --git a/tests/components/device_tracker/test_owntracks.py b/tests/components/device_tracker/test_owntracks.py index 183bbbd994f..4bea0d3d0b3 100644 --- a/tests/components/device_tracker/test_owntracks.py +++ b/tests/components/device_tracker/test_owntracks.py @@ -377,7 +377,7 @@ class TestDeviceTrackerOwnTracks(BaseMQTT): message = REGION_ENTER_MESSAGE.copy() message['desc'] = "inner 2" self.send_message(EVENT_TOPIC, message) - self.assert_location_state('inner_2') + self.assert_location_state('inner 2') message = REGION_LEAVE_MESSAGE.copy() message['desc'] = "inner 2" diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index c3888bd9cf7..fdd9bc90946 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -301,7 +301,7 @@ def test_put_light_state_fan(hass_hue, hue_client): blocking=True) # Emulated hue converts 0-100% to 0-255. - level = 23 + level = 43 brightness = round(level * 255 / 100) fan_result = yield from perform_put_light_state( diff --git a/tests/components/image_processing/test_init.py b/tests/components/image_processing/test_init.py index 77cfd19bf92..2ac64891e95 100644 --- a/tests/components/image_processing/test_init.py +++ b/tests/components/image_processing/test_init.py @@ -61,17 +61,14 @@ class TestImageProcessing(object): config = { ip.DOMAIN: { - 'platform': 'demo' + 'platform': 'test' }, 'camera': { 'platform': 'demo' }, } - with patch('homeassistant.components.image_processing.demo.' - 'DemoImageProcessing.should_poll', - new_callable=PropertyMock(return_value=False)): - setup_component(self.hass, ip.DOMAIN, config) + setup_component(self.hass, ip.DOMAIN, config) state = self.hass.states.get('camera.demo_camera') self.url = "{0}{1}".format( @@ -84,37 +81,36 @@ class TestImageProcessing(object): @patch('homeassistant.components.camera.demo.DemoCamera.camera_image', autospec=True, return_value=b'Test') - @patch('homeassistant.components.image_processing.demo.' - 'DemoImageProcessing.process_image', autospec=True) - def test_get_image_from_camera(self, mock_process, mock_camera): + def test_get_image_from_camera(self, mock_camera): """Grab a image from camera entity.""" self.hass.start() - ip.scan(self.hass, entity_id='image_processing.demo') + ip.scan(self.hass, entity_id='image_processing.test') self.hass.block_till_done() - assert mock_camera.called - assert mock_process.called + state = self.hass.states.get('image_processing.test') - assert mock_process.call_args[0][1] == b'Test' + assert mock_camera.called + assert state.state == '1' + assert state.attributes['image'] == b'Test' @patch('homeassistant.components.camera.async_get_image', side_effect=HomeAssistantError()) - @patch('homeassistant.components.image_processing.demo.' - 'DemoImageProcessing.process_image', autospec=True) - def test_get_image_without_exists_camera(self, mock_process, mock_image): + def test_get_image_without_exists_camera(self, mock_image): """Try to get image without exists camera.""" self.hass.states.remove('camera.demo_camera') - ip.scan(self.hass, entity_id='image_processing.demo') + ip.scan(self.hass, entity_id='image_processing.test') self.hass.block_till_done() + state = self.hass.states.get('image_processing.test') + assert mock_image.called - assert not mock_process.called + assert state.state == '0' class TestImageProcessingAlpr(object): - """Test class for image processing.""" + """Test class for alpr image processing.""" def setup_method(self): """Setup things to be run when tests are started.""" @@ -146,7 +142,7 @@ class TestImageProcessingAlpr(object): """Mock event.""" self.alpr_events.append(event) - self.hass.bus.listen('found_plate', mock_alpr_event) + self.hass.bus.listen('image_processing.found_plate', mock_alpr_event) def teardown_method(self): """Stop everything that was started.""" @@ -215,8 +211,8 @@ class TestImageProcessingAlpr(object): assert event_data[0]['entity_id'] == 'image_processing.demo_alpr' -class TestImageProcessingFaceIdentify(object): - """Test class for image processing.""" +class TestImageProcessingFace(object): + """Test class for face image processing.""" def setup_method(self): """Setup things to be run when tests are started.""" @@ -232,7 +228,7 @@ class TestImageProcessingFaceIdentify(object): } with patch('homeassistant.components.image_processing.demo.' - 'DemoImageProcessingFaceIdentify.should_poll', + 'DemoImageProcessingFace.should_poll', new_callable=PropertyMock(return_value=False)): setup_component(self.hass, ip.DOMAIN, config) @@ -248,7 +244,7 @@ class TestImageProcessingFaceIdentify(object): """Mock event.""" self.face_events.append(event) - self.hass.bus.listen('identify_face', mock_face_event) + self.hass.bus.listen('image_processing.detect_face', mock_face_event) def teardown_method(self): """Stop everything that was started.""" @@ -258,10 +254,10 @@ class TestImageProcessingFaceIdentify(object): """Setup and scan a picture and test faces from event.""" aioclient_mock.get(self.url, content=b'image') - ip.scan(self.hass, entity_id='image_processing.demo_face_identify') + ip.scan(self.hass, entity_id='image_processing.demo_face') self.hass.block_till_done() - state = self.hass.states.get('image_processing.demo_face_identify') + state = self.hass.states.get('image_processing.demo_face') assert len(self.face_events) == 2 assert state.state == 'Hans' @@ -272,5 +268,31 @@ class TestImageProcessingFaceIdentify(object): assert len(event_data) == 1 assert event_data[0]['name'] == 'Hans' assert event_data[0]['confidence'] == 98.34 + assert event_data[0]['gender'] == 'male' assert event_data[0]['entity_id'] == \ - 'image_processing.demo_face_identify' + 'image_processing.demo_face' + + @patch('homeassistant.components.image_processing.demo.' + 'DemoImageProcessingFace.confidence', + new_callable=PropertyMock(return_value=None)) + def test_face_event_call_no_confidence(self, mock_confi, aioclient_mock): + """Setup and scan a picture and test faces from event.""" + aioclient_mock.get(self.url, content=b'image') + + ip.scan(self.hass, entity_id='image_processing.demo_face') + self.hass.block_till_done() + + state = self.hass.states.get('image_processing.demo_face') + + assert len(self.face_events) == 3 + assert state.state == '4' + assert state.attributes['total_faces'] == 4 + + event_data = [event.data for event in self.face_events if + event.data.get('name') == 'Hans'] + assert len(event_data) == 1 + assert event_data[0]['name'] == 'Hans' + assert event_data[0]['confidence'] == 98.34 + assert event_data[0]['gender'] == 'male' + assert event_data[0]['entity_id'] == \ + 'image_processing.demo_face' diff --git a/tests/components/image_processing/test_microsoft_face_detect.py b/tests/components/image_processing/test_microsoft_face_detect.py new file mode 100644 index 00000000000..82fd54f1633 --- /dev/null +++ b/tests/components/image_processing/test_microsoft_face_detect.py @@ -0,0 +1,159 @@ +"""The tests for the microsoft face detect platform.""" +from unittest.mock import patch, PropertyMock + +from homeassistant.core import callback +from homeassistant.const import ATTR_ENTITY_PICTURE +from homeassistant.bootstrap import setup_component +import homeassistant.components.image_processing as ip +import homeassistant.components.microsoft_face as mf + +from tests.common import ( + get_test_home_assistant, assert_setup_component, load_fixture, mock_coro) + + +class TestMicrosoftFaceDetectSetup(object): + """Test class for image processing.""" + + def setup_method(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def teardown_method(self): + """Stop everything that was started.""" + self.hass.stop() + + @patch('homeassistant.components.microsoft_face.' + 'MicrosoftFace.update_store', return_value=mock_coro()()) + def test_setup_platform(self, store_mock): + """Setup platform with one entity.""" + config = { + ip.DOMAIN: { + 'platform': 'microsoft_face_detect', + 'source': { + 'entity_id': 'camera.demo_camera' + }, + 'attributes': ['age', 'gender'], + }, + 'camera': { + 'platform': 'demo' + }, + mf.DOMAIN: { + 'api_key': '12345678abcdef6', + } + } + + with assert_setup_component(1, ip.DOMAIN): + setup_component(self.hass, ip.DOMAIN, config) + + assert self.hass.states.get( + 'image_processing.microsoftface_demo_camera') + + @patch('homeassistant.components.microsoft_face.' + 'MicrosoftFace.update_store', return_value=mock_coro()()) + def test_setup_platform_name(self, store_mock): + """Setup platform with one entity and set name.""" + config = { + ip.DOMAIN: { + 'platform': 'microsoft_face_detect', + 'source': { + 'entity_id': 'camera.demo_camera', + 'name': 'test local' + }, + }, + 'camera': { + 'platform': 'demo' + }, + mf.DOMAIN: { + 'api_key': '12345678abcdef6', + } + } + + with assert_setup_component(1, ip.DOMAIN): + setup_component(self.hass, ip.DOMAIN, config) + + assert self.hass.states.get('image_processing.test_local') + + +class TestMicrosoftFaceDetect(object): + """Test class for image processing.""" + + def setup_method(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + self.config = { + ip.DOMAIN: { + 'platform': 'microsoft_face_detect', + 'source': { + 'entity_id': 'camera.demo_camera', + 'name': 'test local' + }, + 'attributes': ['age', 'gender'], + }, + 'camera': { + 'platform': 'demo' + }, + mf.DOMAIN: { + 'api_key': '12345678abcdef6', + } + } + + def teardown_method(self): + """Stop everything that was started.""" + self.hass.stop() + + @patch('homeassistant.components.image_processing.microsoft_face_detect.' + 'MicrosoftFaceDetectEntity.should_poll', + new_callable=PropertyMock(return_value=False)) + def test_ms_detect_process_image(self, poll_mock, aioclient_mock): + """Setup and scan a picture and test plates from event.""" + aioclient_mock.get( + mf.FACE_API_URL.format("persongroups"), + text=load_fixture('microsoft_face_persongroups.json') + ) + aioclient_mock.get( + mf.FACE_API_URL.format("persongroups/test_group1/persons"), + text=load_fixture('microsoft_face_persons.json') + ) + aioclient_mock.get( + mf.FACE_API_URL.format("persongroups/test_group2/persons"), + text=load_fixture('microsoft_face_persons.json') + ) + + setup_component(self.hass, ip.DOMAIN, self.config) + + state = self.hass.states.get('camera.demo_camera') + url = "{0}{1}".format( + self.hass.config.api.base_url, + state.attributes.get(ATTR_ENTITY_PICTURE)) + + face_events = [] + + @callback + def mock_face_event(event): + """Mock event.""" + face_events.append(event) + + self.hass.bus.listen('image_processing.detect_face', mock_face_event) + + aioclient_mock.get(url, content=b'image') + + aioclient_mock.post( + mf.FACE_API_URL.format("detect"), + text=load_fixture('microsoft_face_detect.json'), + params={'returnFaceAttributes': "age,gender"} + ) + + ip.scan(self.hass, entity_id='image_processing.test_local') + self.hass.block_till_done() + + state = self.hass.states.get('image_processing.test_local') + + assert len(face_events) == 1 + assert state.attributes.get('total_faces') == 1 + assert state.state == '1' + + assert face_events[0].data['age'] == 71.0 + assert face_events[0].data['gender'] == 'male' + assert face_events[0].data['entity_id'] == \ + 'image_processing.test_local' diff --git a/tests/components/image_processing/test_microsoft_face_identify.py b/tests/components/image_processing/test_microsoft_face_identify.py index 8d75f6ff1d3..8812c1c050e 100644 --- a/tests/components/image_processing/test_microsoft_face_identify.py +++ b/tests/components/image_processing/test_microsoft_face_identify.py @@ -106,7 +106,7 @@ class TestMicrosoftFaceIdentify(object): @patch('homeassistant.components.image_processing.microsoft_face_identify.' 'MicrosoftFaceIdentifyEntity.should_poll', new_callable=PropertyMock(return_value=False)) - def test_openalpr_process_image(self, poll_mock, aioclient_mock): + def test_ms_identify_process_image(self, poll_mock, aioclient_mock): """Setup and scan a picture and test plates from event.""" aioclient_mock.get( mf.FACE_API_URL.format("persongroups"), @@ -135,7 +135,7 @@ class TestMicrosoftFaceIdentify(object): """Mock event.""" face_events.append(event) - self.hass.bus.listen('identify_face', mock_face_event) + self.hass.bus.listen('image_processing.detect_face', mock_face_event) aioclient_mock.get(url, content=b'image') diff --git a/tests/components/image_processing/test_openalpr_cloud.py b/tests/components/image_processing/test_openalpr_cloud.py index 8e9f35eb0b2..8bce672e0d9 100644 --- a/tests/components/image_processing/test_openalpr_cloud.py +++ b/tests/components/image_processing/test_openalpr_cloud.py @@ -143,7 +143,7 @@ class TestOpenAlprCloud(object): """Mock event.""" self.alpr_events.append(event) - self.hass.bus.listen('found_plate', mock_alpr_event) + self.hass.bus.listen('image_processing.found_plate', mock_alpr_event) self.params = { 'secret_key': "sk_abcxyz123456", diff --git a/tests/components/image_processing/test_openalpr_local.py b/tests/components/image_processing/test_openalpr_local.py index 5186332661b..ffe2eadc8d6 100644 --- a/tests/components/image_processing/test_openalpr_local.py +++ b/tests/components/image_processing/test_openalpr_local.py @@ -134,7 +134,7 @@ class TestOpenAlprLocal(object): """Mock event.""" self.alpr_events.append(event) - self.hass.bus.listen('found_plate', mock_alpr_event) + self.hass.bus.listen('image_processing.found_plate', mock_alpr_event) def teardown_method(self): """Stop everything that was started.""" diff --git a/tests/components/light/test_rflink.py b/tests/components/light/test_rflink.py new file mode 100644 index 00000000000..2ecaa34d84d --- /dev/null +++ b/tests/components/light/test_rflink.py @@ -0,0 +1,373 @@ +"""Test for RFlink light components. + +Test setup of rflink lights component/platform. State tracking and +control of Rflink switch devices. + +""" + +import asyncio + +from homeassistant.components.light import ATTR_BRIGHTNESS +from homeassistant.components.rflink import EVENT_BUTTON_PRESSED +from homeassistant.const import ( + ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON) +from homeassistant.core import callback + +from ..test_rflink import mock_rflink + +DOMAIN = 'light' + +CONFIG = { + 'rflink': { + 'port': '/dev/ttyABC0', + 'ignore_devices': ['ignore_wildcard_*', 'ignore_light'], + }, + DOMAIN: { + 'platform': 'rflink', + 'devices': { + 'protocol_0_0': { + 'name': 'test', + 'aliasses': ['test_alias_0_0'], + }, + 'dimmable_0_0': { + 'name': 'dim_test', + 'type': 'dimmable', + }, + 'switchable_0_0': { + 'name': 'switch_test', + 'type': 'switchable', + } + }, + }, +} + + +@asyncio.coroutine +def test_default_setup(hass, monkeypatch): + """Test all basic functionality of the rflink switch component.""" + # setup mocking rflink module + event_callback, create, protocol, _ = yield from mock_rflink( + hass, CONFIG, DOMAIN, monkeypatch) + + # make sure arguments are passed + assert create.call_args_list[0][1]['ignore'] + + # test default state of light loaded from config + light_initial = hass.states.get('light.test') + assert light_initial.state == 'off' + assert light_initial.attributes['assumed_state'] + + # light should follow state of the hardware device by interpreting + # incoming events for its name and aliasses + + # mock incoming command event for this device + event_callback({ + 'id': 'protocol_0_0', + 'command': 'on', + }) + yield from hass.async_block_till_done() + + light_after_first_command = hass.states.get('light.test') + assert light_after_first_command.state == 'on' + # also after receiving first command state not longer has to be assumed + assert 'assumed_state' not in light_after_first_command.attributes + + # mock incoming command event for this device + event_callback({ + 'id': 'protocol_0_0', + 'command': 'off', + }) + yield from hass.async_block_till_done() + + assert hass.states.get('light.test').state == 'off' + + # test following aliasses + # mock incoming command event for this device alias + event_callback({ + 'id': 'test_alias_0_0', + 'command': 'on', + }) + yield from hass.async_block_till_done() + + assert hass.states.get('light.test').state == 'on' + + # test event for new unconfigured sensor + event_callback({ + 'id': 'protocol2_0_1', + 'command': 'on', + }) + yield from hass.async_block_till_done() + + assert hass.states.get('light.protocol2_0_1').state == 'on' + + # test changing state from HA propagates to Rflink + hass.async_add_job( + hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: 'light.test'})) + yield from hass.async_block_till_done() + assert hass.states.get('light.test').state == 'off' + assert protocol.send_command_ack.call_args_list[0][0][0] == 'protocol_0_0' + assert protocol.send_command_ack.call_args_list[0][0][1] == 'off' + + hass.async_add_job( + hass.services.async_call(DOMAIN, SERVICE_TURN_ON, + {ATTR_ENTITY_ID: 'light.test'})) + yield from hass.async_block_till_done() + assert hass.states.get('light.test').state == 'on' + assert protocol.send_command_ack.call_args_list[1][0][1] == 'on' + + # protocols supporting dimming and on/off should create hybrid light entity + event_callback({ + 'id': 'newkaku_0_1', + 'command': 'off', + }) + yield from hass.async_block_till_done() + hass.async_add_job( + hass.services.async_call(DOMAIN, SERVICE_TURN_ON, + {ATTR_ENTITY_ID: 'light.newkaku_0_1'})) + yield from hass.async_block_till_done() + + # dimmable should send highest dim level when turning on + assert protocol.send_command_ack.call_args_list[2][0][1] == '15' + + # and send on command for fallback + assert protocol.send_command_ack.call_args_list[3][0][1] == 'on' + + hass.async_add_job( + hass.services.async_call(DOMAIN, SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: 'light.newkaku_0_1', + ATTR_BRIGHTNESS: 128, + })) + yield from hass.async_block_till_done() + + assert protocol.send_command_ack.call_args_list[4][0][1] == '7' + + hass.async_add_job( + hass.services.async_call(DOMAIN, SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: 'light.dim_test', + ATTR_BRIGHTNESS: 128, + })) + yield from hass.async_block_till_done() + + assert protocol.send_command_ack.call_args_list[5][0][1] == '7' + + +@asyncio.coroutine +def test_new_light_group(hass, monkeypatch): + """New devices should be added to configured group.""" + config = { + 'rflink': { + 'port': '/dev/ttyABC0', + }, + DOMAIN: { + 'platform': 'rflink', + 'new_devices_group': 'new_rflink_lights', + }, + } + + # setup mocking rflink module + event_callback, _, _, _ = yield from mock_rflink( + hass, config, DOMAIN, monkeypatch) + + # test event for new unconfigured sensor + event_callback({ + 'id': 'protocol_0_0', + 'command': 'off', + }) + yield from hass.async_block_till_done() + + # make sure new device is added to correct group + group = hass.states.get('group.new_rflink_lights') + assert group.attributes.get('entity_id') == ('light.protocol_0_0',) + + +@asyncio.coroutine +def test_firing_bus_event(hass, monkeypatch): + """Incoming Rflink command events should be put on the HA event bus.""" + config = { + 'rflink': { + 'port': '/dev/ttyABC0', + }, + DOMAIN: { + 'platform': 'rflink', + 'devices': { + 'protocol_0_0': { + 'name': 'test', + 'aliasses': ['test_alias_0_0'], + 'fire_event': True, + }, + }, + }, + } + + # setup mocking rflink module + event_callback, _, _, _ = yield from mock_rflink( + hass, config, DOMAIN, monkeypatch) + + calls = [] + + @callback + def listener(event): + calls.append(event) + hass.bus.async_listen_once(EVENT_BUTTON_PRESSED, listener) + + # test event for new unconfigured sensor + event_callback({ + 'id': 'protocol_0_0', + 'command': 'off', + }) + yield from hass.async_block_till_done() + + assert calls[0].data == {'state': 'off', 'entity_id': 'light.test'} + + +@asyncio.coroutine +def test_signal_repetitions(hass, monkeypatch): + """Command should be sent amount of configured repetitions.""" + config = { + 'rflink': { + 'port': '/dev/ttyABC0', + }, + DOMAIN: { + 'platform': 'rflink', + 'device_defaults': { + 'signal_repetitions': 3, + }, + 'devices': { + 'protocol_0_0': { + 'name': 'test', + 'signal_repetitions': 2, + }, + 'protocol_0_1': { + 'name': 'test1', + }, + 'newkaku_0_1': { + 'type': 'hybrid', + } + }, + }, + } + + # setup mocking rflink module + event_callback, _, protocol, _ = yield from mock_rflink( + hass, config, DOMAIN, monkeypatch) + + # test if signal repetition is performed according to configuration + hass.async_add_job( + hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: 'light.test'})) + + # wait for commands and repetitions to finish + yield from hass.async_block_till_done() + + assert protocol.send_command_ack.call_count == 2 + + # test if default apply to configured devcies + hass.async_add_job( + hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: 'light.test1'})) + + # wait for commands and repetitions to finish + yield from hass.async_block_till_done() + + assert protocol.send_command_ack.call_count == 5 + + # test if device defaults apply to newly created devices + event_callback({ + 'id': 'protocol_0_2', + 'command': 'off', + }) + + # make sure entity is created before setting state + yield from hass.async_block_till_done() + + hass.async_add_job( + hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: 'light.protocol_0_2'})) + + # wait for commands and repetitions to finish + yield from hass.async_block_till_done() + + assert protocol.send_command_ack.call_count == 8 + + +@asyncio.coroutine +def test_signal_repetitions_alternation(hass, monkeypatch): + """Simultaneously switching entities must alternate repetitions.""" + config = { + 'rflink': { + 'port': '/dev/ttyABC0', + }, + DOMAIN: { + 'platform': 'rflink', + 'devices': { + 'protocol_0_0': { + 'name': 'test', + 'signal_repetitions': 2, + }, + 'protocol_0_1': { + 'name': 'test1', + 'signal_repetitions': 2, + }, + }, + }, + } + + # setup mocking rflink module + _, _, protocol, _ = yield from mock_rflink( + hass, config, DOMAIN, monkeypatch) + + hass.async_add_job( + hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: 'light.test'})) + hass.async_add_job( + hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: 'light.test1'})) + + yield from hass.async_block_till_done() + + assert protocol.send_command_ack.call_args_list[0][0][0] == 'protocol_0_0' + assert protocol.send_command_ack.call_args_list[1][0][0] == 'protocol_0_1' + assert protocol.send_command_ack.call_args_list[2][0][0] == 'protocol_0_0' + assert protocol.send_command_ack.call_args_list[3][0][0] == 'protocol_0_1' + + +@asyncio.coroutine +def test_signal_repetitions_cancelling(hass, monkeypatch): + """Cancel outstanding repetitions when state changed.""" + config = { + 'rflink': { + 'port': '/dev/ttyABC0', + }, + DOMAIN: { + 'platform': 'rflink', + 'devices': { + 'protocol_0_0': { + 'name': 'test', + 'signal_repetitions': 3, + }, + }, + }, + } + + # setup mocking rflink module + _, _, protocol, _ = yield from mock_rflink( + hass, config, DOMAIN, monkeypatch) + + hass.async_add_job( + hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: 'light.test'})) + + hass.async_add_job( + hass.services.async_call(DOMAIN, SERVICE_TURN_ON, + {ATTR_ENTITY_ID: 'light.test'})) + + yield from hass.async_block_till_done() + + print(protocol.send_command_ack.call_args_list) + assert protocol.send_command_ack.call_args_list[0][0][1] == 'off' + assert protocol.send_command_ack.call_args_list[1][0][1] == 'on' + assert protocol.send_command_ack.call_args_list[2][0][1] == 'on' + assert protocol.send_command_ack.call_args_list[3][0][1] == 'on' diff --git a/tests/components/media_player/test_demo.py b/tests/components/media_player/test_demo.py index d6079ee0351..1e53245d8a5 100644 --- a/tests/components/media_player/test_demo.py +++ b/tests/components/media_player/test_demo.py @@ -155,28 +155,28 @@ class TestDemoMediaPlayer(unittest.TestCase): state = self.hass.states.get(entity_id) assert 1 == state.attributes.get('media_track') assert 0 == (mp.SUPPORT_PREVIOUS_TRACK & - state.attributes.get('supported_media_commands')) + state.attributes.get('supported_features')) mp.media_next_track(self.hass, entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) assert 2 == state.attributes.get('media_track') assert 0 < (mp.SUPPORT_PREVIOUS_TRACK & - state.attributes.get('supported_media_commands')) + state.attributes.get('supported_features')) mp.media_next_track(self.hass, entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) assert 3 == state.attributes.get('media_track') assert 0 < (mp.SUPPORT_PREVIOUS_TRACK & - state.attributes.get('supported_media_commands')) + state.attributes.get('supported_features')) mp.media_previous_track(self.hass, entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) assert 2 == state.attributes.get('media_track') assert 0 < (mp.SUPPORT_PREVIOUS_TRACK & - state.attributes.get('supported_media_commands')) + state.attributes.get('supported_features')) assert setup_component( self.hass, mp.DOMAIN, @@ -185,21 +185,21 @@ class TestDemoMediaPlayer(unittest.TestCase): state = self.hass.states.get(ent_id) assert 1 == state.attributes.get('media_episode') assert 0 == (mp.SUPPORT_PREVIOUS_TRACK & - state.attributes.get('supported_media_commands')) + state.attributes.get('supported_features')) mp.media_next_track(self.hass, ent_id) self.hass.block_till_done() state = self.hass.states.get(ent_id) assert 2 == state.attributes.get('media_episode') assert 0 < (mp.SUPPORT_PREVIOUS_TRACK & - state.attributes.get('supported_media_commands')) + state.attributes.get('supported_features')) mp.media_previous_track(self.hass, ent_id) self.hass.block_till_done() state = self.hass.states.get(ent_id) assert 1 == state.attributes.get('media_episode') assert 0 == (mp.SUPPORT_PREVIOUS_TRACK & - state.attributes.get('supported_media_commands')) + state.attributes.get('supported_features')) @patch('homeassistant.components.media_player.demo.DemoYoutubePlayer.' 'media_seek', autospec=True) @@ -211,21 +211,21 @@ class TestDemoMediaPlayer(unittest.TestCase): ent_id = 'media_player.living_room' state = self.hass.states.get(ent_id) assert 0 < (mp.SUPPORT_PLAY_MEDIA & - state.attributes.get('supported_media_commands')) + state.attributes.get('supported_features')) assert state.attributes.get('media_content_id') is not None mp.play_media(self.hass, None, 'some_id', ent_id) self.hass.block_till_done() state = self.hass.states.get(ent_id) assert 0 < (mp.SUPPORT_PLAY_MEDIA & - state.attributes.get('supported_media_commands')) + state.attributes.get('supported_features')) assert not 'some_id' == state.attributes.get('media_content_id') mp.play_media(self.hass, 'youtube', 'some_id', ent_id) self.hass.block_till_done() state = self.hass.states.get(ent_id) assert 0 < (mp.SUPPORT_PLAY_MEDIA & - state.attributes.get('supported_media_commands')) + state.attributes.get('supported_features')) assert 'some_id' == state.attributes.get('media_content_id') assert not mock_seek.called diff --git a/tests/components/media_player/test_sonos.py b/tests/components/media_player/test_sonos.py index bcbee544f81..58691d44516 100644 --- a/tests/components/media_player/test_sonos.py +++ b/tests/components/media_player/test_sonos.py @@ -52,6 +52,10 @@ class SoCoMock(): """Clear the sleep timer.""" return + def get_sonos_favorites(self): + """Get favorites list from sonos.""" + return {'favorites': []} + def get_speaker_info(self, force): """Return a dict with various data points about the speaker.""" return {'serial_number': 'B8-E9-37-BO-OC-BA:2', diff --git a/tests/components/media_player/test_soundtouch.py b/tests/components/media_player/test_soundtouch.py index 69d80e30f59..84551241694 100644 --- a/tests/components/media_player/test_soundtouch.py +++ b/tests/components/media_player/test_soundtouch.py @@ -318,7 +318,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): default_component(), mock.MagicMock()) self.assertEqual(mocked_sountouch_device.call_count, 1) - self.assertEqual(soundtouch.DEVICES[0].supported_media_commands, 17853) + self.assertEqual(soundtouch.DEVICES[0].supported_features, 17853) @mock.patch('libsoundtouch.device.SoundTouchDevice.power_off') @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') diff --git a/tests/components/media_player/test_universal.py b/tests/components/media_player/test_universal.py index 4a06d989ce2..1ca0846b1fd 100644 --- a/tests/components/media_player/test_universal.py +++ b/tests/components/media_player/test_universal.py @@ -1,4 +1,5 @@ """The tests for the Universal Media player platform.""" +import asyncio from copy import copy import unittest @@ -9,6 +10,7 @@ import homeassistant.components.input_slider as input_slider import homeassistant.components.input_select as input_select import homeassistant.components.media_player as media_player import homeassistant.components.media_player.universal as universal +from homeassistant.util.async import run_coroutine_threadsafe from tests.common import mock_service, get_test_home_assistant @@ -25,7 +27,7 @@ class MockMediaPlayer(media_player.MediaPlayerDevice): self._volume_level = 0 self._is_volume_muted = False self._media_title = None - self._supported_media_commands = 0 + self._supported_features = 0 self._source = None self._tracks = 12 self._media_image_url = None @@ -89,9 +91,9 @@ class MockMediaPlayer(media_player.MediaPlayerDevice): return self._is_volume_muted @property - def supported_media_commands(self): - """Supported media commands flag.""" - return self._supported_media_commands + def supported_features(self): + """Flag media player features that are supported.""" + return self._supported_features @property def media_image_url(self): @@ -256,15 +258,20 @@ class TestMediaPlayer(unittest.TestCase): bad_config = {'platform': 'universal'} entities = [] + @asyncio.coroutine def add_devices(new_entities): """Add devices to list.""" for dev in new_entities: entities.append(dev) - universal.setup_platform(self.hass, bad_config, add_devices) + run_coroutine_threadsafe( + universal.async_setup_platform(self.hass, bad_config, add_devices), + self.hass.loop).result() self.assertEqual(0, len(entities)) - universal.setup_platform(self.hass, config, add_devices) + run_coroutine_threadsafe( + universal.async_setup_platform(self.hass, config, add_devices), + self.hass.loop).result() self.assertEqual(1, len(entities)) self.assertEqual('test', entities[0].name) @@ -305,25 +312,25 @@ class TestMediaPlayer(unittest.TestCase): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) - ump.update() + run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() self.assertEqual(None, ump._child_state) self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.update_ha_state() - ump.update() + run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() self.assertEqual(self.mock_mp_1.entity_id, ump._child_state.entity_id) self.mock_mp_2._state = STATE_PLAYING self.mock_mp_2.update_ha_state() - ump.update() + run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() self.assertEqual(self.mock_mp_1.entity_id, ump._child_state.entity_id) self.mock_mp_1._state = STATE_OFF self.mock_mp_1.update_ha_state() - ump.update() + run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() self.assertEqual(self.mock_mp_2.entity_id, ump._child_state.entity_id) @@ -352,13 +359,13 @@ class TestMediaPlayer(unittest.TestCase): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) - ump.update() + run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() self.assertTrue(ump.state, STATE_OFF) self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.update_ha_state() - ump.update() + run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() self.assertEqual(STATE_PLAYING, ump.state) def test_state_with_children_and_attrs(self): @@ -368,21 +375,21 @@ class TestMediaPlayer(unittest.TestCase): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) - ump.update() + run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() self.assertEqual(STATE_OFF, ump.state) self.hass.states.set(self.mock_state_switch_id, STATE_ON) - ump.update() + run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() self.assertEqual(STATE_ON, ump.state) self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.update_ha_state() - ump.update() + run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() self.assertEqual(STATE_PLAYING, ump.state) self.hass.states.set(self.mock_state_switch_id, STATE_OFF) - ump.update() + run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() self.assertEqual(STATE_OFF, ump.state) def test_volume_level(self): @@ -392,18 +399,18 @@ class TestMediaPlayer(unittest.TestCase): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) - ump.update() + run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() self.assertEqual(None, ump.volume_level) self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.update_ha_state() - ump.update() + run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() self.assertEqual(0, ump.volume_level) self.mock_mp_1._volume_level = 1 self.mock_mp_1.update_ha_state() - ump.update() + run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() self.assertEqual(1, ump.volume_level) def test_media_image_url(self): @@ -414,14 +421,14 @@ class TestMediaPlayer(unittest.TestCase): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) - ump.update() + run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() self.assertEqual(None, ump.media_image_url) self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1._media_image_url = TEST_URL self.mock_mp_1.update_ha_state() - ump.update() + run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() # mock_mp_1 will convert the url to the api proxy url. This test # ensures ump passes through the same url without an additional proxy. self.assertEqual(self.mock_mp_1.entity_picture, ump.entity_picture) @@ -433,18 +440,18 @@ class TestMediaPlayer(unittest.TestCase): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) - ump.update() + run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() self.assertFalse(ump.is_volume_muted) self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.update_ha_state() - ump.update() + run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() self.assertFalse(ump.is_volume_muted) self.mock_mp_1._is_volume_muted = True self.mock_mp_1.update_ha_state() - ump.update() + run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() self.assertTrue(ump.is_volume_muted) def test_source_list_children_and_attr(self): @@ -495,24 +502,24 @@ class TestMediaPlayer(unittest.TestCase): self.hass.states.set(self.mock_mute_switch_id, STATE_ON) self.assertTrue(ump.is_volume_muted) - def test_supported_media_commands_children_only(self): + def test_supported_features_children_only(self): """Test supported media commands with only children.""" config = self.config_children_only universal.validate_config(config) ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) - ump.update() + run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() - self.assertEqual(0, ump.supported_media_commands) + self.assertEqual(0, ump.supported_features) - self.mock_mp_1._supported_media_commands = 512 + self.mock_mp_1._supported_features = 512 self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.update_ha_state() - ump.update() - self.assertEqual(512, ump.supported_media_commands) + run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + self.assertEqual(512, ump.supported_features) - def test_supported_media_commands_children_and_cmds(self): + def test_supported_features_children_and_cmds(self): """Test supported media commands with children and attrs.""" config = self.config_children_and_attr universal.validate_config(config) @@ -526,17 +533,17 @@ class TestMediaPlayer(unittest.TestCase): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) - ump.update() + run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.update_ha_state() - ump.update() + run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() check_flags = universal.SUPPORT_TURN_ON | universal.SUPPORT_TURN_OFF \ | universal.SUPPORT_VOLUME_STEP | universal.SUPPORT_VOLUME_MUTE \ | universal.SUPPORT_SELECT_SOURCE - self.assertEqual(check_flags, ump.supported_media_commands) + self.assertEqual(check_flags, ump.supported_features) def test_service_call_no_active_child(self): """Test a service call to children with no active child.""" @@ -545,15 +552,17 @@ class TestMediaPlayer(unittest.TestCase): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) - ump.update() + run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() self.mock_mp_1._state = STATE_OFF self.mock_mp_1.update_ha_state() self.mock_mp_2._state = STATE_OFF self.mock_mp_2.update_ha_state() - ump.update() + run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() - ump.turn_off() + run_coroutine_threadsafe( + ump.async_turn_off(), + self.hass.loop).result() self.assertEqual(0, len(self.mock_mp_1.service_calls['turn_off'])) self.assertEqual(0, len(self.mock_mp_2.service_calls['turn_off'])) @@ -564,60 +573,90 @@ class TestMediaPlayer(unittest.TestCase): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) - ump.update() + run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() self.mock_mp_2._state = STATE_PLAYING self.mock_mp_2.update_ha_state() - ump.update() + run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() - ump.turn_off() + run_coroutine_threadsafe( + ump.async_turn_off(), + self.hass.loop).result() self.assertEqual(1, len(self.mock_mp_2.service_calls['turn_off'])) - ump.turn_on() + run_coroutine_threadsafe( + ump.async_turn_on(), + self.hass.loop).result() self.assertEqual(1, len(self.mock_mp_2.service_calls['turn_on'])) - ump.mute_volume(True) + run_coroutine_threadsafe( + ump.async_mute_volume(True), + self.hass.loop).result() self.assertEqual(1, len(self.mock_mp_2.service_calls['mute_volume'])) - ump.set_volume_level(0.5) + run_coroutine_threadsafe( + ump.async_set_volume_level(0.5), + self.hass.loop).result() self.assertEqual( 1, len(self.mock_mp_2.service_calls['set_volume_level'])) - ump.media_play() + run_coroutine_threadsafe( + ump.async_media_play(), + self.hass.loop).result() self.assertEqual(1, len(self.mock_mp_2.service_calls['media_play'])) - ump.media_pause() + run_coroutine_threadsafe( + ump.async_media_pause(), + self.hass.loop).result() self.assertEqual(1, len(self.mock_mp_2.service_calls['media_pause'])) - ump.media_previous_track() + run_coroutine_threadsafe( + ump.async_media_previous_track(), + self.hass.loop).result() self.assertEqual( 1, len(self.mock_mp_2.service_calls['media_previous_track'])) - ump.media_next_track() + run_coroutine_threadsafe( + ump.async_media_next_track(), + self.hass.loop).result() self.assertEqual( 1, len(self.mock_mp_2.service_calls['media_next_track'])) - ump.media_seek(100) + run_coroutine_threadsafe( + ump.async_media_seek(100), + self.hass.loop).result() self.assertEqual(1, len(self.mock_mp_2.service_calls['media_seek'])) - ump.play_media('movie', 'batman') + run_coroutine_threadsafe( + ump.async_play_media('movie', 'batman'), + self.hass.loop).result() self.assertEqual(1, len(self.mock_mp_2.service_calls['play_media'])) - ump.volume_up() + run_coroutine_threadsafe( + ump.async_volume_up(), + self.hass.loop).result() self.assertEqual(1, len(self.mock_mp_2.service_calls['volume_up'])) - ump.volume_down() + run_coroutine_threadsafe( + ump.async_volume_down(), + self.hass.loop).result() self.assertEqual(1, len(self.mock_mp_2.service_calls['volume_down'])) - ump.media_play_pause() + run_coroutine_threadsafe( + ump.async_media_play_pause(), + self.hass.loop).result() self.assertEqual( 1, len(self.mock_mp_2.service_calls['media_play_pause'])) - ump.select_source('dvd') + run_coroutine_threadsafe( + ump.async_select_source('dvd'), + self.hass.loop).result() self.assertEqual( 1, len(self.mock_mp_2.service_calls['select_source'])) - ump.clear_playlist() + run_coroutine_threadsafe( + ump.async_clear_playlist(), + self.hass.loop).result() self.assertEqual( 1, len(self.mock_mp_2.service_calls['clear_playlist'])) @@ -632,11 +671,11 @@ class TestMediaPlayer(unittest.TestCase): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) - ump.update() + run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() self.mock_mp_2._state = STATE_PLAYING self.mock_mp_2.update_ha_state() - ump.update() + run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() - ump.turn_off() + run_coroutine_threadsafe(ump.async_turn_off(), self.hass.loop).result() self.assertEqual(1, len(service)) diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py new file mode 100644 index 00000000000..bf6fa2f2603 --- /dev/null +++ b/tests/components/mqtt/test_discovery.py @@ -0,0 +1,74 @@ +"""The tests for the MQTT component.""" +import asyncio +from unittest.mock import patch + +from homeassistant.components.mqtt.discovery import async_start + +from tests.common import async_fire_mqtt_message, mock_coro + + +@asyncio.coroutine +def test_subscribing_config_topic(hass, mqtt_mock): + """Test setting up discovery.""" + hass_config = {} + discovery_topic = 'homeassistant' + async_start(hass, discovery_topic, hass_config) + assert mqtt_mock.subscribe.called + call_args = mqtt_mock.subscribe.mock_calls[0][1] + assert call_args[0] == discovery_topic + '/#' + assert call_args[1] == 0 + + +@asyncio.coroutine +@patch('homeassistant.components.mqtt.discovery.async_load_platform') +def test_invalid_topic(mock_load_platform, hass, mqtt_mock): + """Test sending in invalid JSON.""" + mock_load_platform.return_value = mock_coro() + async_start(hass, 'homeassistant', {}) + + async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/not_config', + '{}') + yield from hass.async_block_till_done() + assert not mock_load_platform.called + + +@asyncio.coroutine +@patch('homeassistant.components.mqtt.discovery.async_load_platform') +def test_invalid_json(mock_load_platform, hass, mqtt_mock, caplog): + """Test sending in invalid JSON.""" + mock_load_platform.return_value = mock_coro() + async_start(hass, 'homeassistant', {}) + + async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/config', + 'not json') + yield from hass.async_block_till_done() + assert 'Unable to parse JSON' in caplog.text + assert not mock_load_platform.called + + +@asyncio.coroutine +@patch('homeassistant.components.mqtt.discovery.async_load_platform') +def test_only_valid_components(mock_load_platform, hass, mqtt_mock, caplog): + """Test sending in invalid JSON.""" + mock_load_platform.return_value = mock_coro() + async_start(hass, 'homeassistant', {}) + + async_fire_mqtt_message(hass, 'homeassistant/climate/bla/config', '{}') + yield from hass.async_block_till_done() + assert 'Component climate is not supported' in caplog.text + assert not mock_load_platform.called + + +@asyncio.coroutine +def test_correct_config_discovery(hass, mqtt_mock, caplog): + """Test sending in invalid JSON.""" + async_start(hass, 'homeassistant', {}) + + async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/config', + '{ "name": "Beer" }') + yield from hass.async_block_till_done() + + state = hass.states.get('binary_sensor.beer') + + assert state is not None + assert state.name == 'Beer' diff --git a/tests/components/recorder/models_original.py b/tests/components/recorder/models_original.py new file mode 100644 index 00000000000..31ec5ee7ed7 --- /dev/null +++ b/tests/components/recorder/models_original.py @@ -0,0 +1,163 @@ +"""Models for SQLAlchemy. + +This file contains the original models definitions before schema tracking was +implemented. It is used to test the schema migration logic. +""" + +import json +from datetime import datetime +import logging + +from sqlalchemy import (Boolean, Column, DateTime, ForeignKey, Index, Integer, + String, Text, distinct) +from sqlalchemy.ext.declarative import declarative_base + +import homeassistant.util.dt as dt_util +from homeassistant.core import Event, EventOrigin, State, split_entity_id +from homeassistant.remote import JSONEncoder + +# SQLAlchemy Schema +# pylint: disable=invalid-name +Base = declarative_base() + +_LOGGER = logging.getLogger(__name__) + + +class Events(Base): # type: ignore + """Event history data.""" + + __tablename__ = 'events' + event_id = Column(Integer, primary_key=True) + event_type = Column(String(32), index=True) + event_data = Column(Text) + origin = Column(String(32)) + time_fired = Column(DateTime(timezone=True)) + created = Column(DateTime(timezone=True), default=datetime.utcnow) + + @staticmethod + def from_event(event): + """Create an event database object from a native event.""" + return Events(event_type=event.event_type, + event_data=json.dumps(event.data, cls=JSONEncoder), + origin=str(event.origin), + time_fired=event.time_fired) + + def to_native(self): + """Convert to a natve HA Event.""" + try: + return Event( + self.event_type, + json.loads(self.event_data), + EventOrigin(self.origin), + _process_timestamp(self.time_fired) + ) + except ValueError: + # When json.loads fails + _LOGGER.exception("Error converting to event: %s", self) + return None + + +class States(Base): # type: ignore + """State change history.""" + + __tablename__ = 'states' + state_id = Column(Integer, primary_key=True) + domain = Column(String(64)) + entity_id = Column(String(255)) + state = Column(String(255)) + attributes = Column(Text) + event_id = Column(Integer, ForeignKey('events.event_id')) + last_changed = Column(DateTime(timezone=True), default=datetime.utcnow) + last_updated = Column(DateTime(timezone=True), default=datetime.utcnow) + created = Column(DateTime(timezone=True), default=datetime.utcnow) + + __table_args__ = (Index('states__state_changes', + 'last_changed', 'last_updated', 'entity_id'), + Index('states__significant_changes', + 'domain', 'last_updated', 'entity_id'), ) + + @staticmethod + def from_event(event): + """Create object from a state_changed event.""" + entity_id = event.data['entity_id'] + state = event.data.get('new_state') + + dbstate = States(entity_id=entity_id) + + # State got deleted + if state is None: + dbstate.state = '' + dbstate.domain = split_entity_id(entity_id)[0] + dbstate.attributes = '{}' + dbstate.last_changed = event.time_fired + dbstate.last_updated = event.time_fired + else: + dbstate.domain = state.domain + dbstate.state = state.state + dbstate.attributes = json.dumps(dict(state.attributes), + cls=JSONEncoder) + dbstate.last_changed = state.last_changed + dbstate.last_updated = state.last_updated + + return dbstate + + def to_native(self): + """Convert to an HA state object.""" + try: + return State( + self.entity_id, self.state, + json.loads(self.attributes), + _process_timestamp(self.last_changed), + _process_timestamp(self.last_updated) + ) + except ValueError: + # When json.loads fails + _LOGGER.exception("Error converting row to state: %s", self) + return None + + +class RecorderRuns(Base): # type: ignore + """Representation of recorder run.""" + + __tablename__ = 'recorder_runs' + run_id = Column(Integer, primary_key=True) + start = Column(DateTime(timezone=True), default=datetime.utcnow) + end = Column(DateTime(timezone=True)) + closed_incorrect = Column(Boolean, default=False) + created = Column(DateTime(timezone=True), default=datetime.utcnow) + + def entity_ids(self, point_in_time=None): + """Return the entity ids that existed in this run. + + Specify point_in_time if you want to know which existed at that point + in time inside the run. + """ + from sqlalchemy.orm.session import Session + + session = Session.object_session(self) + + assert session is not None, 'RecorderRuns need to be persisted' + + query = session.query(distinct(States.entity_id)).filter( + States.last_updated >= self.start) + + if point_in_time is not None: + query = query.filter(States.last_updated < point_in_time) + elif self.end is not None: + query = query.filter(States.last_updated < self.end) + + return [row[0] for row in query] + + def to_native(self): + """Return self, native format is this model.""" + return self + + +def _process_timestamp(ts): + """Process a timestamp into datetime object.""" + if ts is None: + return None + elif ts.tzinfo is None: + return dt_util.UTC.localize(ts) + else: + return dt_util.as_utc(ts) diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index e8a73e347ff..0bfa3a20997 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -3,17 +3,21 @@ import json from datetime import datetime, timedelta import unittest +from unittest.mock import patch, call, MagicMock import pytest +from sqlalchemy import create_engine + from homeassistant.core import callback from homeassistant.const import MATCH_ALL from homeassistant.components import recorder from homeassistant.bootstrap import setup_component from tests.common import get_test_home_assistant +from tests.components.recorder import models_original -class TestRecorder(unittest.TestCase): - """Test the recorder module.""" +class BaseTestRecorder(unittest.TestCase): + """Base class for common recorder tests.""" def setUp(self): # pylint: disable=invalid-name """Setup things to be run when tests are started.""" @@ -23,7 +27,6 @@ class TestRecorder(unittest.TestCase): recorder.DOMAIN: {recorder.CONF_DB_URL: db_uri}}) self.hass.start() recorder._verify_instance() - self.session = recorder.Session() recorder._INSTANCE.block_till_done() def tearDown(self): # pylint: disable=invalid-name @@ -41,26 +44,25 @@ class TestRecorder(unittest.TestCase): self.hass.block_till_done() recorder._INSTANCE.block_till_done() - for event_id in range(5): - if event_id < 3: - timestamp = five_days_ago - state = 'purgeme' - else: - timestamp = now - state = 'dontpurgeme' + with recorder.session_scope() as session: + for event_id in range(5): + if event_id < 3: + timestamp = five_days_ago + state = 'purgeme' + else: + timestamp = now + state = 'dontpurgeme' - self.session.add(recorder.get_model('States')( - entity_id='test.recorder2', - domain='sensor', - state=state, - attributes=json.dumps(attributes), - last_changed=timestamp, - last_updated=timestamp, - created=timestamp, - event_id=event_id + 1000 - )) - - self.session.commit() + session.add(recorder.get_model('States')( + entity_id='test.recorder2', + domain='sensor', + state=state, + attributes=json.dumps(attributes), + last_changed=timestamp, + last_updated=timestamp, + created=timestamp, + event_id=event_id + 1000 + )) def _add_test_events(self): """Add a few events for testing.""" @@ -70,21 +72,27 @@ class TestRecorder(unittest.TestCase): self.hass.block_till_done() recorder._INSTANCE.block_till_done() - for event_id in range(5): - if event_id < 2: - timestamp = five_days_ago - event_type = 'EVENT_TEST_PURGE' - else: - timestamp = now - event_type = 'EVENT_TEST' - self.session.add(recorder.get_model('Events')( - event_type=event_type, - event_data=json.dumps(event_data), - origin='LOCAL', - created=timestamp, - time_fired=timestamp, - )) + with recorder.session_scope() as session: + for event_id in range(5): + if event_id < 2: + timestamp = five_days_ago + event_type = 'EVENT_TEST_PURGE' + else: + timestamp = now + event_type = 'EVENT_TEST' + + session.add(recorder.get_model('Events')( + event_type=event_type, + event_data=json.dumps(event_data), + origin='LOCAL', + created=timestamp, + time_fired=timestamp, + )) + + +class TestRecorder(BaseTestRecorder): + """Test the recorder module.""" def test_saving_state(self): """Test saving and restoring a state.""" @@ -190,13 +198,70 @@ class TestRecorder(unittest.TestCase): self.assertEqual(states.count(), 5) self.assertEqual(events.count(), 5) + def test_schema_no_recheck(self): + """Test that schema is not double-checked when up-to-date.""" + with patch.object(recorder._INSTANCE, '_apply_update') as update, \ + patch.object(recorder._INSTANCE, '_inspect_schema_version') \ + as inspect: + recorder._INSTANCE._migrate_schema() + self.assertEqual(update.call_count, 0) + self.assertEqual(inspect.call_count, 0) + + def test_invalid_update(self): + """Test that an invalid new version raises an exception.""" + with self.assertRaises(ValueError): + recorder._INSTANCE._apply_update(-1) + + +def create_engine_test(*args, **kwargs): + """Test version of create_engine that initializes with old schema. + + This simulates an existing db with the old schema. + """ + engine = create_engine(*args, **kwargs) + models_original.Base.metadata.create_all(engine) + return engine + + +class TestMigrateRecorder(BaseTestRecorder): + """Test recorder class that starts with an original schema db.""" + + @patch('sqlalchemy.create_engine', new=create_engine_test) + @patch('homeassistant.components.recorder.Recorder._migrate_schema') + def setUp(self, migrate): # pylint: disable=invalid-name + """Setup things to be run when tests are started. + + create_engine is patched to create a db that starts with the old + schema. + + _migrate_schema is mocked to ensure it isn't run, so we can test it + below. + """ + super().setUp() + + def test_schema_update_calls(self): # pylint: disable=no-self-use + """Test that schema migrations occurr in correct order.""" + with patch.object(recorder._INSTANCE, '_apply_update') as update: + recorder._INSTANCE._migrate_schema() + update.assert_has_calls([call(version+1) for version in range( + 0, recorder.models.SCHEMA_VERSION)]) + + def test_schema_migrate(self): # pylint: disable=no-self-use + """Test the full schema migration logic. + + We're just testing that the logic can execute successfully here without + throwing exceptions. Maintaining a set of assertions based on schema + inspection could quickly become quite cumbersome. + """ + recorder._INSTANCE._migrate_schema() + @pytest.fixture def hass_recorder(): """HASS fixture with in-memory recorder.""" hass = get_test_home_assistant() - def setup_recorder(config): + def setup_recorder(config={}): """Setup with params.""" db_uri = 'sqlite://' # In memory DB conf = {recorder.CONF_DB_URL: db_uri} @@ -277,3 +342,61 @@ def test_saving_state_include_domain_exclude_entity(hass_recorder): assert len(states) == 1 assert hass.states.get('test.ok') == states[0] assert hass.states.get('test.ok').state == 'state2' + + +def test_recorder_errors_exceptions(hass_recorder): \ + # pylint: disable=redefined-outer-name + """Test session_scope and get_model errors.""" + # Model cannot be resolved + assert recorder.get_model('dont-exist') is None + + # Verify the instance fails before setup + with pytest.raises(RuntimeError): + recorder._verify_instance() + + # Setup the recorder + hass_recorder() + + recorder._verify_instance() + + # Verify session scope raises (and prints) an exception + with patch('homeassistant.components.recorder._LOGGER.error') as e_mock, \ + pytest.raises(Exception) as err: + with recorder.session_scope() as session: + session.execute('select * from notthere') + assert e_mock.call_count == 1 + assert recorder.ERROR_QUERY[:-4] in e_mock.call_args[0][0] + assert 'no such table' in str(err.value) + + +def test_recorder_bad_commit(hass_recorder): + """Bad _commit should retry 3 times.""" + hass_recorder() + + def work(session): + """Bad work.""" + session.execute('select * from notthere') + + with patch('homeassistant.components.recorder.time.sleep') as e_mock, \ + recorder.session_scope() as session: + res = recorder._INSTANCE._commit(session, work) + assert res is False + assert e_mock.call_count == 3 + + +def test_recorder_bad_execute(hass_recorder): + """Bad execute, retry 3 times.""" + hass_recorder() + + def to_native(): + """Rasie exception.""" + from sqlalchemy.exc import SQLAlchemyError + raise SQLAlchemyError() + + mck1 = MagicMock() + mck1.to_native = to_native + + with patch('homeassistant.components.recorder.time.sleep') as e_mock: + res = recorder.execute((mck1,)) + assert res == [] + assert e_mock.call_count == 3 diff --git a/tests/components/sensor/test_imap_email_content.py b/tests/components/sensor/test_imap_email_content.py index 17619f1efa6..f8e5caf0dd2 100644 --- a/tests/components/sensor/test_imap_email_content.py +++ b/tests/components/sensor/test_imap_email_content.py @@ -46,8 +46,8 @@ class EmailContentSensor(unittest.TestCase): def test_allowed_sender(self): """Test emails from allowed sender.""" test_message = email.message.Message() - test_message['From'] = "sender@test.com" - test_message['Subject'] = "Test" + test_message['From'] = 'sender@test.com' + test_message['Subject'] = 'Test' test_message['Date'] = datetime.datetime(2016, 1, 1, 12, 44, 57) test_message.set_payload("Test Message") @@ -58,19 +58,20 @@ class EmailContentSensor(unittest.TestCase): ["sender@test.com"], None) - sensor.entity_id = "sensor.emailtest" + sensor.entity_id = 'sensor.emailtest' sensor.update() self.assertEqual("Test Message", sensor.state) - self.assertEqual("sender@test.com", sensor.state_attributes["from"]) - self.assertEqual("Test", sensor.state_attributes["subject"]) + self.assertEqual('sender@test.com', + sensor.device_state_attributes['from']) + self.assertEqual('Test', sensor.device_state_attributes['subject']) self.assertEqual(datetime.datetime(2016, 1, 1, 12, 44, 57), - sensor.state_attributes["date"]) + sensor.device_state_attributes['date']) def test_multi_part_with_text(self): """Test multi part emails.""" msg = MIMEMultipart('alternative') - msg['Subject'] = "Link" - msg['From'] = "sender@test.com" + msg['Subject'] = 'Link' + msg['From'] = 'sender@test.com' text = "Test Message" html = "Test Message" @@ -82,11 +83,8 @@ class EmailContentSensor(unittest.TestCase): msg.attach(htmlPart) sensor = imap_email_content.EmailContentSensor( - self.hass, - FakeEMailReader(deque([msg])), - "test_emails_sensor", - ["sender@test.com"], - None) + self.hass, FakeEMailReader(deque([msg])), 'test_emails_sensor', + ['sender@test.com'], None) sensor.entity_id = "sensor.emailtest" sensor.update() @@ -107,11 +105,11 @@ class EmailContentSensor(unittest.TestCase): sensor = imap_email_content.EmailContentSensor( self.hass, FakeEMailReader(deque([msg])), - "test_emails_sensor", - ["sender@test.com"], + 'test_emails_sensor', + ['sender@test.com'], None) - sensor.entity_id = "sensor.emailtest" + sensor.entity_id = 'sensor.emailtest' sensor.update() self.assertEqual( "Test Message", @@ -120,8 +118,8 @@ class EmailContentSensor(unittest.TestCase): def test_multi_part_only_other_text(self): """Test multi part emails with only other text.""" msg = MIMEMultipart('alternative') - msg['Subject'] = "Link" - msg['From'] = "sender@test.com" + msg['Subject'] = 'Link' + msg['From'] = 'sender@test.com' other = "Test Message" @@ -130,13 +128,10 @@ class EmailContentSensor(unittest.TestCase): msg.attach(htmlPart) sensor = imap_email_content.EmailContentSensor( - self.hass, - FakeEMailReader(deque([msg])), - "test_emails_sensor", - ["sender@test.com"], - None) + self.hass, FakeEMailReader(deque([msg])), 'test_emails_sensor', + ['sender@test.com'], None) - sensor.entity_id = "sensor.emailtest" + sensor.entity_id = 'sensor.emailtest' sensor.update() self.assertEqual("Test Message", sensor.state) @@ -145,14 +140,14 @@ class EmailContentSensor(unittest.TestCase): states = [] test_message1 = email.message.Message() - test_message1['From'] = "sender@test.com" - test_message1['Subject'] = "Test" + test_message1['From'] = 'sender@test.com' + test_message1['Subject'] = 'Test' test_message1['Date'] = datetime.datetime(2016, 1, 1, 12, 44, 57) test_message1.set_payload("Test Message") test_message2 = email.message.Message() - test_message2['From'] = "sender@test.com" - test_message2['Subject'] = "Test 2" + test_message2['From'] = 'sender@test.com' + test_message2['Subject'] = 'Test 2' test_message2['Date'] = datetime.datetime(2016, 1, 1, 12, 44, 57) test_message2.set_payload("Test Message 2") @@ -164,18 +159,14 @@ class EmailContentSensor(unittest.TestCase): states_received.set() track_state_change( - self.hass, - ["sensor.emailtest"], - state_changed_listener) + self.hass, ['sensor.emailtest'], state_changed_listener) sensor = imap_email_content.EmailContentSensor( self.hass, FakeEMailReader(deque([test_message1, test_message2])), - "test_emails_sensor", - ["sender@test.com"], - None) + 'test_emails_sensor', ['sender@test.com'], None) - sensor.entity_id = "sensor.emailtest" + sensor.entity_id = 'sensor.emailtest' sensor.update() self.hass.block_till_done() @@ -189,39 +180,34 @@ class EmailContentSensor(unittest.TestCase): def test_sender_not_allowed(self): """Test not whitelisted emails.""" test_message = email.message.Message() - test_message['From'] = "sender@test.com" - test_message['Subject'] = "Test" + test_message['From'] = 'sender@test.com' + test_message['Subject'] = 'Test' test_message['Date'] = datetime.datetime(2016, 1, 1, 12, 44, 57) test_message.set_payload("Test Message") sensor = imap_email_content.EmailContentSensor( - self.hass, - FakeEMailReader(deque([test_message])), - "test_emails_sensor", - ["other@test.com"], - None) + self.hass, FakeEMailReader(deque([test_message])), + 'test_emails_sensor', ['other@test.com'], None) - sensor.entity_id = "sensor.emailtest" + sensor.entity_id = 'sensor.emailtest' sensor.update() self.assertEqual(None, sensor.state) def test_template(self): """Test value template.""" test_message = email.message.Message() - test_message['From'] = "sender@test.com" - test_message['Subject'] = "Test" + test_message['From'] = 'sender@test.com' + test_message['Subject'] = 'Test' test_message['Date'] = datetime.datetime(2016, 1, 1, 12, 44, 57) test_message.set_payload("Test Message") sensor = imap_email_content.EmailContentSensor( - self.hass, - FakeEMailReader(deque([test_message])), - "test_emails_sensor", - ["sender@test.com"], + self.hass, FakeEMailReader(deque([test_message])), + 'test_emails_sensor', ['sender@test.com'], Template("{{ subject }} from {{ from }} with message {{ body }}", self.hass)) - sensor.entity_id = "sensor.emailtest" + sensor.entity_id = 'sensor.emailtest' sensor.update() self.assertEqual( "Test from sender@test.com with message Test Message", diff --git a/tests/components/sensor/test_moon.py b/tests/components/sensor/test_moon.py new file mode 100644 index 00000000000..1125dab1201 --- /dev/null +++ b/tests/components/sensor/test_moon.py @@ -0,0 +1,56 @@ +"""The test for the moon sensor platform.""" +import unittest +from datetime import datetime +from unittest.mock import patch + +import homeassistant.util.dt as dt_util +from homeassistant.bootstrap import setup_component + +from tests.common import get_test_home_assistant + +DAY1 = datetime(2017, 1, 1, 1, tzinfo=dt_util.UTC) +DAY2 = datetime(2017, 1, 18, 1, tzinfo=dt_util.UTC) + + +class TestMoonSensor(unittest.TestCase): + """Test the Moon sensor.""" + + def setup_method(self, method): + """Set up things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + + @patch('homeassistant.components.sensor.moon.dt_util.utcnow', + return_value=DAY1) + def test_moon_day1(self, mock_request): + """Test the Moon sensor.""" + config = { + 'sensor': { + 'platform': 'moon', + 'name': 'moon_day1', + } + } + + assert setup_component(self.hass, 'sensor', config) + + state = self.hass.states.get('sensor.moon_day1') + self.assertEqual(state.state, 'New moon') + + @patch('homeassistant.components.sensor.moon.dt_util.utcnow', + return_value=DAY2) + def test_moon_day2(self, mock_request): + """Test the Moon sensor.""" + config = { + 'sensor': { + 'platform': 'moon', + 'name': 'moon_day2', + } + } + + assert setup_component(self.hass, 'sensor', config) + + state = self.hass.states.get('sensor.moon_day2') + self.assertEqual(state.state, 'Full moon') diff --git a/tests/components/sensor/test_rflink.py b/tests/components/sensor/test_rflink.py new file mode 100644 index 00000000000..8d0c8180f1a --- /dev/null +++ b/tests/components/sensor/test_rflink.py @@ -0,0 +1,103 @@ +"""Test for RFlink sensor components. + +Test setup of rflink sensor component/platform. Verify manual and +automatic sensor creation. + +""" + +import asyncio + +from ..test_rflink import mock_rflink + +DOMAIN = 'sensor' + +CONFIG = { + 'rflink': { + 'port': '/dev/ttyABC0', + 'ignore_devices': ['ignore_wildcard_*', 'ignore_sensor'], + }, + DOMAIN: { + 'platform': 'rflink', + 'devices': { + 'test': { + 'name': 'test', + 'sensor_type': 'temperature', + }, + }, + }, +} + + +@asyncio.coroutine +def test_default_setup(hass, monkeypatch): + """Test all basic functionality of the rflink sensor component.""" + # setup mocking rflink module + event_callback, create, _, _ = yield from mock_rflink( + hass, CONFIG, DOMAIN, monkeypatch) + + # make sure arguments are passed + assert create.call_args_list[0][1]['ignore'] + + # test default state of sensor loaded from config + config_sensor = hass.states.get('sensor.test') + assert config_sensor + assert config_sensor.state == 'unknown' + assert config_sensor.attributes['unit_of_measurement'] == '°C' + + # test event for config sensor + event_callback({ + 'id': 'test', + 'sensor': 'temperature', + 'value': 1, + 'unit': '°C', + }) + yield from hass.async_block_till_done() + + assert hass.states.get('sensor.test').state == '1' + + # test event for new unconfigured sensor + event_callback({ + 'id': 'test2', + 'sensor': 'temperature', + 'value': 0, + 'unit': '°C', + }) + yield from hass.async_block_till_done() + + # test state of new sensor + new_sensor = hass.states.get('sensor.test2') + assert new_sensor + assert new_sensor.state == '0' + assert new_sensor.attributes['unit_of_measurement'] == '°C' + assert new_sensor.attributes['icon'] == 'mdi:thermometer' + + +@asyncio.coroutine +def test_new_sensors_group(hass, monkeypatch): + """New devices should be added to configured group.""" + config = { + 'rflink': { + 'port': '/dev/ttyABC0', + }, + DOMAIN: { + 'platform': 'rflink', + 'new_devices_group': 'new_rflink_sensors', + }, + } + + # setup mocking rflink module + event_callback, _, _, _ = yield from mock_rflink( + hass, config, DOMAIN, monkeypatch) + + # test event for new unconfigured sensor + event_callback({ + 'id': 'test', + 'sensor': 'temperature', + 'value': 0, + 'unit': '°C', + }) + yield from hass.async_block_till_done() + + # make sure new device is added to correct group + group = hass.states.get('group.new_rflink_sensors') + assert group.attributes.get('entity_id') == ('sensor.test',) diff --git a/tests/components/sensor/test_template.py b/tests/components/sensor/test_template.py index 58f0fb84ac7..0f5e863f328 100644 --- a/tests/components/sensor/test_template.py +++ b/tests/components/sensor/test_template.py @@ -41,6 +41,33 @@ class TestTemplateSensor: state = self.hass.states.get('sensor.test_template_sensor') assert state.state == 'It Works.' + def test_icon_template(self): + """Test icon template.""" + with assert_setup_component(1): + assert setup_component(self.hass, 'sensor', { + 'sensor': { + 'platform': 'template', + 'sensors': { + 'test_template_sensor': { + 'value_template': "State", + 'icon_template': + "{% if states.sensor.test_state.state == " + "'Works' %}" + "mdi:check" + "{% endif %}" + } + } + } + }) + + state = self.hass.states.get('sensor.test_template_sensor') + assert 'icon' not in state.attributes + + self.hass.states.set('sensor.test_state', 'Works') + self.hass.block_till_done() + state = self.hass.states.get('sensor.test_template_sensor') + assert state.attributes['icon'] == 'mdi:check' + def test_template_syntax_error(self): """Test templating syntax error.""" with assert_setup_component(0): diff --git a/tests/components/switch/test_rflink.py b/tests/components/switch/test_rflink.py new file mode 100644 index 00000000000..f5a16e14d07 --- /dev/null +++ b/tests/components/switch/test_rflink.py @@ -0,0 +1,100 @@ +"""Test for RFlink switch components. + +Test setup of rflink switch component/platform. State tracking and +control of Rflink switch devices. + +""" + +import asyncio + +from homeassistant.const import ( + ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON) + +from ..test_rflink import mock_rflink + +DOMAIN = 'switch' + +CONFIG = { + 'rflink': { + 'port': '/dev/ttyABC0', + 'ignore_devices': ['ignore_wildcard_*', 'ignore_sensor'], + }, + DOMAIN: { + 'platform': 'rflink', + 'devices': { + 'protocol_0_0': { + 'name': 'test', + 'aliasses': ['test_alias_0_0'], + }, + }, + }, +} + + +@asyncio.coroutine +def test_default_setup(hass, monkeypatch): + """Test all basic functionality of the rflink switch component.""" + # setup mocking rflink module + event_callback, create, protocol, _ = yield from mock_rflink( + hass, CONFIG, DOMAIN, monkeypatch) + + # make sure arguments are passed + assert create.call_args_list[0][1]['ignore'] + + # test default state of switch loaded from config + switch_initial = hass.states.get('switch.test') + assert switch_initial.state == 'off' + assert switch_initial.attributes['assumed_state'] + + # switch should follow state of the hardware device by interpreting + # incoming events for its name and aliasses + + # mock incoming command event for this device + event_callback({ + 'id': 'protocol_0_0', + 'command': 'on', + }) + yield from hass.async_block_till_done() + + switch_after_first_command = hass.states.get('switch.test') + assert switch_after_first_command.state == 'on' + # also after receiving first command state not longer has to be assumed + assert 'assumed_state' not in switch_after_first_command.attributes + + # mock incoming command event for this device + event_callback({ + 'id': 'protocol_0_0', + 'command': 'off', + }) + yield from hass.async_block_till_done() + + assert hass.states.get('switch.test').state == 'off' + + # test following aliasses + # mock incoming command event for this device alias + event_callback({ + 'id': 'test_alias_0_0', + 'command': 'on', + }) + yield from hass.async_block_till_done() + + assert hass.states.get('switch.test').state == 'on' + + # The switch component does not support adding new devices for incoming + # events because every new unkown device is added as a light by default. + + # test changing state from HA propagates to Rflink + hass.async_add_job( + hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: 'switch.test'})) + yield from hass.async_block_till_done() + assert hass.states.get('switch.test').state == 'off' + assert protocol.send_command_ack.call_args_list[0][0][0] == 'protocol_0_0' + assert protocol.send_command_ack.call_args_list[0][0][1] == 'off' + + hass.async_add_job( + hass.services.async_call(DOMAIN, SERVICE_TURN_ON, + {ATTR_ENTITY_ID: 'switch.test'})) + yield from hass.async_block_till_done() + assert hass.states.get('switch.test').state == 'on' + assert protocol.send_command_ack.call_args_list[1][0][1] == 'on' diff --git a/tests/components/test_alert.py b/tests/components/test_alert.py new file mode 100644 index 00000000000..00e4abec25b --- /dev/null +++ b/tests/components/test_alert.py @@ -0,0 +1,172 @@ +"""The tests for the Alert component.""" +# pylint: disable=protected-access +from copy import deepcopy +import unittest + +from homeassistant.bootstrap import setup_component +from homeassistant.core import callback +import homeassistant.components.alert as alert +import homeassistant.components.notify as notify +from homeassistant.const import (CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, + CONF_STATE, STATE_ON, STATE_OFF) + +from tests.common import get_test_home_assistant + +NAME = "alert_test" +NOTIFIER = 'test' +TEST_CONFIG = \ + {alert.DOMAIN: { + NAME: { + CONF_NAME: NAME, + CONF_ENTITY_ID: "sensor.test", + CONF_STATE: STATE_ON, + alert.CONF_REPEAT: 30, + alert.CONF_SKIP_FIRST: False, + alert.CONF_NOTIFIERS: [NOTIFIER]} + }} +TEST_NOACK = [NAME, NAME, "sensor.test", STATE_ON, + [30], False, NOTIFIER, False] +ENTITY_ID = alert.ENTITY_ID_FORMAT.format(NAME) + + +# pylint: disable=invalid-name +class TestAlert(unittest.TestCase): + """Test the alert module.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + def test_is_on(self): + """Test is_on method.""" + self.hass.states.set(ENTITY_ID, STATE_ON) + self.hass.block_till_done() + self.assertTrue(alert.is_on(self.hass, ENTITY_ID)) + self.hass.states.set(ENTITY_ID, STATE_OFF) + self.hass.block_till_done() + self.assertFalse(alert.is_on(self.hass, ENTITY_ID)) + + def test_setup(self): + """Test setup method.""" + assert setup_component(self.hass, alert.DOMAIN, TEST_CONFIG) + self.assertEqual(STATE_IDLE, self.hass.states.get(ENTITY_ID).state) + + def test_fire(self): + """Test the alert firing.""" + assert setup_component(self.hass, alert.DOMAIN, TEST_CONFIG) + self.hass.states.set("sensor.test", STATE_ON) + self.hass.block_till_done() + self.assertEqual(STATE_ON, self.hass.states.get(ENTITY_ID).state) + + def test_silence(self): + """Test silencing the alert.""" + assert setup_component(self.hass, alert.DOMAIN, TEST_CONFIG) + self.hass.states.set("sensor.test", STATE_ON) + self.hass.block_till_done() + alert.turn_off(self.hass, ENTITY_ID) + self.hass.block_till_done() + self.assertEqual(STATE_OFF, self.hass.states.get(ENTITY_ID).state) + + # alert should not be silenced on next fire + self.hass.states.set("sensor.test", STATE_OFF) + self.hass.block_till_done() + self.assertEqual(STATE_IDLE, self.hass.states.get(ENTITY_ID).state) + self.hass.states.set("sensor.test", STATE_ON) + self.hass.block_till_done() + self.assertEqual(STATE_ON, self.hass.states.get(ENTITY_ID).state) + + def test_reset(self): + """Test resetting the alert.""" + assert setup_component(self.hass, alert.DOMAIN, TEST_CONFIG) + self.hass.states.set("sensor.test", STATE_ON) + self.hass.block_till_done() + alert.turn_off(self.hass, ENTITY_ID) + self.hass.block_till_done() + self.assertEqual(STATE_OFF, self.hass.states.get(ENTITY_ID).state) + alert.turn_on(self.hass, ENTITY_ID) + self.hass.block_till_done() + self.assertEqual(STATE_ON, self.hass.states.get(ENTITY_ID).state) + + def test_toggle(self): + """Test toggling alert.""" + assert setup_component(self.hass, alert.DOMAIN, TEST_CONFIG) + self.hass.states.set("sensor.test", STATE_ON) + self.hass.block_till_done() + self.assertEqual(STATE_ON, self.hass.states.get(ENTITY_ID).state) + alert.toggle(self.hass, ENTITY_ID) + self.hass.block_till_done() + self.assertEqual(STATE_OFF, self.hass.states.get(ENTITY_ID).state) + alert.toggle(self.hass, ENTITY_ID) + self.hass.block_till_done() + self.assertEqual(STATE_ON, self.hass.states.get(ENTITY_ID).state) + + def test_hidden(self): + """Test entity hidding.""" + assert setup_component(self.hass, alert.DOMAIN, TEST_CONFIG) + hidden = self.hass.states.get(ENTITY_ID).attributes.get('hidden') + self.assertTrue(hidden) + + self.hass.states.set("sensor.test", STATE_ON) + self.hass.block_till_done() + hidden = self.hass.states.get(ENTITY_ID).attributes.get('hidden') + self.assertFalse(hidden) + + alert.turn_off(self.hass, ENTITY_ID) + hidden = self.hass.states.get(ENTITY_ID).attributes.get('hidden') + self.assertFalse(hidden) + + def test_notification(self): + """Test notifications.""" + events = [] + + @callback + def record_event(event): + """Add recorded event to set.""" + events.append(event) + + self.hass.services.register( + notify.DOMAIN, NOTIFIER, record_event) + + assert setup_component(self.hass, alert.DOMAIN, TEST_CONFIG) + self.assertEqual(0, len(events)) + + self.hass.states.set("sensor.test", STATE_ON) + self.hass.block_till_done() + self.assertEqual(1, len(events)) + + self.hass.states.set("sensor.test", STATE_OFF) + self.hass.block_till_done() + self.assertEqual(1, len(events)) + + def test_skipfirst(self): + """Test skipping first notification.""" + config = deepcopy(TEST_CONFIG) + config[alert.DOMAIN][NAME][alert.CONF_SKIP_FIRST] = True + events = [] + + @callback + def record_event(event): + """Add recorded event to set.""" + events.append(event) + + self.hass.services.register( + notify.DOMAIN, NOTIFIER, record_event) + + assert setup_component(self.hass, alert.DOMAIN, config) + self.assertEqual(0, len(events)) + + self.hass.states.set("sensor.test", STATE_ON) + self.hass.block_till_done() + self.assertEqual(0, len(events)) + + def test_noack(self): + """Test no ack feature.""" + entity = alert.Alert(self.hass, *TEST_NOACK) + self.hass.async_add_job(entity.begin_alerting) + self.hass.block_till_done() + + self.assertEqual(True, entity.hidden) diff --git a/tests/components/test_apiai.py b/tests/components/test_apiai.py new file mode 100644 index 00000000000..9023ee161c5 --- /dev/null +++ b/tests/components/test_apiai.py @@ -0,0 +1,513 @@ +"""The tests for the APIAI component.""" +# pylint: disable=protected-access +import json +import unittest + +import requests + +from homeassistant.core import callback +from homeassistant import bootstrap, const +from homeassistant.components import apiai, http + +from tests.common import get_test_instance_port, get_test_home_assistant + +API_PASSWORD = "test1234" +SERVER_PORT = get_test_instance_port() +BASE_API_URL = "http://127.0.0.1:{}".format(SERVER_PORT) +INTENTS_API_URL = "{}{}".format(BASE_API_URL, apiai.INTENTS_API_ENDPOINT) + +HA_HEADERS = { + const.HTTP_HEADER_HA_AUTH: API_PASSWORD, + const.HTTP_HEADER_CONTENT_TYPE: const.CONTENT_TYPE_JSON, +} + +SESSION_ID = "a9b84cec-46b6-484e-8f31-f65dba03ae6d" +INTENT_ID = "c6a74079-a8f0-46cd-b372-5a934d23591c" +INTENT_NAME = "tests" +REQUEST_ID = "19ef7e78-fe15-4e94-99dd-0c0b1e8753c3" +REQUEST_TIMESTAMP = "2017-01-21T17:54:18.952Z" +CONTEXT_NAME = "78a5db95-b7d6-4d50-9c9b-2fc73a5e34c3_id_dialog_context" +MAX_RESPONSE_TIME = 5 # https://docs.api.ai/docs/webhook + +# An unknown action takes 8s to return. Request timeout should be bigger to +# allow the test to finish +REQUEST_TIMEOUT = 15 + +# pylint: disable=invalid-name +hass = None +calls = [] + + +# pylint: disable=invalid-name +def setUpModule(): + """Initialize a Home Assistant server for testing this module.""" + global hass + + hass = get_test_home_assistant() + + bootstrap.setup_component( + hass, http.DOMAIN, + {http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD, + http.CONF_SERVER_PORT: SERVER_PORT}}) + + @callback + def mock_service(call): + """Mock action call.""" + calls.append(call) + + hass.services.register("test", "apiai", mock_service) + + bootstrap.setup_component(hass, apiai.DOMAIN, { + # Key is here to verify we allow other keys in config too + "homeassistant": {}, + "apiai": { + "intents": { + "WhereAreWeIntent": { + "speech": + """ + {%- if is_state("device_tracker.paulus", "home") + and is_state("device_tracker.anne_therese", + "home") -%} + You are both home, you silly + {%- else -%} + Anne Therese is at {{ + states("device_tracker.anne_therese") + }} and Paulus is at {{ + states("device_tracker.paulus") + }} + {% endif %} + """, + }, + "GetZodiacHoroscopeIntent": { + "speech": "You told us your sign is {{ ZodiacSign }}.", + }, + "CallServiceIntent": { + "speech": "Service called", + "action": { + "service": "test.apiai", + "data_template": { + "hello": "{{ ZodiacSign }}" + }, + "entity_id": "switch.test", + } + } + } + } + }) + + hass.start() + + +# pylint: disable=invalid-name +def tearDownModule(): + """Stop the Home Assistant server.""" + hass.stop() + + +def _intent_req(data): + return requests.post(INTENTS_API_URL, data=json.dumps(data), + timeout=REQUEST_TIMEOUT, headers=HA_HEADERS) + + +class TestApiai(unittest.TestCase): + """Test APIAI.""" + + def tearDown(self): + """Stop everything that was started.""" + hass.block_till_done() + + def test_intent_action_incomplete(self): + """Test when action is not completed.""" + data = { + "id": REQUEST_ID, + "timestamp": REQUEST_TIMESTAMP, + "result": { + "source": "agent", + "resolvedQuery": "my zodiac sign is virgo", + "speech": "", + "action": "GetZodiacHoroscopeIntent", + "actionIncomplete": True, + "parameters": { + "ZodiacSign": "virgo" + }, + "metadata": { + "intentId": INTENT_ID, + "webhookUsed": "true", + "webhookForSlotFillingUsed": "false", + "intentName": INTENT_NAME + }, + "fulfillment": { + "speech": "", + "messages": [ + { + "type": 0, + "speech": "" + } + ] + }, + "score": 1 + }, + "status": { + "code": 200, + "errorType": "success" + }, + "sessionId": SESSION_ID, + "originalRequest": None + } + + req = _intent_req(data) + self.assertEqual(200, req.status_code) + self.assertEqual("", req.text) + + def test_intent_slot_filling(self): + """Test when API.AI asks for slot-filling return none.""" + data = { + "id": REQUEST_ID, + "timestamp": REQUEST_TIMESTAMP, + "result": { + "source": "agent", + "resolvedQuery": "my zodiac sign is", + "speech": "", + "action": "GetZodiacHoroscopeIntent", + "actionIncomplete": True, + "parameters": { + "ZodiacSign": "" + }, + "contexts": [ + { + "name": CONTEXT_NAME, + "parameters": { + "ZodiacSign.original": "", + "ZodiacSign": "" + }, + "lifespan": 2 + }, + { + "name": "tests_ha_dialog_context", + "parameters": { + "ZodiacSign.original": "", + "ZodiacSign": "" + }, + "lifespan": 2 + }, + { + "name": "tests_ha_dialog_params_zodiacsign", + "parameters": { + "ZodiacSign.original": "", + "ZodiacSign": "" + }, + "lifespan": 1 + } + ], + "metadata": { + "intentId": INTENT_ID, + "webhookUsed": "true", + "webhookForSlotFillingUsed": "true", + "intentName": INTENT_NAME + }, + "fulfillment": { + "speech": "What is the ZodiacSign?", + "messages": [ + { + "type": 0, + "speech": "What is the ZodiacSign?" + } + ] + }, + "score": 0.77 + }, + "status": { + "code": 200, + "errorType": "success" + }, + "sessionId": SESSION_ID, + "originalRequest": None + } + + req = _intent_req(data) + self.assertEqual(200, req.status_code) + self.assertEqual("", req.text) + + def test_intent_request_with_parameters(self): + """Test a request with parameters.""" + data = { + "id": REQUEST_ID, + "timestamp": REQUEST_TIMESTAMP, + "result": { + "source": "agent", + "resolvedQuery": "my zodiac sign is virgo", + "speech": "", + "action": "GetZodiacHoroscopeIntent", + "actionIncomplete": False, + "parameters": { + "ZodiacSign": "virgo" + }, + "contexts": [], + "metadata": { + "intentId": INTENT_ID, + "webhookUsed": "true", + "webhookForSlotFillingUsed": "false", + "intentName": INTENT_NAME + }, + "fulfillment": { + "speech": "", + "messages": [ + { + "type": 0, + "speech": "" + } + ] + }, + "score": 1 + }, + "status": { + "code": 200, + "errorType": "success" + }, + "sessionId": SESSION_ID, + "originalRequest": None + } + req = _intent_req(data) + self.assertEqual(200, req.status_code) + text = req.json().get("speech") + self.assertEqual("You told us your sign is virgo.", text) + + def test_intent_request_with_parameters_but_empty(self): + """Test a request with parameters but empty value.""" + data = { + "id": REQUEST_ID, + "timestamp": REQUEST_TIMESTAMP, + "result": { + "source": "agent", + "resolvedQuery": "my zodiac sign is virgo", + "speech": "", + "action": "GetZodiacHoroscopeIntent", + "actionIncomplete": False, + "parameters": { + "ZodiacSign": "" + }, + "contexts": [], + "metadata": { + "intentId": INTENT_ID, + "webhookUsed": "true", + "webhookForSlotFillingUsed": "false", + "intentName": INTENT_NAME + }, + "fulfillment": { + "speech": "", + "messages": [ + { + "type": 0, + "speech": "" + } + ] + }, + "score": 1 + }, + "status": { + "code": 200, + "errorType": "success" + }, + "sessionId": SESSION_ID, + "originalRequest": None + } + req = _intent_req(data) + self.assertEqual(200, req.status_code) + text = req.json().get("speech") + self.assertEqual("You told us your sign is .", text) + + def test_intent_request_without_slots(self): + """Test a request without slots.""" + data = { + "id": REQUEST_ID, + "timestamp": REQUEST_TIMESTAMP, + "result": { + "source": "agent", + "resolvedQuery": "where are we", + "speech": "", + "action": "WhereAreWeIntent", + "actionIncomplete": False, + "parameters": {}, + "contexts": [], + "metadata": { + "intentId": INTENT_ID, + "webhookUsed": "true", + "webhookForSlotFillingUsed": "false", + "intentName": INTENT_NAME + }, + "fulfillment": { + "speech": "", + "messages": [ + { + "type": 0, + "speech": "" + } + ] + }, + "score": 1 + }, + "status": { + "code": 200, + "errorType": "success" + }, + "sessionId": SESSION_ID, + "originalRequest": None + } + req = _intent_req(data) + self.assertEqual(200, req.status_code) + text = req.json().get("speech") + + self.assertEqual("Anne Therese is at unknown and Paulus is at unknown", + text) + + hass.states.set("device_tracker.paulus", "home") + hass.states.set("device_tracker.anne_therese", "home") + + req = _intent_req(data) + self.assertEqual(200, req.status_code) + text = req.json().get("speech") + self.assertEqual("You are both home, you silly", text) + + def test_intent_request_calling_service(self): + """Test a request for calling a service. + + If this request is done async the test could finish before the action + has been executed. Hard to test because it will be a race condition. + """ + data = { + "id": REQUEST_ID, + "timestamp": REQUEST_TIMESTAMP, + "result": { + "source": "agent", + "resolvedQuery": "my zodiac sign is virgo", + "speech": "", + "action": "CallServiceIntent", + "actionIncomplete": False, + "parameters": { + "ZodiacSign": "virgo" + }, + "contexts": [], + "metadata": { + "intentId": INTENT_ID, + "webhookUsed": "true", + "webhookForSlotFillingUsed": "false", + "intentName": INTENT_NAME + }, + "fulfillment": { + "speech": "", + "messages": [ + { + "type": 0, + "speech": "" + } + ] + }, + "score": 1 + }, + "status": { + "code": 200, + "errorType": "success" + }, + "sessionId": SESSION_ID, + "originalRequest": None + } + call_count = len(calls) + req = _intent_req(data) + self.assertEqual(200, req.status_code) + self.assertEqual(call_count + 1, len(calls)) + call = calls[-1] + self.assertEqual("test", call.domain) + self.assertEqual("apiai", call.service) + self.assertEqual(["switch.test"], call.data.get("entity_id")) + self.assertEqual("virgo", call.data.get("hello")) + + def test_intent_with_no_action(self): + """Test a intent with no defined action.""" + data = { + "id": REQUEST_ID, + "timestamp": REQUEST_TIMESTAMP, + "result": { + "source": "agent", + "resolvedQuery": "my zodiac sign is virgo", + "speech": "", + "action": "", + "actionIncomplete": False, + "parameters": { + "ZodiacSign": "" + }, + "contexts": [], + "metadata": { + "intentId": INTENT_ID, + "webhookUsed": "true", + "webhookForSlotFillingUsed": "false", + "intentName": INTENT_NAME + }, + "fulfillment": { + "speech": "", + "messages": [ + { + "type": 0, + "speech": "" + } + ] + }, + "score": 1 + }, + "status": { + "code": 200, + "errorType": "success" + }, + "sessionId": SESSION_ID, + "originalRequest": None + } + req = _intent_req(data) + self.assertEqual(200, req.status_code) + text = req.json().get("speech") + self.assertEqual( + "You have not defined an action in your api.ai intent.", text) + + def test_intent_with_unknown_action(self): + """Test a intent with an action not defined in the conf.""" + data = { + "id": REQUEST_ID, + "timestamp": REQUEST_TIMESTAMP, + "result": { + "source": "agent", + "resolvedQuery": "my zodiac sign is virgo", + "speech": "", + "action": "unknown", + "actionIncomplete": False, + "parameters": { + "ZodiacSign": "" + }, + "contexts": [], + "metadata": { + "intentId": INTENT_ID, + "webhookUsed": "true", + "webhookForSlotFillingUsed": "false", + "intentName": INTENT_NAME + }, + "fulfillment": { + "speech": "", + "messages": [ + { + "type": 0, + "speech": "" + } + ] + }, + "score": 1 + }, + "status": { + "code": 200, + "errorType": "success" + }, + "sessionId": SESSION_ID, + "originalRequest": None + } + req = _intent_req(data) + self.assertEqual(200, req.status_code) + text = req.json().get("speech") + self.assertEqual( + "Intent 'unknown' is not yet configured within Home Assistant.", + text) diff --git a/tests/components/test_ffmpeg.py b/tests/components/test_ffmpeg.py new file mode 100644 index 00000000000..ccce59e754c --- /dev/null +++ b/tests/components/test_ffmpeg.py @@ -0,0 +1,227 @@ +"""The tests for Home Assistant ffmpeg.""" +import asyncio +from unittest.mock import patch, MagicMock + +import homeassistant.components.ffmpeg as ffmpeg +from homeassistant.bootstrap import setup_component +from homeassistant.util.async import ( + run_callback_threadsafe, run_coroutine_threadsafe) + +from tests.common import ( + get_test_home_assistant, assert_setup_component, mock_coro) + + +class MockFFmpegDev(ffmpeg.FFmpegBase): + """FFmpeg device mock.""" + + def __init__(self, initial_state=True, entity_id='test.ffmpeg_device'): + """Initialize mock.""" + super().__init__(initial_state) + + self.entity_id = entity_id + self.ffmpeg = MagicMock + self.called_stop = False + self.called_start = False + self.called_restart = False + + @asyncio.coroutine + def async_start_ffmpeg(self): + """Mock start.""" + self.called_start = True + + @asyncio.coroutine + def async_stop_ffmpeg(self): + """Mock stop.""" + self.called_stop = True + + @asyncio.coroutine + def async_restart_ffmpeg(self): + """Mock restart.""" + self.called_restart = True + + +class TestFFmpegSetup(object): + """Test class for ffmpeg.""" + + def setup_method(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def teardown_method(self): + """Stop everything that was started.""" + self.hass.stop() + + def test_setup_component(self): + """Setup ffmpeg component.""" + with assert_setup_component(2): + setup_component(self.hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + + assert self.hass.data[ffmpeg.DATA_FFMPEG].binary == 'ffmpeg' + + def test_setup_component_test_service(self): + """Setup ffmpeg component test services.""" + with assert_setup_component(2): + setup_component(self.hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + + assert self.hass.services.has_service(ffmpeg.DOMAIN, 'start') + assert self.hass.services.has_service(ffmpeg.DOMAIN, 'stop') + assert self.hass.services.has_service(ffmpeg.DOMAIN, 'restart') + + def test_setup_component_test_register(self): + """Setup ffmpeg component test register.""" + with assert_setup_component(2): + setup_component(self.hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + + self.hass.bus.async_listen_once = MagicMock() + ffmpeg_dev = MockFFmpegDev() + + manager = self.hass.data[ffmpeg.DATA_FFMPEG] + + run_callback_threadsafe( + self.hass.loop, manager.async_register_device, ffmpeg_dev).result() + + assert self.hass.bus.async_listen_once.called + assert self.hass.bus.async_listen_once.call_count == 2 + assert len(manager.entities) == 1 + assert manager.entities[0] == ffmpeg_dev + + def test_setup_component_test_register_no_startup(self): + """Setup ffmpeg component test register without startup.""" + with assert_setup_component(2): + setup_component(self.hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + + self.hass.bus.async_listen_once = MagicMock() + ffmpeg_dev = MockFFmpegDev(False) + + manager = self.hass.data[ffmpeg.DATA_FFMPEG] + + run_callback_threadsafe( + self.hass.loop, manager.async_register_device, ffmpeg_dev).result() + + assert self.hass.bus.async_listen_once.called + assert self.hass.bus.async_listen_once.call_count == 1 + assert len(manager.entities) == 1 + assert manager.entities[0] == ffmpeg_dev + + def test_setup_component_test_servcie_start(self): + """Setup ffmpeg component test service start.""" + with assert_setup_component(2): + setup_component(self.hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + + ffmpeg_dev = MockFFmpegDev(False) + manager = self.hass.data[ffmpeg.DATA_FFMPEG] + + run_callback_threadsafe( + self.hass.loop, manager.async_register_device, ffmpeg_dev).result() + + ffmpeg.start(self.hass) + self.hass.block_till_done() + + assert ffmpeg_dev.called_start + + def test_setup_component_test_servcie_stop(self): + """Setup ffmpeg component test service stop.""" + with assert_setup_component(2): + setup_component(self.hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + + ffmpeg_dev = MockFFmpegDev(False) + manager = self.hass.data[ffmpeg.DATA_FFMPEG] + + run_callback_threadsafe( + self.hass.loop, manager.async_register_device, ffmpeg_dev).result() + + ffmpeg.stop(self.hass) + self.hass.block_till_done() + + assert ffmpeg_dev.called_stop + + def test_setup_component_test_servcie_restart(self): + """Setup ffmpeg component test service restart.""" + with assert_setup_component(2): + setup_component(self.hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + + ffmpeg_dev = MockFFmpegDev(False) + manager = self.hass.data[ffmpeg.DATA_FFMPEG] + + run_callback_threadsafe( + self.hass.loop, manager.async_register_device, ffmpeg_dev).result() + + ffmpeg.restart(self.hass) + self.hass.block_till_done() + + assert ffmpeg_dev.called_restart + + def test_setup_component_test_servcie_start_with_entity(self): + """Setup ffmpeg component test service start.""" + with assert_setup_component(2): + setup_component(self.hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + + ffmpeg_dev = MockFFmpegDev(False) + manager = self.hass.data[ffmpeg.DATA_FFMPEG] + + run_callback_threadsafe( + self.hass.loop, manager.async_register_device, ffmpeg_dev).result() + + ffmpeg.start(self.hass, 'test.ffmpeg_device') + self.hass.block_till_done() + + assert ffmpeg_dev.called_start + + def test_setup_component_test_run_test_false(self): + """Setup ffmpeg component test run_test false.""" + with assert_setup_component(2): + setup_component(self.hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: { + 'run_test': False, + }}) + + manager = self.hass.data[ffmpeg.DATA_FFMPEG] + + assert run_coroutine_threadsafe( + manager.async_run_test("blabalblabla"), self.hass.loop).result() + assert len(manager._cache) == 0 + + @patch('haffmpeg.Test.run_test', + return_value=mock_coro(return_value=True)()) + def test_setup_component_test_run_test(self, mock_test): + """Setup ffmpeg component test run_test.""" + with assert_setup_component(2): + setup_component(self.hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + + manager = self.hass.data[ffmpeg.DATA_FFMPEG] + + assert run_coroutine_threadsafe( + manager.async_run_test("blabalblabla"), self.hass.loop).result() + assert mock_test.called + assert mock_test.call_count == 1 + assert len(manager._cache) == 1 + assert manager._cache['blabalblabla'] + + assert run_coroutine_threadsafe( + manager.async_run_test("blabalblabla"), self.hass.loop).result() + assert mock_test.called + assert mock_test.call_count == 1 + assert len(manager._cache) == 1 + assert manager._cache['blabalblabla'] + + @patch('haffmpeg.Test.run_test', + return_value=mock_coro(return_value=False)()) + def test_setup_component_test_run_test_test_fail(self, mock_test): + """Setup ffmpeg component test run_test.""" + with assert_setup_component(2): + setup_component(self.hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + + manager = self.hass.data[ffmpeg.DATA_FFMPEG] + + assert not run_coroutine_threadsafe( + manager.async_run_test("blabalblabla"), self.hass.loop).result() + assert mock_test.called + assert mock_test.call_count == 1 + assert len(manager._cache) == 1 + assert not manager._cache['blabalblabla'] + + assert not run_coroutine_threadsafe( + manager.async_run_test("blabalblabla"), self.hass.loop).result() + assert mock_test.called + assert mock_test.call_count == 1 + assert len(manager._cache) == 1 + assert not manager._cache['blabalblabla'] diff --git a/tests/components/test_init.py b/tests/components/test_init.py index 833319646a2..14066d65f2a 100644 --- a/tests/components/test_init.py +++ b/tests/components/test_init.py @@ -11,11 +11,12 @@ from homeassistant import config from homeassistant.const import ( STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE) import homeassistant.components as comps +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity from homeassistant.util.async import run_coroutine_threadsafe from tests.common import ( - get_test_home_assistant, mock_service, patch_yaml_files) + get_test_home_assistant, mock_service, patch_yaml_files, mock_coro) class TestComponentsCore(unittest.TestCase): @@ -150,3 +151,44 @@ class TestComponentsCore(unittest.TestCase): assert mock_error.called assert mock_process.called is False + + @patch('homeassistant.core.HomeAssistant.async_stop', + return_value=mock_coro()()) + def test_stop_homeassistant(self, mock_stop): + """Test stop service.""" + comps.stop(self.hass) + self.hass.block_till_done() + assert mock_stop.called + + @patch('homeassistant.core.HomeAssistant.async_stop', + return_value=mock_coro()()) + @patch('homeassistant.config.async_check_ha_config_file', + return_value=mock_coro()()) + def test_restart_homeassistant(self, mock_check, mock_restart): + """Test stop service.""" + comps.restart(self.hass) + self.hass.block_till_done() + assert mock_restart.called + assert mock_check.called + + @patch('homeassistant.core.HomeAssistant.async_stop', + return_value=mock_coro()()) + @patch('homeassistant.config.async_check_ha_config_file', + side_effect=HomeAssistantError("Test error")) + def test_restart_homeassistant_wrong_conf(self, mock_check, mock_restart): + """Test stop service.""" + comps.restart(self.hass) + self.hass.block_till_done() + assert mock_check.called + assert not mock_restart.called + + @patch('homeassistant.core.HomeAssistant.async_stop', + return_value=mock_coro()()) + @patch('homeassistant.config.async_check_ha_config_file', + return_value=mock_coro()()) + def test_check_config(self, mock_check, mock_stop): + """Test stop service.""" + comps.check_config(self.hass) + self.hass.block_till_done() + assert mock_check.called + assert not mock_stop.called diff --git a/tests/components/test_input_select.py b/tests/components/test_input_select.py index 04ab4ceed58..7fb832ddc22 100644 --- a/tests/components/test_input_select.py +++ b/tests/components/test_input_select.py @@ -6,7 +6,8 @@ from tests.common import get_test_home_assistant from homeassistant.bootstrap import setup_component from homeassistant.components.input_select import ( - ATTR_OPTIONS, DOMAIN, select_option, select_next, select_previous) + ATTR_OPTIONS, DOMAIN, SERVICE_SET_OPTIONS, + select_option, select_next, select_previous) from homeassistant.const import ( ATTR_ICON, ATTR_FRIENDLY_NAME) @@ -175,3 +176,38 @@ class TestInputSelect(unittest.TestCase): self.assertEqual('Hello World', state_2.attributes.get(ATTR_FRIENDLY_NAME)) self.assertEqual('mdi:work', state_2.attributes.get(ATTR_ICON)) + + def test_set_options_service(self): + """Test set_options service.""" + self.assertTrue( + setup_component(self.hass, DOMAIN, {DOMAIN: { + 'test_1': { + 'options': [ + 'first option', + 'middle option', + 'last option', + ], + 'initial': 'middle option', + }, + }})) + entity_id = 'input_select.test_1' + + state = self.hass.states.get(entity_id) + self.assertEqual('middle option', state.state) + + data = {ATTR_OPTIONS: ["test1", "test2"], "entity_id": entity_id} + self.hass.services.call(DOMAIN, SERVICE_SET_OPTIONS, data) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual('test1', state.state) + + select_option(self.hass, entity_id, 'first option') + self.hass.block_till_done() + state = self.hass.states.get(entity_id) + self.assertEqual('test1', state.state) + + select_option(self.hass, entity_id, 'test2') + self.hass.block_till_done() + state = self.hass.states.get(entity_id) + self.assertEqual('test2', state.state) diff --git a/tests/components/test_rflink.py b/tests/components/test_rflink.py new file mode 100644 index 00000000000..ad5e7f91b2f --- /dev/null +++ b/tests/components/test_rflink.py @@ -0,0 +1,178 @@ +"""Common functions for Rflink component tests and generic platform tests.""" + +import asyncio +from unittest.mock import Mock + +from homeassistant.bootstrap import async_setup_component +from homeassistant.components.rflink import CONF_RECONNECT_INTERVAL +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF +from tests.common import assert_setup_component + + +@asyncio.coroutine +def mock_rflink(hass, config, domain, monkeypatch, failures=None): + """Create mock Rflink asyncio protocol, test component setup.""" + transport, protocol = (Mock(), Mock()) + + @asyncio.coroutine + def send_command_ack(*command): + return True + protocol.send_command_ack = Mock(wraps=send_command_ack) + + @asyncio.coroutine + def send_command(*command): + return True + protocol.send_command = Mock(wraps=send_command) + + @asyncio.coroutine + def create_rflink_connection(*args, **kwargs): + """Return mocked transport and protocol.""" + # failures can be a list of booleans indicating in which sequence + # creating a connection should success or fail + if failures: + fail = failures.pop() + else: + fail = False + + if fail: + raise ConnectionRefusedError + else: + return transport, protocol + + mock_create = Mock(wraps=create_rflink_connection) + monkeypatch.setattr( + 'rflink.protocol.create_rflink_connection', + mock_create) + + # verify instanstiation of component with given config + with assert_setup_component(1, domain): + yield from async_setup_component(hass, domain, config) + + # hook into mock config for injecting events + event_callback = mock_create.call_args_list[0][1]['event_callback'] + assert event_callback + + disconnect_callback = mock_create.call_args_list[ + 0][1]['disconnect_callback'] + + return event_callback, mock_create, protocol, disconnect_callback + + +@asyncio.coroutine +def test_version_banner(hass, monkeypatch): + """Test sending unknown commands doesn't cause issues.""" + # use sensor domain during testing main platform + domain = 'sensor' + config = { + 'rflink': {'port': '/dev/ttyABC0', }, + domain: { + 'platform': 'rflink', + 'devices': { + 'test': {'name': 'test', 'sensor_type': 'temperature', }, + }, + }, + } + + # setup mocking rflink module + event_callback, _, _, _ = yield from mock_rflink( + hass, config, domain, monkeypatch) + + event_callback({ + 'hardware': 'Nodo RadioFrequencyLink', + 'firmware': 'RFLink Gateway', + 'version': '1.1', + 'revision': '45', + }) + + +@asyncio.coroutine +def test_send_no_wait(hass, monkeypatch): + """Test command sending without ack.""" + domain = 'switch' + config = { + 'rflink': { + 'port': '/dev/ttyABC0', + 'wait_for_ack': False, + }, + domain: { + 'platform': 'rflink', + 'devices': { + 'protocol_0_0': { + 'name': 'test', + 'aliasses': ['test_alias_0_0'], + }, + }, + }, + } + + # setup mocking rflink module + _, _, protocol, _ = yield from mock_rflink( + hass, config, domain, monkeypatch) + + hass.async_add_job( + hass.services.async_call(domain, SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: 'switch.test'})) + yield from hass.async_block_till_done() + assert protocol.send_command.call_args_list[0][0][0] == 'protocol_0_0' + assert protocol.send_command.call_args_list[0][0][1] == 'off' + + +@asyncio.coroutine +def test_reconnecting_after_disconnect(hass, monkeypatch): + """An unexpected disconnect should cause a reconnect.""" + domain = 'sensor' + config = { + 'rflink': { + 'port': '/dev/ttyABC0', + CONF_RECONNECT_INTERVAL: 0, + }, + domain: { + 'platform': 'rflink', + }, + } + + # setup mocking rflink module + _, mock_create, _, disconnect_callback = yield from mock_rflink( + hass, config, domain, monkeypatch) + + assert disconnect_callback, 'disconnect callback not passed to rflink' + + # rflink initiated disconnect + disconnect_callback(None) + + yield from hass.async_block_till_done() + + # we expect 2 call, the initial and reconnect + assert mock_create.call_count == 2 + + +@asyncio.coroutine +def test_reconnecting_after_failure(hass, monkeypatch): + """A failure to reconnect should be retried.""" + domain = 'sensor' + config = { + 'rflink': { + 'port': '/dev/ttyABC0', + CONF_RECONNECT_INTERVAL: 0, + }, + domain: { + 'platform': 'rflink', + }, + } + + # success first time but fail second + failures = [False, True, False] + + # setup mocking rflink module + _, mock_create, _, disconnect_callback = yield from mock_rflink( + hass, config, domain, monkeypatch, failures=failures) + + # rflink initiated disconnect + disconnect_callback(None) + + # wait for reconnects to have happened + yield from hass.async_block_till_done() + yield from hass.async_block_till_done() + + # we expect 3 calls, the initial and 2 reconnects + assert mock_create.call_count == 3 diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index f7985b8af74..0db7c1a5bef 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -341,6 +341,10 @@ class TestTTS(object): assert len(calls) == 1 req = requests.get(calls[0].data[ATTR_MEDIA_CONTENT_ID]) _, demo_data = self.demo_provider.get_tts_audio("bla", 'en') + demo_data = tts.SpeechManager.write_tags( + "265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3", + demo_data, self.demo_provider, + "I person is on front of your door.", 'en', None) assert req.status_code == 200 assert req.content == demo_data @@ -351,6 +355,7 @@ class TestTTS(object): config = { tts.DOMAIN: { 'platform': 'demo', + 'language': 'de', } } @@ -367,6 +372,10 @@ class TestTTS(object): assert len(calls) == 1 req = requests.get(calls[0].data[ATTR_MEDIA_CONTENT_ID]) _, demo_data = self.demo_provider.get_tts_audio("bla", "de") + demo_data = tts.SpeechManager.write_tags( + "265944c108cbb00b2a621be5930513e03a0bb2cd_de_-_demo.mp3", + demo_data, self.demo_provider, + "I person is on front of your door.", 'de', None) assert req.status_code == 200 assert req.content == demo_data diff --git a/tests/conftest.py b/tests/conftest.py index 54f5404d72d..d408ed254f2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,12 +1,14 @@ """Setup some common test helper things.""" import functools import logging +from unittest.mock import patch import pytest import requests_mock as _requests_mock -from homeassistant import util +from homeassistant import util, bootstrap from homeassistant.util import location +from homeassistant.components import mqtt from .common import async_test_home_assistant from .test_util.aiohttp import mock_aiohttp_client @@ -58,3 +60,18 @@ def aioclient_mock(): """Fixture to mock aioclient calls.""" with mock_aiohttp_client() as mock_session: yield mock_session + + +@pytest.fixture +def mqtt_mock(loop, hass): + """Fixture to mock MQTT.""" + with patch('homeassistant.components.mqtt.MQTT') as mock_mqtt: + loop.run_until_complete(bootstrap.async_setup_component( + hass, mqtt.DOMAIN, { + mqtt.DOMAIN: { + mqtt.CONF_BROKER: 'mock-broker', + } + })) + client = mock_mqtt() + client.reset_mock() + return client diff --git a/tests/helpers/test_aiohttp_client.py b/tests/helpers/test_aiohttp_client.py index 83e1275819b..c54770268d0 100644 --- a/tests/helpers/test_aiohttp_client.py +++ b/tests/helpers/test_aiohttp_client.py @@ -1,10 +1,13 @@ """Test the aiohttp client helper.""" +import asyncio import unittest import aiohttp +from homeassistant.bootstrap import setup_component import homeassistant.helpers.aiohttp_client as client -from homeassistant.util.async import run_callback_threadsafe +from homeassistant.util.async import ( + run_callback_threadsafe, run_coroutine_threadsafe) from tests.common import get_test_home_assistant @@ -79,3 +82,100 @@ class TestHelpersAiohttpClient(unittest.TestCase): assert isinstance( self.hass.data[client.DATA_CONNECTOR_NOTVERIFY], aiohttp.TCPConnector) + + def test_get_clientsession_cleanup(self): + """Test init clientsession with ssl.""" + run_callback_threadsafe(self.hass.loop, client.async_get_clientsession, + self.hass).result() + + assert isinstance( + self.hass.data[client.DATA_CLIENTSESSION], aiohttp.ClientSession) + assert isinstance( + self.hass.data[client.DATA_CONNECTOR], aiohttp.TCPConnector) + + run_coroutine_threadsafe( + client.async_cleanup_websession(self.hass), self.hass.loop + ).result() + + assert self.hass.data[client.DATA_CLIENTSESSION].closed + assert self.hass.data[client.DATA_CONNECTOR].closed + + def test_get_clientsession_cleanup_without_ssl(self): + """Test init clientsession with ssl.""" + run_callback_threadsafe(self.hass.loop, client.async_get_clientsession, + self.hass, False).result() + + assert isinstance( + self.hass.data[client.DATA_CLIENTSESSION_NOTVERIFY], + aiohttp.ClientSession) + assert isinstance( + self.hass.data[client.DATA_CONNECTOR_NOTVERIFY], + aiohttp.TCPConnector) + + run_coroutine_threadsafe( + client.async_cleanup_websession(self.hass), self.hass.loop + ).result() + + assert self.hass.data[client.DATA_CLIENTSESSION_NOTVERIFY].closed + assert self.hass.data[client.DATA_CONNECTOR_NOTVERIFY].closed + + +@asyncio.coroutine +def test_fetching_url(aioclient_mock, hass, test_client): + """Test that it fetches the given url.""" + aioclient_mock.get('http://example.com/mjpeg_stream', content=[ + b'Frame1', b'Frame2', b'Frame3' + ]) + + def setup_platform(): + """Setup the platform.""" + assert setup_component(hass, 'camera', { + 'camera': { + 'name': 'config_test', + 'platform': 'mjpeg', + 'mjpeg_url': 'http://example.com/mjpeg_stream', + }}) + + yield from hass.loop.run_in_executor(None, setup_platform) + + client = yield from test_client(hass.http.app) + + resp = yield from client.get('/api/camera_proxy_stream/camera.config_test') + + assert resp.status == 200 + assert aioclient_mock.call_count == 1 + body = yield from resp.text() + assert body == 'Frame3Frame2Frame1' + + aioclient_mock.clear_requests() + aioclient_mock.get( + 'http://example.com/mjpeg_stream', exc=asyncio.TimeoutError(), + content=[b'Frame1', b'Frame2', b'Frame3']) + + resp = yield from client.get('/api/camera_proxy_stream/camera.config_test') + + assert resp.status == 200 + body = yield from resp.text() + assert body == '' + + aioclient_mock.clear_requests() + aioclient_mock.get( + 'http://example.com/mjpeg_stream', exc=asyncio.CancelledError(), + content=[b'Frame1', b'Frame2', b'Frame3']) + + resp = yield from client.get('/api/camera_proxy_stream/camera.config_test') + + assert resp.status == 200 + body = yield from resp.text() + assert body == '' + + aioclient_mock.clear_requests() + aioclient_mock.get( + 'http://example.com/mjpeg_stream', exc=aiohttp.errors.ClientError(), + content=[b'Frame1', b'Frame2', b'Frame3']) + + resp = yield from client.get('/api/camera_proxy_stream/camera.config_test') + + assert resp.status == 200 + body = yield from resp.text() + assert body == '' diff --git a/tests/helpers/test_discovery.py b/tests/helpers/test_discovery.py index 5d6faf2f7c4..fcbec64cc98 100644 --- a/tests/helpers/test_discovery.py +++ b/tests/helpers/test_discovery.py @@ -155,7 +155,8 @@ class TestHelpersDiscovery: assert 'test_component' in self.hass.config.components assert 'switch' in self.hass.config.components - def test_1st_discovers_2nd_component(self): + @patch('homeassistant.bootstrap.async_register_signal_handling') + def test_1st_discovers_2nd_component(self, mock_signal): """Test that we don't break if one component discovers the other. If the first component fires a discovery event to setup the diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 463f073eeb1..b2134879640 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -186,6 +186,20 @@ class TestHelpersTemplate(unittest.TestCase): template.Template('{{ %s | timestamp_local }}' % inp, self.hass).render()) + def test_min(self): + """Test the min filter.""" + self.assertEqual( + '1', + template.Template('{{ [1, 2, 3] | min }}', + self.hass).render()) + + def test_max(self): + """Test the max filter.""" + self.assertEqual( + '3', + template.Template('{{ [1, 2, 3] | max }}', + self.hass).render()) + def test_timestamp_utc(self): """Test the timestamps to local filter.""" tests = { @@ -688,3 +702,14 @@ is_state_attr('device_tracker.phone_2', 'battery', 40) states.sensor.pick_humidity.state ~ „ %“ }} """))) + + self.assertListEqual( + sorted([ + 'sensor.luftfeuchtigkeit_mean', + 'input_slider.luftfeuchtigkeit', + ]), + sorted(template.extract_entities( + "{% if (states('sensor.luftfeuchtigkeit_mean') | int)" + " > (states('input_slider.luftfeuchtigkeit') | int +1.5)" + " %}true{% endif %}" + ))) diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 1d0bbbd8dfd..23dde3a8244 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -101,7 +101,13 @@ class TestCheckConfig(unittest.TestCase): res = check_config.check(get_test_config_dir('platform.yaml')) change_yaml_files(res) self.assertDictEqual( - {'mqtt': {'keepalive': 60, 'port': 1883, 'protocol': '3.1.1'}, + {'mqtt': { + 'keepalive': 60, + 'port': 1883, + 'protocol': '3.1.1', + 'discovery': False, + 'discovery_prefix': 'homeassistant', + }, 'light': []}, res['components'] ) diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 887ef7c2c20..0fede71a63d 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -58,7 +58,8 @@ class TestBootstrap: autospec=True) @mock.patch('homeassistant.util.location.detect_location_info', autospec=True, return_value=None) - def test_from_config_file(self, mock_upgrade, mock_detect): + @mock.patch('homeassistant.bootstrap.async_register_signal_handling') + def test_from_config_file(self, mock_upgrade, mock_detect, mock_signal): """Test with configuration file.""" components = ['browser', 'conversation', 'script'] files = { @@ -289,7 +290,8 @@ class TestBootstrap: assert 'comp' not in self.hass.config.components @mock.patch('homeassistant.bootstrap.enable_logging') - def test_home_assistant_core_config_validation(self, log_mock): + @mock.patch('homeassistant.bootstrap.async_register_signal_handling') + def test_home_assistant_core_config_validation(self, log_mock, sig_mock): """Test if we pass in wrong information for HA conf.""" # Extensive HA conf validation testing is done in test_config.py assert None is bootstrap.from_config_dict({ @@ -393,7 +395,8 @@ class TestBootstrap: assert loader.get_component('disabled_component') is not None assert 'disabled_component' in self.hass.config.components - def test_all_work_done_before_start(self): + @mock.patch('homeassistant.bootstrap.async_register_signal_handling') + def test_all_work_done_before_start(self, signal_mock): """Test all init work done till start.""" call_order = [] diff --git a/tests/test_config.py b/tests/test_config.py index 1197a0130d8..2ad25dece11 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -18,7 +18,7 @@ from homeassistant.util.async import run_coroutine_threadsafe from homeassistant.helpers.entity import Entity from tests.common import ( - get_test_config_dir, get_test_home_assistant) + get_test_config_dir, get_test_home_assistant, mock_generator) CONFIG_DIR = get_test_config_dir() YAML_PATH = os.path.join(CONFIG_DIR, config_util.YAML_CONFIG_FILE) @@ -376,6 +376,37 @@ class TestConfig(unittest.TestCase): assert self.hass.config.units == blankConfig.units assert self.hass.config.time_zone == blankConfig.time_zone + @mock.patch('asyncio.create_subprocess_exec') + def test_check_ha_config_file_correct(self, mock_create): + """Check that restart propagates to stop.""" + process_mock = mock.MagicMock() + attrs = { + 'communicate.return_value': mock_generator(('output', 'error')), + 'wait.return_value': mock_generator(0)} + process_mock.configure_mock(**attrs) + mock_create.return_value = mock_generator(process_mock) + + assert run_coroutine_threadsafe( + config_util.async_check_ha_config_file(self.hass), self.hass.loop + ).result() is None + + @mock.patch('asyncio.create_subprocess_exec') + def test_check_ha_config_file_wrong(self, mock_create): + """Check that restart with a bad config doesn't propagate to stop.""" + process_mock = mock.MagicMock() + attrs = { + 'communicate.return_value': + mock_generator((r'\033[hellom'.encode('utf-8'), 'error')), + 'wait.return_value': mock_generator(1)} + process_mock.configure_mock(**attrs) + mock_create.return_value = mock_generator(process_mock) + + with self.assertRaises(HomeAssistantError): + run_coroutine_threadsafe( + config_util.async_check_ha_config_file(self.hass), + self.hass.loop + ).result() + # pylint: disable=redefined-outer-name @pytest.fixture diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index afe2f626de7..220eccca699 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -25,7 +25,7 @@ class AiohttpClientMocker: content=None, json=None, params=None, - headers=None, + headers={}, exc=None, cookies=None): """Mock a request.""" @@ -39,7 +39,7 @@ class AiohttpClientMocker: url = str(yarl.URL(url).with_query(params)) self._mocks.append(AiohttpClientMockResponse( - method, url, status, content, cookies, exc)) + method, url, status, content, cookies, exc, headers)) def get(self, *args, **kwargs): """Register a mock get request.""" @@ -91,7 +91,8 @@ class AiohttpClientMocker: class AiohttpClientMockResponse: """Mock Aiohttp client response.""" - def __init__(self, method, url, status, response, cookies=None, exc=None): + def __init__(self, method, url, status, response, cookies=None, exc=None, + headers={}): """Initialize a fake response.""" self.method = method self._url = url @@ -101,6 +102,7 @@ class AiohttpClientMockResponse: self.response = response self.exc = exc + self._headers = headers self._cookies = {} if cookies: @@ -109,6 +111,18 @@ class AiohttpClientMockResponse: cookie.value = data self._cookies[name] = cookie + if isinstance(response, list): + self.content = mock.MagicMock() + + @asyncio.coroutine + def read(*argc, **kwargs): + """Read content stream mock.""" + if self.response: + return self.response.pop() + return None + + self.content.read = read + def match_request(self, method, url, params=None): """Test if response answers request.""" if method.lower() != self.method.lower(): @@ -142,6 +156,11 @@ class AiohttpClientMockResponse: return True + @property + def headers(self): + """Return content_type.""" + return self._headers + @property def cookies(self): """Return dict of cookies.""" diff --git a/tests/testing_config/custom_components/image_processing/test.py b/tests/testing_config/custom_components/image_processing/test.py new file mode 100644 index 00000000000..0c538bc6781 --- /dev/null +++ b/tests/testing_config/custom_components/image_processing/test.py @@ -0,0 +1,49 @@ +"""Provide a mock image processing.""" + +from homeassistant.components.image_processing import ImageProcessingEntity + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the test image_processing platform.""" + add_devices([TestImageProcessing('camera.demo_camera', "Test")]) + + +class TestImageProcessing(ImageProcessingEntity): + """Test image processing entity.""" + + def __init__(self, camera_entity, name): + """Initialize test image processing.""" + self._name = name + self._camera = camera_entity + self._count = 0 + self._image = "" + + @property + def should_poll(self): + """Return True if entity has to be polled for state.""" + return False + + @property + def camera_entity(self): + """Return camera entity id from process pictures.""" + return self._camera + + @property + def name(self): + """Return the name of the entity.""" + return self._name + + @property + def state(self): + """Return the state of the entity.""" + return self._count + + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + return {'image': self._image} + + def process_image(self, image): + """Process image.""" + self._image = image + self._count += 1 diff --git a/tox.ini b/tox.ini index 3da1de04bf3..a1a04bd2ea7 100644 --- a/tox.ini +++ b/tox.ini @@ -4,12 +4,13 @@ skip_missing_interpreters = True [testenv] setenv = + PYTHONPATH = {toxinidir}:{toxinidir}/homeassistant ; both temper-python and XBee modules have utf8 in their README files ; which get read in from setup.py. If we don't force our locale to a ; utf8 one, tox's env is reset. And the install of these 2 packages ; fail. - LANG=C.UTF-8 - PYTHONPATH = {toxinidir}:{toxinidir}/homeassistant +whitelist_externals = /usr/bin/env +install_command = /usr/bin/env LANG=C.UTF-8 pip install {opts} {packages} commands = py.test --timeout=30 --duration=10 --cov --cov-report= {posargs} deps = diff --git a/virtualization/Docker/Dockerfile.dev b/virtualization/Docker/Dockerfile.dev index 7968df25b69..4c75db36acc 100644 --- a/virtualization/Docker/Dockerfile.dev +++ b/virtualization/Docker/Dockerfile.dev @@ -5,14 +5,22 @@ FROM python:3.5 MAINTAINER Paulus Schoutsen +# Uncomment any of the following lines to disable the installation. +#ENV INSTALL_TELLSTICK no +#ENV INSTALL_OPENALPR no +#ENV INSTALL_FFMPEG no +#ENV INSTALL_OPENZWAVE no +#ENV INSTALL_LIBCEC no +#ENV INSTALL_PHANTOMJS no + VOLUME /config RUN mkdir -p /usr/src/app WORKDIR /usr/src/app # Copy build scripts -COPY script/setup_docker_prereqs script/build_python_openzwave script/build_libcec script/ -RUN script/setup_docker_prereqs +COPY virtualization/Docker/ virtualization/Docker/ +RUN virtualization/Docker/setup_docker_prereqs # Install hass component dependencies COPY requirements_all.txt requirements_all.txt @@ -40,4 +48,4 @@ RUN tox -e py35 --notest # Copy source COPY . . -CMD [ "python", "-m", "homeassistant", "--config", "/config" ] +CMD [ "python", "-m", "homeassistant", "--config", "/config" ] \ No newline at end of file diff --git a/virtualization/Docker/scripts/ffmpeg b/virtualization/Docker/scripts/ffmpeg new file mode 100755 index 00000000000..81b9ce694f9 --- /dev/null +++ b/virtualization/Docker/scripts/ffmpeg @@ -0,0 +1,16 @@ +#!/bin/bash +# Sets up ffmpeg. + +# Stop on errors +set -e + +PACKAGES=( + ffmpeg +) + +# Add jessie-backports +echo "Adding jessie-backports" +echo "deb http://deb.debian.org/debian jessie-backports main" >> /etc/apt/sources.list +apt-get update + +apt-get install -y --no-install-recommends -t jessie-backports ${PACKAGES[@]} \ No newline at end of file diff --git a/script/build_libcec b/virtualization/Docker/scripts/libcec similarity index 71% rename from script/build_libcec rename to virtualization/Docker/scripts/libcec index 1c30d634437..44e90c40030 100755 --- a/script/build_libcec +++ b/virtualization/Docker/scripts/libcec @@ -1,5 +1,5 @@ #!/bin/sh -# Sets up and builds libcec to be used with Home Assistant. +# Sets up libcec. # Dependencies that need to be installed: # apt-get install cmake libudev-dev libxrandr-dev swig @@ -11,7 +11,6 @@ PYTHON_LIBDIR=$(python -c 'from distutils import sysconfig; print(sysconfig.get_ PYTHON_LDLIBRARY=$(python -c 'from distutils import sysconfig; print(sysconfig.get_config_var("LDLIBRARY"))') PYTHON_LIBRARY="${PYTHON_LIBDIR}/${PYTHON_LDLIBRARY}" PYTHON_INCLUDE_DIR=$(python -c 'from distutils import sysconfig; print(sysconfig.get_python_inc())') -PYTHON_SITE_DIR=$(python -c 'from distutils import sysconfig; print(sysconfig.get_python_lib(prefix=""))') cd "$(dirname "$0")/.." mkdir -p build && cd build @@ -34,12 +33,6 @@ git submodule update --init src/platform make install ) -# Fix upstream install hardcoded Debian path. -# See: https://github.com/Pulse-Eight/libcec/issues/288 -sed -i \ - -e '/DESTINATION/s:lib/python${PYTHON_VERSION}/dist-packages:${PYTHON_SITE_DIR}:' \ - src/libcec/cmake/CheckPlatformSupport.cmake - # Build libcec ( mkdir -p build && cd build @@ -47,9 +40,8 @@ sed -i \ cmake \ -DPYTHON_LIBRARY="${PYTHON_LIBRARY}" \ -DPYTHON_INCLUDE_DIR="${PYTHON_INCLUDE_DIR}" \ - -DPYTHON_SITE_DIR="${PYTHON_SITE_DIR}" \ .. make make install ldconfig -) +) \ No newline at end of file diff --git a/virtualization/Docker/scripts/openalpr b/virtualization/Docker/scripts/openalpr new file mode 100755 index 00000000000..ffecc864914 --- /dev/null +++ b/virtualization/Docker/scripts/openalpr @@ -0,0 +1,29 @@ +#!/bin/bash +# Sets up openalpr. + +# Stop on errors +set -e + +PACKAGES=( + # homeassistant.components.image_processing.openalpr_local + libopencv-dev libtesseract-dev libleptonica-dev liblog4cplus-dev +) + +apt-get install -y --no-install-recommends ${PACKAGES[@]} + +# Clone the latest code from GitHub +git clone https://github.com/openalpr/openalpr.git /usr/local/src/openalpr + +# Setup the build directory +cd /usr/local/src/openalpr/src +mkdir -p build +cd build + +# Setup the compile environment +cmake -DWITH_TEST=FALSE -DWITH_BINDING_JAVA=FALSE --DWITH_BINDING_PYTHON=FALSE --DWITH_BINDING_GO=FALSE -DWITH_DAEMON=FALSE -DCMAKE_INSTALL_PREFIX:PATH=/usr/local .. + +# compile the library +make + +# Install the binaries/libraries to your local system (prefix is /usr/local) +make install \ No newline at end of file diff --git a/script/install_phantomjs b/virtualization/Docker/scripts/phantomjs similarity index 83% rename from script/install_phantomjs rename to virtualization/Docker/scripts/phantomjs index 178dfad540e..7c1e1dd3536 100755 --- a/script/install_phantomjs +++ b/virtualization/Docker/scripts/phantomjs @@ -1,5 +1,5 @@ #!/bin/bash -# Sets up phantomjs to be used with Home Assistant. +# Sets up phantomjs. # Stop on errors set -e @@ -12,4 +12,4 @@ mkdir -p build && cd build curl -LSO https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2 tar -xjf phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2 mv phantomjs-$PHANTOMJS_VERSION-linux-x86_64/bin/phantomjs /usr/bin/phantomjs -/usr/bin/phantomjs -v +/usr/bin/phantomjs -v \ No newline at end of file diff --git a/script/build_python_openzwave b/virtualization/Docker/scripts/python_openzwave similarity index 82% rename from script/build_python_openzwave rename to virtualization/Docker/scripts/python_openzwave index db82fe08d8b..f4eb233e53c 100755 --- a/script/build_python_openzwave +++ b/virtualization/Docker/scripts/python_openzwave @@ -1,5 +1,5 @@ #!/bin/sh -# Sets up and builds python open zwave to be used with Home Assistant. +# Sets up python-openzwave. # Dependencies that need to be installed: # apt-get install cython3 libudev-dev python3-sphinx python3-setuptools @@ -27,3 +27,6 @@ git checkout python3 pip3 install --upgrade cython==0.24.1 PYTHON_EXEC=`which python3` make build PYTHON_EXEC=`which python3` make install + +mkdir -p /usr/local/share/python-openzwave +cp -R openzwave/config /usr/local/share/python-openzwave/config \ No newline at end of file diff --git a/virtualization/Docker/scripts/tellstick b/virtualization/Docker/scripts/tellstick new file mode 100755 index 00000000000..805e411f47b --- /dev/null +++ b/virtualization/Docker/scripts/tellstick @@ -0,0 +1,17 @@ +#!/bin/bash +# Sets up tellstick. + +# Stop on errors +set -e + +PACKAGES=( + # homeassistant.components.tellstick + libtelldus-core2 +) + +# Add Tellstick repository +echo "deb http://download.telldus.com/debian/ stable main" >> /etc/apt/sources.list.d/telldus.list +wget -qO - http://download.telldus.se/debian/telldus-public.key | apt-key add - + +apt-get update +apt-get install -y --no-install-recommends ${PACKAGES[@]} \ No newline at end of file diff --git a/virtualization/Docker/setup_docker_prereqs b/virtualization/Docker/setup_docker_prereqs new file mode 100755 index 00000000000..9a878b3abba --- /dev/null +++ b/virtualization/Docker/setup_docker_prereqs @@ -0,0 +1,69 @@ +#!/bin/bash +# Install requirements and build dependencies for Home Assinstant in Docker. + +# Stop on errors +set -e + +INSTALL_TELLSTICK="${INSTALL_TELLSTICK:-yes}" +INSTALL_OPENALPR="${INSTALL_OPENALPR:-yes}" +INSTALL_FFMPEG="${INSTALL_FFMPEG:-yes}" +INSTALL_OPENZWAVE="${INSTALL_OPENZWAVE:-yes}" +INSTALL_LIBCEC="${INSTALL_LIBCEC:-yes}" +INSTALL_PHANTOMJS="${INSTALL_PHANTOMJS:-yes}" + +# Required debian packages for running hass or components +PACKAGES=( + # build-essential is required for python pillow module on non-x86_64 arch + build-essential + # homeassistant.components.image_processing.openalpr_local + libxrandr-dev + # homeassistant.components.device_tracker.nmap_tracker + nmap net-tools libcurl3-dev + # homeassistant.components.device_tracker.bluetooth_tracker + bluetooth libglib2.0-dev libbluetooth-dev +) + +# Required debian packages for building dependencies +PACKAGES_DEV=( + cmake git + # python-openzwave + cython3 libudev-dev + # libcec + swig +) + +# Install packages +apt-get update +apt-get install -y --no-install-recommends ${PACKAGES[@]} ${PACKAGES_DEV[@]} + +if [ "$INSTALL_TELLSTICK" == "yes" ]; then + virtualization/Docker/scripts/tellstick +fi + +if [ "$INSTALL_OPENALPR" == "yes" ]; then + virtualization/Docker/scripts/openalpr +fi + +if [ "$INSTALL_FFMPEG" == "yes" ]; then + virtualization/Docker/scripts/ffmpeg +fi + +if [ "$INSTALL_OPENZWAVE" == "yes" ]; then + virtualization/Docker/scripts/python_openzwave +fi + +if [ "$INSTALL_LIBCEC" == "yes" ]; then + virtualization/Docker/scripts/libcec +fi + +if [ "$INSTALL_PHANTOMJS" == "yes" ]; then + virtualization/Docker/scripts/phantomjs +fi + +# Remove packages +apt-get remove -y --purge ${PACKAGES_DEV[@]} +apt-get -y --purge autoremove + +# Cleanup +apt-get clean +rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* build/ \ No newline at end of file